Simulate and Decode
SkillDev toolsRun a LightStim circuit through the simulation pipeline and extract logical error rates. Use this skill whenever the user asks to simulate a circuit, get LER vs physical error rate, run PyMatching or BPOSD or MWPF decoding, sweep noise parameters, benchmark a code's threshold, plot logical error rates, configure post-selection (for state injection or distillation), or target specific logical observables in a multi-qubit circuit.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Simulate and Decode skill
What this skill tells your AI
The instructions your AI receives, as published by qutone/lightstim in skills/simulate-decode/SKILL.md and read by ahel’s review.
Takes a noisy stim.Circuit and runs it through SimulationPipeline.
Step 0: Get a noisy circuit
# From CircuitBuilder (custom protocol):
noisy = builder.build_noisy_circuit(NoiseConfig(p_2q=1e-3, p_meas=1e-3), "circuit_level")
# From any experiment class:
noisy = experiment.build() # already returns noisy circuit
SimulationPipeline API
from lightstim.simulation.decoder_backend import SimulationPipeline, DecoderConfig
pipeline = SimulationPipeline(
decoder_config=DecoderConfig("pymatching"), # see decoder table below
max_errors=200, # primary stopping criterion: stop after N logical errors
max_shots=1_000_000, # hard cap on total shots
batch_size=10_000, # shots per batch
num_workers=4, # parallel CPU processes
print_progress=False,
)
stats = pipeline.run(noisy_circuit)
print(stats.logical_error_rate) # errors / post_selected_shots
print(stats.ler_error_bar()) # 95% Wilson CI half-width (z=1.96)
print(stats.ler_error_bar(z=1.0)) # 1-sigma half-width
print(stats.shots) # total shots attempted
print(stats.post_selected_shots) # shots surviving post-selection (= shots if no PS)
Sampling rule of thumb
For reported LER experiments, prefer error-targeted collection: choose a target
number of logical errors and run until that count is reached, with a large
max_shots cap. Fixed-shot simulations are useful for smoke tests, but they often
produce long error bars when the LER is small.
Typical settings:
pipeline = SimulationPipeline(
decoder_config=DecoderConfig("pymatching"),
max_errors=100, # collect this many logical errors per point
max_shots=100_000_000, # hard safety cap
batch_size=10_000,
num_workers=4,
print_progress=False,
)
Use a smaller max_errors only for quick local demos; use 100-200+ errors for
figures or comparisons.
Decoder options
| Name | Backend | When to use |
|---|---|---|
"pymatching" | cpu | Default. MWPM. Always available. Fast for surface codes. |
"bposd" | cpu | BP+OSD via stimbposd. Better for LDPC (BB, PQRM). |
"mwpf" | cpu | Minimum-weight parity factor. Required for PQRM/CrossLS (hyperedges). |
"nv-qldpc-decoder" | gpu | GPU BP+OSD via cudaq_qec. Use batch_size ≥ 50_000, num_workers=1. |
Decoder selection rule:
- Prefer
pymatchingwhenever the detector error model is graphlike / has no hyperedges. This includes ordinary surface-code memory and surface-code lattice-surgery circuits. - Use
mwpforbposdwhen the circuit has hyperedges or important correlated fault mechanisms that should not be decomposed away. - Use
bposdfor LDPC codes such as BB/PQRM when BP+OSD is the intended decoder. - Use
mwpffor CrossLS/PQRM-style hypergraph decoding, especially when high-weight X stabilizers or cross-code surgery create hyperedges.
Post-selection
Use post-selection for state injection (discard shots with injection errors) or distillation (post-select on magic state purity).
# Option A: Auto-detect from circuit tags (detectors tagged 'post-select')
# The SyndromeTracker inserts this tag when post_select_detector_coords is set.
pipeline = SimulationPipeline(
decoder_config=DecoderConfig("pymatching"),
max_errors=200,
# post_select_detector_indices auto-discovered from circuit tags
)
# Option B: Post-select on decoder-corrected observable (distillation output)
# First, identify which observable index corresponds to the output qubit:
from lightstim.simulation.observable_analysis import (
build_obs_patch_matrix, identify_distillation_observables
)
matrix, patch_names = build_obs_patch_matrix(circuit, system)
_, target_obs, ps_obs = identify_distillation_observables(
matrix, patch_names, output_patches=["W4"]
)
pipeline = SimulationPipeline(
decoder_config=DecoderConfig("bposd"),
post_select_corrected_observable_indices=ps_obs, # discard failed distillations
target_observable_indices=target_obs, # measure LER on output qubit only
max_errors=50,
)
stats = pipeline.run(circuit)
print(f"Post-selection rate: {stats.post_selection_rate:.3f}")
print(f"LER on output qubit: {stats.logical_error_rate:.4e}")
Multi-observable circuits
For circuits with k > 1 logical observables (transversal CNOT, Bell pairs, distillation), specify which ones count as errors:
pipeline = SimulationPipeline(
decoder_config=DecoderConfig("pymatching"),
target_observable_indices=[0], # only check logical 0 (ZZ observable of CNOT)
max_errors=200,
)
# Without target_observable_indices, any observable flip counts as an error.
Threshold sweep pattern
import numpy as np
from lightstim.noise.config import NoiseConfig
pipeline = SimulationPipeline(
decoder_config=DecoderConfig("pymatching"),
max_errors=100, max_shots=100_000_000, print_progress=False,
)
results = []
for d in [3, 5, 7]:
for p in np.logspace(-3, -1, 8):
circuit = build_circuit(d=d, p=p) # your build function
stats = pipeline.run(circuit)
results.append({
"d": d, "p": p,
"ler": stats.logical_error_rate,
"eb": stats.ler_error_bar(),
})
Working examples
benchmarks/memory/run_memory.py— full threshold sweep with checkpointing, argparse, CSV outputbenchmarks/logical_circuits/run_logical_circuits.py— multi-experiment runner pattern
Signals
- GitHub stars
- 46
- Forks
- 6
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
simulate-decode- Source
- github.com/qutone/lightstim