Перейти до основного вмісту
Цей проєкт у бета-версії. Знайшов(-ла) помилку? Відкрити issue на GitHub. Є запитання чи ідея? Запитати в Discussions — будемо раді твоєму відгуку!
Яка якість перекладу на цій сторінці?

Крилівська квантова діагоналізація решіткових гамільтоніанів

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

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

  • Як інтерпретувати крилівську квантову діагоналізацію (KQD) як навчання скінченної функції гамільтоніана, яка діє як спектральний фільтр.

  • Як побудувати проєктований гамільтоніан і матриці перекриття за допомогою розширених вимірювань swap-тесту.

  • Як розв'язати отриману узагальнену задачу власних значень (GEVP) і відновити оцінку енергії основного стану для решіткового гамільтоніана.

Передумови​

Теоретичні відомості​

Цей посібник демонструє, як реалізувати алгоритм крилівської квантової діагоналізації (KQD) в контексті патернів Qiskit. Ти спочатку вивчиш теорію, що лежить в основі алгоритму, а потім побачиш демонстрацію його виконання на QPU.

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

Існує кілька квантово-обчислювальних підходів до цієї проблеми. Близькотермінові варіаційні методи, такі як варіаційний квантовий розв'язувач власних значень (VQE), використовують відносно неглибокі параметризовані схеми, але вони вимагають нелінійного циклу класичної оптимізації з багатьма обчисленнями квантової схеми. На іншому кінці, квантова оцінка фази (QPE) забезпечує більш прямий шлях до оцінки власних значень з суворими гарантіями, але стандартна QPE вимагає довгих когерентних схем і в основному підходить для відмовостійких квантових комп'ютерів. KQD перебуває між цими двома підходами: вона використовує еволюцію гамільтоніана в реальному часі, як в алгоритмах на основі оцінки фази, але замінює повну оцінку фази компактною проєктованою задачею власних значень, яку можна розв'язати класично.

Розглянь nn-кубітний гамільтоніан HH та референсний стан ∣ψ0⟩\lvert \psi_{0}\rangle. Метод KQD будує крилівський підпростір з еволюціонованих у реальному часі станів,

∣ψℓ⟩=e−iℓΔtH∣ψ0⟩,ℓ=0,1,…,r−1,\begin{equation*} \lvert \psi_\ell\rangle = e^{-i\ell\Delta t H}\lvert \psi_{0}\rangle, \qquad \ell = 0,1,\ldots,r-1, \end{equation*}

де rr — крилівська розмірність, а Δt\Delta t — крок часу. Будь-який стан у крилівському підпросторі тоді представляється як лінійна комбінація цих базисних станів,

∣ψ(c)⟩=∑ℓ=0r−1cℓ∣ψℓ⟩∥∑ℓ=0r−1cℓ∣ψℓ⟩∥,\begin{equation*} \lvert \psi(\mathbf{c})\rangle = \frac{\sum_{\ell=0}^{r-1} c_\ell \lvert \psi_\ell\rangle} {\left\|\sum_{\ell=0}^{r-1} c_\ell \lvert \psi_\ell\rangle\right\|}, \end{equation*}

where the denominator normalizes the state.

За допомогою простої алгебри ми можемо побачити, що відповідна енергія записується як відношення Релея,

E(c)=⟨ψ(c)∣H∣ψ(c)⟩=∑k,ℓck∗cℓ⟨ψk∣H∣ψℓ⟩∑k,ℓck∗cℓ⟨ψk∣ψℓ⟩=c†Hcc†Sc.\begin{equation*} E(\mathbf{c}) =\langle \psi(\mathbf{c})|H|\psi(\mathbf{c}) \rangle= \frac{ \sum_{k,\ell} c_k^* c_\ell \langle \psi_k\vert H\vert\psi_\ell\rangle }{ \sum_{k,\ell} c_k^* c_\ell \langle \psi_k\vert\psi_\ell\rangle } = \frac{ \mathbf{c}^{\dagger}\mathcal{H}\mathbf{c} }{ \mathbf{c}^{\dagger}\mathcal{S}\mathbf{c} }. \end{equation*}

Тут матриці S\mathcal{S} та H\mathcal{H},

Skℓ=⟨ψk∣ψℓ⟩,Hkℓ=⟨ψk∣H∣ψℓ⟩\begin{equation*} \mathcal{S}_{k\ell}=\langle \psi_k\vert\psi_\ell\rangle, \qquad \mathcal{H}_{k\ell}=\langle \psi_k\vert H\vert\psi_\ell\rangle \end{equation*}

визначають проєктовані матриці перекриття та гамільтоніана. Їхні елементи оцінюються за допомогою вимірювань квантової схеми.

Ми прагнемо знайти коефіцієнт c\mathbf{c}, який дає мінімум E(c)E(\mathbf{c}):

min⁡c≠0E(c).\begin{equation*} \min_{\mathbf{c}\neq \mathbf{0}} E(\mathbf{c}). \end{equation*}

За теоремою Релея-Рітца, ця мінімізація еквівалентна розв'язанню узагальненої задачі власних значень (GEVP),

Hc=ESc.\begin{equation*} \mathcal{H}\mathbf{c}=E \mathcal{S}\mathbf{c}. \end{equation*}

Зауваж, що розмірність rr може бути достатньо малою, щоб класичний комп'ютер міг розв'язати GEVP.

Це той самий варіаційний принцип, що використовується в класичній діагоналізації підпростору, але тут базисні стани генеруються квантовою еволюцією в часі. Порівняно з VQE, KQD зазвичай вимагає глибших схем, оскільки вона покладається на еволюцію в реальному часі. Взамін, KQD уникає нелінійної оптимізації параметрів та ітеративного виконання на квантовому апаратному забезпеченні, і вона систематично покращується зі збільшенням проєктованого підпростору. Алгоритм був продемонстрований у великому масштабі на існуючому квантовому апаратному забезпеченні [2], і його продуктивність можна аналізувати з доказовими гарантіями [1].

Вимоги​

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

  • Qiskit SDK v2.3 або новіше, з підтримкою візуалізації

  • Qiskit Runtime v0.22 або новіше ( pip install qiskit-ibm-runtime )

  • SciPy (pip install scipy)

  • Matplotlib (pip install matplotlib)

  • Pandas (pip install pandas)

Виконання на апаратному забезпеченні вимагає qiskit-ibm-runtime та доступу до облікового запису IBM Quantum®.

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

Комірка налаштування імпортує необхідні модулі та визначає допоміжні функції для робочого процесу:

  1. побудувати гамільтоніан Гайзенберга;

  2. розв'язати порогову GEVP;

  3. обчислити навчений крилівський фільтр;

  4. перетворити значення фільтра на спектральні ваги;

  5. побудувати графіки еталонного та відфільтрованого розподілів енергії.

# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util

_needed = {"matplotlib": "matplotlib", "numpy": "numpy", "pandas": "pandas", "qiskit": "qiskit", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "scipy": "scipy"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
from __future__ import annotations

import warnings

import numpy as np
import pandas as pd
import scipy.linalg as la
import matplotlib.pyplot as plt

from qiskit import QuantumCircuit, transpile
from qiskit.circuit import Parameter
from qiskit.circuit.library import PauliEvolutionGate
from qiskit.primitives import StatevectorEstimator
from qiskit.quantum_info import Operator, SparsePauliOp
from qiskit.synthesis import LieTrotter, SuzukiTrotter
from qiskit.transpiler import PassManager, Layout
from qiskit.transpiler.passes import CommutativeOptimization
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService, EstimatorV2, Batch
from qiskit_ibm_runtime.fake_provider import FakeMarrakesh

warnings.filterwarnings("ignore")

def make_heisenberg_hamiltonian(
num_qubits: int,
coupling: float = 1.0,
) -> SparsePauliOp:
"""Make a Heisenberg Hamiltonian for a 1D chain of qubits with nearest-neighbor interactions."""
terms: list[tuple[str, complex]] = []

def append_term(q0: int, q1: int, pauli: str):
label = ["I"] * num_qubits
label[num_qubits - 1 - q0] = pauli[0]
label[num_qubits - 1 - q1] = pauli[1]
terms.append(("".join(label), coupling))

for pauli in ("XX", "YY", "ZZ"):
for q in range(num_qubits - 1):
append_term(q, q + 1, pauli)
return SparsePauliOp.from_list(terms).simplify()

def _basis_state_transition_amplitude_sparse(
hamiltonian: SparsePauliOp,
bra_state: int,
ket_state: int,
) -> complex:
"""Evaluate <bra_state|H|ket_state> for computational-basis states."""
num_qubits = hamiltonian.num_qubits
amplitude = 0.0 + 0.0j
for pauli, coeff in zip(hamiltonian.paulis, hamiltonian.coeffs):
new_state = ket_state
phase = 1.0 + 0.0j
for q in range(num_qubits):
x = bool(pauli.x[q])
z = bool(pauli.z[q])
if not x and not z:
continue
bit = (new_state >> q) & 1
if x and z:
# Y|0> = i|1>, Y|1> = -i|0>
phase *= 1j if bit == 0 else -1j
new_state ^= 1 << q
elif x:
new_state ^= 1 << q
else:
# Z|0> = |0>, Z|1> = -|1>
if bit:
phase *= -1
if new_state == bra_state:
amplitude += coeff * phase
return amplitude

def basis_state_expectation_sparse(
hamiltonian: SparsePauliOp,
bitstring: str,
) -> complex:
"""Evaluate <bitstring|H|bitstring>."""
state = int(bitstring, 2)
return _basis_state_transition_amplitude_sparse(
hamiltonian,
bra_state=state,
ket_state=state,
)

def diagonalize_single_1_subspace(
hamiltonian: SparsePauliOp,
) -> np.ndarray:
"""Diagonalize the Hamiltonian projected onto the single-excitation subspace."""
num_qubits = hamiltonian.num_qubits

# Integer basis states |...010...>, with the excitation at qubit k.
basis = [1 << k for k in range(num_qubits)]

h_single = np.empty((num_qubits, num_qubits), dtype=complex)

for row, bra_state in enumerate(basis):
for col, ket_state in enumerate(basis):
h_single[row, col] = _basis_state_transition_amplitude_sparse(
hamiltonian,
bra_state=bra_state,
ket_state=ket_state,
)
# Remove floating-point-level asymmetry.
h_single = 0.5 * (h_single + h_single.conj().T)
evals, _ = np.linalg.eigh(h_single)
return np.real(evals)

def simple_transpilation(circuit: QuantumCircuit) -> QuantumCircuit:
"""Transpilation to simplify the circuit"""
pm = PassManager(
[
CommutativeOptimization(),
]
)
circuit = transpile(circuit, optimization_level=3)
circuit = pm.run(circuit)
return circuit

def summarize_circuit(circuit: QuantumCircuit) -> dict[str, int | str]:
"""Summarize the circuit with depth, size, and 2-qubit gate information."""
two_qubit_total = sum(
inst.operation.num_qubits == 2 for inst in circuit.data
)
two_qubit_depth = circuit.depth(lambda x: x[0].num_qubits == 2)
return {
"depth": circuit.depth(),
"size": circuit.size(),
"2q gates": two_qubit_total,
"2q depth": two_qubit_depth,
}

def solve_thresholded_gevp(
h_matrix: np.ndarray,
s_matrix: np.ndarray,
threshold: float = 1e-10,
) -> tuple[float, np.ndarray, int]:
"""Solve H c = E S c using canonical orthogonalization of S."""
s_vals, s_vecs = la.eigh(s_matrix)

valid = s_vals > threshold
if not np.any(valid):
raise ValueError(
"All overlap eigenvalues were removed by thresholding."
)

keep = valid

orthogonalizer = s_vecs[:, keep] @ np.diag(1.0 / np.sqrt(s_vals[keep]))
h_orth = orthogonalizer.conj().T @ h_matrix @ orthogonalizer
h_orth = 0.5 * (h_orth + h_orth.conj().T)

eigvals, eigvecs = la.eigh(h_orth)

coeffs = orthogonalizer @ eigvecs[:, 0]
normalization = np.sqrt(np.real(coeffs.conj().T @ s_matrix @ coeffs))
coeffs /= normalization

return float(np.real(eigvals[0])), coeffs, int(np.sum(keep))

У першій частині цього туторіалу ми демонструємо метод KQD, використовуючи локальний симулятор вектора стану. Далі ми використовуємо реальний квантовий бекенд для розв'язання задачі корисного масштабу.

Ми також визначаємо фейковий бекенд, щоб продемонструвати транспіляцію, специфічну для бекенду, та перевірити отриману схему.

try:
service = QiskitRuntimeService()
except Exception:
QiskitRuntimeService.save_account(
token="<api_token>", instance="<instance>", overwrite=True
)
service = QiskitRuntimeService()

backend = FakeMarrakesh()

Приклад малого масштабу з симулятором​

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

Гамільтоніан і еталонний стан​

У цьому прикладі використовується ланцюжок Гайзенберга з відкритими межами на 12 кубітів (n=12n=12),

H=∑i=0n−2(XiXi+1+YiYi+1+ZiZi+1),\begin{equation*} H=\sum_{i=0}^{n-2}\left(X_iX_{i+1}+Y_iY_{i+1}+Z_iZ_{i+1}\right), \end{equation*}

з добутковим станом з одним збудженням

∣ψ0⟩=∣000001000000⟩\begin{equation*} |\psi_{0}\rangle=|000001000000\rangle \end{equation*}

як еталонний стан. Оскільки визначений вище гамільтоніан Гайзенберга зберігає загальну кількість збуджень, еталонний стан залишається у підпросторі з одним збудженням, розмірність якого зростає лише лінійно з кількістю кубітів. Тому ми можемо ефективно обчислити точну енергію основного стану, діагоналізувавши гамільтоніан, обмежений цим підпростором, і використати це виключно як діагностичний орієнтир для оцінки KQD. Сам робочий процес KQD оцінює проєктовані матричні елементи за допомогою примітивів Qiskit і розв'язує отриману проєктовану задачу класично.

# Problem definition for the simulator example
num_qubits = 12
hamiltonian = make_heisenberg_hamiltonian(num_qubits=num_qubits, coupling=1.0)
ref_bitstring = "000001000000"
ref_energy = basis_state_expectation_sparse(hamiltonian, ref_bitstring)

print("Hamiltonian:")
print(hamiltonian)
print(f"Reference state: |{ref_bitstring}>")
print("Reference energy: ", ref_energy)
Hamiltonian:
SparsePauliOp(['IIIIIIIIIIXX', 'IIIIIIIIIXXI', 'IIIIIIIIXXII', 'IIIIIIIXXIII', 'IIIIIIXXIIII', 'IIIIIXXIIIII', 'IIIIXXIIIIII', 'IIIXXIIIIIII', 'IIXXIIIIIIII', 'IXXIIIIIIIII', 'XXIIIIIIIIII', 'IIIIIIIIIIYY', 'IIIIIIIIIYYI', 'IIIIIIIIYYII', 'IIIIIIIYYIII', 'IIIIIIYYIIII', 'IIIIIYYIIIII', 'IIIIYYIIIIII', 'IIIYYIIIIIII', 'IIYYIIIIIIII', 'IYYIIIIIIIII', 'YYIIIIIIIIII', 'IIIIIIIIIIZZ', 'IIIIIIIIIZZI', 'IIIIIIIIZZII', 'IIIIIIIZZIII', 'IIIIIIZZIIII', 'IIIIIZZIIIII', 'IIIIZZIIIIII', 'IIIZZIIIIIII', 'IIZZIIIIIIII', 'IZZIIIIIIIII', 'ZZIIIIIIIIII'],
coeffs=[1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,
1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,
1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,
1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j])
Reference state: |000001000000>
Reference energy: (7+0j)

Встановлення параметрів алгоритму​

На основі верхніх меж норми гамільтоніана, посилання [1] евристично пропонує часовий крок Δt\Delta t як π/∥H∥\pi/\|H\|. Оскільки спектральну норму ∥H∥\|H\| важко обчислити, ми натомість використовуємо її верхню межу:

∥H∥≤∑i=0n−2∥XiXi+1+YiYi+1+ZiZi+1∥⏟4×4 matrix, easy to calculate the norm=3(n−1).\begin{equation*} \|H\| \le \sum_{i=0}^{n-2}\underbrace{\|X_i X_{i+1}+Y_{i} Y_{i+1}+Z_{i} Z_{i+1}\|}_{4 \times 4 \text{ matrix, easy to calculate the norm}} = 3(n-1). \end{equation*}

Ми встановлюємо крилівську розмірність r=10r=10 і кількість кроків Троттера на часовий крок 55: достатньо великий крилівський підпростір, щоб розрізнити нижню частину спектра, і водночас не надто дорогу найглибшу схему (tmax⁡=(r−1)Δtt_{\max}=(r-1)\Delta t), а також достатньо кроків Троттера, щоб похибка дискретизації на цій найглибшій схемі залишалася малою.

dt = np.pi / (3 * (num_qubits - 1))
print("dt in Krylov basis: ", dt)

krylov_dim = 10
num_trotter_steps = 5
dt in Krylov basis: 0.09519977738150888

Побудова схеми​

Тут ми будуємо схеми для оцінки матричних елементів Hkℓ\mathcal{H}_{k\ell} і Skℓ\mathcal{S}_{k\ell}. Оскільки всі степені HH комутують, маємо

Hkℓ=⟨ψ0∣He−i(ℓ−k)ΔtH∣ψ0⟩=H0,ℓ−k,Skℓ=⟨ψ0∣e−i(ℓ−k)ΔtH∣ψ0⟩=S0,ℓ−k.\begin{equation*} \mathcal{H}_{k\ell} = \langle\psi_{0}|H e^{-i(\ell-k)\Delta tH}|\psi_{0}\rangle=\mathcal{H}_{0,\ell-k}, \qquad \mathcal{S}_{k\ell} = \langle\psi_{0}|e^{-i(\ell-k)\Delta tH}|\psi_{0}\rangle=\mathcal{S}_{0,\ell-k}. \end{equation*}

Such matrices where the elements depend on the index differences in this manner are called Toeplitz and can be reconstructed from first-row elements indexed by d=ℓ−kd=\ell-k.

Тут ми представляємо схему, яка називається extended-swap-test, яка готує

∣Φ0d⟩=∣0⟩∣ψ0⟩+∣1⟩∣ψd⟩2,\begin{equation*} |\Phi_{0d}\rangle= \frac{|0\rangle|\psi_0\rangle+|1\rangle|\psi_d\rangle}{\sqrt{2}}, \end{equation*}

де ∣ψd⟩=e−idΔtH∣ψ0⟩|\psi_d\rangle=e^{-id\Delta tH}|\psi_{0}\rangle.

Еталонний стан​

Ми готуємо еталонний стан ∣ψ0⟩|\psi_0\rangle.

qc_ref = QuantumCircuit(num_qubits)
for i, b in enumerate(reversed(ref_bitstring)):
if b == "1":
qc_ref.x(i)
display(qc_ref.draw("mpl", scale=0.5))

Output of the previous code cell

Часова еволюція​

Реалізуйте оператор часової еволюції, згенерований гамільтоніаном, наближений простою лі-троттеризацією.

t = Parameter("t")

evol_gate = PauliEvolutionGate(
hamiltonian,
time=t,
synthesis=LieTrotter(reps=num_trotter_steps),
label="U(t)",
)

# Synthesize U(t) first and then control the synthesized circuit.
# This makes the controlled structure visible in the circuit drawer.
evolution_circuit = QuantumCircuit(num_qubits, name="U(t)")
evolution_circuit.append(evol_gate, range(num_qubits))
evolution_circuit = simple_transpilation(evolution_circuit)

# Make a controlled version of the evolution circuit.
controlled_evolution_gate = evolution_circuit.to_gate(label="U(t)").control(
1, label="C-U(t)"
)
display(
evolution_circuit.assign_parameters({t: 1.5}).draw(
"mpl", scale=0.5, fold=-1
)
)

Output of the previous code cell

Розширений тест на обмін [3]​

Схема спочатку готує еталонний стан на системному регістрі, тоді як анцила залишається у стані ∣0⟩|0\rangle:

∣0⟩∣0⟩⊗n⟶∣0⟩∣ψ0⟩.\begin{equation*} |0\rangle |0\rangle^{\otimes n} \longrightarrow |0\rangle |\psi_{0}\rangle . \end{equation*}

Потім застосування вентиля Адамара до анцили створює когерентну суперпозицію двох гілок:

∣0⟩∣ψ0⟩⟶∣0⟩+∣1⟩2∣ψ0⟩=∣0⟩∣ψ0⟩+∣1⟩∣ψ0⟩2.\begin{equation*} |0\rangle |\psi_0\rangle \longrightarrow \frac{|0\rangle + |1\rangle}{\sqrt{2}} |\psi_0\rangle = \frac{|0\rangle|\psi_0\rangle + |1\rangle|\psi_0\rangle}{\sqrt{2}} . \end{equation*}

Нарешті, застосовується керований вентиль часової еволюції:

U(t=dΔt)=e−idΔtH\begin{equation*} U(t=d\Delta t) = e^{-id\Delta t H} \end{equation*}

лише коли анцила перебуває у гілці ∣1⟩|1\rangle. Отже,

∣0⟩∣ψ0⟩+∣1⟩∣ψ0⟩2⟶∣0⟩∣ψ0⟩+∣1⟩U(dΔt)∣ψ0⟩2.\begin{equation*} \frac{|0\rangle|\psi_0\rangle + |1\rangle|\psi_0\rangle}{\sqrt{2}} \longrightarrow \frac{|0\rangle|\psi_0\rangle + |1\rangle U(d\Delta t)|\psi_0\rangle}{\sqrt{2}} . \end{equation*}

У наступному блоці коду ми реалізуємо:

∣Ψ(t)⟩=∣0⟩∣ψ0⟩+∣1⟩U(t)∣ψ0⟩2,\begin{equation*} |\Psi(t)\rangle = \frac{|0\rangle|\psi_0\rangle + |1\rangle U(t)|\psi_0\rangle}{\sqrt{2}}, \end{equation*}

яке буде присвоєне як t=dΔtt=d\Delta t для d=1,⋯r−1d=1,\cdots r-1 на етапі виконання.

ancilla = 0
system_qubits = list(range(1, num_qubits + 1))

extended_swap_test = QuantumCircuit(num_qubits + 1)

# Append state preparation part
extended_swap_test = extended_swap_test.compose(qc_ref, system_qubits)

# Prepare the coherent branch label, (|0> + |1>) / sqrt(2).
extended_swap_test.h(ancilla)

# Apply U(t) only to the |1> branch of the ancilla.
extended_swap_test.append(
controlled_evolution_gate, [ancilla] + system_qubits
)

# Decompose once more for visualization so that control bullets are visible.
display(extended_swap_test.draw("mpl", fold=-1))

Output of the previous code cell

Спостережувані величини​

Для будь-якої ермітової системної спостережуваної величини OO, тут ми встановлюємо спостережувані величини для обчислення:

z=⟨ψ0∣O∣ψd⟩.\begin{equation*} z=\langle \psi_0|O|\psi_d\rangle . \end{equation*}

Це тому, що O=IO=I дає елемент перекриття S0d=⟨ψ0∣ψd⟩\mathcal{S}_{0d}=\langle\psi_0|\psi_d\rangle, тоді як O=HO=H дає елемент гамільтоніана H0d=⟨ψ0∣H∣ψd⟩\mathcal{H}_{0d}=\langle\psi_0|H|\psi_d\rangle.

Використовуючи X=∣0⟩⟨1∣+∣1⟩⟨0∣X=|0\rangle\langle 1|+|1\rangle\langle 0|, маємо

⟨Ψ0d∣X⊗O∣Ψ0d⟩=(⟨0∣⟨ψ0∣+⟨1∣⟨ψd∣2)(X⊗O)(∣0⟩∣ψ0⟩+∣1⟩∣ψd⟩2)=12(⟨ψ0∣O∣ψd⟩+⟨ψd∣O∣ψ0⟩)=z+z∗2=Re⁡z.\begin{align*} \langle \Psi_{0d}|X\otimes O| \Psi_{0d} \rangle &= \left(\frac{\langle0|\langle\psi_0| + \langle1| \langle\psi_d|}{\sqrt{2}}\right)(X\otimes O)\left(\frac{|0\rangle|\psi_0\rangle + |1\rangle |\psi_d\rangle}{\sqrt{2}}\right) \\ &= \frac{1}{2} \left( \langle \psi_0|O|\psi_d\rangle + \langle \psi_d|O|\psi_0\rangle \right) \\ &= \frac{z+z^*}{2} = \operatorname{Re} z. \end{align*}

Аналогічно, використовуючи Y=−i∣0⟩⟨1∣+i∣1⟩⟨0∣Y=-i|0\rangle\langle 1|+i|1\rangle\langle 0|,

⟨Ψ0d∣Y⊗O∣Ψ0d⟩=(⟨0∣⟨ψ0∣+⟨1∣⟨ψd∣2)(Y⊗O)(∣0⟩∣ψ0⟩+∣1⟩∣ψd⟩2)=12(−i⟨ψ0∣O∣ψd⟩+i⟨ψd∣O∣ψ0⟩)=−iz+iz∗2=Im⁡z.\begin{align*} \langle \Psi_{0d}|Y\otimes O| \Psi_{0d} \rangle &= \left(\frac{\langle0|\langle\psi_0| + \langle1| \langle\psi_d|}{\sqrt{2}}\right)(Y\otimes O)\left(\frac{|0\rangle|\psi_0\rangle + |1\rangle |\psi_d\rangle}{\sqrt{2}}\right) \\ &= \frac{1}{2} \left( -i\langle \psi_0|O|\psi_d\rangle + i\langle \psi_d|O|\psi_0\rangle \right) \\ &= \frac{-iz+iz^*}{2} = \operatorname{Im} z. \end{align*}

Отже, маємо

⟨Φ0d∣X⊗O∣Φ0d⟩=Re⁡⟨ψ0∣O∣ψd⟩,⟨Φ0d∣Y⊗O∣Φ0d⟩=Im⁡⟨ψ0∣O∣ψd⟩.\begin{equation*} \langle \Phi_{0d}|X\otimes O|\Phi_{0d}\rangle = \operatorname{Re}\langle\psi_0|O|\psi_d\rangle , \quad \langle \Phi_{0d}|Y\otimes O|\Phi_{0d}\rangle = \operatorname{Im}\langle\psi_0|O|\psi_d\rangle . \end{equation*}

Нарешті, для кожного стану ∣Ψ0d⟩|\Psi_{0d}\rangle нам потрібно виміряти:

X⊗I for Re⁡S0d,Y⊗I for Im⁡S0d,X⊗H for Re⁡H0d,Y⊗H for Im⁡H0d.\begin{align*} X \otimes I \text{ for } \operatorname{Re}\mathcal{S}_{0d},\\ Y \otimes I \text{ for } \operatorname{Im}\mathcal{S}_{0d},\\ X \otimes H \text{ for } \operatorname{Re}\mathcal{H}_{0d},\\ Y \otimes H \text{ for } \operatorname{Im}\mathcal{H}_{0d}.\\ \end{align*}
n_qubits = hamiltonian.num_qubits

observable_labels = [
"Re S_0d",
"Im S_0d",
"Re H_0d",
"Im H_0d",
]

# X ⊗ I and Y ⊗ I.
# Qiskit's Pauli-label convention places qubit 0 on the rightmost character,
# so the ancilla Pauli is appended to the right.
obs_x_identity = SparsePauliOp("I" * n_qubits + "X")
obs_y_identity = SparsePauliOp("I" * n_qubits + "Y")

# X ⊗ H and Y ⊗ H.
obs_x_hamiltonian = SparsePauliOp.from_list(
[
(label + "X", coeff)
for label, coeff in zip(
hamiltonian.paulis.to_labels(),
hamiltonian.coeffs,
)
]
)

obs_y_hamiltonian = SparsePauliOp.from_list(
[
(label + "Y", coeff)
for label, coeff in zip(
hamiltonian.paulis.to_labels(),
hamiltonian.coeffs,
)
]
)

observables = [
obs_x_identity,
obs_y_identity,
obs_x_hamiltonian,
obs_y_hamiltonian,
]

for obs, label in zip(observables, observable_labels):
print(f"Observable: {label}")
print(obs)
print()
Observable: Re S_0d
SparsePauliOp(['IIIIIIIIIIIIX'],
coeffs=[1.+0.j])

Observable: Im S_0d
SparsePauliOp(['IIIIIIIIIIIIY'],
coeffs=[1.+0.j])

Observable: Re H_0d
SparsePauliOp(['IIIIIIIIIIXXX', 'IIIIIIIIIXXIX', 'IIIIIIIIXXIIX', 'IIIIIIIXXIIIX', 'IIIIIIXXIIIIX', 'IIIIIXXIIIIIX', 'IIIIXXIIIIIIX', 'IIIXXIIIIIIIX', 'IIXXIIIIIIIIX', 'IXXIIIIIIIIIX', 'XXIIIIIIIIIIX', 'IIIIIIIIIIYYX', 'IIIIIIIIIYYIX', 'IIIIIIIIYYIIX', 'IIIIIIIYYIIIX', 'IIIIIIYYIIIIX', 'IIIIIYYIIIIIX', 'IIIIYYIIIIIIX', 'IIIYYIIIIIIIX', 'IIYYIIIIIIIIX', 'IYYIIIIIIIIIX', 'YYIIIIIIIIIIX', 'IIIIIIIIIIZZX', 'IIIIIIIIIZZIX', 'IIIIIIIIZZIIX', 'IIIIIIIZZIIIX', 'IIIIIIZZIIIIX', 'IIIIIZZIIIIIX', 'IIIIZZIIIIIIX', 'IIIZZIIIIIIIX', 'IIZZIIIIIIIIX', 'IZZIIIIIIIIIX', 'ZZIIIIIIIIIIX'],
coeffs=[1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,
1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,
1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,
1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j])

Observable: Im H_0d
SparsePauliOp(['IIIIIIIIIIXXY', 'IIIIIIIIIXXIY', 'IIIIIIIIXXIIY', 'IIIIIIIXXIIIY', 'IIIIIIXXIIIIY', 'IIIIIXXIIIIIY', 'IIIIXXIIIIIIY', 'IIIXXIIIIIIIY', 'IIXXIIIIIIIIY', 'IXXIIIIIIIIIY', 'XXIIIIIIIIIIY', 'IIIIIIIIIIYYY', 'IIIIIIIIIYYIY', 'IIIIIIIIYYIIY', 'IIIIIIIYYIIIY', 'IIIIIIYYIIIIY', 'IIIIIYYIIIIIY', 'IIIIYYIIIIIIY', 'IIIYYIIIIIIIY', 'IIYYIIIIIIIIY', 'IYYIIIIIIIIIY', 'YYIIIIIIIIIIY', 'IIIIIIIIIIZZY', 'IIIIIIIIIZZIY', 'IIIIIIIIZZIIY', 'IIIIIIIZZIIIY', 'IIIIIIZZIIIIY', 'IIIIIZZIIIIIY', 'IIIIZZIIIIIIY', 'IIIZZIIIIIIIY', 'IIZZIIIIIIIIY', 'IZZIIIIIIIIIY', 'ZZIIIIIIIIIIY'],
coeffs=[1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,
1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,
1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,
1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j])

Для оцінки матричних елементів H\mathcal{H} кількість членів Паулі значно більша, ніж для матричних елементів S\mathcal{S}.

Тепер ми можемо зменшити кількість вимірюваних членів гамільтоніана, використовуючи техніку зсуву [4]. Ми розбиваємо гамільтоніан як:

H=(H−T)+T,\begin{equation*} H = (H-T) + T, \end{equation*}

де TT вибрано так, щоб еталонний стан був його власним станом,

T∣ψ0⟩=τ∣ψ0⟩.\begin{equation*} T|\psi_0\rangle = \tau |\psi_0\rangle . \end{equation*}

Then,

H0d=⟨ψ0∣H∣ψd⟩=⟨ψ0∣(H−T)∣ψd⟩+⟨ψ0∣T∣ψd⟩=⟨ψ0∣(H−T)∣ψd⟩+τ⟨ψ0∣ψd⟩=H~0d+τS0d.\begin{align*} \mathcal{H}_{0d} &= \langle\psi_0|H|\psi_d\rangle \\ &= \langle\psi_0|(H-T)|\psi_d\rangle + \langle\psi_0|T|\psi_d\rangle \\ &= \langle\psi_0|(H-T)|\psi_d\rangle + \tau \langle\psi_0|\psi_d\rangle \\ &= \widetilde{\mathcal{H}}_{0d} + \tau \mathcal{S}_{0d}. \end{align*}

Here,

H~0d=⟨ψ0∣(H−T)∣ψd⟩\begin{equation*} \widetilde{\mathcal{H}}_{0d} = \langle\psi_0|(H-T)|\psi_d\rangle \end{equation*}

— це елемент зсунутої матриці гамільтоніана. Отже, нам потрібно виміряти лише X⊗(H−T)X\otimes (H-T) та Y⊗(H−T)Y\otimes (H-T). Внесок від TT відновлюється класично, використовуючи вже виміряний елемент матриці перекриття S0d\mathcal{S}_{0d}.

У цьому прикладі природним вибором є діагональна частина гамільтоніана Гайзенберга,

T=∑i=0n−2ZiZi+1.\begin{equation*} T = \sum_{i=0}^{n-2} Z_iZ_{i+1}. \end{equation*}

Оскільки еталонний стан є станом обчислювального базису, він є власним станом кожного члена ZiZi+1Z_iZ_{i+1}.

Однак вигідніший вибір — включити не лише діагональні члени ZZZZ, а й члени XX+YYXX+YY, які анулюють еталонний стан.

Для кожної сусідньої пари оператор XX+YYXX+YY задовольняє

(XX+YY)∣00⟩=0,(XX+YY)∣11⟩=0,\begin{equation*} (XX+YY)|00\rangle = 0, \qquad (XX+YY)|11\rangle = 0, \end{equation*}

and

(XX+YY)∣01⟩=2∣10⟩,(XX+YY)∣10⟩=2∣01⟩.\begin{equation*} (XX+YY)|01\rangle = 2|10\rangle, \qquad (XX+YY)|10\rangle = 2|01\rangle . \end{equation*}

Отже, член XX+YYXX+YY дає внесок лише тоді, коли два сусідні кубіти мають різні заповнення в еталонному бітовому рядку. Якщо обидва кубіти дорівнюють 00 або обидва 11, член анулює еталонний стан, тож його також можна віднести до оператора зсуву TT.

Нехай ∣ψref⟩=∣z0⋯zn−1⟩|\psi_{\rm ref}\rangle = |z_0\cdots z_{n-1}\rangle, де zi∈{0,1}z_i \in \{0,1\}. Таким чином, ми можемо вибрати

T=∑i=0n−2ZiZi+1+∑zi=zi+1(XiXi+1+YiYi+1).\begin{equation*} T=\sum_{i=0}^{n-2} Z_iZ_{i+1} + \sum_{z_i={z_{i+1}}} \left( X_iX_{i+1} + Y_iY_{i+1} \right). \end{equation*}

Цей оператор усе ще задовольняє

T∣ψ0⟩=τ∣ψ0⟩,\begin{equation*} T|\psi_0\rangle = \tau |\psi_0\rangle , \end{equation*}

оскільки члени ZZZZ діють діагонально на ∣ψ0⟩|\psi_0\rangle, тоді як зсунуті члени XX+YYXX+YY дають нуль. Відповідне власне значення визначається, отже, лише членами ZZZZ,

τ=∑i=0n−2(−1)zi(−1)zi+1.\begin{equation*} \tau = \sum_{i=0}^{n-2} (-1)^{z_i} (-1)^{z_{i+1}} . \end{equation*}

З цим вибором зсунутий гамільтоніан стає:

H−T=∑zi≠zi+1(XiXi+1+YiYi+1).\begin{equation*} H-T = \sum_{z_i\ne z_{i+1}} \left( X_iX_{i+1} + Y_iY_{i+1} \right). \end{equation*}

Внаслідок цього потрібно вимірювати лише ребра з різними заповненнями в еталонному стані. Усі члени ZZZZ і всі неактивні члени XX+YYXX+YY відновлюються через внесок перекриття τS0d\tau \mathcal{S}_{0d}, або дають нульовий внесок за побудовою.

Це дає меншу спостережувану величину, ніж зсув лише діагональної частини. Зокрема, для еталонного стану обчислювального базису з локалізованим збудженням у H−TH-T залишаються лише ребра, суміжні зі збудженням. Отже, кількість членів Паулі в X⊗(H−T)X\otimes(H-T) та Y⊗(H−T)Y\otimes(H-T) можна суттєво зменшити, тоді як відновлений матричний елемент

H~0d+τS0d\begin{equation*} \widetilde{\mathcal{H}}_{0d} + \tau \mathcal{S}_{0d} \end{equation*}

залишається точно таким самим.

def make_reduced_heisenberg_observables(
ref_bitstring: str,
coupling: float = 1.0,
) -> tuple[SparsePauliOp, SparsePauliOp, float]:
"""Build X⊗(H-T), Y⊗(H-T), and tau."""
n_qubits = len(ref_bitstring)

shifted_terms: list[tuple[str, complex]] = []
tau = 0.0

def bit(q: int) -> str:
return ref_bitstring[n_qubits - 1 - q]

def append_term(q0: int, q1: int, pauli: str):
label = ["I"] * n_qubits
label[n_qubits - 1 - q0] = pauli[0]
label[n_qubits - 1 - q1] = pauli[1]
shifted_terms.append(("".join(label), coupling))

for q in range(n_qubits - 1):
same_occupation = bit(q) == bit(q + 1)

# ZZ contribution to tau
tau += coupling * (1.0 if same_occupation else -1.0)

# XX + YY survives only for opposite occupations.
if not same_occupation:
append_term(q, q + 1, "XX")
append_term(q, q + 1, "YY")

if shifted_terms:
obs_x_shifted_hamiltonian = SparsePauliOp.from_list(
[(label + "X", coeff) for label, coeff in shifted_terms]
)
obs_y_shifted_hamiltonian = SparsePauliOp.from_list(
[(label + "Y", coeff) for label, coeff in shifted_terms]
)
else:
obs_x_shifted_hamiltonian = SparsePauliOp(
"I" * n_qubits + "X", coeffs=[0.0]
)
obs_y_shifted_hamiltonian = SparsePauliOp(
"I" * n_qubits + "Y", coeffs=[0.0]
)

return obs_x_shifted_hamiltonian, obs_y_shifted_hamiltonian, tau

obs_x_shifted_hamiltonian, obs_y_shifted_hamiltonian, shift_tau = (
make_reduced_heisenberg_observables(ref_bitstring)
)

print("Observable: Re shifted H_0d")
print(obs_x_shifted_hamiltonian)
print()

print("Observable: Im shifted H_0d")
print(obs_y_shifted_hamiltonian)
print()

print("tau =", shift_tau)

observables = [
obs_x_identity,
obs_y_identity,
obs_x_shifted_hamiltonian,
obs_y_shifted_hamiltonian,
]
observable_labels = [
"Re S_0d",
"Im S_0d",
"Re shifted H_0d",
"Im shifted H_0d",
]
Observable: Re shifted H_0d
SparsePauliOp(['IIIIIXXIIIIIX', 'IIIIIYYIIIIIX', 'IIIIXXIIIIIIX', 'IIIIYYIIIIIIX'],
coeffs=[1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j])

Observable: Im shifted H_0d
SparsePauliOp(['IIIIIXXIIIIIY', 'IIIIIYYIIIIIY', 'IIIIXXIIIIIIY', 'IIIIYYIIIIIIY'],
coeffs=[1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j])

tau = 7.0

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

Тепер ми перетворюємо абстрактну схему extended-swap-test на шаблон, орієнтований на апаратне забезпечення. Перед цим ми спочатку додатково оптимізуємо схему на абстрактному рівні.

Порівняння порядку членів гамільтоніана​

Спочатку ми порівнюємо різні впорядкування членів Паулі в гамільтоніані Гайзенберга для симуляції гамільтоніана. Сам гамільтоніан не змінюється, але порядок впливає на те, як генерується схема добуткової формули і наскільки структуру можна розпаралелити у схемі. Наприклад, наївне впорядкування перераховує всі найближчі сусідні члени XXXX, потім усі члени YYYY, потім усі члени ZZZZ. Це розміщує суміжні ребра, такі як (0,1)(0,1) і (1,2)(1,2), поруч одне з одним, тому їх не можна виконати паралельно. Впорядкування "спочатку парні, потім непарні" відвідує спочатку непересічні парні ребра, а потім непарні, що відкриває паралельні двокубітні шари. Впорядкування з групуванням парних-непарних ребер йде на крок далі: для кожного ребра воно тримає разом локальні члени XXXX, YYYY і ZZZZ, все ще відвідуючи парні ребра перед непарними. Ми очікуємо, що впорядкування "спочатку парні, потім непарні" та впорядкування з групуванням парних-непарних ребер зменшать глибину схеми, відкриваючи паралельні двокубітні шари, а впорядкування з групуванням ребер додатково зменшить похибку Троттера, оскільки локальна двокубітна взаємодія на тому самому ребрі обробляється як компактний блок.

Pauli evolutions with different term ordering

Порядок гамільтоніана також впливає на похибку Троттера. Якщо ми розміщуємо некомутуючі члени поруч один з одним, зміни базису відбуваються частіше, що спричиняє більшу похибку Троттера. Групуючи члени, що вимагають однакового перетворення базису Паулі, можна уникнути надлишкових змін базису.

Тут порівняння використовує найбільший час еволюції, який з'являється в оцінках крилівського простору першого рядка, tmax⁡=(r−1)Δt,t_{\max} = (r-1)\Delta t, використовуючи ту саму умову транспіляції.

Щоб виміряти похибку Троттера, ми використовуємо неточність процесу (process infidelity) між троттеризованою схемою та точною еволюцією гамільтоніана,

Infidelity(U,V)=1−F(U,V)=1−∣Tr⁡(U†V)∣2d2,\begin{equation*} \text{Infidelity}(U,V) = 1-F(U,V) = 1 - \frac{|\operatorname{Tr}(U^\dagger V)|^2}{d^2}, \end{equation*}

where d=2nd=2^n is the dimension of the Hilbert space.

Ця діагностика використовує щільні матриці, тому вона підходить для цього малого прикладу на 12 кубітів, але не призначена як масштабована підпрограма.

# The comparison uses the largest time that appears in the first-row Krylov estimates.
# Circuit depth does not depend on this numeric value, but the Trotter error does.
comparison_time = (krylov_dim - 1) * dt

def make_heisenberg_hamiltonian_ordered(
num_qubits: int,
ordering: str,
coupling: float = 1.0,
) -> SparsePauliOp:
"""Return the same Heisenberg Hamiltonian with a specified term ordering."""
terms: list[tuple[str, complex]] = []
even_edges = [(q, q + 1) for q in range(0, num_qubits - 1, 2)]
odd_edges = [(q, q + 1) for q in range(1, num_qubits - 1, 2)]

def append_term(q0: int, q1: int, pauli: str):
label = ["I"] * num_qubits
label[num_qubits - 1 - q0] = pauli[0]
label[num_qubits - 1 - q1] = pauli[1]
terms.append(("".join(label), coupling))

if ordering == "naive":
for pauli in ("XX", "YY", "ZZ"):
for q in range(num_qubits - 1):
append_term(q, q + 1, pauli)
elif ordering == "even-then-odd":
for pauli in ("XX", "YY", "ZZ"):
for q0, q1 in even_edges + odd_edges:
append_term(q0, q1, pauli)
elif ordering == "even-odd edge-grouped":
for q0, q1 in even_edges + odd_edges:
for pauli in ("XX", "YY", "ZZ"):
append_term(q0, q1, pauli)
else:
raise ValueError(f"Unknown ordering: {ordering}")

return SparsePauliOp.from_list(terms).simplify()

def build_numeric_evolution_circuit(
hamiltonian: SparsePauliOp,
synthesis,
time_value: float,
**synthesis_kwargs,
) -> QuantumCircuit:
"""Build a numeric circuit for exp(-i H t) with a chosen synthesis rule."""
evolution_gate = PauliEvolutionGate(
hamiltonian,
time=time_value,
synthesis=synthesis(**synthesis_kwargs),
)
circuit = QuantumCircuit(hamiltonian.num_qubits)
circuit.append(evolution_gate, range(hamiltonian.num_qubits))
return circuit

def process_infidelity(
circuit: QuantumCircuit,
exact_matrix: np.ndarray,
) -> float:
"""Return 1 - |Tr(U_circuit† U_exact) / d|²."""
circuit_matrix = np.asarray(Operator(circuit).data)
dim = circuit_matrix.shape[0]
normalized_trace = np.vdot(circuit_matrix, exact_matrix) / dim
fidelity = np.abs(normalized_trace) ** 2
return float(np.clip(1.0 - fidelity, 0.0, 1.0))

hamiltonians_by_ordering = {
ordering: make_heisenberg_hamiltonian_ordered(
num_qubits, ordering, coupling=1.0
)
for ordering in ["naive", "even-then-odd", "even-odd edge-grouped"]
}

print(f"Comparison time: {comparison_time}\n")

for order_name, ham_ordered in hamiltonians_by_ordering.items():
print(f"{order_name}:")
print([op for op, _ in ham_ordered.to_list()])
print()

print("Precomputing the exact evolution operator... ", end="")
exact_matrix = la.expm(-1j * comparison_time * hamiltonian.to_matrix())
print("Done")
Comparison time: 0.8567979964335799

naive:
['IIIIIIIIIIXX', 'IIIIIIIIIXXI', 'IIIIIIIIXXII', 'IIIIIIIXXIII', 'IIIIIIXXIIII', 'IIIIIXXIIIII', 'IIIIXXIIIIII', 'IIIXXIIIIIII', 'IIXXIIIIIIII', 'IXXIIIIIIIII', 'XXIIIIIIIIII', 'IIIIIIIIIIYY', 'IIIIIIIIIYYI', 'IIIIIIIIYYII', 'IIIIIIIYYIII', 'IIIIIIYYIIII', 'IIIIIYYIIIII', 'IIIIYYIIIIII', 'IIIYYIIIIIII', 'IIYYIIIIIIII', 'IYYIIIIIIIII', 'YYIIIIIIIIII', 'IIIIIIIIIIZZ', 'IIIIIIIIIZZI', 'IIIIIIIIZZII', 'IIIIIIIZZIII', 'IIIIIIZZIIII', 'IIIIIZZIIIII', 'IIIIZZIIIIII', 'IIIZZIIIIIII', 'IIZZIIIIIIII', 'IZZIIIIIIIII', 'ZZIIIIIIIIII']

even-then-odd:
['IIIIIIIIIIXX', 'IIIIIIIIXXII', 'IIIIIIXXIIII', 'IIIIXXIIIIII', 'IIXXIIIIIIII', 'XXIIIIIIIIII', 'IIIIIIIIIXXI', 'IIIIIIIXXIII', 'IIIIIXXIIIII', 'IIIXXIIIIIII', 'IXXIIIIIIIII', 'IIIIIIIIIIYY', 'IIIIIIIIYYII', 'IIIIIIYYIIII', 'IIIIYYIIIIII', 'IIYYIIIIIIII', 'YYIIIIIIIIII', 'IIIIIIIIIYYI', 'IIIIIIIYYIII', 'IIIIIYYIIIII', 'IIIYYIIIIIII', 'IYYIIIIIIIII', 'IIIIIIIIIIZZ', 'IIIIIIIIZZII', 'IIIIIIZZIIII', 'IIIIZZIIIIII', 'IIZZIIIIIIII', 'ZZIIIIIIIIII', 'IIIIIIIIIZZI', 'IIIIIIIZZIII', 'IIIIIZZIIIII', 'IIIZZIIIIIII', 'IZZIIIIIIIII']

even-odd edge-grouped:
['IIIIIIIIIIXX', 'IIIIIIIIIIYY', 'IIIIIIIIIIZZ', 'IIIIIIIIXXII', 'IIIIIIIIYYII', 'IIIIIIIIZZII', 'IIIIIIXXIIII', 'IIIIIIYYIIII', 'IIIIIIZZIIII', 'IIIIXXIIIIII', 'IIIIYYIIIIII', 'IIIIZZIIIIII', 'IIXXIIIIIIII', 'IIYYIIIIIIII', 'IIZZIIIIIIII', 'XXIIIIIIIIII', 'YYIIIIIIIIII', 'ZZIIIIIIIIII', 'IIIIIIIIIXXI', 'IIIIIIIIIYYI', 'IIIIIIIIIZZI', 'IIIIIIIXXIII', 'IIIIIIIYYIII', 'IIIIIIIZZIII', 'IIIIIXXIIIII', 'IIIIIYYIIIII', 'IIIIIZZIIIII', 'IIIXXIIIIIII', 'IIIYYIIIIIII', 'IIIZZIIIIIII', 'IXXIIIIIIIII', 'IYYIIIIIIIII', 'IZZIIIIIIIII']

Precomputing the exact evolution operator... Done

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

ordering_comparison_rows = []
infidelity_reps = [1, 2, 4, 8]

for ordering, ham_ordered in hamiltonians_by_ordering.items():
for reps in infidelity_reps:
circuit = build_numeric_evolution_circuit(
ham_ordered,
LieTrotter,
comparison_time,
reps=reps,
)
decomposed_circuit = simple_transpilation(circuit)
infidelity = process_infidelity(decomposed_circuit, exact_matrix)
ordering_comparison_rows.append(
{
"ordering": ordering,
"synthesis": f"LieTrotter(reps={reps})",
"infidelity": infidelity,
**summarize_circuit(decomposed_circuit),
}
)
if reps == 1:
print(f"Circuit for {ordering} ordering:")
print([op for op, _ in ham_ordered.to_list()])
display(decomposed_circuit.draw("mpl", fold=-1, scale=0.6))

ordering_comparison_df = pd.DataFrame(ordering_comparison_rows)
display(ordering_comparison_df)

hamiltonian_for_synthesis = hamiltonians_by_ordering["even-odd edge-grouped"]
Circuit for naive ordering:
['IIIIIIIIIIXX', 'IIIIIIIIIXXI', 'IIIIIIIIXXII', 'IIIIIIIXXIII', 'IIIIIIXXIIII', 'IIIIIXXIIIII', 'IIIIXXIIIIII', 'IIIXXIIIIIII', 'IIXXIIIIIIII', 'IXXIIIIIIIII', 'XXIIIIIIIIII', 'IIIIIIIIIIYY', 'IIIIIIIIIYYI', 'IIIIIIIIYYII', 'IIIIIIIYYIII', 'IIIIIIYYIIII', 'IIIIIYYIIIII', 'IIIIYYIIIIII', 'IIIYYIIIIIII', 'IIYYIIIIIIII', 'IYYIIIIIIIII', 'YYIIIIIIIIII', 'IIIIIIIIIIZZ', 'IIIIIIIIIZZI', 'IIIIIIIIZZII', 'IIIIIIIZZIII', 'IIIIIIZZIIII', 'IIIIIZZIIIII', 'IIIIZZIIIIII', 'IIIZZIIIIIII', 'IIZZIIIIIIII', 'IZZIIIIIIIII', 'ZZIIIIIIIIII']

Output of the previous code cell

Circuit for even-then-odd ordering:
['IIIIIIIIIIXX', 'IIIIIIIIXXII', 'IIIIIIXXIIII', 'IIIIXXIIIIII', 'IIXXIIIIIIII', 'XXIIIIIIIIII', 'IIIIIIIIIXXI', 'IIIIIIIXXIII', 'IIIIIXXIIIII', 'IIIXXIIIIIII', 'IXXIIIIIIIII', 'IIIIIIIIIIYY', 'IIIIIIIIYYII', 'IIIIIIYYIIII', 'IIIIYYIIIIII', 'IIYYIIIIIIII', 'YYIIIIIIIIII', 'IIIIIIIIIYYI', 'IIIIIIIYYIII', 'IIIIIYYIIIII', 'IIIYYIIIIIII', 'IYYIIIIIIIII', 'IIIIIIIIIIZZ', 'IIIIIIIIZZII', 'IIIIIIZZIIII', 'IIIIZZIIIIII', 'IIZZIIIIIIII', 'ZZIIIIIIIIII', 'IIIIIIIIIZZI', 'IIIIIIIZZIII', 'IIIIIZZIIIII', 'IIIZZIIIIIII', 'IZZIIIIIIIII']

Output of the previous code cell

Circuit for even-odd edge-grouped ordering:
['IIIIIIIIIIXX', 'IIIIIIIIIIYY', 'IIIIIIIIIIZZ', 'IIIIIIIIXXII', 'IIIIIIIIYYII', 'IIIIIIIIZZII', 'IIIIIIXXIIII', 'IIIIIIYYIIII', 'IIIIIIZZIIII', 'IIIIXXIIIIII', 'IIIIYYIIIIII', 'IIIIZZIIIIII', 'IIXXIIIIIIII', 'IIYYIIIIIIII', 'IIZZIIIIIIII', 'XXIIIIIIIIII', 'YYIIIIIIIIII', 'ZZIIIIIIIIII', 'IIIIIIIIIXXI', 'IIIIIIIIIYYI', 'IIIIIIIIIZZI', 'IIIIIIIXXIII', 'IIIIIIIYYIII', 'IIIIIIIZZIII', 'IIIIIXXIIIII', 'IIIIIYYIIIII', 'IIIIIZZIIIII', 'IIIXXIIIIIII', 'IIIYYIIIIIII', 'IIIZZIIIIIII', 'IXXIIIIIIIII', 'IYYIIIIIIIII', 'IZZIIIIIIIII']

Output of the previous code cell

ordering synthesis infidelity depth size \
0 naive LieTrotter(reps=1) 0.999917 15 33
1 naive LieTrotter(reps=2) 0.805998 21 66
2 naive LieTrotter(reps=4) 0.271389 33 132
3 naive LieTrotter(reps=8) 0.074590 57 264
4 even-then-odd LieTrotter(reps=1) 0.999917 6 33
5 even-then-odd LieTrotter(reps=2) 0.805998 12 66
6 even-then-odd LieTrotter(reps=4) 0.271389 24 132
7 even-then-odd LieTrotter(reps=8) 0.074590 48 264
8 even-odd edge-grouped LieTrotter(reps=1) 0.998432 6 33
9 even-odd edge-grouped LieTrotter(reps=2) 0.653843 12 66
10 even-odd edge-grouped LieTrotter(reps=4) 0.181529 24 132
11 even-odd edge-grouped LieTrotter(reps=8) 0.045244 48 264

2q gates 2q depth
0 33 15
1 66 21
2 132 33
3 264 57
4 33 6
5 66 12
6 132 24
7 264 48
8 33 6
9 66 12
10 132 24
11 264 48

При reps=1 ми спостерігаємо, що обидва впорядкування — "спочатку парні, потім непарні" та з групуванням парних-непарних ребер — зменшують глибину з 15 до 6, відкриваючи паралельні двокубітні шари, тоді як кількість двокубітних вентилів залишається однаковою для всіх трьох впорядкувань. Однак усі три впорядкування мають неточність, близьку до 1, тому ми перебираємо різну кількість повторень Троттера, щоб чіткіше розрізнити впорядкування. Із зростанням кількості повторень неточність впорядкування even-odd edge-grouped спадає швидше за інші два, досягаючи 0.045 при reps=8 проти 0.075 для наївного та впорядкування "спочатку парні, потім непарні".

Порівняння синтезу добуткової формули​

Далі ми досліджуємо різні розширені налаштування троттеризації, зафіксувавши порядок гамільтоніана на впорядкуванні з групуванням парних-непарних ребер. Ми розглядаємо Лі-Троттер першого порядку, Судзукі-Троттер другого порядку та Судзукі-Троттер четвертого порядку.

synthesis_comparison_rows = []

for num_trotter_steps in [1, 2, 3, 4, 5]:
synthesis_cases = [
("LieTrotter", LieTrotter, {"reps": num_trotter_steps}),
(
"SuzukiTrotter(order=2)",
SuzukiTrotter,
{"order": 2, "reps": num_trotter_steps},
),
(
"SuzukiTrotter(order=4)",
SuzukiTrotter,
{"order": 4, "reps": num_trotter_steps},
),
]
for label, synthesis, kwargs in synthesis_cases:
circuit = build_numeric_evolution_circuit(
hamiltonian_for_synthesis,
synthesis,
comparison_time,
**kwargs,
)
decomposed_circuit = simple_transpilation(circuit)
synthesis_comparison_rows.append(
{
"synthesis": label,
"reps": kwargs["reps"],
"infidelity": process_infidelity(
decomposed_circuit, exact_matrix
),
**summarize_circuit(decomposed_circuit),
}
)

synthesis_comparison_df = pd.DataFrame(synthesis_comparison_rows)
display(synthesis_comparison_df.sort_values(["2q gates", "2q depth"]))

# For memory free
exact_matrix = None
synthesis reps infidelity depth size 2q gates \
0 LieTrotter 1 9.984324e-01 6 33 33
1 SuzukiTrotter(order=2) 1 9.733399e-01 9 51 51
3 LieTrotter 2 6.538427e-01 12 66 66
4 SuzukiTrotter(order=2) 2 2.522533e-01 15 84 84
6 LieTrotter 3 3.197242e-01 18 99 99
7 SuzukiTrotter(order=2) 3 4.804050e-02 21 117 117
9 LieTrotter 4 1.815291e-01 24 132 132
10 SuzukiTrotter(order=2) 4 1.453103e-02 27 150 150
12 LieTrotter 5 1.161770e-01 30 165 165
2 SuzukiTrotter(order=4) 1 2.884402e-01 33 183 183
13 SuzukiTrotter(order=2) 5 5.803455e-03 33 183 183
5 SuzukiTrotter(order=4) 2 1.641162e-03 63 348 348
8 SuzukiTrotter(order=4) 3 2.907076e-05 93 513 513
11 SuzukiTrotter(order=4) 4 2.791061e-06 123 678 678
14 SuzukiTrotter(order=4) 5 4.736685e-07 153 843 843

2q depth
0 6
1 9
3 12
4 15
6 18
7 21
9 24
10 27
12 30
2 33
13 33
5 63
8 93
11 123
14 153

Лі-Троттер дає найменш глибоку схему, але має найбільшу похибку, тоді як Судзукі-Троттер четвертого порядку точніший, але збільшує глибину схеми. У решті туторіалу ми обираємо Судзукі-Троттер другого порядку, оскільки він дає схему малої глибини, суттєво зменшуючи похибку Троттера порівняно з формулою першого порядку.

Видалення керованого вентиля часової еволюції​

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

circuit optimization

Тут BrefB_{\rm ref} готує еталонний стан, Bref∣0n⟩=∣ψ0⟩B_{\rm ref}|0^n\rangle = |\psi_{0}\rangle.

Замість того, щоб спочатку готувати 12(∣0⟩+∣1⟩)∣ψ0⟩\frac{1}{\sqrt{2}}(|0\rangle+|1\rangle)|\psi_0\rangle, а потім застосовувати U(t)U(t) лише на гілці ∣1⟩|1\rangle, схема безпосередньо готує дві гілки як

∣0⟩∣ψ0⟩and∣1⟩∣ψd⟩,\begin{equation*} |0\rangle|\psi_0\rangle \quad \text{and} \quad |1\rangle|\psi_d\rangle , \end{equation*}

where

∣ψd⟩=U(dΔt)∣ψ0⟩.\begin{equation*} |\psi_d\rangle = U(d\Delta t)|\psi_0\rangle . \end{equation*}

Схема спочатку застосовує вентиль Адамара до анцили та готує еталонний стан лише на гілці ∣1⟩|1\rangle:

∣0⟩∣0n⟩⟶∣0⟩∣0n⟩+∣1⟩Bref∣0n⟩2=∣0⟩∣0n⟩+∣1⟩∣ψ0⟩2.\begin{equation*} |0\rangle |0^n\rangle \longrightarrow \frac{|0\rangle |0^n\rangle + |1\rangle B_{\rm ref}|0^n\rangle}{\sqrt{2}} = \frac{|0\rangle |0^n\rangle + |1\rangle |\psi_0\rangle}{\sqrt{2}} . \end{equation*}

Потім некерований оператор часової еволюції застосовується до обох гілок:

∣0⟩∣0n⟩+∣1⟩∣ψ0⟩2⟶∣0⟩U(t)∣0n⟩+∣1⟩U(t)∣ψ0⟩2.\begin{equation*} \frac{|0\rangle |0^n\rangle + |1\rangle |\psi_0\rangle}{\sqrt{2}} \longrightarrow \frac{|0\rangle U(t)|0^n\rangle + |1\rangle U(t)|\psi_0\rangle}{\sqrt{2}} . \end{equation*}

Оскільки гамільтоніан зберігає кількість збуджень, ми бачимо, що ∣0n⟩|0^n\rangle є його власним станом, і, отже, оператор еволюції накопичує лише фазу під дією гамільтоніана:

U(t)∣0n⟩=e−iEvact∣0n⟩.\begin{equation*} U(t)|0^n\rangle = e^{-iE_{\rm vac}t}|0^n\rangle . \end{equation*}

Therefore,

∣0⟩U(t)∣0n⟩+∣1⟩U(t)∣ψ0⟩2=e−iEvact∣0⟩∣0n⟩+∣1⟩U(t)∣ψ0⟩2.\begin{equation*} \frac{|0\rangle U(t)|0^n\rangle + |1\rangle U(t)|\psi_0\rangle}{\sqrt{2}} = \frac{e^{-iE_{\rm vac}t}|0\rangle |0^n\rangle + |1\rangle U(t)|\psi_0\rangle}{\sqrt{2}} . \end{equation*}

Далі BrefB_{\rm ref} застосовується лише на гілці ∣0⟩|0\rangle.

e−iEvact∣0⟩∣ψ0⟩+∣1⟩U(t)∣ψ0⟩2.\begin{equation*} \frac{e^{-iE_{\rm vac}t}|0\rangle |\psi_0\rangle + |1\rangle U(t)|\psi_0\rangle}{\sqrt{2}} . \end{equation*}

На цьому етапі дві гілки мають додаткову відносну фазу. Щоб видалити її, ми застосовуємо вентиль фази анцили

P(−Evact)=(100e−iEvact).\begin{equation*} P(-E_{\rm vac}t) = \begin{pmatrix} 1 & 0 \\ 0 & e^{-iE_{\rm vac}t} \end{pmatrix}. \end{equation*}

Це перетворює стан як

e−iEvact∣0⟩∣ψ0⟩+∣1⟩U(t)∣ψ0⟩2⟶e−iEvact∣0⟩∣ψ0⟩+e−iEvact∣1⟩U(t)∣ψ0⟩2.\begin{equation*} \frac{e^{-iE_{\rm vac}t}|0\rangle |\psi_0\rangle + |1\rangle U(t)|\psi_0\rangle}{\sqrt{2}} \longrightarrow \frac{e^{-iE_{\rm vac}t}|0\rangle |\psi_0\rangle + e^{-iE_{\rm vac}t}|1\rangle U(t)|\psi_0\rangle}{\sqrt{2}} . \end{equation*}

Отже, з точністю до неважливої глобальної фази, ми нарешті підготували

∣Φ0d⟩=∣0⟩∣ψ0⟩+∣1⟩U(t)∣ψ0⟩2.\begin{equation*} |\Phi_{0d}\rangle = \frac{ |0\rangle|\psi_0\rangle + |1\rangle U(t)|\psi_0\rangle }{\sqrt{2}} . \end{equation*}

У наступному блоці коду ми реалізуємо цю схему без керування.

controlled_extended_swap_test = extended_swap_test

# Reuse the ordering and synthesis rule selected by the comparison above.
evol_gate_optimized = PauliEvolutionGate(
hamiltonian_for_synthesis,
time=t,
synthesis=SuzukiTrotter(order=2, reps=num_trotter_steps),
)

uncontrolled_evolution = QuantumCircuit(num_qubits, name="U_ST2(t)")
uncontrolled_evolution.append(evol_gate_optimized, range(num_qubits))

controlled_state_prep = QuantumCircuit(num_qubits + 1, name="C-Prep")
for q, bit in enumerate(reversed(ref_bitstring)):
if bit == "1":
controlled_state_prep.cx(ancilla, system_qubits[q])

vacuum_bitstring = "0" * num_qubits
vacuum_energy = basis_state_expectation_sparse(hamiltonian, "0" * num_qubits)

optimized_extended_swap_test = QuantumCircuit(num_qubits + 1)
optimized_extended_swap_test.h(ancilla)

# Prepare |psi_ref> only on the |1> branch.
optimized_extended_swap_test.compose(controlled_state_prep, inplace=True)
optimized_extended_swap_test.barrier()

# Apply the Trotterized time evolution without control.
optimized_extended_swap_test.compose(
uncontrolled_evolution,
qubits=system_qubits,
inplace=True,
)
optimized_extended_swap_test.barrier()

# Map |0>|0...0> to |0>|psi_ref>, leaving the |1> branch unchanged.
optimized_extended_swap_test.x(ancilla)
optimized_extended_swap_test.compose(controlled_state_prep, inplace=True)
optimized_extended_swap_test.x(ancilla)

# Cancel the known vacuum phase so that the same X/Y observables can be used.
optimized_extended_swap_test.p(-vacuum_energy * t, ancilla)
optimized_extended_swap_test = simple_transpilation(
optimized_extended_swap_test
)

print(f"Vacuum energy E_vac = {vacuum_energy:.1f}")
display(
optimized_extended_swap_test.assign_parameters({t: 1.0}).draw(
"mpl", scale=0.5, fold=26
)
)
Vacuum energy E_vac = 11.0+0.0j

Output of the previous code cell

Транспіляція​

Тепер ми транспілюємо керовану та безкеровану схеми, щоб вони стали виконуваними на апаратному забезпеченні. Порівняймо результат транспільованих схем.

pass_manager = generate_preset_pass_manager(
backend=backend,
optimization_level=3,
)

isa_controlled_extended_swap_test = pass_manager.run(
controlled_extended_swap_test
)

pass_manager = generate_preset_pass_manager(
backend=backend, optimization_level=3, routing_method="none"
)
isa_optimized_extended_swap_test = pass_manager.run(
optimized_extended_swap_test
)

transpilation_result = [
{
"label": "abstract controlled U(t)",
**summarize_circuit(isa_controlled_extended_swap_test),
},
{
"label": "optimized non-controlled U(t)",
**summarize_circuit(isa_optimized_extended_swap_test),
},
]

display(pd.DataFrame(transpilation_result))

def filter_qubits_from_layout(layout):
q_layout = Layout(
{
physical: virtual
for physical, virtual in layout.get_physical_bits().items()
if virtual._register.name == "q"
}
)
return q_layout

print(
filter_qubits_from_layout(
isa_optimized_extended_swap_test.layout.initial_layout
)
)

isa_observables = [
op.apply_layout(isa_optimized_extended_swap_test.layout)
for op in observables
]
label depth size 2q gates 2q depth
0 abstract controlled U(t) 15457 23786 4686 4580
1 optimized non-controlled U(t) 261 1716 307 57
Layout({
18: <Qubit register=(13, "q"), index=0>,
5: <Qubit register=(13, "q"), index=1>,
6: <Qubit register=(13, "q"), index=2>,
7: <Qubit register=(13, "q"), index=3>,
8: <Qubit register=(13, "q"), index=4>,
9: <Qubit register=(13, "q"), index=5>,
10: <Qubit register=(13, "q"), index=6>,
11: <Qubit register=(13, "q"), index=7>,
12: <Qubit register=(13, "q"), index=8>,
13: <Qubit register=(13, "q"), index=9>,
14: <Qubit register=(13, "q"), index=10>,
15: <Qubit register=(13, "q"), index=11>,
19: <Qubit register=(13, "q"), index=12>
})

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

Наступний крок — надіслати ту саму параметризовану схему для кількох значень dd. Для кожного dd ми оцінюємо чотири очікувані значення: X⊗IX\otimes I, Y⊗IY\otimes I, X⊗(H−T)X\otimes (H-T) та Y⊗(H−T)Y\otimes (H-T). Ці чотири числа потім комбінуються в комплексні елементи першого рядка S0d\mathcal{S}_{0d} та H0d\mathcal{H}_{0d}.

Тут S00=1\mathcal{S}_{00}=1 та H00=⟨ψ0∣(H−T)∣ψ0⟩=⟨ψ0∣H∣ψ0⟩−τ\mathcal{H}_{00}=\langle\psi_{0}|(H-T)|\psi_{0}\rangle=\langle\psi_{0}|H|\psi_{0}\rangle-\tau можна обчислити класично, оскільки ∣ψ0⟩\lvert\psi_0\rangle розріджений, тому ми пропускаємо випадок d=0d=0.

pub_list = []
d_values = list(range(1, krylov_dim))

# Exact local statevector estimator.
estimator = StatevectorEstimator()

# We use the circuit before the transpilation for the local simulator,
# but we will use the transpiled circuit for the real backend.
for d in d_values:
parameter_values = [d * dt]

for ob in observables:
pub_list.append(
(
optimized_extended_swap_test,
ob,
parameter_values,
)
)

job = estimator.run(pub_list)

# Local PrimitiveJob does not provide Runtime-style job inputs,
# so preserve the inputs directly.
inputs = pub_list
result = job.result()

print(f"Number of Krylov basis states: r = {len(d_values)}")
print(f"Number of PUBs: {len(pub_list)}")
print(f"Each d uses observables: {observable_labels}")
Number of Krylov basis states: r = 9
Number of PUBs: 36
Each d uses observables: ['Re S_0d', 'Im S_0d', 'Re shifted H_0d', 'Im shifted H_0d']

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

Після оцінки проєктованих матриць ми регуляризуємо і розв'язуємо GEVP

Hc=ESc.\begin{equation*} \mathcal{H}\mathbf{c} = E\mathcal{S}\mathbf{c}. \end{equation*}

Найменше узагальнене власне значення дає оцінку KQD енергії основного стану.

h_shifted_row_est = np.zeros(krylov_dim, dtype=complex)
s_row_est = np.zeros(krylov_dim, dtype=complex)

h_shifted_row_est[0] = ref_energy - shift_tau
s_row_est[0] = 1.0

for idx, (pub_input, pub_result) in enumerate(zip(inputs, result)):
d_index, obs_index = divmod(idx, len(observables))
ev = np.asarray(pub_result.data.evs).reshape(-1)[0]
std = np.asarray(pub_result.data.stds).reshape(-1)[0]

if obs_index == 0:
s_row_est[d_index + 1] = ev
elif obs_index == 1:
s_row_est[d_index + 1] += 1j * ev
elif obs_index == 2:
h_shifted_row_est[d_index + 1] = ev
elif obs_index == 3:
h_shifted_row_est[d_index + 1] += 1j * ev

# H_0d = shifted_H_0d + tau * S_0d.
h_row_est = h_shifted_row_est + shift_tau * s_row_est

h_matrix_est = la.toeplitz(h_row_est.conj(), h_row_est)
s_matrix_est = la.toeplitz(s_row_est.conj(), s_row_est)

s_eigvals = la.eigvalsh(0.5 * (s_matrix_est + s_matrix_est.conj().T))
positive_s_eigvals = s_eigvals[s_eigvals > 1e-12]
s_condition_number = (
positive_s_eigvals[-1] / positive_s_eigvals[0]
if len(positive_s_eigvals) > 0
else np.inf
)

with np.printoptions(precision=3, suppress=True):
print("Estimated first row of S:")
print(s_row_est)
print()
print("Estimated first row of H:")
print(h_row_est)
print()
print("Eigenvalues of the estimated overlap matrix S:")
print(s_eigvals)
print(f"Condition number above 1e-12: {s_condition_number:.3e}")
print()
Estimated first row of S:
[ 1. +0.j 0.758-0.596j 0.203-0.836j -0.291-0.636j -0.444-0.229j
-0.276+0.053j -0.044+0.051j 0.006-0.121j -0.155-0.218j -0.345-0.101j]

Estimated first row of H:
[ 7. +0.j 4.842-4.76j 0.044-6.185j -3.791-3.653j -4.137+0.39j
-1.495+2.654j 1.331+1.777j 1.85 -0.763j -0.032-2.278j -2.222-1.372j]

Eigenvalues of the estimated overlap matrix S:
[-0. 0. 0. 0. 0. 0. 0.01 0.355 3.526 6.109]
Condition number above 1e-12: 1.904e+12

Тепер ми розв'язуємо задачу узагальнених власних значень, використовуючи матриці, реконструйовані з оцінок схеми. В ідеальному розрахунку вектора стану з точною еволюцією в реальному часі це має відтворити точний проєктований результат. На практиці відхилення можуть виникати через троттеризацію, похибку вибірки та числову нестабільність матриці перекриття.

Ми спостерігаємо, як енергія збігається, коли ми збільшуємо розмірність крилівського підпростору.

exact_evals = diagonalize_single_1_subspace(hamiltonian)
exact_ground = min(exact_evals)
print("exact ground state energy: ", exact_ground)

threshold = 1e-12
energy_convergence = []

for r in range(1, krylov_dim + 1):
energy_est_kqd, coeffs_est, retained_est = solve_thresholded_gevp(
h_matrix_est[:r, :r],
s_matrix_est[:r, :r],
threshold=threshold,
)
energy_convergence.append(energy_est_kqd)
print(
f"Krylov ground state energy (dim={r}, retained={retained_est}): ",
energy_est_kqd,
)
exact ground state energy: 3.136296694843727
Krylov ground state energy (dim=1, retained=1): 7.0
Krylov ground state energy (dim=2, retained=2): 4.184510657551266
Krylov ground state energy (dim=3, retained=3): 3.5539074630394136
Krylov ground state energy (dim=4, retained=4): 3.3366270761341044
Krylov ground state energy (dim=5, retained=5): 3.252017453225087
Krylov ground state energy (dim=6, retained=6): 3.2300275138879186
Krylov ground state energy (dim=7, retained=7): 3.2299154099085685
Krylov ground state energy (dim=8, retained=7): 3.2298063744216776
Krylov ground state energy (dim=9, retained=7): 3.2296778282872456
Krylov ground state energy (dim=10, retained=8): 3.223647515867734
def plot_energy_convergence(energy_convergence, exact_ground, krylov_dim):
fig, ax = plt.subplots(figsize=(7, 4.5))

ax.plot(
range(1, krylov_dim + 1),
energy_convergence,
marker="o",
label="KQD estimate",
)

ax.axhline(
exact_ground,
linestyle="--",
label=f"Exact ground energy = {exact_ground:.6f}",
)

ax.set_xlabel("Krylov dimension")
ax.set_ylabel("Ground-state energy")
ax.grid(True, alpha=0.3)
ax.legend()
plt.tight_layout()
plt.show()

plot_energy_convergence(energy_convergence, exact_ground, krylov_dim)

Output of the previous code cell

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

У попередньому розділі використовувалася модель на 12 кубітів, щоб можна було використати симуляцію вектора стану як діагностику. Тепер ми масштабуємо той самий робочий процес KQD до ланцюжка Гайзенберга на 30 кубітів і готуємо навантаження для виконання на апаратному забезпеченні IBM Quantum.

Кроки 1-4, стиснуті в один блок коду​

Тепер ми об'єднуємо всі ці деталі в єдиний робочий процес більшого масштабу, який потім запускається на нашому реальному квантовому апаратному забезпеченні. У цьому розділі ми застосовуємо реалістичні налаштування пом'якшення похибок для покращення надійності результатів. Оскільки матричні елементи, що відповідають різним значенням dd, можна обчислювати паралельно, ми використовуємо режим Batch для їх ефективного виконання.

# -------------------------Step 1-------------------------

# Map the classical problem to quantum circuits and observables.
# Problem and KQD parameters.
large_num_qubits = 30
large_krylov_dim = 7
large_num_trotter_steps = 3
large_dt = np.pi / (3 * (large_num_qubits - 1))
large_t = Parameter("t_large")

# Use a single excitation near the center of the chain.
large_excitation_qubit = large_num_qubits // 2
large_ref_label = ["0"] * large_num_qubits
large_ref_label[large_num_qubits - 1 - large_excitation_qubit] = "1"
large_ref_bitstring = "".join(large_ref_label)

# Use the ordering and product formula selected in the preceding section.
large_hamiltonian = make_heisenberg_hamiltonian_ordered(
large_num_qubits,
ordering="even-odd edge-grouped",
coupling=1.0,
)
large_ref_energy = float(
np.real(
basis_state_expectation_sparse(large_hamiltonian, large_ref_bitstring)
)
)
large_vacuum_energy = float(
np.real(
basis_state_expectation_sparse(
large_hamiltonian, "0" * large_num_qubits
)
)
)

large_evolution_gate = PauliEvolutionGate(
large_hamiltonian,
time=large_t,
synthesis=SuzukiTrotter(order=2, reps=large_num_trotter_steps),
)
large_uncontrolled_evolution = QuantumCircuit(
large_num_qubits,
name="U_ST2_large(t)",
)
large_uncontrolled_evolution.append(
large_evolution_gate,
range(large_num_qubits),
)

# Build the control-free extended-swap-test circuit.
large_ancilla = 0
large_system_qubits = list(range(1, large_num_qubits + 1))

large_controlled_state_prep = QuantumCircuit(
large_num_qubits + 1,
name="C-Prep-large",
)
large_controlled_state_prep.cx(
large_ancilla,
large_system_qubits[large_excitation_qubit],
)

large_extended_swap_test = QuantumCircuit(large_num_qubits + 1)
large_extended_swap_test.h(large_ancilla)
large_extended_swap_test.compose(large_controlled_state_prep, inplace=True)
large_extended_swap_test.compose(
large_uncontrolled_evolution,
qubits=large_system_qubits,
inplace=True,
)
large_extended_swap_test.x(large_ancilla)
large_extended_swap_test.compose(
large_controlled_state_prep.inverse(),
inplace=True,
)
large_extended_swap_test.x(large_ancilla)
large_extended_swap_test.p(
-large_vacuum_energy * large_t,
large_ancilla,
)

# Reuse the Hamiltonian-shifting construction from the preceding section.
(
large_obs_x_shifted_hamiltonian,
large_obs_y_shifted_hamiltonian,
large_shift_tau,
) = make_reduced_heisenberg_observables(large_ref_bitstring)

large_observables = [
SparsePauliOp("I" * large_num_qubits + "X"),
SparsePauliOp("I" * large_num_qubits + "Y"),
large_obs_x_shifted_hamiltonian,
large_obs_y_shifted_hamiltonian,
]
large_observable_labels = [
"Re S_0d",
"Im S_0d",
"Re shifted H_0d",
"Im shifted H_0d",
]

# -------------------------Step 2-------------------------

# Optimize the problem for quantum execution.
# Select a real backend and transpile the parameterized circuit to ISA form.
large_backend = service.backend("ibm_boston")
large_pass_manager = generate_preset_pass_manager(
backend=large_backend, optimization_level=3, routing_method="none"
)
large_isa_circuit = large_pass_manager.run(large_extended_swap_test)
large_isa_observables = [
observable.apply_layout(large_isa_circuit.layout)
for observable in large_observables
]

large_two_qubit_gate_count = sum(
instruction.operation.num_qubits == 2
for instruction in large_isa_circuit.data
)

print(f"Backend: {large_backend.name}")
print(f"System qubits: {large_num_qubits}")
print(f"Total circuit qubits: {large_isa_circuit.num_qubits}")
print(f"Krylov dimension: {large_krylov_dim}")
print(f"Time step: {large_dt:.6f}")
print(f"Shift tau: {large_shift_tau}")
print(f"ISA circuit depth: {large_isa_circuit.depth()}")
print(f"ISA two-qubit gates: {large_two_qubit_gate_count}")
print(
f"ISA two-qubit depth: {large_isa_circuit.depth(lambda x: x[0].num_qubits == 2)}"
)

# -------------------------Step 3-------------------------

# Execute on quantum hardware with Qiskit Runtime primitives.
# Submit one job per d, with all four observables in that job.
large_d_values = list(range(1, large_krylov_dim))
retrieve_batch_id = None
large_jobs = []

if retrieve_batch_id is None:
large_estimator_options = {
"default_shots": 8192,
"dynamical_decoupling": {
"enable": True,
"sequence_type": "XpXm",
},
"resilience": {
"measure_mitigation": True,
"measure_noise_learning": {
"num_randomizations": 32,
"shots_per_randomization": 256,
},
"layer_noise_learning": {
"max_layers_to_learn": 4,
"layer_pair_depths": [0, 1, 2, 4, 16, 32],
"num_randomizations": 32,
"shots_per_randomization": 128,
},
"zne_mitigation": True,
"zne": {
"amplifier": "pea",
"noise_factors": [1.0, 1.5, 2.0],
"extrapolator": ("exponential", "linear"),
},
},
"twirling": {
"enable_gates": True,
"enable_measure": True,
"num_randomizations": 32,
"shots_per_randomization": 256,
"strategy": "active-accum",
},
}

with Batch(backend=large_backend) as large_batch:
large_batch_id = large_batch.session_id
large_estimator = EstimatorV2(
mode=large_batch,
options=large_estimator_options,
)
# Krylov quantum diagonalization of lattice Hamiltonians -> TUT_KQDOLH.
large_estimator.options.environment.job_tags = ["TUT_KQDOLH"]

for d in large_d_values:
parameter_values = [d * large_dt]
pubs_for_d = [
(large_isa_circuit, observable, parameter_values)
for observable in large_isa_observables
]
job = large_estimator.run(pubs_for_d)
large_jobs.append(job)
print(
f"Submitted d={d}: job_id={job.job_id()}, "
f"PUBs={len(pubs_for_d)}"
)

print(f"Batch ID: {large_batch_id}")
else:
large_batch_id = retrieve_batch_id
large_jobs = service.jobs(
session_id=large_batch_id,
limit=None,
descending=False,
)

large_job_ids_by_d = {
d: job.job_id() for d, job in zip(large_d_values, large_jobs)
}
print(f"Job IDs by d: {large_job_ids_by_d}")

# -------------------------Step 4-------------------------

# Post-process the quantum results and solve the classical GEVP.
# Reconstruct the first rows of S and the shifted Hamiltonian matrix.
large_s_row_est = np.zeros(large_krylov_dim, dtype=complex)
large_h_shifted_row_est = np.zeros(large_krylov_dim, dtype=complex)
large_s_row_est[0] = 1.0
large_h_shifted_row_est[0] = large_ref_energy - large_shift_tau

for d, job in zip(large_d_values, large_jobs):
job_result = job.result()

if len(job_result) != len(large_observables):
raise RuntimeError(
f"Expected {len(large_observables)} PUB results for d={d}, "
f"but received {len(job_result)}."
)

expectation_values = [
np.asarray(pub_result.data.evs).reshape(-1)[0]
for pub_result in job_result
]

large_s_row_est[d] = expectation_values[0] + 1j * expectation_values[1]
large_h_shifted_row_est[d] = (
expectation_values[2] + 1j * expectation_values[3]
)

# H_0d = shifted_H_0d + tau * S_0d.
large_h_row_est = large_h_shifted_row_est + large_shift_tau * large_s_row_est

large_s_matrix_est = la.toeplitz(
large_s_row_est.conj(),
large_s_row_est,
)
large_h_matrix_est = la.toeplitz(
large_h_row_est.conj(),
large_h_row_est,
)

large_exact_gnd = min(diagonalize_single_1_subspace(large_hamiltonian))

large_energy_convergence = []

with np.printoptions(precision=5, suppress=True):
print("Estimated first row of S:")
print(large_s_row_est)
print("Estimated first row of H:")
print(large_h_row_est)
print("exact ground state energy: ", large_exact_gnd)

for r in range(1, large_krylov_dim + 1):
energy_est_kqd, coeffs_est, retained_est = solve_thresholded_gevp(
large_h_matrix_est[:r, :r],
large_s_matrix_est[:r, :r],
threshold=5e-2,
)
large_energy_convergence.append(energy_est_kqd)
print(
f"Krylov ground state energy (dim={r}, retained={retained_est}): ",
energy_est_kqd,
)

plot_energy_convergence(
large_energy_convergence, large_exact_gnd, large_krylov_dim
)
Backend: ibm_boston
System qubits: 30
Total circuit qubits: 156
Krylov dimension: 7
Time step: 0.036110
Shift tau: 25.0
ISA circuit depth: 219
ISA two-qubit gates: 728
ISA two-qubit depth: 52
Job IDs by d: {1: 'd9fl4p4jeosc73fk4dg0', 2: 'd9fl4pineu4c739poecg', 3: 'd9fl4q2neu4c739poedg', 4: 'd9fl4qhhtsac739fjgn0', 5: 'd9fl4r4jeosc73fk4dhg', 6: 'd9fl4rkjeosc73fk4dj0'}
Estimated first row of S:
[ 1. +0.j 0.42055-0.65466j -0.17883-0.73125j -0.64523-0.31601j
-0.70781+0.55574j -0.02467+0.70525j 0.59301+0.55602j]
Estimated first row of H:
[ 25. +0.j 10.3419 -16.5586j -5.017 -18.03933j
-16.44797 -6.98173j -17.17081+14.84502j 0.62722+17.81196j
15.68886+12.74875j]
exact ground state energy: 21.021912418526902
Krylov ground state energy (dim=1, retained=1): 25.0
Krylov ground state energy (dim=2, retained=2): 24.432409110686205
Krylov ground state energy (dim=3, retained=3): 24.256856893278556
Krylov ground state energy (dim=4, retained=4): 23.727409826799715
Krylov ground state energy (dim=5, retained=4): 23.324720470780864
Krylov ground state energy (dim=6, retained=5): 21.91957579005085
Krylov ground state energy (dim=7, retained=5): 21.548331214122644

Output of the previous code cell

Додаток: погляд з точки зору функції гамільтоніана (спектрального фільтра)​

Основний робочий процес представив KQD операційно: побудувати крилівський базис зі станів, еволюціонованих у реальному часі, оцінити проєктовані матриці H\mathcal{H} та S\mathcal{S} і розв'язати GEVP. Цей додаток розглядає те саме обчислення з іншого, доповнювального погляду, який пояснює, чому KQD працює: погляд з точки зору функції гамільтоніана, або спектрального фільтра [3], [5]. Він повторно використовує модель на 12 кубітів, часовий крок Δt\Delta t і крилівський розв'язок, уже отриманий вище; нове виконання схеми не потрібне.

Еталонний стан як розподіл енергії​

Нехай гамільтоніан має власний розклад

H=∑mEm ∣Em⟩ ⁣⟨Em∣,E0≤E1≤⋯ ,\begin{equation*} H=\sum_{m} E_m\,|E_m\rangle\!\langle E_m|, \qquad E_0\le E_1\le\cdots, \end{equation*}

з власними станами енергії ∣Em⟩|E_m\rangle. Будь-який еталонний стан можна розкласти в цьому власному базисі,

∣ψ0⟩=∑mam ∣Em⟩,am=⟨Em∣ψ0⟩,\begin{equation*} |\psi_{0}\rangle=\sum_m a_m\,|E_m\rangle, \qquad a_m=\langle E_m|\psi_{0}\rangle, \end{equation*}

тому він несе спектральну вагу pm=∣am∣2p_m=|a_m|^2 на кожній енергії EmE_m. Еталонна енергія — це середнє значення цього розподілу, ⟨H⟩0=∑mpmEm\langle H\rangle_{0}=\sum_m p_m E_m.

Власний розклад загального гамільтоніана з nn кубітів експоненційно дорогий, тому ця картина є лише діагностичною і не є частиною алгоритму. Тут, однак, ми можемо обчислити її дешево для тієї самої задачі n=12n=12: гамільтоніан Гайзенберга зберігає загальну кількість збуджень, а еталон ∣000001000000⟩|000001000000\rangle несе одне збудження, тому весь його спектральний вміст міститься в підпросторі з одним збудженням, розмірність якого зростає лише лінійно з nn. Тому ми повторно використовуємо точний блок з одним збудженням (уже використаний вище як орієнтир) і зчитуємо еталонний розподіл {(Em,pm)}\{(E_m, p_m)\} у цьому підпросторі.

# Exact single-excitation-subspace decomposition of the reference state.
# This reuses make_heisenberg_hamiltonian / _basis_state_transition_amplitude_sparse
# and the n=12 `hamiltonian` and `ref_bitstring` defined in the small-scale example.
single_excitation_states = [1 << k for k in range(num_qubits)]

h_single = np.array(
[
[
_basis_state_transition_amplitude_sparse(hamiltonian, bra, ket)
for ket in single_excitation_states
]
for bra in single_excitation_states
]
)
h_single = 0.5 * (h_single + h_single.conj().T)

subspace_evals, subspace_evecs = la.eigh(h_single)
subspace_evals = np.real(subspace_evals)

# Reference-state coordinates inside the single-excitation subspace.
ref_position = single_excitation_states.index(int(ref_bitstring, 2))
ref_in_subspace = np.zeros(num_qubits, dtype=complex)
ref_in_subspace[ref_position] = 1.0

# Amplitudes and spectral weights of the reference in the energy eigenbasis.
ref_eigen_amplitudes = subspace_evecs.conj().T @ ref_in_subspace
ref_spectral_weights = np.abs(ref_eigen_amplitudes) ** 2

print(f"Single-excitation subspace dimension: {num_qubits}")
print(f"Subspace ground-state energy: {subspace_evals[0]:.6f}")
print(
f"Reference energy (sum p_m E_m): {np.sum(ref_spectral_weights * subspace_evals):.6f}"
)
print(f"Reference weight on subspace ground: {ref_spectral_weights[0]:.6f}")
Single-excitation subspace dimension: 12
Subspace ground-state energy: 3.136297
Reference energy (sum p_m E_m): 7.000000
Reference weight on subspace ground: 0.163827

KQD навчає фільтр, який перетворює цей розподіл​

Функція гамільтоніана f(H)f(H) визначається через спектральне числення,

f(H)=∑mf(Em) ∣Em⟩ ⁣⟨Em∣,\begin{equation*} f(H)=\sum_m f(E_m)\,|E_m\rangle\!\langle E_m|, \end{equation*}

або, іншими словами, зважена сума власних проєкторів. Застосування її до еталона перетворює кожну спектральну амплітуду, am→amf(Em)a_m \to a_m f(E_m):

f(H) ∣ψ0⟩=∑mam f(Em) ∣Em⟩.\begin{equation*} f(H)\,|\psi_{0}\rangle=\sum_m a_m\,f(E_m)\,|E_m\rangle . \end{equation*}

Якби ff була різко піковою на найнижчій енергії (f(E0)=1f(E_0)=1 і f(Em)≈0f(E_m)\approx 0 в інших випадках), то f(H)f(H) діяла б як проєктор основного стану, а нормований результат був би (близьким до) основного стану. Отже, хороший низькочастотний спектральний фільтр за енергією — це саме те, що нам потрібно.

KQD не задає ff наперед. Натомість він розкладає фільтр у базисі еволюції в реальному часі,

fKQD(E)=∑ℓ=0r−1cℓ e−iℓΔtE,\begin{equation*} f_{\rm KQD}(E)=\sum_{\ell=0}^{r-1} c_\ell\, e^{-i\ell\Delta t E}, \end{equation*}

тригонометрична функція енергії, коефіцієнти якої {cℓ}\{c_\ell\} є саме тим власним вектором GEVP, який ми розв'язали вище. Мінімізація відношення Релея c†Hc/c†Sc\mathbf{c}^\dagger\mathcal{H}\mathbf{c}/\mathbf{c}^\dagger\mathcal{S}\mathbf{c}, отже, є тим самим, що й навчання фільтра, який найкраще пригнічує вагу збудженого стану еталона. Більша крилівська розмірність rr дає фільтру більше ступенів свободи та гостріший пік на енергії основного стану.

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

def trigonometric_krylov_filter(
coeffs: np.ndarray,
energies: np.ndarray,
time_step: float,
) -> np.ndarray:
"""Evaluate the learned Krylov filter f(E) = sum_l c_l exp(-i l dt E)."""
values = np.zeros_like(energies, dtype=complex)
for ell, coeff in enumerate(coeffs):
values += coeff * np.exp(-1j * ell * time_step * energies)
return values

def filtered_spectral_weights(
weights: np.ndarray,
filter_values: np.ndarray,
) -> np.ndarray:
"""Reshape spectral weights by |f(E)|^2 and renormalize."""
reshaped = weights * np.abs(filter_values) ** 2
return reshaped / np.sum(reshaped)

# Recover the KQD coefficients from the already-estimated projected matrices.
# The shift only moves H by tau * S, so it does not change the GEVP eigenvector;
# we solve at the full Krylov dimension used in the small-scale example.
_, kqd_coeffs, _ = solve_thresholded_gevp(
h_matrix_est,
s_matrix_est,
threshold=1e-12,
)

filter_on_spectrum = trigonometric_krylov_filter(
kqd_coeffs, subspace_evals, dt
)
filtered_weights = filtered_spectral_weights(
ref_spectral_weights, filter_on_spectrum
)

print(f"Ground-state overlap (reference): {ref_spectral_weights[0]:.4f}")
print(f"Ground-state overlap (filtered): {filtered_weights[0]:.4f}")
print(
f"Mean energy (reference): {np.sum(ref_spectral_weights * subspace_evals):.6f}"
)
print(
f"Mean energy (filtered): {np.sum(filtered_weights * subspace_evals):.6f}"
)
Ground-state overlap (reference): 0.1638
Ground-state overlap (filtered): 0.9638
Mean energy (reference): 7.000000
Mean energy (filtered): 3.164503

Візуалізація фільтра та його гнучкості​

Спочатку ми показуємо навчений фільтр при повній крилівській розмірності, використаній вище, а потім простежуємо, як він загострюється зі зростанням розмірності rr.

Стовпчики показують еталонні спектральні ваги pmp_m (до) та відфільтровані ваги pm∣fKQD(Em)∣2p_m|f_{\rm KQD}(E_m)|^2 (після), разом з інтенсивністю навченого фільтра ∣fKQD(E)∣2|f_{\rm KQD}(E)|^2 на неперервній осі енергії. Фільтр концентрує вагу на найнижчій енергії підпростору з одним збудженням — тій самій енергії, до якої збіглася оцінка KQD у прикладі малого масштабу. Зауважте, що це основний стан в межах сектора з одним збудженням, який є відповідною ціллю для цього еталонного стану, що зберігає збудження, а не глобальний основний стан.

fig, ax = plt.subplots(figsize=(8, 4))

visible = (ref_spectral_weights > 1e-4) | (filtered_weights > 1e-4)
ax.bar(
subspace_evals[visible],
ref_spectral_weights[visible],
width=0.18,
alpha=0.45,
label="reference $p_m$",
)
ax.bar(
subspace_evals[visible],
filtered_weights[visible],
width=0.14,
alpha=0.9,
label=r"filtered $p_m\,|f_{\rm KQD}(E_m)|^2$",
)

energy_grid = np.linspace(
subspace_evals.min() - 0.5, subspace_evals.max() + 0.5, 800
)
filter_intensity = (
np.abs(trigonometric_krylov_filter(kqd_coeffs, energy_grid, dt)) ** 2
)
filter_intensity /= filter_intensity.max()
ax.plot(
energy_grid,
filter_intensity,
color="k",
linewidth=2,
label=r"$|f_{\rm KQD}(E)|^2$ (normalized)",
)

ax.axvline(
subspace_evals[0],
color="C3",
linestyle="--",
linewidth=1,
label="subspace ground energy",
)
ax.set_xlabel("Energy eigenvalue $E_m$")
ax.set_ylabel("Spectral weight")
ax.set_ylim(0, 1)
ax.legend(loc="upper right")
plt.tight_layout()
plt.show()

Output of the previous code cell

Збільшення крилівської розмірності: гнучкість навченої функції​

Пригадаймо, що навчений фільтр — це тригонометричний многочлен від енергії з rr коефіцієнтами,

fKQD(E)=∑ℓ=0r−1cℓ e−iℓΔtE.\begin{equation*} f_{\rm KQD}(E)=\sum_{\ell=0}^{r-1} c_\ell\, e^{-i\ell\Delta t E}. \end{equation*}

Крилівська розмірність rr — це точно кількість вільних коефіцієнтів, тому вона контролює гнучкість функції. Малий rr може виробити лише широкий, плавно змінюваний фільтр, який пропускає вагу в низьколежачі збуджені стани; коли rr зростає, фільтр може сформувати вужчий пік на цільовій енергії та агресивніше пригнічувати решту ваги збудженого стану. Це спектрально-фільтровий аналог збіжності енергії, що спостерігається в прикладі малого масштабу: зі зростанням rr відфільтрований розподіл стягується до основного стану підпростору, а оцінена енергія зменшується до нього.

Ми повторно використовуємо проєктовані матриці, вже оцінені вище, і просто розв'язуємо GEVP на кожному провідному блоці r×rr\times r, а потім обчислюємо та будуємо графік відповідного фільтра.

# Sweep the Krylov dimension using the leading r x r blocks of the estimated matrices.
sweep_dims = [r for r in (2, 4, 6, 8, krylov_dim) if r <= krylov_dim]
sweep_dims = sorted(set(sweep_dims))

energy_grid = np.linspace(
subspace_evals.min() - 0.5, subspace_evals.max() + 0.5, 800
)

sweep_cases = []
print(" r retained ground overlap filtered energy")
print("-- -------- -------------- ---------------")
for r in sweep_dims:
_, coeffs_r, retained_r = solve_thresholded_gevp(
h_matrix_est[:r, :r],
s_matrix_est[:r, :r],
threshold=1e-12,
)
filter_on_spectrum_r = trigonometric_krylov_filter(
coeffs_r, subspace_evals, dt
)
filtered_weights_r = filtered_spectral_weights(
ref_spectral_weights, filter_on_spectrum_r
)
filtered_energy_r = float(np.sum(filtered_weights_r * subspace_evals))

sweep_cases.append((r, coeffs_r, filtered_weights_r))
print(
f"{r:2d} {retained_r:8d} {filtered_weights_r[0]:14.4f} {filtered_energy_r:15.6f}"
)

print(f"\nSubspace ground-state energy (target): {subspace_evals[0]:.6f}")

# One panel per Krylov dimension: filtered spectrum (bars) + filter intensity (curve).
fig, axes = plt.subplots(
len(sweep_cases),
1,
figsize=(8, 2.1 * len(sweep_cases)),
sharex=True,
)
axes = np.atleast_1d(axes)

for idx, (ax, (r, coeffs_r, filtered_weights_r)) in enumerate(
zip(axes, sweep_cases)
):
visible = (ref_spectral_weights > 1e-4) | (filtered_weights_r > 1e-4)
ax.bar(
subspace_evals[visible],
ref_spectral_weights[visible],
width=0.18,
alpha=0.35,
color="C0",
label="reference $p_m$" if idx == 0 else None,
)
ax.bar(
subspace_evals[visible],
filtered_weights_r[visible],
width=0.14,
alpha=0.9,
color="C1",
label="filtered weights" if idx == 0 else None,
)

filter_intensity_r = (
np.abs(trigonometric_krylov_filter(coeffs_r, energy_grid, dt)) ** 2
)
filter_intensity_r /= filter_intensity_r.max()
ax.plot(energy_grid, filter_intensity_r, color="k", linewidth=2)

ax.axvline(subspace_evals[0], color="C3", linestyle="--", linewidth=1)
ax.set_ylim(0, 1)
ax.set_ylabel("weight")
ax.legend(loc="upper right", title=f"$r={r}$")

axes[-1].set_xlabel("Energy eigenvalue $E_m$")
fig.suptitle(
r"KQD-learned filter $|f_{\rm KQD}(E)|^2$ sharpening with Krylov dimension $r$",
y=1.0,
)
fig.tight_layout()
plt.show()
r retained ground overlap filtered energy
-- -------- -------------- ---------------
2 2 0.4549 4.184510
4 4 0.8373 3.310168
6 6 0.9706 3.158100
8 7 0.9701 3.158725
10 8 0.9638 3.164503

Subspace ground-state energy (target): 3.136297

Output of the previous code cell

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

Якщо ця робота здалася тобі цікавою, тебе можуть зацікавити такі матеріали:

Посилання​

[1] E. N. Epperly, L. Lin та Y. Nakatsukasa, A theory of quantum subspace diagonalization, SIAM Journal on Matrix Analysis and Applications 43, 1263-1290 (2022).

[2] N. Yoshioka, M. Amico, W. Kirby та ін., Diagonalization of large many-body Hamiltonians on a quantum processor, arXiv:2407.14431 (2024).

[3] R. M. Parrish and P. L. McMahon, Quantum filter diagonalization: quantum eigendecomposition without full quantum phase estimation, Physical Review Letters 122, 230401 (2019).

[4] G. Lee, S. Choi, J. Huh, and A. F. Izmaylov, Efficient strategies for reducing sampling error in quantum Krylov subspace diagonalization, Digital Discovery 4, 954-969 (2025).

[5] G. Lee, M. Kang, J. Hong, S. Fomichev and J. Huh, Filtered Quantum Phase Estimation, arXiv:2510.04294 (2025).