The Machinery · Topic 41 of 41 · The Workbench: circuits in code

From circuit to chip: native gates, compilation and noise

What does a chip actually run, and what does noise do to the Bell pair you just built?

  1. 01
  2. 02
  3. 03
  4. 04
  5. 05
  6. 06
  7. 07
  8. 08
  9. 09
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
About this section: The Workbench: circuits in code

The Workbench so far simulated the physics under a qubit. These two topics go up a level, to the circuits people write and run. The first writes the same circuits in Qiskit and in Cirq and shows the one trap they share, the order of the bits. The second compiles a circuit for a chip with a small set of gates and a line of couplers, and adds noise to the Bell pair to see how much it takes to lose its entanglement. Every number is from the real packages, Qiskit 2.6.1 and Cirq 1.7.0, and nothing here is run on, or says anything about, a real device.

See it

1. Your circuit 2. Compile 3. Route 4. Add noise H H, then CNOT two gates, written for no chip in particular native gates: cz, rz, sx Rz √X Rz Qiskit: H is three gates PhasedXZ Cirq: H is one gate Bell pair, whole: Qiskit 10 gates, 1 CZ Cirq 5 gates, 1 CZ 012 chip: a line circuit: all three pairs Qiskit: one SWAP, 3 CNOT become 6 CZ Cirq's router: two SWAPs, 9 CZ Bell fidelity F 1 − 4p/5 for a two-qubit depolarising error p F above ½ proves the pair is still entangled; here it stops at p = 0.625
A circuit reaches a result in four steps. The chip fixes the gates it can do and the pairs it can couple; the compiler rewrites your circuit in those terms, and the router adds gates where there is no coupler; noise then acts on every gate that is left. The counts are what Qiskit 2.6.1 and Cirq 1.7.0 returned for a stand-in chip with three qubits in a line, not for any real device.

Try it: a Bell pair with noise

The circuit is the Bell pair of the last topic: a Hadamard on qubit 0, then a CNOT. Set an error after each gate, let the two qubits sit idle with a T1 and a T2, and add a readout error. The table is the density matrix Cirq would compute; the bars are the chance of each outcome, in the state and as read out. Under it is the Cirq code for exactly these settings. Press Depolarise to the line to see the fidelity reach one half.

Honest limits: the error sizes here are round numbers chosen to show the effect, not measurements of any device; the Example values button sets round numbers, not a real chip’s. Real noise also has crosstalk, coherent errors and errors that depend on the gate; none of that is here. The explorer is a small density-matrix simulator written for the page; the verifier compares it with Cirq’s own density-matrix simulator, and with Qiskit’s quantum_info written a second way, on sixteen cases.

The intuition

A chip does not run “H” and “CNOT”. It can do a short list of operations directly, its native gates, and can couple only some pairs of its qubits. Everything you write is first rewritten in those terms. This step is called compilation, or transpiling in Qiskit, and it is why a circuit of two gates can become a program of ten.

Rewriting the gates. Take a stand-in chip whose native gates are CZ, Rz, √X and X. A Hadamard is then three gates, Rz(π/2), √X and Rz(π/2), equal to H up to a global phase of eiπ/4, which cannot be observed. A CNOT is a CZ with a Hadamard on each side of the target, so the Bell pair becomes ten gates (six Rz, three √X and one CZ) at depth 7 in Qiskit. Cirq’s CZTargetGateset is built differently: it keeps CZ and, in this example, merges each run of single-qubit gates into one general gate, PhasedXZ. The Hadamard is a single such gate, and the Bell pair is five operations (four PhasedXZ and one CZ) in three moments. The counts differ because the target gate sets differ and because “depth” is counted differently (every gate layer in Qiskit, moments in Cirq). What both agree on is the number of two-qubit gates, which is 1.

Which pairs can talk. A chip’s couplers form a graph. Take three qubits in a line, 0–1–2, and a circuit with a CNOT between each pair: 0 and 1, 1 and 2, and 0 and 2. The last one has no coupler. The compiler moves a state next to its partner with a SWAP gate, and a SWAP is worth three two-qubit gates. With every pair connected, Qiskit compiles the circuit to 3 CZ. On the line, with seed 11, it returns 6 CZ at each of the optimisation levels 0 to 3: the one SWAP that a triangle on a line cannot avoid costs three more. The answer is not always so stable: over 20 seeds at level 3 the count ranges from 4 to 6, and at level 1 it is 6 every time. Cirq’s RouteCQC with its default settings put in two SWAPs for the same circuit, which compiled to 9 CZ. One SWAP is enough, so the router did not find the cheapest answer; its documentation describes a heuristic cost function, and the documentation of Cirq’s optimiser says it “may not produce optimal results”. Both circuits are correct. The number of gates is a property of the circuit, the chip and the compiler.

Noise. A compiled circuit still runs on imperfect hardware. The simplest model is depolarising noise: with probability p something goes wrong and the qubit is hit by one of the three Pauli errors, X, Y or Z, each with probability p/3 (cirq.depolarize(p)); for two qubits, cirq.depolarize(p, n_qubits=2) picks one of the fifteen non-identity pairs of Paulis, each with probability p/15. The quantity to watch is the state fidelity F of the noisy state with the ideal one, here the chance that a measurement asking “is this the ideal pair?” answers yes. For a two-qubit depolarising error p after the CNOT, F = 1 − 4p/5: 0.992 at p = 1%, 0.960 at 5% and 0.920 at 10%. The 4/5 is there because three of the fifteen errors, XX, YY and ZZ, leave a Bell pair as it was.

Where the error lands matters. The same probability p of an error on one qubit is worth 1 − p after the CNOT but only 1 − 2p/3 before it: an X error on the control qubit just after the Hadamard changes nothing, because the qubit is in |+⟩ and X leaves it there. At p = 5% that is 0.9667 against 0.95. Noise models are worth little unless they say where in the circuit each error sits.

Idling costs too. Qubits decay and dephase when nothing is done to them (topic 28). In Cirq this is two channels, amplitude_damp and phase_damp, with probabilities set by the idle time t: γamp = 1 − e−t/T1 and γphase = 1 − e−2t/Tφ, where 1/Tφ = 1/T2 − 1/(2T1). Two qubits that idle for 300 ns with T1 = 100 µs and T2 = 80 µs leave the Bell pair at F = 0.9948. A depolarising error with the same fidelity would be p = 0.65%, but the two are different channels, and matching one number does not make them the same noise. Remember that T2 can be at most 2T1 (topic 28); the explorer will not let you set more.

What F can prove. If F is above one half, the pair is certainly still entangled: no state without entanglement has a fidelity above one half with a maximally entangled pair. If F is at or below one half, that test no longer proves anything, though an entangled state can sit there. For a Bell pair under two-qubit depolarising noise the line is also exact: the state is entangled when p < 0.625 and not after, at the point where F = ½. Real devices do not lose entanglement that cleanly, so treat that as the geometry of one model, not a property of chips.

The link to the ZZ lesson. An always-on ZZ shift of ζ (topic 32) leaves a conditional phase of 2πζt after a time t, and a CZ is the case where that phase is π. With the shift in the topic 32 table at Δ = +0.6 (−1.765 MHz, a size of 1.765 MHz), that takes 283.3 ns. This is why the ZZ shift is called unwanted: it is a CZ that runs all the time. Devices that make a controlled phase on purpose do it with specific pulses, which this lesson does not model.

The mathematics

A noisy gate is a channel. A noise process acts on a density matrix (topic 2) as ρ → Σk Kk ρ Kk†, with Kraus operators Kk obeying Σ Kk†Kk = I. The four channels used here are, with the Cirq call that makes each:

ChannelCirq callWhat it doesKraus operators
depolarising, one qubitcirq.depolarize(p)with probability p applies X, Y or Z, each p/3√(1−p) I, √(p/3) X, √(p/3) Y, √(p/3) Z
depolarising, two qubitscirq.depolarize(p, n_qubits=2)with probability p applies one of 15 Pauli pairs, each p/15√(1−p) II, and √(p/15) P⊗Q for the other 15
amplitude dampingcirq.amplitude_damp(γ)|1⟩ falls to |0⟩ with probability γ[[1,0],[0,√(1−γ)]], [[0,√γ],[0,0]]
phase dampingcirq.phase_damp(γ)coherences shrink by √(1−γ); populations stay[[1,0],[0,√(1−γ)]], [[0,0],[0,√γ]]

The Bell fidelity under each. Write the ideal pair as |Φ⁺⟩ = (|00⟩ + |11⟩)/√2 and F = ⟨Φ⁺|ρ|Φ⁺⟩. Every Pauli pair P⊗Q maps |Φ⁺⟩ to one of the four Bell states, four Pauli pairs to each. The identity, XX, YY and ZZ give |Φ⁺⟩ back (up to a sign); the other twelve give an orthogonal Bell state. So

two-qubit depolarising p, after the CNOT: F = (1−p) + p·(3/15) = 1 − 4p/5 one-qubit depolarising p, after the CNOT: F = 1 − p before the CNOT (qubit in |+⟩): F = 1 − 2p/3

The last case is the one in which X acts trivially, since X|+⟩ = |+⟩, so only Y and Z, two of the three errors, do harm. The figure shows the three lines and the level F = ½.

0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 1 0.9 0.8 0.7 0.6 0.4 0.3 ½ 1 − p (one qubit, after the CNOT) 1 − 4p/5 (two qubits, after) 1 − 2p/3 (one qubit, before the CNOT) error probability p
Where the error lands, and how much of it hurts. The same probability costs a Bell pair different amounts depending on the channel and its place. The two-qubit curve (the dot) reaches F = ½ at p = 0.625.

The entanglement test. For a product state |a⟩|b⟩ the overlap with |Φ⁺⟩ is ⟨Φ⁺|a b⟩ = (a0b0 + a1b1)/√2, and by the Cauchy–Schwarz inequality |a0b0 + a1b1| ≤ 1, so the overlap squared is at most ½. A state with no entanglement is a mixture of product states, and F is linear in the state, so F ≤ ½ for all of them. Hence F > ½ proves entanglement. For the depolarised pair, ρ = (1−λ)|Φ⁺⟩⟨Φ⁺| + λ I/4, the partial transpose has a negative eigenvalue exactly when F > ½, and for two qubits a negative eigenvalue of the partial transpose is a complete test of entanglement (the Peres–Horodecki criterion), which is why the line at p = 0.625 is exact for this family and only this one. The verifier computes that partial transpose.

Idle noise. After a time t, amplitude damping shrinks the excited population by e−t/T1 and the coherence by e−t/(2T1); phase damping shrinks the coherence further by e−t/Tφ. Together the coherence falls as e−t/T2, which is the relation of topic 28, 1/T2 = 1/(2T1) + 1/Tφ. Cirq’s phase_damp(γ) multiplies a coherence by √(1−γ), so γphase = 1 − e−2t/Tφ, as in the example.

Readout error. If each measured bit is flipped with probability r, the chance P that the two bits agree becomes P + 2r(1−r)(1−2P) when read out: a perfect Bell pair (P = 1) reads as agreeing with probability 1 − 2r(1−r), which is 98.02% at r = 1%.

Try it in Python. The first example compiles on a stand-in chip in Qiskit and in Cirq. GenericBackendV2 is Qiskit’s configurable stand-in for a backend: you set the number of qubits, the gates and the coupling, and its documentation says the rest of its properties are “generated by randomly sampling from default ranges extracted from historical IBM backend data”. It is not a real device, and nothing below depends on its random numbers. Run on Qiskit 2.6.1 and Cirq 1.7.0 with NumPy 2.2.6, 25 October 2026:

import cirq
import networkx as nx
from qiskit import QuantumCircuit
from qiskit.providers.fake_provider import GenericBackendV2
from qiskit.quantum_info import Operator
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

# a stand-in chip: three qubits in a line (0-1-2) and four native gates
line = [[0, 1], [1, 0], [1, 2], [2, 1]]
chip = GenericBackendV2(num_qubits=3, basis_gates=["cz", "rz", "sx", "x"], coupling_map=line, seed=1)

def compile_for(backend, level=1):
    return generate_preset_pass_manager(optimization_level=level, backend=backend, seed_transpiler=11)

# 1. one Hadamard
h = QuantumCircuit(1)
h.h(0)
t = compile_for(chip).run(h)
print("Qiskit, H ->", [(i.operation.name, [round(float(a), 4) for a in i.operation.params]) for i in t.data], "global phase", round(float(t.global_phase), 4))
placed = Operator(QuantumCircuit(3)).compose(Operator(h), qargs=[0])
print("same operation up to a phase:", Operator.from_circuit(t).equiv(placed))

# 2. the Bell pair
bell = QuantumCircuit(2)
bell.h(0)
bell.cx(0, 1)
t = compile_for(chip).run(bell)
print("Qiskit, Bell ->", dict(sorted(t.count_ops().items())), "depth", t.depth())

# 3. a CNOT between every pair of three qubits, on a line and with every pair connected
tri = QuantumCircuit(3)
tri.h(0)
tri.cx(0, 1)
tri.cx(1, 2)
tri.cx(0, 2)
full = GenericBackendV2(num_qubits=3, basis_gates=["cz", "rz", "sx", "x"], seed=1)
print("every pair connected ->", compile_for(full).run(tri).count_ops().get("cz", 0), "cz")
for level in (0, 1, 2, 3):
    print("on the line, level", level, "->", compile_for(chip, level).run(tri).count_ops().get("cz", 0), "cz")

# the same ideas in Cirq: its target gate set is CZ plus one general single-qubit gate
q0, q1, q2 = cirq.LineQubit.range(3)
target = cirq.CZTargetGateset()
h_c = cirq.optimize_for_target_gateset(cirq.Circuit(cirq.H(q0)), gateset=target)
print("Cirq, H ->", [str(op.gate) for op in h_c.all_operations()], "equal to H up to a phase:", cirq.equal_up_to_global_phase(cirq.unitary(h_c), cirq.unitary(cirq.H)))
bell_c = cirq.optimize_for_target_gateset(cirq.Circuit(cirq.H(q0), cirq.CNOT(q0, q1)), gateset=target)
n_two = sum(len(op.qubits) == 2 for op in bell_c.all_operations())
print("Cirq, Bell ->", len(list(bell_c.all_operations())), "operations,", n_two, "two-qubit,", len(bell_c), "moments")

path = nx.Graph([(q0, q1), (q1, q2)])
tri_c = cirq.Circuit(cirq.H(q0), cirq.CNOT(q0, q1), cirq.CNOT(q1, q2), cirq.CNOT(q0, q2))
routed, _, _ = cirq.RouteCQC(path).route_circuit(tri_c)
compiled = cirq.optimize_for_target_gateset(routed, gateset=target)
print("Cirq, triangle on a line ->", sum(op.gate == cirq.SWAP for op in routed.all_operations()), "swaps,", sum(op.gate == cirq.CZ for op in compiled.all_operations()), "cz")
Qiskit, H -> [('rz', [1.5708]), ('sx', []), ('rz', [1.5708])] global phase 0.7854
same operation up to a phase: True
Qiskit, Bell -> {'cz': 1, 'rz': 6, 'sx': 3} depth 7
every pair connected -> 3 cz
on the line, level 0 -> 6 cz
on the line, level 1 -> 6 cz
on the line, level 2 -> 6 cz
on the line, level 3 -> 6 cz
Cirq, H -> ['PhXZ(a=-0.5,x=0.5,z=1)'] equal to H up to a phase: True
Cirq, Bell -> 5 operations, 1 two-qubit, 3 moments
Cirq, triangle on a line -> 2 swaps, 9 cz

The second example adds noise to the Bell pair with Cirq’s density-matrix simulator (asked for double precision, as in the last topic), checks the depolarising result a second way with Qiskit’s quantum_info, and does the arithmetic of the ZZ paragraph. Run on the same versions:

import numpy as np
import cirq
from qiskit import QuantumCircuit
from qiskit.quantum_info import DensityMatrix, Pauli, Statevector, state_fidelity

q0, q1 = cirq.LineQubit.range(2)
bell = np.array([1, 0, 0, 1]) / np.sqrt(2)                      # (|00> + |11>) / sqrt 2
sim = cirq.DensityMatrixSimulator(dtype=np.complex128)

def run(*ops):
    """The Bell circuit with noise operations where you put them; returns the fidelity with the ideal pair, and the purity."""
    rho = sim.simulate(cirq.Circuit(*ops)).final_density_matrix
    return float(cirq.fidelity(rho, bell, qid_shape=(2, 2))), float(np.real(np.trace(rho @ rho)))

H, CNOT = cirq.H(q0), cirq.CNOT(q0, q1)
for p in (0.01, 0.05, 0.10):
    F, purity = run(H, CNOT, cirq.depolarize(p, n_qubits=2).on(q0, q1))
    print(f"two-qubit depolarising p = {p:.2f}: F = {F:.4f} (1 - 4p/5 = {1 - 4 * p / 5:.4f}), purity {purity:.4f}")

# the same error probability on one qubit, before and after the CNOT
p = 0.05
print("one qubit, before the CNOT: F =", round(run(H, cirq.depolarize(p).on(q0), CNOT)[0], 4), " after:", round(run(H, CNOT, cirq.depolarize(p).on(q0))[0], 4))

# idling: both qubits sit for 300 ns with T1 = 100 us and T2 = 80 us (topic 28)
T1, T2, t = 100.0, 80.0, 300.0
g_amp = 1 - np.exp(-(t / 1000) / T1)
g_phase = 1 - np.exp(-2 * (t / 1000) * (1 / T2 - 1 / (2 * T1)))
idle = [op for q in (q0, q1) for op in (cirq.amplitude_damp(g_amp).on(q), cirq.phase_damp(g_phase).on(q))]
F, _ = run(H, CNOT, *idle)
print(f"idle {t:.0f} ns: F = {F:.4f}; a depolarising error with the same F is p = {(1 - F) * 5 / 4:.4f}")

# the line where the fidelity stops proving entanglement
print("p = 0.625: F =", round(run(H, CNOT, cirq.depolarize(0.625, n_qubits=2).on(q0, q1))[0], 4))

# the same two-qubit channel written a second way, in Qiskit's quantum_info: a mixture of the 15 non-identity Pauli pairs
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
rho = DensityMatrix(qc).data
paulis = [Pauli(a + b).to_matrix() for a in "IXYZ" for b in "IXYZ"]
noisy = (1 - 0.05) * rho + sum(0.05 / 15 * m @ rho @ m.conj().T for m in paulis[1:])
print("Qiskit quantum_info, p = 0.05: F =", round(float(state_fidelity(DensityMatrix(noisy), Statevector(qc))), 4))

# a CZ from an always-on ZZ shift (topic 32): the conditional phase is 2 pi zeta t, and it reaches pi at t = 1 / (2 zeta)
zeta = 1.765e-3                                                  # GHz: the 1.765 MHz of topic 32
t_cz = 1 / (2 * zeta)
U = np.diag([1, 1, 1, np.exp(-1j * 2 * np.pi * zeta * t_cz)])
print(f"{t_cz:.1f} ns; the same matrix as cirq.CZ: {np.allclose(U, cirq.unitary(cirq.CZ))}")
two-qubit depolarising p = 0.01: F = 0.9920 (1 - 4p/5 = 0.9920), purity 0.9841
two-qubit depolarising p = 0.05: F = 0.9600 (1 - 4p/5 = 0.9600), purity 0.9221
two-qubit depolarising p = 0.10: F = 0.9200 (1 - 4p/5 = 0.9200), purity 0.8485
one qubit, before the CNOT: F = 0.9667  after: 0.95
idle 300 ns: F = 0.9948; a depolarising error with the same F is p = 0.0065
p = 0.625: F = 0.5
Qiskit quantum_info, p = 0.05: F = 0.96
283.3 ns; the same matrix as cirq.CZ: True
The same circuit has a different number of gates in Qiskit and in Cirq. Which count is right?

Both, because they answer slightly different questions. Each toolkit compiles to the gate set you give it (here Qiskit to rz, √X and CZ, Cirq to a general single-qubit gate and CZ), so single-qubit counts are not comparable. Depth is counted differently: Qiskit counts layers of gates, Cirq counts moments. And each compiler is a heuristic. The Cirq optimiser’s own documentation says it “may not produce optimal results”, and in the example Cirq’s router used two SWAPs where one was enough, while Qiskit’s count at optimisation level 3 varied from 4 to 6 CZ over 20 seeds. When you compare toolkits, compare the number of two-qubit gates after routing for the same chip graph, and say which seed and level you used.

Do not read the stand-in chip as a real one: its gate set and coupling are the ones in the code, and its error data is random.

Where it actually matters

Gate counts, fidelities and the word “compiled” in a result A figure for the number of two-qubit gates, or for a circuit’s depth, is a number after compilation for one chip and one compiler; the same algorithm costs more on a chip with fewer couplers, and a different amount with a different tool. A fidelity quoted for a Bell pair is a statement about a state, and the arithmetic of this topic shows how little it takes to lose entanglement-certifying fidelity in a simple model (at F = ½), and how much of a given error probability hurts depending on where it lands. What this topic leaves out is large: real noise has more structure than depolarising and decay, and error-mitigation and noise-aware compilation are exactly the techniques for dealing with it. It states nothing about any vendor’s device or service, and its chip is a stand-in. To go on, topic 28 has the decay channels in the language of the Lindblad equation, topic 32 the ZZ shift behind the CZ, and the software map says where each toolkit’s own documentation lives.