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,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,7 @@
1
+ """Simulation engine entry points."""
2
+
3
+ from .result import SimulationResult
4
+ from .statevector import StateVectorSimulator
5
+
6
+ __all__ = ["SimulationResult", "StateVectorSimulator"]
7
+
@@ -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
+