Day 1 — Circuit Basics
Lesson
C1000-179 exam cram — QuantumCircuit construction, gates, measurement, composition, properties.
All snippets verified against Qiskit 2.5.1 with the repo venv:
/home/kartikey_purohit/development/ibm-qiskit/.venv/bin/python
1. Constructing a QuantumCircuit
Three equivalent styles:
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
# Style 1: bare integers (auto-creates registers named 'q' and 'c')
qc = QuantumCircuit(2, 2)
print(qc.num_qubits, qc.num_clbits) # 2 2
print(qc.qregs) # [QuantumRegister(2, 'q')]
print(qc.cregs) # [ClassicalRegister(2, 'c')]
# Style 2: explicit registers (you pick the names)
qr = QuantumRegister(3, 'data')
cr = ClassicalRegister(3, 'out')
qc = QuantumCircuit(qr, cr)
# Style 3: qubits only, no clbits
qc = QuantumCircuit(4)Multiple quantum registers simply concatenate:
qc = QuantumCircuit(QuantumRegister(2, 'a'), QuantumRegister(3, 'b'))
print(qc.num_qubits) # 5AncillaRegister
AncillaRegister qubits are ordinary qubits, just tagged as ancillas:
from qiskit.circuit import AncillaRegister
qr = QuantumRegister(2, 'q')
anc = AncillaRegister(1, 'anc')
cr = ClassicalRegister(2, 'c')
qc = QuantumCircuit(qr, anc, cr)
print(qc.num_qubits, qc.num_ancillas, qc.num_clbits) # 3 1 2
print(qc.width()) # 5Common exam traps
num_qubitsincludes ancilla qubits (3 above, not 2).width()= qubits + clbits (3 + 2 = 5), not just qubits.QuantumCircuit(3, 2)→width()is 5.
2. Single-qubit gates
All are methods on the circuit: x, y, z, h, s, sdg, t, tdg, p, rx, ry, rz, u, sx, sxdg, id.
import numpy as np
qc = QuantumCircuit(1)
qc.h(0)
qc.p(np.pi/4, 0) # phase gate: diag(1, e^{i*lam})
qc.rx(np.pi/2, 0) # rotation gates take (angle, qubit)
qc.u(np.pi/2, 0, np.pi, 0) # u(theta, phi, lam, qubit)Key identities (verified with Operator(...).equiv):
from qiskit.quantum_info import Operator
# t;t == s -> True
# sx;sx == x -> True
# s;sdg == identity -> True
# u(pi/2, 0, pi) == h -> True
# u(0, 0, lam) == p(lam) (exact) -> True
# p(pi) == z (exact) -> True
# rz(pi) equiv z (up to GLOBAL PHASE only; not exactly equal)
qz = QuantumCircuit(1); qz.z(0)
qrz = QuantumCircuit(1); qrz.rz(np.pi, 0)
print(Operator(qrz).equiv(Operator(qz))) # True
print(Operator(qrz) == Operator(qz)) # False <- global phase differs!Gate broadcasting over lists works:
qc = QuantumCircuit(3)
qc.x([0, 1, 2]) # also qc.h(range(3))
print(qc.count_ops()) # OrderedDict({'x': 3})Common exam traps
- The identity gate is
qc.id(0).qc.i(0)does not exist in 2.x (hasattr(qc, 'i')isFalse).pvsrz: same up to global phase;p(pi)equalszexactly,rz(pi)does not (it's-i·Z).- Rotation/phase gates take the angle first, then the qubit:
qc.rx(theta, qubit).sdg/tdgare the adjoints (dagger) ofs/t— S and T are not self-inverse (unlike x/y/z/h/cx).
3. Two-qubit gates
cx, cz, cy, ch, swap, iswap, cp, crx, cry, crz, rxx, ryy, rzz, ecr — all circuit methods.
qc = QuantumCircuit(2)
qc.cx(0, 1) # control=0, target=1
qc.cp(np.pi/2, 0, 1) # controlled-phase: (angle, control, target)
qc.rzz(0.3, 0, 1) # two-qubit rotations: (angle, q1, q2)
qc.ecr(0, 1) # echoed cross-resonance: an IBM-hardware-native entanglerSymmetric vs asymmetric (does swapping the two qubit arguments change the operator?):
| Symmetric (order irrelevant) | Asymmetric (order matters) |
|---|---|
cz, cp, swap, iswap, rxx, ryy, rzz |
cx, cy, ch, ecr, crx, cry, crz |
(cz(0,1) == cz(1,0) verified True; cx(0,1) == cx(1,0) verified False.
Note crx/cry/crz are asymmetric even though cz/cp are symmetric.)
Useful equivalence: swap = 3 alternating CNOTs:
sw = QuantumCircuit(2); sw.swap(0, 1)
c3 = QuantumCircuit(2); c3.cx(0, 1); c3.cx(1, 0); c3.cx(0, 1)
print(Operator(sw).equiv(Operator(c3))) # Trueiswap swaps and adds a phase of i to the swapped |01⟩/|10⟩ amplitudes:
from qiskit.quantum_info import Statevector
qc = QuantumCircuit(2)
qc.x(0)
qc.iswap(0, 1)
print(Statevector(qc))
# Statevector([0.+0.j, 0.+0.j, 0.+1.j, 0.+0.j],
# dims=(2, 2)) <- i|10>, i.e. amplitude i on qubit-1=1Common exam traps
qc.cnot()andqc.toffoli()were REMOVED in Qiskit 2.x. Onlycxandccxexist. (hasattr(qc, 'cnot')→False.)- Argument order for controlled gates is
(control, target); for controlled rotations it's(angle, control, target).czis symmetric, so drawings show it as two dots — there is no "target" side.
4. Multi-qubit gates
qc = QuantumCircuit(4)
qc.ccx(0, 1, 2) # Toffoli: controls 0,1 -> target 2
qc.cswap(0, 1, 2) # Fredkin: control 0 swaps 1<->2
qc.mcx([0, 1, 2], 3) # multi-controlled X: control LIST, then target
print(qc.count_ops()) # OrderedDict({'ccx': 1, 'cswap': 1, 'mcx': 1})Build controls from any gate object with .control(n):
from qiskit.circuit.library import XGate
g = XGate().control(2)
print(g.name, g.num_qubits) # ccx 35. Measurement, barrier, reset
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.measure(0, 0) # qubit 0 -> clbit 0
qc.measure([0, 1], [0, 1]) # broadcast form
qc.barrier() # visual/optimization fence
qc.reset(0) # forces qubit back to |0>measure_all() — memorize this behavior
qc = QuantumCircuit(2)
qc.h(0); qc.cx(0, 1)
qc.measure_all()
print(qc.cregs) # [ClassicalRegister(2, 'meas')]
print(qc.count_ops()) # OrderedDict({'measure': 2, 'h': 1, 'cx': 1, 'barrier': 1})measure_all():
- adds a new
ClassicalRegisternamed'meas'(even if you already have clbits!), - inserts a barrier first,
- measures every qubit.
qc = QuantumCircuit(2, 2) # already has creg 'c'
qc.h(0)
qc.measure_all()
print(qc.num_clbits, [r.name for r in qc.cregs]) # 4 ['c', 'meas']
# use qc.measure_all(add_bits=False) to reuse the existing clbits insteadmeasure_active() measures only qubits that have at least one gate on them,
into a new register also named 'meas':
qc = QuantumCircuit(3)
qc.h(0); qc.cx(0, 1) # qubit 2 is idle
qc.measure_active()
print(qc.cregs) # [ClassicalRegister(2, 'meas')]Common exam traps
measure_all()on a circuit that already has a classical register doubles your clbits ('c'+'meas') unless you passadd_bits=False.measure_all()silently inserts a barrier — it shows up incount_ops().- The sampler result attribute is named after the register:
result[0].data.measaftermeasure_all(), butresult[0].data.cfor a defaultClassicalRegister.
6. Bit ordering — little-endian, always
Qiskit is little-endian: qubit 0 is the least significant bit. In drawings q0 is the top wire; in bitstrings q0 is the rightmost character.
from qiskit.primitives import StatevectorSampler
qc = QuantumCircuit(3, 3)
qc.x(0)
qc.x(2)
qc.measure(range(3), range(3))
sampler = StatevectorSampler(seed=1)
print(sampler.run([qc], shots=10).result()[0].data.c.get_counts())
# {'101': 10} <- read as q2 q1 q0 = 1 0 1 (the number 5)Common exam traps
x(0)on a 2-qubit circuit gives counts key'01', not'10'.- Statevector index k corresponds to bitstring of k with q0 as the low bit: Bell state has amplitudes at indices 0 and 3 →
'00'and'11'.
7. Bell and GHZ
bell = QuantumCircuit(2)
bell.h(0)
bell.cx(0, 1)
print(Statevector(bell))
# Statevector([0.70710678+0.j, 0. +0.j, 0. +0.j,
# 0.70710678+0.j],
# dims=(2, 2))Sampled (deterministic with a seed):
bellm = bell.copy()
bellm.measure_all()
sampler = StatevectorSampler(seed=42)
print(sampler.run([bellm], shots=1000).result()[0].data.meas.get_counts())
# {'11': 497, '00': 503}GHZ — two layouts, same state, different depth:
g4 = QuantumCircuit(4) # chain: h, cx(0,1), cx(1,2), cx(2,3)
g4.h(0); g4.cx(0, 1); g4.cx(1, 2); g4.cx(2, 3)
print(g4.depth()) # 4
g4b = QuantumCircuit(4) # tree/fan-out
g4b.h(0); g4b.cx(0, 1); g4b.cx(0, 2); g4b.cx(1, 3)
print(g4b.depth()) # 3
print(Statevector(g4).equiv(Statevector(g4b))) # True8. Composition: compose / append / tensor
compose — merge another circuit onto this one (returns a NEW circuit)
a = QuantumCircuit(2); a.h(0)
b = QuantumCircuit(2); b.cx(0, 1)
c = a.compose(b)
print(c.count_ops()) # OrderedDict({'h': 1, 'cx': 1})
print(a.count_ops()) # OrderedDict({'h': 1}) <- a unchanged (use inplace=True to mutate)qubits=[...]remaps:big.compose(small, qubits=[2, 1])puts small's q0→big's q2, q1→big's q1.front=Trueprepends:
a = QuantumCircuit(1); a.x(0)
b = QuantumCircuit(1); b.h(0)
c = a.compose(b, front=True)
print([ci.operation.name for ci in c.data]) # ['h', 'x']append — add one Instruction/Gate onto specific qubits (mutates in place)
bell = QuantumCircuit(2, name='bell')
bell.h(0); bell.cx(0, 1)
gate = bell.to_gate()
qc = QuantumCircuit(3)
qc.append(gate, [0, 2])
print(qc.count_ops()) # OrderedDict({'bell': 1})
print(qc.depth(), qc.size()) # 1 1 <- opaque gate counts as ONE op
print(qc.decompose().count_ops()) # OrderedDict({'h': 1, 'cx': 1})append with the wrong number of qubits raises CircuitError.
tensor — stack circuits side by side
x1 = QuantumCircuit(1); x1.x(0)
h1 = QuantumCircuit(1); h1.h(0)
t = x1.tensor(h1) # ARGUMENT (h1) goes on the LOWER wires
print(t)
# ┌───┐
# q_0: ┤ H ├
# ├───┤
# q_1: ┤ X ├
# └───┘
print(Statevector(t).probabilities_dict()) # {'10': 0.5, '11': 0.5} (q1 fixed at 1)Common exam traps
compose()returns a new circuit and leaves the original untouched (unlessinplace=True);append()mutates.a.tensor(b): b lands on qubit 0 upward, a on the higher wires (matchesa ⊗ bin little-endian matrix order).- An appended composite gate counts as one op for
size()/depth()/count_ops()until youdecompose().
9. inverse / power / copy
qc = QuantumCircuit(1)
qc.s(0); qc.t(0)
inv = qc.inverse()
print([ci.operation.name for ci in inv.data]) # ['tdg', 'sdg'] <- reversed AND daggeredinverse() on a circuit containing a measurement raises
CircuitError: 'inverse() not implemented for measure.'
qc = QuantumCircuit(1)
qc.t(0)
p = qc.power(2) # wraps the repeated circuit as a sub-gate
print(p.decompose().count_ops()) # OrderedDict({'t': 2})copy() is a real deep copy; plain assignment is just an alias:
qc = QuantumCircuit(1)
alias = qc
cp = qc.copy()
qc.h(0)
print(alias.size(), cp.size()) # 1 0
# qc.copy_empty_like() keeps registers but drops all instructions (size 0)10. to_gate / to_instruction / control
bell = QuantumCircuit(2, name='bell')
bell.h(0); bell.cx(0, 1)
g = bell.to_gate() # Gate: must be UNITARY (no measure/reset)
i = bell.to_instruction() # Instruction: may contain measure/reset
cbell = g.control(1)
print(cbell.name, cbell.num_qubits) # cbell 3 <- control qubit is FIRST when appendedto_gate() on a circuit with clbits fails:
QiskitError: 'Circuit with classical bits cannot be converted to gate.'
Use to_instruction() for anything non-unitary.
11. Circuit properties
qc = QuantumCircuit(2, 2)
qc.h(0); qc.cx(0, 1); qc.measure([0, 1], [0, 1])
print(qc.depth()) # 3 (h -> cx -> measures in parallel)
print(qc.size()) # 4 (h + cx + 2 measures)
print(qc.width()) # 4 (2 qubits + 2 clbits)
print(qc.count_ops()) # OrderedDict({'measure': 2, 'h': 1, 'cx': 1})Depth = longest path through the circuit (critical path), NOT the gate count.
qc = QuantumCircuit(3)
qc.h(0); qc.cx(0, 1); qc.h(2)
print(qc.depth(), qc.size()) # 2 3 <- h(2) runs in parallel with layer 1Barriers: excluded from size(), included in count_ops(), and they
constrain layering (gates can't cross them) even though a barrier itself is not a layer:
qc = QuantumCircuit(2)
qc.h(0); qc.barrier(); qc.x(1)
print(qc.depth(), qc.size(), len(qc.data)) # 2 2 3
print(qc.count_ops()) # OrderedDict({'h': 1, 'barrier': 1, 'x': 1})
# without the barrier: depth would be 1 (h and x in parallel)Common exam traps
depth()is the longest path, so parallel gates don't add depth.size()skips barriers;count_ops()andlen(qc.data)include them.width()counts clbits too.num_qubitsdoesn't.- Measurements count toward both
size()anddepth().
CODING LABS — predict, THEN run
Run each with:
/home/kartikey_purohit/development/ibm-qiskit/.venv/bin/python labN.py
Write your predictions on paper first. Full solutions are in solution-bank.md.
Lab 1 — Depth Detective
Predict depth / size / width / count_ops for A, B, and C before running.
"""Lab 1: Depth Detective. Predict BEFORE running:
depth, size, width, count_ops of each circuit."""
from qiskit import QuantumCircuit
# --- Circuit A ---
qa = QuantumCircuit(3)
qa.h(0)
qa.h(1)
qa.cx(0, 1)
qa.x(2)
qa.cx(1, 2)
print("A depth:", qa.depth())
print("A size: ", qa.size())
print("A width:", qa.width())
print("A ops: ", dict(qa.count_ops()))
# --- Circuit B: same gates, plus a barrier ---
qb = QuantumCircuit(3)
qb.h(0)
qb.h(1)
qb.barrier()
qb.cx(0, 1)
qb.x(2)
qb.cx(1, 2)
print("B depth:", qb.depth())
print("B size: ", qb.size())
print("B ops: ", dict(qb.count_ops()))
# --- Circuit C: B with measure_all ---
qc = qb.copy()
qc.measure_all()
print("C width:", qc.width())
print("C clbit registers:", [r.name for r in qc.cregs])
print("C depth:", qc.depth())Think hard about: does the barrier change B's depth vs A? What does measure_all do to width?
Lab 2 — The Little-Endian Vault
The vault opens only if you predict every bitstring correctly. Predict all three counts dicts.
"""Lab 2: The Little-Endian Vault. Predict every printed bitstring BEFORE running."""
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorSampler
sampler = StatevectorSampler(seed=2026)
# Step 1: write the number 6 (binary 110) into a 3-qubit register.
vault = QuantumCircuit(3, 3)
vault.x(1)
vault.x(2)
vault.measure(range(3), range(3))
counts = sampler.run([vault], shots=100).result()[0].data.c.get_counts()
print("Step 1 counts:", counts)
# Step 2: same circuit but swap qubits 0 and 2 before measuring.
vault2 = QuantumCircuit(3, 3)
vault2.x(1)
vault2.x(2)
vault2.swap(0, 2)
vault2.measure(range(3), range(3))
counts2 = sampler.run([vault2], shots=100).result()[0].data.c.get_counts()
print("Step 2 counts:", counts2)
# Step 3: measure qubits in REVERSED clbit order (q0->c2, q1->c1, q2->c0).
vault3 = QuantumCircuit(3, 3)
vault3.x(1)
vault3.x(2)
vault3.measure([0, 1, 2], [2, 1, 0])
counts3 = sampler.run([vault3], shots=100).result()[0].data.c.get_counts()
print("Step 3 counts:", counts3)Lab 3 — The Bell Gate Factory
You package a Bell pair as a reusable gate, wire it into a GHZ, then control it. Predict every print.
"""Lab 3: The Bell Gate Factory. Predict outputs BEFORE running."""
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorSampler
# Step 1: package a Bell pair as a reusable Gate.
bell = QuantumCircuit(2, name="bell")
bell.h(0)
bell.cx(0, 1)
bell_gate = bell.to_gate()
print("Step 1:", bell_gate.name, "| qubits:", bell_gate.num_qubits)
# Step 2: append it to a 3-qubit circuit on qubits [1, 2], then extend to GHZ.
qc = QuantumCircuit(3)
qc.append(bell_gate, [1, 2])
qc.cx(1, 0)
print("Step 2 ops:", dict(qc.count_ops()))
print("Step 2 depth:", qc.depth())
print("Step 2 decomposed ops:", dict(qc.decompose().count_ops()))
# Step 3: measure and sample. What two bitstrings appear?
qc.measure_all()
sampler = StatevectorSampler(seed=99)
counts = sampler.run([qc], shots=2000).result()[0].data.meas.get_counts()
print("Step 3 counts:", counts)
# Step 4: a CONTROLLED bell gate, control stays |0>. What comes out?
cbell = bell_gate.control(1)
print("Step 4 gate:", cbell.name, "| qubits:", cbell.num_qubits)
qc4 = QuantumCircuit(3)
qc4.append(cbell, [0, 1, 2]) # qubit 0 is the control
qc4.measure_all()
counts4 = sampler.run([qc4], shots=1000).result()[0].data.meas.get_counts()
print("Step 4 counts:", counts4)Hints: in Step 2 the Bell gate is one opaque op; in Step 4 remember what a controlled anything does when the control is |0⟩.