A high-performance quantum circuit simulator written in Rust. Designed for correctness first, with pluggable backends (state vector, MPS, stabilizer), Python bindings, and a path to CUDA acceleration and distributed multi-GPU execution.
Status: v0.2 — Phases 0–4.5 complete: optimized single/multi-threaded CPU state vector, MPS and stabilizer backends, Python bindings on PyPI, and CPU parity vs the references — every parity-matrix cell ≤ 1.2× Qiskit Aer (MT statevector + MPS) / Stim, most cells faster (docs/perf/parity.md). Next: Phase 5 (GPU). See ROADMAP.md for phases and BACKLOG.md for issues.
Requires Rust 1.89+ (edition 2021).
# Build everything
cargo build --workspace
# Run all tests
cargo test --workspace
# Lint + format check (CI gate)
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --check
# Run benchmarks
cargo bench --workspaceFor release builds with native CPU optimizations:
RUSTFLAGS="-C target-cpu=native" cargo build --release --workspaceInstall from PyPI (the package is aleph-sim, the module is aleph;
wheels: Linux x86_64 manylinux_2_28 + macOS arm64, Python ≥ 3.10):
pip install aleph-simWheels are also attached to each GitHub release if you prefer a pinned direct download.
import aleph
c = aleph.Circuit(2)
c.h(0)
c.cx(0, 1)
result = aleph.run(c, shots=1024, seed=0) # backend="auto" → stabilizer (Clifford)
print(result.counts()) # {'00': ~512, '11': ~512}
# statevector() needs the dense backend; "auto" sends this Clifford circuit to
# the stabilizer, which has no amplitudes — ask for "sv" explicitly:
sv = aleph.run(c, shots=1, seed=0, backend="sv").statevector()
print(sv) # numpy complex128 array, 4 amplitudes
# Or load OpenQASM 3.0 (from_qasm_file(path) also exists). backend= accepts the
# same names as the CLI: "auto" (default), "statevector"/"sv",
# "stabilizer"/"stab", "mps".
qasm = """OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
h q[0];
cx q[0], q[1];
"""
print(aleph.run(aleph.Circuit.from_qasm(qasm), backend="mps", seed=0).counts())| backend | representation | capacity | exactness | use it for |
|---|---|---|---|---|
sv |
dense 2ⁿ complex amplitudes | ≤ 28 qubits (default cap) | exact (FP64) | any circuit that fits in memory — the general-purpose workhorse |
mps |
matrix product state (bond dim χ) | 100+ qubits (1024 hard cap) for shallow/local circuits | exact while χ is not binding; controlled truncation otherwise | low-entanglement circuits: shallow brickwork, nearest-neighbour dynamics |
stab |
CHP tableau, O(n²) bits | hundreds of qubits (65,536 hard cap) | exact | Clifford-only circuits: error-correction cycles, stabilizer states |
metal |
dense 2ⁿ complex amplitudes on the GPU | ≤ 28 qubits | approximate (FP32, ~1e-5 vs FP64) | large dense circuits on Apple Silicon — opt-in GPU build |
cuda / cuda-f32 |
dense 2ⁿ amplitudes on an NVIDIA GPU | ≤ 30 (FP64) / ≤ 31 (FP32) in core, higher out-of-core | exact-ish (FP64) / approximate (FP32, ~1e-5) | large dense circuits on NVIDIA — opt-in GPU build |
Backend names are one shared vocabulary across the CLI (--backend) and Python (backend=): auto (default — picks from circuit structure: Clifford → stabilizer, large nearest-neighbour + shallow → MPS, else state vector), statevector (alias sv), stabilizer (alias stab), mps, metal (alias gpu), and cuda / cuda-f32. Under auto, a Clifford circuit routes to the stabilizer backend, which has no dense state vector — pass backend="sv" (or --backend sv) when you need statevector().
The metal backend is an FP32 Apple Silicon GPU state vector. It is opt-in and macOS-only: build the CLI with cargo build -p aleph-cli --features metal, or the Python wheel with maturin develop --features python,metal. auto never selects it (FP32 accuracy + device availability), so request it explicitly. Both sv and metal expose statevector().
The cuda (FP64) / cuda-f32 (FP32) backends are NVIDIA GPU state vectors, opt-in and Linux-only: build the CLI with cargo build -p aleph-cli --features cuda, or the Python wheel with maturin develop --features python,cuda. The --precision flag (CLI) / precision= kwarg (Python) — auto (default), f64, f32 — steers an auto GPU pick: on a CUDA build with a device, large dense circuits route to cuda-f32 by default (2× throughput, one extra qubit of in-core reach). Circuits past the in-core cap stream out-of-core (the state lives in host memory, device-sized tiles flow through the GPU); the CLI runs these and reports ‖ψ‖² (paged readout is norm-only — no sampling/expectation/statevector), and the Python binding raises with guidance to use the CLI/Rust API for them.
v0.2 closes the CPU-parity matrix: at 16 threads with default settings on both sides, aleph's state-vector backend beats Qiskit Aer on QFT (0.81×), Grover (0.60×) and random brickwall (0.59×) at n=25 (GHZ is an allocation-bound tie at 1.03×); aleph-mps is 4.4–14× faster than Aer MPS on every measured workload; and the stabilizer backend now beats Stim on surface-code cycles at every measured distance (0.79× at d=11, after being 1.64× behind in v0.1). Honest caveats: the Grover cell is the iteration-capped fixture, distances beyond d=11 are unmeasured, and the v0.1 single-thread report (docs/perf/v0.1.md) showed raw unfused random circuits 3–5× behind Aer — fusion in the default pipeline is what closes that gap. Full matrix + protocol: docs/perf/parity.md.
After cargo build --release --workspace, the aleph binary lives at
target/release/aleph. Four basic invocations:
# Sample 1024 shots from a Bell-state circuit with a fixed RNG seed.
./target/release/aleph run oracle/circuits/bell_phi_plus.qasm \
--shots 1024 --seed 0
# Print the full final state vector (capped at 10 qubits;
# use --force-statevector to opt out of the cap).
./target/release/aleph run oracle/circuits/bell_phi_plus.qasm --statevector
# Compute ⟨ψ|ZZ|ψ⟩ and ⟨ψ|XX|ψ⟩ in one run.
./target/release/aleph run oracle/circuits/bell_phi_plus.qasm \
--expectation ZZ --expectation XX
# Single-iteration timing breakdown (parse / run / sample / total).
./target/release/aleph bench oracle/circuits/bell_phi_plus.qasmPauli strings for --expectation are positional: qubit 0 is the
leftmost character. IXZI means X on q1, Z on q2. Optional
coeff* prefix: 1.5*ZZ, -0.5*X.
See aleph --help (and aleph run --help / aleph bench --help)
for the full flag list.
Aer-compatible noise via a NoiseModel (Python) or a one-parameter CLI preset.
import aleph
c = aleph.Circuit(2)
c.h(0); c.cx(0, 1)
nm = aleph.NoiseModel()
nm.add_all_qubit_quantum_error(aleph.depolarizing_error(0.01, 1), ["h"])
nm.add_quantum_error(aleph.depolarizing_error(0.02, 2), ["cx"], [0, 1])
nm.add_readout_error([[0.98, 0.02], [0.03, 0.97]], 0)
print(aleph.run(c, shots=100_000, noise=nm, seed=7).counts())Error factories mirror Qiskit Aer names: depolarizing_error(p, num_qubits),
amplitude_damping_error(gamma), phase_damping_error(lam),
pauli_error([("X", 0.1), ("I", 0.9)]), plus bit_flip_error / phase_flip_error.
Noise runs on the state-vector backend as per-shot Monte-Carlo trajectories. Each
trajectory chunk reuses its state and channel scratch; absolute shot seeds keep
counts reproducible across worker counts and memory budgets. CLI/Python use
sparse counts and a 1 GiB incremental buffer budget, rejecting runs that cannot
fit even one trajectory. This budget excludes input models, allocator/runtime
overhead and other concurrent calls; it is not an RSS limit. Override it with
Python noise_memory_budget_bytes=... or CLI --noise-memory-budget-bytes ...;
cap contexts with noise_max_workers=... / --noise-max-workers .... These options
require noise. States at 26–28 qubits need a budget above the default even with
one worker. For example, noise_memory_budget_bytes=8 * 1024**3, noise_max_workers=1 permits planning one 28-qubit trajectory if the host has
sufficient memory. Rust callers can
set noise::NoiseOptions through run_noisy_with_options (legacy dense counts)
or run_noisy_sparse, and inspect the estimate with plan_noisy.
CLI presets — depolarizing on every gate, symmetric readout flip on every qubit (repeatable; forces the state-vector backend, shots-only):
aleph run circuit.qasm --shots 4096 --noise depol:0.01 --noise readout:0.02GPU (Phase 5+):
cargo build --workspace --features cudaBuilding the Python bindings from source (instead of installing a
release wheel): cd crates/aleph-py && maturin develop --release.
aleph ships MWPM, Union-Find, BP, BP-OSD and relay-BP decoders for stim Detector Error Models, usable directly or through sinter:
pip install "aleph-sim[sinter]"import sinter, stim, aleph.sinter
circ = stim.Circuit.generated("surface_code:rotated_memory_x", distance=5, rounds=5,
after_clifford_depolarization=0.003)
stats = sinter.collect(tasks=[sinter.Task(circuit=circ)], num_workers=4,
decoders=["aleph-mwpm", "aleph-relay-bp-osd", "pymatching"],
custom_decoders=aleph.sinter.decoders(), max_shots=100_000)aleph's batch decode parallelizes across shots on all cores, so under
sinter.collect(num_workers=N) set RAYON_NUM_THREADS=1 (or a small number)
to avoid running num_workers × cores threads.
Throughput (scripts/python/bench_qec.py, macOS-26.7-arm64-arm-64bit / arm,
Apple Silicon Mac, 10 logical CPUs; surface d=9 reduced to 5,000 shots —
aleph relay-bp-osd is slow enough that 20,000 would take >60s — every other
row is 20,000). Every aleph decoder is measured on all 10 cores and on one
thread (RAYON_NUM_THREADS=1); pymatching's decode_batch runs on one
thread:
| DEM | decoder | threads | shots | shots/s |
|---|---|---|---|---|
| surface d=5 | aleph mwpm | 10 | 20,000 | 2,196,645 |
| surface d=5 | aleph mwpm | 1 | 20,000 | 733,746 |
| surface d=5 | aleph union-find-weighted | 10 | 20,000 | 2,476,563 |
| surface d=5 | aleph union-find-weighted | 1 | 20,000 | 576,536 |
| surface d=5 | aleph bp-osd | 10 | 20,000 | 18,555 |
| surface d=5 | aleph bp-osd | 1 | 20,000 | 2,987 |
| surface d=5 | aleph relay-bp-osd | 10 | 20,000 | 4,530 |
| surface d=5 | aleph relay-bp-osd | 1 | 20,000 | 756 |
| surface d=5 | pymatching | 1 | 20,000 | 1,503,434 |
| surface d=9 | aleph mwpm | 10 | 5,000 | 202,386 |
| surface d=9 | aleph mwpm | 1 | 5,000 | 33,684 |
| surface d=9 | aleph union-find-weighted | 10 | 5,000 | 392,584 |
| surface d=9 | aleph union-find-weighted | 1 | 5,000 | 82,226 |
| surface d=9 | aleph relay-bp-osd | 10 | 5,000 | 323 |
| surface d=9 | aleph relay-bp-osd | 1 | 5,000 | 55 |
| surface d=9 | pymatching | 1 | 5,000 | 204,928 |
| color d=5 | aleph bp-osd | 10 | 20,000 | 27,827 |
| color d=5 | aleph bp-osd | 1 | 20,000 | 4,628 |
| color d=5 | aleph relay-bp | 10 | 20,000 | 6,042 |
| color d=5 | aleph relay-bp | 1 | 20,000 | 1,047 |
| color d=5 | aleph relay-bp-osd | 10 | 20,000 | 5,729 |
| color d=5 | aleph relay-bp-osd | 1 | 20,000 | 1,040 |
Per core, pymatching is faster. Single-threaded, aleph's dense-blossom
mwpm reaches 0.49× pymatching's throughput at d=5 and 0.16× at d=9, and
union-find-weighted 0.38× and 0.40×: aleph's per-shot matching algorithm is
asymptotically slower than PyMatching's Sparse Blossom (see
docs/perf/qec-q1-mwpm.md; the Sparse Blossom rewrite is tracked in
#331 and not yet done).
On the whole machine, aleph is faster or level. With all 10 cores
against pymatching's one thread, mwpm is 1.46× pymatching at d=5 and
level (0.99×) at d=9, and union-find-weighted is 1.65× and 1.92×.
decode_batch_bit_packed releases the GIL and splits shots across cores, so
this is what a single-process caller sees; a sinter run with many workers
already uses the cores and gets the per-core ratio instead.
ldpc's SinterBpOsdDecoder was installed but has no color-code row here:
its compile_decoder_for_dem raises NotImplementedError on this ldpc
version for a non-decomposed (hypergraph) DEM, so the benchmark skips it
rather than fail. aleph's iterative BP-family decoders (bp-osd, relay-bp,
relay-bp-osd) are two to three orders of magnitude lower throughput than
the one-shot matching decoders — expected, since each shot runs multiple BP
iterations (and, for -osd, a post-processing ordered-statistics step)
rather than a single matching pass.
aleph/
├── crates/
│ ├── aleph-core/ # Complex, StateVector, Gate, Circuit types
│ ├── aleph-ir/ # Circuit IR + optimization passes
│ ├── aleph-parser/ # OpenQASM 3.0 parser
│ ├── aleph-backend/ # Backend trait + naive impl
│ ├── aleph-sv/ # state vector backends (CPU, later GPU)
│ ├── aleph-mps/ # MPS tensor network backend
│ ├── aleph-stab/ # stabilizer (Aaronson–Gottesman) backend
│ ├── aleph-cli/ # `aleph` binary
│ └── aleph-py/ # PyO3 Python bindings
└── scripts/ # GitHub issue sync, etc.
ROADMAP.md— strategy and phase planBACKLOG.md— detailed issue specifications (source of truth)CREATE ISSUES.md— how the GitHub backlog is syncedCLAUDE.md— instructions for AI assistants working in this repo
Per-algorithm guides applying the framework from OPTIMIZATION GUIDE.md to specific quantum algorithms.
OPTIMIZATION GUIDE.md— methodology, principles, checklists.OPTIMIZATION CYCLE.md— step-by-step iteration playbook.- Algorithm-specific playbooks below.
| Algorithm | File | Key win | When to consult |
|---|---|---|---|
| Quantum Fourier Transform | QFT.md | Phase polynomial fusion, AQFT | Working on diagonal gates, QFT/QPE/Shor |
| Grover's algorithm | GROVER.md | Specialized MCZ, diffusion fusion | Working on multi-controlled gates, search |
| Variational Quantum Eigensolver | VQE.md | Symbolic params, Pauli grouping | NISQ chemistry; highest practical ROI |
| QAOA | QAOA.md | Diagonal cost-layer fusion, MPS | Combinatorial optimization, sparse graphs |
| Random Circuits | RANDOM CIRCUIT.md | Generic kernel quality | Stress testing, supremacy benchmarks |
| Stabilizer Circuits | STABILIZER CIRCUITS.md | Bit-packed tableau, batched shots | QEC, surface codes, Clifford-only |
Every playbook follows the same template:
- Quick Reference — algorithm at a glance.
- Algorithm Overview — brief technical recap.
- Computational Profile — where the time goes.
- Optimization Ladder — opportunities in ROI order.
- Pitfalls — algorithm-specific gotchas.
- Baseline Comparisons — what to beat (Qiskit Aer / Stim / cuQuantum).
- Phase-by-Phase Sub-goals — what's expected at each project phase.
- Success Metrics — when an optimization PR is considered successful.
- References — primary literature.
Add a playbook when:
- The project starts targeting an algorithm not covered (e.g., Shor, Hamiltonian simulation).
- An algorithm reveals optimization opportunities not captured by the global guide.
- Multiple PRs on the same algorithm would benefit from shared context.
Follow the template. Open a PR titled [playbook] Add {AlgorithmName} playbook.
Update a playbook when:
- A new optimization opportunity is discovered.
- A pitfall is encountered in review or in production.
- Baseline numbers change (new external version, new reference hardware).
- A sub-goal is achieved or refined.
Open a PR titled [playbook] Update {AlgorithmName}: {reason}.
MIT — see LICENSE — except hw/, which is Apache-2.0
(see hw/LICENSE and the rationale in hw/README.md).
The split is about patents: MIT carries no patent grant, which blocks organisations that would otherwise fabricate the decoder RTL. Apache-2.0 grants one explicitly.