Zum Hauptinhalt springen
Dieses Projekt befindet sich in der Beta-Phase. Einen Fehler gefunden? GitHub Issue erstellen. Frage oder Idee? In Discussions fragen — wir freuen uns über dein Feedback!
Wie ist die Übersetzungsqualität auf dieser Seite?

Sample-based Krylov-Quantendiagonalisierung eines fermionischen Gittermodells

Schätzung des Ressourcenverbrauchs: Neun Sekunden auf einem Heron-r2-Prozessor (HINWEIS: Dies ist nur eine Schätzung. Deine tatsächliche Laufzeit kann abweichen.)

Lernziele​

  • Wie man das SQD Qiskit-Addon verwendet, um die Grundzustandsenergie eines Gittermodells mithilfe von Bitstrings zu approximieren, die von einer Quantum Processing Unit (QPU) gesampelt wurden.

  • Wie du ffsim verwendest, um Schaltkreise für die Zeitentwicklung bei der fermionischen Simulation zu konstruieren.

  • Wie du Stichproben aus mehreren Circuits für die Nachbearbeitung mit dem stichprobenbasierten Krylov-Diagonalisierungsalgorithmus (SKQD) kombinierst.

Voraussetzungen​

Hintergrund​

Dieses Tutorial zeigt, wie man die sample-basierte Quantendiagonalisierung (SQD) verwendet, um die Grundzustandsenergie eines fermionischen Gittermodells zu schätzen. Konkret untersuchen wir das eindimensionale Single-Impurity-Anderson-Modell (SIAM), das zur Beschreibung magnetischer Verunreinigungen in Metallen verwendet wird.

Dieses Tutorial folgt einem ähnlichen Arbeitsablauf wie das verwandte Tutorial Stichprobenbasierte Quantendiagonalisierung eines chemischen Hamiltonoperators. Ein wesentlicher Unterschied liegt jedoch darin, wie die Quantenschaltkreise aufgebaut werden. Das andere Tutorial verwendet einen heuristischen Variationsansatz, der für Chemie-Hamiltonoperatoren mit potenziell Millionen von Wechselwirkungstermen attraktiv ist. Dieses Tutorial hingegen verwendet Schaltkreise, die die Zeitentwicklung durch den Hamiltonoperator approximieren. Solche Schaltkreise können tief sein, was diesen Ansatz besser für Anwendungen auf Gittermodelle macht. Die durch diese Schaltkreise vorbereiteten Zustandsvektoren bilden die Basis für einen Krylov-Unterraum, und daher konvergiert der Algorithmus nachweislich und effizient zum Grundzustand, unter geeigneten Annahmen.

Der in diesem Tutorial verwendete Ansatz kann als Kombination der Techniken aus SQD und Krylov-Quantendiagonalisierung (KQD) betrachtet werden. Der kombinierte Ansatz wird manchmal als sample-basierte Krylov-Quantendiagonalisierung (SQKD) bezeichnet. Unter Krylov-Quantendiagonalisierung von Gitter-Hamiltonoperatoren findest du ein Tutorial zur KQD-Methode.

Dieses Tutorial basiert auf der Arbeit "Quantum-Centric Algorithm for Sample-Based Krylov Diagonalization", in der du weitere Details findest.

Single-Impurity-Anderson-Modell (SIAM)​

Der eindimensionale SIAM-Hamiltonoperator ist eine Summe aus drei Termen:

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

wobei

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

Hier sind cj,σ†/cj,σc^\dagger_{\mathbf{j},\sigma}/c_{\mathbf{j},\sigma} die fermionischen Erzeugungs-/Vernichtungsoperatoren für den jth\mathbf{j}^{\textrm{th}} Badplatz mit Spin σ\sigma, d^σ†/d^σ\hat{d}^\dagger_{\sigma}/\hat{d}_{\sigma} sind Erzeugungs-/Vernichtungsoperatoren für den Verunreinigungsmodus, und n^dσ=d^σ†d^σ\hat{n}_{d\sigma} = \hat{d}^\dagger_{\sigma} \hat{d}_{\sigma}. tt, UU und VV sind reelle Zahlen, die die Hüpf-, On-Site- und Hybridisierungswechselwirkungen beschreiben, und ε\varepsilon ist eine reelle Zahl, die das chemische Potential angibt.

Beachte, dass der Hamiltonoperator eine spezifische Instanz des generischen wechselwirkenden Elektronen-Hamiltonoperators ist,

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

wobei H1H_1 aus Einkörpertermen besteht, die quadratisch in den fermionischen Erzeugungs- und Vernichtungsoperatoren sind, und H2H_2 aus Zweikörpertermen, die quartisch sind. Für das SIAM gilt:

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

und H1H_1 enthält den Rest der Terme im Hamiltonoperator. Um den Hamiltonoperator programmatisch darzustellen, speichern wir die Matrix hpqh_{pq} und den Tensor hpqrsh_{pqrs}.

Orts- und Impulsbasis​

Aufgrund der näherungsweisen Translationssymmetrie in HbathH_\textrm{bath} erwarten wir nicht, dass der Grundzustand in der Ortsbasis dünn besetzt ist (die Orbitalbasis, in der der Hamiltonoperator oben angegeben ist). Die Leistung von SQD ist nur dann garantiert, wenn der Grundzustand dünn besetzt ist, das heißt, er hat signifikantes Gewicht auf nur einer kleinen Anzahl von Rechenbasiszuständen. Um die Dünnbesetzung des Grundzustands zu verbessern, führen wir die Simulation in der Orbitalbasis durch, in der HbathH_\textrm{bath} diagonal ist. Wir nennen diese Basis die Impulsbasis. Da HbathH_\textrm{bath} ein quadratischer fermionischer Hamiltonoperator ist, kann er effizient durch eine Orbitalrotation diagonalisiert werden.

Näherungsweise Zeitentwicklung durch den Hamiltonoperator​

Um die Zeitentwicklung durch den Hamiltonoperator zu approximieren, verwenden wir eine Trotter-Suzuki-Zerlegung zweiter Ordnung,

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

Unter der Jordan-Wigner-Transformation entspricht die Zeitentwicklung durch H2H_2 einem einzelnen CPhase-Gate zwischen den Spin-up- und Spin-down-Orbitalen am Verunreinigungsplatz. Da H1H_1 ein quadratischer fermionischer Hamiltonoperator ist, entspricht die Zeitentwicklung durch H1H_1 einer Orbitalrotation.

Die Krylov-Basiszustände {∣ψk⟩}k=0D−1\{ |\psi_k\rangle \}_{k=0}^{D-1}, wobei DD die Dimension des Krylov-Unterraums ist, werden durch wiederholte Anwendung eines einzelnen Trotter-Schritts gebildet, sodass

∣ψ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}.

Im folgenden SQD-basierten Arbeitsablauf werden wir aus dieser Menge von Schaltkreisen sampeln und die kombinierte Menge von Bitstrings mit SQD nachverarbeiten. Dieser Ansatz unterscheidet sich von dem im verwandten Tutorial Stichprobenbasierte Quantendiagonalisierung eines chemischen Hamiltonoperators, bei dem Samples aus einem einzelnen heuristischen Variationsschaltkreis gezogen wurden.

Anforderungen​

Bevor du dieses Tutorial beginnst, stelle sicher, dass du Folgendes installiert hast:

  • Qiskit SDK v1.0 oder höher, mit Visualisierungsunterstützung

  • Qiskit Runtime v0.22 oder höher (pip install qiskit-ibm-runtime)

  • SQD Qiskit Addon v0.14 oder höher (pip install qiskit-addon-sqd)

  • ffsim v0.0.72 oder höher (pip install ffsim)

Kleinskaliges Simulator-Beispiel​

Schritt 1: Problem auf einen Quantenschaltkreis abbilden​

Zunächst generieren wir den SIAM-Hamiltonoperator in der Ortsbasis. Der Hamiltonoperator wird durch die Matrix hpqh_{pq} und den Tensor hpqrsh_{pqrs} dargestellt. Anschließend rotieren wir ihn in die Impulsbasis. In der Ortsbasis platzieren wir die Verunreinigung am ersten Platz. Wenn wir jedoch in die Impulsbasis wechseln, verschieben wir die Verunreinigung an einen zentralen Platz, um Wechselwirkungen mit anderen Orbitalen zu erleichtern.

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

Als nächstes generieren wir die Schaltkreise zur Erzeugung der Krylov-Basiszustände. Für jede Spinspezies ist der Anfangszustand ∣ψ0⟩\ket{\psi_0} als Superposition aller möglichen Anregungen der drei Elektronen, die der Fermi-Energie am nächsten sind, in die 4 nächstliegenden leeren Moden gegeben, ausgehend vom Zustand ∣00⋯0011⋯11⟩|00\cdots 0011 \cdots 11\rangle, und wird durch die Anwendung von sieben XXPlusYYGates realisiert. Die zeitentwickelten Zustände werden durch aufeinanderfolgende Anwendungen eines Trotter-Schritts zweiter Ordnung erzeugt.

Für eine detailliertere Beschreibung dieses Modells und wie die Schaltkreise entworfen werden, siehe "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

Schritt 2: Problem für die Quantenausführung optimieren​

Als nächstes optimieren wir den Schaltkreis für eine Zielhardware. Zunächst erstellen wir ein generisches Backend mit einer bestimmten Anzahl von Qubits und einem Gate-Set, in das die Zeitentwicklungsschaltkreise auf natürliche Weise zerlegt werden.

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
)

Nun verwenden wir Qiskit, um die Schaltkreise zum Ziel-Backend zu transpilieren.

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)

Schritt 3: Mit Qiskit Primitives ausführen​

Nach der Optimierung der Schaltkreise für die Hardwareausführung sind wir bereit, sie auf der Zielhardware auszuführen und Samples für die Grundzustandsenergieschätzung zu sammeln. Nachdem wir das Sampler Primitive verwendet haben, um Bitstrings aus jedem Schaltkreis zu sampeln, kombinieren wir alle Ergebnisse in einem einzigen Counts-Dictionary und stellen die 20 am häufigsten gesampelten Bitstrings dar.

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

Schritt 4: Nachverarbeitung und Rückgabe des Ergebnisses im gewünschten klassischen Format​

Jetzt führen wir den SQD-Algorithmus mit der Funktion diagonalize_fermionic_hamiltonian aus. Erklärungen zu den Argumenten dieser Funktion findest du in der API-Dokumentation.

Wir verwenden Trim SQD, das aktiviert wird, indem ein TrimPolicy als Argument policy übergeben wird. In jeder Iteration der Konfigurationswiederherstellung diagonalisiert Trim SQD jeden Batch von Bitstrings, beschneidet jeden Batch auf die CI-Strings (die Spin-α\alpha- und Spin-β\beta-Hälften der Bitstrings), die in seiner Grundzustandsnäherung das größte Gewicht tragen, und diagonalisiert die zusammengeführten übrig gebliebenen Strings noch einmal. Bei zwei Batches kostet jede Iteration drei Diagonalisierungen. Der Callback erhält nur das zusammengeführte Ergebnis, daher gibt jede Iteration eine einzelne Energie aus. Die Option trim_ratio legt den Anteil der CI-Strings jedes Batches fest, der das Beschneiden übersteht, und max_dim begrenzt die Anzahl der CI-Strings pro Spin-Sektor in jedem Unterraum, einschließlich des zusammengeführten. Weitere Details findest du im Trim-SQD-Leitfaden.

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

Die folgende Code-Zelle stellt die Ergebnisse dar. Der erste Plot zeigt den Fehler der berechneten Energie als Funktion der Anzahl der Konfigurationswiederherstellungsiterationen, und der zweite Plot zeigt die durchschnittliche Besetzung jedes räumlichen Orbitals nach der letzten Iteration. Da es sich um ein so kleines Problem handelt, bringt uns die erste Iteration bereits sehr nahe an die exakte Energie.

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

Energie verifizieren​

Die von SQD zurückgegebene Energie ist garantiert eine obere Schranke für die wahre Grundzustandsenergie. Der Wert der Energie kann verifiziert werden, da SQD auch die Koeffizienten des Zustandsvektors zurückgibt, der den Grundzustand approximiert. Du kannst die Energie aus dem Zustandsvektor mithilfe seiner reduzierten Ein- und Zweiteilchen-Dichtematrizen berechnen, wie in der folgenden Code-Zelle gezeigt.

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

Großskaliges Hardware-Beispiel​

Jetzt führen wir ein größeres Beispiel auf einer echten QPU aus. Als Referenzenergie verwenden wir die Ergebnisse einer DMRG-Berechnung, die separat durchgeführt wurde. Da dieses System viel mehr mögliche CI-Strings hat, senken wir trim_ratio auf 0.5 und erhöhen max_dim auf 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

Nächste Schritte​

Empfehlungen

Wenn du diese Arbeit interessant findest, könnte dich folgendes Material interessieren: