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.
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
+
@@ -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
+