Case Study 2: The Refactor That Tripled the Depth
The change
A researcher has a Cirq circuit that applies a layer of single-qubit rotations across a register. The original:
circuit = cirq.Circuit()
circuit.append([cirq.ry(angle)(qubit) for qubit, angle in zip(qubits, angles)],
strategy=cirq.InsertStrategy.NEW_THEN_INLINE)
A code review suggests the list comprehension is dense and the loop would be clearer. Reasonable:
circuit = cirq.Circuit()
for qubit, angle in zip(qubits, angles):
circuit.append(cirq.ry(angle)(qubit),
strategy=cirq.InsertStrategy.NEW_THEN_INLINE)
Identical gates. Identical order. Identical parameters. Identical final state. The strategy argument was carried over faithfully. Every test passes, because every test checks the state and the state is genuinely unchanged.
The circuit is now three times deeper.
The measurement
Three H gates on three qubits, appended both ways:
strategy one append call append in a loop
EARLIEST (default) 1 1
NEW 3 3
INLINE 1 1
NEW_THEN_INLINE 1 3
Three of the four strategies are unaffected by the refactor. One is not.
np.allclose(simulate(batched).final_state_vector,
simulate(looped).final_state_vector) # True
len(batched), len(looped) # 1, 3
Same state, three times the depth.
Why
NEW_THEN_INLINE means: open a new moment for the first operation, then inline the rest.
The question the documentation answers, and that nobody reads carefully until this happens, is the first operation of what?
The answer is of that append call. The strategy describes how a single call places its batch of
operations, not how the circuit treats operations globally.
- One call with three gates → one new moment, then two inlines → 1 moment.
- Three calls with one gate each → each opens a new moment, and has nothing left to inline → 3 moments.
Both behaviors follow directly from the definition. Neither is a bug. And the refactor that converts between them is the single most common refactor in Python.
Why it matters, and why nothing caught it
Chapter 10 established depth as the primary determinant of whether a circuit survives; Chapter 11 §11.7 measured thermal error scaling with duration; Chapter 13 §13.5 watched an expectation value decay monotonically with circuit depth from 0.918 to 0.847. Tripling the depth of a layer is a real cost, paid in fidelity on every subsequent run.
And on a simulator it is completely invisible. The state is identical, so:
- state-vector tests pass,
- unitary comparisons pass,
- sampled-histogram tests pass,
- and any test of correctness passes, because the circuit is still correct.
The only observable is len(circuit), and nobody asserts on len(circuit).
The general shape: a refactor that preserves the value a program computes while changing the resources it consumes. Classical software has this too — an accidental $O(n^2)$ in a loop — but there the slowdown eventually shows up in a timing. Here it shows up as slightly worse fidelity on hardware, months later, indistinguishable from a bad calibration day.
The check
def assert_depth(circuit, expected, label=""):
assert len(circuit) == expected, (
f"{label}: depth {len(circuit)}, expected {expected}")
Assert on depth in tests for any circuit whose structure you care about. It is one line, it is free, and it is the only thing that would have caught this.
Chapter 11 §11.8's testing tiers have a natural home for it: tier 1 (exact, noiseless, milliseconds) should assert structure as well as correctness. A circuit is not just a function from inputs to outputs; it is also a resource claim, and resource claims deserve assertions.
The wider point about Cirq
This case study is not an argument against explicit moments. It is the direct consequence of them.
Qiskit does not have this failure mode, because you cannot express it — depth is computed by the scheduler from the dependency structure, and a refactor that preserves the gates preserves the schedule. You get less control and, in exchange, fewer ways to accidentally change the timing.
Cirq gives you the schedule and therefore gives you the responsibility. §14.3 argued that this is the honest abstraction, and it is: duration against $T_1$ and $T_2$ is what actually determines survival, and Cirq refuses to let you build a circuit without deciding it. But an abstraction that exposes a quantity also exposes it to being changed by accident.
The trade is real in both directions. If timing is your experiment — crosstalk characterization, DD sequences, anything pulse-adjacent — you want the control and you should assert on depth. If timing is not your experiment, the scheduler is a feature.
The lessons
A refactor that preserves the output can still change the artifact. "Same gates, same state, all tests pass" is not the same as "same circuit." In a domain where the physical resource — duration — is a structural property, structural properties need tests.
Read what a configuration option is scoped to. NEW_THEN_INLINE is scoped to an append call.
Nothing in the name conveys that, and the two readings of "the first operation" differ by a factor of
three in depth.
Assert on len(circuit). One line. It is the entire mitigation.
Explicit control cuts both ways. This is the fifth instance in the book of a defensible default or option producing a wrong outcome through interaction — Chapter 8's parameter sort, Chapter 10's optimization level, Chapter 11's diagonal-gate removal, Chapter 13's DD placement, and now this. In every case the tooling behaved exactly as documented. The failures live in the composition, and composition is not covered by anyone's unit tests.
Reproduce it: code/example-02-moments.py measures all four strategies both ways and verifies the
states are identical.