Skip to main content
IBM Quantum Platform

Probenbasierte Krylov-Quantendiagonalisierung eines fermionischen Gittermodells

Geschätzte Nutzungsdauer: Neun Sekunden auf einem Heron r2 Prozessor (HINWEIS: Dies ist nur eine Schätzung. Ihre Laufzeit kann variieren.)


Lernziele

  • Wie man das SQD-Qiskit-Add-on verwendet, um die Grundzustandsenergie eines Gittermodells anhand von Bitstrings zu approximieren, die von einer Quantenverarbeitungseinheit (QPU) abgetastet wurden.
  • Wie man mit ffsim Zeitentwicklungsschaltungen für die Fermionensimulation erstellt.
  • Wie man Signale aus mehreren Schaltkreisen für die Weiterverarbeitung mit dem algorithmischen Verfahren der samplebasierten Krylov-Diagonalisierung (SKQD) zusammenführt.

Voraussetzungen


Hintergrund

Dieses Tutorial zeigt, wie man die stichprobenbasierte Quantendiagonalisierung (SQD) verwendet, um die Grundzustandsenergie eines fermionischen Gittermodells abzuschätzen. Konkret untersuchen wir das eindimensionale Single-Impurity-Anderson-Modell (SIAM), das zur Beschreibung von in Metallen eingebetteten magnetischen Verunreinigungen verwendet wird.

Dieses Tutorial folgt einem ähnlichen Arbeitsablauf wie das verwandte Tutorial Probenbasierte Quantendiagonalisierung eines Chemie-Hamiltonians. Ein wesentlicher Unterschied besteht jedoch darin, wie die Quantenschaltungen aufgebaut sind. Das andere Tutorial verwendet einen heuristischen Variationsansatz, der für chemische Hamiltonianer mit potenziell Millionen von Wechselwirkungstermen interessant ist. Andererseits werden in diesem Tutorium Schaltungen verwendet, die die Zeitentwicklung durch den Hamiltonian annähern. Solche Schaltkreise können tief sein, was diesen Ansatz für Anwendungen auf Gittermodelle besser geeignet macht. Die von diesen Schaltkreisen vorbereiteten Zustandsvektoren bilden die Grundlage für einen Krylov-Unterraum, so dass der Algorithmus unter geeigneten Annahmen nachweislich und effizient gegen den Grundzustand konvergiert.

Der in diesem Tutorium verwendete Ansatz kann als eine Kombination aus den Techniken der SQD und der Krylov-Quanten-Diagonalisierung (KQD ) betrachtet werden. Der kombinierte Ansatz wird manchmal als stichprobenbasierte Krylov-Quantendiagonalisierung (SQKD) bezeichnet. Siehe Krylov-Quanten-Diagonalisierung von Gitter-Hamiltonianern für eine Einführung in die KQD-Methode.

Dieses Tutorial basiert auf der Arbeit "Quantum-Centric Algorithm for Sample-Based Krylov Diagonalization", die für weitere Details herangezogen werden kann.

Anderson-Modell mit einer einzigen Verunreinigung (SIAM)

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

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

wo

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-/Annihilationsoperatoren für die jth\mathbf{j}^{\textrm{th}} Badstelle mit dem Spin σ\sigma, d^σ†/d^σ\hat{d}^\dagger_{\sigma}/\hat{d}_{\sigma} sind Erzeugungs-/Annihilationsoperatoren für den Störstellenmodus 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 Sprung-, Vor-Ort- und Hybridisierungswechselwirkungen beschreiben, und ε\varepsilon ist eine reelle Zahl, die das chemische Potenzial angibt.

Es ist zu beachten, dass der Hamiltonian eine spezifische Instanz des allgemeinen Wechselwirkungs-Elektronen-Hamiltonians 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 Annihilationsoperatoren sind, und H2H_2 aus Zweikörpertermen besteht, die quartisch sind. Für die SIAM,

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 Hamiltonian. Um den Hamiltonian programmatisch darstellen zu können, speichern wir die Matrix hpqh_{pq} und den Tensor hpqrsh_{pqrs}.

Positions- und Impulsbasen

Aufgrund der annähernden Translationssymmetrie in HbathH_\textrm{bath} erwarten wir nicht, dass der Grundzustand in der Positionsbasis (der Orbitalbasis, in der der Hamiltonian oben angegeben ist) spärlich ist. Die Leistung von SQD ist nur dann gewährleistet, wenn der Grundzustand spärlich ist, d. h. nur eine kleine Anzahl von Basiszuständen eine signifikante Bedeutung hat. Um die Sparsamkeit 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 Hamiltonian ist, kann er durch eine Orbitalrotation effizient diagonalisiert werden.

Ungefähre zeitliche Entwicklung durch den Hamilton-Operator

Zur Annäherung der Zeitentwicklung durch den Hamiltonian 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 läuft die Zeitentwicklung durch H2H_2 auf ein einziges CPhase-Gate zwischen den Spin-up- und Spin-down-Orbitalen an der Verunreinigungsstelle hinaus. Da es sich bei H1H_1 um einen quadratischen fermionischen Hamiltonian handelt, kommt die Zeitentwicklung über H1H_1 einer Orbitalrotation gleich.

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 einzigen Trotter-Schritts gebildet, so dass

∣ψ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 diesem Satz von Schaltkreisen Stichproben nehmen und den kombinierten Satz von Bitstrings mit SQD nachbearbeiten. Diese Herangehensweise steht im Gegensatz zu der im verwandten Tutorial Sample-based quantum diagonalization of a chemistry Hamiltonian verwendeten, bei der die Proben aus einem einzigen heuristischen Variationskreis gezogen wurden.


Anforderungen

Vergewissern Sie sich vor Beginn dieses Tutorials, dass Sie die folgenden Komponenten installiert haben:

  • Qiskit SDK v1.0 oder später, mit Visual isierungsunterstützung
  • Qiskit Runtime v0.22 oder später (pip install qiskit-ibm-runtime)
  • SQD Qiskit-Add-on v0.11 oder höher (pip install qiskit-addon-sqd)
  • ffsim v0.0.72 oder höher (pip install ffsim)

Beispiel für einen Simulator im kleinen Maßstab

Schritt 1: Problem auf einen Quantenschaltkreis abbilden

Zunächst erzeugen wir den SIAM-Hamiltonschen in der Positionsbasis. Der Hamiltonian wird durch die Matrix hpqh_{pq} und den Tensor hpqrsh_{pqrs} dargestellt. Dann drehen wir ihn in die Impulsbasis um. In der Positionsbasis setzen wir die Verunreinigung an die erste Stelle. Wenn wir jedoch zur Impulsbasis wechseln, verschieben wir die Verunreinigung an einen zentralen Ort, um Wechselwirkungen mit anderen Orbitalen zu erleichtern.

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 erzeugen wir die Schaltkreise, um die Krylov-Basiszustände zu erzeugen. Für jede Spinspezies ist der Ausgangszustand ∣ψ0⟩\ket{\psi_0} durch die Überlagerung aller möglichen Anregungen der drei dem Fermi-Niveau am nächsten liegenden Elektronen in die 4 nächstgelegenen leeren Moden, ausgehend vom Zustand ∣00⋯0011⋯11⟩|00\cdots 0011 \cdots 11\rangle, gegeben und wird durch die Anwendung von sieben XXPlusYYGates realisiert. Die zeitlich veränderten Zustände werden durch aufeinanderfolgende Anwendungen eines Trotter-Schrittes zweiter Ordnung erzeugt.

Eine ausführlichere Beschreibung dieses Modells und des Entwurfs der Schaltkreise finden Sie unter "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."""
    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:

Output of the previous code cell
circuits[-1].draw("mpl", scale=0.4, fold=-1)

Output:

Output of the previous code cell

Schritt 2: Optimierung des Problems für die Quantenausführung

Als Nächstes optimieren wir die Schaltung für eine bestimmte Hardware. Zunächst erstellen wir ein generisches Backend mit einer bestimmten Anzahl von Qubits und einem Gatter-Satz, in den sich die Zeitentwicklungsschaltungen auf natürliche Weise zerlegen lassen.

from qiskit.providers.fake_provider import GenericBackendV2

backend = GenericBackendV2(
    2 * norb, basis_gates=["cp", "xx_plus_yy", "p", "x"]
)

Jetzt verwenden wir Qiskit, um die Schaltkreise in das Ziel-Backend zu transpilieren.

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)

Schritt 3: Führen Sie den Befehl „ Qiskit primitives “ aus

Nach der Optimierung der Schaltungen für die Hardware-Ausführung können wir sie auf der Zielhardware ausführen und Proben für die Abschätzung der Grundzustandsenergie sammeln. Nachdem wir mit dem Sampler-Primitiv Bitstrings aus jedem Schaltkreis gesampelt haben, fassen wir alle Ergebnisse in einem einzigen Zählwörterbuch zusammen und zeichnen die 20 am häufigsten gesampelten Bitstrings auf.

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

# Sample from the circuits
sampler = StatevectorSampler()
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:

Output of the previous code cell

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

Nun führen wir den SQD-Algorithmus mit der Funktion diagonalize_fermionic_hamiltonian aus. Erläuterungen zu den Argumenten für diese Funktion finden Sie in der API-Dokumentation.

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

Output:

Iteration 1
	Subsample 0
		Energy: -13.4222953188441
		Subspace dimension: 529
	Subsample 1
		Energy: -13.42237556285828
		Subspace dimension: 784
	Subsample 2
		Energy: -13.422045397387413
		Subspace dimension: 529
Iteration 2
	Subsample 0
		Energy: -13.422379583305478
		Subspace dimension: 900
	Subsample 1
		Energy: -13.422376197704326
		Subspace dimension: 841
	Subsample 2
		Energy: -13.422421162849295
		Subspace dimension: 1089
Iteration 3
	Subsample 0
		Energy: -13.422421164670345
		Subspace dimension: 1156
	Subsample 1
		Energy: -13.422421492737689
		Subspace dimension: 1156
	Subsample 2
		Energy: -13.422421205869572
		Subspace dimension: 1156
Iteration 4
	Subsample 0
		Energy: -13.422421494558726
		Subspace dimension: 1225
	Subsample 1
		Energy: -13.422421492737689
		Subspace dimension: 1156
	Subsample 2
		Energy: -13.422421492737689
		Subspace dimension: 1156

Die folgende Codezelle stellt die Ergebnisse grafisch dar. Die erste Grafik zeigt die berechnete Energie als Funktion der Anzahl der Iterationen zur Konfigurationswiederherstellung, und die zweite Grafik zeigt die durchschnittliche Besetzung jedes räumlichen Orbitals nach der letzten Iteration. Da es sich um ein so kleines Problem handelt, kommen wir bereits mit der ersten Iteration sehr nahe an den exakten Energiewert heran (beachte die Skalierung der y-Achse).

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

# 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=reference_energy,
    color="#BF5700",
    linestyle="--",
    label="reference 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 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()

Output:

Reference energy: -13.42249
SQD energy: -13.42242
Absolute error: 0.00007
Output of the previous code cell

Überprüfen Sie den Energieverbrauch

Die von SQD zurückgegebene Energie ist garantiert eine Obergrenze der tatsächlichen Grundzustandsenergie. Der Wert der Energie lässt sich überprüfen, da SQD auch die Koeffizienten des Zustandsvektors zurückgibt, der den Grundzustand approximiert. Sie können die Energie aus dem Zustandsvektor anhand seiner reduzierten Dichtematrizen für ein und zwei Teilchen berechnen, wie in der folgenden Codezelle 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}")

Output:

Recomputed energy: -13.42242

Beispiel für groß angelegte Hardware

Nun führen wir ein umfangreicheres Beispiel auf einer echten QPU aus. Als Referenzenergie verwenden wir die Ergebnisse einer separat durchgeführten DMRG -Berechnung.

from qiskit_ibm_runtime import SamplerV2 as Sampler
from qiskit_ibm_runtime import QiskitRuntimeService

# 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"\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,
)


# 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))
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, min_es, label="energy", marker="o")
axs[0].set_xticks(x1)
axs[0].set_xticklabels(x1)
axs[0].axhline(
    y=reference_energy,
    color="#BF5700",
    linestyle="--",
    label="reference 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()
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()

Output:

Using backend ibm_boston
Iteration 1
	Subsample 0
		Energy: -28.63965951544449
		Subspace dimension: 9801
	Subsample 1
		Energy: -28.625588929202006
		Subspace dimension: 9409
	Subsample 2
		Energy: -28.647371834135498
		Subspace dimension: 8281
Iteration 2
	Subsample 0
		Energy: -28.67213260849567
		Subspace dimension: 29584
	Subsample 1
		Energy: -28.670340686158816
		Subspace dimension: 27225
	Subsample 2
		Energy: -28.669976379525988
		Subspace dimension: 31329
Iteration 3
	Subsample 0
		Energy: -28.68622875601382
		Subspace dimension: 36100
	Subsample 1
		Energy: -28.698569623143126
		Subspace dimension: 34225
	Subsample 2
		Energy: -28.694848533971882
		Subspace dimension: 33856
Iteration 4
	Subsample 0
		Energy: -28.69883392844593
		Subspace dimension: 42025
	Subsample 1
		Energy: -28.701289495200996
		Subspace dimension: 38025
	Subsample 2
		Energy: -28.699319594978245
		Subspace dimension: 45369
Iteration 5
	Subsample 0
		Energy: -28.701936886834154
		Subspace dimension: 51076
	Subsample 1
		Energy: -28.702468711812013
		Subspace dimension: 53824
	Subsample 2
		Energy: -28.702298147575938
		Subspace dimension: 52900
Reference energy: -28.70660
SQD energy: -28.70247
Absolute error: 0.00413
Output of the previous code cell

Nächste Schritte

Empfehlungen

Wenn Sie diese Arbeit interessant fanden, könnten Sie auch an folgenden Materialien interessiert sein:

War diese Seite hilfreich?
Melden Sie auf GitHub einen Fehler, einen Tippfehler oder fordern Sie Inhalte an.