Alles zusammen mit Qiskit Runtime
Zusammenfassung
Victoria Lipinska gibt eine abschließende Zusammenfassung dessen, was wir bisher gelernt haben.
Referenzen
Die folgenden Artikel werden im obigen Video referenziert.
- Quantum Chemistry in the Age of Quantum Computing, Cao, et al.
- Quantum computational chemistry, McArdle, et al.
VQE mit Qiskit Patterns
Wir haben alle notwendigen Komponenten für eine VQE-Berechnung:
- Hamiltonian
- Ansatz
- Klassischer Optimierer
Jetzt müssen wir sie nur noch im Qiskit-Patterns-Framework zusammenführen.
Schritt 1: Klassische Eingaben auf ein Quantenproblem abbilden
Wie bereits erwähnt, gehen wir hier davon aus, dass ein geeignet formatierter Hamiltonian bereits generiert wurde. Bei Fragen dazu findest du in der Lektion zu Hamiltonians eine Anleitung. Der folgende Code-Block richtet die in früheren Lektionen erklärten Komponenten ein. Wir haben H2 als Modell gewählt, weil sein Hamiltonian kompakt genug ist, um ihn vollständig aufzuschreiben.
# 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
Wir wählen zunächst einen efficient_su2-Circuit und den COBYLA-Optimierer.
# 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
Jetzt erstellen wir unsere Kostenfunktion. Diese hängt offensichtlich mit dem Hamiltonian zusammen, unterscheidet sich jedoch insofern, als der Hamiltonian ein Operator ist und wir eine Funktion brauchen, die den Erwartungswert dieses Operators mit Estimator zurückgibt. Dazu verwendet sie natürlich den Ansatz und die variationellen Parameter, die daher alle als Argumente auftauchen. Unten definieren wir leicht unterschiedliche Versionen für den Einsatz auf echter Hardware oder Simulatoren.
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
Schritt 2: Problem für die Quantenausführung optimieren
Wir möchten, dass unser Code auf der verwendeten Hardware so effizient wie möglich läuft. Deshalb müssen wir zunächst ein Backend auswählen, um den Optimierungsschritt zu beginnen. Der folgende Code wählt das am wenigsten ausgelastete Backend aus, das dir zur Verfügung steht.
# 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
Die Optimierung des Circuits für den Betrieb auf einem echten Backend ist ein umfangreiches und wichtiges Thema, aber es ist nicht VQE-spezifisch. Wir erinnern hier kurz an zwei wichtige Begriffe:
- optimization_level: Dieser Wert beschreibt, wie gut der Circuit an das Layout des ausgewählten Backends angepasst ist. Die niedrigste Optimierungsstufe macht nur das Nötigste, um den Circuit auf dem Gerät zum Laufen zu bringen: Sie bildet die Circuit-Qubits auf die Gerät-Qubits ab und fügt Swap-Gates hinzu, um alle Zwei-Qubit-Operationen zu ermöglichen. Die höchste Optimierungsstufe ist deutlich intelligenter und verwendet viele Tricks, um die Gesamtanzahl der Gates zu reduzieren. Da Mehr-Qubit-Gates hohe Fehlerraten haben und Qubits über die Zeit dekohärieren, sollten kürzere Circuits bessere Ergebnisse liefern.
- Dynamical Decoupling: Wir können eine Abfolge von Gates auf wartende Qubits anwenden. Dies hebt einige unerwünschte Wechselwirkungen mit der Umgebung auf.
Weitere Informationen zur Optimierung von Circuits findest du in der verlinkten Dokumentation. Der folgende Code generiert einen Pass-Manager mit vordefinierten Pass-Managern aus
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")
Wir müssen die Gerät-Layout-Eigenschaften entsprechend auf den Hamiltonian anwenden.
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])
Schritt 3: Mit Qiskit Primitives ausführen
Bevor wir auf der ausgewählten Hardware ausführen, empfiehlt es sich, einen Simulator zur grundlegenden Fehlersuche und manchmal für Fehlerabschätzungen zu verwenden. Aus diesen Gründen zeigen wir kurz, wie VQE auf einem Simulator ausgeführt wird. Es ist jedoch wichtig zu beachten, dass kein klassischer Computer, Simulator oder GPU die vollständige Funktionalität eines hochverschränkten 127-Qubit-Quantencomputers exakt simulieren kann. In der aktuellen Ära der Quantennützlichkeit werden Simulatoren nur begrenzt eingesetzt werden.
Denk daran, dass für jede Parameterwahl im variationellen Circuit ein Erwartungswert berechnet werden muss (da das der zu minimierende Wert ist). Wie du vielleicht schon vermutest, ist der effizienteste Weg dazu die Verwendung des Qiskit-Primitives Estimator. Wir beginnen mit einem lokalen Simulator, für den wir die lokale Version von Estimator namens BackendEstimator verwenden müssen.
Mit dem echten Backend, das wir zur Optimierung verwendet haben, können wir ein Modell des Rauschverhaltens dieses Geräts importieren und dann mit dem lokalen Simulator unserer Wahl verwenden. Hier verwenden wir den 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)
Jetzt ist es endlich Zeit, VQE zu implementieren und die Kostenfunktion mit dem ausgewählten Hamiltonian, Ansatz, klassischen Optimierer und unserem BackendEstimator zu minimieren, der auf dem echten Backend basiert, das wir für die weitere Verwendung ausgewählt haben. Beachte, dass wir hier eine relativ kleine Anzahl für die maximalen Iterationen gewählt haben. Das liegt daran, dass wir den Simulator lediglich zum Debugging verwenden. VQE-Optimierungsschritte benötigen oft Hunderte von Iterationen, um zu konvergieren.
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
Der Code wurde korrekt ausgeführt, hat aber nicht konvergiert – was wir erwartet haben. Wir fahren mit der Ausführung der Berechnung auf echter Hardware fort und besprechen anschließend die Ausgaben. Für echte Backends verwenden wir den Qiskit Runtime Estimator. Wir möchten dies innerhalb einer Qiskit Runtime Session ausführen und werden in der Regel Optionen für diese Session festlegen wollen.
from qiskit_ibm_runtime import QiskitRuntimeService, Session
from qiskit_ibm_runtime import EstimatorV2 as Estimator
from qiskit_ibm_runtime.options import EstimatorOptions
Die Verwendung einer Session bedeutet unter anderem, dass unser Job nur einmal in der Warteschlange wartet, nämlich zu Beginn. Nachfolgende Iterationen des klassischen Optimierers werden nicht in der Warteschlange eingereiht. In der Session können wir Resilienz- und Optimierungsstufen festlegen. Diese Werkzeuge sind wichtig genug, dass wir eine kurze Übersicht über jeden und seine Bedeutung für VQE einfügen, mit Links zum Weiterlesen:
- Runtime Sessions: VQE ist von Natur aus iterativ, wobei der klassische Optimierer bei jedem weiteren Versuch neue variationelle Parameter auswählt und damit neue Gates verwendet werden. Ohne Sessions könnte dies zu zusätzlichen Wartezeiten in der Warteschlange zwischen jedem Versuch führen. Wenn die VQE-Berechnung innerhalb einer Session gekapselt wird, gibt es nur eine initiale Warteschlange vor dem Start des Jobs, aber keine zusätzliche Wartezeit zwischen den variationellen Schritten. Diese Strategie wurde bereits im Beispiel der vorherigen Lektion verwendet, kann aber eine noch wichtigere Rolle spielen, wenn die Geometrie variiert wird. Mehr über Sessions erfährst du in der Dokumentation zu Ausführungsmodi.
- Estimators integrierte Optimierung: In Estimator gibt es integrierte Optionen zur Optimierung einer Berechnung. In vielen Kontexten (einschließlich Estimator) sind die Einstellungen auf 0 und 1 beschränkt, wobei 0 keine Optimierung bedeutet und 1 (der Standard) eine gewisse Optimierung deines Circuits auf die ausgewählte Hardware. Einige andere Kontexte erlauben Einstellungen von 0, 1, 2 oder 3. Weitere Informationen zu den spezifischen Methoden bei verschiedenen Einstellungen findest du in der Dokumentation. Hier setzen wir die Optimierung tatsächlich auf 0 und verwenden
skip\_transpilation = true, weil wir unseren Circuit bereits mit dem Pass-Manager im Optimierungsabschnitt transpiliert haben. - Estimators integrierte Resilienz: Wie bei der Optimierung hat Estimator integrierte Einstellungen für Resilienz gegenüber Fehlern, die verschiedenen Ansätzen zur Fehlerminderung entsprechen. Informationen zu Resilienzstufeneinstellungen findest du in der Dokumentation.
Es ist erwähnenswert, dass Fehlerminderung eine differenzierte Rolle bei der Konvergenz einer VQE-Berechnung spielt. Der klassische Optimierer sucht im Parameterraum nach den Parametern, die die Energie minimieren. Wenn du weit von den optimalen Parametern entfernt bist, kann ein steiler Gradient für den klassischen Optimierer auch in Gegenwart von Fehlern erkennbar sein. Aber wenn die Berechnung konvergiert und du dich den Optimalwerten näherst, wird der Gradient kleiner und wird leichter durch Fehler überdeckt. Wie viel Fehlerminderung möchtest du verwenden? An welchen Punkten der Konvergenz? Das sind Entscheidungen, die du für deinen jeweiligen Anwendungsfall treffen musst.
Für diesen ersten Hardware-Lauf haben wir die Resilienz auf 0 gesetzt, um einen relativ schnellen Lauf zu ermöglichen. Für jede ernsthafte Anwendung solltest du Fehlerminderung einsetzen. Beachte, dass die folgende Zelle zwei Sets von Optionen enthält: (1) Optionen für die Runtime Session, die wir „session_options" genannt haben, und (2) Optionen für den klassischen Optimierer, hier einfach „options" genannt.
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]
Du kannst den Fortschritt deines Jobs auf der IBM Quantum® Platform unter Workloads verfolgen.
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
Schritt 4: Nachbearbeiten und Ergebnis in klassischem Format zurückgeben
Lass uns einen Moment innehalten, um diese Ausgaben zu verstehen. Die Ausgabe „fun" ist der minimale Wert, den wir für die Kostenfunktion erhalten haben (nicht notwendigerweise der zuletzt berechnete Wert). Dies ist die Gesamtenergie einschließlich der positiven Kernabstoßung, weshalb wir auch electron_energy definiert haben.
Im obigen Fall erhalten wir eine Meldung, dass die maximale Anzahl von Funktionsauswertungen überschritten wurde, und dass die Anzahl der Funktionsauswertungen (nfev) 10 betrug. Das bedeutet einfach, dass andere Kriterien für die Konvergenz der Optimierung nicht erfüllt wurden; mit anderen Worten, es gibt keinen Grund zu glauben, dass wir die Grundzustandsenergie gefunden haben. Das ist auch die Bedeutung von success als „False".
Schließlich haben wir x. Das ist der Vektor der variationellen Parameter. Das sind die Parameter, die bei der Berechnung verwendet wurden und den minimalen Wert der Kostenfunktion (Energieerwartungswert) ergaben. Diese acht Werte entsprechen den acht Rotationswinkeln in denjenigen Gates im Ansatz, die variable Rotationswinkel akzeptieren.
Herzlichen Glückwunsch! Du hast eine VQE-Berechnung auf einem IBM Quantum QPU durchgeführt!
In der nächsten Lektion werden wir sehen, wie dieser Arbeitsablauf angepasst werden kann, um Variablen in deinem Hamiltonian einzubeziehen. Im Kontext quantenchemischer Probleme könnte das bedeuten, die Geometrie zu variieren, um Formen von Molekülen oder Bindungsstellen zu bestimmen.
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