Pular para o conteúdo principal

Simule espalhamento de nêutrons com um fluxo de trabalho Serverless de dinâmica AQC + Trotter

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

Resultados de aprendizagem

  • Como um espectro de espalhamento inelástico de nêutrons se mapeia para o fator de estrutura dinâmico S(q,ω)S(q, \omega) de um ímã quântico 1D.

  • Como preparar o estado fundamental do KCuF3_3 (Heisenberg isotrópico) com o grupo de renormalização da matriz de densidade (DMRG) e a maximização de fidelidade de estado de produto de matrizes (MPS).

  • Como executar a evolução temporal de Trotter, a compressão de circuito por compilação quântica aproximada (AQC) e a execução mitigada como uma única chamada de função.

  • Como pós-processar a série temporal σz(t)\langle \sigma_z \rangle(t) por sítio em S(q,ω)S(q, \omega) e identificar o contínuo de dois spinons.

Pré-requisitos

Contexto

O espalhamento inelástico de nêutrons mede o fator de estrutura dinâmico S(q,ω)S(q, \omega), a transformada de Fourier no espaço e no tempo da função de correlação spin-spin, de modo que reproduzir S(q,ω)S(q, \omega) a partir de um modelo de spin microscópico é um teste direto e refutável de uma simulação quântica. Este tutorial estuda o KCuF3_3, uma cadeia de Heisenberg antiferromagnética de spin-12\frac{1}{2} cujas excitações não são flips de spin único, mas pares de spinons fracionados: em vez de uma dispersão de magnon nítida, S(q,ω)S(q, \omega) mostra um amplo contínuo de dois spinons, limitado abaixo por π2sinq\tfrac{\pi}{2}|\sin q| e acima por πsin(q/2)\pi|\sin(q/2)|. Essas são as curvas tracejadas nos gráficos a seguir. A física completa, e a comparação com dados de nêutrons medidos, são abordadas no tutorial original e em Lee et al., arXiv:2603.15608.

O fluxo de trabalho quântico espelha o experimento de espalhamento:

  1. Prepare o estado fundamental da cadeia ψ0|\psi_0\rangle.

  2. Perturbe-o com uma perturbação local no sítio central, uma rotação ZZ de π/2\pi/2, imitando a transferência de momento e energia do nêutron.

  3. Evolua no tempo sob o Hamiltoniano de Heisenberg, eiHte^{-iHt}, com uma fórmula de produto de Trotter.

  4. Meça a magnetização por sítio σzj(t)\langle \sigma_z^j \rangle(t). Como função do sítio jj e do tempo tt, esta é exatamente a função de Green retardada GR(j,jc,t)G^R(j, j_c, t), portanto nenhuma conversão é necessária antes da transformada de Fourier na etapa 5.

  5. Transforme por Fourier GRG^R em S(q,ω)S(q, \omega).

Problemas podem surgir na etapa 3, quando circuitos de Trotter exatos para evoluções longas se tornam profundos demais para o hardware. A AQC com redes tensoriais resolve isso comprimindo um bloco de passos de Trotter em um ansatz parametrizado fixo e raso, cuja fidelidade de estado em relação à evolução exata é maximizada classicamente com um simulador MPS (arXiv:2301.08609). O AQC Dynamics Template empacota todo esse núcleo quântico (síntese de Trotter, compressão AQC e execução mitigada) por trás de uma única chamada:

PRÉ (este notebook)FUNÇÃO (aqc-dynamics-function)PÓS (este notebook)
Estado fundamental do DMRG mais maximização de fidelidade MPS, com a perturbação do nêutron incorporada no mesmo circuitoSíntese de Trotter → compressão AQC → execução em statevector, fake, ou runtime, retornando σzj(t)\langle \sigma_z^j \rangle(t) por sítioS(q,ω)S(q, \omega), o fator de estrutura dinâmico

O trabalho específico do experimento permanece aqui no notebook: preparação do estado fundamental (PRÉ) e o pós-processamento de S(q,ω)S(q, \omega) (PÓS). As duas etapas de uso intensivo quântico, compressão e execução, são executadas dentro da função.

Este tutorial é um complemento a Simulate neutron scattering in quantum materials with quantum circuits, que constrói o mesmo experimento inline: o mesmo modelo KCuF3_3, preparação do estado fundamental, perturbação do nêutron e pós-processamento, com a síntese de Trotter, compressão AQC e execução mitigada escritas passo a passo. Leia esse tutorial para aprender como funciona a compressão AQC. Leia este para executar o mesmo experimento por meio de um template de função implantado: o núcleo quântico se torna uma única chamada de função, e a compressão AQC de várias horas é executada dentro do worker Serverless em vez de na sua máquina, então você não precisa de um sistema HPC ou de um kernel aberto enquanto ela é executada. A mesma chamada também conduz outros experimentos de dinâmica 1D.

Requisitos

Antes de iniciar este tutorial, certifique-se de ter o seguinte:

  • A função implantada na sua conta do Qiskit Serverless. Execute primeiro o template de função complementar: Deploy and run the AQC + Trotter dynamics function template. Esse guia mostra como obter os arquivos de origem e fazer upload da função para a sua conta. Este tutorial apenas chama a função implantada.

  • Credenciais do IBM Quantum® salvas para QiskitServerless (consulte o template de função). Ambos os exemplos neste tutorial chamam a função implantada, então ambos precisam delas.

  • Qiskit SDK v2.0 ou posterior (pip install qiskit).

  • O cliente Qiskit IBM Catalog (pip install qiskit-ibm-catalog).

  • NumPy, SciPy e Matplotlib (pip install numpy scipy matplotlib). SciPy 1.14 ou posterior é necessário para o otimizador COBYQA usado na preparação do estado fundamental.

  • A pilha de rede tensorial AQC, porque a preparação do estado fundamental na Etapa 1 é executada localmente neste notebook: pip install 'qiskit-addon-aqc-tensor[quimb-jax]==0.3.1'.

A primeira chamada a uma função recém-implantada aguarda enquanto o worker Serverless instala suas dependências, então espere latência extra nessa execução.

Configuração

Importe as bibliotecas e defina os auxiliares específicos do experimento usados posteriormente: build_gs_ansatz (o ansatz variacional do Hamiltoniano, ou HVA, para preparação do estado fundamental), prepare_ground_state (DMRG mais maximização de fidelidade MPS), e get_spectrum, plot_green, e plot_spectrum (o pós-processamento de S(q,ω)S(q, \omega)). Estes são adaptados do tutorial original de espalhamento de nêutrons.

# 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.

Carregue o template de função

Conecte-se ao Qiskit Serverless e carregue a aqc-dynamics-function implantada. Ambos os exemplos neste tutorial chamam o mesmo handle fn, então a função é carregada uma vez, aqui.

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

Exemplo de simulador em pequena escala

Primeiro, executamos o fluxo de trabalho completo em uma cadeia pequena de 10 sítios usando o backend exato statevector. Isso valida o pipeline PRE → FUNCTION → POST antes de gastar qualquer tempo de QPU.

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

Construa o Hamiltoniano de KCuF3_3 como um SparsePauliOp (Heisenberg isotrópico: XX+YY+ZZXX + YY + ZZ com acoplamento 14\tfrac14 em cada ligação de vizinhos mais próximos; as strings são operadores de Pauli, então 14\tfrac14 dá o acoplamento de spin-12\frac{1}{2}). Prepare o estado fundamental com DMRG e maximização da fidelidade MPS, depois incorpore o impulso do nêutron: uma rotação ZZ de π/2\pi/2 no sítio central. O circuito preparado é o que passamos para a função como initial_state. Deixamos observables no seu padrão (ZZ por sítio), que é exatamente a leitura σzj(t)\langle \sigma_z^j \rangle(t) que o fluxo de trabalho de nêutrons precisa.

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.

Etapas 2 e 3: Comprimir e executar com o modelo de função

Em um fluxo de trabalho escrito manualmente, essas são duas etapas separadas: otimizar os circuitos para o hardware (Etapa 2) e executá-los (Etapa 3). O modelo de função combina ambas em uma única chamada. Ele realiza a síntese de Trotter, a compressão AQC e a transpilação para o hardware, depois executa os circuitos (aqui no simulador exato, mais tarde com mitigação de erros integrada no hardware). Os dois parâmetros de ajuste são aqc_segments (o plano de compressão) e aqc_options (as configurações de MPS e do otimizador). Cada segmento {"n_steps": k, "ansatz_steps": m} comprime k passos de Trotter consecutivos em um ansatz construído a partir de um alvo de Trotter de m passos, e quaisquer passos além de sum(n_steps) são executados como Trotter simples. Passos iniciais, de baixo emaranhamento, comprimem bem em um ansatz raso (ansatz_steps=1), então aqui comprimimos os três primeiros passos em um ansatz de camada única e os dois seguintes em um ansatz mais profundo de duas camadas; os cinco passos restantes dos 10 passos de Trotter são executados como Trotter simples. Para aqc_options, espelhamos o tutorial original: dimensão de vínculo MPS max_bond=32, cutoff=1e-8, e um otimizador L-BFGS-B limitado a 100 iterações.

Chame a função carregada em Setup. backend="statevector" executa o caminho de referência exato: sem tempo de QPU, com os circuitos rodando em um simulador exato de statevector dentro do worker serverless (uma conta salva do Qiskit Serverless ainda é necessária para chamá-lo). O initial_state carrega o estado fundamental preparado (incluindo o impulso); observables é omitido para que a função meça o ZZ por sítio padrão.

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)

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

Transforme por Fourier a função de Green em S(q,ω)S(q, \omega), simetrize por espelhamento e recorte os valores negativos: o pós-processamento padrão de nêutrons. A simetrização é exata porque S(q,ω)=S(q,ω)S(q, \omega) = S(-q, \omega) para este modelo, e os valores negativos que sobrevivem são artefatos da transformação de Fourier de uma série temporal finita e discretamente amostrada, então são recortados para zero. Nessa execução exata pequena, o contínuo de dois spinons é apenas resolvido de forma grosseira, mas o mecanismo é idêntico à execução em hardware que se segue.

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

Exemplo de hardware em larga escala

O mesmo fluxo de trabalho é escalado sem alterar nenhum código científico: uma cadeia de 30 sítios, o dobro da profundidade de Trotter (20 passos), um plano de compressão que varia a profundidade do ansatz (um ansatz mais profundo para os passos posteriores, mais emaranhados), e execução em um processador IBM Quantum com a mitigação de erros integrada da função (desacoplamento dinâmico, twirling de Pauli e extinção de erros de leitura por twirling (TREX)). Percorremos as mesmas quatro etapas do exemplo do simulador, reutilizando o identificador fn do Setup.

Pequena escalaGrande escala
Qubits1030
Passos de Trotter1020
Passos comprimidos com AQC (1 camada + 2 camadas)3 + 2 = 56 + 4 = 10
Camadas do ansatz de estado fundamental35
Dimensão máxima de vínculo MPS32128
BackendstatevectorQPU com DD, twirling de Pauli e TREX

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

Construa o mesmo SparsePauliOp de Heisenberg de KCuF3_3 e prepare o estado fundamental, agora com um ansatz mais profundo gs_layers=5 para a cadeia mais longa, depois incorpore o impulso de nêutron ZZ de π/2\pi/2 no sítio central. Isso é idêntico ao mapeamento em pequena escala, mas com n=30n = 30.

Espere uma fidelidade de estado fundamental menor do que na execução de 10 sítios: em torno de 0,82 aqui contra 0,98 para a cadeia menor, porque cinco camadas HVA não conseguem capturar totalmente um estado fundamental de 30 sítios. Isso é esperado, e não uma falha, e o tutorial original aceita aproximadamente 0,65 em 50 sítios pelo mesmo motivo. Aumentar gs_layers ou o limite de iterações do COBYQA melhora isso, com custo clássico adicional.

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.

Etapas 2 e 3: Comprimir e executar com o modelo de função

A mesma chamada única do exemplo do simulador, agora com backend_name apontando para um processador IBM Quantum, para que a função transpile e execute lá. O plano de compressão varia a profundidade do ansatz: os primeiros seis passos de Trotter (de baixo emaranhamento) são comprimidos em um ansatz raso de camada única, os quatro seguintes em um ansatz mais profundo de duas camadas, e os 10 passos restantes dos 20 são executados como Trotter simples. aqc_options aumenta a dimensão de vínculo MPS para max_bond=128 para a cadeia mais longa e mais emaranhada (correspondendo ao original), mantendo o mesmo otimizador L-BFGS-B limitado a 100 iterações. As estimator_options ativam a mitigação de erros integrada: desacoplamento dinâmico (XY4), twirling de portas e mitigação de medição TREX. Os padrões da função já correspondem ao tutorial original para todos esses itens, exceto o orçamento de aprendizado do TREX (measure_noise_learning). O bloco inteiro ainda é escrito por completo porque um estimator_options fornecido pelo chamador substitui totalmente os padrões da função em vez de mesclar-se a eles, então omitir uma chave voltaria ao padrão do IBM Quantum Compute em vez do padrão da função.

# 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
Reconectando a um job de longa duração

A execução em larga escala não é rápida, e a maior parte do tempo é clássica, não na QPU. A compressão AQC é executada dentro da função antes que qualquer coisa chegue à QPU: com 30 sítios e max_bond=128, isso levou quase quatro horas em nossa execução, contra os aproximadamente 18 minutos de tempo de QPU citados na Estimativa de uso no topo deste tutorial. O tempo de espera na fila se soma a ambos. Você não precisa manter este notebook ou kernel aberto enquanto ele é executado.

Copie o ID do job impresso pela célula anterior e salve-o. As próximas três células permitem que você retome a execução mais tarde:

  1. Reconectar, necessário apenas em uma nova sessão de kernel: execute novamente as células de Setup para recriar serverless, depois reconstrua o identificador job a partir do ID que você salvou. Pule esta célula se ainda estiver na sessão em que você submeteu, pois o identificador já está ativo.

  2. Verificar status: execute novamente até que ele reporte DONE.

  3. Buscar o resultado: execute apenas quando o status for DONE.

A célula de reconexão a seguir contém um espaço reservado. Substitua-o pelo seu próprio 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}

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

Pós-processamento idêntico ao da execução no simulador: transforme por Fourier a função de Green em S(q,ω)S(q, \omega), simetrize por espelhamento e recorte os valores negativos. Com a cadeia e a evolução mais longas, o contínuo de dois spinons é muito melhor resolvido. Ele deve preencher a banda entre os limites tracejados, mais brilhante perto de 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

Apêndice

O exemplo de hardware anterior executa um único comprimento de cadeia. Os três espectros a seguir vêm de execuções de hardware anteriores deste mesmo fluxo de trabalho no ibm_pittsburgh com 10, 20 e 30 sítios, com todas as outras entradas mantidas fixas: 20 passos de Trotter com dt = 0.6, o plano de compressão de seis passos de uma camada mais quatro passos de duas camadas comprimidos por AQC e max_bond = 128. Estes são resultados registrados, não a saída das células anteriores.

As mesmas configurações são usadas nos três tamanhos, portanto os espectros são diretamente comparáveis. Ajustá-las por comprimento de cadeia, com mais camadas de ansatz de estado fundamental ou um max_bond maior, por exemplo, pode dar resultados melhores do que os mostrados aqui.

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

Próximos passos

Recomendações