quantbom 0.6.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.
- quantbom/__init__.py +497 -0
- quantbom/_bitorder.py +121 -0
- quantbom/_circuit.py +77 -0
- quantbom/_collect.py +661 -0
- quantbom/_drift.py +333 -0
- quantbom/_fidelity.py +280 -0
- quantbom/_merge.py +187 -0
- quantbom/_qir.py +224 -0
- quantbom/_scan.py +118 -0
- quantbom/_sign.py +284 -0
- quantbom/_stats.py +125 -0
- quantbom/_verify.py +530 -0
- quantbom/actions/__init__.py +27 -0
- quantbom/actions/base.py +68 -0
- quantbom/actions/benchmark.py +53 -0
- quantbom/actions/bind_backend.py +71 -0
- quantbom/actions/capture.py +106 -0
- quantbom/actions/check_qir.py +44 -0
- quantbom/actions/check_results.py +75 -0
- quantbom/actions/compare_native.py +88 -0
- quantbom/actions/disclose_error_handling.py +66 -0
- quantbom/actions/execute.py +85 -0
- quantbom/actions/inventory.py +24 -0
- quantbom/actions/layout.py +118 -0
- quantbom/actions/prove_equivalence.py +225 -0
- quantbom/actions/record_provenance.py +49 -0
- quantbom/actions/verify_conversion.py +53 -0
- quantbom/attest.py +363 -0
- quantbom/cli.py +378 -0
- quantbom/frameworks/__init__.py +67 -0
- quantbom/frameworks/base.py +155 -0
- quantbom/frameworks/braket.py +132 -0
- quantbom/frameworks/cirq.py +106 -0
- quantbom/frameworks/cudaq.py +223 -0
- quantbom/frameworks/pennylane.py +116 -0
- quantbom/frameworks/qiskit.py +67 -0
- quantbom/frameworks/qrisp.py +267 -0
- quantbom/harness.json +1730 -0
- quantbom/harness.py +88 -0
- quantbom-0.6.0.dist-info/METADATA +207 -0
- quantbom-0.6.0.dist-info/RECORD +45 -0
- quantbom-0.6.0.dist-info/WHEEL +5 -0
- quantbom-0.6.0.dist-info/entry_points.txt +2 -0
- quantbom-0.6.0.dist-info/licenses/LICENSE +202 -0
- quantbom-0.6.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Qrisp — compiled by Qrisp, then converted by Qrisp's own `.to_qiskit()`.
|
|
2
|
+
|
|
3
|
+
Two steps, judged separately. Qrisp's compiler (`QuantumSession.compile()`,
|
|
4
|
+
`Jaspr.to_qc()`) turns typed variables into a Qrisp QuantumCircuit; whether
|
|
5
|
+
that matches the developer's intent is checked by declared results
|
|
6
|
+
(QBOM-BENCH-02) and service re-execution. The conversion of that circuit to
|
|
7
|
+
Qiskit is a translation like any other adapter's, and QNT-06 checks it against
|
|
8
|
+
Qrisp's OWN simulator (`get_unitary`, `statevector_array`, big-endian: qubit 0
|
|
9
|
+
most significant), up to the dense and state-vector caps.
|
|
10
|
+
|
|
11
|
+
The native program (`native_export`) is Qrisp's compiled circuit in Qrisp's
|
|
12
|
+
terms (`secqdevops.qrisp-program/v1`): qubit identifiers (`primary.0`), the
|
|
13
|
+
typed variables that own them, the instruction list including Qrisp's own
|
|
14
|
+
bookkeeping (`qb_alloc`), and Qrisp's metrics. It is a faithful record of what
|
|
15
|
+
`to_qiskit()` consumed, not a re-loadable program. There is no decision-diagram
|
|
16
|
+
method for Qrisp: Qrisp's OpenQASM exporters go through Qiskit, so checking
|
|
17
|
+
one against the other would compare Qiskit with itself. Qrisp before 0.9
|
|
18
|
+
simulates in single precision, which cannot meet the 1e-7 tolerance; the
|
|
19
|
+
reference is then undecidable (N-A with the reason), never `unfaithful`.
|
|
20
|
+
|
|
21
|
+
Three input kinds, each with its OWN reading of a bitstring (K0b spike,
|
|
22
|
+
5 Oct 2026 — pinned here and held by the conformance suite, C3/C4):
|
|
23
|
+
|
|
24
|
+
QuantumCircuit `qc.run()` qubit 0 is the RIGHTMOST character (Qiskit's order)
|
|
25
|
+
QuantumSession `QuantumVariable.get_measurement()` qubit 0 is the LEFTMOST character
|
|
26
|
+
Jaspr `measure(qv)` -> integer, bit 0 = qubit 0; `to_qc()` allocates the
|
|
27
|
+
classical bits in reverse, so the LOWERED circuit's measured string
|
|
28
|
+
also reads qubit 0 first
|
|
29
|
+
|
|
30
|
+
A declaration written the way the user sees results is therefore translated
|
|
31
|
+
per kind (`bit_order_for`), and for a Jaspr the lowered circuit's own order is
|
|
32
|
+
not Qiskit's (`measured_order_for`). Getting either wrong fails correct
|
|
33
|
+
releases and can pass wrong ones — the PennyLane lesson.
|
|
34
|
+
|
|
35
|
+
Dynamic programs. Classic Qrisp cannot express feed-forward at all
|
|
36
|
+
(`control(clbit)` is refused by Qrisp); mid-circuit measurement and reset
|
|
37
|
+
lower faithfully and are accepted. Jasp expresses feed-forward on a measured
|
|
38
|
+
bit (`with control(measure(...))`), which lowers to a Qiskit `if_else`. Any
|
|
39
|
+
real-time classical computation — repeat-until-success, arithmetic or
|
|
40
|
+
comparisons on results, loops bounded by a measurement — has no circuit
|
|
41
|
+
representation; Qrisp itself refuses to convert it, and so does this adapter,
|
|
42
|
+
with the reason (the QIR route, K5, is the way to certify those).
|
|
43
|
+
"""
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
from typing import Any
|
|
47
|
+
|
|
48
|
+
from .base import FrameworkAdapter, Undecidable, dist_version, qiskit_version
|
|
49
|
+
|
|
50
|
+
REALTIME_REFUSAL = (
|
|
51
|
+
"Jasp programs with real-time classical computation (repeat-until-success, arithmetic or comparisons "
|
|
52
|
+
"on measurement results, loops bounded by a measurement) have no circuit representation: Qrisp cannot "
|
|
53
|
+
"convert them. Feed-forward on a measured bit alone (`with control(measure(...))`) is supported. "
|
|
54
|
+
"Certifying real-time programs needs the QIR route (HARNESS_KITS_PLAN.md K5)"
|
|
55
|
+
)
|
|
56
|
+
ARGUMENT_REFUSAL = (
|
|
57
|
+
"Jasp programs with arguments are not supported: the certified circuit would depend on values the "
|
|
58
|
+
"evidence does not record. Wrap it in an argument-free function that binds them, then make_jaspr(...)()"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _is_jaspr(obj: Any) -> bool:
|
|
63
|
+
return (type(obj).__module__ or "").startswith("qrisp.jasp") and callable(getattr(obj, "to_qc", None))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _jaspr_arguments(jaspr: Any) -> list[str]:
|
|
67
|
+
"""Argument variables of a traced Jasp program (everything but the quantum state)."""
|
|
68
|
+
return [str(v.aval) for v in getattr(jaspr, "invars", ()) if "QuantumState" not in str(v.aval)]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
#: id(program) -> (program, its compiled Qrisp circuit). `Jaspr.to_qc()` names
|
|
72
|
+
#: qubits from a global counter (qb_59 on one call, qb_77 on the next), so the
|
|
73
|
+
#: circuit is compiled once per program and every reader — lowering, the native
|
|
74
|
+
#: export the QNT-06 report hashes, the file written beside it, both references —
|
|
75
|
+
#: sees the same one. The program is held, so its id cannot be reused meanwhile.
|
|
76
|
+
_COMPILED: dict[int, tuple[Any, Any]] = {}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _compiled_once(program: Any, compile_: Any) -> Any:
|
|
80
|
+
hit = _COMPILED.get(id(program))
|
|
81
|
+
if hit is not None and hit[0] is program:
|
|
82
|
+
return hit[1]
|
|
83
|
+
qc = compile_()
|
|
84
|
+
_COMPILED[id(program)] = (program, qc)
|
|
85
|
+
return qc
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _jaspr_circuit(jaspr: Any) -> Any:
|
|
89
|
+
"""Qrisp's own lowering of a Jasp program to its QuantumCircuit, with refusals explained."""
|
|
90
|
+
if _jaspr_arguments(jaspr):
|
|
91
|
+
raise ValueError(ARGUMENT_REFUSAL)
|
|
92
|
+
|
|
93
|
+
def to_qc() -> Any:
|
|
94
|
+
try:
|
|
95
|
+
_, qc = jaspr.to_qc()
|
|
96
|
+
except Exception as err:
|
|
97
|
+
if "real-time feedback" in str(err):
|
|
98
|
+
raise ValueError(REALTIME_REFUSAL) from err
|
|
99
|
+
raise
|
|
100
|
+
return qc
|
|
101
|
+
|
|
102
|
+
return _compiled_once(jaspr, to_qc)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class QrispAdapter(FrameworkAdapter):
|
|
106
|
+
name = "qrisp"
|
|
107
|
+
component = "qrisp"
|
|
108
|
+
license = "EUPL-1.2"
|
|
109
|
+
bit_order = "by-input"
|
|
110
|
+
bit_orders = {"QuantumCircuit": "qubit0_last", "QuantumSession": "qubit0_first", "Jaspr": "qubit0_first"}
|
|
111
|
+
translation = "qrisp-to_qiskit" # Qrisp's own converter, checked by QNT-06 against Qrisp's simulator
|
|
112
|
+
supports_dynamic = True # mid-circuit measurement and reset (classic), feed-forward (Jasp): verified by C7
|
|
113
|
+
reference = "dense"
|
|
114
|
+
|
|
115
|
+
def detect(self, obj: Any) -> bool:
|
|
116
|
+
# duck-typed: Qrisp QuantumCircuit and QuantumSession both expose it; a Jaspr exposes to_qc
|
|
117
|
+
return callable(getattr(obj, "to_qiskit", None)) or _is_jaspr(obj)
|
|
118
|
+
|
|
119
|
+
def input_kind(self, obj: Any) -> str:
|
|
120
|
+
if _is_jaspr(obj):
|
|
121
|
+
return "Jaspr"
|
|
122
|
+
if type(obj).__name__ == "QuantumCircuit":
|
|
123
|
+
return "QuantumCircuit"
|
|
124
|
+
return "QuantumSession"
|
|
125
|
+
|
|
126
|
+
def measured_order_for(self, obj: Any) -> str:
|
|
127
|
+
# Jaspr.to_qc() numbers classical bits in reverse (qubit i -> clbit n-1-i)
|
|
128
|
+
return "qubit0_first" if self.input_kind(obj) == "Jaspr" else "qubit0_last"
|
|
129
|
+
|
|
130
|
+
def lower(self, obj: Any) -> tuple[Any, str, Any]:
|
|
131
|
+
if _is_jaspr(obj):
|
|
132
|
+
qc = _jaspr_circuit(obj)
|
|
133
|
+
return qc.to_qiskit(), f"qrisp {dist_version('qrisp')} (jasp) / qiskit {qiskit_version()}", qc
|
|
134
|
+
return obj.to_qiskit(), f"qrisp {dist_version('qrisp')} / qiskit {qiskit_version()}", obj
|
|
135
|
+
|
|
136
|
+
# ---- native support: Qrisp's own record of the program, and its own simulator ----
|
|
137
|
+
def _native_circuit(self, obj: Any) -> Any:
|
|
138
|
+
"""The Qrisp QuantumCircuit `to_qiskit()` converts: compiled for a session."""
|
|
139
|
+
kind = self.input_kind(obj)
|
|
140
|
+
if kind == "Jaspr":
|
|
141
|
+
return _jaspr_circuit(obj)
|
|
142
|
+
if kind == "QuantumSession":
|
|
143
|
+
return _compiled_once(obj, obj.compile)
|
|
144
|
+
return obj
|
|
145
|
+
|
|
146
|
+
def native_export(self, obj: Any) -> tuple[str, str]:
|
|
147
|
+
import json
|
|
148
|
+
|
|
149
|
+
qc = self._native_circuit(obj)
|
|
150
|
+
|
|
151
|
+
def name(bit: Any) -> str:
|
|
152
|
+
return str(getattr(bit, "identifier", bit))
|
|
153
|
+
|
|
154
|
+
def param(p: Any) -> Any:
|
|
155
|
+
try:
|
|
156
|
+
return float(p)
|
|
157
|
+
except (TypeError, ValueError):
|
|
158
|
+
return str(p)
|
|
159
|
+
|
|
160
|
+
variables = [{"name": qv.name, "type": type(qv).__name__, "size": int(qv.size),
|
|
161
|
+
"qubits": [name(q) for q in qv.reg]}
|
|
162
|
+
for qv in (getattr(obj, "qv_list", None) or [])] # sessions on Qrisp >= 0.9
|
|
163
|
+
program = {
|
|
164
|
+
"schema": "secqdevops.qrisp-program/v1",
|
|
165
|
+
"framework": "qrisp",
|
|
166
|
+
"qrisp_version": dist_version("qrisp"),
|
|
167
|
+
"input_kind": self.input_kind(obj),
|
|
168
|
+
"qubits": [name(q) for q in qc.qubits],
|
|
169
|
+
"clbits": [name(c) for c in qc.clbits],
|
|
170
|
+
**({"variables": variables} if variables else {}),
|
|
171
|
+
"instructions": [{"op": ins.op.name, "qubits": [name(q) for q in ins.qubits],
|
|
172
|
+
**({"clbits": [name(c) for c in ins.clbits]} if ins.clbits else {}),
|
|
173
|
+
**({"params": [param(p) for p in ins.op.params]} if ins.op.params else {})}
|
|
174
|
+
for ins in qc.data],
|
|
175
|
+
"metrics": {"num_qubits": int(qc.num_qubits()), "depth": int(qc.depth()),
|
|
176
|
+
"cnot_count": int(qc.cnot_count()), "cnot_depth": int(qc.cnot_depth()),
|
|
177
|
+
"t_depth": int(qc.t_depth()),
|
|
178
|
+
"count_ops": {str(k): int(v) for k, v in dict(qc.count_ops()).items()}},
|
|
179
|
+
}
|
|
180
|
+
return json.dumps(program, indent=2, ensure_ascii=True) + "\n", "qrisp-program-json"
|
|
181
|
+
|
|
182
|
+
def _unitary_body(self, obj: Any) -> Any:
|
|
183
|
+
"""The compiled circuit without terminal measurements and allocation bookkeeping;
|
|
184
|
+
a measurement or reset acted on afterwards makes it non-unitary."""
|
|
185
|
+
qc = self._native_circuit(obj)
|
|
186
|
+
body, measured = qc.clearcopy(), set()
|
|
187
|
+
for ins in qc.data:
|
|
188
|
+
op = ins.op.name
|
|
189
|
+
if op in ("qb_alloc", "qb_dealloc", "barrier"):
|
|
190
|
+
continue
|
|
191
|
+
if op == "measure":
|
|
192
|
+
measured.update(ins.qubits)
|
|
193
|
+
continue
|
|
194
|
+
if op == "reset" or measured.intersection(ins.qubits) or ins.clbits:
|
|
195
|
+
raise Undecidable("non-terminal measurement, reset or classical control: the circuit is not a unitary")
|
|
196
|
+
body.append(ins)
|
|
197
|
+
return body
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def _double(array: Any, what: str) -> Any:
|
|
201
|
+
import numpy as np
|
|
202
|
+
|
|
203
|
+
array = np.asarray(array)
|
|
204
|
+
if array.dtype != np.complex128:
|
|
205
|
+
raise Undecidable(f"qrisp {dist_version('qrisp')} returns its {what} as {array.dtype}: single precision "
|
|
206
|
+
"cannot meet the 1e-7 comparison tolerance (Qrisp >= 0.9 simulates in double)")
|
|
207
|
+
return array
|
|
208
|
+
|
|
209
|
+
@staticmethod
|
|
210
|
+
def _simulate(call: Any) -> Any:
|
|
211
|
+
"""Run Qrisp's simulator without its progress bar in the producer's terminal."""
|
|
212
|
+
import contextlib
|
|
213
|
+
import io
|
|
214
|
+
|
|
215
|
+
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
|
|
216
|
+
return call()
|
|
217
|
+
|
|
218
|
+
def reference_unitary(self, obj: Any) -> tuple[Any, str]:
|
|
219
|
+
body = self._unitary_body(obj)
|
|
220
|
+
return (self._double(self._simulate(body.get_unitary), "unitary"),
|
|
221
|
+
f"qrisp {dist_version('qrisp')} QuantumCircuit.get_unitary")
|
|
222
|
+
|
|
223
|
+
def reference_state(self, obj: Any) -> tuple[Any, str]:
|
|
224
|
+
body = self._unitary_body(obj)
|
|
225
|
+
return (self._double(self._simulate(body.statevector_array), "state vector").reshape(-1),
|
|
226
|
+
f"qrisp {dist_version('qrisp')} QuantumCircuit.statevector_array")
|
|
227
|
+
|
|
228
|
+
def native_run(self, obj: Any, shots: int) -> dict[str, Any]:
|
|
229
|
+
"""Qrisp's own simulator on the compiled circuit — the one the native
|
|
230
|
+
export records. Qrisp's QuantumCircuit.run() reads classical bits as the
|
|
231
|
+
lowered circuit's measured string does (clbit 0 rightmost), for every
|
|
232
|
+
input kind, so no reordering is needed."""
|
|
233
|
+
qc = self._native_circuit(obj).copy()
|
|
234
|
+
if not any(ins.op.name == "measure" for ins in qc.data):
|
|
235
|
+
qc.measure(qc.qubits) # clbit i <- qubit i, as measure_all() on the lowered circuit
|
|
236
|
+
counts = self._simulate(lambda: qc.run(shots=shots))
|
|
237
|
+
measured = {str(k).replace(" ", ""): int(v) for k, v in counts.items()}
|
|
238
|
+
run = {"simulator": f"qrisp {dist_version('qrisp')} QuantumCircuit.run", "reading": measured,
|
|
239
|
+
"reading_order": "measured", "measured": measured}
|
|
240
|
+
variables = list(getattr(obj, "qv_list", None) or []) if self.input_kind(obj) == "QuantumSession" else []
|
|
241
|
+
index = {str(getattr(q, "identifier", q)): i for i, q in enumerate(qc.qubits)}
|
|
242
|
+
if variables and not any(ins.op.name == "measure" for ins in self._native_circuit(obj).data):
|
|
243
|
+
# the same outcomes in the developer's vocabulary: each variable's own bits
|
|
244
|
+
def value(qv: Any, key: str) -> Any:
|
|
245
|
+
bits = [key[-1 - index[str(getattr(q, "identifier", q))]] for q in qv.reg]
|
|
246
|
+
return bits[0] == "1" if type(qv).__name__ == "QuantumBool" else "".join(bits)
|
|
247
|
+
|
|
248
|
+
run["by_variable"] = [{"values": {qv.name: value(qv, key) for qv in variables}, "count": n}
|
|
249
|
+
for key, n in sorted(measured.items(), key=lambda kv: -kv[1])]
|
|
250
|
+
return run
|
|
251
|
+
|
|
252
|
+
def export_for_equivalence(self, obj: Any) -> tuple[str, str, str | None]:
|
|
253
|
+
raise Undecidable("Qrisp's OpenQASM exporters go through Qiskit, so a decision-diagram check of the "
|
|
254
|
+
"export would compare Qiskit with itself; Qrisp's own simulator is the reference")
|
|
255
|
+
|
|
256
|
+
def native_sample(self, obj: Any, shots: int = 8) -> dict[str, int]:
|
|
257
|
+
kind = self.input_kind(obj)
|
|
258
|
+
if kind == "QuantumCircuit":
|
|
259
|
+
return {str(k): int(v) for k, v in obj.run(shots=shots).items()}
|
|
260
|
+
if kind == "Jaspr":
|
|
261
|
+
return {str(k): int(v) for k, v in _jaspr_circuit(obj).run(shots=shots).items()}
|
|
262
|
+
# a session: Qrisp's own reading is QuantumVariable.get_measurement() (probabilities)
|
|
263
|
+
variables = list(getattr(obj, "qv_list", []))
|
|
264
|
+
if len(variables) != 1:
|
|
265
|
+
raise Undecidable("native_sample expects a session holding exactly one QuantumVariable")
|
|
266
|
+
probs = variables[0].get_measurement()
|
|
267
|
+
return {str(k): int(round(float(p) * shots)) for k, p in probs.items() if float(p) > 1e-12}
|