Schnellstart
Diese Anleitung zeigt ein minimales Arbeitsbeispiel für das Paket qiskit-addon-sqd. Wir verwenden Sample-based quantum diagonalization (SQD), um verrauschte Samples aus einem Quantenschaltkreis zu verfeinern, der den Grundzustand eines chemischen Hamiltonoperators approximiert. Die Grundzustandsenergien werden approximiert, indem der Hamiltonoperator in Unterräumen diagonalisiert wird, die von den verfeinerten Samples aufgespannt werden.
Ausführlichere End-to-End-Workflows, die auf Quantenhardware laufen, findest du in den Tutorials:
-
Sample-based quantum diagonalization: Workflow für chemische Hamiltonoperatoren
-
Sample-based Krylov quantum diagonalization: Workflow für Fermionische Gittermodelle
1. Die Eingaben für SQD vorbereiten
Zuerst definieren wir den chemischen Hamiltonoperator und den Quantenschaltkreis, der seinen Grundzustand approximiert. Das in diesem Beispiel untersuchte System ist N₂ in Gleichgewichtsgeometrie in der STO-3G-Basis mit zwei eingefrorenen Kernorbitalen. Dieses System hat 8 räumliche Orbitale und 10 Elektronen, was bedeutet, dass der Quantenschaltkreis auf 16 Qubits definiert ist, wobei die Qubits 0-7 Spin-up-Orbitale und die Qubits 8-15 Spin-down-Orbitale darstellen. Da es sich um ein Closed-Shell-System mit 10 Elektronen handelt, wissen wir, dass jedes physikalisch gültige Sample das Hamming-Gewicht 10 haben sollte, wobei das Hamming-Gewicht sowohl der linken als auch der rechten Hälfte des Bitstrings 5 beträgt.
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"numpy": "numpy", "pyscf": "pyscf", "qiskit_addon_sqd": "qiskit-addon-sqd"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
from pyscf import ao2mo, gto, mcscf, scf
# Construct the Hamiltonian, pull out integrals and other information about the system
mol = gto.M(
atom="N 0 0 0; N 0 0 1.09768", basis="sto-3g", spin=0, charge=0, verbose=0
)
mf = scf.RHF(mol).run()
cas = mcscf.CASCI(mf, ncas=8, nelecas=10)
hcore, nuclear_repulsion_energy = cas.get_h1eff()
eri = cas.get_h2eff()
num_orbitals = cas.ncas
eri = ao2mo.restore(1, eri, num_orbitals)
n_alpha, n_beta = cas.nelecas
nelec = (n_alpha, n_beta)
print(
f"Spatial orbitals: {num_orbitals}\nQubits: {num_orbitals * 2}\nElectrons (alpha, beta): {nelec}"
)
Spatial orbitals: 8
Qubits: 16
Electrons (alpha, beta): (5, 5)
2. Verrauschte Samples erzeugen
In einem realen Workflow würdest du einen Ansatz-Schaltkreis entwerfen, ihn für die Hardware optimieren und auf der QPU samplen. Hier überspringen wir das und ziehen stattdessen 10,000 gleichverteilte zufällige Bitstrings. Obwohl unsere Samples keine Information über den tatsächlichen Grundzustand enthalten, werden wir sehen, dass das SQD-Protokoll den wahren Grundzustand dennoch wiederherstellen kann, indem es diese Samples iterativ verfeinert, diagonalisiert und die Energien jeder Iteration nutzt, um die nächste Runde der Bitstring-Verfeinerung zu steuern. Wir können den exakten Grundzustand auf diese Weise letztlich wiederherstellen, weil der Unterraum, der den Grundzustand enthält, klein genug ist, um ihn exakt zu berechnen. Da wir wissen, dass jede Hälfte jedes Bitstrings (8 Bits) das Hamming-Gewicht 5 haben sollte, wissen wir konkret, dass der gesamte Hilbert-Unterraum des Systems die Größe hat.
import numpy as np
from qiskit_addon_sqd.counts import generate_bit_array_uniform
# Generate some uniformly-random bitstrings to simulate QPU samples
rng = np.random.default_rng(24)
bit_array = generate_bit_array_uniform(
10_000, num_orbitals * 2, rand_seed=rng
)
print(
f"Generated {bit_array.num_shots} uniformly-random, {bit_array.num_bits}-qubit samples."
)
Generated 10000 uniformly-random, 16-qubit samples.
3. Nachbearbeitung mit SQD
Das Ziel des SQD-Algorithmus ist es, verrauschte Samples einer QPU zu verfeinern und schließlich den Hamiltonoperator in einem von diesen verfeinerten Samples aufgespannten Unterraum zu diagonalisieren. Das geschieht iterativ wie folgt:
-
Samples verfeinern, indem nur diejenigen nachselektiert werden, die sowohl für das Spin-up- als auch für das Spin-down-Teilsystem die korrekte Teilchenzahl haben
-
In kleinen, von den verfeinerten Samples aufgespannten Unterräumen diagonalisieren und Informationen über die Unterräume sammeln, die die besten (niedrigsten) Energien liefern.
-
Für jede Iteration:
-
Die verrauschten Samples verfeinern, indem Bits in Samples mit falscher Teilchenzahl probabilistisch umgeklappt werden. Die Wahrscheinlichkeiten werden anhand der Unterrauminformationen aus der jüngsten Diagonalisierung berechnet.
-
In kleinen, von den verfeinerten Samples aufgespannten Unterräumen diagonalisieren
-
Informationen über die Unterräume sammeln, die die besten (niedrigsten) Energien liefern
-
Aus der Ausgabe können wir ersehen, dass der Algorithmus nach 6 Iterationen zur exakten Grundzustandsenergie konvergiert und dass die Diagonalisierung über dem vollständigen Hilbert-Unterraum der Dimension durchgeführt wird. Natürlich ist die Diagonalisierung über dem vollständigen Unterraum meist nicht machbar, weshalb Systeme, deren Grundzustände einen dünnbesetzten Träger haben, am ehesten von Unterraum-Diagonalisierungstechniken wie SQD profitieren.
Weitere Informationen zur Funktion diagonalize_fermionic_hamiltonian findest du in der API-Dokumentation.
from functools import partial
from pyscf import fci
from qiskit_addon_sqd.fermion import (
SCIResult,
diagonalize_fermionic_hamiltonian,
solve_sci_batch,
)
exact_energy, _ = fci.direct_spin1.kernel(
hcore, eri, int(num_orbitals), nelec
)
exact_energy += nuclear_repulsion_energy
print(f"Exact (FCI) reference energy: {exact_energy:.6f} Ha\n")
sci_solver = partial(solve_sci_batch, spin_sq=0.0, max_cycle=200)
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):
error = result.energy + nuclear_repulsion_energy - exact_energy
print(f" Subsample {i}")
print(f" Energy: {result.energy + nuclear_repulsion_energy:.6f}")
print(
f" Subspace dimension: {np.prod(result.sci_state.amplitudes.shape)}"
)
print(f" Error vs exact: {error:.6f} Ha")
samples_per_batch = 20
result = diagonalize_fermionic_hamiltonian(
hcore,
eri,
bit_array,
samples_per_batch=samples_per_batch,
norb=num_orbitals,
nelec=nelec,
occupancies_tol=1e-7,
max_iterations=30,
sci_solver=sci_solver,
symmetrize_spin=True,
callback=callback,
seed=np.random.default_rng(32),
)
Exact (FCI) reference energy: -107.652521 Ha
Iteration 1
Subsample 0
Energy: -106.908481
Subspace dimension: 784
Error vs exact: 0.744040 Ha
Iteration 2
Subsample 0
Energy: -106.934921
Subspace dimension: 1764
Error vs exact: 0.717600 Ha
Iteration 3
Subsample 0
Energy: -106.953430
Subspace dimension: 2209
Error vs exact: 0.699091 Ha
Iteration 4
Subsample 0
Energy: -107.651946
Subspace dimension: 2916
Error vs exact: 0.000575 Ha
Iteration 5
Subsample 0
Energy: -107.652003
Subspace dimension: 3025
Error vs exact: 0.000518 Ha
Iteration 6
Subsample 0
Energy: -107.652521
Subspace dimension: 3136
Error vs exact: 0.000000 Ha
Iteration 7
Subsample 0
Energy: -107.652521
Subspace dimension: 3136
Error vs exact: 0.000000 Ha