39 min read

> "Five frameworks, and the thing that transfers between them is not the code."

Prerequisites

  • 6
  • 7
  • 14
  • 15
  • 16
  • 17

Learning Objectives

  • Choose a framework for a given task and defend the choice.
  • Move a circuit between frameworks using OpenQASM.
  • State precisely what OpenQASM preserves and what it does not.
  • Convert results across the endianness boundary safely.
  • Recognize which skills from Parts I–II are framework-independent.

Chapter 18: Framework Comparison and Interoperability

"Five frameworks, and the thing that transfers between them is not the code."

Overview

Part III wrote the same circuits five times. This chapter is the synthesis, and it has one practical result at its centre.

OpenQASM is the interchange format, and it does not solve the endianness problem.

Take an asymmetric two-qubit circuit, export it from Qiskit to OpenQASM 2, import it into Cirq, and compare the state vectors:

  qiskit          : [0, 0.7071, 0, 0.7071]
  cirq (via qasm) : [0, 0, 0.7071, 0.7071]

  match without reversal : False
  match with    reversal : True

The circuit transferred perfectly. The state-vector indexing did not.

That is not a bug in either framework. OpenQASM names qubits explicitlyx q[0]; means qubit 0, unambiguously — so the program moves correctly. But each framework then decides for itself where qubit 0 sits in its own state vector, and that decision is not something QASM has an opinion about.

You still convert at the boundary. Chapter 14's reverse_bits is not made obsolete by having a standard interchange format; it is exactly what the standard leaves to you.

In this chapter, you will learn to:

  • Compare the five frameworks on axes that matter.
  • Use OpenQASM as a lingua franca, and know its limits.
  • Identify what survives translation and what is lost.
  • Choose a framework and defend the choice.
  • Recognize the skills that are framework-independent — which is most of this book.

Learning Paths

How to read this chapter by track. - 🔰 Beginner — §18.2 and §18.6. Pick one framework and go deep; this chapter tells you which. - 🔬 Researcher — §18.4 and §18.5; reproducibility across frameworks is a real problem. - 🤖 Quantum ML — §18.6's table points you at PennyLane, with reasons. - 🏗️ Quantum Engineer — §18.4 and §18.5 are the ones you will implement. - 🔐 Security — §18.6, then Part IV.


18.1 The Same Circuit, Five Ways

A Bell state, in each framework, at its most idiomatic:

# Qiskit                                          3 lines
qc = QuantumCircuit(2, 2)
qc.h(0); qc.cx(0, 1)
qc.measure([0, 1], [0, 1])

# Cirq                                            3 lines
q = cirq.LineQubit.range(2)
c = cirq.Circuit([cirq.H(q[0]), cirq.CNOT(q[0], q[1]),
                  cirq.measure(*q, key="m")])

# Braket                                          1 line
c = Circuit().h(0).cnot(0, 1)

# PennyLane                                       4 lines
@qml.qnode(dev)
def c():
    qml.Hadamard(0); qml.CNOT([0, 1])
    return qml.probs(wires=[0, 1])

# Q#                                              6 lines
operation Bell() : (Result, Result) {
    use (a, b) = (Qubit(), Qubit());
    H(a); CNOT(a, b);
    let r = (M(a), M(b));
    ResetAll([a, b]); return r;
}

Line count is a terrible metric and an instructive one. Braket's single line is the shortest, and Q#'s six are not verbosity — they are declared return types, scoped qubit lifetimes, and an explicit reset. Each framework is charging you for something different, and the charges are visible here:

Framework What the syntax makes you say
Qiskit how many classical bits, and which measurement goes where
Cirq which qubit objects, and (implicitly) the moment structure
Braket almost nothing — the shortest path to a circuit
PennyLane what you want measured, as a return value
Q# the type, the qubit lifetime, and the cleanup

None of these is verbosity for its own sake. Q#'s ResetAll exists because §15.5's runtime checks it; PennyLane's return exists because the return value is what gets differentiated.

The dozen differences that do not matter

Before the chapter's real content, it is worth naming what this comparison is not about, because it is what most framework comparisons spend their length on.

Gate names. Appendix E's translation table lists fifteen gates across six columns. Fourteen of the fifteen rows are pure renames — h/H/Hadamard/H/h/h is one gate under six spellings, and a lookup table converts it perfectly in both directions, forever. The fifteenth ($\sqrt{X}$) has no Q# entry, which is a gate-library gap rather than a semantic difference.

The keyword for shot count. Qiskit shots=, Cirq repetitions=, PennyLane shots= on the device, Braket shots= on the run. Four spellings of an integer.

The result accessor. .result().get_counts() · .histogram(key="m") · qml.counts() · .result().measurement_counts. Four paths to the same dictionary.

The simulator constructor. AerSimulator() · cirq.Simulator() · qml.device("default.qubit") · LocalSimulator().

Mutation versus construction. qc.h(0) mutates a circuit in place; cirq.H(q[0]) returns an operation you then place in a circuit; Circuit().h(0) returns the circuit so calls chain. This one feels significant while you are learning and is mechanical once you have.

All of these cost you an afternoon with Appendix E and never cost you anything again. None of them can produce a wrong answer that runs. That is the test worth applying: a difference that a rename table fixes is not a difference you need a chapter about.

The three that do

Three differences are not renames. They are disagreements about what a circuit is, and no lookup table repairs them:

Difference What disagrees Where it bites
Endianness where qubit 0 sits in a state vector §18.2 — every result you read
Scheduling model whether a circuit is timed or merely ordered §18.3 — every duration-sensitive claim
Differentiability whether a parameter is a number or a graph node §18.4 — every variational algorithm

The diagnostic is whether a translator could fix it mechanically. A gate name can be looked up. A state-vector convention cannot, because the correction depends on how many qubits are in the register and the format does not carry that decision. A moment structure cannot, because the target representation has nowhere to put it. An autodiff graph cannot, because it lives in the calling program, not the circuit.

Everything else in this chapter is a consequence of those three.

18.2 The Endianness Table

Measured, in all three frameworks that expose a state vector, using Chapter 14's one-gate test — apply X to qubit 0 of a two-qubit register and see where the amplitude lands:

Framework Convention Amplitude index Measurement string
Qiskit little-endian 1 (01) '01'
Cirq big-endian 2 (10) integer 2
Braket big-endian 2 (10) '10'

Qiskit is the outlier — one of the three.

This matters more than it should, because Parts I and II are written in Qiskit, and little-endian ordering was presented for thirteen chapters as though it were a fact about quantum computing. It is a fact about Qiskit.

⚠️ Common Pitfall — the convention is not a mistake, and neither reading is "right."

Qiskit's little-endian ordering makes $|q_1 q_0\rangle$ read like a binary number with $q_0$ as the least significant bit, which is exactly how you would write a classical register — convenient for arithmetic circuits, where Chapter 22's QFT and Chapter 23's modular arithmetic live.

Cirq and Braket's big-endian ordering makes the state vector index read in the same order you listed the qubits, which is convenient when reasoning about circuit diagrams.

Both are defensible. Neither is going to change. The only actionable fact is that a boundary exists, and you must convert at it exactly once.

Working the boundary all the way through

The two-qubit table above is the smallest case that shows the effect, which makes it the easiest to under-learn. Here is the same experiment at three qubits, where the permutation stops looking like a swap and starts looking like what it is.

Take X on qubit 0 and H on qubit 1 of a three-qubit register, route it Qiskit → OpenQASM 2 → Cirq, and compare:

  qiskit    : [0.     0.7071 0.     0.7071 0.     0.     0.     0.    ]
  cirq      : [0.     0.     0.     0.     0.7071 0.     0.7071 0.    ]
  reversed  : [0.     0.7071 0.     0.7071 0.     0.     0.     0.    ]

  direct match   : False
  reversed match : True

Every nonzero amplitude moved. In the two-qubit case the population sat at indices 1 and 2 and it was tempting to read the difference as "the two middle entries swapped." At three qubits, indices 1 and 3 become 4 and 6 — nothing swapped; the whole index space was permuted.

The permutation is exactly bit reversal on the index written in binary:

   index   binary   reversed   ->index
       0      000        000          0    (fixed)
       1      001        100          4
       2      010        010          2    (fixed)
       3      011        110          6
       4      100        001          1
       5      101        101          5    (fixed)
       6      110        011          3
       7      111        111          7    (fixed)

Four of eight indices are fixed points, and those four are the entire reason the bug is hard to find. A test whose state has support only on 000, 010, 101, and 111 cannot detect a reversed convention at three qubits, no matter how many shots you run.

🐛 Debug This — the symptom that looks like a hardware problem.

A circuit that returns a clean, high-contrast, wrong answer. Not noisy, not degraded — confidently concentrated on a bitstring you did not expect. The fidelity looks excellent. Nothing in the noise model explains it.

Three checks, in order:

  1. Is the expected outcome a palindrome? If your expected bitstring reads the same backwards (0101 does not; 0110 does), you are in a fixed point and endianness cannot be the cause. If it is not a palindrome, reverse it and see whether the reversed string is the one you got.
  2. Count the conversions on the code path. Bit reversal is its own inverse (§14.5), so two conversions look exactly like zero. A codebase with reversals in three modules has a bug whose presence depends on which modules a given call traverses.
  3. Ask Cirq directly. simulate() exposes result.qubit_map — the authoritative ordering. When reasoning and code disagree, the qubit_map is right.

The fix is never "add a reversal here." It is to find the one boundary, put the single conversion there, and delete every other one.

The width problem, which is worse than the ordering problem

There is a second-order hazard that the endianness discussion hides, and it is measurable in one line.

Export a three-qubit Qiskit circuit that only acts on qubits 0 and 1 — qreg q[3], with qubit 2 idle — and import it into Cirq:

  qasm declares       : qreg q[3]   ->  qiskit sees 3 qubits
  cirq all_qubits()   : [q_0, q_1]  ->  cirq sees 2

  qiskit state vector length : 8
  cirq   state vector length : 4

Cirq's circuit has no register. It has whatever qubits appear in operations. The declaration qreg q[3] is a Qiskit-shaped statement about a register; Cirq's importer builds a circuit out of the operations, and an idle qubit produces no operation, so it produces no qubit.

This is not a bug in either side. It follows directly from Chapter 14 §14.2: Qiskit addresses qubits by index into a register, Cirq addresses them by object identity. A register has a width; a set of objects does not.

Why it is worse than the ordering problem: the reversal permutation is computed from the number of qubits. Reverse a 4-element vector as though it were 8 elements and you do not get a wrong answer — you get an exception or, worse, a silently truncated comparison. The measured consequence:

  np.allclose(qiskit_state, cirq_state)
  ValueError: operands could not be broadcast together with shapes (8,) (4,)

That loud failure is the good case. A pipeline that pads, or that compares dictionaries of counts keyed by bitstring rather than arrays, will compare '01' against '001' and find no matches at all — which reads as "the translation is completely broken" rather than "one qubit is missing," and sends you looking in the wrong place.

The fix is to make the width explicit rather than inferred. Adding an identity on the idle qubit restores it:

  with an explicit id on q2:
    cirq all_qubits() : [q_0, q_1, q_2]     state vector length : 8
    direct match      : False
    reversed match    : True

Now the only remaining difference is the one you knew about.

📐 Math Aside — how often can a random test case detect a reversed convention?

The reversal map on $n$-bit indices is a permutation of $\{0,\dots,2^n-1\}$. A basis state is undetectable precisely when its index is a binary palindrome, because those are the fixed points.

Counting them is a short argument. A palindrome of length $n$ is determined by its first $\lceil n/2 \rceil$ bits — the rest are forced by the mirror. So

$$\#\{\text{fixed points}\} = 2^{\lceil n/2 \rceil}, \qquad > \frac{\#\{\text{fixed points}\}}{2^n} = 2^{-\lfloor n/2 \rfloor}$$

Verified by enumeration:

text n 2^n palindromes fraction 1 2 2 1.0000 2 4 2 0.5000 3 8 4 0.5000 4 16 4 0.2500 6 64 8 0.1250 8 256 16 0.0625 10 1024 32 0.0312 12 4096 64 0.0156

At one qubit the test is guaranteed useless. At two qubits half of all basis states are blind. The blind fraction then halves every two qubits, not every one — the exponent is $\lfloor n/2 \rfloor$, so $n=2$ and $n=3$ share a fraction, as do $n=4$ and $n=5$.

And this calculation is optimistic, which is the part worth keeping. It assumes your test state is a uniformly random basis state. Real first tests are not random: they are Bell states, GHZ states, and uniform superpositions, and all three are symmetric under bit reversal at every width. The set of circuits people actually reach for to check a fresh install is almost entirely contained in the blind set. The $2^{-\lfloor n/2 \rfloor}$ figure describes an adversary picking at random; a beginner picking naturally does much worse than random.

🔀 In Another Framework — Q# puts the convention in the type system.

Q# is the one framework here that does not leave the ordering to documentation. It has a LittleEndian type, so a register whose bits are to be read least-significant-first is declared that way, and passing it where a big-endian register is expected is a compile error rather than a wrong number.

This is the same design instinct as §15.6's is Adj + Ctl functor declarations: make the property a proof obligation the compiler checks rather than a fact the reader must remember.

It is also, per Chapter 15's Case Study 2 scorecard, not enough on its own. Endianness appears in that chapter's list of eight bugs the Q# compiler missed — because the failure this chapter documents happens at the boundary between a Q# program and something else, and a type system only governs the side of the boundary it can see. Types catch errors of form; this is an error of correspondence.

18.3 OpenQASM as the Interchange Format

Chapter 6 introduced OpenQASM as a serialization format. Part III's question is whether it works as a translation format, and the answer is a qualified yes.

Who speaks it, measured:

Framework Exports Imports Note
Qiskit QASM 2 + QASM 3 QASM 2 + QASM 3 native, both directions
Cirq QASM 2 QASM 2 import lives in cirq.contrib and needs pip install ply
Braket QASM 3 QASM 3 OpenQASM 3 is Braket's native IR
PennyLane qml.to_openqasm qml.from_qasm plus native plugins
Q# / QDK qdk.openqasm.compile qdk.openqasm.circuit full OpenQASM module

All five, in both directions. That is a genuinely good state of affairs, and it is the strongest argument for OpenQASM being a real standard rather than one vendor's format.

Two asymmetries worth knowing:

Cirq's QASM import is a second-class citizen. Export works out of the box; import lives in cirq.contrib, requires an extra dependency (ply), and fails with ModuleNotFoundError until you install it. If you are building a translation pipeline through Cirq, that is a dependency you must declare.

Braket's native IR is OpenQASM 3. There is no translation step — the circuit you send to AWS is QASM 3. That makes Braket unusually well-suited as a translation hub.

What it transfers correctly

qc = QuantumCircuit(3)
qc.h(0); qc.cx(0, 1); qc.rz(0.7, 2); qc.cx(1, 2)
qasm2.dumps(qc)
  OPENQASM 2.0;
  include "qelib1.inc";
  qreg q[3];
  h q[0];
  cx q[0],q[1];
  rz(0.7) q[2];
  cx q[1],q[2];

Gate identity, qubit indices, parameters, and ordering all survive. The program is unambiguous: cx q[0],q[1] means control qubit 0, target qubit 1, in any framework.

What it does not transfer

The state-vector convention, as §18.0 measured. And Chapter 6's two losses, re-confirmed here:

  global phase  1.047198  ->  0.000000        LOST
  parameter names  ['theta[0]', 'theta[1]']  ->  ['_theta_0_', '_theta_1_']    MANGLED

Global phase is discarded. Chapter 6 §6.6 measured why this matters: a global phase on a subcircuit becomes a relative phase when that subcircuit is controlled, and Chapter 6's case study watched it invert an answer ({'1': 1757} becoming {'0': 1758}) while passing every equivalence test.

Parameter names are mangled. theta[0] becomes _theta_0_ — brackets are not valid QASM identifiers, so they are escaped. Chapter 6 §6.6 hit this and the fix is the same: normalize both sides rather than trying to reproduce the mangling rule, which has a leading underscore that is easy to miss.

And the layout, which nobody expects to lose

Chapter 12 spent an entire chapter establishing that which physical qubits you run on is worth about 100×, and Chapter 29 measured a hardware-aware level-1 transpilation (0.9116) beating a naive level-3 one (0.7720). If a translation drops the layout, it drops the most valuable decision in the pipeline.

It drops the layout.

Transpile a three-qubit GHZ circuit onto FakeSherbrooke with an explicit initial_layout=[40, 41, 42] and round-trip it:

  initial_layout requested          : [40, 41, 42]
  isa.layout.initial_index_layout() : [40, 41, 42, ...]      127 qubits
  isa.layout is None                : False

  after a QASM 2 round trip
    layout is None    : True
    num_qubits        : 127
    count_ops equal   : True

The gates all survived and the layout decision did not. What comes back is a 127-qubit logical circuit whose operations happen to sit on wires 40, 41, and 42 — which is not the same object at all. Qiskit no longer knows that q[40] means physical qubit 40; it means logical wire 40 of a very wide circuit, and any subsequent transpilation is free to move it.

OpenQASM 3 records more and restores less than you would hope. QASM 3 has physical-qubit syntax — Chapter 6 §6.5 read $0` and `$1 off a transpiled circuit — and the exporter uses it:

  QASM 3 text contains '$40'            : True
  QASM 2 text contains '$' at all       : False

  after a QASM 3 round trip (Qiskit's own importer)
    layout is None                      : False
    initial_index_layout()              : [0, 1, 2, 3, 4, ...]
    num_qubits                          : 43
    count_ops equal                     : True

A layout object exists and it is the identity. The circuit came back sized to the largest physical index it touched — 43 qubits, because it used $42` — with a trivial layout mapping wire $i$ to physical qubit $i$. The40, 41, 42assignment is legible in the *text* and absent from the *reconstructed object*. Anything downstream that reads.layout` gets a confident, wrong answer.

And $ is a Qiskit-flavoured dialect in practice, whatever the specification says. Handing that same QASM 3 text to Cirq's importer:

  cirq.contrib.qasm_import on QASM 3 text
    QasmException: Illegal character '$' at line 9

Cirq's importer is a QASM 2 importer, and QASM 2 has no physical-qubit syntax to be illegal about.

A QASM file is a logical circuit. That is a reasonable thing for an interchange format to be — the whole point of a portable circuit is that it is not yet committed to one device's qubit 40. But it means the transpilation you paid for does not travel with the file, and anything you learned from Chapters 10, 12, 28, and 29 must be re-derived on the far side of the boundary.

⚙️ Under the Transpiler — what to serialize, and when.

The rule that follows from the measurement above: serialize before you transpile, not after.

A QASM file is a good archive of what you meant and a bad archive of what ran. If you need the second — and Chapter 30's benchmarking work does, and Chapter 27's regression tests do — record the layout out of band alongside the file. Chapter 6 §6.5's habit generalizes: the physical qubits, the two-qubit gate count, the real pulse count, and the depth are four numbers you extract once, at transpile time, and store with the result.

The trap is that a round trip through QASM looks lossless on the metric people check. count_ops was equal in both round trips above. Gate counts are preserved exactly; it is the binding to hardware that is gone, and no gate-level comparison can see that — which is the same structural blindness Case Study 1 documents for global phase.

The scheduling model: Moments versus an instruction list

The second of §18.1's three real differences, and the one Appendix E flags as the largest single translation loss.

A Cirq circuit is a list of Moments. Chapter 14 §14.3 established this: len(circuit) is the depth, because a moment is a set of operations that happen simultaneously and a circuit is the sequence of those sets. A Qiskit circuit is a list of instructions, and qc.depth() is a number computed from that list by a scheduling analysis you did not write.

The difference is not stylistic. Cirq lets you state the schedule; Qiskit lets you state the order and delegates the schedule. Chapter 14 measured what that buys and what it costs:

  three independent H gates
    InsertStrategy.EARLIEST  ->  1 moment
    InsertStrategy.NEW       ->  3 moments

  same gates, same unitary (allclose verified), 3x the depth

A constructor argument changed the depth by 3× without changing a gate. In Cirq that is a decision you made; in Qiskit there is no place in the circuit object to have made it.

Now put that circuit through OpenQASM. Both versions export to the same six non-comment lines, and both come back as one moment:

  built with EARLIEST : 1 moment  ->  qasm (6 lines)  ->  1 moment
  built with NEW      : 3 moments ->  qasm (6 lines)  ->  1 moment

  round-trip unitary preserved : True   (both)
  OPENQASM 2.0;
  include "qelib1.inc";
  qreg q[3];
  h q[0];
  h q[1];
  h q[2];

Three moments went in and one came out. The QASM text is byte-for-byte identical for both circuits, because there is nowhere in it to write "these three gates are deliberately in different layers." The unitary is preserved, count_ops is preserved, and the schedule — the thing you used Cirq to express — is gone.

This is the cleanest example in the book of a lossless-looking loss. Every equivalence test passes. The circuits are equivalent, as unitaries. They are not equivalent as experiments.

⚛️ The Physics Underneath — why the moment structure is not cosmetic.

Decoherence is a function of wall-clock duration, not gate count. A qubit idling through a moment is decaying against $T_1$ and dephasing against $T_2$ exactly as hard as a qubit being operated on. Chapter 39 measured the durations on a real device: cz 68–184 ns, sx 32–64 ns, measure 1,560 ns, reset 1,600–1,848 ns — and $T_1$ spanning 15.2 to 483.0 μs across one chip's qubits.

Three H gates in one moment cost one layer of that budget. Three H gates in three moments cost three. On a qubit at the bad end of that $T_1$ spread the difference is measurable, and it is a difference the QASM file cannot express.

The caveat Chapter 14 attached still applies: a moment lasts as long as its slowest member, so depth is a proxy for duration and not a synonym for it. A moment containing a measure (1,560 ns) costs roughly twenty times a moment containing only sx gates (32–64 ns). Counting moments is a better cost model than counting gates and a worse one than summing durations.

This is why Chapter 31's pulse-and-timing work is a separate chapter: the layer where duration becomes explicit is below the layer OpenQASM describes.

What to do about it. There are three honest options and no good one.

Re-derive the schedule on the far side. Transpile with a scheduling pass, or rebuild the moments with an explicit InsertStrategy. This works when the schedule was a consequence of the circuit (the compiler can find it again) and fails when the schedule was the point (a deliberately spaced sequence for a decoupling or calibration experiment).

Carry the schedule out of band. Serialize the moment boundaries as a separate list of indices alongside the QASM, the same way Chapter 6 §6.8 carries the global phase in a comment. Ugly, portable, and it works.

Use OpenQASM 3 and stay inside the tools that implement its timing. QASM 3 has duration, delay, and barrier; QASM 2 has none of them. The specification can express what you want. Whether the tool on the other end implements that part of the specification is a separate question, and Chapter 6's pitfall list already warns you to test against the tool rather than the spec.

🗝️ Version Note — three API facts this section depends on.

qc.qasm() was removed in Qiskit 1.0. Any tutorial that calls it predates the qasm2/qasm3 module split. Use qasm2.dumps(qc) or qasm3.dumps(qc).

qasm2.loads cannot read Qiskit's own transpiled output by default. The exporter emits sx, which is not in qelib1.inc, so the importer rejects it:

text qasm2.loads(qasm2.dumps(isa)) QASM2ParseError: "<input>:3,23: 'sx' is not defined in this scope"

The fix is qasm2.loads(text, custom_instructions=qasm2.LEGACY_CUSTOM_INSTRUCTIONS), which then round-trips with count_ops preserved. A format is only as portable as the gate set you wrote in it — and Qiskit's own basis gates are not all in QASM 2's standard library. Verified with Qiskit 2.5.1.

qiskit.pulse was removed in Qiskit 2.0, along with add_calibration, .calibrations, backend.defaults, instruction_schedule_map, and drive_channel. Any pulse-level detail attached to a circuit was never going to survive a QASM round trip; as of 2.0 there is no longer a Qiskit-side object for it to fail to survive from.

18.4 The Translation Boundary

Putting §18.2 and §18.3 together gives the rule.

OpenQASM transfers the circuit. It does not transfer the indexing, the global phase, or the parameter names.

So a correct cross-framework pipeline has exactly three responsibilities:

# 1. Serialize the circuit.               QASM handles this correctly.
qasm = qasm2.dumps(circuit)

# 2. Convert results at the boundary.     ONE function, tested asymmetrically.
counts = cirq_histogram_to_qiskit_counts(histogram, n_qubits)

# 3. Verify with an asymmetric state.     A Bell state proves nothing (Ch. 14).
assert_same_state(qiskit_circuit, cirq_circuit)

Step 3 is the one people skip, and Chapter 14's Case Study 1 is what happens: four verification tests, all passing, all blind to the bug, because Bell and GHZ states are symmetric under bit reversal.

Differentiability does not cross the boundary

The third of §18.1's three real differences, and the one with no workaround at all.

Chapter 16's central object is the QNode: a Python function whose body is the circuit and whose arguments are the parameters. That is what makes qml.grad possible — the parameters are nodes in an autodiff graph maintained by the calling program, and the gradient is computed by that program calling the circuit $2n+1$ times.

  native QNode(0.7)      :  0.764842187          = cos(0.7)
  qml.grad(native)(0.7)  : -0.644217687          = -sin(0.7)

Now ask what an OpenQASM file could possibly carry of that. QASM 2 declines to try:

  qasm2.dumps(circuit with a free Parameter)
  QASM2ExportError: 'Cannot represent circuits with unbound parameters in OpenQASM 2.'

This is the right behaviour and worth appreciating. QASM 2 is a circuit format; a circuit with an unbound parameter is not a circuit. It fails loudly at the boundary rather than helpfully substituting a zero — Chapter 6 §6.8's recommendation, implemented by the exporter.

QASM 3 can represent the parameter, and that is all it does:

  OPENQASM 3.0;
  include "stdgates.inc";
  input float[64] theta;
  qubit[1] q;
  ry(theta) q[0];

input float[64] theta; is a genuine improvement — the parameter survives as a named input rather than being baked to a literal. But look at what it is: a declaration that this program expects a 64-bit float. It says nothing about the generator of ry, nothing about which shift rule applies, nothing about whether theta is trainable, and nothing about what it is connected to upstream.

The gradient is not a property of the circuit. It is a property of the circuit plus the rule for differentiating each gate plus the graph of what feeds what. Chapter 16 §16.3's parameter-shift rule,

$$\frac{\partial f}{\partial\theta} = \frac{f(\theta + \pi/2) - f(\theta - \pi/2)}{2},$$

is exact only for $U(\theta)=e^{-i\theta P/2}$ with $P^2=I$; a generator with more than two distinct eigenvalues needs more shift points. Deciding which case applies requires knowing the gate's generator, and OpenQASM's ry is a name in an include file, not an algebraic object.

So the honest statement is the one Appendix E makes: the circuit translates and the differentiability does not. If you route a variational loop through QASM you are re-implementing the gradient on the far side — which §16.3 shows is three lines for the simple case and is exactly the sort of hand-written duplicate that Chapter 15 §15.6 argues should be compiler-derived so it cannot drift.

The composition is the way out, not the pipeline. pennylane-qiskit runs a QNode on Qiskit backends directly, so the autodiff graph never crosses a serialization boundary — the circuit is constructed in PennyLane and executed elsewhere. §18.5's "use PennyLane as a layer, not a replacement" is not an ergonomic preference; it is the only architecture in which differentiability survives.

🐛 Debug This — a third asymmetry §18.3's table does not name.

§18.3 flags Cirq's importer as needing pip install ply. Measured on the same environment, PennyLane's importer is gated too:

```text pennylane_qiskit installed : False

qml.from_qasm(text) RuntimeError: Failed to load the qasm plugin. Please ensure that the pennylane-qiskit package is installed. cause -> KeyError: 'qasm' ```

qml.from_qasm is a dispatcher, not a parser. PennyLane 0.45.1 looks up a registered plugin converter for "qasm" and raises if none is installed. The message is a good one — it names the package you need — but the shape of the failure is Cirq's: an import path that appears in the API and is not in the base install.

Two of five importers are optional-dependency-gated, and neither is discoverable from the function signature. If you are building a pipeline, resolve both at import time and fail with a single clear message, the way vqelab/interop.py's from_qasm does — because KeyError: 'qasm' surfacing from four frames down is not a useful diagnostic.

What does not translate: the systematic list

Collecting every loss this Part measured, with what to do about each:

What is lost Where measured Detectable by Remedy
State-vector indexing §18.2, Ch. 14 §14.5 an asymmetric state only convert at exactly one boundary
Global phase §18.3, Ch. 6 §6.6 Operator ==, never .equiv() record out of band; test controlled
Parameter names §18.3, Ch. 6 §6.6 a KeyError on rebind normalize both sides
Register width §18.2 vector-length mismatch act on every declared qubit, or set it explicitly
Layout / physical qubits §18.3 .layout is None after import serialize before transpiling; store the four numbers
Moment / schedule structure §18.3 nothing gate-level re-derive, or carry out of band
Differentiability §18.4 no gradient exists to check do not serialize; compose instead
Q# Adjoint/Controlled functors Ch. 15 §15.6 absence of the operation write the variants out
Braket verbatim boxes Ch. 17 §17.5 the compiler re-optimizes provider-specific; keep on one side
Pulse calibrations Ch. 31 provider-specific; qiskit.pulse gone in 2.0
Dynamic control flow Ch. 9 QASM2ExportError or silence QASM 3 only, and test the target
Noise models Ch. 11 §11.7 never in the circuit; re-specify

Read the third column first. Four of the twelve rows have nothing that detects them at the gate level, and those four — phase, layout, schedule, differentiability — are the ones that produce a running program with a wrong answer. The rows that fail loudly (parameter rebinding, register width, QASM 2 refusing a free parameter) are the cheap ones.

That distribution is the chapter's real warning. The losses a translation pipeline reports are mostly the harmless ones. The losses it cannot report are mostly the expensive ones.

📊 What the Numbers Saycount_ops equality is not a translation test.

Every round trip in this chapter preserved count_ops exactly: the layout round trip, the QASM 3 round trip, the Cirq moment round trip, the phase-dropping round trip. count_ops was equal in every case where something important was destroyed.

It is not a bad check — it catches a broken parser, a truncated file, an unsupported gate silently dropped. It is a check on the transport, and people read it as a check on the translation.

The number that is easy to get is not the number that answers the question. A gate count is the easiest property of a circuit to compare and the least sensitive to everything Part III measured. Chapter 27 §27.4 makes the general version of this a discipline and puts a number on it: of eight computational-basis test inputs, 4/8 were blind to a real defect, and of twenty-seven structured inputs, 11/27 (41%) were. Ask of every assertion what result would indicate the bug, and if you cannot answer, the assertion is decoration.

💰 Cost and Queue — what a translation bug is charged at.

A translation bug that reaches a simulator costs you an afternoon. The same bug reaching hardware is billed.

Chapter 39 measured a single 120-iteration VQE run priced three ways: \$50 on per-minute billing, \$7,432 on per-shot billing, and \$185,542 on trapped-ion per-shot rates — the same computation, three orders of magnitude apart. A reversed-endianness bug does not fail; it returns a full, clean, confidently-wrong result set, and you pay the full price for it before you find out.

The queue makes it worse than the money suggests. Chapter 39 measured utilization at a five-minute queue of 2.31e-05 — 43,340× wall clock against QPU time. A wrong answer costs you the same queue as a right one, and the round trip to discover it is a day, not a minute.

This is why step 3 is not optional. The asymmetric verification costs one extra local simulation, which Chapter 39 measured at 22–74 ms for jobs of this size. Against \$7,432 and a day of queue, a 74 ms check that can actually fail is the best-priced thing in the pipeline.

🧪 Run It — the ten-minute version of this whole section.

Build X on qubit 0 and H on qubit 1 of a three-qubit Qiskit circuit, plus an identity on qubit 2. Then:

  1. Statevector(qc).data — note the amplitudes at indices 1 and 3.
  2. qasm2.dumps(qc), then circuit_from_qasm(...) into Cirq, then cirq.Simulator().simulate(...).final_state_vector — note indices 4 and 6.
  3. Reverse and compare. Confirm direct match: False, reversed match: True.
  4. Now delete the identity on qubit 2 and re-run. Watch the comparison change from a wrong answer to a ValueError about broadcasting (8,) against (4,).
  5. Replace the whole circuit with a Bell state and re-run. Watch every check pass.

Step 5 is the point. You have just built the test suite from Chapter 14's Case Study 1, and it cannot fail.

🧱 Project Checkpointvqelab/interop.py: the translation pipeline.

Chapter 14 built translate.py for the Qiskit↔Cirq boundary. This generalizes it to a pipeline that can route through OpenQASM, and it closes Part III's project arc.

to_qasm(circuit) / from_qasm(qasm, target) route any supported framework's circuit through OpenQASM, dispatching on the target and raising a clear error for the frameworks whose import path needs an optional dependency — because ModuleNotFoundError: No module named 'ply' deep in a pipeline is not a useful diagnostic.

TranslationReport records what was not carried across: whether the source circuit had a nonzero global phase (dropped), whether any parameter names were mangled, and which endianness convention each side uses. The report is returned, not logged — §17.6's lesson about abstractions that answer the question they were asked.

verify_translation(source, target) runs the asymmetric check from Chapter 14 §14.5 and refuses to pass a symmetric test case, raising if you hand it a Bell or GHZ state. That constraint is the entire content of Chapter 14's Case Study 1, encoded so it cannot be forgotten.

Its tests assert: a round trip through QASM preserves gate counts; global phase is reported as lost when present; a Bell state is rejected as a verification case; and an asymmetric state round trips correctly once the endianness conversion is applied.

18.5 Choosing

The decision table, with the chapter that established each entry:

Task Framework Why
Learning quantum computing Qiskit largest ecosystem; open hardware access (Ch. 2)
Running on IBM hardware Qiskit the only open free tier (Ch. 2, Ch. 12)
Device-accurate noise simulation Qiskit from_backend + fake providers (Ch. 11, Ch. 12)
Full layout/routing/optimization Qiskit most developed compiler (Ch. 10, Ch. 14 §14.7)
Explicit timing and scheduling Cirq moments are first-class (Ch. 14 §14.3)
Grid-topology algorithms Cirq GridQubit knows its geometry (Ch. 14 §14.10)
Resource estimation Q# the only production-grade estimator (Ch. 15 §15.7)
Large, long-lived codebases Q# a type system that catches interface errors (Ch. 15)
Variational algorithms, QML PennyLane differentiable QNodes, parameter-shift (Ch. 16)
Autodiff integration (Torch/JAX) PennyLane first-class interfaces (Ch. 16)
Non-superconducting hardware Braket trapped ions, neutral atoms (Ch. 17)
Bypassing the compiler Braket verbatim boxes (Ch. 17 §17.5)

Two recommendations that cut across the table:

Learn one deeply, then learn a second. Chapter 14 §14.1's argument, now demonstrated: little-endian ordering, the primitives architecture, and optimization levels all felt like facts about quantum computing until Part III showed three of them were facts about Qiskit.

Use PennyLane as a layer, not a replacement. Its plugins run QNodes on Qiskit and Cirq backends, so "PennyLane or Qiskit" is often a false choice — you can differentiate a circuit that executes on IBM hardware.

The decision procedure, in four questions

The table above is a lookup. This is the procedure, and it is deliberately short because Case Study 2's team spent three weeks on a decision that deserved an afternoon.

Ask these in order and stop at the first yes.

  1. Do you need to run on real hardware, cheaply, this week?
        -> QISKIT.  IBM's open tier is the only one (Ch. 2, Ch. 12).
           This is an access-policy fact, not a quality claim.

  2. Do you need gradients integrated with PyTorch / JAX / TensorFlow?
        -> PENNYLANE, layered over the answer to question 1.
           Not instead of it -- pennylane-qiskit runs the QNode on IBM backends.

  3. Do you need to know whether your algorithm is ever feasible?
        -> Q#'s ESTIMATOR, without adopting the language (Ch. 15 section 15.9).
           It consumes logical counts extractable from any framework.

  4. Do you need hardware that is not superconducting, or the compiler
     switched off?
        -> BRAKET.  Trapped ions, neutral atoms, verbatim boxes (Ch. 17).

  Otherwise: WHICHEVER YOUR TEAM ALREADY KNOWS.

Three properties of this procedure are load-bearing.

Every question names a capability, not a preference. Nothing in it asks which API you find cleaner. That is deliberate: §18.6's honest assessment is that ergonomics are real and are not a capability, and a decision procedure that admits preference terms will be dominated by them.

Questions 2, 3, and 4 do not exclude question 1. They compose. PennyLane layers over Qiskit; Q#'s estimator consumes any framework's logical counts; Braket's native OpenQASM 3 IR makes it a translation hub rather than an island. Case Study 2's team framed five mutually exclusive options and at least three of them were not exclusive of anything.

The default is not "the best framework." It is the one your team knows, because team fluency is the one advantage that compounds and the one you destroy by switching. This is the strongest argument in the whole comparison and it is almost never the one people make.

Cirq is absent from the procedure, which requires a note. It is not a defect: Cirq's distinctive capabilities — explicit moments, GridQubit geometry, symbolic sweeps — are the right tool when timing is the experiment, which is a real but narrow category. §18.3's measurement is the reason it cannot be a middle layer: route a Cirq circuit through any serialization and the moments collapse, so Cirq's advantage is only available while you stay inside Cirq. Pick it when the schedule is the point, and know that you are picking an endpoint rather than a hub.

Where these recommendations would flip

The table and the procedure are conditional on measurements, and every one of those conditions could change. Naming them is more useful than defending the table.

If IBM's open tier closes, question 1 has no answer. Every hardware result in this book rests on free access to real superconducting devices. That is a business decision, not a technical property, and it could reverse in a quarter. The procedure would then start with "what can you pay for," and Braket's per-task-plus-per-shot pricing becomes the reference rather than the exception.

If a second production-grade resource estimator appears, question 3 dissolves. Q#'s uniqueness here is a fact about tooling maturity in 2026, not about the language. §15.9 already recommends the estimator without the language; the moment an equivalent exists elsewhere, the recommendation loses its last dependency on Q#.

If OpenQASM 3's timing constructs get uniform support, §18.3's largest loss shrinks. duration, delay, and barrier are in the specification. If every framework's importer implemented them, moment structure would survive translation and Cirq's scheduling advantage would become portable — which would move Cirq from an endpoint to a hub.

If parameter-shift rules become a serialization concern, differentiability could travel. Nothing in principle prevents a format from carrying each gate's generator alongside its name. Nothing in OpenQASM 3 does. If that changed, question 2's "layer, do not replace" advice would weaken.

And the recommendation that would flip hardest: "learn Qiskit first." It rests on ecosystem size and open hardware access, both of which are contingent. It does not rest on Qiskit being better designed — Chapter 14's ParamResolver binds by name and makes Chapter 8's worst bug structurally impossible, which is a design point against Qiskit that this book measured and did not let the recommendation override.

🔀 In Another Framework — the same circuit is not the same experiment.

A useful habit when reading any cross-framework claim, including this chapter's: ask what the other framework would have to add to reproduce it exactly.

  • A Qiskit circuit reproduced in Cirq needs an explicit InsertStrategy to fix the schedule Qiskit left to the transpiler.
  • A Cirq circuit reproduced in Qiskit needs a scheduling pass and, on hardware, an explicit layout — Cirq's GridQubit carries geometry, Qiskit's integer index does not.
  • A PennyLane QNode reproduced anywhere needs its gradient rule written out by hand.
  • A Q# operation reproduced anywhere needs its Adjoint and Controlled variants written out, and its qubit-release discipline enforced by convention rather than by the runtime.
  • A Braket verbatim box has no equivalent at all in the other four: it is a request that the compiler stand down, and only Braket exposes one.

Five reproductions, five different things that have to be re-stated. That list is a better description of what each framework is for than any feature table.

18.6 What Actually Transfers

The most important section in the chapter, because it is the answer to "was Part II wasted if I switch frameworks?"

No. Here is the accounting.

Framework-specific (roughly 10% of Parts I–II)

  • API names and call signatures
  • Little-endian bit ordering
  • The primitives architecture (SamplerV2 / EstimatorV2)
  • optimization_level and the transpiler's specific stages
  • NoiseModel.from_backend and the fake-provider fleet

Framework-independent (roughly 90%)

The physics. Superposition, entanglement, interference, measurement. A Bell state is a Bell state.

The noise signatures. Chapter 11 §11.7's two-axis table — error fraction and peak imbalance — reproduced identically in Aer, Cirq, and Braket. Phase damping is invisible in the computational basis in all three, because that is a fact about measurement, not about software.

The diagnostic procedures. Chapter 12 §12.7's noise-or-bug decision tree works anywhere: did it run where you think, does it fail in simulation too, how far from the reference, is the deviation the right size, what shape is the deviation.

The cost models. Two-qubit gates dominate the error budget. Depth is the enemy. $T$ gates dominate fault-tolerant cost (Ch. 15). A gradient costs $2n+1$ circuit executions (Ch. 16). Connectivity overhead is a graph-embedding fact (Ch. 17).

The habits. Compute a reference value independently. Test with an asymmetric case. Check the condition number. Assert on structure, not just output. Report the stack you used.

And the epistemics, which is what this book has actually been teaching:

A number can be precise, reproducible, and about something other than what you think.

That lesson appeared in Chapter 11 (phase damping), Chapter 12 (the averaged readout statistic), Chapter 13 (the dynamical decoupling pass that ran and did nothing), Chapter 14 (the Bell state that could not fail), Chapter 15 (logical versus physical qubit counts), Chapter 16 (a gradient at machine epsilon), and Chapter 17 (a device swap that hid a 3.18× cost).

Seven chapters, seven frameworks-worth of tooling, one lesson. None of it is about Qiskit.

⚛️ The Physics Underneath — why 90% transfers, and why it has to.

The accounting above is not a lucky property of these five libraries. It follows from what a framework is.

A quantum program is a unitary applied to a state, followed by a Born-rule measurement. Every framework here is a different way of writing down the same unitary. The unitary does not care what spelling produced it — which is why Chapter 11 §11.7's noise-signature table reproduced identically in Aer, Cirq, and Braket, with phase damping invisible in the computational basis in all three. That invisibility is $\langle b | \rho | b \rangle$ being insensitive to the off-diagonal elements phase damping attacks. No library can make the computational basis see a relative phase, because the Born rule takes a modulus.

The same argument covers the cost models. Two-qubit gates dominate the error budget because they are physically harder and slower — Chapter 39 measured cz at 68–184 ns against sx at 32–64 ns, and Chapter 31 measured rz at 0.0 ns because it is a virtual Z, a frame change rather than a pulse. Depth is the enemy because decoherence integrates duration. $T$ gates dominate fault-tolerant cost because Clifford gates apply transversally and $T$ gates need distillation — Chapter 15's 450 → 2,882 physical qubits for one $T$ gate.

The 10% that is framework-specific is the part that is about software. The 90% is about physics and about arithmetic, and neither is negotiable by an API.

📊 What the Numbers Say — the 90% is an estimate, not a measurement.

This chapter is careful about the difference and should be careful here too. The 90/10 split is a judgement about the content of Parts I and II, not something that was measured. It was arrived at by listing the framework-specific items — five of them, above — and observing that the remaining categories are larger. There is no experiment behind it.

That matters because the number is doing rhetorical work: it is the answer to "was Part II wasted?", and a confident-sounding percentage is exactly the kind of easy number this book keeps warning about.

Two things are genuinely measured, and they are what the claim rests on. Chapter 11 §11.7's noise signatures reproduced in three frameworks, with the same qualitative table. And Chapter 15's Case Study 2 scorecard — of this book's own documented bugs, the Q# compiler caught 3, could not represent 1, and missed 8, and every one of the eight was an error of correspondence rather than of API. Eight of twelve bugs would have occurred in any framework.

Exercise 18.16 asks you to test the 90% claim yourself against Chapters 10–13's section headings and report where you disagree. That exercise exists because the estimate deserves the challenge, and a reader who classifies the sections and comes back with 75% has done something more useful than accepting 90%.

🔬 Honest Assessment — how much does the framework choice actually matter?

Less than this Part's length suggests.

For learning: barely at all. Any of the five will teach you superposition and entanglement, and the concepts transfer completely.

For production work: it matters, but usually less than three other decisions. Chapter 12 measured that choosing good qubits on one device mattered ~100× more than choosing the device. Chapter 17 measured that choosing the right modality for a circuit shape cost 3.18×. Chapter 16 measured that the ansatz and optimizer decide whether a variational run converges at all. Framework choice sits below all three.

Where it genuinely matters: hardware access (Qiskit, for the open tier), resource estimation (Q#, uniquely), and differentiability (PennyLane, by design). Those are capability differences, not preferences.

The rest is ergonomics — real, worth having opinions about, and not worth a rewrite.

18.7 Summary

Five frameworks write the same Bell state in 1 to 6 lines, and the differences are charges for different things: Q#'s six lines are declared types, qubit lifetimes, and cleanup; PennyLane's return statement is what gets differentiated; Braket's single line is the shortest path to a circuit.

★ Qiskit is the endianness outlier — one of three. X on qubit 0 of a two-qubit register lands at index 1 in Qiskit (little-endian) and index 2 in Cirq and Braket (big-endian). Parts I–II present little-endian ordering as though it were a fact about quantum computing; it is a fact about Qiskit. Neither convention is wrong, and neither will change.

All five frameworks read and write OpenQASM, in both directions — a genuine standard. Two asymmetries: Cirq's import lives in cirq.contrib and needs pip install ply, and Braket's native IR is OpenQASM 3, making it an unusually good translation hub.

★★ OpenQASM transfers the circuit and does not transfer the indexing. Measured: Qiskit → QASM 2 → Cirq on an asymmetric state matches only after bit reversal. QASM names qubits explicitly, so the program is unambiguous; where qubit 0 sits in a state vector is a per-framework decision QASM has no opinion about. A standard interchange format does not remove the boundary — it defines what is left at it.

It also drops the global phase (1.047198 → 0.000000) and mangles parameter names (theta[0]_theta_0_), both re-confirming Chapter 6. Global phase loss is not cosmetic: Chapter 6 watched it invert a controlled operation's answer while passing every equivalence test.

A correct pipeline has three responsibilities: serialize through QASM, convert results at exactly one boundary, and verify with an asymmetric state — since Bell and GHZ states are symmetric under bit reversal and prove nothing (Ch. 14 CS1).

Choose by capability, not preference. Qiskit for open hardware access, device-accurate noise, and the most developed compiler. Cirq for explicit timing and grid topologies. Q# for resource estimation, uniquely. PennyLane for differentiability, and often as a layer over Qiskit or Cirq rather than a replacement. Braket for non-superconducting hardware and for verbatim control.

And roughly 90% of Parts I–II is framework-independent. The physics, the noise signatures (reproduced identically in three frameworks), the diagnostic procedures, the cost models, the habits, and the epistemics. Framework choice sits below qubit choice (~100×, Ch. 12), modality choice (3.18×, Ch. 17), and ansatz/optimizer choice (Ch. 16) in how much it affects your results.


Next: Chapter 19 — Part IV begins, and the subject changes from how to run circuits to what to run. It starts with oracles: the black-box abstraction that nearly every quantum algorithm is built on, the phase-kickback trick that makes them useful, and an honest accounting of what "query complexity" does and does not promise.