Coverage for qml_essentials / ansaetze.py: 94%

288 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-17 20:45 +0000

1from abc import ABC, abstractmethod 

2from typing import Any, Optional, List, Union, Callable, Tuple 

3import jax.numpy as np 

4import logging 

5import warnings 

6 

7from qml_essentials.gates import Gates, PulseInformation 

8from qml_essentials.topologies import Topology 

9 

10log = logging.getLogger(__name__) 

11 

12 

13class Circuit(ABC): 

14 """Abstract base class for quantum circuit ansätze.""" 

15 

16 def __init__(self) -> None: 

17 """Initialize the circuit.""" 

18 pass 

19 

20 @abstractmethod 

21 def n_params_per_layer(self, n_qubits: int) -> int: 

22 """ 

23 Get the number of parameters per circuit layer. 

24 

25 Args: 

26 n_qubits (int): Number of qubits in the circuit. 

27 

28 Returns: 

29 int: Number of parameters required per layer. 

30 

31 Raises: 

32 NotImplementedError: Must be implemented by subclasses. 

33 """ 

34 raise NotImplementedError("n_params_per_layer method is not implemented") 

35 

36 def n_pulse_params_per_layer(self, n_qubits: int) -> int: 

37 """ 

38 Get the number of pulse parameters per circuit layer. 

39 

40 Subclasses that do not use pulse-level simulation do not need to 

41 override this method. 

42 

43 Args: 

44 n_qubits (int): Number of qubits in the circuit. 

45 

46 Returns: 

47 int: Number of pulse parameters required per layer. 

48 

49 Raises: 

50 NotImplementedError: If called but not overridden by subclass. 

51 """ 

52 raise NotImplementedError("n_pulse_params_per_layer method is not implemented") 

53 

54 @abstractmethod 

55 def get_control_indices(self, n_qubits: int) -> Optional[List[int]]: 

56 """ 

57 Get indices for controlled rotation gates in one layer. 

58 

59 Returns slice indices [start:stop:step] for extracting controlled 

60 gate parameters from a full parameter array for one layer. 

61 

62 Args: 

63 n_qubits (int): Number of qubits in the circuit. 

64 

65 Returns: 

66 Optional[List[int]]: List of three integers [start, stop, step] 

67 for slicing, or None if the circuit contains no controlled 

68 rotation gates. 

69 

70 Raises: 

71 NotImplementedError: Must be implemented by subclasses. 

72 """ 

73 raise NotImplementedError("get_control_indices method is not implemented") 

74 

75 def get_control_angles(self, w: np.ndarray, n_qubits: int) -> Optional[np.ndarray]: 

76 """ 

77 Extract angles for controlled rotation gates from parameter array. 

78 

79 Args: 

80 w (np.ndarray): Parameter array for one layer. 

81 n_qubits (int): Number of qubits in the circuit. 

82 

83 Returns: 

84 Optional[np.ndarray]: Array of controlled gate parameters, 

85 or empty array if circuit contains no controlled gates. 

86 """ 

87 indices = self.get_control_indices(n_qubits) 

88 if indices is None: 

89 return np.array([]) 

90 

91 if len(indices) == 3 and None in indices: 

92 return w[indices[0] : indices[1] : indices[2]] 

93 else: 

94 return w.take(np.array(indices)) 

95 

96 def _build(self, w: np.ndarray, n_qubits: int, **kwargs) -> Any: 

97 """ 

98 Build one layer of the circuit using unitary or pulse-level parameters. 

99 

100 Internal method that handles pulse parameter validation and context 

101 management before delegating to the build() method. 

102 

103 Args: 

104 w (np.ndarray): Parameter array for the current layer. 

105 n_qubits (int): Number of qubits in the circuit. 

106 **kwargs: Additional keyword arguments: 

107 - gate_mode (str): "unitary" (default) or "pulse" for 

108 pulse-level simulation. 

109 - pulse_params (np.ndarray): Pulse parameters if gate_mode="pulse". 

110 - noise_params (Dict): Noise parameters dictionary. 

111 

112 Returns: 

113 Any: Result from the build() method. 

114 

115 Raises: 

116 ValueError: If pulse_params length doesn't match expected count. 

117 """ 

118 gate_mode = kwargs.get("gate_mode", "unitary") 

119 

120 if gate_mode == "pulse" and "pulse_params" in kwargs: 

121 pulse_params_per_layer = self.n_pulse_params_per_layer(n_qubits) 

122 

123 if len(kwargs["pulse_params"]) != pulse_params_per_layer: 

124 raise ValueError( 

125 f"Pulse params length {len(kwargs['pulse_params'])} " 

126 f"does not match expected {pulse_params_per_layer} " 

127 f"for {n_qubits} qubits" 

128 ) 

129 

130 with Gates.pulse_manager_context(kwargs["pulse_params"]): 

131 return self.build(w, n_qubits, **kwargs) 

132 else: 

133 return self.build(w, n_qubits, **kwargs) 

134 

135 @abstractmethod 

136 def build(self, w: np.ndarray, n_qubits: int, **kwargs) -> Any: 

137 """ 

138 Build one layer of the quantum circuit. 

139 

140 Args: 

141 w (np.ndarray): Parameter array for the current layer. 

142 n_qubits (int): Number of qubits in the circuit. 

143 **kwargs: Additional keyword arguments passed from _build. 

144 

145 Returns: 

146 Any: Circuit construction result. 

147 

148 Raises: 

149 NotImplementedError: Must be implemented by subclasses. 

150 """ 

151 raise NotImplementedError("build method is not implemented") 

152 

153 def __call__(self, *args: Any, **kwds: Any) -> Any: 

154 """Call the _build method with provided arguments.""" 

155 self._build(*args, **kwds) 

156 

157 

158class DeclarativeCircuit(Circuit): 

159 """ 

160 A circuit defined entirely by a sequence of Block descriptors. 

161 

162 Subclasses only need to set the class attribute `structure` — a tuple of 

163 

164 All of `n_params_per_layer`, `n_pulse_params_per_layer`, 

165 `get_control_indices`, and `build` are derived automatically. 

166 """ 

167 

168 @staticmethod 

169 def structure() -> Tuple[Any, ...]: 

170 """Override in subclass to return the structure tuple.""" 

171 raise NotImplementedError 

172 

173 @classmethod 

174 def n_params_per_layer(cls, n_qubits: int) -> int: 

175 return sum(block.n_params(n_qubits) for block in cls.structure()) 

176 

177 @classmethod 

178 def n_pulse_params_per_layer(cls, n_qubits: int) -> int: 

179 return sum(block.n_pulse_params(n_qubits) for block in cls.structure()) 

180 

181 @classmethod 

182 def get_control_indices(cls, n_qubits: int) -> Optional[List]: 

183 """ 

184 Computes parameter indices for controlled rotation Gates. 

185 Scans the structure for Block with 

186 [start, stop, step] into the flat parameter vector, or None. 

187 """ 

188 structure = cls.structure() 

189 total_params = sum(block.n_params(n_qubits) for block in structure) 

190 

191 # Collect which parameter indices correspond to controlled rotations 

192 controlled_indices = [] 

193 offset = 0 

194 for block in structure: 

195 n = block.n_params(n_qubits) 

196 if block.is_controlled_rotation: 

197 controlled_indices.extend(range(offset, offset + n)) 

198 offset += n 

199 

200 # FIXME: this last part should be reworked 

201 

202 if not controlled_indices: 

203 return None 

204 

205 # Check if indices form a contiguous tail (the common case) 

206 # This preserves backwards compatibility with the [start, None, None] format 

207 if controlled_indices == list( 

208 range(total_params - len(controlled_indices), total_params) 

209 ): 

210 return [-len(controlled_indices), None, None] 

211 

212 # Fallback: return raw indices (future-proof) 

213 return controlled_indices 

214 

215 @classmethod 

216 def build(cls, w: np.ndarray, n_qubits: int, **kwargs) -> None: 

217 structure = cls.structure() 

218 w_idx = 0 

219 for block in structure: 

220 w_idx = block.apply(w, w_idx, n_qubits, **kwargs) 

221 

222 

223class Block: 

224 def __init__( 

225 self, 

226 gate: str, 

227 topology: Any = None, 

228 **kwargs, 

229 ): 

230 """ 

231 Initialize a Block object; the atoms of Ansatzes. 

232 

233 Args: 

234 gate (str): Name of the Gate class to use. 

235 topology (Any, optional): Topology of the gate for entangling gates. 

236 Defaults to None. 

237 kwargs: Additional keyword arguments passed to the topology function. 

238 """ 

239 if isinstance(gate, str): 

240 self.gate = getattr(Gates, gate) 

241 else: 

242 self.gate = gate 

243 

244 if self.is_entangling: 

245 assert ( 

246 topology is not None 

247 ), "Topology must be specified for entangling gates" 

248 

249 self.topology = topology 

250 self.kwargs = kwargs 

251 

252 def __repr__(self): 

253 if self.topology is None: 

254 return f"{self.__class__.__name__}({self.gate.__name__})" 

255 else: 

256 return ( 

257 f"{self.__class__.__name__}" 

258 f"({self.topology.__name__}[{self.gate.__name__}])" 

259 ) 

260 

261 @property 

262 def is_entangling(self): 

263 return Gates.is_entangling(self.gate) 

264 

265 @property 

266 def is_rotational(self): 

267 return Gates.is_rotational(self.gate) 

268 

269 @property 

270 def is_controlled_rotation(self): 

271 return self.is_entangling and self.is_rotational 

272 

273 def enough_qubits(self, n_qubits): 

274 if self.is_entangling: 

275 # NOTE This must be adjusted if default values 

276 # in Topology change 

277 span = self.kwargs.get("span", 1) 

278 if callable(span): 

279 span = span(n_qubits) 

280 

281 return (n_qubits >= 2) and (n_qubits > span) 

282 

283 return n_qubits >= 1 

284 

285 def n_params(self, n_qubits: int) -> int: 

286 assert n_qubits > 0, "Number of qubits must be positive" 

287 

288 if self.is_rotational: 

289 if self.is_entangling: 

290 if not self.enough_qubits(n_qubits): 

291 warnings.warn( 

292 f"Skipping {self.topology.__name__} with n_qubits={n_qubits} " 

293 f"as there are not enough qubits" 

294 f"for this topology." 

295 ) 

296 return 0 

297 else: 

298 return len(self.topology(n_qubits=n_qubits, **self.kwargs)) 

299 else: 

300 return n_qubits if self.gate.__name__ != "Rot" else 3 * n_qubits 

301 

302 return 0 

303 

304 def n_pulse_params(self, n_qubits: int) -> int: 

305 assert n_qubits > 0, "Number of qubits must be positive" 

306 

307 n_pulse_params = PulseInformation.num_params(self.gate) 

308 if self.is_entangling: 

309 if not self.enough_qubits(n_qubits): 

310 warnings.warn( 

311 f"Skipping {self.topology.__name__} with n_qubits={n_qubits} " 

312 f"as there are not enough qubits" 

313 f"for this topology." 

314 ) 

315 return 0 

316 else: 

317 return n_pulse_params * len( 

318 self.topology(n_qubits=n_qubits, **self.kwargs) 

319 ) 

320 return n_pulse_params * n_qubits 

321 

322 def apply(self, w: np.ndarray, w_idx: int, n_qubits: int, **kwargs) -> int: 

323 assert n_qubits > 0, "Number of qubits must be positive" 

324 

325 iterator = ( 

326 self.topology(n_qubits=n_qubits, **self.kwargs) 

327 if self.is_entangling 

328 else range(n_qubits) 

329 ) 

330 

331 for wires in iterator: 

332 if self.is_entangling and not self.enough_qubits(n_qubits): 

333 warnings.warn( 

334 f"Skipping {self.topology.__name__} with n_qubits={n_qubits} " 

335 f"as there are not enough qubits" 

336 f"for this topology." 

337 ) 

338 continue 

339 

340 if self.is_rotational: 

341 if self.gate.__name__ == "Rot": 

342 self.gate( 

343 w[w_idx], w[w_idx + 1], w[w_idx + 2], wires=wires, **kwargs 

344 ) 

345 w_idx += 3 

346 else: 

347 self.gate(w[w_idx], wires=wires, **kwargs) 

348 w_idx += 1 

349 else: 

350 self.gate(wires=wires, **kwargs) 

351 return w_idx 

352 

353 

354class Ansaetze: 

355 def get_available(parameterized_only=False): 

356 # list of parameterized ansaetze 

357 ansaetze = [ 

358 Ansaetze.Circuit_1, 

359 Ansaetze.Circuit_2, 

360 Ansaetze.Circuit_3, 

361 Ansaetze.Circuit_4, 

362 Ansaetze.Circuit_5, 

363 Ansaetze.Circuit_6, 

364 Ansaetze.Circuit_7, 

365 Ansaetze.Circuit_8, 

366 Ansaetze.Circuit_9, 

367 Ansaetze.Circuit_10, 

368 Ansaetze.Circuit_13, 

369 Ansaetze.Circuit_14, 

370 Ansaetze.Circuit_15, 

371 Ansaetze.Circuit_16, 

372 Ansaetze.Circuit_17, 

373 Ansaetze.Circuit_18, 

374 Ansaetze.Circuit_19, 

375 Ansaetze.Circuit_20, 

376 Ansaetze.No_Entangling, 

377 Ansaetze.Strongly_Entangling, 

378 Ansaetze.Hardware_Efficient, 

379 ] 

380 

381 # extend by the non-parameterized ones 

382 if not parameterized_only: 

383 ansaetze += [ 

384 Ansaetze.No_Ansatz, 

385 Ansaetze.GHZ, 

386 ] 

387 

388 return ansaetze 

389 

390 class No_Ansatz(DeclarativeCircuit): 

391 @staticmethod 

392 def structure(): 

393 return () 

394 

395 class GHZ(DeclarativeCircuit): 

396 @staticmethod 

397 def structure(): 

398 return ( 

399 Block(gate=Gates.H), 

400 Block(gate=Gates.CX, topology=Topology.stairs, reverse=True), 

401 ) 

402 

403 @staticmethod 

404 def build(w: np.ndarray, n_qubits: int, **kwargs): 

405 Gates.H(wires=0, **kwargs) 

406 for q in range(n_qubits - 1): 

407 Gates.CX(wires=[q, q + 1], **kwargs) 

408 

409 @staticmethod 

410 def n_pulse_params_per_layer(n_qubits: int) -> int: 

411 n_params = PulseInformation.num_params("H") # only 1 H 

412 n_params += (n_qubits - 1) * PulseInformation.num_params(Gates.CX) 

413 return n_params 

414 

415 class Circuit_1(DeclarativeCircuit): 

416 @staticmethod 

417 def structure(): 

418 return ( 

419 Block(gate=Gates.RX), 

420 Block(gate=Gates.RZ), 

421 ) 

422 

423 class Circuit_2(DeclarativeCircuit): 

424 @staticmethod 

425 def structure(): 

426 return ( 

427 Block(gate=Gates.RX), 

428 Block(gate=Gates.RZ), 

429 Block( 

430 gate=Gates.CX, 

431 topology=Topology.stairs, 

432 ), 

433 ) 

434 

435 class Circuit_3(DeclarativeCircuit): 

436 @staticmethod 

437 def structure(): 

438 return ( 

439 Block(gate=Gates.RX), 

440 Block(gate=Gates.RZ), 

441 Block(gate=Gates.CRZ, topology=Topology.stairs), 

442 ) 

443 

444 class Circuit_4(DeclarativeCircuit): 

445 @staticmethod 

446 def structure(): 

447 return ( 

448 Block(gate=Gates.RX), 

449 Block(gate=Gates.RZ), 

450 Block(gate=Gates.CRX, topology=Topology.stairs), 

451 ) 

452 

453 class Circuit_5(DeclarativeCircuit): 

454 @staticmethod 

455 def structure(): 

456 return ( 

457 Block(gate=Gates.RX), 

458 Block(gate=Gates.RZ), 

459 Block(gate=Gates.CRZ, topology=Topology.all_to_all), 

460 Block(gate=Gates.RX), 

461 Block(gate=Gates.RZ), 

462 ) 

463 

464 class Circuit_6(DeclarativeCircuit): 

465 @staticmethod 

466 def structure(): 

467 return ( 

468 Block(gate=Gates.RX), 

469 Block(gate=Gates.RZ), 

470 Block(gate=Gates.CRX, topology=Topology.all_to_all), 

471 Block(gate=Gates.RX), 

472 Block(gate=Gates.RZ), 

473 ) 

474 

475 class Circuit_7(DeclarativeCircuit): 

476 @staticmethod 

477 def structure(): 

478 return ( 

479 Block(gate=Gates.RX), 

480 Block(gate=Gates.RZ), 

481 Block( 

482 gate=Gates.CRZ, 

483 topology=Topology.bricks, 

484 ), 

485 Block(gate=Gates.RX), 

486 Block(gate=Gates.RZ), 

487 Block( 

488 gate=Gates.CRZ, 

489 topology=Topology.bricks, 

490 offset=1, 

491 ), 

492 ) 

493 

494 class Circuit_8(DeclarativeCircuit): 

495 @staticmethod 

496 def structure(): 

497 return ( 

498 Block(gate=Gates.RX), 

499 Block(gate=Gates.RZ), 

500 Block( 

501 gate=Gates.CRX, 

502 topology=Topology.bricks, 

503 ), 

504 Block(gate=Gates.RX), 

505 Block(gate=Gates.RZ), 

506 Block( 

507 gate=Gates.CRX, 

508 topology=Topology.bricks, 

509 offset=1, 

510 ), 

511 ) 

512 

513 class Circuit_9(DeclarativeCircuit): 

514 @staticmethod 

515 def structure(): 

516 return ( 

517 Block(gate=Gates.H), 

518 Block(gate="CZ", topology=Topology.stairs), 

519 Block(gate=Gates.RX), 

520 ) 

521 

522 class Circuit_10(DeclarativeCircuit): 

523 @staticmethod 

524 def structure(): 

525 return ( 

526 Block(gate=Gates.RY), 

527 Block(gate="CZ", topology=Topology.stairs, offset=-1, wrap=True), 

528 Block(gate=Gates.RY), 

529 ) 

530 

531 class Circuit_13(DeclarativeCircuit): 

532 @staticmethod 

533 def structure(): 

534 return ( 

535 Block(gate=Gates.RY), 

536 Block( 

537 gate=Gates.CRZ, 

538 topology=Topology.stairs, 

539 wrap=True, 

540 reverse=True, 

541 mirror=False, 

542 ), 

543 Block(gate=Gates.RY), 

544 Block( 

545 gate=Gates.CRZ, 

546 topology=Topology.stairs, 

547 reverse=False, 

548 mirror=False, 

549 offset=lambda n: n - 1, 

550 span=3, 

551 wrap=True, 

552 ), 

553 ) 

554 

555 class Circuit_14(DeclarativeCircuit): 

556 @staticmethod 

557 def structure(): 

558 return ( 

559 Block(gate=Gates.RY), 

560 Block( 

561 gate=Gates.CRX, 

562 topology=Topology.stairs, 

563 wrap=True, 

564 reverse=True, 

565 mirror=False, 

566 ), 

567 Block(gate=Gates.RY), 

568 Block( 

569 gate=Gates.CRX, 

570 topology=Topology.stairs, 

571 reverse=False, 

572 mirror=False, 

573 offset=lambda n: n - 1, 

574 span=3, 

575 wrap=True, 

576 ), 

577 ) 

578 

579 class Circuit_15(DeclarativeCircuit): 

580 @staticmethod 

581 def structure(): 

582 return ( 

583 Block(gate=Gates.RY), 

584 Block( 

585 gate=Gates.CX, 

586 topology=Topology.stairs, 

587 wrap=True, 

588 reverse=True, 

589 mirror=False, 

590 ), 

591 Block(gate=Gates.RY), 

592 Block( 

593 gate=Gates.CX, 

594 topology=Topology.stairs, 

595 reverse=False, 

596 mirror=False, 

597 offset=lambda n: n - 1, 

598 span=3, 

599 wrap=True, 

600 ), 

601 ) 

602 

603 class Circuit_16(DeclarativeCircuit): 

604 @staticmethod 

605 def structure(): 

606 return ( 

607 Block(gate=Gates.RX), 

608 Block(gate=Gates.RZ), 

609 Block( 

610 gate=Gates.CRZ, 

611 topology=Topology.bricks, 

612 ), 

613 Block( 

614 gate=Gates.CRZ, 

615 topology=Topology.bricks, 

616 offset=1, 

617 ), 

618 ) 

619 

620 class Circuit_17(DeclarativeCircuit): 

621 @staticmethod 

622 def structure(): 

623 return ( 

624 Block(gate=Gates.RX), 

625 Block(gate=Gates.RZ), 

626 Block( 

627 gate=Gates.CRX, 

628 topology=Topology.bricks, 

629 ), 

630 Block( 

631 gate=Gates.CRX, 

632 topology=Topology.bricks, 

633 offset=1, 

634 ), 

635 ) 

636 

637 class Circuit_18(DeclarativeCircuit): 

638 @staticmethod 

639 def structure(): 

640 return ( 

641 Block(gate=Gates.RX), 

642 Block(gate=Gates.RZ), 

643 Block( 

644 gate=Gates.CRZ, 

645 topology=Topology.stairs, 

646 wrap=True, 

647 mirror=False, 

648 ), 

649 ) 

650 

651 class Circuit_19(DeclarativeCircuit): 

652 @staticmethod 

653 def structure(): 

654 return ( 

655 Block(gate=Gates.RX), 

656 Block(gate=Gates.RZ), 

657 Block( 

658 gate=Gates.CRX, 

659 topology=Topology.stairs, 

660 wrap=True, 

661 mirror=False, 

662 ), 

663 ) 

664 

665 class Circuit_20(DeclarativeCircuit): 

666 @staticmethod 

667 def structure(): 

668 return ( 

669 Block(gate=Gates.RY), 

670 Block( 

671 gate=Gates.CX, 

672 topology=Topology.stairs, 

673 wrap=True, 

674 reverse=True, 

675 mirror=False, 

676 ), 

677 Block(gate=Gates.RY), 

678 Block( 

679 gate=Gates.CX, 

680 topology=Topology.stairs, 

681 reverse=False, 

682 offset=lambda n: n - 2, 

683 span=1, 

684 wrap=True, 

685 ), 

686 ) 

687 

688 class No_Entangling(DeclarativeCircuit): 

689 @staticmethod 

690 def structure(): 

691 return (Block(gate=Gates.Rot),) 

692 

693 class Hardware_Efficient(DeclarativeCircuit): 

694 @staticmethod 

695 def structure(): 

696 return ( 

697 Block(gate=Gates.RY), 

698 Block(gate=Gates.RZ), 

699 Block(gate=Gates.RY), 

700 Block( 

701 gate=Gates.CX, 

702 topology=Topology.bricks, 

703 mirror=False, 

704 ), 

705 Block( 

706 gate=Gates.CX, 

707 topology=Topology.bricks, 

708 offset=-1, 

709 modulo=True, 

710 wrap=True, 

711 mirror=False, 

712 ), 

713 ) 

714 

715 class Strongly_Entangling(DeclarativeCircuit): 

716 @staticmethod 

717 def structure(): 

718 return ( 

719 Block(gate=Gates.Rot), 

720 Block( 

721 gate=Gates.CX, 

722 topology=Topology.stairs, 

723 wrap=True, 

724 reverse=False, 

725 mirror=False, 

726 ), 

727 Block(gate=Gates.Rot), 

728 Block( 

729 gate=Gates.CX, 

730 topology=Topology.stairs, 

731 reverse=False, 

732 span=lambda n: n // 2, 

733 wrap=True, 

734 mirror=False, 

735 ), 

736 ) 

737 

738 

739class Encoding: 

740 def __init__( 

741 self, strategy: str, gates: Union[str, Callable, List[Union[str, Callable]]] 

742 ): 

743 """ 

744 Initializes an Encoding object. 

745 

746 Implementations closely follow https://doi.org/10.22331/q-2023-12-20-1210 

747 

748 Parameters 

749 ---------- 

750 strategy : str 

751 The encoding strategy to use. Available options: 

752 ['hamming', 'binary', 'ternary'] 

753 gates : Union[str, Callable, List[Union[str, Callable]]] 

754 The gates to use for encoding. Can be a string, a callable or a list 

755 of strings or callables. 

756 

757 Returns 

758 ------- 

759 None 

760 

761 Raises 

762 ------- 

763 ValueError 

764 If the encoding strategy is not implemented. 

765 ValueError 

766 If there is an error parsing the Gates. 

767 """ 

768 if strategy not in ["hamming", "binary", "ternary"]: 

769 raise ValueError( 

770 f"Encoding strategy {strategy} not implemented. " 

771 "Available options: ['hamming', 'binary', 'ternary']" 

772 ) 

773 self._strategy = strategy 

774 strategy = getattr(self, strategy) 

775 

776 log.debug(f"Using encoding strategy: '{strategy.__name__}'") 

777 

778 try: 

779 self._gates = Gates.parse_gates(gates, Gates) 

780 except ValueError as e: 

781 raise ValueError(f"Error parsing encodings: {e}") 

782 

783 self.callable = [strategy(g) for g in self._gates] 

784 

785 def __len__(self): 

786 return len(self.callable) 

787 

788 def __getitem__(self, idx): 

789 return self.callable[idx] 

790 

791 def get_n_freqs(self, omegas): 

792 """ 

793 Returns the number of frequencies required for the encoding strategy. 

794 This includes positive and negative side. 

795 

796 Parameters 

797 ---------- 

798 omegas : int 

799 The number of frequencies to encode. 

800 

801 Returns 

802 ------- 

803 int 

804 The number of frequencies required for the encoding strategy. 

805 """ 

806 if self._strategy == "hamming": 

807 return int(2 * omegas + 1) 

808 elif self._strategy == "binary": 

809 return int(2 ** (omegas + 1) - 1) 

810 elif self._strategy == "ternary": 

811 return int(3 ** (omegas)) 

812 else: 

813 raise NotImplementedError 

814 

815 def get_spectrum(self, omegas): 

816 """ 

817 Spectrum for one of the following encoding strategies: 

818 

819 Hamming: {-n_q -(n_q-1), ..., n_q} 

820 Binary: {-2^{n_q}+1, ..., 2^{n_q}-1} 

821 Ternary: {-floor(3^{n_q}/2), ..., floor(3^(n_q)/2)} 

822 

823 See https://doi.org/10.22331/q-2023-12-20-1210 for more details. 

824 

825 Parameters 

826 ---------- 

827 omegas : int 

828 The number of frequencies to encode. 

829 

830 Returns 

831 ------- 

832 np.ndarray 

833 The spectrum of the encoding strategy. 

834 """ 

835 if self._strategy == "hamming": 

836 return np.arange(-omegas, omegas + 1) 

837 elif self._strategy == "binary": 

838 return np.arange(-(2**omegas) + 1, 2**omegas) 

839 elif self._strategy == "ternary": 

840 limit = int(np.floor(3**omegas / 2)) 

841 return np.arange(-limit, limit + 1) 

842 else: 

843 raise NotImplementedError 

844 

845 def hamming(self, enc): 

846 """ 

847 Hamming encoding strategy. 

848 

849 Returns an encoding function that uses the Hamming encoding strategy 

850 which uses 2 * omegas + 1 frequencies for the encoding. 

851 See https://doi.org/10.22331/q-2023-12-20-1210 for more details. 

852 

853 Parameters 

854 ---------- 

855 enc : Callable 

856 The encoding function to be wrapped. 

857 

858 Returns 

859 ------- 

860 Callable 

861 The wrapped encoding function. 

862 """ 

863 return enc 

864 

865 def binary(self, enc): 

866 """ 

867 Binary encoding strategy. 

868 

869 Returns an encoding function that scales the input by a factor of 2^wires. 

870 

871 Binary encoding uses 2^(omegas + 1) - 1 frequencies for the encoding. 

872 See https://doi.org/10.22331/q-2023-12-20-1210 for more details. 

873 

874 Parameters 

875 ---------- 

876 enc : Callable 

877 The encoding function to be wrapped. 

878 

879 Returns 

880 ------- 

881 Callable 

882 The wrapped encoding function. 

883 """ 

884 

885 def _enc(inputs, wires, **kwargs): 

886 return enc(inputs * (2**wires), wires, **kwargs) 

887 

888 return _enc 

889 

890 def ternary(self, enc): 

891 """ 

892 Ternary encoding strategy. 

893 

894 Returns an encoding function that scales the input by a factor of 3^wires. 

895 

896 Ternary encoding uses 3^(omegas + 1) - 1 frequencies for the encoding. 

897 See https://doi.org/10.22331/q-2023-12-20-1210 for more details. 

898 

899 Parameters 

900 ---------- 

901 enc : Callable 

902 The encoding function to be wrapped. 

903 

904 Returns 

905 ------- 

906 Callable 

907 The wrapped encoding function. 

908 """ 

909 

910 def _enc(inputs, wires, **kwargs): 

911 return enc(inputs * (3**wires), wires, **kwargs) 

912 

913 return _enc 

914 

915 def golomb(self, enc): 

916 raise NotImplementedError