Szybki start
Ten przewodnik przedstawia minimalny działający przykład pakietu qiskit-addon-sqd. Używamy próbkowanej diagonalizacji kwantowej (SQD), aby udoskonalić zaszumione próbki pobrane z obwodu kwantowego, który przybliża stan podstawowy hamiltonianu chemicznego. Energie stanu podstawowego są przybliżane przez diagonalizację hamiltonianu w podprzestrzeniach utworzonych z udoskonalonych próbek.
Bardziej szczegółowe przepływy pracy end-to-end uruchamiane na sprzęcie kwantowym znajdziesz w samouczkach:
-
Próbkowana diagonalizacja kwantowa: przepływ pracy dla hamiltonianu chemicznego
-
Próbkowana diagonalizacja kwantowa Kryłowa: przepływ pracy dla fermionowego modelu sieciowego
1. Przygotuj dane wejściowe dla SQD
Najpierw definiujemy hamiltonian chemiczny i obwód kwantowy, który przybliża jego stan podstawowy. Badanym w tym przykładzie układem jest N₂ w geometrii równowagowej w bazie STO-3G z dwoma zamrożonymi orbitalami rdzeniowymi. Układ ma 8 orbitali przestrzennych i 10 elektronów, co oznacza, że obwód kwantowy jest zdefiniowany na 16 kubitach, gdzie kubity 0-7 reprezentują orbitale ze spinem w górę, a kubity 8-15 orbitale ze spinem w dół. Ponieważ jest to układ o zamkniętej powłoce z 10 elektronami, wiemy, że każda fizycznie poprawna próbka powinna mieć wagę Hamminga 10, przy czym waga Hamminga zarówno lewej, jak i prawej połowy ciągu bitów wynosi 5.
# 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. Wygeneruj zaszumione próbki
W prawdziwym przepływie pracy zaprojektowałbyś obwód ansatzu, zoptymalizował go pod kątem sprzętu i próbkował na QPU. Tutaj pomijamy to i zamiast tego losujemy 10,000 jednostajnie losowych ciągów bitów. Choć nasze próbki nie zawierają żadnych informacji o rzeczywistym stanie podstawowym, zobaczymy, że protokół SQD nadal może odzyskać prawdziwy stan podstawowy, iteracyjnie udoskonalając te próbki, diagonalizując i wykorzystując energie z każdej iteracji do ukierunkowania kolejnej rundy udoskonalania ciągów bitów. Możemy w ten sposób ostatecznie odzyskać dokładny stan podstawowy, ponieważ podprzestrzeń zawierająca stan podstawowy jest na tyle mała, że można ją obliczyć dokładnie. Konkretnie, ponieważ wiemy, że każda połowa każdego ciągu bitów (8 bitów) powinna mieć wagę Hamminga 5, wiemy, że pełna podprzestrzeń Hilberta układu ma rozmiar .
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. Przetwórz końcowo za pomocą SQD
Celem algorytmu SQD jest udoskonalenie zaszumionych próbek pobranych z QPU i ostatecznie diagonalizacja hamiltonianu w podprzestrzeni rozpiętej przez te udoskonalone próbki. Odbywa się to iteracyjnie w następujący sposób:
-
Udoskonal próbki, wybierając post-selekcją tylko te, które mają poprawną liczbę cząstek zarówno w podukładzie ze spinem w górę, jak i ze spinem w dół
-
Zdiagonalizuj w małych podprzestrzeniach rozpiętych przez udoskonalone próbki i zbierz informacje o podprzestrzeniach, które dają najlepsze (najniższe) energie.
-
W każdej iteracji:
-
Udoskonal zaszumione próbki, probabilistycznie odwracając bity w próbkach z niepoprawną liczbą cząstek. Prawdopodobieństwa są obliczane na podstawie informacji o podprzestrzeniach z ostatniej diagonalizacji.
-
Zdiagonalizuj w małych podprzestrzeniach rozpiętych przez udoskonalone próbki
-
Zbierz informacje o podprzestrzeniach dających najlepsze (najniższe) energie
-
Z wyników widać, że algorytm zbiega do dokładnej energii stanu podstawowego po 6 iteracjach oraz że diagonalizacja jest wykonywana w pełnej podprzestrzeni Hilberta o wymiarze . Oczywiście diagonalizacja w pełnej podprzestrzeni zwykle nie jest wykonalna, dlatego układy, których stany podstawowe mają rzadkie nośniki, najprawdopodobniej skorzystają z technik diagonalizacji w podprzestrzeni, takich jak SQD.
Więcej informacji o funkcji diagonalize_fermionic_hamiltonian znajdziesz w dokumentacji API.
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