Mejora de Valores Esperados: Absorción de Ruido Propagado (PNA)
En este tutorial, aprenderemos a aprovechar las últimas herramientas del ecosistema Qiskit para implementar un flujo de trabajo completamente personalizable con mitigación de errores. Presentaremos la técnica PNA y la usaremos para mitigar errores de compuerta. También usaremos TREX para mitigar errores de lectura y post-selección para mitigar errores no capturados en el modelo de ruido aprendido.
Esquema
- Dar una breve descripción general de
PNA - Crear un circuito cuántico Trotterizado y un observable. Transpilarlo al backend e incluir mediciones de post-selección.
- Usar
samplomaticpara aplicar twirling a capas de compuertas de 2 qubits y mediciones. Encontrar capas de 2 qubits únicas para reducir el costo de aprendizaje del ruido. - Usar
NoiseLearnerV3para aprender el modelo de error que afecta a las compuertas de 2 qubits y las mediciones. - Usar
qiskit-addon-pnapara generar un observable que mitigue el ruido - Usar el primitivo
qiskit-ibm-runtime.Executorpara generar las muestras brutas de la QPU que reflejen cada disparo para cada aleatorización de twirling y base medida - Usar
qiskit-addon-utilspara post-procesar los datos en un valor esperado mitigado.
¿Qué es la absorción de ruido propagado (PNA)?
Una técnica para mitigar errores de compuerta propagando el observable a través del canal de ruido inverso que afecta a las compuertas de 2 qubits, resultando en un observable que mitiga el ruido.
Las compuertas de 2 qubits en el experimento que queremos ejecutar se verán afectadas por un ruido considerable.
Si aprendemos el modelo de ruido, podemos aplicar su inverso y cancelar el ruido.
En lugar de implementar el canal de ruido inverso muestreándolo en la QPU como en PEC, podemos implementarlo clásicamente en el observable medido usando propagación de Pauli. Esto resulta en un observable más complejo que, cuando se mide, tiene el efecto de mitigar el ruido de compuerta aprendido.

Generar el circuito de Trotter en espejo y el observable
Para este experimento, estudiaremos la dinámica temporal de un modelo de Ising pateado de 30 sitios en una cadena de espines 1D. El Hamiltoniano considerado es:
,
donde describe el acoplamiento de espines vecinos más cercanos, , y el campo transversal global, , se establece en . Cuanto más lejos esté de un ángulo de Clifford (es decir, ), más difícil será propagar los generadores anti-ruido a través del circuito.
Para la elección del observable, consideraremos la magnetización promedio de un solo sitio, , donde es el número de sitios.
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-addon-pna qiskit-addon-utils qiskit-ibm-runtime samplomatic
import numpy as np
from qiskit import QuantumCircuit
from qiskit.quantum_info import Pauli, SparsePauliOp
num_qubits = 30
num_trotter_steps = 10
rx_angle = np.pi / 8
# Avg single-site magnetization
id_pauli = Pauli("I" * num_qubits)
observable = SparsePauliOp([id_pauli.dot(Pauli("Z"), [i]) for i in range(num_qubits)]) / num_qubits
# Implement Trotterized kicked-Ising model
circuit = QuantumCircuit(num_qubits)
for _step in range(num_trotter_steps):
circuit.rx(rx_angle, range(num_qubits))
for first_qubit in (1, 2):
for idx in range(first_qubit, num_qubits, 2):
# equivalent to Rzz(-pi/2):
circuit.sdg([idx - 1, idx])
circuit.cz(idx - 1, idx)
circuit.compose(circuit.inverse(), inplace=True)
circuit.measure_active()
circuit.draw("mpl", fold=-1)

A continuación, elegiremos una cadena de qubits en ibm_kingston que reporten tasas de error bajas y transpilaremos el circuito al backend.
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService
backend_name = "ibm_kingston"
service = QiskitRuntimeService()
backend = service.backend(backend_name, use_fractional_gates=True)
# Use a chain of low-noise qubits
layout = [
44,
45,
46,
47,
57,
67,
68,
69,
78,
89,
88,
87,
97,
107,
106,
105,
117,
125,
126,
127,
128,
129,
118,
109,
110,
111,
98,
91,
92,
93,
]
pm = generate_preset_pass_manager(backend=backend, initial_layout=layout, optimization_level=0)
isa_circuit = pm.run(circuit)
isa_observable = observable.apply_layout(isa_circuit.layout)
isa_circuit.draw("mpl", fold=-1)
qiskit_runtime_service._discover_account:WARNING:2025-11-10 14:30:57,148: Loading account with the given token. A saved account will not be used.

Aplicar twirling a las capas de compuertas de 2 qubits y mediciones, y encontrar capas únicas
Aquí nos aseguramos de que el gestor de pasos anote las cajas con anotaciones Twirl e InjectNoise, que nos permiten aprender el ruido que afectará a nuestro circuito y asociar ese ruido con su capa de circuito correspondiente.
enable_gates/enable_measure: True: Encajonar todas las capas de compuertas de 2 qubits y las mediciones terminales. Las compuertas de un solo qubit se incluirán como vestido izquierdo dentro de las cajas.measure_annotations: allIncluir anotacionesTwirlyChangeBasisen la caja de medicióntwirling_strategy: active: Aplicar twirling a todos los qubits activos en cada caja que contenga compuertas de entrelazamientoinject_noise_targets: gates: Las anotacionesInjectNoisedeben añadirse a todas las cajas anotadas conTwirlque contengan compuertas de entrelazamientoinject_noise_strategy: uniform_modification: Todas las capas de ruido deben escalarse de manera equivalente.
from samplomatic.transpiler import generate_boxing_pass_manager
# Box up circuit with Twirl and InjectNoise annotations
pm = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=True,
measure_annotations="all",
twirling_strategy="active",
inject_noise_targets="gates",
inject_noise_strategy="uniform_modification",
remove_barriers=True,
)
boxed_circuit = pm.run(isa_circuit)
draw_circ = QuantumCircuit(boxed_circuit.num_qubits)
draw_circ.append(boxed_circuit.data[0], qargs=boxed_circuit.data[0].qubits)
draw_circ.append(boxed_circuit.data[1], qargs=boxed_circuit.data[1].qubits)
draw_circ.draw("mpl", fold=-1, scale=0.3, idle_wires=False)

Generar el circuito plantilla y el samplex, definir cómo se muestreará el circuito
Aquí también añadimos mediciones de espectador y post-selección, que son necesarias para realizar la post-selección en las muestras generadas por Executor.
import samplomatic
from qiskit.transpiler import PassManager
from qiskit_addon_utils.noise_management.post_selection.transpiler.passes import (
AddPostSelectionMeasures,
AddSpectatorMeasures,
)
# Build template circuit and samplex for later use with the "Executor"
template_circuit, samplex = samplomatic.build(boxed_circuit)
# Add post-selection instructions to the template circuit
post_selection_pm = PassManager(
[
AddSpectatorMeasures(backend.coupling_map),
AddPostSelectionMeasures(x_pulse_type="rx"),
]
)
template_circuit = post_selection_pm.run(template_circuit)
draw_circ = template_circuit.copy_empty_like()
draw_circ.data = template_circuit.data[:324]
draw_circ.draw("mpl", fold=-1, scale=0.3, idle_wires=False)

Aprende el ruido
Antes de ejecutar los experimentos, aprendemos el modelo de ruido que afecta a las puertas de entrelazamiento y las mediciones del circuito. Contar con un modelo de ruido preciso es necesario para mitigar los errores de forma eficaz. Aprender el ruido justo antes de ejecutar los experimentos ofrece la mejor oportunidad de que el modelo de ruido describa fielmente el ruido real que afecta a las puertas durante la ejecución.
Antes de aprender el ruido, necesitamos encontrar las capas de 2 qubits únicas en nuestro circuito para minimizar el número de shots necesarios para aprender el ruido de todo el circuito. Usamos find_unique_box_instructions de samplomatic para obtener las capas únicas del circuito encajado, incluida la capa de medición. Estas son las capas que pasamos al aprendiz de ruido.
Una vez que conocemos las capas, podemos aprender el ruido. Hay algunos parámetros que consideramos:
num_randomizations: El número de circuitos aleatorios a utilizar por configuración de circuito de aprendizajeshots_per_randomization: Número total de shots a utilizar por circuito de aprendizaje aleatoriolayer_pair_depths: Las profundidades de circuito (medidas en número de pares) a utilizar en los experimentos de aprendizaje.post_selection: Usaremos post-selección basada en aristas durante el aprendizaje utilizando puertasrxpara implementar los pulsos post-medición
from qiskit_ibm_runtime.noise_learner_v3.noise_learner_v3 import NoiseLearnerV3
from qiskit_ibm_runtime.options import NoiseLearnerV3Options
from samplomatic.utils import find_unique_box_instructions
# Load noise learner data from a shared job
load_saved_nl_result = True
# Noise learning parameters
num_randomizations_nl = 64
shots_per_randomization_nl = 128
strategy = "edge"
enable_postsel = True
x_pulse_type = "rx"
# Find the unique instructions (layers) from boxed-up circuit
unique_2q_layers_and_meas = find_unique_box_instructions(
boxed_circuit, normalize_annotations=None, undress_boxes=True
)
noise_learner_params = {
"num_randomizations": num_randomizations_nl,
"shots_per_randomization": shots_per_randomization_nl,
"layer_pair_depths": [1, 2, 4, 8, 12, 16, 24, 32, 40, 48],
"post_selection": {
"enable": enable_postsel,
"strategy": strategy,
"x_pulse_type": x_pulse_type,
},
"experimental": {},
}
# set the options
noise_learner_options = NoiseLearnerV3Options(**noise_learner_params)
# run the noise learner job
noise_learner = NoiseLearnerV3(backend, noise_learner_options)
noise_learner_job = noise_learner.run(unique_2q_layers_and_meas)
noise_learner_result = noise_learner_job.result()
nl_metadata = noise_learner_params | {"layout": layout}
import matplotlib.pyplot as plt
hw_rates_1q = []
hw_rates_2q = []
for nlr in noise_learner_result[:2]:
plm_list = nlr.to_pauli_lindblad_map().to_sparse_list()
hw_rates_1q += [rate for (pstr, qubits, rate) in plm_list if len(pstr) == 1]
hw_rates_2q += [rate for (pstr, qubits, rate) in plm_list if len(pstr) == 2]
hw_rates_1q = sorted(hw_rates_1q)
hw_rates_2q = sorted(hw_rates_2q)
median_1q = hw_rates_1q[len(hw_rates_1q) // 2]
median_2q = hw_rates_2q[len(hw_rates_2q) // 2]
fig, ax = plt.subplots(1, 1, figsize=(14, 5))
ax.scatter(
(hw_rates_1q),
[(i) / (len(hw_rates_1q) - 1) for i in range(len(hw_rates_1q))],
color="red",
label="1q rates",
)
ax.set_xscale("log")
ax.set_ylim(0, 1.1)
ax.vlines(median_1q, 0, 1, color="red")
ax.text(median_1q * 1.1, 0.1, f"{median_1q:.2e}")
ax.scatter(
(hw_rates_2q),
[(i) / (len(hw_rates_2q) - 1) for i in range(len(hw_rates_2q))],
color="blue",
label="2q rates",
)
ax.set_xscale("log")
ax.set_ylim(0, 1.1)
ax.vlines(median_2q, 0, 1, color="blue")
ax.text(median_2q * 1.1, 0.2, f"{median_2q:.2e}")
ax.set_title("Learned noise rates")
ax.set_xlabel("Noise rate")
ax.set_yticks([])
plt.legend()
<matplotlib.legend.Legend at 0x321dd63f0>

Asocia las cajas del circuito con el ruido aprendido
Aquí creamos un mapeo entre los IDs de referencia de InjectNoise de cada caja y el modelo de ruido aprendido (PauliLindbladMap) que afecta a las puertas de entrelazamiento en esa caja.
from samplomatic.annotations import InjectNoise
from samplomatic.utils import get_annotation
# map inject noise refs to pauli lindblad maps
refs_to_noise_models = {}
for instruction, result in zip(unique_2q_layers_and_meas, noise_learner_result, strict=False):
if inject_noise_annot := get_annotation(instruction.operation, InjectNoise):
refs_to_noise_models[inject_noise_annot.ref] = result.to_pauli_lindblad_map()
Propaga el observable a través del anti-ruido aprendido para obtener un observable que mitiga el ruido
Como se explicó anteriormente, esto se realiza en dos pasos. Primero, propagamos un generador de anti-ruido hasta el final del circuito. Después de eso, propagamos el observable a través de ese generador evolucionado. Este proceso se repite para cada generador de anti-ruido en el circuito. En esta implementación, cada generador en una capa dada se propaga hasta el final del circuito en paralelo. Además, se usa multiprocesamiento de Python para realizar tanto la propagación hacia adelante del anti-ruido como la retropropagación del observable en paralelo. Esto evita una acumulación de generadores evolucionados en memoria y maximiza los recursos de cómputo.
Al ejecutar PNA, siempre tendrás que proporcionar un circuito ruidoso y un observable. Si tu circuito ruidoso es un circuito encajado con anotaciones InjectNoise, tendrás que proporcionar el mapeo que creamos en el paso anterior. También se puede pasar un circuito no encajado que contenga instrucciones PauliLindbladError de qiskit-aer. En ese caso, no es necesario proporcionar refs_to_noise_models. Además de las entradas principales, los usuarios querrán considerar:
max_err_terms: El número de términos a conservar en cada generador de anti-ruido mientras se propaga hacia adelante. Permitir que sea mayor generalmente aumenta la precisión, aunque no se garantiza que este comportamiento sea monótono.max_obs_terms: El número de términos a conservar en el observable que mitiga el ruido, , mientras se retropropaga a través del anti-ruido evolucionado. Los valores más grandes generalmente aumentan la precisión, aunque no se garantiza que lo hagan de forma monótona.num_processes: El número de núcleos a dedicar al proceso. Recuerda que los generadores se propagan hacia adelante y se aplican al observable en paralelo.search_step: El paso de retropropagación utiliza un método codicioso para conjugar aproximadamente dos operadores en la base de Pauli. Este método puede acelerarse aumentandosearch_step. Consulta la documentación de pauli-prop para más información.num_to_measure: Aunque esta variable no es una entrada degenerate_noise_mitigating_observable, la usamos para controlar cuántos términos de queremos medir realmente. Aquí solo mediremos los 30 términos principales, que son los términos originales de nuestro observable. Los términos han sido reescalados de modo que medirlos tenga el efecto de mitigar el ruido de puertas aprendido. Aunque solo medimos 30 términos de , a menudo sigue siendo útil permitir que crezca, ya que eso aumenta la precisión de los factores de escala de los términos principales.
from qiskit_addon_pna import generate_noise_mitigating_observable
# PNA parameters
num_processes = 8
max_err_terms = 10_000
max_obs_terms = 10_000
num_to_measure = num_qubits
obs_tilde_isa = generate_noise_mitigating_observable(
boxed_circuit,
isa_observable,
refs_to_noise_models,
max_err_terms=max_err_terms,
max_obs_terms=max_obs_terms,
num_processes=num_processes,
print_progress=True,
search_step=8,
)
p_2_v = {p: v for v, p in enumerate(layout)}
obs_tilde_virtual = SparsePauliOp.from_sparse_list(
[
(pstr, [p_2_v[p] for p in p_qubits], coeff)
for (pstr, p_qubits, coeff) in obs_tilde_isa.to_sparse_list()
],
num_qubits=num_qubits,
)
obs_tilde_virtual = obs_tilde_virtual[np.argsort(np.abs(obs_tilde_virtual.coeffs))[::-1]][
:num_to_measure
]
Finished! 13560 / 13560 generators propagated.
obs_tilde_isa = obs_tilde_isa[np.argsort(np.abs(obs_tilde_isa.coeffs))][::-1]
plt.xscale("log")
plt.yscale("log")
plt.title(r"$\tilde{O}$ coeff magnitudes")
plt.ylabel("Magnitude")
plt.xlabel("Pauli term index")
plt.plot(np.abs(obs_tilde_isa.coeffs), ".")
[<matplotlib.lines.Line2D at 0x16b69e840>]

Transforma las bases de medición a la forma canónica
A continuaci ón, encontraremos un conjunto mínimo de bases a medir de forma que podamos cubrir completamente todos los términos de Pauli del observable medido (muchos observables pueden medirse simultáneamente si conmutan qubit a qubit). Como solo medimos los términos de nuestro observable original, que es la suma de todos los Paulis de Z individual, se necesita una sola base: la base todo-Z.
Además de encontrar un conjunto de bases de medición de Pauli, necesitamos mapear estos términos de Pauli a la forma canónica que espera el primitivo Executor. Para más información sobre el ordenamiento canónico de qubits, visita la documentación de samplomatic.
from qiskit_addon_utils.exp_vals.measurement_bases import get_measurement_bases
meas_box = boxed_circuit.data[-1]
canonical_qubits = [
idx for idx, qubit in enumerate(boxed_circuit.qubits) if qubit in meas_box.qubits
]
c_2_p = {c: p for c, p in enumerate(canonical_qubits)} # canonical -> physical
p_2_v = {p: v for v, p in enumerate(layout)} # physical -> virtual
c_2_v = {c: p_2_v[p] for c, p in c_2_p.items()} # canonical -> virtual
meas_bases, bases_reverser = get_measurement_bases(obs_tilde_virtual)
meas_bases_canonical = [
np.array([base[c_2_v[c]] for c in range(num_qubits)], dtype=np.uint8) for base in meas_bases
]
Especifica cómo muestrear en el QuantumProgram
El QuantumProgram es donde especificamos cómo muestrear el experimento:
template_circuit: El circuito que contiene todas las puertas necesarias para implementar todas las aleatorizaciones deseadas (de las aleatorizaciones de twirling, parámetros, etc.).samplex: Un objeto que define una distribución de probabilidad sobre todas las aleatorizaciones posibles del circuito de las que muestrear.samplex_arguments: Ligaduras necesarias para definir completamente el samplexbasis_changes: Aquí es donde especificamos un conjunto de bases a medir que cubran todos los términos de Pauli del observable medido.noise_scales.ref: Establecemos la escala de cada capa de ruido en0.0para evitar que se inyecte ruido adicional en nuestras muestraspauli_lindblad_maps: Requerido si se pasannoise_scales. Esto simplemente mapea las capas de ruido al modelo de ruido asociado.
shape: Una tupla de forma para extender la forma implícita definida porsamplex_arguments. Los ejes no triviales introducidos por esta extensión enumeran las aleatorizaciones.
from qiskit_ibm_runtime import QuantumProgram
# Control the # of shots during execution
shots_per_randomization_exec = 64
num_randomizations_exec = 6144
# Zero out the noise to prevent noise from being injected during execution.
# We only added InjectNoise annotations so PNA could associate the noise
# to layers in the circuit
samplex_inputs = {f"noise_scales.{ref}": 0.0 for ref in refs_to_noise_models}
samplex_inputs |= {"pauli_lindblad_maps": refs_to_noise_models}
# Specify the bases to measure
bases_broadcastable = np.expand_dims(np.array(meas_bases_canonical), axis=1)
samplex_inputs |= {"basis_changes": {"basis0": bases_broadcastable}}
# Convert samplex_inputs into a dict to pass to QuantumProgram
samplex_arguments = samplex.inputs().make_broadcastable().bind(**samplex_inputs)
# Instantiate the QuantumProgram with the specified parameters
program = QuantumProgram(shots=shots_per_randomization_exec)
program.append(
circuit=template_circuit,
samplex=samplex,
samplex_arguments=samplex_arguments,
shape=(num_randomizations_exec),
)
Muestrea el circuito usando el prototipo de primitivo Executor
Ahora que hemos definido nuestro QuantumProgram, ejecutar el experimento es sencillo. Simplemente instanciamos el objeto Executor, le proporcionamos el backend y ejecutamos el programa.
from qiskit_ibm_runtime import Executor
# Execute (sample) the circuit
executor = Executor(backend)
job_exec = executor.run(program)
exec_results = job_exec.result()
Post-procesa las muestras para calcular un valor esperado con mitigación de errores
Para calcular un valor esperado con mitigación de errores:
- Calcularemos los factores de escala TREX basados en el ruido aprendido que afecta a las mediciones
- Generaremos una máscara para conservar solo las muestras post-seleccionadas
- Usaremos la función
executor_expectation_valuesdeqiskit-addon-utilspara combinar todos los datos en un valor esperado con mitigación de errores.
from qiskit_addon_utils.exp_vals.expectation_values import executor_expectation_values
from qiskit_addon_utils.noise_management import trex_factors
from qiskit_addon_utils.noise_management.post_selection import PostSelector
# Computing the TREX factors
measurement_noise_map = noise_learner_result[2].to_pauli_lindblad_map()
trex_rescale_factors = trex_factors(measurement_noise_map, bases_reverser)
# Post-select the results
post_selector = PostSelector.from_circuit(
circuit=template_circuit, coupling_map=backend.coupling_map
)
# Compute the ps mask for filtering results
mask = post_selector.compute_mask(exec_results[0], strategy="edge")
# Compute expvals using post selected results
results = executor_expectation_values(
exec_results[0]["meas"],
bases_reverser,
meas_basis_axis=0,
avg_axis=1,
measurement_flips=exec_results[0]["measurement_flips.meas"],
pauli_signs=exec_results[0].get("pauli_signs", None),
postselect_mask=mask,
rescale_factors=trex_rescale_factors,
)
bases_reverser_unmit = {Pauli("Z" * num_qubits): [observable]}
args = [
(bases_reverser_unmit, None, None),
(bases_reverser, None, None),
(bases_reverser, None, trex_rescale_factors),
(bases_reverser, mask, None),
(bases_reverser, mask, trex_rescale_factors),
]
evs = []
for reverser, postsel_mask, factors in args:
# Compute expvals using post selected results
res_ps = executor_expectation_values(
exec_results[0]["meas"],
reverser,
meas_basis_axis=0,
avg_axis=1,
measurement_flips=exec_results[0]["measurement_flips.meas"],
pauli_signs=exec_results[0].get("pauli_signs", None),
postselect_mask=postsel_mask,
rescale_factors=factors,
)
res_ps = np.array(res_ps)
evs.append(res_ps[:, 0][0])
experiments = ["PNA", "PNA+TREX", "PNA+PS", "PNA+PS+TREX"]
colors = ["#d9d9d9", "#b0b0b0", "#7f7f7f", "#4c4c4c"]
plt.bar(experiments, evs[1:], color=colors)
plt.axhline(y=1, color="green", linestyle="--", linewidth=2, label="Ideal")
plt.axhline(y=evs[0], color="red", linestyle="--", linewidth=2, label="Unmitigated")
plt.ylabel("Expectation value", fontsize=14)
plt.title(r"30q Mirrored Ising, 10 Trotter steps, $\theta_{rx}=\frac{\pi}{8}$", fontsize=14)
plt.legend(loc="upper left", bbox_to_anchor=(1.05, 1), borderaxespad=0.0)
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
