Observation d'une dynamique hadronique non abélienne robuste et cohérente sur des processeurs quantiques bruités
Utilisation estimée : 6 minutes sur un processeur Heron (ibm_boston ou équivalent) (REMARQUE : il s'agit d'une estimation uniquement. Votre temps d'exécution peut varier.)
Résultats d'apprentissage
-
Comment les théories de jauge sur réseau non abéliennes (en particulier SU(2)) peuvent être reformulées à l'aide du cadre Loop-String-Hadron (LSH) pour une simulation quantique efficace
-
Comment construire des circuits d'évolution temporelle de Trotter pour un hamiltonien de théorie de jauge SU(2) approximatif et les faire correspondre à des qubits
-
Comment exécuter ces circuits sur du matériel IBM Quantum® à l'aide de la primitive Estimator de Qiskit avec atténuation des erreurs de lecture
Prérequis
-
Connaissance de base des concepts de théorie quantique des champs (utile mais non requise ; la section de contexte couvre les éléments essentiels)
Contexte
Motivation
La chromodynamique quantique (QCD), la théorie de jauge SU(3) de la force forte, lie les quarks en hadrons et régit le confinement et la rupture de corde. Les méthodes classiques de QCD sur réseau excellent pour les propriétés statiques mais ne peuvent pas simuler la dynamique en temps réel en raison du problème de signe. Les ordinateurs quantiques offrent un moyen de contourner cette barrière en encodant directement les degrés de liberté des champs de jauge sur des qubits.
Ce tutoriel présente une telle simulation : utiliser du matériel IBM Quantum pour simuler la propagation d'un hadron en temps réel dans une théorie de jauge sur réseau SU(2) à (1+1) dimensions — la théorie de jauge non abélienne la plus simple et un tremplin vers la QCD complète.
Le hamiltonien de Kogut-Susskind
La théorie est formulée sur un réseau spatial 1D avec des fermions décalés (matière) sur les sites et des champs de jauge SU(2) sur les liens. Après remise à l'échelle sous forme adimensionnelle, le hamiltonien est :
où est l'énergie du champ chromoélectrique, est le terme de masse décalé, est le terme d'interaction matière-jauge (saut), encode la masse du fermion, et est l'intensité de l'interaction. La limite continue de la théorie se situe à et .
Le cadre Loop-String-Hadron (LSH)
Un défi clé est que l'espace de Hilbert du champ de jauge sur chaque lien est de dimension infinie. Le cadre Loop-String-Hadron (LSH) résout ce problème en reformulant la théorie en termes de variables invariantes de jauge — des boucles de flux, des cordes reliant des charges séparées, et des hadrons (paires de fermions singulets de jauge sur un site). Dans la base LSH, la loi de Gauss est satisfaite automatiquement par construction, de sorte que chaque état de base est physique. Chaque site du réseau est caractérisé par trois nombres quantiques représentant le nombre de boucles, la corde entrante et la corde sortante, où sont fermioniques et est bosonique. Le nombre fermionique local est défini à partir de ceux-ci comme pour les sites pairs et pour les sites impairs.
Du hamiltonien complet au circuit quantique : trois approximations clés
Le circuit quantique ne simule pas exactement le hamiltonien SU(2) complet. Il met plutôt en œuvre une série contrôlée d'approximations valables dans le régime de couplage faible (). Il est essentiel de comprendre ce qui est approximé et ce qui ne l'est pas :
Approximation 1 — Limite de couplage faible pour : Le hamiltonien d'interaction complet (Éq. 16 dans [1]) contient des préfacteurs qui dépendent du nombre quantique bosonique via des termes comme . Dans le régime de couplage faible (), la dynamique est dominée par le terme électrique , qui favorise les états avec un grand . Pour , le rapport et tous ces préfacteurs se simplifient à l'unité. Le hamiltonien d'interaction se réduit alors à un saut purement local entre plus proches voisins :
qui est indépendant de et n'agit que sur les qubits fermioniques .
Approximation 2 — Flux moyen global pour : L'énergie électrique dépend de sur chaque lien. Dans le vide de couplage faible, est grand et approximativement uniforme. On remplace les valeurs de dépendantes du site par une seule moyenne globale , faisant de une phase diagonale proportionnelle à la configuration fermionique de chaque site :
où somme sur les sites dans la configuration fermionique , et est une phase globale que tu peux ignorer.
Approximation 3 — Trotterisation : L'opérateur d'évolution temporelle pour un pas de durée est décomposé comme :
où , , et . Cette décomposition de Trotter du premier ordre introduit une erreur qui s'annule lorsque . Nous fixons tout au long.
Le résultat de ces trois approximations est que seuls les deux qubits fermioniques par site sont dynamiques — le degré de liberté bosonique a été absorbé dans des paramètres effectifs. Cela donne un circuit compact avec qubits pour sites du réseau, où chaque pas de Trotter a une profondeur de porte à deux qubits constante (13 par pas).
Ce que ce tutoriel simule
Le tutoriel simule la propagation d'un hadron : en partant du vide de couplage fort (un état produit), on place un méson au centre du réseau et on fait évoluer dans le temps. Le protocole de mesure différentielle — exécuter le circuit avec et sans le méson central, puis soustraire — isole le signal hadronique cohérent du bruit matériel et des effets de bord. Le résultat est un motif en cône de lumière d'oscillations de densité fermionique caractéristique d'un mode de respiration de méson confiné.
Prérequis techniques
Avant de commencer ce tutoriel, installe les éléments suivants :
-
Qiskit SDK v2.0 ou une version ultérieure, avec la prise en charge de la visualisation
-
Qiskit Runtime v0.22 ou une version ultérieure (
pip install qiskit-ibm-runtime) -
Le package Pauli Propagation (
pip install pauli-prop) -
NumPy (
pip install numpy) -
Matplotlib (
pip install matplotlib)
Configuration
Commence par importer les bibliothèques nécessaires et définir les fonctions d'aide qui construisent les circuits quantiques pour l'évolution temporelle LSH. Il existe trois fonctions principales de construction de circuits :
-
pair_hamiltonian_circuit: Met en œuvre l'unitaire à deux qubits pour le hamiltonien d'interaction approximatif entre sites voisins. La décomposition en portes est : . -
electric_hamiltonian_circuit: Met en œuvre l'unitaire à deux qubits pour l'énergie du champ électrique approximatif à chaque site. La décomposition en portes est : . -
construct_circuit: Assemble le circuit de Trotter complet, en superposant les termes d'interaction, électrique et de masse avec des portes SWAP pour gérer la connectivité des qubits.
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy pauli-prop qiskit qiskit-ibm-runtime
# Import libraries
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import TwoSlopeNorm
from qiskit.circuit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from typing import Optional
import warnings
warnings.filterwarnings("ignore")
def pair_hamiltonian_circuit(c: float) -> QuantumCircuit:
"""Two-qubit unitary for the approximate interaction Hamiltonian H_I.
Implements exp(-i * c * H_I^approx) for one pair of neighboring sites,
where c = delta_tau * x.
"""
qc_temp = QuantumCircuit(2)
qc_temp.cx(1, 0)
qc_temp.h(1)
qc_temp.rz(-c, 1)
qc_temp.cx(0, 1)
qc_temp.rz(c, 1)
qc_temp.cx(0, 1)
qc_temp.h(1)
qc_temp.cx(1, 0)
return qc_temp
def electric_hamiltonian_circuit(theta: float) -> QuantumCircuit:
"""Two-qubit unitary for the approximate electric field Hamiltonian H_E.
Implements exp(-i * theta * H_E^approx) for one lattice site,
where theta = -delta_tau * (n_bar_l / 2 + 3/4).
"""
qc_temp = QuantumCircuit(2)
qc_temp.x(0)
qc_temp.rz(theta / 2, 0)
qc_temp.cx(0, 1)
qc_temp.rz(-theta / 2, 1)
qc_temp.cx(0, 1)
qc_temp.rz(theta / 2, 1)
qc_temp.x(0)
return qc_temp
def construct_circuit(
num_lattice_point: int,
num_trotter_steps: int,
c: float,
theta: float,
m: float,
theory: Optional[int] = 2,
barriers: Optional[bool] = False,
measurement: Optional[bool] = False,
add_init_state: Optional[bool] = True,
inverse_mid: Optional[bool] = False,
) -> QuantumCircuit:
"""Construct the full Trotterized time-evolution circuit.
Builds a circuit implementing n Trotter steps of the approximate SU(2)
LSH Hamiltonian evolution. The qubit layout uses a zigzag ordering:
n_i(0), n_i(1), n_o(0), n_o(1), n_i(2), n_i(3), n_o(2), n_o(3), ...
which minimizes the number of SWAP layers needed.
Args:
num_lattice_point: Number of lattice sites
(num_qubits = 2 * num_lattice_point).
num_trotter_steps: Number of Trotter steps.
c: Interaction parameter (delta_tau * x).
theta: Electric field phase parameter.
m: Mass parameter (m_tilde = delta_tau * mu).
theory: 1 for single chain, 2 for SU(2). Default 2.
barriers: Insert barriers between Trotter layers for
visualization.
measurement: Append measurements at the end.
add_init_state: Prepare the half-filled (strong-coupling vacuum)
initial state.
inverse_mid: Swap the central sites
(for differential measurement protocol).
"""
num_qubits = theory * num_lattice_point
qc = QuantumCircuit(num_qubits)
if num_trotter_steps <= 0:
return qc
# --- Initial state preparation ---
if add_init_state:
i = 1
while i < num_lattice_point:
for j in range(theory):
qc.x(i + j * num_lattice_point)
i = i + 2
if inverse_mid:
mid_lattice_qubits = [num_qubits // 2 - 1, num_qubits // 2]
qc.x(mid_lattice_qubits)
else:
i = 1
while i < num_qubits - 1:
qc.swap(i, i + 1)
i = i + 4
# --- Trotter steps ---
for step in range(num_trotter_steps):
if barriers:
qc.barrier()
# First SWAP layer (skipped at step 0 — absorbed into initial state mapping)
if step > 0:
i = 1
while i < num_qubits - 1:
qc.swap(i, i + 1)
i = i + 4
# First layer of pair interactions
j = 0
while j < num_qubits - 2:
circ = pair_hamiltonian_circuit(c)
qc.compose(circ, [j, j + 1], inplace=True)
j = j + 2
if num_lattice_point % 2 == 0:
circ = pair_hamiltonian_circuit(c)
qc.compose(circ, [j, j + 1], inplace=True)
# Second SWAP layer
i = 1
while i < num_qubits - 1:
qc.swap(i, i + 1)
i = i + theory
# Second layer of pair interactions
j = 2
while j < num_qubits - 3:
circ = pair_hamiltonian_circuit(c)
qc.compose(circ, [j, j + 1], inplace=True)
j = j + 2
if num_lattice_point % 2 != 0:
circ = pair_hamiltonian_circuit(c)
qc.compose(circ, [j, j + 1], inplace=True)
# Third SWAP layer
i = 3
while i < num_qubits - 1:
qc.swap(i, i + 1)
i = i + 2 * theory
# Electric field term
if theta != 0:
e_circ = electric_hamiltonian_circuit(theta)
for j in range(num_lattice_point):
qc.compose(e_circ, [2 * j, 2 * j + 1], inplace=True)
# Mass term: Rz(-m_tilde) for even sites, Rz(m_tilde) for odd sites
for q in range(num_qubits):
if q % 2 == 0:
qc.rz(-1 * m, q)
else:
qc.rz(m, q)
if measurement:
qc.measure_all()
return qc
def get_probabilities(expval: float):
"""Convert a Z-expectation value to site occupation probability.
Since <Z> = p(0) - p(1), the occupation probability is p(1) = (1 - <Z>) / 2.
"""
p1 = round((1 - expval) / 2, 3)
return p1
def get_number(expval_data, num_lattice_point):
"""Convert raw Z-expectation values to staggered fermion number n_f at each site.
n_f(r) = n_i(r) + n_o(r) for even r
n_f(r) = 2 - [n_i(r) + n_o(r)] for odd r
The two qubits per site encode (n_i, n_o), and occupation probabilities
give us <n_i> and <n_o>.
"""
N = []
for expvals in expval_data:
Pstep = [get_probabilities(expval) for expval in expvals]
Nstep = []
for k in range(num_lattice_point):
val = Pstep[2 * k] + Pstep[2 * k + 1]
a = 2 * (k % 2) + (1 - 2 * (k % 2)) * val
Nstep.append(float(a))
N.append(Nstep)
return N
def calculate_difference(N, N_mid, num_lattice_point):
"""Differential measurement protocol: |n_f(meson) - n_f(vacuum)|.
Subtracting the vacuum (SCV) evolution from the meson evolution
isolates the coherent hadron signal from symmetric noise and boundary effects.
"""
N_diff = []
for i in range(len(N)):
Nstep_diff = []
for j in range(num_lattice_point):
Nstep_diff.append(abs(N[i][j] - N_mid[i][j]))
N_diff.append(Nstep_diff)
return N_diff
Exemple de simulateur à petite échelle
Tout d'abord, montre le flux de travail à petite échelle en utilisant un réseau à six sites (12 qubits), afin de pouvoir vérifier la construction du circuit et comprendre les observables physiques avant d'exécuter sur du matériel.
Étape 1 : Faire correspondre les entrées classiques à un problème quantique
Définis les paramètres physiques correspondant au régime de couplage faible étudié dans l'article (, ). Les paramètres de circuit dérivés sont :
-
(paramètre d'interaction)
-
(phase du champ électrique)
-
(paramètre de masse)
Pour chaque nombre de pas de Trotter, construis deux circuits : l'un initialisant un méson au centre (inverse_mid=True) et l'autre préparant le vide de couplage fort (inverse_mid=False). Le protocole de mesure différentielle soustrait l'évolution du vide pour isoler le signal hadronique.
# Physical / circuit parameters
num_lattice_point = 6 # 6 lattice sites -> 12 qubits for SU(2)
num_qubits = 2 * num_lattice_point
c = 0.15 # delta_tau * x
theta = 0.01 # electric field phase
m = 0.03 # m_tilde = delta_tau * mu
trotter_steps = range(1, 11) # 10 Trotter steps
print(f"Lattice sites: {num_lattice_point}, Qubits: {num_qubits}")
print(f"Parameters: c={c}, theta={theta}, m_tilde={m}")
Lattice sites: 6, Qubits: 12
Parameters: c=0.15, theta=0.01, m_tilde=0.03
# Build circuits: meson initial state and vacuum (SCV) initial state
circuits_mid = [
construct_circuit(
num_lattice_point,
d,
c,
theta,
m,
barriers=False,
measurement=False,
add_init_state=True,
inverse_mid=True,
)
for d in trotter_steps
]
circuits = [
construct_circuit(
num_lattice_point,
d,
c,
theta,
m,
barriers=False,
measurement=False,
add_init_state=True,
inverse_mid=False,
)
for d in trotter_steps
]
# Visualize a single Trotter step
print(
f"Circuit for 1 Trotter step: {circuits[0].num_qubits} qubits, depth {circuits[0].depth()}"
)
circuits[0].draw("mpl", fold=-1)
Circuit for 1 Trotter step: 12 qubits, depth 26

Étape 2 : Optimiser le problème pour l'exécution sur du matériel quantique
Définis les observables : des mesures à un seul qubit sur chaque qubit. À partir de , tu peux extraire les probabilités d'occupation puis le nombre de fermions décalés à chaque site du réseau .
# Z observable on each qubit
observables = [
SparsePauliOp("I" * i + "Z" + "I" * (num_qubits - i - 1))
for i in range(num_qubits)
]
print(f"Number of observables: {len(observables)}")
Number of observables: 12
Étape 3 : Exécuter en utilisant les primitives Qiskit
Utilise StatevectorEstimator pour une simulation exacte sans bruit à petite échelle.
from qiskit.primitives import StatevectorEstimator
estimator = StatevectorEstimator()
# Run meson circuits
pubs_mid = [(circuit, observables) for circuit in circuits_mid]
result_mid = estimator.run(pubs_mid).result()
# Run vacuum (SCV) circuits
pubs = [(circuit, observables) for circuit in circuits]
result = estimator.run(pubs).result()
# Extract expectation values
raw_expvals_mid = [
result_mid[i].data.evs[::-1] for i in range(len(circuits_mid))
]
raw_expvals = [result[i].data.evs[::-1] for i in range(len(circuits))]
print(f"Computed expectation values for {len(raw_expvals)} Trotter steps")
Computed expectation values for 10 Trotter steps
Étape 4 : Post-traiter et retourner le résultat dans le format classique souhaité
Convertis les valeurs moyennes en nombre de fermions décalés et applique le protocole de mesure différentielle (méson vide) pour produire la carte thermique de propagation du hadron. Cela reproduit la structure de la Figure 3 de l'article de référence : le site du réseau sur l'axe des x, le pas de Trotter (temps) sur l'axe des y, et comme échelle de couleur.
# Compute fermion numbers
N_mid_sim = get_number(raw_expvals_mid, num_lattice_point)
N_sim = get_number(raw_expvals, num_lattice_point)
N_diff_sim = calculate_difference(N_mid_sim, N_sim, num_lattice_point)
# --- Reproduce Figure 3 style: Staggered Fermionic Occupation Number Dynamics ---
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
# Convert to numpy arrays for plotting
N_mid_arr = np.array(N_mid_sim)
N_arr = np.array(N_sim)
N_diff_arr = np.array(N_diff_sim)
# Color scheme
vmax = max(max(sublist) for sublist in N_arr)
vmin = -vmax
# Meson evolution
norm1 = TwoSlopeNorm(vmin=vmin, vcenter=0, vmax=vmax)
im0 = axes[0].imshow(
N_mid_arr,
aspect="auto",
origin="lower",
cmap="RdBu_r",
norm=norm1,
extent=[0, num_lattice_point, 0.5, len(trotter_steps) + 0.5],
)
axes[0].set_xlabel("Lattice site $r$", fontsize=12)
axes[0].set_ylabel("Trotter step $t$", fontsize=12)
axes[0].set_title("$n_f(r,t)$ — Meson initial state", fontsize=12)
plt.colorbar(im0, ax=axes[0], label="$n_f(r,t)$")
# Vacuum (SCV) evolution
im1 = axes[1].imshow(
N_arr,
aspect="auto",
origin="lower",
cmap="RdBu_r",
norm=norm1,
extent=[0, num_lattice_point, 0.5, len(trotter_steps) + 0.5],
)
axes[1].set_xlabel("Lattice site $r$", fontsize=12)
axes[1].set_ylabel("Trotter step $t$", fontsize=12)
axes[1].set_title("$n_f(r,t)$ — Vacuum (SCV)", fontsize=12)
plt.colorbar(im1, ax=axes[1], label="$n_f(r,t)$")
# Differential: meson - vacuum
norm2 = TwoSlopeNorm(vmin=vmin, vcenter=0, vmax=vmax)
im2 = axes[2].imshow(
N_diff_arr,
aspect="auto",
origin="lower",
cmap="RdBu_r",
norm=norm2,
extent=[0, num_lattice_point, 0.5, len(trotter_steps) + 0.5],
)
axes[2].set_xlabel("Lattice site $r$", fontsize=12)
axes[2].set_ylabel("Trotter step $t$", fontsize=12)
axes[2].set_title(
"Staggered Fermionic Occupation Number Dynamics\n$|n_f^{\\mathrm{meson}} - n_f^{\\mathrm{vacuum}}|$",
fontsize=12,
)
plt.colorbar(im2, ax=axes[2], label="$n_f(r,t)$")
plt.suptitle(
f"Hadron propagation — {num_lattice_point}-site lattice (StatevectorEstimator)",
fontsize=14,
y=1.02,
)
plt.tight_layout()
plt.show()

Exemple à grande échelle sur du matériel
Nous passons maintenant à un réseau de 30 sites (60 qubits) sur du matériel IBM Quantum. À cette échelle, le circuit à 10 pas de Trotter comprend plus de 3400 portes à deux qubits et 14 000 portes à un qubit.
Étapes 1 à 4 (compressées en un seul bloc de code)
Aspects clés du flux de travail matériel :
-
10 pas de Trotter pour les circuits du méson et du vide (entrelacés pour une dérive minimale)
-
Transpilation avec
optimization_level=1— la disposition du circuit est déjà isomorphe à la topologie de l'appareil (une chaîne linéaire), donc aucun routage par SWAP n'est nécessaire. Le transpileur est utilisé uniquement pour sélectionner une chaîne de qubits physiques à faible bruit et décomposer les portes dans l'ensemble de portes natif. -
EstimatorV2avec atténuation des erreurs de lecture TREX et twirling de Pauli -
Session
Batchpour soumettre tous les jobs ensemble
# -------------------------Step 1: Define parameters & build circuits-------------------------
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import EstimatorV2, Batch
from qiskit_ibm_runtime.options import (
EstimatorOptions,
ResilienceOptionsV2,
TwirlingOptions,
DynamicalDecouplingOptions,
)
service = QiskitRuntimeService()
num_lattice_point_hw = 30
num_qubits_hw = 2 * num_lattice_point_hw # 60 qubits
c_hw = 0.15
theta_hw = 0.01
m_hw = 0.03
trotter_steps_hw = range(1, 11) # 10 Trotter steps
# Build meson and vacuum circuits
circuits_mid_hw = [
construct_circuit(
num_lattice_point_hw,
d,
c_hw,
theta_hw,
m_hw,
barriers=False,
measurement=False,
add_init_state=True,
inverse_mid=True,
)
for d in trotter_steps_hw
]
circuits_hw = [
construct_circuit(
num_lattice_point_hw,
d,
c_hw,
theta_hw,
m_hw,
barriers=False,
measurement=False,
add_init_state=True,
inverse_mid=False,
)
for d in trotter_steps_hw
]
print(f"Built {len(circuits_hw)} circuit pairs for {num_qubits_hw} qubits")
# -------------------------Step 2: Transpile for hardware-------------------------
# The circuit topology is a linear chain, isomorphic to the device topology.
# We use optimization_level=1 since no routing SWAPs are needed — the transpiler
# only needs to select a low-noise qubit chain and decompose to native gates.
backend = service.backend("ibm_boston")
layout = [
140,
141,
142,
143,
136,
123,
122,
121,
116,
101,
102,
103,
96,
83,
82,
81,
76,
61,
62,
63,
64,
65,
66,
67,
68,
69,
78,
89,
88,
87,
97,
107,
106,
105,
117,
125,
126,
127,
137,
147,
148,
149,
150,
151,
152,
153,
154,
155,
139,
135,
134,
133,
132,
131,
130,
129,
118,
109,
110,
111,
]
pm = generate_preset_pass_manager(
optimization_level=1, backend=backend, initial_layout=layout
)
isa_circuits_mid = pm.run(circuits_mid_hw)
isa_circuits = pm.run(circuits_hw)
print(f"Transpiled circuits. Example depth: {isa_circuits[0].depth()}")
# Define and layout-map observables
observables_hw = [
SparsePauliOp("I" * i + "Z" + "I" * (num_qubits_hw - i - 1))
for i in range(num_qubits_hw)
]
isa_observables_mid = [
[obs.apply_layout(isa_circuits_mid[i].layout) for obs in observables_hw]
for i in range(len(isa_circuits_mid))
]
isa_observables = [
[obs.apply_layout(isa_circuits[i].layout) for obs in observables_hw]
for i in range(len(isa_circuits))
]
# Build PUBs — interleave meson and vacuum for each Trotter step
isa_pubs_mid = [
(circ, obs) for circ, obs in zip(isa_circuits_mid, isa_observables_mid)
]
isa_pubs = [(circ, obs) for circ, obs in zip(isa_circuits, isa_observables)]
pubs_to_execute = [
[isa_pubs_mid[i], isa_pubs[i]] for i in range(len(isa_pubs))
]
# -------------------------Step 3: Execute on hardware-------------------------
twirling_options = TwirlingOptions(
enable_gates=True,
enable_measure=True,
shots_per_randomization="auto",
strategy="active-circuit",
)
resilience_options = ResilienceOptionsV2(
measure_mitigation=True, # TREX readout error mitigation
zne_mitigation=False, # ZNE turned off
)
dd_options = DynamicalDecouplingOptions(
enable=False # Circuit is sufficiently dense
)
options = EstimatorOptions(
resilience=resilience_options,
twirling=twirling_options,
dynamical_decoupling=dd_options,
default_shots=10_000,
)
ids = []
with Batch(backend=backend) as batch:
for idx, pub in enumerate(pubs_to_execute):
print(f"Submitting job for Trotter step {idx + 1}")
estimator = EstimatorV2(mode=batch, options=options)
estimator.skip_transpilation = True
job = estimator.run(pub)
ids.append(job.job_id())
batch_id = batch.session_id
job_info = {"ids": ids, "batch_id": batch_id}
print(f"Submitted {len(ids)} jobs. Batch ID: {batch_id}")
print(ids)
# -------------------------Step 4: Post-process results-------------------------
jobs = [service.job(job_id) for job_id in ids]
results = [job.result() for job in jobs]
# Extract expectation values (index 0 = meson, index 1 = vacuum)
raw_expvals_mid_hw = [result[0].data.evs[::-1] for result in results]
raw_expvals_hw = [result[1].data.evs[::-1] for result in results]
# Compute fermion numbers and differential
N_mid_hw = get_number(raw_expvals_mid_hw, num_lattice_point_hw)
N_hw = get_number(raw_expvals_hw, num_lattice_point_hw)
N_diff_hw = calculate_difference(N_mid_hw, N_hw, num_lattice_point_hw)
N_diff_hw_arr = np.array(N_diff_hw)
fig, ax = plt.subplots(figsize=(10, 6))
vmax = np.abs(N_hw).max()
vmin = -vmax
norm = TwoSlopeNorm(vmin=vmin, vcenter=0, vmax=vmax)
im = ax.imshow(
N_diff_hw_arr,
aspect="auto",
origin="lower",
cmap="RdBu_r",
norm=norm,
extent=[0, 8, 0.5, len(trotter_steps_hw) + 0.5],
)
ax.set_xlabel("Lattice site $r$", fontsize=13)
ax.set_ylabel("Trotter step $t$", fontsize=13)
ax.set_title(
"Staggered Fermionic Occupation Number Dynamics\nQuantum Simulation on IBM Hardware — 30-site lattice (60 qubits)",
fontsize=13,
)
cbar = plt.colorbar(im, ax=ax)
cbar.set_label("$n_f(r,t)$", fontsize=12)
plt.tight_layout()
plt.show()
Analyse comparative classique via Pauli Propagation
La méthode de propagation de Pauli (PPM) fournit une simulation classique sans bruit du circuit quantique en rétropropageant les observables mesurées à travers le circuit dans l'image de Heisenberg. Sous les couches de Clifford (portes CNOT, H, S, X), les opérateurs de Pauli se transforment en d'autres opérateurs de Pauli sans augmenter le nombre de termes. Les couches non-Clifford (les portes dans le circuit) peuvent provoquer une ramification — dans le pire des cas, doublant le nombre de termes — mais de nombreuses branches ont de petits coefficients et peuvent être tronquées.
Le flux de travail avec pauli-prop est le suivant :
-
Diviser le circuit en ses parties Clifford et non-Clifford à l'aide de
evolve_through_cliffords. -
Propager chaque observable à travers la partie non-Clifford à l'aide de
propagate_through_circuit, en conservant jusqu'àmax_termstermes de Pauli et en supprimant les termes dont les coefficients sont inférieurs au seuil de troncatureatol. -
Faire évoluer le résultat à travers la partie Clifford en utilisant la prise en charge intégrée de Clifford de Qiskit.
-
Extraire la valeur moyenne en additionnant les coefficients des termes de Pauli diagonaux (contenant uniquement et ).
Seuil de troncature
Le paramètre atol dans propagate_through_circuit contrôle avec quelle agressivité les petites branches de Pauli sont élaguées. Un seuil très strict (par exemple, 1e-12) conserve presque toutes les branches et donne des résultats exacts, mais le temps de simulation augmente fortement avec la profondeur du circuit ; la simulation à 120 qubits de l'article a pris environ 8,5 heures avec les paramètres par défaut. Augmenter le seuil (par exemple, à 1e-6 ou 1e-3) élimine les termes dont les coefficients sont inférieurs à cette valeur, réduisant considérablement le nombre de termes suivis et accélérant le calcul. Le compromis est une erreur d'approximation faible et contrôlable que tu peux valider en comparant les résultats à différents seuils.
import time
from pauli_prop import evolve_through_cliffords, propagate_through_circuit
# ── PPM Configuration ──
# Truncation threshold: controls the speed/accuracy trade-off.
PPM_THRESHOLD = 1e-3
# Maximum Pauli terms to track per observable (hard cap on memory/time)
PPM_MAX_TERMS = 66_000
print(f"PPM settings: atol={PPM_THRESHOLD}, max_terms={PPM_MAX_TERMS}")
# We propagate each single-qubit Z observable through each circuit.
# For PPM, we work with the un-transpiled circuits (ideal noiseless simulation).
observables_pp = [
SparsePauliOp("I" * i + "Z" + "I" * (num_qubits_hw - i - 1))
for i in range(num_qubits_hw)
]
def ppm_expectation_values(
circuit, observables, max_terms=PPM_MAX_TERMS, atol=PPM_THRESHOLD
):
"""Compute expectation values of single-qubit Z observables
via Pauli propagation.
Args:
circuit: The quantum circuit to simulate.
observables: List of single-qubit Z observables.
max_terms: Maximum number of Pauli terms to retain (hard cap).
atol: Absolute tolerance — Pauli terms with coefficients below this
value are discarded during propagation. Larger values give
faster simulation at the cost of approximation accuracy.
"""
circuit = circuit.decompose(["swap"]) # decompose SWAPs into 3 CX gates
cliff, non_cliff = evolve_through_cliffords(circuit)
evs = []
for obs in observables:
evolved_obs = propagate_through_circuit(
obs, non_cliff, max_terms=max_terms, atol=atol, frame="h"
)[0]
evolved_obs.paulis = evolved_obs.paulis.evolve(cliff, frame="h")
diagonal_mask = ~evolved_obs.paulis.x.any(axis=1)
ev = float(evolved_obs.coeffs[diagonal_mask].sum().real)
evs.append(ev)
return np.array(evs)
# Run PPM for each Trotter step and record wall-clock time
pp_expvals_mid = []
pp_expvals = []
pp_times = []
for idx, d in enumerate(trotter_steps_hw):
t_start = time.perf_counter()
# Meson circuit
evs_mid = ppm_expectation_values(circuits_mid_hw[idx], observables_pp)
# Vacuum circuit
evs_vac = ppm_expectation_values(circuits_hw[idx], observables_pp)
elapsed = time.perf_counter() - t_start
pp_times.append(elapsed)
pp_expvals_mid.append(evs_mid[::-1])
pp_expvals.append(evs_vac[::-1])
print(f"Trotter step {d:2d}: {elapsed:.1f} s")
print(f"\nTotal PPM simulation time: {sum(pp_times):.1f} s")
print(f"Truncation threshold used: {PPM_THRESHOLD}")
PPM settings: atol=0.001, max_terms=66000
Trotter step 1: 5.0 s
Trotter step 2: 7.5 s
Trotter step 3: 11.2 s
Trotter step 4: 14.7 s
Trotter step 5: 18.3 s
Trotter step 6: 22.1 s
Trotter step 7: 25.6 s
Trotter step 8: 29.4 s
Trotter step 9: 33.2 s
Trotter step 10: 36.6 s
Total PPM simulation time: 203.6 s
Truncation threshold used: 0.001
# --- PPM simulation time vs. Trotter steps ---
fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(
list(trotter_steps_hw),
pp_times,
"o-",
color="tab:blue",
linewidth=2,
markersize=6,
)
ax.set_xlabel("Trotter step", fontsize=13)
ax.set_ylabel("Wall-clock time (s)", fontsize=13)
ax.set_title(
"Pauli Propagation simulation time vs. Trotter steps\n(30-site lattice, 60 qubits)",
fontsize=13,
)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# --- PPM heatmap and comparison with hardware ---
N_mid_pp = get_number(pp_expvals_mid, num_lattice_point_hw)
N_pp = get_number(pp_expvals, num_lattice_point_hw)
N_diff_pp = calculate_difference(N_mid_pp, N_pp, num_lattice_point_hw)
N_diff_pp_arr = np.array(N_diff_pp)
fig, axes = plt.subplots(1, 2, figsize=(18, 6))
vmax = np.abs(N_hw).max()
vmin = -vmax
norm = TwoSlopeNorm(vmin=vmin, vcenter=0, vmax=vmax)
# PPM result
im0 = axes[0].imshow(
N_diff_pp_arr,
aspect="auto",
origin="lower",
cmap="RdBu_r",
norm=norm,
extent=[0, num_lattice_point_hw, 0.5, len(trotter_steps_hw) + 0.5],
)
axes[0].set_xlabel("Lattice site $r$", fontsize=12)
axes[0].set_ylabel("Trotter step $t$", fontsize=12)
axes[0].set_title(
"Pauli Propagation\n(classical noiseless simulation)", fontsize=12
)
plt.colorbar(im0, ax=axes[0], label="$n_f(r,t)$")
# Hardware result
im1 = axes[1].imshow(
N_diff_hw_arr,
aspect="auto",
origin="lower",
cmap="RdBu_r",
norm=norm,
extent=[0, num_lattice_point_hw, 0.5, len(trotter_steps_hw) + 0.5],
)
axes[1].set_xlabel("Lattice site $r$", fontsize=12)
axes[1].set_ylabel("Trotter step $t$", fontsize=12)
axes[1].set_title(
"Quantum Simulation\n(IBM Hardware, readout error mitigation only)",
fontsize=12,
)
plt.colorbar(im1, ax=axes[1], label="$n_f(r,t)$")
plt.suptitle(
"Staggered Fermionic Occupation Number Dynamics — 30-site lattice",
fontsize=14,
y=1.02,
)
plt.tight_layout()
plt.show()

Étapes suivantes
Si tu as trouvé ce travail intéressant, envisage d'explorer les documents suivants :
-
Documentation de la primitive Estimator de Qiskit — pour plus de détails sur la configuration des options d'atténuation des erreurs
-
Techniques d'atténuation et de suppression des erreurs — pour en savoir plus sur TREX, ZNE et d'autres méthodes d'atténuation
-
Qiskit Pauli Propagation (pauli-prop) — simulation classique accélérée par Rust via la rétropropagation de Pauli
Références
[1] L'article original : Ilčić, Majumdar, Mathew et al. "Observation of Robust and Coherent Non-Abelian Hadron Dynamics on Noisy Quantum Processors" arXiv:2602.18080 (2026)