Coverage for qml_essentials / ansaetze.py: 94%

318 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-05-16 10:19 +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) -> 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) -> 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 @classmethod 

169 def structure(cls) -> 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: Any) -> 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 (Any): 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 topology is not None, ( 

247 "Topology must be specified for entangling gates" 

248 ) 

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 (Any): 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 @classmethod 

412 def structure(cls): 

413 return () 

414 

415 class GHZ(DeclarativeCircuit): 

416 @classmethod 

417 def structure(cls): 

418 return ( 

419 Block(gate=Gates.H), 

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

421 ) 

422 

423 @classmethod 

424 def build(cls, 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 @classmethod 

430 def n_pulse_params_per_layer(cls, 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 @classmethod 

437 def structure(cls): 

438 return ( 

439 Block(gate=Gates.RX), 

440 Block(gate=Gates.RZ), 

441 ) 

442 

443 class Circuit_2(DeclarativeCircuit): 

444 @classmethod 

445 def structure(cls): 

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 @classmethod 

457 def structure(cls): 

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 @classmethod 

466 def structure(cls): 

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 @classmethod 

475 def structure(cls): 

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 @classmethod 

486 def structure(cls): 

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 @classmethod 

497 def structure(cls): 

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 @classmethod 

516 def structure(cls): 

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 @classmethod 

535 def structure(cls): 

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 @classmethod 

544 def structure(cls): 

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 @classmethod 

553 def structure(cls): 

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 @classmethod 

577 def structure(cls): 

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 @classmethod 

601 def structure(cls): 

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 @classmethod 

625 def structure(cls): 

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 @classmethod 

642 def structure(cls): 

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 @classmethod 

659 def structure(cls): 

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 @classmethod 

673 def structure(cls): 

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 @classmethod 

687 def structure(cls): 

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 @classmethod 

710 def structure(cls): 

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

712 

713 class Hardware_Efficient(DeclarativeCircuit): 

714 @classmethod 

715 def structure(cls): 

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 @classmethod 

737 def structure(cls): 

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", "golomb"]: 

789 raise ValueError( 

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

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

792 ) 

793 self._strategy = strategy 

794 strategy_fn = getattr(self, strategy) 

795 

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

797 

798 if self._strategy == "golomb": 

799 self._gates = [] 

800 self.callable = [strategy_fn(None)] 

801 else: 

802 try: 

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

804 except ValueError as e: 

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

806 

807 self.callable = [strategy_fn(g) for g in self._gates] 

808 

809 def __len__(self): 

810 return len(self.callable) 

811 

812 def __getitem__(self, idx): 

813 return self.callable[idx] 

814 

815 def get_n_freqs(self, omegas): 

816 """ 

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

818 This includes positive and negative side. 

819 

820 Parameters 

821 ---------- 

822 omegas : int 

823 The number of frequencies to encode. 

824 

825 Returns 

826 ------- 

827 int 

828 The number of frequencies required for the encoding strategy. 

829 """ 

830 if self._strategy == "hamming": 

831 return int(2 * omegas + 1) 

832 elif self._strategy == "binary": 

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

834 elif self._strategy == "ternary": 

835 return int(3 ** (omegas)) 

836 elif self._strategy == "golomb": 

837 from qml_essentials.unitary import golomb_ruler 

838 

839 n_qubits = getattr(self, "_n_qubits", None) 

840 if n_qubits is None: 

841 raise ValueError("Golomb encoding requires n_qubits to be set") 

842 

843 d = 2**n_qubits 

844 marks = golomb_ruler(d) 

845 max_mark = max(marks) 

846 return int(2 * omegas * max_mark + 1) 

847 else: 

848 raise NotImplementedError 

849 

850 def get_spectrum(self, omegas): 

851 """ 

852 Spectrum for one of the following encoding strategies: 

853 

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

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

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

857 Golomb: all pairwise differences of Golomb ruler marks, 

858 scaled by the number of encoding applications 

859 

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

861 

862 Parameters 

863 ---------- 

864 omegas : int 

865 The number of frequencies to encode. 

866 

867 Returns 

868 ------- 

869 np.ndarray 

870 The spectrum of the encoding strategy. 

871 """ 

872 if self._strategy == "hamming": 

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

874 elif self._strategy == "binary": 

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

876 elif self._strategy == "ternary": 

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

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

879 elif self._strategy == "golomb": 

880 from qml_essentials.unitary import golomb_ruler 

881 

882 n_qubits = getattr(self, "_n_qubits", None) 

883 if n_qubits is None: 

884 raise ValueError("Golomb encoding requires n_qubits to be set") 

885 d = 2**n_qubits 

886 marks = golomb_ruler(d) 

887 max_mark = max(marks) 

888 limit = omegas * max_mark 

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

890 else: 

891 raise NotImplementedError 

892 

893 def hamming(self, enc): 

894 """ 

895 Hamming encoding strategy. 

896 

897 Returns an encoding function that uses the Hamming encoding strategy 

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

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

900 

901 Parameters 

902 ---------- 

903 enc : Callable 

904 The encoding function to be wrapped. 

905 

906 Returns 

907 ------- 

908 Callable 

909 The wrapped encoding function. 

910 """ 

911 return enc 

912 

913 def binary(self, enc): 

914 """ 

915 Binary encoding strategy. 

916 

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

918 

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

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

921 

922 Parameters 

923 ---------- 

924 enc : Callable 

925 The encoding function to be wrapped. 

926 

927 Returns 

928 ------- 

929 Callable 

930 The wrapped encoding function. 

931 """ 

932 

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

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

935 

936 return _enc 

937 

938 def ternary(self, enc): 

939 """ 

940 Ternary encoding strategy. 

941 

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

943 

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

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

946 

947 Parameters 

948 ---------- 

949 enc : Callable 

950 The encoding function to be wrapped. 

951 

952 Returns 

953 ------- 

954 Callable 

955 The wrapped encoding function. 

956 """ 

957 

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

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

960 

961 return _enc 

962 

963 @property 

964 def is_golomb(self): 

965 """Whether this encoding uses the Golomb (multi-qubit diagonal) strategy.""" 

966 return self._strategy == "golomb" 

967 

968 def golomb(self, enc): 

969 """Golomb encoding strategy. 

970 

971 Returns a callable that applies a multi-qubit diagonal unitary 

972 ``S(x) = exp(-i H x)`` where ``H = diag(golomb_marks)`` to all 

973 qubits simultaneously. This produces the largest possible 

974 ``|Ω| = d(d-1)+1`` for any *d*-dimensional Hamiltonian, with 

975 ``|R(k)| = 1`` for all nonzero frequencies *k*. 

976 

977 Unlike the other strategies, Golomb encoding does *not* wrap a 

978 per-qubit gate. Instead, the model's ``_iec`` method detects 

979 ``is_golomb`` and applies a single ``GolombEncoding`` gate on 

980 all qubits. 

981 

982 See Peters et al., arXiv:2209.05523, Sec. 3.1 and Appendix C.4. 

983 

984 Parameters 

985 ---------- 

986 enc : Callable or None 

987 Ignored (Golomb encoding uses its own multi-qubit gate). 

988 

989 Returns 

990 ------- 

991 Callable 

992 A callable with the same signature as per-qubit encoding 

993 functions but that applies ``Gates.GolombEncoding``. 

994 """ 

995 

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

997 # `wires` here is a list of all qubit indices, set by _iec 

998 Gates.GolombEncoding(w=inputs, wires=wires, **kwargs) 

999 

1000 return _enc