होम/ब्लॉग/Quantum Error Mitigation: A Practical Guide (And When It Lies to You)
Error CorrectionQiskitPerformance

Quantum Error Mitigation: A Practical Guide (And When It Lies to You)

Hands-on error mitigation for NISQ hardware — readout calibration, zero-noise extrapolation, Pauli twirling, and dynamical decoupling — plus the ZNE failure mode that manufactures fake improvements.

FreeQuantumComputing
·· 10 min read

You ran your circuit on real hardware. The answer is wrong. Not catastrophically wrong — just wrong enough to be useless. Error mitigation is the set of tricks that gets you from "wrong" to "usable" without waiting a decade for fault tolerance.

This guide covers what actually works, in the order you should try it, with runnable Qiskit code. It also covers the part most tutorials skip: how the most popular mitigation technique can produce a convincing improvement that is entirely fictitious, and the one-line sanity check that catches it.

Mitigation Is Not Correction

These get conflated constantly, so let's be precise.

Quantum error correction detects and fixes errors during the computation. It encodes one logical qubit across many physical qubits, measures syndromes with ancillas, and applies corrections in real time. It preserves arbitrary quantum states to arbitrary depth — but it demands hardware below the fault-tolerance threshold and roughly 1,000× qubit overhead. Our error correction primer walks through how the surface code does this.

Error mitigation does nothing during the computation. It runs the noisy circuit, collects noisy results, and applies classical statistics afterwards to estimate what a noiseless machine would have said.

The practical differences matter:

CorrectionMitigation
CostExtra qubits (~1000×)Extra shots (2–100×)
RecoversThe quantum stateAn expectation value
Scales toArbitrary depthShallow-to-moderate depth only
Available todayBarelyYes, right now

That last row is why mitigation dominates NISQ-era work. But note row three: mitigation's overhead grows exponentially with circuit depth and error rate. There is a depth beyond which no amount of shots rescues you. Knowing where that wall is — for your circuit, on your device — is the entire skill.

Step 1: Readout Error Mitigation (Do This First)

Measurement errors are the cheapest thing to fix and often the biggest single contributor. A superconducting qubit typically misreads 1–5% of the time. On 4 qubits at 3% each, you're losing ~11% of your signal before any gate error is counted.

The fix: characterize the readout with calibration circuits, then invert. Prepare each computational basis state, measure it, and record where the counts actually land. That gives you an assignment matrix M where M[i][j] is the probability of reading i when the true state was j. Your observed distribution is M @ p_true, so apply the pseudo-inverse.

import numpy as np
from qiskit import QuantumCircuit, transpile
from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel, depolarizing_error, ReadoutError

def build_noise(p1=0.002, p2=0.02, p_read=0.05):
    nm = NoiseModel()
    nm.add_all_qubit_quantum_error(depolarizing_error(p1, 1), ["rz", "sx", "x", "h"])
    nm.add_all_qubit_quantum_error(depolarizing_error(p2, 2), ["cx", "cz", "ecr"])
    nm.add_all_qubit_readout_error(
        ReadoutError([[1 - p_read, p_read], [p_read, 1 - p_read]])
    )
    return nm

backend = AerSimulator(noise_model=build_noise())
SHOTS = 40000

def calibration_matrix(n, backend, shots=20000):
    M = np.zeros((2**n, 2**n))
    for i in range(2**n):
        qc = QuantumCircuit(n, n)
        bits = format(i, f"0{n}b")[::-1]      # Qiskit is little-endian
        for q, b in enumerate(bits):
            if b == "1":
                qc.x(q)
        qc.measure(range(n), range(n))
        counts = backend.run(transpile(qc, backend), shots=shots).result().get_counts()
        for key, v in counts.items():
            M[int(key, 2), i] = v / shots
    return M

n = 2
Minv = np.linalg.pinv(calibration_matrix(n, backend))

bell = QuantumCircuit(2, 2)
bell.h(0)
bell.cx(0, 1)
bell.measure([0, 1], [0, 1])

raw = backend.run(transpile(bell, backend), shots=SHOTS).result().get_counts()
vec = np.array([raw.get(format(i, f"0{n}b"), 0) for i in range(2**n)], float) / SHOTS

mit = np.clip(Minv @ vec, 0, None)
mit /= mit.sum()

print("raw      :", {format(i, "02b"): round(vec[i], 4) for i in range(4)})
print("mitigated:", {format(i, "02b"): round(mit[i], 4) for i in range(4)})

Actual output from running this (Qiskit 2.5.0, Aer 0.17.2, 5% readout error):

raw      : {'00': 0.4456, '01': 0.053, '10': 0.0501, '11': 0.4513}
mitigated: {'00': 0.4931, '01': 0.0048, '10': 0.0033, '11': 0.4988}

A Bell state should produce only 00 and 11. The raw run leaks 10.3% into forbidden outcomes; after mitigation, 0.8%. That is a 13× reduction in spurious counts for the cost of 2^n short calibration circuits.

Two caveats. The 2^n scaling means full calibration matrices die above ~10 qubits — beyond that, use tensored (per-qubit) or correlated-subset calibration. And matrix inversion amplifies shot noise, which is why the code clips negatives; run your calibration circuits with more shots than your actual experiment, not fewer.

Step 2: Dynamical Decoupling (Nearly Free)

Idle qubits are not safe qubits. While one part of your circuit executes, everything else is dephasing at its T2 rate. Dynamical decoupling inserts pulse sequences — typically X–delay–X–delay — into idle windows, echoing away slow, low-frequency noise.

In Qiskit this is a transpilation pass, not an algorithm change:

from qiskit.transpiler import PassManager
from qiskit.transpiler.passes import ALAPScheduleAnalysis, PadDynamicalDecoupling
from qiskit.circuit.library import XGate

pm = PassManager([
    ALAPScheduleAnalysis(durations=backend.instruction_durations),
    PadDynamicalDecoupling(
        durations=backend.instruction_durations,
        dd_sequence=[XGate(), XGate()],   # XX sequence
    ),
])
dd_circuit = pm.run(transpiled_circuit)

Cost: zero extra shots. Typical gain: 5–20% fidelity improvement on circuits with heterogeneous qubit usage. Circuits where every qubit is busy every cycle see nothing. Try it — it's the best effort-to-payoff ratio in the whole toolkit.

Step 3: Pauli Twirling / Randomized Compiling

Coherent errors are the nasty kind. A systematic over-rotation of 0.5° per gate doesn't average out — it accumulates linearly in amplitude, so error in the expectation value grows quadratically with depth. Stochastic errors merely accumulate additively.

Twirling converts the former into the latter. You compile many logically equivalent copies of your circuit, each wrapping the two-qubit gates in randomly chosen Pauli operators (with compensating Paulis so the ideal unitary is unchanged), then average the results. Coherent error terms average toward zero; what survives is a well-behaved stochastic Pauli channel.

Twirling rarely improves accuracy much on its own. Its real value is that it makes the noise model-able — and every other technique on this list (ZNE, PEC) assumes a stochastic noise channel. Twirling is what makes that assumption true. Run it underneath your other mitigation, not instead of it.

Step 4: Zero-Noise Extrapolation

ZNE is the technique behind most headline NISQ results, including IBM's quantum utility demonstration on a 127-qubit Eagle processor.

The idea: you can't reduce hardware noise, but you can increase it in a controlled way. Run the same circuit at noise scale factors λ = 1, 3, 5 (via unitary folding: replace U with U(U†U)^k, which is logically identity-preserving but physically 3× or 5× as noisy), then fit a curve through the results and extrapolate back to λ = 0.

def fold_global(qc, scale):
    """Odd integer scale: U -> U (U^dag U)^k"""
    k = (scale - 1) // 2
    out, inv = qc.copy(), qc.inverse()
    for _ in range(k):
        out = out.compose(inv).compose(qc)
    return out

def expval_parity(qc, backend, shots=40000):
    c = qc.copy()
    c.measure_all()
    counts = backend.run(
        transpile(c, backend, optimization_level=0), shots=shots
    ).result().get_counts()
    return sum((-1) ** (k.replace(" ", "").count("1") % 2) * v
               for k, v in counts.items()) / shots

# 4-qubit GHZ padded with cancelling CX pairs; ideal <ZZZZ> = +1.0
base = QuantumCircuit(4)
base.h(0)
for q in range(3):
    base.cx(q, q + 1)
for _ in range(4):
    for q in range(3):
        base.cx(q, q + 1)
        base.cx(q, q + 1)

scales = [1, 3, 5]
vals = [expval_parity(fold_global(base, s), backend) for s in scales]
zne = np.polyval(np.polyfit(scales, vals, 2), 0)   # Richardson, order 2
print(vals, "->", zne)

Real measured output, same versions, readout error switched off to isolate gate noise. With a 0.4% two-qubit error rate:

scale 1: 0.8973   scale 3: 0.7286   scale 5: 0.5767
ZNE estimate: 0.988   (exact: 1.000)

Raw error 0.103, mitigated error 0.012. ZNE removed 88% of the bias. This is what the technique looks like when it works.

When ZNE Lies

Now turn the two-qubit error rate up to 4% and rerun the identical code:

scale 1: 0.3311   scale 3: 0.0320   scale 5: 0.0008
ZNE estimate: 0.581   (exact: 1.000)

Look at what a results table would show: raw 0.33, mitigated 0.58, ideal 1.00. Mitigation "recovered" 76% more signal! It looks like a triumph. It is garbage. The λ=5 point is 0.0008 — the signal is gone. The fit is being driven by two nearly-zero numbers and one noisy one, and it happens to land somewhere between raw and truth because the curve shape forces it to.

This is precisely the failure mode Köster and Mauerer characterize in Artefactual Improvements in Zero-Noise Extrapolation (2026). Their finding: once noise amplification pushes past usable signal, Richardson extrapolation degenerates into a fixed rescaling of a single noisy measurement. The output is no longer a function of the noise amplification it supposedly corrects for — the amplified points contribute nothing but their own vanishing. On real hardware they measured runs that overshot the ideal value by up to 21%.

Their sharpest result is a negative control. They defined "garbage folding" — circuit modifications with no relationship to the identity-insertion ZNE requires, i.e. deliberately meaningless — and pushed those through the same pipeline. It produced larger apparent improvements than legitimate folding. If nonsense scores better than the real method, the score is not measuring what you think it is.

The operational lesson: improvement magnitude never validates a mitigation result. A number moving toward the expected answer is not evidence the method worked; it may be evidence the method collapsed into a rescaling that happens to move numbers in that direction.

The Sanity Checks That Actually Catch This

Three of them, none expensive:

1. Inspect the raw scale-factor points, always. If your highest-λ value has collapsed toward the fully-depolarized value (0 for a parity observable, 1/2^n for a probability), discard it. Fitting through dead points is fitting noise. In the failing run above, one glance at 0.0008 tells you the extrapolation is unsupported.

2. Run a negative control. Build a variant of your folding that shouldn't work — random gates of comparable depth instead of U†U pairs — and push it through the identical pipeline. In my failing case, garbage folding returned 0.186 against a raw value of 0.329: it moved the answer away from truth. That divergence is the flag. Whether your control produces spurious improvement or spurious degradation, any large response to meaningless input means your pipeline is fitting artifacts.

3. Verify against a simulator at small scale. Run the same mitigation pipeline on a problem size you can compute exactly. If ZNE doesn't recover the known answer at 6 qubits, nothing about 60 qubits will be trustworthy.

Probabilistic Error Cancellation (Briefly)

PEC is the rigorous option. Characterize the noise channel precisely, express the inverse channel as a quasi-probability distribution over implementable operations, then sample from it with signed weights. It's unbiased in principle — no extrapolation guesswork.

The cost is brutal. Sampling overhead scales as γ², where γ grows exponentially in circuit depth × error rate. A modest circuit can need 100–10,000× more shots than the unmitigated run, on top of an expensive noise-characterization phase (gate set tomography or cycle benchmarking). PEC is appropriate for small, high-value circuits where you need a trustworthy number and can afford the shot budget. For most work, it isn't.

What To Actually Do

In order:

  1. Readout mitigation — always. Cheap, bounded, low-risk.
  2. Dynamical decoupling — always, if your circuit has idle qubits. Free.
  3. Twirling — if you plan to use ZNE or PEC, so the noise is stochastic.
  4. ZNE — only with scale-factor inspection and a negative control.
  5. PEC — only when the circuit is small and the answer matters more than the cost.

Skip mitigation entirely when: your raw signal is already below ~0.2 of its ideal magnitude (nothing to extrapolate from), your circuit is deep enough that the λ=3 run is fully depolarized, or you're doing variational optimization where the optimizer only needs the gradient direction — bias that's roughly constant across parameter space often cancels out, and you're better off spending those shots on more iterations. Techniques for that are in cutting your shot count.

Mitigation buys you a few years of useful computation on machines that shouldn't be useful yet. It does not buy you fault tolerance, and it will happily hand you a confident wrong answer if you skip the controls. Get comfortable with the Qiskit tooling, always sanity-check against exact simulation, and treat any unvalidated improvement as a hypothesis rather than a result.

Related: Error correction explained · ZNE artefacts paper · Quantum utility