Saltar al contenido principal

Ruido cuántico y mitigación de errores

nota

Toshinari Itoko (28 de junio de 2024)

Descarga el PDF de la clase original. Ten en cuenta que algunos fragmentos de código pueden estar desactualizados, ya que son imágenes estáticas.

El tiempo aproximado de QPU para este experimento es de 1 min 40 s.

1. Introducción

En esta lección investigamos el ruido y cómo puede mitigarse en los computadores cuánticos. Comenzamos observando los efectos del ruido utilizando un simulador que puede simular ruido de diversas maneras, incluyendo perfiles de ruido de computadores cuánticos reales. Luego pasamos a computadores cuánticos reales, donde el ruido es inherente. Investigamos los efectos de la mitigación de errores, incluyendo combinaciones como la extrapolación a ruido cero (ZNE) y el gate twirling.

Comenzamos cargando algunos paquetes.

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib qiskit qiskit-aer qiskit-ibm-runtime
# !pip install qiskit qiskit_aer qiskit_ibm_runtime
# !pip install jupyter
# !pip install matplotlib pylatexenc
import qiskit

qiskit.__version__
'2.0.2'
import qiskit_aer

qiskit_aer.__version__
'0.17.1'
import qiskit_ibm_runtime

qiskit_ibm_runtime.__version__
'0.40.1'

2. Simulación con ruido sin mitigación de errores

Qiskit Aer es un simulador clásico para la computación cuántica. Puede simular no solo la ejecución ideal, sino también la ejecución con ruido de circuitos cuánticos. Este notebook muestra cómo realizar una simulación con ruido utilizando Qiskit Aer:

  1. Crear un modelo de ruido
  2. Crear un sampler con ruido (simulador) con el modelo de ruido
  3. Ejecutar un circuito cuántico en el sampler con ruido
noise_model = NoiseModel()
...
noisy_sampler = Sampler(options={"backend_options": {"noise_model": noise_model}})
job = noisy_sampler.run([circuit])

2.1 Construir un circuito de prueba

Consideramos circuitos simples de 1 qubit que repiten puertas X d veces (d=0 ... 100) y miden el observable Z.

from qiskit.circuit import QuantumCircuit

MAX_DEPTH = 100
circuits = []
for d in range(MAX_DEPTH + 1):
circ = QuantumCircuit(1)
for _ in range(d):
circ.x(0)
circ.barrier(0)
circ.measure_all()
circuits.append(circ)

display(circuits[3].draw(output="mpl"))

Salida de la celda de código anterior

from qiskit.quantum_info import SparsePauliOp

obs = SparsePauliOp.from_list([("Z", 1.0)])
obs
SparsePauliOp(['Z'],
coeffs=[1.+0.j])

2.2 Construir un modelo de ruido

Para la simulación con ruido necesitamos especificar un NoiseModel. En esta sección mostramos cómo crear un NoiseModel. Primero debemos definir errores cuánticos (o de lectura) que se agregarán al modelo de ruido.

from qiskit_aer.noise.errors import (
coherent_unitary_error,
amplitude_damping_error,
ReadoutError,
)
from qiskit.circuit.library import RXGate

# Coherent (unitary) error: Over X-rotation error
# https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.noise.coherent_unitary_error.html#qiskit_aer.noise.coherent_unitary_error
OVER_ROTATION_ANGLE = 0.05
coherent_error = coherent_unitary_error(RXGate(OVER_ROTATION_ANGLE).to_matrix())

# Incoherent error: Amplitude dumping error
# https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.noise.amplitude_damping_error.html#qiskit_aer.noise.amplitude_damping_error
AMPLITUDE_DAMPING_PARAM = 0.02 # in [0, 1] (0: no error)
incoherent_error = amplitude_damping_error(AMPLITUDE_DAMPING_PARAM)

# Readout (measurement) error: Readout error
# https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.noise.ReadoutError.html#qiskit_aer.noise.ReadoutError
PREP0_MEAS1 = 0.03 # P(1|0): Probability of preparing 0 and measuring 1
PREP1_MEAS0 = 0.08 # P(0|1): Probability of preparing 1 and measuring 0
readout_error = ReadoutError(
[[1 - PREP0_MEAS1, PREP0_MEAS1], [PREP1_MEAS0, 1 - PREP1_MEAS0]]
)
from qiskit_aer.noise import NoiseModel

noise_model = NoiseModel()
noise_model.add_quantum_error(coherent_error.compose(incoherent_error), "x", (0,))
noise_model.add_readout_error(readout_error, (0,))

2.3 Construir un sampler con ruido con el modelo de ruido

from qiskit_aer.primitives import SamplerV2 as Sampler

noisy_sampler = Sampler(options={"backend_options": {"noise_model": noise_model}})

2.4 Ejecutar circuitos cuánticos en el sampler con ruido

job = noisy_sampler.run(circuits, shots=400)
result = job.result()
result[0].data.meas.get_counts()
{'0': 389, '1': 11}

2.5 Graficar resultados

import matplotlib.pyplot as plt

plt.title("Noisy simulation")
ds = list(range(MAX_DEPTH + 1))
plt.plot(
ds,
[result[d].data.meas.expectation_values(["Z"]) for d in ds],
color="gray",
linestyle="-",
)
plt.scatter(ds, [result[d].data.meas.expectation_values(["Z"]) for d in ds], marker="o")
plt.hlines(0, xmin=0, xmax=MAX_DEPTH, colors="black")
plt.ylim(-1, 1)
plt.xlabel("Circuit depth")
plt.ylabel("Measured <Z>")
plt.show()

2.6 Simulación ideal

ideal_sampler = Sampler()
job_ideal = ideal_sampler.run(circuits)
result_ideal = job_ideal.result()
plt.title("Ideal simulation")
ds = list(range(MAX_DEPTH + 1))
plt.plot(
ds,
[result_ideal[d].data.meas.expectation_values(["Z"]) for d in ds],
color="gray",
linestyle="-",
)
plt.scatter(
ds, [result_ideal[d].data.meas.expectation_values(["Z"]) for d in ds], marker="o"
)
plt.hlines(0, xmin=0, xmax=MAX_DEPTH, colors="black")
plt.xlabel("Circuit depth")
plt.ylabel("Measured <Z>")
plt.show()

Salida de la celda de código anterior

2.7 Ejercicio

Modifica el siguiente código:

  • Prueba con 25 veces más shots (= 10.000 shots) y asegúrate de que se obtiene un gráfico más suave
  • Cambia los parámetros de ruido (OVER_ROTATION_ANGLE, AMPLITUDE_DAMPING_PARAM, PREP0_MEAS1 o PREP1_MEAS0) y observa cómo cambia el gráfico
OVER_ROTATION_ANGLE = 0.05
coherent_error = coherent_unitary_error(RXGate(OVER_ROTATION_ANGLE).to_matrix())
AMPLITUDE_DAMPING_PARAM = 0.02 # in [0, 1] (0: no error)
incoherent_error = amplitude_damping_error(AMPLITUDE_DAMPING_PARAM)
PREP0_MEAS1 = 0.1 # P(1|0): Probability of preparing 0 and measuring 1
PREP1_MEAS0 = 0.05 # P(0|1): Probability of preparing 1 and measuring 0
readout_error = ReadoutError(
[[1 - PREP0_MEAS1, PREP0_MEAS1], [PREP1_MEAS0, 1 - PREP1_MEAS0]]
)
noise_model = NoiseModel()
noise_model.add_quantum_error(coherent_error.compose(incoherent_error), "x", (0,))
noise_model.add_readout_error(readout_error, (0,))
options = {
"backend_options": {"noise_model": noise_model},
}
noisy_sampler = Sampler(options=options)
job = noisy_sampler.run(circuits, shots=400)
result = job.result()
plt.title("Noisy simulation")
ds = list(range(MAX_DEPTH + 1))
plt.plot(
ds,
[result[d].data.meas.expectation_values(["Z"]) for d in ds],
marker="o",
linestyle="-",
)
plt.hlines(0, xmin=0, xmax=MAX_DEPTH, colors="black")
plt.ylim(-1, 1)
plt.xlabel("Depth")
plt.ylabel("Measured <Z>")
plt.show()

Salida de la celda de código anterior

2.8 Simulación con ruido más realista

from qiskit_aer import AerSimulator
from qiskit_ibm_runtime import SamplerV2 as Sampler, QiskitRuntimeService

service = QiskitRuntimeService()
real_backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
) # Eagle
<IBMBackend('ibm_strasbourg')>
aer = AerSimulator.from_backend(real_backend)
noisy_sampler = Sampler(mode=aer)
job = noisy_sampler.run(circuits)
result = job.result()
plt.title("Noisy simulation with noise model from real backend")
ds = list(range(MAX_DEPTH + 1))
plt.plot(
ds,
[result[d].data.meas.expectation_values(["Z"]) for d in ds],
marker="o",
linestyle="-",
)
plt.hlines(0, xmin=0, xmax=MAX_DEPTH, colors="black")
plt.ylim(-1, 1)
plt.xlabel("Depth")
plt.ylabel("Measured <Z>")
plt.show()

Salida de la celda de código anterior

3. Computación cuántica real con mitigación de errores

En esta parte mostramos cómo obtener resultados con mitigación de errores (valores esperados) utilizando el Estimator de Qiskit. Consideramos circuitos Trotterizados de 6 qubits para simular la evolución temporal del modelo de Ising unidimensional y vemos cómo escala el error con el número de pasos temporales.

backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
) # Eagle
backend
<IBMBackend('ibm_strasbourg')>
NUM_QUBITS = 6
NUM_TIME_STEPS = list(range(8))
RX_ANGLE = 0.1
RZZ_ANGLE = 0.1

3.1 Construir circuitos

# Build circuits with different number of time steps
circuits = []
for n_steps in NUM_TIME_STEPS:
circ = QuantumCircuit(NUM_QUBITS)
for i in range(n_steps):
# rx layer
for q in range(NUM_QUBITS):
circ.rx(RX_ANGLE, q)
# 1st rzz layer
for q in range(1, NUM_QUBITS - 1, 2):
circ.rzz(RZZ_ANGLE, q, q + 1)
# 2nd rzz layer
for q in range(0, NUM_QUBITS - 1, 2):
circ.rzz(RZZ_ANGLE, q, q + 1)
circ.barrier() # need not to optimize the circuit
# Uncompute stage
for i in range(n_steps):
for q in range(0, NUM_QUBITS - 1, 2):
circ.rzz(-RZZ_ANGLE, q, q + 1)
for q in range(1, NUM_QUBITS - 1, 2):
circ.rzz(-RZZ_ANGLE, q, q + 1)
for q in range(NUM_QUBITS):
circ.rx(-RX_ANGLE, q)
circuits.append(circ)

Para conocer el resultado ideal de antemano, utilizamos circuitos Compute-Uncompute que consisten en una primera fase en la que se aplica el circuito original UU, y una segunda fase en la que se invierte UU^\dagger. Ten en cuenta que el resultado ideal de tales circuitos es trivialmente el estado de entrada 000000|000000\rangle, que tiene los valores esperados triviales para observables de Pauli arbitrarios, p. ej., IIIIIZ=1\langle IIIIIZ \rangle = 1.

# Print the circuit with 2 time steps
circuits[2].draw(output="mpl")

Salida de la celda de código anterior

Nota: Como se muestra arriba, el circuito con kk pasos temporales tiene 4k4k capas de puertas de dos qubits.

obs = SparsePauliOp.from_sparse_list([("Z", [0], 1.0)], num_qubits=NUM_QUBITS)
obs
SparsePauliOp(['IIIIIZ'],
coeffs=[1.+0.j])

3.2 Transpilar circuitos

Transpilamos los circuitos para el backend con optimización (optimization_level=1).

from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

pm = generate_preset_pass_manager(optimization_level=1, backend=backend)
isa_circuits = pm.run(circuits)
display(isa_circuits[2].draw("mpl", idle_wires=False, fold=-1))

Salida de la celda de código anterior

3.3 Ejecución con el Estimator (con diferentes niveles de resiliencia)

Establecer el nivel de resiliencia (estimator.options.resilience_level) es el método más sencillo para aplicar mitigación de errores al utilizar el Estimator de Qiskit. El Estimator soporta los siguientes niveles de resiliencia (a fecha del 28.06.2024). Puedes encontrar más detalles en la guía Configurar la mitigación de errores.

image.png

from qiskit_ibm_runtime import Batch
from qiskit_ibm_runtime import EstimatorV2 as Estimator

jobs = []
job_ids = []
with Batch(backend=backend):
for resilience_level in [0, 1, 2]:
estimator = Estimator()
estimator.options.resilience_level = resilience_level
job = estimator.run(
[(circ, obs.apply_layout(circ.layout)) for circ in isa_circuits]
)
job_ids.append(job.job_id())
print(f"Job ID (rl={resilience_level}): {job.job_id()}")
jobs.append(job)
Job ID (rl=0): d146vcnmya70008emprg
Job ID (rl=1): d146vdnqf56g0081sva0
Job ID (rl=2): d146ven5z6q00087c61g
# check job status
for job in jobs:
print(job.status())
DONE
DONE
DONE
# REPLACE WITH YOUR OWN JOB IDS
jobs = [service.job(job_id) for job_id in job_ids]
# Get results
results = [job.result() for job in jobs]

3.4 Graficar resultados

plt.title("Error mitigation with different resilience levels")
labels = ["0 (No mitigation)", "1 (TREX)", "2 (ZNE + Gate twirling)"]
steps = NUM_TIME_STEPS
for result, label in zip(results, labels):
plt.errorbar(
x=steps,
y=[result[s].data.evs for s in steps],
yerr=[result[s].data.stds for s in steps],
marker="o",
linestyle="-",
capsize=4,
label=label,
)
plt.hlines(
1.0, min(steps), max(steps), linestyle="dashed", label="Ideal", colors="black"
)
plt.xlabel("Time steps")
plt.ylabel("Mitigated <IIIIIZ>")
plt.legend()
plt.show()

Salida de la celda de código anterior

4. (Opcional) Personalizar opciones de mitigación de errores

Podemos personalizar la aplicación de técnicas de mitigación de errores a través de opciones, como se muestra a continuación.

# TREX
estimator.options.twirling.enable_measure = True
estimator.options.twirling.num_randomizations = "auto"
estimator.options.twirling.shots_per_randomization = "auto"

# Gate twirling
estimator.options.twirling.enable_gates = True
# ZNE
estimator.options.resilience.zne_mitigation = True
estimator.options.resilience.zne.noise_factors = [1, 3, 5]
estimator.options.resilience.zne.extrapolator = ("exponential", "linear")

# Dynamical decoupling
estimator.options.dynamical_decoupling.enable = True # Default: False
estimator.options.dynamical_decoupling.sequence_type = "XX"

# Other options
estimator.options.default_shots = 10_000

Para más detalles sobre las opciones de mitigación de errores, consulta las siguientes guías y la referencia de la API: