simulate-decode · diff
git:20260520.26da6bb to git:20260526.5b92aea
108 added, 30 removed. Audit A to A.
---
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, or plot logical error rates.
- Also trigger when the user has a built circuit and asks "how do I know if it's
- working?" or "what's the error rate?"
+ 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 built `stim.Circuit`, runs it through `SimulationPipeline`, and
- returns `SimulationStats` with `logical_error_rate`, `shots`, and `errors`.
+ Takes a noisy `stim.Circuit` and runs it through `SimulationPipeline`.
- ## How to help the user
+ ## Step 0: Get a noisy circuit
- 1. Confirm they have (or help them build) a noisy circuit — see
- `memory-experiment` skill if needed.
- 2. Ask which decoder they want. Default is `pymatching` (always available).
- `bposd` and `mwpf` require optional dependencies.
- 3. Ask how many errors to collect (`max_errors`). 100–200 is fast; 1000+ gives
- tight error bars.
- 4. Read `scripts/template.py` and adapt for their circuit and parameters.
- 5. If they want a threshold plot (LER vs p for multiple distances), show the
- sweep loop pattern from the template.
+ ```python
+ # From CircuitBuilder (custom protocol):
+ noisy = builder.build_noisy_circuit(NoiseConfig(p_2q=1e-3, p_meas=1e-3), "circuit_level")
- ## SimulationPipeline API
+ # 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(name='pymatching'), # or 'bposd', 'mwpf'
- max_errors=200, # stop after this many logical errors
+ 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(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)
- print(stats.post_selected_shots) # shots that survived post-selection (if any)
+ 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 | Notes |
+ | Name | Backend | When to use |
|---|---|---|
- | `pymatching` | cpu | Always available, fast, approximate for hyperedges |
- | `bposd` | cpu / gpu | Requires `stimbposd`; better for LDPC codes |
- | `mwpf` | cpu | Requires `mwpf`; exact, handles hyperedges |
- | `nv-qldpc-decoder` | gpu | Requires `cudaq-qec`; GPU BP+OSD |
+ | `"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 sweep over (distance, p) pairs.
+ Read `scripts/template.py` for a complete threshold sweep with progress tracking
+ and CSV output.