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
quantbom/_drift.py ADDED
@@ -0,0 +1,333 @@
1
+ """The semantic drift sentinel (QUANTUM_INNOVATION_OPPORTUNITIES.md I4).
2
+
3
+ Toolchains change under an unchanged program: a new SDK, a new transpiler, a
4
+ new frontend exporter. The compiled artifact then differs, and the question
5
+ that matters for compliance is not *whether* it differs but *what kind* of
6
+ difference it is (QUANTUM_CHALLENGES_PLAN.md; the CRA reading in the
7
+ 7 Oct 2026 drift analysis):
8
+
9
+ none the compiled circuit is the one recorded in the baseline
10
+ nondeterministic a different realisation under the SAME toolchain as the
11
+ baseline — the compiler itself is not deterministic (Qrisp's
12
+ is not); same meaning, so benign, but worth knowing
13
+ structural a different realisation with the same unitary — benign;
14
+ document the toolchain, nothing else changes
15
+ observational the unitaries differ by what no measurement observes
16
+ (identical measurement statistics) — benign for the program's
17
+ behaviour, worth knowing about
18
+ semantic the compiled circuit no longer means what the source means —
19
+ a potential substantial modification; fails
20
+ inconclusive the checker could not decide — reported, never silently passed
21
+
22
+ Every classification is machine-checked with mqt.qcec: the compiled circuit
23
+ against the SOURCE (does it still mean what was written?) and, when a baseline
24
+ exists, against the baseline's compiled circuit (did meaning change since it
25
+ was recorded?). Structure is compared by gate counts and depth; bytes are not
26
+ compared, because QPY serialisation changes between Qiskit versions without
27
+ anything else changing.
28
+
29
+ A corpus is a Python module exposing PROGRAMS: {name: (framework, builder)},
30
+ the builder returning the framework's native object. The baseline is a JSON
31
+ file recorded with `quantbom drift --update-baseline`; it stores each compiled
32
+ circuit as OpenQASM 3 text so a later toolchain can load it.
33
+
34
+ The baseline is recorded in CI on a PINNED toolchain (drift_corpus/toolchain.txt),
35
+ never on a developer machine: `--require-pins` refuses to record when the
36
+ installed versions differ from the pins, and the matrix's reference job runs
37
+ `--strict` on those pins, where anything but `none` or `nondeterministic` means
38
+ the baseline no longer matches the toolchain it claims to describe.
39
+ """
40
+ from __future__ import annotations
41
+
42
+ import hashlib
43
+ import importlib.util
44
+ import json
45
+ import sys
46
+ from datetime import datetime, timezone
47
+ from importlib.metadata import version as _version
48
+ from pathlib import Path
49
+ from typing import Any, Callable
50
+
51
+ SCHEMA = "secqdevops.drift-baseline/v1"
52
+ ORDER = ["none", "nondeterministic", "structural", "observational", "inconclusive", "semantic"]
53
+ FAILS = {"semantic"}
54
+ STRICT_OK = {"none", "nondeterministic", "skipped"} # what a run ON the pinned toolchain may report
55
+
56
+
57
+ def read_pins(path: str | Path) -> dict[str, str]:
58
+ """`dist==version` lines (requirements syntax); comments and blanks ignored."""
59
+ pins: dict[str, str] = {}
60
+ for line in Path(path).read_text(encoding="utf-8").splitlines():
61
+ line = line.split("#", 1)[0].strip()
62
+ if not line:
63
+ continue
64
+ if "==" not in line:
65
+ raise ValueError(f"pins must be exact (dist==version): {line!r}")
66
+ dist, ver = (x.strip() for x in line.split("==", 1))
67
+ pins[dist] = ver
68
+ return pins
69
+
70
+
71
+ def pin_mismatches(pins: dict[str, str]) -> list[str]:
72
+ """Installed versions that differ from the pins (missing counts as a mismatch)."""
73
+ out = []
74
+ for dist, want in pins.items():
75
+ try:
76
+ have = _version(dist)
77
+ except Exception:
78
+ have = None
79
+ if have != want:
80
+ out.append(f"{dist}: pinned {want}, installed {have or 'not installed'}")
81
+ return out
82
+
83
+
84
+ def strict_violations(report: dict[str, Any]) -> list[str]:
85
+ """Programs whose drift a pinned-toolchain run must not show."""
86
+ return [f"{n}: {e.get('drift')}" for n, e in report["programs"].items() if e.get("drift") not in STRICT_OK]
87
+
88
+
89
+ def toolchain() -> dict[str, str]:
90
+ out: dict[str, str] = {}
91
+ for dist in ("qiskit", "qrisp", "cirq-core", "amazon-braket-sdk", "pennylane", "cudaq", "mqt.qcec", "qiskit-qasm3-import"):
92
+ try:
93
+ out[dist] = _version(dist)
94
+ except Exception:
95
+ pass
96
+ return out
97
+
98
+
99
+ def fingerprint(qc) -> dict[str, Any]:
100
+ return {"ops": {k: int(v) for k, v in sorted(qc.count_ops().items())}, "depth": int(qc.depth()),
101
+ "size": int(qc.size()), "qubits": int(qc.num_qubits)}
102
+
103
+
104
+ def qasm3_text(qc) -> str | None:
105
+ try:
106
+ from qiskit import qasm3
107
+
108
+ return qasm3.dumps(qc)
109
+ except Exception:
110
+ return None
111
+
112
+
113
+ def qasm3_sha(qc) -> str | None:
114
+ text = qasm3_text(qc)
115
+ return "sha256:" + hashlib.sha256(text.encode("utf-8")).hexdigest() if text is not None else None
116
+
117
+
118
+ def load_qasm3(text: str):
119
+ from qiskit import qasm3
120
+
121
+ return qasm3.loads(text)
122
+
123
+
124
+ def _final_measurements(circuit) -> bool:
125
+ from .actions.prove_equivalence import final_measurements
126
+
127
+ return final_measurements(circuit)
128
+
129
+
130
+ def _dynamic(circuit) -> bool:
131
+ from ._circuit import inspect_circuit
132
+
133
+ return bool(inspect_circuit(circuit)["dynamic"])
134
+
135
+
136
+ def meaning(source, compiled) -> tuple[str, dict[str, str]]:
137
+ """How `compiled` relates to `source`: unitary | dynamic | observational | semantic | inconclusive."""
138
+ from mqt import qcec
139
+
140
+ detail: dict[str, str] = {}
141
+ if _dynamic(source):
142
+ # the collector's own path for dynamic circuits (prove_equivalence.verify_dynamic)
143
+ from .actions.prove_equivalence import verify_dynamic
144
+
145
+ try:
146
+ result, dyn_detail = verify_dynamic(qcec, source, compiled)
147
+ except Exception as err:
148
+ return "inconclusive", {"error": f"{type(err).__name__}: {str(err)[:160]}"}
149
+ crit = str(result.equivalence).split(".")[-1]
150
+ detail["dynamic"] = crit
151
+ if result.considered_equivalent() and not (dyn_detail or {}).get("unverified_gates"):
152
+ return "dynamic", detail
153
+ return ("semantic" if crit == "not_equivalent" else "inconclusive"), detail
154
+ try:
155
+ full = qcec.verify(source, compiled)
156
+ except Exception as err:
157
+ return "inconclusive", {"error": f"{type(err).__name__}: {str(err)[:160]}"}
158
+ crit = str(full.equivalence).split(".")[-1]
159
+ detail["unitary"] = crit
160
+ if full.considered_equivalent():
161
+ return "unitary", detail
162
+ if crit != "not_equivalent":
163
+ return "inconclusive", detail
164
+ if _final_measurements(source):
165
+ from .actions.prove_equivalence import partial_check
166
+
167
+ try:
168
+ partial = partial_check(qcec, source, compiled)
169
+ except Exception as err:
170
+ detail["partial"] = f"error: {type(err).__name__}"
171
+ return "semantic", detail
172
+ detail["partial"] = str(partial.equivalence).split(".")[-1]
173
+ if partial.considered_equivalent():
174
+ return "observational", detail
175
+ if detail["partial"] != "not_equivalent": # the partial check could not decide: no evidence either way
176
+ return "inconclusive", detail
177
+ return "semantic", detail
178
+
179
+
180
+ def classify(source, compiled, baseline: dict[str, Any] | None, same_toolchain: bool = False) -> dict[str, Any]:
181
+ """One program's drift record. `baseline` is this program's entry from the baseline file, or None;
182
+ `same_toolchain` says whether the baseline was recorded with exactly this toolchain."""
183
+ kind, detail = meaning(source, compiled)
184
+ fp = fingerprint(compiled)
185
+ record: dict[str, Any] = {"compiled": fp, "vs_source": kind, "checks": detail, "vs_baseline": "n/a"}
186
+ if kind == "semantic":
187
+ drift = "semantic"
188
+ elif baseline is not None and baseline.get("compiled") == fp:
189
+ # the realisation is byte-identical to the baseline: no drift, whatever the
190
+ # meaning check managed this time (a timeout or an undecided QCEC check must
191
+ # not read as drift for an unchanged realisation)
192
+ drift, record["vs_baseline"] = "none", "unchanged"
193
+ elif kind == "inconclusive":
194
+ drift = "inconclusive"
195
+ elif baseline is None:
196
+ drift = "unbaselined"
197
+ else:
198
+ # the realisation changed since the baseline: did its meaning?
199
+ base_kind = "unknown"
200
+ if baseline.get("compiled_qasm3"):
201
+ try:
202
+ base_kind, base_detail = meaning(load_qasm3(baseline["compiled_qasm3"]), compiled)
203
+ record["checks"]["vs_baseline"] = base_detail
204
+ except Exception as err:
205
+ record["checks"]["vs_baseline"] = {"error": f"{type(err).__name__}: {str(err)[:160]}"}
206
+ record["vs_baseline"] = base_kind
207
+ if base_kind == "semantic":
208
+ drift = "semantic"
209
+ elif kind == "observational" or base_kind == "observational":
210
+ drift = "observational"
211
+ elif base_kind == "inconclusive" or base_kind == "unknown":
212
+ drift = "inconclusive"
213
+ elif same_toolchain:
214
+ drift = "nondeterministic" # nothing changed but the compiler's own choices
215
+ else:
216
+ drift = "structural"
217
+ record["drift"] = drift
218
+ return record
219
+
220
+
221
+ def load_corpus(path: str | Path) -> dict[str, tuple[str, Callable[[], Any]]]:
222
+ p = Path(path)
223
+ spec = importlib.util.spec_from_file_location(p.stem, p)
224
+ module = importlib.util.module_from_spec(spec)
225
+ sys.path.insert(0, str(p.parent))
226
+ try:
227
+ spec.loader.exec_module(module)
228
+ finally:
229
+ sys.path.remove(str(p.parent))
230
+ programs = getattr(module, "PROGRAMS", None)
231
+ if not isinstance(programs, dict) or not programs:
232
+ raise ValueError(f"{p} must define PROGRAMS = {{name: (framework, builder)}}")
233
+ return programs
234
+
235
+
236
+ def lower(obj):
237
+ """The program as the collector sees it: a Qiskit circuit, through the adapter."""
238
+ from qiskit import QuantumCircuit
239
+
240
+ if isinstance(obj, QuantumCircuit):
241
+ return obj
242
+ from .frameworks import detect
243
+
244
+ return detect(obj).lower(obj)[0]
245
+
246
+
247
+ def compile_(source):
248
+ from qiskit import transpile
249
+
250
+ from . import _collect as C
251
+
252
+ return transpile(source, basis_gates=C.BASIS_GATES, optimization_level=2, seed_transpiler=C.SEED_TRANSPILER)
253
+
254
+
255
+ def run(programs: dict[str, tuple[str, Callable[[], Any]]], baseline: dict[str, Any] | None) -> tuple[dict[str, Any], int]:
256
+ """Compile every program in THIS toolchain, classify, and return (report, exit_code).
257
+
258
+ exit 1 when any program drifted semantically; 0 otherwise (structural and
259
+ observational drift are reported, inconclusive and skipped programs too).
260
+ """
261
+ base_programs = (baseline or {}).get("programs", {})
262
+ report: dict[str, Any] = {"schema": "secqdevops.drift-report/v1",
263
+ "run_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
264
+ "toolchain": toolchain(),
265
+ "baseline_toolchain": (baseline or {}).get("recorded_with"),
266
+ "programs": {}}
267
+ for name, (framework, builder) in programs.items():
268
+ entry: dict[str, Any] = {"framework": framework}
269
+ try:
270
+ native = builder()
271
+ except ImportError as err:
272
+ entry.update(drift="skipped", reason=f"{framework} not installed ({err.name})")
273
+ report["programs"][name] = entry
274
+ continue
275
+ except Exception as err: # the framework refused the program in this version — that is itself a finding
276
+ entry.update(drift="skipped", reason=f"program could not be built: {type(err).__name__}: {str(err)[:160]}")
277
+ report["programs"][name] = entry
278
+ continue
279
+ try:
280
+ source = lower(native)
281
+ compiled = compile_(source)
282
+ except Exception as err:
283
+ entry.update(drift="inconclusive", reason=f"could not lower/compile: {type(err).__name__}: {str(err)[:160]}")
284
+ report["programs"][name] = entry
285
+ continue
286
+ entry["source_qasm3_sha256"] = qasm3_sha(source)
287
+ base = base_programs.get(name)
288
+ if base and base.get("source_qasm3_sha256") not in (None, entry["source_qasm3_sha256"]):
289
+ entry["source_changed"] = True # the program itself (or its exporter) changed since the baseline
290
+ entry.update(classify(source, compiled, base, same_toolchain=(report["toolchain"] == report["baseline_toolchain"])))
291
+ entry["compiled_qasm3"] = qasm3_text(compiled)
292
+ report["programs"][name] = entry
293
+ worst = max((e.get("drift") for e in report["programs"].values() if e.get("drift") in ORDER), key=ORDER.index, default="none")
294
+ report["summary"] = {
295
+ "worst": worst,
296
+ "counts": {k: sum(1 for e in report["programs"].values() if e.get("drift") == k)
297
+ for k in ORDER + ["unbaselined", "skipped"]},
298
+ "nondeterministic_programs": sorted(n for n, e in report["programs"].items() if e.get("drift") == "nondeterministic"),
299
+ }
300
+ return report, (1 if worst in FAILS else 0)
301
+
302
+
303
+ def baseline_from(report: dict[str, Any], pins_path: str | Path | None = None) -> dict[str, Any]:
304
+ """Record this run as the baseline later runs are compared with."""
305
+ import os
306
+
307
+ provenance = {k: os.environ[k] for k in ("GITHUB_REPOSITORY", "GITHUB_RUN_ID", "GITHUB_SHA", "GITHUB_WORKFLOW") if os.environ.get(k)}
308
+ return {
309
+ "schema": SCHEMA,
310
+ "recorded_at": report["run_at"],
311
+ "recorded_with": report["toolchain"],
312
+ "pins": str(pins_path) if pins_path else None,
313
+ "pins_sha256": ("sha256:" + hashlib.sha256(Path(pins_path).read_bytes()).hexdigest()) if pins_path else None,
314
+ "recorded_in": provenance or {"note": "recorded outside CI — provisional until re-recorded on the pinned toolchain"},
315
+ "programs": {name: {"framework": e["framework"], "source_qasm3_sha256": e.get("source_qasm3_sha256"),
316
+ "compiled": e.get("compiled"), "vs_source": e.get("vs_source"),
317
+ "compiled_qasm3": e.get("compiled_qasm3")}
318
+ for name, e in report["programs"].items() if e.get("compiled")},
319
+ }
320
+
321
+
322
+ def render(report: dict[str, Any]) -> str:
323
+ lines = [f"drift sentinel — toolchain {report['toolchain']}"]
324
+ if report.get("baseline_toolchain"):
325
+ lines.append(f"baseline recorded with {report['baseline_toolchain']}")
326
+ w = max((len(n) for n in report["programs"]), default=8)
327
+ for name, e in report["programs"].items():
328
+ d = e.get("drift", "?")
329
+ extra = e.get("reason") or (f"vs source: {e.get('vs_source')}; vs baseline: {e.get('vs_baseline')}" if "vs_source" in e else "")
330
+ lines.append(f" {name:{w}} {d:13} {extra}")
331
+ s = report["summary"]
332
+ lines.append(f"worst: {s['worst']} {s['counts']}")
333
+ return "\n".join(lines)
quantbom/_fidelity.py ADDED
@@ -0,0 +1,280 @@
1
+ """QNT-06 — frontend conversion fidelity (client side).
2
+
3
+ Every framework is lowered to one Qiskit circuit before evidence is collected,
4
+ and QNT-05 then proves *that* circuit equivalent to its transpiled form. For
5
+ Cirq, Braket and PennyLane there is an earlier step — native object ->
6
+ OpenQASM -> Qiskit — that QNT-05 cannot see: if an adapter mistranslates a
7
+ gate, QNT-05 faithfully certifies the wrong circuit.
8
+
9
+ This module closes that step. It computes the circuit's unitary with the
10
+ framework's OWN simulator (the authoritative reference for what the developer
11
+ wrote) and compares it with the converted Qiskit circuit, up to global phase.
12
+
13
+ Design rules, matching the rest of the scheme:
14
+ * Qubit order is pinned per framework, never "whichever matches". All three
15
+ put qubit 0 first (big-endian), Qiskit puts it last; a converter that
16
+ reversed the register would be a genuine mistranslation and must fail.
17
+ * Anything that cannot be decided — too many qubits for a dense unitary,
18
+ mid-circuit measurement, mismatched register size, an unexpected error —
19
+ is `inconclusive`, which the gate reports as N-A. Never affirmative,
20
+ never a blocker.
21
+ * Runs only where a translation exists. Qiskit inputs need none; Qrisp's
22
+ `.to_qiskit()` *is* Qrisp's compiler, whose intent is checked by
23
+ QBOM-BENCH-02 and re-execution rather than by unitary comparison.
24
+
25
+ The native program never leaves the client, so the server cannot recompute
26
+ this verdict. It is client-computed evidence, hash-bound (CRA-INTEG-01),
27
+ signature-bound (CRA-SIGN-01) and consistency-checked against its declared
28
+ verdict — the same trust model as QNT-05.
29
+ """
30
+ from __future__ import annotations
31
+
32
+ import hashlib
33
+ import os
34
+ from typing import Any
35
+
36
+ from .frameworks import ADAPTERS, Undecidable
37
+ from .frameworks import get as _adapter
38
+
39
+ SCHEMA = "secqdevops.conversion-fidelity/v1"
40
+ # Frameworks whose adapter performs a translation QNT-06 must check — derived
41
+ # from the adapter registry, so a new adapter is covered without editing this.
42
+ TRANSLATED_FRONTENDS = tuple(a.name for a in ADAPTERS if a.translates)
43
+ TOLERANCE = 1e-7
44
+ NON_UNITARY_OPS = {"measure", "reset", "delay", "initialize"}
45
+
46
+ _Inconclusive = Undecidable # former name, kept for callers
47
+
48
+
49
+ # A dense 2^n x 2^n unitary: 10 qubits is 16 MiB of complex128, 12 is 256 MiB.
50
+ # Declared per framework in harness_manifest.json (capabilities.max_reference_qubits)
51
+ # and held to this constant by the contract check.
52
+ DEFAULT_MAX_QUBITS = 10
53
+ # A state vector is 2^n amplitudes: 20 qubits is 16 MiB. The scalable reference
54
+ # beyond the dense cap (K3); declared as capabilities.max_statevector_qubits.
55
+ DEFAULT_MAX_STATE_QUBITS = 20
56
+ METHODS = ("dense", "statevector", "decision-diagram")
57
+
58
+
59
+ def max_qubits() -> int:
60
+ return int(os.environ.get("QUANTBOM_FIDELITY_MAX_QUBITS", str(DEFAULT_MAX_QUBITS)))
61
+
62
+
63
+ def max_state_qubits() -> int:
64
+ return int(os.environ.get("QUANTBOM_STATEVECTOR_MAX_QUBITS", str(DEFAULT_MAX_STATE_QUBITS)))
65
+
66
+
67
+ def _strip_measurements(converted: Any):
68
+ qc = converted.remove_final_measurements(inplace=False)
69
+ remaining = NON_UNITARY_OPS & set(qc.count_ops())
70
+ if remaining:
71
+ raise _Inconclusive(f"converted circuit is not unitary ({', '.join(sorted(remaining))})")
72
+ return qc
73
+
74
+
75
+ def _converted_unitary(converted: Any):
76
+ from qiskit.quantum_info import Operator
77
+
78
+ # Qiskit is little-endian; the native frameworks are big-endian (see module doc).
79
+ return Operator(_strip_measurements(converted)).reverse_qargs().data
80
+
81
+
82
+ def _converted_state(converted: Any):
83
+ from qiskit.quantum_info import Statevector
84
+
85
+ return Statevector(_strip_measurements(converted)).reverse_qargs().data
86
+
87
+
88
+ def _aligned_deviation(native, converted_arr, np) -> tuple[float, str | None]:
89
+ """Max |native - phase * converted| after aligning the global phase at the
90
+ native array's largest entry; a reason when no alignment is possible."""
91
+ if native.shape != converted_arr.shape:
92
+ raise _Inconclusive(f"register size differs: native {native.shape[0]} vs converted {converted_arr.shape[0]} "
93
+ "basis states — not comparable like for like")
94
+ i = np.unravel_index(np.argmax(np.abs(native)), native.shape)
95
+ if abs(converted_arr[i]) < 1e-12:
96
+ return float(np.max(np.abs(native - converted_arr))), "the converted circuit has no amplitude where the native circuit's largest one is"
97
+ aligned = (native[i] / converted_arr[i]) * converted_arr
98
+ return float(np.max(np.abs(native - aligned))), None
99
+
100
+
101
+ def _entry(name: str, description: str, reference: str | None = None, verdict: str = "inconclusive",
102
+ reason: str | None = None, deviation: float | None = None) -> dict[str, Any]:
103
+ return {"name": name, "description": description, "reference": reference, "verdict": verdict,
104
+ "reason": reason, "max_abs_deviation": deviation}
105
+
106
+
107
+ def _dense(adapter, native, converted, np) -> dict[str, Any]:
108
+ desc = "unitary comparison up to global phase"
109
+ try:
110
+ native_u, reference = adapter.reference_unitary(native)
111
+ native_u = np.asarray(native_u, dtype=complex)
112
+ deviation, why = _aligned_deviation(native_u, _converted_unitary(converted), np)
113
+ if why:
114
+ return _entry("dense", desc, reference, "unfaithful", why, deviation)
115
+ if deviation <= TOLERANCE:
116
+ return _entry("dense", desc, reference, "faithful", None, deviation)
117
+ return _entry("dense", desc, reference, "unfaithful",
118
+ "the converted circuit computes a different transformation from the native one", deviation)
119
+ except _Inconclusive as why:
120
+ return _entry("dense", desc, None, "inconclusive", str(why))
121
+ except Exception as err:
122
+ return _entry("dense", desc, None, "inconclusive", f"comparison failed: {type(err).__name__}: {err}")
123
+
124
+
125
+ def _statevector(adapter, native, converted, np) -> dict[str, Any]:
126
+ desc = ("state-vector comparison on |0...0> up to global phase — the framework's own simulator, "
127
+ "but one input state: weaker than a unitary comparison")
128
+ try:
129
+ native_s, reference = adapter.reference_state(native)
130
+ native_s = np.asarray(native_s, dtype=complex).reshape(-1)
131
+ deviation, why = _aligned_deviation(native_s, _converted_state(converted), np)
132
+ if why:
133
+ return _entry("statevector", desc, reference, "unfaithful", why, deviation)
134
+ if deviation <= TOLERANCE:
135
+ return _entry("statevector", desc, reference, "faithful", None, deviation)
136
+ return _entry("statevector", desc, reference, "unfaithful",
137
+ "the converted circuit maps |0...0> to a different state from the native one", deviation)
138
+ except _Inconclusive as why:
139
+ return _entry("statevector", desc, None, "inconclusive", str(why))
140
+ except Exception as err:
141
+ return _entry("statevector", desc, None, "inconclusive", f"comparison failed: {type(err).__name__}: {err}")
142
+
143
+
144
+ def _decision_diagram(adapter, native, converted) -> dict[str, Any]:
145
+ desc = ("decision-diagram equivalence (mqt.qcec) of the hash-bound native export, parsed by mqt-core "
146
+ "independently of Qiskit's loader, against the converted circuit — reference = the exported "
147
+ "text, not the framework's simulator")
148
+ try:
149
+ import importlib
150
+
151
+ qcec = importlib.import_module("mqt.qcec") # honours an absent/blocked module even when `mqt` is loaded
152
+ except ImportError:
153
+ return _entry("decision-diagram", desc, None, "inconclusive", "mqt.qcec is not installed (pip install 'quantbom[verify]')")
154
+ import tempfile
155
+ from pathlib import Path
156
+
157
+ try:
158
+ text, fmt, note = adapter.export_for_equivalence(native)
159
+ reference = f"native export ({fmt}) parsed by mqt-core" + (f"; {note}" if note else "")
160
+ with tempfile.TemporaryDirectory() as d:
161
+ path = Path(d) / f"native.{'qasm' if fmt.startswith('openqasm') else 'txt'}"
162
+ path.write_text(text, encoding="utf-8")
163
+ result = qcec.verify(str(path), converted)
164
+ criterion = str(result.equivalence).split(".")[-1]
165
+ if result.considered_equivalent():
166
+ return _entry("decision-diagram", desc, reference, "faithful")
167
+ if criterion == "not_equivalent":
168
+ return _entry("decision-diagram", desc, reference, "unfaithful",
169
+ "the exported text and the converted circuit compute different transformations")
170
+ return _entry("decision-diagram", desc, reference, "inconclusive", f"mqt.qcec: {criterion}")
171
+ except Exception as err:
172
+ return _entry("decision-diagram", desc, None, "inconclusive", f"comparison failed: {type(err).__name__}: {str(err)[:160]}")
173
+
174
+
175
+ def assess_conversion(native: Any, converted: Any, source_format: str) -> dict[str, Any] | None:
176
+ """Return a conversion-fidelity report, or None when no translation exists.
177
+
178
+ Methods, by size (K3): `dense` up to the dense cap; beyond it `statevector`
179
+ (the framework's own simulator, one input state) up to the state-vector cap
180
+ together with `decision-diagram` (the exported text through an independent
181
+ parser, full operator). When `dense` is within the cap but cannot decide
182
+ (K1: CUDA-Q drops idle qubits from its unitary; a framework without
183
+ `get_unitary`), the same two scalable methods are tried next — a verdict
184
+ then names the weaker reference it rests on. Any `unfaithful` method decides
185
+ `unfaithful`; else any `faithful` method decides `faithful` and `reference`
186
+ names which; else `inconclusive`. Every method's result is in the report.
187
+ Never raises.
188
+ """
189
+ if source_format not in TRANSLATED_FRONTENDS:
190
+ return None
191
+ import numpy as np
192
+
193
+ cap, scap = max_qubits(), max_state_qubits()
194
+ n = int(getattr(converted, "num_qubits", 0) or 0)
195
+ report: dict[str, Any] = {
196
+ "schema": SCHEMA,
197
+ "frontend": source_format,
198
+ "method": None,
199
+ "methods": [],
200
+ "qubit_order": "framework qubit 0 is most significant; compared against the bit-reversed Qiskit operator or state",
201
+ "tolerance": TOLERANCE,
202
+ "max_qubits": cap,
203
+ "max_statevector_qubits": scap,
204
+ "qubits": n,
205
+ "reference": None,
206
+ "max_abs_deviation": None,
207
+ "native_export_format": None,
208
+ "native_export_sha256": None,
209
+ "verdict": "inconclusive",
210
+ "reason": None,
211
+ }
212
+ try:
213
+ adapter = _adapter(source_format)
214
+ if adapter.reference != "dense":
215
+ raise _Inconclusive(f"the {source_format} adapter declares no dense reference ({adapter.reference!r}); "
216
+ "no method is available to compare the translation")
217
+ text, fmt = adapter.native_export(native)
218
+ report["native_export_format"] = fmt
219
+ report["native_export_sha256"] = "sha256:" + hashlib.sha256(text.encode("utf-8")).hexdigest()
220
+
221
+ entries: list[dict[str, Any]] = []
222
+ if n <= cap:
223
+ entries.append(_dense(adapter, native, converted, np))
224
+ if n > cap or entries[-1]["verdict"] == "inconclusive":
225
+ if n <= scap:
226
+ entries.append(_statevector(adapter, native, converted, np))
227
+ else:
228
+ entries.append(_entry("statevector", "state-vector comparison", None, "inconclusive",
229
+ f"{n} qubits exceeds the state-vector cap of {scap} (QUANTBOM_STATEVECTOR_MAX_QUBITS)"))
230
+ entries.append(_decision_diagram(adapter, native, converted))
231
+ report["methods"] = entries
232
+ deviations = [e["max_abs_deviation"] for e in entries if e["max_abs_deviation"] is not None]
233
+ report["max_abs_deviation"] = max(deviations) if deviations else None
234
+ unfaithful = [e for e in entries if e["verdict"] == "unfaithful"]
235
+ faithful = [e for e in entries if e["verdict"] == "faithful"]
236
+ decisive = unfaithful or faithful
237
+ report["method"] = "+".join(e["name"] for e in decisive) if decisive else "none"
238
+ report["reference"] = "; ".join(f"{e['name']}: {e['reference']}" for e in decisive if e["reference"]) or None
239
+ if unfaithful:
240
+ report["verdict"] = "unfaithful"
241
+ report["reason"] = "; ".join(f"{e['name']}: {e['reason']}" for e in unfaithful)
242
+ elif faithful:
243
+ report["verdict"] = "faithful"
244
+ else:
245
+ report["reason"] = "; ".join(f"{e['name']}: {e['reason']}" for e in entries if e["reason"]) or "no method could decide"
246
+ except _Inconclusive as why:
247
+ report["reason"] = str(why)
248
+ except Exception as err: # never block collection, never affirm
249
+ report["reason"] = f"comparison failed: {type(err).__name__}: {err}"
250
+ return report
251
+
252
+
253
+ # ---- QNT-09: the native program on its own framework's simulator ----
254
+ DEFAULT_NATIVE_SHOTS = 8192
255
+
256
+
257
+ def native_shots() -> int:
258
+ return int(os.environ.get("QUANTBOM_NATIVE_SHOTS", str(DEFAULT_NATIVE_SHOTS)))
259
+
260
+
261
+ def sample_native(native: Any, converted: Any, source_format: str) -> dict[str, Any] | None:
262
+ """Run the native program on its framework's own simulator (QNT-09).
263
+
264
+ None for inputs that are the shared format itself. Otherwise the adapter's
265
+ native_run() result with "framework" and "shots", or {"reason": ...} when
266
+ the framework could not run it or the program is beyond the state-vector cap
267
+ (every framework simulator here is exponential in qubits). Never raises.
268
+ """
269
+ if source_format not in TRANSLATED_FRONTENDS:
270
+ return None
271
+ n = int(getattr(converted, "num_qubits", 0) or 0)
272
+ if n > max_state_qubits():
273
+ return {"reason": f"{n} qubits exceeds the state-vector cap of {max_state_qubits()} (QUANTBOM_STATEVECTOR_MAX_QUBITS)"}
274
+ try:
275
+ shots = native_shots()
276
+ run = _adapter(source_format).native_run(native, shots)
277
+ return {**run, "framework": source_format, "shots": shots}
278
+ except Exception as err: # Undecidable carries the adapter's own reason
279
+ why = str(err) if isinstance(err, _Inconclusive) else f"{type(err).__name__}: {err}"
280
+ return {"reason": why}