Coverage for qml_essentials/ansaetze.py: 94%

286 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2026-02-20 14:03 +0000

1from abc import ABC, abstractmethod 

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

3import jax.numpy as np 

4import jax 

5import logging 

6import warnings 

7 

8from qml_essentials.gates import Gates, PulseInformation 

9from qml_essentials.topologies import Topology 

10 

11jax.config.update("jax_enable_x64", True) 

12log = logging.getLogger(__name__) 

13 

14 

15class Circuit(ABC): 

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

17 

18 def __init__(self) -> None: 

19 """Initialize the circuit.""" 

20 pass 

21 

22 @abstractmethod 

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

24 """ 

25 Get the number of parameters per circuit layer. 

26 

27 Args: 

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

29 

30 Returns: 

31 int: Number of parameters required per layer. 

32 

33 Raises: 

34 NotImplementedError: Must be implemented by subclasses. 

35 """ 

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

37 

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

39 """ 

40 Get the number of pulse parameters per circuit layer. 

41 

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

43 override this method. 

44 

45 Args: 

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

47 

48 Returns: 

49 int: Number of pulse parameters required per layer. 

50 

51 Raises: 

52 NotImplementedError: If called but not overridden by subclass. 

53 """ 

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

55 

56 @abstractmethod 

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

58 """ 

59 Get indices for controlled rotation gates in one layer. 

60 

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

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

63 

64 Args: 

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

66 

67 Returns: 

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

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

70 rotation gates. 

71 

72 Raises: 

73 NotImplementedError: Must be implemented by subclasses. 

74 """ 

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

76 

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

78 """ 

79 Extract angles for controlled rotation gates from parameter array. 

80 

81 Args: 

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

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

84 

85 Returns: 

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

87 or empty array if circuit contains no controlled gates. 

88 """ 

89 indices = self.get_control_indices(n_qubits) 

90 if indices is None: 

91 return np.array([]) 

92 

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

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

95 else: 

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

97 

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

99 """ 

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

101 

102 Internal method that handles pulse parameter validation and context 

103 management before delegating to the build() method. 

104 

105 Args: 

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

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

108 **kwargs: Additional keyword arguments: 

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

110 pulse-level simulation. 

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

112 - noise_params (Dict): Noise parameters dictionary. 

113 

114 Returns: 

115 Any: Result from the build() method. 

116 

117 Raises: 

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

119 """ 

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

121 

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

123 pulse_params_per_layer = self.n_pulse_params_per_layer(n_qubits) 

124 

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

126 raise ValueError( 

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

128 f"does not match expected {pulse_params_per_layer} " 

129 f"for {n_qubits} qubits" 

130 ) 

131 

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

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

134 else: 

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

136 

137 @abstractmethod 

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

139 """ 

140 Build one layer of the quantum circuit. 

141 

142 Args: 

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

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

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

146 

147 Returns: 

148 Any: Circuit construction result. 

149 

150 Raises: 

151 NotImplementedError: Must be implemented by subclasses. 

152 """ 

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

154 

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

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

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

158 

159 

160class DeclarativeCircuit(Circuit): 

161 """ 

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

163 

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

165 

166 All of `n_params_per_layer`, `n_pulse_params_per_layer`, 

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

168 """ 

169 

170 @staticmethod 

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

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

173 raise NotImplementedError 

174 

175 @classmethod 

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

177 structure = cls.structure() 

178 n_params = 0 

179 for block in structure: 

180 # we can rely on n_params only returning a valid number 

181 _n_params = block.n_params(n_qubits) 

182 

183 n_params += _n_params 

184 

185 return n_params 

186 

187 @classmethod 

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

189 structure = cls.structure() 

190 return sum(block.n_pulse_params(n_qubits) for block in structure) 

191 

192 @classmethod 

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

194 """ 

195 Computes parameter indices for controlled rotation Gates. 

196 Scans the structure for Block with 

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

198 """ 

199 structure = cls.structure() 

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

201 

202 # Collect which parameter indices correspond to controlled rotations 

203 controlled_indices = [] 

204 offset = 0 

205 for block in structure: 

206 n = block.n_params(n_qubits) 

207 if block.is_controlled_rotation: 

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

209 offset += n 

210 

211 # FIXME: this last part should be reworked 

212 

213 if not controlled_indices: 

214 return None 

215 

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

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

218 if controlled_indices == list( 

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

220 ): 

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

222 

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

224 return controlled_indices 

225 

226 @classmethod 

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

228 structure = cls.structure() 

229 w_idx = 0 

230 for block in structure: 

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

232 

233 

234class Block: 

235 def __init__( 

236 self, 

237 gate: str, 

238 topology: Any = None, 

239 **kwargs, 

240 ): 

241 """ 

242 Initialize a Block object; the atoms of Ansatzes. 

243 

244 Args: 

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

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

247 Defaults to None. 

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

249 """ 

250 if isinstance(gate, str): 

251 self.gate = getattr(Gates, gate) 

252 else: 

253 self.gate = gate 

254 

255 if self.is_entangling: 

256 assert ( 

257 topology is not None 

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

259 

260 self.topology = topology 

261 self.kwargs = kwargs 

262 

263 def __repr__(self): 

264 if self.topology is None: 

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

266 else: 

267 return ( 

268 f"{self.__class__.__name__}" 

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

270 ) 

271 

272 @property 

273 def is_entangling(self): 

274 return Gates.is_entangling(self.gate) 

275 

276 @property 

277 def is_rotational(self): 

278 return Gates.is_rotational(self.gate) 

279 

280 @property 

281 def is_controlled_rotation(self): 

282 return self.is_entangling and self.is_rotational 

283 

284 def enough_qubits(self, n_qubits): 

285 if self.is_entangling: 

286 # NOTE This must be adjusted if default values 

287 # in Topology change 

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

289 if callable(span): 

290 span = span(n_qubits) 

291 

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

293 

294 return n_qubits >= 1 

295 

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

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

298 

299 if self.is_rotational: 

300 if self.is_entangling: 

301 if not self.enough_qubits(n_qubits): 

302 warnings.warn( 

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

304 f"as there are not enough qubits" 

305 f"for this topology." 

306 ) 

307 return 0 

308 else: 

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

310 else: 

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

312 

313 return 0 

314 

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

316 return PulseInformation.num_params(self.gate) * n_qubits 

317 

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

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

320 

321 iterator = ( 

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

323 if self.is_entangling 

324 else range(n_qubits) 

325 ) 

326 

327 for wires in iterator: 

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

329 warnings.warn( 

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

331 f"as there are not enough qubits" 

332 f"for this topology." 

333 ) 

334 continue 

335 

336 if self.is_rotational: 

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

338 self.gate( 

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

340 ) 

341 w_idx += 3 

342 else: 

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

344 w_idx += 1 

345 else: 

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

347 return w_idx 

348 

349 

350class Ansaetze: 

351 def get_available(): 

352 return [ 

353 Ansaetze.No_Ansatz, 

354 Ansaetze.Circuit_1, 

355 Ansaetze.Circuit_2, 

356 Ansaetze.Circuit_3, 

357 Ansaetze.Circuit_4, 

358 Ansaetze.Circuit_5, 

359 Ansaetze.Circuit_6, 

360 Ansaetze.Circuit_7, 

361 Ansaetze.Circuit_8, 

362 Ansaetze.Circuit_9, 

363 Ansaetze.Circuit_10, 

364 Ansaetze.Circuit_13, 

365 Ansaetze.Circuit_14, 

366 Ansaetze.Circuit_15, 

367 Ansaetze.Circuit_16, 

368 Ansaetze.Circuit_17, 

369 Ansaetze.Circuit_18, 

370 Ansaetze.Circuit_19, 

371 Ansaetze.Circuit_20, 

372 Ansaetze.No_Entangling, 

373 Ansaetze.Strongly_Entangling, 

374 Ansaetze.Hardware_Efficient, 

375 Ansaetze.GHZ, 

376 ] 

377 

378 class No_Ansatz(DeclarativeCircuit): 

379 @staticmethod 

380 def structure(): 

381 return () 

382 

383 class GHZ(DeclarativeCircuit): 

384 @staticmethod 

385 def structure(): 

386 return ( 

387 Block(gate=Gates.H), 

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

389 ) 

390 

391 @staticmethod 

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

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

394 for q in range(n_qubits - 1): 

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

396 

397 @staticmethod 

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

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

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

401 return n_params 

402 

403 class Circuit_1(DeclarativeCircuit): 

404 @staticmethod 

405 def structure(): 

406 return ( 

407 Block(gate=Gates.RX), 

408 Block(gate=Gates.RZ), 

409 ) 

410 

411 class Circuit_2(DeclarativeCircuit): 

412 @staticmethod 

413 def structure(): 

414 return ( 

415 Block(gate=Gates.RX), 

416 Block(gate=Gates.RZ), 

417 Block( 

418 gate=Gates.CX, 

419 topology=Topology.stairs, 

420 ), 

421 ) 

422 

423 class Circuit_3(DeclarativeCircuit): 

424 @staticmethod 

425 def structure(): 

426 return ( 

427 Block(gate=Gates.RX), 

428 Block(gate=Gates.RZ), 

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

430 ) 

431 

432 class Circuit_4(DeclarativeCircuit): 

433 @staticmethod 

434 def structure(): 

435 return ( 

436 Block(gate=Gates.RX), 

437 Block(gate=Gates.RZ), 

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

439 ) 

440 

441 class Circuit_5(DeclarativeCircuit): 

442 @staticmethod 

443 def structure(): 

444 return ( 

445 Block(gate=Gates.RX), 

446 Block(gate=Gates.RZ), 

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

448 Block(gate=Gates.RX), 

449 Block(gate=Gates.RZ), 

450 ) 

451 

452 class Circuit_6(DeclarativeCircuit): 

453 @staticmethod 

454 def structure(): 

455 return ( 

456 Block(gate=Gates.RX), 

457 Block(gate=Gates.RZ), 

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

459 Block(gate=Gates.RX), 

460 Block(gate=Gates.RZ), 

461 ) 

462 

463 class Circuit_7(DeclarativeCircuit): 

464 @staticmethod 

465 def structure(): 

466 return ( 

467 Block(gate=Gates.RX), 

468 Block(gate=Gates.RZ), 

469 Block( 

470 gate=Gates.CRZ, 

471 topology=Topology.bricks, 

472 ), 

473 Block(gate=Gates.RX), 

474 Block(gate=Gates.RZ), 

475 Block( 

476 gate=Gates.CRZ, 

477 topology=Topology.bricks, 

478 offset=1, 

479 ), 

480 ) 

481 

482 class Circuit_8(DeclarativeCircuit): 

483 @staticmethod 

484 def structure(): 

485 return ( 

486 Block(gate=Gates.RX), 

487 Block(gate=Gates.RZ), 

488 Block( 

489 gate=Gates.CRX, 

490 topology=Topology.bricks, 

491 ), 

492 Block(gate=Gates.RX), 

493 Block(gate=Gates.RZ), 

494 Block( 

495 gate=Gates.CRX, 

496 topology=Topology.bricks, 

497 offset=1, 

498 ), 

499 ) 

500 

501 class Circuit_9(DeclarativeCircuit): 

502 @staticmethod 

503 def structure(): 

504 return ( 

505 Block(gate=Gates.H), 

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

507 Block(gate=Gates.RX), 

508 ) 

509 

510 class Circuit_10(DeclarativeCircuit): 

511 @staticmethod 

512 def structure(): 

513 return ( 

514 Block(gate=Gates.RY), 

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

516 Block(gate=Gates.RY), 

517 ) 

518 

519 class Circuit_13(DeclarativeCircuit): 

520 @staticmethod 

521 def structure(): 

522 return ( 

523 Block(gate=Gates.RY), 

524 Block( 

525 gate=Gates.CRZ, 

526 topology=Topology.stairs, 

527 wrap=True, 

528 reverse=True, 

529 mirror=False, 

530 ), 

531 Block(gate=Gates.RY), 

532 Block( 

533 gate=Gates.CRZ, 

534 topology=Topology.stairs, 

535 reverse=False, 

536 mirror=False, 

537 offset=lambda n: n - 1, 

538 span=3, 

539 wrap=True, 

540 ), 

541 ) 

542 

543 class Circuit_14(DeclarativeCircuit): 

544 @staticmethod 

545 def structure(): 

546 return ( 

547 Block(gate=Gates.RY), 

548 Block( 

549 gate=Gates.CRX, 

550 topology=Topology.stairs, 

551 wrap=True, 

552 reverse=True, 

553 mirror=False, 

554 ), 

555 Block(gate=Gates.RY), 

556 Block( 

557 gate=Gates.CRX, 

558 topology=Topology.stairs, 

559 reverse=False, 

560 mirror=False, 

561 offset=lambda n: n - 1, 

562 span=3, 

563 wrap=True, 

564 ), 

565 ) 

566 

567 class Circuit_15(DeclarativeCircuit): 

568 @staticmethod 

569 def structure(): 

570 return ( 

571 Block(gate=Gates.RY), 

572 Block( 

573 gate=Gates.CX, 

574 topology=Topology.stairs, 

575 wrap=True, 

576 reverse=True, 

577 mirror=False, 

578 ), 

579 Block(gate=Gates.RY), 

580 Block( 

581 gate=Gates.CX, 

582 topology=Topology.stairs, 

583 reverse=False, 

584 mirror=False, 

585 offset=lambda n: n - 1, 

586 span=3, 

587 wrap=True, 

588 ), 

589 ) 

590 

591 class Circuit_16(DeclarativeCircuit): 

592 @staticmethod 

593 def structure(): 

594 return ( 

595 Block(gate=Gates.RX), 

596 Block(gate=Gates.RZ), 

597 Block( 

598 gate=Gates.CRZ, 

599 topology=Topology.bricks, 

600 ), 

601 Block( 

602 gate=Gates.CRZ, 

603 topology=Topology.bricks, 

604 offset=1, 

605 ), 

606 ) 

607 

608 class Circuit_17(DeclarativeCircuit): 

609 @staticmethod 

610 def structure(): 

611 return ( 

612 Block(gate=Gates.RX), 

613 Block(gate=Gates.RZ), 

614 Block( 

615 gate=Gates.CRX, 

616 topology=Topology.bricks, 

617 ), 

618 Block( 

619 gate=Gates.CRX, 

620 topology=Topology.bricks, 

621 offset=1, 

622 ), 

623 ) 

624 

625 class Circuit_18(DeclarativeCircuit): 

626 @staticmethod 

627 def structure(): 

628 return ( 

629 Block(gate=Gates.RX), 

630 Block(gate=Gates.RZ), 

631 Block( 

632 gate=Gates.CRZ, 

633 topology=Topology.stairs, 

634 wrap=True, 

635 mirror=False, 

636 ), 

637 ) 

638 

639 class Circuit_19(DeclarativeCircuit): 

640 @staticmethod 

641 def structure(): 

642 return ( 

643 Block(gate=Gates.RX), 

644 Block(gate=Gates.RZ), 

645 Block( 

646 gate=Gates.CRX, 

647 topology=Topology.stairs, 

648 wrap=True, 

649 mirror=False, 

650 ), 

651 ) 

652 

653 class Circuit_20(DeclarativeCircuit): 

654 @staticmethod 

655 def structure(): 

656 return ( 

657 Block(gate=Gates.RY), 

658 Block( 

659 gate=Gates.CX, 

660 topology=Topology.stairs, 

661 wrap=True, 

662 reverse=True, 

663 mirror=False, 

664 ), 

665 Block(gate=Gates.RY), 

666 Block( 

667 gate=Gates.CX, 

668 topology=Topology.stairs, 

669 reverse=False, 

670 offset=lambda n: n - 2, 

671 span=1, 

672 wrap=True, 

673 ), 

674 ) 

675 

676 class No_Entangling(DeclarativeCircuit): 

677 @staticmethod 

678 def structure(): 

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

680 

681 class Hardware_Efficient(DeclarativeCircuit): 

682 @staticmethod 

683 def structure(): 

684 return ( 

685 Block(gate=Gates.RY), 

686 Block(gate=Gates.RZ), 

687 Block(gate=Gates.RY), 

688 Block( 

689 gate=Gates.CX, 

690 topology=Topology.bricks, 

691 mirror=False, 

692 ), 

693 Block( 

694 gate=Gates.CX, 

695 topology=Topology.bricks, 

696 offset=-1, 

697 modulo=True, 

698 wrap=True, 

699 mirror=False, 

700 ), 

701 ) 

702 

703 class Strongly_Entangling(DeclarativeCircuit): 

704 @staticmethod 

705 def structure(): 

706 return ( 

707 Block(gate=Gates.Rot), 

708 Block( 

709 gate=Gates.CX, 

710 topology=Topology.stairs, 

711 wrap=True, 

712 reverse=False, 

713 mirror=False, 

714 ), 

715 Block(gate=Gates.Rot), 

716 Block( 

717 gate=Gates.CX, 

718 topology=Topology.stairs, 

719 reverse=False, 

720 span=lambda n: n // 2, 

721 wrap=True, 

722 mirror=False, 

723 ), 

724 ) 

725 

726 

727class Encoding: 

728 def __init__( 

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

730 ): 

731 """ 

732 Initializes an Encoding object. 

733 

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

735 

736 Parameters 

737 ---------- 

738 strategy : str 

739 The encoding strategy to use. Available options: 

740 ['hamming', 'binary', 'ternary'] 

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

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

743 of strings or callables. 

744 

745 Returns 

746 ------- 

747 None 

748 

749 Raises 

750 ------- 

751 ValueError 

752 If the encoding strategy is not implemented. 

753 ValueError 

754 If there is an error parsing the Gates. 

755 """ 

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

757 raise ValueError( 

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

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

760 ) 

761 self._strategy = strategy 

762 strategy = getattr(self, strategy) 

763 

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

765 

766 try: 

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

768 except ValueError as e: 

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

770 

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

772 

773 def __len__(self): 

774 return len(self.callable) 

775 

776 def __getitem__(self, idx): 

777 return self.callable[idx] 

778 

779 def get_n_freqs(self, omegas): 

780 """ 

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

782 

783 Parameters 

784 ---------- 

785 omegas : int 

786 The number of frequencies to encode. 

787 

788 Returns 

789 ------- 

790 int 

791 The number of frequencies required for the encoding strategy. 

792 """ 

793 if self._strategy == "hamming": 

794 return int(2 * omegas + 1) 

795 elif self._strategy == "binary": 

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

797 elif self._strategy == "ternary": 

798 return int(3 ** (omegas)) 

799 else: 

800 raise NotImplementedError 

801 

802 def get_spectrum(self, omegas): 

803 """ 

804 Spectrum for one of the following encoding strategies: 

805 

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

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

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

809 

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

811 

812 Parameters 

813 ---------- 

814 omegas : int 

815 The number of frequencies to encode. 

816 

817 Returns 

818 ------- 

819 np.ndarray 

820 The spectrum of the encoding strategy. 

821 """ 

822 if self._strategy == "hamming": 

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

824 elif self._strategy == "binary": 

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

826 elif self._strategy == "ternary": 

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

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

829 else: 

830 raise NotImplementedError 

831 

832 def hamming(self, enc): 

833 """ 

834 Hamming encoding strategy. 

835 

836 Returns an encoding function that uses the Hamming encoding strategy 

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

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

839 

840 Parameters 

841 ---------- 

842 enc : Callable 

843 The encoding function to be wrapped. 

844 

845 Returns 

846 ------- 

847 Callable 

848 The wrapped encoding function. 

849 """ 

850 return enc 

851 

852 def binary(self, enc): 

853 """ 

854 Binary encoding strategy. 

855 

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

857 

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

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

860 

861 Parameters 

862 ---------- 

863 enc : Callable 

864 The encoding function to be wrapped. 

865 

866 Returns 

867 ------- 

868 Callable 

869 The wrapped encoding function. 

870 """ 

871 

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

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

874 

875 return _enc 

876 

877 def ternary(self, enc): 

878 """ 

879 Ternary encoding strategy. 

880 

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

882 

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

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

885 

886 Parameters 

887 ---------- 

888 enc : Callable 

889 The encoding function to be wrapped. 

890 

891 Returns 

892 ------- 

893 Callable 

894 The wrapped encoding function. 

895 """ 

896 

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

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

899 

900 return _enc 

901 

902 def golomb(self, enc): 

903 raise NotImplementedError