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,53 @@
|
|
|
1
|
+
"""benchmark — circuit metrics and a hash-verified report, Roadmap §8.5 (feeds QBOM-BENCH-01)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from .base import PRODUCED, ActionResult, Context
|
|
7
|
+
|
|
8
|
+
ID = "benchmark"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run(ctx: Context) -> ActionResult:
|
|
12
|
+
from .. import _collect as C
|
|
13
|
+
|
|
14
|
+
metrics = ctx.state["metrics"]
|
|
15
|
+
rr = ctx.state["runtime_result"]
|
|
16
|
+
counts = rr.get("counts") or {}
|
|
17
|
+
shots = rr.get("shots") or 0
|
|
18
|
+
report = {
|
|
19
|
+
"schema": "secqdevops.quantum-benchmark/v1",
|
|
20
|
+
"release_id": ctx.release_id,
|
|
21
|
+
"framing": {
|
|
22
|
+
"approach": "bottom-up structural characterization",
|
|
23
|
+
"reference": "CEN/CENELEC Standardization Roadmap on Quantum Technologies R1.1 §8.5",
|
|
24
|
+
"harmonisation_targets": ["IEEE P7131 (in development)", "IEC/ISO JTC 3 benchmarking (in progress)"],
|
|
25
|
+
},
|
|
26
|
+
"metrics": metrics,
|
|
27
|
+
"transpiled_metrics": C._circuit_metrics(ctx.state["transpiled"]),
|
|
28
|
+
"result_fidelity": {
|
|
29
|
+
"shots": shots,
|
|
30
|
+
"result_status": ctx.state["runtime_job"]["result_status"],
|
|
31
|
+
"distinct_outcomes": len(counts),
|
|
32
|
+
"top_outcome_probability": round(max(counts.values()) / shots, 6) if counts and shots else None,
|
|
33
|
+
"declared_expectation": ctx.state.get("fidelity_summary"),
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
raw = (json.dumps(report, indent=2, ensure_ascii=True) + "\n").encode("utf-8")
|
|
37
|
+
ctx.quantum_dir.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
(ctx.quantum_dir / "benchmark-report.json").write_bytes(raw)
|
|
39
|
+
benchmark_sha = C.sha256_bytes(raw)
|
|
40
|
+
ctx.state["benchmark_sha"] = benchmark_sha
|
|
41
|
+
|
|
42
|
+
props = [
|
|
43
|
+
{"name": "secqdevops:qubit-count", "value": str(metrics["qubits"])},
|
|
44
|
+
{"name": "secqdevops:circuit-depth", "value": str(metrics["depth"])},
|
|
45
|
+
{"name": "secqdevops:two-qubit-depth", "value": str(metrics["two_qubit_depth"])},
|
|
46
|
+
{"name": "secqdevops:t-depth", "value": str(metrics["t_depth"])},
|
|
47
|
+
{"name": "secqdevops:gate-count", "value": str(metrics["size"])},
|
|
48
|
+
{"name": "secqdevops:benchmark-ref", "value": "quantum/benchmark-report.json"},
|
|
49
|
+
{"name": "secqdevops:benchmark-sha256", "value": benchmark_sha},
|
|
50
|
+
]
|
|
51
|
+
if "cnot_depth" in metrics:
|
|
52
|
+
props.append({"name": "secqdevops:cnot-depth", "value": str(metrics["cnot_depth"])})
|
|
53
|
+
return ActionResult(ID, PRODUCED, props)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""bind_backend — the backend target and calibration snapshot, hash-verified (feeds QNT-03)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from .base import PRODUCED, ActionResult, Context
|
|
7
|
+
|
|
8
|
+
ID = "bind_backend"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run(ctx: Context) -> ActionResult:
|
|
12
|
+
from .. import _collect as C
|
|
13
|
+
|
|
14
|
+
rr = ctx.state["runtime_result"]
|
|
15
|
+
metrics = ctx.state["metrics"]
|
|
16
|
+
q = ctx.quantum_dir
|
|
17
|
+
q.mkdir(parents=True, exist_ok=True)
|
|
18
|
+
|
|
19
|
+
calibration_sha = None
|
|
20
|
+
calibration = ctx.state.get("calibration")
|
|
21
|
+
age = C.calibration_age_hours(calibration) if calibration is not None else None
|
|
22
|
+
if age is not None: # written INTO the snapshot so the declared age is hash-bound to it
|
|
23
|
+
calibration["calibration_age_hours"] = f"{age:.1f}"
|
|
24
|
+
if calibration is not None:
|
|
25
|
+
calibration_bytes = (json.dumps(calibration, indent=2, ensure_ascii=True) + "\n").encode("utf-8")
|
|
26
|
+
(q / "backend-calibration.json").write_bytes(calibration_bytes)
|
|
27
|
+
calibration_sha = C.sha256_bytes(calibration_bytes)
|
|
28
|
+
ctx.state["calibration_sha"] = calibration_sha
|
|
29
|
+
|
|
30
|
+
transpile_report = {
|
|
31
|
+
"target_backend": rr["backend_name"],
|
|
32
|
+
"basis_gates": C.BASIS_GATES,
|
|
33
|
+
"optimization_level": 2,
|
|
34
|
+
"seed_transpiler": C.SEED_TRANSPILER,
|
|
35
|
+
"execution_mode": ctx.state["execution_mode"],
|
|
36
|
+
"qiskit_version": ctx.qiskit_version,
|
|
37
|
+
"source_format": ctx.source_format,
|
|
38
|
+
"circuit_metrics": metrics,
|
|
39
|
+
"dynamic_features": ctx.circuit_info.get("dynamic_features", []),
|
|
40
|
+
}
|
|
41
|
+
backend_target = {
|
|
42
|
+
"provider": rr["provider"],
|
|
43
|
+
"backend_name": rr["backend_name"],
|
|
44
|
+
"num_qubits": metrics["qubits"],
|
|
45
|
+
"simulator": rr["simulator"],
|
|
46
|
+
# what the evidence may claim about where it ran; bound to
|
|
47
|
+
# secqdevops:execution-target by the gate (CRA-INTEG-01)
|
|
48
|
+
"execution_target": rr.get("execution_target", "simulator" if rr["simulator"] else "hardware"),
|
|
49
|
+
"noise": rr.get("noise"),
|
|
50
|
+
"native_gates": C.BASIS_GATES,
|
|
51
|
+
}
|
|
52
|
+
C._write_json(q / "transpile-report.json", transpile_report)
|
|
53
|
+
# The backend target carries the provenance claim — which device. Binding
|
|
54
|
+
# its digest is what lets QNT-03 affirm rather than report N-A.
|
|
55
|
+
backend_target_sha = C._write_json(q / "backend-target.json", backend_target)
|
|
56
|
+
|
|
57
|
+
props = [
|
|
58
|
+
{"name": "secqdevops:transpile-report-ref", "value": "quantum/transpile-report.json"},
|
|
59
|
+
{"name": "secqdevops:backend-provider", "value": rr["provider"]},
|
|
60
|
+
{"name": "secqdevops:backend-name", "value": rr["backend_name"]},
|
|
61
|
+
{"name": "secqdevops:execution-mode", "value": ctx.state["execution_mode"]},
|
|
62
|
+
{"name": "secqdevops:execution-target", "value": backend_target["execution_target"]},
|
|
63
|
+
{"name": "secqdevops:backend-target-ref", "value": "quantum/backend-target.json"},
|
|
64
|
+
{"name": "secqdevops:backend-target-sha256", "value": backend_target_sha},
|
|
65
|
+
*([{"name": "secqdevops:calibration-ref", "value": "quantum/backend-calibration.json"},
|
|
66
|
+
{"name": "secqdevops:calibration-sha256", "value": calibration_sha},
|
|
67
|
+
{"name": "secqdevops:calibration-captured-at", "value": calibration["captured_at"]}] if calibration_sha else []),
|
|
68
|
+
*([{"name": "secqdevops:calibration-age-hours", "value": f"{age:.1f}"}] if age is not None else []),
|
|
69
|
+
{"name": "secqdevops:backend-target-layer", "value": "hardware-abstraction-layer"},
|
|
70
|
+
]
|
|
71
|
+
return ActionResult(ID, PRODUCED, props)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""capture — source and compiled circuit (QPY, OpenQASM 3, QIR via the QDK) with byte digests.
|
|
2
|
+
|
|
3
|
+
Feeds QNT-02, QNT-05/06 (the circuits they compare) and CRA-INTEG-01.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from .base import PRODUCED, ActionResult, Context
|
|
8
|
+
|
|
9
|
+
ID = "capture"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run(ctx: Context) -> ActionResult:
|
|
13
|
+
from qiskit import transpile
|
|
14
|
+
|
|
15
|
+
from .. import _collect as C
|
|
16
|
+
|
|
17
|
+
qc = ctx.qiskit_circuit
|
|
18
|
+
metrics = C._circuit_metrics(qc, ctx.qrisp_circuit)
|
|
19
|
+
transpiled = transpile(qc, basis_gates=C.BASIS_GATES, optimization_level=2, seed_transpiler=C.SEED_TRANSPILER)
|
|
20
|
+
circuit_qpy = C._qpy_bytes(qc)
|
|
21
|
+
transpiled_qpy = C._qpy_bytes(transpiled)
|
|
22
|
+
try: # OpenQASM3 export from the normalized IR (works for any frontend)
|
|
23
|
+
from qiskit import qasm3
|
|
24
|
+
|
|
25
|
+
qasm_text = qasm3.dumps(qc)
|
|
26
|
+
except Exception:
|
|
27
|
+
qasm_text = ""
|
|
28
|
+
|
|
29
|
+
q = ctx.quantum_dir
|
|
30
|
+
q.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
(q / "circuit.qpy").write_bytes(circuit_qpy)
|
|
32
|
+
(q / "transpiled.qpy").write_bytes(transpiled_qpy)
|
|
33
|
+
# write_bytes (not write_text) so the hash-verified bytes are platform-independent
|
|
34
|
+
qasm_bytes = qasm_text.encode("utf-8")
|
|
35
|
+
(q / "program.qasm").write_bytes(qasm_bytes)
|
|
36
|
+
qasm_sha = C.sha256_bytes(qasm_bytes)
|
|
37
|
+
|
|
38
|
+
# QIR (K5): the QDK compiles the same OpenQASM 3 text that is hash-bound as
|
|
39
|
+
# program.qasm — Base profile for a static circuit, Adaptive_RI for a dynamic
|
|
40
|
+
# one. It is a derived format from a second compiler, so check_qir compares
|
|
41
|
+
# it with the circuit and the measured counts before QNT-07 can affirm it.
|
|
42
|
+
# Without the toolchain QIR is honestly absent and the harness log says why.
|
|
43
|
+
qir_text, qir_profile, qir_reason = _qir_export(qasm_text, bool(ctx.circuit_info.get("dynamic")),
|
|
44
|
+
measured=any(i.operation.name == "measure" for i in qc.data))
|
|
45
|
+
qir_sha = None
|
|
46
|
+
if qir_text:
|
|
47
|
+
qir_bytes = qir_text.encode("utf-8")
|
|
48
|
+
(q / "program.ll").write_bytes(qir_bytes)
|
|
49
|
+
qir_sha = C.sha256_bytes(qir_bytes)
|
|
50
|
+
|
|
51
|
+
transpile_profile = {
|
|
52
|
+
"basis_gates": C.BASIS_GATES,
|
|
53
|
+
"optimization_level": 2,
|
|
54
|
+
"seed_transpiler": C.SEED_TRANSPILER,
|
|
55
|
+
"qiskit_version": ctx.qiskit_version,
|
|
56
|
+
}
|
|
57
|
+
circuit_sha = C.sha256_bytes(circuit_qpy)
|
|
58
|
+
transpiled_sha = C.sha256_bytes(transpiled_qpy)
|
|
59
|
+
ctx.state.update(metrics=metrics, transpiled=transpiled, circuit_qpy=circuit_qpy, transpiled_qpy=transpiled_qpy,
|
|
60
|
+
circuit_sha=circuit_sha, transpiled_sha=transpiled_sha, qasm_sha=qasm_sha, qir_sha=qir_sha,
|
|
61
|
+
qir_text=qir_text or None, qir_profile=qir_profile, qir_reason=qir_reason)
|
|
62
|
+
|
|
63
|
+
props = [
|
|
64
|
+
{"name": "secqdevops:circuit-format", "value": "qpy"},
|
|
65
|
+
{"name": "secqdevops:circuit-artifact-ref", "value": "quantum/circuit.qpy"},
|
|
66
|
+
{"name": "secqdevops:circuit-sha256", "value": circuit_sha},
|
|
67
|
+
{"name": "secqdevops:transpiled-circuit-ref", "value": "quantum/transpiled.qpy"},
|
|
68
|
+
{"name": "secqdevops:transpiled-circuit-sha256", "value": transpiled_sha},
|
|
69
|
+
{"name": "secqdevops:qasm-program-ref", "value": "quantum/program.qasm"},
|
|
70
|
+
{"name": "secqdevops:qasm-program-sha256", "value": qasm_sha},
|
|
71
|
+
*([{"name": "secqdevops:qir-program-ref", "value": "quantum/program.ll"},
|
|
72
|
+
{"name": "secqdevops:qir-program-sha256", "value": qir_sha},
|
|
73
|
+
{"name": "secqdevops:qir-profile", "value": qir_profile}] if qir_sha else []),
|
|
74
|
+
{"name": "secqdevops:transpile-profile-hash",
|
|
75
|
+
"value": C.sha256_bytes(C.canonical_json(transpile_profile).encode("utf-8"))},
|
|
76
|
+
{"name": "secqdevops:circuit-layer", "value": "assembly-ir"},
|
|
77
|
+
# dynamic circuits are accepted (K0); recorded because they change how
|
|
78
|
+
# the circuit is executed (qiskit-aer) and proved equivalent (QCEC)
|
|
79
|
+
{"name": "secqdevops:circuit-dynamic", "value": "true" if ctx.circuit_info.get("dynamic") else "false"},
|
|
80
|
+
{"name": "secqdevops:transpiled-circuit-layer", "value": "programming"},
|
|
81
|
+
]
|
|
82
|
+
ir_standards = [n for n, present in (("openqasm3", bool(qasm_text)), ("qir", bool(qir_sha))) if present]
|
|
83
|
+
if ir_standards: # standards-named IRs exported alongside the vendor IR (Roadmap §8.3 / QIR Alliance)
|
|
84
|
+
props.append({"name": "secqdevops:ir-standard", "value": ",".join(ir_standards)})
|
|
85
|
+
return ActionResult(ID, PRODUCED, props, reason=qir_reason)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _qir_export(qasm_text: str, dynamic: bool, measured: bool = True) -> tuple[str, str | None, str | None]:
|
|
89
|
+
"""(QIR text, profile, reason it is absent) — the QDK route (_qir.py)."""
|
|
90
|
+
from .. import _qir
|
|
91
|
+
|
|
92
|
+
if not qasm_text:
|
|
93
|
+
return "", None, "QIR not emitted: the OpenQASM 3 export is empty"
|
|
94
|
+
if not measured:
|
|
95
|
+
return "", None, ("QIR not emitted: the source circuit declares no measurements, and QIR records only declared "
|
|
96
|
+
"outputs (the run measured every qubit, but the hash-bound program is the circuit as given); "
|
|
97
|
+
"declare measurements to get QIR and its consistency check")
|
|
98
|
+
available, why = _qir.toolchain()
|
|
99
|
+
if not available:
|
|
100
|
+
return "", None, why
|
|
101
|
+
try:
|
|
102
|
+
text, profile = _qir.compile_qir(qasm_text, dynamic)
|
|
103
|
+
except Exception as err: # recorded, never hidden; the release is still collected without QIR
|
|
104
|
+
first = str(err).strip().splitlines()[0] if str(err).strip() else ""
|
|
105
|
+
return "", None, f"QIR not emitted: the QDK could not compile the OpenQASM 3 export: {type(err).__name__}: {first[:160]}"
|
|
106
|
+
return text, profile, None
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""check_qir — the QIR derived from the source program behaves like the program (feeds QNT-07).
|
|
2
|
+
|
|
3
|
+
QIR is produced by a second compiler (the QDK), so it is checked, not trusted:
|
|
4
|
+
qubit and result counts against the circuit, qirrunner's output distribution
|
|
5
|
+
against the counts the release actually measured, the output order pinned by
|
|
6
|
+
a probe. The gate fails the release on `inconsistent` (K5 decision).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
from .base import NOT_APPLICABLE, PRODUCED, UNSUPPORTED, ActionResult, Context
|
|
13
|
+
|
|
14
|
+
ID = "check_qir"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run(ctx: Context) -> ActionResult:
|
|
18
|
+
qir_text = ctx.state.get("qir_text")
|
|
19
|
+
if not qir_text:
|
|
20
|
+
return ActionResult(ID, NOT_APPLICABLE, reason=ctx.state.get("qir_reason") or "no QIR was emitted for this release")
|
|
21
|
+
if ctx.state.get("run_status", "completed") != "completed":
|
|
22
|
+
rr = ctx.state.get("runtime_result") or {}
|
|
23
|
+
return ActionResult(ID, NOT_APPLICABLE,
|
|
24
|
+
reason=f"no measured results to compare the QIR with — {rr.get('reason') or 'the run did not complete'}")
|
|
25
|
+
from .. import _collect as C
|
|
26
|
+
from .. import _qir
|
|
27
|
+
|
|
28
|
+
rr = ctx.state["runtime_result"]
|
|
29
|
+
try:
|
|
30
|
+
report = _qir.assess(qir_text, ctx.qiskit_circuit, rr.get("counts") or {}, int(rr.get("shots") or 0))
|
|
31
|
+
except Exception as err: # the module never raises for undecidable cases; this is a toolchain failure
|
|
32
|
+
return ActionResult(ID, UNSUPPORTED, reason=f"QIR consistency check could not run: {type(err).__name__}: {str(err)[:200]}")
|
|
33
|
+
report["release_id"] = ctx.release_id
|
|
34
|
+
raw = (json.dumps(report, indent=2, ensure_ascii=True, sort_keys=True) + "\n").encode("utf-8")
|
|
35
|
+
ctx.quantum_dir.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
(ctx.quantum_dir / "qir-consistency.json").write_bytes(raw)
|
|
37
|
+
ctx.state["qir_verdict"] = report["verdict"]
|
|
38
|
+
props = [
|
|
39
|
+
{"name": "secqdevops:qir-consistency-ref", "value": "quantum/qir-consistency.json"},
|
|
40
|
+
{"name": "secqdevops:qir-consistency-sha256", "value": C.sha256_bytes(raw)},
|
|
41
|
+
{"name": "secqdevops:qir-consistency-verdict", "value": report["verdict"]},
|
|
42
|
+
]
|
|
43
|
+
reason = report.get("reason") if report["verdict"] == "inconclusive" else None
|
|
44
|
+
return ActionResult(ID, PRODUCED, props, reason=reason)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""check_results — measured outcomes vs the declared expectation (feeds QBOM-BENCH-02).
|
|
2
|
+
|
|
3
|
+
The SDK has already translated the declaration into measured (Qiskit) order and
|
|
4
|
+
validated its width (`_bitorder`); this action records it, measures the
|
|
5
|
+
deviation (total-variation distance) with its sampling error (`_stats`, one
|
|
6
|
+
definition shared with the gate) and leaves the verdict to the gate. A
|
|
7
|
+
comparison the shots cannot decide at the declared tolerance is `undecided`
|
|
8
|
+
— recorded as such, never rounded to within or exceeded (Q1.1).
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
|
|
14
|
+
from .base import NOT_APPLICABLE, NOT_REQUESTED, PRODUCED, ActionResult, Context
|
|
15
|
+
|
|
16
|
+
ID = "check_results"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def run(ctx: Context) -> ActionResult:
|
|
20
|
+
ctx.state["fidelity_summary"] = None
|
|
21
|
+
if not ctx.expected_distribution:
|
|
22
|
+
return ActionResult(ID, NOT_REQUESTED, reason="no expected results declared")
|
|
23
|
+
from .. import _collect as C
|
|
24
|
+
|
|
25
|
+
rr = ctx.state["runtime_result"]
|
|
26
|
+
if ctx.state.get("run_status", "completed") != "completed":
|
|
27
|
+
# nothing was measured: comparing an expectation with no results would
|
|
28
|
+
# blame the circuit for a run that never happened
|
|
29
|
+
return ActionResult(ID, NOT_APPLICABLE,
|
|
30
|
+
reason=f"no measured results to compare — {rr.get('reason') or 'the run did not complete'}")
|
|
31
|
+
from .._stats import CONFIDENCE, assess
|
|
32
|
+
|
|
33
|
+
declared = {str(k).replace(" ", ""): float(v) for k, v in ctx.expected_distribution.items()}
|
|
34
|
+
tolerance = float(ctx.expected_tolerance)
|
|
35
|
+
stats = assess(rr.get("counts") or {}, int(rr.get("shots") or 0), declared, tolerance, CONFIDENCE)
|
|
36
|
+
tvd, verdict = stats["tvd"], stats["verdict"]
|
|
37
|
+
doc = {
|
|
38
|
+
"schema": "secqdevops.expected-distribution/v1",
|
|
39
|
+
"release_id": ctx.release_id,
|
|
40
|
+
"metric": "total_variation_distance",
|
|
41
|
+
"tolerance": tolerance,
|
|
42
|
+
"confidence": CONFIDENCE,
|
|
43
|
+
"distribution": declared,
|
|
44
|
+
}
|
|
45
|
+
decl = ctx.expected_declaration
|
|
46
|
+
if decl:
|
|
47
|
+
# `distribution` is what was compared, in measured order. Record the
|
|
48
|
+
# declaration as the producer wrote it and the order it was read in,
|
|
49
|
+
# so the translation itself is hash-bound and auditable.
|
|
50
|
+
doc["distribution_order"] = decl.get("measured_order")
|
|
51
|
+
doc["declared_order"] = decl.get("declared_order")
|
|
52
|
+
doc["width"] = decl.get("width")
|
|
53
|
+
if decl.get("translated"):
|
|
54
|
+
doc["declared_distribution"] = decl.get("declared_distribution")
|
|
55
|
+
exp_bytes = (json.dumps(doc, indent=2, ensure_ascii=True, sort_keys=True) + "\n").encode("utf-8")
|
|
56
|
+
ctx.quantum_dir.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
(ctx.quantum_dir / "expected-distribution.json").write_bytes(exp_bytes)
|
|
58
|
+
ctx.state["fidelity_summary"] = {**stats, "tolerance": tolerance}
|
|
59
|
+
|
|
60
|
+
props = [
|
|
61
|
+
{"name": "secqdevops:expected-distribution-ref", "value": "quantum/expected-distribution.json"},
|
|
62
|
+
{"name": "secqdevops:expected-distribution-sha256", "value": C.sha256_bytes(exp_bytes)},
|
|
63
|
+
{"name": "secqdevops:result-tvd", "value": f"{tvd:.6f}"},
|
|
64
|
+
{"name": "secqdevops:result-tolerance", "value": f"{tolerance:.6f}"},
|
|
65
|
+
{"name": "secqdevops:result-fidelity-verdict", "value": verdict},
|
|
66
|
+
{"name": "secqdevops:result-confidence", "value": f"{CONFIDENCE:.2f}"},
|
|
67
|
+
{"name": "secqdevops:result-sampling-error", "value": f"{stats['sampling_error']:.6f}"},
|
|
68
|
+
{"name": "secqdevops:result-shots-required", "value": str(stats["required_shots"])},
|
|
69
|
+
]
|
|
70
|
+
if verdict == "undecided":
|
|
71
|
+
reason = (f"undecided: {stats['shots']} shots give a sampling error of {stats['sampling_error']:.4f} at "
|
|
72
|
+
f"{CONFIDENCE:.0%} confidence, which cannot decide tolerance {tolerance} for a TVD of {tvd:.4f}; "
|
|
73
|
+
f"{stats['required_shots']} shots would")
|
|
74
|
+
return ActionResult(ID, PRODUCED, props, reason=reason)
|
|
75
|
+
return ActionResult(ID, PRODUCED, props)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""compare_native — the program on its own framework's simulator agrees with the shared-format run (feeds QNT-09).
|
|
2
|
+
|
|
3
|
+
Every framework is lowered to one Qiskit circuit and the evidence run executes
|
|
4
|
+
that. The SDK also samples the NATIVE program on the framework's own simulator
|
|
5
|
+
(`FrameworkAdapter.native_run`, where the native object is still in hand); this
|
|
6
|
+
action compares the two samples by total-variation distance with their joint
|
|
7
|
+
sampling error (`_stats.compare_samples`, which the gate re-derives) and writes
|
|
8
|
+
`quantum/native-result.json`, hash-bound: both readings of the native run (the
|
|
9
|
+
framework's own, and keyed as the measured string), Qrisp's per-variable view,
|
|
10
|
+
and the comparison.
|
|
11
|
+
|
|
12
|
+
Unlike QNT-06's unitary comparison it reaches dynamic programs (mid-circuit
|
|
13
|
+
measurement, feed-forward), which have no unitary; unlike it, it is statistical:
|
|
14
|
+
it catches a translation that changes outcome probabilities, not a phase.
|
|
15
|
+
|
|
16
|
+
Not applicable when there is nothing native to compare (Qiskit and file inputs
|
|
17
|
+
are the shared format), when the framework could not run the program, when the
|
|
18
|
+
shared-format run did not complete, or when it ran on a noisy device or hardware:
|
|
19
|
+
the framework's simulator is ideal, so a disagreement would measure noise.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
|
|
26
|
+
from .base import NOT_APPLICABLE, PRODUCED, ActionResult, Context
|
|
27
|
+
|
|
28
|
+
ID = "compare_native"
|
|
29
|
+
SCHEMA = "secqdevops.native-result/v1"
|
|
30
|
+
IDEAL_BACKENDS = ("basic_simulator", "aer_simulator")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def tolerance() -> float:
|
|
34
|
+
return float(os.environ.get("QUANTBOM_NATIVE_TOLERANCE", "0.15"))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run(ctx: Context) -> ActionResult:
|
|
38
|
+
native = ctx.native_result
|
|
39
|
+
if native is None:
|
|
40
|
+
return ActionResult(ID, NOT_APPLICABLE, reason=f"{ctx.source_format} input is the shared format itself: "
|
|
41
|
+
"there is no native program to run")
|
|
42
|
+
if native.get("reason"):
|
|
43
|
+
return ActionResult(ID, NOT_APPLICABLE, reason=f"the {ctx.source_format} simulator could not run the program: "
|
|
44
|
+
f"{native['reason']}")
|
|
45
|
+
if ctx.state.get("run_status", "completed") != "completed":
|
|
46
|
+
return ActionResult(ID, NOT_APPLICABLE, reason="the shared-format run did not complete, so there is nothing to compare")
|
|
47
|
+
shared_run = ctx.state.get("runtime_result") or {}
|
|
48
|
+
backend = str(shared_run.get("backend_name") or "")
|
|
49
|
+
if ctx.backend_choice != "basic" or backend not in IDEAL_BACKENDS:
|
|
50
|
+
return ActionResult(ID, NOT_APPLICABLE, reason=f"the shared-format run used {backend or ctx.backend_choice}, which is not "
|
|
51
|
+
"an ideal simulator: compared with the framework's ideal one, a "
|
|
52
|
+
"difference would measure noise, not the translation")
|
|
53
|
+
from .. import _collect as C
|
|
54
|
+
from .._stats import CONFIDENCE, compare_samples
|
|
55
|
+
|
|
56
|
+
shared = {str(k).replace(" ", ""): int(v) for k, v in (shared_run.get("counts") or {}).items()}
|
|
57
|
+
stats = compare_samples(shared, int(shared_run.get("shots") or 0), native["measured"], int(native["shots"]),
|
|
58
|
+
tolerance(), CONFIDENCE)
|
|
59
|
+
document = {
|
|
60
|
+
"schema": SCHEMA,
|
|
61
|
+
"release_id": ctx.release_id,
|
|
62
|
+
"framework": native["framework"],
|
|
63
|
+
"simulator": native["simulator"],
|
|
64
|
+
"shots": int(native["shots"]),
|
|
65
|
+
"reading_order": native["reading_order"],
|
|
66
|
+
"reading": native["reading"],
|
|
67
|
+
"measured": native["measured"],
|
|
68
|
+
**({"by_variable": native["by_variable"]} if native.get("by_variable") else {}),
|
|
69
|
+
"shared_backend": backend,
|
|
70
|
+
"comparison": stats,
|
|
71
|
+
"tvd": stats["tvd"],
|
|
72
|
+
"verdict": stats["verdict"],
|
|
73
|
+
}
|
|
74
|
+
raw = (json.dumps(document, indent=2, ensure_ascii=True) + "\n").encode("utf-8")
|
|
75
|
+
ctx.quantum_dir.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
(ctx.quantum_dir / "native-result.json").write_bytes(raw)
|
|
77
|
+
ctx.state["native_verdict"] = stats["verdict"]
|
|
78
|
+
props = [
|
|
79
|
+
{"name": "secqdevops:native-result-ref", "value": "quantum/native-result.json"},
|
|
80
|
+
{"name": "secqdevops:native-result-sha256", "value": C.sha256_bytes(raw)},
|
|
81
|
+
{"name": "secqdevops:native-result-verdict", "value": stats["verdict"]},
|
|
82
|
+
{"name": "secqdevops:native-result-tvd", "value": f"{stats['tvd']:.6f}"},
|
|
83
|
+
]
|
|
84
|
+
why = None
|
|
85
|
+
if stats["verdict"] == "undecided":
|
|
86
|
+
why = (f"the samples cannot decide agreement at tolerance {stats['tolerance']}: TVD {stats['tvd']} with joint "
|
|
87
|
+
f"sampling error {stats['sampling_error']} over {stats['classes']} outcome classes — more shots would decide")
|
|
88
|
+
return ActionResult(ID, PRODUCED, props, reason=why)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""disclose_error_handling — what the backend did about errors (feeds QNT-08).
|
|
2
|
+
|
|
3
|
+
Error mitigation (readout correction, ZNE, PEC) and error suppression (dynamical
|
|
4
|
+
decoupling, twirling) change results after the fact, often by provider defaults
|
|
5
|
+
the developer did not choose, and a mitigated distribution can look right for
|
|
6
|
+
the wrong reason. This action records what the run's backend reports about
|
|
7
|
+
them — as a hash-bound document and a bound status — so a certificate reader
|
|
8
|
+
knows whether the measured counts are raw, corrected, or of unknown treatment.
|
|
9
|
+
|
|
10
|
+
mitigation_status none the backend reports raw counts with no mitigation
|
|
11
|
+
(every local simulator; IBM SamplerV2)
|
|
12
|
+
applied the backend reports mitigation; the methods are listed
|
|
13
|
+
unknown the backend does not report what it did — the gate
|
|
14
|
+
reports QNT-08 as N-A with that reason. Absence of
|
|
15
|
+
the record is the record.
|
|
16
|
+
|
|
17
|
+
Q1.2 of QUANTUM_CHALLENGES_PLAN.md. Limit stated: the disclosure is what the
|
|
18
|
+
provider *reports*; mitigation a provider applies silently is not visible here.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
|
|
24
|
+
from .base import PRODUCED, ActionResult, Context
|
|
25
|
+
|
|
26
|
+
ID = "disclose_error_handling"
|
|
27
|
+
STATUSES = ("none", "applied", "unknown")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def run(ctx: Context) -> ActionResult:
|
|
31
|
+
from .. import _collect as C
|
|
32
|
+
|
|
33
|
+
rr = ctx.state["runtime_result"]
|
|
34
|
+
eh = dict(rr.get("error_handling") or {})
|
|
35
|
+
status = eh.get("mitigation_status") or "unknown"
|
|
36
|
+
if status not in STATUSES:
|
|
37
|
+
status = "unknown"
|
|
38
|
+
if status == "applied" and not eh.get("mitigation"):
|
|
39
|
+
# a backend that says "mitigated" without saying how has not disclosed anything
|
|
40
|
+
status, eh["note"] = "unknown", "backend reported mitigation without naming a method"
|
|
41
|
+
doc = {
|
|
42
|
+
"schema": "secqdevops.error-handling/v1",
|
|
43
|
+
"release_id": ctx.release_id,
|
|
44
|
+
"backend": rr.get("backend_name"),
|
|
45
|
+
"provider": rr.get("provider"),
|
|
46
|
+
"execution_target": rr.get("execution_target", "simulator" if rr.get("simulator") else "hardware"),
|
|
47
|
+
"declared": status != "unknown",
|
|
48
|
+
"source": eh.get("source", "backend reports nothing about error handling"),
|
|
49
|
+
"mitigation_status": status,
|
|
50
|
+
"mitigation": eh.get("mitigation", []),
|
|
51
|
+
"suppression": eh.get("suppression", []),
|
|
52
|
+
"raw_counts": eh.get("raw_counts"),
|
|
53
|
+
"noise": rr.get("noise"),
|
|
54
|
+
**({"note": eh["note"]} if eh.get("note") else {}),
|
|
55
|
+
}
|
|
56
|
+
ctx.quantum_dir.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
sha = C._write_json(ctx.quantum_dir / "error-handling.json", doc)
|
|
58
|
+
ctx.state["error_handling"] = doc
|
|
59
|
+
props = [
|
|
60
|
+
{"name": "secqdevops:error-handling-ref", "value": "quantum/error-handling.json"},
|
|
61
|
+
{"name": "secqdevops:error-handling-sha256", "value": sha},
|
|
62
|
+
{"name": "secqdevops:error-mitigation", "value": status},
|
|
63
|
+
]
|
|
64
|
+
if status == "unknown":
|
|
65
|
+
return ActionResult(ID, PRODUCED, props, reason=f"the backend does not report its error handling ({doc['source']})")
|
|
66
|
+
return ActionResult(ID, PRODUCED, props)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""execute — run the circuit and bind the job receipt (feeds QNT-04).
|
|
2
|
+
|
|
3
|
+
The receipt says what actually happened. A run that could not happen (a
|
|
4
|
+
dynamic circuit without qiskit-aer, a failed simulation) is recorded with its
|
|
5
|
+
status and reason, and the action reports `unsupported` — never `produced`
|
|
6
|
+
with empty results that a later check would judge as if they were measured.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
from .base import PRODUCED, UNSUPPORTED, ActionResult, Context
|
|
13
|
+
|
|
14
|
+
ID = "execute"
|
|
15
|
+
DEFAULT_SHOTS = 1024
|
|
16
|
+
MAX_SHOTS = 65536
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def shots_for(ctx: Context) -> tuple[int, str]:
|
|
20
|
+
"""How many shots, and why: enough to decide the declared tolerance (Q1.1),
|
|
21
|
+
the default otherwise; QUANTBOM_SHOTS overrides (cost control, recorded)."""
|
|
22
|
+
override = os.environ.get("QUANTBOM_SHOTS")
|
|
23
|
+
if override:
|
|
24
|
+
return max(1, int(override)), "QUANTBOM_SHOTS override"
|
|
25
|
+
if ctx.expected_distribution:
|
|
26
|
+
from .._stats import CONFIDENCE, outcome_classes, required_shots
|
|
27
|
+
|
|
28
|
+
need = required_shots(outcome_classes(ctx.expected_distribution), float(ctx.expected_tolerance), CONFIDENCE)
|
|
29
|
+
shots = min(max(DEFAULT_SHOTS, need), MAX_SHOTS)
|
|
30
|
+
return shots, (f"sized for tolerance {ctx.expected_tolerance} at {CONFIDENCE:.0%} confidence "
|
|
31
|
+
f"({need} required{'; capped' if need > MAX_SHOTS else ''})")
|
|
32
|
+
return DEFAULT_SHOTS, "default"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run(ctx: Context) -> ActionResult:
|
|
36
|
+
from .. import _collect as C
|
|
37
|
+
|
|
38
|
+
shots, shots_policy = shots_for(ctx)
|
|
39
|
+
runtime_result = C._run_backend(ctx.qiskit_circuit, ctx.backend_choice, shots=shots,
|
|
40
|
+
dynamic=bool(ctx.circuit_info.get("dynamic")))
|
|
41
|
+
runtime_result["shots_policy"] = shots_policy
|
|
42
|
+
status = runtime_result.get("status", "completed")
|
|
43
|
+
calibration = runtime_result.pop("calibration", None) # its own artifact (bind_backend)
|
|
44
|
+
job_id = runtime_result.get("job_id") or f"job-{ctx.release_id}-001"
|
|
45
|
+
session_id = runtime_result.get("session_id")
|
|
46
|
+
execution_mode = "sampler/session" if session_id else "sampler"
|
|
47
|
+
runtime_job = {
|
|
48
|
+
"runtime_job_id": job_id,
|
|
49
|
+
"job_status": status,
|
|
50
|
+
"shots": runtime_result["shots"],
|
|
51
|
+
"result_status": "success" if status == "completed" else status,
|
|
52
|
+
**({"reason": runtime_result["reason"]} if runtime_result.get("reason") else {}),
|
|
53
|
+
"execution_mode": execution_mode,
|
|
54
|
+
"backend": runtime_result["backend_name"],
|
|
55
|
+
"session_id": session_id,
|
|
56
|
+
}
|
|
57
|
+
q = ctx.quantum_dir
|
|
58
|
+
q.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
# The job receipt carries the provenance claim — which job. Binding its
|
|
60
|
+
# digest is what lets QNT-04 affirm rather than report N-A.
|
|
61
|
+
runtime_job_sha = C._write_json(q / "runtime-job.json", runtime_job)
|
|
62
|
+
# The measured counts are what the result-fidelity score is computed from;
|
|
63
|
+
# binding their digest lets the gate re-derive that score (CRA-INTEG-01)
|
|
64
|
+
# and QBOM-BENCH-02 affirm rather than report N-A.
|
|
65
|
+
summary_sha = C._write_json(q / "result-summary.json", {"release_id": ctx.release_id, **runtime_result})
|
|
66
|
+
ctx.state.update(runtime_result=runtime_result, calibration=calibration, runtime_job=runtime_job,
|
|
67
|
+
execution_mode=execution_mode, session_id=session_id, run_status=status)
|
|
68
|
+
|
|
69
|
+
props = [
|
|
70
|
+
{"name": "secqdevops:runtime-job-id", "value": runtime_job["runtime_job_id"]},
|
|
71
|
+
{"name": "secqdevops:runtime-job-ref", "value": "quantum/runtime-job.json"},
|
|
72
|
+
{"name": "secqdevops:runtime-job-sha256", "value": runtime_job_sha},
|
|
73
|
+
{"name": "secqdevops:runtime-job-status", "value": status},
|
|
74
|
+
{"name": "secqdevops:result-summary-ref", "value": "quantum/result-summary.json"},
|
|
75
|
+
{"name": "secqdevops:result-summary-sha256", "value": summary_sha},
|
|
76
|
+
{"name": "secqdevops:runtime-job-layer", "value": "service"},
|
|
77
|
+
]
|
|
78
|
+
if session_id:
|
|
79
|
+
props.append({"name": "secqdevops:runtime-session-id", "value": str(session_id)})
|
|
80
|
+
if runtime_result.get("execution_target") == "hardware" and runtime_result.get("job_id"):
|
|
81
|
+
# the provider's own job id: what a hardware claim must carry (QNT-04)
|
|
82
|
+
props.append({"name": "secqdevops:provider-job-id", "value": str(runtime_result["job_id"])})
|
|
83
|
+
if status != "completed":
|
|
84
|
+
return ActionResult(ID, UNSUPPORTED, props, reason=runtime_result.get("reason") or f"run {status}")
|
|
85
|
+
return ActionResult(ID, PRODUCED, props)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""inventory — the quantum stack, the normalized IR and the standards anchors (feeds QNT-01).
|
|
2
|
+
|
|
3
|
+
The framework's SBOM component itself comes from its adapter, placed by the
|
|
4
|
+
SBOM builder (`_collect._build_sbom`).
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from .base import PRODUCED, ActionResult, Context
|
|
9
|
+
|
|
10
|
+
ID = "inventory"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run(ctx: Context) -> ActionResult:
|
|
14
|
+
props = [
|
|
15
|
+
{"name": "secqdevops:quantum-stack", "value": ctx.frontend_label},
|
|
16
|
+
{"name": "secqdevops:normalized-ir", "value": "qiskit.quantumcircuit"},
|
|
17
|
+
{"name": "secqdevops:source-format", "value": ctx.source_format},
|
|
18
|
+
# CEN/CLC TR 18202 layer model + standards anchors (RECOMMENDED, forward-compatible)
|
|
19
|
+
{"name": "secqdevops:layer-model-ref", "value": "CEN/CLC/TR 18202:2025"},
|
|
20
|
+
{"name": "secqdevops:vocabulary-ref", "value": "ISO/IEC 4879:2024"},
|
|
21
|
+
{"name": "secqdevops:cyclonedx-edition", "value": "ECMA-424:2025"}, # 2nd ed. = CycloneDX 1.7
|
|
22
|
+
{"name": "secqdevops:qbom-profile-version", "value": "0.3.0"},
|
|
23
|
+
]
|
|
24
|
+
return ActionResult(ID, PRODUCED, props)
|