CHSH-Ungleichung
Nutzungsschätzung: Zwei Minuten auf einem Heron r3-Prozessor (HINWEIS: Dies ist nur eine Schätzung. Deine Laufzeit kann variieren.)
Lernziele
Nach Abschluss dieses Tutorials kannst du folgende Themen verstehen:
- Wie man einen parametrisierten Bell-Zustand-CHSH-Circuit erstellt und die vier Erwartungswerte misst, die die CHSH-Zeugen bilden.
- Wie man Erwartungswerte mehrerer Observablen bei einem Parameter-Sweep in einem einzigen Aufruf des
EstimatorV2-Primitivs berechnet. - Wie man einen Quantenworkflow auf einem verrauschten lokalen Simulator mit
AerSimulator.from_backendvalidiert, bevor er an Hardware übermittelt wird. - Wie man ein CHSH-Experiment in einen geräteweiten Verschränkungs-Benchmark skaliert, indem viele unabhängige Bell-Paare parallel auf IBM Quantum®-Hardware ausgeführt werden.
Voraussetzungen
Es wird empfohlen, dich mit diesen Themen vertraut zu machen:
- Entanglement in action, eine Kurslektion über Bell-Zustände und das CHSH-Spiel.
SparsePauliOpund die Qiskit-Primitive Einführung.
Hintergrund
In diesem Tutorial führst du ein Experiment auf einem Quantencomputer aus, um die Verletzung der CHSH-Ungleichung mit dem Estimator-Primitiv zu demonstrieren.
Die CHSH-Ungleichung, benannt nach Clauser, Horne, Shimony und Holt, wird verwendet, um Bells Theorem (1969) experimentell zu testen. Das Theorem besagt, dass lokale Theorien mit verborgenen Variablen einige Konsequenzen der Verschränkung in der Quantenmechanik nicht erklären können. Die Verletzung der CHSH-Ungleichung zeigt, dass die Quantenmechanik mit lokalen Theorien mit verborgenen Variablen nicht vereinbar ist – ein Experiment, das grundlegend für unser Verständnis der Quantenmechanik ist.
Der Nobelpreis für Physik 2022 wurde an Alain Aspect, John Clauser und Anton Zeilinger verliehen, unter anderem für ihre Pionierarbeit in der Quanteninformationswissenschaft und insbesondere für ihre Experimente mit verschränkten Photonen, die die Verletzung der Bellschen Ungleichungen demonstrierten.
Für dieses Experiment erstellen wir ein verschränktes Paar, bei dem wir jedes Qubit in zwei verschiedenen Basen messen. Wir bezeichnen die Basen für das erste Qubit mit und und die Basen für das zweite Qubit mit und . Dies erlaubt uns, die CHSH-Größe zu berechnen:
Jede Observable ist entweder oder . Klar ist, dass einer der Terme gleich sein muss und der andere sein muss. Daher ist . Der Durchschnittswert von muss die Ungleichung erfüllen:
Die Entwicklung von in Bezug auf , , und ergibt:
Du kannst eine weitere CHSH-Größe definieren:
was zu einer weiteren Ungleichung führt:
Wenn die Quantenmechanik durch lokale Theorien mit verborgenen Variablen beschrieben werden könnte, müssten diese Ungleichungen immer gelten. Wie in diesem Tutorial gezeigt wird, können sie auf einem Quantencomputer verletzt werden – die Quantenmechanik ist daher nicht mit lokalen Theorien mit verborgenen Variablen vereinbar.
Wir erstellen das verschränkte Paar, indem wir den Bell-Zustand erzeugen. Mit dem Estimator-Primitiv erhalten wir die Erwartungswerte und direkt, ohne sie aus rohen Zählergebnissen zu rekonstruieren. Wir messen das zweite Qubit in den - und -Basen. Das erste Qubit wird ebenfalls in orthogonalen Basen gemessen, jedoch mit einem Rotationswinkel , den wir zwischen und variieren. Das Estimator-Primitiv wertet diesen Parameter-Sweep in einem einzigen primitive unified bloc (PUB) aus.
Anforderungen
Stelle vor Beginn dieses Tutorials sicher, dass du Folgendes installiert hast:
- Qiskit SDK v2.0 oder neuer, mit Visualisierung-Unterstützung
- Qiskit Runtime v0.40 oder neuer (
pip install qiskit-ibm-runtime) - Qiskit Aer v0.17 oder neuer (
pip install qiskit-aer)
Einrichtung
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-aer qiskit-ibm-runtime
# 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
'ibm_pittsburgh'
Kleines Simulatorbeispiel
Bevor wir einen Hardware-Job einreichen, validieren wir den gesamten Workflow auf einem lokalen verrauschten Simulator. Wir verwenden AerSimulator.from_backend(backend), um einen Simulator zu erstellen, der das Rauschmodell und die Kopplungskarte des ausgewählten Backends übernimmt, sodass die Simulatorantwort qualitativ dem entspricht, was wir von der Hardware erwarten.
Schritt 1: Klassische Eingaben auf ein Quantenproblem abbilden
Wir schreiben den CHSH-Circuit mit einem einzigen Parameter , der die Messbasis des ersten Qubits variiert. Das Estimator-Primitiv vereinfacht die Analyse: Es liefert Erwartungswerte von Observablen direkt und kann einen parametrisierten Circuit in einem einzigen Aufruf bei vielen Parameterwerten auswerten.
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")
Als Nächstes erstellen wir eine Liste von 21 Phasenwerten von bis , bei denen der parametrisierte Circuit ausgewertet wird (, , , ..., , ).
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]
Schließlich definieren wir die Observablen. Das erste Qubit wird entlang von durch rotierten Achsen gemessen; das zweite Qubit wird in und gemessen. Mit diesen Wahlen entsprechen die vier CHSH-Korrelatoren den Pauli-Operatoren , , und :
# <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)]
)
Schritt 2: Problem für die Ausführung auf Quantenhardware optimieren
V2-Primitive akzeptieren nur Circuits und Observablen, die den vom Zielsystem unterstützten Anweisungen und der Konnektivität entsprechen (Instruction Set Architecture, oder ISA, Circuits und Observablen). Wir erstellen den AerSimulator aus dem Backend und transpilieren gegen das Ziel des Simulators, sodass derselbe Pass-Manager vollständig durchlaufen wird.
# 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")
Wir transformieren außerdem die Observablen, damit sie zum Qubit-Layout des transpilierten Circuits passen, indem wir SparsePauliOp.apply_layout verwenden.
isa_observable1 = observable1.apply_layout(layout=chsh_isa_circuit.layout)
isa_observable2 = observable2.apply_layout(layout=chsh_isa_circuit.layout)
Schritt 3: Ausführen mit Qiskit-Primitiven
Wir führen den Parameter-Sweep mit EstimatorV2 im aer_sim-Modus aus. Die run()-Methode des Estimators nimmt ein Iterable von PUBs entgegen. Jedes PUB hat das Format (circuit, observables, parameter_values, precision). Wir übergeben beide Observablen gemeinsam, damit sie denselben Parameter-Sweep teilen.
# 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()
Schritt 4: Nachbearbeitung und Rückgabe des Ergebnisses im gewünschten klassischen Format
Der Estimator gibt Erwartungswerte für beide Observablen zurück. Wir tragen sie gegen auf, zusammen mit der klassischen Grenze () und der Tsirelson-Grenze (). Die grau schattierten Bereiche markieren den Abstand zwischen beiden. Punkte, die in diese Bänder fallen, verletzen die CHSH-Ungleichung.
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)",
)
Die CHSH-Zeugen des Simulators überschreiten die klassische Grenze von bereits bei mehreren Werten von , auch mit dem Rauschmodell des Backends. Die Spitzen bleiben knapp unterhalb der Tsirelson-Grenze , weil simuliertes Geräterauschen auftritt. Mit dem validierten Workflow gehen wir nun zur echten Hardware über.
Großes Hardware-Beispiel
Ein CHSH-Test ist von Natur aus ein Zwei-Qubit-Experiment und skaliert daher nicht durch einen größeren Circuit. Stattdessen skaliert er durch viele parallele Tests. Hier kacheln wir das Backend mit so vielen disjunkten Bell-Paaren, wie es die Konnektivität erlaubt (ein Matching der Kopplungskarte), und führen auf jedem Paar einen unabhängigen CHSH-Teilcircuit aus – alles in einem einzigen Job.
Dadurch wird CHSH zu einem geräteweiten Benchmark der Verschränkungsqualität: Statt eines einzelnen handverlesenen Paares testen wir die Verschränkung über einen großen Teil des Chips gleichzeitig, unter realistischen Bedingungen, bei denen jedes Paar dem Crosstalk seiner Nachbarn und parallelen Gate-Fehlern ausgesetzt ist. Die gleichzeitige Verletzung der Ungleichung auf jedem Paar bestätigt, dass echte Verschränkung auf dem gesamten Gerät verfügbar ist.
# -------------------------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()
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)

Die schwachen Kurven zeigen die einzelnen Bell-Paare, die fetten Kurven deren Mittelwert über das gesamte Gerät. Jedes Paar folgt derselben Sinuswelle, die von der Quantenmechanik vorhergesagt wird, und die Streuung zwischen den schwachen Kurven spiegelt die Variation im Rauschen von Paar zu Paar wider. Wo immer eine Kurve in die grauen Bänder eintritt, hat sie die klassische Grenze von überschritten, und die ausgegebene Zusammenfassung bestätigt, dass praktisch jedes Paar gleichzeitig die CHSH-Ungleichung verletzt.
Die Spitzen bleiben unterhalb der Tsirelson-Grenze wegen Geräterauschen, aber die Schlussfolgerung ist eindeutig: Das Backend hält echte Verschränkung auf dem gesamten Chip gleichzeitig aufrecht, nicht nur auf einem einzelnen handverlesenen Paar. Das ist der Sinn, in dem das CHSH-Experiment „skaliert": nicht als ein größerer Circuit, sondern als ein paralleler Benchmark, der Verschränkung überall auf einmal bestätigt.
Nächste Schritte
Falls dich diese Arbeit interessiert, könnte dich folgendes Material interessieren:
- Entanglement in action: eine Kurslektion von John Watrous über Bell-Zustände und das CHSH-Spiel.
- Erste Schritte mit dem Estimator-Primitiv: ein Leitfaden über PUBs und Parameter-Sweeps.
- Real-time benchmarking for qubit selection: eine weitere Möglichkeit, die Qubit- und Verschränkungsqualität über ein Gerät zu charakterisieren.
SparsePauliOpAPI-Referenz.