qomputing-simulator 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,49 @@
1
+ """NumPy-based state vector simulator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Dict, List, Optional
6
+
7
+ import numpy as np
8
+
9
+ from .. import linalg
10
+ from ..circuit import QuantumCircuit
11
+ from . import measurements, registry
12
+ from .result import SimulationResult
13
+
14
+
15
+ class StateVectorSimulator:
16
+ """State vector simulator delegating gate logic to handler modules."""
17
+
18
+ def __init__(self, *, dtype: np.dtype = np.complex128) -> None:
19
+ self.dtype = dtype
20
+
21
+ def run(
22
+ self,
23
+ circuit: QuantumCircuit,
24
+ *,
25
+ shots: int | None = None,
26
+ seed: int | None = None,
27
+ ) -> SimulationResult:
28
+ state = linalg.initial_state(circuit.num_qubits, dtype=self.dtype)
29
+ for gate in circuit.gates:
30
+ state = registry.apply_gate(state, gate, circuit.num_qubits, self.dtype)
31
+
32
+ probabilities = np.abs(state) ** 2
33
+ result_shots = shots if shots and shots > 0 else None
34
+ samples: Optional[List[str]] = None
35
+ counts: Optional[Dict[str, int]] = None
36
+ if result_shots:
37
+ rng = np.random.default_rng(seed)
38
+ samples = measurements.sample_measurements(probabilities, circuit.num_qubits, result_shots, rng)
39
+ counts = measurements.counts_from_samples(samples)
40
+
41
+ return SimulationResult(
42
+ circuit=circuit,
43
+ final_state=state,
44
+ probabilities=probabilities,
45
+ shots=result_shots,
46
+ samples=samples,
47
+ counts=counts,
48
+ )
49
+
@@ -0,0 +1,2 @@
1
+
2
+
@@ -0,0 +1,126 @@
1
+ """Sample circuits that can be executed with the state vector simulator.
2
+
3
+ Examples included:
4
+ * Bell state (2 qubits)
5
+ * Deutsch-Jozsa (2 qubits, constant or balanced oracle)
6
+ * GHZ / Greenberger–Horne–Zeilinger (3 qubits)
7
+
8
+ Run with:
9
+
10
+ python -m qomputing_simulator.examples.demo_circuits --example bell
11
+ python -m qomputing_simulator.examples.demo_circuits --example deutsch-jozsa --oracle balanced --shots 1024
12
+ python -m qomputing_simulator.examples.demo_circuits --example ghz --shots 1000 --seed 42
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ from typing import Callable, Dict
19
+
20
+ import numpy as np
21
+
22
+ from ..circuit import QuantumCircuit
23
+ from ..simulator import StateVectorSimulator
24
+
25
+
26
+ def build_bell_circuit() -> QuantumCircuit:
27
+ """Prepare the Bell state (|00> + |11>)/sqrt(2)."""
28
+ circuit = QuantumCircuit(2)
29
+ circuit.h(0).cx(0, 1)
30
+ return circuit
31
+
32
+
33
+ def build_deutsch_jozsa_circuit(oracle: str) -> QuantumCircuit:
34
+ """Construct a two-qubit Deutsch–Jozsa circuit for the requested oracle."""
35
+ oracle = oracle.lower()
36
+ if oracle not in {"constant", "balanced"}:
37
+ raise ValueError("Deutsch–Jozsa oracle must be 'constant' or 'balanced'")
38
+
39
+ circuit = QuantumCircuit(2)
40
+ # Initialise |0>|1>
41
+ circuit.x(1)
42
+ # Create |+>|-> before the oracle.
43
+ circuit.h(0).h(1)
44
+
45
+ if oracle == "balanced":
46
+ # Oracle implements f(x) = x => CNOT
47
+ circuit.cx(0, 1)
48
+ else:
49
+ # Oracle implements f(x) = 0 => identity (no additional gates)
50
+ pass
51
+
52
+ # Interfere the first qubit to reveal the oracle type.
53
+ circuit.h(0)
54
+ return circuit
55
+
56
+
57
+ def build_ghz_circuit() -> QuantumCircuit:
58
+ """Prepare a three-qubit GHZ state (|000> + |111>)/sqrt(2)."""
59
+ circuit = QuantumCircuit(3)
60
+ circuit.h(0).cx(0, 1).cx(1, 2)
61
+ return circuit
62
+
63
+
64
+ def run_example(factory: Callable[[], QuantumCircuit], shots: int, seed: int | None) -> None:
65
+ circuit = factory()
66
+ simulator = StateVectorSimulator()
67
+ result = simulator.run(circuit, shots=shots if shots > 0 else None, seed=seed)
68
+
69
+ labels = [format(index, f"0{circuit.num_qubits}b") for index in range(2**circuit.num_qubits)]
70
+
71
+ print(f"Number of qubits: {circuit.num_qubits}")
72
+ print("Final state amplitudes:")
73
+ print(np.array2string(result.final_state, precision=6, suppress_small=True))
74
+
75
+ print("Measurement probabilities per computational basis state:")
76
+ for label, probability in zip(labels, result.probabilities):
77
+ if probability > 1e-6:
78
+ print(f" P(|{label}⟩) = {probability:.6f}")
79
+
80
+ if result.counts:
81
+ print(f"\nSampled {result.shots} shots (seed={seed}):")
82
+ for bitstring, count in sorted(result.counts.items()):
83
+ print(f" {bitstring}: {count} times")
84
+
85
+
86
+ def parse_args() -> argparse.Namespace:
87
+ parser = argparse.ArgumentParser(description="Run example circuits on the state vector simulator.")
88
+ parser.add_argument(
89
+ "--example",
90
+ type=str,
91
+ required=True,
92
+ choices=["bell", "deutsch-jozsa", "ghz"],
93
+ help="Which example circuit to run.",
94
+ )
95
+ parser.add_argument(
96
+ "--oracle",
97
+ type=str,
98
+ default="constant",
99
+ help="Oracle type for the Deutsch–Jozsa example (constant or balanced).",
100
+ )
101
+ parser.add_argument("--shots", type=int, default=0, help="Number of measurement shots to sample.")
102
+ parser.add_argument("--seed", type=int, default=None, help="Random seed for sampling.")
103
+ return parser.parse_args()
104
+
105
+
106
+ def main() -> int:
107
+ args = parse_args()
108
+
109
+ example_builders: Dict[str, Callable[[], QuantumCircuit]] = {
110
+ "bell": build_bell_circuit,
111
+ "ghz": build_ghz_circuit,
112
+ }
113
+
114
+ if args.example == "deutsch-jozsa":
115
+ factory = lambda: build_deutsch_jozsa_circuit(args.oracle) # noqa: E731
116
+ else:
117
+ factory = example_builders[args.example]
118
+
119
+ run_example(factory, shots=args.shots, seed=args.seed)
120
+ return 0
121
+
122
+
123
+ if __name__ == "__main__": # pragma: no cover
124
+ raise SystemExit(main())
125
+
126
+
@@ -0,0 +1,27 @@
1
+ """Gate registries and default gate sets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .single_qubit import GATE_NAMES as SINGLE_QUBIT_NAMES
6
+ from .single_qubit import HANDLERS as SINGLE_QUBIT_HANDLERS
7
+ from .two_qubit import GATE_NAMES as TWO_QUBIT_NAMES
8
+ from .two_qubit import HANDLERS as TWO_QUBIT_HANDLERS
9
+ from .multi_qubit import GATE_NAMES as MULTI_QUBIT_NAMES
10
+ from .multi_qubit import HANDLERS as MULTI_QUBIT_HANDLERS
11
+
12
+ DEFAULT_SINGLE_QUBIT_GATES = ["h", "rx", "ry", "rz", "s", "t"]
13
+ DEFAULT_TWO_QUBIT_GATES = ["cx", "cz", "swap"]
14
+ DEFAULT_MULTI_QUBIT_GATES = []
15
+
16
+ __all__ = [
17
+ "SINGLE_QUBIT_HANDLERS",
18
+ "TWO_QUBIT_HANDLERS",
19
+ "MULTI_QUBIT_HANDLERS",
20
+ "DEFAULT_SINGLE_QUBIT_GATES",
21
+ "DEFAULT_TWO_QUBIT_GATES",
22
+ "DEFAULT_MULTI_QUBIT_GATES",
23
+ "SINGLE_QUBIT_NAMES",
24
+ "TWO_QUBIT_NAMES",
25
+ "MULTI_QUBIT_NAMES",
26
+ ]
27
+
@@ -0,0 +1,60 @@
1
+ """Multi-qubit (three or more qubits) gate implementations and registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Callable, Dict, List
6
+
7
+ import numpy as np
8
+
9
+ from .. import linalg
10
+ from ..circuit import Gate
11
+
12
+ MultiQubitHandler = Callable[[np.ndarray, Gate, int, np.dtype], np.ndarray]
13
+
14
+
15
+ def apply_ccx(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
16
+ if len(gate.controls) != 2 or len(gate.targets) != 1:
17
+ raise ValueError("ccx gate requires two controls and one target")
18
+ controls = list(gate.controls)
19
+ target = gate.targets[0]
20
+ matrix = np.eye(8, dtype=dtype)
21
+ matrix[6, 6] = 0
22
+ matrix[7, 7] = 0
23
+ matrix[6, 7] = 1
24
+ matrix[7, 6] = 1
25
+ return linalg.apply_unitary(state, [*controls, target], matrix, num_qubits)
26
+
27
+
28
+ def apply_ccz(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
29
+ if len(gate.controls) != 2 or len(gate.targets) != 1:
30
+ raise ValueError("ccz gate requires two controls and one target")
31
+ controls = list(gate.controls)
32
+ target = gate.targets[0]
33
+ matrix = np.eye(8, dtype=dtype)
34
+ matrix[7, 7] = -1
35
+ return linalg.apply_unitary(state, [*controls, target], matrix, num_qubits)
36
+
37
+
38
+ def apply_cswap(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
39
+ if len(gate.controls) != 1 or len(gate.targets) != 2:
40
+ raise ValueError("cswap gate requires one control and two targets")
41
+ control = gate.controls[0]
42
+ q1, q2 = gate.targets
43
+ matrix = np.eye(8, dtype=dtype)
44
+ matrix[5, 5] = 0
45
+ matrix[6, 6] = 0
46
+ matrix[5, 6] = 1
47
+ matrix[6, 5] = 1
48
+ return linalg.apply_unitary(state, [control, q1, q2], matrix, num_qubits)
49
+
50
+
51
+ HANDLERS: Dict[str, MultiQubitHandler] = {
52
+ "ccx": apply_ccx,
53
+ "ccz": apply_ccz,
54
+ "cswap": apply_cswap,
55
+ }
56
+
57
+ GATE_NAMES: List[str] = list(HANDLERS.keys())
58
+
59
+ __all__ = ["HANDLERS", "GATE_NAMES", "MultiQubitHandler"]
60
+
@@ -0,0 +1,204 @@
1
+ """Single-qubit gate implementations and registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import cmath
6
+ import math
7
+ from typing import Callable, Dict, List
8
+
9
+ import numpy as np
10
+
11
+ from .. import linalg
12
+ from ..circuit import Gate
13
+
14
+ SingleQubitHandler = Callable[[np.ndarray, Gate, int, np.dtype], np.ndarray]
15
+
16
+
17
+ def _apply_matrix(state: np.ndarray, gate: Gate, num_qubits: int, matrix: np.ndarray) -> np.ndarray:
18
+ if len(gate.targets) != 1:
19
+ raise ValueError(f"{gate.name} gate expects one target qubit")
20
+ target = gate.targets[0]
21
+ tensor = linalg.reshape_state(state, num_qubits)
22
+ axis = linalg.axis_for_qubit(target)
23
+ updated = np.tensordot(matrix, tensor, axes=([1], [axis]))
24
+ updated = np.moveaxis(updated, 0, axis)
25
+ return updated.reshape(state.shape)
26
+
27
+
28
+ def apply_identity(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
29
+ return _apply_matrix(state, gate, num_qubits, np.eye(2, dtype=dtype))
30
+
31
+
32
+ def apply_x(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
33
+ return _apply_matrix(state, gate, num_qubits, np.array([[0, 1], [1, 0]], dtype=dtype))
34
+
35
+
36
+ def apply_y(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
37
+ return _apply_matrix(state, gate, num_qubits, np.array([[0, -1j], [1j, 0]], dtype=dtype))
38
+
39
+
40
+ def apply_z(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
41
+ return _apply_matrix(state, gate, num_qubits, np.array([[1, 0], [0, -1]], dtype=dtype))
42
+
43
+
44
+ def apply_h(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
45
+ return _apply_matrix(
46
+ state,
47
+ gate,
48
+ num_qubits,
49
+ (1 / math.sqrt(2)) * np.array([[1, 1], [1, -1]], dtype=dtype),
50
+ )
51
+
52
+
53
+ def apply_s(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
54
+ return _apply_matrix(state, gate, num_qubits, np.array([[1, 0], [0, 1j]], dtype=dtype))
55
+
56
+
57
+ def apply_sdg(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
58
+ return _apply_matrix(state, gate, num_qubits, np.array([[1, 0], [0, -1j]], dtype=dtype))
59
+
60
+
61
+ def apply_t(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
62
+ return _apply_matrix(
63
+ state,
64
+ gate,
65
+ num_qubits,
66
+ np.array([[1, 0], [0, cmath.exp(1j * math.pi / 4)]], dtype=dtype),
67
+ )
68
+
69
+
70
+ def apply_tdg(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
71
+ return _apply_matrix(
72
+ state,
73
+ gate,
74
+ num_qubits,
75
+ np.array([[1, 0], [0, cmath.exp(-1j * math.pi / 4)]], dtype=dtype),
76
+ )
77
+
78
+
79
+ def apply_sx(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
80
+ matrix = 0.5 * np.array(
81
+ [
82
+ [1 + 1j, 1 - 1j],
83
+ [1 - 1j, 1 + 1j],
84
+ ],
85
+ dtype=dtype,
86
+ )
87
+ return _apply_matrix(state, gate, num_qubits, matrix)
88
+
89
+
90
+ def apply_sxdg(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
91
+ matrix = 0.5 * np.array(
92
+ [
93
+ [1 - 1j, 1 + 1j],
94
+ [1 + 1j, 1 - 1j],
95
+ ],
96
+ dtype=dtype,
97
+ )
98
+ return _apply_matrix(state, gate, num_qubits, matrix)
99
+
100
+
101
+ def apply_rx(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
102
+ theta = float(gate.params.get("theta", 0.0))
103
+ half = theta / 2.0
104
+ matrix = np.array(
105
+ [
106
+ [math.cos(half), -1j * math.sin(half)],
107
+ [-1j * math.sin(half), math.cos(half)],
108
+ ],
109
+ dtype=dtype,
110
+ )
111
+ return _apply_matrix(state, gate, num_qubits, matrix)
112
+
113
+
114
+ def apply_ry(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
115
+ theta = float(gate.params.get("theta", 0.0))
116
+ half = theta / 2.0
117
+ matrix = np.array(
118
+ [
119
+ [math.cos(half), -math.sin(half)],
120
+ [math.sin(half), math.cos(half)],
121
+ ],
122
+ dtype=dtype,
123
+ )
124
+ return _apply_matrix(state, gate, num_qubits, matrix)
125
+
126
+
127
+ def apply_rz(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
128
+ theta = float(gate.params.get("theta", 0.0))
129
+ half = theta / 2.0
130
+ matrix = np.array(
131
+ [
132
+ [cmath.exp(-1j * half), 0],
133
+ [0, cmath.exp(1j * half)],
134
+ ],
135
+ dtype=dtype,
136
+ )
137
+ return _apply_matrix(state, gate, num_qubits, matrix)
138
+
139
+
140
+ def apply_u1(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
141
+ lam = float(gate.params.get("lambda", 0.0))
142
+ matrix = np.array(
143
+ [
144
+ [1.0, 0.0],
145
+ [0.0, cmath.exp(1j * lam)],
146
+ ],
147
+ dtype=dtype,
148
+ )
149
+ return _apply_matrix(state, gate, num_qubits, matrix)
150
+
151
+
152
+ def apply_u2(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
153
+ phi = float(gate.params.get("phi", 0.0))
154
+ lam = float(gate.params.get("lambda", 0.0))
155
+ matrix = (1 / math.sqrt(2)) * np.array(
156
+ [
157
+ [1.0, -cmath.exp(1j * lam)],
158
+ [cmath.exp(1j * phi), cmath.exp(1j * (phi + lam))],
159
+ ],
160
+ dtype=dtype,
161
+ )
162
+ return _apply_matrix(state, gate, num_qubits, matrix)
163
+
164
+
165
+ def apply_u3(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
166
+ theta = float(gate.params.get("theta", 0.0))
167
+ phi = float(gate.params.get("phi", 0.0))
168
+ lam = float(gate.params.get("lambda", 0.0))
169
+ cos = math.cos(theta / 2.0)
170
+ sin = math.sin(theta / 2.0)
171
+ matrix = np.array(
172
+ [
173
+ [cos, -cmath.exp(1j * lam) * sin],
174
+ [cmath.exp(1j * phi) * sin, cmath.exp(1j * (phi + lam)) * cos],
175
+ ],
176
+ dtype=dtype,
177
+ )
178
+ return _apply_matrix(state, gate, num_qubits, matrix)
179
+
180
+
181
+ HANDLERS: Dict[str, SingleQubitHandler] = {
182
+ "id": apply_identity,
183
+ "x": apply_x,
184
+ "y": apply_y,
185
+ "z": apply_z,
186
+ "h": apply_h,
187
+ "s": apply_s,
188
+ "sdg": apply_sdg,
189
+ "t": apply_t,
190
+ "tdg": apply_tdg,
191
+ "sx": apply_sx,
192
+ "sxdg": apply_sxdg,
193
+ "rx": apply_rx,
194
+ "ry": apply_ry,
195
+ "rz": apply_rz,
196
+ "u1": apply_u1,
197
+ "u2": apply_u2,
198
+ "u3": apply_u3,
199
+ }
200
+
201
+ GATE_NAMES: List[str] = list(HANDLERS.keys())
202
+
203
+ __all__ = ["HANDLERS", "GATE_NAMES", "SingleQubitHandler"]
204
+
@@ -0,0 +1,196 @@
1
+ """Two-qubit gate implementations and registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import cmath
6
+ import math
7
+ from typing import Callable, Dict, List
8
+
9
+ import numpy as np
10
+
11
+ from .. import linalg
12
+ from ..circuit import Gate
13
+
14
+ TwoQubitHandler = Callable[[np.ndarray, Gate, int, np.dtype], np.ndarray]
15
+
16
+
17
+ def _apply_unitary(state: np.ndarray, qubits: List[int], matrix: np.ndarray, num_qubits: int) -> np.ndarray:
18
+ return linalg.apply_unitary(state, qubits, matrix, num_qubits)
19
+
20
+
21
+ def apply_cnot(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
22
+ if len(gate.controls) != 1 or len(gate.targets) != 1:
23
+ raise ValueError("cx gate requires one control and one target")
24
+ control = gate.controls[0]
25
+ target = gate.targets[0]
26
+ matrix = np.array(
27
+ [
28
+ [1, 0, 0, 0],
29
+ [0, 1, 0, 0],
30
+ [0, 0, 0, 1],
31
+ [0, 0, 1, 0],
32
+ ],
33
+ dtype=dtype,
34
+ )
35
+ return _apply_unitary(state, [control, target], matrix, num_qubits)
36
+
37
+
38
+ def apply_cy(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
39
+ if len(gate.controls) != 1 or len(gate.targets) != 1:
40
+ raise ValueError("cy gate requires one control and one target")
41
+ control = gate.controls[0]
42
+ target = gate.targets[0]
43
+ matrix = np.array(
44
+ [
45
+ [1, 0, 0, 0],
46
+ [0, 1, 0, 0],
47
+ [0, 0, 0, -1j],
48
+ [0, 0, 1j, 0],
49
+ ],
50
+ dtype=dtype,
51
+ )
52
+ return _apply_unitary(state, [control, target], matrix, num_qubits)
53
+
54
+
55
+ def apply_cz(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
56
+ if len(gate.controls) != 1 or len(gate.targets) != 1:
57
+ raise ValueError("cz gate requires one control and one target")
58
+ control = gate.controls[0]
59
+ target = gate.targets[0]
60
+ matrix = np.diag([1, 1, 1, -1]).astype(dtype)
61
+ return _apply_unitary(state, [control, target], matrix, num_qubits)
62
+
63
+
64
+ def apply_cp(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
65
+ if len(gate.controls) != 1 or len(gate.targets) != 1:
66
+ raise ValueError("cp gate requires one control and one target")
67
+ phi = float(gate.params.get("phi", 0.0))
68
+ matrix = np.diag([1, 1, 1, cmath.exp(1j * phi)]).astype(dtype)
69
+ return _apply_unitary(state, [gate.controls[0], gate.targets[0]], matrix, num_qubits)
70
+
71
+
72
+ def apply_swap(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
73
+ if len(gate.targets) != 2:
74
+ raise ValueError("swap gate requires two target qubits")
75
+ q1, q2 = gate.targets
76
+ matrix = np.array(
77
+ [
78
+ [1, 0, 0, 0],
79
+ [0, 0, 1, 0],
80
+ [0, 1, 0, 0],
81
+ [0, 0, 0, 1],
82
+ ],
83
+ dtype=dtype,
84
+ )
85
+ return _apply_unitary(state, [q1, q2], matrix, num_qubits)
86
+
87
+
88
+ def apply_iswap(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
89
+ if len(gate.targets) != 2:
90
+ raise ValueError("iswap gate requires two target qubits")
91
+ q1, q2 = gate.targets
92
+ matrix = np.array(
93
+ [
94
+ [1, 0, 0, 0],
95
+ [0, 0, 1j, 0],
96
+ [0, 1j, 0, 0],
97
+ [0, 0, 0, 1],
98
+ ],
99
+ dtype=dtype,
100
+ )
101
+ return _apply_unitary(state, [q1, q2], matrix, num_qubits)
102
+
103
+
104
+ def apply_sqrtiswap(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
105
+ if len(gate.targets) != 2:
106
+ raise ValueError("sqrtiswap gate requires two target qubits")
107
+ q1, q2 = gate.targets
108
+ matrix = np.array(
109
+ [
110
+ [1.0, 0.0, 0.0, 0.0],
111
+ [0.0, 0.5 + 0.5j, 0.5 - 0.5j, 0.0],
112
+ [0.0, 0.5 - 0.5j, 0.5 + 0.5j, 0.0],
113
+ [0.0, 0.0, 0.0, 1.0],
114
+ ],
115
+ dtype=dtype,
116
+ )
117
+ return _apply_unitary(state, [q1, q2], matrix, num_qubits)
118
+
119
+
120
+ def _rotation_gate(pauli: np.ndarray, theta: float, dtype: np.dtype) -> np.ndarray:
121
+ half = theta / 2.0
122
+ return math.cos(half) * np.eye(4, dtype=dtype) - 1j * math.sin(half) * pauli
123
+
124
+
125
+ def apply_rxx(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
126
+ if len(gate.targets) != 2:
127
+ raise ValueError("rxx gate requires two target qubits")
128
+ theta = float(gate.params.get("theta", 0.0))
129
+ pauli = np.kron(np.array([[0, 1], [1, 0]], dtype=dtype), np.array([[0, 1], [1, 0]], dtype=dtype))
130
+ matrix = _rotation_gate(pauli, theta, dtype)
131
+ return _apply_unitary(state, gate.targets, matrix, num_qubits)
132
+
133
+
134
+ def apply_ryy(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
135
+ if len(gate.targets) != 2:
136
+ raise ValueError("ryy gate requires two target qubits")
137
+ theta = float(gate.params.get("theta", 0.0))
138
+ pauli = np.kron(
139
+ np.array([[0, -1j], [1j, 0]], dtype=dtype),
140
+ np.array([[0, -1j], [1j, 0]], dtype=dtype),
141
+ )
142
+ matrix = _rotation_gate(pauli, theta, dtype)
143
+ return _apply_unitary(state, gate.targets, matrix, num_qubits)
144
+
145
+
146
+ def apply_rzz(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
147
+ if len(gate.targets) != 2:
148
+ raise ValueError("rzz gate requires two target qubits")
149
+ theta = float(gate.params.get("theta", 0.0))
150
+ pauli = np.kron(
151
+ np.array([[1, 0], [0, -1]], dtype=dtype),
152
+ np.array([[1, 0], [0, -1]], dtype=dtype),
153
+ )
154
+ matrix = _rotation_gate(pauli, theta, dtype)
155
+ return _apply_unitary(state, gate.targets, matrix, num_qubits)
156
+
157
+
158
+ def apply_csx(state: np.ndarray, gate: Gate, num_qubits: int, dtype: np.dtype) -> np.ndarray:
159
+ if len(gate.controls) != 1 or len(gate.targets) != 1:
160
+ raise ValueError("csx gate requires one control and one target")
161
+ control = gate.controls[0]
162
+ target = gate.targets[0]
163
+ sx = 0.5 * np.array(
164
+ [
165
+ [1 + 1j, 1 - 1j],
166
+ [1 - 1j, 1 + 1j],
167
+ ],
168
+ dtype=dtype,
169
+ )
170
+ matrix = np.block(
171
+ [
172
+ [np.eye(2, dtype=dtype), np.zeros((2, 2), dtype=dtype)],
173
+ [np.zeros((2, 2), dtype=dtype), sx],
174
+ ]
175
+ )
176
+ return _apply_unitary(state, [control, target], matrix, num_qubits)
177
+
178
+
179
+ HANDLERS: Dict[str, TwoQubitHandler] = {
180
+ "cx": apply_cnot,
181
+ "cy": apply_cy,
182
+ "cz": apply_cz,
183
+ "cp": apply_cp,
184
+ "swap": apply_swap,
185
+ "iswap": apply_iswap,
186
+ "sqrtiswap": apply_sqrtiswap,
187
+ "rxx": apply_rxx,
188
+ "ryy": apply_ryy,
189
+ "rzz": apply_rzz,
190
+ "csx": apply_csx,
191
+ }
192
+
193
+ GATE_NAMES: List[str] = list(HANDLERS.keys())
194
+
195
+ __all__ = ["HANDLERS", "GATE_NAMES", "TwoQubitHandler"]
196
+