Lab 02 — Meet the quantum gates
In Lab 01 a qubit did something no classical bit can. In this lab you meet the tools that make it happen: the quantum gates. You will flip phases that never show up in the counts, rotate a qubit by any angle you choose, and finish by entangling two qubits so tightly that they always agree with each other.
Wake up your lab
Everything from Lab 01 is still on your machine. Reactivate the environment and confirm the quantum stack still answers:
conda activate qc-lab # (qc-lab) should appear in your prompt
import qiskit print(qiskit.__version__)
2.x prints. No reinstalls today — we go straight to the gates.conda is not found, the environment step from Lab 01 was skipped — go back and do steps 1–4 there first.See the state itself: Statevector
Measurement counts tell you what the qubit measured as. The Statevector shows you what the qubit actually is — including the parts measurement cannot see. It needs a circuit with no measurements in it:
from qiskit import QuantumCircuit from qiskit.quantum_info import Statevector from qiskit_aer import AerSimulator sim = AerSimulator() qc = QuantumCircuit(1) qc.x(0) sv = Statevector(qc) print(sv.to_dict())
{'1': (1+0j)} — the qubit is |1⟩, which is why it always measures 1. Two views of the same truth: counts show outcomes, the statevector shows the state.Z gate: the invisible flip
The Z gate flips the phase of |1⟩: |1⟩ becomes −|1⟩, while |0⟩ is untouched. Watch what the two views say:
qc = QuantumCircuit(1) qc.x(0) qc.z(0) # flip the phase of |1> print(Statevector(qc).to_dict())
qc2 = QuantumCircuit(1, 1) qc2.x(0) qc2.z(0) qc2.measure(0, 0) print(sim.run(qc2, shots=1000).result().get_counts())
{'1': (-1+0j)} — the minus sign is really there — yet the counts still read {'1': 1000}. Measurement is blind to phase. Phase only reveals itself when states interfere — which is exactly what the next steps exploit.Y gate: flip with a twist
The Y gate does both jobs at once — it flips |0⟩ to |1⟩ and adds a phase:
qc = QuantumCircuit(1) qc.y(0) print(Statevector(qc).to_dict())
{'1': 1j} — the qubit flipped (it's |1⟩) and picked up an imaginary phase. The counts would still be a boring {'1': 1000}, because — say it with me — measurement can't see phase.S and T: fractions of a flip
Phases come in fractions too. S is the square root of Z — apply it twice and you get Z. T is the square root of S:
qc = QuantumCircuit(1) qc.x(0) qc.s(0) qc.s(0) # S twice = Z print(Statevector(qc).to_dict())
qc2 = QuantumCircuit(1) qc2.x(0) qc2.t(0) qc2.t(0) # T twice = S print(Statevector(qc2).to_dict())
{'1': (-1+0j)} (that's Z), the second {'1': 1j} (that's S). Gates have square roots — quantum control is finer than just on/off.Your turn: HZH = X
Here is a famous identity: a Z gate surrounded by two Hadamards behaves exactly like an X gate. Before running — predict the counts out loud:
qc = QuantumCircuit(1, 1) qc.h(0) qc.z(0) qc.h(0) qc.measure(0, 0) print(sim.run(qc, shots=1000).result().get_counts())
{'1': 1000} — HZH flipped |0⟩ to |1⟩, exactly like X. Why it works: the Hadamards change the basis, turning a phase-flip into a bit-flip. You just used interference on purpose — the thing measurement couldn't see is now doing visible work.Rotation gates: any angle you want
Gates aren't limited to fixed flips. RY(θ) rotates the qubit by any angle θ you choose — a dial, not a switch. Half of a half-turn behaves like H:
import math qc = QuantumCircuit(1, 1) qc.ry(math.pi/2, 0) # 90-degree rotation qc.measure(0, 0) print(sim.run(qc, shots=1000).result().get_counts())
Now dial it down to 45 degrees and watch the bias appear:
qc2 = QuantumCircuit(1, 1) qc2.ry(math.pi/4, 0) # 45-degree rotation qc2.measure(0, 0) print(sim.run(qc2, shots=1000).result().get_counts())
{'0': 853, '1': 147}. You just chose a probability. No classical bit can do that — this continuous control is the real engine behind quantum algorithms.Two qubits: CNOT
The CNOT (controlled-NOT) is the first gate that needs two qubits: it flips the target only when the control is |1⟩. Control on qubit 0, target on qubit 1:
qc = QuantumCircuit(2, 2) qc.x(0) # control qubit -> |1> qc.cx(0, 1) # flip target, because control is |1> qc.measure(0, 0) qc.measure(1, 1) print(qc.draw()) print(sim.run(qc, shots=1000).result().get_counts())
{'11': 1000} — read the bitstring right-to-left as (qubit 1, qubit 0): both are 1. Now delete the qc.x(0) line and rerun: {'00': 1000}. Control |0⟩ → the target is never touched. The gate conditions on another qubit — that conditional power is what makes multi-qubit circuits interesting.Your turn: the Bell pair
Put the control qubit in superposition first, then CNOT. Before running, predict: which of the four outcomes — 00, 01, 10, 11 — will appear?
bell = QuantumCircuit(2, 2) bell.h(0) # control -> superposition bell.cx(0, 1) # entangle target with it bell.measure(0, 0) bell.measure(1, 1) counts = sim.run(bell, shots=1000).result().get_counts() print(counts)
{'00': ~500, '11': ~500} — never 01, never 10. The two qubits are entangled: measure one and you instantly know the other, however far apart they are. Einstein called it "spooky action at a distance." You just made it on your laptop in four lines.01 or 10? Check the gate order — the H must come before the CNOT, and the CNOT's control must be qubit 0: cx(0, 1), not cx(1, 0).Debrief — what did you prove?
Gates are the verbs of quantum computing. You verified that phase is real but invisible to measurement (Z, Y, S, T), that rotations give you continuous control over probability (RY), and that a controlled gate can tie two qubits into a single entangled fate (CNOT → Bell pair). Ten gates, one simulator, zero hardware.
Next labs string these gates into real protocols — key distribution, teleportation. To understand why the H gate creates superposition in the first place, visit the qubit lesson.