Coverage for qml_essentials / ansaetze.py: 94%

291 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-30 11:43 +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(n_qubits, w, w_idx, **kwargs) 

221 Gates.Barrier(wires=list(range(n_qubits)), **kwargs) 

222 

223 

224class Block: 

225 def __init__( 

226 self, 

227 gate: str, 

228 topology: Any = None, 

229 **kwargs, 

230 ): 

231 """ 

232 Initialize a Block object; the atoms of Ansatzes. 

233 

234 Args: 

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

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

237 Defaults to None. 

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

239 """ 

240 if isinstance(gate, str): 

241 self.gate = getattr(Gates, gate) 

242 else: 

243 self.gate = gate 

244 

245 if self.is_entangling: 

246 assert ( 

247 topology is not None 

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

249 

250 self.topology = topology 

251 self.kwargs = kwargs 

252 

253 def __repr__(self): 

254 if self.topology is None: 

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

256 else: 

257 return ( 

258 f"{self.__class__.__name__}" 

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

260 ) 

261 

262 @property 

263 def is_entangling(self): 

264 return Gates.is_entangling(self.gate) 

265 

266 @property 

267 def is_rotational(self): 

268 return Gates.is_rotational(self.gate) 

269 

270 @property 

271 def is_controlled_rotation(self): 

272 return self.is_entangling and self.is_rotational 

273 

274 def enough_qubits(self, n_qubits): 

275 if self.is_entangling: 

276 # NOTE This must be adjusted if default values 

277 # in Topology change 

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

279 if callable(span): 

280 span = span(n_qubits) 

281 

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

283 

284 return n_qubits >= 1 

285 

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

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

288 

289 if self.is_rotational: 

290 if self.is_entangling: 

291 if not self.enough_qubits(n_qubits): 

292 warnings.warn( 

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

294 f"as there are not enough qubits" 

295 f"for this topology." 

296 ) 

297 return 0 

298 else: 

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

300 else: 

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

302 

303 return 0 

304 

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

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

307 

308 n_pulse_params = PulseInformation.num_params(self.gate) 

309 if self.is_entangling: 

310 if not self.enough_qubits(n_qubits): 

311 warnings.warn( 

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

313 f"as there are not enough qubits" 

314 f"for this topology." 

315 ) 

316 return 0 

317 else: 

318 return n_pulse_params * len( 

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

320 ) 

321 return n_pulse_params * n_qubits 

322 

323 def apply( 

324 self, n_qubits: int, w: np.ndarray = None, w_idx: int = None, **kwargs 

325 ) -> int: 

326 """ 

327 Applies the block to the given circuit. 

328 

329 Args: 

330 n_qubits (int): Number of qubits, the block is applied to. 

331 w (np.ndarray, optional): Weights to use for rotational gates. 

332 Defaults to None. 

333 w_idx (int, optional): Index of weights to use for rotational gates. 

334 Defaults to None. 

335 **kwargs: Keyword arguments passed to the gate. 

336 

337 Returns: 

338 int: The new index of weights after applying the block. 

339 """ 

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

341 

342 iterator = ( 

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

344 if self.is_entangling 

345 else range(n_qubits) 

346 ) 

347 

348 for wires in iterator: 

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

350 warnings.warn( 

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

352 f"as there are not enough qubits" 

353 f"for this topology." 

354 ) 

355 continue 

356 

357 if self.is_rotational: 

358 assert w is not None, "w must be provided for rotational gates" 

359 assert w_idx is not None, "w_idx must be provided for rotational gates" 

360 

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

362 self.gate( 

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

364 ) 

365 w_idx += 3 

366 else: 

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

368 w_idx += 1 

369 else: 

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

371 return w_idx 

372 

373 

374class Ansaetze: 

375 def get_available(parameterized_only=False): 

376 # list of parameterized ansaetze 

377 ansaetze = [ 

378 Ansaetze.Circuit_1, 

379 Ansaetze.Circuit_2, 

380 Ansaetze.Circuit_3, 

381 Ansaetze.Circuit_4, 

382 Ansaetze.Circuit_5, 

383 Ansaetze.Circuit_6, 

384 Ansaetze.Circuit_7, 

385 Ansaetze.Circuit_8, 

386 Ansaetze.Circuit_9, 

387 Ansaetze.Circuit_10, 

388 Ansaetze.Circuit_13, 

389 Ansaetze.Circuit_14, 

390 Ansaetze.Circuit_15, 

391 Ansaetze.Circuit_16, 

392 Ansaetze.Circuit_17, 

393 Ansaetze.Circuit_18, 

394 Ansaetze.Circuit_19, 

395 Ansaetze.Circuit_20, 

396 Ansaetze.No_Entangling, 

397 Ansaetze.Strongly_Entangling, 

398 Ansaetze.Hardware_Efficient, 

399 ] 

400 

401 # extend by the non-parameterized ones 

402 if not parameterized_only: 

403 ansaetze += [ 

404 Ansaetze.No_Ansatz, 

405 Ansaetze.GHZ, 

406 ] 

407 

408 return ansaetze 

409 

410 class No_Ansatz(DeclarativeCircuit): 

411 @staticmethod 

412 def structure(): 

413 return () 

414 

415 class GHZ(DeclarativeCircuit): 

416 @staticmethod 

417 def structure(): 

418 return ( 

419 Block(gate=Gates.H), 

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

421 ) 

422 

423 @staticmethod 

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

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

426 for q in range(n_qubits - 1): 

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

428 

429 @staticmethod 

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

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

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

433 return n_params 

434 

435 class Circuit_1(DeclarativeCircuit): 

436 @staticmethod 

437 def structure(): 

438 return ( 

439 Block(gate=Gates.RX), 

440 Block(gate=Gates.RZ), 

441 ) 

442 

443 class Circuit_2(DeclarativeCircuit): 

444 @staticmethod 

445 def structure(): 

446 return ( 

447 Block(gate=Gates.RX), 

448 Block(gate=Gates.RZ), 

449 Block( 

450 gate=Gates.CX, 

451 topology=Topology.stairs, 

452 ), 

453 ) 

454 

455 class Circuit_3(DeclarativeCircuit): 

456 @staticmethod 

457 def structure(): 

458 return ( 

459 Block(gate=Gates.RX), 

460 Block(gate=Gates.RZ), 

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

462 ) 

463 

464 class Circuit_4(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.stairs), 

471 ) 

472 

473 class Circuit_5(DeclarativeCircuit): 

474 @staticmethod 

475 def structure(): 

476 return ( 

477 Block(gate=Gates.RX), 

478 Block(gate=Gates.RZ), 

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

480 Block(gate=Gates.RX), 

481 Block(gate=Gates.RZ), 

482 ) 

483 

484 class Circuit_6(DeclarativeCircuit): 

485 @staticmethod 

486 def structure(): 

487 return ( 

488 Block(gate=Gates.RX), 

489 Block(gate=Gates.RZ), 

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

491 Block(gate=Gates.RX), 

492 Block(gate=Gates.RZ), 

493 ) 

494 

495 class Circuit_7(DeclarativeCircuit): 

496 @staticmethod 

497 def structure(): 

498 return ( 

499 Block(gate=Gates.RX), 

500 Block(gate=Gates.RZ), 

501 Block( 

502 gate=Gates.CRZ, 

503 topology=Topology.bricks, 

504 ), 

505 Block(gate=Gates.RX), 

506 Block(gate=Gates.RZ), 

507 Block( 

508 gate=Gates.CRZ, 

509 topology=Topology.bricks, 

510 offset=1, 

511 ), 

512 ) 

513 

514 class Circuit_8(DeclarativeCircuit): 

515 @staticmethod 

516 def structure(): 

517 return ( 

518 Block(gate=Gates.RX), 

519 Block(gate=Gates.RZ), 

520 Block( 

521 gate=Gates.CRX, 

522 topology=Topology.bricks, 

523 ), 

524 Block(gate=Gates.RX), 

525 Block(gate=Gates.RZ), 

526 Block( 

527 gate=Gates.CRX, 

528 topology=Topology.bricks, 

529 offset=1, 

530 ), 

531 ) 

532 

533 class Circuit_9(DeclarativeCircuit): 

534 @staticmethod 

535 def structure(): 

536 return ( 

537 Block(gate=Gates.H), 

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

539 Block(gate=Gates.RX), 

540 ) 

541 

542 class Circuit_10(DeclarativeCircuit): 

543 @staticmethod 

544 def structure(): 

545 return ( 

546 Block(gate=Gates.RY), 

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

548 Block(gate=Gates.RY), 

549 ) 

550 

551 class Circuit_13(DeclarativeCircuit): 

552 @staticmethod 

553 def structure(): 

554 return ( 

555 Block(gate=Gates.RY), 

556 Block( 

557 gate=Gates.CRZ, 

558 topology=Topology.stairs, 

559 wrap=True, 

560 reverse=True, 

561 mirror=False, 

562 ), 

563 Block(gate=Gates.RY), 

564 Block( 

565 gate=Gates.CRZ, 

566 topology=Topology.stairs, 

567 reverse=False, 

568 mirror=False, 

569 offset=lambda n: n - 1, 

570 span=3, 

571 wrap=True, 

572 ), 

573 ) 

574 

575 class Circuit_14(DeclarativeCircuit): 

576 @staticmethod 

577 def structure(): 

578 return ( 

579 Block(gate=Gates.RY), 

580 Block( 

581 gate=Gates.CRX, 

582 topology=Topology.stairs, 

583 wrap=True, 

584 reverse=True, 

585 mirror=False, 

586 ), 

587 Block(gate=Gates.RY), 

588 Block( 

589 gate=Gates.CRX, 

590 topology=Topology.stairs, 

591 reverse=False, 

592 mirror=False, 

593 offset=lambda n: n - 1, 

594 span=3, 

595 wrap=True, 

596 ), 

597 ) 

598 

599 class Circuit_15(DeclarativeCircuit): 

600 @staticmethod 

601 def structure(): 

602 return ( 

603 Block(gate=Gates.RY), 

604 Block( 

605 gate=Gates.CX, 

606 topology=Topology.stairs, 

607 wrap=True, 

608 reverse=True, 

609 mirror=False, 

610 ), 

611 Block(gate=Gates.RY), 

612 Block( 

613 gate=Gates.CX, 

614 topology=Topology.stairs, 

615 reverse=False, 

616 mirror=False, 

617 offset=lambda n: n - 1, 

618 span=3, 

619 wrap=True, 

620 ), 

621 ) 

622 

623 class Circuit_16(DeclarativeCircuit): 

624 @staticmethod 

625 def structure(): 

626 return ( 

627 Block(gate=Gates.RX), 

628 Block(gate=Gates.RZ), 

629 Block( 

630 gate=Gates.CRZ, 

631 topology=Topology.bricks, 

632 ), 

633 Block( 

634 gate=Gates.CRZ, 

635 topology=Topology.bricks, 

636 offset=1, 

637 ), 

638 ) 

639 

640 class Circuit_17(DeclarativeCircuit): 

641 @staticmethod 

642 def structure(): 

643 return ( 

644 Block(gate=Gates.RX), 

645 Block(gate=Gates.RZ), 

646 Block( 

647 gate=Gates.CRX, 

648 topology=Topology.bricks, 

649 ), 

650 Block( 

651 gate=Gates.CRX, 

652 topology=Topology.bricks, 

653 offset=1, 

654 ), 

655 ) 

656 

657 class Circuit_18(DeclarativeCircuit): 

658 @staticmethod 

659 def structure(): 

660 return ( 

661 Block(gate=Gates.RX), 

662 Block(gate=Gates.RZ), 

663 Block( 

664 gate=Gates.CRZ, 

665 topology=Topology.stairs, 

666 wrap=True, 

667 mirror=False, 

668 ), 

669 ) 

670 

671 class Circuit_19(DeclarativeCircuit): 

672 @staticmethod 

673 def structure(): 

674 return ( 

675 Block(gate=Gates.RX), 

676 Block(gate=Gates.RZ), 

677 Block( 

678 gate=Gates.CRX, 

679 topology=Topology.stairs, 

680 wrap=True, 

681 mirror=False, 

682 ), 

683 ) 

684 

685 class Circuit_20(DeclarativeCircuit): 

686 @staticmethod 

687 def structure(): 

688 return ( 

689 Block(gate=Gates.RY), 

690 Block( 

691 gate=Gates.CX, 

692 topology=Topology.stairs, 

693 wrap=True, 

694 reverse=True, 

695 mirror=False, 

696 ), 

697 Block(gate=Gates.RY), 

698 Block( 

699 gate=Gates.CX, 

700 topology=Topology.stairs, 

701 reverse=False, 

702 offset=lambda n: n - 2, 

703 span=1, 

704 wrap=True, 

705 ), 

706 ) 

707 

708 class No_Entangling(DeclarativeCircuit): 

709 @staticmethod 

710 def structure(): 

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

712 

713 class Hardware_Efficient(DeclarativeCircuit): 

714 @staticmethod 

715 def structure(): 

716 return ( 

717 Block(gate=Gates.RY), 

718 Block(gate=Gates.RZ), 

719 Block(gate=Gates.RY), 

720 Block( 

721 gate=Gates.CX, 

722 topology=Topology.bricks, 

723 mirror=False, 

724 ), 

725 Block( 

726 gate=Gates.CX, 

727 topology=Topology.bricks, 

728 offset=-1, 

729 modulo=True, 

730 wrap=True, 

731 mirror=False, 

732 ), 

733 ) 

734 

735 class Strongly_Entangling(DeclarativeCircuit): 

736 @staticmethod 

737 def structure(): 

738 return ( 

739 Block(gate=Gates.Rot), 

740 Block( 

741 gate=Gates.CX, 

742 topology=Topology.stairs, 

743 wrap=True, 

744 reverse=False, 

745 mirror=False, 

746 ), 

747 Block(gate=Gates.Rot), 

748 Block( 

749 gate=Gates.CX, 

750 topology=Topology.stairs, 

751 reverse=False, 

752 span=lambda n: n // 2, 

753 wrap=True, 

754 mirror=False, 

755 ), 

756 ) 

757 

758 

759class Encoding: 

760 def __init__( 

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

762 ): 

763 """ 

764 Initializes an Encoding object. 

765 

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

767 

768 Parameters 

769 ---------- 

770 strategy : str 

771 The encoding strategy to use. Available options: 

772 ['hamming', 'binary', 'ternary'] 

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

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

775 of strings or callables. 

776 

777 Returns 

778 ------- 

779 None 

780 

781 Raises 

782 ------- 

783 ValueError 

784 If the encoding strategy is not implemented. 

785 ValueError 

786 If there is an error parsing the Gates. 

787 """ 

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

789 raise ValueError( 

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

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

792 ) 

793 self._strategy = strategy 

794 strategy = getattr(self, strategy) 

795 

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

797 

798 try: 

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

800 except ValueError as e: 

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

802 

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

804 

805 def __len__(self): 

806 return len(self.callable) 

807 

808 def __getitem__(self, idx): 

809 return self.callable[idx] 

810 

811 def get_n_freqs(self, omegas): 

812 """ 

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

814 This includes positive and negative side. 

815 

816 Parameters 

817 ---------- 

818 omegas : int 

819 The number of frequencies to encode. 

820 

821 Returns 

822 ------- 

823 int 

824 The number of frequencies required for the encoding strategy. 

825 """ 

826 if self._strategy == "hamming": 

827 return int(2 * omegas + 1) 

828 elif self._strategy == "binary": 

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

830 elif self._strategy == "ternary": 

831 return int(3 ** (omegas)) 

832 else: 

833 raise NotImplementedError 

834 

835 def get_spectrum(self, omegas): 

836 """ 

837 Spectrum for one of the following encoding strategies: 

838 

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

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

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

842 

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

844 

845 Parameters 

846 ---------- 

847 omegas : int 

848 The number of frequencies to encode. 

849 

850 Returns 

851 ------- 

852 np.ndarray 

853 The spectrum of the encoding strategy. 

854 """ 

855 if self._strategy == "hamming": 

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

857 elif self._strategy == "binary": 

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

859 elif self._strategy == "ternary": 

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

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

862 else: 

863 raise NotImplementedError 

864 

865 def hamming(self, enc): 

866 """ 

867 Hamming encoding strategy. 

868 

869 Returns an encoding function that uses the Hamming encoding strategy 

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

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

872 

873 Parameters 

874 ---------- 

875 enc : Callable 

876 The encoding function to be wrapped. 

877 

878 Returns 

879 ------- 

880 Callable 

881 The wrapped encoding function. 

882 """ 

883 return enc 

884 

885 def binary(self, enc): 

886 """ 

887 Binary encoding strategy. 

888 

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

890 

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

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

893 

894 Parameters 

895 ---------- 

896 enc : Callable 

897 The encoding function to be wrapped. 

898 

899 Returns 

900 ------- 

901 Callable 

902 The wrapped encoding function. 

903 """ 

904 

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

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

907 

908 return _enc 

909 

910 def ternary(self, enc): 

911 """ 

912 Ternary encoding strategy. 

913 

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

915 

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

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

918 

919 Parameters 

920 ---------- 

921 enc : Callable 

922 The encoding function to be wrapped. 

923 

924 Returns 

925 ------- 

926 Callable 

927 The wrapped encoding function. 

928 """ 

929 

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

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

932 

933 return _enc 

934 

935 def golomb(self, enc): 

936 raise NotImplementedError