Funciones de coste
En esta lección aprenderemos a evaluar una función de coste :
- En primer lugar, conoceremos las primitivas Qiskit Runtime
- Defina una función de coste . Se trata de una función específica del problema que define el objetivo que el optimizador debe minimizar (o maximizar)
- Definición de una estrategia de medición con las primitivas Qiskit Runtime para optimizar la velocidad frente a la precisión
Primitivos
Todos los sistemas físicos, ya sean clásicos o cuánticos, pueden existir en diferentes estados. Por ejemplo, un coche en una carretera puede tener una masa, posición, velocidad o aceleración determinadas que caracterizan su estado. Del mismo modo, los sistemas cuánticos también pueden tener diferentes configuraciones o estados, pero difieren de los sistemas clásicos en la forma de tratar las mediciones y la evolución de los estados. Esto da lugar a propiedades únicas como la superposición y el entrelazamiento, exclusivas de la mecánica cuántica. Al igual que podemos describir el estado de un coche mediante propiedades físicas como la velocidad o la aceleración, también podemos describir el estado de un sistema cuántico mediante observables, que son objetos matemáticos.
En mecánica cuántica, los estados se representan mediante vectores columna complejos normalizados, o kets ( ), y los observables son operadores lineales hermitianos ( ) que actúan sobre los kets. Un vector propio ( ) de un observable se conoce como estado propio. La medición de un observable para uno de sus estados propios ( ) nos dará como lectura el valor propio correspondiente ( ).
Si se pregunta cómo medir un sistema cuántico y qué puede medir, Qiskit ofrece dos sitios web Operaciones básicas y fundamentales o tipo de datos. Qiskit dispone de las primitivas Sampler y Estimator que sirven como bloques de construcción para construir fácilmente cargas de trabajo algorítmicas complejas. que pueden ayudarle:
Sampler: Dado un estado cuántico , esta primitiva obtiene la probabilidad de cada posible estado base computacional.Estimator: Dado un observable cuántico y un estado , esta primitiva calcula el valor esperado de .
La primitiva Sampler
La primitiva Sampler calcula la probabilidad de obtener cada posible estado a partir de la base computacional, dado un circuito cuántico que prepara el estado . Calcula
Donde es el número de qubits, y la representación entera de cualquier posible cadena binaria de salida (es decir, enteros de base ).
El sitio Qiskit Runtime Sampler ejecuta el circuito varias veces en un dispositivo cuántico, realizando mediciones en cada ejecución y reconstruyendo la distribución de probabilidad a partir de las cadenas de bits recuperadas. Cuantas más ejecuciones (o disparos ) realice, más precisos serán los resultados, pero esto requiere más tiempo y recursos cuánticos.
Sin embargo, dado que el número de posibles salidas crece exponencialmente con el número de qubits (es decir, ), el número de disparos tendrá que crecer exponencialmente también para capturar una distribución de probabilidad densa. Por lo tanto, Sampler sólo es eficiente para distribuciones de probabilidad dispersas; donde el estado objetivo debe ser expresable como una combinación lineal de los estados base computacionales, con el número de términos creciendo como máximo polinomialmente con el número de qubits:
El Sampler también puede configurarse para recuperar probabilidades de una subsección del circuito, que representa un subconjunto del total de estados posibles.
La primitiva Estimator
La primitiva Estimator calcula el valor de expectativa de un observable para un estado cuántico ; donde las probabilidades del observable pueden expresarse como , siendo los estados propios del observable . El valor de expectativa se define entonces como la media de todos los resultados posibles (es decir, los valores propios del observable) de una medición del estado , ponderada por las probabilidades correspondientes:
Sin embargo, calcular el valor de expectativa de un observable no siempre es posible, ya que a menudo no conocemos su base propia. El sitio Qiskit Runtime Estimator utiliza un proceso algebraico complejo para estimar el valor de expectativa en un dispositivo cuántico real descomponiendo el observable en una combinación de otros observables cuyas bases propias sí conocemos.
En términos más sencillos, Estimator descompone cualquier observable que no sabe cómo medir en observables más simples y medibles denominados Conjunto de matrices utilizadas habitualmente en informática cuántica para representar y manipular estados cuánticos, formado por la matriz identidad y las tres matrices de Pauli (X, Y y Z)..
Cualquier operador puede expresarse como una combinación de operadores de Pauli.
tal que
donde es el número de qubits, para (es decir, enteros de base ), y .
Después de realizar esta descomposición, Estimator deriva un nuevo circuito para cada observable (del circuito original), para diagonalizar efectivamente el observable Pauli en la base computacional y medirlo. Podemos medir fácilmente los observables de Pauli porque conocemos de antemano, lo que no ocurre generalmente con otros observables.
Para cada , el Estimator ejecuta el circuito correspondiente en un dispositivo cuántico varias veces, mide el estado de salida en la base computacional y calcula la probabilidad de obtener cada salida posible . A continuación, busca el valor propio de correspondiente a cada salida , multiplica por , y suma todos los resultados para obtener el valor esperado del observable para el estado dado .
Dado que calcular el valor de la expectativa de Paulis es poco práctico (es decir, crece exponencialmente), Estimator sólo puede ser eficiente cuando una gran cantidad de son cero (es decir, descomposición Pauli dispersa en lugar de densa ). Formalmente decimos que, para que este cálculo sea eficientemente resoluble, el número de términos distintos de cero tiene que crecer como mucho polinómicamente con el número de qubits :
El lector puede notar la suposición implícita de que la probabilidad El proceso de tomar varias medidas de una o varias cosas. también tiene que ser eficiente como se explica para Sampler, lo que significa que
Ejemplo guiado para calcular valores esperados
Supongamos el estado single-qubit , y observable
con el siguiente valor teórico esperado
Como no sabemos cómo medir este observable, no podemos calcular su valor de expectativa directamente, y tenemos que volver a expresarlo como . Se puede demostrar que se evalúa con el mismo resultado observando que , y .
Veamos cómo calcular directamente y . Dado que y no conmutan (es decir, no comparten la misma base propia), no pueden medirse simultáneamente, por lo que necesitamos los circuitos auxiliares:
from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
# The following code will work for any other initial single-qubit state and observable
original_circuit = QuantumCircuit(1)
original_circuit.h(0)
H = SparsePauliOp(["X", "Z"], [2, -1])
aux_circuits = []
for pauli in H.paulis:
aux_circ = original_circuit.copy()
aux_circ.barrier()
if str(pauli) == "X":
aux_circ.h(0)
elif str(pauli) == "Y":
aux_circ.sdg(0)
aux_circ.h(0)
else:
aux_circ.id(0)
aux_circ.measure_all()
aux_circuits.append(aux_circ)
original_circuit.draw("mpl")Output:
# Auxiliary circuit for X
aux_circuits[0].draw("mpl")Output:
# Auxiliary circuit for Z
aux_circuits[1].draw("mpl")Output:
Ahora podemos realizar el cálculo manualmente utilizando Sampler y comprobar los resultados en Estimator:
from qiskit.primitives import StatevectorSampler, StatevectorEstimator
from qiskit.result import QuasiDistribution
import numpy as np
## SAMPLER
shots = 10000
sampler = StatevectorSampler()
job = sampler.run(aux_circuits, shots=shots)
# Run the sampler job and step through results
expvals = []
for index, pauli in enumerate(H.paulis):
data_pub = job.result()[index].data
bitstrings = data_pub.meas.get_bitstrings()
counts = data_pub.meas.get_counts()
quasi_dist = QuasiDistribution(
{outcome: freq / shots for outcome, freq in counts.items()}
)
# Use the probabilities and known eigenvalues of Pauli operators to estimate
# the expectation value.
val = 0
if str(pauli) == "X":
val += -1 * quasi_dist.get(1, 0)
val += 1 * quasi_dist.get(0, 0)
if str(pauli) == "Y":
val += -1 * quasi_dist.get(1, 0)
val += 1 * quasi_dist.get(0, 0)
if str(pauli) == "Z":
val += 1 * quasi_dist.get(0, 0)
val += -1 * quasi_dist.get(1, 0)
expvals.append(val)
# Print expectation values
print("Sampler results:")
for pauli, expval in zip(H.paulis, expvals):
print(f" >> Expected value of {str(pauli)}: {expval:.5f}")
total_expval = np.sum(H.coeffs * expvals).real
print(f" >> Total expected value: {total_expval:.5f}")
# Use estimator for comparison
observables = [
*H.paulis,
H,
] # Note: run for individual Paulis as well as full observable H
estimator = StatevectorEstimator()
job = estimator.run([(original_circuit, observables)])
estimator_expvals = job.result()[0].data.evs
# Print results
print("Estimator results:")
for obs, expval in zip(observables, estimator_expvals):
if obs is not H:
print(f" >> Expected value of {str(obs)}: {expval:.5f}")
else:
print(f" >> Total expected value: {expval:.5f}")Output:
Sampler results:
>> Expected value of X: 1.00000
>> Expected value of Z: 0.00420
>> Total expected value: 1.99580
Estimator results:
>> Expected value of X: 1.00000
>> Expected value of Z: 0.00000
>> Total expected value: 2.00000
Rigor matemático (opcional)
Expresando con respecto a la base de estados propios de , , se deduce:
Dado que no conocemos los valores propios ni los estados propios del observable objetivo , primero tenemos que considerar su diagonalización. Dado que es Una hermitiana es una matriz cuadrada que es igual a su propia transpuesta conjugada, o un operador lineal que es autoadjunto., existe una transformación unitaria tal que donde es la matriz diagonal de valores propios, por lo que si , y .
Esto implica que el valor esperado puede reescribirse como:
Dado que si un sistema se encuentra en el estado la probabilidad de medir es , el valor esperado anterior puede expresarse como:
Es muy importante tener en cuenta que las probabilidades se toman del estado en lugar de . Por este motivo, la matriz es absolutamente necesaria.
Quizá se pregunte cómo obtener la matriz y los valores propios . Si ya tuvieras los valores propios, no habría necesidad de utilizar un ordenador cuántico, ya que el objetivo de los algoritmos variacionales es encontrar estos valores propios de .
Afortunadamente, existe una forma de evitarlo: cualquier matriz puede escribirse como una combinación lineal de productos tensoriales de matrices e identidades Pauli, todas ellas hermitianas y unitarias con y conocidas. Esto es lo que el Runtime Estimator hace internamente descomponiendo cualquier Operator en un objeto SparsePauliOp.
Éstos son los operadores que se pueden utilizar:
Así que vamos a reescribir con respecto a los Paulis y las identidades:
donde para (es decir, la base ), y :
donde y , tales que:
Funciones de coste
En general, las funciones de coste se utilizan para describir el objetivo de un problema y el rendimiento de un estado de prueba con respecto a ese objetivo. Esta definición puede aplicarse a varios ejemplos de química, aprendizaje automático, finanzas, optimización, etc.
Veamos un ejemplo sencillo para encontrar el estado fundamental de un sistema. Nuestro objetivo es minimizar el valor de la expectativa del observable que representa la energía (Hamiltoniano ):
Podemos utilizar el Estimator para evaluar el valor de la expectativa y pasar este valor a un optimizador para minimizar. Si la optimización tiene éxito, devolverá un conjunto de valores óptimos de los parámetros , a partir de los cuales podremos construir el estado de solución propuesto y calcular el valor de expectativa observado como .
Observe que sólo podremos minimizar la función de coste para el conjunto limitado de estados que estamos considerando. Esto nos lleva a dos posibilidades distintas:
- Nuestro ansatz no define el estado de la solución en todo el espacio de búsqueda : Si este es el caso, nuestro optimizador nunca encontrará la solución, y tenemos que experimentar con otros ansatzes que podrían ser capaces de representar nuestro espacio de búsqueda con mayor precisión.
- Nuestro optimizador es incapaz de encontrar esta solución válida : La optimización puede ser definida globalmente y definida localmente. Más adelante veremos qué significa esto.
En definitiva, estaremos realizando un bucle de optimización clásico pero confiando la evaluación de la función de coste a un ordenador cuántico. Desde esta perspectiva, se podría pensar en la optimización como un esfuerzo puramente clásico en el que llamamos a algunos Dispositivo hipotético o componente de software que realiza una función específica, pero cuyo funcionamiento interno se desconoce. Un usuario sólo conoce las entradas y salidas de la caja negra, y no sabe cómo ésta procesa las entradas para producir las salidas. cada vez que el optimizador necesita evaluar la función de coste.
def cost_func_vqe(params, circuit, 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 = (circuit, hamiltonian, params)
cost = estimator.run([pub]).result()[0].data.evs
return costfrom qiskit.circuit.library import TwoLocal
observable = SparsePauliOp.from_list([("XX", 1), ("YY", -3)])
reference_circuit = QuantumCircuit(2)
reference_circuit.x(0)
variational_form = TwoLocal(
2,
rotation_blocks=["rz", "ry"],
entanglement_blocks="cx",
entanglement="linear",
reps=1,
)
ansatz = reference_circuit.compose(variational_form)
theta_list = (2 * np.pi * np.random.rand(1, 8)).tolist()
ansatz.decompose().draw("mpl")Output:
Primero lo haremos con un simulador: StatevectorEstimator. Esto suele ser aconsejable para la depuración, pero seguiremos inmediatamente la ejecución de depuración con un cálculo en hardware cuántico real. Cada vez más, los problemas de interés ya no son simulables clásicamente sin instalaciones de supercomputación de última generación.
estimator = StatevectorEstimator()
cost = cost_func_vqe(theta_list, ansatz, observable, estimator)
print(cost)Output:
[-0.58744589]
Ahora procederemos a ejecutarlo en un ordenador cuántico real. Observe los cambios de sintaxis. Los pasos relacionados con el pass_manager se tratarán con más detalle en el siguiente ejemplo. Un paso de especial importancia en los algoritmos variacionales es el uso de una sesión Qiskit Runtime. Iniciar una sesión permite ejecutar múltiples iteraciones de un algoritmo variacional sin tener que esperar en una nueva cola cada vez que se actualizan los parámetros. Esto es importante si los tiempos de espera son largos y/o se necesitan muchas iteraciones. Sólo los socios de la red IBM Quantum® pueden utilizar las sesiones Runtime. Si no tiene acceso a las sesiones, puede reducir el número de iteraciones que envía en un momento dado y guardar los parámetros más recientes para utilizarlos en futuras ejecuciones. Si envía demasiadas iteraciones o se encuentra con tiempos de espera demasiado largos, puede encontrarse con el código de error 1217, que se refiere a largos retrasos entre envíos de trabajos.
# Estimated usage: < 1 min. Benchmarked at 7 seconds on an Eagle processor
# Load necessary packages:
from qiskit_ibm_runtime import (
QiskitRuntimeService,
Session,
EstimatorOptions,
EstimatorV2 as Estimator,
)
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
# Select the least busy backend:
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, min_num_qubits=ansatz.num_qubits, simulator=False
)
# Or get a specific backend:
# backend = service.backend("ibm_brisbane")
# Use a pass manager to transpile the circuit and observable for the specific backend being used:
pm = generate_preset_pass_manager(backend=backend, optimization_level=1)
isa_ansatz = pm.run(ansatz)
isa_observable = observable.apply_layout(layout=isa_ansatz.layout)
# Set estimator options
estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)
# Open a Runtime session:
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
cost = cost_func_vqe(theta_list, isa_ansatz, isa_observable, estimator)
session.close()
print(cost)Obsérvese que los valores obtenidos de los dos cálculos anteriores son muy similares. Las técnicas para mejorar los resultados se analizarán más adelante.
Ejemplo de mapeo a sistemas no físicos
El problema del corte máximo (max-cut) es un problema de optimización combinatoria que consiste en dividir los vértices de un grafo en dos conjuntos disjuntos de tal manera que se maximice el número de aristas entre ambos conjuntos. En términos más formales, dado un grafo no dirigido , donde es el conjunto de vértices y es el conjunto de aristas, el problema del corte máximo consiste en dividir los vértices en dos subconjuntos disjuntos, y , de tal manera que se maximice el número de aristas con un extremo en y el otro en .
Podemos aplicar el método «max-cut» para resolver diversos problemas, entre los que se incluyen: la agrupación en clústeres, el diseño de redes, las transiciones de fase, etc. Empezaremos creando un gráfico del problema:
import rustworkx as rx
from rustworkx.visualization import mpl_draw
n = 4
G = rx.PyGraph()
G.add_nodes_from(range(n))
# The edge syntax is (start, end, weight)
edges = [(0, 1, 1.0), (0, 2, 1.0), (0, 3, 1.0), (1, 2, 1.0), (2, 3, 1.0)]
G.add_edges_from(edges)
mpl_draw(
G, pos=rx.shell_layout(G), with_labels=True, edge_labels=str, node_color="#1192E8"
)Output:
Este problema puede expresarse como un problema de optimización binario. Para cada nodo , donde es el número de nodos del grafo (en este caso ), consideraremos la variable binaria . Esta variable tendrá el valor si el nodo es uno de los grupos que etiquetaremos como y si está en el otro grupo, que etiquetaremos como . También denotaremos como (elemento de la matriz de adyacencia ) el peso de la arista que va del nodo al nodo . Dado que el grafo es no dirigido, . Entonces podemos formular nuestro problema como la maximización de la siguiente función de coste:
Para resolver este problema con un ordenador cuántico, vamos a expresar la función de coste como el valor esperado de un observable. Sin embargo, los observables que admite Qiskit consisten nativamente en operadores de Pauli, que tienen valores propios y en lugar de y . Por eso vamos a hacer el siguiente cambio de variable:
Donde . Podemos utilizar la matriz de adyacencia para acceder cómodamente a los pesos de todas las aristas. Esto se utilizará para obtener nuestra función de costes:
Esto implica que:
Así que la nueva función de coste que queremos maximizar es:
Además, la tendencia natural de un ordenador cuántico es encontrar mínimos (normalmente la energía más baja) en lugar de máximos, así que en lugar de maximizar vamos a minimizar:
Ahora que tenemos una función de coste a minimizar cuyas variables pueden tener los valores y , podemos hacer la siguiente analogía con la de Pauli :
En otras palabras, la variable será equivalente a una puerta que actúa sobre el qubit . Además:
Entonces el observable que vamos a considerar es:
al que tendremos que añadir después el término independiente:
El operador es una combinación lineal de términos con operadores Z en nodos conectados por una arista (recuérdese que el qubit 0 está más a la derecha): . Una vez construido el operador, el ansatz para el algoritmo QAOA se puede construir fácilmente utilizando el circuito QAOAAnsatz de la biblioteca de circuitos Qiskit.
from qiskit.circuit.library import QAOAAnsatz
from qiskit.quantum_info import SparsePauliOp
hamiltonian = SparsePauliOp.from_list(
[("IIZZ", 1), ("IZIZ", 1), ("IZZI", 1), ("ZIIZ", 1), ("ZZII", 1)]
)
ansatz = QAOAAnsatz(hamiltonian, reps=2)
# Draw
ansatz.decompose(reps=3).draw("mpl")Output:
# Sum the weights, and divide by 2
offset = -sum(edge[2] for edge in edges) / 2
print(f"""Offset: {offset}""")Output:
Offset: -2.5
Con el Estimador de Tiempo de Ejecución tomando directamente un Hamiltoniano y un ansatz parametrizado, y devolviendo la energía necesaria, La función de coste para una instancia QAOA es bastante simple:
def cost_func(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
# cost = estimator.run(ansatz, hamiltonian, parameter_values=params).result().values[0]
return costimport numpy as np
x0 = 2 * np.pi * np.random.rand(ansatz.num_parameters)
estimator = StatevectorEstimator()
cost = cost_func_vqe(x0, ansatz, hamiltonian, estimator)
print(cost)Output:
1.473098768180865
# Estimated usage: < 1 min, benchmarked at 6 seconds on ibm_osaka, 5-23-24
# Load some necessary packages:
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import Session, EstimatorV2 as Estimator
# Select the least busy backend:
backend = service.least_busy(
operational=True, min_num_qubits=ansatz.num_qubits, simulator=False
)
# Or get a specific backend:
# backend = service.backend("ibm_brisbane")
# Use a pass manager to transpile the circuit and observable for the specific backend being used:
pm = generate_preset_pass_manager(backend=backend, optimization_level=1)
isa_ansatz = pm.run(ansatz)
isa_hamiltonian = hamiltonian.apply_layout(layout=isa_ansatz.layout)
# Set estimator options
estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)
# Open a Runtime session:
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
cost = cost_func_vqe(x0, isa_ansatz, isa_hamiltonian, estimator)
# Close session after done
session.close()
print(cost)Output:
1.1120776913677988
Volveremos sobre este ejemplo en Aplicaciones para explorar cómo aprovechar un optimizador para iterar a través del espacio de búsqueda. En términos generales, esto incluye:
- Utilizar un optimizador para encontrar los parámetros óptimos
- Vinculación de parámetros óptimos al ansatz para hallar los valores propios
- Trasladando los valores propios a la definición de nuestro problema
Estrategia de medición: velocidad frente a precisión
Como se ha mencionado, estamos utilizando un ordenador cuántico ruidoso como oráculo de caja negra, donde el ruido puede hacer que los valores recuperados no sean deterministas, dando lugar a fluctuaciones aleatorias que, a su vez, perjudicarán -o incluso impedirán por completo- la convergencia de ciertos optimizadores a una solución propuesta. Se trata de un problema general que debemos abordar a medida que exploramos progresivamente la utilidad cuántica y avanzamos hacia la ventaja cuántica:
Podemos utilizar las opciones de supresión y mitigación de errores de Qiskit Runtime Primitive para hacer frente al ruido y maximizar la utilidad de los ordenadores cuánticos actuales.
Supresión de errores
La supresión de errores se refiere a las técnicas utilizadas para optimizar y transformar un circuito durante la compilación con el fin de minimizar los errores. Se trata de una técnica básica de gestión de errores que suele suponer una pequeña fase de preprocesamiento Costes adicionales introducidos por las nuevas técnicas, en relación con una aplicación de base. en el tiempo de ejecución total. La sobrecarga implica la transpilación de circuitos para que se ejecuten en hardware cuántico mediante:
- Expresar el circuito utilizando las puertas nativas disponibles en un sistema cuántico
- Asignación de qubits virtuales a qubits físicos
- Añadir SWAPs en función de los requisitos de conectividad
- Optimización de las compuertas 1Q y 2Q
- Añadir desacoplamiento dinámico a los qubits inactivos para evitar los efectos de la decoherencia.
Las primitivas permiten utilizar técnicas de supresión de errores configurando la opción optimization_level y seleccionando opciones avanzadas de transpilación. En un curso posterior, profundizaremos en diferentes métodos de construcción de circuitos para mejorar los resultados, pero para la mayoría de los casos, recomendamos configurar optimization_level=3.
Visualizaremos el valor de la optimización creciente en el proceso de transpilación observando un circuito de ejemplo con un comportamiento ideal simple.
from qiskit.circuit import Parameter, QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
theta = Parameter("theta")
qc = QuantumCircuit(2)
qc.x(1)
qc.h(0)
qc.cp(theta, 0, 1)
qc.h(0)
observables = SparsePauliOp.from_list([("ZZ", 1)])
qc.draw("mpl")Output:
El circuito anterior puede producir valores de expectativa sinusoidales del observable dado, siempre que insertemos fases que abarquen un intervalo apropiado, como .
## Setup phases
import numpy as np
phases = np.linspace(0, 2 * np.pi, 50)
# phases need to be expressed as a list of lists in order to work
individual_phases = [[phase] for phase in phases]Podemos utilizar un simulador para demostrar la utilidad de una transpilación optimizada. Más adelante volveremos a utilizar hardware real para demostrar la utilidad de la mitigación de errores. Usaremos QiskitRuntimeService para obtener un backend real (en este caso, ibm_brisbane), y usaremos AerSimulator para simular ese backend, incluyendo su comportamiento frente al ruido.
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_aer import AerSimulator
# get a real backend from the runtime service
service = QiskitRuntimeService()
backend = service.backend("ibm_brisbane")
# generate a simulator that mimics the real quantum system with the latest calibration results
backend_sim = AerSimulator.from_backend(backend)Ahora podemos utilizar un gestor de pases para transpilar el circuito a la "arquitectura de conjunto de instrucciones" o ISA del backend. Este es un nuevo requisito en Qiskit Runtime : todos los circuitos enviados a un backend deben ajustarse a las restricciones del objetivo del backend, lo que significa que deben estar escritos en términos del ISA del backend, es decir, el conjunto de instrucciones que el dispositivo puede entender y ejecutar. Estas restricciones vienen definidas por factores como las puertas base nativas del dispositivo, su conectividad qubit y, cuando proceda, sus especificaciones de temporización de impulsos y otras instrucciones.
Tenga en cuenta que en el presente caso, vamos a hacer esto dos veces: una vez con optimization_level = 0, y una vez con él fijado en 3. Cada vez utilizaremos la primitiva Estimator para estimar los valores de la expectativa del observable en diferentes valores de fase.
# Import estimator and specify that we are using the simulated backend:
from qiskit_ibm_runtime import EstimatorV2 as Estimator
estimator = Estimator(mode=backend_sim)
circuit = qc# Use a pass manager to transpile the circuit and observable for the backend being simulated.
# Start with no optimization:
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
pm = generate_preset_pass_manager(backend=backend_sim, optimization_level=0)
isa_circuit = pm.run(circuit)
isa_observables = observables.apply_layout(layout=isa_circuit.layout)
noisy_exp_values = []
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
noisy_exp_values = cost[0]
# Repeat above steps, but now with optimization = 3:
exp_values_with_opt_es = []
pm = generate_preset_pass_manager(backend=backend_sim, optimization_level=3)
isa_circuit = pm.run(circuit)
isa_observables = observables.apply_layout(layout=isa_circuit.layout)
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
exp_values_with_opt_es = cost[0]Por último, podemos representar gráficamente los resultados, y vemos que la precisión del cálculo era bastante buena incluso sin optimización, pero mejoró definitivamente al aumentar la optimización al nivel 3. Tenga en cuenta que en circuitos más profundos y complicados, es probable que la diferencia entre los niveles de optimización de 0 y 3 sea más significativa. Se trata de un circuito muy sencillo utilizado como modelo de juguete.
import matplotlib.pyplot as plt
plt.plot(phases, noisy_exp_values, "o", label="opt=0")
plt.plot(phases, exp_values_with_opt_es, "o", label="opt=3")
plt.plot(phases, 2 * np.sin(phases / 2) ** 2 - 1, label="ideal")
plt.ylabel("Expectation")
plt.legend()
plt.show()Output:
Mitigación de errores
La mitigación de errores se refiere a las técnicas que permiten a los usuarios reducir los errores de circuito mediante la modelización del ruido del dispositivo en el momento de la ejecución. Por lo general, esto conlleva una sobrecarga de preprocesamiento cuántico relacionada con el entrenamiento del modelo y una sobrecarga de posprocesamiento clásico para mitigar los errores en los resultados brutos mediante el uso del modelo generado.
La opción resilience_level de la primitiva Qiskit Runtime especifica la cantidad de resiliencia a construir contra errores. Los niveles más altos generan resultados más precisos a costa de tiempos de procesamiento más largos debido a la sobrecarga del muestreo cuántico. Los niveles de resistencia pueden utilizarse para configurar la compensación entre coste y precisión al aplicar la mitigación de errores a su consulta primitiva.
Al aplicar cualquier técnica de mitigación de errores, esperamos que la Desviación sistemática de las magnitudes medidas, generalmente causada por errores. de nuestros resultados se reduzca con respecto al sesgo anterior no mitigado. En algunos casos, el sesgo puede incluso desaparecer. Sin embargo, esto tiene un coste. A medida que reduzcamos el sesgo de nuestras cantidades estimadas, aumentará la variabilidad estadística (es decir, la varianza), que podemos tener en cuenta aumentando aún más el número de disparos por circuito en nuestro proceso de muestreo. Esto introducirá una sobrecarga mayor que la necesaria para reducir el sesgo, por lo que no se hace por defecto. Podemos optar fácilmente por este comportamiento ajustando el número de disparos por circuito en options.executions.shots, como se muestra en el ejemplo siguiente.
En este curso, exploraremos estos modelos de mitigación de errores a un alto nivel para ilustrar la mitigación de errores que las primitivas Qiskit Runtime pueden realizar sin requerir detalles completos de implementación.
Extinción del error de lectura giratoria (T-REx)
La extinción de errores de lectura por giro (T-REx) utiliza una técnica conocida como giro de Pauli para reducir el ruido introducido durante el proceso de medición cuántica. Esta técnica no presupone ninguna forma específica de ruido, lo que la hace muy general y eficaz.
Flujo de trabajo general:
- Adquisición de datos para el estado cero con cambio aleatorio de bits (Pauli X antes de la medición)
- Adquirir datos para el estado deseado (ruidoso) con cambios aleatorios de bits (Pauli X antes de la medición)
- Calcula la función especial para cada conjunto de datos y divídela.
Podemos configurarlo con options.resilience_level = 1, como se muestra en el siguiente ejemplo.
Extrapolación sin ruido
La extrapolación de ruido cero (ZNE) funciona amplificando primero el ruido en el circuito que está preparando el estado cuántico deseado, obteniendo mediciones para varios niveles diferentes de ruido y utilizando esas mediciones para inferir el resultado sin ruido.
Flujo de trabajo general:
- Amplificar el ruido del circuito para varios factores de ruido
- Ejecuta todos los circuitos amplificados por ruido
- Extrapolar hasta el límite de ruido cero
Podemos configurarlo con options.resilience_level = 2. Podemos optimizar esto aún más explorando diversas opciones de noise_factors, noise_amplifiers, y extrapolators, pero esto queda fuera del alcance de este curso. Te animamos a que pruebes estas opciones tal y como se describen aquí.
Cada método lleva asociada su propia sobrecarga: un compromiso entre el número de cálculos cuánticos necesarios (tiempo) y la precisión de nuestros resultados:
Uso de las opciones de mitigación y supresión de Qiskit Runtime
A continuación se explica cómo calcular un valor de expectativa utilizando la mitigación y supresión de errores en Qiskit Runtime. Podemos utilizar exactamente el mismo circuito y observable que antes, pero esta vez manteniendo el nivel de optimización fijo en el nivel 2, y ahora ajustando la resiliencia o la(s) técnica(s) de mitigación de errores utilizada(s). Este proceso de mitigación de errores se produce varias veces a lo largo de un bucle de optimización.
Realizamos esta parte en hardware real, ya que la mitigación de errores no está disponible en simuladores.
# Estimated usage: 8 minutes, benchmarked on an Eagle processor, 5-23-24
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import (
Session,
EstimatorOptions,
EstimatorV2 as Estimator,
)
# We select the least busy backend
# Select the least busy backend
# backend = service.least_busy(
# operational=True, min_num_qubits=ansatz.num_qubits, simulator=False
# )
# Or use a specific backend
backend = service.backend("ibm_brisbane")
# Initialize some variables to save the results from different runs:
exp_values_with_em0_es = []
exp_values_with_em1_es = []
exp_values_with_em2_es = []
# Use a pass manager to optimize the circuit and observables for the backend chosen:
pm = generate_preset_pass_manager(backend=backend, optimization_level=2)
isa_circuit = pm.run(circuit)
isa_observables = observables.apply_layout(layout=isa_circuit.layout)
# Open a session and run with no error mitigation:
estimator_options = EstimatorOptions(resilience_level=0, default_shots=10_000)
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
session.close()
exp_values_with_em0_es = cost[0]
# Open a session and run with resilience = 1:
estimator_options = EstimatorOptions(resilience_level=1, default_shots=10_000)
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
session.close()
exp_values_with_em1_es = cost[0]
# Open a session and run with resilience = 2:
estimator_options = EstimatorOptions(resilience_level=2, default_shots=10_000)
with Session(backend=backend) as session:
estimator = Estimator(mode=session, options=estimator_options)
pub = (isa_circuit, isa_observables, [individual_phases])
cost = estimator.run([pub]).result()[0].data.evs
session.close()
exp_values_with_em2_es = cost[0]Como antes, podemos trazar los valores de expectativa resultantes en función del ángulo de fase para los tres niveles de mitigación de errores utilizados. Con gran dificultad, se puede ver que la mitigación de errores mejora ligeramente los resultados. De nuevo, este efecto es mucho más pronunciado en circuitos más profundos y complicados.
import matplotlib.pyplot as plt
plt.plot(phases, exp_values_with_em0_es, "o", label="unmitigated")
plt.plot(phases, exp_values_with_em1_es, "o", label="resil = 1")
plt.plot(phases, exp_values_with_em2_es, "o", label="resil = 2")
plt.plot(phases, 2 * np.sin(phases / 2) ** 2 - 1, label="ideal")
plt.ylabel("Expectation")
plt.legend()
plt.show()Output:
Resumen
Con esta lección has aprendido a crear una función de coste:
- Crear una función de costes
- Cómo aprovechar las primitivas Qiskit Runtime para mitigar y suprimir el ruido
- Cómo definir una estrategia de medición para optimizar la velocidad frente a la precisión
Aquí está nuestra carga de trabajo variacional de alto nivel:
Nuestra función de costes se ejecuta durante cada iteración del bucle de optimización. La siguiente lección explorará cómo el optimizador clásico utiliza nuestra evaluación de la función de coste para seleccionar nuevos parámetros.
import qiskit
import qiskit_ibm_runtime
print(qiskit.version.get_version_info())
print(qiskit_ibm_runtime.version.get_version_info())Output:
1.1.0
0.23.0