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,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()}
|