Aprimorando valores esperados: Absorção de Ruído Propagado (PNA)
Neste tutorial, aprenderemos como aproveitar as ferramentas mais recentes do ecossistema Qiskit para implementar um fluxo de trabalho totalmente personalizável e mitigado contra erros. Apresentaremos a técnica PNA e a usaremos para mitigar erros de portas. Também usaremos o TREX para mitigar erros de leitura e a pós-seleção para mitigar erros não capturados pelo modelo de ruído aprendido.
Estrutura
- Apresentar uma visão geral do
PNA - Criar um circuito quântico Trotterizado e um observável. Transpilá-lo para o backend e incluir medições de pós-seleção.
- Usar o
samplomaticpara realizar twirling em camadas de portas de 2 qubits e medições. Encontrar camadas únicas de 2 qubits para reduzir o custo de aprendizado de ruído. - Usar o
NoiseLearnerV3para aprender o modelo de erro que afeta as portas de 2 qubits e as medições. - Usar o
qiskit-addon-pnapara gerar um observável mitigador de ruído - Usar a primitiva
qiskit-ibm-runtime.Executorpara gerar as amostras brutas da QPU refletindo cada shot para cada randomização de twirling e base medida - Usar o
qiskit-addon-utilspara pós-processar os dados em um valor esperado mitigado.
O que é a absorção de ruído propagado (PNA)?
Uma técnica para mitigar erros de portas propagando o observável através do canal de ruído inverso que afeta as portas de 2 qubits, resultando em um observável mitigador de ruído.
As portas de 2 qubits no experimento que queremos executar serão afetadas por ruído substancial.
Se aprendermos o modelo de ruído, podemos aplicar seu inverso e cancelar o ruído.
Em vez de implementar o canal de ruído inverso amostrando-o na QPU como no PEC, podemos implementá-lo classicamente no observável medido usando propagação de Pauli. Isso resulta em um observável mais complexo que, quando medido, tem o efeito de mitigar o ruído de porta aprendido.

Gerar o circuito Trotter espelhado e o observável
Para este experimento, estudaremos a dinâmica temporal de um modelo de Ising chutado de 30 sítios em uma cadeia de spin 1D. O Hamiltoniano considerado é:
,
onde descreve o acoplamento de spins vizinhos mais próximos, , e o campo transversal global, , é definido como . Quanto mais distante estiver de um ângulo Clifford (ou seja, ), mais difícil se torna propagar os geradores de anti-ruído através do circuito.
Para a escolha do observável, consideraremos a magnetização média de sítio único, , onde é o número de sítios.
# 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)

Em seguida, escolheremos uma cadeia de qubits no ibm_kingston que reportam baixas taxas de erro e transpilaremos o circuito para o 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.

Realizar twirling nas camadas de portas de 2 qubits e medições e encontrar camadas únicas
Aqui garantimos que o pass manager anote as caixas com anotações Twirl e InjectNoise, que nos permitem aprender o ruído que afetará nosso circuito e associar esse ruído à sua camada de circuito correspondente.
enable_gates/enable_measure: True: Encaixotar todas as camadas de portas de 2 qubits e medições terminais. Portas de qubit único serão dispostas à esquerda dentro das caixas.measure_annotations: allIncluir anotaçõesTwirleChangeBasisna caixa de mediçãotwirling_strategy: active: Realizar twirling em todos os qubits ativos em cada caixa contendo portas de emaranhamentoinject_noise_targets: gates: AnotaçõesInjectNoisedevem ser adicionadas a todas as caixas anotadas comTwirlcontendo portas de emaranhamentoinject_noise_strategy: uniform_modification: Todas as camadas de ruído devem ser escaladas equivalentemente.
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)

Gerar o circuito modelo e o samplex, definir como o circuito será amostrado
Aqui também adicionamos medições de espectador e de pós-seleção, que são necessárias para realizar a pós-seleção nas amostras de saída do 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)

Aprender o ruído
Antes de executar os experimentos, aprendemos o modelo de ruído que afeta as portas de emaranhamento e as medições no circuito. Ter um modelo de ruído preciso é necessário para mitigar erros de forma eficaz. Aprender o ruído pouco antes de executar os experimentos oferece a melhor chance de o modelo de ruído descrever fielmente o ruído real que afeta as portas durante a execução.
Antes de aprender o ruído, precisamos encontrar as camadas únicas de 2 qubits em nosso circuito, para que possamos minimizar o número de shots necessários para aprender o ruído de todo o circuito. Usamos find_unique_box_instructions do samplomatic para nos fornecer as camadas únicas do circuito encaixotado, incluindo a camada de medição. Estas são as camadas que passamos ao aprendiz de ruído.
Uma vez que conhecemos as camadas, podemos aprender o ruído. Há alguns parâmetros que consideramos:
num_randomizations: O número de circuitos aleatórios a serem usados por configuração de circuito de aprendizadoshots_per_randomization: Número total de shots a serem usados por circuito de aprendizado aleatóriolayer_pair_depths: As profundidades de circuito (medidas em número de pares) a serem usadas em experimentos de aprendizado.post_selection: Usaremos pós-seleção baseada em arestas durante o aprendizado usando portasrxpara implementar os pulsos pós-medição
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>

Associar caixas do circuito com o ruído aprendido
Aqui, criamos um mapeamento entre os IDs de referência InjectNoise de cada caixa e o modelo de ruído aprendido (PauliLindbladMap) que afeta as portas de emaranhamento nessa caixa.
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()
Propagar o observável através do anti-ruído aprendido para obter um observável mitigador de ruído
Como discutido acima, isso é feito em duas etapas. Primeiro, propagamos um gerador de anti-ruído até o final do circuito. Depois disso, propagamos o observável através desse gerador evoluído. Esse processo é repetido para cada gerador de anti-ruído no circuito. Nesta implementação, cada gerador em uma dada camada é propagado até o final do circuito em paralelo. Adicionalmente, o multiprocessamento Python é usado para realizar tanto a propagação direta do anti-ruído quanto a propagação reversa do observável em paralelo. Isso evita o acúmulo de geradores evoluídos na memória e também maximiza os recursos de computação.
Ao executar PNA, você sempre precisará fornecer um circuito ruidoso e um observável. Se seu circuito ruidoso for um circuito encaixotado com anotações InjectNoise, você precisará fornecer o mapeamento que criamos na etapa acima. Também é possível passar um circuito não encaixotado contendo instruções PauliLindbladError do qiskit-aer. Nesse caso, refs_to_noise_models não precisa ser fornecido. Além das entradas primárias, os usuários vão querer considerar:
max_err_terms: O número de termos a serem mantidos em cada gerador de anti-ruído à medida que ele é propagado para frente. Permitir que isso seja maior geralmente aumenta a precisão, mas esse comportamento não é garantido como monotônico.max_obs_terms: O número de termos a serem mantidos no observável mitigador de ruído, , à medida que ele é propagado de volta através do anti-ruído evoluído. Valores maiores geralmente aumentam a precisão, mas isso não é garantido de forma monotônica.num_processes: O número de núcleos a serem dedicados ao processo. Lembre-se de que os geradores são propagados para frente e aplicados ao observável em paralelo.search_step: A etapa de propagação reversa usa um método guloso para conjugar aproximadamente dois operadores na base de Pauli. Esse método pode ser acelerado aumentandosearch_step. Veja a documentação do pauli-prop para mais informações.num_to_measure: Embora essa variável não seja uma entrada paragenerate_noise_mitigating_observable, nós a usamos para controlar quantos termos de realmente queremos medir. Aqui mediremos apenas os 30 termos principais, que são os termos originais em nosso observável. Os termos foram agora reescalonados de forma que medi-los tenha o efeito de mitigar o ruído de porta aprendido. Embora meçamos apenas 30 termos de , muitas vezes ainda é útil permitir que ele cresça muito, pois isso aumenta a precisão dos fatores de escala dos termos principais.
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>]

Transformar as bases de medição para a forma canônica
Em seguida, encontraremos um conjunto mínimo de bases para medir, de forma que possamos cobrir totalmente cada termo de Pauli no observável medido (muitos observáveis podem ser medidos simultaneamente se eles comutam por qubit). Como estamos medindo apenas os termos em nosso observável original, que é a soma de todos os Paulis de Z único, uma única base é necessária — a base toda em Z.
Além de encontrar um conjunto de bases de medição de Pauli, precisamos mapear esses termos de Pauli para a forma canônica esperada pela primitiva Executor. Para mais informações sobre a ordenação canônica de qubits, visite a documentação do 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
]
Especificar como amostrar no QuantumProgram
O QuantumProgram é onde especificamos como amostrar o experimento:
template_circuit: O circuito contendo todas as portas necessárias para implementar todas as randomizações desejadas (de randomizações de twirling, parâmetros, etc.).samplex: Um objeto que define uma distribuição de probabilidade sobre todas as randomizações de circuito possíveis das quais amostrar.samplex_arguments: Vinculações necessárias para definir totalmente o samplexbasis_changes: Aqui é onde especificamos um conjunto de bases para medir, que cobrirá todos os termos de Pauli no observável medido.noise_scales.ref: Definimos a escala de cada camada de ruído como0.0para evitar que qualquer ruído adicional seja injetado em nossas amostraspauli_lindblad_maps: Necessário senoise_scalesforem passados. Isso apenas mapeia camadas de ruído para o modelo de ruído associado.
shape: Uma tupla de formato para estender o formato implícito definido porsamplex_arguments. Eixos não triviais introduzidos por essa extensão enumeram randomizações.
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),
)
Amostrar o circuito usando o protótipo da primitiva Executor
Agora que definimos nosso QuantumProgram, executar o experimento é simples. Basta instanciarmos o objeto Executor, fornecer-lhe o backend e executar o programa.
from qiskit_ibm_runtime import Executor
# Execute (sample) the circuit
executor = Executor(backend)
job_exec = executor.run(program)
exec_results = job_exec.result()
Pós-processar as amostras para calcular um valor esperado mitigado contra erros
Para calcular um valor esperado mitigado contra erros, faremos o seguinte:
- Calcular os fatores de escala TREX com base no ruído aprendido que afeta as medições
- Gerar uma máscara para manter apenas as amostras pós-selecionadas
- Usar a função
executor_expectation_valuesdoqiskit-addon-utilspara combinar todos os dados em um valor esperado mitigado contra erros.
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()
