Skip to main contentIBM Quantum Documentation Preview
This is a preview build of IBM Quantum® documentation. Refer to quantum.cloud.ibm.com/docs for the official documentation.

CHSH inequality

Usage estimate: Two minutes on a Heron r3 processor (NOTE: This is an estimate only. Your runtime might vary.)


Learning outcomes

After completing this tutorial, you can expect to understand the following information:

  • How to construct a parameterized Bell-state CHSH circuit and measure the four expectation values that make up the CHSH witnesses.
  • How to compute expectation values of multiple observables on a parameter sweep in a single call to the EstimatorV2 primitive.
  • How to validate a quantum workflow on a noisy local simulator with AerSimulator.from_backend before submitting to hardware.
  • How to scale a CHSH experiment into a device-wide entanglement benchmark by running many independent Bell pairs in parallel on IBM Quantum® hardware.

Prerequisites

It is recommended that you familiarize yourself with these topics:


Background

In this tutorial, you will run an experiment on a quantum computer to demonstrate the violation of the CHSH inequality with the Estimator primitive.

The CHSH inequality, named after Clauser, Horne, Shimony, and Holt, is used to experimentally test Bell's theorem (1969). The theorem asserts that local hidden-variable theories cannot account for some consequences of entanglement in quantum mechanics. Demonstrating a violation of the CHSH inequality shows that quantum mechanics is incompatible with local hidden-variable theories, an experiment that is foundational to our understanding of quantum mechanics.

The 2022 Nobel Prize for Physics was awarded to Alain Aspect, John Clauser, and Anton Zeilinger in part for their pioneering work in quantum information science, and in particular, for their experiments with entangled photons demonstrating violation of Bell's inequalities.

For this experiment, we will create an entangled pair on which we measure each qubit in two different bases. We will label the bases for the first qubit AA and aa and the bases for the second qubit BB and bb. This allows us to compute the CHSH quantity S1S_1:

S1=A(Bb)+a(B+b).S_1 = A(B-b) + a(B+b).

Each observable is either +1+1 or 1-1. Clearly, one of the terms B±bB\pm b must be 00, and the other must be ±2\pm 2. Therefore, S1=±2S_1 = \pm 2. The average value of S1S_1 must satisfy the inequality:

S12.|\langle S_1 \rangle|\leq 2.

Expanding S1S_1 in terms of AA, aa, BB, and bb gives:

S1=ABAb+aB+ab2.|\langle S_1 \rangle| = |\langle AB \rangle - \langle Ab \rangle + \langle aB \rangle + \langle ab \rangle| \leq 2.

You can define another CHSH quantity S2S_2:

S2=A(B+b)a(Bb),S_2 = A(B+b) - a(B-b),

which leads to another inequality:

S2=AB+AbaB+ab2.|\langle S_2 \rangle| = |\langle AB \rangle + \langle Ab \rangle - \langle aB \rangle + \langle ab \rangle| \leq 2.

If quantum mechanics could be described by local hidden-variable theories, these inequalities would always hold. As demonstrated in this tutorial, they can be violated on a quantum computer, so quantum mechanics is not compatible with local hidden-variable theories.

We create the entangled pair by preparing the Bell state Φ+=00+112|\Phi^+\rangle = \frac{|00\rangle + |11\rangle}{\sqrt{2}}. Using the Estimator primitive, we obtain the expectation values AB,Ab,aB\langle AB \rangle, \langle Ab \rangle, \langle aB \rangle, and ab\langle ab \rangle directly, without reconstructing them from raw counts. We measure the second qubit in the ZZ and XX bases. The first qubit is measured in orthogonal bases as well, but with a rotation angle θ\theta that we sweep between 00 and 2π2\pi. The Estimator primitive evaluates this parameter sweep in a single primitive unified bloc (PUB).


Requirements

Before starting this tutorial, be sure you have the following installed:

  • Qiskit SDK v2.0 or later, with visualization support
  • Qiskit Runtime v0.40 or later (pip install qiskit-ibm-runtime)
  • Qiskit Aer v0.17 or later (pip install qiskit-aer)

Setup

# General
import numpy as np

# Qiskit imports
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

# Qiskit Runtime imports
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import EstimatorV2 as Estimator

# Qiskit Aer for local noisy simulation
from qiskit_aer import AerSimulator

# Plotting routines
import matplotlib.pyplot as plt
import matplotlib.ticker as tck
# Select an IBM Quantum backend.
service = QiskitRuntimeService()
backend = service.least_busy(
    min_num_qubits=127, operational=True, simulator=False
)
backend.name

Output:

'ibm_pittsburgh'

Small-scale simulator example

Before submitting a hardware job, we validate the entire workflow on a local noisy simulator. We use AerSimulator.from_backend(backend) to build a simulator that inherits the noise model and coupling map of the backend you selected, so the simulator response is qualitatively similar to what we expect from hardware.

Step 1: Map classical inputs to a quantum problem

We write the CHSH circuit with a single parameter θ\theta, which sweeps the measurement basis of the first qubit. The Estimator primitive simplifies the analysis: it returns expectation values of observables directly, and it can evaluate a parameterized circuit at many parameter values in a single call.

theta = Parameter(r"$\theta$")

chsh_circuit = QuantumCircuit(2)
chsh_circuit.h(0)
chsh_circuit.cx(0, 1)
chsh_circuit.ry(theta, 0)
chsh_circuit.draw(output="mpl", idle_wires=False, style="iqp")

Output:

Output of the previous code cell

Next, we create a list of 21 phase values from 00 to 2π2\pi at which to evaluate the parameterized circuit (00, 0.1π0.1\pi, 0.2π0.2\pi, ..., 1.9π1.9\pi, 2π2\pi).

number_of_phases = 21
phases = np.linspace(0, 2 * np.pi, number_of_phases)
# Phases need to be expressed as a list of lists for the Estimator PUB
individual_phases = [[ph] for ph in phases]

Finally we define the observables. The first qubit is measured along axes rotated by θ\theta; the second qubit is measured in ZZ and XX. With those choices, the four CHSH correlators map to the Pauli operators ZZZZ, ZXZX, XZXZ, and XXXX:

S1=ZZZX+XZ+XX,\langle S_1 \rangle = \langle ZZ \rangle - \langle ZX \rangle + \langle XZ \rangle + \langle XX \rangle, S2=ZZ+ZXXZ+XX.\langle S_2 \rangle = \langle ZZ \rangle + \langle ZX \rangle - \langle XZ \rangle + \langle XX \rangle.
# <S_1> = <ZZ> - <ZX> + <XZ> + <XX>
observable1 = SparsePauliOp.from_list(
    [("ZZ", 1), ("ZX", -1), ("XZ", 1), ("XX", 1)]
)

# <S_2> = <ZZ> + <ZX> - <XZ> + <XX>
observable2 = SparsePauliOp.from_list(
    [("ZZ", 1), ("ZX", 1), ("XZ", -1), ("XX", 1)]
)

Step 2: Optimize problem for quantum hardware execution

V2 primitives only accept circuits and observables that conform to the instructions and connectivity supported by the target system (instruction set architecture, or ISA, circuits and observables). We build the AerSimulator from the backend and transpile against the simulator's target so the same pass manager is exercised end-to-end.

# Build a noisy simulator from the ibm_pittsburgh backend
aer_sim = AerSimulator.from_backend(backend)

pm = generate_preset_pass_manager(target=aer_sim.target, optimization_level=3)
chsh_isa_circuit = pm.run(chsh_circuit)
chsh_isa_circuit.draw(output="mpl", idle_wires=False, style="iqp")

Output:

Output of the previous code cell

We also transform the observables to match the transpiled circuit's qubit layout using SparsePauliOp.apply_layout.

isa_observable1 = observable1.apply_layout(layout=chsh_isa_circuit.layout)
isa_observable2 = observable2.apply_layout(layout=chsh_isa_circuit.layout)

Step 3: Execute using Qiskit primitives

Run the parameter sweep with EstimatorV2 in aer_sim mode. The Estimator run() method takes an iterable of PUBs. Each PUB has the format (circuit, observables, parameter_values, precision). We pass both observables together so they share the same parameter sweep.

# Use the AerSimulator-backed Estimator to validate the workflow locally
estimator_sim = Estimator(mode=aer_sim)

pub = (
    chsh_isa_circuit,  # ISA circuit
    [[isa_observable1], [isa_observable2]],  # ISA observables
    individual_phases,  # Parameter values
)

sim_result = estimator_sim.run(pubs=[pub]).result()

Step 4: Post-process and return result in desired classical format

The Estimator returns expectation values for both observables. We plot them against θ\theta together with the classical bound (±2\pm 2) and the Tsirelson bound (±22\pm 2\sqrt{2}). The shaded grey regions mark the gap between the two. Points that lie inside these bands violate the CHSH inequality.

chsh1_sim = sim_result[0].data.evs[0]
chsh2_sim = sim_result[0].data.evs[1]


def plot_chsh(phases, chsh1, chsh2, title):
    fig, ax = plt.subplots(figsize=(10, 6))

    ax.plot(
        phases / np.pi, chsh1, "o-", label=r"$\langle S_1 \rangle$", zorder=3
    )
    ax.plot(
        phases / np.pi, chsh2, "o-", label=r"$\langle S_2 \rangle$", zorder=3
    )

    # classical bound +-2
    ax.axhline(y=2, color="0.9", linestyle="--")
    ax.axhline(y=-2, color="0.9", linestyle="--")

    # quantum bound, +-2*sqrt(2)
    ax.axhline(y=np.sqrt(2) * 2, color="0.9", linestyle="-.")
    ax.axhline(y=-np.sqrt(2) * 2, color="0.9", linestyle="-.")
    ax.fill_between(phases / np.pi, 2, 2 * np.sqrt(2), color="0.6", alpha=0.7)
    ax.fill_between(
        phases / np.pi, -2, -2 * np.sqrt(2), color="0.6", alpha=0.7
    )

    ax.xaxis.set_major_formatter(tck.FormatStrFormatter("%g $\\pi$"))
    ax.xaxis.set_major_locator(tck.MultipleLocator(base=0.5))

    ax.set_xlabel(r"$\theta$")
    ax.set_ylabel("CHSH witness")
    ax.set_title(title)
    ax.legend()
    plt.show()


plot_chsh(
    phases,
    chsh1_sim,
    chsh2_sim,
    "CHSH witnesses from AerSimulator (ibm_pittsburgh noise model)",
)

Output:

Output of the previous code cell

The simulator's CHSH witnesses already exceed the classical bound of ±2\pm 2 at several values of θ\theta, even with the backend's noise model. The peaks fall just short of the Tsirelson bound ±22\pm 2\sqrt{2} because of simulated device noise. With the workflow validated, we move on to actual hardware.


Large-scale hardware example

A CHSH test is intrinsically a two-qubit experiment, so it does not scale by making one circuit bigger. Instead, it scales by running many tests in parallel. Here we tile the backend with as many disjoint Bell pairs as its connectivity allows (a matching of the coupling map) and run an independent CHSH sub-circuit on every pair, all in a single job.

This turns CHSH into a device-wide benchmark of entanglement quality: rather than a single hand-picked pair, we test entanglement across a large fraction of the chip at once, under realistic conditions where every pair contends with its neighbors' crosstalk and parallel-gate errors. Violating the inequality on every pair simultaneously certifies that genuine entanglement is available everywhere on the device.

# -------------------------Step 1: Map classical inputs to a quantum problem-------------------------
# A CHSH test is bipartite, so we scale up by running one independent CHSH
# experiment on every disjoint Bell pair the device can host. A greedy
# matching of the coupling map gives a set of edges that share no qubits.
num_qubits = backend.num_qubits
used = set()
pairs = []
for qa, qb in backend.coupling_map.get_edges():
    if qa not in used and qb not in used:
        pairs.append((qa, qb))
        used.update((qa, qb))
num_pairs = len(pairs)
print(
    f"Tiling {backend.name} with {num_pairs} parallel Bell pairs "
    f"({2 * num_pairs} of {num_qubits} qubits)"
)

# One parameterized CHSH sub-circuit per pair, all sharing the angle theta
theta = Parameter(r"$\theta$")
chsh_circuit = QuantumCircuit(num_qubits)
for qa, qb in pairs:
    chsh_circuit.h(qa)
    chsh_circuit.cx(qa, qb)
    chsh_circuit.ry(theta, qa)

# Embed the two CHSH observables onto each pair's qubits (identity elsewhere)
obs1 = SparsePauliOp.from_list([("ZZ", 1), ("ZX", -1), ("XZ", 1), ("XX", 1)])
obs2 = SparsePauliOp.from_list([("ZZ", 1), ("ZX", 1), ("XZ", -1), ("XX", 1)])
observables = []
for qa, qb in pairs:
    observables.append([obs1.apply_layout([qa, qb], num_qubits)])
    observables.append([obs2.apply_layout([qa, qb], num_qubits)])

number_of_phases = 21
phases = np.linspace(0, 2 * np.pi, number_of_phases)
individual_phases = [[ph] for ph in phases]

# -------------------------Step 2: Optimize problem for quantum hardware execution-------------------------
pm = generate_preset_pass_manager(target=backend.target, optimization_level=3)
chsh_isa_circuit = pm.run(chsh_circuit)
isa_observables = [
    [o[0].apply_layout(chsh_isa_circuit.layout)] for o in observables
]

# -------------------------Step 3: Execute using Qiskit primitives-------------------------
estimator_hw = Estimator(mode=backend)
estimator_hw.options.environment.job_tags = ["TUT_CI"]

pub = (chsh_isa_circuit, isa_observables, individual_phases)
job = estimator_hw.run(pubs=[pub])
print(f"Job ID: {job.job_id()}")
hw_result = job.result()

# -------------------------Step 4: Post-process and return result in desired classical format-------------------------
# evs has shape (2 * num_pairs, number_of_phases); rows alternate S1, S2
evs = np.asarray(hw_result[0].data.evs)
chsh1_all = evs[0::2]
chsh2_all = evs[1::2]

# A pair "violates" CHSH if its strongest witness exceeds the classical bound
peak = np.maximum(
    np.abs(chsh1_all).max(axis=1), np.abs(chsh2_all).max(axis=1)
)
n_violate = int(np.sum(peak > 2))
print(
    f"{n_violate}/{num_pairs} Bell pairs violated the CHSH inequality "
    f"(mean peak witness {peak.mean():.2f}, classical bound 2)"
)

fig, ax = plt.subplots(figsize=(10, 6))

# Faint individual per-pair curves
for row in chsh1_all:
    ax.plot(phases / np.pi, row, color="#1f77b4", alpha=0.2, lw=1)
for row in chsh2_all:
    ax.plot(phases / np.pi, row, color="#ff7f0e", alpha=0.2, lw=1)

# Bold mean curves across all pairs
ax.plot(
    phases / np.pi,
    chsh1_all.mean(axis=0),
    color="#1f77b4",
    lw=2.5,
    label=r"$\langle S_1 \rangle$ (mean)",
)
ax.plot(
    phases / np.pi,
    chsh2_all.mean(axis=0),
    color="#ff7f0e",
    lw=2.5,
    label=r"$\langle S_2 \rangle$ (mean)",
)

# classical bound +-2 and Tsirelson bound +-2*sqrt(2)
ax.axhline(y=2, color="0.9", linestyle="--")
ax.axhline(y=-2, color="0.9", linestyle="--")
ax.axhline(y=np.sqrt(2) * 2, color="0.9", linestyle="-.")
ax.axhline(y=-np.sqrt(2) * 2, color="0.9", linestyle="-.")
ax.fill_between(phases / np.pi, 2, 2 * np.sqrt(2), color="0.6", alpha=0.7)
ax.fill_between(phases / np.pi, -2, -2 * np.sqrt(2), color="0.6", alpha=0.7)

ax.xaxis.set_major_formatter(tck.FormatStrFormatter("%g $\\pi$"))
ax.xaxis.set_major_locator(tck.MultipleLocator(base=0.5))
ax.set_xlabel(r"$\theta$")
ax.set_ylabel("CHSH witness")
ax.set_title(
    f"CHSH witnesses for {num_pairs} parallel Bell pairs on {backend.name}"
)
ax.legend()
plt.show()

Output:

Tiling ibm_pittsburgh with 64 parallel Bell pairs (128 of 156 qubits)
Job ID: d86efd5g7okc73el0rp0
63/64 Bell pairs violated the CHSH inequality (mean peak witness 2.75, classical bound 2)
Output of the previous code cell

The faint curves are the individual Bell pairs and the bold curves are their mean across the device. Every pair traces the same sinusoid predicted by quantum mechanics, and the spread between the faint curves reflects the variation in noise from pair to pair. Wherever a curve enters the grey bands, it has crossed the classical bound of ±2\pm 2, and the printed summary confirms that essentially every pair violates the CHSH inequality at the same time.

The peaks fall short of the Tsirelson bound ±22\pm 2\sqrt{2} because of device noise, but the conclusion is unambiguous: the backend sustains genuine entanglement across the whole chip simultaneously, not just on a single hand-picked pair. This is the sense in which the CHSH experiment "scales": not as one larger circuit, but as a parallel benchmark that certifies entanglement everywhere at once.


Next steps

Recommendations

If you found this work interesting, you might be interested in the following material: