Case Study 2: Reading the Compiler's Mind
"'The transpiler optimized it' is not an explanation. The diff is."
Executive Summary
A circuit that works at optimization level 3 fails at level 1 — or runs three times slower, or uses different qubits, or produces a different answer. Something in the compiler changed and you need to know what.
There is a precise way to find out, and it takes about a minute: dump the QASM at both levels and diff them. The output is a line-by-line record of every decision the compiler made, in a format you can read.
This case study builds that technique on three circuits of increasing size, extracts a repeatable method, and ends with a small tool you can keep. It is the most immediately practical hour in Part I, and the skill is used constantly from Chapter 10 onward.
Skills applied: reading transpiled QASM (§6.5); physical qubit notation and inline gate
definitions (§6.5); virtual rz (Chapter 3 §3.8); routing costs (Chapter 4 §4.6).
Reproducibility. FakeSherbrooke, seed_transpiler=42, entirely local.
The Technique
import difflib
from qiskit import qasm3
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime.fake_provider import FakeSherbrooke
backend = FakeSherbrooke()
def qasm_at(qc, level):
pm = generate_preset_pass_manager(optimization_level=level, backend=backend,
seed_transpiler=42)
return qasm3.dumps(pm.run(qc)).splitlines()
def compare(qc, a=0, b=3):
print("\n".join(difflib.unified_diff(qasm_at(qc, a), qasm_at(qc, b),
f"level {a}", f"level {b}", lineterm="")))
Three lines of real work. Now use it.
Investigation 1: The Bell State
Chapter 2's Exercise 2.20 measured level 0 at 25 operations and level 3 at 9, without saying which sixteen disappeared. Now we can look.
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure([0, 1], [0, 1])
compare(qc)
The diff shows a long run of rz/sx pairs at level 0 collapsing into a much shorter sequence at
level 3, with the ecr and the two measure statements unchanged in both.
What the compiler did, read off the diff:
It merged adjacent single-qubit gates. Level 0 lowers each logical gate independently — h
becomes its own $R_z\sqrt{X}R_z$ sequence, and the single-qubit corrections around the ecr become
theirs. Level 3 notices that consecutive single-qubit gates on the same wire compose into one
arbitrary single-qubit unitary, and Chapter 3's Case Study 2 established that an arbitrary
single-qubit gate costs exactly three rz and two sx regardless of complexity. Ten operations
become five, and five of the ten were free anyway.
It did not touch the two-qubit gate. One ecr before, one ecr after. There is nothing to
optimize about a single entangling gate, and this is the general pattern: optimization levels move
single-qubit gate counts around and rarely change two-qubit counts on small circuits.
That second observation is the useful one, because two-qubit gates dominate the error budget. It means that on a circuit like this, the difference between levels is real but modest — consistent with Chapter 2's measurement of 95.6% versus 98.3%.
Investigation 2: Where Routing Appears
Small circuits do not exercise the interesting part. Scale up:
ghz = QuantumCircuit(5, 5)
ghz.h(0)
for i in range(4):
ghz.cx(i, i + 1)
ghz.measure(range(5), range(5))
Now compare not just the text but the extracted facts:
import re
def facts(qc, level):
pm = generate_preset_pass_manager(optimization_level=level, backend=backend,
seed_transpiler=42)
isa = pm.run(qc)
text = qasm3.dumps(isa)
ops = dict(isa.count_ops())
return {
"physical": sorted({int(m) for m in re.findall(r"\$(\d+)", text)}),
"depth": isa.depth(),
"two_qubit": sum(v for k, v in ops.items() if k in ("ecr", "cz", "cx")),
"real_pulses": sum(v for k, v in ops.items() if k in ("sx", "x")),
"virtual": ops.get("rz", 0),
}
for level in (0, 1, 2, 3):
print(level, facts(ghz, level))
Two things to look for in the output, and they are the whole point of scaling up.
Did the two-qubit count exceed what you wrote? You wrote four CNOTs. If a level reports more than four two-qubit gates, the router inserted SWAPs — three CNOTs each — because it needed to connect qubits that are not physically adjacent. This is Chapter 4 §4.6's hidden cost, made visible, and it is usually the single largest difference between a good layout and a bad one.
Did the physical qubit set change between levels? Levels 0 and 1 assign by position; levels 2 and 3 assign using error data. Chapter 4's Case Study 1 showed what that difference can be worth: an eightfold change in the correctness of the answer, from nothing but qubit choice.
⚙️ Under the Transpiler — The four numbers to extract from any diff.
Number Where What it tells you physical qubits the $nidentifierswhich layout was chosen; record this with every result two-qubit gate count count ecr/cz/cxwhether SWAPs were inserted — compare to what you wrote real pulses count sx+xthe true single-qubit cost ( rzis free)depth isa.depth()the coherence budget consumed If you extract nothing else from a transpiled circuit, extract these four. They answer most of the questions that arise when a circuit behaves differently than expected, and three of the four are readable straight from the QASM text.
Investigation 3: When the Diff Is Empty
Run the comparison on a circuit that is already minimal — say, a single cx between two adjacent
physical qubits — and the diff comes back empty.
That is a result, not a failure. It says the compiler found nothing to do, which means:
- Your circuit is already in the device's basis and layout.
- Any performance problem you have is not a compilation problem, and looking for one is wasted effort.
- The difference between optimization levels, for this circuit, is exactly zero — so use level 1 and save the transpilation time.
An empty diff is one of the most useful outputs of this technique, because it eliminates a whole class of hypothesis. Chapter 26 will make the general version of this argument: a diagnostic that rules something out is worth as much as one that rules something in.
The Method, Extracted
def transpiler_report(qc, backend, levels=(0, 1, 2, 3), seed=42):
"""What each optimization level does to a circuit. Print, read, decide."""
rows = []
for level in levels:
pm = generate_preset_pass_manager(optimization_level=level, backend=backend,
seed_transpiler=seed)
isa = pm.run(qc)
text = qasm3.dumps(isa)
ops = dict(isa.count_ops())
rows.append({
"level": level,
"depth": isa.depth(),
"2q": sum(v for k, v in ops.items() if k in ("ecr", "cz", "cx")),
"pulses": sum(v for k, v in ops.items() if k in ("sx", "x")),
"free": ops.get("rz", 0),
"qubits": sorted({int(m) for m in re.findall(r"\$(\d+)", text)}),
})
return rows
Use it like this, in order:
- Run
transpiler_reportfirst. Four numbers per level tell you most of what you need. - If a level surprises you, diff its QASM against the neighbouring level. The specific lines that changed are the specific decisions that differed.
- If the two-qubit count exceeds what you wrote, you have a routing problem — go to Chapter 10 for the pass manager and Chapter 29 for layout.
- If the physical qubits differ between levels, check their calibration — Chapter 4's Case Study 1 is the procedure.
- If the diff is empty, stop looking at the compiler. The problem is elsewhere.
Lessons
- Diff the QASM. Three lines, and it converts "the transpiler optimized it" into a specific, readable list of decisions.
- Optimization levels mostly move single-qubit gates. Two-qubit counts change on larger circuits, through routing, and that is where the error budget lives.
- A two-qubit count higher than what you wrote means SWAPs were inserted, at three CNOTs each.
rzis free — do not count it as cost. Countsxandx.- The physical qubit set is part of your result. Record it; it changes between optimization levels and it changes your answer.
- An empty diff is informative. It rules out compilation as the cause.
- Extract four numbers from every transpiled circuit: physical qubits, two-qubit count, real pulses, depth.
Questions
-
Run
compare()on the Bell circuit yourself. Count exactly how manyrzandsxoperations appear at each level. Does the reduction match Chapter 2's 25 → 9? -
Build
transpiler_reportand run it on the 5-qubit GHZ chain. Does the two-qubit count ever exceed 4? If so, at which levels, and what does that tell you about the layouts chosen? -
Construct a circuit that forces routing: a CNOT between two qubits you know are far apart on the heavy-hex lattice (use
initial_layoutto pin them). Report the two-qubit gate count before and after transpilation, and compute how many SWAPs were inserted. -
Find a circuit where level 2 and level 3 produce identical QASM. What does that tell you about when level 3 is worth its extra compilation time?
-
Find a circuit where level 3 is worse than level 2 on some metric. (Hint: the levels optimize different things, and transpilation time is itself a metric.) Report both.
-
Extend
transpiler_reportto also report the estimated total error of the transpiled circuit, using the backend's per-gate error rates. Which level wins on that metric, and does it agree with the one that wins on depth? -
Hardest. The diff shows you what changed but not why. Qiskit's pass managers expose the list of passes that ran (
pm.stages, and the pass manager'srunaccepts a callback). Instrument a transpilation to record the circuit after each pass, then identify which specific pass is responsible for the largest reduction on your circuit. What would you do with that information?