Mejorar los valores esperados mediante la absorción de ruido propagado (PNA)
Tiempo de ejecución estimado: 10 minutos en un procesador Heron (NOTA: Se trata únicamente de una estimación. (El tiempo de ejecución puede variar.)
Resultados del aprendizaje
Una vez completado este tutorial, los usuarios deberían comprender:
- En qué consiste la absorción de ruido propagado (PNA) y cómo mitiga el ruido de las puertas de dos qubits al absorber los canales de ruido inversos aprendidos en el observable medido
- Cómo utilizar
samplomaticpara enmarcar y anotar capas de circuitos con el fin de realizar giros, cambios de base e inyección de ruido - Cómo aprender el ruido de capa con
[NoiseLearnerV3]( /docs/guides/directed-execution-model#noiselearnerv3 ) y propagarlo a un observable que mitigue el ruido medianteqiskit-addon-pna - Cómo obtener muestras de circuitos aleatorios con las
QuantumProgramclases yExecutoren « Qiskit Runtime », y combinar PNA con TREX y la selección posterior
Requisitos previos
Recomendamos a los usuarios que se familiaricen con los siguientes temas antes de seguir este tutorial:
- El flujo de trabajo de Qiskit Patterns
- Uso de la primitiva «Estimator» para calcular los valores esperados de una variable observable
- Técnicas de mitigación de errores como el «Pauli twirling» y TREX, que se tratan en «Combinar opciones de mitigación de errores con la primitiva Estimator»
En segundo plano
En este tutorial, mostramos cómo aprovechar las herramientas avanzadas de mitigación de errores de Qiskit para mejorar la estimación del valor esperado en experimentos cuánticos con ruido.
¿Qué es la absorción del ruido propagado (PNA)?
La absorción de ruido propagada es una técnica para mitigar los errores de las puertas cuánticas mediante la propagación de la observable a través del canal de ruido inverso que afecta a las puertas de dos qubits, lo que da lugar a una observable que mitiga el ruido.
Podemos utilizar la mitigación de errores cuánticos para extraer valores esperados útiles a partir de hardware cuántico con ruido sin necesidad de una tolerancia total a los fallos. La PNA se centra específicamente en absorber los efectos del ruido en el propio observable, en lugar de modificar el funcionamiento del circuito.
Cada puerta con ruido de un circuito cuántico puede modelarse como una puerta ideal seguida de un canal de ruido. La PNA aprende o caracteriza estos canales de ruido y define sus inversas. En lugar de incorporar las operaciones inversas en la ejecución del hardware (lo cual suele ser inviable), la PNA propaga los canales de ruido inversos a lo largo del circuito y los aplica a la observable. Este proceso transforma el operador observable en un nuevo operador , de tal forma que la medición de en el circuito con ruido arroja el mismo valor esperado que la medición de en un circuito ideal, libre de ruido.
Esto se puede resumir así:
- Modela cada puerta con ruido como una operación ideal seguida de un canal de ruido .
- Determina o calcula cada utilizando herramientas de caracterización del ruido.
- Definir y propagar los mapas de ruido inversos a lo largo del circuito utilizando técnicas de transferencia de Pauli.
- Incorpora estas inversas en el observable, lo que da como resultado un operador con ruido atenuado .
Cuando el ruido se describe como un canal de Pauli (o, de forma más general, como un canal de Pauli-Lindblad disperso), esta propagación puede llevarse a cabo de manera eficiente mediante la propagación de Pauli. La propagación de Pauli ofrece un marco para aproximar cómo se transforman los canales de ruido inversos a medida que atraviesan capas de operaciones de Clifford y no Clifford, al tiempo que se controla la complejidad computacional.
Al trasladar la mitigación al ámbito observable, la PNA evita la elevada sobrecarga de muestreo que, de otro modo, se produciría al insertar operaciones de corrección física en el circuito. En su lugar, se ejecuta el circuito ruidoso original, mientras que el observable se transforma en un nuevo operador cuyo valor esperado anula los efectos del ruido.
El flujo de trabajo de la PNA
El proceso puede entenderse a través de las siguientes etapas conceptuales. El primer esquema ilustra un experimento estándar con ruido.
Si aprendemos el modelo de ruido, podemos aplicar su inversa y eliminar el ruido.
En lugar de implementar el canal de ruido inverso mediante muestreo en la QPU, como ocurre en la cancelación probabilística de errores (PEC), lo aplicamos de forma clásica al observable medido utilizando la propagación de Pauli. El observable resultante mitiga de forma efectiva el ruido de la puerta aprendido cuando se mide.
Mitigación modular de errores con Samplomatic y Executor
El enfoque PNA se basa en una arquitectura modular de mitigación de errores de Qiskit. Esta arquitectura utiliza la samplomatic biblioteca junto con las QuantumProgram clases y Executor (añadidas a Qiskit Runtime en qiskit-ibm-runtimev0.47.0 ) para que técnicas como la absorción de ruido propagado y el giro de Pauli sean combinables y reutilizables en diferentes experimentos.
En lugar de integrar la lógica de mitigación en la propia definición del circuito, la mitigación se expresa de forma declarativa mediante samplomatic anotaciones y se gestiona mediante programación a través de Executor, que controla cómo se generan, ejecutan y procesan posteriormente los circuitos aleatorios.
En este tutorial implementamos un patrón de Qiskit para demostrar cómo PNA puede propagar canales de ruido de Pauli inversos y modificar el observable en consecuencia, con el fin de mejorar la estimación de los valores esperados en QPU con ruido.
Resumen del flujo de trabajo
- Paso 1: Traslación al problema cuántico
- Construye un modelo de Ising «kicked» trotterizado con simetría especular y una observable objetivo.
- Paso 2: Caracterizar y propagar el ruido
- Utiliza
samplomaticpara identificar y anotar las capas únicas de dos qubits y las mediciones del circuito. - Descubre el ruido que afecta a cada capa concreta utilizando
NoiseLearnerV3. - Asigna cada
InjectNoiseanotación a su modelo de ruido aprendido correspondiente. - Utiliza
qiskit-addon-pnapara propagar los canales de ruido inversos hacia adelante a través del circuito e integrarlos en el observable objetivo.
- Utiliza
- Paso 3: Realizar experimentos cuánticos
- Define un
QuantumProgrampara especificar un muestreo aleatorio mediantesamplexy ejecuta los experimentos en el backend utilizando elExecutor.
- Define un
- Paso 4: Reconstruir y analizar los resultados
- Compara las estrategias de mitigación (PNA, PNA+TREX, PNA+PS, PNA+PS+TREX) y visualiza la mejora con respecto a los resultados sin medidas de mitigación.
Requisitos
Antes de empezar este tutorial, asegúrate de tener instalado lo siguiente:
- Qiskit SDK v2.2 o posterior, con soporte para visualización
- Qiskit Runtime v0.47 o posterior (
pip install qiskit-ibm-runtime) - Samplomatic v0.13 o o posterior (
pip install samplomatic) - Complemento PNA Qiskit (
pip install qiskit-addon-pna) - Utilidades del complemento de Qiskit (
pip install qiskit-addon-utils)
Configuración
from qiskit import QuantumCircuit
from qiskit.quantum_info import Pauli, SparsePauliOp
from qiskit.transpiler import generate_preset_pass_manager, PassManager
from qiskit_ibm_runtime import (
QiskitRuntimeService,
QuantumProgram,
Executor,
NoiseLearnerV3,
)
from qiskit_addon_utils.exp_vals.measurement_bases import (
get_measurement_bases,
)
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
from qiskit_addon_utils.noise_management.post_selection.transpiler.passes import (
AddPostSelectionMeasures,
AddSpectatorMeasures,
)
from qiskit_addon_pna import generate_noise_mitigating_observable
import samplomatic
from samplomatic.transpiler import generate_boxing_pass_manager
from samplomatic.annotations import InjectNoise
from samplomatic.utils import get_annotation, find_unique_box_instructions
import numpy as np
import matplotlib.pyplot as plt
# Selects a connected chain of low-error qubits on the target backend.
# The mirrored kicked-Ising circuit is a 1D chain, so we only need a line
# of connected physical qubits; the helper walks the backend's coupling map
# and grows a chain along the lowest-error two-qubit edges, so it works for
# any backend rather than relying on a hardcoded layout.
def find_qubit_chain(backend, length):
"""Find a connected chain of ``length`` physical qubits on ``backend``.
The chain is grown greedily along the lowest-error two-qubit edges, so it
favors better-performing qubits. Because the mirrored kicked-Ising circuit
is a 1D chain, a connected line is all we need.
"""
target = backend.target
# Identify the native two-qubit gate and build a per-edge error lookup.
two_qubit_gate = next(
name
for name in target.operation_names
if target[name]
and all(q is not None and len(q) == 2 for q in target[name])
)
edge_error = {
frozenset(qargs): (
1.0 if props is None or props.error is None else props.error
)
for qargs, props in target[two_qubit_gate].items()
}
graph = backend.coupling_map.graph.to_undirected(multigraph=False)
neighbors = {n: list(graph.neighbors(n)) for n in graph.node_indices()}
def first_chain_from(start):
path, visited = [start], {start}
def grow():
if len(path) == length:
return True
node = path[-1]
order = sorted(
neighbors[node],
key=lambda m: edge_error.get(frozenset((node, m)), 1.0),
)
for nxt in order:
if nxt not in visited:
visited.add(nxt)
path.append(nxt)
if grow():
return True
path.pop()
visited.remove(nxt)
return False
return path if grow() else None
def chain_cost(path):
return sum(
edge_error.get(frozenset((path[i], path[i + 1])), 1.0)
for i in range(len(path) - 1)
)
# Try low-degree qubits first (the natural ends of long chains) and keep
# the lowest-error chain found.
best_path, best_cost = None, float("inf")
for start in sorted(neighbors, key=lambda n: len(neighbors[n])):
chain = first_chain_from(start)
if chain is not None and (cost := chain_cost(chain)) < best_cost:
best_path, best_cost = chain, cost
if best_path is None:
raise ValueError(
f"Could not find a connected chain of {length} qubits "
f"on '{backend.name}'."
)
return best_pathEjemplo de simulador a pequeña escala
La PNA mitiga el ruido físico de las puertas de dos qubits de un procesador cuántico concreto. El flujo de trabajo depende de dos servicios de hardware que no tienen un equivalente significativo en un simulador ideal:
NoiseLearnerV3caracteriza experimentalmente el canal de ruido de Pauli-Lindblad disperso asociado a cada capa única de dos qubits del circuito transpilado. En un simulador sin ruido no hay ruido que aprender, y el canal inverso propagado sería el canal de identidad.- La función
Executor«primitive» muestrea los circuitos aleatorios y retorcidos generados porsamplomaticen un backend.
En principio, se podría sustituir por un modelo de ruido sintético. Por ejemplo, podrías adjuntar PauliLindbladError instrucciones con Qiskit Aer y pasar el circuito con ruido resultante directamente a generate_noise_mitigating_observable. Sin embargo, esto solo valida la contabilidad clásica frente al ruido que tú mismo has introducido, y desvirtúa el objetivo de la técnica. Por ese motivo, nos saltamos el ejemplo del simulador y mostramos el flujo de trabajo completo de PNA directamente en el hardware, desglosando a continuación cada paso del patrón de Qiskit.
Ejemplo de hardware a gran escala
Ahora ejecutamos el flujo de trabajo completo de PNA en un modelo de Ising de 30 sitios con «kicking», ejecutado en un hardware de l IBM Quantum®, siguiendo los cuatro pasos de un patrón de Qiskit.
Paso 1: Traslación a un problema cuántico
Genera el circuito de Trotter reflejado y el observable
En este experimento, estudiaremos la dinámica temporal de un modelo de Ising con 30 sitios y «kicked» en una cadena de espines « 1D ». El hamiltoniano que se considera es:
,
donde describe el acoplamiento entre los espines más próximos, , y el campo transversal global, , se establece en . Cuanto más se aleja de un ángulo de Clifford (es decir, ), más difícil resulta propagar los generadores de antirruido a través del circuito.
Para la elección de la magnitud observable, consideraremos la magnetización media por sitio, , donde es el número de sitios.
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)
# Append the inverse circuit to complete the mirroring
circuit.compose(circuit.inverse(), inplace=True)
circuit.measure_active()
circuit.draw("mpl", fold=-1)Output:
Paso 2: Optimizar el problema para su ejecución en hardware
El siguiente paso consiste en preparar nuestro circuito Trotter duplicado para su ejecución en un hardware real de IBM Quantum. Para ejecutar un programa en una QPU no basta con construir el circuito abstracto, ya que debemos optimizarlo para que:
-
Respeta el conjunto de puertas y la conectividad nativas del backend. La transpilación mapea el circuito lógico a un circuito ISA compatible con el backend de destino. Esto garantiza que cada interacción entre una puerta y un qubit sea físicamente realizable.
-
Podemos caracterizar el ruido a nivel de las capas del circuito. La PNA se basa en el aprendizaje y la propagación de canales de ruido inversos. Para hacerlo de forma eficiente, dividimos el circuito transpilado en capas «encajonadas» únicas de dos qubits. Estas casillas nos permiten asociar cada capa del circuito con su propio modelo de ruido aprendido.
-
Podemos introducir modelos de ruido realistas en PNA. Una vez que el circuito se ha encapsulado, utilizamos el
NoiseLearnerV3servicio para determinar experimentalmente los canales de ruido de Pauli que afectan a cada capa única de dos qubits. A continuación, estos modelos entrenados se vinculan de nuevo al circuito a través de las anotaciones de Samplomatic.
En definitiva, este paso salva la distancia entre un circuito especular idealizado y un circuito listo para su implementación en hardware con modelos de ruido aprendidos. Con esta configuración, el PNA puede propagar canales de ruido inversos a través del circuito y ajustar la observable en consecuencia.
Conectarse al backend y compilarlo en un circuito ISA
En primer lugar, inicializamos el servicio Qiskit Runtime y seleccionamos un backend. Inicia sesión con tu propia cuenta siguiendo las instrucciones para guardar tus credenciales; a partir de ese QiskitRuntimeService() momento, el sistema las recuperará automáticamente.
# Initialize the Qiskit Runtime service using your saved credentials
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=num_qubits
)
# Re-fetch with fractional gates enabled (least_busy does not forward this)
# Fractional gates are enabled so the non-Clifford Rx rotations are supported natively.
backend = service.backend(backend.name, use_fractional_gates=True)
print(f"Selected backend: {backend.name}")Output:
Selected backend: ibm_fez
A continuación, elegimos una cadena conectada de qubits en el backend y compilamos el circuito en ella.
Utilizando la find_qubit_chain función auxiliar definida en la sección «Configuración», seleccionamos una línea de num_qubits qubits físicos conectados. A continuación, realizamos la transpilación con optimization_level=0 y fijamos esta cadena como el initial_layout, lo que conserva exactamente la estructura por capas de puertas de dos qubits del circuito reflejado. Esa estructura es en la que se basan las etapas de «boxing» y de aprendizaje de ruido, por lo que aquí hay que evitar un nivel de optimización más alto (que anularía las puertas reflejadas).
# Find a connected, low-error chain of qubits on the chosen backend
layout = find_qubit_chain(backend, num_qubits)
# Transpile the circuit for the target backend, pinning the chain as the layout.
# optimization_level=0 preserves the mirrored two-qubit-gate layers that the
# boxing and noise-learning steps rely on.
pm = generate_preset_pass_manager(
backend=backend, optimization_level=0, initial_layout=layout
)
isa_circuit = pm.run(circuit)
isa_observable = observable.apply_layout(isa_circuit.layout)
isa_circuit.draw("mpl", fold=-1)Output:
Gira las capas de puertas de dos qubits y las mediciones, y encuentra las capas únicas
Utilizamos samplomatic para delimitar el circuito e identificar capas únicas de dos qubits. Un bloque es una estructura que agrupa instrucciones para que, posteriormente, se puedan aplicar de manera uniforme determinadas intenciones o anotaciones a todas las puertas que lo componen.
generate_boxing_pass_managerEn este caso, llamamos al método, que va más allá de la simple identificación de capas de dos qubits. Desempeña varias funciones clave:
- Agrupa todas las capas de dos qubits del circuito,
- Aplica las
Twirlanotaciones yChangeBasisa esas capas, - Agrupa las operaciones de medición en sus propias secciones enmarcadas, y
- Aplica la
InjectNoiseanotación a cada capa de dos qubits.
Estas anotaciones definen cómo se gestionan el ruido, los cambios de base y el giro a lo largo de todo el circuito. Además, establecen la estructura que se utilizará posteriormente para el aprendizaje y la reducción del ruido.
Las opciones de configuración más importantes son:
enable_gates/enable_measures: True: Enmarca todas las capas de puertas de dos qubits y las mediciones finales. Las puertas de un solo qubit se representan con la orientación «left-dressed» dentro de los recuadros.measure_annotations: all: IncluyeTwirllas anotaciones yChangeBasisen el recuadro de medidas.twirling_strategy: active: Gira todos los qubits activos de cada casilla que contenga puertas de entrelazamiento.Twirl``inject_noise_targets: gates: AñadirInjectNoiseanotaciones a todos los recuadros anotados que contengan puertas de entrelazamiento.inject_noise_strategy: uniform_modification: Ajusta el volumen de todas las capas de ruido de forma equivalente en todo el circuito.
# 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",
)
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)Output:
Genera el circuito de plantilla y samplex, que definen cómo se muestreará el circuito.
Aquí también añadimos las mediciones de espectador y de postselección, que son necesarias para llevar a cabo la postselección en las muestras generadas por el Executor.
# 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)Output:
Aprende el sonido utilizando NoiseLearnerV3
Antes de poder aplicar el PNA para la mitigación de errores, primero debemos caracterizar el ruido que actúa sobre cada una de las capas de dos qubits y sobre la capa de medición de nuestro circuito. Para ello, utilizamos el NoiseLearnerV3 programa para aprender de forma experimental los modelos de ruido de cada capa identificada anteriormente. El alumno lleva a cabo experimentos de tipo «benchmarking» que estiman el ruido del canal que afecta a cada capa y devuelve un objeto de resultado que contiene el modelo aprendido.
Comenzamos identificando las capas únicas de nuestro circuito utilizando find_unique_box_instructions de samplomatic. Esto garantiza que solo aprendamos el ruido una vez por cada tipo de capa distinto, lo que minimiza el número de experimentos y el coste total de las ejecuciones. La lista de capas resultante se envía al modelo de aprendizaje de ruido.
Hay algunos parámetros clave que determinan cómo se aprende el ruido:
num_randomizations: Número de circuitos aleatorios utilizados por configuración de aprendizaje.shots_per_randomization: Número de disparos realizados por cada circuito de aprendizaje aleatorio.layer_pair_depths: Las profundidades del circuito (medidas en número de pares) que se utilizarán en los experimentos de aprendizaje.post_selection: Permite la postselección basada en flancos medianterxpuertas para aplicar pulsos posteriores a la medición.
# Noise learning parameters
num_randomizations_nl = 64
shots_per_randomization_nl = 128
# Match the deepest noise-learning depth to the depth of the circuit being
# mitigated. ``layer_pair_depths`` are measured in layer pairs (a layer + its
# inverse), so the deepest value is half the circuits's two-qubit-layer depth.
# Learning to this depth improves the quality of the mitigation.
depth_2q = isa_circuit.depth(lambda instr: instr.operation.num_qubits == 2)
max_layer_pair_depth = depth_2q // 2
# Use a fixed schedule of learning depths, but drop any that exceed the circuit's
# depth and always cap the deepest value at ``max_layer_pair_depth`` so we never
# learn deeper than the circuit being mitigated
candidate_depths = [1, 2, 4, 8, 12, 16, 24, 32, 40, 48]
layer_pair_depths = sorted(
{d for d in candidate_depths if d < max_layer_pair_depth}
| {max_layer_pair_depth}
)
# Find the unique instructions (layers) from the boxed-up circuit
unique_2q_layers_and_meas = find_unique_box_instructions(
boxed_circuit, normalize_annotations=None, undress_boxes=True
)
# Configure and run the noise learner on the unique layers.
# Options can be passed directly as a dictionary.
noise_learner_options = {
"num_randomizations": num_randomizations_nl,
"shots_per_randomization": shots_per_randomization_nl,
"layer_pair_depths": layer_pair_depths,
"post_selection": {
"enable": True,
"strategy": "edge",
"x_pulse_type": "rx",
},
"environment": {"job_tags": ["TUT_PNA"]},
}
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()Visualizar las tasas de ruido aprendidas
Una vez aprendidos los modelos de ruido, podemos examinar la distribución de las tasas de error inferidas tanto para las operaciones de un qubit como para las de dos qubits. El código que figura a continuación extrae las representaciones de Pauli-Lindblad a partir de los resultados del ruido aprendido y recopila las tasas de ruido correspondientes.
Para cada capa entrenada:
- Convertimos el modelo de ruido en una lista dispersa de
(pstr, qubits, rate)tuplas, dondepstres la cadena de Pauli que actúa sobre los qubits dados yratees la tasa de error asociada. - Desglosamos las tasas en términos de un qubit (
len(pstr) == 1) y de dos qubits (len(pstr) == 2). - A continuación, se ordenan las listas de tasas y se calculan sus valores medianos.
Representamos gráficamente las distribuciones de la tasa de ruido de un qubit (rojo) y de dos qubits (azul) en una escala logarítmica, con sus valores medianos marcados por líneas verticales, de modo que podamos comparar las magnitudes relativas de los generadores de Pauli-Lindblad aprendidos. El orden de las tasas de un qubit y de dos qubits depende del dispositivo y de las capas específicas que se estén caracterizando; en esta serie de pruebas, los generadores de un solo qubit ( weight-1 ) presentan la tasa mediana más elevada.
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()Output:
<matplotlib.legend.Legend at 0x125336120>
Relacionar las cajas de circuitos con el ruido aprendido
Una vez que hayamos obtenido los modelos de ruido para cada capa única de dos qubits, debemos vincularlos a las anotaciones correspondientes InjectNoise dentro del circuito enmarcado.
La InjectNoise directiva es una samplomatic anotación que utiliza los «dressers» de un solo qubit para inyectar ruido en el circuito de forma controlada y configurable. Permite realizar una modelización modular del ruido en diferentes capas.
Cada InjectNoise anotación incluye:
InjectNoise.ref- un identificador único para la anotación. Elsamplexobjeto utiliza esto para asignar correctamente el modelo de ruido correspondiente.InjectNoise.modifier_ref(opcional) - una referencia secundaria que permite escalar el modelo de ruido asignado mediante un factor multiplicativo.
En este paso, establecemos una correspondencia entre cada InjectNoise.ref y su modelo de ruido aprendido correspondiente (PauliLindbladMap). Esta correspondencia garantiza que cada capa de puertas de entrelazamiento del circuito se asocie al modelo de ruido adecuado, de modo que los efectos del ruido se apliquen con precisión durante el muestreo y los pasos posteriores de mitigación del ruido.
# 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()
)Propagar la variable observable a través del antirruido aprendido
Como se ha comentado anteriormente, esto se lleva a cabo en dos pasos. En primer lugar, extendemos un generador antirruido hasta el final del circuito. A continuación, propagamos el observable a través de ese generador evolucionado. Este proceso se repite para cada generador antirruido del circuito. En esta implementación, cada generador de una capa determinada se propaga en paralelo hasta el final del circuito. Además, se utiliza el multiprocesamiento « Python » para realizar en paralelo tanto la propagación hacia adelante del antirruido como la propagación hacia atrás de la observable. Esto evita que se acumulen generadores evolucionados en la memoria y, además, optimiza al máximo los recursos de cálculo.
Al ejecutar PNA, siempre hay que proporcionar un circuito con ruido y una variable observable. Si tu circuito ruidoso es un circuito en recuadro con InjectNoise anotaciones, debes proporcionar la correspondencia que hemos creado en el paso anterior. También se puede pasar un circuito no empaquetado que contenga PauliLindbladError instrucciones procedentes de qiskit-aer. En ese caso, refs_to_noise_models no es necesario indicarlo. Además de los datos principales, ten en cuenta también lo siguiente:
max_err_terms: El número de términos que se deben conservar en cada generador antirruido a medida que se propaga hacia adelante. Aumentar este valor suele mejorar la precisión, pero no se garantiza que este comportamiento sea monótono.max_obs_terms: El número de términos que se deben mantener en la observable de mitigación del ruido, , a medida que se propaga hacia atrás a través del antirruido evolucionado. Por lo general, los valores más altos aumentan la precisión, pero no se garantiza que lo hagan de forma monótona.num_processes: El número de núcleos que se van a dedicar al proceso. Recuerda que los generadores se propagan hacia adelante y se aplican a la observable en paralelo.search_step: El paso de retropropagación utiliza un método «greedy» para conjugar de forma aproximada dos operadores en la base de Pauli. Este método puede acelerarse aumentandosearch_step. Ver elpauli-propConsulte la documentación para obtener más información.num_to_measure: Aunque esta variable no es un parámetro de entrada degenerate_noise_mitigating_observable, la utilizamos para controlar cuántos términos de queremos medir realmente. Aquí solo medimos los 30 términos principales, que son los términos originales de nuestro observable. Los términos se han reescalado de tal forma que su medición tiene como efecto mitigar el ruido de puerta aprendido. Aunque solo medimos 30 términos de , a menudo sigue siendo útil dejar que el número de términos aumente, ya que eso aumenta la precisión de los factores de escala de los términos principales.
# 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]Output:
Finished! 13740 / 13740 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), ".")Output:
[<matplotlib.lines.Line2D at 0x124b0a120>]
Transforma las bases de medida a forma canónica
A continuación, buscamos un conjunto mínimo de bases de medición que nos permita cubrir por completo todos los términos de Pauli en la observable medida ( se pueden medir muchas observables simultáneamente si conmutan a nivel de qubit ). Dado que solo estamos midiendo los términos de nuestro observable original —que es la suma de todos los Paulis individuales—Z , solo se necesita una base: la base «todoZ ».
Además de hallar un conjunto de bases de medición de Pauli, debemos expresar estos términos de Pauli en la forma canónica que espera el Executor. Para obtener más información sobre el orden canónico de los qubits, consulta la documentación de Samplomatic.
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
]Paso 3: Realizar experimentos cuánticos
Especifica cómo realizar el muestreo en el QuantumProgram
Ahora configuramos el QuantumProgram, que actúa como contenedor central de todos los circuitos y configuraciones de muestreo que ejecutará el Executor. Este objeto define cómo se generan, se agrupan y se ejecutan las instancias de circuitos aleatorias para obtener los resultados de medición utilizados en PNA.
Un QuantumProgram puede contener varios elementos, cada uno de los cuales consta de un circuito de plantilla y un objeto correspondiente samplex que define cómo se aplican las aleatorizaciones. Esta abstracción permite que el Executor programa gestione todo el flujo de trabajo como un único programa modular, desde la generación aleatoria de circuitos hasta la recopilación y agregación de tomas.
En este paso, creamos un QuantumProgram que ejecuta nuestro experimento PNA utilizando el circuito modelo y samplex que hemos construido anteriormente. La configuración incluye los siguientes elementos:
template_circuit: El circuito que contiene todas las puertas lógicas necesarias para implementar todas las aleatorizaciones deseadas (desde aleatorizaciones de tipo «twirling» hasta parámetros, etc.).samplex: Un objeto que define una distribución de probabilidad sobre todas las posibles aleatorizaciones del circuito, de las que se tomarán muestras.samplex_arguments: Las relaciones necesarias para definir completamente elsamplexbasis_changes: Aquí es donde especificamos un conjunto de bases de medición que cubra todos los términos de Pauli en el observable medido.noise_scales.ref: Ajustamos la escala de cada capa de ruido a0.0para evitar que se introduzca ruido adicional en nuestras muestras.pauli_lindblad_maps: Obligatorio sinoise_scalesse pasan. Esto simplemente asigna las capas de ruido al modelo de ruido correspondiente.
shape: Una tupla de forma para ampliar la forma implícita definida porsamplex_arguments. Los ejes no triviales introducidos por esta extensión enumeran las aleatorizaciones.
# 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. The samplex exposes one basis-change input per
# ChangeBasis-annotated box; here a single all-Z basis covers every term. We
# look up the basis-change interface name rather than hardcoding an index, since
# the name depends on the circuit's box structure.
bases_broadcastable = np.expand_dims(np.array(meas_bases_canonical), axis=1)
samplex_inputs |= {
spec.name: bases_broadcastable
for spec in samplex.inputs().get_specs(r"^basis_changes\.")
}
# 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_samplex_item(
circuit=template_circuit,
samplex=samplex,
samplex_arguments=samplex_arguments,
shape=(num_randomizations_exec,),
)Mide la tensión del circuito utilizando el Executor
Ahora que hemos definido nuestro QuantumProgram, llevar a cabo el experimento es muy sencillo. Simplemente creamos una instancia del Executor objeto, le asignamos el backend y ejecutamos el programa.
# Execute (sample) the circuit
executor = Executor(backend)
job_exec = executor.run(program)
exec_results = job_exec.result()Paso 4: Reconstruir y analizar los resultados
Para calcular un valor esperado con error mitigado, procedemos de la siguiente manera:
- Calcular los factores de escalado de TREX basándose en el ruido detectado que afecta a las mediciones,
- Genera una máscara para conservar únicamente las muestras seleccionadas posteriormente, y
- Utiliza la
executor_expectation_valuesfunción deqiskit-addon-utilspara combinar todos los datos en un valor esperado con el error minimizado.
# 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,
)Comparar estrategias de mitigación: PNA, PNA+TREX, PNA+PS, PNA+PS+TREX
A partir de los Executor resultados, calculamos y visualizamos los valores esperados para varias variantes de mitigación.
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()Output:
Los resultados ponen de manifiesto las ventajas acumulativas que supone combinar diferentes técnicas de mitigación de errores. El enfoque PNA básico ya restaura el valor esperado a un nivel cercano al de referencia ideal, lo que indica que la propagación de canales de ruido inversos en la observable compensa eficazmente los errores de las puertas de dos qubits.
- La incorporación de la reponderación TREX (PNA+TREX) mejora ligeramente la estimación al corregir el desequilibrio de muestreo en los circuitos aleatorizados.
- La postselección (PNA+PS) proporciona una mejora más notable al filtrar los resultados de medición incoherentes que probablemente se deban a errores residuales.
- Por último, la combinación de ambas (PNA+PS+TREX) ofrece el resultado más preciso, que se aproxima mucho al valor ideal, lo que demuestra cómo estas estrategias de mitigación se refuerzan mutuamente.
En general, la comparación pone de manifiesto que PNA constituye una base sólida para la estimación del valor esperado teniendo en cuenta el ruido, mientras que TREX y la postselección ofrecen mejoras complementarias para obtener una mayor precisión.
Próximos pasos
Si este trabajo te ha parecido interesante, quizá te interese el siguiente material: