Pular para o conteúdo principal

Modelo de Ising de Campo Transverso com Gerenciamento de Desempenho da Q-CTRL

Estimativa de uso: 2 minutos em um processador Heron r2. (NOTA: Esta é apenas uma estimativa. Seu tempo de execução pode variar.)

Contexto

O Modelo de Ising de Campo Transverso (TFIM) é importante para o estudo do magnetismo quântico e transições de fase. Ele descreve um conjunto de spins organizados em uma rede, onde cada spin interage com seus vizinhos enquanto também é influenciado por um campo magnético externo que impulsiona flutuações quânticas.

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

Neste tutorial, iremos:

  • Construir o Hamiltoniano TFIM em um grafo de triângulos de spin conectados
  • Simular a evolução temporal com circuitos Trotterizados em diferentes profundidades
  • Calcular e visualizar magnetizações de qubit único Zi\langle Z_i \rangle ao longo do tempo
  • Comparar simulações 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 Transverso (TFIM) é um modelo de spin quântico que captura características essenciais de transições de fase quânticas. O Hamiltoniano é definido como:

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

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

O desempenho da implementação TFIM proposta é avaliado comparando simulações sem ruído com backends ruidosos. Os recursos aprimorados de execução e supressão de erro do Fire Opal são usados para mitigar o efeito do ruído em 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, certifique-se de ter o seguinte instalado:

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

Configuração

Primeiro, autentique usando sua chave de API IBM Quantum. Em seguida, selecione a Função Qiskit da seguinte forma. (Este código assume que você já salvou sua conta em seu ambiente local.)

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib networkx numpy qctrlvisualizer qiskit qiskit-aer qiskit-ibm-catalog qiskit-ibm-runtime
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 grafo TFIM

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

A função auxiliar connected_triangles_adj_matrix constrói a matriz de adjacência para esta estrutura. Para uma cadeia de nn triângulos, o grafo 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 rede que acabamos de definir, podemos plotar a cadeia de triângulos conectados e rotular cada nó. A função abaixo constrói o grafo 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 cadeia de 20 triângulos.

n_triangles = 20
n_qubits = 2 * n_triangles + 1
plot_triangle_chain(n_triangles, side=1.0)
plt.show()

Output of the previous code cell

Colorindo arestas do grafo

Para implementar o acoplamento spin-spin, é útil agrupar arestas 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 arestas [1], que atribui uma cor a cada aresta de modo que arestas 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())

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

Gerar circuitos Trotterizados em grafos de spin

Para simular a dinâmica do TFIM, construímos circuitos que aproximam o operador de evolução temporal.

U(t)=eiHt,ondeH=Ji,jZiZjhiXi.U(t) = e^{-i H t}, \quad \text{onde} \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 arestas do grafo de interação.

Os ângulos dessas portas são determinados pelo campo transverso hh, pela constante de acoplamento JJ e pelo passo de tempo Δt\Delta t. Ao empilhar múltiplos passos de Trotter, geramos circuitos de profundidade crescente que aproximam a 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 grafo arbitrário de interação de spin.

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

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

Em simulações, podemos calcular isso diretamente a partir dos resultados das medições. A função z_expectation processa as contagens de bitstrings e retorna o valor de Zi\langle Z_i \rangle para um índice de qubit escolhido. Em hardware real, avaliamos a mesma quantidade especificando o operador de Pauli usando a função generate_z_observables, e então o backend calcula o valor esperado.

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: Executar usando primitivas Qiskit

Executar simulação MPS

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

Executar em hardware com Fire Opal

Avaliamos a dinâmica de magnetização em hardware quântico real. O Fire Opal fornece uma função Qiskit que estende a primitiva Estimator padrão do Qiskit Runtime com supressão automática de erros e gerenciamento de desempenho. Submetemos os circuitos trotterizados diretamente a um backend IBM® enquanto o Fire Opal gerencia a execução com consciência de ruído.

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

Passo 4: Pós-processar e retornar o resultado no formato clássico desejado

Finalmente, comparamos a curva de magnetização do simulador com os resultados obtidos em hardware real. Plotar ambos lado a lado mostra quão próxima a execução em hardware com Fire Opal corresponde à linha de base sem ruído ao longo dos passos 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 of the previous code cell

Referências

[1] Graph coloring. Wikipedia. Retrieved September 15, 2025, from https://en.wikipedia.org/wiki/Graph_coloring

Pesquisa do tutorial

Por favor, reserve um minuto para fornecer feedback sobre este tutorial. Suas percepções nos ajudarão a melhorar nossas ofertas de conteúdo e experiência do usuário.

Link to survey