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.
- examples/library_usage.py +30 -0
- qomputing_simulator/__init__.py +22 -0
- qomputing_simulator/circuit.py +231 -0
- qomputing_simulator/circuit_builders.py +185 -0
- qomputing_simulator/cli.py +121 -0
- qomputing_simulator/engine/__init__.py +7 -0
- qomputing_simulator/engine/measurements.py +26 -0
- qomputing_simulator/engine/registry.py +32 -0
- qomputing_simulator/engine/result.py +31 -0
- qomputing_simulator/engine/statevector.py +49 -0
- qomputing_simulator/examples/__init__.py +2 -0
- qomputing_simulator/examples/demo_circuits.py +126 -0
- qomputing_simulator/gates/__init__.py +27 -0
- qomputing_simulator/gates/multi_qubit.py +60 -0
- qomputing_simulator/gates/single_qubit.py +204 -0
- qomputing_simulator/gates/two_qubit.py +196 -0
- qomputing_simulator/linalg.py +62 -0
- qomputing_simulator/run.py +85 -0
- qomputing_simulator/simulator.py +6 -0
- qomputing_simulator/xeb.py +77 -0
- qomputing_simulator-0.1.0.dist-info/METADATA +314 -0
- qomputing_simulator-0.1.0.dist-info/RECORD +27 -0
- qomputing_simulator-0.1.0.dist-info/WHEEL +5 -0
- qomputing_simulator-0.1.0.dist-info/entry_points.txt +2 -0
- qomputing_simulator-0.1.0.dist-info/top_level.txt +4 -0
- tests/test_parity.py +125 -0
- tools/cirq_comparison.py +419 -0
|
@@ -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,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
|
+
|