Modèle d'Ising à champ transversal avec gestion des performances Q-CTRL
Estimation d'utilisation : 2 minutes sur un processeur Heron r2. (NOTE : Il s'agit uniquement d'une estimation. Votre durée d'exécution peut varier.)
Arrière-plan
Le modèle d'Ising à champs transversaux (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 entraîne 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 les rotations d'un seul qubit et les interactions de deux qubits enchevêtrés. Cependant, cette simulation sur du matériel réel est difficile en raison du bruit et de la décohérence, qui entraînent des écarts par rapport à la dynamique réelle. Pour y remédier, nous utilisons les outils de suppression des erreurs et de gestion des performances Fire Opal de Q-CTRL, proposés en tant que fonction Qiskit (voir la documentation Fire Opal). Fire Opal optimise automatiquement l'exécution du circuit en appliquant un découplage dynamique, une disposition avancée, un routage et d'autres techniques de suppression des erreurs, toutes destinées à réduire le bruit. Grâce à ces améliorations, les résultats matériels s'alignent plus étroitement sur les simulations sans bruit, ce qui nous permet d'étudier la dynamique de l'aimantation du TFIM avec une plus grande fidélité.
Dans ce tutoriel, nous allons :
- Construire l'hamiltonien TFIM sur un graphe de triangles de spin connectés
- Simuler l'évolution temporelle avec des circuits troptérisés à différentes profondeurs
- Calculer et visualiser les magnétisations d'un qubit unique au fil du temps
- Comparer les simulations de base avec les résultats d'exécutions matérielles à l'aide de la gestion des performances Fire Opal de Q-CTRL
Aperçu
Le modèle d'Ising à champ transverse (TFIM) est un modèle de spin quantique qui capture les caractéristiques essentielles des transitions de phase quantiques. L'hamiltonien est défini comme suit
où et sont des opérateurs de Pauli agissant sur le qubit , est la force de couplage entre les spins voisins, et est la force du champ magnétique transversal. Le premier terme représente les interactions ferromagnétiques classiques, tandis que le second introduit des fluctuations quantiques à travers le champ transversal. Pour simuler la dynamique de TFIM, vous utilisez une décomposition de Trotter de l'opérateur d'évolution unitaire , mise en œuvre par des couches de portes RX et RZZ basées sur un graphe personnalisé de triangles de spin connectés. La simulation explore la façon dont l'aimantation évolue avec l'augmentation des pas de Trotter.
La performance de l'implémentation TFIM proposée est évaluée en comparant des simulations sans bruit avec des backends bruyants. Les fonctions améliorées d'exécution et de suppression des erreurs de Fire Opal sont utilisées pour atténuer l'effet du bruit dans le matériel réel, ce qui permet d'obtenir des estimations plus fiables des observables de spin comme et les corrélateurs .
Exigences
Avant de commencer ce tutoriel, assurez-vous que les éléments suivants sont installés :
- Qiskit SDK v1.4 ou plus tard, avec prise en charge de la visualisation
- Qiskit Runtime v0.40 ou plus tard (
pip install qiskit-ibm-runtime) - Qiskit Functions Catalog v0.9.0 (
pip install qiskit-ibm-catalog) - Fire Opal SDK v9.0.2 ou plus récent (
pip install fire-opal) - Q-CTRL Visualizer v8.0.2 ou plus récent (
pip install qctrl-visualizer)
Configuration
Commencez par vous authentifier à l'aide de votre clé API IBM Quantum. Ensuite, sélectionnez la fonction Qiskit comme suit. (Ce code part du principe que vous avez déjà enregistré votre compte dans votre environnement local.)
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 qvcatalog = QiskitFunctionsCatalog(channel="ibm_quantum_platform")
# Access Function
perf_mgmt = catalog.load("q-ctrl/performance-management")Étape 1 : Mettre en correspondance les entrées classiques avec un problème quantique
Générer un graphique TFIM
Nous commençons par définir le réseau de spins et les couplages entre eux. Dans ce tutoriel, le treillis est construit à partir de triangles connectés disposés en chaîne linéaire. Chaque triangle est constitué de trois nœuds relié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 d'aide connected_triangles_adj_matrix construit la matrice d'adjacence pour cette structure. Pour une chaîne de triangles, le graphe résultant contient 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_matrixPour visualiser le treillis 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 graphique 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, posPour 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:
Coloration des arêtes d'un graphe
Pour mettre en œuvre le couplage spin-spin, il est utile de regrouper les arêtes qui ne se chevauchent pas. Cela nous permet d'appliquer des portes à deux qubits en parallèle. Pour ce faire, nous utilisons une procédure simple de coloration des arêtes [1], qui attribue une couleur à chaque arête de manière à ce que les arêtes qui se rencontrent 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 du matériel quantique
Générer des circuits trotterisés sur des graphes de spin
Pour simuler la dynamique du TFIM, nous construisons des circuits qui se rapprochent de l'opérateur d'évolution temporelle.
Nous utilisons une décomposition de Trotter du second ordre :
où et .
- Le terme est mis en œuvre avec des couches de rotations
RX. - Le terme est implémenté avec des couches de portes
RZZle long des arêtes du graphe d'interaction.
Les angles de ces portes sont déterminés par le champ transversal , la constante de couplage , et le pas de temps . En empilant plusieurs étapes de Trotter, nous générons des circuits de profondeur croissante qui se rapprochent de 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 spin 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 circuitsEstimer les magnétisations d'un seul qubit
Pour étudier la dynamique du modèle, nous voulons mesurer l'aimantation de chaque qubit, définie par la valeur d'espérance .
Dans les simulations, nous pouvons le calculer directement à partir des résultats des mesures. La fonction z_expectation traite les comptages de chaînes de bits et renvoie la valeur de pour un indice de qubit choisi. 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 de l'espérance.
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 / totdef 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 observablesobservables = generate_z_observables(n_qubits)Nous définissons maintenant les paramètres de génération des circuits trotterisés. Dans ce tutoriel, le treillis 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écutez à l'aide d' Qiskit primitives
Lancer 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 plans . 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ème considérées ici, la dimension de liaison par défaut est suffisante pour capturer la dynamique de l'aimantation avec une grande fidélité. Les comptes bruts sont normalisés et, à partir de ceux-ci, nous calculons les valeurs d'espérance d'un qubit unique à chaque étape de Trotter. Enfin, nous calculons la moyenne sur l'ensemble des qubits pour obtenir une courbe unique qui montre l'évolution de l'aimantation dans le 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)Fonctionner sur du 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)Fonctionne sur du matériel équipé de Fire Opal
Nous évaluons la dynamique de magnétisation sur du matériel quantique réel. Fire Opal propose une fonction Qiskit qui étend la primitive standard « IBM Quantum Estimator » en y ajoutant des fonctionnalités de suppression automatique des erreurs et de gestion des performances. Nous soumettons les circuits « trotterisés » directement à un backend « IBM® », tandis que Fire Opal se charge de l'exécution tenant compte du bruit.
Nous préparons une liste de pubs, où chaque élément contient un circuit et les observables Pauli-Z correspondantes. Ces valeurs sont transmises à la fonction d'estimation de Fire Opal, qui renvoie les valeurs d'espérance 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-traitement et restitution du résultat dans le format classique souhaité
Enfin, nous comparons la courbe d'aimantation du simulateur avec les résultats obtenus sur le matériel réel. Le tracé des deux côte à côte montre à quel point l'exécution matérielle avec Fire Opal correspond à la ligne de base 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:
Références
[1] Coloration du graphique. Wikipedia. Consulté le 15 septembre 2025, à l' adresse suivante : https://en.wikipedia.org/wiki/Graph _coloring
Enquête tutorielle
Veuillez prendre une minute pour nous faire part de vos commentaires sur ce tutoriel. Vos commentaires nous aideront à améliorer notre offre de contenu et l'expérience des utilisateurs.