Guida rapida
Questa guida mostra un esempio funzionante minimo del pacchetto qiskit-addon-sqd. Usiamo la diagonalizzazione quantistica basata su campioni (SQD) per raffinare campioni rumorosi prelevati da un circuito quantistico che approssima lo stato fondamentale di un'hamiltoniana chimica. Le energie dello stato fondamentale vengono approssimate diagonalizzando l'hamiltoniana in sottospazi formati dai campioni raffinati.
Per workflow end-to-end più esplicativi che vengono eseguiti su hardware quantistico, consulta i tutorial:
-
Diagonalizzazione quantistica basata su campioni: workflow con hamiltoniana chimica
-
Diagonalizzazione quantistica di Krylov basata su campioni: workflow con modello reticolare fermionico
1. Prepara gli input per SQD
Per prima cosa definiamo l'hamiltoniana chimica e il circuito quantistico che ne approssima lo stato fondamentale. Il sistema studiato in questo esempio è N₂ alla geometria di equilibrio in base STO-3G con due orbitali di core congelati. Questo sistema ha 8 orbitali spaziali e 10 elettroni, il che significa che il circuito quantistico è definito su 16 qubit, dove i qubit 0-7 rappresentano gli orbitali di spin up e i qubit 8-15 rappresentano gli orbitali di spin down. Poiché si tratta di un sistema a guscio chiuso con 10 elettroni, sappiamo che ogni campione fisicamente valido dovrebbe avere peso di Hamming 10, dove il peso di Hamming sia della metà sinistra sia di quella destra della stringa di bit è 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. Genera campioni rumorosi
In un workflow reale progetteresti un circuito ansatz, lo ottimizzeresti per l'hardware e lo campioneresti sulla QPU. Qui saltiamo questo passaggio ed estraiamo invece 10,000 stringhe di bit casuali uniformi. Anche se i nostri campioni non contengono alcuna informazione sul vero stato fondamentale, vedremo che il protocollo SQD può comunque recuperare il vero stato fondamentale raffinando iterativamente questi campioni, diagonalizzando e usando le energie di ogni iterazione per guidare il successivo ciclo di raffinamento delle stringhe di bit. Possiamo recuperare alla fine lo stato fondamentale esatto in questo modo perché il sottospazio che contiene lo stato fondamentale è abbastanza piccolo da essere calcolato esattamente. In particolare, poiché sappiamo che ogni metà di ogni stringa di bit (8 bit) dovrebbe avere peso di Hamming 5, sappiamo che il sottospazio di Hilbert completo del sistema ha dimensione .
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. Post-elabora con SQD
L'obiettivo dell'algoritmo SQD è raffinare i campioni rumorosi prelevati da una QPU e infine diagonalizzare l'hamiltoniana in un sottospazio generato da quei campioni raffinati. Questo avviene in modo iterativo come segue:
-
Raffina i campioni post-selezionando solo quelli che hanno il numero corretto di particelle sia per il sottosistema di spin up sia per quello di spin down
-
Diagonalizza in piccoli sottospazi generati dai campioni raffinati e raccogli informazioni sui sottospazi che producono le energie migliori (più basse).
-
Per ogni iterazione:
-
Raffina i campioni rumorosi invertendo probabilisticamente i bit nei campioni con numero di particelle errato. Le probabilità sono calcolate usando le informazioni sul sottospazio dell'ultima diagonalizzazione.
-
Diagonalizza in piccoli sottospazi generati dai campioni raffinati
-
Raccogli informazioni sui sottospazi che producono le energie migliori (più basse)
-
Dall'output possiamo vedere che l'algoritmo converge all'energia esatta dello stato fondamentale dopo 6 iterazioni e che la diagonalizzazione viene eseguita sull'intero sottospazio di Hilbert di dimensione . Naturalmente, diagonalizzare sull'intero sottospazio di solito non è fattibile e, di conseguenza, i sistemi con stati fondamentali a supporto sparso sono quelli che più probabilmente beneficeranno di tecniche di diagonalizzazione su sottospazi come SQD.
Per maggiori informazioni sulla funzione diagonalize_fermionic_hamiltonian, visita la documentazione 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