ILPDecoder is a Python package for maximum-likelihood quantum error correction decoding using integer linear programming (ILP). It turns parity-check matrices or Stim DetectorErrorModels into an ILP and solves it with a built-in backend out of the box. It is aimed at correctness-focused baselines, solver comparisons, and small-to-medium code studies rather than high-throughput production decoding.
Documentation: https://nzy1997.github.io/ILPDecoder/
What it does well:
- Out-of-the-box decoding with minimal setup.
- Inputs from parity-check matrices or Stim DetectorErrorModel.
- Maximum-likelihood decoding via weights or error probabilities.
- PyMatching-like API for easy experimentation.
When it is not a fit:
- Large code distances or high-shot workloads where ILP scaling dominates; use MWPM/BPOSD for throughput.
# Basic installation
pip install ilpdecoder
# With Stim support
pip install ilpdecoder[stim]
# With SciPy sparse-matrix support
pip install ilpdecoder[scipy]# Clone the repository
git clone https://github.com/nzy1997/ILPDecoder
cd ILPDecoder
# Create virtual environment (using uv)
uv venv
source .venv/bin/activate
# Install with dev dependencies
uv add highspy numpy scipy stim
uv add pyomo --dev
uv add pytest --dev
# Or using pip
pip install -e ".[dev]"# Activate virtual environment
source .venv/bin/activate
# Run all tests
pytest tests/ -v
# Run specific test file
pytest tests/test_decoder.py -v
# Run a quick functionality check
python main.pypython examples/basic_usage.py
python examples/surface_code_example.py
benchmark/.venv/bin/python benchmark/benchmark_decoders.py --shots 10000 --distance 3 --rounds 3 --noise 0.01import numpy as np
from ilpdecoder import Decoder
# Define a simple repetition code parity-check matrix
H = np.array([
[1, 1, 0, 0, 0],
[0, 1, 1, 0, 0],
[0, 0, 1, 1, 0],
[0, 0, 0, 1, 1],
])
# Create decoder
decoder = Decoder.from_parity_check_matrix(H)
# Decode a syndrome
syndrome = [1, 0, 0, 1]
correction = decoder.decode(syndrome)
print(f"Correction: {correction}")Note: passing SciPy sparse matrices requires scipy to be installed (e.g., pip install ilpdecoder[scipy]).
import stim
from ilpdecoder import Decoder
# Generate a surface code circuit
circuit = stim.Circuit.generated(
"surface_code:rotated_memory_x",
distance=3,
rounds=3,
after_clifford_depolarization=0.01
)
# Get detector error model
dem = circuit.detector_error_model(decompose_errors=True)
# Create decoder
decoder = Decoder.from_stim_dem(dem)
# Sample and decode
sampler = circuit.compile_detector_sampler()
detection_events, observables = sampler.sample(shots=100, separate_observables=True)
for i in range(10):
_, predicted_obs = decoder.decode(detection_events[i])
print(f"Shot {i}: predicted={predicted_obs}, actual={observables[i]}")- Only
error(p)lines are parsed; tags inerror[...]are ignored.detectorandlogical_observablemetadata lines are ignored.shift_detectorsoffsets are applied.repeatblocks are flattened by default; this can expand large DEMs.detector_separatoris unsupported and raises an error. - The
^separator is treated as whitespace and does not change parsing. - If you want to fail fast instead of flattening, pass
flatten_dem=False.
import numpy as np
from ilpdecoder import Decoder
H = np.array([[1, 1, 0], [0, 1, 1]])
error_probs = [0.1, 0.01, 0.1]
# Weights are computed automatically from probabilities
decoder = Decoder.from_parity_check_matrix(H, error_probabilities=error_probs)
syndrome = [1, 1]
correction, weight = decoder.decode(syndrome, return_weight=True)
print(f"ML correction: {correction}, weight: {weight}")Note: error_probabilities must be in (0, 0.5]; pass explicit weights for p > 0.5.
Install optional deps for the benchmarks:
pip install stim pymatching ldpcNotes:
- BPOSD runs with
max_iter=50,osd_order=0, andbp_method=minimum_sum.
benchmark/.venv/bin/python benchmark/benchmark_decoders.py --compare-ilp-solvers --ilp-solvers highs,scip,gurobi,cbc,glpk --shots 10000 --distance 3 --rounds 3 --noise 0.01Results from a local macOS arm64 run (shots=10000, your numbers will vary):
| Decoder | Time (ms/shot) | Logical Error Rate |
|---|---|---|
| ILP[highs] (direct) | 2.7469 | 1.610% |
| ILP[gurobi] (direct) | 0.5923 | 1.620% |
| ILP[scip] | 27.1241 | 1.620% |
| ILP[cbc] | 13.7808 | 1.620% |
| ILP[glpk] | 7.8176 | 1.610% |
| MWPM (pymatching) | 0.0034 | 2.090% |
| BPOSD (ldpc) | 0.0308 | 7.740% |
benchmark/.venv/bin/python benchmark/benchmark_decoders.py --noise-model code_capacity --compare-ilp-solvers --ilp-solvers highs,scip,gurobi,cbc,glpk --shots 10000 --distance 3 --rounds 1 --noise 0.01Results from a local macOS arm64 run (shots=10000, your numbers will vary):
| Decoder | Time (ms/shot) | Logical Error Rate |
|---|---|---|
| ILP[highs] (direct) | 3.1914 | 0.120% |
| ILP[gurobi] (direct) | 0.0826 | 0.120% |
| ILP[scip] | 22.6194 | 0.120% |
| ILP[cbc] | 9.8211 | 0.120% |
| ILP[glpk] | 4.7919 | 0.120% |
| MWPM (pymatching) | 0.0033 | 0.120% |
| BPOSD (ldpc) | 0.0029 | 0.120% |
benchmark/.venv/bin/python benchmark/benchmark_decoders.py --code-task color_code:memory_xyz --compare-ilp-solvers --ilp-solvers highs,scip,gurobi,cbc,glpk --shots 10000 --distance 3 --rounds 3 --noise 0.01Results from a local macOS arm64 run (shots=10000, your numbers will vary):
| Decoder | Time (ms/shot) | Logical Error Rate |
|---|---|---|
| ILP[highs] (direct) | 2.0008 | 4.510% |
| ILP[gurobi] (direct) | 0.3164 | 4.500% |
| ILP[scip] | 24.0461 | 4.500% |
| ILP[cbc] | 11.0780 | 4.510% |
| ILP[glpk] | 5.8961 | 4.500% |
| MWPM (pymatching) | 0.0041 | 13.610% |
| BPOSD (ldpc) | 0.0124 | 9.970% |
Main decoder class.
Class Methods:
from_parity_check_matrix(H, weights=None, error_probabilities=None, solver=None)- Create from parity-check matrixfrom_stim_dem(dem, solver=None, merge_parallel_edges=True, flatten_dem=True)- Create from Stim DetectorErrorModel
Instance Methods:
decode(syndrome, return_weight=False)- Decode a single syndromedecode_batch(syndromes)- Decode multiple syndromesset_solver(name, **options)- Switch solver
Properties:
num_detectors- Number of parity checks/detectorsnum_errors- Number of error mechanismsnum_observables- Number of logical observables (for DEM)solver_name- Current solver name
Returns a list of available solver names.
from ilpdecoder import get_available_solvers
print(get_available_solvers()) # e.g., ['scip', 'highs', 'cbc']MIT License