Integrando todo con IBM Quantum Compute Service
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 mencionó 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 — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"numpy": "numpy", "qiskit": "qiskit", "qiskit_aer": "qiskit-aer", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "scipy": "scipy"}
_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")
# 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 pierden coherencia 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 usando primitivos de IBM Quantum.
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, debe calcularse un valor esperado (puesto que es el valor que queremos minimizar). Como quizá ya hayas adivinado, la forma más eficiente de hacerlo es usar la primitiva Estimator del SDK de Qiskit. Comenzaremos usando un simulador local, que requerirá que usemos 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 Estimator, this time from Qiskit (we will import from IBM Quantum for real QPUs)
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
Este código se evaluó correctamente, aunque no convergió, lo cual esperábamos. Procederemos ejecutando el cálculo en hardware real, y luego discutiremos los outputs. Para backends reales, usaremos el Estimator IBM Quantum. Queremos ejecutar esto dentro de una sesión y generalmente querremos especificar opciones para esa 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:
-
Sessions: VQE es inherentemente iterativo: el optimizador clásico selecciona nuevos parámetros variacionales y, por tanto, usa puertas nuevas en cada intento subsiguiente. Sin usar sesiones, esto podría resultar en tiempo de espera en cola adicional entre cada intento del circuito. Encapsular el cálculo VQE en una sesión hace que solo haya una espera inicial en cola previa al inicio del job, pero sin tiempo de espera en cola adicional entre pasos variacionales. Esta estrategia ya se usó 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. Para saber más sobre sesiones, consulta la documentación de modos de ejecución.
-
Optimización integrada de Estimator: en Estimator hay opciones integradas para optimizar un cálculo. En muchos contextos (incluido Estimator), los ajustes se limitan a 0 y 1, donde 0 indica que no hay optimización y 1 (el valor predeterminado) aplica cierta optimización de tu circuito según el hardware seleccionado. Algunos otros contextos permiten ajustes de 0, 1, 2 o 3. Para más información sobre los métodos específicos usados en los distintos ajustes, consulta la documentación. Aquí, en realidad estableceremos la optimización en 0, y usaremos 'skip_transpilation = true', porque ya hemos transpilado nuestro circuito usando el pass manager anterior, en la sección de optimización.
-
Resiliencia integrada de Estimator: al igual que con la optimización, Estimator cuenta con ajustes integrados de resiliencia frente a errores, que corresponden a diferentes enfoques de mitigación de errores. Para obtener información sobre los ajustes 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 fijado el nivel de resiliencia en 0 para facilitar una ejecución relativamente rápida. Para cualquier aplicación seria, querrás usar mitigación de errores. Ten en cuenta que en la celda de abajo hay dos conjuntos de opciones: (1) opciones para la sesión, que hemos nombrado "session_options", y (2) opciones para el optimizador clásico, simplemente llamadas "options" aquí.
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