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,118 @@
|
|
|
1
|
+
"""The QBOM property layout — the order properties appear in `metadata.properties`.
|
|
2
|
+
|
|
3
|
+
Actions emit properties in whatever order suits them; the collector places them
|
|
4
|
+
by this table, so the SBOM is byte-stable and adding a property is an explicit
|
|
5
|
+
decision. A property missing from this table is an error, not an afterthought.
|
|
6
|
+
"""
|
|
7
|
+
PROPERTY_ORDER = (
|
|
8
|
+
"secqdevops:provenance-ref",
|
|
9
|
+
"secqdevops:signature-ref",
|
|
10
|
+
"secqdevops:provenance-artifact-ref",
|
|
11
|
+
"secqdevops:signature-artifact-ref",
|
|
12
|
+
"secqdevops:quantum-stack",
|
|
13
|
+
"secqdevops:normalized-ir",
|
|
14
|
+
"secqdevops:source-format",
|
|
15
|
+
"secqdevops:circuit-format",
|
|
16
|
+
"secqdevops:circuit-artifact-ref",
|
|
17
|
+
"secqdevops:circuit-sha256",
|
|
18
|
+
"secqdevops:transpiled-circuit-ref",
|
|
19
|
+
"secqdevops:transpiled-circuit-sha256",
|
|
20
|
+
"secqdevops:qasm-program-ref",
|
|
21
|
+
"secqdevops:qasm-program-sha256",
|
|
22
|
+
"secqdevops:qir-program-ref",
|
|
23
|
+
"secqdevops:qir-program-sha256",
|
|
24
|
+
"secqdevops:qir-profile",
|
|
25
|
+
"secqdevops:transpile-report-ref",
|
|
26
|
+
"secqdevops:backend-provider",
|
|
27
|
+
"secqdevops:backend-name",
|
|
28
|
+
"secqdevops:execution-mode",
|
|
29
|
+
"secqdevops:execution-target",
|
|
30
|
+
"secqdevops:transpile-profile-hash",
|
|
31
|
+
"secqdevops:backend-target-ref",
|
|
32
|
+
"secqdevops:backend-target-sha256",
|
|
33
|
+
"secqdevops:calibration-ref",
|
|
34
|
+
"secqdevops:calibration-sha256",
|
|
35
|
+
"secqdevops:calibration-captured-at",
|
|
36
|
+
"secqdevops:calibration-age-hours",
|
|
37
|
+
"secqdevops:error-handling-ref",
|
|
38
|
+
"secqdevops:error-handling-sha256",
|
|
39
|
+
"secqdevops:error-mitigation",
|
|
40
|
+
"secqdevops:runtime-job-id",
|
|
41
|
+
"secqdevops:runtime-job-ref",
|
|
42
|
+
"secqdevops:runtime-job-sha256",
|
|
43
|
+
"secqdevops:runtime-job-status",
|
|
44
|
+
"secqdevops:result-summary-ref",
|
|
45
|
+
"secqdevops:result-summary-sha256",
|
|
46
|
+
"secqdevops:qubit-count",
|
|
47
|
+
"secqdevops:circuit-depth",
|
|
48
|
+
"secqdevops:two-qubit-depth",
|
|
49
|
+
"secqdevops:t-depth",
|
|
50
|
+
"secqdevops:gate-count",
|
|
51
|
+
"secqdevops:benchmark-ref",
|
|
52
|
+
"secqdevops:benchmark-sha256",
|
|
53
|
+
"secqdevops:layer-model-ref",
|
|
54
|
+
"secqdevops:vocabulary-ref",
|
|
55
|
+
"secqdevops:cyclonedx-edition",
|
|
56
|
+
"secqdevops:qbom-profile-version",
|
|
57
|
+
"secqdevops:circuit-layer",
|
|
58
|
+
"secqdevops:circuit-dynamic",
|
|
59
|
+
"secqdevops:transpiled-circuit-layer",
|
|
60
|
+
"secqdevops:backend-target-layer",
|
|
61
|
+
"secqdevops:runtime-job-layer",
|
|
62
|
+
"secqdevops:equivalence-ref",
|
|
63
|
+
"secqdevops:equivalence-sha256",
|
|
64
|
+
"secqdevops:equivalence-verdict",
|
|
65
|
+
"secqdevops:equivalence-tool",
|
|
66
|
+
"secqdevops:equivalence-kind",
|
|
67
|
+
"secqdevops:expected-distribution-ref",
|
|
68
|
+
"secqdevops:expected-distribution-sha256",
|
|
69
|
+
"secqdevops:result-tvd",
|
|
70
|
+
"secqdevops:result-tolerance",
|
|
71
|
+
"secqdevops:result-fidelity-verdict",
|
|
72
|
+
"secqdevops:result-confidence",
|
|
73
|
+
"secqdevops:result-sampling-error",
|
|
74
|
+
"secqdevops:result-shots-required",
|
|
75
|
+
"secqdevops:conversion-fidelity-ref",
|
|
76
|
+
"secqdevops:conversion-fidelity-sha256",
|
|
77
|
+
"secqdevops:conversion-fidelity-verdict",
|
|
78
|
+
"secqdevops:conversion-fidelity-method",
|
|
79
|
+
"secqdevops:native-program-ref",
|
|
80
|
+
"secqdevops:native-program-sha256",
|
|
81
|
+
"secqdevops:native-program-format",
|
|
82
|
+
"secqdevops:native-result-ref",
|
|
83
|
+
"secqdevops:native-result-sha256",
|
|
84
|
+
"secqdevops:native-result-verdict",
|
|
85
|
+
"secqdevops:native-result-tvd",
|
|
86
|
+
"secqdevops:qir-consistency-ref",
|
|
87
|
+
"secqdevops:qir-consistency-sha256",
|
|
88
|
+
"secqdevops:qir-consistency-verdict",
|
|
89
|
+
"secqdevops:runtime-session-id",
|
|
90
|
+
"secqdevops:provider-job-id",
|
|
91
|
+
"secqdevops:cnot-depth",
|
|
92
|
+
"secqdevops:ir-standard",
|
|
93
|
+
"secqdevops:harness-version",
|
|
94
|
+
"secqdevops:harness-log-ref",
|
|
95
|
+
"secqdevops:harness-log-sha256",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def assemble(results) -> list[dict[str, str]]:
|
|
100
|
+
"""Merge every action's properties into the canonical layout.
|
|
101
|
+
|
|
102
|
+
Raises if two actions emit the same property (a collision like the one that
|
|
103
|
+
once silently dropped result-fidelity evidence) or if a property has no
|
|
104
|
+
place in the layout.
|
|
105
|
+
"""
|
|
106
|
+
owner: dict[str, str] = {}
|
|
107
|
+
values: dict[str, str] = {}
|
|
108
|
+
for result in results:
|
|
109
|
+
for prop in result.properties:
|
|
110
|
+
name = prop["name"]
|
|
111
|
+
if name in owner:
|
|
112
|
+
raise RuntimeError(f"{name} emitted by both {owner[name]!r} and {result.action!r}")
|
|
113
|
+
owner[name] = result.action
|
|
114
|
+
values[name] = prop["value"]
|
|
115
|
+
unplaced = sorted(set(values) - set(PROPERTY_ORDER))
|
|
116
|
+
if unplaced:
|
|
117
|
+
raise RuntimeError(f"properties with no place in the QBOM layout: {unplaced}")
|
|
118
|
+
return [{"name": n, "value": values[n]} for n in PROPERTY_ORDER if n in values]
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""prove_equivalence — the compiled circuit computes the same function as the source (feeds QNT-05).
|
|
2
|
+
|
|
3
|
+
The collector only RECORDS the checker's verdict, including `not_equivalent`
|
|
4
|
+
(honesty over convenience); the gate judges it.
|
|
5
|
+
|
|
6
|
+
Kinds of equivalence (decision 7 Oct 2026, `secqdevops:equivalence-kind`):
|
|
7
|
+
unitary the compiled circuit implements the same unitary (up to global phase)
|
|
8
|
+
observational the unitaries differ, but by nothing any measurement can observe —
|
|
9
|
+
identical measurement statistics (QCEC partial equivalence). A
|
|
10
|
+
measurement-aware optimiser (Qiskit level 3) legitimately produces
|
|
11
|
+
this; the full check alone would call a correct compilation wrong.
|
|
12
|
+
Only attempted for circuits whose measurements are all final.
|
|
13
|
+
dynamic a dynamic circuit, compared under QCEC's deferred-measurement
|
|
14
|
+
transformation (see verify_dynamic)
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
|
|
20
|
+
from .base import NOT_REQUESTED, PRODUCED, UNSUPPORTED, ActionResult, Context
|
|
21
|
+
|
|
22
|
+
ID = "prove_equivalence"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run(ctx: Context) -> ActionResult:
|
|
26
|
+
ctx.state["equivalence_verdict"] = None
|
|
27
|
+
if not ctx.equivalence_check:
|
|
28
|
+
return ActionResult(ID, NOT_REQUESTED, reason="equivalence proof not requested")
|
|
29
|
+
try:
|
|
30
|
+
from importlib.metadata import version as _pkg_version
|
|
31
|
+
|
|
32
|
+
from mqt import qcec
|
|
33
|
+
except ImportError:
|
|
34
|
+
return ActionResult(ID, UNSUPPORTED, reason="mqt.qcec is not installed (pip install 'quantbom[verify]')")
|
|
35
|
+
from .. import _collect as C
|
|
36
|
+
|
|
37
|
+
dynamic = bool(ctx.circuit_info.get("dynamic"))
|
|
38
|
+
dynamic_detail = None
|
|
39
|
+
partial_detail = None
|
|
40
|
+
kind = "dynamic" if dynamic else "unitary"
|
|
41
|
+
try:
|
|
42
|
+
if dynamic:
|
|
43
|
+
# dynamic circuits: QCEC defers measurements and turns classical
|
|
44
|
+
# control into quantum control before comparing (decision 2 Oct 2026)
|
|
45
|
+
result, dynamic_detail = verify_dynamic(qcec, ctx.qiskit_circuit, ctx.state["transpiled"])
|
|
46
|
+
else:
|
|
47
|
+
result = qcec.verify(ctx.qiskit_circuit, ctx.state["transpiled"])
|
|
48
|
+
if _criterion(result) == "not_equivalent" and final_measurements(ctx.qiskit_circuit):
|
|
49
|
+
# the unitaries differ: do the differences reach any measurement?
|
|
50
|
+
result, kind, partial_detail = verify_observational(qcec, ctx.qiskit_circuit, ctx.state["transpiled"], result)
|
|
51
|
+
except Exception as err: # contained: one undecidable proof must never abort the collection
|
|
52
|
+
return ActionResult(ID, UNSUPPORTED,
|
|
53
|
+
reason=f"mqt.qcec could not check this circuit: {type(err).__name__}: {str(err)[:200]}")
|
|
54
|
+
criterion = _criterion(result)
|
|
55
|
+
if result.considered_equivalent() and not (dynamic_detail or {}).get("unverified_gates"):
|
|
56
|
+
verdict = "equivalent"
|
|
57
|
+
elif criterion == "not_equivalent":
|
|
58
|
+
verdict = "not_equivalent"
|
|
59
|
+
else: # timeout / no_information / a decomposition step not verified: declared but non-probative
|
|
60
|
+
verdict = "inconclusive"
|
|
61
|
+
tool = f"mqt.qcec@{_pkg_version('mqt.qcec')}"
|
|
62
|
+
method = "mqt.qcec verify() default portfolio (decision diagrams + ZX-calculus)"
|
|
63
|
+
if dynamic:
|
|
64
|
+
method += "; dynamic circuit transformed (deferred measurement, classical -> quantum control)"
|
|
65
|
+
if dynamic_detail and dynamic_detail.get("decomposed_source"):
|
|
66
|
+
method += ("; source decomposed to the basis at optimization level 0 before the comparison, each branch "
|
|
67
|
+
"gate's decomposition verified separately up to global phase — the deferred-measurement model "
|
|
68
|
+
"is sensitive to the global phase of gates inside classical branches (z vs rz(pi))")
|
|
69
|
+
if kind == "observational":
|
|
70
|
+
method += ("; observational equivalence (QCEC partial equivalence): the unitaries differ, but by nothing "
|
|
71
|
+
f"the {partial_detail['measured_qubits']} measured qubit(s) can observe — identical measurement statistics; "
|
|
72
|
+
f"partial check: {PARTIAL_CHECKERS}")
|
|
73
|
+
report = {
|
|
74
|
+
"schema": "secqdevops.equivalence/v1",
|
|
75
|
+
"release_id": ctx.release_id,
|
|
76
|
+
"tool": tool,
|
|
77
|
+
"method": method,
|
|
78
|
+
"criterion": criterion,
|
|
79
|
+
"verdict": verdict,
|
|
80
|
+
"equivalence_kind": kind,
|
|
81
|
+
"circuits": {"source": "quantum/circuit.qpy", "transpiled": "quantum/transpiled.qpy"},
|
|
82
|
+
**({"dynamic_check": dynamic_detail} if dynamic_detail else {}),
|
|
83
|
+
**({"partial_check": partial_detail} if partial_detail else {}),
|
|
84
|
+
}
|
|
85
|
+
raw = (json.dumps(report, indent=2, ensure_ascii=True) + "\n").encode("utf-8")
|
|
86
|
+
ctx.quantum_dir.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
(ctx.quantum_dir / "equivalence-report.json").write_bytes(raw)
|
|
88
|
+
ctx.state["equivalence_verdict"] = verdict
|
|
89
|
+
|
|
90
|
+
props = [
|
|
91
|
+
{"name": "secqdevops:equivalence-ref", "value": "quantum/equivalence-report.json"},
|
|
92
|
+
{"name": "secqdevops:equivalence-sha256", "value": C.sha256_bytes(raw)},
|
|
93
|
+
{"name": "secqdevops:equivalence-verdict", "value": verdict},
|
|
94
|
+
{"name": "secqdevops:equivalence-tool", "value": tool},
|
|
95
|
+
{"name": "secqdevops:equivalence-kind", "value": kind},
|
|
96
|
+
]
|
|
97
|
+
return ActionResult(ID, PRODUCED, props)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _criterion(result) -> str:
|
|
101
|
+
return str(result.equivalence).split(".")[-1]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def final_measurements(circuit) -> bool:
|
|
105
|
+
"""True when the circuit measures at least one qubit and every measurement is
|
|
106
|
+
final (nothing but barriers after a measurement on its qubit) — the shape on
|
|
107
|
+
which measurement statistics are a meaningful notion of equivalence."""
|
|
108
|
+
measured: set = set()
|
|
109
|
+
index = {q: i for i, q in enumerate(circuit.qubits)}
|
|
110
|
+
for inst in circuit.data:
|
|
111
|
+
name = inst.operation.name
|
|
112
|
+
qubits = [index[q] for q in inst.qubits]
|
|
113
|
+
if name == "measure":
|
|
114
|
+
measured.update(qubits)
|
|
115
|
+
elif name != "barrier" and any(q in measured for q in qubits):
|
|
116
|
+
return False
|
|
117
|
+
return bool(measured)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# The partial check runs QCEC's portfolio without the ZX-calculus checker. ZX does not
|
|
121
|
+
# support partial equivalence (QCEC refuses method="zx" with it), yet the default
|
|
122
|
+
# portfolio still runs it: it compares the full unitaries, which are known to differ
|
|
123
|
+
# here, and says probably_not_equivalent. When that answer arrived before the decision
|
|
124
|
+
# diagrams', it conflicted with the simulation checker's "probably equivalent" and QCEC
|
|
125
|
+
# returned no_information — a correct level-3 compilation declared inconclusive in about
|
|
126
|
+
# 1 run in 100 (measured 24 Oct 2026, mqt.qcec 3.11.0). Without ZX the alternating
|
|
127
|
+
# decision-diagram checker, which is complete, decides every time.
|
|
128
|
+
PARTIAL_CHECKERS = "decision diagrams (alternating) + simulation; ZX-calculus off (it does not support partial equivalence)"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def partial_check(qcec, source, compiled):
|
|
132
|
+
"""QCEC partial (observational) equivalence, the one configuration producer, service and drift sentinel share."""
|
|
133
|
+
return qcec.verify(source, compiled, check_partial_equivalence=True, run_zx_checker=False)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def verify_observational(qcec, source, transpiled, unitary_result):
|
|
137
|
+
"""Re-check `not_equivalent` circuits for identical measurement statistics.
|
|
138
|
+
|
|
139
|
+
QCEC's partial equivalence treats measured qubits as the outputs that count
|
|
140
|
+
and ignores what no measurement observes (phases before a measurement,
|
|
141
|
+
unmeasured qubits). Returns (result, kind, detail): kind "observational" when
|
|
142
|
+
the partial check holds, otherwise the original result and "unitary".
|
|
143
|
+
"""
|
|
144
|
+
second = partial_check(qcec, source, transpiled)
|
|
145
|
+
measured = len({q for inst in source.data if inst.operation.name == "measure" for q in inst.qubits})
|
|
146
|
+
detail = {
|
|
147
|
+
"unitary_criterion": _criterion(unitary_result),
|
|
148
|
+
"partial_criterion": _criterion(second),
|
|
149
|
+
"measured_qubits": measured,
|
|
150
|
+
"total_qubits": source.num_qubits,
|
|
151
|
+
"checkers": PARTIAL_CHECKERS,
|
|
152
|
+
}
|
|
153
|
+
if second.considered_equivalent():
|
|
154
|
+
return second, "observational", detail
|
|
155
|
+
if _criterion(second) != "not_equivalent":
|
|
156
|
+
# the partial check could not decide (no_information / timeout): no evidence either
|
|
157
|
+
# way, so the verdict is inconclusive (QNT-05 N-A), never a refutation. Found 20 Oct
|
|
158
|
+
# 2026 in CI: a correct level-3 compilation was declared not_equivalent and the
|
|
159
|
+
# service's re-derivation rightly disagreed.
|
|
160
|
+
# The drift sentinel already read it this way (_drift.meaning).
|
|
161
|
+
return second, "observational", detail
|
|
162
|
+
return unitary_result, "unitary", detail
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _collect_branch_ops(circuit, inside: bool, found: dict) -> None:
|
|
166
|
+
for inst in circuit.data:
|
|
167
|
+
op = inst.operation
|
|
168
|
+
blocks = getattr(op, "blocks", None)
|
|
169
|
+
if blocks:
|
|
170
|
+
for block in blocks:
|
|
171
|
+
_collect_branch_ops(block, True, found)
|
|
172
|
+
elif inside and op.name not in ("measure", "reset", "barrier", "delay"):
|
|
173
|
+
key = (op.name, tuple(round(float(p), 12) for p in op.params if isinstance(p, (int, float))), op.num_qubits)
|
|
174
|
+
found.setdefault(key, op)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def verify_dynamic(qcec, source, transpiled):
|
|
178
|
+
"""QCEC on a dynamic circuit, with its one known blind spot closed.
|
|
179
|
+
|
|
180
|
+
QCEC's deferred-measurement transformation turns a classical branch into
|
|
181
|
+
quantum control. A gate's global phase is irrelevant under classical
|
|
182
|
+
control but becomes a relative phase under quantum control, so a body that
|
|
183
|
+
transpiles `z` to `rz(pi)` is reported `not_equivalent` although the
|
|
184
|
+
dynamic circuits agree (found 5 Oct 2026, mqt.qcec 3.10.1). The fix keeps
|
|
185
|
+
every step machine-checked: on `not_equivalent`, (1) decompose the source
|
|
186
|
+
to the basis at optimization level 0 (gate definitions only, no
|
|
187
|
+
optimisation), (2) verify each distinct gate inside a branch against its
|
|
188
|
+
own decomposition with QCEC in static mode (equivalent up to global phase
|
|
189
|
+
is exactly what classical control permits), (3) compare the decomposed
|
|
190
|
+
source with the transpiled circuit under the transformation. Equivalence
|
|
191
|
+
follows only if (2) and (3) both hold; a failed (2) makes the result
|
|
192
|
+
inconclusive, a failed (3) is a real `not_equivalent`.
|
|
193
|
+
|
|
194
|
+
Returns (result, detail-or-None); detail is recorded in the report.
|
|
195
|
+
"""
|
|
196
|
+
from qiskit import QuantumCircuit, transpile
|
|
197
|
+
|
|
198
|
+
from .. import _collect as C
|
|
199
|
+
|
|
200
|
+
first = qcec.verify(source, transpiled, transform_dynamic_circuit=True)
|
|
201
|
+
if first.considered_equivalent() or _criterion(first) != "not_equivalent":
|
|
202
|
+
return first, None
|
|
203
|
+
decomposed = transpile(source, basis_gates=C.BASIS_GATES, optimization_level=0, seed_transpiler=C.SEED_TRANSPILER)
|
|
204
|
+
ops: dict = {}
|
|
205
|
+
_collect_branch_ops(source, False, ops)
|
|
206
|
+
gate_checks = []
|
|
207
|
+
unverified = []
|
|
208
|
+
for (name, params, n), op in sorted(ops.items(), key=lambda kv: kv[0][0]):
|
|
209
|
+
single = QuantumCircuit(n)
|
|
210
|
+
single.append(op, range(n))
|
|
211
|
+
single_decomposed = transpile(single, basis_gates=C.BASIS_GATES, optimization_level=0, seed_transpiler=C.SEED_TRANSPILER)
|
|
212
|
+
check = qcec.verify(single, single_decomposed)
|
|
213
|
+
gate_checks.append({"gate": name, "params": list(params), "criterion": _criterion(check)})
|
|
214
|
+
if not check.considered_equivalent():
|
|
215
|
+
unverified.append(name)
|
|
216
|
+
second = qcec.verify(decomposed, transpiled, transform_dynamic_circuit=True)
|
|
217
|
+
detail = {
|
|
218
|
+
"first_criterion": _criterion(first),
|
|
219
|
+
"decomposed_source": True,
|
|
220
|
+
"branch_gate_decompositions": gate_checks,
|
|
221
|
+
"decomposed_criterion": _criterion(second),
|
|
222
|
+
}
|
|
223
|
+
if unverified:
|
|
224
|
+
detail["unverified_gates"] = unverified
|
|
225
|
+
return second, detail
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""record_provenance — build provenance and a manifest-digest record.
|
|
2
|
+
|
|
3
|
+
The `signature.json` written here is an honest, labelled placeholder
|
|
4
|
+
(`manifest-digest/unsigned`), not a cryptographic signature: real signing is
|
|
5
|
+
the `sign` action (sign_evidence.py), verified by CRA-SIGN-01.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from .base import PRODUCED, ActionResult, Context
|
|
10
|
+
|
|
11
|
+
ID = "record_provenance"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def run(ctx: Context) -> ActionResult:
|
|
15
|
+
from .. import _collect as C
|
|
16
|
+
|
|
17
|
+
provenance = {
|
|
18
|
+
"predicateType": "https://slsa.dev/provenance/v1",
|
|
19
|
+
# the same builder identity the producer's signed statement declares (_sign.BUILDER_ID)
|
|
20
|
+
"builder": {"id": "secqdevops/quantbom-sdk", "version": {"quantbom": C.sdk_version()}},
|
|
21
|
+
"buildType": "https://secqdevops/build/qrisp-qiskit",
|
|
22
|
+
"subject": [
|
|
23
|
+
{"name": "quantum/circuit.qpy", "digest": {"sha256": C.sha256_bytes(ctx.state["circuit_qpy"]).split(":", 1)[1]}},
|
|
24
|
+
{"name": "quantum/transpiled.qpy",
|
|
25
|
+
"digest": {"sha256": C.sha256_bytes(ctx.state["transpiled_qpy"]).split(":", 1)[1]}},
|
|
26
|
+
],
|
|
27
|
+
"metadata": {
|
|
28
|
+
"release_id": ctx.release_id,
|
|
29
|
+
"qrisp_version": ctx.qrisp_version,
|
|
30
|
+
"qiskit_version": ctx.qiskit_version,
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
ctx.evidence_dir.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
C._write_json(ctx.evidence_dir / "provenance.json", provenance)
|
|
35
|
+
signature = {
|
|
36
|
+
"signature_type": "manifest-digest/unsigned",
|
|
37
|
+
"subject": ctx.release_id,
|
|
38
|
+
"manifest_sha256": C.sha256_bytes(C.canonical_json(provenance).encode("utf-8")),
|
|
39
|
+
"note": "placeholder: replace with sigstore/in-toto or Ed25519 signing",
|
|
40
|
+
}
|
|
41
|
+
C._write_json(ctx.evidence_dir / "signature.json", signature)
|
|
42
|
+
|
|
43
|
+
props = [
|
|
44
|
+
{"name": "secqdevops:provenance-ref", "value": "evidence/provenance.json"},
|
|
45
|
+
{"name": "secqdevops:signature-ref", "value": "evidence/signature.json"},
|
|
46
|
+
{"name": "secqdevops:provenance-artifact-ref", "value": "evidence/provenance.json"},
|
|
47
|
+
{"name": "secqdevops:signature-artifact-ref", "value": "evidence/signature.json"},
|
|
48
|
+
]
|
|
49
|
+
return ActionResult(ID, PRODUCED, props)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""verify_conversion — the framework-to-Qiskit translation is faithful (feeds QNT-06).
|
|
2
|
+
|
|
3
|
+
The comparison itself runs in the SDK (`_fidelity`), where the native
|
|
4
|
+
Cirq/Braket/PennyLane object is still in hand; this action records the report
|
|
5
|
+
verbatim — including `unfaithful` — for the gate to judge.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
from .base import NOT_APPLICABLE, PRODUCED, ActionResult, Context
|
|
12
|
+
|
|
13
|
+
ID = "verify_conversion"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run(ctx: Context) -> ActionResult:
|
|
17
|
+
ctx.state["conversion_verdict"] = None
|
|
18
|
+
if not ctx.conversion_fidelity:
|
|
19
|
+
return ActionResult(ID, NOT_APPLICABLE,
|
|
20
|
+
reason=f"no adapter-side translation for {ctx.source_format} input")
|
|
21
|
+
from .. import _collect as C
|
|
22
|
+
|
|
23
|
+
report = {**ctx.conversion_fidelity, "release_id": ctx.release_id}
|
|
24
|
+
verdict = str(report.get("verdict"))
|
|
25
|
+
raw = (json.dumps(report, indent=2, ensure_ascii=True) + "\n").encode("utf-8")
|
|
26
|
+
ctx.quantum_dir.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
(ctx.quantum_dir / "conversion-fidelity.json").write_bytes(raw)
|
|
28
|
+
ctx.state["conversion_verdict"] = verdict
|
|
29
|
+
|
|
30
|
+
props = [
|
|
31
|
+
{"name": "secqdevops:conversion-fidelity-ref", "value": "quantum/conversion-fidelity.json"},
|
|
32
|
+
{"name": "secqdevops:conversion-fidelity-sha256", "value": C.sha256_bytes(raw)},
|
|
33
|
+
{"name": "secqdevops:conversion-fidelity-verdict", "value": verdict},
|
|
34
|
+
# which method decided (K3): dense, statevector, decision-diagram, a '+'-joined set, or none
|
|
35
|
+
{"name": "secqdevops:conversion-fidelity-method", "value": str(report.get("method") or "none")},
|
|
36
|
+
]
|
|
37
|
+
if ctx.native_export:
|
|
38
|
+
# the native program itself, as the framework exported it and as QNT-06
|
|
39
|
+
# compared it: hash-bound, signed, and bound to the report's own digest
|
|
40
|
+
# of the same bytes (CRA-INTEG-01), so the fingerprint is of the program,
|
|
41
|
+
# not only of a report about it
|
|
42
|
+
text, fmt = ctx.native_export
|
|
43
|
+
ext = "qasm" if str(fmt).startswith("openqasm") else "json" if str(fmt).endswith("json") else "txt"
|
|
44
|
+
native_raw = str(text).encode("utf-8")
|
|
45
|
+
(ctx.quantum_dir / f"native-program.{ext}").write_bytes(native_raw)
|
|
46
|
+
props += [
|
|
47
|
+
{"name": "secqdevops:native-program-ref", "value": f"quantum/native-program.{ext}"},
|
|
48
|
+
{"name": "secqdevops:native-program-sha256", "value": C.sha256_bytes(native_raw)},
|
|
49
|
+
{"name": "secqdevops:native-program-format", "value": str(fmt)},
|
|
50
|
+
]
|
|
51
|
+
# an inconclusive report says why, so QNT-06's N-A names the real reason
|
|
52
|
+
why = f"inconclusive: {report.get('reason')}" if verdict == "inconclusive" and report.get("reason") else None
|
|
53
|
+
return ActionResult(ID, PRODUCED, props, reason=why)
|