Combinar opciones de mitigación de errores con la primitiva Estimator
Tiempo estimado de ejecución: 7 minutos en un procesador Heron r2 (NOTA: Se trata solo de una estimación. (El tiempo de ejecución puede variar.)
Resultados del aprendizaje
Recomendamos a los usuarios que se familiaricen con los siguientes temas antes de seguir este tutorial:
- Los conceptos básicos del desacoplamiento dinámico, la mitigación de errores de medición, el «gate twirling» y la extrapolación sin ruido, tal y como se describen en esta guía.
Requisitos previos
Una vez completado este tutorial, los usuarios deberían comprender:
- Cómo se implementan de forma selectiva en el hardware las técnicas de mitigación de errores mencionadas anteriormente.
- Cómo se comparan en cuanto a su capacidad para mitigar el ruido del hardware.
En segundo plano
Este tutorial analiza las opciones de supresión y mitigación de errores disponibles con la primitiva Estimator de Qiskit Runtime. Este tutorial muestra cómo implementar cada uno de los siguientes métodos por separado:
- Desacoplamiento dinámico
- Reducción de los errores de medición
- Puerta giratoria
- Extrapolación de ruido cero (ZNE)
Ten en cuenta que una alternativa a la implementación individual de estas técnicas es implementarlas utilizando un nivel de resiliencia, que resilience_level puede tomar los valores 0, 1 y 2:
- 0: No se ha aplicado ninguna medida de mitigación.
- 1: Se ha implementado la mitigación de errores de medición.
- 2: Se han implementado el giro de compuertas, la mitigación de errores de medición y el ZNE.
En este tutorial, construirás un circuito y un observador, y enviarás trabajos utilizando la primitiva Estimator con diferentes combinaciones de ajustes de mitigación de errores. A continuación, representará gráficamente los resultados para observar los efectos de los distintos ajustes. La mayor parte del tutorial utiliza un circuito de 10 qubits para facilitar la visualización y, al final, ampliarás el flujo de trabajo a 50 qubits.
Requisitos
Antes de comenzar este tutorial, asegúrese de tener instalado lo siguiente:
- Qiskit SDK v2.1 o posterior, con soporte de visualización
- Qiskit Runtime v0.40 o posterior (
pip install qiskit-ibm-runtime)
Configuración
import matplotlib.pyplot as plt
import numpy as np
from qiskit.circuit.library import efficient_su2, unitary_overlap
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import Batch, EstimatorV2 as EstimatorEjemplo de simulador a pequeña escala
Omitiremos este paso, ya que la mitigación de errores en tiempo de ejecución no es compatible con los simuladores.
Ejemplo de hardware
Paso 1: Asignar entradas clásicas a un problema cuántico
Este tutorial asume que el problema clásico ya se ha mapeado al cuántico. Comience construyendo un circuito y un observador para medir. Aunque las técnicas utilizadas aquí se aplican a muchos tipos diferentes de circuitos, para simplificar, este tutorial utiliza el efficient_su2 circuito incluido en la biblioteca de circuitos de Qiskit.
efficient_su2 es un circuito cuántico parametrizado diseñado para ejecutarse de manera eficiente en hardware cuántico con conectividad limitada entre qubits, sin dejar de ser lo suficientemente expresivo como para resolver problemas en ámbitos de aplicación como la optimización y la química. Se construye alternando capas de puertas de un solo qubit parametrizadas con una capa que contiene un patrón fijo de puertas de dos qubits, durante un número determinado de repeticiones. El usuario puede definir la configuración de las puertas de dos qubits. Aquí puedes utilizar el patrón pairwise integrado, ya que minimiza la profundidad del circuito al agrupar las puertas de dos qubits de la forma más compacta posible. Este patrón se puede ejecutar utilizando únicamente conectividad lineal entre qubits.
n_qubits = 10
reps = 1
circuit = efficient_su2(n_qubits, entanglement="pairwise", reps=reps)
circuit.decompose().draw("mpl", scale=0.7)Output:
Como observable, tomemos el operador de Pauli que actúa sobre el último qubit, . Cabe señalar que el hecho de que el último qubit corresponda al primer elemento de esta cadena se debe a que Qiskit utiliza la notación «little-endian».
# Z on the last qubit (index -1) with coefficient 1.0
observable = SparsePauliOp.from_sparse_list(
[("Z", [-1], 1.0)], num_qubits=n_qubits
)En este punto, ya puedes poner en marcha el circuito y medir la magnitud observable. Sin embargo, también es necesario comparar el resultado del dispositivo cuántico con la respuesta correcta, es decir, el valor teórico de la observable, si el circuito se hubiera ejecutado sin errores. En el caso de los circuitos cuánticos pequeños, este valor se puede calcular simulando el circuito en un ordenador clásico, pero esto no es posible con circuitos más grandes, a escala industrial. Este problema se puede solucionar mediante la técnica del «circuito espejo» (también conocida como «compute-uncompute»), que resulta útil para evaluar el rendimiento de los dispositivos cuánticos.
Circuito espejo
En la técnica del circuito espejo, se concatena el circuito con su circuito inverso, que se forma invirtiendo cada puerta del circuito en orden inverso. El circuito resultante implementa el operador de identidad, que puede simularse trivialmente. Dado que la estructura del circuito original se conserva en el circuito espejo, la ejecución de éste permite hacerse una idea de cómo funcionaría el dispositivo cuántico en el circuito original.
La siguiente celda de código asigna parámetros aleatorios a tu circuito y, a continuación, construye el circuito simétrico utilizando la unitary_overlap clase. Antes de duplicar el circuito, añádele una instrucción de barrera para evitar que el transpilador fusione las dos partes del circuito a ambos lados de la barrera y dé como resultado un circuito transpilado sin ninguna puerta lógica.
# Generate random parameters
rng = np.random.default_rng(1234)
params = rng.uniform(-np.pi, np.pi, size=circuit.num_parameters)
# Assign the parameters to the circuit
assigned_circuit = circuit.assign_parameters(params)
# Add a barrier to prevent circuit optimization of mirrored operators
assigned_circuit.barrier()
# Construct mirror circuit
mirror_circuit = unitary_overlap(assigned_circuit, assigned_circuit)
mirror_circuit.decompose().draw("mpl", scale=0.7)Output:
Paso 2: Optimizar el problema para la ejecución en hardware cuántico
Debes optimizar tu circuito antes de ejecutarlo en hardware. Este proceso consta de varios pasos:
- Elige una disposición de qubits que asigne los qubits virtuales de tu circuito a qubits físicos en el hardware.
- Inserta puertas de intercambio según sea necesario para dirigir las interacciones entre qubits que no estén conectados.
- Traduce las puertas de tu circuito a instrucciones ISA (Instruction Set Architecture ) que puedan ejecutarse directamente en el hardware.
- Realiza optimizaciones de circuitos para minimizar la profundidad del circuito y el número de compuertas.
El transpilador integrado en Qiskit puede realizar todos estos pasos por usted. Dado que este ejemplo utiliza un circuito eficiente en términos de hardware, el transpilador debería ser capaz de elegir una disposición de qubits que no requiera la inserción de puertas de intercambio para enrutar las interacciones.
Debes seleccionar el dispositivo de hardware que vas a utilizar antes de optimizar el circuito. La siguiente celda de código solicita el dispositivo menos ocupado que tenga al menos 127 qubits.
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)print(backend)Output:
<IBMBackend('ibm_fez')>
Puedes transpilar tu circuito al backend que elijas creando un gestor de pasadas y, a continuación, ejecutándolo en el circuito. Una forma sencilla de crear un gestor de contraseñas es utilizar la generate_preset_pass_manager función. Consulte «Transpilación con gestores de pasadas» para obtener una explicación más detallada sobre la transpilación con gestores de pasadas.
pass_manager = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=1234
)
isa_circuit = pass_manager.run(mirror_circuit)
isa_circuit.draw("mpl", idle_wires=False, scale=0.7, fold=-1)Output:
El circuito transpilado ahora contiene únicamente instrucciones ISA. Todas las puertas se han descompuesto en puertas de tipo « », rotaciones de tipo « » y puertas CZ.
El proceso de transpilación ha mapeado los qubits virtuales del circuito a qubits físicos en el hardware. La información sobre la disposición de los qubits se almacena en el atributo layout del circuito transpilado. El observable también se definió en términos de qubits virtuales, por lo que es necesario aplicar esta disposición al observable, lo que se puede hacer con el método apply_layout de SparsePauliOp.
isa_observable = observable.apply_layout(isa_circuit.layout)
print("Original observable:")
print(observable)
print()
print("Observable with layout applied:")
print(isa_observable)Output:
Original observable:
SparsePauliOp(['ZIIIIIIIII'],
coeffs=[1.+0.j])
Observable with layout applied:
SparsePauliOp(['IIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j])
Paso 3: Ejecutar utilizando Qiskit primitives
Ahora estás listo para ejecutar tu circuito utilizando la primitiva Estimator.
A continuación, enviará cinco trabajos distintos, empezando sin supresión ni mitigación de errores y activando sucesivamente las distintas opciones de supresión y mitigación de errores disponibles en Qiskit Runtime. Para obtener información sobre las opciones, consulte las páginas siguientes:
- Resumen de todas las opciones
- Desacoplamiento dinámico
- Resistencia, incluida la mitigación de errores de medición y la extrapolación de ruido cero (ZNE)
- Girando
Dado que estos trabajos pueden ejecutarse independientemente unos de otros, puede utilizar el modo por lotes para que Qiskit Runtime optimice el tiempo de su ejecución.
pub = (isa_circuit, isa_observable)
jobs = []
with Batch(backend=backend) as batch:
estimator = Estimator(mode=batch)
estimator.options.environment.job_tags = [
"TUT_CEM_SS"
] # add tag for this small scale job
# Set number of shots
estimator.options.default_shots = 100_000
# Disable runtime compilation and error mitigation
estimator.options.resilience_level = 0
# Run job with no error mitigation
job0 = estimator.run([pub])
jobs.append(job0)
# Add dynamical decoupling (DD)
estimator.options.dynamical_decoupling.enable = True
estimator.options.dynamical_decoupling.sequence_type = "XpXm"
job1 = estimator.run([pub])
jobs.append(job1)
# Add readout error mitigation (DD + TREX)
estimator.options.resilience.measure_mitigation = True
job2 = estimator.run([pub])
jobs.append(job2)
# Add gate twirling (DD + TREX + Gate Twirling)
estimator.options.twirling.enable_gates = True
estimator.options.twirling.num_randomizations = "auto"
job3 = estimator.run([pub])
jobs.append(job3)
# Add zero-noise extrapolation (DD + TREX + Gate Twirling + ZNE)
estimator.options.resilience.zne_mitigation = True
estimator.options.resilience.zne.noise_factors = (1, 3, 5)
estimator.options.resilience.zne.extrapolator = ("exponential", "linear")
job4 = estimator.run([pub])
jobs.append(job4)Paso 4: Procesamiento posterior y devolución del resultado en el formato clásico deseado
Por último, puedes analizar los datos. Aquí recuperará los resultados de los trabajos, extraerá de ellos los valores de las expectativas medidas y trazará los valores, incluidas las barras de error de una desviación estándar.
# Retrieve the job results
results = [job.result() for job in jobs]
# Unpack the PUB results (there's only one PUB result in each job result)
pub_results = [result[0] for result in results]
# Unpack the expectation values and standard errors
expectation_vals = np.array(
[float(pub_result.data.evs) for pub_result in pub_results]
)
standard_errors = np.array(
[float(pub_result.data.stds) for pub_result in pub_results]
)
# Plot the expectation values
fig, ax = plt.subplots()
labels = ["No mitigation", "+ DD", "+ TREX", "+ Twirling", "+ ZNE"]
ax.bar(
range(len(labels)),
expectation_vals,
yerr=standard_errors,
label="experiment",
)
ax.axhline(y=1.0, color="gray", linestyle="--", label="ideal")
ax.set_xticks(range(len(labels)))
ax.set_xticklabels(labels)
ax.set_ylabel("Expectation value")
ax.legend(loc="upper left")
plt.show()Output:
A esta escala tan pequeña, es difícil ver el efecto de la mayoría de las técnicas de mitigación de errores, pero la extrapolación de ruido cero ofrece una mejora notable. Sin embargo, hay que tener en cuenta que esta mejora no es gratuita, ya que el resultado de la ZNE también tiene una barra de error mayor.
Ejemplo de hardware a gran escala
Al desarrollar un experimento, es útil comenzar con un circuito pequeño para facilitar las visualizaciones y simulaciones. Ahora que ha desarrollado y probado nuestro flujo de trabajo en un circuito de 10 qubits, puede ampliarlo hasta 50 qubits. La siguiente celda de código repite todos los pasos de este tutorial, pero ahora los aplica a un circuito de 50 qubits.
n_qubits = 50
reps = 1
# Construct circuit and observable
circuit = efficient_su2(n_qubits, entanglement="pairwise", reps=reps)
observable = SparsePauliOp.from_sparse_list(
[("Z", [-1], 1.0)], num_qubits=n_qubits
)
# Assign parameters to circuit
params = rng.uniform(-np.pi, np.pi, size=circuit.num_parameters)
assigned_circuit = circuit.assign_parameters(params)
assigned_circuit.barrier()
# Construct mirror circuit
mirror_circuit = unitary_overlap(assigned_circuit, assigned_circuit)
# Transpile circuit and observable
isa_circuit = pass_manager.run(mirror_circuit)
isa_observable = observable.apply_layout(isa_circuit.layout)
# Run jobs
pub = (isa_circuit, isa_observable)
jobs = []
with Batch(backend=backend) as batch:
estimator = Estimator(mode=batch)
estimator.options.environment.job_tags = [
"TUT_CEM_LS"
] # add tag for this large scale job
# Set number of shots
estimator.options.default_shots = 100_000
# Disable runtime compilation and error mitigation
estimator.options.resilience_level = 0
# Run job with no error mitigation
job0 = estimator.run([pub])
jobs.append(job0)
# Add dynamical decoupling (DD)
estimator.options.dynamical_decoupling.enable = True
estimator.options.dynamical_decoupling.sequence_type = "XpXm"
job1 = estimator.run([pub])
jobs.append(job1)
# Add readout error mitigation (DD + TREX)
estimator.options.resilience.measure_mitigation = True
job2 = estimator.run([pub])
jobs.append(job2)
# Add gate twirling (DD + TREX + Gate Twirling)
estimator.options.twirling.enable_gates = True
estimator.options.twirling.num_randomizations = "auto"
job3 = estimator.run([pub])
jobs.append(job3)
# Add zero-noise extrapolation (DD + TREX + Gate Twirling + ZNE)
estimator.options.resilience.zne_mitigation = True
estimator.options.resilience.zne.noise_factors = (1, 3, 5)
estimator.options.resilience.zne.extrapolator = ("exponential", "linear")
job4 = estimator.run([pub])
jobs.append(job4)
# Retrieve the job results
results = [job.result() for job in jobs]
# Unpack the PUB results (there's only one PUB result in each job result)
pub_results = [result[0] for result in results]
# Unpack the expectation values and standard errors
expectation_vals = np.array(
[float(pub_result.data.evs) for pub_result in pub_results]
)
standard_errors = np.array(
[float(pub_result.data.stds) for pub_result in pub_results]
)
# Plot the expectation values
fig, ax = plt.subplots()
labels = ["No mitigation", "+ DD", "+ TREX", "+ Twirling", "+ ZNE"]
ax.bar(
range(len(labels)),
expectation_vals,
yerr=standard_errors,
label="experiment",
)
ax.axhline(y=1.0, color="gray", linestyle="--", label="ideal")
ax.set_xticks(range(len(labels)))
ax.set_xticklabels(labels)
ax.set_ylabel("Expectation value")
ax.legend(loc="upper left")
plt.show()Output:
Al comparar los resultados de 50 qubits con los resultados de 10 qubits de antes, puede que observe lo siguiente (sus resultados pueden diferir en las distintas ejecuciones):
- Todos los experimentos arrojan resultados más cercanos al valor ideal y todas las barras de error son más pequeñas.
- Es posible que la incorporación del desacoplamiento dinámico haya empeorado el rendimiento en comparación con el caso sin medidas de mitigación. Esto no es de extrañar, ya que el circuito es muy denso. El desacoplamiento dinámico resulta especialmente útil cuando hay largos intervalos en el circuito, durante los cuales los qubits permanecen inactivos sin que se les apliquen puertas lógicas. Cuando no existen estos intervalos, el desacoplamiento dinámico no resulta eficaz y, de hecho, puede empeorar el rendimiento debido a errores en los propios pulsos de desacoplamiento dinámico. Es posible que el circuito de 10 qubits fuera demasiado pequeño para que pudiéramos observar este efecto.
- Con la extrapolación sin ruido, el resultado es muy cercano al valor ideal. Esto demuestra el potencial de ZNE.
Próximos pasos
Si este trabajo te ha parecido interesante, quizá te interese el siguiente material sobre algunas técnicas adicionales de mitigación y supresión de errores que no se han mencionado en este tutorial: