§ 1Workspace at a glance
ppvm is a Cargo workspace of focused crates. A foundation trio —ppvm-traits, ppvm-pauli-word, andppvm-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, useGeneralizedTableau<T, I, C>. The Iparameter 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 thatppvm-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) andreset 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 aGeneralizedTableau. 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 — theppvm-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 implementingInto<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 GeneralizedTableaualike:
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 theDeveloper Guide for the end-to-end recipe.
§ 10Custom truncation strategies
PauliSum takes a truncation policy as part of itsConfig. Pre-built strategies inppvm_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 atruncate 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 callstate.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.