quantbom 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. quantbom/__init__.py +497 -0
  2. quantbom/_bitorder.py +121 -0
  3. quantbom/_circuit.py +77 -0
  4. quantbom/_collect.py +661 -0
  5. quantbom/_drift.py +333 -0
  6. quantbom/_fidelity.py +280 -0
  7. quantbom/_merge.py +187 -0
  8. quantbom/_qir.py +224 -0
  9. quantbom/_scan.py +118 -0
  10. quantbom/_sign.py +284 -0
  11. quantbom/_stats.py +125 -0
  12. quantbom/_verify.py +530 -0
  13. quantbom/actions/__init__.py +27 -0
  14. quantbom/actions/base.py +68 -0
  15. quantbom/actions/benchmark.py +53 -0
  16. quantbom/actions/bind_backend.py +71 -0
  17. quantbom/actions/capture.py +106 -0
  18. quantbom/actions/check_qir.py +44 -0
  19. quantbom/actions/check_results.py +75 -0
  20. quantbom/actions/compare_native.py +88 -0
  21. quantbom/actions/disclose_error_handling.py +66 -0
  22. quantbom/actions/execute.py +85 -0
  23. quantbom/actions/inventory.py +24 -0
  24. quantbom/actions/layout.py +118 -0
  25. quantbom/actions/prove_equivalence.py +225 -0
  26. quantbom/actions/record_provenance.py +49 -0
  27. quantbom/actions/verify_conversion.py +53 -0
  28. quantbom/attest.py +363 -0
  29. quantbom/cli.py +378 -0
  30. quantbom/frameworks/__init__.py +67 -0
  31. quantbom/frameworks/base.py +155 -0
  32. quantbom/frameworks/braket.py +132 -0
  33. quantbom/frameworks/cirq.py +106 -0
  34. quantbom/frameworks/cudaq.py +223 -0
  35. quantbom/frameworks/pennylane.py +116 -0
  36. quantbom/frameworks/qiskit.py +67 -0
  37. quantbom/frameworks/qrisp.py +267 -0
  38. quantbom/harness.json +1730 -0
  39. quantbom/harness.py +88 -0
  40. quantbom-0.6.0.dist-info/METADATA +207 -0
  41. quantbom-0.6.0.dist-info/RECORD +45 -0
  42. quantbom-0.6.0.dist-info/WHEEL +5 -0
  43. quantbom-0.6.0.dist-info/entry_points.txt +2 -0
  44. quantbom-0.6.0.dist-info/licenses/LICENSE +202 -0
  45. quantbom-0.6.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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)