Ejemplos y aplicaciones
Durante esta lección, exploraremos algunos ejemplos de algoritmos variacionales y cómo aplicarlos:
- Cómo escribir un algoritmo variacional personalizado
- Cómo aplicar un algoritmo variacional para encontrar valores propios mínimos
- Cómo utilizar algoritmos variacionales para resolver casos de uso en aplicaciones
Ten en cuenta que el marco de patrones de Qiskit puede aplicarse a todos los problemas que presentamos aquí. Sin embargo, para evitar repetición, solo mencionaremos explícitamente los pasos del marco en un caso de ejemplo, ejecutado en hardware real.
Definiciones de problemas
Imagina que queremos usar un algoritmo variacional para encontrar el valor propio del siguiente observable:
Este observable tiene los siguientes valores propios:
Y los siguientes estados propios:
# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-ibm-runtime rustworkx scipy
from qiskit.quantum_info import SparsePauliOp
observable_1 = SparsePauliOp.from_list([("II", 2), ("XX", -2), ("YY", 3), ("ZZ", -3)])
VQE personalizado
Primero exploraremos cómo construir una instancia de VQE manualmente para encontrar el valor propio más bajo de . Esto incorporará una variedad de técnicas que hemos visto a lo largo de este curso.
def cost_func_vqe(params, ansatz, hamiltonian, estimator):
"""Return estimate of energy from estimator
Parameters:
params (ndarray): Array of ansatz parameters
ansatz (QuantumCircuit): Parameterized ansatz circuit
hamiltonian (SparsePauliOp): Operator representation of Hamiltonian
estimator (Estimator): Estimator primitive instance
Returns:
float: Energy estimate
"""
pub = (ansatz, hamiltonian, params)
cost = estimator.run([pub]).result()[0].data.evs
return cost
from qiskit.circuit.library.n_local import n_local
from qiskit import QuantumCircuit
import numpy as np
reference_circuit = QuantumCircuit(2)
reference_circuit.x(0)
variational_form = n_local(
num_qubits=2,
rotation_blocks=["rz", "ry"],
entanglement_blocks="cx",
entanglement="linear",
reps=1,
)
raw_ansatz = reference_circuit.compose(variational_form)
raw_ansatz.decompose().draw("mpl")

Comenzaremos depurando en simuladores locales.
from qiskit.primitives import StatevectorEstimator as Estimator
from qiskit.primitives import StatevectorSampler as Sampler
estimator = Estimator()
sampler = Sampler()
Ahora establecemos un conjunto inicial de parámetros:
x0 = np.ones(raw_ansatz.num_parameters)
print(x0)
[1. 1. 1. 1. 1. 1. 1. 1.]
Podemos minimizar esta función de costo para calcular los parámetros óptimos
# SciPy minimizer routine
from scipy.optimize import minimize
import time
start_time = time.time()
result = minimize(
cost_func_vqe,
x0,
args=(raw_ansatz, observable_1, estimator),
method="COBYLA",
options={"maxiter": 1000, "disp": True},
)
end_time = time.time()
execution_time = end_time - start_time
Return from COBYLA because the trust region radius reaches its lower bound.
Number of function values = 103 Least value of F = -5.999999998357189
The corresponding X is:
[2.27483579e+00 8.37593091e-01 1.57080508e+00 5.82932911e-06
2.49973063e+00 6.41884255e-01 6.33686904e-01 6.33688223e-01]
result
message: Return from COBYLA because the trust region radius reaches its lower bound.
success: True
status: 0
fun: -5.999999998357189
x: [ 2.275e+00 8.376e-01 1.571e+00 5.829e-06 2.500e+00
6.419e-01 6.337e-01 6.337e-01]
nfev: 103
maxcv: 0.0
Como este problema de ejemplo solo utiliza dos qubits, podemos verificarlo usando el solucionador de álgebra lineal de NumPy.
from numpy.linalg import eigvalsh
solution_eigenvalue = min(eigvalsh(observable_1.to_matrix()))
print(f"""Number of iterations: {result.nfev}""")
print(f"""Time (s): {execution_time}""")
print(
f"Percent error: {100*abs((result.fun - solution_eigenvalue)/solution_eigenvalue):.2e}"
)
Number of iterations: 103
Time (s): 0.4394676685333252
Percent error: 2.74e-08
Como puedes ver, el resultado es extremadamente cercano al ideal.
Experimentando para mejorar la velocidad y la precisión
Agregar estado de referencia
En el ejemplo anterior no hemos utilizado ningún operador de referencia . Ahora pensemos en cómo puede obtenerse el estado propio ideal . Considera el siguiente circuito.
from qiskit import QuantumCircuit
ideal_qc = QuantumCircuit(2)
ideal_qc.h(0)
ideal_qc.cx(0, 1)
ideal_qc.draw("mpl")
Podemos verificar rápidamente que este circuito nos da el estado deseado.
from qiskit.quantum_info import Statevector
Statevector(ideal_qc)
Statevector([0.70710678+0.j, 0. +0.j, 0. +0.j,
0.70710678+0.j],
dims=(2, 2))
Ahora que hemos visto cómo luce un circuito que prepara el estado solución, parece razonable usar una puerta Hadamard como circuito de referencia, de modo que el ansatz completo sea:
reference = QuantumCircuit(2)
reference.h(0)
reference.cx(0, 1)
# Include barrier to separate reference from variational form
reference.barrier()
ref_ansatz = variational_form.decompose().compose(reference, front=True)
ref_ansatz.draw("mpl")

Para este nuevo circuito, la solución ideal podría alcanzarse con todos los parámetros en , lo que confirma que la elección del circuito de referencia es razonable.
Ahora comparemos el número de evaluaciones de la función de costo, las iteraciones del optimizador y el tiempo empleado con los del intento anterior.
import time
start_time = time.time()
ref_result = minimize(
cost_func_vqe, x0, args=(ref_ansatz, observable_1, estimator), method="COBYLA"
)
end_time = time.time()
execution_time = end_time - start_time
Usando nuestros parámetros óptimos para calcular el valor propio mínimo:
experimental_min_eigenvalue_ref = cost_func_vqe(
ref_result.x, ref_ansatz, observable_1, estimator
)
print(experimental_min_eigenvalue_ref)
-5.999999996759607
print("ADDED REFERENCE STATE:")
print(f"""Number of iterations: {ref_result.nfev}""")
print(f"""Time (s): {execution_time}""")
print(
f"Percent error: {100*abs((experimental_min_eigenvalue_ref - solution_eigenvalue)/solution_eigenvalue):.2e}"
)
ADDED REFERENCE STATE:
Number of iterations: 127
Time (s): 0.5620882511138916
Percent error: 5.40e-08
Dependiendo de tu sistema específico, esto puede o no resultar en una mejora de velocidad o precisión en este ejemplo de escala muy pequeña. Lo importante es que partir de estados de referencia motivados físicamente se vuelve cada vez más relevante para mejorar la velocidad y la precisión a medida que los problemas escalan.
Cambiar el punto inicial
Ahora que hemos visto el efecto de agregar el estado de referencia, veamos qué ocurre cuando elegimos distintos puntos iniciales . En particular, usaremos y .
Recuerda que, como se discutió cuando se introdujo el estado de referencia, la solución ideal se encontraría cuando todos los parámetros sean , por lo que el primer punto inicial debería dar menos evaluaciones.
import time
start_time = time.time()
x0 = [0, 0, 0, 0, 6, 0, 0, 0]
x0_1_result = minimize(
cost_func_vqe, x0, args=(raw_ansatz, observable_1, estimator), method="COBYLA"
)
end_time = time.time()
execution_time = end_time - start_time
print("INITIAL POINT 1:")
print(f"""Number of iterations: {x0_1_result.nfev}""")
print(f"""Time (s): {execution_time}""")
INITIAL POINT 1:
Number of iterations: 108
Time (s): 0.4492197036743164
Ajustando el punto inicial a :
import time
start_time = time.time()
x0 = 6 * np.ones(raw_ansatz.num_parameters)
x0_2_result = minimize(
cost_func_vqe, x0, args=(raw_ansatz, observable_1, estimator), method="COBYLA"
)
end_time = time.time()
execution_time = end_time - start_time
print("INITIAL POINT 2:")
print(f"""Number of iterations: {x0_2_result.nfev}""")
print(f"""Time (s): {execution_time}""")
INITIAL POINT 2:
Number of iterations: 107
Time (s): 0.40889453887939453
Al experimentar con distintos puntos iniciales, es posible que logres una convergencia más rápida y con menos evaluaciones de la función.
Experimentar con distintos optimizadores
Podemos ajustar el optimizador usando el argumento method de minimize de SciPy, con más opciones disponibles aquí. Originalmente usamos un minimizador con restricciones (COBYLA). En este ejemplo, exploraremos el uso de un minimizador sin restricciones (BFGS) en su lugar.
import time
start_time = time.time()
result = minimize(
cost_func_vqe, x0, args=(raw_ansatz, observable_1, estimator), method="BFGS"
)
end_time = time.time()
execution_time = end_time - start_time
print("CHANGED TO BFGS OPTIMIZER:")
print(f"""Number of iterations: {result.nfev}""")
print(f"""Time (s): {execution_time}""")
CHANGED TO BFGS OPTIMIZER:
Number of iterations: 117
Time (s): 0.31656408309936523
Ejemplo de VQD
Aquí implementamos el marco de patrones de Qiskit de forma explícita.
Paso 1: Mapear las entradas clásicas a un problema cuántico
En lugar de buscar solo el autovalor más bajo de nuestros observables, buscaremos los (donde ).
Recuerda que las funciones de costo de VQD son:
Esto es particularmente importante porque un vector (en este caso ) debe pasarse como argumento al definir el objeto VQD.
Además, en la implementación de VQD de Qiskit, en lugar de considerar los observables efectivos descritos en el cuaderno anterior, las fidelidades se calculan directamente mediante el algoritmo ComputeUncompute, que se apoya en una primitiva Sampler para muestrear la probabilidad de obtener para el circuito
. Esto funciona precisamente porque dicha probabilidad es
ansatz = n_local(
num_qubits=2,
rotation_blocks=["ry", "rz"],
entanglement_blocks="cz",
# entanglement="linear",
reps=1,
)
ansatz.decompose().draw("mpl")

Comencemos examinando el siguiente observable:
Este observable tiene los siguientes autovalores:
Y los autoestados: