Aller au contenu principal

Modèle d'Ising en champ transverse avec la gestion des performances de Q-CTRL

Estimation d'utilisation : 2 minutes sur un processeur Heron r2. (REMARQUE : ceci est une estimation uniquement. Votre temps d'exécution peut varier.)

Contexte

Le modèle d'Ising en champ transverse (TFIM) est important pour l'étude du magnétisme quantique et des transitions de phase. Il décrit un ensemble de spins disposés sur un réseau, où chaque spin interagit avec ses voisins tout en étant influencé par un champ magnétique externe qui induit des fluctuations quantiques.

Une approche courante pour simuler ce modèle consiste à utiliser la décomposition de Trotter pour approximer l'opérateur d'évolution temporelle, en construisant des circuits qui alternent entre des rotations à un seul Qubit et des interactions intriquées à deux Qubits. Cependant, cette simulation sur du matériel réel est difficile en raison du bruit et de la décohérence, entraînant des écarts par rapport à la dynamique réelle. Pour surmonter cela, nous utilisons les outils de suppression d'erreurs et de gestion des performances Fire Opal de Q-CTRL, proposés en tant que Qiskit Function (voir la documentation Fire Opal). Fire Opal optimise automatiquement l'exécution des circuits en appliquant le découplage dynamique, le placement avancé, le routage et d'autres techniques de suppression d'erreurs, le tout visant à réduire le bruit. Grâce à ces améliorations, les résultats matériels s'alignent plus étroitement avec les simulations sans bruit, et nous pouvons ainsi étudier la dynamique de magnétisation du TFIM avec une fidélité accrue.

Dans ce tutoriel, nous allons :

  • Construire le Hamiltonien TFIM sur un graphe de triangles de spins connectés
  • Simuler l'évolution temporelle avec des circuits trotterisés à différentes profondeurs
  • Calculer et visualiser les magnétisations à un seul Qubit Zi\langle Z_i \rangle au cours du temps
  • Comparer les simulations de référence avec les résultats d'exécutions matérielles utilisant la gestion des performances Fire Opal de Q-CTRL

Vue d'ensemble

Le modèle d'Ising en champ transverse (TFIM) est un modèle de spin quantique qui capture les caractéristiques essentielles des transitions de phase quantiques. Le Hamiltonien est défini comme :

H=JiZiZi+1hiXiH = -J \sum_{i} Z_i Z_{i+1} - h \sum_{i} X_i

ZiZ_i et XiX_i sont les opérateurs de Pauli agissant sur le Qubit ii, JJ est la constante de couplage entre spins voisins, et hh est l'intensité du champ magnétique transverse. Le premier terme représente les interactions ferromagnétiques classiques, tandis que le second introduit des fluctuations quantiques par le biais du champ transverse. Pour simuler la dynamique du TFIM, vous utilisez une décomposition de Trotter de l'opérateur d'évolution unitaire eiHte^{-iHt}, implémentée à travers des couches de Gates RX et RZZ basées sur un graphe personnalisé de triangles de spins connectés. La simulation explore comment la magnétisation Z\langle Z \rangle évolue avec le nombre croissant d'étapes de Trotter.

Les performances de l'implémentation TFIM proposée sont évaluées en comparant les simulations sans bruit avec les Backends bruités. Les fonctionnalités d'exécution améliorée et de suppression d'erreurs de Fire Opal sont utilisées pour atténuer l'effet du bruit sur le matériel réel, produisant des estimations plus fiables des observables de spin comme Zi\langle Z_i \rangle et des corrélateurs ZiZj\langle Z_i Z_j \rangle.

Prérequis

Avant de commencer ce tutoriel, assurez-vous d'avoir installé les éléments suivants :

  • Qiskit SDK v1.4 ou ultérieur, avec le support de visualization
  • Qiskit Runtime v0.40 ou ultérieur (pip install qiskit-ibm-runtime)
  • Qiskit Functions Catalog v0.9.0 (pip install qiskit-ibm-catalog)
  • Fire Opal SDK v9.0.2 ou ultérieur (pip install fire-opal)
  • Q-CTRL Visualizer v8.0.2 ou ultérieur (pip install qctrl-visualizer)

Configuration

Commencez par vous authentifier avec votre clé API IBM Quantum. Ensuite, sélectionnez la Qiskit Function comme suit. (Ce code suppose que vous avez déjà enregistré votre compte dans votre environnement local.)

# Added by doQumentation — installs packages not in the Binder environment
%pip install -q networkx qctrlvisualizer
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")

Étape 1 : Transformer les entrées classiques en un problème quantique

Générer le graphe TFIM

Nous commençons par définir le réseau de spins et les couplages entre eux. Dans ce tutoriel, le réseau est construit à partir de triangles connectés disposés en chaîne linéaire. Chaque triangle est constitué de trois nœuds connectés en boucle fermée, et la chaîne est formée en reliant un nœud de chaque triangle au triangle précédent.

La fonction auxiliaire connected_triangles_adj_matrix construit la matrice d'adjacence pour cette structure. Pour une chaîne de nn triangles, le graphe résultant contient 2n+12n+1 nœuds.

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

Pour visualiser le réseau que nous venons de définir, nous pouvons tracer la chaîne de triangles connectés et étiqueter chaque nœud. La fonction ci-dessous construit le graphe pour un nombre choisi de triangles et l'affiche.

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

Pour ce tutoriel, nous utiliserons une chaîne de 20 triangles.

n_triangles = 20
n_qubits = 2 * n_triangles + 1
plot_triangle_chain(n_triangles, side=1.0)
plt.show()

Output of the previous code cell

Coloration des arêtes du graphe

Pour implémenter le couplage spin-spin, il est utile de regrouper les arêtes qui ne se chevauchent pas. Cela nous permet d'appliquer des Gates à deux Qubits en parallèle. Nous pouvons le faire avec une procédure simple de coloration d'arêtes [1], qui attribue une couleur à chaque arête de sorte que les arêtes se rejoignant au même nœud soient placées dans des groupes différents.

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())

Étape 2 : Optimiser le problème pour l'exécution sur matériel quantique

Générer des circuits trotterisés sur des graphes de spins

Pour simuler la dynamique du TFIM, nous construisons des circuits qui approximent l'opérateur d'évolution temporelle.

U(t)=eiHt,whereH=Ji,jZiZjhiXi.U(t) = e^{-i H t}, \quad \text{where} \quad H = -J \sum_{\langle i,j \rangle} Z_i Z_j - h \sum_i X_i .

Nous utilisons une décomposition de Trotter au second ordre :

eiHΔteiHXΔt/2eiHZΔteiHXΔt/2,e^{-i H \Delta t} \approx e^{-i H_X \Delta t / 2}\, e^{-i H_Z \Delta t}\, e^{-i H_X \Delta t / 2},

HX=hiXiH_X = -h \sum_i X_i et HZ=Ji,jZiZjH_Z = -J \sum_{\langle i,j \rangle} Z_i Z_j.

  • Le terme HXH_X est implémenté avec des couches de rotations RX.
  • Le terme HZH_Z est implémenté avec des couches de Gates RZZ le long des arêtes du graphe d'interaction.

Les angles de ces Gates sont déterminés par le champ transverse hh, la constante de couplage JJ et le pas de temps Δt\Delta t. En empilant plusieurs étapes de Trotter, nous générons des circuits de profondeur croissante qui approximent la dynamique du système. Les fonctions generate_tfim_circ_custom_graph et trotter_circuits construisent un Circuit quantique trotterisé à partir d'un graphe d'interaction de spins arbitraire.

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

Estimer les magnétisations à un seul Qubit Zi\langle Z_i \rangle

Pour étudier la dynamique du modèle, nous souhaitons mesurer la magnétisation de chaque Qubit, définie par la valeur d'expectation Zi=ψZiψ\langle Z_i \rangle = \langle \psi | Z_i | \psi \rangle.

Dans les simulations, nous pouvons calculer cela directement à partir des résultats de mesure. La fonction z_expectation traite les comptages de chaînes de bits et renvoie la valeur de Zi\langle Z_i \rangle pour un indice de Qubit donné. Sur le matériel réel, nous évaluons la même quantité en spécifiant l'opérateur de Pauli à l'aide de la fonction generate_z_observables, puis le Backend calcule la valeur d'expectation.

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)

Nous définissons maintenant les paramètres pour la génération des circuits trotterisés. Dans ce tutoriel, le réseau est une chaîne de 20 triangles connectés, ce qui correspond à un système de 41 Qubits.

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

Étape 3 : Exécuter à l'aide des primitives Qiskit

Exécuter la simulation MPS

La liste des circuits trotterisés est exécutée à l'aide du simulateur matrix_product_state avec un choix arbitraire de 40964096 coups. La méthode MPS fournit une approximation efficace de la dynamique du Circuit, avec une précision déterminée par la dimension de liaison choisie. Pour les tailles de systèmes considérées ici, la dimension de liaison par défaut est suffisante pour capturer la dynamique de magnétisation avec une haute fidélité. Les comptages bruts sont normalisés, et à partir de ceux-ci nous calculons les valeurs d'expectation à un seul Qubit Zi\langle Z_i \rangle à chaque étape de Trotter. Enfin, nous calculons la moyenne sur tous les Qubits pour obtenir une courbe unique montrant comment la magnétisation évolue au cours du temps.

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)

Exécuter sur le matériel

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)

Exécuter sur le matériel avec Fire Opal

Nous évaluons la dynamique de magnétisation sur du matériel quantique réel. Fire Opal fournit une Qiskit Function qui étend la primitive Estimator standard de Qiskit Runtime avec une suppression d'erreurs automatisée et une gestion des performances. Nous soumettons les circuits trotterisés directement à un Backend IBM® tandis que Fire Opal gère l'exécution adaptée au bruit.

Nous préparons une liste de pubs, où chaque élément contient un Circuit et les observables Pauli-Z correspondantes. Ceux-ci sont transmis à la fonction estimateur de Fire Opal, qui renvoie les valeurs d'expectation Zi\langle Z_i \rangle pour chaque Qubit à chaque étape de Trotter. Les résultats peuvent ensuite être moyennés sur les Qubits pour obtenir la courbe de magnétisation à partir du matériel.

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()

Étape 4 : Post-traiter et renvoyer le résultat dans le format classique souhaité

Enfin, nous comparons la courbe de magnétisation du simulateur avec les résultats obtenus sur le matériel réel. L'affichage côte à côte montre à quel point l'exécution matérielle avec Fire Opal correspond à la référence sans bruit à travers les étapes de 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,
)

Output of the previous code cell

Références

[1] Graph coloring. Wikipedia. Retrieved September 15, 2025, from https://en.wikipedia.org/wiki/Graph_coloring

Enquête sur le tutoriel

Veuillez prendre une minute pour donner votre avis sur ce tutoriel. Vos retours nous aideront à améliorer notre contenu et l'expérience utilisateur.

Lien vers l'enquête