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
« 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
7from qml_essentials.gates import Gates, PulseInformation
8from qml_essentials.topologies import Topology
10log = logging.getLogger(__name__)
13class Circuit(ABC):
14 """Abstract base class for quantum circuit ansätze."""
16 def __init__(self) -> None:
17 """Initialize the circuit."""
18 pass
20 @abstractmethod
21 def n_params_per_layer(self, n_qubits: int) -> int:
22 """
23 Get the number of parameters per circuit layer.
25 Args:
26 n_qubits (int): Number of qubits in the circuit.
28 Returns:
29 int: Number of parameters required per layer.
31 Raises:
32 NotImplementedError: Must be implemented by subclasses.
33 """
34 raise NotImplementedError("n_params_per_layer method is not implemented")
36 def n_pulse_params_per_layer(self, n_qubits: int) -> int:
37 """
38 Get the number of pulse parameters per circuit layer.
40 Subclasses that do not use pulse-level simulation do not need to
41 override this method.
43 Args:
44 n_qubits (int): Number of qubits in the circuit.
46 Returns:
47 int: Number of pulse parameters required per layer.
49 Raises:
50 NotImplementedError: If called but not overridden by subclass.
51 """
52 raise NotImplementedError("n_pulse_params_per_layer method is not implemented")
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.
59 Returns slice indices [start:stop:step] for extracting controlled
60 gate parameters from a full parameter array for one layer.
62 Args:
63 n_qubits (int): Number of qubits in the circuit.
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.
70 Raises:
71 NotImplementedError: Must be implemented by subclasses.
72 """
73 raise NotImplementedError("get_control_indices method is not implemented")
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.
79 Args:
80 w (np.ndarray): Parameter array for one layer.
81 n_qubits (int): Number of qubits in the circuit.
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([])
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))
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.
100 Internal method that handles pulse parameter validation and context
101 management before delegating to the build() method.
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.
112 Returns:
113 Any: Result from the build() method.
115 Raises:
116 ValueError: If pulse_params length doesn't match expected count.
117 """
118 gate_mode = kwargs.get("gate_mode", "unitary")
120 if gate_mode == "pulse" and "pulse_params" in kwargs:
121 pulse_params_per_layer = self.n_pulse_params_per_layer(n_qubits)
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 )
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)
135 @abstractmethod
136 def build(self, w: np.ndarray, n_qubits: int, **kwargs: Any) -> Any:
137 """
138 Build one layer of the quantum circuit.
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.
145 Returns:
146 Any: Circuit construction result.
148 Raises:
149 NotImplementedError: Must be implemented by subclasses.
150 """
151 raise NotImplementedError("build method is not implemented")
153 def __call__(self, *args: Any, **kwds: Any) -> Any:
154 """Call the _build method with provided arguments."""
155 self._build(*args, **kwds)
158class DeclarativeCircuit(Circuit):
159 """
160 A circuit defined entirely by a sequence of Block descriptors.
162 Subclasses only need to set the class attribute `structure` — a tuple of
164 All of `n_params_per_layer`, `n_pulse_params_per_layer`,
165 `get_control_indices`, and `build` are derived automatically.
166 """
168 @classmethod
169 def structure(cls) -> Tuple[Any, ...]:
170 """Override in subclass to return the structure tuple."""
171 raise NotImplementedError
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())
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())
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)
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
200 # FIXME: this last part should be reworked
202 if not controlled_indices:
203 return None
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]
212 # Fallback: return raw indices (future-proof)
213 return controlled_indices
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)
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.
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
245 if self.is_entangling:
246 assert topology is not None, (
247 "Topology must be specified for entangling gates"
248 )
250 self.topology = topology
251 self.kwargs = kwargs
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 )
262 @property
263 def is_entangling(self):
264 return Gates.is_entangling(self.gate)
266 @property
267 def is_rotational(self):
268 return Gates.is_rotational(self.gate)
270 @property
271 def is_controlled_rotation(self):
272 return self.is_entangling and self.is_rotational
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)
282 return (n_qubits >= 2) and (n_qubits > span)
284 return n_qubits >= 1
286 def n_params(self, n_qubits: int) -> int:
287 assert n_qubits > 0, "Number of qubits must be positive"
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
303 return 0
305 def n_pulse_params(self, n_qubits: int) -> int:
306 assert n_qubits > 0, "Number of qubits must be positive"
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
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.
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.
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"
342 iterator = (
343 self.topology(n_qubits=n_qubits, **self.kwargs)
344 if self.is_entangling
345 else range(n_qubits)
346 )
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
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"
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
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 ]
401 # extend by the non-parameterized ones
402 if not parameterized_only:
403 ansaetze += [
404 Ansaetze.No_Ansatz,
405 Ansaetze.GHZ,
406 ]
408 return ansaetze
410 class No_Ansatz(DeclarativeCircuit):
411 @classmethod
412 def structure(cls):
413 return ()
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 )
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)
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
435 class Circuit_1(DeclarativeCircuit):
436 @classmethod
437 def structure(cls):
438 return (
439 Block(gate=Gates.RX),
440 Block(gate=Gates.RZ),
441 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
708 class No_Entangling(DeclarativeCircuit):
709 @classmethod
710 def structure(cls):
711 return (Block(gate=Gates.Rot),)
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 )
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 )
759class Encoding:
760 def __init__(
761 self, strategy: str, gates: Union[str, Callable, List[Union[str, Callable]]]
762 ):
763 """
764 Initializes an Encoding object.
766 Implementations closely follow https://doi.org/10.22331/q-2023-12-20-1210
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.
777 Returns
778 -------
779 None
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)
796 log.debug(f"Using encoding strategy: '{strategy_fn.__name__}'")
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}")
807 self.callable = [strategy_fn(g) for g in self._gates]
809 def __len__(self):
810 return len(self.callable)
812 def __getitem__(self, idx):
813 return self.callable[idx]
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.
820 Parameters
821 ----------
822 omegas : int
823 The number of frequencies to encode.
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
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")
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
850 def get_spectrum(self, omegas):
851 """
852 Spectrum for one of the following encoding strategies:
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
860 See https://doi.org/10.22331/q-2023-12-20-1210 for more details.
862 Parameters
863 ----------
864 omegas : int
865 The number of frequencies to encode.
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
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
893 def hamming(self, enc):
894 """
895 Hamming encoding strategy.
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.
901 Parameters
902 ----------
903 enc : Callable
904 The encoding function to be wrapped.
906 Returns
907 -------
908 Callable
909 The wrapped encoding function.
910 """
911 return enc
913 def binary(self, enc):
914 """
915 Binary encoding strategy.
917 Returns an encoding function that scales the input by a factor of 2^wires.
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.
922 Parameters
923 ----------
924 enc : Callable
925 The encoding function to be wrapped.
927 Returns
928 -------
929 Callable
930 The wrapped encoding function.
931 """
933 def _enc(inputs, wires, **kwargs):
934 return enc(inputs * (2**wires), wires, **kwargs)
936 return _enc
938 def ternary(self, enc):
939 """
940 Ternary encoding strategy.
942 Returns an encoding function that scales the input by a factor of 3^wires.
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.
947 Parameters
948 ----------
949 enc : Callable
950 The encoding function to be wrapped.
952 Returns
953 -------
954 Callable
955 The wrapped encoding function.
956 """
958 def _enc(inputs, wires, **kwargs):
959 return enc(inputs * (3**wires), wires, **kwargs)
961 return _enc
963 @property
964 def is_golomb(self):
965 """Whether this encoding uses the Golomb (multi-qubit diagonal) strategy."""
966 return self._strategy == "golomb"
968 def golomb(self, enc):
969 """Golomb encoding strategy.
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*.
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.
982 See Peters et al., arXiv:2209.05523, Sec. 3.1 and Appendix C.4.
984 Parameters
985 ----------
986 enc : Callable or None
987 Ignored (Golomb encoding uses its own multi-qubit gate).
989 Returns
990 -------
991 Callable
992 A callable with the same signature as per-qubit encoding
993 functions but that applies ``Gates.GolombEncoding``.
994 """
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)
1000 return _enc