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
tests/test_parity.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import numpy as np
|
|
3
|
+
import cirq
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from qomputing_simulator import QuantumCircuit, StateVectorSimulator
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _align_global_phase(reference: np.ndarray, candidate: np.ndarray) -> np.ndarray:
|
|
10
|
+
phase = 1 + 0j
|
|
11
|
+
for ref, cand in zip(reference, candidate):
|
|
12
|
+
if abs(cand) > 1e-12:
|
|
13
|
+
phase = ref / cand
|
|
14
|
+
break
|
|
15
|
+
return candidate * phase
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _to_little_endian(state: np.ndarray, num_qubits: int) -> np.ndarray:
|
|
19
|
+
reordered = np.zeros_like(state)
|
|
20
|
+
for index, amplitude in enumerate(state):
|
|
21
|
+
little_index = int("{:0{width}b}".format(index, width=num_qubits)[::-1], 2)
|
|
22
|
+
reordered[little_index] = amplitude
|
|
23
|
+
return reordered
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _run_and_compare(circuit: QuantumCircuit) -> float:
|
|
27
|
+
ours = StateVectorSimulator().run(circuit).final_state
|
|
28
|
+
q = cirq.LineQubit.range(circuit.num_qubits)
|
|
29
|
+
ops = []
|
|
30
|
+
for gate in circuit.gates:
|
|
31
|
+
name = gate.name
|
|
32
|
+
t = gate.targets
|
|
33
|
+
c = gate.controls
|
|
34
|
+
params = gate.params
|
|
35
|
+
if name == "h":
|
|
36
|
+
ops.append(cirq.H(q[t[0]]))
|
|
37
|
+
elif name == "x":
|
|
38
|
+
ops.append(cirq.X(q[t[0]]))
|
|
39
|
+
elif name == "y":
|
|
40
|
+
ops.append(cirq.Y(q[t[0]]))
|
|
41
|
+
elif name == "z":
|
|
42
|
+
ops.append(cirq.Z(q[t[0]]))
|
|
43
|
+
elif name == "s":
|
|
44
|
+
ops.append(cirq.S(q[t[0]]))
|
|
45
|
+
elif name == "sdg":
|
|
46
|
+
ops.append((cirq.S ** -1)(q[t[0]]))
|
|
47
|
+
elif name == "t":
|
|
48
|
+
ops.append(cirq.T(q[t[0]]))
|
|
49
|
+
elif name == "tdg":
|
|
50
|
+
ops.append((cirq.T ** -1)(q[t[0]]))
|
|
51
|
+
elif name == "sx":
|
|
52
|
+
ops.append((cirq.X ** 0.5)(q[t[0]]))
|
|
53
|
+
elif name == "sxdg":
|
|
54
|
+
ops.append((cirq.X ** -0.5)(q[t[0]]))
|
|
55
|
+
elif name == "rx":
|
|
56
|
+
ops.append(cirq.rx(params["theta"])(q[t[0]]))
|
|
57
|
+
elif name == "ry":
|
|
58
|
+
ops.append(cirq.ry(params["theta"])(q[t[0]]))
|
|
59
|
+
elif name == "rz":
|
|
60
|
+
ops.append(cirq.rz(params["theta"])(q[t[0]]))
|
|
61
|
+
elif name == "cx":
|
|
62
|
+
ops.append(cirq.CNOT(q[c[0]], q[t[0]]))
|
|
63
|
+
elif name == "cz":
|
|
64
|
+
ops.append(cirq.CZ(q[c[0]], q[t[0]]))
|
|
65
|
+
elif name == "swap":
|
|
66
|
+
ops.append(cirq.SWAP(q[t[0]], q[t[1]]))
|
|
67
|
+
elif name == "rxx":
|
|
68
|
+
ops.append(cirq.XXPowGate(exponent=params["theta"] / np.pi)(q[t[0]], q[t[1]]))
|
|
69
|
+
elif name == "ccx":
|
|
70
|
+
ops.append(cirq.CCX(q[c[0]], q[c[1]], q[t[0]]))
|
|
71
|
+
elif name == "ccz":
|
|
72
|
+
ops.append(cirq.CCZ(q[c[0]], q[c[1]], q[t[0]]))
|
|
73
|
+
elif name == "cswap":
|
|
74
|
+
ops.append(cirq.CSWAP(q[c[0]], q[t[0]], q[t[1]]))
|
|
75
|
+
else:
|
|
76
|
+
raise ValueError(f"Unsupported gate {name} in parity test")
|
|
77
|
+
cirq_state = cirq.Simulator(dtype=np.complex128).simulate(cirq.Circuit(ops), qubit_order=q).final_state_vector
|
|
78
|
+
cirq_state = _to_little_endian(cirq_state, circuit.num_qubits)
|
|
79
|
+
cirq_state = _align_global_phase(ours, cirq_state)
|
|
80
|
+
return float(np.max(np.abs(ours - cirq_state)))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@pytest.mark.parametrize("gate_name", ["h", "sx", "sxdg", "sdg", "t", "tdg"])
|
|
84
|
+
def test_single_qubit_gates(gate_name):
|
|
85
|
+
qc = QuantumCircuit(1)
|
|
86
|
+
qc.ry(0, 0.73).rz(0, -0.42)
|
|
87
|
+
getattr(qc, gate_name)(0)
|
|
88
|
+
assert _run_and_compare(qc) < 1e-7
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@pytest.mark.parametrize("gate_name", ["rx", "ry", "rz"])
|
|
92
|
+
def test_single_qubit_rotations(gate_name):
|
|
93
|
+
angle = 0.37 + {"rx": 0.1, "ry": 0.2, "rz": 0.3}[gate_name]
|
|
94
|
+
qc = QuantumCircuit(1)
|
|
95
|
+
qc.h(0)
|
|
96
|
+
getattr(qc, gate_name)(0, angle)
|
|
97
|
+
qc.h(0)
|
|
98
|
+
assert _run_and_compare(qc) < 1e-7
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@pytest.mark.parametrize("gate_name", ["cx", "cz", "swap", "rxx"])
|
|
102
|
+
def test_two_qubit_gates(gate_name):
|
|
103
|
+
qc = QuantumCircuit(2)
|
|
104
|
+
qc.h(0).ry(1, 0.56)
|
|
105
|
+
if gate_name == "rxx":
|
|
106
|
+
qc.rxx(0, 1, 0.42)
|
|
107
|
+
elif gate_name == "swap":
|
|
108
|
+
qc.swap(0, 1)
|
|
109
|
+
else:
|
|
110
|
+
getattr(qc, gate_name)(0, 1)
|
|
111
|
+
qc.rx(0, 0.11).rz(1, -0.33)
|
|
112
|
+
assert _run_and_compare(qc) < 1e-7
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@pytest.mark.parametrize("gate_name", ["ccx", "ccz", "cswap"])
|
|
116
|
+
def test_multi_qubit_gates(gate_name):
|
|
117
|
+
qc = QuantumCircuit(3)
|
|
118
|
+
qc.h(0).h(1).rx(2, 0.25)
|
|
119
|
+
if gate_name == "cswap":
|
|
120
|
+
qc.cswap(0, 1, 2)
|
|
121
|
+
else:
|
|
122
|
+
getattr(qc, gate_name)(0, 1, 2)
|
|
123
|
+
qc.ry(0, 0.1).rz(1, -0.2)
|
|
124
|
+
assert _run_and_compare(qc) < 1e-7
|
|
125
|
+
|
tools/cirq_comparison.py
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"""Utility for comparing the state vector simulator against Cirq.
|
|
2
|
+
|
|
3
|
+
This script generates random quantum circuits, executes them with both
|
|
4
|
+
the in-tree state vector simulator and Cirq's reference simulator, and
|
|
5
|
+
reports discrepancies in amplitudes, probabilities, and XEB fidelity.
|
|
6
|
+
|
|
7
|
+
Usage (after installing Cirq):
|
|
8
|
+
|
|
9
|
+
python tools/cirq_comparison.py --max-qubits 3 --depths 3 5
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import cmath
|
|
16
|
+
import math
|
|
17
|
+
import random
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Dict, Iterable, List, Sequence, Tuple
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import cirq
|
|
23
|
+
except ImportError as exc: # pragma: no cover - runtime guard
|
|
24
|
+
raise SystemExit(
|
|
25
|
+
"Cirq is required for this comparison script. Install with `pip install cirq`."
|
|
26
|
+
) from exc
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
|
|
30
|
+
from qomputing_simulator import (
|
|
31
|
+
QuantumCircuit,
|
|
32
|
+
StateVectorSimulator,
|
|
33
|
+
compute_linear_xeb_fidelity,
|
|
34
|
+
)
|
|
35
|
+
from qomputing_simulator import cli as _cli
|
|
36
|
+
from qomputing_simulator.gates import (
|
|
37
|
+
DEFAULT_MULTI_QUBIT_GATES,
|
|
38
|
+
DEFAULT_SINGLE_QUBIT_GATES,
|
|
39
|
+
DEFAULT_TWO_QUBIT_GATES,
|
|
40
|
+
)
|
|
41
|
+
GLOBAL_PHASE_TOLERANCE = 1e-12
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class ComparisonMetrics:
|
|
46
|
+
num_qubits: int
|
|
47
|
+
depth: int
|
|
48
|
+
circuit_index: int
|
|
49
|
+
amplitude_error: float
|
|
50
|
+
probability_error: float
|
|
51
|
+
xeb_error: float | None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
55
|
+
parser = _build_parser()
|
|
56
|
+
args = parser.parse_args(argv)
|
|
57
|
+
|
|
58
|
+
rng = random.Random(args.seed)
|
|
59
|
+
simulator = StateVectorSimulator()
|
|
60
|
+
|
|
61
|
+
metrics: List[ComparisonMetrics] = []
|
|
62
|
+
|
|
63
|
+
for num_qubits in range(args.min_qubits, args.max_qubits + 1):
|
|
64
|
+
for depth in args.depths:
|
|
65
|
+
for circuit_index in range(args.circuits_per_config):
|
|
66
|
+
circuit_seed = rng.randint(0, 2**31 - 1)
|
|
67
|
+
circuit_rng = random.Random(circuit_seed)
|
|
68
|
+
circuit = _cli._random_circuit(
|
|
69
|
+
num_qubits=num_qubits,
|
|
70
|
+
depth=depth,
|
|
71
|
+
single_qubit_gates=args.single_qubit_gates,
|
|
72
|
+
two_qubit_gates=args.two_qubit_gates,
|
|
73
|
+
multi_qubit_gates=args.multi_qubit_gates,
|
|
74
|
+
seed=circuit_seed,
|
|
75
|
+
)
|
|
76
|
+
metric = _compare_with_cirq(
|
|
77
|
+
circuit,
|
|
78
|
+
simulator=simulator,
|
|
79
|
+
shots=args.shots,
|
|
80
|
+
seed=circuit_seed,
|
|
81
|
+
depth=depth,
|
|
82
|
+
circuit_index=circuit_index,
|
|
83
|
+
)
|
|
84
|
+
metrics.append(metric)
|
|
85
|
+
_print_metric(metric)
|
|
86
|
+
|
|
87
|
+
summary = _compute_summary(metrics)
|
|
88
|
+
_print_summary(summary)
|
|
89
|
+
|
|
90
|
+
max_amp_err = summary["max_amplitude_error"]
|
|
91
|
+
max_prob_err = summary["max_probability_error"]
|
|
92
|
+
amp_ok = max_amp_err <= args.amplitude_tolerance
|
|
93
|
+
prob_ok = max_prob_err <= args.probability_tolerance
|
|
94
|
+
xeb_ok = True
|
|
95
|
+
if args.shots > 0:
|
|
96
|
+
max_xeb_err = summary["max_xeb_error"]
|
|
97
|
+
xeb_ok = max_xeb_err <= args.xeb_tolerance
|
|
98
|
+
|
|
99
|
+
return 0 if amp_ok and prob_ok and xeb_ok else 1
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
103
|
+
parser = argparse.ArgumentParser(description="Compare simulator outputs with Cirq")
|
|
104
|
+
parser.add_argument("--min-qubits", type=int, default=1, help="Minimum number of qubits to test")
|
|
105
|
+
parser.add_argument("--max-qubits", type=int, default=3, help="Maximum number of qubits to test")
|
|
106
|
+
parser.add_argument(
|
|
107
|
+
"--depths",
|
|
108
|
+
type=int,
|
|
109
|
+
nargs="+",
|
|
110
|
+
default=[3],
|
|
111
|
+
help="List of circuit depths to evaluate",
|
|
112
|
+
)
|
|
113
|
+
parser.add_argument(
|
|
114
|
+
"--circuits-per-config",
|
|
115
|
+
type=int,
|
|
116
|
+
default=3,
|
|
117
|
+
help="Number of random circuits per (qubits, depth) combo",
|
|
118
|
+
)
|
|
119
|
+
parser.add_argument(
|
|
120
|
+
"--single-qubit-gates",
|
|
121
|
+
type=str,
|
|
122
|
+
nargs="*",
|
|
123
|
+
default=DEFAULT_SINGLE_QUBIT_GATES,
|
|
124
|
+
help="Pool of single-qubit gates to sample from",
|
|
125
|
+
)
|
|
126
|
+
parser.add_argument(
|
|
127
|
+
"--two-qubit-gates",
|
|
128
|
+
type=str,
|
|
129
|
+
nargs="*",
|
|
130
|
+
default=DEFAULT_TWO_QUBIT_GATES,
|
|
131
|
+
help="Pool of two-qubit gates to sample from",
|
|
132
|
+
)
|
|
133
|
+
parser.add_argument(
|
|
134
|
+
"--multi-qubit-gates",
|
|
135
|
+
type=str,
|
|
136
|
+
nargs="*",
|
|
137
|
+
default=DEFAULT_MULTI_QUBIT_GATES,
|
|
138
|
+
help="Pool of multi-qubit gates to sample from",
|
|
139
|
+
)
|
|
140
|
+
parser.add_argument("--shots", type=int, default=512, help="Number of samples for XEB comparison")
|
|
141
|
+
parser.add_argument("--seed", type=int, default=1234, help="Master random seed")
|
|
142
|
+
parser.add_argument(
|
|
143
|
+
"--amplitude-tolerance",
|
|
144
|
+
type=float,
|
|
145
|
+
default=1e-6,
|
|
146
|
+
help="Maximum tolerated amplitude sup-norm difference",
|
|
147
|
+
)
|
|
148
|
+
parser.add_argument(
|
|
149
|
+
"--probability-tolerance",
|
|
150
|
+
type=float,
|
|
151
|
+
default=1e-6,
|
|
152
|
+
help="Maximum tolerated probability sup-norm difference",
|
|
153
|
+
)
|
|
154
|
+
parser.add_argument(
|
|
155
|
+
"--xeb-tolerance",
|
|
156
|
+
type=float,
|
|
157
|
+
default=1e-3,
|
|
158
|
+
help="Maximum tolerated linear XEB fidelity difference",
|
|
159
|
+
)
|
|
160
|
+
return parser
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _compare_with_cirq(
|
|
164
|
+
circuit: QuantumCircuit,
|
|
165
|
+
*,
|
|
166
|
+
simulator: StateVectorSimulator,
|
|
167
|
+
shots: int,
|
|
168
|
+
seed: int,
|
|
169
|
+
depth: int,
|
|
170
|
+
circuit_index: int,
|
|
171
|
+
) -> ComparisonMetrics:
|
|
172
|
+
ours = simulator.run(circuit, shots=shots, seed=seed)
|
|
173
|
+
cirq_circuit, qubits = _circuit_to_cirq(circuit)
|
|
174
|
+
cirq_sim = cirq.Simulator(dtype=np.complex128)
|
|
175
|
+
cirq_result = cirq_sim.simulate(cirq_circuit)
|
|
176
|
+
cirq_state = _to_little_endian_state(cirq_result.final_state_vector, circuit.num_qubits)
|
|
177
|
+
|
|
178
|
+
aligned_cirq_state = _align_global_phase(ours.final_state, cirq_state)
|
|
179
|
+
amplitude_error = _max_difference(ours.final_state, aligned_cirq_state)
|
|
180
|
+
|
|
181
|
+
cirq_probabilities = [abs(amplitude) ** 2 for amplitude in aligned_cirq_state]
|
|
182
|
+
probability_error = _max_difference(ours.probabilities, cirq_probabilities)
|
|
183
|
+
|
|
184
|
+
xeb_error = None
|
|
185
|
+
if shots > 0:
|
|
186
|
+
sample_rng = random.Random(seed)
|
|
187
|
+
samples = _sample_bitstrings(sample_rng, cirq_probabilities, circuit.num_qubits, shots)
|
|
188
|
+
xeb_ours = compute_linear_xeb_fidelity(ours.probabilities, samples)
|
|
189
|
+
xeb_cirq = compute_linear_xeb_fidelity(cirq_probabilities, samples)
|
|
190
|
+
xeb_error = abs(xeb_ours - xeb_cirq)
|
|
191
|
+
|
|
192
|
+
return ComparisonMetrics(
|
|
193
|
+
num_qubits=circuit.num_qubits,
|
|
194
|
+
depth=depth,
|
|
195
|
+
circuit_index=circuit_index,
|
|
196
|
+
amplitude_error=amplitude_error,
|
|
197
|
+
probability_error=probability_error,
|
|
198
|
+
xeb_error=xeb_error,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _circuit_to_cirq(circuit: QuantumCircuit) -> Tuple[cirq.Circuit, Tuple[cirq.Qid, ...]]:
|
|
203
|
+
qubits = tuple(cirq.LineQubit.range(circuit.num_qubits))
|
|
204
|
+
operations: List[cirq.Operation] = []
|
|
205
|
+
for gate in circuit.gates:
|
|
206
|
+
if len(gate.targets) == 1 and not gate.controls:
|
|
207
|
+
target = qubits[gate.targets[0]]
|
|
208
|
+
operations.append(_single_qubit_operation(gate.name, target, gate.params))
|
|
209
|
+
elif gate.name == "cx" and len(gate.controls) == 1 and len(gate.targets) == 1:
|
|
210
|
+
control = qubits[gate.controls[0]]
|
|
211
|
+
target = qubits[gate.targets[0]]
|
|
212
|
+
operations.append(cirq.CNOT(control, target))
|
|
213
|
+
elif gate.name == "cy" and len(gate.controls) == 1 and len(gate.targets) == 1:
|
|
214
|
+
control = qubits[gate.controls[0]]
|
|
215
|
+
target = qubits[gate.targets[0]]
|
|
216
|
+
operations.append(cirq.ControlledGate(cirq.Y)(control, target))
|
|
217
|
+
elif gate.name == "cz" and len(gate.controls) == 1 and len(gate.targets) == 1:
|
|
218
|
+
control = qubits[gate.controls[0]]
|
|
219
|
+
target = qubits[gate.targets[0]]
|
|
220
|
+
operations.append(cirq.CZ(control, target))
|
|
221
|
+
elif gate.name == "cp" and len(gate.controls) == 1 and len(gate.targets) == 1:
|
|
222
|
+
control = qubits[gate.controls[0]]
|
|
223
|
+
target = qubits[gate.targets[0]]
|
|
224
|
+
phi = float(gate.params.get("phi", 0.0))
|
|
225
|
+
operations.append(cirq.CZPowGate(exponent=phi / np.pi)(control, target))
|
|
226
|
+
elif gate.name == "swap" and len(gate.targets) == 2:
|
|
227
|
+
q1 = qubits[gate.targets[0]]
|
|
228
|
+
q2 = qubits[gate.targets[1]]
|
|
229
|
+
operations.append(cirq.SWAP(q1, q2))
|
|
230
|
+
elif gate.name == "iswap" and len(gate.targets) == 2:
|
|
231
|
+
q1 = qubits[gate.targets[0]]
|
|
232
|
+
q2 = qubits[gate.targets[1]]
|
|
233
|
+
operations.append(cirq.ISWAP(q1, q2))
|
|
234
|
+
elif gate.name == "sqrtiswap" and len(gate.targets) == 2:
|
|
235
|
+
q1 = qubits[gate.targets[0]]
|
|
236
|
+
q2 = qubits[gate.targets[1]]
|
|
237
|
+
operations.append((cirq.ISWAP ** 0.5)(q1, q2))
|
|
238
|
+
elif gate.name in {"rxx", "ryy", "rzz"} and len(gate.targets) == 2:
|
|
239
|
+
q1 = qubits[gate.targets[0]]
|
|
240
|
+
q2 = qubits[gate.targets[1]]
|
|
241
|
+
theta = float(gate.params.get("theta", 0.0))
|
|
242
|
+
exponent = theta / np.pi
|
|
243
|
+
if gate.name == "rxx":
|
|
244
|
+
operations.append(cirq.XXPowGate(exponent=exponent)(q1, q2))
|
|
245
|
+
elif gate.name == "ryy":
|
|
246
|
+
operations.append(cirq.YYPowGate(exponent=exponent)(q1, q2))
|
|
247
|
+
else:
|
|
248
|
+
operations.append(cirq.ZZPowGate(exponent=exponent)(q1, q2))
|
|
249
|
+
elif gate.name == "csx" and len(gate.controls) == 1 and len(gate.targets) == 1:
|
|
250
|
+
control = qubits[gate.controls[0]]
|
|
251
|
+
target = qubits[gate.targets[0]]
|
|
252
|
+
operations.append(cirq.ControlledGate(cirq.X ** 0.5)(control, target))
|
|
253
|
+
elif gate.name == "ccx" and len(gate.controls) == 2 and len(gate.targets) == 1:
|
|
254
|
+
c1 = qubits[gate.controls[0]]
|
|
255
|
+
c2 = qubits[gate.controls[1]]
|
|
256
|
+
target = qubits[gate.targets[0]]
|
|
257
|
+
operations.append(cirq.CCX(c1, c2, target))
|
|
258
|
+
elif gate.name == "ccz" and len(gate.controls) == 2 and len(gate.targets) == 1:
|
|
259
|
+
c1 = qubits[gate.controls[0]]
|
|
260
|
+
c2 = qubits[gate.controls[1]]
|
|
261
|
+
target = qubits[gate.targets[0]]
|
|
262
|
+
operations.append(cirq.CCZ(c1, c2, target))
|
|
263
|
+
elif gate.name == "cswap" and len(gate.controls) == 1 and len(gate.targets) == 2:
|
|
264
|
+
control = qubits[gate.controls[0]]
|
|
265
|
+
q1 = qubits[gate.targets[0]]
|
|
266
|
+
q2 = qubits[gate.targets[1]]
|
|
267
|
+
operations.append(cirq.CSWAP(control, q1, q2))
|
|
268
|
+
else: # pragma: no cover - defensive
|
|
269
|
+
raise ValueError(f"Unsupported gate for Cirq conversion: {gate}")
|
|
270
|
+
return cirq.Circuit(operations), qubits
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _single_qubit_operation(name: str, qubit: cirq.Qid, params: Dict[str, float]) -> cirq.Operation:
|
|
274
|
+
two_pi = 2.0 * math.pi
|
|
275
|
+
if name == "id":
|
|
276
|
+
return cirq.I(qubit)
|
|
277
|
+
if name == "x":
|
|
278
|
+
return cirq.X(qubit)
|
|
279
|
+
if name == "y":
|
|
280
|
+
return cirq.Y(qubit)
|
|
281
|
+
if name == "z":
|
|
282
|
+
return cirq.Z(qubit)
|
|
283
|
+
if name == "h":
|
|
284
|
+
return cirq.H(qubit)
|
|
285
|
+
if name == "s":
|
|
286
|
+
return cirq.S(qubit)
|
|
287
|
+
if name == "sdg":
|
|
288
|
+
return (cirq.S ** -1)(qubit)
|
|
289
|
+
if name == "t":
|
|
290
|
+
return cirq.T(qubit)
|
|
291
|
+
if name == "tdg":
|
|
292
|
+
return (cirq.T ** -1)(qubit)
|
|
293
|
+
if name == "sx":
|
|
294
|
+
return (cirq.X ** 0.5)(qubit)
|
|
295
|
+
if name == "sxdg":
|
|
296
|
+
return (cirq.X ** -0.5)(qubit)
|
|
297
|
+
if name in {"rx", "ry", "rz"}:
|
|
298
|
+
theta = float(params.get("theta", 0.0))
|
|
299
|
+
if name == "rx":
|
|
300
|
+
return cirq.rx(theta)(qubit)
|
|
301
|
+
if name == "ry":
|
|
302
|
+
return cirq.ry(theta)(qubit)
|
|
303
|
+
return cirq.rz(theta)(qubit)
|
|
304
|
+
if name == "u1":
|
|
305
|
+
lam = float(params.get("lambda", 0.0))
|
|
306
|
+
matrix = np.array([[1.0, 0.0], [0.0, cmath.exp(1j * lam)]], dtype=complex)
|
|
307
|
+
return cirq.MatrixGate(matrix)(qubit)
|
|
308
|
+
if name == "u2":
|
|
309
|
+
phi = float(params.get("phi", 0.0))
|
|
310
|
+
lam = float(params.get("lambda", 0.0))
|
|
311
|
+
matrix = (1 / math.sqrt(2)) * np.array(
|
|
312
|
+
[
|
|
313
|
+
[1.0, -cmath.exp(1j * lam)],
|
|
314
|
+
[cmath.exp(1j * phi), cmath.exp(1j * (phi + lam))],
|
|
315
|
+
],
|
|
316
|
+
dtype=complex,
|
|
317
|
+
)
|
|
318
|
+
return cirq.MatrixGate(matrix)(qubit)
|
|
319
|
+
if name == "u3":
|
|
320
|
+
theta = float(params.get("theta", 0.0))
|
|
321
|
+
phi = float(params.get("phi", 0.0))
|
|
322
|
+
lam = float(params.get("lambda", 0.0))
|
|
323
|
+
cos = math.cos(theta / 2.0)
|
|
324
|
+
sin = math.sin(theta / 2.0)
|
|
325
|
+
matrix = np.array(
|
|
326
|
+
[
|
|
327
|
+
[cos, -cmath.exp(1j * lam) * sin],
|
|
328
|
+
[cmath.exp(1j * phi) * sin, cmath.exp(1j * (phi + lam)) * cos],
|
|
329
|
+
],
|
|
330
|
+
dtype=complex,
|
|
331
|
+
)
|
|
332
|
+
return cirq.MatrixGate(matrix)(qubit)
|
|
333
|
+
raise ValueError(f"Unsupported single-qubit gate: {name}")
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _align_global_phase(
|
|
337
|
+
reference: Sequence[complex],
|
|
338
|
+
target: Sequence[complex],
|
|
339
|
+
) -> List[complex]:
|
|
340
|
+
phase = 1 + 0j
|
|
341
|
+
for ref, tgt in zip(reference, target):
|
|
342
|
+
if abs(tgt) > GLOBAL_PHASE_TOLERANCE:
|
|
343
|
+
phase = ref / tgt
|
|
344
|
+
break
|
|
345
|
+
return [tgt * phase for tgt in target]
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _max_difference(
|
|
349
|
+
reference: Sequence[float | complex],
|
|
350
|
+
candidate: Sequence[float | complex],
|
|
351
|
+
) -> float:
|
|
352
|
+
return max(abs(r - c) for r, c in zip(reference, candidate))
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _to_little_endian_state(state: Sequence[complex], num_qubits: int) -> List[complex]:
|
|
356
|
+
"""Reorder Cirq's big-endian state vector into little-endian ordering."""
|
|
357
|
+
size = 1 << num_qubits
|
|
358
|
+
if len(state) != size:
|
|
359
|
+
raise ValueError("State vector size does not match qubit count")
|
|
360
|
+
reordered = [0j] * size
|
|
361
|
+
for index, amplitude in enumerate(state):
|
|
362
|
+
little_index = _reverse_bits(index, num_qubits)
|
|
363
|
+
reordered[little_index] = amplitude
|
|
364
|
+
return reordered
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _reverse_bits(value: int, width: int) -> int:
|
|
368
|
+
reversed_value = 0
|
|
369
|
+
for _ in range(width):
|
|
370
|
+
reversed_value = (reversed_value << 1) | (value & 1)
|
|
371
|
+
value >>= 1
|
|
372
|
+
return reversed_value
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _sample_bitstrings(
|
|
376
|
+
rng: random.Random,
|
|
377
|
+
probabilities: Sequence[float],
|
|
378
|
+
num_qubits: int,
|
|
379
|
+
shots: int,
|
|
380
|
+
) -> List[str]:
|
|
381
|
+
outcomes = [format(index, f"0{num_qubits}b") for index in range(len(probabilities))]
|
|
382
|
+
return rng.choices(outcomes, weights=probabilities, k=shots)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _print_metric(metric: ComparisonMetrics) -> None:
|
|
386
|
+
summary = (
|
|
387
|
+
f"[qubits={metric.num_qubits} depth={metric.depth} circuit={metric.circuit_index}] "
|
|
388
|
+
f"amp_err={metric.amplitude_error:.3e} "
|
|
389
|
+
f"prob_err={metric.probability_error:.3e}"
|
|
390
|
+
)
|
|
391
|
+
if metric.xeb_error is not None:
|
|
392
|
+
summary += f" xeb_err={metric.xeb_error:.3e}"
|
|
393
|
+
print(summary)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _compute_summary(metrics: List[ComparisonMetrics]) -> Dict[str, float]:
|
|
397
|
+
max_amp = max(metric.amplitude_error for metric in metrics) if metrics else 0.0
|
|
398
|
+
max_prob = max(metric.probability_error for metric in metrics) if metrics else 0.0
|
|
399
|
+
max_xeb = max(
|
|
400
|
+
(metric.xeb_error for metric in metrics if metric.xeb_error is not None),
|
|
401
|
+
default=0.0,
|
|
402
|
+
)
|
|
403
|
+
return {
|
|
404
|
+
"max_amplitude_error": max_amp,
|
|
405
|
+
"max_probability_error": max_prob,
|
|
406
|
+
"max_xeb_error": max_xeb,
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _print_summary(summary: Dict[str, float]) -> None:
|
|
411
|
+
print("=== Comparison Summary ===")
|
|
412
|
+
print(f"Max amplitude error : {summary['max_amplitude_error']:.6e}")
|
|
413
|
+
print(f"Max probability error : {summary['max_probability_error']:.6e}")
|
|
414
|
+
print(f"Max XEB error : {summary['max_xeb_error']:.6e}")
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
if __name__ == "__main__": # pragma: no cover
|
|
418
|
+
raise SystemExit(main())
|
|
419
|
+
|