simulate-decode · git:20260526.5b92aea · 2026-05-26 · sha256 466e780ece63b1a2
simulate-decode git:20260526.5b92aeaA
Immutable. This exact content is served forever at /api/v1/blob/466e780ece63b1a2.
---
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
```python
# 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
```python
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)
```
## 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:**
- Surface codes (rotated, unrotated, toric): `pymatching`
- LDPC codes (BB, PQRM) with no hyperedges: `bposd`
- CrossLS / PQRM (X-stabs weight > 2 → hyperedges): `mwpf`
## Post-selection
Use post-selection for state injection (discard shots with injection errors) or
distillation (post-select on magic state purity).
```python
# 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:
```python
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
```python
import numpy as np
from lightstim.noise.config import NoiseConfig
pipeline = SimulationPipeline(
decoder_config=DecoderConfig("pymatching"),
max_errors=200, max_shots=500_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(),
})
```
## Reference script
Read `scripts/template.py` for a complete threshold sweep with progress tracking
and CSV output.