Concepts Beginner Free 40/53 in series 20 min

Quantum Measurement Explained

How quantum measurement works: the Born rule, wavefunction collapse, measurement bases, and why measurement is irreversible in quantum computing.

What you'll learn

  • quantum measurement
  • Born rule
  • collapse
  • probability
  • quantum states

Prerequisites

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

Measurement is one of the most important and counterintuitive aspects of quantum computing. Every quantum computation ends with a measurement, and the rules governing measurement are fundamentally different from anything in classical computing. This tutorial explains how measurement works, what the Born rule says, and why measurement irreversibility shapes the design of quantum algorithms. It also covers advanced topics: generalized measurements (POVMs), weak measurement, mid-circuit measurement, the quantum Zeno effect, state tomography, Bell inequality violations, and common mistakes that trip up beginners.

The Core Problem: You Cannot Observe a Superposition Directly

A qubit in a superposition holds complex amplitudes for both |0> and |1> simultaneously. But when you measure it, you only ever get one definite outcome: 0 or 1. The superposition is not preserved during measurement; the act of measuring forces the qubit into a definite classical state.

This is not a limitation of measurement technology. It is a fundamental feature of quantum mechanics. The output of any quantum computation is always classical bits, extracted by measurement from a quantum state.

The Born Rule

The Born rule tells you the probability of each outcome. For a single qubit in state:

|psi> = alpha |0> + beta |1>

the probability of measuring 0 is |alpha|^2, and the probability of measuring 1 is |beta|^2:

import numpy as np

alpha = 1 / np.sqrt(3)
beta  = np.sqrt(2 / 3)

prob_0 = abs(alpha)**2
prob_1 = abs(beta)**2

print(f"P(0) = {prob_0:.3f}")  # 0.333
print(f"P(1) = {prob_1:.3f}")  # 0.667
print(f"Sum  = {prob_0 + prob_1:.3f}")  # 1.000

The probabilities always sum to 1 because the state is normalized. Quantum mechanics requires states to be normalized precisely so that Born rule probabilities are consistent.

Wavefunction Collapse

After measurement, the qubit collapses to the state corresponding to the outcome. If you measured 0, the qubit is now in state |0>. If you measured 1, it is in state |1>. The original superposition is destroyed.

from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator

qc = QuantumCircuit(1, 1)
qc.h(0)         # Create |+> = (|0> + |1>) / sqrt(2)
qc.measure(0, 0)

sim = AerSimulator()
result = sim.run(qc, shots=1000).result()
print(result.get_counts())  # {'0': ~500, '1': ~500}

If you measure the same qubit again immediately after the first measurement, you always get the same result. The collapse is real: the qubit is in a definite state after measurement.

Why Measurement Is Irreversible

Every quantum gate is a reversible operation; you can always apply the inverse gate to undo it. Measurement is different. Once a qubit collapses to |0> or |1>, there is no way to recover the original superposition from the measurement result alone. The information in the amplitudes is lost.

This has a profound consequence for algorithm design: you cannot measure in the middle of a computation and continue using the quantum state as if nothing happened. Mid-circuit measurements can be useful for specific purposes (like quantum error correction or teleportation), but they fundamentally alter the computation.

This irreversibility is also why quantum computing is not the same as running all possible classical computations in parallel. The superposition exists, but you can only extract one classical outcome from it.

Measurement Bases

By default, qubits are measured in the computational basis (the Z basis): the outcomes are labeled 0 and 1, corresponding to basis states |0> and |1>. But you can measure in any basis by applying a gate that rotates the state before measuring.

Z Basis (Computational Basis)

No rotation needed. The measure() gate in Qiskit always performs a Z-basis measurement.

X Basis

Measuring in the X basis (the {|+>, |->} basis) requires applying a Hadamard gate before the measurement:

qc = QuantumCircuit(1, 1)

# Prepare the |+> state
qc.h(0)

# Measure in X basis: apply H then measure in computational basis
qc.h(0)
qc.measure(0, 0)

result = sim.run(qc, shots=1000).result()
print(result.get_counts())  # {'0': 1000}  -- |+> is an eigenstate of X

The |+> state is an eigenstate of X with eigenvalue +1. Measuring it in the X basis always gives 0 (the +1 outcome). Measuring |+> in the Z basis (computational basis) gives random results.

Y Basis

Measuring in the Y basis requires an S-dagger gate followed by a Hadamard before the measurement:

qc = QuantumCircuit(1, 1)
qc.h(0)         # Prepare |+>
qc.sdg(0)       # S-dagger rotates to Y basis
qc.h(0)
qc.measure(0, 0)

Basis Rotation Summary

Target basisGates before measure()Why it works
Z(none)Default measurement basis
XHMaps |+> to |0>, |-> to |1>
YSdg, HMaps |i+> to |0>, |i-> to |1>

This table is worth memorizing. Every time you want to measure in a non-Z basis, you apply the basis rotation gates and then call measure().

Measuring a Bell State in All Three Bases

Measuring the same entangled state in different bases reveals different correlations. The Bell state (|00> + |11>) / sqrt(2) has perfect correlations in Z, perfect correlations in X, and perfect anti-correlations in Y.

from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator

sim = AerSimulator()

def make_bell():
    """Create a Bell state (|00> + |11>) / sqrt(2)."""
    qc = QuantumCircuit(2, 2)
    qc.h(0)
    qc.cx(0, 1)
    return qc

# Z-basis measurement: no rotation needed
qc_z = make_bell()
qc_z.measure([0, 1], [0, 1])
counts_z = sim.run(qc_z, shots=4000).result().get_counts()
print("Z basis:", counts_z)  # {'00': ~2000, '11': ~2000}

# X-basis measurement: apply H to both qubits
qc_x = make_bell()
qc_x.h(0)
qc_x.h(1)
qc_x.measure([0, 1], [0, 1])
counts_x = sim.run(qc_x, shots=4000).result().get_counts()
print("X basis:", counts_x)  # {'00': ~2000, '11': ~2000}

# Y-basis measurement: apply Sdg then H to both qubits
qc_y = make_bell()
qc_y.sdg(0)
qc_y.sdg(1)
qc_y.h(0)
qc_y.h(1)
qc_y.measure([0, 1], [0, 1])
counts_y = sim.run(qc_y, shots=4000).result().get_counts()
print("Y basis:", counts_y)  # {'01': ~2000, '10': ~2000}

In the Z basis, both qubits always agree (00 or 11). In the X basis, they also always agree. In the Y basis, they always disagree (01 or 10). This pattern of correlations is a signature of entanglement and plays a central role in Bell inequality tests.

Multi-Qubit Measurement

For multi-qubit systems, measuring all qubits simultaneously collapses the entire state to one of the basis vectors. The probability of each n-bit outcome is the squared magnitude of the corresponding amplitude:

# Prepare a Bell state: (|00> + |11>) / sqrt(2)
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
qc.measure([0, 1], [0, 1])

result = sim.run(qc, shots=1000).result()
print(result.get_counts())
# {'00': ~500, '11': ~500}
# Never '01' or '10' -- Bell state correlations

You can also measure only a subset of qubits (partial measurement). The unmeasured qubits remain in a quantum state that depends on the measurement outcome:

# Measure only qubit 0
qc = QuantumCircuit(2, 1)
qc.h(0)
qc.cx(0, 1)
qc.measure(0, 0)  # Measure qubit 0 only

# After measuring qubit 0:
# - If outcome is 0, qubit 1 is in state |0>
# - If outcome is 1, qubit 1 is in state |1>

The Measurement Operators (Projective Measurement)

Formally, measurements correspond to sets of projectors. In the computational basis, the projectors are:

P0 = |0><0| = [[1, 0], [0, 0]]
P1 = |1><1| = [[0, 0], [0, 1]]
import numpy as np

ket_0 = np.array([1, 0], dtype=complex)
ket_1 = np.array([0, 1], dtype=complex)

P0 = np.outer(ket_0, ket_0)
P1 = np.outer(ket_1, ket_1)

# Completeness: P0 + P1 = I
print(P0 + P1)  # [[1, 0], [0, 1]]

# Probability of outcome 0: <psi|P0|psi>
psi = np.array([1/np.sqrt(3), np.sqrt(2/3)], dtype=complex)
prob_0 = np.real(psi.conj() @ P0 @ psi)
print(f"P(0) = {prob_0:.3f}")  # 0.333

# Post-measurement state after outcome 0: P0|psi> / sqrt(P(0))
post_state = P0 @ psi / np.sqrt(prob_0)
print(post_state)  # [1.+0.j, 0.+0.j] = |0>

These projectors satisfy two key properties: they are idempotent (P0^2 = P0), and they are complete (P0 + P1 = I). Any set of orthogonal projectors that sums to the identity defines a valid projective measurement.

Generalized Measurement (POVM)

Quantum mechanics allows measurements more general than projective ones. A POVM (Positive Operator-Valued Measure) is a set of positive semi-definite operators {E_k} satisfying:

sum_k E_k = I

The probability of outcome k is:

P(k) = <psi| E_k |psi>  =  Tr(E_k |psi><psi|)

Projective measurements are a special case where each E_k is a projector (E_k^2 = E_k). But POVMs can have more outcomes than the dimension of the Hilbert space. This extra freedom is essential for tasks like optimal state discrimination.

Why POVMs Matter: State Discrimination

Suppose you receive a qubit prepared in one of three non-orthogonal states, each with equal probability:

|psi_0> = |0>
|psi_1> = -1/2 |0> + sqrt(3)/2 |1>
|psi_2> = -1/2 |0> - sqrt(3)/2 |1>

These are the “trine states,” separated by 120 degrees on the Bloch sphere equator. No projective measurement can optimally distinguish all three, because projective measurements on a qubit have at most 2 outcomes. But a 3-outcome POVM can:

import numpy as np

# Define the three trine states
psi_0 = np.array([1, 0], dtype=complex)
psi_1 = np.array([-0.5, np.sqrt(3)/2], dtype=complex)
psi_2 = np.array([-0.5, -np.sqrt(3)/2], dtype=complex)

# The optimal POVM elements: E_k = (2/3) |psi_k_perp><psi_k_perp|
# where |psi_k_perp> is the state orthogonal to |psi_k>
def perp(state):
    """Return the state orthogonal to the given qubit state."""
    return np.array([-np.conj(state[1]), np.conj(state[0])], dtype=complex)

psi_0_perp = perp(psi_0)
psi_1_perp = perp(psi_1)
psi_2_perp = perp(psi_2)

E0 = (2/3) * np.outer(psi_0_perp, psi_0_perp.conj())
E1 = (2/3) * np.outer(psi_1_perp, psi_1_perp.conj())
E2 = (2/3) * np.outer(psi_2_perp, psi_2_perp.conj())

# Verify completeness: E0 + E1 + E2 = I
print("Sum of POVM elements:")
print(np.round(E0 + E1 + E2, 10))  # [[1, 0], [0, 1]]

# Compute discrimination probabilities
# P(correct | state k) = <psi_k| E_k |psi_k>
# The trick: E_k is built from the state ORTHOGONAL to psi_k,
# so P(outcome k | state k) = 0 for the wrong state.
# When we get outcome k, we KNOW the state was NOT psi_k.
# With 3 equally likely states, this gives conclusive identification
# with probability 2/3 per trial (the remaining 1/3 is inconclusive).

for k, (psi, E) in enumerate([(psi_0, E0), (psi_1, E1), (psi_2, E2)]):
    probs = [np.real(p.conj() @ E @ p) for p in [psi_0, psi_1, psi_2]]
    print(f"P(outcome {k} | state 0,1,2) = "
          f"{probs[0]:.3f}, {probs[1]:.3f}, {probs[2]:.3f}")
# Outcome 0 never fires for state 0, etc.
# This means outcome 0 rules out state 0, leaving states 1 or 2.

The POVM achieves the theoretical optimum for unambiguous discrimination of equally likely states. No projective measurement can match this performance.

Weak Measurement

Standard projective measurement fully collapses the quantum state. Weak measurement extracts partial information while causing minimal disturbance. The idea is to couple the system to an ancilla qubit with a small interaction strength and then measure the ancilla instead of the system.

How Weak Measurement Works

Consider measuring the Z observable on a qubit in state rho. A strong (projective) measurement produces the post-measurement state:

rho_strong = P0 rho P0 + P1 rho P1

This completely destroys coherence between |0> and |1>. A weak measurement with strength epsilon (0 <= epsilon <= 1) produces:

rho_weak = (1 - epsilon) * rho  +  epsilon * (P0 rho P0 + P1 rho P1)

When epsilon = 0, the state is undisturbed and you learn nothing. When epsilon = 1, it is a full projective measurement. For small epsilon, the state barely changes, but you gain a small amount of information from the ancilla measurement.

Weak Measurement Circuit

import numpy as np
from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator

sim = AerSimulator()

def weak_measurement_circuit(theta_system, epsilon):
    """
    Prepare a system qubit and perform a weak measurement.
    theta_system: Ry rotation angle to prepare the system state
    epsilon: measurement strength (0 = no disturbance, pi/2 = strong)

    The weak measurement is implemented by a controlled-Ry on an ancilla.
    A small epsilon means a small rotation, extracting little information.
    """
    qc = QuantumCircuit(2, 1)  # qubit 0 = system, qubit 1 = ancilla

    # Prepare the system qubit
    qc.ry(theta_system, 0)

    # Weak coupling: controlled rotation of the ancilla
    # A small angle means weak measurement
    qc.cry(2 * epsilon, 0, 1)

    # Measure the ancilla only (system is barely disturbed)
    qc.measure(1, 0)
    return qc

# Compare weak vs. strong measurement
# Prepare |+> state (theta = pi/2)
for eps_label, eps_val in [("Weak (eps=0.1)", 0.1),
                            ("Medium (eps=0.5)", 0.5),
                            ("Strong (eps=pi/2)", np.pi/2)]:
    qc = weak_measurement_circuit(np.pi / 2, eps_val)
    counts = sim.run(qc, shots=4000).result().get_counts()
    p0 = counts.get('0', 0) / 4000
    p1 = counts.get('1', 0) / 4000
    print(f"{eps_label}: P(ancilla=0)={p0:.3f}, P(ancilla=1)={p1:.3f}")

# Weak measurement: ancilla is nearly always 0 (little information gained)
# Strong measurement: ancilla outcome correlates fully with system state

Weak measurement is used in practice for quantum feedback control, continuous monitoring of quantum systems, and experiments that probe quantum foundations (like direct measurement of the wavefunction).

The Measurement Problem and Interpretations

The Born rule and collapse postulate tell you how to calculate measurement outcomes. But they leave open a deeper question: what physically happens during a measurement? Different interpretations of quantum mechanics give different answers.

Copenhagen Interpretation

The quantum state is a mathematical tool for computing probabilities, not a description of physical reality. “Collapse” is Bayesian updating: when you learn the outcome, you update your description of the system. There is no physical process of collapse; the question “what is the state between preparation and measurement?” is considered meaningless.

Many-Worlds Interpretation

There is no collapse. The measurement entangles the observer with the system, and the combined wavefunction evolves unitarily. All outcomes occur, each in a different branch of the universal wavefunction. When you measure a qubit in state (|0> + |1>) / sqrt(2), the universe branches: in one branch you see 0, in another you see 1. Both branches are equally real.

QBism (Quantum Bayesianism)

Quantum states represent an agent’s personal beliefs about future measurement outcomes, not properties of the physical world. Collapse is a belief update. Two agents can assign different quantum states to the same system without contradiction, just as two people can hold different Bayesian priors.

Relational Quantum Mechanics

Quantum states are defined relative to an observer. The state of a system is not absolute; it depends on who is asking. A measurement establishes a relation between the observer and the system. Different observers may give different (but consistent) accounts of the same events.

Which Interpretation Matters for Quantum Computing?

All these interpretations are experimentally indistinguishable. They agree on every prediction the Born rule makes. For quantum computing practice, the Copenhagen viewpoint is the most pragmatic: prepare a state, apply gates, measure, use the probabilities. The other interpretations provide philosophical context but do not change how you write or run quantum circuits.

Mid-Circuit Measurement in Qiskit

Modern quantum hardware and simulators support measurement in the middle of a circuit, with classical feed-forward: subsequent gates can depend on measurement results. This is essential for quantum error correction, teleportation, and repeat-until-success protocols.

from qiskit import QuantumCircuit
from qiskit.circuit import ClassicalRegister
from qiskit_aer import AerSimulator

sim = AerSimulator()

# Quantum teleportation correction step:
# Prepare a Bell pair, measure qubit 0, conditionally correct qubit 1
qc = QuantumCircuit(3, 2)

# Prepare the state to teleport on qubit 0
qc.ry(1.2, 0)  # Some arbitrary state

# Create Bell pair between qubits 1 and 2
qc.h(1)
qc.cx(1, 2)

# Bell measurement on qubits 0 and 1
qc.cx(0, 1)
qc.h(0)
qc.measure(0, 0)
qc.measure(1, 1)

# Classical feed-forward: correct qubit 2 based on measurement results
with qc.if_test((1, 1)):  # If classical bit 1 is 1
    qc.x(2)                # Apply X correction
with qc.if_test((0, 1)):  # If classical bit 0 is 1
    qc.z(2)                # Apply Z correction

print(qc.draw())

On real hardware, mid-circuit measurement introduces latency. The classical processing loop (measure, decide, apply correction) typically takes 1 to 10 microseconds on superconducting hardware. This is comparable to the T2 coherence time of many qubits, which limits how many rounds of conditional logic are practical before decoherence degrades the state.

The Deferred Measurement Principle

Any circuit with mid-circuit measurement and classical feed-forward can be replaced by an equivalent circuit that defers all measurements to the end. The trick is to replace each mid-circuit measurement with a CNOT to an ancilla, and replace each classically controlled gate with a quantum-controlled gate from the ancilla.

Example: Two Equivalent Circuits

Circuit A uses mid-circuit measurement. Circuit B defers the measurement.

from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator

sim = AerSimulator()

# Circuit A: Mid-circuit measurement with classical feed-forward
qc_a = QuantumCircuit(2, 2)
qc_a.h(0)
qc_a.measure(0, 0)
with qc_a.if_test((0, 1)):  # If qubit 0 measured 1
    qc_a.x(1)                # Flip qubit 1
qc_a.measure(1, 1)

counts_a = sim.run(qc_a, shots=4000).result().get_counts()
print("Circuit A (mid-circuit measurement):", counts_a)
# Outcomes: '00' (~50%) and '11' (~50%)
# Qubit 1 always matches qubit 0's measurement result

# Circuit B: Deferred measurement (no mid-circuit measurement)
qc_b = QuantumCircuit(2, 2)
qc_b.h(0)
qc_b.cx(0, 1)  # Replace measure + conditional-X with a CNOT
qc_b.measure([0, 1], [0, 1])

counts_b = sim.run(qc_b, shots=4000).result().get_counts()
print("Circuit B (deferred measurement):", counts_b)
# Same outcomes: '00' (~50%) and '11' (~50%)

Both circuits produce identical output distributions. The deferred measurement principle is useful because:

  1. It simplifies circuit analysis (all measurements at the end).
  2. Some simulators and hardware backends do not support mid-circuit measurement.
  3. It proves that mid-circuit measurement never adds computational power; it is a convenience, not a necessity.

The Quantum Zeno Effect

Repeated frequent measurement of a quantum system can freeze its evolution. This is the quantum Zeno effect, named after Zeno’s paradox of the arrow.

The intuition: a small unitary rotation moves the state slightly away from |0>. If you measure immediately after, the probability of finding |0> is very high (the rotation was tiny), so the state collapses back to |0> almost every time. Frequent measurement keeps “resetting” the state before it can evolve.

import numpy as np
from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator

sim = AerSimulator()

def zeno_experiment(n_rotations, measure_every_k):
    """
    Apply n_rotations small Ry rotations to a qubit starting in |0>.
    Measure in the Z basis every measure_every_k rotations.
    Return the probability of finding the qubit still in |0>.

    Each small rotation is Ry(pi / n_rotations), so n_rotations of them
    would take |0> to |1> without any measurements (a full pi rotation).
    """
    angle = np.pi / n_rotations  # Small rotation angle
    qc = QuantumCircuit(1, 1)

    for step in range(n_rotations):
        qc.ry(angle, 0)

        # Measure every k steps (using measure + reset pattern)
        if (step + 1) % measure_every_k == 0 and step < n_rotations - 1:
            qc.measure(0, 0)
            # Reset to |0> if measured |1> (simulates post-selection on |0>)
            with qc.if_test((0, 1)):
                qc.x(0)  # Flip back to |0> (conditional reset)

    # Final measurement
    qc.measure(0, 0)
    counts = sim.run(qc, shots=4000).result().get_counts()
    prob_0 = counts.get('0', 0) / 4000
    return prob_0

n = 100  # Total number of small rotations

# No intermediate measurements: the state rotates fully to |1>
p_no_measure = zeno_experiment(n, measure_every_k=n + 1)
print(f"No measurement:         P(|0>) = {p_no_measure:.3f}")  # ~0.000

# Measure every 10 steps
p_every_10 = zeno_experiment(n, measure_every_k=10)
print(f"Measure every 10 steps: P(|0>) = {p_every_10:.3f}")  # ~0.73

# Measure every step (strong Zeno effect)
p_every_1 = zeno_experiment(n, measure_every_k=1)
print(f"Measure every step:     P(|0>) = {p_every_1:.3f}")  # ~0.97

The math behind this: each small rotation by angle theta = pi/n changes the state from |0> to cos(theta/2)|0> + sin(theta/2)|1>. The probability of measuring |0> after one rotation is cos^2(theta/2). After n measurements (one per rotation), the probability of staying in |0> the entire time is:

P(stay in |0>) = cos^2(pi / 2n)^n  -->  1  as n --> infinity

The more frequently you measure, the more likely the state is to remain frozen. This effect is experimentally verified and has practical applications in quantum error suppression.

Expectation Value Estimation

Quantum algorithms often need to estimate expectation values like <Z>, not just sample individual outcomes. The expectation value of Z for a qubit state is:

<Z> = P(0) * (+1) + P(1) * (-1) = P(0) - P(1)

Given n shots with n0 counts of outcome 0 and n1 counts of outcome 1:

<Z>_estimated = (n0 - n1) / n

The statistical uncertainty (standard error) is:

sigma = sqrt(1 - <Z>^2) / sqrt(n)

For a maximally uncertain state (<Z> = 0): sigma = 1/sqrt(n). For a nearly deterministic state (<Z> = 0.9): sigma = sqrt(1 - 0.81) / sqrt(n) = 0.44 / sqrt(n).

import numpy as np
from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator

sim = AerSimulator()

# Prepare a state with known <Z>
# Ry(theta)|0> = cos(theta/2)|0> + sin(theta/2)|1>
# <Z> = cos^2(theta/2) - sin^2(theta/2) = cos(theta)
theta = 1.2  # radians
expected_z = np.cos(theta)

qc = QuantumCircuit(1, 1)
qc.ry(theta, 0)
qc.measure(0, 0)

n_shots = 4000
counts = sim.run(qc, shots=n_shots).result().get_counts()

n0 = counts.get('0', 0)
n1 = counts.get('1', 0)

z_est = (n0 - n1) / n_shots
sigma = np.sqrt(1 - z_est**2) / np.sqrt(n_shots)

print(f"True <Z>:      {expected_z:.4f}")
print(f"Estimated <Z>: {z_est:.4f}")
print(f"Uncertainty:   {sigma:.4f}")
print(f"Within 2 sigma: {abs(z_est - expected_z) < 2 * sigma}")

This procedure generalizes to any Pauli observable. To estimate <X>, apply H before measuring. To estimate <Y>, apply Sdg then H. For multi-qubit Paulis like <Z0 Z1>, measure both qubits and compute the parity: the eigenvalue is (+1) if the outcomes agree and (-1) if they disagree.

Joint Measurement and the CHSH Bell Inequality

The CHSH inequality is the most famous test of quantum entanglement. It bounds the correlations achievable by any local hidden variable theory. Quantum mechanics violates this bound.

Setup

For a Bell state, choose two measurement settings for each qubit:

  • Alice measures Z or X on qubit 0
  • Bob measures (Z + X) / sqrt(2) or (Z - X) / sqrt(2) on qubit 1

The correlation for a pair of measurement settings (A, B) is:

C(A, B) = P(same outcome) - P(different outcome)

The CHSH parameter S combines four correlations:

S = C(a0, b0) + C(a0, b1) + C(a1, b0) - C(a1, b1)

Classical physics requires |S| <= 2. Quantum mechanics allows S = 2 * sqrt(2) = 2.83.

import numpy as np
from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator

sim = AerSimulator()

def measure_correlation(angle_a, angle_b, n_shots=8000):
    """
    Prepare a Bell state and measure qubit 0 at angle_a,
    qubit 1 at angle_b (both in the ZX plane).
    Return the correlation <A * B>.

    Rotation Ry(-2*angle) maps the measurement axis to Z.
    """
    qc = QuantumCircuit(2, 2)

    # Prepare Bell state (|00> + |11>) / sqrt(2)
    qc.h(0)
    qc.cx(0, 1)

    # Rotate measurement bases
    qc.ry(-2 * angle_a, 0)
    qc.ry(-2 * angle_b, 1)

    qc.measure([0, 1], [0, 1])

    counts = sim.run(qc, shots=n_shots).result().get_counts()

    # Compute correlation: +1 for same outcome, -1 for different
    corr = 0
    for bitstring, count in counts.items():
        b0 = int(bitstring[0])  # qubit 1 (Qiskit little-endian)
        b1 = int(bitstring[1])  # qubit 0
        sign = 1 if b0 == b1 else -1
        corr += sign * count
    return corr / n_shots

# CHSH measurement angles (optimal for maximum violation)
a0 = 0              # Alice setting 0: Z
a1 = np.pi / 4      # Alice setting 1: (Z+X)/sqrt(2), i.e. pi/4
b0 = np.pi / 8      # Bob setting 0: pi/8
b1 = -np.pi / 8     # Bob setting 1: -pi/8

# Four correlations
C00 = measure_correlation(a0, b0)
C01 = measure_correlation(a0, b1)
C10 = measure_correlation(a1, b0)
C11 = measure_correlation(a1, b1)

S = C00 + C01 + C10 - C11

print(f"C(a0,b0) = {C00:.3f}")
print(f"C(a0,b1) = {C01:.3f}")
print(f"C(a1,b0) = {C10:.3f}")
print(f"C(a1,b1) = {C11:.3f}")
print(f"S = {S:.3f}")
print(f"Classical bound: |S| <= 2")
print(f"Quantum prediction: S = 2*sqrt(2) = {2*np.sqrt(2):.3f}")
print(f"Violation: {abs(S) > 2}")

The measured S is approximately 2.83, well above the classical limit of 2. This demonstrates that entangled qubits exhibit correlations that no classical system can reproduce.

Quantum State Tomography from Measurements

State tomography reconstructs the full density matrix of a qubit from measurements in multiple bases. For a single qubit, the density matrix is:

rho = (I + <X> X + <Y> Y + <Z> Z) / 2

where <X>, <Y>, <Z> are the expectation values of the three Pauli operators. You estimate each one by measuring in the corresponding basis with many shots.

import numpy as np
from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator

sim = AerSimulator()

def estimate_expectation(state_prep_fn, basis, n_shots=4000):
    """
    Estimate the expectation value of a Pauli operator.
    state_prep_fn: function that adds state preparation gates to a circuit
    basis: 'Z', 'X', or 'Y'
    """
    qc = QuantumCircuit(1, 1)
    state_prep_fn(qc)

    # Basis rotation
    if basis == 'X':
        qc.h(0)
    elif basis == 'Y':
        qc.sdg(0)
        qc.h(0)
    # Z: no rotation needed

    qc.measure(0, 0)
    counts = sim.run(qc, shots=n_shots).result().get_counts()
    n0 = counts.get('0', 0)
    n1 = counts.get('1', 0)
    return (n0 - n1) / n_shots

# Prepare a known state: Ry(0.8) Rz(1.5) |0>
def prep(qc):
    qc.rz(1.5, 0)
    qc.ry(0.8, 0)

# Estimate Bloch vector components
exp_x = estimate_expectation(prep, 'X')
exp_y = estimate_expectation(prep, 'Y')
exp_z = estimate_expectation(prep, 'Z')

print(f"<X> = {exp_x:.3f}")
print(f"<Y> = {exp_y:.3f}")
print(f"<Z> = {exp_z:.3f}")

# Reconstruct density matrix
I = np.eye(2, dtype=complex)
X = np.array([[0, 1], [1, 0]], dtype=complex)
Y = np.array([[0, -1j], [1j, 0]], dtype=complex)
Z = np.array([[1, 0], [0, -1]], dtype=complex)

rho_reconstructed = (I + exp_x * X + exp_y * Y + exp_z * Z) / 2
print("\nReconstructed density matrix:")
print(np.round(rho_reconstructed, 3))

# Compare with the true state vector
from qiskit.quantum_info import Statevector

qc_true = QuantumCircuit(1)
prep(qc_true)
psi_true = Statevector.from_instruction(qc_true)
rho_true = np.outer(psi_true.data, psi_true.data.conj())
print("\nTrue density matrix:")
print(np.round(rho_true, 3))

# Compute fidelity: F = Tr(rho_true @ rho_reconstructed) for pure states
fidelity = np.real(np.trace(rho_true @ rho_reconstructed))
print(f"\nFidelity: {fidelity:.4f}")

On real hardware, this procedure is how experimentalists verify that their qubits are in the intended state. The fidelity metric quantifies how close the reconstructed state is to the target, with 1.0 being a perfect match. With 4000 shots per basis, you can expect fidelities above 0.99 for ideal simulation and 0.90 to 0.99 on current hardware, depending on error rates.

Ancilla-Assisted Measurement: The Hadamard Test

Many quantum algorithms use ancilla qubits to estimate properties of a unitary operator without directly measuring the main register. The Hadamard test is the simplest example.

To estimate Re(<psi|U|psi>) for a unitary U and state |psi>:

  1. Prepare an ancilla qubit in |+> = (|0> + |1>) / sqrt(2).
  2. Apply controlled-U: if the ancilla is |1>, apply U to |psi>.
  3. Apply H to the ancilla.
  4. Measure the ancilla.

The probability of measuring 0 on the ancilla is:

P(0) = (1 + Re(<psi|U|psi>)) / 2

So Re(<psi|U|psi>) = 2 * P(0) - 1.

from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator

sim = AerSimulator()

# Hadamard test: estimate Re(<+|Z|+>)
# True value: <+|Z|+> = 0 (the |+> state has equal weight on |0> and |1>)

qc = QuantumCircuit(2, 1)

# Prepare system qubit (qubit 1) in |+>
qc.h(1)

# Prepare ancilla (qubit 0) in |+>
qc.h(0)

# Controlled-Z: apply Z to system qubit if ancilla is |1>
qc.cz(0, 1)

# Apply H to ancilla
qc.h(0)

# Measure ancilla
qc.measure(0, 0)

counts = sim.run(qc, shots=4000).result().get_counts()
p0 = counts.get('0', 0) / 4000
re_expectation = 2 * p0 - 1
print(f"P(0) = {p0:.3f}")
print(f"Re(<+|Z|+>) estimated = {re_expectation:.3f}")  # ~0.000
print(f"True value = 0.000")

To also estimate Im(<psi|U|psi>), replace the initial H on the ancilla with H followed by Sdg (preparing |0> - i|1>) / sqrt(2)). The ancilla measurement then gives:

P(0) = (1 + Im(<psi|U|psi>)) / 2

The Hadamard test is the building block for quantum phase estimation, quantum chemistry algorithms, and many variational methods.

Designing Algorithms Around Measurement

Because measurement is irreversible and random, quantum algorithms must be structured so that the correct answer has high probability before measurement:

  1. Start with all qubits in a known state (usually |0>).
  2. Apply gates to create a superposition over many possibilities.
  3. Use interference to amplify amplitudes of correct answers.
  4. Measure and use the result.

Grover’s algorithm, for example, applies a sequence of reflections that iteratively rotate the state toward the target answer. After O(sqrt(N)) iterations, the target answer has amplitude close to 1, so measurement almost certainly gives the correct result.

Common Mistakes with Quantum Measurement

1. Measuring mid-circuit to “debug” the state

A common beginner mistake is inserting measurements in the middle of a circuit to check intermediate states. This collapses the superposition and ruins all subsequent quantum operations. If you need to inspect intermediate states during development, use the statevector simulator instead:

from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector

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

# Inspect without collapsing (simulation only)
sv = Statevector.from_instruction(qc)
print(sv)  # Shows the full quantum state

2. Qiskit’s little-endian qubit ordering

In Qiskit, qubit 0 is the rightmost bit in the output bitstring. A measurement result of '01' means qubit 0 = 1 and qubit 1 = 0. This is the opposite of what many beginners expect.

from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator

sim = AerSimulator()

qc = QuantumCircuit(2, 2)
qc.x(0)  # Flip qubit 0 to |1>
# Qubit 0 = |1>, Qubit 1 = |0>
qc.measure([0, 1], [0, 1])

counts = sim.run(qc, shots=1).result().get_counts()
print(counts)  # {'01'} -- rightmost bit is qubit 0
# bitstring '01': qubit 1 = 0 (left), qubit 0 = 1 (right)

3. Assuming two measurements always agree

Measuring the same qubit twice gives the same result only if no gates are applied between measurements. If you apply a gate between the two measurements, the second measurement depends on both the first result and the gate.

from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator

sim = AerSimulator()

# Two measurements with a gate in between
qc = QuantumCircuit(1, 2)
qc.h(0)
qc.measure(0, 0)  # First measurement: random 0 or 1
qc.h(0)           # Apply H after collapse
qc.measure(0, 1)  # Second measurement: can differ from the first

counts = sim.run(qc, shots=4000).result().get_counts()
print(counts)
# All four outcomes {00, 01, 10, 11} are possible

4. Confusing expectation value with most probable outcome

The expectation value <Z> is NOT the same as the most probable measurement outcome. For a state with P(0) = 0.6 and P(1) = 0.4:

<Z> = 0.6 * (+1) + 0.4 * (-1) = 0.2

The most probable outcome is 0 (which gives Z eigenvalue +1), but the expectation value is 0.2, not 1. This distinction matters when using measurement results in variational algorithms, where the cost function is an expectation value.

5. Forgetting basis rotations

Measurement in Qiskit always happens in the Z basis. To measure in another basis, you must apply the appropriate rotation gates before calling measure(). Forgetting this is a common source of wrong results:

# WRONG: This measures in Z basis, not X basis
qc_wrong = QuantumCircuit(1, 1)
qc_wrong.h(0)           # Prepare |+>
qc_wrong.measure(0, 0)  # Z-basis measurement: random result

# CORRECT: Apply H to rotate X basis to Z basis, then measure
qc_right = QuantumCircuit(1, 1)
qc_right.h(0)           # Prepare |+>
qc_right.h(0)           # Rotate X to Z basis
qc_right.measure(0, 0)  # Always gives 0 (|+> is X eigenstate)

Next Steps

Was this tutorial helpful?