Extrapolación a ruido cero con plegado de puertas y extrapolación personalizada
La extrapolación a ruido cero (ZNE) es una técnica para mitigar el ruido cuántico en el cálculo de valores esperados de observables. Funciona ejecutando un circuito cuántico con escalas de ruido crecientes, tomando valores esperados respecto a esas escalas de ruido y extrapolando para estimar un valor esperado a ruido cero. Tanto la amplificación del ruido como las etapas de extrapolación se pueden implementar de diferentes maneras. Esta guía amplifica el ruido con "plegado digital de puertas", lo que significa que las puertas de dos qubits se sustituyen por secuencias equivalentes de la puerta y su inversa. Por ejemplo, sustituir un unitario por produciría un factor de amplificación del ruido de 3. También puedes implementar la amplificación con amplificación probabilística de errores (PEA), que requiere un paso adicional de aprendizaje de un modelo de ruido y se explica en la guía de amplificación probabilística de errores. Esta guía extrapola usando un par de estrategias de ajuste integradas (lineal y exponencial), y también muestra cómo implementar tu propia función de ajuste personalizada.
Ten en cuenta que, aunque la ZNE puede ser eficaz para mitigar el ruido cuántico con un coste de muestreo menor que otros métodos, no ofrece ninguna cota sobre el sesgo.
El flujo de trabajo es el siguiente:
- Especificar un circuito y un observable
- Calcular valores esperados exactos con el simulador de vector de estado de Qiskit
- Calcular valores esperados con ruido y sin mitigar
- Preparar un programa de ZNE con plegado digital de puertas y circuitos de muestra con distintas escalas de ruido
- Extrapolar al límite de ruido cero con funciones de ajuste integradas y personalizadas
- Comparar los valores esperados mitigados
Especificar un circuito y un observable
Calcula la magnetización promediada por sitios de un modelo de Ising de campo transversal (TFIM) trotterizado en ocho qubits con topología de anillo. Barrerás varios valores de la intensidad del campo transversal y representarás la magnetización en función de la intensidad del campo.
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"IPython": "IPython", "matplotlib": "matplotlib", "numpy": "numpy", "qiskit": "qiskit", "qiskit_aer": "qiskit-aer", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "qiskit_mitigation": "qiskit-mitigation", "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")
import warnings
import numpy as np
from qiskit.circuit import Parameter, QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
warnings.filterwarnings("ignore")
def trotter_circuit(num_qubits, theta, layers, num_steps):
"""Trotterized transverse-field Ising evolution: RX on every site, then a CZ layer per coupling layer."""
circuit = QuantumCircuit(num_qubits)
for _ in range(num_steps):
circuit.rx(theta, range(num_qubits))
for layer in layers:
circuit.barrier()
for edge in layer:
circuit.cz(*edge)
circuit.sdg(edge)
circuit.barrier()
return circuit
num_qubits = 8
trotter_steps = 15
ring_coupling = [(i, (i + 1) % num_qubits) for i in range(num_qubits)]
layers = [
ring_coupling[0::2],
ring_coupling[1::2],
] # two disjoint layers of CZ gates
theta = Parameter("theta")
circuit = trotter_circuit(num_qubits, theta, layers, trotter_steps)
# Parameter values: theta in [0, 0.25 pi] - values for the transverse-field sweep
data_points = 6
parameter_values = np.linspace(0, 0.25 * np.pi, data_points).reshape(
(data_points, 1)
)
# Define observables - Z on each site
observables = [
SparsePauliOp("I" * (num_qubits - q - 1) + "Z" + "I" * q)
for q in range(num_qubits)
]
print("Visualization of 1 Trotter step")
trotter_circuit(num_qubits, theta, layers, 1).draw("mpl", scale=0.7)
Visualization of 1 Trotter step
Calcular valores esperados exactos con el simulador de vector de estado de Qiskit
Usa la clase Statevector de Qiskit para calcular el valor esperado exacto de en cada sitio para cada valor de .
import matplotlib.pyplot as plt
from qiskit.quantum_info import Statevector
exact_evs = np.array(
[
[
Statevector(circuit.assign_parameters([value]))
.expectation_value(obs)
.real
for value in parameter_values.ravel()
]
for obs in observables
]
) # shape (observables, parameter values)
x_vals = parameter_values.ravel() / np.pi
fig, ax = plt.subplots(figsize=(7, 4))
ax.plot(x_vals, exact_evs.mean(axis=0), "ko-", label="Exact")
ax.set_xlabel(r"$\theta/\pi$")
ax.set_ylabel(r"$\langle Z \rangle_{\text{avg}}$")
ax.legend()
plt.tight_layout()
plt.show()
Calcular valores esperados con ruido y sin mitigar
Un QuantumProgram contiene un circuito cuántico y un samplex en un formato adecuado para la primitiva Executor. MitigationTask.prepare construye un QuantumProgram a partir del circuito, los observables y los valores de los parámetros, y encierra el circuito en cajas con las opciones de boxing indicadas. Estableces la opción de boxing measure_annotations en "change_basis", que rota las mediciones a las bases necesarias, y añades add_tags: "unique_box" para que el simulador pueda asignar un modelo de ruido a las cajas del circuito. El programa se somete a Pauli twirling de forma predeterminada, lo que en hardware convierte el ruido coherente en el ruido de Pauli que asumen los métodos de mitigación. num_randomizations establece cuántas instancias con twirling de cada circuito se muestrean y shots_per_randomization establece cuántos shots recibe cada instancia, de modo que su producto es el presupuesto de shots por circuito.
Un usuario con acceso a una QPU ejecutaría el programa con un Executor construido a partir de un Backend del QiskitRuntimeService. Aquí pasarás un AerSimulator al Executor, que ejecuta el programa localmente, y usarás su opción de simulador layer_noise_model para inyectar un modelo de ruido de Pauli-Lindblad aleatorio en cada capa de puertas entrelazantes, generado con random_pauli_lindblad_noise de qiskit-mitigation con un error por puerta en capa (EPLG) de 0.003, típico de las QPU Heron actuales.
from qiskit.circuit import CircuitInstruction
from qiskit.quantum_info import PauliLindbladMap, pauli_basis
from qiskit_aer import AerSimulator
from qiskit_ibm_runtime import Executor
from qiskit_mitigation import MitigationTask, find_combined_unique_layers
def generate_random_pauli_lindblad_noise(
layers,
num_qubits: int,
eplg: float = 3e-3,
seed: int | None = None,
) -> list[tuple[CircuitInstruction, PauliLindbladMap]]:
"""Generate random Pauli-Lindblad noise over the full Pauli basis."""
layer_noise_model = []
for layer in layers:
layer_instructions = layer.operation.params[0]
contain_measurement = any(
inst.name.startswith("measure") for inst in layer_instructions
)
if contain_measurement: # do not add measurement noise
layer_noise = PauliLindbladMap.identity(num_qubits)
else:
edges = [
(inst.qubits[0]._index, inst.qubits[1]._index)
for inst in layer_instructions
] # extract the edges of the layer
basis_paulis = [p for p in pauli_basis(2) if np.sum(p.x + p.z)]
rng = np.random.default_rng(seed=seed)
num_generators = len(basis_paulis)
rates = rng.random(len(basis_paulis)) * eplg / num_generators
layer_noise = PauliLindbladMap.from_sparse_list(
[
(pauli.to_label(), edge, rate)
for pauli, rate in zip(basis_paulis, rates, strict=True)
for edge in edges
],
num_qubits=num_qubits,
)
layer_noise_model.append((layer, layer_noise))
return layer_noise_model
num_randomizations = 50
shots_per_randomization = 64
boxing_options = {
"measure_annotations": "change_basis",
"add_tags": "unique_box",
}
# find the unique layers to generate the layered noise for
box_layers = find_combined_unique_layers(
[circuit], custom_boxing_options=boxing_options
)
layer_noise_model = generate_random_pauli_lindblad_noise(
box_layers, num_qubits, seed=42
)
# a local noisy simulator in place of a QPU backend
noisy_executor = Executor(
AerSimulator(),
options={"simulator": {"layer_noise_model": layer_noise_model}},
)
task = MitigationTask()
program = task.prepare(
circuit=circuit,
observables=observables,
parameters=parameter_values,
num_randomizations=num_randomizations,
shots_per_randomization=shots_per_randomization,
custom_boxing_options=boxing_options,
)
program._semantic_role = "simulation" # flag the program for the simulator
unmitigated_res = task.postprocess(noisy_executor.run(program).result())
fig, ax = plt.subplots(figsize=(7, 4))
ax.plot(x_vals, exact_evs.mean(axis=0), "k-", label="Exact")
ax.errorbar(
x_vals,
np.mean(unmitigated_res.data.evs, axis=0),
np.mean(unmitigated_res.data.stds, axis=0),
fmt="o-",
label="Unmitigated",
)
ax.set_xlabel(r"$\theta/\pi$")
ax.set_ylabel(r"$\langle Z \rangle_{\text{avg}}$")
ax.legend()
plt.tight_layout()
plt.show()
Preparar un programa de ZNE con plegado digital de puertas y circuitos de muestra con distintas escalas de ruido
Prepara un programa de ZNE que contenga circuitos con escalas de ruido crecientes. Cada factor de ruido representa el múltiplo de puertas entrelazantes que contiene el circuito (un factor de ruido de 3.0 contiene tres veces más puertas entrelazantes que el circuito original). El circuito de cada factor de ruido se somete a twirling con el mismo número de aleatorizaciones y shots que el programa sin mitigar.
from qiskit_mitigation import GateFolding
task_gf = GateFolding()
program_zne = task_gf.prepare(
circuit=circuit,
observables=observables,
parameters=parameter_values,
num_randomizations=num_randomizations,
shots_per_randomization=shots_per_randomization,
custom_boxing_options=boxing_options,
noise_factors=[1, 3, 5], # ZNE option
)
Dibuja el primer paso de Trotter de la plantilla con twirling con factor de ruido 1 y con factor de ruido 3 para visualizar la amplificación. Para el factor de ruido 3.0, cada capa del circuito se sustituye por la capa, su inversa y de nuevo la capa.
import IPython.display
def first_trotter_step(template, noise_factor):
"""The template up to the closing barrier of its first Trotter step (two layers, each folded ``noise_factor`` times)."""
toy = template.copy_empty_like()
for instruction in template.data:
toy.append(instruction)
if (
instruction.operation.name == "barrier"
and instruction.operation.label.startswith(
f"R{2 * noise_factor - 1}@"
)
):
break
return toy
IPython.display.display(
first_trotter_step(program_zne.items[0].circuit, 1).draw(
"mpl", scale=0.7, plot_barriers=False
)
)
first_trotter_step(program_zne.items[1].circuit, 3).draw(
"mpl", fold=-1, plot_barriers=False
)

Por último, ejecuta el programa en el simulador con ruido. Sus resultados alimentan la extrapolación del siguiente paso.
program_zne._semantic_role = (
"simulation" # flag the program for the simulator
)
job_zne_results = noisy_executor.run(program_zne).result()
Extrapolar al límite de ruido cero con funciones de ajuste integradas y personalizadas
Ahora que tienes datos para cada escala de ruido, puedes ajustar una función a los datos y extrapolar hacia atrás hasta el valor de ruido cero para estimar un valor esperado del observable sin ruido. Hay muchas maneras de ajustar datos, y la elección correcta de la función depende de la naturaleza de los datos muestreados. GateFolding.postprocess proporciona varias funciones de extrapolación integradas, entre ellas linear, exponential y double_exponential. El método postprocess toma una lista de métodos de extrapolación, los evalúa en orden y devuelve el primer valor con valores físicamente válidos y varianza finita. Esto significa que el orden en que especificas los extrapoladores tiene un gran impacto en cuál se elige. Enviar fallback como extrapolador significa que, si no se encontraron respuestas válidas, se elegirá y devolverá el valor esperado del factor de ruido más pequeño. Si no se envía fallback y ningún extrapolador produce un valor físicamente válido, se devuelve el resultado con el std más pequeño. Bajo ruido de Pauli, un observable decae exponencialmente con el factor de ruido, así que con 30 capas (15 pasos de Trotter, 2 capas por paso) de puertas entrelazantes el extrapolador exponencial es la elección natural; también puedes extrapolar linealmente, que es una opción habitual cuando el decaimiento es pequeño.
Para usar una función de ajuste personalizada, pasa el extrapolador "custom" junto con una tupla custom_fit que contenga la función de ajuste, una lista con una estimación inicial para cada uno de sus parámetros y los límites. Estos se reenvían a scipy.optimize.curve_fit. El posprocesamiento se puede repetir con diferentes extrapoladores sin volver a ejecutar el programa. Aquí usas una función de ajuste personalizada de raíz cuadrada. Para visualizar la extrapolación, representa la magnetización promediada por sitios calculada en cada factor de ruido para , los tres ajustes y el promedio por sitios de los valores esperados extrapolados en el punto de ruido cero. Los tres puntos de datos parecen casi lineales, pero la extrapolación lineal queda visiblemente por debajo del valor exacto. Como se predijo, el ajuste exponencial es el que mejor estima el valor esperado sin ruido.
from scipy.optimize import curve_fit
zne_mitigated_exp = task_gf.postprocess(
job_zne_results, extrapolator=["exponential", "linear"]
)
zne_mitigated_lin = task_gf.postprocess(
job_zne_results, extrapolator=["linear"]
)
def sqrt_fit(x, a, b):
return a * np.sqrt(x) + b
custom_fit = (sqrt_fit, [1.0, 1.0], (-np.inf, np.inf))
zne_mitigated_custom = task_gf.postprocess(
job_zne_results,
extrapolator=["custom", "exponential", "linear"],
custom_fit=custom_fit,
)
# The result holds the expectation value at each noise factor and the extrapolated value, but
# not the fit parameters, so we refit the same models here to draw the curves.
noise_factors = np.array([1, 3, 5])
angle = 3 # theta = 0.15 pi, the angle with the largest unmitigated error
amplifications = zne_mitigated_exp.data.evs_noise_factors[:, angle, :].mean(
axis=0
)
x_fit = np.linspace(0, 5, 51)
fits = {
"exponential": (
lambda x, a, b: a * np.exp(b * x),
zne_mitigated_exp,
"black",
),
"linear": (lambda x, a, b: a * x + b, zne_mitigated_lin, "blue"),
"square-root": (sqrt_fit, zne_mitigated_custom, "green"),
}
plt.plot(
noise_factors, amplifications, "o", color="red", label="amplifications"
)
for name, (fit, res, color) in fits.items():
params, _ = curve_fit(fit, noise_factors, amplifications, p0=[1.0, -0.1])
plt.plot(
x_fit, fit(x_fit, *params), "--", color=color, label=f"{name} fit"
)
plt.plot(
[0],
res.data.evs[:, angle].mean(),
"*",
color=color,
markersize=15,
label=f"{name} extrapolation",
)
plt.plot(
[0],
exact_evs[:, angle].mean(),
"_",
color="red",
markersize=15,
markeredgewidth=3,
label="exact",
)
plt.xlabel("noise factor")
plt.ylabel(r"$\langle Z \rangle_{\text{avg}}$")
plt.legend()
plt.show()

Comparar los valores esperados mitigados
Representa la magnetización promediada por sitios de cada método a lo largo del barrido de ángulos de rotación de Ising. La extrapolación exponencial recupera la magnetización exacta dentro de sus barras de error, mientras que la extrapolación lineal elimina gran parte del sesgo pero se queda sistemáticamente corta, porque el decaimiento real es exponencial. El ajuste de raíz cuadrada se pasa muchísimo.
results = {
"Unmitigated": unmitigated_res,
"ZNE exponential": zne_mitigated_exp,
"ZNE linear": zne_mitigated_lin,
"ZNE square-root": zne_mitigated_custom,
}
fig, ax = plt.subplots(figsize=(7, 4))
ax.plot(x_vals, exact_evs.mean(axis=0), "k-", label="Exact")
for label, res in results.items():
ax.errorbar(
x_vals,
np.mean(res.data.evs, axis=0),
np.mean(res.data.stds, axis=0),
fmt="o-",
label=label,
)
ax.set_xlabel(r"$\theta/\pi$")
ax.set_ylabel(r"$\langle Z \rangle_{\text{avg}}$")
ax.legend()
ax.set_title(
f"Error-Mitigated Magnetization [{num_qubits}-qubit, {trotter_steps} Trotter steps]"
)
plt.tight_layout()
plt.show()
