| name | simulate-decode |
| description | Run 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.
|
| user-invocable | true |
Simulate and Decode
Takes a noisy stim.Circuit and runs it through SimulationPipeline.
Step 0: Get a noisy circuit
noisy = builder.build_noisy_circuit(NoiseConfig(p_2q=1e-3, p_meas=1e-3), "circuit_level")
noisy = experiment.build()
SimulationPipeline API
from lightstim.simulation.decoder_backend import SimulationPipeline, DecoderConfig
pipeline = SimulationPipeline(
decoder_config=DecoderConfig("pymatching"),
max_errors=200,
max_shots=1_000_000,
batch_size=10_000,
num_workers=4,
print_progress=False,
)
stats = pipeline.run(noisy_circuit)
print(stats.logical_error_rate)
print(stats.ler_error_bar())
print(stats.ler_error_bar(z=1.0))
print(stats.shots)
print(stats.post_selected_shots)
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,
max_shots=100_000_000,
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
pymatching whenever the detector error model is graphlike / has no
hyperedges. This includes ordinary surface-code memory and surface-code
lattice-surgery circuits.
- Use
mwpf or bposd when the circuit has hyperedges or important correlated
fault mechanisms that should not be decomposed away.
- Use
bposd for LDPC codes such as BB/PQRM when BP+OSD is the intended decoder.
- Use
mwpf for 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).
pipeline = SimulationPipeline(
decoder_config=DecoderConfig("pymatching"),
max_errors=200,
)
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,
target_observable_indices=target_obs,
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],
max_errors=200,
)
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)
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 output
benchmarks/logical_circuits/run_logical_circuits.py — multi-experiment runner pattern