Concepts Beginner Free 39/53 in series 15 min read

Quantum Gates Explained

A clear explanation of the fundamental quantum gates, Hadamard, Pauli, CNOT, and Toffoli, with circuit diagrams and what they actually do to qubit states.

What you'll learn

  • quantum gates
  • hadamard gate
  • cnot
  • pauli gates
  • quantum circuits
  • unitary operations

Prerequisites

  • Basic Python (variables, functions, loops)
  • No quantum physics background needed

What Is a Quantum Gate?

Quantum gates are the building blocks of quantum circuits. They function as the quantum equivalent of classical logic gates, such as AND, OR, and NOT. However, two key differences set them apart. First, quantum gates are reversible. Every quantum gate has an inverse operation. Classical gates like AND are not reversible because you cannot recover the original inputs knowing only the output. Second, quantum gates are unitary. Mathematically, a gate is represented by a unitary matrix UU, meaning UU=IU^\dagger U = I. This constraint guarantees that the total probability of all possible outcomes is always preserved.

Every quantum computation involves a sequence of quantum gates applied to qubits, followed by a measurement.

The Pauli Gates: X, Y, Z

The X gate (the NOT gate) flips a qubit: 01|0\rangle \rightarrow |1\rangle and 10|1\rangle \rightarrow |0\rangle. This operation serves as the quantum counterpart to the classical NOT gate.


Matrix:
[0  1]
[1  0]

Circuit symbol: ─[X]─
# Qiskit
from qiskit import QuantumCircuit
qc = QuantumCircuit(1)
qc.x(0)  # Flip qubit 0 from |0⟩ to |1⟩

The Z gate leaves 0|0\rangle unchanged, but it applies a phase flip to 1|1\rangle: 11|1\rangle \rightarrow -|1\rangle. This operation does not change the measurement probabilities directly; rather, it affects interference patterns.


Matrix:
[1   0]
[0  -1]

Circuit symbol: ─[Z]─

Y Gate

Y combines the effects of X and Z with a complex phase. It appears in Pauli decompositions, error correction, and the relationship Y=iXZY = iXZ.


Matrix:
[0  -i]
[i   0]

The Hadamard Gate (H)

The Hadamard gate is a fundamental gate in quantum computing. It creates superposition, placing a qubit into an equal mixture of 0|0\rangle and 1|1\rangle.

|0⟩  →  (|0⟩ + |1⟩) / √2   (equal superposition)
|1⟩  →  (|0⟩ - |1⟩) / √2   (superposition with phase)

Matrix:
[1   1] × 1/√2
[1  -1]

Circuit symbol: ─[H]─

Why it matters: Without Hadamard (or equivalent gates), quantum algorithms could not explore multiple states through interference. Every quantum algorithm that claims “quantum parallelism” relies on Hadamard or similar gates to prepare superposition states.

A common misconception deserves correction here: superposition does not mean the qubit “tries all answers simultaneously.” Rather, it means the qubit exists in a linear combination of basis states, and gates manipulate the amplitudes of those states so that correct answers interfere constructively while wrong answers interfere destructively.

The H Gate Is Its Own Inverse

Apply H twice and you get back where you started: HH=IH \cdot H = I (identity). This property makes H an involution. If you need to “undo” a Hadamard, just apply another one.

# Qiskit: put qubit in superposition, then measure
from qiskit import QuantumCircuit
qc = QuantumCircuit(1, 1)
qc.h(0)           # |0⟩ → (|0⟩ + |1⟩)/√2
qc.measure(0, 0)  # Will give 0 or 1 with equal probability

The CNOT Gate (Controlled-NOT)

The CNOT (or CX) gate operates on two qubits: a control and a target. If the control qubit is 1|1\rangle, it flips the target qubit. If the control is 0|0\rangle, nothing happens.


Truth table:
|00⟩ → |00⟩   (control=0, target unchanged)
|01⟩ → |01⟩   (control=0, target unchanged)
|10⟩ → |11⟩   (control=1, target flipped)
|11⟩ → |10⟩   (control=1, target flipped)

Circuit symbol:
control ─●─

target  ─⊕─

Creating Entanglement with H + CNOT

The sequence H followed by CNOT is perhaps the most well-known two-gate operation in quantum computing. This procedure creates a Bell state, which is a maximally entangled pair of qubits.

# Qiskit: create Bell state
from qiskit import QuantumCircuit
qc = QuantumCircuit(2, 2)
qc.h(0)           # Hadamard on qubit 0: superposition
qc.cx(0, 1)       # CNOT: entangle qubit 0 → qubit 1
qc.measure([0,1], [0,1])
# Result: always "00" or "11", never "01" or "10"

This two-gate circuit forms the basis for quantum teleportation, superdense coding, and quantum key distribution.

Phase Gates: S and T

Phase gates rotate the qubit around the Z axis of the Bloch sphere. While they do not change measurement probabilities in the standard basis, they affect interference, the mechanism that gives quantum algorithms their speedup.

The S Gate (Phase Gate) applies a 90-degree phase rotation to 1|1\rangle. Two S gates are equivalent to one Z gate: S2=ZS^2 = Z.


Matrix:
[1  0]
[0  i]

T Gate (pi/8 Gate)

This gate applies a 45-degree phase rotation. The T gate (and its inverse TT^\dagger) is essential for universal quantum computing. Having just H and T gates (plus CNOT) allows us to approximate any quantum operation to arbitrary precision via the Solovay-Kitaev theorem.


Matrix:
[1  0       ]
[0  e^(iπ/4)]
# Qiskit
from qiskit import QuantumCircuit
qc = QuantumCircuit(1)
qc.s(0)   # S gate
qc.t(0)   # T gate
qc.tdg(0) # T-dagger (inverse T gate)

The Toffoli Gate (CCNOT)

The Toffoli gate features two control qubits and one target. The target qubit flips only when both control qubits are in the 1|1\rangle state. This gate acts as a reversible AND gate: it computes the AND of the two control bits into the target. Because any classical circuit can be built from AND and NOT gates, and the Toffoli can simulate both reversibly (a NOT is achieved by setting both controls to 1|1\rangle), it is universal for classical reversible computation.


Circuit symbol:
control₁ ─●─

control₂ ─●─

target   ─⊕─

The Toffoli gate appears in quantum arithmetic circuits (quantum adders), error correction, and algorithms that need to implement classical Boolean logic within a quantum circuit.

# Qiskit
from qiskit import QuantumCircuit
qc = QuantumCircuit(3)
qc.ccx(0, 1, 2)  # Toffoli: flip qubit 2 if qubits 0 AND 1 are |1⟩

The SWAP Gate

SWAP exchanges two qubit states: abba|ab\rangle \rightarrow |ba\rangle. It is used in quantum circuits when qubit connectivity is limited and you need to route qubits past each other.


Circuit symbol:
─[×]─

─[×]─

A SWAP gate can be decomposed into three CNOT gates:

# Qiskit
from qiskit import QuantumCircuit
qc = QuantumCircuit(2)
qc.swap(0, 1)   # Direct SWAP
# Or equivalently:
qc2 = QuantumCircuit(2)
qc2.cx(0, 1); qc2.cx(1, 0); qc2.cx(0, 1)

Rotation Gates: Rx, Ry, Rz

For fine-grained control, you can rotate a qubit by any angle θ\theta around any axis of the Bloch sphere. These appear constantly in variational quantum algorithms like VQE and QAOA.

import numpy as np
from qiskit import QuantumCircuit

qc = QuantumCircuit(1)
qc.rx(np.pi / 4, 0)   # Rotate 45° around X axis
qc.ry(np.pi / 3, 0)   # Rotate 60° around Y axis
qc.rz(np.pi / 2, 0)   # Rotate 90° around Z axis (same as S gate up to global phase)

The rotation gate matrices are:

Rx(θ)=(cos(θ/2)isin(θ/2)isin(θ/2)cos(θ/2))R_x(\theta) = \begin{pmatrix} \cos(\theta/2) & -i\sin(\theta/2) \\ -i\sin(\theta/2) & \cos(\theta/2) \end{pmatrix}

Ry(θ)=(cos(θ/2)sin(θ/2)sin(θ/2)cos(θ/2))R_y(\theta) = \begin{pmatrix} \cos(\theta/2) & -\sin(\theta/2) \\ \sin(\theta/2) & \cos(\theta/2) \end{pmatrix}

Rz(θ)=(eiθ/200eiθ/2)R_z(\theta) = \begin{pmatrix} e^{-i\theta/2} & 0 \\ 0 & e^{i\theta/2} \end{pmatrix}

Any single-qubit unitary can be written as U=eiαRz(β)Ry(γ)Rz(δ)U = e^{i\alpha} R_z(\beta) R_y(\gamma) R_z(\delta) for some choice of angles. This is the ZYZ Euler decomposition and it proves that {Ry, Rz} is a universal set for single-qubit gates.

Gate Sets and Universality

You do not need to implement every possible gate in quantum hardware. A universal gate set is a small collection of gates that can approximate any quantum operation.

Gate SetNotes
{H, T, CNOT}Standard universal set for fault-tolerant QC
{Rx, Ry, Rz, CNOT}Common for variational algorithms
{Rz, SX, X, CNOT}IBM Quantum’s native basis gate set
{Rz, SX, CZ}Google Sycamore’s native gate set

Hardware providers have native gate sets. When you compile a circuit, your gates are decomposed into only the gates the hardware can actually execute. We explore this process in the transpilation section below.

How Gates Are Physically Implemented

Understanding what happens at the hardware level helps explain why some gates are “harder” than others. Two dominant hardware platforms implement gates in fundamentally different ways.

Superconducting Qubits (IBM, Google, Rigetti)

A superconducting qubit is a tiny circuit containing a Josephson junction, cooled to about 15 millikelvin. It behaves like an artificial atom with two energy levels separated by a frequency of roughly 5 GHz.

To apply a gate, the control electronics send a shaped microwave pulse at the qubit’s resonant frequency. The qubit absorbs the pulse and its state rotates on the Bloch sphere. The rotation angle depends on the pulse amplitude and duration. A pi-pulse (an X gate) drives a full Rabi oscillation from 0|0\rangle to 1|1\rangle. On IBM hardware, a typical single-qubit gate takes about 20 to 200 nanoseconds, depending on the gate type and calibration.

The pulse envelope shape matters. A simple square pulse causes the qubit to “leak” population into higher energy levels outside the computational space. Gaussian-shaped pulses reduce this leakage. The DRAG (Derivative Removal by Adiabatic Gate) pulse shape adds a correction term proportional to the time derivative of the Gaussian envelope, suppressing leakage to the third energy level. This is why pulse-level programming frameworks like Qiskit Pulse exist: precise control of the pulse shape directly affects gate fidelity.

Two-qubit gates on superconducting hardware rely on coupling between neighboring qubits, typically through a shared microwave resonator or a tunable coupler. An echoed cross-resonance (ECR) pulse implements a CNOT-equivalent operation in about 300 to 500 nanoseconds on IBM hardware. Google’s Sycamore chip uses tunable couplers to implement CZ gates in about 12 nanoseconds.

Trapped Ions (IonQ, Quantinuum)

In a trapped-ion processor, individual atomic ions (commonly ytterbium-171 or barium-133) are held in place by electromagnetic fields inside a vacuum chamber. Two internal electronic states of each ion serve as 0|0\rangle and 1|1\rangle.

Single-qubit gates are implemented by shining a focused laser beam onto an individual ion. The laser drives stimulated Raman transitions between the two qubit states. A pi-pulse on a trapped-ion system typically takes 5 to 20 microseconds, roughly 1000 times slower than superconducting gates.

Two-qubit gates use the shared motion (phonon modes) of the ion chain. The Molmer-Sorensen gate or the light-shift gate entangles two ions by coupling their internal states through the collective vibrational mode. This process typically takes 100 to 200 microseconds.

The Speed-Coherence Trade-off

Superconducting gates are about 1000 times faster than trapped-ion gates. However, trapped-ion qubits have coherence times of seconds to minutes, while superconducting qubits typically maintain coherence for 100 to 300 microseconds. The ratio of coherence time to gate time (the number of gates you can perform before decoherence dominates) is comparable between the two platforms, typically allowing hundreds to low thousands of sequential gates.

PropertySuperconductingTrapped Ion
Single-qubit gate time~20-200 ns~5-20 μs
Two-qubit gate time~100-500 ns~100-200 μs
T1 coherence time~100-300 μs~1-10 s
T2 coherence time~50-200 μs~0.5-5 s
ConnectivityNearest-neighborAll-to-all

Gate Fidelity and Error Rates

No physical gate operation is perfect. Gate fidelity quantifies how close the actual operation is to the ideal unitary.

For a target unitary UU and the actual operation VV, the average gate fidelity is:

F=Tr(UV)2+dd2+dF = \frac{|\text{Tr}(U^\dagger V)|^2 + d}{d^2 + d}

where dd is the Hilbert space dimension (d=2d = 2 for single-qubit, d=4d = 4 for two-qubit). In practice, fidelities are measured using randomized benchmarking, which provides a robust estimate that is insensitive to state preparation and measurement errors.

A fidelity of 99.9% means a 0.1% error rate per gate. This sounds small, but a circuit with 1000 gates accumulates roughly 1(0.999)100063%1 - (0.999)^{1000} \approx 63\% total error probability, making the output nearly useless without error correction.

Fidelity Comparison Across Platforms (2024 benchmarks)

PlatformSingle-qubit fidelityTwo-qubit fidelity
IBM Eagle/Heron (superconducting)~99.9%~99.0-99.5%
Google Sycamore (superconducting)~99.9%~99.5%
IonQ Forte (trapped ion)~99.97%~99.5%
Quantinuum H2 (trapped ion)~99.99%~99.7%

Why are two-qubit gates harder? A two-qubit gate requires coupling two distinct physical oscillators (or two ions through a shared phonon bus). This coupling introduces additional decoherence pathways: energy exchange with the environment, crosstalk with neighboring qubits, and frequency collisions. Each additional coupling mechanism adds a potential error source. This is a fundamental reason why quantum circuit optimization focuses heavily on minimizing two-qubit gate count.

The Controlled-Z (CZ) Gate

The CZ gate applies a phase of 1-1 when both qubits are in 1|1\rangle, and acts as the identity otherwise.

CZ Matrix:
[1  0  0   0]
[0  1  0   0]
[0  0  1   0]
[0  0  0  -1]

That is: diag(1, 1, 1, -1)

Relationship to CNOT

CZ and CNOT are closely related. You can convert between them using Hadamard gates on the target qubit:

CNOT=(IH)CZ(IH)\text{CNOT} = (I \otimes H) \cdot \text{CZ} \cdot (I \otimes H)

This means: apply H to the target, then CZ, then H to the target again, and you get a CNOT.

An important property of CZ that CNOT lacks is symmetry: there is no designated control or target qubit. CZ01=CZ10\text{CZ}_{01} = \text{CZ}_{10}. Either qubit can be called the “control.” This symmetry makes CZ a natural choice for hardware where the coupling between qubits is symmetric.

Google’s Sycamore processor, Rigetti’s chips, and several other platforms natively implement CZ rather than CNOT. Every CNOT in your circuit is compiled into CZ plus two Hadamard gates when targeting these backends.

# Verify the CZ-to-CNOT identity numerically
import numpy as np

# Define the gates
H = np.array([[1, 1], [1, -1]]) / np.sqrt(2)
I = np.eye(2)
CZ = np.diag([1, 1, 1, -1])

# CNOT from CZ: (I ⊗ H) · CZ · (I ⊗ H)
IH = np.kron(I, H)
CNOT_from_CZ = IH @ CZ @ IH

# Standard CNOT matrix
CNOT = np.array([
    [1, 0, 0, 0],
    [0, 1, 0, 0],
    [0, 0, 0, 1],
    [0, 0, 1, 0]
])

print("CNOT from CZ equals standard CNOT:", np.allclose(CNOT_from_CZ, CNOT))
# Output: True

The iSWAP Gate

The iSWAP gate is native on some superconducting platforms, particularly those that use a resonant coupling scheme between qubits. Its matrix is:

iSWAP Matrix:
[1  0  0  0]
[0  0  i  0]
[0  i  0  0]
[0  0  0  1]

The iSWAP gate swaps 01|01\rangle and 10|10\rangle while applying a phase factor of ii:

iSWAP01=i10\text{iSWAP}|01\rangle = i|10\rangle iSWAP10=i01\text{iSWAP}|10\rangle = i|01\rangle

The states 00|00\rangle and 11|11\rangle are left unchanged.

Physically, the iSWAP arises naturally when two superconducting qubits with the same frequency are coupled: the excitation swaps between them with a phase accumulation. This natural origin is why some platforms (including Google’s earlier processors) use iSWAP as a native gate.

To build a CNOT from iSWAP gates, you need two iSWAP gates plus single-qubit rotations:

import numpy as np

# iSWAP matrix
iSWAP = np.array([
    [1, 0, 0, 0],
    [0, 0, 1j, 0],
    [0, 1j, 0, 0],
    [0, 0, 0, 1]
])

# Verify action on |01⟩
state_01 = np.array([0, 1, 0, 0])
result = iSWAP @ state_01
print("|01⟩ after iSWAP:", result)
# Output: [0.+0.j  0.+0.j  0.+1.j  0.+0.j]
# This is i|10⟩, confirming the swap with phase i

Circuit Identity Proofs

Understanding circuit identities is essential for both hand-optimization and understanding how compilers simplify quantum circuits. Here are several important identities with numerical verification.

Identity 1: HXH = Z

Conjugating X by Hadamard gates produces Z. Intuitively, H rotates the Bloch sphere so that the X axis maps to the Z axis.

Identity 2: HZH = X

The reverse also holds: conjugating Z by Hadamard gives X.

Identity 3: XZ = iY (up to global phase)

The product of X and Z gates equals iYiY.

Identity 4: CNOT = (H ⊗ H) CZ (H ⊗ H)

Wrapping CZ with Hadamards on both qubits also produces a CNOT (with swapped control and target compared to the single-Hadamard identity above).

import numpy as np

# Define basic gates
X = np.array([[0, 1], [1, 0]])
Y = np.array([[0, -1j], [1j, 0]])
Z = np.array([[1, 0], [0, -1]])
H = np.array([[1, 1], [1, -1]]) / np.sqrt(2)
I = np.eye(2)

# Identity 1: H X H = Z
result1 = H @ X @ H
print("HXH = Z:", np.allclose(result1, Z))  # True

# Identity 2: H Z H = X
result2 = H @ Z @ H
print("HZH = X:", np.allclose(result2, X))  # True

# Identity 3: X Z = iY
result3 = X @ Z
print("XZ = iY:", np.allclose(result3, 1j * Y))  # True

# Identity 4: (H⊗H) CZ (H⊗H) gives a CNOT (with swapped control/target)
CZ = np.diag([1, 1, 1, -1])
HH = np.kron(H, H)
result4 = HH @ CZ @ HH

# This equals CNOT with qubit 1 as control, qubit 0 as target
CNOT_10 = np.array([
    [1, 0, 0, 0],
    [0, 0, 0, 1],
    [0, 0, 1, 0],
    [0, 1, 0, 0]
])
print("(H⊗H) CZ (H⊗H) = CNOT_10:", np.allclose(result4, CNOT_10))  # True

These identities are how quantum circuit compilers simplify gate sequences. For example, if a compiler sees H-X-H in sequence, it can replace it with a single Z gate, reducing the circuit depth by two thirds.

Multi-Qubit Controlled Gates

The CNOT gate is a special case of a general controlled-U gate, where U is any single-qubit unitary. The controlled-U gate applies U to the target qubit only when the control qubit is 1|1\rangle.

The matrix form is:

C(U)=00I+11UC(U) = |0\rangle\langle 0| \otimes I + |1\rangle\langle 1| \otimes U

This means: if the control is 0|0\rangle, do nothing; if the control is 1|1\rangle, apply UU.

Important special cases include:

  • Controlled-Z (CZ): U=ZU = Z, the gate we discussed above
  • Controlled-S (CS): U=SU = S, applies a 90-degree phase when both qubits are 1|1\rangle
  • Controlled-T (CT): U=TU = T, applies a 45-degree phase when both qubits are 1|1\rangle

Decomposing Controlled-S into Native Gates

Any controlled-phase gate can be decomposed into CNOT gates plus single-qubit rotations. For the controlled-S gate:

from qiskit import QuantumCircuit
import numpy as np

# Build controlled-S from CNOT and Rz gates
qc = QuantumCircuit(2)
qc.rz(np.pi / 4, 1)    # Rz(pi/4) on target
qc.cx(0, 1)             # CNOT
qc.rz(-np.pi / 4, 1)   # Rz(-pi/4) on target
qc.cx(0, 1)             # CNOT
qc.rz(np.pi / 4, 0)    # Rz(pi/4) on control (for global phase correction)

print(qc.draw())

The Significance of Controlled-T

The controlled-T gate holds a special position in fault-tolerant quantum computing. The gate set {H, S, CNOT} generates the Clifford group (discussed below), which is efficiently simulable by classical computers. Adding the T gate breaks out of the Clifford group and, together with the Clifford gates, generates a set that is dense in all unitaries. The controlled-T gate is the key ingredient that enables the full Clifford+T hierarchy, which is the standard framework for fault-tolerant quantum computation.

Clifford Gates and the Clifford Group

The Clifford group consists of all unitary operations that map Pauli operators to Pauli operators under conjugation. More precisely, a unitary CC is Clifford if, for every Pauli operator PP, the conjugation CPCC P C^\dagger yields another Pauli operator (up to a phase of ±1\pm 1 or ±i\pm i).

The Clifford group is generated by three gates: {H, S, CNOT}. All Pauli gates (X, Y, Z) are also Clifford gates, since they are products of H and S.

Here are the key conjugation relations that define how Clifford generators transform Pauli operators:

import numpy as np

X = np.array([[0, 1], [1, 0]])
Y = np.array([[0, -1j], [1j, 0]])
Z = np.array([[1, 0], [0, -1]])
H = np.array([[1, 1], [1, -1]]) / np.sqrt(2)
S = np.array([[1, 0], [0, 1j]])

# H conjugation: H maps X ↔ Z
print("H X H† = Z:", np.allclose(H @ X @ H.conj().T, Z))   # True
print("H Z H† = X:", np.allclose(H @ Z @ H.conj().T, X))   # True

# S conjugation: S maps X → Y
print("S X S† = Y:", np.allclose(S @ X @ S.conj().T, Y))    # True
print("S Z S† = Z:", np.allclose(S @ Z @ S.conj().T, Z))    # True

For CNOT, the Pauli propagation rules involve both qubits:

CNOT(XI)CNOT=XX\text{CNOT} \cdot (X \otimes I) \cdot \text{CNOT}^\dagger = X \otimes X CNOT(IX)CNOT=IX\text{CNOT} \cdot (I \otimes X) \cdot \text{CNOT}^\dagger = I \otimes X CNOT(ZI)CNOT=ZI\text{CNOT} \cdot (Z \otimes I) \cdot \text{CNOT}^\dagger = Z \otimes I CNOT(IZ)CNOT=ZZ\text{CNOT} \cdot (I \otimes Z) \cdot \text{CNOT}^\dagger = Z \otimes Z

The first rule is particularly important: an X error on the control qubit “spreads” to the target qubit through a CNOT. Understanding how errors propagate through Clifford gates is central to quantum error correction.

Why Clifford Gates Are Special: The Gottesman-Knill Theorem

The Gottesman-Knill theorem states that any quantum circuit composed entirely of Clifford gates, starting from computational basis states, with measurements only in the computational basis, can be efficiently simulated on a classical computer. “Efficiently” means in polynomial time and space, regardless of the number of qubits.

This is a profound result. It means that entanglement alone is not sufficient for quantum speedup. A Bell state circuit (H + CNOT) is a Clifford circuit and is classically simulable. Quantum advantage requires non-Clifford gates, and the T gate is the simplest non-Clifford gate.

You can verify that T is not Clifford by checking its conjugation of X:

TXT=12(X+Y)T X T^\dagger = \frac{1}{\sqrt{2}}(X + Y)

This is not a Pauli operator. The T gate maps Paulis outside the Pauli group, so T is not a member of the Clifford group.

Ancilla Qubits and Gate Decomposition

An ancilla qubit is a helper qubit that starts in a known state, participates in a computation, and is either restored to its original state or measured and discarded. Ancilla qubits enable the construction of complex multi-qubit gates from simpler operations.

The Toffoli Decomposition

The Toffoli gate is a three-qubit gate, but most hardware only provides one- and two-qubit native gates. Decomposing the Toffoli into these primitives is therefore essential. A well-known decomposition uses 6 CNOT gates along with H, T, and TT^\dagger gates, requiring no ancilla qubits:

from qiskit import QuantumCircuit

# Toffoli decomposition into {H, T, T†, CNOT}
# Control qubits: 0, 1. Target qubit: 2
qc = QuantumCircuit(3)

qc.h(2)
qc.cx(1, 2)
qc.tdg(2)
qc.cx(0, 2)
qc.t(2)
qc.cx(1, 2)
qc.tdg(2)
qc.cx(0, 2)
qc.t(1)
qc.t(2)
qc.h(2)
qc.cx(0, 1)
qc.t(0)
qc.tdg(1)
qc.cx(0, 1)

print(qc.draw())

This decomposition matters enormously for fault-tolerant quantum computing. In surface code error correction, Clifford gates (H, S, CNOT) can be implemented “transversally” at relatively low cost. T gates, however, require a resource called magic state distillation, which is expensive. Each T gate costs roughly 10 to 100 times more than a Clifford gate in terms of physical qubits and time. The Toffoli decomposition above uses 7 T/TT^\dagger gates, so a single Toffoli is quite expensive in a fault-tolerant setting. Reducing the T-count of circuits is an active area of research.

Gate Noise in Practice

Real quantum gates are imperfect. To understand the impact of noise on a computation, you can simulate gate errors using Qiskit Aer’s noise model.

from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel, depolarizing_error

# Build a Bell state circuit
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure([0, 1], [0, 1])

# Create a noise model with realistic error rates
noise_model = NoiseModel()

# Single-qubit depolarizing error: 0.1% error rate
error_1q = depolarizing_error(0.001, 1)
noise_model.add_all_qubit_quantum_error(error_1q, ['h', 'x', 'y', 'z', 's', 't'])

# Two-qubit depolarizing error: 1% error rate
error_2q = depolarizing_error(0.01, 2)
noise_model.add_all_qubit_quantum_error(error_2q, ['cx'])

# Run with noise
simulator = AerSimulator(noise_model=noise_model)
result = simulator.run(qc, shots=10000).result()
counts = result.get_counts()

print("Noisy Bell state results:", counts)
# Ideal result: {'00': 5000, '11': 5000}
# Noisy result (typical): {'00': ~4925, '11': ~4925, '01': ~75, '10': ~75}

In the ideal case, a Bell state produces only “00” and “11” outcomes. With noise, you see “leakage” into “01” and “10”. The contamination rate tells you the effective circuit error: if about 1.5% of shots produce wrong outcomes, your total circuit error is approximately 1.5%. This matches what you would expect from the error budget: one H gate with 0.1% error and one CNOT with 1% error gives roughly 1.1% total error, with the remainder from depolarization spreading errors across multiple outcomes.

Understanding this error budget is critical for deciding whether a quantum circuit is feasible on current hardware. If your circuit has 100 CNOT gates at 1% error each, the total error is approximately 1(0.99)10063%1 - (0.99)^{100} \approx 63\%, making the output unreliable.

Parameterized Gates in Variational Algorithms

The rotation gates Rx, Ry, and Rz are the workhorses of variational quantum algorithms, where classical optimization adjusts gate parameters to minimize a cost function.

VQE-style Ansatz

In the Variational Quantum Eigensolver (VQE), a typical ansatz uses Ry gates for single-qubit rotations and CNOT for entanglement. Here is a 2-qubit example with 4 parameters:

import numpy as np
from qiskit import QuantumCircuit

def vqe_ansatz(params):
    """Create a 2-qubit VQE ansatz with 4 parameters."""
    qc = QuantumCircuit(2)
    # Layer 1: single-qubit rotations
    qc.ry(params[0], 0)
    qc.ry(params[1], 1)
    # Entangling layer
    qc.cx(0, 1)
    # Layer 2: single-qubit rotations
    qc.ry(params[2], 0)
    qc.ry(params[3], 1)
    return qc

# Example circuit with specific angles
params = [0.3, 0.7, 1.2, 0.5]
qc = vqe_ansatz(params)
print(qc.draw())

QAOA Gate Structure

In the Quantum Approximate Optimization Algorithm (QAOA), two types of parameterized layers alternate. The cost layer applies Rzz gates (implemented as CNOT-Rz-CNOT), and the mixer layer applies Rx gates:

import numpy as np
from qiskit import QuantumCircuit

def qaoa_layer(gamma, beta, n_qubits=2):
    """One layer of QAOA for a simple 2-qubit problem."""
    qc = QuantumCircuit(n_qubits)

    # Cost layer: Rzz(gamma) on each pair
    # Rzz(gamma) = CNOT · (I ⊗ Rz(gamma)) · CNOT
    qc.cx(0, 1)
    qc.rz(gamma, 1)
    qc.cx(0, 1)

    # Mixer layer: Rx(beta) on each qubit
    qc.rx(beta, 0)
    qc.rx(beta, 1)

    return qc

qc = qaoa_layer(gamma=0.5, beta=0.3)
print(qc.draw())

The Parameter-Shift Rule

To optimize variational circuits, you need gradients. The parameter-shift rule provides exact gradients (not numerical approximations) for rotation gates. For any rotation gate R(θ)R(\theta) appearing in a circuit, the partial derivative of the expectation value E\langle E \rangle with respect to θ\theta is:

Eθk=E(θk+π/2)E(θkπ/2)2\frac{\partial \langle E \rangle}{\partial \theta_k} = \frac{\langle E(\theta_k + \pi/2) \rangle - \langle E(\theta_k - \pi/2) \rangle}{2}

This requires two circuit evaluations per parameter. For a circuit with pp parameters, computing the full gradient requires 2p2p circuit evaluations.

import numpy as np

def parameter_shift_gradient(cost_fn, params, param_index):
    """
    Compute the gradient of cost_fn with respect to params[param_index]
    using the parameter-shift rule.
    """
    shift = np.pi / 2

    # Evaluate at theta + pi/2
    params_plus = params.copy()
    params_plus[param_index] += shift
    cost_plus = cost_fn(params_plus)

    # Evaluate at theta - pi/2
    params_minus = params.copy()
    params_minus[param_index] -= shift
    cost_minus = cost_fn(params_minus)

    return (cost_plus - cost_minus) / 2.0

# Example: compute gradient for all 4 parameters
params = np.array([0.3, 0.7, 1.2, 0.5])
# In practice, cost_fn would run the VQE circuit and measure
# the expectation value of a Hamiltonian

The parameter-shift rule works because rotation gates have exactly two eigenvalues, ±1/2\pm 1/2, in their generator. This is a mathematical property specific to quantum gates and has no classical analogue.

Native Gate Transpilation

When you write a circuit using abstract gates like H, T, or S, the quantum compiler must translate these into the hardware’s native gate set. For IBM Quantum processors, the native gates are {Rz, SX, X, CNOT}, where SX is the square root of X (a 90-degree rotation around the X axis).

Here is how common gates decompose into IBM’s native set:

  • H gate: Rz(π/2)SXRz(π/2)\text{Rz}(\pi/2) \cdot \text{SX} \cdot \text{Rz}(\pi/2)
  • T gate: Rz(π/4)\text{Rz}(\pi/4)
  • S gate: Rz(π/2)\text{Rz}(\pi/2)
  • Y gate: XRz(π)\text{X} \cdot \text{Rz}(\pi) (or equivalently, SX composed with Rz gates)
from qiskit import QuantumCircuit
from qiskit.compiler import transpile

# Original circuit with abstract gates
qc = QuantumCircuit(2)
qc.h(0)
qc.t(0)
qc.s(1)
qc.cx(0, 1)
qc.h(1)

print("Original circuit:")
print(qc.draw())
print(f"Original gate count: {qc.size()}")

# Transpile to IBM's native gate set
transpiled = transpile(qc, basis_gates=['rz', 'sx', 'x', 'cx'], optimization_level=2)

print("\nTranspiled circuit:")
print(transpiled.draw())
print(f"Transpiled gate count: {transpiled.size()}")

# Count each gate type
gate_counts = transpiled.count_ops()
print(f"Gate breakdown: {dict(gate_counts)}")

When you run this, you will see that the 5 abstract gates expand into a larger number of native gates. The H gates each become three native gates (Rz-SX-Rz), while T and S gates each become a single Rz. The transpiler also applies optimizations: adjacent Rz gates are merged, and redundant gates are cancelled.

This is why native gate count, not abstract gate count, determines the true cost of a circuit. A circuit that looks simple in terms of H and T gates may expand significantly after transpilation.

Common Mistakes to Avoid

1. Confusing CX and CZ

CX (CNOT) flips the target qubit when the control is 1|1\rangle. CZ applies a phase of 1-1 when both qubits are 1|1\rangle. They are related by Hadamard gates on the target: CX=(IH)CZ(IH)\text{CX} = (I \otimes H) \cdot \text{CZ} \cdot (I \otimes H). Using one where you need the other produces wrong results without any error message, because both are valid two-qubit gates. If your entangling operation is producing unexpected phases instead of bit flips (or vice versa), check whether you used CX when you meant CZ.

2. Using Degrees Instead of Radians

All Qiskit rotation gates use radians, not degrees. Writing qc.rx(90, 0) does not rotate by 90 degrees. It rotates by 90 radians, which is approximately 14.3 full rotations. The result is effectively a random rotation. Always use np.pi/2 for a 90-degree rotation, np.pi for 180 degrees, and so on.

import numpy as np
from qiskit import QuantumCircuit

# WRONG: this rotates by 90 radians, not 90 degrees
qc_wrong = QuantumCircuit(1)
qc_wrong.rx(90, 0)

# CORRECT: 90-degree rotation around X axis
qc_right = QuantumCircuit(1)
qc_right.rx(np.pi / 2, 0)

3. Treating Global Phase as Significant

The states ψ|\psi\rangle and eiϕψe^{i\phi}|\psi\rangle are physically identical quantum states. They produce the same measurement probabilities and the same expectation values for all observables. Code that compares statevectors using == or np.array_equal will often fail because different implementations may produce the same state with different global phases. Instead, compare states using fidelity: ψϕ2=1|\langle\psi|\phi\rangle|^2 = 1 if the states are identical up to global phase.

import numpy as np

# These represent the same quantum state
state1 = np.array([1, 0])       # |0⟩
state2 = np.array([-1, 0])      # -|0⟩ (global phase of pi)
state3 = np.array([1j, 0])      # i|0⟩ (global phase of pi/2)

# Direct comparison fails
print(np.array_equal(state1, state2))  # False (but they are the same state!)

# Fidelity comparison works
fidelity = abs(np.dot(state1.conj(), state2))**2
print(f"Fidelity: {fidelity}")  # 1.0 (correctly identifies them as identical)

4. Forgetting That Measurement Is Not a Gate

Measurement collapses the quantum state and cannot be undone. You cannot apply a gate after measurement to “undo” the collapse. In Qiskit, operations placed after a measurement on the same qubit may produce unpredictable results or errors, depending on the backend.

Always place all quantum gates before the measurement barrier. If you need to apply conditional operations based on measurement results, use classical feedforward (mid-circuit measurement), which is a distinct operation that explicitly conditions subsequent gates on classical bit values.

from qiskit import QuantumCircuit

# WRONG: gate after measurement has no defined quantum behavior
qc_wrong = QuantumCircuit(1, 1)
qc_wrong.h(0)
qc_wrong.measure(0, 0)
qc_wrong.x(0)  # This does NOT undo the measurement

# CORRECT: all gates before measurement
qc_right = QuantumCircuit(1, 1)
qc_right.h(0)
qc_right.x(0)
qc_right.measure(0, 0)

5. Misidentifying Clifford vs. Non-Clifford Gates

The T gate is not a Clifford gate. This distinction matters for two reasons. First, the Gottesman-Knill theorem says circuits with only Clifford gates are efficiently simulable classically. Including even a single T gate (in a non-trivial way) can push the circuit beyond classical simulability. Second, in fault-tolerant computing, T gates are far more expensive than Clifford gates due to the need for magic state distillation.

If you are running a classical simulation for testing purposes and only need Clifford operations, verify that your circuit contains no T gates. Adding a T gate accidentally can cause your simulation to slow down dramatically when using stabilizer-based simulators.

from qiskit import QuantumCircuit

qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.s(1)
qc.cx(1, 2)

# Check for non-Clifford gates
clifford_gates = {'h', 'x', 'y', 'z', 's', 'sdg', 'cx', 'cz', 'swap', 'id', 'barrier'}
non_clifford = [
    (gate.operation.name, gate.qubits)
    for gate in qc.data
    if gate.operation.name not in clifford_gates
]

if non_clifford:
    print(f"Warning: circuit contains non-Clifford gates: {non_clifford}")
else:
    print("Circuit is purely Clifford, classically simulable via stabilizer formalism.")

Where to Go Next

Two combinations are worth committing to memory: H + CNOT creates entanglement, and H + T + CNOT gives universal quantum computing. Every quantum algorithm, from Shor’s factoring to variational eigensolvers, is built from these fundamental pieces.

From here, you can explore quantum error correction, which shows how to protect computations from the gate noise we discussed above. You can also study variational algorithms to see parameterized gates in action on real optimization problems.

Was this tutorial helpful?