Skip to main content
IBM Quantum Platform

Modello di Ising a campo trasversale con gestione delle prestazioni di Q-CTRL

Stima di utilizzo: 2 minuti su un processore Heron r2. (NOTA: questa è solo una stima. Il tempo di esecuzione può variare)


Sfondo

Il modello 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 è influenzato da un campo magnetico esterno che determina 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 è difficile a causa del rumore e della decoerenza, che portano a deviazioni dalla dinamica reale. Per ovviare a questo problema, utilizziamo gli strumenti di soppressione degli errori e di gestione delle prestazioni Fire Opal di Q-CTRL, offerti come funzione Qiskit (vedere la documentazione di Fire Opal ). Fire Opal ottimizza automaticamente l'esecuzione dei circuiti applicando il disaccoppiamento dinamico, il layout avanzato, l'instradamento e altre tecniche di soppressione degli errori, tutte finalizzate alla riduzione del rumore. Grazie a questi miglioramenti, i risultati hardware si allineano maggiormente con le simulazioni senza rumore e quindi possiamo studiare le dinamiche di magnetizzazione del TFIM con maggiore fedeltà.

In questa esercitazione ci occuperemo di:

  • Costruire l'hamiltoniana TFIM su un grafo di triangoli di spin collegati
  • Simulare l'evoluzione temporale con circuiti trotterizzati a diverse profondità
  • Calcolo e visualizzazione di magnetizzazioni a singolo qubit Zi\langle Z_i \rangle nel tempo
  • Confrontare le simulazioni di base con i risultati delle esecuzioni hardware utilizzando la gestione delle prestazioni di Fire Opal di Q-CTRL

Panoramica

Il modello di Ising a campo trasverso (TFIM) è un modello quantistico di spin che cattura le caratteristiche essenziali delle transizioni di fase quantistiche. L'hamiltoniana è definita come:

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

dove ZiZ_i e XiX_i sono operatori di Pauli che agiscono sul qubit ii, JJ è la forza di accoppiamento tra spin vicini e hh è la forza del campo magnetico trasversale. 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 Trotter dell'operatore di evoluzione unitario eiHte^{-iHt}, implementato attraverso strati di porte RX e RZZ basate su un grafo personalizzato di triangoli di spin connessi. La simulazione analizza l'evoluzione della magnetizzazione Z\langle Z \rangle con l'aumento dei passi di Trotter.

Le prestazioni dell'implementazione TFIM proposta sono valutate confrontando simulazioni senza rumore con backend rumorosi. Le funzioni avanzate di Fire Opal per l'esecuzione e la soppressione degli errori vengono utilizzate per mitigare l'effetto del rumore nell'hardware reale, ottenendo stime più affidabili delle osservabili di spin come Zi\langle Z_i \rangle e dei correlatori ZiZj\langle Z_i Z_j \rangle.


Requisiti

Prima di iniziare questa esercitazione, assicuratevi di aver installato quanto segue:

  • Qiskit SDK v1.4 o versioni successive, con supporto alla visualizzazione
  • Qiskit Runtime v0.40 o più tardi (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)

Configura

Innanzitutto, autenticati utilizzando la tua chiave API IBM Quantum. Quindi, selezionare la funzione Qiskit come segue. (Questo codice presuppone che tu abbia già salvato il tuo account nell'ambiente locale.)

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

Fase 1: mappare gli input classici su un problema quantistico

Genera grafico TFIM

Iniziamo definendo il reticolo di spin e gli accoppiamenti tra di essi. In questa esercitazione, il reticolo è costruito da triangoli connessi disposti in una catena lineare. Ogni triangolo è composto da tre nodi collegati in un anello chiuso e la catena si forma 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 nn triangoli, il grafo risultante contiene 2n+12n+1 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 appena definito, possiamo tracciare la catena di triangoli connessi ed etichettare ogni nodo. La funzione seguente costruisce il grafico 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 questa esercitazione 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()

Output:

Output of the previous code cell

Colorazione dei bordi dei grafici

Per implementare l'accoppiamento spin-spin, è utile raggruppare i bordi che non si sovrappongono. Questo ci permette di applicare porte a due qubit in parallelo. Possiamo farlo con una semplice procedura di colorazione degli spigoli [1], che assegna un colore a ogni spigolo in modo che gli spigoli 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())

Fase 2: Ottimizzazione del problema per l'esecuzione su hardware quantistico

Generare circuiti Trotterizzati su grafici di spin

Per simulare la dinamica del TFIM, costruiamo circuiti che approssimano l'operatore di evoluzione temporale.

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 .

Utilizziamo una decomposizione di Trotter del secondo ordine:

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},

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

  • Il termine HXH_X è implementato con strati di rotazioni RX .
  • Il termine HZH_Z è implementato con strati di porte RZZ lungo i bordi del grafo di interazione.

Gli angoli di queste porte sono determinati dal campo trasversale hh, dalla costante di accoppiamento JJ e dal passo temporale Δt\Delta t. Impilando 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 a partire da un grafo di interazione di spin arbitrario.

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

Stima delle magnetizzazioni a singolo qubit Zi\langle Z_i \rangle

Per studiare la dinamica del modello, vogliamo misurare la magnetizzazione di ciascun qubit, definita dal valore di aspettativa Zi=ψZiψ\langle Z_i \rangle = \langle \psi | Z_i | \psi \rangle.

Nelle simulazioni, possiamo calcolarlo direttamente dai risultati della misurazione. La funzione z_expectation elabora il conteggio delle stringhe di bit e restituisce il valore di Zi\langle Z_i \rangle per un indice di qubit scelto. Su un hardware reale, valutiamo la stessa quantità specificando l'operatore di Pauli con la funzione generate_z_observables, e poi il backend calcola il valore di aspettativa.

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)

Definiamo ora i parametri per la generazione dei circuiti trotterizzati. In questa esercitazione, 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

Passaggio 3: eseguire utilizzando Qiskit primitives

Esegui simulazione MPS

L'elenco dei circuiti trotterizzati viene eseguito utilizzando il simulatore matrix_product_state con una scelta arbitraria di colpi 40964096. Il metodo MPS fornisce un'approssimazione efficiente della dinamica del circuito, con una precisione determinata dalla dimensione del legame scelta. Per le dimensioni del sistema qui considerate, la dimensione predefinita del legame è sufficiente per catturare la dinamica della magnetizzazione con alta fedeltà. I conteggi grezzi vengono normalizzati e da questi si calcolano i valori di aspettativa a singolo qubit Zi\langle Z_i \rangle a 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)

Eseguibile su hardware con Fire Opal

Valutiamo la dinamica della magnetizzazione su hardware quantistico reale. Fire Opal offre una funzione Qiskit che estende la primitiva standard " IBM Quantum Estimator" con la soppressione automatica degli errori e la gestione delle prestazioni. Invieremo i circuiti “Trotterizzati” direttamente a un backend IBM®, mentre Fire Opal si occuperà dell’esecuzione sensibile al rumore.

Prepariamo un elenco di pubs, dove ogni elemento contiene un circuito e le corrispondenti osservabili Pauli-Z. Questi vengono passati alla funzione stimatore di Fire Opal, che restituisce i valori di aspettativa Zi\langle Z_i \rangle per ogni qubit a ogni passo di Trotter. I risultati possono poi 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()

Fase 4: Post-elaborazione e restituzione del risultato nel formato classico desiderato

Infine, confrontiamo la curva di magnetizzazione ottenuta dal simulatore con i risultati ottenuti sull'hardware reale. I grafici affiancati mostrano quanto l'esecuzione hardware con Fire Opal si avvicini 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,
)

Output:

Output of the previous code cell

Riferimenti

[1] Colorazione dei grafici. Wikipedia. Recuperato il 15 settembre 2025, da https://en.wikipedia.org/wiki/Graph _coloring


Sondaggio tutorial

Dedicate un minuto a fornire un feedback su questa esercitazione. Le vostre indicazioni ci aiuteranno a migliorare la nostra offerta di contenuti e l'esperienza degli utenti.

Collegamento al sondaggio

Questa pagina è stata utile?
Segnala un bug, un errore di battitura o richiedi contenuti su GitHub.