Case Study 1: The Pipeline That Passed Its Tests and Lost a Phase
The system
A group maintains an algorithm library in Qiskit and needs to run parts of it on hardware reachable only through another framework. Rather than maintain two implementations, they build a translation pipeline:
Qiskit circuit -> OpenQASM 2 -> target framework -> execute -> results back
This is the right architecture. OpenQASM is a real standard, all five frameworks speak it (§18.3), and one serialization path is far better than $n^2$ pairwise translators.
They test it carefully.
The test suite
Gate counts survive. Round-trip a circuit through QASM and compare count_ops(). Passes.
Unitaries match. For each subroutine, compare Operator(original) against Operator(round_tripped)
with .equiv(). Passes.
Bell and GHZ states match. Run both versions and compare histograms. Passes.
Bit ordering is handled. They read Chapter 14, they know Qiskit is little-endian and the target is
big-endian, and they wrote a single reverse_bits conversion at the boundary. They test it with X
on qubit 0. Passes.
Four categories, all green. They ship it.
The failure
An algorithm that uses a subroutine under control returns the wrong answer. Not noisily wrong — inverted. The outcome that should dominate is the one that never appears.
The subroutine works standalone. The controlled version does not. And the controlled version works in the original Qiskit implementation.
The bug
OpenQASM 2 does not carry global phase.
before round trip: global_phase = 1.047198
after round trip: global_phase = 0.000000
LOST: True
Chapter 6 §6.6 measured exactly this and explained why it is not cosmetic:
A global phase is unobservable on the state it is attached to. It becomes observable the moment that state is put in superposition with something else — which is precisely what controlling an operation does.
$$U \to e^{i\phi}U \quad\text{is unobservable}$$ $$\text{controlled-}U \to \text{controlled-}(e^{i\phi}U) \quad\text{is a relative phase on the control}$$
Chapter 6's case study watched this invert a measurement outcome: {'1': 1757} becoming
{'0': 1758}. The same failure, arriving through a different door.
Why every test missed it
This is the instructive part, and it rhymes with Chapter 14's Case Study 1.
Gate counts. A global phase is not a gate. Counting gates cannot see it.
Unitary comparison — and this is the subtle one. Qiskit's Operator.equiv() compares unitaries
up to global phase, deliberately, because a global phase is physically unobservable. The test
was designed to ignore exactly the thing that was lost. It is not a bad test; it is a correct test
of the wrong property.
Bell and GHZ histograms. Global phase is unobservable in any direct measurement — that is the definition. A histogram cannot detect it, at any shot count.
The endianness test. Correct, well-designed, and about a different axis entirely. Passing it says nothing about phase.
The pattern: every test was blind to the specific loss, and three of them were blind by construction.
equiv()ignores global phase on purpose. Histograms cannot see it in principle. Gate counts are the wrong data type.A test suite is a set of hypotheses about how you might be wrong. These four covered gates, unitaries-up-to-phase, distributions, and bit order. There was no hypothesis for "the serialization format drops a field," so nothing tested it.
What would have caught it
Two things, both cheap.
Test the controlled version. The subroutine was tested standalone, where its global phase is genuinely unobservable. Control it and the phase becomes observable — which means the standalone test could never fail and the controlled test could never pass. Chapter 14's lesson in a new setting: a test whose expected output is invariant under the bug you fear is not a test for that bug.
Compare unitaries including phase. Operator(a) == Operator(b) rather than .equiv(). This
would have failed immediately and pointed straight at the phase.
The general form: equiv() is the right comparison for a circuit you will run standalone and the
wrong one for a circuit you will control. Which comparison to use is a statement about how the
circuit will be used, and no property of the circuit itself.
The fix
Not "stop using OpenQASM" — the pipeline architecture is correct, and QASM's phase loss is documented behavior, not a bug.
The fix is that a translation must report what it dropped. vqelab/interop.py returns a
TranslationReport alongside every conversion:
qasm, report = to_qasm(circuit, target="cirq")
if not report.is_lossless:
for warning in report.warnings():
print(warning)
qiskit is little-endian and cirq is big-endian; convert results with
reverse_bits at exactly ONE boundary
global phase 1.047198 was dropped; this becomes a RELATIVE phase if the
circuit is controlled (Chapter 6 section 6.5)
The report is returned, not logged. A logged warning is a warning nobody reads; a returned value is one the caller must at least decide to ignore. And the message names the consequence — "becomes a relative phase if controlled" — rather than merely stating the fact, because the fact alone did not help anyone here.
The module also refuses to verify a translation with a symmetric test case:
with pytest.raises(TranslationError, match="symmetric"):
verify_translation(bell_state, ..., "cirq")
Chapter 14's Case Study 1 and this one have the same root, and the tooling now blocks both.
The lessons
A standard interchange format defines what is left at the boundary, not that there is no boundary. OpenQASM moves the circuit correctly and drops the phase, the indexing, and the parameter names. Knowing which three is the whole skill.
Tests can be blind by construction. Operator.equiv() ignores global phase deliberately and
correctly. Three of four tests here could not fail on this bug, and nothing in their names said so.
Ask of every test: what result would indicate the bug?
Standalone and controlled are different test cases. A global phase is unobservable in one and observable in the other. Any subroutine that will be controlled must be tested controlled.
Make the loss a return value. Silent lossy conversions are how correct components compose into an incorrect system. §17.6's abstraction lesson, applied to a serialization format: the pipeline answered the question it was asked.
And the recurring theme, once more: the failure was not a wrong answer from any component. QASM
behaved as specified, equiv() behaved as documented, the endianness conversion was right. Every
part was correct and the composition was not — which is the fifth time this book has landed there
(Chapter 8's parameter sort, Chapter 10's optimization level, Chapter 11's diagonal-gate removal,
Chapter 13's DD placement, Chapter 14's insertion strategy).
Reproduce it: code/example-01-qasm-interop.py measures all three losses;
code/project-checkpoint.py builds the report object and the symmetric-case refusal.