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

Circuits in code: Qiskit and Cirq side by side

How do you write the same circuit in the two most common Python toolkits, and why can they agree on the state and still disagree on its name?

  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

Qiskit: qc.x(0) Cirq: cirq.X(q0) q0q1 q0q1 X X the state vector, four numbers the state vector, four numbers 0100 0010 index 0index 1index 2index 3 index 0index 1index 2index 3 00011011 00011011 bitstrings read q1 q0: the 1 is at index 1 bitstrings read q0 q1: the 1 is at index 2
One state, two names. Both programs apply X to qubit 0 and both are right. Qiskit writes qubit 0 as the rightmost bit of a bitstring, so the excited qubit is index 1, “01”; Cirq writes the first qubit leftmost, so it is index 2, “10”. A result carried from one toolkit to the other without reordering is the mirror image of what was meant.

Try it: build a circuit and get the code in both toolkits

Add gates to three qubits that start in 000. The bars are the exact probabilities of the eight outcomes; the yellow marks are a sample of shots from them. Switch the naming between Qiskit’s order and Cirq’s and the same bars get different names. Under the bars are the Qiskit and the Cirq code for exactly this circuit, and the matrix product it stands for. Try the X on qubit 0 preset: it is the figure above with a third qubit, so the 1 sits at index 1 in Qiskit and at index 4 in Cirq.

Honest limits: this is a state-vector simulator written for the page, not Qiskit or Cirq. It agrees with both on random circuits of one to four qubits to twelve digits (the verifier checks that, and runs the code it writes), but its samples come from the browser’s own random numbers, so a seed here does not give the counts a package gives for the same seed. It has no noise (the next topic adds it) and no hardware.

The intuition

A quantum circuit is a list of gates on numbered wires, usually ending in a measurement. The arithmetic behind it is the arithmetic of topic 24: a state of n qubits is a column of 2n numbers, a gate is a small matrix, and applying a gate is a multiplication. Qiskit (made by IBM) and Cirq (made by Google) are two Python packages that hold such a list, simulate it on a laptop, and, for a chip they know about, translate it into that chip’s instructions (topic 41). Neither needs a quantum computer to run the examples here, and this topic does not touch one.

Same circuit, two vocabularies. In Qiskit a circuit is created for a number of qubits and the qubits are just numbers: qc = QuantumCircuit(2), then qc.h(0). In Cirq the qubits are objects that carry their own identity (LineQubit, GridQubit and NamedQubit are three kinds), and a circuit is a series of moments, the gates that happen at the same step. The Bell pair of topic 24 is two lines in each. What you get back is also different in kind: in the calls below Qiskit hands you a bitstring as text (“00”) and Cirq hands you the same outcome as an integer (0).

The exact state, and the sample. A simulator can show you something no chip can: the amplitudes themselves. A real device only ever returns shots, one bitstring per run of the circuit, and a count of shots only approximates the probabilities. With 1,000 shots of a Bell pair you expect about 500 of each outcome, give or take one standard deviation of √(1000 × ½ × ½) = 15.8. The two packages draw from different random-number generators, so with the same seed of 7 they print different splits: Qiskit 502 and 498, Cirq 512 and 488. Both are within one standard deviation of 500 and neither is wrong. A seed makes a run repeat; it does not make it more accurate, and four times as many shots only halve the error.

Which bit is qubit 0? This is the trap of the topic. Qiskit writes qubit 0 as the rightmost bit of a bitstring and the least significant bit of a state-vector index (this is called little-endian, and its own documentation for QuantumCircuit.reverse_bits uses the word, for converting “a circuit written in little-endian convention to the big-endian equivalent, and vice versa”). Cirq writes the first qubit leftmost and as the most significant bit, as numpy.kron does; its documentation says its bitstrings run “from most to least significant qubit, i.e. in big-endian ordering”. Apply X to qubit 0 of two qubits and the figure shows the result: index 1 in Qiskit, index 2 in Cirq, the same physical state. Neither convention is better. A program that moves a number from one toolkit into the other without reordering acts on, or reports, the wrong qubit, and nothing complains, because the state it produces is a perfectly valid one. The example below prints both sides, and topic 24 links the Qiskit guide that describes its own.

Three qubits, and a state that is all or nothing. Add one more CNOT to the Bell pair and you have the GHZ state (|000⟩ + |111⟩)/√2: measure it and all three bits agree, every time. It is the same state in both packages, because it is symmetric under reversing the bits. That symmetry is why a GHZ circuit is a poor test of whether you have the bit order right, and why the test to run is one gate on one qubit.

The mathematics

The index of a basis state. For qubit values q0, q1, …, qn−1 (each 0 or 1) the position of the amplitude in the state vector is

Qiskit: index = Σk qk 2k Cirq: index = Σk qk 2n−1−k

so for two qubits the four physical states land like this:

qubit 0qubit 1Qiskit index (bitstring q1 q0)Cirq index (bitstring q0 q1)
000 (“00”)0 (“00”)
101 (“01”)2 (“10”)
012 (“10”)1 (“01”)
113 (“11”)3 (“11”)

Going from one convention to the other reverses the n bits of every index. The two entries that do not move (0 and 3) are the ones whose bits are all equal, which is why a Bell pair or a GHZ state, made only of such entries, looks identical in both.

A circuit is a product of Kronecker products. A gate G on qubit k of n is I ⊗ … ⊗ G ⊗ … ⊗ I, with the identity on every other qubit (topic 24), and the circuit’s matrix is the product of its gates, the first gate rightmost. In Cirq’s order, with qubit 0 as the left factor, the Bell circuit is

UCirq = CNOT · (H ⊗ I) UQiskit = CNOT0→1 · (I ⊗ H) = SWAP · UCirq · SWAP

In Qiskit’s order the Hadamard on qubit 0 is the right factor, and the CNOT with control 0 and target 1 is a different 4 × 4 matrix, with its control on the right-hand bit. The two circuit matrices are the same operation written in two labellings, related by swapping the two qubit labels at both ends; for n qubits the SWAP becomes the permutation that reverses the bits of the index. The matrices in the example below are those two.

The sampling error. If an outcome has probability p, the number of times it appears in N shots has mean Np and standard deviation √(Np(1−p)); the share of shots has standard deviation √(p(1−p)/N). For p = ½ and N = 1,000 that is 15.8 shots, or 0.0158; for N = 4,000 it is 0.0079. To learn an unknown probability to ±0.005 at one standard deviation you need on the order of p(1−p)/0.005² shots, which is 10,000 when p = ½.

Try it in Python. The first example builds the Bell pair in both packages, prints the exact amplitudes, samples 1,000 shots with seed 7 from each, then does the same for the three-qubit GHZ state. Install with pip install qiskit==2.6.1 cirq-core==1.7.0; Cirq’s simulators work in single precision unless you ask for dtype=np.complex128, as here, so that the amplitudes carry sixteen digits instead of seven. Run on Qiskit 2.6.1 and Cirq 1.7.0 with NumPy 2.2.6, 25 October 2026:

import numpy as np
import cirq
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorSampler
from qiskit.quantum_info import Statevector

# the same Bell pair in each package
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)

q0, q1 = cirq.LineQubit.range(2)
circuit = cirq.Circuit(cirq.H(q0), cirq.CNOT(q0, q1))

# exact amplitudes
print("Qiskit state:", np.round(Statevector(qc).data.real, 4))
print("Cirq state:  ", np.round(cirq.Simulator(dtype=np.complex128).simulate(circuit).final_state_vector.real, 4))

# 1000 shots from each, with a seed so the run repeats
measured = qc.copy()
measured.measure_all()
q_counts = StatevectorSampler(seed=7).run([measured], shots=1000).result()[0].data.meas.get_counts()
c_run = cirq.Circuit(cirq.H(q0), cirq.CNOT(q0, q1), cirq.measure(q0, q1, key="m"))
c_counts = cirq.Simulator(seed=7).run(c_run, repetitions=1000).histogram(key="m")
print("Qiskit counts:", dict(sorted(q_counts.items())))
print("Cirq counts:  ", dict(sorted(c_counts.items())))
print("one standard deviation of a count:", round(float(np.sqrt(1000 * 0.5 * 0.5)), 1))

# three qubits: the GHZ state, as probabilities
ghz = QuantumCircuit(3)
ghz.h(0)
ghz.cx(0, 1)
ghz.cx(1, 2)
a, b, c = cirq.LineQubit.range(3)
ghz_c = cirq.Circuit(cirq.H(a), cirq.CNOT(a, b), cirq.CNOT(b, c))
print({str(k): round(float(v), 4) for k, v in Statevector(ghz).probabilities_dict().items()})
state = cirq.Simulator(dtype=np.complex128).simulate(ghz_c).final_state_vector
print({int(i): round(float(abs(state[i]) ** 2), 4) for i in np.flatnonzero(np.abs(state) > 1e-9)})
Qiskit state: [0.7071 0.     0.     0.7071]
Cirq state:   [0.7071 0.     0.     0.7071]
Qiskit counts: {'00': 502, '11': 498}
Cirq counts:   {0: 512, 3: 488}
one standard deviation of a count: 15.8
{'000': 0.5, '111': 0.5}
{0: 0.5, 7: 0.5}

The second example is the trap and the matrices. It puts one X on qubit 0 and finds where the 1 lands, then builds the Bell circuit’s matrix from kron exactly as topic 24 does and checks it against each package. cirq.unitary and Qiskit’s Operator give the circuit’s matrix directly. Run on the same versions:

import numpy as np
import cirq
from qiskit import QuantumCircuit
from qiskit.quantum_info import Operator, Statevector

# X on qubit 0, and where the 1 lands
qx = QuantumCircuit(2)
qx.x(0)
q0, q1 = cirq.LineQubit.range(2)
cx = cirq.Circuit(cirq.X(q0), cirq.I(q1))
sv_q = Statevector(qx)
sv_c = cirq.Simulator().simulate(cx)
print("Qiskit: index", int(np.argmax(np.abs(sv_q.data))), "bitstring", list(sv_q.probabilities_dict())[0])
print("Cirq:   index", int(np.argmax(np.abs(sv_c.final_state_vector))), "state", sv_c.dirac_notation())

# the Bell circuit's matrix in each package, built from kron() as in topic 24
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
bell = cirq.Circuit(cirq.H(q0), cirq.CNOT(q0, q1))
I2 = np.eye(2)
H = np.array([[1, 1], [1, -1]]) / np.sqrt(2)
CNOT = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]])      # control = the left-hand qubit
U_cirq = CNOT @ np.kron(H, I2)                                                   # qubit 0 is the left factor
print("Cirq's matrix is CNOT @ kron(H, I):", np.allclose(cirq.unitary(bell), U_cirq))
SWAP = np.array([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]])
print("Qiskit's matrix is SWAP @ Cirq's @ SWAP:", np.allclose(Operator(qc).data, SWAP @ U_cirq @ SWAP))
print(np.round(Operator(qc).data.real, 4) + 0.0)
print(np.round(cirq.unitary(bell).real, 4) + 0.0)
Qiskit: index 1 bitstring 01
Cirq:   index 2 state |10⟩
Cirq's matrix is CNOT @ kron(H, I): True
Qiskit's matrix is SWAP @ Cirq's @ SWAP: True
[[ 0.7071  0.7071  0.      0.    ]
 [ 0.      0.      0.7071 -0.7071]
 [ 0.      0.      0.7071  0.7071]
 [ 0.7071 -0.7071  0.      0.    ]]
[[ 0.7071  0.      0.7071  0.    ]
 [ 0.      0.7071  0.      0.7071]
 [ 0.      0.7071  0.     -0.7071]
 [ 0.7071  0.     -0.7071  0.    ]]
My result looks like the mirror image of what I expected. What went wrong?

Almost certainly the bit order: the two toolkits number the qubits of a bitstring from opposite ends (the example above prints it). Test it with one gate: put an X on qubit 0 of two or three qubits and see which index or bitstring lights up. If your code moves results between the two toolkits, or compares them with a paper that uses the textbook order (qubit 0 on the left), reverse the bits explicitly. Qiskit has QuantumCircuit.reverse_bits() for converting a circuit; for a state vector, reversing the bits of every index does it. In Cirq the order is the one you give as qubit_order, so pass it every time you read a state or a matrix of a circuit that may leave a qubit idle, or that qubit will be missing from the result.

The state vector of a Bell pair or a GHZ state will not reveal the mistake, because those states look the same either way round.

Where it actually matters

Every time a number crosses a toolkit boundary Results travel between people, papers, tutorials and packages, and each uses a convention without saying which. The mistake costs nothing visible: a valid state comes out, just not the one meant. What this topic does not cover is running anything on a real device: that needs an account with the hardware’s owner, a queue, and that owner’s terms, none of which the lesson describes or can check. It also leaves out Qiskit Aer, pulse-level control and mid-circuit measurement. What it does give you is a way to test any claim of the form “this circuit gives that state” on a laptop in a few lines, in whichever toolkit you meet, and the habit of checking the bit order first. For how the two toolkits compare as software, and what to read next, see the software map.