This is the workflow you will actually use. Stim builds a noisy syndrome-extraction circuit and distills it into a detector error model (DEM); Glean ingests the DEM and decodes batches of syndromes entirely in Rust, crossing the Python boundary once per batch.
Why a DEM
A detector error model is the decoding-ready form of a circuit: a list of independent error mechanisms, each with a prior probability, the detectors (syndrome bits) it flips, and
the logical observables it flips. It is exactly the check matrix H, the
observable matrix L, and the per-column priors a decoder needs — derived from the
real circuit-level noise, not an idealized code-capacity model.
1 · Generate a circuit + DEM
Use Stim to build a circuit and call detector_error_model(flatten_loops=True).
Flattening unrolls the measurement rounds so the DEM is a flat list of mechanisms — the form glean.load_dem expects.
2 · Load it into Glean
glean.load_dem(text, num_detectors, num_observables) parses the flattened DEM
string and returns a DetectorErrorModel. Pass the authoritative counts straight
from the DEM object (dem.num_detectors, dem.num_observables). The
Tanner graph is built once and cached, then reused for every decode.
3 · Sample syndromes
Stim's detector sampler gives you both halves of each shot: det (the detector syndromes a decoder sees) and obs (the true
observable flips, the answer key). Request them separately with separate_observables=True.
4 · Decode the batch
g.decode_batch(syndromes, ...) decodes every shot and returns one predicted
observable-flip list per shot — the entire loop stays in Rust. parallel=True releases the GIL and spreads shots across cores with rayon; the
output is bit-identical to serial, so it is a pure throughput knob.
import glean, stim, numpy as np
# 1 — a surface-code memory experiment with circuit-level depolarizing noise
circuit = stim.Circuit.generated(
"surface_code:rotated_memory_z", distance=5, rounds=5,
after_clifford_depolarization=1e-3)
dem = circuit.detector_error_model(flatten_loops=True)
# 2 — hand the flattened DEM to Glean (the Tanner graph is built once, cached)
g = glean.load_dem(str(dem), dem.num_detectors, dem.num_observables)
# 3 — sample detector syndromes + the true observable flips from Stim
sampler = circuit.compile_detector_sampler()
det, obs = sampler.sample(shots=10_000, separate_observables=True)
# 4 — decode the whole batch in Rust; parallel=True spreads shots over cores
pred = np.asarray(g.decode_batch(det.tolist(), osd_order=10, parallel=True))
# 5 — a logical error is any predicted observable flip that misses the truth
ler = (pred != obs).any(axis=1).mean()
print(f"logical error rate = {ler:.3e}")osd_order selects the post-processor: osd_order=10 is BP+OSD-CS-10, the validated default. A negative value selects
the cheaper OSD-0. See the conventions in the API
reference.
5 · Logical error rate
Compare the predicted flips to Stim's true observables: a shot is a logical failure if any observable is mispredicted. Averaging over shots gives the LER. This is the number that goes in a paper — and the spine of Glean's benchmarks.
B2 — on a surface-code DEM, Glean agrees with PyMatching within ≤1.3σ at d=5.
qLDPC & hypergraph DEMs
The same three lines decode the qLDPC frontier. Swap the circuit for the bivariate-bicycle [[144, 12, 12]] gross code and the rest is unchanged. That DEM has detectors of
degree up to 6 — a hypergraph, where matching decoders like MWPM do not apply, which
is the whole reason BP-based decoders matter here.
B3 — on the gross-code circuit-level DEM, Glean's
BP+OSD-CS matches Roffe's ldpc to 99.8% / <0.4σ, ~10× faster batched.
Now you have a working decode, the next question is which decoder — Glean ships three paradigms for this DEM.