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.
Files changed (45) hide show
  1. quantbom/__init__.py +497 -0
  2. quantbom/_bitorder.py +121 -0
  3. quantbom/_circuit.py +77 -0
  4. quantbom/_collect.py +661 -0
  5. quantbom/_drift.py +333 -0
  6. quantbom/_fidelity.py +280 -0
  7. quantbom/_merge.py +187 -0
  8. quantbom/_qir.py +224 -0
  9. quantbom/_scan.py +118 -0
  10. quantbom/_sign.py +284 -0
  11. quantbom/_stats.py +125 -0
  12. quantbom/_verify.py +530 -0
  13. quantbom/actions/__init__.py +27 -0
  14. quantbom/actions/base.py +68 -0
  15. quantbom/actions/benchmark.py +53 -0
  16. quantbom/actions/bind_backend.py +71 -0
  17. quantbom/actions/capture.py +106 -0
  18. quantbom/actions/check_qir.py +44 -0
  19. quantbom/actions/check_results.py +75 -0
  20. quantbom/actions/compare_native.py +88 -0
  21. quantbom/actions/disclose_error_handling.py +66 -0
  22. quantbom/actions/execute.py +85 -0
  23. quantbom/actions/inventory.py +24 -0
  24. quantbom/actions/layout.py +118 -0
  25. quantbom/actions/prove_equivalence.py +225 -0
  26. quantbom/actions/record_provenance.py +49 -0
  27. quantbom/actions/verify_conversion.py +53 -0
  28. quantbom/attest.py +363 -0
  29. quantbom/cli.py +378 -0
  30. quantbom/frameworks/__init__.py +67 -0
  31. quantbom/frameworks/base.py +155 -0
  32. quantbom/frameworks/braket.py +132 -0
  33. quantbom/frameworks/cirq.py +106 -0
  34. quantbom/frameworks/cudaq.py +223 -0
  35. quantbom/frameworks/pennylane.py +116 -0
  36. quantbom/frameworks/qiskit.py +67 -0
  37. quantbom/frameworks/qrisp.py +267 -0
  38. quantbom/harness.json +1730 -0
  39. quantbom/harness.py +88 -0
  40. quantbom-0.6.0.dist-info/METADATA +207 -0
  41. quantbom-0.6.0.dist-info/RECORD +45 -0
  42. quantbom-0.6.0.dist-info/WHEEL +5 -0
  43. quantbom-0.6.0.dist-info/entry_points.txt +2 -0
  44. quantbom-0.6.0.dist-info/licenses/LICENSE +202 -0
  45. 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}