SQD pour l'estimation énergétique d'un hamiltonien chimique
Dans cette leçon, nous appliquerons la méthode SQD pour estimer l'énergie de l'état fondamental d'une molécule.
En particulier, nous aborderons les sujets suivants en utilisant l'approche du modèle Qiskit -step :
- Étape 1 : Tracer le problème à l'aide de circuits et d'opérateurs quantiques
- Établir l'hamiltonien moléculaire pour .
- Expliquer le cluster unitaire local de Jastrow (LUCJ) inspiré de la chimie et adapté au matériel [1]
- Étape 2 : Optimisation pour le matériel cible
- Optimiser le nombre de portes et la disposition de l'ansatz pour l'exécution matérielle
- Étape 3 : Exécution sur le matériel cible
- Exécuter le circuit optimisé sur une QPU réelle pour générer des échantillons du sous-espace.
- Étape 4 : Post-traitement des résultats
- Introduire la boucle de récupération de la configuration autoconsistante [2]
- Post-traitement de l'ensemble des échantillons de chaînes de bits, en utilisant la connaissance préalable du nombre de particules et de l'occupation orbitale moyenne calculée lors de l'itération la plus récente.
- Créer de manière probabiliste des lots de sous-échantillons à partir des chaînes de bits récupérées.
- Projeter et diagonaliser l'hamiltonien moléculaire sur chaque sous-espace échantillonné.
- Enregistrez l'énergie minimale de l'état fondamental trouvée dans tous les lots et mettez à jour l'occupation orbitale moyenne.
- Introduire la boucle de récupération de la configuration autoconsistante [2]
Nous utiliserons plusieurs logiciels tout au long de la leçon.
PySCFpour définir la molécule et configurer le hamiltonien.ffsimpour construire l'ansatz LUCJ.Qiskitpour transposer l'ansatz en vue d'une exécution matérielle.Qiskit IBM Runtimepour exécuter le circuit sur une QPU et collecter des échantillons.Qiskit addon SQDrécupération de la configuration et estimation de l'énergie de l'état fondamental à l'aide de la projection du sous-espace et de la diagonalisation de la matrice.
1. Mapper le problème sur des circuits quantiques et des opérateurs
Hamiltonien moléculaire
Un hamiltonien moléculaire prend la forme générique suivante
/ sont les opérateurs de création/annihilation fermioniques associés au -ème élément de base et au spin . et sont les intégrales électroniques à un et deux corps. En utilisant pySCF,, nous définirons la molécule et calculerons les intégrales à un et deux corps de l'hamiltonien pour l'ensemble de base 6-31g.
import warnings
import pyscf
import pyscf.cc
import pyscf.mcscf
warnings.filterwarnings("ignore")
# Specify molecule properties
open_shell = False
spin_sq = 0
# Build N2 molecule
mol = pyscf.gto.Mole()
mol.build(
atom=[["N", (0, 0, 0)], ["N", (1.0, 0, 0)]], # Two N atoms 1 angstrom apart
basis="6-31g",
symmetry="Dooh",
)
# Define active space
n_frozen = 2
active_space = range(n_frozen, mol.nao_nr())
# Get molecular integrals
scf = pyscf.scf.RHF(mol).run()
num_orbitals = len(active_space)
n_electrons = int(sum(scf.mo_occ[active_space]))
num_elec_a = (n_electrons + mol.spin) // 2
num_elec_b = (n_electrons - mol.spin) // 2
cas = pyscf.mcscf.CASCI(scf, num_orbitals, (num_elec_a, num_elec_b))
mo = cas.sort_mo(active_space, base=0)
hcore, nuclear_repulsion_energy = cas.get_h1cas(mo) # hcore: one-body integrals
eri = pyscf.ao2mo.restore(1, cas.get_h2cas(mo), num_orbitals) # eri: two-body integrals
# Compute exact energy for comparison
exact_energy = cas.run().e_totOutput:
converged SCF energy = -108.835236570774
CASCI E = -109.046671778080 E(CI) = -32.8155692383188 S^2 = 0.0000000
Dans cette leçon, nous utiliserons la transformation de Jordan-Wigner (JW) pour convertir une fonction d'onde fermionique en une fonction d'onde qubit afin qu'elle puisse être préparée à l'aide d'un circuit quantique. La transformation JW fait correspondre l'espace de Fock des fermions dans M orbitales spatiales à l'espace de Hilbert de 2M qubits, c'est-à-dire qu'une orbitale spatiale est divisée en deux orbitales de spin, l'une associée à un électron de spin haut ( ) et l'autre à un électron de spin bas ( ). Une orbitale de spin peut être occupée ou inoccupée. En général, lorsque nous parlons du nombre d'orbitales, nous utilisons le nombre d'orbitales spatiales. Le nombre d'orbitales de spin sera doublé. Dans les circuits quantiques, nous représentons chaque orbitale de spin par un qubit. Ainsi, un ensemble de qubits représentera des orbitales de spin-up ou - et un autre ensemble représentera des orbitales de spin-down ou -. Par exemple, la molécule pour l'ensemble de base 6-31g possède des orbitales spatiales (c'est-à-dire des orbitales de spin + = ). Nous aurons donc besoin d'un circuit quantique -qubit (nous pourrions avoir besoin de qubits ancillaires supplémentaires, comme nous le verrons plus loin). Les qubits sont mesurés dans une base de calcul pour générer des chaînes de bits, qui représentent des configurations électroniques ou des déterminants (de Slater). Tout au long de cette leçon, nous utiliserons indifféremment les termes chaînes de bits, configurations et déterminants. Les chaînes de bits indiquent l'occupation des électrons dans les orbitales de spin : une adresse dans une position de bit signifie que l'orbitale de spin correspondante est occupée, tandis qu'une adresse signifie que l'orbitale de spin est vide. Comme les problèmes de structure électronique préservent les particules, seul un nombre fixe d'orbitales de spin doit être occupé. La molécule possède des électrons ( ) et ( ). Ainsi, toute chaîne de bits représentant les orbitales et doit comporter cinq pour la molécule .
1.1 Circuit quantique pour la génération d'échantillons : l'approche LUCJ
Dans cette leçon, nous utiliserons l'anatz de Jastrow (LUCJ) \ [1] pour la préparation d'un état quantique et l'échantillonnage subséquent. Tout d'abord, nous expliquerons les différents éléments constitutifs de l'ansatz UCJ complet et les approximations faites dans la version locale de celui-ci. Ensuite, en utilisant le paquetage ffsim, nous construirons l'ansatz LUCJ et l'optimiserons en utilisant le transpileur Qiskit pour l'exécution matérielle.
L'ansatz UCJ a la forme suivante (pour un produit de couches ou répétitions de l'opérateur UCJ)
où est un état de référence, généralement l'état de Hartree-Fock (HF). L'état Hartree-Fock étant défini comme ayant les orbitales les plus basses occupées, la préparation de l'état HF consistera à appliquer des portes X pour mettre à un les qubits correspondant aux orbitales occupées. Par exemple, le bloc de préparation de l'état HF pour 4 orbitales spatiales et 2 spins ascendants et 2 spins descendants peut ressembler à ce qui suit :
Schéma 
Une seule répétition de l'opérateur UCJ consiste en une évolution de Coulomb diagonale ( ) entrecoupée de rotations orbitales ( et ).
Les blocs de rotation orbitale fonctionnent sur une seule espèce de spin ( (up-spin)/ (down-spin)). Pour chaque espèce d'électron, la rotation orbitale consiste en une couche de portes à qubit unique, suivie d'une séquence de portes de rotation de Given à deux qubits (portes ).
Les portes à 2 qubits agissent sur des spin-orbitaux adjacents (qubits les plus proches) et peuvent donc être mises en œuvre sur les QPU IBM® sans nécessiter de portes SWAP.
Schéma 
Le , également connu sous le nom d'opérateur de Coulomb diagonal, se compose de trois blocs. Deux d'entre eux travaillent sur les mêmes secteurs de rotation ( et ), et un travaille entre deux secteurs de rotation ( ).
Tous les blocs de sont constitués de portes numériques [1]. Une porte peut être décomposée en une porte suivie de deux portes à qubit unique agissant sur deux qubits distincts.
Les composants de même spin ( et ) ont des portes entre toutes les paires possibles de qubits. Cependant, comme les QPU supraconducteurs ont une connectivité restrictive, les qubits doivent être échangés pour réaliser des portes entre des qubits non adjacents.
Par exemple, considérons le bloc suivant (ou ) pour les orbitales spatiales . Pour une connectivité linéaire des qubits, les trois dernières portes ne sont pas directement réalisables car elles fonctionnent entre des qubits non adjacents (par exemple, Q0 et Q2 ne sont pas directement connectés). Nous avons donc besoin de portes SWAP pour les rendre adjacentes (la figure suivante montre un exemple avec portes SWAP).
Ensuite, le site met en œuvre des portes entre les mêmes orbitales indexées de différents secteurs de spin (par exemple, entre et ). De même, si les qubits ne sont pas physiquement adjacents sur une QPU, ces portes nécessiteront également des SWAP.
Il ressort de la discussion ci-dessus que l'ansatz UCJ se heurte à certains obstacles pour l'exécution matérielle, car il nécessite des portes SWAP en raison des interactions entre qubits non adjacents. La variante locale de l'ansatz UCJ, LUCJ, relève ce défi en supprimant certaines de l'opérateur de Coulomb diagonal.
Dans les mêmes blocs d'espèces d'électrons ( et ), nous ne conservons que les portes compatibles avec la connectivité du plus proche voisin et nous supprimons les portes entre qubits non adjacents dans la version LUCJ. La figure suivante montre le bloc LUCJ après l'élimination des portes non adjacentes.
Ensuite, la version LUCJ du bloc qui fonctionne entre différentes espèces d'électrons peut prendre différentes formes en fonction de la topologie du dispositif.
Ici aussi, la version LUCJ permet de se débarrasser des portes non compatibles. La figure ci-dessous montre des variantes du bloc pour différentes topologies de qubits : grille, hexagonal, lourd-hex et linéaire.
- Carré : nous pouvons avoir des portes entre toutes les orbitales et sans SWAP, et par conséquent, il n'est pas nécessaire de supprimer les portes .
- Heavy-hex : Les interactions - sont maintenues entre chaque -th indexed (such as the 0th, 4th, and 8th) spin orbitals and are ancilla mediated, that is, we need ancilla qubits between the linear chains representing and orbitals. Cet arrangement nécessite un nombre limité de SWAP.
- Hexagonale : Toutes les autres orbitales, telles que les orbitales indexées 0e, 2e et 4e, deviennent les plus proches voisines lorsque et sont disposées en deux chaînes linéaires adjacentes.
- Linéaire : Seules les orbites et sont connectées, ce qui signifie que le bloc n'aura qu'une seule porte.
Schémas de 
Si la suppression des portes de l'ansatz UCJ pour construire la version LUCJ le rend plus compatible avec le HW, l'ansatz perd une partie de son expressivité. Par conséquent, davantage de répétitions ( ) de l'opérateur UCJ modifié peuvent être nécessaires lors de l'utilisation de l'ansatz LUCJ.
1.2 Initialisation de l'approche LUCJ
La LUCJ est un ansatz paramétré, et nous devons initialiser les paramètres avant l'exécution matérielle. Une façon d'initialiser l'ansatz est d'utiliser les amplitudes t1 et t2 de la méthode classique des grappes couplées simples et doubles (CCSD), où les amplitudes t1 sont le coefficient des opérateurs d'excitation simple et les amplitudes t2 sont celles des opérateurs d'excitation double.
Notez que si l'initialisation de l'ansatz LUCJ avec t1 et t2 amplitudes génère des résultats satisfaisants, les paramètres de l'ansatz peuvent nécessiter une optimisation plus poussée.
# Get CCSD t2 amplitudes for initializing the ansatz
ccsd = pyscf.cc.CCSD(
scf, frozen=[i for i in range(mol.nao_nr()) if i not in active_space]
)
ccsd.run()
t1 = ccsd.t1
t2 = ccsd.t2Output:
E(CCSD) = -109.0398256929733 E_corr = -0.20458912219883
1.3 Construction de l'approche LUCJ à l'aide de ffsim
Nous utiliserons le paquetage ffsim pour créer et initialiser l'ansatz avec t1 et t2 amplitudes calculées ci-dessus. Notre molécule ayant un état Hartree-Fock à coquille fermée, nous utiliserons la variante à spin équilibré de l'ansatz UCJ, UCJOpSpinBalanced.
Comme le matériel IBM a une topologie en hexagone lourd, nous adopterons le modèle en zig-zag utilisé dans [1] et expliqué ci-dessus pour les interactions entre qubits. Dans ce schéma, les orbitales (qubits) ayant le même spin sont reliées par une topologie linéaire (cercles rouges et bleus). En raison de la topologie hexagonale lourde, les orbitales de différents spins ont des connexions entre toutes les 4 orbitales, c'est-à-dire la 0e, la 4e, la 8e et ainsi de suite (cercles violets).
import ffsim
from qiskit import QuantumCircuit, QuantumRegister
n_reps = 2
alpha_alpha_indices = [(p, p + 1) for p in range(num_orbitals - 1)]
alpha_beta_indices = [(p, p) for p in range(0, num_orbitals, 4)]
ucj_op = ffsim.UCJOpSpinBalanced.from_t_amplitudes(
t2=t2,
t1=t1,
n_reps=n_reps,
interaction_pairs=(alpha_alpha_indices, alpha_beta_indices),
)
nelec = (num_elec_a, num_elec_b)
# create an empty quantum circuit
qubits = QuantumRegister(2 * num_orbitals, name="q")
circuit = QuantumCircuit(qubits)
# prepare Hartree-Fock state as the reference state and append it to the quantum circuit
circuit.append(ffsim.qiskit.PrepareHartreeFockJW(num_orbitals, nelec), qubits)
# apply the UCJ operator to the reference state
circuit.append(ffsim.qiskit.UCJOpSpinBalancedJW(ucj_op), qubits)
circuit.measure_all()
# circuit.decompose().draw("mpl", scale=0.5, fold=-1)L'ansatz LUCJ avec des couches répétées peut être optimisé en fusionnant certains blocs adjacents. Prenons le cas de n_reps=2. Les deux blocs de rotation orbitale au milieu peuvent être fusionnés en un seul bloc de rotation orbitale. Le paquet ffsim dispose d'un gestionnaire de passage nommé ffsim.qiskit.PRE_INIT pour optimiser le circuit en fusionnant ces blocs adjacents.
2. Optimisation pour le matériel cible
Tout d'abord, nous recherchons un backend de notre choix. Nous optimiserons notre circuit pour le backend, puis nous exécuterons le circuit optimisé sur le même backend afin de générer des échantillons pour le sous-espace.
from qiskit_ibm_runtime import QiskitRuntimeService
service = QiskitRuntimeService()
# Use the least-busy backend or specify a quantum computer using the syntax commented out below.
backend = service.least_busy(operational=True, simulator=False)
# backend = service.backend("ibm_brisbane")Ensuite, nous recommandons les étapes suivantes pour optimiser l'ansatz et le rendre compatible avec le matériel.
- Sélectionner des qubits physiques (
initial_layout) dans le matériel cible qui respecte le schéma en zig-zag (deux chaînes linéaires avec un qubit ancilla entre les deux) décrit ci-dessus. La disposition des qubits selon ce schéma permet d'obtenir un circuit efficace compatible avec le matériel et comportant moins de portes. - Générez un gestionnaire de laissez-passer par étapes en utilisant la fonction
generate_preset_pass_managerde Qiskit avec votre choix debackendetinitial_layout. - Réglez l'étape
pre_initde votre gestionnaire de passes par étapes surffsim.qiskit.PRE_INIT.ffsim.qiskit.PRE_INITinclut des passes de transposition Qiskit qui décomposent les portes en rotations orbitales et fusionnent ensuite les rotations orbitales, ce qui permet de réduire le nombre de portes dans le circuit final. - Lancez le gestionnaire de passes sur votre circuit.
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
spin_a_layout = [0, 14, 18, 19, 20, 33, 39, 40, 41, 53, 60, 61, 62, 72, 81, 82]
spin_b_layout = [2, 3, 4, 15, 22, 23, 24, 34, 43, 44, 45, 54, 64, 65, 66, 73]
initial_layout = spin_a_layout + spin_b_layout
pass_manager = generate_preset_pass_manager(
optimization_level=3, backend=backend, initial_layout=initial_layout
)
# without PRE_INIT passes
isa_circuit = pass_manager.run(circuit)
print(f"Gate counts (w/o pre-init passes): {isa_circuit.count_ops()}")
# with PRE_INIT passes
# We will use the circuit generated by this pass manager for hardware execution
pass_manager.pre_init = ffsim.qiskit.PRE_INIT
isa_circuit = pass_manager.run(circuit)
print(f"Gate counts (w/ pre-init passes): {isa_circuit.count_ops()}")Output:
Gate counts (w/o pre-init passes): OrderedDict({'rz': 7579, 'sx': 6106, 'ecr': 2316, 'x': 336, 'measure': 32, 'barrier': 1})
Gate counts (w/ pre-init passes): OrderedDict({'rz': 4088, 'sx': 3125, 'ecr': 1262, 'x': 201, 'measure': 32, 'barrier': 1})
3. Exécuter sur le matériel cible
Après avoir optimisé le circuit en vue de son exécution sur le matériel, nous sommes prêts à l'exécuter sur le matériel cible et à collecter des échantillons afin d'estimer l'énergie de l'état fondamental. Comme nous n'avons qu'un seul circuit, nous allons utiliser le et qiskit-ibm-runtimeMode d'exécution des tâches exécuter notre circuit.
from qiskit_ibm_runtime import SamplerV2 as Sampler
sampler = Sampler(mode=backend)
sampler.options.dynamical_decoupling.enable = True
job = sampler.run([isa_circuit], shots=10_000) # Takes approximately 5sec of QPU time# Run cell after IQX job completion
primitive_result = job.result()
pub_result = primitive_result[0]
counts = pub_result.data.meas.get_counts()4. Résultats post-traitement
La partie post-traitement du flux de travail SQD peut être résumée à l'aide du diagramme suivant.
L'échantillonnage de l'ansatz LUCJ dans la base de calcul génère un ensemble de configurations bruitées , qui sont utilisées dans la routine de post-traitement. Il s'agit d'une méthode appelée (détails discutés plus loin) récupération de configuration pour corriger de manière probabiliste les configurations avec des nombres d'électrons incorrects. Les configurations ne comportant que des numéros d'électrons corrects ( ) sont ensuite sous-échantillonnées et réparties en plusieurs lots en fonction de la fréquence d'apparition de chaque configuration unique. Chaque lot d'échantillons définit un sous-espace ( ). Ensuite, l'hamiltonien moléculaire, , est projeté sur des sous-espaces :
Chaque hamiltonien projeté est ensuite introduit dans un résolveur d'équations, où il est diagonalisé pour calculer les valeurs propres et les vecteurs propres afin de reconstruire un état propre. Dans cette leçon, nous projetons et diagonalisons le hamiltonien à l'aide du paquetage qiskit-addon-sqd qui utilise la méthode de Davidson de PySCF pour la diagonalisation.
Nous recueillons ensuite la valeur propre la plus faible (énergie) des lots et calculons également l'occupation orbitale moyenne, . Les informations relatives à l'occupation moyenne sont utilisées dans l'étape de récupération de la configuration pour corriger de manière probabiliste les configurations bruitées.
Ensuite, nous expliquons en détail la boucle de récupération de la configuration autoconsistante et montrons des exemples de code concrets pour mettre en œuvre les étapes susmentionnées afin d'estimer l'énergie de l'état fondamental de l'hamiltonien .
4.1 Récupération de la configuration : aperçu général
Chaque bit d'une chaîne de bits (déterminant de Slater) représente une orbitale de spin. La moitié droite d'une chaîne de bits représente les orbitales de spin supérieur et la moitié gauche les orbitales de spin inférieur. Un 1 signifie que l'orbitale est occupée par un électron, et un 0 signifie que l'orbitale est vide. Nous connaissons a priori le nombre exact de particules (l'électron à spin ascendant et l'électron à spin descendant). Supposons que nous ayons un déterminant avec électrons (c'est-à-dire qu'il y a nombres de s dans la chaîne de bits). Le nombre correct de particules est . Si , nous savons que la chaîne de bits est corrompue par le bruit. La routine de configuration autoconsistante tente de corriger la chaîne de bits en inversant de manière probabiliste les bits en s'appuyant sur les informations relatives à l'occupation moyenne de l'orbite. L'occupation orbitale moyenne ( ) indique la probabilité qu'une orbitale soit occupée par un électron. Si , nous avons moins d'électrons et devons inverser certains s en s et vice versa.
La probabilité de retournement peut être pour i-th spin orbital. Dans [2], les auteurs ont utilisé une probabilité pondérée de retournement en utilisant la fonction ReLU modifiée.
Ici, définit l'emplacement du "coin" de la fonction ReLU, et le paramètre définit la valeur de la fonction ReLU au niveau du coin. Pour , devient la vraie fonction ReLU, et pour , elle devient ReLU* modifiée*. Dans l'article, les auteurs ont utilisé et nombre de particules alpha (ou bêta) / nombre d'orbitales de spin alpha (ou bêta) (facteur de remplissage).
L'occupation orbitale moyenne ( ) n'est pas connue a priori. La première itération de l'estimation de l'état fondamental commence par des configurations ne comportant que des nombres corrects de particules dans les deux espèces de spin. Après la première itération, nous disposons d'une estimation de l'état fondamental et, à l'aide de cette estimation, nous pouvons construire la première estimation de . Cette estimation de est utilisée pour récupérer des configurations, exécuter l'itération suivante de l'estimation de l'état fondamental et affiner de manière autoconsistante l'estimation de . Le processus se répète jusqu'à ce qu'un critère d'arrêt soit rempli.
Prenons l'exemple suivant pour et ( ). Nous devons faire passer l'un des 0s à 1 pour le corriger en fonction des nombres de particules, et les choix sont 1100, 1010, et 1001. En fonction de la probabilité de renversement, l'un des choix sera sélectionné comme configuration récupérée (ou la chaîne de bits avec le nombre correct de particules).
Supposons qu'au cours de la première itération, nous exécutions deux lots, et que les états de base estimés à partir de ces lots soient les suivants :
En utilisant les états de base de calcul et leurs amplitudes, nous pouvons calculer la probabilité d'occupation des électrons (en bref, les occupations ) par spin-orbite (qubit) (notez que probabilité = |amplitude| ). Nous présentons ci-dessous les occupations par qubit pour chaque chaîne de bits apparaissant dans l'état fondamental estimé et nous calculons l'occupation orbitale totale pour un lot. Notez que, conformément à la convention d'ordonnancement de Qiskit, le bit le plus à droite représente qubit-0 ( Q0 ), et le bit le plus à gauche représente Q3.
Occupation ( Batch0 ) :
Chronique « 1 » | Q3 | Q2 | Q1 | Q0 |
|---|---|---|---|---|
| 1001 | 0.64 | 0.0 | 0.0 | 0.64 |
| 0110 | 0.0 | 0.36 | 0.36 | 0.0 |
| n (Batch0) | 0.64 | 0.36 | 0.36 | 0.64 |
Occupation ( Batch1 )
Chronique « 1 » | Q3 | Q2 | Q1 | Q0 |
|---|---|---|---|---|
| 1001 | 0.33 | 0.00 | 0.00 | 0.33 |
| 0101 | 0.0 | 0.33 | 0.00 | 0.33 |
| 0110 | 0.0 | 0.33 | 0.33 | 0.00 |
| n (Batch1) | 0.33 | 0.66 | 0.33 | 0.66 |
Occupation (moyenne des lots)
Chronique « 1 » | Q3 | Q2 | Q1 | Q0 |
|---|---|---|---|---|
| n (Batch0) | 0.64 | 0.36 | 0.36 | 0.64 |
| n (Batch1) | 0.33 | 0.66 | 0.33 | 0.66 |
| n (moyenne) | 0.49 | 0.51 | 0.35 | 0.65 |
En utilisant l'occupation moyenne des orbitales calculée ci-dessus, nous pouvons trouver les probabilités d'inversion pour les différentes orbitales de la configuration . Comme l'orbitale représentée par Q3 est déjà occupée et n'a pas besoin d'être retournée, nous fixons son p(flip) à . Pour les autres orbitales, qui sont inoccupées, la probabilité de retournement est de chacune. Avec p(flip), nous calculons également le poids de probabilité associé au flip à l'aide de la fonction ReLU modifiée décrite ci-dessus.
Probabilité de retournement ( , , )
Chronique « 1 » | Q3 | Q2 | Q1 | Q0 |
|---|---|---|---|---|
| p(flip) ( ) | 0 | 0.51 | 0.35 | 0.65 |
| w(p(flip)) | 0 | 0.03 | 0.007 | 0.31 |
Enfin, en utilisant les probabilités pondérées ci-dessus, nous pouvons inverser l'une des orbitales inoccupées Q2, Q1 et Q0. Sur la base des valeurs ci-dessus, Q0 sera très probablement inversé, et une configuration récupérée possible peut être .
Le processus complet de récupération de la configuration autoconsistante peut être résumé comme suit :
Première itération : Supposons que les chaînes de bits (configurations ou déterminants de Slater) générées par l'ordinateur quantique forment un ensemble , qui comprend à la fois des configurations avec un nombre correct ( ) et incorrect ( ) de particules dans chaque secteur de spin.
- Les configurations de ( ) sont échantillonnées au hasard pour créer des lots de vecteurs pour la projection dans le sous-espace. Le nombre de lots et d'échantillons dans chaque lot sont des paramètres définis par l'utilisateur. Plus le nombre d'échantillons dans chaque lot est important, plus la dimension du sous-espace est grande et plus la diagonalisation est exigeante en termes de calcul. D'autre part, un nombre trop faible d'échantillons peut ne pas tenir compte des vecteurs de soutien de l'état fondamental et conduire à une estimation incorrecte.
- Exécuter le solveur d'états propres (c'est-à-dire la projection sur le sous-espace et la diagonalisation) sur les lots et obtenir des états propres approximatifs. .
- A partir des états propres approximatifs, construire la première estimation de .
Itérations ultérieures :
- En utilisant , corrigez les configurations dont le nombre de particules est erroné dans . Supposons que nous les nommions . Ensuite, forme le nouvel ensemble de configurations avec les bons numéros de particules.
- est échantillonné pour créer des lots .
- Le solveur d'états propres fonctionne avec de nouveaux lots et génère de nouvelles estimations des états fondamentaux .
- A partir des états propres approximatifs, construire une estimation affinée pour .
- Si le critère d'arrêt n'est pas respecté, retourner à l'étape
2.1.
4.2 Estimation de l'état fondamental
Tout d'abord, nous transformerons les comptages en une matrice de chaînes de bits et un tableau de probabilités pour le post-traitement.
Chaque ligne de la matrice représente une chaîne de bits unique. Comme les qubits sont indexés à partir de la droite d'une chaîne de bits dans Qiskit, la colonne 0 représente le qubit N-1, et la colonne N-1 représente le qubit 0, où N est le nombre de qubits.
Les orbitales alpha sont représentées dans la plage d'indices de colonne (N, N/2] (moitié droite), et les orbitales bêta sont représentées dans la plage de colonnes (N/2, 0] (moitié gauche).
from qiskit_addon_sqd.counts import counts_to_arrays
# Convert counts into bitstring and probability arrays
bitstring_matrix_full, probs_arr_full = counts_to_arrays(counts)Quelques options contrôlées par l'utilisateur sont importantes pour cette technique :
iterations: Nombre d'itérations de récupération de la configuration autoconsistanten_batches: Nombre de lots de configurations utilisés par les différents appels au solveur d'états propressamples_per_batch: Nombre de configurations uniques à inclure dans chaque lotmax_davidson_cycles: Nombre maximum de cycles de Davidson exécutés par chaque résolveur d'équations
import numpy as np
from qiskit_addon_sqd.configuration_recovery import recover_configurations
from qiskit_addon_sqd.fermion import (
bitstring_matrix_to_ci_strs,
solve_fermion,
)
from qiskit_addon_sqd.subsampling import postselect_and_subsample
rng = np.random.default_rng(24)
# SQD options
iterations = 5
# Eigenstate solver options
n_batches = 5
samples_per_batch = 500
max_davidson_cycles = 300
# Self-consistent configuration recovery loop
e_hist = np.zeros((iterations, n_batches)) # energy history
s_hist = np.zeros((iterations, n_batches)) # spin history
occupancy_hist = []
avg_occupancy = None
for i in range(iterations):
print(f"Starting configuration recovery iteration {i}")
# On the first iteration, we have no orbital occupancy information from the
# solver, so we begin with the full set of noisy configurations.
if avg_occupancy is None:
bs_mat_tmp = bitstring_matrix_full
probs_arr_tmp = probs_arr_full
# If we have average orbital occupancy information, we use it to refine
# the full set of noisy configurations.
else:
bs_mat_tmp, probs_arr_tmp = recover_configurations(
bitstring_matrix_full,
probs_arr_full,
avg_occupancy,
num_elec_a,
num_elec_b,
rand_seed=rng,
)
# Create batches of subsamples. We postselect here to remove configurations
# with incorrect hamming weight during iteration 0, since no config recovery was performed.
batches = postselect_and_subsample(
bs_mat_tmp,
probs_arr_tmp,
hamming_right=num_elec_a,
hamming_left=num_elec_b,
samples_per_batch=samples_per_batch,
num_batches=n_batches,
rand_seed=rng,
)
# Run eigenstate solvers in a loop. This loop should be parallelized for larger problems.
e_tmp = np.zeros(n_batches)
s_tmp = np.zeros(n_batches)
occs_tmp = []
coeffs = []
for j in range(n_batches):
strs_a, strs_b = bitstring_matrix_to_ci_strs(batches[j])
print(f" Batch {j} subspace dimension: {len(strs_a) * len(strs_b)}")
energy_sci, coeffs_sci, avg_occs, spin = solve_fermion(
batches[j],
hcore,
eri,
open_shell=open_shell,
spin_sq=spin_sq,
max_cycle=max_davidson_cycles,
)
energy_sci += nuclear_repulsion_energy
e_tmp[j] = energy_sci
s_tmp[j] = spin
occs_tmp.append(avg_occs)
coeffs.append(coeffs_sci)
# Combine batch results
avg_occupancy = tuple(np.mean(occs_tmp, axis=0))
# Track optimization history
e_hist[i, :] = e_tmp
s_hist[i, :] = s_tmp
occupancy_hist.append(avg_occupancy)Output:
Starting configuration recovery iteration 0
Batch 0 subspace dimension: 21609
Batch 1 subspace dimension: 21609
Batch 2 subspace dimension: 21609
Batch 3 subspace dimension: 21609
Batch 4 subspace dimension: 21609
Starting configuration recovery iteration 1
Batch 0 subspace dimension: 609961
Batch 1 subspace dimension: 616225
Batch 2 subspace dimension: 627264
Batch 3 subspace dimension: 633616
Batch 4 subspace dimension: 624100
Starting configuration recovery iteration 2
Batch 0 subspace dimension: 564001
Batch 1 subspace dimension: 605284
Batch 2 subspace dimension: 582169
Batch 3 subspace dimension: 559504
Batch 4 subspace dimension: 591361
Starting configuration recovery iteration 3
Batch 0 subspace dimension: 550564
Batch 1 subspace dimension: 549081
Batch 2 subspace dimension: 531441
Batch 3 subspace dimension: 527076
Batch 4 subspace dimension: 531441
Starting configuration recovery iteration 4
Batch 0 subspace dimension: 544644
Batch 1 subspace dimension: 580644
Batch 2 subspace dimension: 527076
Batch 3 subspace dimension: 531441
Batch 4 subspace dimension: 537289
4.3 Discussion des résultats
Le premier graphique montre qu'après quelques itérations, nous estimons l'énergie de l'état fondamental à ~24 mH (la précision chimique est généralement acceptée comme étant de 1 kcal/mol 1.6 mH ). Le deuxième graphique montre l'occupation moyenne de chaque orbite spatiale après la dernière itération. Nous pouvons constater que les électrons de spin-up et de spin-down occupent les cinq premières orbitales avec une forte probabilité dans nos solutions.
Bien que l'énergie estimée de l'état fondamental soit convenable, elle ne se situe pas dans la limite de la précision chimique ( mH ). Cet écart peut être attribué à la petite dimension du sous-espace que nous avons utilisée ci-dessus pour la projection et la diagonalisation. Comme nous avons utilisé samples_per_batch=500, le sous-espace est couvert par le maximum de vecteurs , c'est-à-dire les vecteurs manquants du support de l'état fondamental. L'augmentation du paramètre samples_per_batch devrait améliorer la précision au détriment des ressources informatiques classiques et du temps d'exécution.
# Data for energies plot
x1 = range(iterations)
min_e = [np.min(e) for e in e_hist]
e_diff = [abs(e - exact_energy) for e in min_e]
yt1 = [1.0, 1e-1, 1e-2, 1e-3, 1e-4, 1e-5]
# Chemical accuracy (+/- 1 milli-Hartree)
chem_accuracy = 0.001
# Data for avg spatial orbital occupancy
y2 = occupancy_hist[-1][0] + occupancy_hist[-1][1]
x2 = range(len(y2))import matplotlib.pyplot as plt
fig, axs = plt.subplots(1, 2, figsize=(12, 6))
# Plot energies
axs[0].plot(x1, e_diff, label="energy error", marker="o")
axs[0].set_xticks(x1)
axs[0].set_xticklabels(x1)
axs[0].set_yticks(yt1)
axs[0].set_yticklabels(yt1)
axs[0].set_yscale("log")
axs[0].set_ylim(1e-6)
axs[0].axhline(
y=chem_accuracy, color="#BF5700", linestyle="--", label="chemical accuracy"
)
axs[0].set_title("Approximated Ground State Energy Error vs SQD Iterations")
axs[0].set_xlabel("Iteration Index", fontdict={"fontsize": 12})
axs[0].set_ylabel("Energy Error (Ha)", 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"Exact energy: {exact_energy:.5f} Ha")
print(f"SQD energy: {min_e[-1]:.5f} Ha")
print(f"Absolute error: {e_diff[-1]:.5f} Ha")
plt.tight_layout()
plt.show()Output:
Exact energy: -109.04667 Ha
SQD energy: -109.02234 Ha
Absolute error: 0.02434 Ha
Exercice pour le lecteur
Augmentez progressivement le paramètre samples_per_batch (par exemple, de à avec un pas de ; autorisé par la mémoire de votre ordinateur) et comparez les énergies estimées de l'état fondamental.
Références
[1] M. Motta et al, "Bridging physical intuition and hardware efficiency for correlated electronic states : the local unitary cluster Jastrow ansatz for electronic structure" (2023). Chimie Sci, 2023, 14, 11213.
[2] J. Robledo-Moreno et al, "La chimie au-delà des solutions exactes sur un superordinateur centré sur les quanta" (2024). arXiv:quant-ph/2405.05068.