Coverage for qml_essentials / topologies.py: 98%

45 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-30 11:43 +0000

1from typing import List, Callable, Union 

2import logging 

3 

4log = logging.getLogger(__name__) 

5 

6 

7class Topology: 

8 """ 

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

10 

11 All public methods are static and share a small set of private 

12 helpers so that related topologies (e.g. ``linear`` / ``circular``, 

13 ``brick_layer`` / ``brick_layer_wrap``) re-use the same core logic. 

14 

15 Raises 

16 ------ 

17 ValueError 

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

19 """ 

20 

21 @classmethod 

22 def stairs( 

23 cls, 

24 n_qubits: int, 

25 offset: Union[int, Callable] = 0, 

26 wrap=False, 

27 reverse: bool = True, 

28 mirror: bool = True, 

29 span: Union[int, Callable] = 1, 

30 stride: int = 1, 

31 modulo: bool = True, 

32 ) -> List[List[int]]: 

33 """ 

34 Unified generator for nearest-neighbour and spand pair topologies. 

35 Produces ``[control, target]`` pairs of qubits. 

36 

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

38 without wrapping around the last gate. 

39 

40 Parameters 

41 ---------- 

42 n_qubits : int 

43 Number of qubits. 

44 offset : Union[int, Callable] 

45 Offset for starting the entangling sequence. 

46 Can either be a integer or a callable that takes n_qubits as input. 

47 wrap : bool 

48 Wraps around the entangling gates. 

49 reverse : bool 

50 Reverses both the iteration direction (upstairs/ downstairs) 

51 mirror: bool 

52 Flip target/ control qubit 

53 span : int 

54 Offset between control and target qubit. Defaults to 1 

55 stride : int 

56 Step size for entangling gates. Defaults to 1, meaning a stair 

57 pattern will be generated. 

58 modulo : bool 

59 If a gate should be placed when the iterator decreases below 0 

60 or exceeds n_qubits. Defaults to True 

61 

62 Returns 

63 ------- 

64 List[List[int]] 

65 """ 

66 ctrls = [] 

67 targets = [] 

68 

69 n_gates = n_qubits if wrap else n_qubits - 1 

70 _offset = offset(n_qubits) if callable(offset) else offset 

71 _span = span(n_qubits) if callable(span) else span 

72 

73 for q in range(0, n_gates, stride): 

74 _target = q + _offset + _span 

75 if _target >= n_qubits and not modulo: 

76 continue 

77 _control = q + _offset 

78 if _control < 0 and not modulo: 

79 continue 

80 

81 _target = _target % n_qubits 

82 _control = _control % n_qubits 

83 

84 if _target == _control: 

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

86 continue 

87 

88 targets += [_target] 

89 ctrls += [_control] 

90 

91 if reverse: 

92 ctrls = reversed(ctrls) 

93 targets = reversed(targets) 

94 

95 if mirror: 

96 ctrls, targets = targets, ctrls 

97 

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

99 

100 return pairs 

101 

102 @classmethod 

103 def bricks(cls, n_qubits: int, **kwargs) -> List[List[int]]: 

104 kwargs.setdefault("stride", 2) 

105 kwargs.setdefault("modulo", False) 

106 return cls.stairs(n_qubits=n_qubits, **kwargs) 

107 

108 @classmethod 

109 def all_to_all(cls, n_qubits: int) -> List[List[int]]: 

110 """Every ordered pair ``(i, j)`` with ``i ≠ j``.""" 

111 pairs: List[List[int]] = [] 

112 for ql in range(n_qubits): 

113 for q in range(n_qubits): 

114 if q != ql: 

115 pairs.append( 

116 [ 

117 n_qubits - ql - 1, 

118 (n_qubits - q - 1) % n_qubits, 

119 ] 

120 ) 

121 return pairs