Integrando todo con Qiskit Runtime
Resumen
Victoria Lipinska ofrece un resumen final de lo que hemos aprendido hasta ahora.
Referencias
Los siguientes artículos se mencionan en el video anterior.
- Quantum Chemistry in the Age of Quantum Computing, Cao, et al.
- Quantum computational chemistry, McArdle, et al.
VQE con Qiskit Patterns
Tenemos todos los componentes necesarios para un cálculo VQE:
- Hamiltoniano
- Ansatz
- Optimizador clásico
Ahora solo necesitamos integrarlos en el framework de Qiskit Patterns.
Paso 1: Mapear entradas clásicas a un problema cuántico
Como se menciónó anteriormente, aquí asumimos que ya se ha generado un hamiltoniano con el formato adecuado. Si tienes preguntas al respecto, en la lección sobre hamiltonianos encontrarás una guía. El siguiente bloque de código configura los componentes explicados en lecciones anteriores. Hemos elegido H2 como modelo porque su hamiltoniano es lo suficientemente compacto como para escribirlo completamente.
# 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
Elegimos primero un circuito efficient_su2 y el optimizador COBYLA.
# 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
Ahora creamos nuestra función de costo. Esta está obviamente relacionada con el hamiltoniano, pero difiere en que el hamiltoniano es un operador y necesitamos una función que devuelva el valor esperado de ese operador con Estimator. Para ello, naturalmente utiliza el ansatz y los parámetros variacionales, que por lo tanto aparecen todos como argumentos. A continuación definimos versiones ligeramente diferentes para su uso en hardware real o simuladores.
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
Paso 2: Optimizar el problema para la ejecución cuántica
Queremos que nuestro código se ejecute de la manera más eficiente posible en el hardware utilizado. Por eso, primero debemos seleccionar un backend para iniciar el paso de optimización. El siguiente código selecciona el backend menos ocupado disponible para ti.
# 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
La optimización del circuito para su operación en un backend real es un tema amplio e importante, pero no es específico de VQE. Recordamos aquí brevemente dos conceptos importantes:
- optimization_level: Este valor describe qué tan bien se adapta el circuito al layout del backend seleccionado. El nivel de optimización más bajo hace solo lo mínimo necesario para que el circuito funcione en el dispositivo: mapea los qubits del circuito a los qubits del dispositivo y añade puertas swap para permitir todas las operaciones de dos qubits. El nivel de optimización más alto es significativamente más inteligente y utiliza muchos trucos para reducir el número total de puertas. Dado que las puertas multi-qubit tienen altas tasas de error y los qubits decoherencian con el tiempo, los circuitos más cortos deberían producir mejores resultados.
- Dynamical Decoupling: Podemos aplicar una secuencia de puertas a qubits en espera. Esto cancela algunas interacciones no deseadas con el entorno.
Para más información sobre la optimización de circuitos, consulta la documentación enlazada. El siguiente código genera un pass manager con pass managers predefinidos 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")
Debemos aplicar las propiedades del layout del dispositivo de manera correspondiente al hamiltoniano.
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])
Paso 3: Ejecutar con Qiskit Primitives
Antes de ejecutar en el hardware seleccionado, es recomendable usar un simulador para la depuración básica y a veces para estimaciones de errores. Por estas razones, mostramos brevemente cómo ejecutar VQE en un simulador. Sin embargo, es importante tener en cuenta que ningún computador clásico, simulador o GPU puede simular exactamente la funcionalidad completa de un computador cuántico de 127 qubits altamente entrelazado. En la era actual de la utilidad cuántica, los simuladores se utilizarán de forma limitada.
Recuerda que para cada elección de parámetros en el circuito variacional, se debe calcular un valor esperado (ya que ese es el valor a minimizar). Como probablemente sospechas, la forma más eficiente de hacerlo es usando el Qiskit Primitive Estimator. Comenzamos con un simulador local, para el cual necesitamos usar la versión local de Estimator llamada BackendEstimator.
Con el backend real que usamos para la optimización, podemos importar un modelo del comportamiento de ruido de ese dispositivo y luego usarlo con el simulador local de nuestra elección. Aquí usamos el 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)
Ahora es finalmente el momento de implementar VQE y minimizar la función de costo con el hamiltoniano, ansatz, optimizador clásico y nuestro BackendEstimator seleccionados, que está basado en el backend real que elegimos para uso posterior. Ten en cuenta que aquí hemos elegido un número relativamente pequeño para las iteraciones máximas. Esto se debe a que solo estamos usando el simulador para depuración. Los pasos de optimización de VQE a menudo requieren cientos de iteraciones para 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
El código se ejecutó correctamente, pero no convergió — lo cual era de esperar. Procedemos con la ejecución del cálculo en hardware real y luego discutiremos las salidas. Para backends reales, usamos el Qiskit Runtime Estimator. Queremos ejecutar esto dentro de una sesión de Qiskit Runtime y generalmente querremos establecer opciones para esta sesión.
from qiskit_ibm_runtime import QiskitRuntimeService, Session
from qiskit_ibm_runtime import EstimatorV2 as Estimator
from qiskit_ibm_runtime.options import EstimatorOptions
El uso de una sesión significa, entre otras cosas, que nuestro job solo espera en la cola una vez, al principio. Las iteraciones subsiguientes del optimizador clásico no se ponen en cola. En la sesión podemos establecer niveles de resiliencia y optimización. Estas herramientas son lo suficientemente importantes como para incluir una breve descripción de cada una y su significado para VQE, con enlaces para mayor lectura:
- Sesiones de Runtime: VQE es inherentemente iterativo, donde el optimizador clásico selecciona nuevos parámetros variacionales en cada intento subsiguiente, utilizando así nuevas puertas. Sin sesiones, esto podría resultar en tiempos de espera adicionales en la cola entre cada intento. Cuando el cálculo VQE se encapsula dentro de una sesión, solo hay una espera inicial en la cola antes del inicio del job, pero no hay tiempo de espera adicional entre los pasos variacionales. Esta estrategia ya se utilizó en el ejemplo de la lección anterior, pero puede desempeñar un papel aún más importante cuando se varía la geometría. Aprende más sobre sesiones en la documentación sobre modos de ejecución.
- Optimización integrada de Estimator: En Estimator hay opciones integradas para optimizar un cálculo. En muchos contextos (incluido Estimator), las configuraciones están limitadas a 0 y 1, donde 0 significa sin optimización y 1 (el valor predeterminado) significa cierta optimización de tu circuito para el hardware seleccionado. Algunos otros contextos permiten configuraciones de 0, 1, 2 o 3. Para más información sobre los métodos específicos en diferentes configuraciones, consulta la documentación. Aquí de hecho establecemos la optimización en 0 y usamos
skip\_transpilation = true, porque ya transpilamos nuestro circuito con el pass manager en la sección de optimización. - Resiliencia integrada de Estimator: Al igual que con la optimización, Estimator tiene configuraciones integradas para la resiliencia frente a errores, que corresponden a diferentes enfoques de mitigación de errores. Para información sobre las configuraciones de nivel de resiliencia, consulta la documentación.
Vale la pena mencionar que la mitigación de errores desempeña un papel matizado en la convergencia de un cálculo VQE. El optimizador clásico busca en el espacio de parámetros los parámetros que minimizan la energía. Cuando estás lejos de los parámetros óptimos, un gradiente pronunciado puede ser reconocible para el optimizador clásico incluso en presencia de errores. Pero cuando el cálculo converge y te acercas a los valores óptimos, el gradiente se hace más pequeño y se oculta más fácilmente por los errores. ¿Cuánta mitigación de errores quieres usar? ¿En qué puntos de la convergencia? Esas son decisiones que debes tomar para tu caso de uso particular.
Para esta primera ejecución en hardware, hemos establecido la resiliencia en 0 para permitir una ejecución relativamente rápida. Para cualquier aplicación seria, debes emplear mitigación de errores. Ten en cuenta que la siguiente celda contiene dos conjuntos de opciones: (1) opciones para la sesión de Runtime, que hemos llamado "session_options", y (2) opciones para el optimizador clásico, aquí simplemente llamadas "options".
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]
Puedes seguir el progreso de tu job en la plataforma IBM Quantum® en 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
Paso 4: Posprocesar y devolver el resultado en formato clásico
Detengámonos un momento para entender estas salidas. La salida "fun" es el valor mínimo que obtuvimos para la función de costo (no necesariamente el último valor calculado). Esta es la energía total incluyendo la repulsión nuclear positiva, por eso también definimos electron_energy.
En el caso anterior obtenemos un mensaje de que se excedió el número máximo de evaluaciones de la función, y que el número de evaluaciones de la función (nfev) fue 10. Esto simplemente significa que otros criterios para la convergencia de la optimización no se cumplieron; en otras palabras, no hay razón para creer que hemos encontrado la energía del estado fundamental. Ese es también el significado de success como "False".
Finalmente tenemos x. Este es el vector de parámetros variacionales. Estos son los parámetros que se utilizaron en el cálculo y que produjeron el valor mínimo de la función de costo (valor esperado de la energía). Estos ocho valores corresponden a los ocho ángulos de rotación en aquellas puertas del ansatz que aceptan ángulos de rotación variables.
¡Felicidades! ¡Has realizado un cálculo VQE en una QPU de IBM Quantum!
En la próxima lección veremos cómo este flujo de trabajo puede adaptarse para incluir variables en tu hamiltoniano. En el contexto de problemas de química cuántica, esto podría significar variar la geometría para determinar formas de moléculas o sitios de enlace.
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