Modello di Ising a Campo Trasverso con il Performance Management di Q-CTRL
Stima d'uso: 2 minuti su un processore Heron r2. (NOTA: Questa è solo una stima. Il vostro tempo di esecuzione può variare.)
Contesto​
Il Modello di Ising a Campo Trasverso (TFIM) è importante per lo studio del magnetismo quantistico e delle transizioni di fase. Descrive un insieme di spin disposti su un reticolo, dove ogni spin interagisce con i suoi vicini ed è anche influenzato da un campo magnetico esterno che guida le fluttuazioni quantistiche.
Un approccio comune per simulare questo modello consiste nell'utilizzare la decomposizione di Trotter per approssimare l'operatore di evoluzione temporale, costruendo circuiti che alternano rotazioni a singolo qubit e interazioni a due qubit entangling. Tuttavia, questa simulazione su hardware reale è impegnativa a causa del rumore e della decoerenza, che portano a deviazioni dalla dinamica reale. Per superare questo problema, utilizziamo gli strumenti di soppressione degli errori e gestione delle prestazioni Fire Opal di Q-CTRL, offerti come Qiskit Function (vedere la documentazione di Fire Opal). Fire Opal ottimizza automaticamente l'esecuzione del circuito applicando disaccoppiamento dinamico, layout avanzato, routing e altre tecniche di soppressione degli errori, tutte finalizzate alla riduzione del rumore. Con questi miglioramenti, i risultati hardware si allineano più strettamente alle simulazioni senza rumore, e quindi possiamo studiare la dinamica di magnetizzazione del TFIM con maggiore fedeltà .
In questo tutorial:
- Costruiremo l'Hamiltoniana TFIM su un grafo di triangoli di spin connessi
- Simuleremo l'evoluzione temporale con circuiti trotterizzati a diverse profonditÃ
- Calcoleremo e visualizzeremo le magnetizzazioni a singolo qubit nel tempo
- Confronteremo simulazioni di base con risultati da esecuzioni hardware utilizzando il performance management Fire Opal di Q-CTRL
Panoramica​
Il Modello di Ising a Campo Trasverso (TFIM) è un modello di spin quantistico che cattura caratteristiche essenziali delle transizioni di fase quantistiche. L'Hamiltoniana è definita come:
dove e sono operatori di Pauli che agiscono sul qubit , è la forza di accoppiamento tra spin vicini, e è l'intensità del campo magnetico trasverso. Il primo termine rappresenta le interazioni ferromagnetiche classiche, mentre il secondo introduce fluttuazioni quantistiche attraverso il campo trasverso. Per simulare la dinamica del TFIM, si utilizza una decomposizione di Trotter dell'operatore di evoluzione unitaria , implementato attraverso strati di porte RX e RZZ basati su un grafo personalizzato di triangoli di spin connessi. La simulazione esplora come la magnetizzazione evolve con l'aumentare dei passi di Trotter.
Le prestazioni dell'implementazione TFIM proposta vengono valutate confrontando simulazioni senza rumore con backend rumorosi. Le funzionalità di esecuzione migliorata e soppressione degli errori di Fire Opal vengono utilizzate per mitigare l'effetto del rumore nell'hardware reale, ottenendo stime più affidabili degli osservabili di spin come e correlatori .
Requisiti​
Prima di iniziare questo tutorial, assicuratevi di avere installato quanto segue:
- Qiskit SDK v1.4 o successivo, con supporto per la visualizzazione
- Qiskit Runtime v0.40 o successivo (
pip install qiskit-ibm-runtime) - Qiskit Functions Catalog v0.9.0 (
pip install qiskit-ibm-catalog) - Fire Opal SDK v9.0.2 o successivo (
pip install fire-opal) - Q-CTRL Visualizer v8.0.2 o successivo (
pip install qctrl-visualizer)
Configurazione​
Innanzitutto, autenticatevi utilizzando la vostra chiave API IBM Quantum. Quindi, selezionate la Qiskit Function come segue. (Questo codice presuppone che abbiate già salvato il vostro account nel vostro ambiente locale.)
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib networkx numpy qctrlvisualizer qiskit qiskit-aer qiskit-ibm-catalog qiskit-ibm-runtime
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit import QuantumCircuit
from qiskit_ibm_catalog import QiskitFunctionsCatalog
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import SamplerV2 as Sampler
from qiskit.quantum_info import SparsePauliOp
from qiskit_aer import AerSimulator
import numpy as np
import networkx as nx
import matplotlib.pyplot as plt
import qctrlvisualizer as qv
catalog = QiskitFunctionsCatalog(channel="ibm_quantum_platform")
# Access Function
perf_mgmt = catalog.load("q-ctrl/performance-management")
Passo 1: Mappare gli input classici a un problema quantistico​
Generare il grafo TFIM​
Iniziamo definendo il reticolo di spin e gli accoppiamenti tra di essi. In questo tutorial, il reticolo è costruito da triangoli connessi disposti in una catena lineare. Ogni triangolo è costituito da tre nodi connessi in un anello chiuso, e la catena è formata collegando un nodo di ogni triangolo al triangolo precedente.
La funzione helper connected_triangles_adj_matrix costruisce la matrice di adiacenza per questa struttura. Per una catena di triangoli, il grafo risultante contiene nodi.
def connected_triangles_adj_matrix(n):
"""
Generate the adjacency matrix for 'n' connected triangles in a chain.
"""
num_nodes = 2 * n + 1
adj_matrix = np.zeros((num_nodes, num_nodes), dtype=int)
for i in range(n):
a, b, c = i * 2, i * 2 + 1, i * 2 + 2 # Nodes of the current triangle
# Connect the three nodes in a triangle
adj_matrix[a, b] = adj_matrix[b, a] = 1
adj_matrix[b, c] = adj_matrix[c, b] = 1
adj_matrix[a, c] = adj_matrix[c, a] = 1
# If not the first triangle, connect to the previous triangle
if i > 0:
adj_matrix[a, a - 1] = adj_matrix[a - 1, a] = 1
return adj_matrix
Per visualizzare il reticolo che abbiamo appena definito, possiamo tracciare la catena di triangoli connessi e etichettare ogni nodo. La funzione seguente costruisce il grafo per un numero scelto di triangoli e lo visualizza.
def plot_triangle_chain(n, side=1.0):
"""
Plot a horizontal chain of n equilateral triangles.
Baseline: even nodes (0,2,4,...,2n) on y=0
Apexes: odd nodes (1,3,5,...,2n-1) above the midpoint.
"""
# Build graph
A = connected_triangles_adj_matrix(n)
G = nx.from_numpy_array(A)
h = np.sqrt(3) / 2 * side
pos = {}
# Place baseline nodes
for k in range(n + 1):
pos[2 * k] = (k * side, 0.0)
# Place apex nodes
for k in range(n):
x_left = pos[2 * k][0]
x_right = pos[2 * k + 2][0]
pos[2 * k + 1] = ((x_left + x_right) / 2, h)
# Draw
fig, ax = plt.subplots(figsize=(1.5 * n, 2.5))
nx.draw(
G,
pos,
ax=ax,
with_labels=True,
font_size=10,
font_color="white",
node_size=600,
node_color=qv.QCTRL_STYLE_COLORS[0],
edge_color="black",
width=2,
)
ax.set_aspect("equal")
ax.margins(0.2)
plt.show()
return G, pos
Per questo tutorial utilizzeremo una catena di 20 triangoli.
n_triangles = 20
n_qubits = 2 * n_triangles + 1
plot_triangle_chain(n_triangles, side=1.0)
plt.show()

Colorazione dei bordi del grafo​
Per implementare l'accoppiamento spin-spin, è utile raggruppare i bordi che non si sovrappongono. Questo ci consente di applicare porte a due qubit in parallelo. Possiamo farlo con una semplice procedura di colorazione dei bordi [1], che assegna un colore ad ogni bordo in modo che i bordi che si incontrano nello stesso nodo siano collocati in gruppi diversi.
def edge_coloring(graph):
"""
Takes a NetworkX graph and returns a list of lists where each inner list contains
the edges assigned the same color.
"""
line_graph = nx.line_graph(graph)
edge_colors = nx.coloring.greedy_color(line_graph)
color_groups = {}
for edge, color in edge_colors.items():
if color not in color_groups:
color_groups[color] = []
color_groups[color].append(edge)
return list(color_groups.values())
Passo 2: Ottimizzare il problema per l'esecuzione su hardware quantistico​
Generare circuiti trotterizzati su grafi di spin​
Per simulare la dinamica del TFIM, costruiamo circuiti che approssimano l'operatore di evoluzione temporale.
Utilizziamo una decomposizione di Trotter del secondo ordine:
dove e .
- Il termine è implementato con strati di rotazioni
RX. - Il termine è implementato con strati di porte
RZZlungo i bordi del grafo di interazione.
Gli angoli di queste porte sono determinati dal campo trasverso , dalla costante di accoppiamento e dal passo temporale . Sovrapponendo più passi di Trotter, generiamo circuiti di profondità crescente che approssimano la dinamica del sistema. Le funzioni generate_tfim_circ_custom_graph e trotter_circuits costruiscono un circuito quantistico trotterizzato da un grafo arbitrario di interazione di spin.
def generate_tfim_circ_custom_graph(
steps, h, J, dt, psi0, graph: nx.graph.Graph, meas_basis="Z", mirror=False
):
"""
Generate a second order trotter of the form e^(a+b) ~ e^(b/2) e^a e^(b/2) for simulating a transverse field ising model:
e^{-i H t} where the Hamiltonian H = -J \\sum_i Z_i Z_{i+1} + h \\sum_i X_i.
steps: Number of trotter steps
theta_x: Angle for layer of X rotations
theta_zz: Angle for layer of ZZ rotations
theta_x: Angle for second layer of X rotations
J: Coupling between nearest neighbor spins
h: The transverse magnetic field strength
dt: t/total_steps
psi0: initial state (assumed to be prepared in the computational basis).
meas_basis: basis to measure all correlators in
This is a second order trotter of the form e^(a+b) ~ e^(b/2) e^a e^(b/2)
"""
theta_x = h * dt
theta_zz = -2 * J * dt
nq = graph.number_of_nodes()
color_edges = edge_coloring(graph)
circ = QuantumCircuit(nq, nq)
# Initial state, for typical cases in the computational basis
for i, b in enumerate(psi0):
if b == "1":
circ.x(i)
# Trotter steps
for step in range(steps):
for i in range(nq):
circ.rx(theta_x, i)
if mirror:
color_edges = [sublist[::-1] for sublist in color_edges[::-1]]
for edge_list in color_edges:
for edge in edge_list:
circ.rzz(theta_zz, edge[0], edge[1])
for i in range(nq):
circ.rx(theta_x, i)
# some typically used basis rotations
if meas_basis == "X":
for b in range(nq):
circ.h(b)
elif meas_basis == "Y":
for b in range(nq):
circ.sdg(b)
circ.h(b)
for i in range(nq):
circ.measure(i, i)
return circ
def trotter_circuits(G, d_ind_tot, J, h, dt, meas_basis, mirror=True):
"""
Generates a sequence of Trotterized circuits, each with increasing depth.
Given a spin interaction graph and Hamiltonian parameters, it constructs
a list of circuits with 1 to d_ind_tot Trotter steps
G: Graph defining spin interactions (edges = ZZ couplings)
d_ind_tot: Number of Trotter steps (maximum depth)
J: Coupling between nearest neighboring spins
h: Transverse magnetic field strength
dt: (t / total_steps
meas_basis: Basis to measure all correlators in
mirror: If True, mirror the Trotter layers
"""
qubit_count = len(G)
circuits = []
psi0 = "0" * qubit_count
for steps in range(1, d_ind_tot + 1):
circuits.append(
generate_tfim_circ_custom_graph(
steps, h, J, dt, psi0, G, meas_basis, mirror
)
)
return circuits
Stimare le magnetizzazioni a singolo qubit ​
Per studiare la dinamica del modello, vogliamo misurare la magnetizzazione di ogni qubit, definita dal valore di aspettazione .
Nelle simulazioni, possiamo calcolare questo direttamente dai risultati delle misurazioni. La funzione z_expectation elabora i conteggi delle stringhe di bit e restituisce il valore di per un indice di qubit scelto. Su hardware reale, valutiamo la stessa quantità specificando l'operatore di Pauli utilizzando la funzione generate_z_observables, e quindi il backend calcola il valore di aspettazione.
def z_expectation(counts, index):
"""
counts: Dict of mitigated bitstrings.
index: Index i in the single operator expectation value < II...Z_i...I > to be calculated.
return: < Z_i >
"""
z_exp = 0
tot = 0
for bitstring, value in counts.items():
bit = int(bitstring[index])
sign = 1
if bit % 2 == 1:
sign = -1
z_exp += sign * value
tot += value
return z_exp / tot
def generate_z_observables(nq):
observables = []
for i in range(nq):
pauli_string = "".join(["Z" if j == i else "I" for j in range(nq)])
observables.append(SparsePauliOp(pauli_string))
return observables
observables = generate_z_observables(n_qubits)
Ora definiamo i parametri per generare i circuiti trotterizzati. In questo tutorial, il reticolo è una catena di 20 triangoli connessi, che corrisponde a un sistema a 41 qubit.
all_circs_mirror = []
for num_triangles in [n_triangles]:
for meas_basis in ["Z"]:
A = connected_triangles_adj_matrix(num_triangles)
G = nx.from_numpy_array(A)
nq = len(G)
d_ind_tot = 22
dt = 2 * np.pi * 1 / 30 * 0.25
J = 1
h = -7
all_circs_mirror.extend(
trotter_circuits(G, d_ind_tot, J, h, dt, meas_basis, True)
)
circs = all_circs_mirror
Step 3: Esecuzione utilizzando le primitive Qiskit​
Eseguire la simulazione MPS​
La lista dei circuiti trotterizzati viene eseguita utilizzando il simulatore matrix_product_state con una scelta arbitraria di shot. Il metodo MPS fornisce un'approssimazione efficiente delle dinamiche del circuito, con un'accuratezza determinata dalla dimensione di legame scelta. Per le dimensioni del sistema considerate qui, la dimensione di legame predefinita è sufficiente per catturare le dinamiche di magnetizzazione con alta fedeltà . I conteggi grezzi vengono normalizzati, e da questi calcoliamo i valori di aspettazione a singolo qubit ad ogni passo di Trotter. Infine, calcoliamo la media su tutti i qubit per ottenere una singola curva che mostra come la magnetizzazione cambia nel tempo.
backend_sim = AerSimulator(method="matrix_product_state")
def normalize_counts(counts_list, shots):
new_counts_list = []
for counts in counts_list:
a = {k: v / shots for k, v in counts.items()}
new_counts_list.append(a)
return new_counts_list
def run_sim(circ_list):
shots = 4096
res = backend_sim.run(circ_list, shots=shots)
normed = normalize_counts(res.result().get_counts(), shots)
return normed
sim_counts = run_sim(circs)
Eseguire su hardware​
service = QiskitRuntimeService()
backend = service.backend("ibm_marrakesh")
def run_qiskit(circ_list):
shots = 4096
pm = generate_preset_pass_manager(backend=backend)
isa_circuits = [pm.run(qc) for qc in circ_list]
sampler = Sampler(mode=backend)
res = sampler.run(isa_circuits, shots=shots)
res = [r.data.c.get_counts() for r in res.result()]
normed = normalize_counts(res, shots)
return normed
qiskit_counts = run_qiskit(circs)
Eseguire su hardware con Fire Opal​
Valutiamo le dinamiche di magnetizzazione su hardware quantistico reale. Fire Opal fornisce una Funzione Qiskit che estende la primitiva Estimator standard di Qiskit Runtime con soppressione automatica degli errori e gestione delle prestazioni. Inviamo i circuiti trotterizzati direttamente a un backend IBM® mentre Fire Opal gestisce l'esecuzione consapevole del rumore.
Prepariamo una lista di pubs, dove ogni elemento contiene un circuito e le corrispondenti osservabili Pauli-Z. Queste vengono passate alla funzione estimator di Fire Opal, che restituisce i valori di aspettazione per ogni qubit ad ogni passo di Trotter. I risultati possono quindi essere mediati sui qubit per ottenere la curva di magnetizzazione dall'hardware.
backend_name = "ibm_marrakesh"
estimator_pubs = [(qc, observables) for qc in all_circs_mirror[:]]
# Run the circuit using the estimator
qctrl_estimator_job = perf_mgmt.run(
primitive="estimator",
pubs=estimator_pubs,
backend_name=backend_name,
options={"default_shots": 4096},
)
result_qctrl = qctrl_estimator_job.result()
Step 4: Post-elaborazione e restituzione del risultato nel formato classico desiderato​
Infine, confrontiamo la curva di magnetizzazione dal simulatore con i risultati ottenuti su hardware reale. Tracciare entrambi fianco a fianco mostra quanto strettamente l'esecuzione hardware con Fire Opal corrisponda alla linea di base senza rumore attraverso i passi di Trotter.
def make_correlators(test_counts, nq, d_ind_tot):
mz = np.empty((nq, d_ind_tot))
for d_ind in range(d_ind_tot):
counts = test_counts[d_ind]
for i in range(nq):
mz[i, d_ind] = z_expectation(counts, i)
average_z = np.mean(mz, axis=0)
return np.concatenate((np.array([1]), average_z), axis=0)
sim_exp = make_correlators(sim_counts[0:22], nq=nq, d_ind_tot=22)
qiskit_exp = make_correlators(qiskit_counts[0:22], nq=nq, d_ind_tot=22)
qctrl_exp = [ev.data.evs for ev in result_qctrl[:]]
qctrl_exp_mean = np.concatenate(
(np.array([1]), np.mean(qctrl_exp, axis=1)), axis=0
)
def make_expectations_plot(
sim_z,
depths,
exp_qctrl=None,
exp_qctrl_error=None,
exp_qiskit=None,
exp_qiskit_error=None,
plot_from=0,
plot_upto=23,
):
import numpy as np
import matplotlib.pyplot as plt
depth_ticks = [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22]
d = np.asarray(depths)[plot_from:plot_upto]
sim = np.asarray(sim_z)[plot_from:plot_upto]
qk = (
None
if exp_qiskit is None
else np.asarray(exp_qiskit)[plot_from:plot_upto]
)
qc = (
None
if exp_qctrl is None
else np.asarray(exp_qctrl)[plot_from:plot_upto]
)
qk_err = (
None
if exp_qiskit_error is None
else np.asarray(exp_qiskit_error)[plot_from:plot_upto]
)
qc_err = (
None
if exp_qctrl_error is None
else np.asarray(exp_qctrl_error)[plot_from:plot_upto]
)
# ---- helper(s) ----
def rmse(a, b):
if a is None or b is None:
return None
a = np.asarray(a, dtype=float)
b = np.asarray(b, dtype=float)
mask = np.isfinite(a) & np.isfinite(b)
if not np.any(mask):
return None
diff = a[mask] - b[mask]
return float(np.sqrt(np.mean(diff**2)))
def plot_panel(ax, method_y, method_err, color, label, band_color=None):
# Noiseless reference
ax.plot(d, sim, color="grey", label="Noiseless simulation")
# Method line + band
if method_y is not None:
ax.plot(d, method_y, color=color, label=label)
if method_err is not None:
lo = np.clip(method_y - method_err, -1.05, 1.05)
hi = np.clip(method_y + method_err, -1.05, 1.05)
ax.fill_between(
d,
lo,
hi,
alpha=0.18,
color=band_color if band_color else color,
label=f"{label} ± error",
)
else:
ax.text(
0.5,
0.5,
"No data",
transform=ax.transAxes,
ha="center",
va="center",
fontsize=10,
color="0.4",
)
# RMSE box (vs sim)
r = rmse(method_y, sim)
if r is not None:
ax.text(
0.98,
0.02,
f"RMSE: {r:.4f}",
transform=ax.transAxes,
va="bottom",
ha="right",
fontsize=8,
bbox=dict(
boxstyle="round,pad=0.35", fc="white", ec="0.7", alpha=0.9
),
)
# Axes
ax.set_xticks(depth_ticks)
ax.set_ylim(-1.05, 1.05)
ax.grid(True, which="both", linewidth=0.4, alpha=0.4)
ax.set_axisbelow(True)
ax.legend(prop={"size": 8}, loc="best")
fig, axes = plt.subplots(1, 2, figsize=(10, 4), dpi=300, sharey=True)
axes[0].set_title("Fire Opal (Q-CTRL)", fontsize=10)
plot_panel(
axes[0],
qc,
qc_err,
color="#680CE9",
label="Fire Opal",
band_color="#680CE9",
)
axes[0].set_xlabel("Trotter step")
axes[0].set_ylabel(r"$\langle Z \rangle$")
axes[1].set_title("Qiskit", fontsize=10)
plot_panel(
axes[1], qk, qk_err, color="blue", label="Qiskit", band_color="blue"
)
axes[1].set_xlabel("Trotter step")
plt.tight_layout()
plt.show()
depths = list(range(d_ind_tot + 1))
errors = np.abs(np.array(qctrl_exp_mean) - np.array(sim_exp))
errors_qiskit = np.abs(np.array(qiskit_exp) - np.array(sim_exp))
make_expectations_plot(
sim_exp,
depths,
exp_qctrl=qctrl_exp_mean,
exp_qctrl_error=errors,
exp_qiskit=qiskit_exp,
exp_qiskit_error=errors_qiskit,
)

Riferimenti​
[1] Graph coloring. Wikipedia. Retrieved September 15, 2025, from https://en.wikipedia.org/wiki/Graph_coloring
Sondaggio sul tutorial​
Vi preghiamo di dedicare un minuto per fornire un feedback su questo tutorial. Le vostre opinioni ci aiuteranno a migliorare la nostra offerta di contenuti e l'esperienza utente.