55 min read

> *"The fastest way to find out which of the last seven chapters was about Qiskit and which was about

Prerequisites

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

Learning Objectives

  • Write, simulate, and sample circuits in Cirq.
  • Explain why Cirq makes moments explicit and what that buys.
  • Control circuit depth directly through insertion strategies.
  • Translate between Cirq and Qiskit without introducing endianness bugs.
  • Use gatesets in place of a transpilation pipeline.
  • Judge which framework fits a given task.

Chapter 14: Google Cirq

"The fastest way to find out which of the last seven chapters was about Qiskit and which was about quantum computing is to write the same circuits in something else."

Overview

Part II went deep on one framework. That depth has a cost: it is now genuinely hard to tell which of your habits are quantum computing and which are Qiskit.

This chapter answers that by rebuilding familiar circuits in Cirq, Google's framework. Cirq makes different choices at almost every level, and the differences are instructive rather than arbitrary. The two that matter most:

Moments are explicit. In Qiskit you append gates and the scheduler works out the timing. In Cirq a circuit is a list of moments — time slices — and len(circuit) is the depth. You control it directly, and you are expected to.

And the bit ordering is reversed. Flip qubit 0 of a two-qubit register and Qiskit's state vector has its amplitude at index 1; Cirq's is at index 2. Same operation, same physics, different convention.

That second one is the chapter's most practically important content, because of how it hides:

  cirq   Bell state histogram: {0: 2043, 3: 2053}
  qiskit Bell state counts:    {'00': ..., '11': ...}

A Bell state cannot reveal the discrepancy. It is symmetric, so both frameworks agree. The first circuit everyone writes when learning a new framework is precisely the circuit that cannot catch the bug — and the bug then surfaces later, in an asymmetric result, as a wrong answer rather than an error.

In this chapter, you will learn to:

  • Build and run circuits in Cirq.
  • Think in moments, and set depth deliberately.
  • Use InsertStrategy to control parallelism.
  • Translate between frameworks without endianness bugs.
  • Replace transpilation with gatesets.
  • Choose the right framework for a job.

Learning Paths

How to read this chapter by track. - 🔰 Beginner — §14.2, §14.3, §14.5. The endianness section is not optional. - 🔬 Researcher — §14.3 and §14.7; explicit moments matter when timing is your experiment. - 🤖 Quantum ML — §14.8's sweeps, then Chapter 16, which is built for your use case. - 🏗️ Quantum Engineer — §14.4 and §14.7, plus §14.10 on devices. - 🔐 Security — skim; the translation table in §14.11 is the part you will reuse.


14.1 Why Learn a Second Framework

Three reasons, in increasing order of importance.

Portability. Hardware access changes. A result that only exists as Qiskit code is coupled to one vendor's stack.

Ideas travel. Cirq's explicit moments will change how you read a Qiskit circuit even if you never run Cirq again.

And the real one: you cannot see a convention until you have seen two. Little-endian bit ordering felt like a fact about quantum computing for the last thirteen chapters. It is a fact about Qiskit. So is the primitives architecture, the transpiler's optimization levels, and the shape of a counts dictionary. The only way to separate the field from the library is comparison.

14.2 The Same Bell State

import cirq

q = cirq.LineQubit.range(2)
circuit = cirq.Circuit([
    cirq.H(q[0]),
    cirq.CNOT(q[0], q[1]),
    cirq.measure(*q, key="m"),
])
print(circuit)
  0: ───H───@───M('m')───
            │   │
  1: ───────X───M────────
result = cirq.Simulator(seed=42).run(circuit, repetitions=4096)
print(result.histogram(key="m"))
  {0: 2043, 3: 2053}

Five differences are already visible.

Qubits are objects, not indices. cirq.LineQubit(0) is a value you pass around, compare, and sort. There is no register that owns it and no implicit numbering — a circuit's qubits are simply whichever qubits its operations mention.

Gates are applied to qubits, not the other way around. cirq.H(q[0]) builds an operation — a gate bound to qubits — which is a first-class object you can store in a list. Qiskit's qc.h(0) mutates a circuit; Cirq's cirq.H(q[0]) returns a value.

A circuit is built from a list. cirq.Circuit([...]) takes operations and arranges them. You can also append, but the constructor form is idiomatic and it makes circuits feel like data.

Measurements carry a key. key="m" names the result, and you retrieve it by name. Multiple measurement keys in one circuit are normal.

And results are integers, not bitstrings. {0: 2043, 3: 2053} rather than {'00': 2043, '11': 2053}. Which raises the obvious question — which bit is which qubit? — and §14.5 answers it, unfavorably.

🔀 In Another Framework — the same circuit, side by side.

Qiskit Cirq
qubits QuantumCircuit(2) — indices cirq.LineQubit.range(2) — objects
apply a gate qc.h(0) (mutates) cirq.H(q[0]) (returns an operation)
build append in sequence cirq.Circuit([ops])
measure qc.measure([0,1],[0,1]) cirq.measure(*q, key="m")
run SamplerV2(mode=...).run([isa]) cirq.Simulator().run(c, repetitions=n)
results {'00': 2043} bitstrings {0: 2043} integers
depth qc.depth() (computed) len(circuit) (structural)

The last row is the one to notice. In Qiskit depth is a property you query; in Cirq it is what the object is.

Qubits as objects: the consequence everything else follows from

Of the five differences, the first is the one that generates the others. It sounds cosmetic. It is not.

A Cirq circuit has no register, and therefore no width. QuantumCircuit(3) declares three qubits and owns them whether or not any gate touches them. A Cirq circuit owns nothing — its qubits are exactly the set of qubits its operations happen to mention. Measured:

  Circuit([H(LineQubit(0)), CNOT(LineQubit(0), LineQubit(1))])
    all_qubits()          [LineQubit(0), LineQubit(1)]
    state vector length   4

  ... the same circuit + I(LineQubit(5))
    all_qubits()          [LineQubit(0), LineQubit(1), LineQubit(5)]
    state vector length   8
    qubit_map             {LineQubit(0): 0, LineQubit(1): 1, LineQubit(5): 2}

Read the last line carefully. LineQubit(5) is not at position 5. It is at position 2, because it is the third qubit in sorted order among the qubits that appear. The integer inside the qubit's name is a label, not an index, and the map from label to state-vector position is recomputed every time the set of mentioned qubits changes.

That is the whole design in one behaviour. A qubit is a value with identity; a circuit is a collection of operations over such values; and positions in a state vector are derived, at the end, by sorting.

⚠️ Common Pitfall — a Cirq circuit's width is not a number you set, and it can shrink.

Delete the last operation touching a qubit and that qubit leaves the circuit. The state vector gets shorter. Every index into it moves. Nothing raises.

Chapter 18 §18.2 measured the cross-framework version of this, and it is worse than the endianness problem it hides behind. Export a Qiskit circuit that declares qreg q[3] but only acts on qubits 0 and 1, then import it into Cirq:

```text 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 ```

The idle qubit produced no operation, so it produced no qubit. Comparing the two states then fails with ValueError: operands could not be broadcast together with shapes (8,) (4,).

This is the good outcome. A width mismatch throws; an endianness mismatch returns a clean wrong answer. If you are going to have a convention bug, have the loud one.

The defensive habit: name the ordering explicitly when it matters. simulate() takes a qubit_order= argument, and passing one converts an implicit derivation into a stated assumption. Note that run() does not take it — the sampling path has no such escape hatch, so a shot-based result is always in sorted-qubit order.

14.3 Moments: The Central Difference

A Cirq Circuit is a sequence of Moments. A moment is a set of operations that act on disjoint qubits and happen at the same time.

for i, moment in enumerate(circuit):
    print(i, moment)
  moments: 3

Three moments for the Bell circuit: H, then CNOT, then the measurement. len(circuit) is 3, and that is the depth — not a computed metric but the literal length of the list.

You can build moments explicitly:

explicit = cirq.Circuit([
    cirq.Moment([cirq.H(q[0])]),
    cirq.Moment([cirq.CNOT(q[0], q[1])]),
])

and this is equal to the inferred version:

  explicit == cirq.Circuit([cirq.H(q[0]), cirq.CNOT(q[0], q[1])])   ->  True

So the default insertion behavior is a convenience, and the moment structure is the ground truth.

Why this matters

Three qubits, three H gates — an operation with no dependencies at all:

  EARLIEST strategy:  1 moment
  NEW strategy:       3 moments

Same gates, same result, three times the depth. Chapter 10 spent a whole chapter on depth as the enemy; here it is a constructor argument.

In Qiskit, parallelism is inferred by the scheduler and you influence it through optimization levels and barriers. In Cirq, you write it down. For most work the default is right and the difference is philosophical. For anything where timing is the experiment — crosstalk characterization, dynamical decoupling, pulse-adjacent work — writing the schedule beats negotiating with a scheduler.

What a Moment guarantees, exactly

The word "moment" is doing precise work, and the precision is enforced. A Moment is a set of operations acting on pairwise disjoint qubits. Both halves of that are checked:

  cirq.Moment([H(q0), X(q0)])  ->  ValueError: Overlapping operations:
                                   (cirq.H(cirq.LineQubit(0)), cirq.X(cirq.LineQubit(0)))
  cirq.Moment([H(q0), X(q1)])  ->  OK, 2 operations

  Moment([H(q0), X(q1)]) == Moment([X(q1), H(q0)])   ->  True

The equality is the interesting one. A moment does not remember the order you wrote its operations in, because within a moment there is no order — that is the claim the object exists to make. Two operations in one moment are simultaneous, and simultaneity is not a sequence.

So the type has real content. Moment is not a formatting hint or a diagram row; it is an assertion about the physical schedule, and Cirq refuses to construct one that cannot hold.

📐 Math Aside: EARLIEST is ASAP scheduling, and ASAP is depth-optimal.

Fix a sequence of operations $o_1, \dots, o_m$. Define a dependency DAG: an edge $o_i \to o_j$ whenever $i < j$ and the two operations share a qubit. Any valid moment assignment must place $o_j$ strictly after every predecessor, because operations sharing a qubit cannot be simultaneous.

Give each operation a level:

$$\ell(o_j) = 1 + \max_{o_i \to o_j} \ell(o_i), \qquad \ell = 1 \text{ for a source}$$

Two facts follow immediately. $\ell$ is a lower bound: an operation at level $k$ has a chain of $k-1$ predecessors behind it, each needing its own moment, so no schedule places it earlier. And $\ell$ is achievable: putting every operation in moment $\ell(o)$ is legal, because two operations at the same level cannot share a qubit (if they did, one would be a predecessor of the other and their levels would differ).

So $\max_j \ell(o_j)$ — the longest path in the DAG — is exactly the minimum depth, and assigning each operation its level is exactly what "slide left into the earliest moment with room" does. EARLIEST computes a critical path.

Verified on an eight-operation circuit with a deliberately non-obvious dependency structure — four H gates on four qubits, then CNOT(0,1) and CNOT(2,3), then CNOT(1,2) and CNOT(0,3):

text operations 8 EARLIEST len(circuit) 3 NEW len(circuit) 8 hand-computed ASAP levels 3 <- matches EARLIEST same final state vector True

Three moments for eight operations, and no schedule can do better. NEW gives 8, which is the other extreme and is also exactly predictable: one moment per appended operation.

The two strategies therefore bracket the achievable range. Every circuit you build in Cirq sits at depth between the critical path and the operation count, and you choose where by naming a strategy.

From moments to nanoseconds

Depth is a proxy for duration. On a real device it stops being a proxy, and Cirq's device objects carry the conversion factor. cirq_google.Sycamore's metadata declares a duration for each gate family:

  gate family                         declared duration
  SYC (the Sycamore two-qubit gate)                12 ns
  Z  (VIRTUAL, untagged)                            0
  Z  (PHYSICAL, tagged PhysicalZTag)               20 ns
  H, X, Y, PhasedXZ, single-qubit Clifford         25 ns
  sqrt(iSWAP), sqrt(iSWAP)^-1                      32 ns
  wait, CouplerPulse                                0
  measurement                                       4 ms

Two entries deserve attention.

The virtual Z costs zero and the physical Z costs 20 ns. That is Chapter 31's finding arriving from a completely different direction: Chapter 31 §31.1 measured rz = 0.0 ns on IBM hardware and explained it as a frame change rather than a pulse. Here the same physics appears as a declared property of a Google device object, with the tagged and untagged versions of the same gate given different durations. Two vendors, two stacks, one mechanism — and in Cirq's case the distinction is visible without running anything.

And measurement is declared at 4 ms, against 25 ns for a Hadamard — a factor of 160,000. This is the caveat in the callout above, quantified. A moment containing a measurement is not comparable to a moment containing gates, and any depth count that treats them as equal units is off by five orders of magnitude on that one layer.

Now the headline of §14.3, priced:

  three independent H gates
    EARLIEST   1 moment    ->  1 x 25 ns  =  25 ns
    NEW        3 moments   ->  3 x 25 ns  =  75 ns
    difference                              50 ns

Fifty nanoseconds. Whether that matters depends entirely on $T_1$, and Chapter 39 measured $T_1$ spanning 15.2 to 483.0 μs across a single chip's qubits. Working the exposure $1 - e^{-\Delta t / T_1}$ through both ends of that range:

  T1 =  15.2 us    extra decay probability over 50 ns   0.3284 %
  T1 = 483.0 us    extra decay probability over 50 ns   0.0104 %

A factor of 32 between the best and worst qubit on one chip, for the identical refactor. The comparison mixes a Google-declared gate duration with an IBM-measured $T_1$ and is therefore an order-of-magnitude estimate rather than a prediction about either device — but the shape is the point: the cost of a depth mistake is not a property of your code, it is a property of which qubit your code landed on. Chapter 29 measured that consequence directly, where a hardware-aware layout at optimization level 1 scored 0.9116 against a naive level-3 layout's 0.7720.

📊 What the Numbers Saylen(circuit) is a resource claim, not a runtime.

Three numbers describe the same circuit and they are not interchangeable:

  • Operation count — what count_ops reports, and what survives translation (Chapter 18 measured count_ops preserved exactly through every round trip that destroyed everything else).
  • Moment countlen(circuit), the number of layers. A better cost model than operation count, because decoherence is charged per layer of wall clock rather than per gate.
  • Duration — the sum over moments of the slowest member's duration. The only one that is a time.

Counting moments is a better cost model than counting gates and a worse one than summing durations. Reach for the middle one by default, and reach for the third the moment a measurement, a reset, or a wait enters the circuit — because those are the moments where the proxy fails, and the table above says it fails by up to 160,000×.

⚛️ The Physics Underneath — why a "moment" is the honest abstraction.

A quantum computer does not execute gates one at a time. It applies control pulses, and pulses that act on disjoint qubits overlap in time because there is no reason for them not to. The physical quantity that determines whether your state survives is wall-clock duration measured against $T_1$ and $T_2$ (Chapter 11 §11.7) — and duration is set by how many layers you have, not how many gates.

Qiskit's depth() and Cirq's len(circuit) are both attempts to name that quantity. Cirq's version is more honest in that it refuses to let you build a circuit without having decided the answer.

The caveat, which §14.10 returns to: a moment is not a fixed unit of time. Gates within one moment can have very different durations, and a moment lasts as long as its slowest member. Depth is a proxy for duration, not a synonym.

What the Moment model costs: it does not survive translation

Explicit scheduling buys you a statement about time. The bill comes due the moment that statement has to leave Cirq.

Chapter 18 §18.3 measured it on exactly the circuit above. Take the three independent H gates, build them both ways, export each to OpenQASM 2, and re-import:

  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 the two circuits, because there is nowhere in QASM 2 to write down "these three gates are deliberately in different layers." The unitary survives. count_ops survives. The schedule — the entire reason you reached for Cirq — is gone, and no gate-level equivalence check can see that it left.

This produces an uncomfortable structural fact about the framework, which Chapter 18 §18.5 states plainly: Cirq makes a poor middle layer. Route a circuit through any serialization and the moments collapse, so Cirq's distinctive advantage is only available while you stay inside Cirq. It is an excellent endpoint and a lossy waypoint.

The three honest responses, none of them good:

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 and the compiler can find it again. It fails precisely when the schedule was the point — a deliberately spaced sequence for a decoupling or calibration experiment is not recoverable from the gate list, because the gate list is what it is not determined by.

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

Use OpenQASM 3 and stay inside 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 importer on the other end implements that part of the specification is a separate question, and Chapter 6's pitfall list already tells you to test the tool rather than the spec.

🔬 Honest Assessment — what the moment model is and is not good for.

It is good for: anything where timing is the experiment. Crosstalk characterization, dynamical decoupling sequences, deliberately idle circuits, calibration routines, and any protocol where "these two operations happen together" is a claim you are making rather than an accident of the compiler. Chapter 31's territory, in a framework that lets you say it.

It is not good for: portability, and it is actively worse than nothing if you believe the schedule travels. A team that expresses a timing-critical experiment in moments and then ships it as QASM has encoded an assumption in a format that silently discards it, and every test they can write will pass.

And it is neutral for most work. If your schedule is whatever the compiler decides, EARLIEST gives you the critical path, which is what Qiskit's scheduler would have found anyway. The difference is philosophical until the moment it is not.

14.4 InsertStrategy

Four strategies, controlling where a new operation lands:

Strategy Behavior
EARLIEST slide left into the earliest moment with room (the default)
NEW always start a new moment
INLINE put it in the most recent moment if it fits
NEW_THEN_INLINE new moment for the first operation, inline the rest

Measured on three H gates, appended two ways — all in one append call, versus one call per gate:

  strategy                one append call    append in a loop
  EARLIEST (default)                    1                   1
  NEW                                   3                   3
  INLINE                                1                   1
  NEW_THEN_INLINE                       1                   3

EARLIEST is the default and packs aggressively, which is usually what you want. NEW is how you force serialization — useful for isolating a gate's effect, or for building the deliberately-idle circuits that Chapter 13 §13.6 needed.

And note the last row. NEW_THEN_INLINE gives 1 moment or 3 depending only on how the gates were handed to append.

⚠️ Common Pitfall — the strategy applies per append call, not per operation.

NEW_THEN_INLINE opens a new moment for the first operation of that call and inlines the rest. Hand it three gates in one call and you get one moment; call it three times and you get three.

So a refactor that splits one append into a loop can change your circuit's depth without changing a single gate. The gates are identical, the final state is identical, and the circuit is three times deeper — which on hardware means three times the exposure to decoherence.

This is the flip side of §14.3's virtue. Cirq gives you direct control of depth, and direct control means depth is now something a routine refactor can silently alter.

⚠️ Common PitfallEARLIEST can silently reorder relative to your mental model.

Operations slide as far left as they fit. If you append a gate expecting it to land after something else, and the two act on disjoint qubits, it will land in parallel with it instead. The circuit is still correct — disjoint operations commute — but the depth and the timing are not what you pictured, and on hardware timing has physical consequences.

When the schedule matters, use explicit cirq.Moment objects and stop guessing. That is the entire reason they are exposed.

The measurement the table above cannot make

The four-strategy table uses three H gates on three disjoint qubits, and that circuit is chosen to expose the NEW_THEN_INLINE trap. It has a blind spot: three of the four strategies give the same answer in both columns, so it never separates EARLIEST from INLINE.

Separating them needs a circuit with an existing moment structure to insert into. Start with a two-moment circuit — H(q0) then CNOT(q0, q1) — and append a single X(q2) on an untouched qubit:

  strategy            resulting depth   X lands in moment
  EARLIEST                          2                   0
  NEW                               3                   2
  INLINE                            2                   1
  NEW_THEN_INLINE                   3                   2

One gate, three different placements. EARLIEST slides it all the way to the front, because nothing blocks it. INLINE puts it in the most recent moment, which is the last one. NEW and NEW_THEN_INLINE both open a fresh moment, and for a single operation they are identical — the difference between them only exists when there is a second operation in the same call to inline.

So the two strategies that leave depth unchanged put the gate two moments apart. Depth is preserved and the schedule is not, and depth is the only thing anyone asserts on.

This is the general shape of §14.3's bargain, stated as a rule:

$$\text{critical path} \;\le\; \text{len(circuit)} \;\le\; \text{number of operations}$$

EARLIEST sits at the left bound and NEW at the right, with INLINE and NEW_THEN_INLINE in between at positions that depend on how the operations were batched into append calls.

🧪 Run It — make the depth trap fire, then make it impossible.

Ten minutes, no hardware, and the second half is the part you keep.

1. Fire it. Build a layer of ry rotations across four qubits two ways — one append with a list comprehension, and a for loop with one append per gate — both with strategy=cirq.InsertStrategy.NEW_THEN_INLINE. Print len(circuit) for each, then confirm the states match:

python np.allclose(sim.simulate(batched).final_state_vector, sim.simulate(looped).final_state_vector) # True len(batched), len(looped) # 1, 4

2. Confirm that no correctness test can see it. Sample both at 4,096 shots and compare histograms. Compare cirq.unitary() of both. Compare count_ops. Every check passes.

3. Make it impossible. Add one line to your test file:

python def assert_depth(circuit, expected, label=""): assert len(circuit) == expected, f"{label}: depth {len(circuit)}, expected {expected}"

4. Then price it. Multiply the depth difference by 25 ns from §14.3's Sycamore table and work $1 - e^{-\Delta t/T_1}$ at $T_1 = 15.2\ \mu\text{s}$. For a four-gate layer the answer is small. Now do it for a 20-layer ansatz, where the same refactor costs 60 extra moments.

The point of step 4 is that the answer is sometimes "this does not matter." Knowing which case you are in requires the arithmetic, and the arithmetic takes one line.

14.5 Endianness: The Trap

The most important section in the chapter, and the shortest to state.

Flip qubit 0 of a two-qubit register. Both frameworks, same operation:

  CIRQ     state vector [0, 0, 1, 0]   nonzero at index 2 = binary 10
  QISKIT   state vector [0, 1, 0, 0]   nonzero at index 1 = binary 01

$$\textbf{Cirq: qubit 0 is the MOST significant bit.}\qquad \textbf{Qiskit: qubit 0 is the LEAST significant bit.}$$

Cirq is big-endian: qubits appear in the state vector in the order you list them, left to right. Qiskit is little-endian: qubit 0 is the rightmost bit, matching the convention of writing $|q_1 q_0\rangle$.

Confirmed on the measurement side too:

  CIRQ   measure(q0, q1) histogram: {2: 100}       raw array row: [1, 0]
  QISKIT counts:                    {'01': 100}

Cirq's integer 2 and Qiskit's string '01' describe the same physical outcome. Qubit 0 is excited; the frameworks disagree only about where to write it down.

Why this is worse than an ordinary convention difference

Because of how it hides.

  cirq   Bell histogram: {0: 2043, 3: 2053}
  qiskit Bell counts:    {'00': ~2040, '11': ~2050}

A Bell state is symmetric under bit reversal. 00 reversed is 00; 11 reversed is 11. The two frameworks produce the same answer, and they would produce the same answer if one of them were wrong.

So does a GHZ state. So does any uniform superposition. So does essentially every circuit used to verify a framework installation.

Exactly how many test states are blind to it

"Symmetric under bit reversal" is a property you can count, and counting it turns a vague warning into a number.

A basis state $|x\rangle$ is invariant under the convention swap precisely when the bitstring $x$ is a palindrome. So the question "how many of my possible outcomes cannot detect this bug?" has an exact answer.

📐 Math Aside: the fixed points of bit reversal, counted.

Reversal maps bit $i$ to bit $n-1-i$. A string is fixed iff $x_i = x_{n-1-i}$ for all $i$, which pairs the positions up: position 0 with $n-1$, position 1 with $n-2$, and so on. Each pair contributes one free bit, and if $n$ is odd the middle position is its own partner and contributes one more.

The number of free bits is therefore $\lceil n/2 \rceil$, giving

$$\#\{\text{palindromes of length } n\} = 2^{\lceil n/2\rceil}, > \qquad \text{fraction} = \frac{2^{\lceil n/2\rceil}}{2^{n}} = 2^{-\lfloor n/2\rfloor}$$

Enumerated directly and checked against the formula:

text n 2^n palindromes 2^ceil(n/2) fraction 2 4 2 2 0.500000 3 8 4 4 0.500000 4 16 4 4 0.250000 6 64 8 8 0.125000 8 256 16 16 0.062500 10 1,024 32 32 0.031250 12 4,096 64 64 0.015625 16 65,536 256 256 0.003906 20 1,048,576 1,024 1,024 0.000977

At two qubits, half of all basis states are blind. At twenty qubits, under one in a thousand is.

That decay is the good news and it is also the trap, because it says the bug gets easier to catch as circuits grow — so the natural inference is that a small circuit is a safe place to test. It is the opposite. The blind fraction is highest exactly where everyone tests.

Two qubits is the worst possible width to verify a port at, and it is the width everybody uses.

And the counting understates the problem, because a distribution can be invariant without any individual outcome being a palindrome. The exact condition for a measured histogram to be blind is

$$p(x) = p(\mathrm{rev}(x)) \quad \text{for every } x$$

A Bell state satisfies this because both its outcomes are palindromes. A uniform superposition satisfies it for a different reason entirely: reversal permutes a flat distribution into itself regardless of width, so H on every qubit is blind at every $n$, not just small $n$. The one test that scales — "run it wide and see" — is the one test that never works.

That is three of Case Study 1's four verifications accounted for, by two different mechanisms.

📊 What the Numbers Say — a passing test can carry exactly zero bits of information.

Case Study 1's team ran four checks and all four passed. The natural reading is "four independent confirmations." The correct reading is measured, and it is uncomfortable:

text correct convention: {0: 2043, 3: 2053} WRONG convention: {0: 2043, 3: 2053} identical? True

Applying the bug produces the same output as not applying it. A test whose result distribution is identical under the hypothesis and its negation has a likelihood ratio of exactly 1. Passing it does not raise your confidence, because failing it was never possible.

Compare the one-gate test on the same scale:

text correct convention: {2: 100} WRONG convention: {1: 100} identical? False

Here the two hypotheses predict disjoint outcomes, so a single shot settles it.

The generalization is the book's most reusable rule: before trusting a test, ask what result the bug would produce. If the answer is "the same one," you have run a ritual. This is the same structural blindness Chapter 27 §27.6 found in blind statistical tests and Chapter 30 §30.5 found in randomized benchmarking — a measurement that cannot detect the thing being asked about. Six instances across the book, and this is the cheapest one to fix.

🐛 Debug This — the first circuit you write is the one that cannot catch this.

You port a working algorithm from Qiskit to Cirq. You check it with a Bell state: matches. You check it with a GHZ state: matches. You run the real circuit, and the answer is wrong in a way that looks like a physics problem, or like noise, or like an off-by-one in your oracle.

Test with an asymmetric state. The cheapest possible one:

```python

In BOTH frameworks: excite qubit 0 only, and check where it lands.

cirq: cirq.Circuit([cirq.X(q[0]), cirq.I(q[1])]) -> index 2 qiskit: qc = QuantumCircuit(2); qc.x(0) -> index 1 ```

One X gate. It takes ten seconds and it is the single highest-value test in any cross-framework port. Chapter 12 §12.7 step 2 in a new setting: run the case that can actually fail.

Translating safely

Reverse the qubit order at the boundary, and do it in exactly one place:

def cirq_int_to_qiskit_bitstring(value: int, n: int) -> str:
    """Cirq measurement integer -> Qiskit-style bitstring."""
    bits = format(value, f"0{n}b")      # cirq order: q0 first (MSB)
    return bits[::-1]                   # qiskit order: q0 last (LSB)

Do not scatter reversals through your code. One conversion function, tested against an asymmetric state, at the point where data crosses between frameworks. Every additional [::-1] is a place for the sign to be applied twice — and applying a bit reversal twice is the identity, which means the bug disappears and reappears depending on how many times you touched the data.

Three qubits, where it stops looking like a swap

The two-qubit case is the smallest that shows the effect, which makes it the easiest to under-learn. At two qubits the population sits at indices 1 and 2 and it is tempting to read the difference as "the middle two entries swapped" — a local fix for a local problem.

Chapter 18 §18.2 measured the same experiment one qubit wider. Apply X to qubit 0 and H to 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

Nothing swapped. The whole index space was permuted. Indices 1 and 3 became 4 and 6. Written out, the permutation is bit reversal on the index:

   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 — and that is $2^{\lceil 3/2 \rceil} = 2^2 = 4$, the formula above, landing exactly. The four survivors are 000, 010, 101, 111, and a test with support only on those four cannot detect a reversed convention at three qubits no matter how many shots you run.

The practical consequence for translation code: a correction that hard-codes a swap of two positions happens to be right at $n = 2$ and is wrong at every larger width. Write the reversal, not the swap.

Where this would flip

Two questions worth asking, because neither convention is a mistake and the chapter should not read as though Cirq's is a quirk.

When is big-endian the better default? When you are reasoning from the circuit diagram. Cirq's state-vector index reads top-to-bottom in the order the qubits appear on the page, so |q_0 q_1 q_2⟩ in the diagram is |q_0 q_1 q_2⟩ in the index. Qiskit's convention requires a mental reversal every time you move between the picture and the vector.

When is little-endian the better default? When the register holds a number. Qiskit's ordering makes $|q_1 q_0\rangle$ read as a binary numeral with $q_0$ as the ones place, which is how every classical register is written and which makes arithmetic circuits read naturally. That is not a small category: Chapter 22's QFT and Chapter 23's modular exponentiation are both arithmetic on registers, and Chapter 23 factored 15 with $a = 7$, $t = 8$ by reading a measured integer out of a phase register. Getting the endianness of that register wrong is not a display issue.

And there is a third case where the difference disappears entirely: when you never leave one framework. The bug is not created by either convention. It is created by a boundary, and a project with no boundary has no bug. This is the honest version of the chapter's advice — the risk is proportional to how many frameworks touch your data, not to which one you chose.

⚠️ Common Pitfallqubit_order looks like a fix, and it is a trap.

simulate() accepts a qubit_order= argument, and reversing it makes Cirq agree with Qiskit:

text simulate(X(q0), I(q1)) -> [0, 0, 1, 0] index 2 simulate(X(q0), I(q1), qubit_order=[q1, q0]) -> [0, 1, 0, 0] index 1

One argument, and the state vector matches. It is tempting to sprinkle it wherever the two frameworks disagree.

Do not. Three reasons, in increasing severity.

First, it is a second place that can flip bit order, which is exactly what §14.5's "convert at one boundary" rule exists to prevent. Now you have [::-1] calls and qubit_order= arguments, and both are their own inverse.

Second, run() does not accept it. Your state-vector path and your sampling path would use different mechanisms to reach the same convention, which is how a codebase ends up correct in simulation and wrong in sampling.

Third, it changes what the simulation means rather than translating its output. qubit_order is the right tool when you genuinely want a non-default ordering — comparing against a reference that uses one, for instance. It is the wrong tool for reconciling two frameworks, because the reconciliation belongs at the boundary and this is inside.

🧱 Project Checkpointvqelab/translate.py: the framework boundary.

vqelab has been Qiskit-only for seven chapters. Part III makes it portable, and the first requirement is a translation layer that does not introduce endianness bugs.

reverse_bits(value, n) is the only function in vqelab permitted to touch bit order. Everything else — cirq_histogram_to_qiskit_counts, cirq_statevector_to_qiskit, and their inverses — routes through it. The docstring states why: the operation is its own inverse, so a stray second call makes an endianness bug vanish until a refactor removes the stray call.

qiskit_to_cirq(circuit) translates a deliberately small gate set and raises NotImplementedError on anything else. A translator that silently approximates a gate is worse than one that refuses.

assert_same_state(qiskit_circuit, cirq_circuit) compares two circuits' state vectors across the convention boundary, correcting for unobservable global phase.

Its twelve tests include test_x_on_qubit_zero_lands_at_different_indices — which asserts the amplitude is at index 2 in Cirq and index 1 in Qiskit — and, unusually, test_bell_state_cannot_detect_a_reversed_convention, which asserts the weakness of the obvious test. That one exists so nobody later deletes the asymmetric tests on the grounds that the Bell test already passes.

14.6 simulate() versus run()

Cirq separates two things Qiskit splits across Statevector and the primitives:

cirq.Simulator().simulate(circuit).final_state_vector    # the STATE
cirq.Simulator().run(circuit, repetitions=4096)          # SAMPLES
  simulate() -> [0.7071+0j, 0+0j, 0+0j, 0.7071+0j]
  run()      -> {0: 503, 3: 497}

simulate() needs no measurement — it returns the full state vector, amplitudes and phases included. run() requires one:

  ValueError: Circuit has no measurements to sample.

This is a good error, and a better default than it looks. Chapter 5 §5.2 spent real effort on the distinction between a state and samples from it; Cirq builds that distinction into the method names, and refuses to let you sample a circuit that never measured anything.

simulate() also exposes the qubit ordering explicitly:

  {cirq.LineQubit(0): 0, cirq.LineQubit(1): 1}

Read that map when you are unsure. It is the authoritative statement of which qubit occupies which position, and it is right there in the result object.

14.7 Gatesets Instead of Transpilation

Cirq has no transpile() with optimization levels. It has target gatesets:

optimized = cirq.optimize_for_target_gateset(circuit, gateset=cirq.CZTargetGateset())
  before: ['H', 'CNOT']                     2 moments
  after:  ['PhXZ(a=0.5,x=0.5,z=0)', 'PhXZ(a=0.5,x=0.5,z=0)', 'CZ',
           'PhXZ(a=-0.5,x=1,z=0)', 'PhXZ(a=0.5,x=0.5,z=-1)']    3 moments

The same decomposition Chapter 10 watched Qiskit perform: an abstract CNOT becomes a native two-qubit gate (CZ here rather than ECR) wrapped in single-qubit rotations. Cirq's single-qubit form is PhXZ — a three-parameter gate that covers all of $SU(2)$, playing the role Qiskit's rz/sx decomposition plays.

⚙️ Under the Transpiler — what Cirq does not give you.

Chapter 10 catalogued six transpiler stages; Cirq's optimize_for_target_gateset corresponds to translation and optimization only. There is no unified layout-and-routing stage with selectable heuristics, no optimization_level dial, and no seed_transpiler.

Routing exists — cirq.RouteCQC implements a routing algorithm — but it is a separate, explicitly invoked tool rather than a stage in a standard pipeline.

This is a real difference in maturity of the compilation stack, and it reflects a difference in purpose. Qiskit's transpiler is built to hand arbitrary user circuits to a fleet of heterogeneous devices with a queue in front of them. Cirq grew up alongside a small number of Google devices with a research team that knew their topology. If you need the full Chapter 10 pipeline, Qiskit's is substantially more developed.

What the decomposition costs, counted

The before-and-after above is easy to skim. Counted:

                    operations   moments
  abstract circuit           2         2
  CZTargetGateset            5         3

2.5× the operations and 1.5× the depth, for a two-gate circuit — and the unitary is preserved (allclose verified). That ratio is not a Cirq fact; it is the price of the abstract-to-native translation Chapter 10 charged in Qiskit, showing up in a second framework with different native gates. CNOT is not a thing a superconducting chip does. CZ wrapped in single-qubit rotations is.

Where it matters is that the added operations and the added moment both become noise budget, and §14.9 measures exactly that.

14.8 Parameter Sweeps

Cirq uses sympy symbols directly, which is more elegant than Qiskit's Parameter class and has one important consequence.

import sympy
t = sympy.Symbol("t")

circuit = cirq.Circuit([cirq.ry(t)(q[0]), cirq.measure(q[0], key="m")])
sweep = cirq.Linspace(t, start=0, stop=np.pi, length=5)
results = cirq.Simulator(seed=42).run_sweep(circuit, params=sweep, repetitions=1000)
  t=0.0000  P(1)=0.000   (expected 0.000)
  t=0.7854  P(1)=0.149   (expected 0.146)
  t=1.5708  P(1)=0.502   (expected 0.500)
  t=2.3562  P(1)=0.859   (expected 0.854)
  t=3.1416  P(1)=1.000   (expected 1.000)

Matching $\sin^2(t/2)$ throughout — the same rotation-to-probability relationship Chapter 3 §3.4 established, arrived at from a different direction.

The consequence of using sympy: parameters are symbolic expressions, so 2*t + sympy.pi/4 is a valid gate argument with no special API. Qiskit's ParameterExpression supports arithmetic too, but sympy is a full CAS and the ergonomics show.

And a hazard it avoids. Chapter 8's parameter-ordering trap — where theta1 … theta12 sorts lexicographically and binding a list puts eleven of twelve values in the wrong gate — cannot happen here, because cirq.ParamResolver binds by name, not by position:

resolver = cirq.ParamResolver({t: 0.5})    # explicit, unordered, unambiguous

Sweeps are dict-based, so there is no order to get wrong. One framework's ergonomic choice eliminates another's most expensive silent bug.

The sort, worked all the way out

"Eleven of twelve values in the wrong gate" is the kind of claim worth checking, because it sounds like rounding. It is exact. Sorting theta1 … theta12 as strings:

  ['theta1', 'theta10', 'theta11', 'theta12', 'theta2', 'theta3',
   'theta4',  'theta5',  'theta6',  'theta7', 'theta8', 'theta9']

Bind a positional list [v1 … v12] against that order and position $i$ receives $v_i$:

  position   parameter   receives   should have received
         1   theta1            v1   v1     <- correct
         2   theta10           v2   v10
         3   theta11           v3   v11
         4   theta12           v4   v12
         5   theta2            v5   v2
         6   theta3            v6   v3
         ...
        12   theta9           v12   v9

Exactly one of twelve lands correctly, and it is the first, because theta1 is the lexicographic minimum and the numeric minimum simultaneously. Every other parameter is displaced. Verified:

  positions where sorted[i] == theta{i+1}   :  1 of 12
  parameters receiving the WRONG value      : 11 of 12

Note which one survives. The first parameter is right, which is the worst possible arrangement: a spot-check of "did theta1 get 0.1?" passes.

The same numbers through ParamResolver:

  cirq  value_of(theta2)  = 0.2      (as written)
  cirq  value_of(theta10) = 1.0      (as written)

  what a positional bind into lexicographic order would have given:
        theta2  -> 0.5
        theta10 -> 0.2

What Cirq does when a symbol is missing

Binding by name eliminates the ordering bug and introduces a different question: what happens when the dict is incomplete? A misspelled key is now possible where a misordered list was.

The answer is the good one. Resolving with a missing or misspelled symbol leaves the circuit partially parameterized, and sampling it raises:

  cirq.resolve_parameters(circuit, ParamResolver({Symbol("aa"): 0.5, b: 0.25}))
    is_parameterized  -> True
    free symbols      -> {'a'}

  Simulator().run(...)
    ValueError: Circuit contains ops whose symbols were not specified
                in parameter sweep. Ops: [cirq.Ry(rads=...

run_sweep raises the same error for a sweep that misses a symbol.

So the two failure modes are not comparable in severity. Qiskit's positional bind produces a circuit that runs, returns numbers, and is wrong. Cirq's name-based bind produces an exception with the offending operation in the message. Both are failures of the same underlying task — get the right value into the right gate — and one of them stops you.

That asymmetry is worth stating as a design principle rather than a framework score: an API that can fail should fail loudly, and an API that binds by position cannot tell a permutation from an intention. Chapter 8's bug was not a Qiskit defect so much as the inevitable consequence of a positional interface over a set with no natural order.

🗝️ Version NoteParamResolver accepts strings as well as symbols.

cirq.ParamResolver({"a": 0.5}) and cirq.ParamResolver({sympy.Symbol("a"): 0.5}) both work, and both fully resolve a circuit parameterized by sympy.Symbol("a"). Verified on cirq 1.7.0.

Convenient, and one more reason to keep symbol names short and unambiguous — the string key gives you no help from your editor and no error until run time.

14.9 Noise

noisy = circuit.with_noise(cirq.depolarize(p=0.05))
  moments 2 -> 4        operations 2 -> 6
  noisy Bell: {0: 1906, 1: 183, 2: 198, 3: 1809}
  error fraction: 0.0930

with_noise inserts a noise moment after every existing moment — visible in the moment count doubling. The error fraction of 0.093 for $p = 0.05$ on two qubits is the same order as Chapter 11 §11.7 measured for depolarizing noise in Aer.

Cirq's noise channels (depolarize, amplitude_damp, phase_damp, bit_flip, phase_flip, asymmetric_depolarize) map closely onto Aer's, and the concepts from Chapter 11 transfer directly. The physics is framework-independent; only the constructor names change.

What Cirq does not have is Chapter 11's NoiseModel.from_backend for IBM hardware, or Chapter 12's fake-backend fleet with real calibration snapshots. Device-accurate noise modeling is where Qiskit's ecosystem is furthest ahead.

Deriving the 0.0930

The error fraction is not an empirical mystery. It is computable in closed form from $p$ and the moment structure, and doing so is the fastest way to understand what with_noise actually did.

📐 Math Aside: where 0.0930 comes from.

Step 1 — count the channel applications. with_noise inserts a depolarizing channel on every qubit after every moment. The Bell circuit has 2 moments and 2 qubits, so:

$$2 \text{ moments} \times 2 \text{ qubits} = 4 \text{ channel applications}$$

which is exactly the measured operation count: $2 \to 6$, the two originals plus four.

Step 2 — what the channel does. cirq.depolarize(p) applies $I$ with probability $1-p$ and each of $X$, $Y$, $Z$ with probability $p/3$. Its Kraus operators confirm it:

text 4 Kraus operators, coefficients 0.974679, 0.129099, 0.129099, 0.129099 squared -> 0.95, 0.016667, 0.016667, 0.016667

Step 3 — ask which errors are visible. The measurement reports outcome 0 or 3 (even parity) when correct, 1 or 2 (odd parity) when not. Only errors that flip the parity are observable, and this is where three of the four applications turn out to be free. Simulated one Pauli at a time:

text X on q0 after H (before CNOT) P(odd parity) = 0.000 Z on q0 after H (before CNOT) P(odd parity) = 0.000 Y on q0 after H (before CNOT) P(odd parity) = 0.000 X on q1 after H (before CNOT) P(odd parity) = 1.000 Z on q1 after H (before CNOT) P(odd parity) = 0.000 Y on q1 after H (before CNOT) P(odd parity) = 1.000 X on q0 after CNOT P(odd parity) = 1.000 Z on q0 after CNOT P(odd parity) = 0.000

Every Pauli on the control qubit before the CNOT is invisible. $|+\rangle$ is an eigenstate of $X$, so $X$ does nothing at all; $Z$ maps $|+\rangle \to |-\rangle$, which the CNOT carries to $|\Phi^-\rangle$ — still even parity in the computational basis; and $Y \propto ZX$ inherits both. $Z$ anywhere is invisible for the same reason Chapter 11 §11.7 gave: the computational basis is blind to phase.

That leaves three parity-flip opportunities, each firing on $X$ or $Y$ only:

$$f = \frac{2p}{3} = \frac{2(0.05)}{3} = 0.03\overline{3}$$

Step 4 — combine them. Three independent flips; an odd number of them produces a visible error:

$$P(\text{odd}) = \frac{1 - (1-2f)^3}{2} = \frac{1 - (28/30)^3}{2} = 0.093481$$

Checked against the exact density matrix, which needs no derivation at all:

text diagonal of the final density matrix : [0.453259 0.046741 0.046741 0.453259] EXACT P(odd parity) : 0.093481 derivation : 0.093481 (agree to 1.5e-08, float32 simulator) measured at 4,096 shots : 0.0930 (0.10 SE from prediction)

One standard error at 4,096 shots is 0.004549, so the measurement sits a tenth of an error bar from the theory. The 0.0930 is not approximately right; it is the number.

📉 Noise Report — half the depolarizing channel does not show up, and that is a warning.

The obvious estimate for four applications of a $p = 0.05$ channel is "some error happened":

$$1 - (1-p)^4 = 0.185494$$

The truth is 0.093481 — a factor of 1.9843 smaller. Almost exactly half the channel's action is invisible to this measurement, for two separate reasons that happen to sum to about half: $Z$ errors never move computational-basis populations, and the pre-CNOT errors on the control commute through into unobservable phase.

This is not good news. The error is still there. The state is genuinely more mixed, the phase information is genuinely damaged, and a subsequent operation in a rotated basis will find it. What the histogram tells you is 0.093481; what the channel did is 0.185494; and the gap is exactly the part your measurement was not built to see.

Now compound it with the gateset. Run the same noise parameter over §14.7's decomposed circuit — the one CZTargetGateset produced, with 5 operations across 3 moments:

text moments ops noisy moments noisy ops EXACT P(odd) measured abstract H, CNOT 2 2 4 6 0.093481 0.0930 CZTargetGateset 3 5 6 11 0.145877 0.1494

Compiling to the native gateset raised the modelled error from 9.3% to 14.6% — a 1.56× increase from a transformation that preserves the unitary exactly.

Read that correctly, because the easy reading is wrong. This is not evidence that decomposition hurts on hardware. It is a property of Cirq's uniform noise model, which charges every qubit the same depolarizing rate after every moment, so more moments means more noise by construction. Real hardware does the opposite of uniform: Chapter 30 measured one chip's two-qubit error spanning 0.00750 to 0.07205 — a factor of 9.6 across the same device — while single-qubit gates are typically an order of magnitude better than either. A decomposition that trades one abstract CNOT for one native CZ plus four single-qubit rotations is usually cheaper on hardware, not more expensive.

The model measured the moment count, which is what it was built to measure. It cannot answer the question it appears to answer — the same failure Chapter 11 §11.7 warns about and the reason Chapter 12's calibration-backed noise models exist. Uniform depolarizing noise is a tool for reasoning about depth, and it is honest about depth. Ask it about gate choice and it will answer confidently and wrongly.

14.10 Devices

import cirq_google
device = cirq_google.Sycamore
  GridDevice, 54 qubits

Cirq's qubit types encode topology directly:

Type Use
LineQubit 1-D chain; LineQubit.range(n)
GridQubit 2-D lattice; GridQubit.rect(rows, cols) — Google's architecture
NamedQubit arbitrary labels; useful for abstract algorithms

GridQubit is the tell. Google's superconducting processors are two-dimensional grids, and Cirq makes that structure a property of the qubit object rather than a coupling map consulted at compile time. cirq.GridQubit(3, 4) knows it is adjacent to (3, 5).

Qubits are also ordered (LineQubit(0) < LineQubit(1)), which is what makes the canonical state-vector ordering well defined — and, per §14.5, that canonical order is the opposite of Qiskit's.

Why qubit-as-object makes Cirq device-aware for free

The geometry is not consulted. It is asked:

  GridQubit(3, 4).is_adjacent(GridQubit(3, 5))  ->  True
  GridQubit(3, 4).is_adjacent(GridQubit(0, 0))  ->  False
  GridQubit(3, 4) + (1, 0)                      ->  GridQubit(4, 4)
  GridQubit(3, 4).neighbors()                   ->  {(2,4), (3,3), (3,5), (4,4)}
  GridQubit(3, 4) < GridQubit(3, 5)             ->  True

Every one of those is a method on the qubit, answerable without a device, a backend, a coupling map, or a network call. In Qiskit the equivalent question — is qubit 3 adjacent to qubit 4? — has no answer at all until you name a backend, because integer indices carry no geometry.

That is the payoff of §14.2's first difference, and it is why an algorithm written for a lattice reads naturally in Cirq. A surface-code layout, a nearest-neighbour Ising chain, a 2-D QAOA mixer — these are all expressions over qubit coordinates, and Cirq lets you write them as such.

The cost is the mirror image. A qubit that carries geometry carries the wrong geometry the moment you target a different device, and there is no register abstraction to insulate you. A GridQubit circuit is a circuit about a grid.

What Sycamore's topology buys, and what the object does not carry

  cirq_google.Sycamore     GridDevice, 54 qubits, 88 coupling pairs
  mean degree              2 x 88 / 54 = 3.2593
  degree distribution      degree 1:  2 qubits
                           degree 2: 17 qubits
                           degree 4: 35 qubits
  graph diameter           11
  connected                True

Compare a 54-qubit line, which is the topology a naive LineQubit.range(54) implies: 53 couplers, mean degree 1.96, diameter 53. The grid's diameter is 11 — under a quarter — and diameter is close to the right cost model for routing, because moving a state from one end of the chip to the other costs a SWAP chain proportional to distance. Chapter 29 §29.1 made the same argument for IBM's heavy-hex lattice: the chip is not a complete graph, and how far it is from one determines your SWAP overhead.

Note also the two degree-1 qubits. Thirty-five of 54 qubits have four neighbours and two have one, which means "pick a qubit" is not a uniform choice even before calibration enters the picture.

And now the thing the object does not have. Its metadata exposes qubit_set, qubit_pairs, nx_graph, gateset, compilation_target_gatesets, gate_durations, isolated_qubits, and qubit_attributes — and on Sycamore, qubit_attributes is {}. There is no per-qubit $T_1$, no $T_2$, no readout error, and no per-pair gate error anywhere in the object.

That is a hard blocker for the method Chapter 29 is built on. Chapter 29 §29.4 scored candidate qubit chains using live calibration and found a hand-picked chain returning 0.6790 against a calibration-picked chain's 0.9764 — the entire difference coming from data that a Cirq device object does not contain. Chapter 30 §30.3 measured why it matters: one chip's two-qubit error ranged 0.00750 to 0.07205, a factor of 9.6, on the same day.

A topology tells you which pairs exist. Calibration tells you which of them work. Cirq gives you the first and Qiskit's fake-provider fleet gives you both, and that gap — not framework quality — is why Part V's hardware-aware chapters are written in Qiskit.

The Sycamore experiment, and what it did and did not show

Cirq's design stops looking arbitrary once you know what it was built around. The 2019 Sycamore result — Arute et al., Nature 574, 505 — was a random circuit sampling experiment on a 2-D grid of superconducting qubits, scored by cross-entropy benchmarking. Read the framework against that sentence and every choice in this chapter falls out of it:

  • GridQubit is first-class because the device is a grid.
  • Moments are explicit because random circuit sampling is defined as an alternating sequence of single-qubit layers and two-qubit layers, and "layer" is the unit of the experiment.
  • cirq.experiments ships the toolchain, which Chapter 30 §30.7 inventoried: linear_xeb_fidelity, log_xeb_fidelity, xeb_fitting, random_rotations_between_grid_interaction_layers_circuit, one- and two-qubit randomized benchmarking, and both serial and parallel readout-error estimation.

That last item is not a coincidence of taste. It is the supremacy experiment's own toolchain, shipped, which is why the random-circuit generator has "grid interaction layers" in its name.

What the result showed. That a 2-D superconducting processor at that scale could execute a deep random circuit and produce samples measurably concentrated on the ideal distribution's high-probability strings — an XEB fidelity distinguishable from zero at a width where the ideal distribution was expensive to compute. That is a genuine and difficult engineering achievement, and nothing below detracts from it.

What it did not show. Three things, each of which the book has already established the tools to say.

It did not show that the device runs your program well. Chapter 30 §30.7 is blunt about this: XEB benchmarks a random circuit, which is the most favourable possible case for a quantum device and the least favourable for a classical simulator. A comparison against a task chosen to suit you is not a comparison — the same objection Chapter 21 §21.7 raised against Grover demonstrations and Chapter 24 §24.5 raised against QAOA. Your program is not a random circuit.

It did not show a fixed classical cost. The headline of any such experiment is a runtime ratio, and half of that ratio is an estimate of what the best known classical algorithm would need. That is a claim about the state of classical simulation, not a theorem, and it is revised whenever someone writes a better simulator. Appendix J's timeline records the outcome in three words: the 2019 claim was "disputed almost immediately." Chapter 11 §11.5 explains the asymmetry that makes this permanent — proving a circuit is easy is far easier than proving it is hard, so a classical simulation result establishes an upper bound on difficulty and never a lower one, and advantage claims can only be narrowed by later work, never widened.

And it did not show that XEB is a fidelity. Chapter 30 §30.7 derives what $F_\text{XEB}$ requires: that noise pushes the output distribution toward uniform in proportion to the fidelity — the global depolarizing, or white-noise, model. Randomized benchmarking manufactures that condition with a Clifford twirl. XEB assumes it. The assumption is well supported empirically at supremacy depths and it is still an assumption rather than a theorem.

📊 What the Numbers Say — how to read any advantage claim, using this one as the worked example.

Four questions, in order. They are not specific to Sycamore.

1. Compared to what? A quantum runtime is meaningless alone. The comparison target is a classical algorithm, and the right question is which one, run by whom, with how much effort spent on it.

2. Who chose the task? If the benchmark is a random circuit, the task was chosen by the party being benchmarked. Chapter 40 §40.3's six head-to-head comparisons on tasks not chosen that way returned zero quantum wins.

3. Is the verification inside the claimed-hard regime? XEB requires simulating the ideal distribution, which is the thing being claimed intractable. Verification therefore happens at sizes where simulation still works and the claim is extrapolated upward — Chapter 35 puts the classical-checkability boundary at 30–35 qubits, and above it a result is not an advantage so much as an unverifiable assertion.

4. What does it produce, besides a score? Chapter 37 §37.7 makes this the deciding question: Goemans–Williamson emits an SDP bound certifying 0.9975 of optimal, and QAOA emits a bitstring and a hope. Random circuit sampling produces samples. There is no certificate and no artefact anyone wanted independently of the demonstration.

None of this makes the experiment dishonest, and the fourth question is not a criticism of it — it was a physics demonstration and was presented as one. The four questions are how you tell a demonstration from a deployment, and the answer for every quantum result in this book so far is "demonstration."

🔬 Honest Assessment — hardware access is the asymmetry.

Everything above runs on your laptop. The gap opens at hardware.

IBM offers open access to real devices: sign up, get a token, run a circuit today. That is the entire basis of Chapter 2, and it is why this book's hardware chapters could be written against real calibration data.

Google's Quantum Computing Service is not open in the same way. Access has historically been through research partnerships and programs rather than a public free tier. Check the current terms before planning work around it — this is exactly the kind of thing that changes, and it is the kind of claim a textbook gets wrong by being a year old.

The practical consequence for a learner: Cirq is excellent to learn and simulate with, and Qiskit is where you will most easily touch hardware. That is a statement about access policy, not about framework quality.

💰 Cost and Queue — this chapter costs nothing, and that is worth understanding rather than enjoying.

Every measurement in this chapter ran on a laptop. No token, no queue, no dollars. Timed locally on the same machine, seven runs each, cirq 1.7.0:

text circuit min median max Bell, 4,096 shots 8.10 ms 9.53 ms 29.69 ms QFT-8, 4,096 shots 30.25 ms 51.04 ms 85.13 ms

Now put those next to Chapter 39 §39.2, which scheduled the same two circuits against a real backend's instruction durations:

text circuit 2q gates depth device time @ 4,096 shots Bell 2 8 6.93 ms QFT-8 137 252 43.20 ms

The laptop and the QPU are the same order of magnitude. For circuits this size, simulation is not a fallback — it is simply faster end to end, because the QPU's 6.93 ms sits behind a queue and the laptop's 9.53 ms does not. Chapter 39 §39.3 priced that queue: at a five-minute wait, a Bell job's utilization is 2.31e-05 and you wait 43,340× longer than you compute. The device spends 99.998% of your job's lifetime working for somebody else.

The three pricing models Chapter 39 §39.5 compared make the same point in dollars: the same VQE run costs $50 per-minute, $7,432 per-shot, or $185,542 on trapped ions. Local simulation costs zero under all three.

Where this flips is width, not framework. Chapter 35 puts the classical-simulation boundary at 30–35 qubits — below it a result is checkable and therefore not an advantage; above it your laptop is out and the queue is the only option. Cirq's local-first workflow is comfortable precisely because almost everything a learner does lives below that line.

The honest framing: the reason this chapter has no hardware results is not that Cirq is a toy. It is that Google's access model is not IBM's, and at these circuit sizes the difference costs you nothing except the ability to see real noise.

14.11 Choosing

Task Reach for
Running on IBM hardware today Qiskit
Device-accurate noise simulation Qiskit (from_backend, fake providers)
Full layout/routing/optimization pipeline Qiskit
Explicit control of timing and parallelism Cirq
Grid-topology algorithms Cirq (GridQubit)
Symbolic parameters and sweeps Cirq (sympy)
Circuits as immutable data Cirq
Variational / ML workloads with autodiff PennyLane (Ch. 16)
Multi-vendor hardware from one API Braket (Ch. 17)
A real type system and resource estimation Q# (Ch. 15)

These are not competitors so much as different bets about what is hard. Qiskit bets that compilation and hardware heterogeneity are the problem. Cirq bets that precise control is. Both bets are defensible, and the answer changes with what you are building.

Where these recommendations would flip

A table like the one above is a snapshot of an ecosystem, and the honest thing to do with a snapshot is say what would change it. Four things would, and none is far-fetched.

If Google opened general hardware access. Half of this chapter's Qiskit recommendations are downstream of one fact — that IBM lets anyone with a token run a circuit today. Change that and the "running on hardware" row moves, the "device-accurate noise" row moves as soon as calibration data ships with the device objects, and the §14.10 assessment becomes obsolete. This is the single most likely change and the one this chapter is most exposed to.

If OpenQASM 3's timing constructs were widely implemented. Chapter 18 §18.5 puts this precisely: duration, delay, and barrier are in the specification, and if every importer implemented them, moment structure would survive translation and Cirq's scheduling advantage would become portable. That would move Cirq from an endpoint to a hub — the one change that would undo §14.3's cost without touching its benefit.

If your work is chemistry. OpenFermion is built on Cirq, and for Chapter 36's territory that is the strongest single reason to choose the framework, independent of everything in this chapter's table. Chapter 36 measured what that territory costs — H₂ at 4 qubits and 15 Pauli terms, LiH at 12 and 631, H₂O at 14 and 1,086 — and library support at that scale is worth more than ergonomics.

If you need XEB or parallel readout characterization. Chapter 30 §30.7's inventory is not a tiebreaker, it is a capability difference: cirq.experiments ships parallel single-qubit readout estimation, which measures all qubits simultaneously and therefore catches the crosstalk that Chapter 30 §30.5 lists as invisible to standard randomized benchmarking. Cirq gives you a measurement Qiskit's benchmark cannot make. If that is your question, the table's first three rows do not apply.

What would not flip it: endianness. Neither convention is going to change, both are defensible, and the only actionable fact is that a boundary exists. Choosing a framework to avoid §14.5 is choosing the wrong variable.

🗝️ Version Note — a Windows papercut.

Cirq's circuit diagrams use box-drawing characters, and printing one on a Windows console defaults to cp1252:

text UnicodeEncodeError: 'charmap' codec can't encode characters in position 3-5

Fix by forcing UTF-8 before printing:

python import sys sys.stdout.reconfigure(encoding="utf-8")

or set PYTHONIOENCODING=utf-8 in the environment. Verified with cirq 1.7.0 on Windows 10.

14.12 Summary

Cirq makes moments explicit. A circuit is a list of Moments, len(circuit) is the depth, and explicitly-constructed moments compare equal to inferred ones. Three parallel H gates take 1 moment under EARLIEST and 3 under NEW — same gates, same result, three times the depth, set by a constructor argument rather than negotiated with a scheduler.

A Moment is a set of operations on pairwise disjoint qubits, and both halves are enforced. Moment([H(q0), X(q0)]) raises ValueError: Overlapping operations, and Moment([H(q0), X(q1)]) == Moment([X(q1), H(q0)]) is True — within a moment there is no order, because simultaneity is not a sequence.

InsertStrategy controls packing: EARLIEST (default, packs left), NEW (serialize), INLINE, NEW_THEN_INLINE. EARLIEST can silently place operations in parallel with things you expected them to follow — correct, but not the timing you pictured.

EARLIEST is ASAP scheduling, and ASAP is depth-optimal — it assigns each operation the longest path to it in the dependency DAG, which is simultaneously a lower bound and achievable. Measured on an eight-operation circuit: EARLIEST 3 moments, NEW 8, hand-computed ASAP levels 3. So the two strategies bracket the range, $\text{critical path} \le \texttt{len(circuit)} \le \text{operations}$, and you choose where in it you sit. On a two-moment circuit, appending one X on a free qubit lands it in moment 0, 1, or 2 depending on strategy — the two that preserve depth place it two moments apart, and depth is the only thing anyone asserts on.

★ The moment structure does not survive translation. Chapter 18 §18.3 exported the three-H circuit built both ways: 3 moments in, 1 moment out, with byte-for-byte identical QASM, the unitary preserved and count_ops preserved. Every equivalence test passes; the circuits are equivalent as unitaries and not as experiments. Cirq is an excellent endpoint and a lossy waypoint — its advantage exists only while you stay inside it.

Cirq's device objects convert moments into nanoseconds. cirq_google.Sycamore declares durations per gate family: SYC 12 ns, single-qubit gates 25 ns, √iSWAP 32 ns, physical Z 20 ns, virtual Z 0, measurement 4 ms. The virtual/physical Z split is Chapter 31 §31.1's measured rz = 0.0 ns appearing as a declared property in a second vendor's stack; the 4 ms measurement is 160,000× a Hadamard, which is why a moment count is a resource claim and not a runtime. §14.3's headline priced out: 1 moment = 25 ns, 3 moments = 75 ns, and 50 ns costs 0.3284% of a qubit at $T_1 = 15.2\ \mu$s against 0.0104% at 483.0 μs — a factor of 32 for the identical refactor, depending only on which qubit you landed on.

★ Cirq is big-endian and Qiskit is little-endian. Flip qubit 0 of a two-qubit register: Cirq's amplitude lands at index 2, Qiskit's at index 1. Cirq's measurement integer 2 and Qiskit's '01' are the same physical outcome.

And a Bell state cannot reveal the difference — it is symmetric under bit reversal, as are GHZ states and uniform superpositions, which is to say every circuit anyone uses to check a fresh install. Test a port with one X gate on qubit 0. Convert at exactly one boundary; scattered [::-1] calls cancel in pairs and make the bug intermittent.

How blind the standard tests are is exactly countable. A basis state survives bit reversal iff it is a palindrome, so there are $2^{\lceil n/2\rceil}$ blind outcomes out of $2^n$ — a fraction of $2^{-\lfloor n/2\rfloor}$. At 2 qubits half of all basis states are blind; at 20 qubits fewer than 1 in 1,000 are. The blind fraction is highest exactly where everyone tests. And a uniform superposition is blind at every width, because reversal permutes a flat distribution into itself — so the one test that scales is the one that never works.

At three qubits the permutation stops looking like a swap. Chapter 18 §18.2 measured amplitudes at indices 1 and 3 arriving at 4 and 6: nothing swapped, the whole index space was permuted, and four of eight indices are fixed — which is $2^{\lceil 3/2\rceil} = 4$, the formula landing exactly. Write the reversal, not the swap; a two-position swap is accidentally right at $n = 2$ and wrong everywhere above it. And do not reach for qubit_order= to reconcile frameworks: it is a second place that flips bit order, and run() does not accept it, so your state-vector path and your sampling path would disagree.

simulate() returns the state; run() returns samples and refuses to run without a measurement (ValueError: Circuit has no measurements to sample.). The result object exposes its qubit_map — the authoritative ordering, when in doubt.

Gatesets replace transpilation. optimize_for_target_gateset translates and optimizes; H, CNOTPhXZ …, CZ, PhXZ …. There is no optimization_level, no unified layout/routing stage, and no seed_transpiler — Qiskit's compilation stack is substantially more developed, reflecting a different problem (a fleet of heterogeneous devices behind a queue).

Parameters are sympy symbols, bound by name through ParamResolver and swept with cirq.Linspace + run_sweep. Because binding is dict-based rather than positional, Chapter 8's parameter-ordering catastrophe cannot occur — one framework's ergonomics eliminate another's most expensive silent bug.

A missing symbol fails loudly. ParamResolver with a misspelled or omitted key leaves the circuit parameterized, and run raises ValueError: Circuit contains ops whose symbols were not specified in parameter sweep with the offending operation named. Qiskit's positional bind puts 11 of 12 values in the wrong gate and returns numbers. Both are failures of the same task; only one stops you.

Noise concepts transfer directly (depolarize, amplitude_damp, phase_damp, …), and with_noise doubles the moment count. What does not transfer is device-accurate modeling: Cirq has no equivalent of NoiseModel.from_backend plus a fake-provider fleet carrying real calibration snapshots.

★ The measured 0.0930 is derivable in closed form, and half the channel is invisible. with_noise applies 2 moments × 2 qubits = 4 depolarizing channels (ops 2 → 6, exactly). Of those, every Pauli on the control before the CNOT is invisible ($|+\rangle$ is an $X$ eigenstate; $Z$ only produces $|\Phi^-\rangle$), and $Z$ is invisible everywhere because the computational basis is blind to phase. That leaves three parity-flip opportunities at $f = 2p/3$, giving $P(\text{odd}) = (1 - (1-2f)^3)/2 = 0.093481$ — matching the exact density matrix and sitting 0.10 SE from the 4,096-shot measurement. The naive $1 - (1-p)^4 = 0.185494$ is 1.98× too high. The error is still there; the histogram just cannot see it.

And running the same noise over the gateset-compiled circuit gives 0.1494 (exact 0.145877) versus 0.0930 — a 1.56× increase from a unitary-preserving transformation. Read that as a fact about the model, not the hardware: uniform per-moment depolarizing charges depth, and depth went 2 → 3. Real chips are the opposite of uniform (Chapter 30 §30.3: one chip's two-qubit error spanning 0.00750 to 0.07205), and trading a CNOT for a CZ plus rotations is usually cheaper there. A measurement that cannot detect the thing being asked about, in a fifth setting.

GridQubit encodes Google's 2-D topology in the qubit object itself. Qubits are ordered, which is what makes the canonical state ordering well defined — in the opposite direction from Qiskit. GridQubit(3,4).is_adjacent(...), .neighbors(), and coordinate arithmetic all answer without a backend, which is the payoff of qubits-as-objects and the reason lattice algorithms read naturally here. The cost is the mirror image: a GridQubit circuit is a circuit about a grid.

cirq_google.Sycamore carries topology and not calibration. 54 qubits, 88 coupling pairs, mean degree 3.26, degree distribution {1: 2, 2: 17, 4: 35}, diameter 11 against a 54-qubit line's 53. Its qubit_attributes is {}no $T_1$, no $T_2$, no readout error, no per-pair gate error. That blocks Chapter 29 §29.4's method outright, where calibration-picked qubits scored 0.9764 against a hand-picked chain's 0.6790.

The Sycamore experiment explains the framework's design and does not license its conclusions. Random circuit sampling on a 2-D grid, scored by XEB — hence GridQubit, hence explicit layers, hence cirq.experiments shipping linear_xeb_fidelity, xeb_fitting, and parallel readout estimation as first-class API. What it showed: a device of that scale producing samples measurably concentrated on the ideal distribution. What it did not show: that the device runs your program (XEB benchmarks a random circuit — the most favourable possible task, chosen by the party being benchmarked); a fixed classical cost (that half of the ratio is an estimate of the best known classical algorithm, revised whenever someone writes a better one — Appendix J: "disputed almost immediately"); or that XEB is a fidelity (it assumes the white-noise model that randomized benchmarking manufactures with a twirl).

Hardware access is the real asymmetry. IBM's open access underpins this book's hardware chapters; Google's service has historically run through research partnerships. Learn and simulate in Cirq; expect to touch hardware through Qiskit — a statement about access policy, not quality.


Next: Chapter 15 — Microsoft's Q#, which is not a Python library at all but a language, with a type system, use blocks that manage qubit lifetime, and a compiler that will refuse to build a program this chapter's frameworks would happily run and get wrong.