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

Симуляція розсіювання нейтронів за допомогою робочого процесу Serverless з динамікою AQC + Троттера

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

Результати навчання

  • Як спектр непружного розсіювання нейтронів відображається на динамічний структурний фактор S(q,ω)S(q, \omega) одновимірного квантового магніту.

  • Як підготувати основний стан KCuF3_3 (ізотропний гейзенбергівський) за допомогою групи ренормалізації матриці густини (DMRG) та максимізації вірності стану матричного добутку (MPS).

  • Як виконати часову еволюцію Троттера, стиснення схем наближеного квантового компілювання (AQC) та виконання зі зменшенням похибок в одному виклику функції.

  • Як здійснити постобробку часового ряду σz(t)\langle \sigma_z \rangle(t) по вузлах у S(q,ω)S(q, \omega) та ідентифікувати двоспінонний континуум.

Передумови

Передумови

Непружне розсіювання нейтронів вимірює динамічний структурний фактор S(q,ω)S(q, \omega) — просторово-часове перетворення Фур'є кореляційної функції спін-спін, тому відтворення S(q,ω)S(q, \omega) з мікроскопічної спінової моделі є прямим, спростовуваним тестом квантової симуляції. У цьому туторіалі досліджується KCuF3_3 — спін-12\frac{1}{2} антиферомагнітний гейзенбергівський ланцюг, збудження якого є не одиничними спін-флипами, а парами фракціоналізованих спінонів: замість чіткої дисперсії магнонів, S(q,ω)S(q, \omega) показує широкий двоспінонний континуум, обмежений знизу π2sinq\tfrac{\pi}{2}|\sin q| і зверху πsin(q/2)\pi|\sin(q/2)|. Це пунктирні криві на наступних графіках. Повна фізика та порівняння з виміряними нейтронними даними висвітлені в оригінальному туторіалі та в Lee et al., arXiv:2603.15608.

Квантовий робочий процес відображає експеримент з розсіювання:

  1. Підготувати основний стан ланцюга ψ0|\psi_0\rangle.

  2. Збудити його локальним збуренням у центральному вузлі, поворотом π/2\pi/2 ZZ, що імітує передачу імпульсу та енергії від нейтрона.

  3. Виконати часову еволюцію під гамільтоніаном Гейзенберга, eiHte^{-iHt}, за допомогою формули добутку Троттера.

  4. Виміряти намагніченість по вузлах σzj(t)\langle \sigma_z^j \rangle(t). Як функція вузла jj та часу tt, це точно уповільнена функція Гріна GR(j,jc,t)G^R(j, j_c, t), тому перед перетворенням Фур'є на кроці 5 не потрібне жодне перетворення.

  5. Перетворити GRG^R у S(q,ω)S(q, \omega) за допомогою перетворення Фур'є.

Проблеми можуть виникнути на кроці 3, коли точні схеми Троттера для тривалих еволюцій стають надто глибокими для апаратного забезпечення. AQC з тензорними мережами вирішує цю проблему, стискаючи блок кроків Троттера у фіксований, неглибокий параметризований анзац, вірність стану якого до точної еволюції максимізується класично за допомогою MPS-симулятора (arXiv:2301.08609). AQC Dynamics Template упаковує все це квантове ядро (синтез Троттера, стиснення AQC та виконання зі зменшенням похибок) в один виклик:

PRE (цей блокнот)ФУНКЦІЯ (aqc-dynamics-function)POST (цей блокнот)
Основний стан з DMRG плюс максимізація вірності MPS, з нейтронним поштовхом, вбудованим у ту саму схемуСинтез Троттера → стиснення AQC → виконання на statevector, fake або runtime, повертаючи σzj(t)\langle \sigma_z^j \rangle(t) по вузлахS(q,ω)S(q, \omega), динамічний структурний фактор

Специфічна для експерименту робота залишається тут, у блокноті: підготовка основного стану (PRE) та постобробка S(q,ω)S(q, \omega) (POST). Два кроки, насичені квантовими обчисленнями, — стиснення та виконання — виконуються всередині функції.

Цей туторіал є супутником до Simulate neutron scattering in quantum materials with quantum circuits, який будує той самий експеримент вбудовано: та сама модель KCuF3_3, підготовка основного стану, нейтронний поштовх та постобробка, з синтезом Троттера, стисненням AQC та виконанням зі зменшенням похибок, розписаними крок за кроком. Прочитай той туторіал, щоб дізнатися, як працює стиснення AQC. Прочитай цей, щоб виконати той самий експеримент через розгорнутий шаблон функції: квантове ядро стає одним викликом функції, а багатогодинне стиснення AQC виконується всередині воркера Serverless, а не на твоїй машині, тому тобі не потрібна система HPC або відкрите ядро, поки воно виконується. Той самий виклик також керує іншими одновимірними експериментами з динаміки.

Вимоги

Перш ніж почати цей туторіал, переконайся, що маєш таке:

  • Функцію, розгорнуту в твоєму обліковому записі Qiskit Serverless. Спочатку запусти супутній шаблон функції: Deploy and run the AQC + Trotter dynamics function template. Цей посібник проведе тебе через отримання вихідних файлів та завантаження функції у твій обліковий запис. Цей туторіал лише викликає розгорнуту функцію.

  • Облікові дані IBM Quantum®, збережені для QiskitServerless (див. шаблон функції). Обидва приклади в цьому туторіалі викликають розгорнуту функцію, тому обом вони потрібні.

  • Qiskit SDK v2.0 або новішу версію (pip install qiskit).

  • Клієнт Qiskit IBM Catalog (pip install qiskit-ibm-catalog).

  • NumPy, SciPy та Matplotlib (pip install numpy scipy matplotlib). SciPy 1.14 або новіша версія потрібна для оптимізатора COBYQA, що використовується в підготовці основного стану.

  • Стек тензорних мереж AQC, оскільки підготовка основного стану на кроці 1 виконується локально в цьому блокноті: pip install 'qiskit-addon-aqc-tensor[quimb-jax]==0.3.1'.

Перший виклик щойно розгорнутої функції очікує, поки воркер Serverless встановлює свої залежності, тому очікуй додаткову затримку під час цього запуску.

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

Імпортуй бібліотеки та визнач специфічні для експерименту допоміжні функції, що використовуються пізніше: build_gs_ansatz (варіаційний анзац гамільтоніана, або HVA, для підготовки основного стану), prepare_ground_state (DMRG плюс максимізація вірності MPS) та get_spectrum, plot_green і plot_spectrum (постобробка S(q,ω)S(q, \omega)). Вони адаптовані з оригінального туторіалу з розсіювання нейтронів.

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-addon-aqc-tensor qiskit-ibm-catalog quimb scipy
from functools import partial

import matplotlib.pyplot as plt
import numpy as np
import scipy.optimize

import quimb.tensor as qtn
from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from qiskit_addon_aqc_tensor.simulation import tensornetwork_from_circuit
from qiskit_addon_aqc_tensor.simulation.quimb import QuimbSimulator
from qiskit_ibm_catalog import QiskitServerless
# Dynamical structure factor via discrete Fourier transform

def get_spectrum(n, Gjjc, dt, time_steps, q_steps, w_steps):
"""Compute the dynamical structure factor from the retarded Green's function.

Uses the center-site approximation and a discrete Fourier transform.
"""
green = Gjjc / 4 # sigma -> S=1/2
omega_max = np.pi / dt
qpoints = np.arange(0, 2 * np.pi, 2 * np.pi / q_steps)
omegas = np.arange(0, omega_max, omega_max / w_steps)
green_map = np.zeros((omegas.shape[0], qpoints.shape[0]))
center = n // 2 - 1
for iw, w in enumerate(omegas):
exponent = np.exp(1j * w * dt * np.arange(1, time_steps + 1))
S_w = np.dot(green.T, exponent) * dt
for iq, q in enumerate(qpoints):
q_matrix = np.exp(-1j * q * np.arange(-center, center + 2, 1))
green_map[iw, iq] = np.imag(np.dot(S_w, q_matrix))
return green_map

# Plotting helpers

def plot_spectrum(
dsf,
dt,
q_steps,
w_steps,
lower_bound=False,
upper_bound=False,
title=None,
):
"""Heat-map of the dynamical structure factor."""
omega_max = np.pi / dt
qpoints = np.arange(0, 2 * np.pi, 2 * np.pi / q_steps)
omegas = np.arange(0, omega_max, omega_max / w_steps)
x, y = np.meshgrid(qpoints, omegas)
fig, ax = plt.subplots(figsize=(8, 5))
c = ax.pcolormesh(x, y, dsf / np.max(dsf), cmap="viridis", shading="auto")
fig.colorbar(c, ax=ax, label="Normalized intensity")
if lower_bound:
ax.plot(
qpoints,
np.pi * np.abs(np.sin(qpoints)) / 2,
"--",
color="white",
lw=1.5,
label="Lower bound",
)
if upper_bound:
ax.plot(
qpoints,
np.pi * np.abs(np.sin(qpoints / 2)),
"--",
color="red",
lw=1.5,
label="Upper bound",
)
ax.set_ylim(0, 3.6)
ax.set_xlim(0, 2 * np.pi - 2 * np.pi / q_steps)
ax.set_xlabel(r"$q$", fontsize=16)
ax.set_ylabel(r"$\tilde{\omega} = \omega / J$", fontsize=16)
ax.set_xticks([0, np.pi / 2, np.pi, 3 * np.pi / 2, 2 * np.pi])
ax.set_xticklabels(["0", r"$\pi/2$", r"$\pi$", r"$3\pi/2$", r"$2\pi$"])
if lower_bound or upper_bound:
ax.legend(loc="upper right", fontsize=11)
if title:
ax.set_title(title, fontsize=14)
plt.tight_layout()
plt.show()

def plot_green(n, Gjjc, time_steps, dt, title=None):
"""Heat-map of the retarded Green's function in real space and time."""
fig, ax = plt.subplots(figsize=(8, 6))
t_axis = np.arange(1, time_steps + 1) * dt
site_axis = np.arange(n)
x, y = np.meshgrid(t_axis, site_axis)
c = ax.pcolormesh(
x,
y,
np.real(Gjjc).T,
cmap="RdBu",
vmax=0.5,
vmin=-0.5,
shading="auto",
)
fig.colorbar(c, ax=ax, label=r"Re $G^R(j, j_c, t)$")
ax.set_xlabel(r"Time ($t / J^{-1}$)", fontsize=16)
ax.set_ylabel("Site index $j$", fontsize=16)
if title:
ax.set_title(title, fontsize=14)
plt.tight_layout()
plt.show()

# Variational ground-state ansatz (HVA)

def _apply_xxz_pair_gate(qc, q0, q1, theta):
"""Apply the parameterized XXZ-type two-qubit gate used in the HVA."""
qc.cx(q0, q1)
qc.rz(theta, q1)
qc.h(q0)
qc.rz(theta + np.pi / 2, q0)
qc.cx(q0, q1)
qc.rz(-theta, q1)
qc.h(q1)
qc.cx(q1, q0)
qc.rz(np.pi / 2, q1)
qc.rz(-np.pi / 2, q0)
qc.h(q1)
qc.h(q0)

def build_gs_ansatz(n, params, layers):
"""Build the Hamiltonian variational ansatz (HVA) circuit for
ground-state preparation of the 1D Heisenberg model.

Starts from a product of singlet pairs and applies alternating
odd/even layers of parameterized XXZ gates. For layer r,
params[2 * r] is the odd-layer (inter-pair) angle and
params[2 * r + 1] is the even-layer (intra-pair) angle.
"""
qc = QuantumCircuit(n)
# Initial singlet product state
for i in range(n // 2):
qc.x(2 * i)
qc.x(2 * i + 1)
qc.h(2 * i + 1)
qc.cx(2 * i + 1, 2 * i)
# Variational layers
for r in range(layers):
for i in range(1, (n + 1) // 2): # odd layer
_apply_xxz_pair_gate(qc, 2 * i - 1, 2 * i, params[2 * r])
for i in range(n // 2): # even layer
_apply_xxz_pair_gate(qc, 2 * i, 2 * i + 1, params[2 * r + 1])
return qc

def prepare_ground_state(n, gs_layers=5, max_bond=128, cutoff=1e-8):
"""Prepare the KCuF3 (isotropic Heisenberg) ground state as a QuantumCircuit.

Runs DMRG (quimb MPO + DMRG2) to get the chain's ground state, then optimizes
the HVA angles to maximize the MPS overlap |<psi_ansatz|psi_DMRG>|^2. No exact
diagonalization, so it scales to larger n.
"""
J = Jz = 1.0
builder = qtn.SpinHam1D(S=1 / 2)
builder += J * 0.5, "+", "-"
builder += J * 0.5, "-", "+"
builder += Jz, "Z", "Z"
H_mpo = builder.build_mpo(L=n)
dmrg = qtn.DMRG2(H_mpo)
dmrg.solve(tol=1e-8, verbosity=0)

gs_sim = QuimbSimulator(
quimb_circuit_factory=partial(
qtn.CircuitMPS, gate_opts=dict(cutoff=cutoff, max_bond=max_bond)
),
autodiff_backend="jax",
)

def gs_infidelity(params):
psi = tensornetwork_from_circuit(
build_gs_ansatz(n, params, gs_layers), gs_sim
).psi
return 1 - abs(psi.H @ dmrg.state) ** 2

# Seed and optimizer match the original tutorial. Each layer starts at
# [0, pi/2]: an odd-layer angle of 0 makes the inter-pair gate the identity,
# and an even-layer angle of pi/2 makes the intra-pair gate a SWAP (since
# 0.5 * (XX + YY + ZZ) = SWAP - I/2). That puts the seed at the singlet-pair
# product limit, which is already a decent approximation to the Heisenberg
# ground state, so the optimizer only has to refine it. The small jitter
# (fixed RNG seed, so runs are reproducible) breaks the exact symmetry
# between layers; COBYQA then runs for up to 100 iterations.
rng = np.random.default_rng(12345)
x0 = np.tile([0.0, np.pi / 2], gs_layers) + rng.normal(
scale=0.1, size=2 * gs_layers
)
result_gs = scipy.optimize.minimize(
gs_infidelity, x0, method="COBYQA", options={"maxiter": 100}
)
print(f"DMRG ground-state energy: {dmrg.energy:.6f}")
print(f"GS fidelity: {1 - result_gs.fun:.4f}")
return build_gs_ansatz(n, result_gs.x, gs_layers)

print("Setup complete - helpers defined.")
Setup complete - helpers defined.

Завантаження шаблону функції

Підключися до Qiskit Serverless та завантаж розгорнуту функцію aqc-dynamics-function. Обидва приклади в цьому туторіалі викликають один і той самий дескриптор fn, тому функція завантажується лише один раз, тут.

# Credentials are read from the account saved once via QiskitServerless.save_account(...)
serverless = QiskitServerless()
fn = serverless.load("aqc-dynamics-function")

Приклад на малому симуляторі

Спочатку ми запускаємо весь робочий процес на невеликому ланцюгу з 10 вузлів, використовуючи точний бекенд statevector. Це підтверджує конвеєр PRE → ФУНКЦІЯ → POST, перш ніж витрачати будь-який час QPU.

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

Побудуй гамільтоніан KCuF3_3 як SparsePauliOp (ізотропний гейзенбергівський: XX+YY+ZZXX + YY + ZZ з коефіцієнтом зв'язку 14\tfrac14 на кожному зв'язку найближчих сусідів; рядки — це оператори Паулі, тому 14\tfrac14 дає спін-12\frac{1}{2} зв'язок). Підготуй основний стан за допомогою DMRG плюс максимізації вірності MPS, потім вбудуй нейтронний поштовх: поворот π/2\pi/2 ZZ у центральному вузлі. Підготовлена схема — це те, що ми передаємо функції як initial_state. Ми залишаємо observables за замовчуванням (по вузлах ZZ), що є саме тим зчитуванням σzj(t)\langle \sigma_z^j \rangle(t), яке потрібне нейтронному робочому процесу.

n = 10
dt = 0.6 # physical time per Trotter step (also the omega-axis unit in POST)
time_steps = 10
center = n // 2 - 1

# MPS-simulator settings, shared by the ground-state prep here and the AQC
# compression inside the function (matches the original tutorial).
mps_max_bond = 32
mps_cutoff = 1e-8

# 1D isotropic Heisenberg (KCuF3) Hamiltonian on n qubits
H = SparsePauliOp.from_sparse_list(
[(p, [i, i + 1], 0.25) for i in range(n - 1) for p in ("XX", "YY", "ZZ")],
num_qubits=n,
)

# Ground state (DMRG + fidelity max) + neutron kick baked into the same circuit
gs_circuit = prepare_ground_state(
n, gs_layers=3, max_bond=mps_max_bond, cutoff=mps_cutoff
)
gs_circuit.rz(
np.pi / 2, center
) # exp(-i (pi/2)/2 Z_center): the neutron perturbation
print(
f"Prepared {n}-qubit ground state with the neutron kick at site {center}."
)
DMRG ground-state energy: -4.258035
GS fidelity: 0.9841
Prepared 10-qubit ground state with the neutron kick at site 4.

Кроки 2 та 3: стиснення та виконання за допомогою шаблону функції

У ручному робочому процесі це два окремі етапи: оптимізація схем для апаратного забезпечення (крок 2) та їх виконання (крок 3). Шаблон функції об'єднує обидва в один виклик. Він виконує синтез Троттера, стиснення AQC та транспіляцію для апаратного забезпечення, а потім виконує схеми (тут — на точному симуляторі, пізніше — з вбудованим зменшенням похибок на апаратному забезпеченні). Два параметри налаштування — це aqc_segments (план стиснення) та aqc_options (налаштування MPS та оптимізатора). Кожен сегмент {"n_steps": k, "ansatz_steps": m} стискає k послідовних кроків Троттера в анзац, побудований з m-крокової цілі Троттера, а будь-які кроки понад sum(n_steps) виконуються як звичайний Троттер. Ранні кроки з низькою заплутаністю добре стискаються в неглибокий (ansatz_steps=1) анзац, тому тут ми стискаємо перші три кроки в одношаровий анзац, а наступні два — у глибший двошаровий анзац; решта п'ять із 10 кроків Троттера виконуються як звичайний Троттер. Для aqc_options ми повторюємо оригінальний туторіал: розмір зв'язку MPS max_bond=32, cutoff=1e-8 та оптимізатор L-BFGS-B з обмеженням у 100 ітерацій.

Виклич функцію, завантажену в розділі Налаштування. backend="statevector" запускає точний еталонний шлях: без часу QPU, зі схемами, що виконуються на точному симуляторі станів-векторів усередині воркера serverless (збережений обліковий запис Qiskit Serverless все одно потрібен для його виклику). initial_state містить підготовлений основний стан (включно з поштовхом); observables опущено, тому функція вимірює за замовчуванням по вузлах ZZ.

job = fn.run(
t_steps=time_steps,
aqc_segments=[
{
"n_steps": 3,
"ansatz_steps": 1,
}, # early steps -> shallow 1-layer ansatz
{
"n_steps": 2,
"ansatz_steps": 2,
}, # later steps -> deeper 2-layer ansatz
],
aqc_options={
"max_bond": mps_max_bond, # MPS bond dimension for AQC compression
"cutoff": mps_cutoff,
"optimizer_settings": {
"method": "L-BFGS-B",
"jac": True,
"options": {"maxiter": 100},
},
},
dt=dt,
hamiltonian=H,
initial_state=gs_circuit, # prepared ground state including the neutron kick
# observables omitted -> default per-site Z (the neutron sigma_z readout)
backend="statevector",
)
print(job.status()) # rerun this cell until status says DONE
DONE
# The per-site <sigma_z>(t) the function returns is the retarded Green's function
# G(j, j_c, t). The workflow samples t = 1..time_steps, so drop the t = 0 row (the
# prepared+kicked state before any evolution) before post-processing.
result = job.result()
print(
"AQC fidelities:",
{k: round(v, 4) for k, v in result["metadata"]["aqc_fidelities"].items()},
)

ev = np.array(result["expectation_values"])
Gjjc = ev[1:] # shape (time_steps, n)
print("Green's function shape:", Gjjc.shape)
AQC fidelities: {'1': 1.0, '2': 0.9999, '3': 0.9992, '4': 0.9998, '5': 0.9995}
Green's function shape: (10, 10)

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

Виконай перетворення Фур'є функції Гріна в S(q,ω)S(q, \omega), дзеркально симетризуй та обріж негативні значення: стандартна нейтронна постобробка. Дзеркальне відображення є точним, оскільки S(q,ω)=S(q,ω)S(q, \omega) = S(-q, \omega) для цієї моделі, а негативні значення, що залишаються, є артефактами перетворення Фур'є скінченного, дискретно вибраного часового ряду, тому вони обрізаються до нуля. У цьому невеликому точному запуску двоспінонний континуум розрізняється лише грубо, але механізм ідентичний наступному апаратному запуску.

q_res, w_res = 100, 100
spectrum = get_spectrum(n, Gjjc, dt, time_steps, q_res, w_res)
spectrum = -(spectrum + spectrum[:, ::-1]) / 2 # mirror symmetry
spectrum = np.clip(spectrum, a_min=0, a_max=None) # clip negatives

plot_green(
n,
Gjjc,
time_steps,
dt,
title=f"Retarded Green's function - {n} qubits (AQC, statevector)",
)
plot_spectrum(
spectrum,
dt,
q_res,
w_res,
lower_bound=True,
upper_bound=True,
title=f"Dynamical structure factor - {n} qubits (AQC, statevector)",
)

Output of the previous code cell

Output of the previous code cell

Приклад на великомасштабному апаратному забезпеченні

Той самий робочий процес масштабується без будь-яких змін у науковому коді: ланцюг із 30 вузлів, удвічі більша глибина Троттера (20 кроків), план стиснення, що варіює глибину анзацу (глибший анзац для пізніших, більш заплутаних кроків), та виконання на процесорі IBM Quantum з вбудованим зменшенням похибок функції (динамічне розв'язання, твірлінг Паулі та твірлінгове усунення похибок зчитування (TREX)). Ми пройдемо ті самі чотири кроки, що й у прикладі із симулятором, повторно використовуючи дескриптор fn з розділу Налаштування.

Малий масштабВеликий масштаб
Кубіти1030
Кроки Троттера1020
Стиснуті AQC кроки (1-шаровий + 2-шаровий)3 + 2 = 56 + 4 = 10
Шари анзацу основного стану35
Максимальний розмір зв'язку MPS32128
БекендstatevectorQPU з DD, твірлінгом Паулі та TREX

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

Побудуй той самий гейзенбергівський SparsePauliOp KCuF3_3 та підготуй основний стан, тепер з глибшим анзацом gs_layers=5 для довшого ланцюга, потім вбудуй нейтронний поштовх π/2\pi/2 ZZ у центральному вузлі. Це ідентично малому масштабуванню, але при n=30n = 30.

Очікуй нижчу вірність основного стану, ніж для запуску на 10 вузлах: близько 0,82 тут проти 0,98 для меншого ланцюга, тому що п'ять шарів HVA не можуть повністю охопити основний стан на 30 вузлах. Це очікувано, а не є невдачею, і оригінальний туторіал приймає приблизно 0,65 при 50 вузлах з тієї ж причини. Збільшення gs_layers або межі ітерацій COBYQA покращує це ціною додаткових класичних витрат.

n = 30
dt = 0.6
time_steps = 20
center = n // 2 - 1

# Same MPS settings as the original large-scale run: a larger bond for the
# longer, more-entangled chain (shared by GS prep and AQC compression).
mps_max_bond = 128
mps_cutoff = 1e-8

# Same KCuF3 Hamiltonian and ground-state prep, on a larger chain
H = SparsePauliOp.from_sparse_list(
[(p, [i, i + 1], 0.25) for i in range(n - 1) for p in ("XX", "YY", "ZZ")],
num_qubits=n,
)
gs_circuit = prepare_ground_state(
n, gs_layers=5, max_bond=mps_max_bond, cutoff=mps_cutoff
)
gs_circuit.rz(np.pi / 2, center) # neutron kick at the center site
print(
f"Prepared {n}-qubit ground state with the neutron kick at site {center}."
)
DMRG ground-state energy: -13.111355
GS fidelity: 0.8201
Prepared 30-qubit ground state with the neutron kick at site 14.

Кроки 2 та 3: стиснення та виконання за допомогою шаблону функції

Той самий одиничний виклик, що й у прикладі із симулятором, тепер з backend_name, що вказує на процесор IBM Quantum, тому функція транспілює та виконує там. План стиснення варіює глибину анзацу: перші шість (з низькою заплутаністю) кроків Троттера стискаються в неглибокий одношаровий анзац, наступні чотири — у глибший двошаровий анзац, а решта 10 із 20 кроків виконуються як звичайний Троттер. aqc_options підвищує розмір зв'язку MPS до max_bond=128 для довшого, більш заплутаного ланцюга (відповідно до оригіналу), зберігаючи той самий оптимізатор L-BFGS-B з обмеженням у 100 ітерацій. estimator_options вмикає вбудоване зменшення похибок: динамічне розв'язання (XY4), твірлінг вентилів та TREX-пом'якшення вимірювань. Значення за замовчуванням функції вже відповідають оригінальному туторіалу для всіх з них, окрім бюджету навчання TREX (measure_noise_learning). Весь блок все одно виписано повністю, оскільки наданий викликачем estimator_options повністю замінює значення за замовчуванням функції, замість того щоб зливатися з ними, тому пропуск ключа призвів би до значення за замовчуванням IBM Quantum Compute, а не функції.

# Steps 2 + 3: the function compresses (varied ansatz) and executes on hardware.
job = fn.run(
t_steps=time_steps,
aqc_segments=[
{
"n_steps": 6,
"ansatz_steps": 1,
}, # early steps -> shallow 1-layer ansatz
{
"n_steps": 4,
"ansatz_steps": 2,
}, # later steps -> deeper 2-layer ansatz
],
aqc_options={
"max_bond": mps_max_bond, # 128 for the longer chain
"cutoff": mps_cutoff,
"optimizer_settings": {
"method": "L-BFGS-B",
"jac": True,
"options": {"maxiter": 100},
},
},
dt=dt,
hamiltonian=H,
initial_state=gs_circuit,
backend_name="ibm_pittsburgh",
# Mitigation settings from the original tutorial. Only the two
# measure_noise_learning values differ from the function's defaults; the rest
# restates them, because a caller-supplied estimator_options dict replaces the
# function's defaults wholesale rather than merging into them.
estimator_options={
"environment": {"job_tags": ["TUT-SNS"]},
"dynamical_decoupling": {"enable": True, "sequence_type": "XY4"},
"twirling": {
"enable_gates": True,
"num_randomizations": 1000,
"shots_per_randomization": 128,
},
"resilience": {
"measure_mitigation": True,
"measure_noise_learning": {
"num_randomizations": 32,
"shots_per_randomization": 100,
},
},
},
)
print("job ID (save this to reconnect later):", job.job_id)
job ID (save this to reconnect later): 43ed8d07-6d7d-4f33-b70a-7f31b765b310
Повторне підключення до довготривалого завдання

Великомасштабний запуск не швидкий, і більша частина часу є класичною, а не на QPU. Стиснення AQC виконується всередині функції до того, як щось потрапить на QPU: при 30 вузлах з max_bond=128 це зайняло майже чотири години в нашому запуску, порівняно з приблизно 18 хвилинами часу QPU, зазначеними в Оцінці часу використання на початку цього туторіалу. Очікування в черзі додається до обох. Тобі не потрібно тримати цей блокнот або ядро відкритим під час виконання.

Скопіюй ID завдання, виведений попередньою коміркою, та збережи його. Наступні три комірки дозволяють тобі повернутися до цього пізніше:

  1. Повторне підключення, потрібне лише в новій сесії ядра: повторно виконай комірки Налаштування, щоб відтворити serverless, потім відбудуй дескриптор job з ID, який ти зберіг. Пропусти цю комірку, якщо ти все ще перебуваєш у сесії, де надіслав завдання, оскільки дескриптор вже активний.

  2. Перевір статус: повторно виконуй, поки не буде повідомлено DONE.

  3. Отримай результат: виконуй лише коли статус — DONE.

Наступна комірка повторного підключення містить заповнювач. Заміни його на свій власний job_id:

# Reconnect to a previously submitted job by its ID. Only needed in a NEW kernel
# session; if you are still in the session where you submitted, the `job` handle
# from the preceding cell is already live, so skip this cell. Replace the ID that follows with your own.
job = serverless.get_job_by_id("<your job ID>")
# Check where the job is. Re-run this until it reports DONE before fetching the
# result in the following cell: QUEUED -> INITIALIZING -> RUNNING: OPTIMIZING_FOR_HARDWARE ->
# RUNNING: WAITING_FOR_QPU -> RUNNING: EXECUTING_QPU -> RUNNING: POST_PROCESSING
# -> DONE.
print(job.status())
DONE
# Run this only once the preceding status cell reports DONE. result() blocks until
# the job finishes, so calling it earlier just waits (possibly for hours).
result = job.result()
print(
"AQC fidelities:",
{k: round(v, 4) for k, v in result["metadata"]["aqc_fidelities"].items()},
)

ev = np.array(result["expectation_values"])
Gjjc = ev[1:] # drop the t = 0 row -> shape (time_steps, n)
AQC fidelities: {'1': 1.0, '2': 0.9994, '3': 0.9944, '4': 0.9853, '5': 0.9747, '6': 0.959, '7': 0.9495, '8': 0.9542, '9': 0.9533, '10': 0.9451}

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

Ідентична постобробка, як і для запуску на симуляторі: перетворення Фур'є функції Гріна в S(q,ω)S(q, \omega), дзеркальна симетризація та обрізання негативних значень. З довшим ланцюгом та еволюцією двоспінонний континуум розрізняється набагато краще. Він повинен заповнювати смугу між пунктирними межами, найяскравіше поблизу q=πq = \pi.

n = result["metadata"]["n"]
q_res, w_res = 100, 100
spectrum = get_spectrum(n, Gjjc, dt, time_steps, q_res, w_res)
spectrum = -(spectrum + spectrum[:, ::-1]) / 2 # mirror symmetry
spectrum = np.clip(spectrum, a_min=0, a_max=None) # clip negatives

plot_green(
n,
Gjjc,
time_steps,
dt,
title=f"Retarded Green's function - {n} qubits (AQC, hardware)",
)
plot_spectrum(
spectrum,
dt,
q_res,
w_res,
lower_bound=True,
upper_bound=True,
title=f"Dynamical structure factor - {n} qubits (AQC, hardware)",
)

Output of the previous code cell

Output of the previous code cell

Додаток

Попередній апаратний приклад запускає лише один довжину ланцюга. Три спектри, що йдуть далі, походять з попередніх апаратних запусків цього самого робочого процесу на ibm_pittsburgh при 10, 20 та 30 вузлах, з усіма іншими вхідними даними фіксованими: 20 кроків Троттера при dt = 0.6, план стиснення з шести одношарових плюс чотирьох двошарових стиснутих AQC кроків та max_bond = 128. Це записані результати, а не вихідні дані попередніх комірок.

Ті самі налаштування використовуються для всіх трьох розмірів, тому спектри безпосередньо порівнювані. Налаштування їх під кожну довжину ланцюга, наприклад з більшою кількістю шарів анзацу основного стану або більшим max_bond, може дати кращі результати, ніж будь-який з показаних тут.

Dynamical structure factor at 10 sites, a single sharp bright peak at q = pi near the lower bound

Dynamical structure factor at 20 sites, spectral weight filling the band between the two dashed two-spinon bounds

Dynamical structure factor at 30 sites, the continuum resolved more finely with fainter contrast and some weight outside the bounds

Наступні кроки

Рекомендації
  • Адаптуй цей робочий процес до своєї власної системи: функція приймає будь-який одновимірний SparsePauliOp найближчих сусідів, тому інший гамільтоніан ланцюга, початковий стан або набір спостережуваних величин виконує той самий конвеєр PRE → ФУНКЦІЯ → POST. Дивись повний контракт вхідних/вихідних даних у AQC Dynamics Template на GitHub.

  • Прочитай статтю, з якої походить цей тест: Lee et al., Benchmarking quantum simulation with neutron-scattering experiments (arXiv:2603.15608).

  • Порівняй з оригінальним туторіалом "Simulate neutron scattering", вбудованим робочим процесом, який цей туторіал переносить на розгорнутий шаблон функції.

  • Заглибся в техніки зменшення та придушення похибок, застосовані під час апаратного запуску: динамічне розв'язання, твірлінг Паулі та TREX.