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

1from typing import List, Callable, Sequence, Tuple, Union 

2from itertools import combinations 

3import logging 

4 

5log = logging.getLogger(__name__) 

6 

7 

8class Topology: 

9 """ 

10 Generates [control, target] wire-pair lists for two-qubit gates. 

11 

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. 

15 

16 Raises 

17 ------ 

18 ValueError 

19 If ``n_qubits < 2`` is passed to any topology method. 

20 """ 

21 

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. 

37 

38 The default values, produce an "upstairs" entangling sequence 

39 without wrapping around the last gate. 

40 

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 

62 

63 Returns 

64 ------- 

65 List[List[int]] 

66 """ 

67 ctrls = [] 

68 targets = [] 

69 

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 

73 

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 

81 

82 _target = _target % n_qubits 

83 _control = _control % n_qubits 

84 

85 if _target == _control: 

86 log.warning("Skipping gate where control == target") 

87 continue 

88 

89 targets += [_target] 

90 ctrls += [_control] 

91 

92 if reverse: 

93 ctrls = reversed(ctrls) 

94 targets = reversed(targets) 

95 

96 if mirror: 

97 ctrls, targets = targets, ctrls 

98 

99 pairs = list(zip(ctrls, targets, strict=True)) 

100 

101 return pairs 

102 

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) 

108 

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. 

115 

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. 

120 

121 Parameters 

122 ---------- 

123 n_qubits : int 

124 Number of qubits. 

125 edges : Sequence[Sequence[int]] 

126 ``(control, target)`` qubit pairs. 

127 

128 Returns 

129 ------- 

130 List[Tuple[int, int]] 

131 

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)) 

146 

147 return pairs 

148 

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)] 

153 

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