Tout assembler avec Qiskit Runtime
Résumé
Victoria Lipinska fait un récapitulatif final de ce que nous avons appris jusqu'à présent.
Références
Les articles suivants sont cités dans la vidéo ci-dessus.
- Quantum Chemistry in the Age of Quantum Computing, Cao, et al.
- Quantum computational chemistry, McArdle, et al.
VQE avec les patterns Qiskit
Nous disposons de tous les composants nécessaires à un calcul VQE :
- Hamiltonien
- Ansatz
- Optimiseur classique
Il ne nous reste plus qu'à les assembler dans le framework des patterns Qiskit.
Étape 1 : Mapper les entrées classiques vers un problème quantique
Comme indiqué précédemment, nous supposerons ici qu'un Hamiltonien d'intérêt correctement formaté a déjà été généré. Si tu as des questions à ce sujet, consulte la leçon sur les Hamiltoniens pour obtenir des conseils. Le bloc de code ci-dessous configure les composants expliqués dans les leçons précédentes. Nous avons choisi de modéliser H2 parce que son Hamiltonien est suffisamment compact pour être écrit en entier.
# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-aer qiskit-ibm-runtime scipy
# General imports
import numpy as np
from qiskit.quantum_info import SparsePauliOp
# Hamiltonian obtained from a previous lesson
H = SparsePauliOp(
[
"IIII",
"IIIZ",
"IZII",
"IIZI",
"ZIII",
"IZIZ",
"IIZZ",
"ZIIZ",
"IZZI",
"ZZII",
"ZIZI",
"YYYY",
"XXYY",
"YYXX",
"XXXX",
],
coeffs=[
-0.09820182 + 0.0j,
-0.1740751 + 0.0j,
-0.1740751 + 0.0j,
0.2242933 + 0.0j,
0.2242933 + 0.0j,
0.16891402 + 0.0j,
0.1210099 + 0.0j,
0.16631441 + 0.0j,
0.16631441 + 0.0j,
0.1210099 + 0.0j,
0.17504456 + 0.0j,
0.04530451 + 0.0j,
0.04530451 + 0.0j,
0.04530451 + 0.0j,
0.04530451 + 0.0j,
],
)
nuclear_repulsion = 0.7199689944489797
Nous sélectionnons un circuit efficient_su2 et l'optimiseur COBYLA pour commencer.
# Pre-defined ansatz circuit
from qiskit.circuit.library import efficient_su2
# SciPy minimizer routine
from scipy.optimize import minimize
# Plotting functions
# Random initial state and efficient_su2 ansatz
ansatz = efficient_su2(H.num_qubits, su2_gates=["rx"], entanglement="linear", reps=1)
x0 = 2 * np.pi * np.random.random(ansatz.num_parameters)
print(ansatz.decompose().depth())
ansatz.decompose().draw("mpl")
5
Nous construisons maintenant notre fonction de coût. Celle-ci est bien sûr liée au Hamiltonien, mais elle s'en distingue en ce que le Hamiltonien est un opérateur, alors que nous voulons une fonction qui retourne la valeur d'espérance de cet opérateur, à l'aide d'Estimator. Bien entendu, elle y parvient en utilisant l'ansatz et les paramètres variationnels, qui apparaissent tous comme arguments. Ci-dessous, nous définissons des versions légèrement différentes pour une utilisation sur du vrai matériel ou des simulateurs.
def cost_func(params, ansatz, H, estimator):
pub = (ansatz, [H], [params])
result = estimator.run(pubs=[pub]).result()
energy = result[0].data.evs[0]
return energy
# def cost_func_sim(params, ansatz, H, estimator):
# energy = estimator.run(ansatz, H, parameter_values=params).result().values[0]
# return energy
Étape 2 : Optimiser le problème pour l'exécution quantique.
Nous voulons que notre code s'exécute aussi efficacement que possible sur le matériel utilisé. Nous devons donc sélectionner un backend pour commencer l'étape d'optimisation. Le code ci-dessous sélectionne le backend le moins occupé disponible pour toi.
# To run on hardware, select the backend with the fewest number of jobs in the queue
from qiskit_ibm_runtime import QiskitRuntimeService
service = QiskitRuntimeService(channel="ibm_quantum_platform")
backend = service.least_busy(operational=True, simulator=False)
backend.name
Optimiser le circuit pour l'exécution sur un vrai backend est un sujet riche et fondamental. Mais ce n'est pas spécifique à VQE. Pour l'instant, rappelons simplement deux termes importants :
- optimization_level : Cela décrit à quel point le circuit est adapté à la topologie du backend sélectionné. Le niveau d'optimisation le plus bas fait uniquement le strict minimum pour permettre l'exécution du circuit sur l'appareil ; il mappe les qubits du circuit vers les qubits de l'appareil et ajoute des portes SWAP pour permettre toutes les opérations à deux qubits. Le niveau d'optimisation le plus élevé est bien plus intelligent et utilise de nombreuses astuces pour réduire le nombre total de portes. Comme les portes multi-qubits ont des taux d'erreur élevés et que les qubits se décohèrent avec le temps, les circuits plus courts devraient donner de meilleurs résultats.
- Dynamical Decoupling : On peut appliquer une séquence de portes aux qubits inactifs. Cela annule certaines interactions indésirables avec l'environnement.
Consulte la documentation liée pour plus d'informations sur l'optimisation des circuits. Le code ci-dessous génère un gestionnaire de passes en utilisant des pass managers prédéfinis issus de
qiskit.transpiler.
from qiskit.transpiler import PassManager
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.transpiler.passes import (
ALAPScheduleAnalysis,
PadDynamicalDecoupling,
ConstrainedReschedule,
)
from qiskit.circuit.library import XGate
target = backend.target
pm = generate_preset_pass_manager(target=target, optimization_level=3)
pm.scheduling = PassManager(
[
ALAPScheduleAnalysis(target=target),
ConstrainedReschedule(
acquire_alignment=target.acquire_alignment,
pulse_alignment=target.pulse_alignment,
target=target,
),
PadDynamicalDecoupling(
target=target,
dd_sequence=[XGate(), XGate()],
pulse_alignment=target.pulse_alignment,
),
]
)
# Use the pass manager and draw the resulting circuit
ansatz_isa = pm.run(ansatz)
ansatz_isa.draw(output="mpl", idle_wires=False, style="iqp")
Nous devons aussi appliquer les caractéristiques de topologie de l'appareil au Hamiltonien.
hamiltonian_isa = H.apply_layout(ansatz_isa.layout)
hamiltonian_isa
SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIZIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIZII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZIII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZZII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIZII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYYYII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIYYXXII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXYYII', 'IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIXXXXII'],
coeffs=[-0.09820182+0.j, -0.1740751 +0.j, -0.1740751 +0.j, 0.2242933 +0.j,
0.2242933 +0.j, 0.16891402+0.j, 0.1210099 +0.j, 0.16631441+0.j,
0.16631441+0.j, 0.1210099 +0.j, 0.17504456+0.j, 0.04530451+0.j,
0.04530451+0.j, 0.04530451+0.j, 0.04530451+0.j])
Étape 3 : Exécuter avec les Primitives Qiskit.
Avant d'exécuter sur le matériel sélectionné, il est judicieux d'utiliser un simulateur pour un débogage rapide, et parfois pour des estimations d'erreur. Pour ces raisons, nous montrons brièvement comment exécuter VQE sur un simulateur. Mais il est essentiel de noter qu'aucun ordinateur classique, simulateur ou GPU ne peut simuler avec précision la pleine fonctionnalité d'un ordinateur quantique à 127 qubits fortement intriqués. Dans l'ère actuelle de l'utilité quantique, les simulateurs auront une utilité limitée.
Rappelle-toi que pour chaque choix de paramètres dans le circuit variationnel, une valeur d'espérance doit être calculée (puisque c'est la valeur à minimiser). Comme tu l'as peut-être déjà deviné, la façon la plus efficace d'y parvenir est d'utiliser la primitive Qiskit, Estimator. Nous commencerons par utiliser un simulateur local, ce qui nécessitera d'utiliser la version locale d'Estimator appelée BackendEstimator.
En conservant le vrai backend que nous avons utilisé pour l'optimisation, nous pouvons importer un modèle du comportement de bruit de cet appareil pour l'utiliser ensuite avec le simulateur local de notre choix. Ici, nous utiliserons l'aer_simulator_statevector.
# We will start by using a local simulator
from qiskit_aer import AerSimulator
# Import an estimator, this time from qiskit (we will import from Runtime for real hardware)
from qiskit.primitives import BackendEstimatorV2
# generate a simulator that mimics the real quantum system
backend_sim = AerSimulator.from_backend(backend)
estimator = BackendEstimatorV2(backend=backend_sim)
Il est enfin temps d'implémenter VQE, en minimisant la fonction de coût à l'aide du Hamiltonien sélectionné, de l'ansatz, de l'optimiseur classique et de notre BackendEstimator, basé sur le vrai backend que nous avons sélectionné pour une utilisation ultérieure. Note qu'ici nous avons choisi un nombre relativement faible pour le nombre maximal d'itérations. C'est parce que nous utilisons simplement le simulateur pour déboguer. Les étapes d'optimisation VQE nécessitent souvent des centaines d'itérations pour converger.
res = minimize(
cost_func,
x0,
args=(ansatz_isa, hamiltonian_isa, estimator),
method="cobyla",
options={"maxiter": 10, "disp": True},
)
print(getattr(res, "fun") - nuclear_repulsion)
print(res)
Return from COBYLA because the objective function has been evaluated MAXFUN times.
Number of function values = 10 Least value of F = -0.11556938907226563
The corresponding X is:
[4.11796514 4.52126324 0.69570423 4.12781503 6.55507846 1.80713073
0.9645473 6.23812214]
-0.8355383835212453
message: Return from COBYLA because the objective function has been evaluated MAXFUN times.
success: False
status: 3
fun: -0.11556938907226563
x: [ 4.118e+00 4.521e+00 6.957e-01 4.128e+00 6.555e+00
1.807e+00 9.645e-01 6.238e+00]
nfev: 10
maxcv: 0.0
Ce code s'est exécuté correctement, bien qu'il n'ait pas convergé, ce à quoi nous nous attendions. Nous allons procéder à l'exécution du calcul sur du vrai matériel, puis discuter des résultats. Pour les vrais backends, nous utiliserons le Runtime Estimator de Qiskit. Nous voudrons exécuter cela à l'intérieur d'une session Qiskit Runtime et nous voudrons généralement spécifier des options pour cette session.
from qiskit_ibm_runtime import QiskitRuntimeService, Session
from qiskit_ibm_runtime import EstimatorV2 as Estimator
from qiskit_ibm_runtime.options import EstimatorOptions
Entre autres choses, l'utilisation d'une session signifie que notre job n'attendra dans la file d'attente qu'une seule fois, au démarrage. Les itérations suivantes de l'optimiseur classique ne seront pas mises en file d'attente. Dans la session, nous pouvons définir les niveaux de résilience et d'optimisation. Ces outils sont suffisamment importants pour que nous en donnions une brève présentation et expliquions leur importance dans VQE, avec des liens pour en savoir plus :
- Sessions Runtime : VQE est par nature itératif, l'optimiseur classique sélectionnant de nouveaux paramètres variationnels, et donc de nouvelles portes utilisées, à chaque essai suivant. Sans sessions, cela pourrait entraîner un temps d'attente supplémentaire en file d'attente entre chaque circuit d'essai. Encapsuler le calcul VQE dans une session ne génère qu'une seule file d'attente initiale avant le démarrage du job, sans temps d'attente supplémentaire entre les étapes variationnelles. Cette stratégie était déjà utilisée dans l'exemple de la leçon précédente, mais peut jouer un rôle encore plus important lors de la variation de géométrie. Pour en savoir plus sur les sessions, consulte la documentation sur les modes d'exécution.
- Optimisation intégrée d'Estimator : Dans Estimator, il existe des options intégrées pour optimiser un calcul. Dans de nombreux contextes (Estimator inclus), les paramètres sont limités à 0 et 1, 0 indiquant aucune optimisation, et 1 (la valeur par défaut) indiquant une certaine optimisation de ton circuit pour le matériel sélectionné. Certains autres contextes permettent des paramètres de 0, 1, 2 ou 3. Pour plus d'informations sur les méthodes spécifiques utilisées dans différents paramètres, consulte la documentation. Ici, nous allons en fait définir l'optimisation à 0 et utiliser 'skip_transpilation = true', car nous avons déjà transpilé notre circuit à l'aide du pass manager ci-dessus, dans la section d'optimisation.
- Résilience intégrée d'Estimator : Comme pour l'optimisation, Estimator dispose de paramètres intégrés pour la résilience aux erreurs, correspondant à différentes approches de mitigation des erreurs. Pour en savoir plus sur les paramètres de niveau de résilience, consulte la documentation.
Il convient de noter que la mitigation des erreurs joue un rôle nuancé dans la convergence d'un calcul VQE. L'optimiseur classique recherche dans l'espace des paramètres ceux qui minimisent l'énergie. Lorsque tu es très loin des paramètres optimaux, un gradient prononcé peut être apparent pour l'optimiseur classique même en présence d'erreurs. Mais à mesure que le calcul converge et que tu t'approches des valeurs optimales, le gradient devient plus faible et est plus facilement noyé par les erreurs. Quelle quantité de mitigation des erreurs veux-tu utiliser ? À quels moments de la convergence ? Ce sont des choix que tu dois faire pour ton cas d'usage particulier.
Pour ce premier passage sur le vrai matériel, nous avons fixé la résilience à 0 pour faciliter une exécution relativement rapide. Pour toute application sérieuse, tu voudras utiliser la mitigation des erreurs. Note que dans la cellule ci-dessous, il y a deux ensembles d'options : (1) les options pour la session Runtime, que nous avons nommées "session_options", et (2) les options pour l'optimiseur classique, simplement appelées "options" ici.
estimator_options = EstimatorOptions(resilience_level=0, default_shots=2000)
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
res = minimize(
cost_func,
x0,
args=(ansatz_isa, hamiltonian_isa, estimator),
method="cobyla",
options={"maxiter": 10, "disp": True},
)
Return from COBYLA because the objective function has been evaluated MAXFUN times.
Number of function values = 10 Least value of F = -0.11691688904
The corresponding X is:
[5.11796514 5.52126324 0.69570423 5.12781503 6.55507846 1.80713073
1.9645473 6.23812214]
Tu peux suivre la progression de ton job sur IBM Quantum® Platform sous Workloads.
print(getattr(res, "fun") - nuclear_repulsion)
print(res)
-0.8368858834889796
message: Return from COBYLA because the objective function has been evaluated MAXFUN times.
success: False
status: 3
fun: -0.11691688904
x: [ 5.118e+00 5.521e+00 6.957e-01 5.128e+00 6.555e+00
1.807e+00 1.965e+00 6.238e+00]
nfev: 10
maxcv: 0.0
Étape 4 : Post-traitement, retourner le résultat en format classique.
Prenons un moment pour nous assurer que nous comprenons ces résultats. La sortie "fun" est la valeur minimale que nous avons obtenue pour la fonction de coût (pas nécessairement la dernière valeur calculée). Il s'agit de l'énergie totale, y compris la répulsion nucléaire positive, c'est pourquoi nous avons aussi défini electron_energy.
Dans le cas ci-dessus, nous avons un message indiquant que le nombre maximal d'évaluations de la fonction a été dépassé, et que le nombre d'évaluations de la fonction (nfev) était de 10. Cela signifie simplement que les autres critères de convergence de l'optimisation n'ont pas été satisfaits ; en d'autres termes, il n'y a aucune raison de penser que nous avons trouvé l'énergie de l'état fondamental. C'est aussi la signification de success étant "False".
Enfin, nous avons x. C'est le vecteur des paramètres variationnels. Ce sont les paramètres utilisés dans le calcul qui a donné la fonction de coût minimale (valeur d'espérance de l'énergie). Ces huit valeurs correspondent aux huit angles de rotation dans les portes de l'ansatz qui prennent des angles de rotation variables.
Félicitations ! Tu as exécuté un calcul VQE sur un QPU IBM Quantum !
Dans la prochaine leçon, nous verrons comment adapter ce flux de travail pour inclure des variables dans ton Hamiltonien. Dans le contexte des problèmes de chimie quantique, cela pourrait signifier faire varier la géométrie pour déterminer les formes des molécules ou des sites de liaison.
import qiskit
import qiskit_ibm_runtime
print(qiskit.version.get_version_info())
print(qiskit_ibm_runtime.version.get_version_info())
2.1.0
0.40.1