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 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:
onde e são operadores de Pauli agindo no qubit , é a força de acoplamento entre spins vizinhos, e é 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 , 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 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 e correlacionadores .
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 triângulos, o grafo resultante contém 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()

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.
Usamos uma decomposição de Trotter de segunda ordem:
onde e .
- O termo é implementado com camadas de rotações
RX. - O termo é implementado com camadas de portas
RZZao longo das arestas do grafo de interação.
Os ângulos dessas portas são determinados pelo campo transverso , pela constante de acoplamento e pelo passo de tempo . 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
Para estudar a dinâmica do modelo, queremos medir a magnetização de cada qubit, definida pelo valor esperado .
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 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 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 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 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,
)

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.