compactq 0.1.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.
- compactq/__init__.py +62 -0
- compactq/__main__.py +210 -0
- compactq/adapters.py +107 -0
- compactq/approximate.py +27 -0
- compactq/bench.py +116 -0
- compactq/benchmarks.py +110 -0
- compactq/cdr.py +155 -0
- compactq/circuit.py +99 -0
- compactq/cirq_bridge.py +37 -0
- compactq/clifford.py +448 -0
- compactq/cliffordt.py +253 -0
- compactq/cp_pass.py +147 -0
- compactq/equivalence.py +205 -0
- compactq/errors.py +15 -0
- compactq/hardware.py +507 -0
- compactq/interfaces.py +49 -0
- compactq/io_qasm.py +425 -0
- compactq/io_qasm3.py +17 -0
- compactq/kak.py +831 -0
- compactq/linalg.py +253 -0
- compactq/mcx.py +84 -0
- compactq/metrics.py +106 -0
- compactq/mitigate.py +158 -0
- compactq/native.py +342 -0
- compactq/noise.py +162 -0
- compactq/optimize.py +125 -0
- compactq/parity.py +234 -0
- compactq/permkak.py +97 -0
- compactq/plugins/__init__.py +1 -0
- compactq/plugins/qiskit_plugin.py +108 -0
- compactq/qiskit_bridge.py +105 -0
- compactq/report.py +103 -0
- compactq/resources.py +100 -0
- compactq/search.py +199 -0
- compactq/shadows.py +92 -0
- compactq/simulate.py +256 -0
- compactq/solvers.py +111 -0
- compactq/stabilizer.py +246 -0
- compactq/stabsim.py +287 -0
- compactq/suppress.py +587 -0
- compactq/symbolic.py +187 -0
- compactq/target.py +254 -0
- compactq/templates.py +91 -0
- compactq/tests/__init__.py +0 -0
- compactq/tests/conftest.py +3 -0
- compactq/tests/test_compactq.py +34 -0
- compactq/transforms.py +274 -0
- compactq/verify_large.py +135 -0
- compactq/winmerge.py +124 -0
- compactq/zne.py +180 -0
- compactq-0.1.0.dist-info/METADATA +569 -0
- compactq-0.1.0.dist-info/RECORD +56 -0
- compactq-0.1.0.dist-info/WHEEL +5 -0
- compactq-0.1.0.dist-info/entry_points.txt +4 -0
- compactq-0.1.0.dist-info/licenses/LICENSE +21 -0
- compactq-0.1.0.dist-info/top_level.txt +1 -0
compactq/__init__.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Q-PROOF Compact — the verified quantum circuit optimizer.
|
|
2
|
+
|
|
3
|
+
Smaller circuits, proven.
|
|
4
|
+
|
|
5
|
+
Smaller, shallower quantum circuits, exact by construction and verified
|
|
6
|
+
before return. Works standalone or alongside Qiskit.
|
|
7
|
+
|
|
8
|
+
>>> import compactq
|
|
9
|
+
>>> from compactq.benchmarks import qft
|
|
10
|
+
>>> opt = compactq.optimize(qft(4))
|
|
11
|
+
>>> opt.two_qubit_count() <= qft(4).two_qubit_count()
|
|
12
|
+
True
|
|
13
|
+
"""
|
|
14
|
+
from .circuit import Circuit, Gate
|
|
15
|
+
from .optimize import optimize, optimize_deep
|
|
16
|
+
from .search import optimize_search
|
|
17
|
+
from .approximate import approximate
|
|
18
|
+
from .target import Target, optimize_for, approximate_for_target
|
|
19
|
+
from .mcx import expand_mcx, expand_mcp
|
|
20
|
+
from .templates import template_pass
|
|
21
|
+
from .stabilizer import clifford_equal, is_clifford
|
|
22
|
+
from .io_qasm import from_qasm, from_qasm3, to_qasm
|
|
23
|
+
from .errors import UnsupportedCircuitError
|
|
24
|
+
from .symbolic import param, bind, structure_optimize
|
|
25
|
+
from .verify_large import optimize_large, states_agree
|
|
26
|
+
from .io_qasm3 import to_qasm3
|
|
27
|
+
from .qiskit_bridge import from_qiskit, to_qiskit, compactq_pass
|
|
28
|
+
from .hardware import exact_placement
|
|
29
|
+
from .noise import NoiseModel, default_model
|
|
30
|
+
from .suppress import (pauli_twirl, insert_dd, suppress_plan,
|
|
31
|
+
suppress_execute, expand_for_suppression)
|
|
32
|
+
from .mitigate import mitigate_counts, mitigate_mle
|
|
33
|
+
from .adapters import RunTarget, qiskit_runtime, braket_device
|
|
34
|
+
from .report import SuppressionReport
|
|
35
|
+
from .simulate import simulate_counts
|
|
36
|
+
from .stabsim import stab_sample
|
|
37
|
+
from .zne import fold_global, zne_expectation, zne_execute
|
|
38
|
+
from .cdr import cdr_execute, near_clifford_variants
|
|
39
|
+
from .metrics import layer_fidelity, eplg, suppression_metrics
|
|
40
|
+
from .shadows import shadow_snapshots, shadow_estimate_parity
|
|
41
|
+
from .resources import resource_estimate, t_depth, rebase_cliffordt
|
|
42
|
+
from .solvers import maxcut_qaoa, brute_force_maxcut
|
|
43
|
+
from .simulate import statevector, exact_probabilities
|
|
44
|
+
from . import benchmarks
|
|
45
|
+
|
|
46
|
+
__version__ = "0.1.0"
|
|
47
|
+
__all__ = ["Circuit", "Gate", "optimize", "optimize_deep", "optimize_search", "is_clifford", "clifford_equal", "approximate", "Target", "optimize_for", "approximate_for_target", "from_qasm3", "expand_mcx", "expand_mcp", "template_pass", "from_qasm", "to_qasm",
|
|
48
|
+
"to_qasm3", "benchmarks", "__version__", "param", "bind",
|
|
49
|
+
"structure_optimize", "optimize_large", "states_agree",
|
|
50
|
+
"UnsupportedCircuitError",
|
|
51
|
+
"NoiseModel", "default_model", "pauli_twirl", "insert_dd",
|
|
52
|
+
"suppress_plan", "suppress_execute", "expand_for_suppression",
|
|
53
|
+
"mitigate_counts", "mitigate_mle", "simulate_counts",
|
|
54
|
+
"SuppressionReport", "to_device",
|
|
55
|
+
"RunTarget", "qiskit_runtime", "braket_device", "stab_sample",
|
|
56
|
+
"fold_global", "zne_expectation", "zne_execute",
|
|
57
|
+
"cdr_execute", "near_clifford_variants", "statevector",
|
|
58
|
+
"exact_probabilities", "layer_fidelity", "eplg",
|
|
59
|
+
"suppression_metrics", "maxcut_qaoa", "brute_force_maxcut",
|
|
60
|
+
"shadow_snapshots", "shadow_estimate_parity",
|
|
61
|
+
"resource_estimate", "t_depth", "rebase_cliffordt",
|
|
62
|
+
"exact_placement"]
|
compactq/__main__.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""compactq command line interface.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
compactq INPUT.qasm [-o OUTPUT.qasm] [--stats] [--approx FIDELITY]
|
|
5
|
+
[--no-verify] [--native {cz,ecr,iswap}] [--json]
|
|
6
|
+
|
|
7
|
+
Reads an OpenQASM 2.0 file, optimizes it, and writes the result (stdout by
|
|
8
|
+
default). Exit code is 0 on success.
|
|
9
|
+
|
|
10
|
+
Verification policy (reported on stderr and in --json):
|
|
11
|
+
exact-unitary whole-circuit dense proof (default, within the prover's
|
|
12
|
+
qubit limit: 8 with the native kernels, 6 without)
|
|
13
|
+
randomized-exact large circuits: K=32 random-state verification
|
|
14
|
+
(probabilistically exact; see compactq.verify_large)
|
|
15
|
+
approximate --approx mode: per-block fidelity guarantees
|
|
16
|
+
unverified --no-verify, or circuits beyond the randomized prover's
|
|
17
|
+
reach (> 30 qubits)
|
|
18
|
+
|
|
19
|
+
Trailing measurements in the input are dropped (the unitary core is
|
|
20
|
+
optimized); mid-circuit measurement and reset are rejected.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _write_out(dest: str, text: str) -> None:
|
|
30
|
+
"""Write CLI output to the user-specified destination safely: the
|
|
31
|
+
path is expanded and fully resolved (no ambiguity about where the
|
|
32
|
+
bytes land) and its parent directory must already exist."""
|
|
33
|
+
out_path = Path(dest).expanduser().resolve()
|
|
34
|
+
if not out_path.parent.is_dir():
|
|
35
|
+
raise SystemExit(f"--output directory does not exist: "
|
|
36
|
+
f"{out_path.parent}")
|
|
37
|
+
out_path.write_text(text, encoding="utf-8")
|
|
38
|
+
print(f"compactq: wrote {out_path}", file=sys.stderr)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _stats_dict(circ):
|
|
42
|
+
return {
|
|
43
|
+
"gates": len(circ.ops),
|
|
44
|
+
"two_qubit": circ.two_qubit_count(),
|
|
45
|
+
"depth": circ.depth(),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _suppress_main(args, circ, to_qasm):
|
|
50
|
+
"""--suppress: the automated suppression pipeline, offline."""
|
|
51
|
+
import json
|
|
52
|
+
import compactq
|
|
53
|
+
from compactq import default_model, NoiseModel
|
|
54
|
+
from compactq.suppress import suppress_plan
|
|
55
|
+
|
|
56
|
+
if args.noise:
|
|
57
|
+
with open(args.noise, "r", encoding="utf-8") as f:
|
|
58
|
+
noise = NoiseModel.from_dict(circ.num_qubits, json.load(f))
|
|
59
|
+
else:
|
|
60
|
+
noise = default_model(circ.num_qubits)
|
|
61
|
+
|
|
62
|
+
plan = suppress_plan(circ, noise, variants=max(1, args.variants),
|
|
63
|
+
dd_sequence=args.dd_sequence)
|
|
64
|
+
report = plan["report"]
|
|
65
|
+
out = plan["variants"][0]
|
|
66
|
+
|
|
67
|
+
qasm = to_qasm(out)
|
|
68
|
+
if args.output:
|
|
69
|
+
_write_out(args.output, qasm)
|
|
70
|
+
else:
|
|
71
|
+
sys.stdout.write(qasm)
|
|
72
|
+
|
|
73
|
+
print(str(report), file=sys.stderr)
|
|
74
|
+
if args.report:
|
|
75
|
+
_write_out(args.report, json.dumps(report.to_dict(), indent=2) + "\n")
|
|
76
|
+
|
|
77
|
+
if args.json:
|
|
78
|
+
payload = {
|
|
79
|
+
"version": compactq.__version__,
|
|
80
|
+
"status": "suppressed",
|
|
81
|
+
"num_qubits": circ.num_qubits,
|
|
82
|
+
"variants": plan["num_variants"],
|
|
83
|
+
"dd_sequence": args.dd_sequence,
|
|
84
|
+
"mapping": plan["mapping"],
|
|
85
|
+
"before": _stats_dict(circ),
|
|
86
|
+
"after": _stats_dict(out),
|
|
87
|
+
"report": report.to_dict(),
|
|
88
|
+
}
|
|
89
|
+
sys.stdout.write(json.dumps(payload, indent=2) + "\n")
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _optimize_dispatch(circ, approx, verify):
|
|
94
|
+
"""Returns (circuit, proof_status, dense_limit). Never raises on
|
|
95
|
+
circuit width: wide circuits fall back to unverified optimization
|
|
96
|
+
with an honest status instead of crashing."""
|
|
97
|
+
from compactq.equivalence import _MAX_QUBITS as dense_limit
|
|
98
|
+
if approx is not None:
|
|
99
|
+
from compactq.approximate import approximate
|
|
100
|
+
return approximate(circ, min_fidelity=approx), "approximate", dense_limit
|
|
101
|
+
if not verify:
|
|
102
|
+
from compactq import optimize_search
|
|
103
|
+
return optimize_search(circ, verify=False), "unverified", dense_limit
|
|
104
|
+
if circ.num_qubits <= dense_limit:
|
|
105
|
+
from compactq import optimize_search
|
|
106
|
+
return optimize_search(circ, verify=True), "exact-unitary", dense_limit
|
|
107
|
+
try:
|
|
108
|
+
from compactq.verify_large import optimize_large
|
|
109
|
+
out, st = optimize_large(circ)
|
|
110
|
+
if st == "exact":
|
|
111
|
+
return out, "randomized-exact", dense_limit
|
|
112
|
+
# randomized verification rejected the optimized circuit: the
|
|
113
|
+
# original is returned unoptimized rather than shipping unproven
|
|
114
|
+
return out, "verification-rejected (original returned)", dense_limit
|
|
115
|
+
except ValueError:
|
|
116
|
+
# beyond the randomized prover's reach: optimize without proof
|
|
117
|
+
from compactq import optimize_search
|
|
118
|
+
return optimize_search(circ, verify=False), "unverified", dense_limit
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def main(argv=None) -> int:
|
|
122
|
+
parser = argparse.ArgumentParser(
|
|
123
|
+
prog="compactq",
|
|
124
|
+
description="compactq: the verified quantum circuit optimizer",
|
|
125
|
+
)
|
|
126
|
+
parser.add_argument("input", help="input OpenQASM 2.0 file ('-' = stdin)")
|
|
127
|
+
parser.add_argument("-o", "--output", help="output file (default: stdout)")
|
|
128
|
+
parser.add_argument("--stats", action="store_true",
|
|
129
|
+
help="print before/after statistics to stderr")
|
|
130
|
+
parser.add_argument("--approx", type=float, default=None, metavar="FIDELITY",
|
|
131
|
+
help="approximate mode: per-block fidelity floor "
|
|
132
|
+
"(e.g. 0.99); trades bounded fidelity for fewer "
|
|
133
|
+
"2-qubit gates")
|
|
134
|
+
parser.add_argument("--no-verify", action="store_true",
|
|
135
|
+
help="skip the whole-circuit proof (large circuits "
|
|
136
|
+
"automatically use randomized state verification)")
|
|
137
|
+
parser.add_argument("--native", default=None, choices=["cz", "ecr", "iswap"],
|
|
138
|
+
help="re-express 2q gates in a machine-native basis")
|
|
139
|
+
parser.add_argument("--json", action="store_true",
|
|
140
|
+
help="print a machine-readable result summary (JSON) "
|
|
141
|
+
"to stdout; the QASM circuit is then written "
|
|
142
|
+
"only when -o is given")
|
|
143
|
+
parser.add_argument("--suppress", action="store_true",
|
|
144
|
+
help="run the error-suppression pipeline (optimize, "
|
|
145
|
+
"layout/route, twirl, decouple) and emit the "
|
|
146
|
+
"suppressed circuit plus a per-stage report")
|
|
147
|
+
parser.add_argument("--noise", default=None, metavar="JSON",
|
|
148
|
+
help="with --suppress: NoiseModel calibrations "
|
|
149
|
+
"(T1_us, T2_us, readout, gate_infidelity, "
|
|
150
|
+
"drift_rate, durations_ns); defaults to a "
|
|
151
|
+
"representative superconducting device")
|
|
152
|
+
parser.add_argument("--report", default=None, metavar="JSON",
|
|
153
|
+
help="with --suppress: write the SuppressionReport "
|
|
154
|
+
"artifact (per-stage proof levels) here")
|
|
155
|
+
parser.add_argument("--dd-sequence", default="auto",
|
|
156
|
+
choices=["auto", "xy4", "xy8", "xzx", "pdd4"],
|
|
157
|
+
help="dynamical-decoupling sequence family "
|
|
158
|
+
"(default: auto)")
|
|
159
|
+
parser.add_argument("--variants", type=int, default=4, metavar="K",
|
|
160
|
+
help="with --suppress: number of twirled variants "
|
|
161
|
+
"to build (default 4)")
|
|
162
|
+
args = parser.parse_args(argv)
|
|
163
|
+
|
|
164
|
+
import compactq
|
|
165
|
+
from compactq import from_qasm, to_qasm
|
|
166
|
+
|
|
167
|
+
text = sys.stdin.read() if args.input == "-" else open(
|
|
168
|
+
args.input, "r", encoding="utf-8").read()
|
|
169
|
+
circ = from_qasm(text)
|
|
170
|
+
|
|
171
|
+
if args.suppress:
|
|
172
|
+
return _suppress_main(args, circ, to_qasm)
|
|
173
|
+
|
|
174
|
+
out, status, dense_limit = _optimize_dispatch(
|
|
175
|
+
circ, args.approx, verify=not args.no_verify)
|
|
176
|
+
if args.native is not None:
|
|
177
|
+
from compactq.native import rebase
|
|
178
|
+
out = rebase(out, args.native)
|
|
179
|
+
|
|
180
|
+
result = {
|
|
181
|
+
"version": compactq.__version__,
|
|
182
|
+
"status": status,
|
|
183
|
+
"num_qubits": circ.num_qubits,
|
|
184
|
+
"dense_proof_limit": dense_limit,
|
|
185
|
+
"native": args.native,
|
|
186
|
+
"before": _stats_dict(circ),
|
|
187
|
+
"after": _stats_dict(out),
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if args.json:
|
|
191
|
+
import json
|
|
192
|
+
sys.stdout.write(json.dumps(result, indent=2) + "\n")
|
|
193
|
+
if args.output:
|
|
194
|
+
_write_out(Path(args.output), to_qasm(out))
|
|
195
|
+
else:
|
|
196
|
+
qasm = to_qasm(out)
|
|
197
|
+
if args.output:
|
|
198
|
+
_write_out(Path(args.output), qasm)
|
|
199
|
+
else:
|
|
200
|
+
sys.stdout.write(qasm)
|
|
201
|
+
|
|
202
|
+
print(f"compactq: proof status: {status}", file=sys.stderr)
|
|
203
|
+
if args.stats:
|
|
204
|
+
print(f"before: {circ.stats()}", file=sys.stderr)
|
|
205
|
+
print(f"after : {out.stats()}", file=sys.stderr)
|
|
206
|
+
return 0
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
if __name__ == "__main__":
|
|
210
|
+
raise SystemExit(main())
|
compactq/adapters.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Thin execution adapters - the `execute(...)` box of the pipeline.
|
|
2
|
+
|
|
3
|
+
Turn a hardware handle into the pair the suppression pipeline consumes:
|
|
4
|
+
a `run_fn(circuit, *, seed, shots) -> counts` callable plus a NoiseModel
|
|
5
|
+
ingested from the same device's calibrations.
|
|
6
|
+
|
|
7
|
+
from compactq import suppress_execute
|
|
8
|
+
from compactq.adapters import qiskit_runtime
|
|
9
|
+
|
|
10
|
+
rt = qiskit_runtime(backend) # BackendV2, incl. fakes
|
|
11
|
+
result = suppress_execute(circ, rt.noise_model, rt.run_fn)
|
|
12
|
+
|
|
13
|
+
Every import beyond the standard library is lazy: the CORE stays
|
|
14
|
+
zero-dependency, and adapters raise a clear RuntimeError when the
|
|
15
|
+
optional stack (qiskit-ibm-runtime, amazon-braket-sdk) is missing.
|
|
16
|
+
|
|
17
|
+
Counts convention: hardware APIs return most-significant-bit-first
|
|
18
|
+
bitstrings; the adapters reverse them into compactq's little-endian
|
|
19
|
+
convention (char i of the string is wire i).
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from typing import Callable
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class RunTarget:
|
|
29
|
+
"""A ready-to-use execution target: callable + calibrations."""
|
|
30
|
+
run_fn: Callable
|
|
31
|
+
noise_model: object
|
|
32
|
+
name: str = "device"
|
|
33
|
+
meta: dict = field(default_factory=dict)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _reverse_counts(raw: dict, n: int) -> dict:
|
|
37
|
+
"""qiskit/braket count strings are MSB-first; compactq wants char i =
|
|
38
|
+
wire i. Truncate to n wires and reverse."""
|
|
39
|
+
out = {}
|
|
40
|
+
for s, c in raw.items():
|
|
41
|
+
clean = s.replace(" ", "")
|
|
42
|
+
if len(clean) < n:
|
|
43
|
+
continue
|
|
44
|
+
out[clean[::-1][:n]] = out.get(clean[::-1][:n], 0) + c
|
|
45
|
+
return out
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def qiskit_runtime(backend) -> RunTarget:
|
|
49
|
+
"""Wrap a qiskit BackendV2 (real via qiskit-ibm-runtime, or a
|
|
50
|
+
fake-provider backend) into a RunTarget. Uses SamplerV2; measurements
|
|
51
|
+
are appended to a copy of each submitted circuit."""
|
|
52
|
+
from .noise import NoiseModel
|
|
53
|
+
noise = NoiseModel.from_qiskit_backend(backend)
|
|
54
|
+
name = getattr(backend, "name", None) or str(getattr(backend,
|
|
55
|
+
"backend_name", ""))
|
|
56
|
+
try:
|
|
57
|
+
from qiskit_ibm_runtime import SamplerV2
|
|
58
|
+
sampler = SamplerV2(mode=backend)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
raise RuntimeError(
|
|
61
|
+
f"qiskit-ibm-runtime SamplerV2 unavailable for {name}: {e}. "
|
|
62
|
+
"Install qiskit-ibm-runtime or pass run_fn manually.") from e
|
|
63
|
+
|
|
64
|
+
def run_fn(circ, *, seed: int = 0, shots: int = 1024) -> dict:
|
|
65
|
+
from .qiskit_bridge import to_qiskit
|
|
66
|
+
qc = to_qiskit(circ)
|
|
67
|
+
qc.measure_all()
|
|
68
|
+
try:
|
|
69
|
+
job = sampler.run([qc], shots=shots)
|
|
70
|
+
except TypeError:
|
|
71
|
+
job = sampler.run([qc]) # some backends fix shots elsewhere
|
|
72
|
+
result = job.result()
|
|
73
|
+
data = result[0].data
|
|
74
|
+
raw = data.meas.get_counts() if hasattr(data, "meas") \
|
|
75
|
+
else data.get_counts()
|
|
76
|
+
return _reverse_counts(raw, circ.num_qubits)
|
|
77
|
+
|
|
78
|
+
return RunTarget(run_fn=run_fn, noise_model=noise,
|
|
79
|
+
name=str(name), meta={"stack": "qiskit-ibm-runtime"})
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def braket_device(device) -> RunTarget:
|
|
83
|
+
"""Wrap an Amazon Braket device (AwsDevice) into a RunTarget via the
|
|
84
|
+
QASM bridge. Best-effort: exact kwargs vary across Braket SDK
|
|
85
|
+
versions; failures surface as RuntimeError from run_fn."""
|
|
86
|
+
from .noise import NoiseModel
|
|
87
|
+
name = getattr(device, "name", None) or str(device)
|
|
88
|
+
# Braket exposes device calibrations in its own schema; a full mapping
|
|
89
|
+
# lands with a hardware-verified run. Defaults keep the pipeline usable.
|
|
90
|
+
noise = NoiseModel(getattr(device, "qubit_count", None) or 8)
|
|
91
|
+
|
|
92
|
+
def run_fn(circ, *, seed: int = 0, shots: int = 1024) -> dict:
|
|
93
|
+
try:
|
|
94
|
+
from braket.circuits import Circuit as BKCircuit
|
|
95
|
+
except Exception as e:
|
|
96
|
+
raise RuntimeError(
|
|
97
|
+
"amazon-braket-sdk not installed; pip install "
|
|
98
|
+
"amazon-braket-sdk to use braket_device().") from e
|
|
99
|
+
from .io_qasm import to_qasm
|
|
100
|
+
bk = BKCircuit.from_qasm(to_qasm(circ))
|
|
101
|
+
task = device.run(bk, shots=shots)
|
|
102
|
+
result = task.result()
|
|
103
|
+
raw = result.measurement_counts()
|
|
104
|
+
return _reverse_counts(raw, circ.num_qubits)
|
|
105
|
+
|
|
106
|
+
return RunTarget(run_fn=run_fn, noise_model=noise,
|
|
107
|
+
name=str(name), meta={"stack": "amazon-braket-sdk"})
|
compactq/approximate.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Approximate optimization: trade a bounded, quantified amount of fidelity
|
|
2
|
+
for fewer two-qubit gates.
|
|
3
|
+
|
|
4
|
+
Two-qubit blocks are re-synthesized into the cheapest CX class whose average
|
|
5
|
+
gate fidelity |Tr(U_ref^dag U)|/d (d = 4 for a two-qubit block) is at least
|
|
6
|
+
`min_fidelity`. Each synthesized block carries that per-block guarantee from
|
|
7
|
+
the exact KAK fidelity table; the whole-circuit infidelity is therefore
|
|
8
|
+
bounded by (number of approximated blocks) * (1 - min_fidelity).
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from .circuit import Circuit
|
|
13
|
+
from .search import optimize_search
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def approximate(circ: Circuit, min_fidelity: float = 0.999) -> Circuit:
|
|
17
|
+
"""Approximately optimize `circ`, prioritizing 2-qubit gate count.
|
|
18
|
+
|
|
19
|
+
Per-block average gate fidelity is guaranteed >= min_fidelity; total
|
|
20
|
+
circuit infidelity is bounded by (#approximated blocks) * (1 -
|
|
21
|
+
min_fidelity). An exact baseline is always computed first and an
|
|
22
|
+
approximate candidate is returned only when it is strictly smaller.
|
|
23
|
+
"""
|
|
24
|
+
if not 0.0 < min_fidelity <= 1.0:
|
|
25
|
+
raise ValueError("min_fidelity must be in (0, 1]")
|
|
26
|
+
return optimize_search(circ, verify=None,
|
|
27
|
+
fidelity_tolerance=min_fidelity)
|
compactq/bench.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Benchmark CLI: compactq vs. raw circuits (and vs. Qiskit when installed).
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
python -m compactq.bench [--quick] [--out FILE]
|
|
5
|
+
|
|
6
|
+
Fairness rules: every optimizer receives the *same* input circuit (our QASM
|
|
7
|
+
serialization), the same basis set, fixed seeds, and best-of-3 timings.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import time
|
|
13
|
+
|
|
14
|
+
from .benchmarks import default_suite
|
|
15
|
+
from .io_qasm import from_qasm, to_qasm
|
|
16
|
+
from .optimize import optimize
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _two_q(ops) -> int:
|
|
20
|
+
return sum(1 for g in ops if len(g.qubits) == 2)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _metrics(circ):
|
|
24
|
+
return len(circ.ops), _two_q(circ.ops), circ.depth()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _qiskit_baseline(qasm: str):
|
|
28
|
+
"""Run Qiskit optimization_level=3 on the same QASM. Returns (gates, 2q, depth, ms) or None."""
|
|
29
|
+
try:
|
|
30
|
+
from qiskit import transpile
|
|
31
|
+
from qiskit import qasm2 as qk_qasm
|
|
32
|
+
except Exception:
|
|
33
|
+
return None
|
|
34
|
+
qc = qk_qasm.loads(qasm)
|
|
35
|
+
basis = ["h", "x", "y", "z", "s", "sdg", "t", "tdg", "rx", "ry", "rz", "p", "cx", "cz", "swap"]
|
|
36
|
+
best = None
|
|
37
|
+
for _ in range(3):
|
|
38
|
+
t0 = time.perf_counter()
|
|
39
|
+
out = transpile(qc, basis_gates=basis, optimization_level=3, seed_transpiler=42)
|
|
40
|
+
dt = (time.perf_counter() - t0) * 1000.0
|
|
41
|
+
best = dt if best is None else min(best, dt)
|
|
42
|
+
ops = out.count_ops()
|
|
43
|
+
total = int(sum(v for k, v in ops.items() if k not in ("barrier",)))
|
|
44
|
+
two_q = int(ops.get("cx", 0) + ops.get("cz", 0) + ops.get("swap", 0)
|
|
45
|
+
+ ops.get("ecr", 0) + ops.get("rxx", 0))
|
|
46
|
+
depth = int(out.depth())
|
|
47
|
+
return total, two_q, depth, best
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run_suite(quick: bool = False):
|
|
51
|
+
suite = default_suite()
|
|
52
|
+
if quick:
|
|
53
|
+
suite = {k: v for k, v in suite.items() if k in ("ghz-5", "qft-3", "clifford-ladder-4")}
|
|
54
|
+
rows = []
|
|
55
|
+
for name, circ in suite.items():
|
|
56
|
+
qasm = to_qasm(circ)
|
|
57
|
+
raw = _metrics(circ)
|
|
58
|
+
|
|
59
|
+
best_ms = None
|
|
60
|
+
for _ in range(3):
|
|
61
|
+
t0 = time.perf_counter()
|
|
62
|
+
opt = optimize(from_qasm(qasm), verify=circ.num_qubits <= 6)
|
|
63
|
+
dt = (time.perf_counter() - t0) * 1000.0
|
|
64
|
+
best_ms = dt if best_ms is None else min(best_ms, dt)
|
|
65
|
+
ours = _metrics(opt)
|
|
66
|
+
|
|
67
|
+
qk = _qiskit_baseline(qasm)
|
|
68
|
+
rows.append((name, raw, ours, best_ms, qk))
|
|
69
|
+
return rows
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _fmt_row(name, raw, ours, ours_ms, qk):
|
|
73
|
+
def cell(v):
|
|
74
|
+
return str(v) if v is not None else "n/a"
|
|
75
|
+
line = f"| {name} | {raw[0]} / {raw[1]} / {raw[2]} | {ours[0]} / {ours[1]} / {ours[2]} | {ours_ms:.2f} |"
|
|
76
|
+
if qk is not None:
|
|
77
|
+
qt, q2, qd, qms = qk
|
|
78
|
+
gain = f"-{(1 - ours[1] / q2) * 100:.0f}%" if q2 else "n/a"
|
|
79
|
+
line += f" {qt} / {q2} / {qd} | {qms:.2f} | {gain} |"
|
|
80
|
+
return line
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def main() -> int:
|
|
84
|
+
ap = argparse.ArgumentParser(prog="compactq-bench", description=__doc__)
|
|
85
|
+
ap.add_argument("--quick", action="store_true", help="run a 3-circuit quick suite")
|
|
86
|
+
ap.add_argument("--out", default=None, help="also write the markdown table to FILE")
|
|
87
|
+
args = ap.parse_args()
|
|
88
|
+
|
|
89
|
+
rows = run_suite(quick=args.quick)
|
|
90
|
+
has_qk = rows and rows[0][4] is not None
|
|
91
|
+
|
|
92
|
+
lines = []
|
|
93
|
+
header = "| circuit | raw gates/2q/depth | compactq gates/2q/depth | compactq ms |"
|
|
94
|
+
sep = "|---|---|---|---|"
|
|
95
|
+
if has_qk:
|
|
96
|
+
header += " qiskit L3 gates/2q/depth | qiskit ms | 2q-gate gain vs qiskit |"
|
|
97
|
+
sep += "---|---|---|"
|
|
98
|
+
lines.append(header)
|
|
99
|
+
lines.append(sep)
|
|
100
|
+
for name, raw, ours, ms, qk in rows:
|
|
101
|
+
lines.append(_fmt_row(name, raw, ours, ms, qk))
|
|
102
|
+
|
|
103
|
+
table = "\n".join(lines)
|
|
104
|
+
print(table)
|
|
105
|
+
if args.out:
|
|
106
|
+
from pathlib import Path
|
|
107
|
+
out_path = Path(args.out).expanduser().resolve()
|
|
108
|
+
if not out_path.parent.is_dir():
|
|
109
|
+
raise SystemExit(f"--out directory does not exist: "
|
|
110
|
+
f"{out_path.parent}")
|
|
111
|
+
out_path.write_text(table + "\n", encoding="utf-8")
|
|
112
|
+
return 0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == "__main__":
|
|
116
|
+
raise SystemExit(main())
|
compactq/benchmarks.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Standard benchmark circuits, generated locally (no downloads needed)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import math
|
|
5
|
+
import random
|
|
6
|
+
|
|
7
|
+
from .circuit import Circuit, Gate
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def ghz(n: int) -> Circuit:
|
|
11
|
+
c = Circuit(n, [])
|
|
12
|
+
c.append(Gate("h", (), (0,)))
|
|
13
|
+
for k in range(1, n):
|
|
14
|
+
c.append(Gate("cx", (), (0, k)))
|
|
15
|
+
return c
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _cp(c: Circuit, theta: float, ctrl: int, tgt: int) -> None:
|
|
19
|
+
"""Controlled-phase via the standard 2-CX identity (exact up to phase)."""
|
|
20
|
+
c.append(Gate("p", (theta / 2,), (ctrl,)))
|
|
21
|
+
c.append(Gate("cx", (), (ctrl, tgt)))
|
|
22
|
+
c.append(Gate("p", (-theta / 2,), (tgt,)))
|
|
23
|
+
c.append(Gate("cx", (), (ctrl, tgt)))
|
|
24
|
+
c.append(Gate("p", (theta / 2,), (ctrl,)))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def qft(n: int) -> Circuit:
|
|
28
|
+
c = Circuit(n, [])
|
|
29
|
+
for i in range(n):
|
|
30
|
+
c.append(Gate("h", (), (i,)))
|
|
31
|
+
for j in range(i + 1, n):
|
|
32
|
+
_cp(c, math.pi / (2 ** (j - i)), j, i)
|
|
33
|
+
return c
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def clifford_ladder(n: int, seed: int = 7) -> Circuit:
|
|
37
|
+
"""Random Clifford circuit — the optimizer should compress these hard."""
|
|
38
|
+
rng = random.Random(seed)
|
|
39
|
+
c = Circuit(n, [])
|
|
40
|
+
for _ in range(3 * n):
|
|
41
|
+
pick = rng.random()
|
|
42
|
+
q = rng.randrange(n)
|
|
43
|
+
if pick < 0.45:
|
|
44
|
+
c.append(Gate(rng.choice(["h", "s", "sdg", "x", "z"]), (), (q,)))
|
|
45
|
+
else:
|
|
46
|
+
a = rng.randrange(n)
|
|
47
|
+
b = (a + 1 + rng.randrange(n - 1)) % n
|
|
48
|
+
c.append(Gate("cx", (), (a, b)))
|
|
49
|
+
return c
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def brickwork(n: int, layers: int, seed: int = 11) -> Circuit:
|
|
53
|
+
"""Nearest-neighbour 2-qubit brickwork with rotations (hardware-friendly)."""
|
|
54
|
+
rng = random.Random(seed)
|
|
55
|
+
c = Circuit(n, [])
|
|
56
|
+
for _ in range(layers):
|
|
57
|
+
for q in range(n):
|
|
58
|
+
c.append(Gate(rng.choice(["rz", "rx", "ry"]), (rng.uniform(-3, 3),), (q,)))
|
|
59
|
+
for pair in range(0, n - 1, 2):
|
|
60
|
+
c.append(Gate("cx", (), (pair, pair + 1)))
|
|
61
|
+
for q in range(n):
|
|
62
|
+
c.append(Gate("rz", (rng.uniform(-3, 3),), (q,)))
|
|
63
|
+
for pair in range(1, n - 1, 2):
|
|
64
|
+
c.append(Gate("cx", (), (pair, pair + 1)))
|
|
65
|
+
return c
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def random_circuit(n: int, depth: int, seed: int = 0) -> Circuit:
|
|
69
|
+
rng = random.Random(seed)
|
|
70
|
+
c = Circuit(n, [])
|
|
71
|
+
for _ in range(depth):
|
|
72
|
+
pick = rng.random()
|
|
73
|
+
if pick < 0.30:
|
|
74
|
+
c.append(Gate(rng.choice(["h", "t", "tdg", "s", "x"]), (), (rng.randrange(n),)))
|
|
75
|
+
elif pick < 0.60:
|
|
76
|
+
c.append(Gate(rng.choice(["rx", "ry", "rz"]), (rng.uniform(-3, 3),), (rng.randrange(n),)))
|
|
77
|
+
elif pick < 0.92:
|
|
78
|
+
a = rng.randrange(n)
|
|
79
|
+
b = rng.randrange(n)
|
|
80
|
+
while b == a:
|
|
81
|
+
b = rng.randrange(n)
|
|
82
|
+
c.append(Gate("cx", (), (a, b)))
|
|
83
|
+
else:
|
|
84
|
+
a = rng.randrange(n)
|
|
85
|
+
b = rng.randrange(n)
|
|
86
|
+
while b == a:
|
|
87
|
+
b = rng.randrange(n)
|
|
88
|
+
c.append(Gate("swap", (), (a, b)))
|
|
89
|
+
return c
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def default_suite() -> dict:
|
|
93
|
+
suite = {
|
|
94
|
+
"ghz-5": ghz(5),
|
|
95
|
+
"qft-3": qft(3),
|
|
96
|
+
"qft-4": qft(4),
|
|
97
|
+
"clifford-ladder-4": clifford_ladder(4, seed=7),
|
|
98
|
+
"clifford-ladder-5": clifford_ladder(5, seed=13),
|
|
99
|
+
"brickwork-4x4": brickwork(4, 4, seed=11),
|
|
100
|
+
"random-4q-40": random_circuit(4, 40, seed=1),
|
|
101
|
+
"random-5q-60": random_circuit(5, 60, seed=2),
|
|
102
|
+
}
|
|
103
|
+
return suite
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def load_qasm(path: str) -> Circuit:
|
|
107
|
+
"""Load a QASM2 file (convenience wrapper)."""
|
|
108
|
+
from .io_qasm import from_qasm
|
|
109
|
+
with open(path) as f:
|
|
110
|
+
return from_qasm(f.read())
|