Osservazione di una dinamica adronica non abeliana robusta e coerente su processori quantistici soggetti a rumore
Stima del tempo di esecuzione: 6 minuti su un processore Heron (ibm_boston o equivalente) (NOTA: Si tratta solo di una stima. (La durata potrebbe variare.)
Risultati di apprendimento
Al termine di questo tutorial, avrai appreso quanto segue:
- Come le teorie di gauge su reticoli non abeliani (in particolare SU(2)) possano essere riformulate utilizzando il quadro Loop-String-Hadron (LSH) per una simulazione quantistica efficiente
- Come costruire circuiti di evoluzione temporale di tipo Trotter per un hamiltoniano approssimativo di una teoria di gauge SU(2) e mapparli su qubit
- Come eseguire questi circuiti su un hardware d IBM Quantum® e utilizzando la primitiva Qiskit Estimator con mitigazione degli errori di lettura
Prerequisiti
Si consiglia di approfondire i seguenti argomenti:
- Nozioni di base sui circuiti e sulle porte quantistiche
- Introduzione alla primitiva Estimator di Qiskit
- Conoscenza di base dei concetti della teoria quantistica dei campi (utile ma non indispensabile; la sezione dedicata alle nozioni di base ne illustra gli elementi essenziali)
Sfondo
Motivazione
La cromodinamica quantistica (QCD), la teoria di gauge SU(3) della forza forte, lega i quark in adroni e regola il confinamento e la rottura delle stringhe. I metodi classici della QCD su reticolo eccellono nell'analisi delle proprietà statiche, ma non sono in grado di simulare le dinamiche in tempo reale a causa del problema del segno. I computer quantistici offrono una via per aggirare questa barriera codificando i gradi di libertà dei campi di gauge direttamente sui qubit.
Questo tutorial illustra una simulazione di questo tipo: utilizza l'hardware " IBM Quantum " per simulare la propagazione degli adroni in tempo reale in una teoria di gauge a reticolo SU(2) a (1+1) dimensioni — la teoria di gauge non abeliana più semplice e un primo passo verso la QCD completa.
L'hamiltoniano di Kogut-Susskind
La teoria è formulata su un reticolo spaziale di tipo “ 1D ”, con fermioni sfalsati (materia) sui siti e campi di gauge SU(2) sui legami. Dopo averlo riportato in forma adimensionale, l'hamiltoniano è:
dove rappresenta l'energia del campo cromoelettrico, è il termine di massa sfalsata, è il termine di interazione materia-calibro (hopping), codifica la massa del fermione e è l'intensità di interazione. Il limite al continuo della teoria è descritto all'indirizzo e .
Il framework Loop-String-Hadron (LSH)
Una delle principali difficoltà risiede nel fatto che lo spazio di Hilbert del campo di gauge su ciascun collegamento è di dimensione infinita. Il quadro teorico Loop-String-Hadron (LSH) affronta questo problema riformulando la teoria in termini di variabili invarianti di gauge: loop di flusso, stringhe che collegano cariche separate e adroni (coppie di fermioni singolette di gauge in un sito). Nella base LSH, la legge di Gauss è soddisfatta automaticamente per costruzione, quindi ogni stato di base è fisico. Ogni sito del reticolo è caratterizzato da tre numeri quantici che rappresentano il numero di anello, la stringa in entrata e la stringa in uscita, dove sono fermionici e è bosonico. Da queste espressioni si definisce il numero locale di fermioni come per i siti pari e per i siti dispari.
Dall’Hamiltoniano completo al circuito quantistico: tre approssimazioni fondamentali
Il circuito quantistico non simula esattamente l'Hamiltoniano SU(2) completo. Al contrario, implementa una serie controllata di approssimazioni valide nel regime di accoppiamento debole ( ). È fondamentale comprendere cosa viene approssimato e cosa no:
Approssimazione 1 — Limite di accoppiamento debole per l’ o: l’Hamiltoniano a interazione completa (Eq. 16 in [1] ) contiene prefattori che dipendono dal numero quantico bosonico tramite termini del tipo . Nel regime di accoppiamento debole ( ), la dinamica è dominata dal termine elettrico , che favorisce stati con un valore elevato di . Per , il rapporto e tutti questi prefattori si riducono all’unità. L'Hamiltoniano di interazione si riduce quindi a un salto tra vicini più prossimi di natura puramente locale:
che è indipendente dall’ e e agisce solo sui qubit fermionici .
Approssimazione 2 — Flusso medio globale per : L'energia elettrica dipende da in ciascun collegamento. Nel vuoto a accoppiamento debole, l' e è elevata e approssimativamente uniforme. Sostituire i valori di , che dipendono dal sito, con un unico valore medio globale , rendendo una fase diagonale proporzionale alla configurazione dei fermioni in ciascun sito:
dove si somma su tutti i siti nella configurazione fermionica , e è una fase globale che si può ignorare.
Approssimazione 3 — Trotterizzazione: l'operatore di evoluzione temporale per un passo di durata si scompone come segue:
dove , e . Questa decomposizione di Trotter di primo ordine introduce un errore che si annulla quando . Si fissa per tutto il calcolo.
Il risultato di queste tre approssimazioni è che solo i due qubit fermionici per sito sono dinamici — il grado di libertà bosonico è stato assorbito nei parametri effettivi. Si ottiene così un circuito compatto con qubit per siti del reticolo, in cui ogni passo di Trotter presenta una profondità costante di porte a due qubit (13 per passo).
Cosa simula questo tutorial
Il tutorial simula la propagazione degli adroni : partendo dal vuoto a forte accoppiamento (uno stato di prodotto), si posiziona un mesone al centro del reticolo e si osserva l'evoluzione nel tempo. Il protocollo di misurazione differenziale — che prevede di far funzionare il circuito con e senza il mesone centrale, per poi effettuare la sottrazione — isola il segnale adronico coerente sia dal rumore dell'hardware che dagli effetti di confine. Il risultato è un motivo a cono di luce costituito da oscillazioni della densità dei fermioni, caratteristico di una modalità di respirazione mesonica confinata.
Requisiti
Prima di iniziare questo tutorial, installa quanto segue:
- Qiskit SDK v2.0 o versioni successive, con supporto alla visualizzazione
- Qiskit Runtime v0.22 o versioni successive (
pip install qiskit-ibm-runtime) - Pacchetto Pauli Propagation (
pip install pauli-prop) - NumPy (
pip install numpy) - Matplotlib (
pip install matplotlib)
Configura
Inizia importando le librerie necessarie e definendo le funzioni di supporto che costruiscono i circuiti quantistici per l'evoluzione temporale LSH. Esistono tre funzioni fondamentali per la creazione di circuiti:
-
pair_hamiltonian_circuit: Implementa l' e unitaria a due qubit per l'Hamiltoniano di interazione approssimativo tra siti adiacenti. La scomposizione in porte è la seguente: . -
electric_hamiltonian_circuit: Implementa l' e unitaria a due qubit per l'energia approssimativa del campo elettrico in ciascun sito. La scomposizione in porte è la seguente: . -
construct_circuit: Realizza il circuito "Trotterizzato" completo, sovrapponendo i termini di interazione, elettrici e di massa con porte SWAP per gestire la connettività dei qubit.
# 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 qcdef 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_diffEsempio di simulatore su piccola scala
In primo luogo, illustra il flusso di lavoro su piccola scala utilizzando un reticolo a sei siti (12 qubit), in modo da poter verificare la costruzione del circuito e comprendere le grandezze fisiche osservabili prima di eseguire il codice sull'hardware.
Fase 1: Mappare gli input classici su un problema quantistico
Definire i parametri fisici corrispondenti al regime di accoppiamento debole studiato nell'articolo ( , ). I parametri del circuito ricavati sono:
- (parametro di interazione)
- (fase del campo elettrico)
- (parametro di massa)
Per ogni conteggio di passi di Trotter, si costruiscono due circuiti : uno che inizializza un mesone al centro (inverse_mid=True) e uno che prepara il vuoto a forte accoppiamento (inverse_mid=False). Il protocollo di misurazione differenziale sottrae l'evoluzione del vuoto per isolare il segnale adronico.
# 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}")Output:
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)Output:
Circuit for 1 Trotter step: 12 qubits, depth 26
Fase 2: Ottimizzare il problema per l'esecuzione su hardware quantistico
Definire le grandezze osservabili: misurazioni di tipo “ ” su un singolo qubit per ciascun qubit. Da è possibile ricavare le probabilità di occupazione e quindi il numero di fermioni sfalsato in ciascun sito del reticolo .
# 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)}")Output:
Number of observables: 12
Passaggio 3: Eseguire il comando utilizzando Qiskit primitives
Utilizzare StatevectorEstimator per una simulazione esatta e priva di rumore su piccola scala.
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")Output:
Computed expectation values for 10 Trotter steps
Fase 4: Elaborazione finale e restituzione del risultato nel formato classico desiderato
Convertire i valori attesi nel numero di fermioni sfalsato e applicare il protocollo di misurazione differenziale (mesone vuoto) per generare la mappa termica della propagazione degli adroni. Questo grafico riproduce la struttura della Figura 3 dell'articolo di riferimento: il sito reticolare sull'asse x, il passo di Trotter (tempo) sull'asse y e come scala cromatica.
# 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()Output:
Esempio di hardware su larga scala
Ora passiamo a un reticolo di 30 siti (60 qubit) su un hardware dell IBM Quantum. A questa scala, il circuito a 10 passi di Trotter comprende oltre 3.400 porte a due qubit e 14.000 porte a un qubit.
Passaggi da 1 a 4 (raggruppati in un unico blocco di codice)
Aspetti chiave del flusso di lavoro hardware:
- 10 passi di Trotter per i circuiti del mesone e del vuoto (intercalati per ridurre al minimo la deriva)
- Trasposizione con
optimization_level=1— il layout del circuito è già isomorfo alla topologia del dispositivo (una catena lineare), quindi non sono necessari SWAP di instradamento. Il transpiler viene utilizzato esclusivamente per selezionare una catena di qubit fisici a basso rumore e per scomporre i gate nel set di gate nativo. EstimatorV2con la mitigazione degli errori di lettura TREX e il “Pauli twirling”Batchsessione per inviare tutti i lavori contemporaneamente
# -------------------------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()Output:
Benchmarking classico tramite la propagazione di Pauli
Il metodo di propagazione di Pauli (PPM) fornisce una simulazione classica priva di rumore del circuito quantistico, propagando a ritroso le grandezze osservabili misurate attraverso il circuito nella rappresentazione di Heisenberg. Negli strati di Clifford (porte CNOT, H, S, X), gli operatori di Pauli si mappano su altri operatori di Pauli senza aumentare il numero di termini. Gli strati non-Clifford (le porte " " presenti nel circuito) possono causare ramificazioni — nel peggiore dei casi, raddoppiando il numero di termini — ma molte ramificazioni hanno coefficienti piccoli e possono essere troncate.
Il flusso di lavoro con pauli-prop è il seguente:
- Dividi il circuito nelle sue parti Clifford e non Clifford utilizzando
evolve_through_cliffords. atolPropagare ciascun osservabile attraverso la parte non-Clifford utilizzandopropagate_through_circuit, mantenendo fino amax_termstermini di Pauli ed eliminando i termini con coefficienti inferiori alla soglia di troncamento.- Evolvi il risultato attraverso la parte Clifford utilizzando il supporto integrato di Qiskit per Clifford.
- Si ricava il valore atteso sommando i coefficienti dei termini di Pauli diagonali (che contengono solo e ).
Soglia di troncamento
Il atol parametro in propagate_through_circuit determina l'intensità con cui vengono eliminati i rami di Pauli di piccole dimensioni. Una soglia molto stretta (ad esempio, 1e-12) conserva quasi tutti i rami e fornisce risultati esatti, ma il tempo di simulazione cresce rapidamente con la profondità del circuito; la simulazione a 120 qubit descritta nell ’articolo ha richiesto circa 8.5 ore con le impostazioni predefinite. Alzando la soglia (ad esempio, a 1e-6 o 1e-3) si scartano i termini i cui coefficienti sono inferiori a tale valore, riducendo drasticamente il numero di termini monitorati e velocizzando il calcolo. Il compromesso consiste in un errore di approssimazione minimo e controllabile, che è possibile verificare confrontando i risultati ottenuti con soglie diverse.
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}")Output:
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()Output:
# --- 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()Output:
Passi successivi
Se questo lavoro ti è sembrato interessante, ti invitiamo a dare un'occhiata al seguente materiale:
- Documentazione sulle primitive di Qiskit Estimator — per ulteriori dettagli sulla configurazione delle opzioni di mitigazione degli errori
- Tecniche di mitigazione e soppressione degli errori — per saperne di più su TREX, ZNE e altri metodi di mitigazione
- Qiskit Pauli Propagation (pauli-prop) — Simulazione classica accelerata da Rust tramite retropropagazione di Pauli
Riferimenti
[1] L'articolo originale: Ilčić, Majumdar, Mathew et al. "Osservazione di dinamiche adroniche non abeliane robuste e coerenti su processori quantistici soggetti a rumore" arXiv:2602.18080 (2026)