Skip to main content
IBM Quantum Platform

Modelo Ising de campo transversal com gerenciamento de desempenho da Q-CTRL

Estimativa de uso: 2 minutos em um processador Heron r2. (OBSERVAÇÃO: essa é apenas uma estimativa. Seu tempo de execução pode variar)


Segundo plano

O Modelo de Ising de Campo Transversal (TFIM) é importante para o estudo do magnetismo quântico e das transições de fase. Ele descreve um conjunto de spins dispostos em uma rede, em que cada spin interage com seus vizinhos e, ao mesmo tempo, é influenciado por um campo magnético externo que gera flutuações quânticas.

Uma abordagem comum para simular esse modelo é usar a decomposição de Trotter para aproximar o operador de evolução temporal, construindo circuitos que alternam entre rotações de um único qubit e interações de dois qubits emaranhados. No entanto, essa simulação em hardware real é desafiadora devido ao ruído e à decoerência, o que leva a desvios da dinâmica real. Para superar isso, usamos as ferramentas de gerenciamento de desempenho e supressão de erros Fire Opal da Q-CTRL, oferecidas como uma função Qiskit (consulte a documentação do Fire Opal ). O Fire Opal otimiza automaticamente a execução do circuito aplicando o desacoplamento dinâmico, o layout avançado, o roteamento e outras técnicas de supressão de erros, todas com o objetivo de reduzir o ruído. Com esses aprimoramentos, os resultados do hardware se alinham mais estreitamente com as simulações sem ruído e, portanto, podemos estudar a dinâmica de magnetização do TFIM com maior fidelidade.

Neste tutorial, veremos:

  • Construa o Hamiltoniano TFIM em um gráfico de triângulos de spin conectados
  • Simular a evolução do tempo com circuitos Trotterizados em diferentes profundidades
  • Calcule e visualize magnetizações de um único qubit Zi\langle Z_i \rangle ao longo do tempo
  • Compare simulações de linha de base com resultados de execuções de hardware usando o gerenciamento de desempenho Fire Opal da Q-CTRL

Visão geral

O Modelo de Ising de Campo Transversal (TFIM) é um modelo de spin quântico que captura os recursos essenciais das transições de fase quântica. O Hamiltoniano é definido como:

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

em que ZiZ_i e XiX_i são operadores Pauli que atuam no qubit ii, JJ é a força de acoplamento entre os spins vizinhos e hh é a força do campo magnético transversal. O primeiro termo representa interações ferromagnéticas clássicas, enquanto o segundo introduz flutuações quânticas por meio do campo transversal. Para simular a dinâmica do TFIM, você usa uma decomposição de Trotter do operador de evolução unitária eiHte^{-iHt}, implementado por meio de camadas de portas RX e RZZ com base em um gráfico personalizado de triângulos de spin conectados. A simulação explora como a magnetização Z\langle Z \rangle evolui com o aumento das etapas de Trotter.

O desempenho da implementação proposta do TFIM é avaliado comparando simulações sem ruído com backends com ruído. Os recursos aprimorados de execução e supressão de erros do Fire Opal são usados para atenuar o efeito do ruído no hardware real, produzindo estimativas mais confiáveis de observáveis de spin, como Zi\langle Z_i \rangle e correlacionadores ZiZj\langle Z_i Z_j \rangle.


Requisitos

Antes de iniciar este tutorial, verifique se você tem os seguintes itens instalados:

  • Qiskit SDK v1.4 ou posterior, com suporte para visualização
  • Qiskit Runtime v0.40 ou posterior (pip install qiskit-ibm-runtime)
  • Qiskit Functions Catalog v0.9.0 (pip install qiskit-ibm-catalog)
  • Fire Opal SDK v9.0.2 ou posterior (pip install fire-opal)
  • Q-CTRL Visualizer v8.0.2 ou posterior (pip install qctrl-visualizer)

Instalação

Primeiro, autentique-se usando sua chave de API IBM Quantum. Em seguida, selecione a função do Qiskit da seguinte maneira. (Este código pressupõe que você já tenha salvo sua conta no seu ambiente 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")

Passo 1: Mapear entradas clássicas para um problema quântico

Gerar gráfico TFIM

Começamos definindo a rede de spins e os acoplamentos entre eles. Neste tutorial, a estrutura é construída a partir de triângulos conectados dispostos em uma cadeia linear. Cada triângulo consiste em três nós conectados em um loop fechado, e a cadeia é formada pela ligação de um nó de cada triângulo ao triângulo anterior.

A função auxiliar connected_triangles_adj_matrix cria a matriz de adjacência para essa estrutura. Para uma cadeia de nn triângulos, o gráfico resultante contém 2n+12n+1 nós.

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 a estrutura que acabamos de definir, podemos desenhar a cadeia de triângulos conectados e rotular cada nó. A função abaixo cria o gráfico para um número escolhido de triângulos e o exibe.

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, usaremos uma corrente 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

Colorir arestas de grafos

Para implementar o acoplamento spin-spin, é útil agrupar as bordas que não se sobrepõem. Isso nos permite aplicar portas de dois qubits em paralelo. Podemos fazer isso com um procedimento simples de coloração de bordas [1], que atribui uma cor a cada borda para que as bordas que se encontram no mesmo nó sejam colocadas em 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())

Etapa 2: Otimizar o problema para execução em hardware quântico

Gerar circuitos trotterizados em gráficos de spin

Para simular a dinâmica do TFIM, construímos circuitos que aproximam o operador de evolução 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 .

Usamos uma decomposição de Trotter de segunda ordem:

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

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

  • O termo HXH_X é implementado com camadas de rotações RX .
  • O termo HZH_Z é implementado com camadas de portas RZZ ao longo das bordas do gráfico de interação.

Os ângulos dessas portas são determinados pelo campo transversal hh, pela constante de acoplamento JJ e pela etapa de tempo Δt\Delta t. Ao empilhar várias etapas de Trotter, geramos circuitos de profundidade crescente que se aproximam da dinâmica do sistema. As funções generate_tfim_circ_custom_graph e trotter_circuits constroem um circuito quântico Trotterizado a partir de um gráfico de interação de spin arbitrário.

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

Estimar magnetizações de um único qubit Zi\langle Z_i \rangle

Para estudar a dinâmica do modelo, queremos medir a magnetização de cada qubit, definida pelo valor de expectativa Zi=ψZiψ\langle Z_i \rangle = \langle \psi | Z_i | \psi \rangle.

Nas simulações, podemos calcular isso diretamente a partir dos resultados das medições. A função z_expectation processa as contagens de bitstring e retorna o valor de Zi\langle Z_i \rangle para um índice de qubit escolhido. No hardware real, avaliamos a mesma quantidade especificando o operador Pauli usando a função generate_z_observables e, em seguida, o backend calcula o valor da 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)

Agora, definimos os parâmetros para gerar os circuitos Trotterizados. Neste tutorial, a rede é uma cadeia de 20 triângulos conectados, o que corresponde a um 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

Passo 3: Execute usando Qiskit primitives

Executar simulação MPS

A lista de circuitos Trotterizados é executada usando o simulador matrix_product_state com uma escolha arbitrária de 40964096 tiros. O método MPS fornece uma aproximação eficiente da dinâmica do circuito, com precisão determinada pela dimensão da ligação escolhida. Para os tamanhos de sistema considerados aqui, a dimensão padrão da ligação é suficiente para capturar a dinâmica da magnetização com alta fidelidade. As contagens brutas são normalizadas e, a partir delas, calculamos os valores de expectativa de um único qubit Zi\langle Z_i \rangle em cada etapa de Trotter. Por fim, calculamos a média de todos os qubits para obter uma única curva que mostra como a magnetização muda com o 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)

Executar no 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)

Execute em hardware com Fire Opal

Avaliamos a dinâmica da magnetização em hardware quântico real. O Fire Opal fornece uma função Qiskit que amplia a primitiva padrão do Qiskit Runtime Estimator com supressão automatizada de erros e gerenciamento de desempenho. Enviamos os circuitos Trotterizados diretamente para um backend IBM®, enquanto o Fire Opal cuida da execução com reconhecimento de ruído.

Preparamos uma lista de pubs, em que cada item contém um circuito e os observáveis Pauli-Z correspondentes. Eles são passados para a função estimadora do Fire Opal, que retorna os valores de expectativa Zi\langle Z_i \rangle para cada qubit em cada etapa do Trotter. Os resultados podem, então, ser calculados pela média dos qubits para obter a curva de magnetização do 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()

Etapa 4: Pós-processamento e retorno do resultado no formato clássico desejado

Por fim, comparamos a curva de magnetização do simulador com os resultados obtidos no hardware real. A plotagem de ambos lado a lado mostra a proximidade da execução do hardware com o Fire Opal com a linha de base sem ruído nas etapas do 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

Referências

[1] Coloração de gráficos. Wikipedia. Recuperado em 15 de setembro de 2025, de https://en.wikipedia.org/wiki/Graph _coloring


Pesquisa tutorial

Reserve um minuto para fazer comentários sobre este tutorial. Suas percepções nos ajudarão a melhorar nossas ofertas de conteúdo e a experiência do usuário.

Link para a pesquisa

Esta página foi útil?
Relate um bug, erro de digitação ou solicite conteúdo no GitHub.