Case Study 1: The Dead Qubit
"An unexplained cliff in a scaling curve is more often a broken component than a law of nature. Check the hardware before you theorize about it."
Executive Summary
You measure how well a GHZ state survives on a quantum processor as you add qubits. Two through six give a smooth, believable decay. Seven falls off a cliff — from 81% correct to 21%, for one extra qubit and one extra gate.
The temptation is to explain this. Coherence limits, error accumulation, a threshold effect, something about depth. All plausible; all wrong. One physical qubit on the chip is broken, the compiler walked straight into it, and a one-line change recovers 85% fidelity at eight qubits where the default gave 11%.
This case study is that investigation, start to finish. It is the most practically valuable hour in Part I, because the failure mode is common, the diagnosis is fast, the fix is trivial, and almost nobody checks.
Skills applied: GHZ construction (§4.7); entangled-state fidelity on hardware (§4.9); reading calibration data (Chapter 2 Case Study 2); the transpiler's layout decision (§4.9, previewing Chapter 10).
Reproducibility. Everything below runs locally against FakeSherbrooke with
seed_transpiler=42 and seed_simulator=1234. No account, no queue, about ten seconds.
The Measurement
The setup is exactly §4.9's. Build an $n$-qubit GHZ state, transpile it for the backend, run 4,096 shots, and count how often we get all-zeros or all-ones.
from qiskit import QuantumCircuit
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import SamplerV2 as Sampler
from qiskit_ibm_runtime.fake_provider import FakeSherbrooke
backend = FakeSherbrooke()
def ghz(n):
qc = QuantumCircuit(n)
qc.h(0)
for i in range(n - 1):
qc.cx(i, i + 1)
qc.measure_all()
return qc
def fidelity(n, level):
pm = generate_preset_pass_manager(optimization_level=level, backend=backend,
seed_transpiler=42)
isa = pm.run(ghz(n))
sampler = Sampler(mode=backend)
sampler.options.simulator.seed_simulator = 1234
counts = sampler.run([isa], shots=4096).result()[0].data.meas.get_counts()
return (counts.get("0" * n, 0) + counts.get("1" * n, 0)) / 4096, \
isa.layout.final_index_layout()
for n in range(2, 9):
f, layout = fidelity(n, level=1)
print(f" n={n} {f:.4f} qubits {layout}")
n=2 0.9561 qubits [0, 1]
n=3 0.9290 qubits [0, 1, 2]
n=4 0.9084 qubits [0, 1, 2, 3]
n=5 0.8813 qubits [0, 1, 2, 3, 4]
n=6 0.8127 qubits [0, 1, 2, 3, 4, 5]
n=7 0.2148 qubits [0, 1, 2, 3, 4, 5, 6]
n=8 0.1091 qubits [0, 1, 2, 3, 4, 5, 6, 7]
Five smooth points, then the floor gives out.
Step 1: Resist the Physics Explanation
Here is the reasoning to not do, and it is seductive because every step of it is individually sensible:
"Each qubit adds a CNOT and a readout, so error compounds. At some point the accumulated error crosses a threshold where the state is essentially destroyed. Seven qubits appears to be that point on this device. The nonlinearity suggests a coherence limit — the circuit depth at $n=7$ probably exceeds what $T_2$ supports."
That paragraph would pass a casual review. It has a mechanism, it invokes real physics, and it is consistent with the data.
It is also wrong, and testable, and the test takes two minutes.
The tell is the shape. Compounding multiplicative errors produce a smooth exponential decay: $0.956, 0.929, 0.908, 0.881, 0.813$ is consistent with a per-qubit factor around 0.96. A step from 0.813 to 0.215 is a factor of 0.26 in one qubit — six times worse than the trend.
Smooth processes do not produce cliffs. When a curve breaks sharply, look for a discrete cause: a broken component, a threshold in the software, a change in what the compiler decided to do. Only after ruling those out should you reach for physics.
Step 2: Ask What Changed
Two things change from $n=6$ to $n=7$: one more gate, and one more physical qubit. The gate count went up by exactly one, which cannot explain a factor of six. So look at the qubit.
At $n=7$ the layout is [0, 1, 2, 3, 4, 5, 6]. The new member is physical qubit 6.
target = backend.target
for q in range(8):
print(f" q{q}: readout error {target['measure'][(q,)].error:.4f}")
q0: readout error 0.0112
q1: readout error 0.0244
q2: readout error 0.0215
q3: readout error 0.0120
q4: readout error 0.0208
q5: readout error 0.0605
q6: readout error 0.2573 <-- twenty times its neighbours
q7: readout error 0.0498
There it is, in one query. Qubit 6 misreads more than a quarter of the time.
And the entangling gate is worse:
for pair in [(1, 0), (1, 2), (3, 2), (4, 3), (5, 4), (6, 5), (7, 6)]:
props = target["ecr"].get(pair)
if props and props.error is not None:
print(f" ecr{pair}: error {props.error:.4f}")
ecr(1, 0): error 0.0075
ecr(1, 2): error 0.0088
ecr(3, 2): error 0.0087
ecr(4, 3): error 0.0070
ecr(5, 4): error 0.0100
ecr(6, 5): error 1.0000 <-- a gate error of ONE
ecr(7, 6): error 1.0000
An error rate of 1.0. That is not a noisy gate; it is a gate that does not work. Every connection to qubit 6 is dead on this snapshot.
Diagnosis complete. Elapsed: about two minutes.
Step 3: Why the Compiler Did This
The compiler is not malfunctioning. It is doing exactly what optimization level 1 specifies.
Preset optimization levels differ in how they choose which physical qubits your logical qubits map onto:
| Level | Layout strategy | Uses error data? |
|---|---|---|
| 0 | Trivial: logical $i$ → physical $i$ | No |
| 1 | Trivial, then VF2 if the trivial layout does not fit | Only as a fallback |
| 2 | VF2 with a noise-aware scoring pass | Yes |
| 3 | VF2 with noise-aware scoring, plus heavier optimization | Yes |
At level 1, our GHZ circuit maps onto physical qubits $0 \ldots n-1$ because it fits there — the device's connectivity happens to include that chain, so the trivial layout is valid, and a valid layout is accepted without asking whether it is any good.
"Valid" and "good" are different questions, and level 1 only asks the first one.
That is a defensible default. Layout search is expensive, and for most circuits on most days the difference is modest. It happens to be catastrophic when the trivial layout runs through a dead qubit.
Step 4: The Fix
for n in range(2, 9):
f1, l1 = fidelity(n, level=1)
f3, l3 = fidelity(n, level=3)
print(f" n={n} opt1 {f1:.4f} {str(l1):<26} opt3 {f3:.4f} {l3}")
n=2 opt1 0.9561 [0, 1] opt3 0.9827 [124, 123]
n=3 opt1 0.9290 [0, 1, 2] opt3 0.9736 [124, 123, 122]
n=4 opt1 0.9084 [0, 1, 2, 3] opt3 0.9585 [125, 124, 123, 122]
n=5 opt1 0.8813 [0, 1, 2, 3, 4] opt3 0.9309 [122, 123, 124, 125, 126]
n=6 opt1 0.8127 [0, 1, 2, 3, 4, 5] opt3 0.8850 [125, 124, 123, 122, 121, 120]
n=7 opt1 0.2148 [0, 1, 2, 3, 4, 5, 6] opt3 0.8906 [122, 123, 124, 125, 126, 112, 108]
n=8 opt1 0.1091 [0, 1, 2, 3, 4, 5, 6, 7] opt3 0.8569 [124, 123, 122, 121, 120, 119, 118, 110]
One integer changed. At $n=8$, fidelity went from 10.9% to 85.7% — a factor of eight.
Note where level 3 went: the 118–126 region, entirely away from the low-numbered qubits. It did not "avoid qubit 6" as such; it scored candidate layouts by their error rates and the good ones happened to live at the other end of the chip.
Note also the smaller wins at $n=2$ through $n=6$, where level 1 was already working: 95.6% → 98.3%, 81.3% → 88.5%. Even without a dead qubit, noise-aware layout is worth several points. The cliff is dramatic; the everyday gain is quieter and still real.
Step 5: What to Actually Do
Four practices, in order of value.
Use optimization level 2 or 3 for anything you care about. Level 1 is a reasonable default for iteration speed; it is not a reasonable default for a result you will report. The transpilation cost is milliseconds on circuits of this size.
Record the layout with every result. isa.layout.final_index_layout() is one call, and without
it a hardware result is not reproducible — the same circuit on the same device on the same day gives
materially different answers depending on which qubits it landed on. This belongs in your methods
section.
Sanity-check the qubits before a long run. A single query for the worst readout error and the worst gate error among your chosen qubits catches this failure before you spend queue time on it:
def check_layout(backend, layout):
"""Warn about unusable qubits before you spend queue time."""
t, problems = backend.target, []
for q in layout:
e = t["measure"][(q,)].error
if e is not None and e > 0.1:
problems.append(f"q{q} readout error {e:.3f}")
for a, b in zip(layout, layout[1:]):
for pair in ((a, b), (b, a)):
p = t["ecr"].get(pair)
if p is not None and p.error is not None and p.error > 0.05:
problems.append(f"ecr{pair} error {p.error:.3f}")
return problems
Treat an unexplained cliff as a component failure until proven otherwise. This is the transferable habit and it generalizes far beyond quantum computing.
🔬 Honest Assessment — What this says about the field.
A 127-qubit processor is not 127 working qubits. It is a population with a quality distribution, and on any given day some members of that population are unusable. That is normal, expected, and managed — vendors publish the calibration data precisely so you can route around it.
It also means "our processor has $N$ qubits" is close to meaningless as a capability claim, which is exactly what Chapter 1 §1.5's five-question test was getting at. Here is that abstraction with a number: the difference between the best and worst usable configurations on this one chip, for this one circuit, is a factor of eight in the correctness of the answer.
When you read a paper reporting a hardware result, "which qubits, and what were their error rates that day?" is a fair and necessary question. Good papers answer it. Many do not.
Lessons
- Smooth processes do not produce cliffs. A sharp break points to a discrete cause: a broken component, a software threshold, or a change in what the compiler decided.
- Check the hardware before theorizing about it. Two calibration queries, two minutes, and the physics explanation was never needed.
- Optimization level determines layout, and layout determines correctness. Levels 0 and 1 pick qubits by position; 2 and 3 pick them by measured error rate.
- "Valid" and "good" are different questions. The trivial layout was valid. It was also catastrophic.
- Noise-aware layout helps even when nothing is broken — several points of fidelity at every size here.
- Record the layout with every hardware result. Without it the result is not reproducible.
- Check your qubits before a long run. A ten-line preflight saves queue time and confusion.
- Qubit count is not capability. A factor of eight lived inside one chip, one circuit, one day.
Questions
-
Reproduce the whole investigation. Then change
seed_transpilerto 7 and rerun at level 1 — does the cliff move? What does that tell you about the reproducibility of an unpinned transpiler seed? -
Level 1 fell into the trap and level 3 did not. Test level 2. Is noise-aware layout enough on its own, or did level 3's extra optimization contribute? What does that isolate?
-
Write
check_layoutfrom Step 5, and run it on the level-1 layouts for $n = 2$ through $8$. At which $n$ does it first fire? Does it fire before the fidelity cliff, and why does that matter? -
Search
FakeSherbrookefor every pair with an ECR error above 0.5. How many are there? What fraction of the device's connections are unusable on this snapshot? -
The level-3 layout at $n=7$ is
[122, 123, 124, 125, 126, 112, 108]— not a contiguous run. Examine the coupling map: is that path connected? Did the transpiler insert any SWAPs? Checkisa.count_ops()and explain. -
Design a fair experiment to measure whether noise-aware layout helps on average, not just in the presence of a dead qubit. What would you vary, what would you hold fixed, and how many repetitions would you need? (Chapter 30 formalizes this.)
-
Hardest. The
check_layoutfunction uses fixed thresholds (0.1 for readout, 0.05 for gates). Fixed thresholds are a blunt instrument. Design a better preflight that scores a layout against the distribution of error rates on that specific device — so it flags "these are the worst qubits on this chip" rather than "these exceed a number I chose." What statistic would you use, and what would you do about the fact that a good chip and a bad chip have different distributions?