A static analysis tool for quantum error correction
Project description
emlint
stim simulates. sinter samples. emlint verifies.
emlint is a static linter for Detector Error Models (DEMs). A DEM describes a circuit's noise as a list of error mechanisms — independent faults, each with a probability and a syndrome footprint: which detectors it fires and which logical observables it flips. It catches structural bugs in milliseconds — before you run a single shot.
The problem
A DETECTOR instruction omitted in round 2 of a d=5 surface code leaves
detector D17 wired to nothing. The decoder silently miscorrects. The logical
error rate rises. The bug is invisible until ~10⁶ Sinter shots (≈45 minutes
on a standard laptop).
$ emlint check examples/buggy.dem
Detectors: 24 Observables: 1 Error mechanisms: 35
✓ detectability: All error mechanisms that flip observables also trigger detectors.
✗ sensitivity: 1 detector(s) are never triggered by any error mechanism.
Counter-example: Detector(s) not triggered by any error mechanism: D17
✓ observable_coverage: All declared observables are flipped by at least one error mechanism.
✓ probability_bounds: All error probabilities are within (0, 0.5].
✓ duplicates: No duplicate error mechanisms found.
✓ correctability: All syndromes map to a unique observable set.
2ms. One line. No simulation. D17 carries coordinates (1, 0, 2) — round 2,
position (1, 0) — so you know exactly where in the circuit to look.
$ emlint check examples/fixed.dem
✓ detectability ✓ sensitivity ✓ observable_coverage
✓ probability_bounds ✓ duplicates ✓ correctability
Installation
pip install emlint
Usage
CLI
emlint check path/to/circuit.dem
emlint check path/to/circuit.dem --format json
emlint check path/to/circuit.dem --check detectability,sensitivity
emlint check path/to/circuit.dem --severity error
Exit code 0 when all checks pass. Exit code 1 when any error-severity
check fails. Exit code 2 when there are warnings but no errors (usable in CI
to optionally escalate warnings without breaking the build).
--severity error suppresses warning-severity findings entirely — only
error-severity violations appear in output and affect the exit code. Useful
in CI pipelines where warnings are acknowledged but should not break the build.
Python API
import stim
import emlint
circuit = stim.Circuit.generated(
"surface_code:rotated_memory_z",
rounds=3,
distance=5,
after_clifford_depolarization=0.001,
)
report = emlint.check(circuit.detector_error_model(decompose_errors=True))
print(emlint.format_text(report))
emlint.check() also accepts a pathlib.Path or a raw DEM string.
CI integration
Add to .github/workflows/qec.yml:
- name: Lint DEM
run: pip install emlint && emlint check circuit.dem
Checks
| Check | What it catches | Severity |
|---|---|---|
detectability |
Errors that flip observables but trigger no detectors (undetectable) | error |
sensitivity |
Detectors never triggered by any error mechanism (dead detectors) | warning |
observable_coverage |
Logical observables never flipped by any error mechanism | error |
probability_bounds |
Error probabilities outside (0, 0.5] |
error |
duplicates |
Error mechanisms with identical (detector, observable) signatures | warning |
correctability |
Syndromes mapping to multiple distinct observable sets | warning |
All checks operate on the standalone .dem file — no original circuit required.
Each failing check produces a counter-example pointing to the specific mechanism
or detector at fault.
Connecting a failed check to the circuit bug
When a check fails, the counter-example tells you where in the DEM the problem is. The table below tells you what circuit-level mistake most likely produced it and where to look in your circuit source.
detectability fails
error(0.001) L0 — flips observable but triggers 0 detectors
The listed mechanism flips a logical observable but no detector fires — the decoder has no syndrome signal to correct it.
What went wrong in the circuit:
- A
DETECTORinstruction was omitted from a syndrome measurement round. The round still runs and produces data, but no detector was wired to it. OBSERVABLE_INCLUDEtargets a qubit that is outside the fault support of every modelled error (wrong qubit index, wrong basis).
Where to look: search your circuit for the round that should cover the data
qubits referenced in the failing mechanism's observable. Check that every syndrome
extraction round ends with a DETECTOR pointing at the right ancilla measurement.
sensitivity fails
D17@(1,0,2) not triggered by any error mechanism
The listed detector is declared but no error mechanism ever fires it.
What went wrong in the circuit:
- The ancilla qubit for this detector is measuring in the wrong Pauli basis
(
XvsZmismatch), so the errors that should flip it pass through silently. - The detector's coordinates point to the wrong spatial position — the qubit index
in
DETECTOR rec[…]is off by one. - A syndrome round was accidentally removed from the circuit, leaving its detector declaration orphaned.
Where to look: the detector's coordinates (shown as D17@(col,row,round))
identify the syndrome round and stabilizer position. Verify that the corresponding
ancilla qubit is connected to data qubits via the expected CNOT schedule, and that
the noise model includes errors on those CNOTs.
observable_coverage fails
L1 not flipped by any error mechanism
The listed logical observable is declared but no physical error can flip it.
What went wrong in the circuit:
OBSERVABLE_INCLUDEon the wrong qubit or at the wrong point in the circuit (e.g. applied to an ancilla instead of a data qubit, or after a reset that erases the correlation).- Observable index off-by-one in a multi-observable code.
Where to look: the counter-example gives you the observable index (e.g.
L1). Search your circuit source for every OBSERVABLE_INCLUDE with that
index. Verify that the qubit it references is part of the logical operator's
support and that errors on that qubit survive to the measurement record.
(emlint check --format json outputs counter-examples as structured data,
making it easy to script a targeted search when the circuit is auto-generated.)
probability_bounds fails
error(0.0) D0 D1 — zero probability
error(nan) D3 — NaN probability
An error mechanism has a probability of exactly 0 (a no-op), is negative, or is NaN — all of which indicate a fault in DEM generation, not a physical noise model.
What went wrong in the circuit / DEM generation script:
- Numerical underflow: a product of small probabilities rounded to 0 instead of being pruned or clamped.
- Naive addition of two complementary probabilities exceeded 1, producing a negative remainder.
- A copy-paste error in a manual DEM left a placeholder (
nan,-1,0) instead of a real probability.
Where to look: the mechanism's detector and observable targets identify which fault path the DEM compiler was trying to express. Trace that fault path back to the noise model parameters in your DEM generation script.
duplicates fails
error(0.001, 0.001) share signature D3 D7; XOR-fused probability is 0.001998
Two or more mechanisms share the same (detectors, observables) signature — the
same physical fault path is counted more than once.
What went wrong in the circuit / DEM assembly:
- Sub-circuit DEMs were concatenated (e.g. bulk + boundary + initialisation)
without merging mechanisms that touch the same fault path. The correct operation
is XOR-fusion:
p_eff = p1(1−p2) + p2(1−p1). - An X error and a Y error on the same data qubit at a repetition code boundary
each trigger the same detector set and flip the same observable (legitimate
physics; use
--severity errorin CI to suppress this warning for known-correct code families).
Where to look: if the duplicate arises from concatenation, find the boundary between the sub-circuits where the mechanism originates. If you are manually composing DEMs with Python, apply XOR-fusion instead of list concatenation.
correctability fails
syndrome {D3 D7} maps to observable sets {L0}, {L1}
The same detector syndrome is produced by mechanisms that flip different observables — the decoder cannot determine the correct logical correction.
What went wrong in the circuit:
- Two physically distinct errors (e.g. an X error on qubit A and a Z error on qubit B) produce the same syndrome but flip different logical observables. This is a genuine code distance problem: the code cannot distinguish them.
- The DEM was generated with
decompose_errors=True, which breaks hyperedge mechanisms into sub-mechanisms that can create artificial syndrome collisions (use--severity errorto suppress this known artefact in CI). - A degenerate code family (colour codes) has syndromes that legitimately map to multiple valid logical corrections — this is an expected property, not a bug.
Where to look: the counter-example gives you the exact syndrome (e.g.
{D3 D7}) and the conflicting observable sets. Search your .dem file for
every error(…) line whose detector targets match that set — those are the two
colliding mechanisms. In the original circuit, find the gate locations
corresponding to each mechanism's targets (the detector coordinates annotate
the round and spatial position). If both locations are in the same Pauli
basis and the same round, this is likely a genuine distance-1 path. If they
are in different bases or rounds, look for a missing stabilizer measurement
or a mis-wired CNOT that merges two logically distinct fault paths.
Scope note: this check examines each mechanism individually. It does not detect cases where two co-occurring faults combine to produce a syndrome that maps to conflicting corrections — that is the code distance problem and is not checked here. A future version will address multi-fault correctability via compositional reasoning (pre/post-conditions on circuit segments).
qLDPC & lifted-product codes
emlint accepts any DEM, including those generated from hypergraph product (HGP) and lifted-product (Bivariate Bicycle) codes. Pass the DEM from any source:
# From a qldpc-generated code
import qldpc, stim, emlint
classical = qldpc.codes.ClassicalCode.random(7, 5, seed=0)
code = qldpc.codes.HGPCode(classical)
noise = qldpc.circuits.DepolarizingNoiseModel(0.001)
circuit = qldpc.circuits.get_memory_experiment(code, num_rounds=5, noise_model=noise)
report = emlint.check(circuit.detector_error_model(decompose_errors=False))
The six structural checks apply universally — they do not assume surface code topology and do not require knowledge of the underlying code family.
Known false positives
correctability with decompose_errors=True
When a DEM is generated with stim's decompose_errors=True, the hyperedge
decomposition can create sub-mechanism pairs that share the same detector set
but differ in observables — a syndrome collision that does not exist in the
original hyperedge model. The check fires at warning severity.
Mitigation: use --severity error in CI, or pass decompose_errors=False
to get a clean correctability result with no artefacts.
duplicates at boundaries in degenerate code families
Degenerate fault paths (e.g. X and Y errors at a repetition code boundary that produce the same syndrome and flip the same observable) are correctly flagged as duplicates. The warning is accurate — the XOR-fused probability is shown — but may not require user action in degenerate code families.
Mitigation: use --severity error in CI to suppress.
License
Apache 2.0
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file emlint-0.1.0.tar.gz.
File metadata
- Download URL: emlint-0.1.0.tar.gz
- Upload date:
- Size: 29.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ec4e6c62427ffd0db6613dc22c42b060f9e262f75bc595e1ce359e137f323ba1
|
|
| MD5 |
268372c9d5b635dc9c9e8d3a6e4d3cc8
|
|
| BLAKE2b-256 |
5ec86900b94e713f074f6bc8340647970a7432824f6c7f72b34075cfe4f259e2
|
File details
Details for the file emlint-0.1.0-py3-none-any.whl.
File metadata
- Download URL: emlint-0.1.0-py3-none-any.whl
- Upload date:
- Size: 15.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fbff712e9a8c1dc8cc8c0909edcb10decfe68942c1d6270830e7d111763c95db
|
|
| MD5 |
260442928cb284ee18757b432ef8e569
|
|
| BLAKE2b-256 |
d3d5574cef9956c4378968ad6b227c9fae994cdb9dddca63974f0073390a0015
|