Skip to content
AbeCoullPublic

About

Fast Rust quantum circuit simulator. OpenQASM 3.0, multiple backends, AVX2 SIMD kernels, optional CUDA and MPI, QEC tooling, Python bindings.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

8 stars

Watchers

1 watching

Forks

PRISM-Q: High-Performance Quantum Circuit Simulator in Rust

PRISM-Q is an open-source quantum circuit simulator written in Rust, with Python bindings. It reads OpenQASM 3.0 (with backward-compatible 2.0 syntax), selects a simulation backend from the circuit's structure, runs a multi-pass gate fusion pipeline, and executes on SIMD kernels: AVX2, FMA and BMI2 on x86-64, NEON on ARM64. Two optional features extend the dense path: CUDA acceleration for the statevector, stabilizer (experimental) and density-matrix backends, and an MPI-distributed statevector for registers larger than one host's memory.

The simulation methods are a dense statevector, a stabilizer tableau, a sparse amplitude map, matrix product states (MPS), a product state, a tensor network, an exact density matrix, and two factored variants that split independent registers. Beside the backends sit Clifford+T engines (stabilizer rank and Pauli propagation) and a native quantum error-correction (QEC) program path with detector sampling and decoding. Circuits export back to OpenQASM 3.0.

Crates.io docs.rs PyPI CI Coverage MSRV License OpenQASM

 ██████╗ ██████╗ ██╗███████╗███╗   ███╗       ██████╗
 ██╔══██╗██╔══██╗██║██╔════╝████╗ ████║      ██╔═══██╗
 ██████╔╝██████╔╝██║███████╗██╔████╔██║█████╗██║   ██║
 ██╔═══╝ ██╔══██╗██║╚════██║██║╚██╔╝██║╚════╝██║▄▄ ██║
 ██║     ██║  ██║██║███████║██║ ╚═╝ ██║      ╚██████╔╝
 ╚═╝     ╚═╝  ╚═╝╚═╝╚══════╝╚═╝     ╚═╝       ╚══▀▀═╝

Install

cargo add prism-q                          # Rayon parallelism and faer SVD (default)
cargo add prism-q --no-default-features    # single-threaded, minimal dependencies
pip install prism-q                        # Python
pip install "prism-q[cuda12]"              # Python, plus NVRTC for the CUDA backends

The gpu feature builds without a CUDA toolkit and loads the NVIDIA driver and NVRTC at run time; running it needs a CUDA device, a CUDA 12 driver, and NVRTC from a CUDA 12 toolkit or the cuda12 Python extra. Build with cargo build --release --features "parallel gpu". Building from source and pinning a git revision are covered in CONTRIBUTING.md.

Quick start

use prism_q::run_qasm;

let qasm = r#"
    OPENQASM 3.0;
    include "stdgates.inc";
    qubit[2] q;
    bit[2] c;
    h q[0];
    cx q[0], q[1];
    c[0] = measure q[0];
    c[1] = measure q[1];
"#;

let result = run_qasm(qasm, 42).unwrap();
println!("{:?}", result.probabilities);
// Bell state: ~50% |00⟩, ~50% |11⟩

The Python bindings in bindings/python expose simulation, noise, QEC and the CUDA backends, with NumPy output:

import prism_q

circuit = prism_q.CircuitBuilder(2).h(0).cx(0, 1).build()
print(prism_q.simulate(circuit).seed(42).run().probabilities)  # [0.5, 0, 0, 0.5]

Count keys and measurement bits are LSB-first (q[0] is the least significant qubit), the reverse of Qiskit. See the Python guide.

Shots

use prism_q::{bitstring, circuit::openqasm, simulate};

let circuit = openqasm::parse(qasm).unwrap();
let result = simulate(&circuit).seed(42).shots(1024).unwrap();
println!("{result}");
// 00: 512
// 11: 512

let counts = simulate(&circuit)
    .seed(42)
    .sample_counts(1024)
    .unwrap();
for (bits, count) in counts.into_counts() {
    println!("{}: {count}", bitstring(&bits, circuit.num_classical_bits));
}

Expectation values and marginals

use prism_q::{simulate, CircuitBuilder, PauliTerm};

let bell = CircuitBuilder::new(2).h(0).cx(0, 1).build();

let observables = [
    vec![PauliTerm::z(0), PauliTerm::z(1)],
    vec![PauliTerm::x(0), PauliTerm::x(1)],
];
let values = simulate(&bell).seed(42).expectation_values(&observables).unwrap();
// [1.0, 1.0]: ⟨ZZ⟩ and ⟨XX⟩ on the Bell state.

let marginals = simulate(&bell).seed(42).marginals().unwrap();
// Per-qubit (P(0), P(1)) pairs: [(0.5, 0.5), (0.5, 0.5)].

An observable is a product of single-qubit Paulis with identity factors omitted. Clifford circuits propagate it exactly, and past the statevector memory budget the selected backend answers from its own representation. To start somewhere other than |0...0⟩, pass initial_state a normalized amplitude vector of length 2^n, qubit 0 in the least significant bit.

Parameters and gradients

Mark angles as parameters while building, then rebind without rebuilding. PreparedCircuit reuses one fusion plan across bindings and falls back to a full fusion pass when a binding changes what fusion would emit.

use prism_q::{simulate, CircuitBuilder, PauliTerm, PreparedCircuit};

let (template, params) = CircuitBuilder::new(2)
    .ry(0.0, 0)
    .param(0)
    .cx(0, 1)
    .rz(0.0, 1)
    .param(1)
    .build_parametric();

let bound = params.bind(&template, &[0.3, 1.1]).unwrap();
let result = simulate(&bound).seed(42).run().unwrap();

let mut prepared = PreparedCircuit::new(template, params.clone()).unwrap();
let fused = prepared.bind_fused(&[0.4, 1.2]).unwrap();

let hamiltonian = vec![(1.0, vec![PauliTerm::z(0)])];
let g = simulate(&bound)
    .seed(42)
    .expectation_gradient(&hamiltonian, &params)
    .unwrap();
println!("<H> = {}, gradient = {:?}", g.value, g.gradient);

expectation_gradient uses the adjoint method on the statevector backend. expectation_gradient_shift uses the parameter-shift rule and covers the backends and circuit shapes the adjoint declines.

Learn

The Tutorials walk through sampling, variational gradients, noise, a QEC memory experiment, dynamic circuits and wide structured circuits, in Python with the Rust equivalents. Runnable scripts sit in bindings/python/examples and examples:

python bindings/python/examples/qec_memory.py
cargo run --release --example qec_memory

Backends and automatic selection

Backend Best for Scaling Key property
Statevector General circuits O(2ⁿ) SIMD, tiled L2/L3 kernels, optional CUDA path
Stabilizer Clifford only O(n²) SIMD, thousands of qubits
Factored Stabilizer Clifford with independent blocks O(n²) per cluster Per-cluster tableaux, dynamic merge and split
Sparse Few live amplitudes O(k) HashMap with parallel measurement
MPS Low entanglement or 1D O(nχ²) Hybrid faer / Jacobi SVD
Product State No entanglement O(n) Per qubit
Tensor Network Low treewidth Depends on contraction order Greedy min size heuristic
Factored Partial entanglement Dynamic Tracks independent sub-states
Density Matrix Exact noisy evolution O(4ⁿ) Explicit dispatch only, reuses statevector kernels
Distributed Statevector Beyond single-host memory O(2ⁿ) over MPI ranks distributed feature, exact results

BackendKind::Auto is the default: PRISM-Q selects a backend from the circuit's structure and the host's memory, and the run metadata reports which one ran. The choice is a shape rule, not a cost model, so an explicit backend can still win for a circuit the rules misjudge. Circuits with no entangling gates go to Product State. All-Clifford circuits go to Stabilizer, or to Factored Stabilizer when a large circuit splits into independent blocks. Circuits past the statevector memory budget go to Sparse when the state never holds more than 128 basis states and to MPS with bond dimension 256 otherwise; partially independent circuits go to Factored, and the rest run on Statevector. Before that tree, a Clifford+T circuit with few T gates can take the stabilizer-rank sampler for shots and Pauli propagation for marginals. The budget is half the machine's physical memory, read once and cached. Limits overrides it or any single cap, per request with .limits(...) or for the process with set_default_limits, and the PRISM_MAX_* variables set the same caps from the environment. Density Matrix and Distributed Statevector are never selected automatically; name them.

To choose a backend yourself, call .backend(BackendKind::Stabilizer) (or BackendKind::Mps { max_bond_dim: 64 }, BackendKind::Sparse, and so on) on the simulate builder. Choosing a backend and the backends deep dive cover when each one wins.

OpenQASM

The parser accepts the stdgates.inc set, common controlled and multi-controlled variants, Qiskit exporter gates, IonQ and Google/Cirq native gate names, decomposed multi-instruction gates, IBM legacy u1/u2/u3, and user-defined gate declarations. The inv @, ctrl @ and pow(k) @ modifiers chain on direct gates. qasm_export::to_qasm3 writes a Circuit back out as OpenQASM 3.0 that re-parses to the same instruction stream with inline angles exact; fused payloads have no OpenQASM spelling and are rejected with an error naming the instruction.

The OpenQASM guide lists the accepted subset. The parser itself, src/circuit/openqasm.rs (resolve_gate() and resolve_decomposed_gate()), is the authoritative gate list.

GPU

The gpu feature compiles PTX at runtime through NVRTC for the device's compute capability. Opt in through the simulation builder:

use prism_q::{gpu::GpuContext, simulate};

let ctx = GpuContext::new(0)?;
let result = simulate(&circuit).gpu(ctx).seed(42).run()?;

The circuit still goes through fusion and subsystem decomposition, and a size crossover keeps small sub-circuits on the CPU. simulate(&circuit).gpu_auto(ctx) runs automatic dispatch with the device opted in, BackendKind::StabilizerGpu runs Clifford circuits on the device, and CompiledSampler::with_gpu(ctx) moves large compiled BTS shot counts onto it. The GPU guide covers the kernels, the crossover thresholds and their environment overrides.

Distributed execution (MPI)

The distributed feature shards one exact statevector across 2^p ranks: the top p qubits select the rank and the rest index a local slice that runs on the same SIMD kernels and fusion pipeline as the single-host backend. distributed-mpi adds the MPI transport over rsmpi and needs a system MPI installation.

use prism_q::{distributed::DistributedContext, simulate};

let context = DistributedContext::world()?;   // MPI_Init at MPI_THREAD_FUNNELED
let result = simulate(&circuit).distributed(context).seed(42).run()?;

Results do not depend on the rank count. The distributed guide covers the layout, the qubit relabeling that keeps busy qubits local, the launch scripts, and the limits (power-of-two ranks, no noise models, CPU only).

Benchmarks

cargo bench --bench circuits     --features parallel         # circuit macrobenchmarks
cargo bench --bench bench_driver --features parallel         # gate microbenchmarks
cargo bench --bench bench_gpu    --features "parallel gpu"   # GPU dispatch benchmarks

Baselines were taken with parallel enabled, so keep it on. Run one cargo bench at a time: concurrent Rayon pools contend for cores and skew results. RAYON_NUM_THREADS caps the thread count. The published timings, the circuit definitions, the warmup and timing rules, and how to run a like-for-like comparison against another simulator are in Benchmark Methodology. The A/B and regression workflow used for PRs is in CONTRIBUTING.md and benches/README.md.

Documentation map

Task Page
Install the crate or the wheel Installation
Work through a task end to end Tutorials
Let the dispatcher pick a backend, or override it Choosing a Backend
Compare the backends' memory and scaling Backends Deep Dive
Use the simulator from Python Python Bindings
Check which OpenQASM constructs parse OpenQASM Support
Tune threads, tiles and memory caps Performance and SIMD
Run on a CUDA device GPU Backend
Run across MPI ranks Distributed Statevector
Simulate Clifford+T circuits past the dense limit Clifford+T Simulation
Add noise, detectors and decoding Noise and QEC
Reproduce or extend the benchmarks Benchmark Methodology
See how it measures beside other simulators Comparative Measurements, harness in comparison/
Read how the layers fit together Architecture

Roadmap

  • Multi-GPU and distributed GPU execution. A GPU context binds one device and the distributed backend is CPU only; sharding one statevector across devices also needs peer access, since a host-staged exchange costs far more than the gate it serves.
  • ROCm ports of the CUDA statevector and stabilizer kernels.
  • Noisy shots on the distributed backend. It rejects noise models today because trajectories run as Rayon tasks whose order differs per rank, so their collectives would not line up. The planned route runs shots in lockstep: every rank takes the shots in the same order and seeds each shot's noise stream from the run seed and the shot index, as the host trajectory loop already does, so all ranks draw the same Pauli and Kraus branches without exchanging them. Pauli and depolarizing events read nothing from the state. A damping or thermal-relaxation event weighs its branches by the qubit's excited population, one Allreduce of rank-local sums per event, and a custom Kraus channel also needs the qubit's coherence, so a qubit sitting in a rank bit is relabeled local before that reduction. Two-qubit Kraus channels stay out, since the backend has no two-qubit reduced density matrix, and shots run one at a time across all ranks rather than in parallel on each.

Contributing

CONTRIBUTING.md has the build, test, coverage, profiling and benchmark workflow. The architecture reference covers the layered design, backend trait, SIMD strategy, fusion pipeline and compiled samplers.

Citing

CITATION.cff carries the citation metadata; GitHub renders it under "Cite this repository". Name the release version used, since timings and dispatch rules change between releases.

License

Dual-licensed under MIT or Apache-2.0, at your option.

About

Fast Rust quantum circuit simulator. OpenQASM 3.0, multiple backends, AVX2 SIMD kernels, optional CUDA and MPI, QEC tooling, Python bindings.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

8 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages