Chapter 14 — Key Takeaways (Google Cirq)
The Cirq page. §14.5 (endianness) is the one that will cost you a day if you skip it.
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")])
result = cirq.Simulator(seed=42).run(circuit, repetitions=4096)
result.histogram(key="m") # {0: 2043, 3: 2053}
| 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 |
★ Moments are the central difference
A circuit is a list of Moments. len(circuit) is the depth.
explicit = cirq.Circuit([cirq.Moment([cirq.H(q[0])]),
cirq.Moment([cirq.CNOT(q[0], q[1])])])
explicit == cirq.Circuit([cirq.H(q[0]), cirq.CNOT(q[0], q[1])]) # True
Default insertion is a convenience; moment structure is the ground truth.
three independent H gates EARLIEST -> 1 moment NEW -> 3 moments
Same gates, same state (allclose verified), 3× the depth — a constructor argument.
⚛️ A moment is the honest abstraction: what kills your state is wall-clock duration against $T_1$/$T_2$, set by layers, not gate count. Caveat: a moment lasts as long as its slowest member, so depth is a proxy for duration, not a synonym.
InsertStrategy — and the per-call trap
| Strategy | Behavior |
|---|---|
EARLIEST |
slide left into the earliest moment with room (default) |
NEW |
always start a new moment |
INLINE |
most recent moment if it fits |
NEW_THEN_INLINE |
new moment for the first op, inline the rest |
strategy one append call append in a loop
EARLIEST 1 1
NEW 3 3
INLINE 1 1
NEW_THEN_INLINE 1 3 <- !!
⚠️ The strategy applies PER
appendCALL, not per operation. A refactor that splits oneappendinto a loop triples the depth without changing a gate. Assert onlen(circuit).
★★★ ENDIANNESS — the trap
X on qubit 0 of a 2-qubit register:
CIRQ amplitude at index 2 (binary 10) -> qubit 0 is the MOST significant bit
QISKIT amplitude at index 1 (binary 01) -> qubit 0 is the LEAST significant bit
CIRQ measure(q0,q1) histogram: {2: 100} raw row: [1, 0]
QISKIT counts: {'01': 100}
Cirq's 2 and Qiskit's '01' are the same physical outcome.
Why it hides
correct convention: {0: 2043, 3: 2053}
WRONG convention: {0: 2043, 3: 2053} identical? True
A Bell state is symmetric under bit reversal. So is GHZ. So is any uniform superposition. So is essentially every circuit used to check a fresh install.
🐛 The first circuit you write is the one that cannot catch this. Test with one X gate:
text correct: {2: 100} WRONG: {1: 100} identical? False <- NOW it can failA test whose expected output is invariant under the bug you fear is not a test for that bug.
def reverse_bits(value, n): # ONE place, and only one
return int(format(value, f"0{n}b")[::-1], 2)
Bit reversal is its own inverse, so scattered conversions cancel in pairs and make the bug intermittent. Convert once, at the boundary.
simulate() vs run()
simulate() -> the STATE [0.7071, 0, 0, 0.7071] (no measurement needed)
run() -> SAMPLES {0: 503, 3: 497} (measurement REQUIRED)
run() on an unmeasured circuit: ValueError: Circuit has no measurements to sample.
simulate() exposes result.qubit_map — the authoritative ordering when in doubt.
Gatesets instead of transpilation
cirq.optimize_for_target_gateset(circuit, gateset=cirq.CZTargetGateset())
['H', 'CNOT'] -> ['PhXZ(a=0.5,x=0.5,z=0)', 'PhXZ(...)', 'CZ', 'PhXZ(...)', 'PhXZ(...)']
moments 2 -> 3 unitary preserved (allclose verified)
PhXZ is Cirq's three-parameter $SU(2)$ gate — the role Qiskit's rz/sx pair plays.
⚙️ No
optimization_level, no unified layout+routing stage, noseed_transpiler. Routing exists (cirq.RouteCQC) as a separate tool. Qiskit's compilation stack is substantially more developed — it solves a different problem: arbitrary circuits onto heterogeneous devices behind a queue.
Sweeps — sympy, bound BY NAME
t = sympy.Symbol("t")
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 (sin²(t/2)=0.000) t=1.5708 P(1)=0.502 (0.500)
t=0.7854 P(1)=0.149 (0.146) t=2.3562 P(1)=0.859 (0.854)
★ Chapter 8's worst bug cannot happen here.
theta1…theta12sorts lexicographically astheta1, theta10, theta11, theta12, theta2…, and Qiskit's positional list binding puts 11 of 12 values in the wrong gate.cirq.ParamResolverbinds by name, through a dict. There is no order to get wrong.
Symbolic expressions come free: cirq.ry(2*t + sympy.pi/4) needs no special API.
Noise and devices
noisy = circuit.with_noise(cirq.depolarize(p=0.05)) # moments 2->4, ops 2->6
Error fraction 0.0930 — same order as Chapter 11's Aer measurement. Channels map onto Aer's
(depolarize, amplitude_damp, phase_damp, bit_flip, phase_flip). Phase damping is invisible
in the computational basis here too (γ = 0.0/0.3/0.6 → indistinguishable histograms) — Chapter 11
§11.7 confirmed in a second framework.
| Qubit type | Use |
|---|---|
LineQubit |
1-D chain |
GridQubit |
2-D lattice — Google's architecture; GridQubit(3,4).is_adjacent(GridQubit(3,5)) → True |
NamedQubit |
arbitrary labels |
cirq_google.Sycamore: GridDevice, 54 qubits, 88 coupling pairs. Carries topology, not
calibration — no per-qubit readout error or $T_1$/$T_2$. No Cirq equivalent of
NoiseModel.from_backend or the fake-provider fleet.
🔬 Hardware access is the real asymmetry
IBM offers open access — token, run today. That is the basis of Chapter 2 and every hardware result in this book. Google's service has historically run through research partnerships; check current terms. Learn and simulate in Cirq; expect to touch hardware through Qiskit. A statement about access policy, not quality.
🗝️ Windows papercut
UnicodeEncodeError: 'charmap' codec can't encode characters in position 3-5
Cirq diagrams use box-drawing characters. Fix: sys.stdout.reconfigure(encoding="utf-8") or
PYTHONIOENCODING=utf-8. Verified with cirq 1.7.0 on Windows 10.
Choosing
| Task | Reach for |
|---|---|
| IBM hardware today · device-accurate noise · full compilation pipeline | Qiskit |
| Explicit timing · grid topologies · symbolic sweeps · circuits as data | Cirq |
| Variational/ML with autodiff | PennyLane (Ch. 16) |
| Multi-vendor from one API | Braket (Ch. 17) |
| Type system + resource estimation | Q# (Ch. 15) |
Not competitors — different bets about what is hard. Qiskit bets on compilation and hardware heterogeneity; Cirq bets on precise control.
Common pitfalls
- Verifying a port with Bell/GHZ states only.
- Scattering
[::-1]instead of converting once. - Assuming
optimization_levelexists. - Refactoring an
appendinto a loop underNEW_THEN_INLINE. - Expecting
run()to work without a measurement. - Expecting Cirq device objects to carry calibration data.
Project piece added this chapter
vqelab/translate.py — reverse_bits() (the only function permitted to touch bit order),
histogram/state-vector converters, qiskit_to_cirq() (raises on unsupported gates rather than
approximating), assert_same_state(). 12 tests pass, including
test_x_on_qubit_zero_lands_at_different_indices and — deliberately —
test_bell_state_cannot_detect_a_reversed_convention, which asserts the weakness of the obvious
test so nobody deletes the asymmetric ones as redundant.