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.
Files changed (56) hide show
  1. compactq/__init__.py +62 -0
  2. compactq/__main__.py +210 -0
  3. compactq/adapters.py +107 -0
  4. compactq/approximate.py +27 -0
  5. compactq/bench.py +116 -0
  6. compactq/benchmarks.py +110 -0
  7. compactq/cdr.py +155 -0
  8. compactq/circuit.py +99 -0
  9. compactq/cirq_bridge.py +37 -0
  10. compactq/clifford.py +448 -0
  11. compactq/cliffordt.py +253 -0
  12. compactq/cp_pass.py +147 -0
  13. compactq/equivalence.py +205 -0
  14. compactq/errors.py +15 -0
  15. compactq/hardware.py +507 -0
  16. compactq/interfaces.py +49 -0
  17. compactq/io_qasm.py +425 -0
  18. compactq/io_qasm3.py +17 -0
  19. compactq/kak.py +831 -0
  20. compactq/linalg.py +253 -0
  21. compactq/mcx.py +84 -0
  22. compactq/metrics.py +106 -0
  23. compactq/mitigate.py +158 -0
  24. compactq/native.py +342 -0
  25. compactq/noise.py +162 -0
  26. compactq/optimize.py +125 -0
  27. compactq/parity.py +234 -0
  28. compactq/permkak.py +97 -0
  29. compactq/plugins/__init__.py +1 -0
  30. compactq/plugins/qiskit_plugin.py +108 -0
  31. compactq/qiskit_bridge.py +105 -0
  32. compactq/report.py +103 -0
  33. compactq/resources.py +100 -0
  34. compactq/search.py +199 -0
  35. compactq/shadows.py +92 -0
  36. compactq/simulate.py +256 -0
  37. compactq/solvers.py +111 -0
  38. compactq/stabilizer.py +246 -0
  39. compactq/stabsim.py +287 -0
  40. compactq/suppress.py +587 -0
  41. compactq/symbolic.py +187 -0
  42. compactq/target.py +254 -0
  43. compactq/templates.py +91 -0
  44. compactq/tests/__init__.py +0 -0
  45. compactq/tests/conftest.py +3 -0
  46. compactq/tests/test_compactq.py +34 -0
  47. compactq/transforms.py +274 -0
  48. compactq/verify_large.py +135 -0
  49. compactq/winmerge.py +124 -0
  50. compactq/zne.py +180 -0
  51. compactq-0.1.0.dist-info/METADATA +569 -0
  52. compactq-0.1.0.dist-info/RECORD +56 -0
  53. compactq-0.1.0.dist-info/WHEEL +5 -0
  54. compactq-0.1.0.dist-info/entry_points.txt +4 -0
  55. compactq-0.1.0.dist-info/licenses/LICENSE +21 -0
  56. 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"})
@@ -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())