Chapter 26 — Key Takeaways (Debugging Quantum Programs)
Part V opens here. Twenty-five chapters produced bugs; this is the method that finds them.
Why the classical loop is unavailable
- You cannot print an intermediate state — reading collapses it. A
printis a measurement. - You cannot step through execution — no breakpoint pauses a superposition.
- You cannot read the output — it is a sampled distribution, and a subtly wrong circuit differs from a correct one by about shot noise ($0.5/\sqrt N$, Ch. 24).
- You cannot tell a bug from noise — Ch. 19's dirty ancilla was indistinguishable from decoherence.
Debug on a simulator, where you can see everything. Validate on hardware, where you can see nothing. Two activities, two toolsets. Conflating them is the field's most common mistake.
What the simulator gives back
sv = Statevector.from_instruction(qc) # every amplitude, exactly, no shots
def prefix(qc, k): # a stepping debugger in four lines
out = QuantumCircuit(qc.num_qubits)
for inst in qc.data[:k]: out.append(inst)
return out
save_statevector(label=...) does the same from inside Aer, for circuits that must run through it
anyway. And the wall both hit:
28 qubits: 268,435,456 amplitudes = 4 GB
30 qubits: 1,073,741,824 amplitudes = 16 GB
40 qubits: 1.1 x 10^12 amplitudes = 16 TB
Dies around 30–35 qubits. Past that: stabilizer method if Clifford (Ch. 11), MPS if entanglement is low, or debug subcircuits.
★ Four equality tests, four questions
For $XZXZ = -I$ versus $I$:
Operator(p) == Operator(q) False
process_fidelity == 1 True
Statevector.equiv True
state_fidelity == 1 True
All four are correct. Global phase cancels in $|\langle\psi|\psi\rangle|^2$ — until you control the gate:
Operator(ctrl-p) == Operator(ctrl-q) False <-- phase became RELATIVE
Operator(ctrl-p).equiv(Operator(ctrl-q)) False <-- and not a phase convention
⚛️ Choose by USE, not by strictness.
text standalone program -> equiv / process_fidelity will be CONTROLLED -> Operator equality, exactly after a specific input -> state_fidelity input-independent whole map -> process_fidelity or OperatorCh. 18 §18.4 hit this from the other side: translation loses global phase, harmless until the result becomes a controlled subroutine.
★★ Circuit bisection
Binary search the first divergent instruction. Valid because divergence is monotone — once two circuits differ, more gates cannot make them agree.
circuit ops linear bisection
500 500 10
3,368 3,368 13 <-- Ch. 23's Shor(15)
20,000,000 20,000,000 26 <-- Shor on a 2048-bit key
★★ ...and the bug inside the debugger
Bisecting on states from $|000\rangle$ found nothing for a circuit with process fidelity 0.9498 against its reference. Every control reaching the buggy gate was in $|0\rangle$, so it never fired.
$|{+}{+}{+}\rangle$ was blind too — the Hadamard maps $|+\rangle \to |0\rangle$ first. Counting properly:
computational basis states blind: 4/8 ['000','010','100','110']
states over {0, 1, +} blind: 11/27 (41%)
random states blind: 0/100
⚠️ You cannot reason your way to a safe test input — choosing a good one requires already knowing where the bug is. $|1{+}0\rangle$ was written into the checkpoint as one that "obviously" catches it. It does not.
Use
Operatorwhile the circuit is small enough (~12–14 qubits). Past that,random_statevector, and more than one.
And the tool must refuse: DIVERGED / AGREE / BLIND. A state-based bisection that finds
nothing cross-checks against the operator first. "I found nothing" and "there is nothing" are
different claims, and a debugger that conflates them certifies broken circuits.
The gallery — one diagnostic each
| Bug | Ch. | Diagnostic | Result |
|---|---|---|---|
| Bit order | 14 | Operator(a) == Operator(b.reverse_bits()) |
True → convention, not logic |
| Dirty ancilla | 19 | purity(partial_trace(sv, anc)) |
1.0000 → 0.6250 |
| Wrong convention | 22 | process_fidelity vs library gate |
1.0000 vs 0.2500 / 0.1547 / 0.2500 |
| Deleted gates | 25 | diff count_ops() across transpile |
id: 1 → 0 |
| Global phase | 18 | the equality test matching the use | see above |
A pure state that has gone mixed on a NOISELESS simulator is not noise. There is no noise. It is entanglement with something you forgot to uncompute — and the number is exact, not statistical.
Assertions inside circuits
p_dirty = 1 - Statevector.from_instruction(qc).probabilities(ancillas)[0]
uncompute=True P(ancilla != |0>) = 0.0000 CLEAN
uncompute=False P(ancilla != |0>) = 0.2500 DIRTY
Also worth asserting: unentangled registers (purity 1), uniform distributions, real amplitudes where the algorithm says they should be, and norm preservation.
★ Debugging what actually runs
initial layout (virtual -> physical): [3, 2, 4, 1, 0]
final layout (after routing): [4, 2, 3]
Three ways to verify a transpiled circuit:
1. Operator(logical) vs Operator(transpiled) QiskitError: 3 qubits vs 5
2. padded, IGNORING the layout 0.001406 <-- FALSE ALARM
3. Operator.from_circuit(transpiled) 1.0000000000 <-- CORRECT
⚠️ A comparison that runs is not a comparison that is right.
0.001406is type-correct, exception-free, deterministic, reproducible — and comparing two circuits on mismatched wires.
And the ceiling: the same circuit on a 127-qubit backend gives
ValueError: Maximum allowed dimension exceeded. On real-sized hardware you cannot verify
transpilation by building operators at all.
What you lose on hardware
qubits state tomography process tomography
5 243 248,832
10 59,049 61,917,364,224
20 3,486,784,401 3.8 x 10^21
$3^n$ settings for states, $\sim12^n$ for processes. A diagnostic for one or two qubits.
So hardware "debugging" is really four coarse checks: does the distribution match the simulator's; does it degrade the way noise degrades; does a scaled-down version work; do the classical checks pass?
Ch. 23's Shor verifies its factors classically and Ch. 24's QAOA verifies its cut. That Las Vegas structure is not only an efficiency trick — it is the only form of hardware debugging that scales.
The protocol
1. Diff count_ops() before/after transpilation.
2. Compare the UNITARY against a reference -- not a single nice input.
3. If it differs, BISECT (operator, or random input).
4. Check convention: reverse_bits, and the phase-insensitive test.
5. Check ancilla cleanliness: purity == 1.
6. Only then scale up.
7. On hardware: compare distributions, verify classically. You are validating now.
Steps 1–5 are exact, cheap, and simulator-only. Exhaust them before touching hardware, because after that the tools are gone and every symptom looks like noise.
Common pitfalls
- Debugging on hardware, where you cannot see anything.
- Testing with $|0\dots0\rangle$ — or $|{+}\dots{+}\rangle$, which is no safer.
- Reading "I found nothing" as "there is nothing."
- Picking an equality test by strictness rather than by how the circuit will be used.
- Comparing a logical circuit to a transpiled one without applying the layout.
- Trusting a verification tool that has no positive and negative control.
- Assuming your bug is in the library. It usually is not.
Project piece added this chapter
vqelab/debugging.py — prefix; bisect returning DIVERGED/AGREE/BLIND with an
operator cross-check before ever reporting agreement; equality_verdict(a, b, will_be_controlled)
with no default for the flag, because both defaults are wrong half the time; ancilla_report;
count_ops_diff; is_bit_order_only; verify_transpilation that applies the layout and raises
rather than returning a wrong number. 32 tests pass, including
test_bisecting_from_zero_reports_BLIND_not_AGREE,
test_HALF_the_computational_basis_states_are_blind, and
test_verify_transpilation_REFUSES_rather_than_returning_a_wrong_number.