Inégalité CHSH
Estimation d'utilisation : deux minutes sur un processeur Heron r3 (REMARQUE : il s'agit uniquement d'une estimation. Ton temps d'exécution peut varier.)
Objectifs d'apprentissage
Après avoir suivi ce tutoriel, tu pourras comprendre les informations suivantes :
- Comment construire un Circuit CHSH à état de Bell paramétré et mesurer les quatre valeurs d'espérance qui constituent les témoins CHSH.
- Comment calculer les valeurs d'espérance de plusieurs observables sur un balayage de paramètres en un seul appel à la primitive
EstimatorV2. - Comment valider un flux de travail quantique sur un simulateur local bruité avec
AerSimulator.from_backendavant de soumettre à du matériel réel. - Comment transformer une expérience CHSH en un benchmark d'intrication à l'échelle d'un dispositif en exécutant de nombreuses paires de Bell indépendantes en parallèle sur du matériel IBM Quantum®.
Prérequis
Il est recommandé de te familiariser avec ces sujets :
- L'intrication en action, une leçon de cours sur les états de Bell et le jeu CHSH.
SparsePauliOpet l'introduction aux primitives Qiskit.
Contexte
Dans ce tutoriel, tu vas exécuter une expérience sur un ordinateur quantique pour démontrer la violation de l'inégalité CHSH avec la primitive Estimator.
L'inégalité CHSH, nommée d'après ses auteurs Clauser, Horne, Shimony et Holt, est utilisée pour tester expérimentalement le théorème de Bell (1969). Ce théorème affirme que les théories à variables cachées locales ne peuvent pas rendre compte de certaines conséquences de l'intrication en mécanique quantique. Démontrer une violation de l'inégalité CHSH montre que la mécanique quantique est incompatible avec les théories à variables cachées locales, une expérience qui est fondamentale pour notre compréhension de la mécanique quantique.
Le prix Nobel de physique 2022 a été décerné à Alain Aspect, John Clauser et Anton Zeilinger en partie pour leurs travaux pionniers en science de l'information quantique, et en particulier pour leurs expériences avec des photons intriqués démontrant la violation des inégalités de Bell.
Pour cette expérience, nous allons créer une paire intriquée sur laquelle nous mesurerons chaque qubit dans deux bases différentes. Nous désignerons les bases du premier qubit par et , et les bases du second qubit par et . Cela nous permet de calculer la quantité CHSH :
Chaque observable vaut soit , soit . De toute évidence, l'un des termes doit être , et l'autre doit être . Par conséquent, . La valeur moyenne de doit satisfaire l'inégalité :
En développant en fonction de , , et , on obtient :
Tu peux définir une autre quantité CHSH :
ce qui conduit à une autre inégalité :
Si la mécanique quantique pouvait être décrite par des théories à variables cachées locales, ces inégalités seraient toujours vérifiées. Comme le démontre ce tutoriel, elles peuvent être violées sur un ordinateur quantique, ce qui montre que la mécanique quantique n'est pas compatible avec les théories à variables cachées locales.
Nous créons la paire intriquée en préparant l'état de Bell . En utilisant la primitive Estimator, nous obtenons directement les valeurs d'espérance et , sans les reconstruire à partir de comptages bruts. Nous mesurons le second qubit dans les bases et . Le premier qubit est également mesuré dans des bases orthogonales, mais avec un angle de rotation que nous faisons varier entre et . La primitive Estimator évalue ce balayage de paramètres en un seul bloc unifié de primitive (PUB).
Configuration requise
Avant de commencer ce tutoriel, assure-toi que les éléments suivants sont installés :
- Qiskit SDK v2.0 ou version ultérieure, avec le support de visualisation
- Qiskit Runtime v0.40 ou version ultérieure (
pip install qiskit-ibm-runtime) - Qiskit Aer v0.17 ou version ultérieure (
pip install qiskit-aer)
Configuration
# 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'
Exemple à petite échelle avec simulateur
Avant de soumettre une tâche sur du matériel réel, nous validons l'ensemble du flux de travail sur un simulateur local bruité. Nous utilisons AerSimulator.from_backend(backend) pour construire un simulateur qui hérite du modèle de bruit et de la carte de connectivité du backend sélectionné, de sorte que la réponse du simulateur soit qualitativement similaire à ce que l'on attend du matériel réel.
Étape 1 : Formuler les entrées classiques en un problème quantique
Nous écrivons le Circuit CHSH avec un seul paramètre , qui balaye la base de mesure du premier qubit. La primitive Estimator simplifie l'analyse : elle retourne directement les valeurs d'espérance des observables, et elle peut évaluer un circuit paramétré pour de nombreuses valeurs de paramètres en un seul appel.
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")
Ensuite, nous créons une liste de 21 valeurs de phase allant de à pour évaluer le circuit paramétré (, , , ..., , ).
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]
Enfin, nous définissons les observables. Le premier qubit est mesuré le long d'axes tournés de ; le second qubit est mesuré dans les bases et . Avec ces choix, les quatre corrélateurs CHSH correspondent aux opérateurs de Pauli , , et :
# <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)]
)
Étape 2 : Optimiser le problème pour l'exécution sur du matériel quantique
Les primitives V2 n'acceptent que des circuits et des observables conformes aux instructions et à la connectivité prises en charge par le système cible (circuits et observables ISA, pour Instruction Set Architecture). Nous construisons l'AerSimulator à partir du backend et transpilons par rapport à la cible du simulateur afin que le même gestionnaire de passes soit exercé de bout en bout.
# 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")
Nous transformons également les observables pour les faire correspondre à la disposition des qubits du circuit transpilé à l'aide de SparsePauliOp.apply_layout.
isa_observable1 = observable1.apply_layout(layout=chsh_isa_circuit.layout)
isa_observable2 = observable2.apply_layout(layout=chsh_isa_circuit.layout)
Étape 3 : Exécuter à l'aide des primitives Qiskit
On exécute le balayage de paramètres avec EstimatorV2 en mode aer_sim. La méthode run() de l'Estimator prend un itérable de PUBs. Chaque PUB a le format (circuit, observables, parameter_values, precision). Nous passons les deux observables ensemble afin qu'ils partagent le même balayage de paramètres.
# 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()
Étape 4 : Post-traiter et renvoyer le résultat dans le format classique souhaité
L'Estimator retourne les valeurs d'espérance pour les deux observables. Nous les traçons en fonction de avec la borne classique () et la borne de Tsirelson (). Les zones grises indiquent l'écart entre les deux. Les points qui se trouvent dans ces bandes violent l'inégalité CHSH.
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)",
)
Les témoins CHSH du simulateur dépassent déjà la borne classique de pour plusieurs valeurs de , même avec le modèle de bruit du backend. Les pics sont légèrement en deçà de la borne de Tsirelson en raison du bruit simulé du dispositif. Le flux de travail étant validé, nous passons au matériel réel.
Exemple à grande échelle sur matériel réel
Un test CHSH est intrinsèquement une expérience à deux qubits, donc il ne s'agrandit pas en faisant un Circuit plus grand. Il s'agrandit plutôt en exécutant de nombreux tests en parallèle. Ici, nous pavons le backend avec autant de paires de Bell disjointes que sa connectivité le permet (un couplage de la carte de connectivité) et nous exécutons un sous-circuit CHSH indépendant sur chaque paire, le tout en une seule tâche.
Cela transforme CHSH en un benchmark d'intrication à l'échelle du dispositif : plutôt qu'une seule paire choisie à la main, nous testons l'intrication sur une grande fraction du chip à la fois, dans des conditions réalistes où chaque paire est soumise à la diaphonie de ses voisines et aux erreurs de portes parallèles. Violer l'inégalité sur chaque paire simultanément certifie qu'une véritable intrication est disponible partout sur le dispositif.
# -------------------------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)

Les courbes en traits fins sont les paires de Bell individuelles et les courbes en gras représentent leur moyenne sur l'ensemble du dispositif. Chaque paire trace le même sinusoïde prédit par la mécanique quantique, et l'écart entre les courbes fines reflète la variation du bruit d'une paire à l'autre. Partout où une courbe entre dans les bandes grises, elle a franchi la borne classique de , et le résumé affiché confirme que pratiquement chaque paire viole l'inégalité CHSH en même temps.
Les pics sont en deçà de la borne de Tsirelson en raison du bruit du dispositif, mais la conclusion est sans ambiguïté : le backend maintient une véritable intrication sur l'ensemble du chip simultanément, et pas seulement sur une seule paire choisie à la main. C'est en ce sens que l'expérience CHSH « s'agrandit » : non pas comme un seul Circuit plus grand, mais comme un benchmark parallèle qui certifie l'intrication partout à la fois.
Prochaines étapes
Si tu as trouvé ce travail intéressant, tu pourrais être intéressé par les ressources suivantes :
- L'intrication en action : une leçon de cours par John Watrous sur les états de Bell et le jeu CHSH.
- Débuter avec la primitive Estimator : un guide sur les PUBs et les balayages de paramètres.
- Benchmarking en temps réel pour la sélection de qubits : une autre façon de caractériser la qualité des qubits et de l'intrication sur un dispositif.
- Référence API
SparsePauliOp.