Case Study 1: The Transpiler Deleted the Errors

The result

A team is building an error-correction demonstration. The circuit is standard: encode a qubit into the three-qubit bit-flip code, apply noise, extract a syndrome, correct, decode, measure.

They sweep the physical error rate and get this:

     p     unencoded    encoded
  0.01        0.0100     0.0000
  0.05        0.0500     0.0000
  0.10        0.1000     0.0000
  0.20        0.2000     0.0000
  0.30        0.3000     0.0000
  0.50        0.5000     0.0000
  0.60        0.6000     0.0000

Perfect correction at every noise rate. Reproducible across runs and seeds.

It is worth pausing on how good this looks. The curve is not noisy. It is not approximately right. Every entry is exactly zero, which is what a correct implementation of an ideal code might plausibly produce at low $p$ — and the low-$p$ rows are the ones a reviewer scans first.

The row that gives it away

$p = 0.6$.

At a physical error rate of 0.6, each of three qubits flips more often than not. A majority vote is wrong whenever two or more flip, which is $3p^2 - 2p^3 = 0.648$. The code should be failing 65% of the time. A three-qubit code cannot correct two errors — that is what distance 3 means.

A result that is impossible in principle is not a good result. It is a broken measurement.

That is the whole diagnostic move, and it is available before any debugging: find the parameter regime where you know what must happen, and check there. Low $p$ tells you nothing, because "very small" and "zero" look alike. High $p$ tells you everything, because the correct answer is loudly non-zero.

What was wrong

Nothing in the code, the encoder, the syndrome extraction, or the decoder. The problem was one line:

transpiled = transpile(qc, sim)          # <-- the bug

Aer attaches noise to gates. The circuit marks its noise locations with identity gates:

for q in data:
    qc.id(q)

and the noise model attaches an $X$ error to id on each data qubit. But id gates are, by construction, removable — and the transpiler removes them:

   optimization_level=0:  id gates surviving = 3   <- noise attaches here
   optimization_level=1:  id gates surviving = 0   <- NOISE SLOTS DELETED

Qiskit's default is not level 0. So transpile(qc, sim) silently produced a circuit with no noise locations, the noise model found nothing to attach to, and the simulator faithfully executed a noiseless circuit. Logical error rate: exactly zero, at every $p$, forever.

The transpiler did nothing wrong. Removing identity gates is correct behaviour — Chapter 10 covered exactly this optimization as a feature. The gate that marks "noise goes here" and the gate that means "do nothing" are the same gate, and only one of those meanings survives optimization.

The fix, and the better fix

The fix is one argument:

transpiled = transpile(qc, sim, optimization_level=0)

The better fix is to stop trusting that it worked:

t = transpile(qc, sim, optimization_level=0)
assert t.count_ops().get("id", 0) == 3, (
    f"noise slots were optimized away: {dict(t.count_ops())}"
)

Every circuit builder in vqelab.errorcorrection carries that check, and it raises rather than asserting so it survives python -O. The assertion is not defensive programming. It is the experiment's control. A noise study whose noise is unverified is not a noise study, and the failure mode is silent.

Why this one is dangerous

Chapter 13's mitigation bug announced itself — the numbers moved in the wrong direction. Chapter 22's AQFT fidelity table decreased with cutoff when it should have increased. Those are visible.

This one produces the answer you were hoping for.

The failure mode of a noise experiment is a result that is too clean, and the failure mode of a too-clean result is that nobody investigates it.

Confirmation bias is doing real work here. A team demonstrating error correction wants to see the logical error rate drop. A run that shows it dropping to zero gets celebrated, screenshotted, and put in the slide deck. A run showing 0.648 at $p = 0.6$ gets debugged.

What to take away

Verify that your noise is in the circuit. Count the operations after transpilation, not before. The circuit you build and the circuit that runs are different objects — Chapter 10's entire point, and Chapter 12 §12.4 made the same point about layout.

Test where you know the answer. Every code has a regime where it must fail: above its threshold, beyond its distance, outside its promise. Chapter 20's Deutsch–Jozsa implementation has the same property — feed it a function violating the promise and it must produce garbage. A test suite that only checks cases that should succeed cannot distinguish a working implementation from a disconnected one.

Be more suspicious of good news than bad. A result that matches your hopes gets less scrutiny exactly when it needs more. This chapter's §25.3 contains a second instance of the same failure, found the same way and with the same tell: a logical error rate of zero at a noise rate where zero is impossible.

And notice what this shares with the rest of the book. Chapter 24's exact simulator hid shot noise. Chapter 19's oracle "worked" because it was only ever tested in the computational basis. Chapter 16's barren-plateau fit was beautiful and measured an artifact. Every one of them is the same shape:

A number can be precise, reproducible, and about something other than what you think.

Here it was about a circuit with the noise removed.


Reproduce it: code/example-01-the-bit-flip-code.py prints the surviving id-gate count at both optimization levels before it reports any error rates, and test_the_noise_slots_are_verified_not_assumed in code/project-checkpoint.py will fail loudly if a future Qiskit changes the behaviour.