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.