Démarrage rapide
Ce guide présente un exemple minimal fonctionnel du package qiskit-addon-sqd. Nous utilisons la diagonalisation quantique basée sur l'échantillonnage (SQD) pour affiner des échantillons bruités issus d'un circuit quantique qui approxime l'état fondamental d'un hamiltonien chimique. Les énergies de l'état fondamental sont approximées en diagonalisant le hamiltonien dans des sous-espaces formés par les échantillons affinés.
Pour des workflows de bout en bout plus explicatifs qui s'exécutent sur du matériel quantique, consulte les tutoriels :
-
Diagonalisation quantique basée sur l'échantillonnage : workflow pour un hamiltonien chimique
-
Diagonalisation quantique de Krylov basée sur l'échantillonnage : workflow pour un modèle de réseau fermionique
1. Préparer les entrées pour SQD
Tout d'abord, nous définissons le hamiltonien chimique et le circuit quantique qui approxime son état fondamental. Le système étudié dans cet exemple est N₂ à la géométrie d'équilibre dans la base STO-3G, avec deux orbitales de cœur gelées. Ce système possède 8 orbitales spatiales et 10 électrons, ce qui signifie que le circuit quantique est défini sur 16 qubits, où les qubits 0-7 représentent les orbitales de spin up et les qubits 8-15 représentent les orbitales de spin down. Comme il s'agit d'un système à couches fermées avec 10 électrons, nous savons que chaque échantillon physiquement valide doit avoir un poids de Hamming de 10, le poids de Hamming de la moitié gauche et de la moitié droite de la chaîne de bits étant chacun de 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. Générer des échantillons bruités
Dans un vrai workflow, tu concevrais un circuit d'ansatz, l'optimiserais pour le matériel et l'échantillonnerais sur le QPU. Ici, nous sautons cette étape et tirons à la place 10,000 chaînes de bits uniformément aléatoires. Bien que nos échantillons ne contiennent aucune information sur l'état fondamental réel, nous verrons que le protocole SQD peut tout de même retrouver le véritable état fondamental en affinant itérativement ces échantillons, en diagonalisant, et en utilisant les énergies de chaque itération pour guider le tour suivant d'affinage des chaînes de bits. Nous pouvons finir par retrouver l'état fondamental exact de cette manière parce que le sous-espace contenant l'état fondamental est assez petit pour être calculé exactement. Plus précisément, comme nous savons que chaque moitié de chaque chaîne de bits (8 bits) doit avoir un poids de Hamming de 5, nous savons que le sous-espace de Hilbert complet du système a pour taille .
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-traiter avec SQD
L'objectif de l'algorithme SQD est d'affiner des échantillons bruités prélevés sur un QPU et, finalement, de diagonaliser le hamiltonien dans un sous-espace engendré par ces échantillons affinés. Cela se fait de manière itérative comme suit :
-
Affiner les échantillons en post-sélectionnant uniquement ceux qui ont le bon nombre de particules pour les sous-systèmes de spin up et de spin down
-
Diagonaliser dans de petits sous-espaces engendrés par les échantillons affinés et collecter des informations sur les sous-espaces qui donnent les meilleures énergies (les plus basses).
-
Pour chaque itération :
-
Affiner les échantillons bruités en inversant de façon probabiliste des bits dans les échantillons dont le nombre de particules est incorrect. Les probabilités sont calculées à partir des informations sur les sous-espaces issues de la diagonalisation la plus récente.
-
Diagonaliser dans de petits sous-espaces engendrés par les échantillons affinés
-
Collecter des informations sur les sous-espaces qui donnent les meilleures énergies (les plus basses)
-
Nous voyons dans la sortie que l'algorithme converge vers l'énergie exacte de l'état fondamental après 6 itérations et que la diagonalisation est effectuée sur le sous-espace de Hilbert complet de dimension . Bien sûr, diagonaliser sur le sous-espace complet n'est généralement pas faisable ; ce sont donc les systèmes dont les états fondamentaux ont un support creux qui ont le plus de chances de bénéficier de techniques de diagonalisation par sous-espaces comme SQD.
Pour plus d'informations sur la fonction diagonalize_fermionic_hamiltonian, consulte la documentation de l'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