Przejdź do głównej treści
Ten projekt jest w wersji beta. Znalazłeś błąd? Otwórz zgłoszenie na GitHub. Masz pytanie lub pomysł? Zapytaj w Discussions — chętnie poznamy Twoją opinię!
Jak oceniasz jakość tłumaczenia tej strony?

Diagonalizacja kwantowa Krylova oparta na próbkowaniu dla fermionowego modelu sieciowego

Szacowane użycie: Dziewięć sekund na procesorze Heron r2 (UWAGA: To jest tylko szacunek. Twój czas wykonania może być inny.)

Efekty uczenia się​

  • Jak używać dodatku SQD do Qiskit, aby przybliżyć energię stanu podstawowego modelu sieciowego przy użyciu bitstrings próbkowanych z jednostki przetwarzania kwantowego (QPU).

  • Jak używać ffsim do konstruowania obwodów ewolucji czasowej dla symulacji fermionowej.

  • Jak łączyć próbki z wielu obwodów do post-processingu przy użyciu algorytmu diagonalizacji kwantowej Krylova opartej na próbkowaniu (SKQD).

Wymagania wstępne​

Tło​

Ten samouczek pokazuje, jak używać kwantowej diagonalizacji opartej na próbkowaniu (SQD) do szacowania energii stanu podstawowego fermionowego modelu sieciowego. Konkretnie badamy jednowymiarowy jednodomieszkowy model Andersona (SIAM), który służy do opisu magnetycznych domieszek osadzonych w metalach.

Ten samouczek podąża za podobnym przepływem pracy co powiązany samouczek Diagonalizacja kwantowa oparta na próbkowaniu hamiltonianu chemicznego. Jednak kluczowa różnica leży w sposobie budowania obwodów kwantowych. Drugi samouczek używa heurystycznego wariacyjnego ansatzu, który jest atrakcyjny dla hamiltonianów chemicznych potencjalnie z milionami wyrazów oddziaływań. Z drugiej strony, ten samouczek używa obwodów przybliżających ewolucję czasową przez hamiltonian. Takie obwody mogą być głębokie, co sprawia, że to podejście jest lepsze dla zastosowań do modeli sieciowych. Wektory stanów przygotowane przez te obwody tworzą bazę dla podprzestrzeni Krylova, a w rezultacie można dowieść, że algorytm efektywnie zbiega do stanu podstawowego, przy odpowiednich założeniach.

Podejście używane w tym samouczku można traktować jako kombinację technik używanych w SQD i kwantowej diagonalizacji Krylova (KQD). Połączone podejście jest czasami nazywane kwantową diagonalizacją Krylova opartą na próbkowaniu (SQKD). Zobacz Diagonalizację kwantową Krylova dla hamiltonianów sieciowych jako samouczek dotyczący metody KQD.

Ten samouczek jest oparty na pracy "Quantum-Centric Algorithm for Sample-Based Krylov Diagonalization", do której można się odwołać po więcej szczegółów.

Jednodomieszkowy model Andersona (SIAM)​

Jednowymiarowy hamiltonian SIAM jest sumą trzech wyrazów:

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

gdzie

Himp=ε(n^d↑+n^d↓)+Un^d↑n^d↓,Hbath=−t∑j=0σ∈{↑,↓}L−1(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*}

Tutaj cj,σ†/cj,σc^\dagger_{\mathbf{j},\sigma}/c_{\mathbf{j},\sigma} to fermionowe operatory kreacji/anihilacji dla jth\mathbf{j}^{\textrm{th}} miejsca kąpieli ze spinem σ\sigma, d^σ†/d^σ\hat{d}^\dagger_{\sigma}/\hat{d}_{\sigma} to operatory kreacji/anihilacji dla modu domieszki, a n^dσ=d^σ†d^σ\hat{n}_{d\sigma} = \hat{d}^\dagger_{\sigma} \hat{d}_{\sigma}. tt, UU i VV to liczby rzeczywiste opisujące odpowiednio oddziaływania przeskokowe, na miejscu i hybrydyzacyjne, zaś ε\varepsilon to liczba rzeczywista określająca potencjał chemiczny.

Zauważ, że hamiltonian jest szczególnym przypadkiem ogólnego hamiltoniana elektronów oddziałujących,

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*}

gdzie H1H_1 składa się z wyrazów jednociałowych, które są kwadratowe w fermionowych operatorach kreacji i anihilacji, a H2H_2 składa się z wyrazów dwuciałowych, które są czwartego stopnia. Dla SIAM,

H2=Un^d↑n^d↓H_2 = U \hat{n}_{d\uparrow}\hat{n}_{d\downarrow}

a H1H_1 zawiera pozostałe wyrazy hamiltoniana. Aby reprezentować hamiltonian programowo, przechowujemy macierz hpqh_{pq} i tensor hpqrsh_{pqrs}.

Bazy położenia i pędu​

Ze względu na przybliżoną symetrię translacyjną w HbathH_\textrm{bath}, nie oczekujemy, że stan podstawowy będzie rzadki w bazie położenia (bazie orbitalnej, w której hamiltonian jest określony powyżej). Wydajność SQD jest gwarantowana tylko wtedy, gdy stan podstawowy jest rzadki, tzn. ma znaczące wagi tylko na małej liczbie stanów bazy obliczeniowej. Aby poprawić rzadkość stanu podstawowego, przeprowadzamy symulację w bazie orbitalnej, w której HbathH_\textrm{bath} jest diagonalny. Tę bazę nazywamy bazą pędową. Ponieważ HbathH_\textrm{bath} jest kwadratowym fermionowym hamiltonianem, może być efektywnie zdiagonalizowany przez obrót orbitalny.

Przybliżona ewolucja czasowa przez hamiltonian​

Aby przybliżyć ewolucję czasową przez hamiltonian, używamy dekompozycji Trottera-Suzukiego drugiego rzędu,

e−iΔtH≈e−iΔt2H2e−iΔtH1e−iΔ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}.

W ramach transformacji Jordana-Wignera, ewolucja czasowa przez H2H_2 sprowadza się do pojedynczej bramki CPhase między orbitalami spin-góra i spin-dół w miejscu domieszki. Ponieważ H1H_1 jest kwadratowym fermionowym hamiltonianem, ewolucja czasowa przez H1H_1 sprowadza się do obrotu orbitalnego.

Stany bazowe Krylova {∣ψk⟩}k=0D−1\{ |\psi_k\rangle \}_{k=0}^{D-1}, gdzie DD jest wymiarem podprzestrzeni Krylova, są tworzone przez wielokrotne zastosowanie pojedynczego kroku Trottera, więc

∣ψk⟩≈[e−iΔt2H2e−iΔtH1e−iΔ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}.

W poniższym przepływie pracy opartym na SQD będziemy próbkować z tego zestawu obwodów i post-procesować połączony zestaw bitstrings przy użyciu SQD. To podejście kontrastuje z tym stosowanym w powiązanym samouczku Diagonalizacja kwantowa oparta na próbkowaniu hamiltonianu chemicznego, gdzie próbki były pobierane z jednego heurystycznego wariacyjnego obwodu.

Wymagania​

Przed rozpoczęciem tego samouczka upewnij się, że masz zainstalowane:

  • Qiskit SDK v1.0 lub nowszy, z obsługą wizualizacji

  • Qiskit Runtime v0.22 lub nowszy (pip install qiskit-ibm-runtime)

  • SQD Qiskit addon v0.14 lub nowszy (pip install qiskit-addon-sqd)

  • ffsim v0.0.72 lub nowszy (pip install ffsim)

Przykład na małą skalę z symulatorem​

Krok 1: Odwzorowanie problemu na obwód kwantowy​

Najpierw generujemy hamiltonian SIAM w bazie położenia. Hamiltonian jest reprezentowany przez macierz hpqh_{pq} i tensor hpqrsh_{pqrs}. Następnie obracamy go do bazy pędowej. W bazie położenia umieszczamy domieszkę na pierwszym miejscu. Jednak gdy obracamy do bazy pędowej, przesuwamy domieszkę na centralne miejsce, aby ułatwić oddziaływania z innymi orbitalami.

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

_needed = {"ffsim": "ffsim", "matplotlib": "matplotlib", "numpy": "numpy", "pyscf": "pyscf", "qiskit": "qiskit", "qiskit_addon_sqd": "qiskit-addon-sqd", "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")
import numpy as np
import pyscf.fci

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 = 8

# 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

# Use PySCF to compute the exact ground state energy
reference_energy, _ = pyscf.fci.direct_spin1.kernel(h1e, h2e, norb, nelec)

Następnie generujemy obwody do produkcji stanów bazowych Krylova. Dla każdego gatunku spinowego, stan początkowy ∣ψ0⟩\ket{\psi_0} jest zadany przez superpozycję wszystkich możliwych wzbudzeń trzech elektronów najbliższych poziomu Fermiego do 4 najbliższych pustych trybów startując od stanu ∣00⋯0011⋯11⟩|00\cdots 0011 \cdots 11\rangle, realizowaną przez zastosowanie siedmiu bramek XXPlusYYGate. Stany po ewolucji czasowej są produkowane przez kolejne zastosowania kroku Trottera drugiego rzędu.

Bardziej szczegółowy opis tego modelu i sposobu projektowania obwodów znajduje się w pracy "Quantum-Centric Algorithm for Sample-Based Krylov Diagonalization".

from collections.abc 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."""
assert norb >= 8
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: Optymalizacja problemu pod kątem wykonania kwantowego​

Następnie optymalizujemy obwód dla docelowego sprzętu. Na razie utworzymy generyczny Backend z określoną liczbą qubitów i zestawem bramek, do których obwody ewolucji czasowej naturalnie się rozkładają.

from qiskit.providers.fake_provider import GenericBackendV2

rng = np.random.default_rng(12345)
backend = GenericBackendV2(
2 * norb, basis_gates=["cp", "xx_plus_yy", "p", "x"], seed=rng
)

Teraz używamy Qiskit do transpilacji obwodów do docelowego backendu.

from qiskit.transpiler import generate_preset_pass_manager

pass_manager = generate_preset_pass_manager(
optimization_level=3,
backend=backend,
seed_transpiler=int(rng.integers(2**31)),
)
isa_circuits = pass_manager.run(circuits)

Krok 3: Wykonanie przy użyciu prymitywów Qiskit​

Po optymalizacji obwodów do wykonania na sprzęcie jesteśmy gotowi uruchomić je na docelowym sprzęcie i zebrać próbki do estymacji energii stanu podstawowego. Po użyciu prymitywu Sampler do próbkowania bitstrings z każdego obwodu łączymy wszystkie wyniki w jeden słownik zliczeń i rysujemy 20 najczęściej próbkowanych bitstrings.

from qiskit.primitives import StatevectorSampler
from qiskit.visualization import plot_histogram

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

# Combine the shots 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)

Output of the previous code cell

Krok 4: Post-processing i zwrócenie wyniku do żądanego formatu klasycznego​

Teraz uruchamiamy algorytm SQD przy użyciu funkcji diagonalize_fermionic_hamiltonian. Zobacz dokumentację API, aby zapoznać się z wyjaśnieniami argumentów tej funkcji.

Używamy trim SQD, który włączamy, przekazując TrimPolicy jako argument policy. Każda iteracja odzyskiwania konfiguracji w trim SQD składa się z następujących kroków:

  1. Zdiagonalizuj każdą partię bitciągów.

  2. Przytnij każdą partię tak, aby pozostały tylko ciągi interakcji konfiguracji (CI) o największej wadze w aproksymacji stanu podstawowego tej partii. Ciągi CI to połówki bitciągów ze spinem α\alpha i spinem β\beta.

  3. Połącz pozostałe ciągi CI ze wszystkich partii i zdiagonalizuj hamiltonian ponownie w połączonej podprzestrzeni.

Przy dwóch partiach każda iteracja kosztuje trzy diagonalizacje. Callback otrzymuje tylko połączony wynik, więc każda iteracja wypisuje jedną energię. Opcja trim_ratio ustala ułamek ciągów CI każdej partii, które przetrwają przycinanie. Argument max_dim ogranicza liczbę ciągów CI na sektor spinowy w każdej podprzestrzeni, w tym w połączonej. Więcej szczegółów znajdziesz w przewodniku po trim SQD.

from qiskit_addon_sqd.fermion import (
SCIResult,
diagonalize_fermionic_hamiltonian,
)
from qiskit_addon_sqd.trim import TrimPolicy

# 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"\tEnergy: {result.energy}")
print(
f"\tSubspace dimension: {np.prod(result.sci_state.amplitudes.shape)}"
)

result = diagonalize_fermionic_hamiltonian(
h1e_momentum,
h2e_momentum,
bit_array,
samples_per_batch=1000,
norb=norb,
nelec=nelec,
num_batches=2,
policy=TrimPolicy(
trim_ratio=0.8,
carryover_threshold=1e-4,
carryover_ratio=0.5,
),
max_dim=100,
max_iterations=5,
symmetrize_spin=True,
callback=callback,
seed=rng,
)
Iteration 1
Energy: -13.422366648114345
Subspace dimension: 729
Iteration 2
Energy: -13.422366646833664
Subspace dimension: 729

Poniższy fragment kodu rysuje wyniki. Pierwszy wykres pokazuje błąd obliczonej energii w funkcji liczby iteracji odzyskiwania konfiguracji, a drugi wykres pokazuje średnie obsadzenie każdego orbitalu przestrzennego po ostatniej iteracji. Ponieważ jest to tak mały problem, już pierwsza iteracja przybliża nas bardzo blisko dokładnej energii.

import matplotlib.pyplot as plt

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))
e_diff = [abs(e - reference_energy) for e in min_es]

# 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, e_diff, label="energy error", marker="o")
axs[0].set_xticks(x1)
axs[0].set_xticklabels(x1)
axs[0].set_yscale("log")
axs[0].set_ylim(1e-5, 1e-2)
axs[0].set_title("Approximated Ground State Energy Error vs SQD Iterations")
axs[0].set_xlabel("Iteration Index", fontdict={"fontsize": 12})
axs[0].set_ylabel("Energy Error", 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 energy: {reference_energy:.5f}")
print(f"SQD energy: {min_e:.5f}")
print(f"Absolute error: {abs(min_e - reference_energy):.5f}")
plt.tight_layout()
plt.show()
Reference energy: -13.42249
SQD energy: -13.42237
Absolute error: 0.00012

Output of the previous code cell

Weryfikacja energii​

Energia zwrócona przez SQD jest gwarantowana jako górne ograniczenie prawdziwej energii stanu podstawowego. Wartość energii można zweryfikować, ponieważ SQD zwraca również współczynniki wektora stanu przybliżającego stan podstawowy. Możesz obliczyć energię z wektora stanu przy użyciu jego jedno- i dwucząstkowych zredukowanych macierzy gęstości, jak pokazano w poniższym fragmencie kodu.

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: -13.42237

Przykład na dużą skalę na prawdziwym sprzęcie​

Teraz uruchamiamy większy przykład na prawdziwym QPU. Jako energię referencyjną używamy wyników obliczenia DMRG przeprowadzonego osobno. Ponieważ ten układ ma znacznie więcej możliwych ciągów CI niż mały przykład, obniżamy trim_ratio do 0.5 i zwiększamy max_dim do 200.

from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import SamplerV2 as Sampler

# Model parameters
norb = 20
nelec = (norb // 2, norb // 2)
n_bath = norb - 1
hybridization = 1.0
hopping = 1.0
onsite = 10.0
chemical_potential = -0.5 * onsite

# Generate Hamiltonian and orbital rotation
h1e, h2e = siam_hamiltonian(
norb=norb,
hopping=hopping,
onsite=onsite,
hybridization=hybridization,
chemical_potential=chemical_potential,
)
orbital_rotation = momentum_basis(norb)
h1e_momentum, h2e_momentum = rotated(h1e, h2e, orbital_rotation.T.conj())
impurity_index = n_bath // 2

# Set reference energy to DMRG value computed separately
reference_energy = -28.70659686

# Algorithm parameters
time_step = 0.2
krylov_dim = 8

# Construct circuits
qubits = QuantumRegister(2 * norb, name="q")
circuit = QuantumCircuit(qubits)
for instruction in prepare_initial_state(qubits, norb=norb, nocc=norb // 2):
circuit.append(instruction)
circuit.measure_all()
circuits = [circuit.copy()]
one_body_evolution = scipy.linalg.expm(-1j * time_step * h1e_momentum)
for i in range(krylov_dim - 1):
circuit.remove_final_measurements()
for instruction in trotter_step(
qubits,
time_step,
one_body_evolution,
h2e_momentum,
impurity_index,
norb,
):
circuit.append(instruction)
circuit.measure_all()
circuits.append(circuit.copy())

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

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

# Sample from the circuits
sampler = Sampler(backend)
sampler.options.environment.job_tags = ["TUT_SKQD"]
job = sampler.run(isa_circuits, shots=500)

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

# Run configuration recovery and diagonalization
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"\tEnergy: {result.energy}")
print(
f"\tSubspace dimension: {np.prod(result.sci_state.amplitudes.shape)}"
)

result = diagonalize_fermionic_hamiltonian(
h1e_momentum,
h2e_momentum,
bit_array,
samples_per_batch=1000,
norb=norb,
nelec=nelec,
num_batches=2,
policy=TrimPolicy(
trim_ratio=0.5,
carryover_threshold=1e-4,
carryover_ratio=0.5,
),
max_dim=200,
max_iterations=5,
symmetrize_spin=True,
callback=callback,
seed=rng,
)

# Plot results
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])
x1 = range(len(result_history))
e_diff = [abs(e - reference_energy) for e in min_es]
y2 = np.sum(result.orbital_occupancies, axis=0)
x2 = range(len(y2))
fig, axs = plt.subplots(1, 2, figsize=(12, 6))
axs[0].plot(x1, e_diff, label="energy error", marker="o")
axs[0].set_xticks(x1)
axs[0].set_xticklabels(x1)
axs[0].set_yscale("log")
axs[0].set_ylim(1e-3, 1e-1)
axs[0].set_title("Approximated Ground State Energy Error vs SQD Iterations")
axs[0].set_xlabel("Iteration Index", fontdict={"fontsize": 12})
axs[0].set_ylabel("Energy Error", fontdict={"fontsize": 12})
axs[0].legend()
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 energy: {reference_energy:.5f}")
print(f"SQD energy: {min_e:.5f}")
print(f"Absolute error: {abs(min_e - reference_energy):.5f}")
plt.tight_layout()
plt.show()
Using backend ibm_fez
Iteration 1
Energy: -28.632642067246973
Subspace dimension: 13689
Iteration 2
Energy: -28.7030144883651
Subspace dimension: 18769
Iteration 3
Energy: -28.70519592695907
Subspace dimension: 13456
Iteration 4
Energy: -28.705288597420616
Subspace dimension: 15129
Iteration 5
Energy: -28.70502224787334
Subspace dimension: 15129
Reference energy: -28.70660
SQD energy: -28.70529
Absolute error: 0.00131

Output of the previous code cell

Następne kroki​

Rekomendacje

Jeśli ta praca Cię zainteresowała, możesz być zainteresowany następującymi materiałami: