Перейти до основного вмісту

Модель Ізінга в поперечному полі з керуванням продуктивністю Q-CTRL

Оцінка використання: 2 хвилини на процесорі Heron r2. (ПРИМІТКА: Це лише оцінка. Ваш час виконання може відрізнятися.)

Передумови

Модель Ізінга в поперечному полі (TFIM) є важливою для вивчення квантового магнетизму та фазових переходів. Вона описує набір спінів, розташованих на ґратці, де кожен спін взаємодіє зі своїми сусідами, водночас перебуваючи під впливом зовнішнього магнітного поля, яке спричиняє квантові флуктуації.

Поширеним підходом до симуляції цієї моделі є використання декомпозиції Троттера для апроксимації оператора еволюції в часі, побудова схем, які чергують одно-кубітні обертання та двокубітні взаємодії з заплутуванням. Однак ця симуляція на реальному обладнанні є складною через шум та декогеренцію, що призводить до відхилень від справжньої динаміки. Щоб подолати це, ми використовуємо інструменти придушення помилок та керування продуктивністю Fire Opal від Q-CTRL, які надаються як функція Qiskit (див. документацію Fire Opal). Fire Opal автоматично оптимізує виконання схем, застосовуючи динамічне роз'єднання, розширене розміщення, маршрутизацію та інші техніки придушення помилок, спрямовані на зменшення шуму. Завдяки цим покращенням результати на обладнанні краще узгоджуються з безшумними симуляціями, і таким чином ми можемо вивчати динаміку намагніченості TFIM з вищою точністю.

У цьому посібнику ми:

  • Побудуємо гамільтоніан TFIM на графі з'єднаних трикутників спінів
  • Симулюватимемо еволюцію в часі з троттеризованими схемами на різних глибинах
  • Обчислимо та візуалізуємо одно-кубітні намагніченості Zi\langle Z_i \rangle з часом
  • Порівняємо базові симуляції з результатами запусків на обладнанні з використанням керування продуктивністю Fire Opal від Q-CTRL

Огляд

Модель Ізінга в поперечному полі (TFIM) є моделлю квантових спінів, яка відображає суттєві особливості квантових фазових переходів. Гамільтоніан визначається як:

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

де ZiZ_i та XiX_i є операторами Паулі, які діють на кубіт ii, JJ є силою зв'язку між сусідніми спінами, а hh є силою поперечного магнітного поля. Перший доданок представляє класичні феромагнітні взаємодії, тоді як другий вносить квантові флуктуації через поперечне поле. Щоб симулювати динаміку TFIM, Ви використовуєте декомпозицію Троттера унітарного оператора еволюції eiHte^{-iHt}, реалізованого через шари RX та RZZ гейтів на основі користувацького графа з'єднаних трикутників спінів. Симуляція досліджує, як намагніченість Z\langle Z \rangle еволюціонує зі збільшенням кроків Троттера.

Продуктивність запропонованої реалізації TFIM оцінюється шляхом порівняння безшумних симуляцій з шумними бекендами. Функції покращеного виконання та придушення помилок Fire Opal використовуються для пом'якшення впливу шуму в реальному обладнанні, що дає більш надійні оцінки спінових спостережуваних величин, таких як Zi\langle Z_i \rangle та кореляторів ZiZj\langle Z_i Z_j \rangle.

Вимоги

Перед початком цього посібника переконайтеся, що у Вас встановлено наступне:

  • Qiskit SDK v1.4 або пізніше, з підтримкою візуалізації
  • Qiskit Runtime v0.40 або пізніше (pip install qiskit-ibm-runtime)
  • Qiskit Functions Catalog v0.9.0 (pip install qiskit-ibm-catalog)
  • Fire Opal SDK v9.0.2 або пізніше (pip install fire-opal)
  • Q-CTRL Visualizer v8.0.2 або пізніше (pip install qctrl-visualizer)

Налаштування

Спочатку виконайте автентифікацію, використовуючи Ваш ключ API IBM Quantum. Потім виберіть функцію Qiskit наступним чином. (Цей код передбачає, що Ви вже зберегли свій обліковий запис у Вашому локальному середовищі.)

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

Крок 1: Відображення класичних входів на квантову задачу

Генерація графа TFIM

Ми починаємо з визначення ґратки спінів та зв'язків між ними. У цьому посібнику ґратка побудована з з'єднаних трикутників, розташованих у лінійний ланцюг. Кожен трикутник складається з трьох вузлів, з'єднаних у замкнутий контур, і ланцюг формується шляхом з'єднання одного вузла кожного трикутника з попереднім трикутником.

Допоміжна функція connected_triangles_adj_matrix будує матрицю суміжності для цієї структури. Для ланцюга з nn трикутників результуючий граф містить 2n+12n+1 вузлів.

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

Щоб візуалізувати ґратку, яку ми щойно визначили, можна побудувати ланцюг з'єднаних трикутників та підписати кожен вузол. Функція нижче будує граф для обраної кількості трикутників і відображає його.

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

Для цього посібника ми використаємо ланцюг з 20 трикутників.

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

Розфарбування ребер графа

Для реалізації спін-спінового зв'язку корисно групувати ребра, які не перекриваються. Це дозволяє нам застосовувати двокубітні гейти паралельно. Ми можемо зробити це за допомогою простої процедури розфарбування ребер [1], яка призначає колір кожному ребру так, щоб ребра, які зустрічаються в одному вузлі, були розміщені в різних групах.

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

Крок 2: Оптимізація задачі для виконання на квантовому обладнанні

Генерація троттеризованих схем на графах спінів

Щоб симулювати динаміку TFIM, ми будуємо схеми, які апроксимують оператор еволюції в часі.

U(t)=eiHt,деH=Ji,jZiZjhiXi.U(t) = e^{-i H t}, \quad \text{де} \quad H = -J \sum_{\langle i,j \rangle} Z_i Z_j - h \sum_i X_i .

Ми використовуємо декомпозицію Троттера другого порядку:

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

де HX=hiXiH_X = -h \sum_i X_i та HZ=Ji,jZiZjH_Z = -J \sum_{\langle i,j \rangle} Z_i Z_j.

  • Доданок HXH_X реалізується з шарами обертань RX.
  • Доданок HZH_Z реалізується з шарами гейтів RZZ вздовж ребер графа взаємодії.

Кути цих гейтів визначаються поперечним полем hh, константою зв'язку JJ та часовим кроком Δt\Delta t. Складаючи кілька кроків Троттера, ми генеруємо схеми зростаючої глибини, які апроксимують динаміку системи. Функції generate_tfim_circ_custom_graph та trotter_circuits будують троттеризовану квантову схему з довільного графа взаємодії спінів.

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

Оцінка одно-кубітних намагніченостей Zi\langle Z_i \rangle

Щоб вивчити динаміку моделі, ми хочемо виміряти намагніченість кожного кубіта, визначену очікуваним значенням Zi=ψZiψ\langle Z_i \rangle = \langle \psi | Z_i | \psi \rangle.

У симуляціях ми можемо обчислити це безпосередньо з результатів вимірювань. Функція z_expectation обробляє підрахунки бітових рядків і повертає значення Zi\langle Z_i \rangle для обраного індексу кубіта. На реальному обладнанні ми оцінюємо ту саму величину, вказуючи оператор Паулі за допомогою функції generate_z_observables, а потім бекенд обчислює очікуване значення.

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)

Тепер визначимо параметри для генерації троттеризованих схем. У цьому посібнику ґратка є ланцюгом з 20 з'єднаних трикутників, що відповідає системі з 41 кубіта.

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

Крок 3: Виконання з використанням примітивів Qiskit

Запуск MPS симуляції

Список троттеризованих схем виконується з використанням симулятора matrix_product_state з довільним вибором 40964096 знімків. Метод MPS забезпечує ефективну апроксимацію динаміки схеми, з точністю, що визначається обраним розміром зв'язку. Для розмірів системи, які розглядаються тут, розмір зв'язку за замовчуванням є достатнім для захоплення динаміки намагніченості з високою точністю. Необроблені відліки нормалізуються, і з них ми обчислюємо очікувані значення одного кубіта Zi\langle Z_i \rangle на кожному кроці Троттера. Нарешті, ми обчислюємо середнє значення по всіх кубітах, щоб отримати одну криву, яка показує, як намагніченість змінюється з часом.

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)

Запуск на обладnanні

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)

Запуск на обладnanні з Fire Opal

Ми оцінюємо динаміку намагніченості на реальному квантовому обладnanні. Fire Opal надає функцію Qiskit, яка розширює стандартний примітив Estimator з Qiskit Runtime автоматичним придушенням помилок та керуванням продуктивністю. Ми надсилаємо троттеризовані схеми безпосередньо на бекенд IBM®, тоді як Fire Opal обробляє виконання з урахуванням шуму.

Ми готуємо список pubs, де кожен елемент містить схему та відповідні спостережувані Паулі-Z. Вони передаються функції оцінювача Fire Opal, яка повертає очікувані значення Zi\langle Z_i \rangle для кожного кубіта на кожному кроці Троттера. Результати можна потім усереднити по кубітах, щоб отримати криву намагніченості з обладнання.

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

Крок 4: Постобробка та повернення результату в бажаному класичному форматі

Нарешті, ми порівнюємо криву намагніченості з симулятора з результатами, отриманими на реальному обладnanні. Побудова обох поруч показує, наскільки близько виконання на обладnanні з Fire Opal відповідає базовій лінії без шуму на кроках Троттера.

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

Посилання

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

Опитування до навчального посібника

Будь ласка, приділіть хвилину, щоб надати відгук про цей навчальний посібник. Ваші думки допоможуть нам покращити наші пропозиції контенту та досвід користувачів.

Link to survey