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,30 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Example: using the state-vector simulator as a library."""
|
|
3
|
+
|
|
4
|
+
from qomputing_simulator import (
|
|
5
|
+
QuantumCircuit,
|
|
6
|
+
load_circuit,
|
|
7
|
+
random_circuit,
|
|
8
|
+
run,
|
|
9
|
+
run_xeb,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
# 1) Build a circuit in code and run it (no shots = state vector only)
|
|
13
|
+
circuit = QuantumCircuit(2)
|
|
14
|
+
circuit.h(0).cx(0, 1)
|
|
15
|
+
result = run(circuit)
|
|
16
|
+
print("Bell circuit final state (real):", result.final_state.real.round(4).tolist())
|
|
17
|
+
print("Probabilities:", result.probabilities.round(4).tolist())
|
|
18
|
+
|
|
19
|
+
# 2) Run with measurement shots
|
|
20
|
+
result = run(circuit, shots=1000, seed=42)
|
|
21
|
+
print("Counts:", result.counts)
|
|
22
|
+
|
|
23
|
+
# 3) Load a circuit from JSON and run
|
|
24
|
+
# result = run("circuits/example.json", shots=512, seed=123)
|
|
25
|
+
|
|
26
|
+
# 4) Random circuit and XEB
|
|
27
|
+
qc = random_circuit(num_qubits=3, depth=5, seed=7)
|
|
28
|
+
xeb = run_xeb(qc, shots=1000, seed=7)
|
|
29
|
+
print("XEB fidelity:", round(xeb.fidelity, 4))
|
|
30
|
+
print("Sample probabilities:", xeb.sample_probabilities)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""State Vector Simulator package."""
|
|
2
|
+
|
|
3
|
+
from .circuit import Gate, QuantumCircuit
|
|
4
|
+
from .engine import SimulationResult, StateVectorSimulator
|
|
5
|
+
from .run import load_circuit, random_circuit, run, run_xeb
|
|
6
|
+
from .xeb import LinearXEBResult, compute_linear_xeb_fidelity, run_linear_xeb_experiment
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Gate",
|
|
10
|
+
"QuantumCircuit",
|
|
11
|
+
"StateVectorSimulator",
|
|
12
|
+
"SimulationResult",
|
|
13
|
+
"LinearXEBResult",
|
|
14
|
+
"compute_linear_xeb_fidelity",
|
|
15
|
+
"run_linear_xeb_experiment",
|
|
16
|
+
# Library API
|
|
17
|
+
"load_circuit",
|
|
18
|
+
"run",
|
|
19
|
+
"run_xeb",
|
|
20
|
+
"random_circuit",
|
|
21
|
+
]
|
|
22
|
+
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Circuit primitives for the state vector simulator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Dict, Iterable, List, Mapping, Sequence, Tuple
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _ensure_tuple(values: Sequence[int]) -> Tuple[int, ...]:
|
|
10
|
+
return tuple(int(v) for v in values)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class Gate:
|
|
15
|
+
"""Immutable gate description."""
|
|
16
|
+
|
|
17
|
+
name: str
|
|
18
|
+
targets: Tuple[int, ...]
|
|
19
|
+
controls: Tuple[int, ...] = field(default_factory=tuple)
|
|
20
|
+
params: Mapping[str, float] = field(default_factory=dict)
|
|
21
|
+
|
|
22
|
+
def __post_init__(self) -> None:
|
|
23
|
+
name = self.name.lower()
|
|
24
|
+
object.__setattr__(self, "name", name)
|
|
25
|
+
|
|
26
|
+
if not self.targets:
|
|
27
|
+
raise ValueError("Gate must have at least one target qubit")
|
|
28
|
+
if len(set(self.targets)) != len(self.targets):
|
|
29
|
+
raise ValueError(f"Duplicate target qubits in gate {self.name}")
|
|
30
|
+
if any(t < 0 for t in self.targets):
|
|
31
|
+
raise ValueError("Target qubit indices must be non-negative")
|
|
32
|
+
|
|
33
|
+
if len(set(self.controls)) != len(self.controls):
|
|
34
|
+
raise ValueError(f"Duplicate control qubits in gate {self.name}")
|
|
35
|
+
if any(c < 0 for c in self.controls):
|
|
36
|
+
raise ValueError("Control qubit indices must be non-negative")
|
|
37
|
+
|
|
38
|
+
overlap = set(self.targets) & set(self.controls)
|
|
39
|
+
if overlap:
|
|
40
|
+
raise ValueError(f"Control and target sets overlap for gate {self.name}: {overlap}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class QuantumCircuit:
|
|
44
|
+
"""Simple representation of a quantum circuit."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, num_qubits: int) -> None:
|
|
47
|
+
if num_qubits <= 0:
|
|
48
|
+
raise ValueError("Circuit must have at least one qubit")
|
|
49
|
+
self.num_qubits = int(num_qubits)
|
|
50
|
+
self._gates: List[Gate] = []
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def gates(self) -> Tuple[Gate, ...]:
|
|
54
|
+
return tuple(self._gates)
|
|
55
|
+
|
|
56
|
+
def add_gate(
|
|
57
|
+
self,
|
|
58
|
+
name: str,
|
|
59
|
+
targets: Sequence[int],
|
|
60
|
+
*,
|
|
61
|
+
controls: Sequence[int] | None = None,
|
|
62
|
+
params: Mapping[str, float] | None = None,
|
|
63
|
+
) -> "QuantumCircuit":
|
|
64
|
+
gate = Gate(
|
|
65
|
+
name=name,
|
|
66
|
+
targets=_ensure_tuple(targets),
|
|
67
|
+
controls=_ensure_tuple(controls or ()),
|
|
68
|
+
params=dict(params or {}),
|
|
69
|
+
)
|
|
70
|
+
self._validate_gate_qubits(gate)
|
|
71
|
+
self._gates.append(gate)
|
|
72
|
+
return self
|
|
73
|
+
|
|
74
|
+
# Convenience builders -------------------------------------------------
|
|
75
|
+
def i(self, target: int) -> "QuantumCircuit":
|
|
76
|
+
return self.add_gate("id", [target])
|
|
77
|
+
|
|
78
|
+
def x(self, target: int) -> "QuantumCircuit":
|
|
79
|
+
return self.add_gate("x", [target])
|
|
80
|
+
|
|
81
|
+
def y(self, target: int) -> "QuantumCircuit":
|
|
82
|
+
return self.add_gate("y", [target])
|
|
83
|
+
|
|
84
|
+
def z(self, target: int) -> "QuantumCircuit":
|
|
85
|
+
return self.add_gate("z", [target])
|
|
86
|
+
|
|
87
|
+
def h(self, target: int) -> "QuantumCircuit":
|
|
88
|
+
return self.add_gate("h", [target])
|
|
89
|
+
|
|
90
|
+
def s(self, target: int) -> "QuantumCircuit":
|
|
91
|
+
return self.add_gate("s", [target])
|
|
92
|
+
|
|
93
|
+
def sdg(self, target: int) -> "QuantumCircuit":
|
|
94
|
+
return self.add_gate("sdg", [target])
|
|
95
|
+
|
|
96
|
+
def t(self, target: int) -> "QuantumCircuit":
|
|
97
|
+
return self.add_gate("t", [target])
|
|
98
|
+
|
|
99
|
+
def tdg(self, target: int) -> "QuantumCircuit":
|
|
100
|
+
return self.add_gate("tdg", [target])
|
|
101
|
+
|
|
102
|
+
def sx(self, target: int) -> "QuantumCircuit":
|
|
103
|
+
return self.add_gate("sx", [target])
|
|
104
|
+
|
|
105
|
+
def sxdg(self, target: int) -> "QuantumCircuit":
|
|
106
|
+
return self.add_gate("sxdg", [target])
|
|
107
|
+
|
|
108
|
+
def rx(self, target: int, theta: float) -> "QuantumCircuit":
|
|
109
|
+
return self.add_gate("rx", [target], params={"theta": float(theta)})
|
|
110
|
+
|
|
111
|
+
def ry(self, target: int, theta: float) -> "QuantumCircuit":
|
|
112
|
+
return self.add_gate("ry", [target], params={"theta": float(theta)})
|
|
113
|
+
|
|
114
|
+
def rz(self, target: int, theta: float) -> "QuantumCircuit":
|
|
115
|
+
return self.add_gate("rz", [target], params={"theta": float(theta)})
|
|
116
|
+
|
|
117
|
+
def u1(self, target: int, lam: float) -> "QuantumCircuit":
|
|
118
|
+
return self.add_gate("u1", [target], params={"lambda": float(lam)})
|
|
119
|
+
|
|
120
|
+
def u2(self, target: int, phi: float, lam: float) -> "QuantumCircuit":
|
|
121
|
+
return self.add_gate("u2", [target], params={"phi": float(phi), "lambda": float(lam)})
|
|
122
|
+
|
|
123
|
+
def u3(self, target: int, theta: float, phi: float, lam: float) -> "QuantumCircuit":
|
|
124
|
+
return self.add_gate(
|
|
125
|
+
"u3",
|
|
126
|
+
[target],
|
|
127
|
+
params={"theta": float(theta), "phi": float(phi), "lambda": float(lam)},
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def cx(self, control: int, target: int) -> "QuantumCircuit":
|
|
131
|
+
return self.add_gate("cx", [target], controls=[control])
|
|
132
|
+
|
|
133
|
+
def cy(self, control: int, target: int) -> "QuantumCircuit":
|
|
134
|
+
return self.add_gate("cy", [target], controls=[control])
|
|
135
|
+
|
|
136
|
+
def cz(self, control: int, target: int) -> "QuantumCircuit":
|
|
137
|
+
return self.add_gate("cz", [target], controls=[control])
|
|
138
|
+
|
|
139
|
+
def cp(self, control: int, target: int, phi: float) -> "QuantumCircuit":
|
|
140
|
+
return self.add_gate("cp", [target], controls=[control], params={"phi": float(phi)})
|
|
141
|
+
|
|
142
|
+
def swap(self, q1: int, q2: int) -> "QuantumCircuit":
|
|
143
|
+
if q1 == q2:
|
|
144
|
+
raise ValueError("Swap operands must refer to distinct qubits")
|
|
145
|
+
return self.add_gate("swap", [q1, q2])
|
|
146
|
+
|
|
147
|
+
def iswap(self, q1: int, q2: int) -> "QuantumCircuit":
|
|
148
|
+
if q1 == q2:
|
|
149
|
+
raise ValueError("iswap operands must refer to distinct qubits")
|
|
150
|
+
return self.add_gate("iswap", [q1, q2])
|
|
151
|
+
|
|
152
|
+
def sqrt_iswap(self, q1: int, q2: int) -> "QuantumCircuit":
|
|
153
|
+
if q1 == q2:
|
|
154
|
+
raise ValueError("sqrtiswap operands must refer to distinct qubits")
|
|
155
|
+
return self.add_gate("sqrtiswap", [q1, q2])
|
|
156
|
+
|
|
157
|
+
def rxx(self, q1: int, q2: int, theta: float) -> "QuantumCircuit":
|
|
158
|
+
if q1 == q2:
|
|
159
|
+
raise ValueError("rxx operands must refer to distinct qubits")
|
|
160
|
+
return self.add_gate("rxx", [q1, q2], params={"theta": float(theta)})
|
|
161
|
+
|
|
162
|
+
def ryy(self, q1: int, q2: int, theta: float) -> "QuantumCircuit":
|
|
163
|
+
if q1 == q2:
|
|
164
|
+
raise ValueError("ryy operands must refer to distinct qubits")
|
|
165
|
+
return self.add_gate("ryy", [q1, q2], params={"theta": float(theta)})
|
|
166
|
+
|
|
167
|
+
def rzz(self, q1: int, q2: int, theta: float) -> "QuantumCircuit":
|
|
168
|
+
if q1 == q2:
|
|
169
|
+
raise ValueError("rzz operands must refer to distinct qubits")
|
|
170
|
+
return self.add_gate("rzz", [q1, q2], params={"theta": float(theta)})
|
|
171
|
+
|
|
172
|
+
def csx(self, control: int, target: int) -> "QuantumCircuit":
|
|
173
|
+
return self.add_gate("csx", [target], controls=[control])
|
|
174
|
+
|
|
175
|
+
def ccx(self, control1: int, control2: int, target: int) -> "QuantumCircuit":
|
|
176
|
+
return self.add_gate("ccx", [target], controls=[control1, control2])
|
|
177
|
+
|
|
178
|
+
def ccz(self, control1: int, control2: int, target: int) -> "QuantumCircuit":
|
|
179
|
+
return self.add_gate("ccz", [target], controls=[control1, control2])
|
|
180
|
+
|
|
181
|
+
def cswap(self, control: int, q1: int, q2: int) -> "QuantumCircuit":
|
|
182
|
+
if q1 == q2:
|
|
183
|
+
raise ValueError("cswap target qubits must be distinct")
|
|
184
|
+
return self.add_gate("cswap", [q1, q2], controls=[control])
|
|
185
|
+
|
|
186
|
+
# Serialisation --------------------------------------------------------
|
|
187
|
+
def to_dict(self) -> Dict[str, object]:
|
|
188
|
+
return {
|
|
189
|
+
"num_qubits": self.num_qubits,
|
|
190
|
+
"gates": [
|
|
191
|
+
{
|
|
192
|
+
"name": gate.name,
|
|
193
|
+
"targets": list(gate.targets),
|
|
194
|
+
**(
|
|
195
|
+
{"controls": list(gate.controls)}
|
|
196
|
+
if gate.controls
|
|
197
|
+
else {}
|
|
198
|
+
),
|
|
199
|
+
**({"params": dict(gate.params)} if gate.params else {}),
|
|
200
|
+
}
|
|
201
|
+
for gate in self._gates
|
|
202
|
+
],
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@classmethod
|
|
206
|
+
def from_dict(cls, payload: Mapping[str, object]) -> "QuantumCircuit":
|
|
207
|
+
num_qubits = int(payload["num_qubits"])
|
|
208
|
+
circuit = cls(num_qubits)
|
|
209
|
+
for gate_payload in payload.get("gates", []):
|
|
210
|
+
if not isinstance(gate_payload, Mapping):
|
|
211
|
+
raise TypeError("Gate payload must be a mapping")
|
|
212
|
+
circuit.add_gate(
|
|
213
|
+
str(gate_payload["name"]),
|
|
214
|
+
gate_payload.get("targets", ()),
|
|
215
|
+
controls=gate_payload.get("controls"),
|
|
216
|
+
params=gate_payload.get("params"),
|
|
217
|
+
)
|
|
218
|
+
return circuit
|
|
219
|
+
|
|
220
|
+
# Internal helpers -----------------------------------------------------
|
|
221
|
+
def _validate_gate_qubits(self, gate: Gate) -> None:
|
|
222
|
+
for index in gate.targets + gate.controls:
|
|
223
|
+
if index < 0 or index >= self.num_qubits:
|
|
224
|
+
raise ValueError(
|
|
225
|
+
f"Qubit index {index} for gate {gate.name} "
|
|
226
|
+
f"is out of range for circuit with {self.num_qubits} qubits",
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def __repr__(self) -> str: # pragma: no cover - debug helper
|
|
230
|
+
return f"QuantumCircuit(num_qubits={self.num_qubits}, gates={self._gates!r})"
|
|
231
|
+
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Circuit builders for programmatic use and random-circuit XEB."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import random
|
|
7
|
+
from typing import Dict, Iterable, Sequence, Tuple
|
|
8
|
+
|
|
9
|
+
from .circuit import QuantumCircuit
|
|
10
|
+
from .gates import (
|
|
11
|
+
DEFAULT_MULTI_QUBIT_GATES,
|
|
12
|
+
DEFAULT_SINGLE_QUBIT_GATES,
|
|
13
|
+
DEFAULT_TWO_QUBIT_GATES,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def random_circuit(
|
|
18
|
+
*,
|
|
19
|
+
num_qubits: int,
|
|
20
|
+
depth: int,
|
|
21
|
+
single_qubit_gates: Iterable[str] | None = None,
|
|
22
|
+
two_qubit_gates: Iterable[str] | None = None,
|
|
23
|
+
multi_qubit_gates: Iterable[str] | None = None,
|
|
24
|
+
seed: int | None = None,
|
|
25
|
+
) -> QuantumCircuit:
|
|
26
|
+
"""Build a random circuit with one layer of single-qubit gates and optional two/multi-qubit gates per depth.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
num_qubits: Number of qubits.
|
|
30
|
+
depth: Number of layers (each layer: single-qubit gates on all qubits, then one two-qubit, optionally one multi-qubit).
|
|
31
|
+
single_qubit_gates: Gate names to sample from; default is DEFAULT_SINGLE_QUBIT_GATES.
|
|
32
|
+
two_qubit_gates: Two-qubit gate names; default is DEFAULT_TWO_QUBIT_GATES.
|
|
33
|
+
multi_qubit_gates: Multi-qubit gate names (e.g. ccx, ccz, cswap); default is DEFAULT_MULTI_QUBIT_GATES.
|
|
34
|
+
seed: Random seed for reproducibility.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
A QuantumCircuit ready for simulation or XEB.
|
|
38
|
+
"""
|
|
39
|
+
if num_qubits <= 0:
|
|
40
|
+
raise ValueError("Number of qubits must be positive")
|
|
41
|
+
if depth <= 0:
|
|
42
|
+
raise ValueError("Depth must be positive")
|
|
43
|
+
|
|
44
|
+
single_qubit_gates = list(single_qubit_gates or DEFAULT_SINGLE_QUBIT_GATES)
|
|
45
|
+
if not single_qubit_gates:
|
|
46
|
+
raise ValueError("Single-qubit gate set must not be empty")
|
|
47
|
+
|
|
48
|
+
two_qubit_gates = list(two_qubit_gates or DEFAULT_TWO_QUBIT_GATES)
|
|
49
|
+
multi_qubit_gates = list(multi_qubit_gates or DEFAULT_MULTI_QUBIT_GATES)
|
|
50
|
+
if num_qubits > 1 and not two_qubit_gates:
|
|
51
|
+
raise ValueError("Two-qubit gate set must not be empty when num_qubits > 1")
|
|
52
|
+
|
|
53
|
+
multi_specs: Dict[str, Tuple[int, int]] = {
|
|
54
|
+
"ccx": (2, 1),
|
|
55
|
+
"ccz": (2, 1),
|
|
56
|
+
"cswap": (1, 2),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
rng = random.Random(seed)
|
|
60
|
+
circuit = QuantumCircuit(num_qubits)
|
|
61
|
+
|
|
62
|
+
for _ in range(depth):
|
|
63
|
+
for qubit in range(num_qubits):
|
|
64
|
+
gate_name = rng.choice(single_qubit_gates)
|
|
65
|
+
_apply_single_qubit_gate(circuit, gate_name, qubit, rng)
|
|
66
|
+
|
|
67
|
+
if num_qubits > 1 and two_qubit_gates:
|
|
68
|
+
gate_name = rng.choice(two_qubit_gates)
|
|
69
|
+
q1, q2 = rng.sample(range(num_qubits), 2)
|
|
70
|
+
_apply_two_qubit_gate(circuit, gate_name, q1, q2, rng)
|
|
71
|
+
|
|
72
|
+
if num_qubits > 2 and multi_qubit_gates:
|
|
73
|
+
gate_name = rng.choice(multi_qubit_gates)
|
|
74
|
+
if gate_name not in multi_specs:
|
|
75
|
+
raise ValueError(f"Unsupported multi-qubit gate in random circuit: {gate_name}")
|
|
76
|
+
controls_required, targets_required = multi_specs[gate_name]
|
|
77
|
+
total_required = controls_required + targets_required
|
|
78
|
+
if num_qubits >= total_required:
|
|
79
|
+
selected = rng.sample(range(num_qubits), total_required)
|
|
80
|
+
controls = selected[:controls_required]
|
|
81
|
+
targets = selected[controls_required:]
|
|
82
|
+
_apply_multi_qubit_gate(circuit, gate_name, controls, targets, rng)
|
|
83
|
+
|
|
84
|
+
return circuit
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _apply_single_qubit_gate(
|
|
88
|
+
circuit: QuantumCircuit,
|
|
89
|
+
gate_name: str,
|
|
90
|
+
qubit: int,
|
|
91
|
+
rng: random.Random,
|
|
92
|
+
) -> None:
|
|
93
|
+
two_pi = 2.0 * math.pi
|
|
94
|
+
if gate_name == "id":
|
|
95
|
+
circuit.i(qubit)
|
|
96
|
+
elif gate_name == "x":
|
|
97
|
+
circuit.x(qubit)
|
|
98
|
+
elif gate_name == "y":
|
|
99
|
+
circuit.y(qubit)
|
|
100
|
+
elif gate_name == "z":
|
|
101
|
+
circuit.z(qubit)
|
|
102
|
+
elif gate_name == "h":
|
|
103
|
+
circuit.h(qubit)
|
|
104
|
+
elif gate_name == "s":
|
|
105
|
+
circuit.s(qubit)
|
|
106
|
+
elif gate_name == "sdg":
|
|
107
|
+
circuit.sdg(qubit)
|
|
108
|
+
elif gate_name == "t":
|
|
109
|
+
circuit.t(qubit)
|
|
110
|
+
elif gate_name == "tdg":
|
|
111
|
+
circuit.tdg(qubit)
|
|
112
|
+
elif gate_name == "sx":
|
|
113
|
+
circuit.sx(qubit)
|
|
114
|
+
elif gate_name == "sxdg":
|
|
115
|
+
circuit.sxdg(qubit)
|
|
116
|
+
elif gate_name == "rx":
|
|
117
|
+
circuit.rx(qubit, rng.uniform(0.0, two_pi))
|
|
118
|
+
elif gate_name == "ry":
|
|
119
|
+
circuit.ry(qubit, rng.uniform(0.0, two_pi))
|
|
120
|
+
elif gate_name == "rz":
|
|
121
|
+
circuit.rz(qubit, rng.uniform(0.0, two_pi))
|
|
122
|
+
elif gate_name == "u1":
|
|
123
|
+
circuit.u1(qubit, rng.uniform(0.0, two_pi))
|
|
124
|
+
elif gate_name == "u2":
|
|
125
|
+
circuit.u2(qubit, rng.uniform(0.0, two_pi), rng.uniform(0.0, two_pi))
|
|
126
|
+
elif gate_name == "u3":
|
|
127
|
+
circuit.u3(
|
|
128
|
+
qubit,
|
|
129
|
+
rng.uniform(0.0, two_pi),
|
|
130
|
+
rng.uniform(0.0, two_pi),
|
|
131
|
+
rng.uniform(0.0, two_pi),
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
raise ValueError(f"Unsupported single-qubit gate: {gate_name}")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _apply_two_qubit_gate(
|
|
138
|
+
circuit: QuantumCircuit,
|
|
139
|
+
gate_name: str,
|
|
140
|
+
q1: int,
|
|
141
|
+
q2: int,
|
|
142
|
+
rng: random.Random,
|
|
143
|
+
) -> None:
|
|
144
|
+
two_pi = 2.0 * math.pi
|
|
145
|
+
if gate_name == "cx":
|
|
146
|
+
circuit.cx(q1, q2)
|
|
147
|
+
elif gate_name == "cy":
|
|
148
|
+
circuit.cy(q1, q2)
|
|
149
|
+
elif gate_name == "cz":
|
|
150
|
+
circuit.cz(q1, q2)
|
|
151
|
+
elif gate_name == "cp":
|
|
152
|
+
circuit.cp(q1, q2, rng.uniform(0.0, two_pi))
|
|
153
|
+
elif gate_name == "swap":
|
|
154
|
+
circuit.swap(q1, q2)
|
|
155
|
+
elif gate_name == "iswap":
|
|
156
|
+
circuit.iswap(q1, q2)
|
|
157
|
+
elif gate_name == "sqrtiswap":
|
|
158
|
+
circuit.sqrt_iswap(q1, q2)
|
|
159
|
+
elif gate_name == "rxx":
|
|
160
|
+
circuit.rxx(q1, q2, rng.uniform(0.0, two_pi))
|
|
161
|
+
elif gate_name == "ryy":
|
|
162
|
+
circuit.ryy(q1, q2, rng.uniform(0.0, two_pi))
|
|
163
|
+
elif gate_name == "rzz":
|
|
164
|
+
circuit.rzz(q1, q2, rng.uniform(0.0, two_pi))
|
|
165
|
+
elif gate_name == "csx":
|
|
166
|
+
circuit.csx(q1, q2)
|
|
167
|
+
else:
|
|
168
|
+
raise ValueError(f"Unsupported two-qubit gate in random circuit: {gate_name}")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _apply_multi_qubit_gate(
|
|
172
|
+
circuit: QuantumCircuit,
|
|
173
|
+
gate_name: str,
|
|
174
|
+
controls: Sequence[int],
|
|
175
|
+
targets: Sequence[int],
|
|
176
|
+
rng: random.Random,
|
|
177
|
+
) -> None:
|
|
178
|
+
if gate_name == "ccx":
|
|
179
|
+
circuit.ccx(controls[0], controls[1], targets[0])
|
|
180
|
+
elif gate_name == "ccz":
|
|
181
|
+
circuit.ccz(controls[0], controls[1], targets[0])
|
|
182
|
+
elif gate_name == "cswap":
|
|
183
|
+
circuit.cswap(controls[0], targets[0], targets[1])
|
|
184
|
+
else:
|
|
185
|
+
raise ValueError(f"Unsupported multi-qubit gate in random circuit: {gate_name}")
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Command line interface for the state vector simulator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Sequence
|
|
9
|
+
|
|
10
|
+
from .circuit import QuantumCircuit
|
|
11
|
+
from .circuit_builders import random_circuit
|
|
12
|
+
from .gates import (
|
|
13
|
+
DEFAULT_MULTI_QUBIT_GATES,
|
|
14
|
+
DEFAULT_SINGLE_QUBIT_GATES,
|
|
15
|
+
DEFAULT_TWO_QUBIT_GATES,
|
|
16
|
+
)
|
|
17
|
+
from .simulator import StateVectorSimulator
|
|
18
|
+
from .xeb import run_linear_xeb_experiment
|
|
19
|
+
|
|
20
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
21
|
+
parser = _build_parser()
|
|
22
|
+
args = parser.parse_args(argv)
|
|
23
|
+
|
|
24
|
+
if args.command == "simulate":
|
|
25
|
+
return _cmd_simulate(args)
|
|
26
|
+
if args.command == "random-circuit":
|
|
27
|
+
return _cmd_random_circuit(args)
|
|
28
|
+
|
|
29
|
+
parser.print_help()
|
|
30
|
+
return 0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
34
|
+
parser = argparse.ArgumentParser(description="State vector simulator CLI")
|
|
35
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
36
|
+
|
|
37
|
+
simulate_parser = subparsers.add_parser("simulate", help="Simulate a circuit from JSON")
|
|
38
|
+
simulate_parser.add_argument("--circuit", type=Path, required=True, help="Path to circuit JSON file")
|
|
39
|
+
simulate_parser.add_argument("--shots", type=int, default=0, help="Number of measurement shots to sample")
|
|
40
|
+
simulate_parser.add_argument("--seed", type=int, default=None, help="Random seed for sampling")
|
|
41
|
+
|
|
42
|
+
rand_parser = subparsers.add_parser("random-circuit", help="Generate a random circuit and run XEB")
|
|
43
|
+
rand_parser.add_argument("--qubits", type=int, required=True, help="Number of qubits")
|
|
44
|
+
rand_parser.add_argument("--depth", type=int, required=True, help="Circuit depth (layers)")
|
|
45
|
+
rand_parser.add_argument("--shots", type=int, required=True, help="Number of measurement shots")
|
|
46
|
+
rand_parser.add_argument("--seed", type=int, default=None, help="Random seed for circuit generation and sampling")
|
|
47
|
+
rand_parser.add_argument(
|
|
48
|
+
"--single-qubit-gates",
|
|
49
|
+
type=str,
|
|
50
|
+
nargs="*",
|
|
51
|
+
default=DEFAULT_SINGLE_QUBIT_GATES,
|
|
52
|
+
help="Set of single-qubit gates to sample from",
|
|
53
|
+
)
|
|
54
|
+
rand_parser.add_argument(
|
|
55
|
+
"--two-qubit-gates",
|
|
56
|
+
type=str,
|
|
57
|
+
nargs="*",
|
|
58
|
+
default=DEFAULT_TWO_QUBIT_GATES,
|
|
59
|
+
help="Set of two-qubit gates to sample from",
|
|
60
|
+
)
|
|
61
|
+
rand_parser.add_argument(
|
|
62
|
+
"--multi-qubit-gates",
|
|
63
|
+
type=str,
|
|
64
|
+
nargs="*",
|
|
65
|
+
default=DEFAULT_MULTI_QUBIT_GATES,
|
|
66
|
+
help="Set of multi-qubit gates (three or more qubits) to sample from",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return parser
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _cmd_simulate(args: argparse.Namespace) -> int:
|
|
73
|
+
circuit = _load_circuit(args.circuit)
|
|
74
|
+
simulator = StateVectorSimulator()
|
|
75
|
+
result = simulator.run(circuit, shots=args.shots, seed=args.seed)
|
|
76
|
+
payload = {
|
|
77
|
+
"num_qubits": circuit.num_qubits,
|
|
78
|
+
"shots": result.shots,
|
|
79
|
+
"final_state_real": result.final_state.real.tolist(),
|
|
80
|
+
"final_state_imag": result.final_state.imag.tolist(),
|
|
81
|
+
"probabilities": result.probabilities.tolist(),
|
|
82
|
+
"counts": result.counts,
|
|
83
|
+
"samples": result.samples,
|
|
84
|
+
}
|
|
85
|
+
print(json.dumps(payload, indent=2))
|
|
86
|
+
return 0
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _cmd_random_circuit(args: argparse.Namespace) -> int:
|
|
90
|
+
circuit = random_circuit(
|
|
91
|
+
num_qubits=args.qubits,
|
|
92
|
+
depth=args.depth,
|
|
93
|
+
single_qubit_gates=args.single_qubit_gates,
|
|
94
|
+
two_qubit_gates=args.two_qubit_gates,
|
|
95
|
+
multi_qubit_gates=args.multi_qubit_gates,
|
|
96
|
+
seed=args.seed,
|
|
97
|
+
)
|
|
98
|
+
xeb_result = run_linear_xeb_experiment(
|
|
99
|
+
circuit,
|
|
100
|
+
shots=args.shots,
|
|
101
|
+
seed=args.seed,
|
|
102
|
+
)
|
|
103
|
+
payload = {
|
|
104
|
+
"num_qubits": circuit.num_qubits,
|
|
105
|
+
"depth": args.depth,
|
|
106
|
+
"shots": args.shots,
|
|
107
|
+
"fidelity": xeb_result.fidelity,
|
|
108
|
+
"sample_probabilities": xeb_result.sample_probabilities,
|
|
109
|
+
}
|
|
110
|
+
print(json.dumps(payload, indent=2))
|
|
111
|
+
return 0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _load_circuit(path: Path) -> QuantumCircuit:
|
|
115
|
+
from .run import load_circuit
|
|
116
|
+
return load_circuit(path)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == "__main__": # pragma: no cover
|
|
120
|
+
raise SystemExit(main())
|
|
121
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Measurement sampling utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Dict, Iterable, List
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def sample_measurements(
|
|
11
|
+
probabilities: np.ndarray,
|
|
12
|
+
num_qubits: int,
|
|
13
|
+
shots: int,
|
|
14
|
+
rng: np.random.Generator,
|
|
15
|
+
) -> List[str]:
|
|
16
|
+
probabilities = probabilities / probabilities.sum()
|
|
17
|
+
basis_indices = rng.choice(len(probabilities), size=shots, p=probabilities)
|
|
18
|
+
return [format(index, f"0{num_qubits}b") for index in basis_indices]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def counts_from_samples(samples: Iterable[str]) -> Dict[str, int]:
|
|
22
|
+
counts: Dict[str, int] = {}
|
|
23
|
+
for measurement in samples:
|
|
24
|
+
counts[measurement] = counts.get(measurement, 0) + 1
|
|
25
|
+
return counts
|
|
26
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Gate registry mapping names to handler implementations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from ..circuit import Gate
|
|
8
|
+
from ..gates import (
|
|
9
|
+
MULTI_QUBIT_HANDLERS,
|
|
10
|
+
SINGLE_QUBIT_HANDLERS,
|
|
11
|
+
TWO_QUBIT_HANDLERS,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def apply_gate(
|
|
16
|
+
state: np.ndarray,
|
|
17
|
+
gate: Gate,
|
|
18
|
+
num_qubits: int,
|
|
19
|
+
dtype: np.dtype,
|
|
20
|
+
) -> np.ndarray:
|
|
21
|
+
name = gate.name
|
|
22
|
+
if name in SINGLE_QUBIT_HANDLERS and not gate.controls:
|
|
23
|
+
handler = SINGLE_QUBIT_HANDLERS[name]
|
|
24
|
+
return handler(state, gate, num_qubits, dtype)
|
|
25
|
+
if name in TWO_QUBIT_HANDLERS:
|
|
26
|
+
handler = TWO_QUBIT_HANDLERS[name]
|
|
27
|
+
return handler(state, gate, num_qubits, dtype)
|
|
28
|
+
if name in MULTI_QUBIT_HANDLERS:
|
|
29
|
+
handler = MULTI_QUBIT_HANDLERS[name]
|
|
30
|
+
return handler(state, gate, num_qubits, dtype)
|
|
31
|
+
raise ValueError(f"Unsupported gate: {gate.name}")
|
|
32
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Simulation result dataclass."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from ..circuit import QuantumCircuit
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class SimulationResult:
|
|
15
|
+
"""Results of a simulation run."""
|
|
16
|
+
|
|
17
|
+
circuit: QuantumCircuit
|
|
18
|
+
final_state: np.ndarray
|
|
19
|
+
probabilities: np.ndarray
|
|
20
|
+
shots: Optional[int] = None
|
|
21
|
+
samples: Optional[List[str]] = None
|
|
22
|
+
counts: Optional[Dict[str, int]] = None
|
|
23
|
+
|
|
24
|
+
def bitstring_probabilities(self) -> Dict[str, float]:
|
|
25
|
+
"""Return probabilities keyed by little-endian bitstrings."""
|
|
26
|
+
probs: Dict[str, float] = {}
|
|
27
|
+
for index, probability in enumerate(self.probabilities):
|
|
28
|
+
bitstring = format(index, f"0{self.circuit.num_qubits}b")
|
|
29
|
+
probs[bitstring] = float(probability)
|
|
30
|
+
return probs
|
|
31
|
+
|