Přeskočit na hlavní obsah

Vzorkování-based Krylovova kvantová diagonalizace fermionického mřížkového modelu

Odhad využití: Devět sekund na procesoru Heron r2 (POZNÁMKA: Toto je pouze odhad. Tvůj skutečný čas se může lišit.)

Pozadí

Tento tutoriál ukazuje, jak použít vzorkování-based kvantovou diagonalizaci (SQD) k odhadnutí energie základního stavu fermionického mřížkového modelu. Konkrétně studujeme jednorozměrný model single-impurity Anderson (SIAM), který se používá k popisu magnetických nečistot zakotvených v kovech.

Tento tutoriál sleduje podobný postup jako příbuzný tutoriál Vzorkování-based kvantová diagonalizace chemického hamiltoniánu. Klíčový rozdíl spočívá v tom, jak jsou sestaveny kvantové Circuit. Druhý tutoriál používá heuristický variační ansatz, který je atraktivní pro chemické hamiltoniány s potenciálně miliony interakčních členů. Na druhou stranu, tento tutoriál používá Circuit, které aproximují časový vývoj podle hamiltoniánu. Takovéto Circuit mohou být hluboké, což tento přístup lépe hodí pro aplikace na mřížkové modely. Stavové vektory připravené těmito Circuit tvoří základ pro Krylovův podprostor, a v důsledku toho algoritmus prokazatelně a efektivně konverguje k základnímu stavu za vhodných předpokladů.

Přístup použitý v tomto tutoriálu lze chápat jako kombinaci technik použitých v SQD a Krylovově kvantové diagonalizaci (KQD). Kombinovaný přístup se někdy označuje jako vzorkování-based Krylovova kvantová diagonalizace (SQKD). Viz Krylovova kvantová diagonalizace mřížkových hamiltoniánů pro tutoriál o metodě KQD.

Tento tutoriál vychází z práce "Quantum-Centric Algorithm for Sample-Based Krylov Diagonalization", na kterou se můžeš odkázat pro více podrobností.

Model single-impurity Anderson (SIAM)

Jednorozměrný hamiltonián SIAM je součtem tří členů:

H=Himp+Hbath+Hhyb,H = H_{\textrm{imp}}+ H_\textrm{bath} + H_\textrm{hyb},

kde

Himp=ε(n^d+n^d)+Un^dn^d,Hbath=tj=0σ{,}L1(c^j,σc^j+1,σ+c^j+1,σc^j,σ),Hhyb=Vσ{,}(d^σc^0,σ+c^0,σd^σ).\begin{align*} H_\textrm{imp} &= \varepsilon \left( \hat{n}_{d\uparrow} + \hat{n}_{d\downarrow} \right) + U \hat{n}_{d\uparrow}\hat{n}_{d\downarrow}, \\ H_\textrm{bath} &= -t \sum_{\substack{\mathbf{j} = 0\\ \sigma\in \{\uparrow, \downarrow\}}}^{L-1} \left(\hat{c}^\dagger_{\mathbf{j}, \sigma}\hat{c}_{\mathbf{j}+1, \sigma} + \hat{c}^\dagger_{\mathbf{j}+1, \sigma}\hat{c}_{\mathbf{j}, \sigma} \right), \\ H_\textrm{hyb} &= V\sum_{\sigma \in \{\uparrow, \downarrow \}} \left(\hat{d}^\dagger_\sigma \hat{c}_{0, \sigma} + \hat{c}^\dagger_{0, \sigma} \hat{d}_{\sigma} \right). \end{align*}

Zde cj,σ/cj,σc^\dagger_{\mathbf{j},\sigma}/c_{\mathbf{j},\sigma} jsou fermionské operátory vytváření/anihilace pro jth\mathbf{j}^{\textrm{th}} místo lázně se spinem σ\sigma, d^σ/d^σ\hat{d}^\dagger_{\sigma}/\hat{d}_{\sigma} jsou operátory vytváření/anihilace pro módu nečistoty, a n^dσ=d^σd^σ\hat{n}_{d\sigma} = \hat{d}^\dagger_{\sigma} \hat{d}_{\sigma}. tt, UU a VV jsou reálná čísla popisující hopping, on-site a hybridizační interakce, a ε\varepsilon je reálné číslo specifikující chemický potenciál.

Všimni si, že hamiltonián je konkrétní instance obecného hamiltoniánu interagujících elektronů,

H=p,qσhpqa^pσa^qσ+p,q,r,sστhpqrs2a^pσa^qτa^sτa^rσ=H1+H2,\begin{align*} H &= \sum_{\substack{p, q \\ \sigma}} h_{pq} \hat{a}^\dagger_{p\sigma} \hat{a}_{q\sigma} + \sum_{\substack{p, q, r, s \\ \sigma \tau}} \frac{h_{pqrs}}{2} \hat{a}^\dagger_{p\sigma} \hat{a}^\dagger_{q\tau} \hat{a}_{s\tau} \hat{a}_{r\sigma} \\ &= H_1 + H_2, \end{align*}

kde H1H_1 sestává z jednočásticových členů, které jsou kvadratické ve fermionských operátorech vytváření a anihilace, a H2H_2 sestává z dvočásticových členů, které jsou kvartické. Pro SIAM,

H2=Un^dn^dH_2 = U \hat{n}_{d\uparrow}\hat{n}_{d\downarrow}

a H1H_1 obsahuje zbytek členů hamiltoniánu. Abychom hamiltonián reprezentovali programově, ukládáme matici hpqh_{pq} a tenzor hpqrsh_{pqrs}.

Polohová a impulzová báze

Kvůli přibližné translační symetrii v HbathH_\textrm{bath} neočekáváme, že základní stav bude řídký v polohové bázi (orbitální báze, v níž je hamiltonián výše specifikován). Výkon SQD je zaručen pouze tehdy, pokud je základní stav řídký, tj. má výraznou váhu pouze na malém počtu stavů výpočetní báze. Abychom zlepšili řídkost základního stavu, provádíme simulaci v orbitální bázi, ve které je HbathH_\textrm{bath} diagonální. Tuto bázi nazýváme impulzová báze. Protože HbathH_\textrm{bath} je kvadratický fermionský hamiltonián, lze jej efektivně diagonalizovat orbitální rotací.

Přibližný časový vývoj podle hamiltoniánu

K aproximaci časového vývoje podle hamiltoniánu používáme Trotter-Suzukiho rozklad druhého řádu,

eiΔtHeiΔt2H2eiΔtH1eiΔt2H2. e^{-i \Delta t H} \approx e^{-i\frac{\Delta t}{2} H_2} e^{-i\Delta t H_1} e^{-i\frac{\Delta t}{2} H_2}.

Pomocí Jordanovy-Wignerovy transformace se časový vývoj podle H2H_2 rovná jednomu Gate CPhase mezi spin-up a spin-down orbitaly na místě nečistoty. Protože H1H_1 je kvadratický fermionský hamiltonián, časový vývoj podle H1H_1 odpovídá orbitální rotaci.

Krylovovy bázové stavy {ψk}k=0D1\{ |\psi_k\rangle \}_{k=0}^{D-1}, kde DD je dimenze Krylovova podprostoru, jsou tvořeny opakovanou aplikací jednoho Trotterova kroku, takže

ψk[eiΔt2H2eiΔtH1eiΔt2H2]kψ0. |\psi_k\rangle \approx \left[e^{-i\frac{\Delta t}{2} H_2} e^{-i\Delta t H_1} e^{-i\frac{\Delta t}{2} H_2} \right]^k\ket{\psi_0}.

V následujícím pracovním postupu založeném na SQD budeme vzorkovat z této sady Circuit a post-procesovat kombinovanou sadu bitstrings pomocí SQD. Tento přístup je v kontrastu s přístupem používaným v příbuzném tutoriálu Vzorkování-based kvantová diagonalizace chemického hamiltoniánu, kde byly vzorky odebírány z jednoho heuristického variačního Circuit.

Požadavky

Před zahájením tohoto tutoriálu se ujisti, že máš nainstalováno následující:

  • Qiskit SDK v1.0 nebo novější, s podporou vizualizace
  • Qiskit Runtime v0.22 nebo novější (pip install qiskit-ibm-runtime)
  • SQD Qiskit addon v0.11 nebo novější (pip install qiskit-addon-sqd)
  • ffsim (pip install ffsim)

Krok 1: Namapuj problém na kvantový Circuit

Nejprve vygenerujeme hamiltonián SIAM v polohové bázi. Hamiltonián je reprezentován maticí hpqh_{pq} a tenzorem hpqrsh_{pqrs}. Poté jej rotujeme do impulzové báze. V polohové bázi umístíme nečistotu na první místo. Když však rotujeme do impulzové báze, přesuneme nečistotu na centrální místo, abychom usnadnili interakce s ostatními orbitaly.

# Added by doQumentation — required packages for this notebook
!pip install -q ffsim matplotlib numpy qiskit qiskit-addon-sqd qiskit-ibm-runtime scipy
import numpy as np

def siam_hamiltonian(
norb: int,
hopping: float,
onsite: float,
hybridization: float,
chemical_potential: float,
) -> tuple[np.ndarray, np.ndarray]:
"""Hamiltonian for the single-impurity Anderson model."""
# Place the impurity on the first site
impurity_orb = 0

# One body matrix elements in the "position" basis
h1e = np.zeros((norb, norb))
np.fill_diagonal(h1e[:, 1:], -hopping)
np.fill_diagonal(h1e[1:, :], -hopping)
h1e[impurity_orb, impurity_orb + 1] = -hybridization
h1e[impurity_orb + 1, impurity_orb] = -hybridization
h1e[impurity_orb, impurity_orb] = chemical_potential

# Two body matrix elements in the "position" basis
h2e = np.zeros((norb, norb, norb, norb))
h2e[impurity_orb, impurity_orb, impurity_orb, impurity_orb] = onsite

return h1e, h2e

def momentum_basis(norb: int) -> np.ndarray:
"""Get the orbital rotation to change from the position to the momentum basis."""
n_bath = norb - 1

# Orbital rotation that diagonalizes the bath (non-interacting system)
hopping_matrix = np.zeros((n_bath, n_bath))
np.fill_diagonal(hopping_matrix[:, 1:], -1)
np.fill_diagonal(hopping_matrix[1:, :], -1)
_, vecs = np.linalg.eigh(hopping_matrix)

# Expand to include impurity
orbital_rotation = np.zeros((norb, norb))
# Impurity is on the first site
orbital_rotation[0, 0] = 1
orbital_rotation[1:, 1:] = vecs

# Move the impurity to the center
new_index = n_bath // 2
perm = np.r_[1 : (new_index + 1), 0, (new_index + 1) : norb]
orbital_rotation = orbital_rotation[:, perm]

return orbital_rotation

def rotated(
h1e: np.ndarray, h2e: np.ndarray, orbital_rotation: np.ndarray
) -> tuple[np.ndarray, np.ndarray]:
"""Rotate the orbital basis of a Hamiltonian."""
h1e_rotated = np.einsum(
"ab,Aa,Bb->AB",
h1e,
orbital_rotation,
orbital_rotation.conj(),
optimize="greedy",
)
h2e_rotated = np.einsum(
"abcd,Aa,Bb,Cc,Dd->ABCD",
h2e,
orbital_rotation,
orbital_rotation.conj(),
orbital_rotation,
orbital_rotation.conj(),
optimize="greedy",
)
return h1e_rotated, h2e_rotated

# Total number of spatial orbitals, including the bath sites and the impurity
# This should be an even number
norb = 20

# System is half-filled
nelec = (norb // 2, norb // 2)
# One orbital is the impurity, the rest are bath sites
n_bath = norb - 1

# Hamiltonian parameters
hybridization = 1.0
hopping = 1.0
onsite = 10.0
chemical_potential = -0.5 * onsite

# Generate Hamiltonian in position basis
h1e, h2e = siam_hamiltonian(
norb=norb,
hopping=hopping,
onsite=onsite,
hybridization=hybridization,
chemical_potential=chemical_potential,
)

# Rotate to momentum basis
orbital_rotation = momentum_basis(norb)
h1e_momentum, h2e_momentum = rotated(h1e, h2e, orbital_rotation.T.conj())
# In the momentum basis, the impurity is placed in the center
impurity_index = n_bath // 2

Dále vygenerujeme Circuit pro přípravu Krylovových bázových stavů. Pro každý spinový druh je počáteční stav ψ0\ket{\psi_0} dán superpozicí všech možných excitací tří elektronů nejbližších Fermiho hladině do 4 nejbližších prázdných módů, vycházejících ze stavu 00001111|00\cdots 0011 \cdots 11\rangle, realizovaných aplikací sedmi XXPlusYYGate. Časově vyvinuté stavy jsou produkovány postupnými aplikacemi Trotterova kroku druhého řádu.

Pro podrobnější popis tohoto modelu a způsobu, jakým jsou Circuit navrženy, viz "Quantum-Centric Algorithm for Sample-Based Krylov Diagonalization".

from typing import Sequence

import ffsim
import scipy
from qiskit import QuantumCircuit, QuantumRegister
from qiskit.circuit import CircuitInstruction, Qubit
from qiskit.circuit.library import CPhaseGate, XGate, XXPlusYYGate

def prepare_initial_state(qubits: Sequence[Qubit], norb: int, nocc: int):
"""Prepare initial state."""
x_gate = XGate()
rot = XXPlusYYGate(0.5 * np.pi, -0.5 * np.pi)
for i in range(nocc):
yield CircuitInstruction(x_gate, [qubits[i]])
yield CircuitInstruction(x_gate, [qubits[norb + i]])
for i in range(3):
for j in range(nocc - i - 1, nocc + i, 2):
yield CircuitInstruction(rot, [qubits[j], qubits[j + 1]])
yield CircuitInstruction(
rot, [qubits[norb + j], qubits[norb + j + 1]]
)
yield CircuitInstruction(rot, [qubits[j + 1], qubits[j + 2]])
yield CircuitInstruction(
rot, [qubits[norb + j + 1], qubits[norb + j + 2]]
)

def trotter_step(
qubits: Sequence[Qubit],
time_step: float,
one_body_evolution: np.ndarray,
h2e: np.ndarray,
impurity_index: int,
norb: int,
):
"""A Trotter step."""
# Assume the two-body interaction is just the on-site interaction of the impurity
onsite = h2e[
impurity_index, impurity_index, impurity_index, impurity_index
]
# Two-body evolution for half the time
yield CircuitInstruction(
CPhaseGate(-0.5 * time_step * onsite),
[qubits[impurity_index], qubits[norb + impurity_index]],
)
# One-body evolution for the full time
yield CircuitInstruction(
ffsim.qiskit.OrbitalRotationJW(norb, one_body_evolution), qubits
)
# Two-body evolution for half the time
yield CircuitInstruction(
CPhaseGate(-0.5 * time_step * onsite),
[qubits[impurity_index], qubits[norb + impurity_index]],
)

# Time step
time_step = 0.2
# Number of Krylov basis states
krylov_dim = 8

# Initialize circuit
qubits = QuantumRegister(2 * norb, name="q")
circuit = QuantumCircuit(qubits)

# Generate initial state
for instruction in prepare_initial_state(qubits, norb=norb, nocc=norb // 2):
circuit.append(instruction)
circuit.measure_all()

# Create list of circuits, starting with the initial state circuit
circuits = [circuit.copy()]

# Add time evolution circuits to the list
one_body_evolution = scipy.linalg.expm(-1j * time_step * h1e_momentum)
for i in range(krylov_dim - 1):
# Remove measurements
circuit.remove_final_measurements()
# Append another Trotter step
for instruction in trotter_step(
qubits,
time_step,
one_body_evolution,
h2e_momentum,
impurity_index,
norb,
):
circuit.append(instruction)
# Measure qubits
circuit.measure_all()
# Add a copy of the circuit to the list
circuits.append(circuit.copy())
circuits[0].draw("mpl", scale=0.4, fold=-1)

Output of the previous code cell

circuits[-1].draw("mpl", scale=0.4, fold=-1)

Output of the previous code cell

Krok 2: Optimalizace problému pro kvantové spuštění

Teď, když jsme vytvořili okruhy, je můžeme optimalizovat pro cílový hardware. Vybereme nejméně vytížený QPU s alespoň 127 qubity. Více informací najdeš v dokumentaci Qiskit IBM® Runtime.

from qiskit_ibm_runtime import QiskitRuntimeService

service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)
print(f"Using backend {backend.name}")
Using backend ibm_fez

Teď použijeme Qiskit k transpilaci okruhů na cílový Backend.

from qiskit.transpiler import generate_preset_pass_manager

pass_manager = generate_preset_pass_manager(
optimization_level=3, backend=backend
)
isa_circuits = pass_manager.run(circuits)

Krok 3: Spuštění pomocí primitiv Qiskit

Po optimalizaci okruhů pro spuštění na hardware jsme připraveni spustit je na cílovém hardware a shromáždit vzorky pro odhad energie základního stavu. Po použití primitivy Sampler k vzorkování bitových řetězců z každého okruhu zkombinujeme všechny výsledky do jediného slovníku počtů a vykreslíme 20 nejčastěji vzorkovaných bitových řetězců.

from qiskit.visualization import plot_histogram
from qiskit_ibm_runtime import SamplerV2 as Sampler

# Sample from the circuits
sampler = Sampler(backend)
job = sampler.run(isa_circuits, shots=500)
from qiskit.primitives import BitArray

# Combine the counts from the individual Trotter circuits
bit_array = BitArray.concatenate_shots(
[result.data.meas for result in job.result()]
)

plot_histogram(bit_array.get_counts(), number_to_keep=20)

Výstup předchozí buňky kódu

Krok 4: Následné zpracování a vrácení výsledku do požadovaného klasického formátu

Teď spustíme algoritmus SQD pomocí funkce diagonalize_fermionic_hamiltonian. Vysvětlení argumentů této funkce najdeš v dokumentaci API.

from qiskit_addon_sqd.fermion import (
SCIResult,
diagonalize_fermionic_hamiltonian,
)

# List to capture intermediate results
result_history = []

def callback(results: list[SCIResult]):
result_history.append(results)
iteration = len(result_history)
print(f"Iteration {iteration}")
for i, result in enumerate(results):
print(f"\tSubsample {i}")
print(f"\t\tEnergy: {result.energy}")
print(
f"\t\tSubspace dimension: {np.prod(result.sci_state.amplitudes.shape)}"
)

rng = np.random.default_rng(24)
result = diagonalize_fermionic_hamiltonian(
h1e_momentum,
h2e_momentum,
bit_array,
samples_per_batch=100,
norb=norb,
nelec=nelec,
num_batches=3,
max_iterations=5,
symmetrize_spin=True,
callback=callback,
seed=rng,
)
Iteration 1
Subsample 0
Energy: -28.61321893815165
Subspace dimension: 10609
Subsample 1
Energy: -28.628985564542244
Subspace dimension: 13924
Subsample 2
Energy: -28.620151775558114
Subspace dimension: 10404
Iteration 2
Subsample 0
Energy: -28.656893066053115
Subspace dimension: 34225
Subsample 1
Energy: -28.65277622004119
Subspace dimension: 38416
Subsample 2
Energy: -28.670856034959165
Subspace dimension: 39601
Iteration 3
Subsample 0
Energy: -28.684787675404362
Subspace dimension: 42436
Subsample 1
Energy: -28.676984757118426
Subspace dimension: 50176
Subsample 2
Energy: -28.671581704249885
Subspace dimension: 40804
Iteration 4
Subsample 0
Energy: -28.6859683054753
Subspace dimension: 47961
Subsample 1
Energy: -28.69418206537316
Subspace dimension: 51529
Subsample 2
Energy: -28.686083516445752
Subspace dimension: 51529
Iteration 5
Subsample 0
Energy: -28.694665630711178
Subspace dimension: 50625
Subsample 1
Energy: -28.69505984237118
Subspace dimension: 47524
Subsample 2
Energy: -28.6942873883992
Subspace dimension: 48841

Následující buňka kódu vykresluje výsledky. První graf zobrazuje vypočtenou energii jako funkci počtu iterací obnovy konfigurace, druhý graf zobrazuje průměrnou obsazenost každého prostorového orbitalu po poslední iteraci. Jako referenční energii používáme výsledky výpočtu metodou DMRG, který byl proveden samostatně.

import matplotlib.pyplot as plt

dmrg_energy = -28.70659686

min_es = [
min(result, key=lambda res: res.energy).energy
for result in result_history
]
min_id, min_e = min(enumerate(min_es), key=lambda x: x[1])

# Data for energies plot
x1 = range(len(result_history))

# Data for avg spatial orbital occupancy
y2 = np.sum(result.orbital_occupancies, axis=0)
x2 = range(len(y2))

fig, axs = plt.subplots(1, 2, figsize=(12, 6))

# Plot energies
axs[0].plot(x1, min_es, label="energy", marker="o")
axs[0].set_xticks(x1)
axs[0].set_xticklabels(x1)
axs[0].axhline(
y=dmrg_energy, color="#BF5700", linestyle="--", label="DMRG energy"
)
axs[0].set_title("Approximated Ground State Energy vs SQD Iterations")
axs[0].set_xlabel("Iteration Index", fontdict={"fontsize": 12})
axs[0].set_ylabel("Energy", fontdict={"fontsize": 12})
axs[0].legend()

# Plot orbital occupancy
axs[1].bar(x2, y2, width=0.8)
axs[1].set_xticks(x2)
axs[1].set_xticklabels(x2)
axs[1].set_title("Avg Occupancy per Spatial Orbital")
axs[1].set_xlabel("Orbital Index", fontdict={"fontsize": 12})
axs[1].set_ylabel("Avg Occupancy", fontdict={"fontsize": 12})

print(f"Reference (DMRG) energy: {dmrg_energy:.5f}")
print(f"SQD energy: {min_e:.5f}")
print(f"Absolute error: {abs(min_e - dmrg_energy):.5f}")
plt.tight_layout()
plt.show()
Reference (DMRG) energy: -28.70660
SQD energy: -28.69506
Absolute error: 0.01154

Výstup předchozí buňky kódu

Ověření energie

Energie vrácená algoritmem SQD je zaručeně horní mezí skutečné energie základního stavu. Hodnotu energie lze ověřit, protože SQD také vrací koeficienty vektoru stavu aproximujícího základní stav. Energii z vektoru stavu můžeš vypočítat pomocí jeho jednočásticových a dvoučásticových redukovaných hustotních matic, jak ukazuje následující buňka kódu.

rdm1 = result.sci_state.rdm(rank=1, spin_summed=True)
rdm2 = result.sci_state.rdm(rank=2, spin_summed=True)

energy = np.sum(h1e_momentum * rdm1) + 0.5 * np.sum(h2e_momentum * rdm2)

print(f"Recomputed energy: {energy:.5f}")
Recomputed energy: -28.69506

Reference