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
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}
|