Skip to main content

Violating the CHSH Inequality on IBM Quantum Backends


The CHSH inequality is an important result related to Bell’s theorem, which shows that quantum mechanics is incompatible with local hidden-variable theories. This means that there are results in quantum mechanics (see the Einstein-Podolsky-Rosen paradox) which cannot be explained by some unknown parameters that are local to the system being observed.

There are simple experiments on quantum computers that can show violation of the CHSH inequality. These experiments involve running computations on systems of entangled qubits. With Covalent QElectrons and free access to IBM Quantum backends, we can easily do this.

The CHSH Inequality

Physical systems that are compatible with local hidden variables must obey the following inequality.

S=ABAb±aB+ab2(1)|\langle S_{\mp} \rangle| = |\langle AB \rangle \mp \langle Ab \rangle \pm \langle aB \rangle + \langle ab \rangle| \leq 2 \qquad (1)

Here, \langle \, \cdot \, \rangle refers to the expectation value of some observable. The value AB\langle AB \rangle, for example, is the average outcome of an experiment involving an AA-basis measurement on the first qubit and a BB-basis measurement on the second.

Testing the CHSH inequality requires two different measurement bases for each qubit, or four two-qubit measurement bases for the entire system. In our experiment, we take bb and BB to be the Pauli ZZ and XX bases, while aa and AA taken to be the bases ZZ^\prime and XX^\prime. Crucially, the latter two are rotated (in the same plane) by some angle θ\theta relative to ZZ and XX.

Experimental Demonstration of Bell’s Theorem

Our goal here will be to verify that the CHSH inequality (1)(1) is violated by a quantum system of two entangled qubits.


This will involve the following steps:

  1. Prepare the two-qubit system in a Bell state.
  2. Apply YY-rotation by an angle θ\theta to the first qubit only.
  3. Measure the system in the ZZZZ, ZXZX, XZXZ, or XXXX basis.
  4. Repeat steps 1-3 many times to estimate the expectation value.
  5. Repeat steps 1-4 to obtain estimates for all four bases which are used to compute S\langle S_{\mp} \rangle.

Note that YY-rotating the first qubit before measurement is equivalent here to measuring that qubit in a rotated basis. Therefore, the expectation values we obtain are indeed the ZZ\langle Z^\prime Z \rangle, ZX\langle Z^\prime X \rangle, XZ\langle X^\prime Z \rangle, and XX\langle X^\prime X \rangle required to compute S|\langle S_{\mp} \rangle|.

Quantum Circuit

The circuit we need is one that prepares a Bell state:

Φ+=12(00+11)|\Phi^{+}\rangle = \frac{1}{\sqrt{2}}(|00\rangle + |11\rangle)

Briefly, the state Φ+|\Phi^{+}\rangle is said to be “maximally entangled” because measuring either qubit will determine the measurement outcome of the other with perfect accuracy (when both qubits are measured in the same basis). This is easy to verify by looking at the basis states that make up Φ+|\Phi^{+}\rangle.

State preparation is straightforward. Starting with the 00|00\rangle state, applying a Hadamard on the first qubit, followed by a CNOT gate produces Φ+|\Phi^{+}\rangle. We also apply an additional YY-rotation of the first qubit by θ\theta to control offset between the respective measurements.

import pennylane as qml

observables = [
qml.PauliZ(0) @ qml.PauliZ(1),
qml.PauliZ(0) @ qml.PauliX(1),
qml.PauliX(0) @ qml.PauliZ(1),
qml.PauliX(0) @ qml.PauliX(1),

dev = qml.device("default.qubit", wires=2, shots=8192)

def chsh_circuit(theta):
# Prepare Bell state.
qml.CNOT(wires=[0, 1])

# Apply Y-rotation by angle `theta`.
qml.RY(theta, wires=0)

# Multiple returns to get all 4 expectation values.
return [qml.expval(obs) for obs in observables]

Creating QElectrons

With the code above, we can easily perform a simulated version of this experiment using regular Pennylane. To make for a more interesting experiment, we can use Covalent QElectrons to try this on various IBM QPUs.

For example, a QElectron that runs on "ibmq_lima" is defined below.

# Define a quantum executor that targets the "ibmq_lima" QPU.
ibmq_lima = ct.executor.QiskitExecutor(

# Create a QElectron from the Pennylane QNode.
# qcircuit_lima = ct.qelectron(chsh_circuit, executors=ibmq_lima)

When making a QiskitExecutor, users can additionally pass the keyword arguments

  • hub
  • group
  • project

to take advantage of privileged IBM Quantum access by their organization. The executor’s ibmqx_token argument can be omitted if provided in the Covalent configuration file (typically at ~/.config/covalent/covalent.conf). We shall assume that this is the case for brevity going forward.

Below we define a few more quantum executors for later use.

# NOTE: assume `ibmqx_token` provided in Covalent configuration.

# Define a quantum executor that targets "ibm_lagos" QPU.
ibm_lagos = ct.executor.QiskitExecutor(

# Define quantum executors for two IBM simulators.
ibmq_qasm_simulator = ct.executor.QiskitExecutor(

simulator_statevector = ct.executor.QiskitExecutor(

Covalent Workflow

Let’s use these executors in a Covalent workflow that tests the CHSH inequality. To briefly outline the workflow, we have

  • a task that runs the experiment on "ibmq_lima"
  • a task that runs the experiment on "ibm_lagos"
  • a task that uses both "ibmq_qasm_simulator" and "simulator_statevector" as a Quantum Cluster, distributing circuits evenly between the two.

Finally, the compute_S_mp() task uses the estimated expectation values to calculate S\langle S_{\mp} \rangle for all three cases.

import covalent as ct
from pennylane import numpy as np

def get_thetas():
"""Generates a range of angles `theta`."""
return np.linspace(0, 2 * np.pi, 25)

def chsh_on_lima(thetas):
"""Runs the angle sweep on 'ibmq_lima' QPU."""
qelectron = ct.qelectron(chsh_circuit, executors=ibmq_lima)
expvals_list = qelectron(thetas)
return np.asarray(expvals_list)

def chsh_on_lagos(thetas):
"""Runs the angle sweep on 'ibm_lagos' QPU."""
qelectron = ct.qelectron(chsh_circuit, executors=ibm_lagos)
expvals_list = qelectron(thetas)
return np.asarray(expvals_list)

def chsh_on_simulators(thetas):
Uses a cluster of of two simulators to run the angle sweep.
executors = [ibmq_qasm_simulator, simulator_statevector]
qelectron = ct.qelectron(chsh_circuit, executors=executors)
expvals_list = qelectron(thetas)
return np.asarray(expvals_list)

def compute_S_mp(expvals: dict):
"""Computes S_mins and S_plus from Equation (1)."""
results = {}
for name, expvals_arr in expvals.items():

AB, Ab, aB, ab = expvals_arr
S_minus = AB - Ab + aB + ab
S_plus = AB + Ab - aB + ab
results[name] = (S_minus, S_plus)

return results

def workflow():

thetas = get_thetas()
expvals = {}

# QPU experiments.
expvals["lima"] = chsh_on_lima(thetas)
expvals["lagos"] = chsh_on_lagos(thetas)

# Simulator experiment.
expvals["simulators"] = chsh_on_simulators(thetas)

# Evaluate the CHSH inequality.
results = compute_S_mp(expvals)

return thetas, expvals, results

Running the following will dispatch the workflow and wait for the results.

dispatch_id = ct.dispatch(workflow)()
thetas, expvals, results = ct.get_result(dispatch_id, wait=True).result

Looking at the Covalent UI, we can see that the simulator experiments completed in ~1 minute. The QPU experiments (chsh_on_lagos() and chsh_on_lima()) can take much longer depending on the queue times for the two devices. In our workflow, Covalent mitigates this at the task level, with both tasks queueing for QPUs at the same time.

IBM Quantum queue showing “ibmq_lima” and “ibm_lagos” jobs queued simultaneously.

Covalent workflow showing the completed simulator task and pending QPU tasks.

Note that there is additionally circuit-level parallelism provided by QElectrons themselves. In this workflow, chsh_on_simulators() uses a cluster of simulators and distributes individual circuit calls across two different backends.

Altogether, the experiment completed in about 40 minutes.


The workflow results are plotted below, using a function given in the Appendix.

Four curves inhabit each subplot. The dashed and dotted grey lines represent S\langle S_{-} \rangle and S+\langle S_{+} \rangle from the cluster of simulators. These curves are shown on both subplots for comparison. The orange and purples lines represent S\langle S_{-} \rangle and S+\langle S_{+} \rangle for "ibmq_lima" on the left, and similarly for "ibm_lagos" on the right.

It’s easiest to see from the simulated (sim.) results that either the S|\langle S_{-} \rangle| or S+|\langle S_{+} \rangle| is always greater than 22 for all θ\theta between 00 and 2π2\pi. In other words, the CHSH inequality is violated for any non-trivial rotation between the pairs of bases.

This is not so obvious from the hardware results, where noise at large and small θ\theta somewhat obscures the intended outcome. Interesting to note, there is also significant phase error in the hardware results—and this is quite convenient for our plot.

This tutorial is based on the Qiskit Runtime example found here.