Simulare la diffusione dei neutroni nei materiali quantistici con circuiti quantistici
Stima del tempo di esecuzione: 13 minuti su un processore Heron r2 (NOTA: si tratta solo di una stima. (La durata potrebbe variare.)
Risultati di apprendimento
Al termine di questo tutorial, avrai acquisito le seguenti conoscenze:
- In che modo gli spettri di diffusione inelastica dei neutroni (INS) sono collegati ai fattori di struttura dinamici (DSF) dei modelli quantistici di spin.
- Come preparare uno stato fondamentale, applicare una perturbazione locale ed eseguire l'evoluzione temporale di Trotter su un circuito quantistico.
- Come utilizzare la compilazione quantistica approssimativa (AQC) con
qiskit-addon-aqc-tensorper comprimere i circuiti di Trotter profondi in vista dell'esecuzione su hardware. - Come ricavare la funzione di Green ritardata (RGF) dai valori attesi dei qubit e trasformarla tramite la trasformata di Fourier in una DSF.
Prerequisiti
Si consiglia di approfondire i seguenti argomenti:
- Fondamenti dell'informazione quantistica
- Progettazione di algoritmi variazionali
- Introduzione a " Qiskit primitives " (Stima e campionamento)
Sfondo
In questo tutorial riproduciamo i risultati di Lee et al., arXiv:2603.15608.
Diffusione anelastica dei neutroni e fattore di struttura dinamico
La diffusione anelastica dei neutroni (INS) è uno degli strumenti sperimentali più efficaci per lo studio delle eccitazioni magnetiche nei materiali quantistici. Quando un fascio di neutroni termici o freddi colpisce un cristallo, i singoli neutroni scambiano sia quantità di moto che energia con il sottosistema magnetico. L'intensità di diffusione misurata è proporzionale al fattore di struttura dinamico (DSF),
che codifica tutte le correlazioni spazio-temporali dei gradi di libertà di spin.
KCuF: un magnete canonico a liquido di Luttinger
Il fluoruro di rame e potassio ( KCuF ) è un antiferromagnete quasi unidimensionale in cui catene di ioni spin- Cu interagiscono tramite un’ di scambio di Heisenberg tra vicini più prossimi, mentre l’accoppiamento intercatena è pari solo a di . A , dove sono disponibili dati INS, lo spettro è dominato da eccitazioni spinoniche frazionarie caratteristiche di un liquido di Tomonaga-Luttinger. Poiché le dinamiche intracatenarie sono ben descritte dall’hamiltoniano unidimensionale di tipo spin- XXZ nel punto isotropico ( ),
KCuF rappresenta un punto di riferimento ideale per la simulazione quantistica: l’hamiltoniano è abbastanza semplice da poter essere implementato su un processore quantistico, ma lo stato fondamentale è fortemente intrecciato e lo spettro di eccitazione presenta un ampio continuum a due spinoni.
Nota: questo tutorial imposta l' e come unità di energia e adotta la normalizzazione , che corrisponde all'Hamiltoniano locale utilizzato nell'implementazione circuitale descritta nell'articolo (Fig. S3 (del supplemento). L'Hamiltoniano completo del sistema (Eq. 3) comporta un fattore complessivo aggiuntivo pari a 2, pertanto l’ e dell’articolo è il doppio dell’ e qui utilizzato.
Cosa simuliamo e misuriamo
La grandezza fisica che calcoliamo è la funzione di Green ritardata (RGF), definita come la funzione di correlazione spin-spin dipendente dal tempo
dove è un sito di riferimento (il centro della catena) e è l'operatore di spin nella rappresentazione di Heisenberg. In questo tutorial ci concentreremo sul componente " " ( ). Su un computer quantistico, si accede all'RGF preparando lo stato fondamentale, applicando una perturbazione locale all'indirizzo , facendo evolvere nel tempo lo stato perturbato e misurando il valore atteso del singolo qubit in ogni sito per ogni passo temporale. Il concetto fondamentale è che ogni fornisce la differenza rispetto alla magnetizzazione dello stato fondamentale; poiché l’antiferromagnete isotropico di Heisenberg ha una magnetizzazione netta per sito pari a zero ( ), il valore grezzo misurato fornisce direttamente l’ e senza alcuna sottrazione esplicita.
Raccogliendo i valori di su tutti i siti e in tutti gli intervalli di tempo, si ottiene un set di dati bidimensionale che viene poi sottoposto a trasformata di Fourier sia nello spazio che nel tempo per ottenere il fattore di struttura dinamico . Il DSF è la grandezza misurata direttamente in un esperimento INS: ci indica quali eccitazioni magnetiche esistono per ogni momento ed energia . Per la catena di Heisenberg isotropa, lo spettro di eccitazione esatto è un continuo a due spinoni, un’ampia banda di intensità di scattering la cui forma funge da rigoroso punto di riferimento end-to-end per la simulazione quantistica: convalida contemporaneamente la preparazione dello stato fondamentale, la perturbazione, l’evoluzione temporale di Trotter e il protocollo di misurazione.
Flusso di lavoro della simulazione quantistica
Il flusso di lavoro rispecchia la dinamica di un evento INS. Noi (1) prepariamo lo stato fondamentale a molti corpi su qubit, (2) applichiamo una perturbazione locale di inversione di spin al centro della catena per simulare il trasferimento di spin del neutrone, (3) facciamo evolvere il sistema per passi temporali discreti utilizzando la trotterizzazione di secondo ordine, e (4) misuriamo su ogni qubit ad ogni passo per ottenere l'RGF. Una trasformata di Fourier discreta bidimensionale fornisce quindi il DSF .
L'osservabile che misuriamo ad ogni passo temporale è l' e su ciascun qubit . In Qiskit ciò è rappresentato come una lista di SparsePauliOp operatori: un singolo qubit incorporato nella stringa di identità a -qubit per ciascun sito. Queste grandezze osservabili vengono calcolate una sola volta per ogni istanza del problema durante la fase di mappatura del problema (Fase 1) e riutilizzate per ogni circuito a quel livello.
Compilazione quantistica approssimativa (AQC)
I circuiti Deep Trotter possono essere compressi mediante la compilazione quantistica approssimativa (AQC), che sostituisce i primi strati di Trotter con un ansatz parametrizzato più breve, i cui parametri vengono ottimizzati classicamente per massimizzare la fedeltà a livello MPS rispetto al circuito deep originale. I restanti passaggi di Trotter vengono aggiunti esattamente come sono, dando origine a un circuito “misto” AQC + Trotter con un numero notevolmente inferiore di porte a due qubit.
Simulazione MPS
Per un sistema unidimensionale, i metodi basati sullo stato di prodotto matriciale (MPS) consentono di simulare in modo efficiente sia la preparazione dello stato fondamentale (con il gruppo di rinormalizzazione della matrice di densità, o DMRG) sia l'evoluzione temporale a livello di circuito. Regolando la dimensione del legame , si ottiene un compromesso tra precisione e costo computazionale. In questo tutorial utilizziamo la simulazione MPS per qiskit-addon-aqc-tensor calcolare approcci AQC ad alta fedeltà che comprimono i circuiti di Trotter profondi in vista dell'esecuzione su hardware.
Requisiti
Prima di iniziare questo tutorial, assicurati di avere installato quanto segue:
- Qiskit SDK con supporto alla visualizzazione
- Qiskit Runtime (
pip install qiskit-ibm-runtime) qiskit-addon-aqc-tensorconquimbe JAX extras (pip install 'qiskit-addon-aqc-tensor[quimb-jax]')
Configura
import timeit
import warnings
from collections.abc import Iterator, Sequence
from functools import partial
import matplotlib.pyplot as plt
import numpy as np
import quimb.tensor as qtn
import scipy.optimize
from numpy.typing import NDArray
from qiskit import QuantumCircuit
from qiskit.circuit import CircuitInstruction, ParameterVector, Qubit
from qiskit.circuit.library import PauliEvolutionGate
from qiskit.primitives import StatevectorEstimator
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_addon_aqc_tensor import generate_ansatz_from_circuit
from qiskit_addon_aqc_tensor.objective import MaximizeStateFidelity
from qiskit_addon_aqc_tensor.simulation import tensornetwork_from_circuit
from qiskit_addon_aqc_tensor.simulation.quimb import QuimbSimulator
from qiskit_ibm_runtime import EstimatorV2 as Estimator
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_quimb import quimb_circuit
from scipy.sparse import SparseEfficiencyWarning
# scipy.sparse.linalg.expm (reached via PauliEvolutionGate.to_matrix) prefers
# CSC and warns when handed the CSR matrix from SparsePauliOp.to_matrix; the
# result is unaffected, so silence the cosmetic warning.
warnings.filterwarnings("ignore", category=SparseEfficiencyWarning)
def xxz_hamiltonian_mpo(
n_qubits: int, interaction: float = 1.0, anisotropy: float = 1.0
) -> qtn.MatrixProductOperator:
"""1D XXZ Hamiltonian as a quimb MPO.
Builds the Hamiltonian using ``qtn.SpinHam1D``.
Args:
n_qubits: Number of sites.
interaction: Overall interaction strength (J in the paper).
anisotropy: XY/Z anisotropy (ε in the paper). ``ε = 1`` is the
isotropic Heisenberg point; ``ε = 0`` is the Ising limit.
Returns:
The Hamiltonian as a matrix product operator.
"""
builder = qtn.SpinHam1D(S=1 / 2)
builder += interaction * anisotropy * 0.5, "+", "-"
builder += interaction * anisotropy * 0.5, "-", "+"
builder += interaction, "Z", "Z"
return builder.build_mpo(L=n_qubits)
def build_ground_state_ansatz(n_qubits: int, n_layers: int) -> QuantumCircuit:
"""Hamiltonian variational ansatz (HVA) circuit for ground-state preparation.
Starts from a product of singlet pairs and applies alternating
odd/even layers of parameterized XXZ pair-evolution gates.
The returned circuit is parameterized: it carries a ``ParameterVector``
named ``"theta"`` of length ``2 * n_layers`` whose values must be
assigned (e.g. via ``circuit.assign_parameters``) before simulation.
Element ``2 * r`` is the odd-layer angle and element ``2 * r + 1`` is the
even-layer angle of layer ``r``.
Args:
n_qubits: Number of qubits (must be even).
n_layers: Number of HVA layers.
Returns:
The parameterized HVA preparation circuit.
"""
theta = ParameterVector("theta", 2 * n_layers)
circuit = QuantumCircuit(n_qubits)
# Initial singlet product state
for i in range(n_qubits // 2):
circuit.x(2 * i)
circuit.x(2 * i + 1)
circuit.h(2 * i + 1)
circuit.cx(2 * i + 1, 2 * i)
# Variational layers: each pair gate is exp(-i theta (XX+YY+ZZ)/2)
pair_ham = SparsePauliOp(
["XX", "YY", "ZZ"], coeffs=[0.5, 0.5, 0.5]
) # H_pair (HVA form)
for r in range(n_layers):
for i in range(1, (n_qubits + 1) // 2): # odd layer
circuit.append(
PauliEvolutionGate(pair_ham, time=theta[2 * r]),
[2 * i - 1, 2 * i],
)
for i in range(n_qubits // 2): # even layer
circuit.append(
PauliEvolutionGate(pair_ham, time=theta[2 * r + 1]),
[2 * i, 2 * i + 1],
)
return circuit
def optimize_ground_state_ansatz(
ansatz: QuantumCircuit,
x0: NDArray[np.floating],
target_mps: qtn.MatrixProductState,
*,
max_bond: int | None = None,
cutoff: float = 1e-10,
method: str = "COBYQA",
options: dict | None = None,
) -> scipy.optimize.OptimizeResult:
"""Optimize HVA parameters by maximizing fidelity with a target MPS.
The HVA circuit is simulated as a matrix product state with the given
bond-dimension truncation, and the parameters are optimized to maximize
the state fidelity ``|<psi_HVA | target_mps>|**2`` with the DMRG ground
state. Both states are normalized, so the minimized objective is the
infidelity ``1 - |<psi_HVA | target_mps>|**2``.
Args:
ansatz: Parameterized HVA circuit from ``build_ground_state_ansatz``.
The length of ``x0`` must equal ``ansatz.num_parameters``.
x0: Initial parameters.
target_mps: Target MPS (DMRG ground state) to maximize fidelity with.
max_bond: Maximum MPS bond dimension during gate application.
cutoff: Singular-value cutoff during gate application.
method: ``scipy.optimize.minimize`` method.
options: Options dict forwarded to ``scipy.optimize.minimize``.
Returns:
The Scipy OptimizeResult.
"""
def infidelity(params: NDArray[np.floating]) -> float:
circuit = ansatz.assign_parameters(params)
circuit_mps = quimb_circuit(
circuit.decompose(["PauliEvolution"]),
quimb_circuit_class=qtn.CircuitMPS,
max_bond=max_bond,
cutoff=cutoff,
)
return 1 - abs(circuit_mps.psi.H @ target_mps) ** 2
return scipy.optimize.minimize(
infidelity, np.asarray(x0), method=method, options=options
)
def trotter_evolution(
qubits: Sequence[Qubit],
interaction: float,
anisotropy: float,
time_step: float,
n_steps: int,
) -> Iterator[CircuitInstruction]:
"""Second-order Trotter steps of the XXZ pair Hamiltonian.
While the paper used a hand-optimized circuit for the Trotter steps, we use
PauliEvolutionGate here for simplicity and generality. The final two-qubit gate
count and gate depth are equivalent when transpiled with ``optimization_level=3``.
Args:
qubits: Qubits to act on (length ``n_qubits``).
interaction: Overall interaction strength (J in the paper).
anisotropy: XY/Z anisotropy (ε in the paper). ``ε = 1`` is the
isotropic Heisenberg point; ``ε = 0`` is the Ising limit.
time_step: Per-step Trotter time.
n_steps: Number of Trotter steps.
Yields:
``CircuitInstruction``s implementing the Trotter steps.
"""
if n_steps == 0:
return
n_qubits = len(qubits)
pair_ham = SparsePauliOp(
["XX", "YY", "ZZ"],
coeffs=[
0.25 * interaction * anisotropy,
0.25 * interaction * anisotropy,
0.25 * interaction,
],
)
half_evo = PauliEvolutionGate(pair_ham, time=time_step / 2)
full_evo = PauliEvolutionGate(pair_ham, time=time_step)
for i in range(n_qubits // 2): # half even layer
yield CircuitInstruction(half_evo, (qubits[2 * i], qubits[2 * i + 1]))
for i in range(n_qubits // 2 - 1): # full odd layer
yield CircuitInstruction(
full_evo, (qubits[2 * i + 1], qubits[2 * i + 2])
)
for _ in range(n_steps - 1): # interior steps
for i in range(n_qubits // 2):
yield CircuitInstruction(
full_evo, (qubits[2 * i], qubits[2 * i + 1])
)
for i in range(n_qubits // 2 - 1):
yield CircuitInstruction(
full_evo, (qubits[2 * i + 1], qubits[2 * i + 2])
)
for i in range(n_qubits // 2): # half even layer
yield CircuitInstruction(half_evo, (qubits[2 * i], qubits[2 * i + 1]))
def get_dsf(
n_qubits: int,
rgf_mat: NDArray[np.floating],
time_step: float,
n_steps: int,
n_points_momentum: int,
n_points_frequency: int,
) -> NDArray[np.floating]:
"""Compute the dynamical structure factor from the retarded Green's function.
Uses the center-site approximation and a discrete Fourier transform.
The result is symmetrized about the momentum axis and clipped to
non-negative values, ready for plotting.
Args:
n_qubits: Number of qubits (sites).
rgf_mat: RGF matrix of shape ``(n_steps, n_qubits)``.
time_step: Trotter time-step size.
n_steps: Number of time steps.
n_points_momentum: Number of momentum points.
n_points_frequency: Number of frequency points.
Returns:
DSF array of shape ``(n_points_frequency, n_points_momentum)``,
symmetrized about the momentum axis and clipped to non-negative values.
"""
max_frequency = np.pi / time_step
momentum_range = np.linspace(0, 2 * np.pi, n_points_momentum)
frequency_range = np.linspace(0, max_frequency, n_points_frequency)
result = np.zeros((frequency_range.shape[0], momentum_range.shape[0]))
center = n_qubits // 2 - 1
for iw, w in enumerate(frequency_range):
exponent = np.exp(1j * w * time_step * np.arange(1, n_steps + 1))
# S = sigma/2, so two spin operators contribute a factor of (1/2)(1/2) = 1/4.
rgf_omega = (
np.dot(rgf_mat.T, exponent) * time_step / 4
) # S(omega): time Fourier slice of the Green's function
for iq, q in enumerate(momentum_range):
momentum_phases = np.exp(
-1j * q * np.arange(-center, center + 2, 1)
)
result[iw, iq] = np.imag(np.dot(rgf_omega, momentum_phases))
result = -(result + result[:, ::-1]) / 2
result = np.clip(result, a_min=0, a_max=None)
return result
def plot_dsf(
dsf: NDArray[np.floating],
time_step: float,
n_points_momentum: int,
n_points_frequency: int,
title: str | None = None,
) -> None:
"""Heat-map of the dynamical structure factor.
Args:
dsf: DSF array of shape ``(n_points_frequency, n_points_momentum)``.
time_step: Trotter time-step size.
n_points_momentum: Number of momentum points.
n_points_frequency: Number of frequency points.
title: Optional plot title.
"""
max_frequency = np.pi / time_step
momentum_range = np.linspace(0, 2 * np.pi, n_points_momentum)
frequency_range = np.linspace(0, max_frequency, n_points_frequency)
x, y = np.meshgrid(momentum_range, frequency_range)
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")
ax.set_ylim(0, 3.6)
ax.set_xlim(0, 2 * np.pi)
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 title:
ax.set_title(title, fontsize=14)
plt.tight_layout()
plt.show()
def plot_rgf(
n_qubits: int,
rgf_mat: NDArray[np.floating],
time_step: float,
n_steps: int,
title: str | None = None,
) -> None:
"""Heat-map of the retarded Green's function in real space and time.
Args:
n_qubits: Number of qubits (sites).
rgf_mat: RGF matrix of shape ``(n_steps, n_qubits)``.
time_step: Trotter time-step size.
n_steps: Number of time steps.
title: Optional plot title.
"""
fig, ax = plt.subplots(figsize=(8, 6))
qubit_axis = np.arange(n_qubits)
t_axis = np.arange(1, n_steps + 1) * time_step
x, y = np.meshgrid(qubit_axis, t_axis)
c = ax.pcolormesh(
x,
y,
np.real(rgf_mat),
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("Qubit", fontsize=16)
ax.xaxis.set_major_locator(
plt.matplotlib.ticker.MaxNLocator(integer=True)
)
ax.set_ylabel(r"Time", fontsize=16)
if title:
ax.set_title(title, fontsize=14)
plt.tight_layout()
plt.show()
def uniform_2q_depth(circuit: QuantumCircuit) -> int:
"""Two-qubit gate depth in a standardized basis."""
pass_manager = generate_preset_pass_manager(
optimization_level=0, basis_gates=["cz", "id", "rz", "sx", "x"]
)
return pass_manager.run(circuit).depth(
lambda inst: inst.operation.num_qubits == 2
)Esempio di simulatore su piccola scala
In primo luogo illustriamo l'intero flusso di lavoro su 10 qubit, ottimizzando l'ansatz dello stato fondamentale HVA tramite simulazione MPS e utilizzando il simulatore di vettori di stato di Qiskit per l'evoluzione temporale. Il DMRG fornisce un'energia di riferimento dello stato fondamentale e un MPS di riferimento. Questo esempio su piccola scala ci permette di verificare ogni fase prima di passare a una scala più ampia.
Fase 1: Mappare gli input classici su un problema quantistico
Iniziamo definendo il modello fisico e costruendo i circuiti quantistici.
Hamiltoniano. KCuF è modellato dall’Hamiltoniano XXZ di 1D nel punto isotropico ( , , considerando come unità di energia).
Stato fondamentale. Utilizziamo un circuito basato sull'approccio variazionale hamiltoniano (HVA) come circuito di preparazione dello stato fondamentale build_ground_state_ansatz. CircuitMPSI parametri HVA vengono ottimizzati in modo classico massimizzando la fedeltà dello stato con l'MPS dello stato fondamentale DMRG, dove lo stato HVA viene valutato simulando il circuito come uno stato di prodotto matriciale con quimb's. Per l'ottimizzazione utilizziamo scipy.optimize.minimize . A titolo di riferimento, viene calcolata anche l' e energetica dell'ansatz ottimizzato.
Cancelli per cavalli al trotto. Ogni termine di interazione "vicino più prossimo" con è costruito con PauliEvolutionGate(H_pair, time=time_step). Qiskit sintetizza il tutto nella decomposizione ottimale in tre CNOT durante la transpilazione.
Perturbazione. Un gate di tipo “ ” applicato al qubit centrale implementa un’operazione “ ”, riproducendo il ribaltamento di spin locale prodotto da un neutrone diffuso.
Osservabili. Costruiamo un osservabile di tipo “ ” per ciascun sito di qubit. Questi SparsePauliOp oggetti vengono passati alla primitiva Estimator nella Fase 3 per estrarre l' e ad ogni passo temporale.
# -- Physical parameters --
n_qubits = 10
interaction = 1.0 # J
anisotropy = 1.0 # ε (isotropic point)
time_step = 0.6
n_steps = 10
mps_max_bond = 32
mps_cutoff = 1e-8
center = n_qubits // 2 - 1
# -- Hamiltonian MPO --
ham_mpo = xxz_hamiltonian_mpo(n_qubits, interaction, anisotropy)
# -- Reference ground-state energy via DMRG --
dmrg = qtn.DMRG2(ham_mpo)
dmrg.solve(tol=1e-8)
print(f"Ground-state energy (DMRG): {dmrg.energy:.6f}")
# -- Build ground state ansatz circuit --
gs_n_layers = 3
gs_ansatz = build_ground_state_ansatz(n_qubits, gs_n_layers)
# -- Optimize ground state ansatz parameters to maximize fidelity with the DMRG MPS --
# Initialize odd-layer angles near 0 (where the inter-pair gate is the
# identity) and even-layer angles near pi/2 (where the intra-pair gate
# is a SWAP, since 0.5*(XX + YY + ZZ) = SWAP - I/2).
rng = np.random.default_rng(12345)
x0 = np.tile([0, np.pi / 2], gs_n_layers) + rng.normal(
scale=0.1, size=2 * gs_n_layers
)
print("Optimizing ground state ansatz...")
t0 = timeit.default_timer()
result = optimize_ground_state_ansatz(
gs_ansatz,
x0,
dmrg.state,
max_bond=mps_max_bond,
cutoff=mps_cutoff,
options=dict(maxiter=100),
)
t1 = timeit.default_timer()
print(f"Finished optimizing ground state ansatz in {t1 - t0} seconds.")
print(f"Ground state ansatz fidelity: {1 - result.fun:.6f}")
gs_circuit = gs_ansatz.assign_parameters(result.x)
gs_circuit_mps = quimb_circuit(
gs_circuit.decompose(["PauliEvolution"]),
quimb_circuit_class=qtn.CircuitMPS,
max_bond=mps_max_bond,
cutoff=mps_cutoff,
)
gs_ansatz_energy = qtn.expec_TN_1D(
gs_circuit_mps.psi.H, ham_mpo, gs_circuit_mps.psi
)
print(f"Ground state ansatz energy: {gs_ansatz_energy:.6f}")
# -- Build circuits for each time step --
perturbed = gs_circuit.copy()
perturbed.rz(np.pi / 2, center)
circuits = []
for t in range(1, n_steps + 1):
circuit = perturbed.copy()
for instr in trotter_evolution(
circuit.qubits, interaction, anisotropy, time_step, t
):
circuit.append(instr)
circuits.append(circuit)
print(
f"Built {len(circuits)} circuits, deepest 2q depth (uniform basis) = "
f"{uniform_2q_depth(circuits[-1])}"
)
# -- Observables: Z on each qubit site --
observables = [
SparsePauliOp.from_sparse_list([("Z", [i], 1)], num_qubits=n_qubits)
for i in range(n_qubits)
]
print(f"Defined {len(observables)} Z observables.")Output:
Ground-state energy (DMRG): -4.258035
Optimizing ground state ansatz...
Finished optimizing ground state ansatz in 8.850687621976249 seconds.
Ground state ansatz fidelity: 0.984277
Ground state ansatz energy: -4.232565
Built 10 circuits, deepest 2q depth (uniform basis) = 163
Defined 10 Z observables.
Fase 2: Ottimizzare il problema per l'esecuzione su hardware quantistico
Per l'hardware reale, i circuiti di Trotter sopra riportati risulterebbero troppo complessi. La compilazione quantistica approssimativa (AQC) risolve questo problema sostituendo i primi strati di Trotter (compreso il circuito allo stato fondamentale) con un ansatz parametrizzato più breve, ottimizzato per massimizzare la fedeltà a livello MPS rispetto al circuito profondo originale. I restanti passaggi di Trotter vengono aggiunti esattamente come sono, dando origine a un circuito “AQC + Trotter” meno complesso.
Per trovare un equilibrio tra espressività e profondità del circuito, vengono utilizzati due approcci: un approccio a un livello (generato da un singolo passo di Trotter) comprime i primi passi temporali, mentre un approccio più profondo a due livelli (generato da due passi di Trotter) comprime i passi successivi, dove è richiesta una maggiore fedeltà.
Il flusso di lavoro AQC prevede quattro fasi secondarie:
- Realizzare i circuiti di destinazione : i primi circuiti “ ” del Passo 1 fungono direttamente da obiettivi AQC.
- Calcolare l'MPS di destinazione — simulare ciascun circuito di destinazione come stato di prodotto matriciale utilizzando
quimb.tensor.CircuitMPS. - Generazione e ottimizzazione degli ansatz —
generate_ansatz_from_circuitcrea un ansatz parametrizzato a un livello e uno a due livelli; i parametri vengono ottimizzati utilizzando L-BFGS-B con gradienti accelerati da JAX per minimizzare . I primi passi utilizzano l’ansatz a un livello, mentre i passi successivi utilizzano l’ansatz a due livelli; all’interno di ciascuna fase, ogni passo parte dai parametri ottimizzati del passo precedente (warm-start), e i parametri vengono reimpostati ai valori predefiniti della fase al confine tra una fase e l’altra. - Assemblare circuiti misti — per gli intervalli di tempo successivi al checkpoint AQC, aggiungere strati di Trotter esatti al circuito AQC ottimizzato a due strati utilizzando
trotter_evolution.
# Number of time steps to compress into an AQC ansatz with one layer
aqc_n_steps_1 = 3
# Number of time steps to compress into an AQC ansatz with two layers
aqc_n_steps_2 = 2
aqc_n_steps_total = aqc_n_steps_1 + aqc_n_steps_2
# ── Step 2a: Target circuits (first aqc_n_steps_total circuits from Step 1) ──
target_circuits = {
k: circuits[k - 1] for k in range(1, aqc_n_steps_total + 1)
}
# ── Step 2b: Compute target MPS ──
# Decompose PauliEvolutionGate to RXX/RYY/RZZ before passing to the AQC MPS
# backend, which does not understand PauliEvolutionGate natively.
aqc_sim = QuimbSimulator(
quimb_circuit_factory=partial(
qtn.CircuitMPS,
gate_opts=dict(max_bond=mps_max_bond, cutoff=mps_cutoff),
),
autodiff_backend="jax",
)
print("\nStep 2b — target MPS:")
target_mps = {}
for k in range(1, aqc_n_steps_total + 1):
target_mps[k] = tensornetwork_from_circuit(
target_circuits[k].decompose(["PauliEvolution"]), aqc_sim
)
print(f" k={k}: max bond = {target_mps[k].psi.max_bond()}")
# ── Step 2c: Generate ansätze (one-layer and two-layer) and optimize parameters ──
ansatz_1, initial_params_1 = generate_ansatz_from_circuit(
target_circuits[1].decompose(["PauliEvolution"]),
qubits_initially_zero=True,
)
initial_params_1 = np.array(initial_params_1)
ansatz_2, initial_params_2 = generate_ansatz_from_circuit(
target_circuits[2].decompose(["PauliEvolution"]),
qubits_initially_zero=True,
)
initial_params_2 = np.array(initial_params_2)
print(
f"\nStep 2c — one-layer ansatz: {ansatz_1.num_parameters} parameters, "
f"2q depth (uniform basis) = {uniform_2q_depth(ansatz_1)}"
)
print(
f"Step 2c — two-layer ansatz: {ansatz_2.num_parameters} parameters, "
f"2q depth (uniform basis) = {uniform_2q_depth(ansatz_2)}"
)
aqc_circuits = {}
aqc_params = {}
for k in range(1, aqc_n_steps_total + 1):
if k <= aqc_n_steps_1:
ansatz, base_params = ansatz_1, initial_params_1
else:
ansatz, base_params = ansatz_2, initial_params_2
# Warm-start from the previous step only within the same stage
same_stage = (k - 1 >= 1) and (
(k - 1 <= aqc_n_steps_1) == (k <= aqc_n_steps_1)
)
x0 = aqc_params[k - 1] if same_stage else base_params
obj = MaximizeStateFidelity(target_mps[k], ansatz, aqc_sim)
t0 = timeit.default_timer()
result = scipy.optimize.minimize(
obj.loss_function,
x0,
method="L-BFGS-B",
jac=True,
options=dict(maxiter=100),
)
elapsed = timeit.default_timer() - t0
aqc_params[k] = result.x
aqc_circuits[k] = ansatz.assign_parameters(result.x)
print(
f" k={k}: fidelity = {1 - result.fun:.4f}, "
f"2q depth (uniform basis) = "
f"{uniform_2q_depth(aqc_circuits[k])}, "
f"{elapsed:.1f}s"
)
# ── Step 2d: Assemble full circuit set (AQC + Trotter) ──
all_circuits = []
for k in range(1, aqc_n_steps_total + 1):
all_circuits.append(aqc_circuits[k])
base = aqc_circuits[aqc_n_steps_total]
for k in range(1, n_steps - aqc_n_steps_total + 1):
circuit = base.copy()
for instr in trotter_evolution(
circuit.qubits, interaction, anisotropy, time_step, k
):
circuit.append(instr)
all_circuits.append(circuit)
full_depths = [
uniform_2q_depth(circuits[k - 1]) for k in range(1, n_steps + 1)
]
aqc_depths = [uniform_2q_depth(circuit) for circuit in all_circuits]
full_2q = [
circuits[k - 1].decompose(["PauliEvolution"]).num_nonlocal_gates()
for k in range(1, n_steps + 1)
]
aqc_2q = [
circuit.decompose(["PauliEvolution"]).num_nonlocal_gates()
for circuit in all_circuits
]
print(f"\nStep 2d — assembled {len(all_circuits)} circuits")
print(
f" At step {n_steps} (uniform basis): "
f"Trotter 2q depth = {full_depths[-1]}, AQC+Trotter 2q depth = {aqc_depths[-1]}; "
f"2q gates: Trotter = {full_2q[-1]}, AQC+Trotter = {aqc_2q[-1]}"
)
steps_axis = np.arange(1, n_steps + 1)
fig, ax = plt.subplots(figsize=(7, 4))
ax.plot(steps_axis, full_depths, "-o", color="black", label="Full Trotter")
ax.plot(
steps_axis, aqc_depths, "-o", color="cadetblue", label="AQC + Trotter"
)
ax.set_xlabel("Trotter steps", fontsize=13)
ax.xaxis.set_major_locator(plt.matplotlib.ticker.MaxNLocator(integer=True))
ax.set_ylabel("2q gate depth (uniform basis)", fontsize=13)
ax.set_title(f"AQC circuit-depth reduction ({n_qubits} qubits)", fontsize=13)
ax.legend(fontsize=11)
plt.tight_layout()
plt.show()Output:
Step 2b — target MPS:
k=1: max bond = 22
k=2: max bond = 22
k=3: max bond = 26
k=4: max bond = 27
k=5: max bond = 30
Step 2c — one-layer ansatz: 389 parameters, 2q depth (uniform basis) = 27
Step 2c — two-layer ansatz: 470 parameters, 2q depth (uniform basis) = 33
k=1: fidelity = 1.0000, 2q depth (uniform basis) = 27, 11.8s
k=2: fidelity = 0.9980, 2q depth (uniform basis) = 27, 13.2s
k=3: fidelity = 0.9890, 2q depth (uniform basis) = 27, 13.7s
k=4: fidelity = 0.9983, 2q depth (uniform basis) = 33, 16.4s
k=5: fidelity = 0.9957, 2q depth (uniform basis) = 33, 16.4s
Step 2d — assembled 10 circuits
At step 10 (uniform basis): Trotter 2q depth = 163, AQC+Trotter 2q depth = 99; 2q gates: Trotter = 371, AQC+Trotter = 200
Fase 3: Eseguire il comando utilizzando Qiskit primitives
Simuliamo ogni circuito compilato da AQC utilizzando la StatevectorEstimator primitiva.
estimator = StatevectorEstimator()
pubs = [(circuit, observables) for circuit in all_circuits]
job = estimator.run(pubs)
result = job.result()Fase 4: Elaborazione successiva e restituzione del risultato nel formato classico desiderato
Ora calcoliamo il valore atteso per ogni qubit in ogni istante . Questi valori formano la matrice della funzione di Green ritardata . Successivamente, applichiamo la trasformata di Fourier alla RGF per ottenere il fattore di struttura dinamico e tracciamo sia la RGF che il DSF. get_dsf applica la simmetria speculare e limita internamente i valori negativi.
rgf_mat = np.stack([pub_result.data.evs for pub_result in result])
# -- Compute DSF --
n_points_momentum, n_points_frequency = 100, 100
spectrum = get_dsf(
n_qubits,
rgf_mat,
time_step,
n_steps,
n_points_momentum,
n_points_frequency,
)
# -- Plot retarded Green's function --
plot_rgf(
n_qubits,
rgf_mat,
time_step,
n_steps,
title=f"Retarded Green's function — {n_qubits} qubits (simulation)",
)
# -- Plot DSF --
plot_dsf(
spectrum,
time_step,
n_points_momentum,
n_points_frequency,
title=f"Dynamical structure factor — {n_qubits} qubits (simulation)",
)Output:
Esecuzione hardware su larga scala
Ora passiamo a 50 qubit. A questa scala, le ottimizzazioni raggiungono livelli di fedeltà inferiori rispetto all’esempio su piccola scala: la fedeltà dell’ansatz dello stato fondamentale scende a circa 0.65, mentre le fedeltà AQC diminuiscono fino a circa 0.7 nei checkpoint successivi. Si tratta di un fenomeno prevedibile, ed è possibile migliorare tali livelli di fedeltà aumentando il numero di livelli dell'ansatz dello stato fondamentale (gs_n_layers) o il numero di iterazioni di ottimizzazione (maxiter) a un costo classico aggiuntivo. Si noti inoltre che le fedeltà dell’AQC a due livelli risultano inferiori rispetto a quelle a un solo livello. Non si tratta di una regressione: i passi temporali successivi generano un maggiore entanglement e sono semplicemente più difficili da comprimere, motivo per cui per essi viene utilizzato l’ansatz a due livelli, più espressivo.
L'ottimizzazione AQC può richiedere anche diverse ore di calcolo classico (circa sei ore nell'esecuzione qui illustrata, la maggior parte delle quali dedicata ai checkpoint a due livelli nella fase 2c ). Per ridurre il tempo di esecuzione effettivo, si consiglia di eseguire questo notebook su un hardware tradizionale più potente, come ad esempio un sistema di calcolo ad alte prestazioni (HPC). In alternativa, è possibile ridimensionare il problema a un'istanza più piccola (ad esempio, con un numero inferiore di qubit o di passi temporali); in tal caso, i risultati saranno diversi da quelli qui riportati.
Il codice riportato di seguito segue la stessa struttura in quattro fasi dell'esempio su piccola scala. I parametri dello stato fondamentale dell'HVA vengono nuovamente ottimizzati mediante simulazione MPS. Sulla QPU, attiviamo il disaccoppiamento dinamico (DD), il “Pauli twirling” e l’estinzione degli errori di lettura con rotazione (TREX) per la soppressione e la mitigazione degli errori. La tabella seguente riassume le differenze tra l'esperimento su larga scala e quello su piccola scala:
Su piccola scala | BILANCIA | |
|---|---|---|
| Qubit | 10 | 50 |
| Intervalli di tempo | 10 | 20 |
| Punti di controllo AQC (1 strato + 2 strati) | 3 + 2 = 5 | 6 + 4 = 10 |
| Strati di ansatz dello stato fondamentale | 3 | 5 |
| Dimensione massima del collegamento MPS | 32 | 128 |
| Stimatore | StatevectorEstimator | QPU con DD, Pauli twirling e TREX |
Durante l'ottimizzazione dell'AQC (Fase 2c di seguito) potrebbero comparire stderr messaggi del tipo:
[Compiling module jit_MakeArrayFn for CPU] Very slow compile? ...
The operation took 2m14s
Si tratta di messaggi di diagnostica non critici provenienti da XLA, il compilatore alla base qiskit-addon-aqc-tensordella funzione "autodiff" di JAX. Con 50 qubit e una dimensione del legame MPS pari a 128, XLA impiega un paio di minuti a compilare la funzione gradiente la prima volta che viene tracciata. La compilazione va a buon fine e i risultati dell'ottimizzazione non subiscono alcuna variazione.
# ── Parameters ──────────────────────────────────────────────────────────────
n_qubits = 50 # 10 → 50
interaction = 1.0 # J
anisotropy = 1.0 # ε (isotropic point)
time_step = 0.6
n_steps = 20 # 10 → 20
aqc_n_steps_1 = 6 # 3 → 6
aqc_n_steps_2 = 4 # 2 → 4
aqc_n_steps_total = aqc_n_steps_1 + aqc_n_steps_2
gs_n_layers = 5 # 3 → 5
mps_max_bond = 128 # 32 -> 128
mps_cutoff = 1e-8
center = n_qubits // 2 - 1
# ── Step 1: Map ──────────────────────────────────────────────────────────────
ham_mpo = xxz_hamiltonian_mpo(n_qubits, interaction, anisotropy)
dmrg = qtn.DMRG2(ham_mpo)
dmrg.solve(tol=1e-8)
print(f"Ground-state energy (DMRG): {dmrg.energy:.6f}")
gs_ansatz = build_ground_state_ansatz(n_qubits, gs_n_layers)
rng = np.random.default_rng(12345)
x0 = np.tile([0.0, np.pi / 2], gs_n_layers) + rng.normal(
scale=0.1, size=2 * gs_n_layers
)
print("Optimizing ground state ansatz...")
t0 = timeit.default_timer()
result = optimize_ground_state_ansatz(
gs_ansatz,
x0,
dmrg.state,
max_bond=mps_max_bond,
cutoff=mps_cutoff,
options=dict(maxiter=100),
)
t1 = timeit.default_timer()
print(f"Finished optimizing ground state ansatz in {t1 - t0} seconds.")
print(f"Ground state ansatz fidelity: {1 - result.fun:.6f}")
gs_circuit = gs_ansatz.assign_parameters(result.x)
gs_circuit_mps = quimb_circuit(
gs_circuit.decompose(["PauliEvolution"]),
quimb_circuit_class=qtn.CircuitMPS,
max_bond=mps_max_bond,
cutoff=mps_cutoff,
)
gs_ansatz_energy = qtn.expec_TN_1D(
gs_circuit_mps.psi.H, ham_mpo, gs_circuit_mps.psi
)
print(f"Ground state ansatz energy: {gs_ansatz_energy:.6f}")
perturbed = gs_circuit.copy()
perturbed.rz(np.pi / 2, center)
circuits = []
for t in range(1, n_steps + 1):
circuit = perturbed.copy()
for instr in trotter_evolution(
circuit.qubits, interaction, anisotropy, time_step, t
):
circuit.append(instr)
circuits.append(circuit)
print(
f"Built {len(circuits)} circuits, deepest 2q depth (uniform basis) = "
f"{uniform_2q_depth(circuits[-1])}"
)
observables = [
SparsePauliOp.from_sparse_list([("Z", [i], 1)], num_qubits=n_qubits)
for i in range(n_qubits)
]
print(f"Defined {len(observables)} Z observables.")
# ── Step 2: AQC ──────────────────────────────────────────────────────────────
target_circuits = {
k: circuits[k - 1] for k in range(1, aqc_n_steps_total + 1)
}
aqc_sim = QuimbSimulator(
quimb_circuit_factory=partial(
qtn.CircuitMPS,
gate_opts=dict(max_bond=mps_max_bond, cutoff=mps_cutoff),
),
autodiff_backend="jax",
)
print("\nStep 2b — target MPS:")
target_mps = {}
for k in range(1, aqc_n_steps_total + 1):
target_mps[k] = tensornetwork_from_circuit(
target_circuits[k].decompose(["PauliEvolution"]), aqc_sim
)
print(f" k={k}: max bond = {target_mps[k].psi.max_bond()}")
ansatz_1, initial_params_1 = generate_ansatz_from_circuit(
target_circuits[1].decompose(["PauliEvolution"]),
qubits_initially_zero=True,
)
initial_params_1 = np.array(initial_params_1)
ansatz_2, initial_params_2 = generate_ansatz_from_circuit(
target_circuits[2].decompose(["PauliEvolution"]),
qubits_initially_zero=True,
)
initial_params_2 = np.array(initial_params_2)
print(
f"\nStep 2c — one-layer ansatz: {ansatz_1.num_parameters} parameters, "
f"2q depth (uniform basis) = {uniform_2q_depth(ansatz_1)}"
)
print(
f"Step 2c — two-layer ansatz: {ansatz_2.num_parameters} parameters, "
f"2q depth (uniform basis) = {uniform_2q_depth(ansatz_2)}"
)
aqc_circuits = {}
aqc_params = {}
for k in range(1, aqc_n_steps_total + 1):
if k <= aqc_n_steps_1:
ansatz, base_params = ansatz_1, initial_params_1
else:
ansatz, base_params = ansatz_2, initial_params_2
# Warm-start from the previous step only within the same stage
same_stage = (k - 1 >= 1) and (
(k - 1 <= aqc_n_steps_1) == (k <= aqc_n_steps_1)
)
x0 = aqc_params[k - 1] if same_stage else base_params
obj = MaximizeStateFidelity(target_mps[k], ansatz, aqc_sim)
t0 = timeit.default_timer()
result = scipy.optimize.minimize(
obj.loss_function,
x0,
method="L-BFGS-B",
jac=True,
options=dict(maxiter=100),
)
elapsed = timeit.default_timer() - t0
aqc_params[k] = result.x
aqc_circuits[k] = ansatz.assign_parameters(result.x)
print(
f" k={k}: fidelity = {1 - result.fun:.4f}, "
f"2q depth (uniform basis) = "
f"{uniform_2q_depth(aqc_circuits[k])}, "
f"{elapsed:.1f}s"
)
all_circuits = []
for k in range(1, aqc_n_steps_total + 1):
all_circuits.append(aqc_circuits[k])
base = aqc_circuits[aqc_n_steps_total]
for k in range(1, n_steps - aqc_n_steps_total + 1):
circuit = base.copy()
for instr in trotter_evolution(
circuit.qubits, interaction, anisotropy, time_step, k
):
circuit.append(instr)
all_circuits.append(circuit)
full_depths = [
uniform_2q_depth(circuits[k - 1]) for k in range(1, n_steps + 1)
]
aqc_depths = [uniform_2q_depth(circuit) for circuit in all_circuits]
full_2q = [
circuits[k - 1].decompose(["PauliEvolution"]).num_nonlocal_gates()
for k in range(1, n_steps + 1)
]
aqc_2q = [
circuit.decompose(["PauliEvolution"]).num_nonlocal_gates()
for circuit in all_circuits
]
print(f"\nStep 2d — assembled {len(all_circuits)} circuits")
print(
f" At step {n_steps} (uniform basis): "
f"Trotter 2q depth = {full_depths[-1]}, AQC+Trotter 2q depth = {aqc_depths[-1]}; "
f"2q gates: Trotter = {full_2q[-1]}, AQC+Trotter = {aqc_2q[-1]}"
)
steps_axis = np.arange(1, n_steps + 1)
fig, ax = plt.subplots(figsize=(7, 4))
ax.plot(steps_axis, full_depths, "-o", color="black", label="Full Trotter")
ax.plot(
steps_axis, aqc_depths, "-o", color="cadetblue", label="AQC + Trotter"
)
ax.set_xlabel("Trotter steps", fontsize=13)
ax.xaxis.set_major_locator(plt.matplotlib.ticker.MaxNLocator(integer=True))
ax.set_ylabel("2q gate depth (uniform basis)", fontsize=13)
ax.set_title(f"AQC circuit-depth reduction ({n_qubits} qubits)", fontsize=13)
ax.legend(fontsize=11)
plt.tight_layout()
plt.show()
# ── Step 3: Execute on IBM Quantum hardware ───────────────────────────────────
# (replaces StatevectorEstimator)
service = QiskitRuntimeService()
backend = service.least_busy(
min_num_qubits=n_qubits,
operational=True,
simulator=False,
filters=lambda x: x.configuration().processor_type["family"] == "Heron",
)
print(f"Backend: {backend.name}")
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)
isa_circuits = pm.run(all_circuits, num_processes=1)
isa_2q_depths = [
isa_circuit.depth(lambda inst: inst.operation.num_qubits == 2)
for isa_circuit in isa_circuits
]
print(
f"Transpiled 2q depth (deepest, ISA on {backend.name}): "
f"{max(isa_2q_depths)} "
f"(2q gates: {max(isa_circuit.num_nonlocal_gates() for isa_circuit in isa_circuits)})"
)
estimator = Estimator(backend)
estimator.options.environment.job_tags = ["TUT_SNS"]
estimator.options.dynamical_decoupling.enable = True
estimator.options.dynamical_decoupling.sequence_type = "XY4"
estimator.options.twirling.enable_gates = True
estimator.options.twirling.num_randomizations = 1000
estimator.options.twirling.shots_per_randomization = 128
estimator.options.resilience.measure_mitigation = True
estimator.options.resilience.measure_noise_learning.num_randomizations = 32
estimator.options.resilience.measure_noise_learning.shots_per_randomization = 100
pubs = [
(
isa_circuit,
[obs.apply_layout(isa_circuit.layout) for obs in observables],
)
for isa_circuit in isa_circuits
]
job = estimator.run(pubs)
print(f"Job ID: {job.job_id()}")
result = job.result()
# ── Step 4: Post-process ──────────────────────────────────────────────────────
rgf_mat = np.stack([pub_result.data.evs for pub_result in result])
n_points_momentum, n_points_frequency = 100, 100
spectrum = get_dsf(
n_qubits,
rgf_mat,
time_step,
n_steps,
n_points_momentum,
n_points_frequency,
)
plot_rgf(
n_qubits,
rgf_mat,
time_step,
n_steps,
title=f"Retarded Green's function — {n_qubits} qubits (QPU)",
)
plot_dsf(
spectrum,
time_step,
n_points_momentum,
n_points_frequency,
title=rf"KCuF$_3$ DSF — {n_qubits} qubits (QPU)"
"\n(AQC + DD + Pauli twirling + TREX)",
)Output:
Ground-state energy (DMRG): -21.972109
Optimizing ground state ansatz...
Finished optimizing ground state ansatz in 132.2076231740648 seconds.
Ground state ansatz fidelity: 0.645956
Ground state ansatz energy: -21.616744
Built 20 circuits, deepest 2q depth (uniform basis) = 307
Defined 50 Z observables.
Step 2b — target MPS:
k=1: max bond = 44
k=2: max bond = 46
k=3: max bond = 53
k=4: max bond = 62
k=5: max bond = 75
k=6: max bond = 96
k=7: max bond = 118
k=8: max bond = 128
k=9: max bond = 128
k=10: max bond = 128
Step 2c — one-layer ansatz: 2971 parameters, 2q depth (uniform basis) = 39
Step 2c — two-layer ansatz: 3412 parameters, 2q depth (uniform basis) = 45
k=1: fidelity = 1.0000, 2q depth (uniform basis) = 39, 215.6s
k=2: fidelity = 0.9676, 2q depth (uniform basis) = 39, 269.8s
k=3: fidelity = 0.9014, 2q depth (uniform basis) = 39, 293.5s
k=4: fidelity = 0.8755, 2q depth (uniform basis) = 39, 279.6s
k=5: fidelity = 0.8450, 2q depth (uniform basis) = 39, 266.4s
k=6: fidelity = 0.8146, 2q depth (uniform basis) = 39, 371.0s
E0626 02:59:07.229070 1508496 slow_operation_alarm.cc:73]
********************************
[Compiling module jit_MakeArrayFn for CPU] Very slow compile? If you want to file a bug, run with envvar XLA_FLAGS=--xla_dump_to=/tmp/foo and attach the results.
********************************
E0626 02:59:21.713796 1508477 slow_operation_alarm.cc:140] The operation took 2m14.484932074s
********************************
[Compiling module jit_MakeArrayFn for CPU] Very slow compile? If you want to file a bug, run with envvar XLA_FLAGS=--xla_dump_to=/tmp/foo and attach the results.
********************************
k=7: fidelity = 0.7312, 2q depth (uniform basis) = 45, 10865.3s
k=8: fidelity = 0.7720, 2q depth (uniform basis) = 45, 1450.3s
k=9: fidelity = 0.7316, 2q depth (uniform basis) = 45, 3388.6s
E0626 07:21:05.148724 1508496 slow_operation_alarm.cc:73]
********************************
[Compiling module jit_MakeArrayFn for CPU] Very slow compile? If you want to file a bug, run with envvar XLA_FLAGS=--xla_dump_to=/tmp/foo and attach the results.
********************************
E0626 07:21:24.286095 1508477 slow_operation_alarm.cc:140] The operation took 2m19.137566498s
********************************
[Compiling module jit_MakeArrayFn for CPU] Very slow compile? If you want to file a bug, run with envvar XLA_FLAGS=--xla_dump_to=/tmp/foo and attach the results.
********************************
k=10: fidelity = 0.6726, 2q depth (uniform basis) = 45, 3396.1s
Step 2d — assembled 20 circuits
At step 20 (uniform basis): Trotter 2q depth = 307, AQC+Trotter 2q depth = 171; 2q gates: Trotter = 3775, AQC+Trotter = 1913
Backend: ibm_fez
Transpiled 2q depth (deepest, ISA on ibm_fez): 105 (2q gates: 2574)
Job ID: d8v39vhropqc738biotg
I risultati ottenuti con l'hardware riproducono le caratteristiche principali del continuo a due spinoni: l'intensità di scattering è concentrata in prossimità del vettore d'onda antiferromagnetico alle basse energie ed è limitata dal basso dalla dispersione sinusoidale degli spinoni, con un ampio continuo di peso spettrale al di sopra di essa anziché una singola modalità ben definita. Si tratta della stessa struttura misurata mediante diffusione inelastica di neutroni su un sistema di tipo “ KCuF ”, e conferma la validità dell’intero flusso di lavoro, che comprende la preparazione dello stato fondamentale, la perturbazione, l’evoluzione di Trotter compressa con AQC e la misurazione con mitigazione degli errori su una scala di 50 qubit.
Passi successivi
Se questo lavoro ti è sembrato interessante, potrebbero interessarti anche i seguenti contenuti:
- Lee et al., "Benchmarking della simulazione quantistica con esperimenti di diffusione dei neutroni" ( arXiv:2603.15608 ) — l'articolo di riferimento su cui si basa questo tutorial
- Tecniche di mitigazione e soppressione degli errori — DD, Pauli twirling e TREX utilizzate negli esperimenti hardware
- Compilazione quantistica approssimativa per circuiti di evoluzione temporale — tutorial su AQC-Tensor
- Documentazione di AQC-Tensor