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,132 @@
1
+ """Amazon Braket — translated through OpenQASM 3 with gate-name rewriting.
2
+
3
+ Braket's OpenQASM uses its own gate names; they are rewritten (word-bounded)
4
+ to stdgates.inc names. Gates outside the table fail to import with an explicit
5
+ error — never a silent mistranslation (a survey of every Braket gate found
6
+ none). Qubit 0 first.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from typing import Any
12
+
13
+ from .base import FrameworkAdapter, dist_version, qiskit_version
14
+
15
+ GATE_ALIASES = ( # braket OpenQASM3 names -> stdgates names (word-bounded)
16
+ ("cnot", "cx"), ("ccnot", "ccx"), ("ti", "tdg"), ("si", "sdg"),
17
+ ("vi", "sxdg"), ("v", "sx"), ("phaseshift", "p"), ("cphaseshift", "cp"),
18
+ ("i", "id"), # Braket's identity is `i`; stdgates.inc names it `id`
19
+ ("xx", "rxx"), ("yy", "ryy"), ("zz", "rzz"), # Braket's Ising gates are exp(-i theta/2 P⊗P): Qiskit's rxx/ryy/rzz
20
+ )
21
+
22
+ # Braket's hardware-native gates (IonQ, Rigetti, OQC), which stdgates.inc does
23
+ # not define. Each body was checked against Braket's own `Circuit.to_unitary()`
24
+ # (tests/frontends/test_frontend_fidelity.py; the conformance suite's C5 keeps
25
+ # a mixed fixture faithful). Parameters are named p0, p1 on purpose: Qiskit's
26
+ # OpenQASM 3 importer binds a gate's parameters by sorted NAME, not position
27
+ # (observed on qiskit 2.5.2: `gate g(theta, phi)` called as g(0.7, 0.3) bound
28
+ # phi=0.7), so names in alphabetical order are correct under either reading.
29
+ NATIVE_GATE_DEFS = """
30
+ gate rxx(p0) a, b { h a; h b; cx a, b; rz(p0) b; cx a, b; h a; h b; }
31
+ gate ryy(p0) a, b { rx(pi/2) a; rx(pi/2) b; cx a, b; rz(p0) b; cx a, b; rx(-pi/2) a; rx(-pi/2) b; }
32
+ gate rzz(p0) a, b { cx a, b; rz(p0) b; cx a, b; }
33
+ gate rzx(p0) a, b { h b; cx a, b; rz(p0) b; cx a, b; h b; }
34
+ gate iswap a, b { s a; s b; h a; cx a, b; cx b, a; h b; }
35
+ gate pswap(p0) a, b { swap a, b; x a; cp(p0) a, b; x a; x b; cp(p0) a, b; x b; }
36
+ gate xy(p0) a, b { rxx(-p0/2) a, b; ryy(-p0/2) a, b; }
37
+ gate ecr a, b { rzx(pi/4) a, b; x a; rzx(-pi/4) a, b; }
38
+ gate cv a, b { h b; cp(pi/2) a, b; h b; }
39
+ gate cphaseshift00(p0) a, b { x a; x b; cp(p0) a, b; x a; x b; }
40
+ gate cphaseshift01(p0) a, b { x a; cp(p0) a, b; x a; }
41
+ gate cphaseshift10(p0) a, b { x b; cp(p0) a, b; x b; }
42
+ gate prx(p0, p1) a { rz(-p1) a; rx(p0) a; rz(p1) a; }
43
+ gate gpi(p0) a { rz(-p0) a; x a; rz(p0) a; }
44
+ gate gpi2(p0) a { prx(pi/2, p0) a; }
45
+ """
46
+ NATIVE_GATES = ("iswap", "pswap", "xy", "xx", "yy", "zz", "ecr", "cv", "cphaseshift00", "cphaseshift01", "cphaseshift10", "gpi", "gpi2", "prx")
47
+
48
+
49
+ def _root(obj: Any) -> str:
50
+ return (type(obj).__module__ or "").split(".")[0]
51
+
52
+
53
+ class BraketAdapter(FrameworkAdapter):
54
+ name = "braket"
55
+ component = "amazon-braket-sdk"
56
+ license = "Apache-2.0"
57
+ bit_order = "qubit0_first"
58
+ translation = "openqasm3"
59
+ supports_dynamic = False # the SDK refuses gates on measured qubits and has no branching: nothing dynamic to lower
60
+ reference = "dense"
61
+
62
+ def detect(self, obj: Any) -> bool:
63
+ module = type(obj).__module__ or ""
64
+ return module == "braket.circuits" or module.startswith("braket.circuits.") # not braket.ahs (analog): K7 refuses it
65
+
66
+ def _openqasm(self, obj: Any) -> str:
67
+ from braket.circuits.serialization import IRType
68
+
69
+ return obj.to_ir(ir_type=IRType.OPENQASM).source
70
+
71
+ def open_parameters(self, obj: Any) -> list[str]:
72
+ return sorted(str(getattr(p, "name", p)) for p in (getattr(obj, "parameters", None) or ()))
73
+
74
+ def _std_source(self, obj: Any) -> str:
75
+ """Braket's OpenQASM 3 with stdgates.inc names: what any standard parser reads."""
76
+ source = self._openqasm(obj)
77
+ header = 'OPENQASM 3.0;\ninclude "stdgates.inc";'
78
+ if re.search(r"\bvi\b", source):
79
+ # stdgates.inc defines sx but not sxdg. Braket's vi is V-dagger =
80
+ # sxdg, which is exactly H·Sdg·H (since sx = H·S·H exactly).
81
+ header += "\ngate sxdg a { h a; sdg a; h a; }"
82
+ if any(re.search(rf"\b{g}\b", source) for g in NATIVE_GATES):
83
+ header += NATIVE_GATE_DEFS # only when a native gate is used: the export stays minimal otherwise
84
+ source = source.replace("OPENQASM 3.0;", header, 1)
85
+ for braket_name, std_name in GATE_ALIASES:
86
+ source = re.sub(rf"\b{braket_name}\b", std_name, source)
87
+ return source
88
+
89
+ def lower(self, obj: Any) -> tuple[Any, str, Any]:
90
+ from qiskit import qasm3
91
+
92
+ return qasm3.loads(self._std_source(obj)), f"braket {dist_version('amazon-braket-sdk')} / qiskit {qiskit_version()}", None
93
+
94
+ def native_export(self, obj: Any) -> tuple[str, str]:
95
+ return self._openqasm(obj), "openqasm3"
96
+
97
+ def reference_unitary(self, obj: Any) -> tuple[Any, str]:
98
+ return obj.to_unitary(), f"amazon-braket-sdk {dist_version('amazon-braket-sdk')} Circuit.to_unitary"
99
+
100
+ def reference_state(self, obj: Any) -> tuple[Any, str]:
101
+ import numpy as np
102
+ from braket.circuits import Circuit
103
+ from braket.devices import LocalSimulator
104
+
105
+ body = Circuit()
106
+ for inst in obj.instructions:
107
+ if getattr(inst.operator, "name", "") == "Measure":
108
+ continue
109
+ body.add_instruction(inst)
110
+ body.state_vector()
111
+ state = np.asarray(LocalSimulator().run(body, shots=0).result().values[0], dtype=complex)
112
+ return state, f"amazon-braket-sdk {dist_version('amazon-braket-sdk')} LocalSimulator state_vector"
113
+
114
+ def export_for_equivalence(self, obj: Any) -> tuple[str, str, str | None]:
115
+ """Braket's OpenQASM uses its own gate names (cnot, ccnot, vi, ...); an independent
116
+ parser knows the stdgates names, so they are aliased — textually, word-bounded —
117
+ and the note says so."""
118
+ return self._std_source(obj), "openqasm3", ("Braket gate names aliased to stdgates.inc (cnot->cx, ccnot->ccx, ti->tdg, si->sdg, vi->sxdg, v->sx, "
119
+ "phaseshift->p, cphaseshift->cp, i->id, xx/yy/zz->rxx/ryy/rzz); hardware-native gates defined in the header")
120
+
121
+ def native_sample(self, obj: Any, shots: int = 8) -> dict[str, int]:
122
+ from braket.devices import LocalSimulator
123
+
124
+ return {str(k): int(v) for k, v in LocalSimulator().run(obj, shots=shots).result().measurement_counts.items()}
125
+
126
+ def native_run(self, obj: Any, shots: int) -> dict[str, Any]:
127
+ """Braket's LocalSimulator; its measurement_counts read qubit 0 first over every qubit."""
128
+ from braket.devices import LocalSimulator
129
+
130
+ counts = LocalSimulator().run(obj, shots=shots).result().measurement_counts
131
+ return self._reversed_reading({str(k): int(v) for k, v in counts.items()},
132
+ f"amazon-braket-sdk {dist_version('amazon-braket-sdk')} LocalSimulator")
@@ -0,0 +1,106 @@
1
+ """Cirq — translated through OpenQASM 2 (`cirq.qasm`). Qubit 0 first."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from .base import FrameworkAdapter, Undecidable, dist_version, qiskit_version
7
+
8
+
9
+ def _root(obj: Any) -> str:
10
+ return (type(obj).__module__ or "").split(".")[0]
11
+
12
+
13
+ class CirqAdapter(FrameworkAdapter):
14
+ name = "cirq"
15
+ component = "cirq-core"
16
+ license = "Apache-2.0"
17
+ bit_order = "qubit0_first"
18
+ translation = "openqasm2"
19
+ supports_dynamic = False
20
+ reference = "dense"
21
+
22
+ def detect(self, obj: Any) -> bool:
23
+ return _root(obj) == "cirq"
24
+
25
+ def open_parameters(self, obj: Any) -> list[str]:
26
+ import cirq
27
+
28
+ return sorted(cirq.parameter_names(obj)) if cirq.is_parameterized(obj) else []
29
+
30
+ def dynamic_features(self, obj: Any) -> list[str]:
31
+ import cirq
32
+
33
+ out: list[str] = []
34
+ if not obj.are_all_measurements_terminal():
35
+ out.append("mid-circuit-measurement")
36
+ if any(isinstance(op, cirq.ClassicallyControlledOperation) for op in obj.all_operations()):
37
+ out.append("classical-control-flow")
38
+ return out
39
+
40
+ def lower(self, obj: Any) -> tuple[Any, str, Any]:
41
+ import cirq
42
+ from qiskit import QuantumCircuit
43
+
44
+ return QuantumCircuit.from_qasm_str(cirq.qasm(obj)), f"cirq {cirq.__version__} / qiskit {qiskit_version()}", None
45
+
46
+ def native_export(self, obj: Any) -> tuple[str, str]:
47
+ import cirq
48
+
49
+ return cirq.qasm(obj), "openqasm2"
50
+
51
+ def reference_unitary(self, obj: Any) -> tuple[Any, str]:
52
+ import cirq
53
+
54
+ if not obj.are_all_measurements_terminal():
55
+ raise Undecidable("non-terminal measurement: the circuit is not a unitary")
56
+ qubits = sorted(obj.all_qubits()) # the order cirq.qasm numbers them in
57
+ body = cirq.Circuit(op for op in obj.all_operations() if not cirq.is_measurement(op))
58
+ return (
59
+ body.unitary(qubit_order=qubits, qubits_that_should_be_present=qubits),
60
+ f"cirq-core {dist_version('cirq-core')} Circuit.unitary",
61
+ )
62
+
63
+ def reference_state(self, obj: Any) -> tuple[Any, str]:
64
+ import cirq
65
+ import numpy as np
66
+
67
+ if not obj.are_all_measurements_terminal():
68
+ raise Undecidable("non-terminal measurement: the circuit is not a unitary")
69
+ qubits = sorted(obj.all_qubits())
70
+ body = cirq.Circuit(op for op in obj.all_operations() if not cirq.is_measurement(op))
71
+ return (cirq.final_state_vector(body, qubit_order=qubits, dtype=np.complex128),
72
+ f"cirq-core {dist_version('cirq-core')} final_state_vector")
73
+
74
+ def native_sample(self, obj: Any, shots: int = 8) -> dict[str, int]:
75
+ import cirq
76
+
77
+ keys = sorted({k for op in obj.all_operations() if cirq.is_measurement(op) for k in [cirq.measurement_key_name(op)]})
78
+ if len(keys) != 1:
79
+ raise Undecidable("native_sample expects exactly one measurement key")
80
+ n = sum(len(op.qubits) for op in obj.all_operations() if cirq.is_measurement(op))
81
+ hist = cirq.Simulator().run(obj, repetitions=shots).histogram(key=keys[0])
82
+ return {format(k, f"0{n}b"): int(v) for k, v in hist.items()}
83
+
84
+ def native_run(self, obj: Any, shots: int) -> dict[str, Any]:
85
+ """cirq.Simulator over every measurement key, keys in the order their
86
+ measurements first appear (the order cirq.qasm declares their registers),
87
+ each key's bits in its gate's qubit order — qubit 0 first. A circuit
88
+ without measurements is measured on every qubit, sorted."""
89
+ import cirq
90
+
91
+ circuit, order = obj, []
92
+ for op in obj.all_operations():
93
+ if cirq.is_measurement(op):
94
+ key = cirq.measurement_key_name(op)
95
+ if key in order:
96
+ raise Undecidable(f"measurement key {key!r} is written more than once")
97
+ order.append(key)
98
+ if not order:
99
+ circuit = obj + cirq.Circuit(cirq.measure(*sorted(obj.all_qubits()), key="m"))
100
+ order = ["m"]
101
+ result = cirq.Simulator().run(circuit, repetitions=shots)
102
+ rows = ["".join("".join(str(int(b)) for b in result.measurements[k][r]) for k in order) for r in range(shots)]
103
+ reading: dict[str, int] = {}
104
+ for row in rows:
105
+ reading[row] = reading.get(row, 0) + 1
106
+ return self._reversed_reading(reading, f"cirq-core {dist_version('cirq-core')} Simulator.run")
@@ -0,0 +1,223 @@
1
+ """CUDA-Q (NVIDIA) — translated through OpenQASM 2 (`cudaq.translate`). Qubit 0 first.
2
+
3
+ Accepts kernels without arguments, written with `@cudaq.kernel` or the
4
+ builder (`cudaq.make_kernel()`). A kernel that takes arguments is refused with
5
+ an explanation: the circuit would depend on values the evidence cannot see —
6
+ wrap it in an argument-free kernel that binds them.
7
+
8
+ Verified by the conformance suite on CUDA-Q 0.16 (qpp-cpu target, K1; the
9
+ record is in harness_manifest.json). What that run found, and the adapter
10
+ now accounts for:
11
+
12
+ * `cudaq.sample` bitstrings put qubit 0 first (C3).
13
+ * `cudaq.get_unitary` indexes the basis with qubit 0 MOST significant while
14
+ `cudaq.get_state` indexes it with qubit 0 LEAST significant — the two APIs
15
+ disagree, so each is pinned by its OWN probe (X on qubit 0, H on qubit 1:
16
+ the support of the first column / the state tells the order) and
17
+ normalised to qubit-0-most-significant, which is what QNT-06 compares.
18
+ * CUDA-Q's compiler removes idle qubits before `get_unitary` (a 3-qubit
19
+ kernel touching one qubit yields a 2x2 matrix) but not before `get_state`
20
+ or `translate`. A probe with an idle qubit therefore answered for the
21
+ wrong register — the original probe did exactly that. The probes now act
22
+ on every qubit and require the full dimension; an elided reference is
23
+ reported as undecidable, and QNT-06 falls through to the state-vector
24
+ method, whose register is complete.
25
+ * `cudaq.translate` on a kernel with arguments raises a RuntimeError
26
+ ("Use synthesize before translate"); the kernel's signature is read
27
+ before lowering so the refusal is the adapter's explanation.
28
+ """
29
+ from __future__ import annotations
30
+
31
+ from typing import Any
32
+
33
+ from .base import FrameworkAdapter, Undecidable, dist_version, qiskit_version
34
+
35
+
36
+ def _root(obj: Any) -> str:
37
+ return (type(obj).__module__ or "").split(".")[0]
38
+
39
+
40
+ def _version() -> str:
41
+ for dist in ("cudaq", "cuda-quantum-cu12", "cuda-quantum-cu11", "cuda-quantum"):
42
+ v = dist_version(dist)
43
+ if v != "unknown":
44
+ return v
45
+ return "unknown"
46
+
47
+
48
+ class CudaqAdapter(FrameworkAdapter):
49
+ name = "cudaq"
50
+ component = "cudaq"
51
+ license = "Apache-2.0"
52
+ bit_order = "qubit0_first"
53
+ translation = "openqasm2"
54
+ supports_dynamic = False
55
+ reference = "dense"
56
+
57
+ def detect(self, obj: Any) -> bool:
58
+ return _root(obj) == "cudaq"
59
+
60
+ def open_parameters(self, obj: Any) -> list[str]:
61
+ """Kernel arguments, read before lowering: a `@cudaq.kernel` carries them in
62
+ its signature, a builder kernel in `argument_count`."""
63
+ sig = getattr(obj, "signature", None)
64
+ arg_types = list(getattr(sig, "arg_types", None) or [])
65
+ if arg_types:
66
+ import inspect
67
+
68
+ fn = getattr(obj, "kernelFunction", None)
69
+ try:
70
+ names = list(inspect.signature(fn).parameters) if fn is not None else []
71
+ except (TypeError, ValueError):
72
+ names = []
73
+ return names or [f"arg{i}" for i in range(len(arg_types))]
74
+ count = getattr(obj, "argument_count", None)
75
+ if isinstance(count, int) and count > 0:
76
+ return [f"arg{i}" for i in range(count)]
77
+ return []
78
+
79
+ def _qasm(self, obj: Any) -> str:
80
+ import cudaq
81
+
82
+ try:
83
+ text = cudaq.translate(obj, format="openqasm2")
84
+ except (TypeError, RuntimeError) as err: # a kernel that needs arguments ("Use synthesize before translate")
85
+ if isinstance(err, RuntimeError) and "synthesize" not in str(err) and "argument" not in str(err):
86
+ raise
87
+ raise ValueError(
88
+ "CUDA-Q kernels with arguments are not supported: the certified circuit would depend on "
89
+ f"values the evidence does not record. Wrap it in an argument-free kernel ({err})"
90
+ ) from err
91
+ if "OPENQASM" not in str(text):
92
+ # K1: CUDA-Q 0.16 returns the text "{translation failed}" (and prints
93
+ # "'cf.cond_br' op unable to translate") instead of raising when a kernel
94
+ # branches on a measurement result. Say so, rather than letting Qiskit's
95
+ # parser fail on a placeholder.
96
+ raise ValueError(
97
+ f"CUDA-Q could not export this kernel to OpenQASM 2 ({str(text).strip()[:60]}): kernels that branch on "
98
+ f"measurement results (classical feed-forward) are not exportable by CUDA-Q {_version()}, so dynamic "
99
+ "CUDA-Q programs are not supported — measure at the end, or move the branch to the host"
100
+ )
101
+ return text
102
+
103
+ def _qubits(self, obj: Any) -> int:
104
+ """Qubits of the kernel as its OpenQASM export declares them (the compiler
105
+ keeps the register there even when it drops idle qubits elsewhere)."""
106
+ import re
107
+
108
+ return sum(int(n) for n in re.findall(r"qreg\s+\w+\[(\d+)\]", self._qasm(obj)))
109
+
110
+ def lower(self, obj: Any) -> tuple[Any, str, Any]:
111
+ from qiskit import QuantumCircuit
112
+
113
+ qc = QuantumCircuit.from_qasm_str(self._qasm(obj))
114
+ if "measure" not in qc.count_ops(): # cudaq.sample measures every qubit when the kernel does not
115
+ qc.measure_all()
116
+ return qc, f"cuda-q {_version()} / qiskit {qiskit_version()}", None
117
+
118
+ def native_export(self, obj: Any) -> tuple[str, str]:
119
+ return self._qasm(obj), "openqasm2"
120
+
121
+ @staticmethod
122
+ def _unitary(cudaq: Any, kernel: Any):
123
+ import numpy as np
124
+
125
+ get_unitary = getattr(cudaq, "get_unitary", None)
126
+ if get_unitary is None:
127
+ raise Undecidable(f"CUDA-Q {_version()} has no cudaq.get_unitary, so the translation cannot be checked")
128
+ try:
129
+ return np.asarray(get_unitary(kernel), dtype=complex)
130
+ except Exception as err: # e.g. measurements inside the kernel, or an unsupported target
131
+ raise Undecidable(f"cudaq.get_unitary could not compute the reference: {type(err).__name__}: {err}") from err
132
+
133
+ @staticmethod
134
+ def _probe(cudaq: Any):
135
+ """X on qubit 0 and H on qubit 1: every qubit is acted on (nothing for the
136
+ compiler to drop) and the support of X⊗H|00> names the index order —
137
+ {2, 3} when qubit 0 is most significant, {1, 3} when least."""
138
+ probe = cudaq.make_kernel()
139
+ q = probe.qalloc(2)
140
+ probe.x(q[0])
141
+ probe.h(q[1])
142
+ return probe
143
+
144
+ @staticmethod
145
+ def _order_from_support(vector, what: str) -> str:
146
+ import numpy as np
147
+
148
+ vector = np.asarray(vector).reshape(-1)
149
+ if vector.shape[0] != 4:
150
+ raise Undecidable(f"cudaq.{what} probe returned {vector.shape[0]} amplitudes for a 2-qubit kernel: "
151
+ "the compiler dropped a qubit, so the index order cannot be read")
152
+ support = {int(i) for i in np.nonzero(np.abs(vector) > 1e-9)[0]}
153
+ if support == {2, 3}:
154
+ return "qubit0_msb"
155
+ if support == {1, 3}:
156
+ return "qubit0_lsb"
157
+ raise Undecidable(f"cudaq.{what} probe landed on basis states {sorted(support)}; its index order is unknown")
158
+
159
+ def _index_order(self, cudaq: Any) -> str:
160
+ """Basis-index order of cudaq.get_unitary, measured — never assumed."""
161
+ return self._order_from_support(self._unitary(cudaq, self._probe(cudaq))[:, 0], "get_unitary")
162
+
163
+ def _state_order(self, cudaq: Any) -> str:
164
+ """Basis-index order of cudaq.get_state — pinned separately: on CUDA-Q 0.16
165
+ it differs from get_unitary's."""
166
+ return self._order_from_support(self._state(cudaq, self._probe(cudaq)), "get_state")
167
+
168
+ @staticmethod
169
+ def _state(cudaq: Any, kernel: Any):
170
+ import numpy as np
171
+
172
+ get_state = getattr(cudaq, "get_state", None)
173
+ if get_state is None:
174
+ raise Undecidable(f"CUDA-Q {_version()} has no cudaq.get_state, so no state reference is available")
175
+ try:
176
+ return np.asarray(get_state(kernel), dtype=complex).reshape(-1)
177
+ except Exception as err:
178
+ raise Undecidable(f"cudaq.get_state could not compute the reference: {type(err).__name__}: {err}") from err
179
+
180
+ def reference_unitary(self, obj: Any) -> tuple[Any, str]:
181
+ import cudaq
182
+ import numpy as np
183
+
184
+ order = self._index_order(cudaq)
185
+ u = self._unitary(cudaq, obj)
186
+ n_declared = self._qubits(obj)
187
+ if n_declared and u.shape[0] != 1 << n_declared:
188
+ raise Undecidable(f"cudaq.get_unitary returned a {u.shape[0]}x{u.shape[0]} matrix for a {n_declared}-qubit kernel: "
189
+ "CUDA-Q's compiler removed idle qubits, so the unitary does not cover the register "
190
+ "(the state-vector method decides instead)")
191
+ if order == "qubit0_lsb":
192
+ n = u.shape[0].bit_length() - 1
193
+ perm = [int(format(i, f"0{n}b")[::-1], 2) for i in range(1 << n)]
194
+ u = u[np.ix_(perm, perm)]
195
+ return u, f"cudaq {_version()} get_unitary (target {cudaq.get_target().name}; index order {order}, normalised)"
196
+
197
+ def reference_state(self, obj: Any) -> tuple[Any, str]:
198
+ import cudaq
199
+
200
+ order = self._state_order(cudaq) # get_state has its own order (it is not get_unitary's)
201
+ state = self._state(cudaq, obj)
202
+ n_declared = self._qubits(obj)
203
+ if n_declared and state.shape[0] != 1 << n_declared:
204
+ raise Undecidable(f"cudaq.get_state returned {state.shape[0]} amplitudes for a {n_declared}-qubit kernel")
205
+ if order == "qubit0_lsb":
206
+ n = state.shape[0].bit_length() - 1
207
+ perm = [int(format(i, f"0{n}b")[::-1], 2) for i in range(1 << n)]
208
+ state = state[perm]
209
+ return state, f"cudaq {_version()} get_state (index order {order}, normalised)"
210
+
211
+ def native_sample(self, obj: Any, shots: int = 8) -> dict[str, int]:
212
+ import cudaq
213
+
214
+ result = cudaq.sample(obj, shots_count=shots)
215
+ return {str(bits): int(count) for bits, count in result.items()}
216
+
217
+ def native_run(self, obj: Any, shots: int) -> dict[str, Any]:
218
+ """cudaq.sample on the kernel; bitstrings read qubit 0 first (conformance C3)."""
219
+ import cudaq
220
+
221
+ result = cudaq.sample(obj, shots_count=shots)
222
+ return self._reversed_reading({str(bits): int(count) for bits, count in result.items()},
223
+ f"cudaq {_version()} sample (target {cudaq.get_target().name})")
@@ -0,0 +1,116 @@
1
+ """PennyLane — translated through OpenQASM 2 (`qml.to_openqasm`), wires pinned.
2
+
3
+ `qml.to_openqasm` numbers qubits by the order wires FIRST APPEAR in the tape,
4
+ not device order: a program starting with `PauliX(2)` was exported as
5
+ `x q[0]` and certified as a circuit flipping qubit 0 (QNT-06 caught it). The
6
+ translation, QNT-06's reference unitary and its export fingerprint therefore
7
+ all use one order, defined once in `wire_order`. Qubit 0 first.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from .base import FrameworkAdapter, Undecidable, dist_version, qiskit_version
14
+
15
+
16
+ def _root(obj: Any) -> str:
17
+ return (type(obj).__module__ or "").split(".")[0]
18
+
19
+
20
+ class PennyLaneAdapter(FrameworkAdapter):
21
+ name = "pennylane"
22
+ component = "pennylane"
23
+ license = "Apache-2.0"
24
+ bit_order = "qubit0_first"
25
+ translation = "openqasm2"
26
+ supports_dynamic = False
27
+ reference = "dense"
28
+
29
+ def detect(self, obj: Any) -> bool:
30
+ return _root(obj) == "pennylane"
31
+
32
+ def dynamic_features(self, obj: Any) -> list[str]:
33
+ """Mid-circuit measurements (`qml.measure`) and conditionals (`qml.cond`) on the tape."""
34
+ import pennylane as qml
35
+
36
+ try:
37
+ ops = list(qml.workflow.construct_tape(obj)().operations)
38
+ except Exception: # an open QNode: refused for its parameters before this matters
39
+ return []
40
+ names = {type(op).__name__ for op in ops}
41
+ out: list[str] = []
42
+ if names & {"MidMeasureMP", "MidMeasure"}:
43
+ out.append("mid-circuit-measurement")
44
+ if "Conditional" in names:
45
+ out.append("classical-control-flow")
46
+ return out
47
+
48
+ def wire_order(self, obj: Any) -> list:
49
+ """The device's wires, or — for a device without fixed wires — the tape's, sorted."""
50
+ wires = list(getattr(getattr(obj, "device", None), "wires", None) or [])
51
+ if wires:
52
+ return wires
53
+ import pennylane as qml
54
+
55
+ tape_wires = list(qml.workflow.construct_tape(obj)().wires)
56
+ try:
57
+ return sorted(tape_wires)
58
+ except TypeError: # mixed label types: no natural order, keep the tape's
59
+ return tape_wires
60
+
61
+ def open_parameters(self, obj: Any) -> list[str]:
62
+ """QNode arguments without a default: the circuit depends on values not given."""
63
+ import inspect
64
+
65
+ func = getattr(obj, "func", None)
66
+ if func is None:
67
+ return []
68
+ return [n for n, p in inspect.signature(func).parameters.items()
69
+ if p.default is inspect.Parameter.empty and p.kind not in (p.VAR_POSITIONAL, p.VAR_KEYWORD)]
70
+
71
+ def _openqasm(self, obj: Any) -> str:
72
+ import pennylane as qml
73
+
74
+ exported = qml.to_openqasm(obj, wires=self.wire_order(obj))
75
+ return exported() if callable(exported) else exported
76
+
77
+ def lower(self, obj: Any) -> tuple[Any, str, Any]:
78
+ import pennylane as qml
79
+ from qiskit import QuantumCircuit
80
+
81
+ return QuantumCircuit.from_qasm_str(self._openqasm(obj)), f"pennylane {qml.__version__} / qiskit {qiskit_version()}", None
82
+
83
+ def native_export(self, obj: Any) -> tuple[str, str]:
84
+ return self._openqasm(obj), "openqasm2"
85
+
86
+ def reference_unitary(self, obj: Any) -> tuple[Any, str]:
87
+ import pennylane as qml
88
+
89
+ return qml.matrix(obj, wire_order=self.wire_order(obj))(), f"pennylane {dist_version('pennylane')} qml.matrix"
90
+
91
+ def reference_state(self, obj: Any) -> tuple[Any, str]:
92
+ import numpy as np
93
+ import pennylane as qml
94
+
95
+ order = self.wire_order(obj) # the same order the unitary reference and the export use
96
+ tape = qml.workflow.construct_tape(obj)()
97
+ script = qml.tape.QuantumScript(tape.operations, [qml.state()], shots=None)
98
+ device = obj.device if list(getattr(obj.device, "wires", None) or []) == order else qml.device("default.qubit", wires=order)
99
+ state = np.asarray(qml.execute([script], device)[0], dtype=complex)
100
+ return state, f"pennylane {dist_version('pennylane')} qml.state() on wire order {order}"
101
+
102
+ def native_sample(self, obj: Any, shots: int = 8) -> dict[str, int]:
103
+ """PennyLane's own reading: the QNode's probabilities over its wire order."""
104
+ probs = obj()
105
+ n = len(self.wire_order(obj))
106
+ return {format(i, f"0{n}b"): int(round(float(p) * shots)) for i, p in enumerate(probs) if float(p) > 1e-12}
107
+
108
+ def native_run(self, obj: Any, shots: int) -> dict[str, Any]:
109
+ """PennyLane's own device: the QNode's exact probabilities over its wire
110
+ order, scaled to `shots`. The comparison still treats them as a sample of
111
+ `shots`, which can only widen its error bound."""
112
+ try:
113
+ reading = self.native_sample(obj, shots)
114
+ except Exception as err:
115
+ raise Undecidable(f"the QNode does not return qml.probs over its wires ({type(err).__name__}: {err})") from err
116
+ return self._reversed_reading(reading, f"pennylane {dist_version('pennylane')} QNode probabilities")
@@ -0,0 +1,67 @@
1
+ """Qiskit — the normalized IR itself: no translation, nothing for QNT-06 to check."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from .base import FrameworkAdapter, Undecidable, qiskit_version
7
+
8
+
9
+ class QiskitAdapter(FrameworkAdapter):
10
+ name = "qiskit"
11
+ component = "qiskit" # always listed by the SBOM builder, as the IR
12
+ license = "Apache-2.0"
13
+ bit_order = "qubit0_last"
14
+ translation = None
15
+ supports_dynamic = True
16
+ reference = "none" # the input already is the IR: nothing to compare
17
+
18
+ def detect(self, obj: Any) -> bool:
19
+ try:
20
+ from qiskit import QuantumCircuit
21
+ except Exception:
22
+ return False
23
+ return isinstance(obj, QuantumCircuit)
24
+
25
+ def lower(self, obj: Any) -> tuple[Any, str, Any]:
26
+ return obj, f"qiskit {qiskit_version()} (in-memory)", None
27
+
28
+ def load(self, kind: str, path: str) -> tuple[Any, str, str]:
29
+ """File inputs: (qiskit_circuit, label, source_format)."""
30
+ qkv = qiskit_version()
31
+ if kind == "qpy":
32
+ from qiskit import qpy
33
+
34
+ with open(path, "rb") as fh:
35
+ return qpy.load(fh)[0], f"qiskit {qkv} (qpy import)", "qpy"
36
+ if kind == "qasm3":
37
+ from qiskit import qasm3
38
+
39
+ return qasm3.load(path), f"qiskit {qkv} (openqasm3)", "openqasm3"
40
+ if kind == "qasm2":
41
+ from qiskit import QuantumCircuit
42
+
43
+ return QuantumCircuit.from_qasm_file(path), f"qiskit {qkv} (openqasm2)", "openqasm2"
44
+ raise ValueError(f"unknown circuit source: {kind}")
45
+
46
+ def native_sample(self, obj: Any, shots: int = 8) -> dict[str, int]:
47
+ from qiskit import transpile
48
+
49
+ from .._circuit import CONTROL_FLOW
50
+
51
+ circ = obj.copy()
52
+ if not any(i.operation.name == "measure" for i in circ.data):
53
+ circ.measure_all()
54
+ if any(i.operation.name in CONTROL_FLOW for i in circ.data):
55
+ # BasicSimulator cannot run classical control flow; qiskit-aer can
56
+ try:
57
+ from qiskit_aer import AerSimulator
58
+ except Exception as exc:
59
+ raise Undecidable("sampling a dynamic circuit needs qiskit-aer (pip install 'quantbom[sim]')") from exc
60
+ backend = AerSimulator()
61
+ counts = backend.run(transpile(circ, backend), shots=shots, seed_simulator=11).result().get_counts()
62
+ else:
63
+ from qiskit.providers.basic_provider import BasicSimulator
64
+
65
+ backend = BasicSimulator()
66
+ counts = backend.run(transpile(circ, backend), shots=shots).result().get_counts()
67
+ return {str(k).replace(" ", ""): int(v) for k, v in counts.items()}