Skip to main content
IBM Quantum Platform

Modelo de Ising de campo transversal con gestión del rendimiento de Q-CTRL

Estimación de uso: 2 minutos en un procesador Heron r2. (NOTA: Esto es sólo una estimación. Su tiempo de ejecución puede variar)


En segundo plano

El modelo de Ising de campo transversal (TFIM) es importante para estudiar el magnetismo cuántico y las transiciones de fase. Describe un conjunto de espines dispuestos en un entramado, donde cada espín interactúa con sus vecinos a la vez que recibe la influencia de un campo magnético externo que impulsa las fluctuaciones cuánticas.

Un enfoque común para simular este modelo es utilizar la descomposición de Trotter para aproximar el operador de evolución temporal, construyendo circuitos que alternan entre rotaciones de un solo qubit e interacciones de dos qubits enredados. Sin embargo, esta simulación en hardware real es un reto debido al ruido y la decoherencia, que provocan desviaciones de la dinámica real. Para superar esto, utilizamos las herramientas de gestión de rendimiento y supresión de errores Fire Opal de Q-CTRL, ofrecidas como una función Qiskit (consulte la documentación de Fire Opal ). Fire Opal optimiza automáticamente la ejecución del circuito aplicando técnicas de desacoplamiento dinámico, diseño avanzado, enrutamiento y otras técnicas de supresión de errores, todas ellas encaminadas a reducir el ruido. Con estas mejoras, los resultados del hardware se ajustan más a las simulaciones sin ruido y, por tanto, podemos estudiar la dinámica de magnetización del TFIM con mayor fidelidad.

En este tutorial vamos a:

  • Construir el Hamiltoniano TFIM en un gráfico de triángulos de espín conectados
  • Simular la evolución temporal con circuitos trotterizados a diferentes profundidades
  • Calcular y visualizar las magnetizaciones de un solo qubit Zi\langle Z_i \rangle a lo largo del tiempo
  • Comparar simulaciones de referencia con resultados de ejecuciones de hardware mediante la gestión del rendimiento Fire Opal de Q-CTRL

Visión general

El modelo de Ising de campo transversal (TFIM) es un modelo cuántico de espín que capta características esenciales de las transiciones cuánticas de fase. El Hamiltoniano se define como:

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

donde ZiZ_i y XiX_i son operadores de Pauli que actúan sobre el qubit ii, JJ es la fuerza de acoplamiento entre espines vecinos, y hh es la fuerza del campo magnético transversal. El primer término representa las interacciones ferromagnéticas clásicas, mientras que el segundo introduce fluctuaciones cuánticas a través del campo transversal. Para simular la dinámica TFIM, se utiliza una descomposición Trotter del operador unitario de evolución eiHte^{-iHt}, implementado a través de capas de puertas RX y RZZ basadas en un gráfico personalizado de triángulos de espín conectados. La simulación explora cómo evoluciona la magnetización Z\langle Z \rangle al aumentar los pasos de Trotter.

El rendimiento de la implementación TFIM propuesta se evalúa comparando simulaciones sin ruido con backends ruidosos. Las funciones mejoradas de ejecución y supresión de errores de Fire Opal se utilizan para mitigar el efecto del ruido en el hardware real, con lo que se obtienen estimaciones más fiables de observables de espín como Zi\langle Z_i \rangle y correladores ZiZj\langle Z_i Z_j \rangle.


Requisitos

Antes de empezar este tutorial, asegúrate de tener instalado lo siguiente:

  • Qiskit SDK v1.4 o posterior, con soporte de visualización
  • Qiskit Runtime v0.40 o posterior (pip install qiskit-ibm-runtime)
  • Qiskit Functions Catalog v0.9.0 (pip install qiskit-ibm-catalog)
  • Fire Opal SDK v9.0.2 o posterior (pip install fire-opal)
  • Q-CTRL Visualizer v8.0.2 o posterior (pip install qctrl-visualizer)

Configuración

Primero, autentíquese utilizando su clave API IBM Quantum. A continuación, selecciona la función de Qiskit de la siguiente manera. (Este código da por hecho que ya has guardado tu cuenta en tu entorno 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")

Paso 1: Asignar entradas clásicas a un problema cuántico

Generar gráfico TFIM

Comenzamos definiendo la red de espines y los acoplamientos entre ellos. En este tutorial, la celosía se construye a partir de triángulos conectados dispuestos en una cadena lineal. Cada triángulo consta de tres nodos conectados en un bucle cerrado, y la cadena se forma uniendo un nodo de cada triángulo al triángulo anterior.

La función de ayuda connected_triangles_adj_matrix construye la matriz de adyacencia para esta estructura. Para una cadena de triángulos nn, el gráfico resultante contiene 2n+12n+1 nodos.

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

Para visualizar la red que acabamos de definir, podemos trazar la cadena de triángulos conectados y etiquetar cada nodo. La siguiente función construye el gráfico para un número determinado de triángulos y lo muestra.

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

Para este tutorial utilizaremos una cadena de 20 triángulos.

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

Colorear los bordes de los gráficos

Para implementar el acoplamiento espín-espín, es útil agrupar aristas que no se solapen. Esto nos permite aplicar puertas de dos qubits en paralelo. Podemos hacerlo con un sencillo procedimiento de coloración de aristas [1], que asigna un color a cada arista para que las aristas que se encuentran en el mismo nodo se coloquen en grupos diferentes.

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

Paso 2: Optimizar el problema para la ejecución en hardware cuántico

Generar circuitos trotterizados en grafos de espín

Para simular la dinámica del TFIM, construimos circuitos que aproximan el operador de evolución temporal.

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 .

Utilizamos una descomposición de Trotter de segundo orden:

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

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

  • El término HXH_X se implementa con capas de rotaciones RX .
  • El término HZH_Z se implementa con capas de puertas RZZ a lo largo de las aristas del grafo de interacción.

Los ángulos de estas puertas están determinados por el campo transversal hh, la constante de acoplamiento JJ, y el paso de tiempo Δt\Delta t. Apilando múltiples pasos de Trotter, generamos circuitos de profundidad creciente que aproximan la dinámica del sistema. Las funciones generate_tfim_circ_custom_graph y trotter_circuits construyen un circuito cuántico trotterizado a partir de un grafo de interacción de espín 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

Estimación de magnetizaciones de un solo qubit Zi\langle Z_i \rangle

Para estudiar la dinámica del modelo, queremos medir la magnetización de cada qubit, definida por el valor de expectativa Zi=ψZiψ\langle Z_i \rangle = \langle \psi | Z_i | \psi \rangle.

En las simulaciones, podemos calcularlo directamente a partir de los resultados de las mediciones. La función z_expectation procesa los recuentos de la cadena de bits y devuelve el valor de Zi\langle Z_i \rangle para un índice de qubit elegido. En hardware real, evaluamos la misma cantidad especificando el operador de Pauli mediante la función generate_z_observables, y luego el backend calcula el valor de la expectativa.

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)

A continuación definimos los parámetros para generar los circuitos trotterizados. En este tutorial, la red es una cadena de 20 triángulos conectados, lo que corresponde a un sistema 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

Paso 3: Ejecutar utilizando Qiskit primitives

Ejecutar simulación MPS

La lista de circuitos trotterizados se ejecuta utilizando el simulador matrix_product_state con una elección arbitraria de 40964096 disparos. El método MPS proporciona una aproximación eficiente de la dinámica del circuito, con una precisión determinada por la dimensión de enlace elegida. Para los tamaños de sistema considerados aquí, la dimensión de enlace por defecto es suficiente para capturar la dinámica de magnetización con alta fidelidad. Los recuentos brutos se normalizan, y a partir de ellos calculamos los valores de expectativa de un solo qubit Zi\langle Z_i \rangle en cada paso de Trotter. Por último, calculamos la media de todos los qubits para obtener una curva única que muestre cómo cambia la magnetización con el tiempo.

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)

Ejecutar en 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)

Ejecutar en hardware con Fire Opal

Evaluamos la dinámica de magnetización en hardware cuántico real. Fire Opal proporciona una función Qiskit que amplía la primitiva del estimador estándar Qiskit Runtime con supresión automática de errores y gestión del rendimiento. Enviamos los circuitos Trotterizados directamente a un backend IBM® mientras Fire Opal se encarga de la ejecución consciente del ruido.

Preparamos una lista de pubs, donde cada elemento contiene un circuito y los observables Pauli-Z correspondientes. Estos se pasan a la función estimadora de Fire Opal, que devuelve los valores de expectativa Zi\langle Z_i \rangle para cada qubit en cada paso de Trotter. A continuación, los resultados pueden promediarse sobre los qubits para obtener la curva de magnetización a partir del 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()

Paso 4: Procesamiento posterior y devolución del resultado en el formato clásico deseado

Por último, comparamos la curva de magnetización del simulador con los resultados obtenidos en hardware real. Si se comparan ambos gráficos, se aprecia hasta qué punto la ejecución del hardware con Fire Opal coincide con la línea de base sin ruido en los pasos 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

Referencias

[1] Coloreado de gráficos. Wikipedia. Extraído el 15 de septiembre de 2025, de https://en.wikipedia.org/wiki/Graph _coloring


Encuesta tutorial

Tómese un minuto para comentar este tutorial. Su opinión nos ayudará a mejorar nuestra oferta de contenidos y la experiencia de los usuarios.

Enlace a la encuesta

¿Le ha resultado útil esta página?
Informe de un error, de una errata o solicite contenido en GitHub.