Coverage for qml_essentials / topologies.py: 98%
61 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-09-03 21:15 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-09-03 21:15 +0000
1from typing import List, Callable, Sequence, Tuple, Union
2from itertools import combinations
3import logging
5log = logging.getLogger(__name__)
8class Topology:
9 """
10 Generates [control, target] wire-pair lists for two-qubit gates.
12 All public methods are static and share a small set of private
13 helpers so that related topologies (e.g. ``linear`` / ``circular``,
14 ``brick_layer`` / ``brick_layer_wrap``) re-use the same core logic.
16 Raises
17 ------
18 ValueError
19 If ``n_qubits < 2`` is passed to any topology method.
20 """
22 @classmethod
23 def stairs(
24 cls,
25 n_qubits: int,
26 offset: Union[int, Callable] = 0,
27 wrap=False,
28 reverse: bool = True,
29 mirror: bool = True,
30 span: Union[int, Callable] = 1,
31 stride: int = 1,
32 modulo: bool = True,
33 ) -> List[List[int]]:
34 """
35 Unified generator for nearest-neighbour and spand pair topologies.
36 Produces ``[control, target]`` pairs of qubits.
38 The default values, produce an "upstairs" entangling sequence
39 without wrapping around the last gate.
41 Parameters
42 ----------
43 n_qubits : int
44 Number of qubits.
45 offset : Union[int, Callable]
46 Offset for starting the entangling sequence.
47 Can either be a integer or a callable that takes n_qubits as input.
48 wrap : bool
49 Wraps around the entangling gates.
50 reverse : bool
51 Reverses both the iteration direction (upstairs/ downstairs)
52 mirror: bool
53 Flip target/ control qubit
54 span : int
55 Offset between control and target qubit. Defaults to 1
56 stride : int
57 Step size for entangling gates. Defaults to 1, meaning a stair
58 pattern will be generated.
59 modulo : bool
60 If a gate should be placed when the iterator decreases below 0
61 or exceeds n_qubits. Defaults to True
63 Returns
64 -------
65 List[List[int]]
66 """
67 ctrls = []
68 targets = []
70 n_gates = n_qubits if wrap else n_qubits - 1
71 _offset = offset(n_qubits) if callable(offset) else offset
72 _span = span(n_qubits) if callable(span) else span
74 for q in range(0, n_gates, stride):
75 _target = q + _offset + _span
76 if _target >= n_qubits and not modulo:
77 continue
78 _control = q + _offset
79 if _control < 0 and not modulo:
80 continue
82 _target = _target % n_qubits
83 _control = _control % n_qubits
85 if _target == _control:
86 log.warning("Skipping gate where control == target")
87 continue
89 targets += [_target]
90 ctrls += [_control]
92 if reverse:
93 ctrls = reversed(ctrls)
94 targets = reversed(targets)
96 if mirror:
97 ctrls, targets = targets, ctrls
99 pairs = list(zip(ctrls, targets, strict=True))
101 return pairs
103 @classmethod
104 def bricks(cls, n_qubits: int, **kwargs) -> List[List[int]]:
105 kwargs.setdefault("stride", 2)
106 kwargs.setdefault("modulo", False)
107 return cls.stairs(n_qubits=n_qubits, **kwargs)
109 @classmethod
110 def graph(
111 cls, n_qubits: int, *, edges: Sequence[Sequence[int]]
112 ) -> List[Tuple[int, int]]:
113 """
114 Explicit edge list as a topology.
116 The given order and orientation are preserved, so the resulting
117 circuit is deterministic and directed gates act on the wires as
118 written. Both orientations of the same qubit pair are therefore
119 allowed; only a repeated ``(control, target)`` pair is rejected.
121 Parameters
122 ----------
123 n_qubits : int
124 Number of qubits.
125 edges : Sequence[Sequence[int]]
126 ``(control, target)`` qubit pairs.
128 Returns
129 -------
130 List[Tuple[int, int]]
132 Raises
133 ------
134 ValueError
135 If an edge leaves the qubit range, is a self-loop or repeats.
136 """
137 seen = set()
138 pairs = []
139 for q, r in edges:
140 if not (0 <= q < n_qubits and 0 <= r < n_qubits) or q == r:
141 raise ValueError(f"edge ({q}, {r}) invalid on {n_qubits} qubits")
142 if (q, r) in seen:
143 raise ValueError(f"duplicate edge ({q}, {r})")
144 seen.add((q, r))
145 pairs.append((q, r))
147 return pairs
149 @classmethod
150 def all_pairs(cls, n_qubits: int) -> List[List[int]]:
151 """Every unordered pair ``[j, k]`` with ``j < k``."""
152 return [[j, k] for j, k in combinations(range(n_qubits), 2)]
154 @classmethod
155 def all_to_all(cls, n_qubits: int) -> List[List[int]]:
156 """Every ordered pair ``(i, j)`` with ``i ≠ j``."""
157 pairs: List[List[int]] = []
158 for ql in range(n_qubits):
159 for q in range(n_qubits):
160 if q != ql:
161 pairs.append(
162 [
163 n_qubits - ql - 1,
164 (n_qubits - q - 1) % n_qubits,
165 ]
166 )
167 return pairs