Logical Error Rate & Fidelity Tutorial 08 of 10
The verdict of one shot
A round of error correction ends with a single yes/no question: did the logical qubit survive? We run the noisy circuit, read out the syndrome, hand it to a decoder, and the decoder announces which logical observables it thinks were flipped: its prediction. We also know the truth, because in simulation we tracked the true errors. The shot is a logical error when the prediction disagrees with the truth.
That is the whole verdict, and it is what qliff computes: predicted / true for the logical observable. Click a cell to flip the decoder's guess and watch the running rate.
Shot verdicts
Legend:
- green = survived (predicted = true)
- red = logical error (predicted != true)
Fidelity and LER
Stack up many shots and the rate of red cells is the logical error rate. Its complement is the logical fidelity, the fraction of shots that came through clean. In qliff this is one line (codes.py):
Here
A subtlety appears when a code protects more than one logical qubit (the toric code has two). Each shot then has a row of observables. qliff's rule is strict: the shot fails if any column disagrees, in code
Try the np.any rule
It's a Monte-Carlo estimate, so it has error bars
We never see the true LER directly; we estimate it by sampling a finite number of shots
The demo below runs a seeded Monte-Carlo in your browser: the distance-
Convergence with N
Legend:
- purple line = running LER estimate
- band =
binomial - green dash = exact LER
An error bar from 37 failures in N = 4000 shots
- Count the verdicts: the decoder was wrong on 37 of
shots (qliff: a shot fails iff predicted observed). - The Monte-Carlo estimate is the failure fraction:
. - Fidelity is its complement:
. - The binomial standard error:
. - To halve the bar you need
the shots ( ): drops to .
Result: Report
Why rare errors are expensive
Push p down or d up and the true LER can fall below 0.45%. With few shots you may see zero errors and report LER = 0 with a deceptively tiny error bar. To resolve a LER of
The weighted case: coherent noise
Coherent noise (a small over-rotation, amplitude damping) is not a random Pauli flip, so no valid detector-error model exists to sample from. qliff's way around this is importance sampling: it draws stabilizer trajectories from a quasiprobability and attaches a signed weight threshold.py: _weighted_error_rate):
with the result clamped to
The negativity logical_error_rate(..., stratify=True) runs.
Coherent shot cost
Legend:
- Pauli = binomial shot budget
- coherent = budget inflated by
The gamma cost: how many more shots at gamma = 2?
- Take a true LER of
(the repetition code at ). - A Pauli (binomial) run needs
shots for a 10% relative bar. - Coherent noise carries weights of magnitude
, inflating the variance by . - So the same bar now costs
shots.
Result: A modest
The sweep: LER versus p
One LER is a single dot. The interesting object is the curve: how the logical error rate responds as we dial the physical rate sweep(circuit_fn, p_values, ...) does this: it rebuilds the circuit at each
Below, the smooth line is the exact repetition-code LER and the points are seeded Monte-Carlo with binomial error bars. At small
LER vs p sweep
Legend:
- curve = exact LER(p)
- point = Monte-Carlo (
)
The threshold plot
This plot is the central result of the field. Draw LER versus
- Below
: bigger gives lower LER. Adding qubits drives the error rate toward zero: error correction works, and works better the more you scale. - Above
: bigger gives higher LER. Your hardware is too noisy; adding qubits only adds places to fail.
The plot below uses a standard phenomenological model (it is a model, not a fit):
Threshold crossing
Legend:
- line = one distance
= curves cross - break-even =
Read it off
Find the crossing. To its left, the steepest (highest-
The whole pipeline
Every number on this page comes from the same five-stage loop, repeated
Stage 1 injects errors (Pauli channels sample directly; coherent channels need the weighted sampler of Tutorial 06, or its stratified refinement). Stage 2 reads the stabilizers into a syndrome. Stage 3 is the decoder (MWPM, belief propagation, or a tensor network from the first three pages). Stage 4 is the one-bit verdict of section 1. Stage 5 averages the verdicts into the LER and its error bar.
Implementation
The same pipeline at three levels of detail in the tabs below: language-free pseudocode, a self-contained NumPy miniature you can check by hand, and the qliff API.
Pseudocode. Everything on this page reduces to a counting loop plus one square root; the threshold plot is that loop inside two more.
Std Python. A miniature end-to-end pipeline with no qliff: the distance-
Qliff. The same pipeline against the qliff API: logical_error_rate wraps sample -> decode -> compare for one circuit, and sweep (or the streaming isweep) repeats it across physical rates with the seed held fixed. The first stanza builds the pieces by hand with DetectorSampler, make_decoder, and logical_fidelity; import qliff.qec.threshold directly rather than through qliff.qec, so that plain import qliff stays numpy-only.
estimate_ler(code, p, shots, seed):
circuit = build_noisy_memory(code, p) # syndrome-extraction rounds at rate p
detectors, truth = sample(circuit, shots, seed)
errors = 0
for each shot s:
prediction = decode(detectors[s]) # decoder's guess at the observable flips
if prediction != truth[s]: # any observable wrong -> logical error
errors = errors + 1
ler = errors / shots
stderr = sqrt(ler * (1 - ler) / shots) # binomial error bar
return ler, stderr
threshold_plot(code_family, distances, p_values):
for d in distances:
for p in p_values:
plot(p, estimate_ler(code_family(d), p, shots, seed))
# one curve per distance; the curves cross at the threshold p_thfrom math import comb
import numpy as np
def repetition_ler(d, p, shots, seed):
"""Monte-Carlo LER of a distance-d repetition code under iid bit flips."""
rng = np.random.default_rng(seed)
flips = rng.random((shots, d)) < p
# majority vote fails exactly when more than d/2 bits flipped
wrong = np.sum(flips, axis=1) > d / 2
ler = float(np.mean(wrong))
stderr = float(np.sqrt(ler * (1.0 - ler) / shots))
return ler, stderr
def exact_ler(d, p):
"""Exact LER: the binomial tail P[more than d/2 of d bits flip]."""
tail = 0.0
for k in range(d // 2 + 1, d + 1):
tail += comb(d, k) * p**k * (1.0 - p) ** (d - k)
return tail
shots = 200000
for p in [0.05, 0.6]:
for d in [3, 5]:
ler, stderr = repetition_ler(d, p, shots, seed=42)
print(f"p={p} d={d}: LER = {ler:.5f} +/- {stderr:.5f} (exact {exact_ler(d, p):.5f})")
# -> p=0.05 d=3: LER = 0.00710 +/- 0.00019 (exact 0.00725)
# -> p=0.05 d=5: LER = 0.00099 +/- 0.00007 (exact 0.00116)
# -> p=0.6 d=3: LER = 0.64917 +/- 0.00107 (exact 0.64800)
# -> p=0.6 d=5: LER = 0.68407 +/- 0.00104 (exact 0.68256)from qliff.qec.codes import logical_fidelity, repetition_code, rotated_surface_code
from qliff.qec.decoder import make_decoder
from qliff.qec.dem import DetectorErrorModel
from qliff.qec.sampler import DetectorSampler
from qliff.qec.threshold import logical_error_rate, sweep
# the pipeline by hand: sample -> decode -> compare
circuit = repetition_code(distance=5, rounds=3, p=0.05, channel="X_ERROR")
dets, obs = DetectorSampler(circuit).sample(4000, seed=7)
decoder = make_decoder("mwpm", DetectorErrorModel(circuit))
predictions = decoder.decode_batch(dets)
print(f"LER = {1.0 - logical_fidelity(predictions, obs):.4f}")
# -> LER = 0.0022
# the same point in one call, with the binomial error bar included
ler, stderr = logical_error_rate(circuit, decoder_name="mwpm", shots=4000, seed=7)
print(f"LER = {ler:.4f} +/- {stderr:.4f}")
# -> LER = 0.0022 +/- 0.0007
# the sweep: circuit_fn(p) rebuilds the code at each physical rate
def surface(d):
def circuit_fn(p):
return rotated_surface_code(rows=d, cols=d, rounds=3, p=p)
return circuit_fn
for d in [3, 5]:
curve = sweep(
surface(d),
p_values=[0.01, 0.08],
decoder_name="mwpm",
shots=1000,
seed=11,
)
for p, ler, stderr in curve:
print(f"d={d} p={p}: LER = {ler:.4f} +/- {stderr:.4f}")
# -> d=3 p=0.01: LER = 0.0030 +/- 0.0017
# -> d=3 p=0.08: LER = 0.1090 +/- 0.0099
# -> d=5 p=0.01: LER = 0.0010 +/- 0.0010
# -> d=5 p=0.08: LER = 0.0750 +/- 0.0083Both physical rates sit below this circuit family's threshold, so
The series in summary
Three decoders turn a syndrome into a correction: matching, belief propagation, and tensor networks. Three noise pages model the input: coherent channels as signed quasiprobabilities, sampled into weighted trajectories, then stratified by fault count so the weights' common factor cancels. This final page is the scoreboard: feed decoder and noise into sample -> decode -> compare, average the verdicts, and read the logical error rate, its error bar, and the threshold that says whether scaling up will save you.