Skip to main content

UnitaryGuard

Property-based semantic-equivalence checking for quantum circuit transforms. Framework-independent since v0.2 — no Qiskit or other SDK dependency anywhere (see "Why v0.2 has no Qiskit dependency" below).

Points it at any Circuit -> Circuit function (a transform wrapping an external engine, a compression step, anything), samples random circuits, and checks one invariant that almost every transform is supposed to preserve but almost no test suite checks directly: does the output implement the same unitary as the input? When it finds a circuit where that's not true, it automatically shrinks it to a small reproduction — the same thing you'd do by hand while debugging, but for free.

Why

This project exists because that exact manual process — sample circuits, compare Operator() before/after a transform, shrink the failing case by hand — found two real, silent correctness bugs in widely-used Clifford+T tooling:

Both bugs had the same shape: the transform's gate-count/T-count output was correct, while the circuit's actual unitary silently changed. Neither was caught by T-count/gate-count/depth checks — only an explicit unitary comparison catches this class of bug. UnitaryGuard is that check, packaged so it doesn't have to be reinvented by hand each time. tests/test_known_bugs.py reproduces both bug shapes as regression tests and proves the tool catches them and shrinks each to a small reproduction.

See DESIGN.md for the full design rationale and current scope/limitations, and CASE_STUDIES.md for every method used to find a real bug so far, each with a runnable example (random sampling, exhaustive search, cross-validating the tool's own oracle, and writing a native-language binding to reach code a wrapper doesn't exercise).

Why v0.2 has no Qiskit dependency

v0.1 checked equivalence via qiskit.quantum_info.Operator, which works fine when the transform under test is unrelated to Qiskit -- but breaks down exactly when the transform IS a real Qiskit pass: the code being tested and the code judging it become the same library, so a shared bug in Qiskit's own gate-matrix definitions can never be caught, structurally, no matter how many circuits you sample. v0.2 replaces the Qiskit-based QuantumCircuit representation with a tiny native Circuit/Gate pair and a gate-matrix table (unitaryguard/matrices.py) derived directly from each gate's standard mathematical definition, built with numpy alone. See DESIGN.md ("v0.2: dropped the Qiskit dependency entirely") for the full writeup. The real-world case that prompted this: validating AutoQ EngineBR, an external, from-scratch transpiler -- see that project's own docs for a worked adapter example (not included in this repo, since it's specific to that engine's own wire protocol).

Install

pip install -e .

Use as a library

from unitaryguard import Circuit, Gate, CheckConfig, check_transform

def my_pass(circ: Circuit) -> Circuit:
    ...  # your Circuit -> Circuit transform (e.g. call an external engine)
    return transformed_circ

cfg = CheckConfig(
    n_qubits=3,
    gate_set=["h", "s", "sdg", "t", "tdg", "cx"],
    n_samples=500,
    seed=42,
)
report = check_transform(my_pass, cfg)
print(report.summary())
assert report.ok

Circuit(n_qubits, gates) and Gate(kind, qubits, params=()) are plain dataclasses -- Gate("cx", (0, 1)), Gate("rz", (0,), (1.23,)). See unitaryguard/matrices.py::GATE_TABLE for the full supported vocabulary.

Use from the CLI

unitaryguard check mymodule:my_pass --qubits 3 --gates h,s,sdg,t,tdg,cx --samples 500 --seed 42

my_pass must be an importable Circuit -> Circuit callable.

Exhaustive mode (deterministic, targets gate-order/inversion bugs)

unitaryguard exhaustive mymodule:my_pass --qubits 1 --gates h,s,sdg,t,tdg --max-length 6

Tests every discrete circuit up to --max-length gates (no continuous parameters), shortest first. A gate-order/conjugation-formula transcription bug (like both bugs this tool was built to generalize from) is deterministic, not statistical -- exhaustive search guarantees finding the smallest counterexample the vocabulary can express, with no separate shrinking step needed.

Add --workers N to split each length's search across a process pool. Defaults to 1 (sequential) unless you pass it explicitly.

Measured, not assumed -- on a 20-logical-core machine (Xeon E5-2666, figures below are specific to that machine, not a universal constant): parallelism only pays off once the workload is large enough to amortize each worker's one-time Qiskit import cost (~1.4s/process). On a small search (1555 circuits, max length 4), throughput peaked at --workers 4 (1.6x) and degraded past that. On a larger search (9331 circuits, max length 5), --workers 8 gave a real 3.3x speedup, plateauing (not improving further) from 12 workers up to the machine's full 20.

The principle generalizes across hardware, the numbers don't -- if you're on a 4-core laptop, "8-12 workers" is meaningless (there's no 12 cores to use, and oversubscribing usually hurts, not helps). Re-run the same small-vs-large comparison on your own machine before picking a --workers value for anything you'll run repeatedly; as a starting point, try --workers <your core count> and only go lower if you see the same kind of degradation documented above for a small search.

Current scope (v0.2)

  • Transforms that preserve qubit count (most gate-level optimization/ synthesis passes). Ancilla-widening transforms are not yet supported — see DESIGN.md.
  • Unitary-only transforms — no mid-circuit measurement / classical control yet.
  • Gate vocabulary: h,x,y,z,s,sdg,t,tdg,sx,sxdg (0-param, 1-qubit), rz,ry,rx,p (1-param, 1-qubit), u (3-param), r (2-param), cx,cz,swap,iswap,dcx,ecr,cy,ch,csx (0-param, 2-qubit), crz,crx,cry,cp,rzz,rxx,ryy,rzx (1-param, 2-qubit), cu,xx_plus_yy,xx_minus_yy (multi-param, 2-qubit), ccx,cswap,ccz,rccx (0-param, 3-qubit) — see unitaryguard/matrices.py::GATE_TABLE. No generic arbitrary-unitary sampling (UnitaryGate-equivalent) yet — extending vocabulary further is a matter of adding matrix functions + table entries to matrices.py; every entry added so far was cross-validated against two independent oracles (Ket and Qiskit's Operator()) before being trusted — see CASE_STUDIES.md.
  • No longer wraps real Qiskit PassManager/pass objects directly (that capability was removed in v0.2 along with the Qiskit dependency — see "Why v0.2 has no Qiskit dependency" above).

Status

Early prototype, private (not yet published to PyPI). v0.2 (2026-09-21): dropped Qiskit dependency entirely, see DESIGN.md.

Release files for unitaryguard 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for unitaryguard 0.2.0
File Size Uploaded
unitaryguard-0.2.0.tar.gz 35.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for unitaryguard 0.2.0
File Interpreter ABI Platform
unitaryguard-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 59.0 kB

Release files / unitaryguard-0.2.0.tar.gz

Download URL unitaryguard-0.2.0.tar.gz
Size 35.0 kB
Tags Source
SHA-256 checksum
How to use checksums
f6dc75bca7bd5b39f745b1d793ce7971845f8fd7383ac9d6c66362878b7143c9
BLAKE2b-256 checksum
How to use checksums
28c98c2f70962bd673fefb2e8656a33c4a7dbb4251208be44e830eacef94dd48
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.12

Release files / unitaryguard-0.2.0-py3-none-any.whl

Download URL unitaryguard-0.2.0-py3-none-any.whl
Size 24.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b75740e6e5d3045de40b32e46757d20e3fd8087df3b859a6e88a187f18904a4c
BLAKE2b-256 checksum
How to use checksums
2bc30cde896c31ae6f0b5c736f830138ba5c27867503c5d1a6a17dfa4925c309
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.12

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page