§ 1Workspace at a glance
ppvm is a Cargo workspace of focused crates. A foundation trio —
ppvm-traits, ppvm-pauli-word, and
ppvm-pauli-sum — provides Pauli propagation; everything else
builds on it. Depend on what your workload actually needs — there is no
umbrella crate.
ppvm-traitsfoundation-
The trait system: the
Configtrait that bundles storage / coefficient type / hasher / truncation strategy, the single-qubitPaulialphabet, the gate & noise trait hierarchy (Clifford,TGate, rotations, depolarizing, loss, …), and the sharedACMap/Tracemap impls. Required by every other ppvm crate. ppvm-pauli-wordfoundation-
Packed Pauli strings:
PauliWord,PhasedPauliWord,LossyPauliWord, andPauliPattern. Built onppvm-traits. ppvm-pauli-sumbackend-
The Pauli-propagation engine: the
PauliSum<Config>(andLossyPauliSum) type, the truncationstrategytypes, and the concreteConfigbundles (fxhash,indexmap,dashmap,gxhash). The usual entry point for native Pauli propagation. ppvm-tableaubackend-
Generalized stabilizer-tableau simulator with non-Clifford support via
sparse coefficient tracking. Provides
Tableau,GeneralizedTableau, and theSparseVectortrait (indexable byusize,u128, orbnumtypes for > 64 qubits). Optionalrayonfeature for parallel sampling. ppvm-stiminterop-
Parse and execute Stim programs against
ppvm-tableau. Re-exports the parser, providesparse_extended,execute, and asamplehelper for multi-shot loops. Depends on the Pauli crates,ppvm-tableau, andstim-parser. ppvm-symbackend-
Symbolic (parametric) Pauli propagation. Pauli sums carry coefficients
of type
Term— polynomials in sines and cosines of symbolic parameters — useful for analytic studies of variational circuits and Trotter schedules. stim-parserparser-
Standalone parser for the Stim circuit format. Use directly if you want
to consume Stim programs without pulling in a simulator;
ppvm-stimalready depends on it.
§ 2Pick crates by use case
- Pauli propagation only (Heisenberg-picture observables,
noisy circuits at scale):
ppvm-pauli-sum. - Stabilizer / generalized-tableau simulation
(Schrödinger picture, mid-circuit measurement, loss):
ppvm-pauli-sum+ppvm-tableau. - Run Stim programs against the tableau:
ppvm-pauli-sum+ppvm-tableau+ppvm-stim. - Symbolic / parametric circuits:
ppvm-pauli-sum+ppvm-sym. - Just parse Stim, no simulator:
stim-parser.
§ 3Setup
Requires Rust ≥ 1.83 (edition 2024). Add the crates relevant to your workload — the git dependency form is identical for each:
[dependencies]
ppvm-pauli-sum = { git = "https://github.com/QuEraComputing/ppvm" }
ppvm-tableau = { git = "https://github.com/QuEraComputing/ppvm" }
ppvm-stim = { git = "https://github.com/QuEraComputing/ppvm" }
ppvm-sym = { git = "https://github.com/QuEraComputing/ppvm" }
On x86_64, set RUSTFLAGS="-C target-feature=+aes,+sse2" —
gxhash, used by the default Config bundles, needs the AES
instruction set. CI sets this automatically. On targets without AES, build
with --no-default-features --features=indexmap,ahash to drop
gxhash.
§ 4ppvm-pauli-sum — Pauli propagation
PauliSum<T: Config> is generic over a configuration
bundle (storage, coefficient type, hasher, truncation strategy). Pick a
pre-built config from ppvm_pauli_sum::config:
indexmap::ByteFxHashF64<N>— deterministic ordering,f64coefficients. Good default.dashmap::ByteFxHash<N>— concurrent map for use withrayon.fxhash::Byte<N>— plainHashMap; fastest single-threaded.
N is the number of bytes per Pauli word: N = ceil(n_qubits / 8).
Twelve qubits → N = 2.
use ppvm_pauli_sum::prelude::*;
fn main() {
let mut state: PauliSum<config::indexmap::ByteFxHashF64<1>> =
PauliSum::builder().n_qubits(2).build();
state += ("ZZ", 1.0);
// Circuit is H(0); CNOT(0, 1) — apply in reverse for Heisenberg propagation.
state.cnot(0, 1);
state.h(0);
// <0…0| state |0…0> via a pattern match.
let zero_state: PauliPattern = "Z?*".into();
println!("{}", state.trace(&zero_state));
} The same circuit and the same gate vocabulary the Python bindings sit on top of — every Python call goes through this layer.
§ 5ppvm-tableau — generalized stabilizer tableau
For full-state simulation in the Schrödinger picture, use
GeneralizedTableau<T, I, C>. The I
parameter is the bitstring index type for the sparse coefficient
vector — pick by qubit count:
usize— up to ~64 qubits (platform dependent).u128— up to 128 qubits.bnum::types::U256/U512/U1024— for larger systems.
Using usize past 64 qubits silently overflows — pick the
next-larger type if in doubt.
use ppvm_pauli_sum::prelude::*;
use ppvm_tableau::prelude::*;
fn main() {
// GeneralizedTableau::new takes (n_qubits, coefficient_threshold).
let mut tab: GeneralizedTableau<config::indexmap::ByteFxHashF64<1>, usize, _>
= GeneralizedTableau::new(2, 1e-10);
tab.h(0);
tab.cnot(0, 1);
let r0 = tab.measure(0);
let r1 = tab.measure(1);
assert_eq!(r0, r1); // GHZ correlation
}
Gates run in forward order here — the tableau is in the
Schrödinger picture, not the Heisenberg picture that
ppvm-pauli-sum uses. The same trait methods
(h, cnot, rx, t, …)
apply to both backends; only the picture changes.
Non-Clifford gates (t, u3, rotations) and
reset live on the tableau. For mid-circuit measurement, the
outcome is a tri-state value (Some(true), Some(false),
None for loss) — loss is first-class.
§ 6ppvm-stim — running Stim programs
ppvm-stim parses extended Stim and executes against a
GeneralizedTableau. The two-stage pipeline — parse once,
reuse the program across shots — matters when sampling at scale.
use ppvm_stim::{parse_extended, sample};
use ppvm_tableau::prelude::*;
use ppvm_pauli_sum::prelude::*;
fn run_shots(src: &str, n_qubits: usize) -> Result<Vec<Vec<Option<bool>>>, ppvm_stim::Error> {
let prog = parse_extended(src)?;
// Multi-shot: pass a factory closure — sample reuses the parsed program.
let shots = sample(&prog, 10_000, || {
GeneralizedTableau::<config::indexmap::ByteFxHashF64<1>, usize, _>::new(n_qubits, 1e-10)
})?;
Ok(shots)
}
For single-shot demos there are also run_string /
run_file, but they re-parse on every call — never use them
inside a sampling loop.
§ 7ppvm-sym — symbolic propagation
Substitute f64 coefficients for Term — the
ppvm-sym polynomial-in-sin/cos representation — to propagate
Paulis through parametric circuits without committing to angle values:
use ppvm_pauli_sum::prelude::*;
use ppvm_sym::Term;
fn main() {
let mut sum = PauliSum::<config::fxhash::Byte<2, Term>>::builder()
.n_qubits(2)
.build();
sum += ("ZZ", Term::from(1.0));
sum.rz(0, Term::var(0));
sum.ry(0, Term::var(1));
sum.cnot(0, 1);
sum.rx(1, Term::var(1));
let pat: PauliPattern = "Z?*".into();
println!("Trace expression: {}", sum.trace(&pat));
// Evaluate at concrete angles.
let value = sum.trace(&pat).eval(&[0.3, 0.5]).unwrap();
println!("Value at (0.3, 0.5): {}", value);
}
The same PauliSum, the same gate trait set — only the
coefficient type changes. Term::var(i) introduces a free
parameter; gate methods accept anything implementing
Into<T::Coeff>. Call .eval(&[...]) on
a Term to substitute numeric values at the end.
§ 8stim-parser — standalone Stim parsing
Use stim-parser when you want to consume the Stim circuit
format without a simulator — e.g., to translate Stim into your own IR or
to lint circuits in CI. ppvm-stim already depends on this
crate and re-exports its prelude.
use stim_parser::prelude::*;
let prog = parse_extended(stim_src)?;
for inst in prog.instructions() {
// walk gates, measurements, annotations…
} § 9The trait hierarchy
Gate behaviour is exposed through small, composable traits defined once
in ppvm_traits::traits and implemented by every backend.
Generic code that takes a T: Clifford + RotationOne bound
works against PauliSum and GeneralizedTableau
alike:
Clifford/CliffordExtensions— Pauli, Hadamard, S, CNOT, CZ, CY, √X, √Y (and their adjoints).TGate,RotationOne,RotationTwo,U3Gate— non-Clifford rotations that branch the Pauli sum.Measure/LossyMeasure— Z-basis measurement, with and without loss-aware outcomes.Depolarizing,PauliError,LossChannel,CorrelatedLossChannel,AmplitudeDamping— noise channels.Reset— ground-state reset (tableau).
Adding a new gate is a matter of implementing the relevant trait for whichever simulator types you care about — see the Developer Guide for the end-to-end recipe.
§ 10Custom truncation strategies
PauliSum takes a truncation policy as part of its
Config. Pre-built strategies in
ppvm_pauli_sum::strategy cover the common cases —
CoefficientThreshold(eps), MaxPauliWeight(w),
MaxLossWeight(w), CombinedStrategy(a, b).
Pass one through the builder:
use ppvm_pauli_sum::{prelude::*, strategy::CoefficientThreshold};
type State = PauliSum<config::indexmap::ByteFxHashF64<2, CoefficientThreshold>>;
let mut state: State = PauliSum::builder()
.n_qubits(12)
.strategy(CoefficientThreshold(1e-6))
.capacity(400) // capacity tuning has a large perf impact
.build();
To roll your own, implement the Strategy trait — which
provides capacity (an initial-size hint for the map) and a
truncate method that drops entries from the map according to
your policy:
use ppvm_traits::traits::strategy::Strategy;
#[derive(Debug, Clone, Copy, Default)]
struct DropTinyHighWeight;
impl Strategy for DropTinyHighWeight {
fn capacity(&self, n_qubits: usize) -> usize {
16 * n_qubits
}
fn truncate<S, V, H, M, W>(&self, map: &mut M)
where
// bounds elided — see ppvm_traits::traits::strategy
{
map.retain(|word, coeff| {
coeff.abs() > 1e-8 || word.weight() <= 8
});
}
}
Once the strategy is part of the Config, truncation runs
automatically inside the gate methods. You can also call
state.truncate() manually at sync points if you want
explicit control.
§ 11Next steps
- Browse the full Rust API reference, grouped by crate and kind.
- Read
examples/symbolic.rs,examples/trotter.rs, andexamples/msd.rsin the repository for end-to-end pipelines. - If your user-facing surface is Python, you don't have to drop down to Rust — the Python Quick Start covers the common cases.
- For architecture details (Config-trait generics, dual-map optimisation, where to put new gates), see the Developer Guide.