Skip to main content
IBM Quantum Platform

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 Zi\langle Z_i \rangle 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

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

ZiZ_i et XiX_i sont des opérateurs de Pauli agissant sur le qubit ii, JJ est la force de couplage entre les spins voisins, et hh 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 eiHte^{-iHt}, 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 Z\langle Z \rangle é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 Zi\langle Z_i \rangle et les corrélateurs ZiZj\langle Z_i Z_j \rangle.


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 qv
catalog = 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 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 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, 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:

Output of the previous code cell

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.

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 du 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 mis en œuvre avec des couches de rotations RX .
  • Le terme HZH_Z est implémenté avec des couches de portes RZZ le long des arêtes du graphe d'interaction.

Les angles de ces portes sont déterminés par le champ transversal 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 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 circuits

Estimer les magnétisations d'un seul qubit Zi\langle Z_i \rangle

Pour étudier la dynamique du modèle, nous voulons mesurer l'aimantation de chaque qubit, définie par la valeur d'espérance Zi=ψZiψ\langle Z_i \rangle = \langle \psi | Z_i | \psi \rangle.

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 Zi\langle Z_i \rangle 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 / 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 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 40964096. 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 Zi\langle Z_i \rangle à 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 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-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:

Output of the previous code cell

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.

Lien vers l'enquête

Cette page a-t-elle été utile ?
Signaler un bogue, une coquille ou proposer du contenu sur GitHub.