Îmbunătățirea Valorilor de Așteptare: Absorbția Propagată a Zgomotului (PNA)
În acest tutorial, vei învăța cum să valorifici cele mai noi instrumente din ecosistemul Qiskit pentru a implementa un flux de lucru complet personalizabil, cu reducerea erorilor. Vom prezenta tehnica PNA și o vom folosi pentru a atenua erorile de Gate. Vom utiliza, de asemenea, TREX pentru a atenua erorile de citire și post-selecția pentru a atenua erorile care nu sunt capturate în modelul de zgomot învățat.
Rezumat
- Oferim o scurtă prezentare a
PNA - Creăm un Circuit cuantic Trotterizat și un observabil. Îl transpilăm pentru Backend și includem măsurători de post-selecție.
- Folosim
samplomaticpentru a răsuci straturi de porți cu 2 Qubiți și măsurători. Identificăm straturi unice cu 2 Qubiți pentru a reduce costul de învățare a zgomotului. - Folosim
NoiseLearnerV3pentru a învăța modelul de erori care afectează porțile cu 2 Qubiți și măsurătorile. - Folosim
qiskit-addon-pnapentru a genera un observabil de reducere a zgomotului - Folosim primitivul
qiskit-ibm-runtime.Executorpentru a genera eșantioanele brute QPU reflectând fiecare shot pentru fiecare randomizare de răsucire și bază măsurată - Folosim
qiskit-addon-utilspentru a post-procesa datele într-o valoare de așteptare atenuată.
Ce este absorbția propagată a zgomotului (PNA)?
O tehnică de atenuare a erorilor de Gate prin propagarea observabilului prin canalul invers de zgomot care afectează porțile cu 2 Qubiți, rezultând un observabil de reducere a zgomotului.
Porțile 2Q din experimentul pe care vrem să îl rulăm vor fi afectate de zgomot substanțial.
Dacă învățăm modelul de zgomot, putem aplica inversul acestuia și anula zgomotul.
În loc să implementăm canalul invers de zgomot prin eșantionarea acestuia pe QPU, cum se face în PEC, îl putem implementa clasic în observabilul măsurat folosind propagarea Pauli. Aceasta rezultă într-un observabil mai complex care, atunci când este măsurat, are efectul de a atenua zgomotul de Gate învățat.

Generarea Circuitului Trotter în oglindă și a observabilului
Pentru acest experiment, vom studia dinamica temporală a unui model Ising cu kick pe 30 de site-uri, pe un lanț de spin 1D. Hamiltonianul considerat este:
,
unde descrie cuplajul spinilor vecini apropiați, , iar câmpul transversal global, , este setat la . Cu cât este mai departe de un unghi Clifford (adică ), cu atât devine mai dificil să propagăm generatorii anti-zgomot prin Circuit.
Pentru alegerea observabilului, vom considera magnetizarea medie pe un singur site, , unde este numărul de site-uri.
# 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)

Apoi, vom alege un lanț de Qubiți pe ibm_kingston care raportează rate de erori scăzute și vom transpila Circuitul pentru 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.

Răsucirea straturilor de porți cu 2 Qubiți și a măsurătorilor și identificarea straturilor unice
Aici ne asigurăm că pass manager-ul adnotează cutiile cu adnotările Twirl și InjectNoise, care ne permit să învățăm zgomotul ce va afecta Circuitul nostru și să asociem acel zgomot cu stratul corespunzător din Circuit.
enable_gates/enable_measure: True: Încadrează toate straturile de porți 2Q și măsurătorile terminale. Porțile cu un singur Qubit vor fi îmbrăcate în stânga în interiorul cutiilor.measure_annotations: allInclude adnotărileTwirlșiChangeBasispe cutia de măsuraretwirling_strategy: active: Răsucește toți Qubiții activi din fiecare cutie care conține porți cu entanglareinject_noise_targets: gates: AdnotărileInjectNoisetrebuie adăugate la toate cutiile adnotate cuTwirlcare conțin porți cu entanglareinject_noise_strategy: uniform_modification: Toate straturile de zgomot trebuie scalate în mod echivalent.
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)

Generează circuitul template și samplex, definește cum va fi eșantionat circuitul
Aici adăugăm și măsurători spectator și post-selecție, necesare pentru a efectua post-selecție pe eșantioanele generate de 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)

Învață zgomotul
Înainte de a rula experimentele, învățăm modelul de zgomot care afectează porțile de intricate și măsurătorile din circuit. Existența unui model de zgomot precis este necesară pentru a atenua eficient erorile. Învățarea zgomotului imediat înainte de executarea experimentelor oferă cea mai bună șansă ca modelul de zgomot să descrie fidel zgomotul real care afectează porțile în timpul execuției.
Înainte de a învăța zgomotul, trebuie să găsim straturile unice de 2-qubiți din circuitul nostru, pentru a minimiza numărul de shot-uri necesare pentru a învăța zgomotul întregului circuit. Folosim find_unique_box_instructions din samplomatic pentru a obține straturile unice din circuitul boxat, inclusiv stratul de măsurătoare. Acestea sunt straturile pe care le transmitem către noise learner.
Odată ce cunoaștem straturile, putem învăța zgomotul. Există câțiva parametri de luat în considerare:
num_randomizations: Numărul de circuite aleatoare utilizate per configurație de circuit de învățareshots_per_randomization: Numărul total de shot-uri folosite per circuit de învățare aleatorlayer_pair_depths: Adâncimile de circuit (măsurate în număr de perechi) utilizate în experimentele de învățare.post_selection: Vom folosi post-selecție bazată pe muchii în timpul învățării, utilizând porțirxpentru a implementa pulsurile post-măsurătoare
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>

Asociază boxurile de circuit cu zgomotul învățat
Aici creăm o mapare între ID-urile de referință InjectNoise ale fiecărui box și modelul de zgomot învățat (PauliLindbladMap) care afectează porțile de intricate din acel box.
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()
Propagă observabilul prin anti-zgomotul învățat pentru a obține un observabil care atenuează zgomotul
Așa cum s-a discutat mai sus, acest lucru se realizează în două etape. Mai întâi, propagăm un generator anti-zgomot până la sfârșitul circuitului. După aceea, propagăm observabilul prin acel generator evoluat. Acest proces se repetă pentru fiecare generator anti-zgomot din circuit. În această implementare, fiecare generator dintr-un strat dat este propagat până la sfârșitul circuitului în paralel. În plus, multiprocessing-ul Python este folosit pentru a realiza atât propagarea directă a anti-zgomotului, cât și propagarea inversă a observabilului în paralel. Aceasta previne acumularea generatorilor evoluați în memorie și maximizează totodată resursele de calcul.
Când rulezi PNA, va trebui întotdeauna să furnizezi un circuit zgomotos și un observabil. Dacă circuitul tău zgomotos este un circuit boxat cu adnotări InjectNoise, va trebui să furnizezi maparea creată la pasul de mai sus. Se poate transmite și un circuit non-boxat care conține instrucțiuni PauliLindbladError din qiskit-aer. În acel caz, refs_to_noise_models nu trebuie furnizat. Pe lângă intrările primare, utilizatorii vor dori să ia în considerare:
max_err_terms: Numărul de termeni de păstrat în fiecare generator anti-zgomot pe măsură ce este propagat direct. A permite un număr mai mare crește în general acuratețea, dar acest comportament nu este garantat să fie monoton.max_obs_terms: Numărul de termeni de păstrat în observabilul care atenuează zgomotul, , pe măsură ce este propagat invers prin anti-zgomotul evoluat. Valorile mai mari cresc în general acuratețea, dar nu este garantat că o fac monoton.num_processes: Numărul de nuclee dedicate procesului. Ține minte că generatorii sunt propagați direct și aplicați observabilului în paralel.search_step: Pasul de propagare inversă folosește o metodă greedy pentru a aproxima conjugatul a doi operatori în baza Pauli. Această metodă poate fi accelerată prin creșterea valoriisearch_step. Consultă documentația pauli-prop pentru mai multe informații.num_to_measure: Deși această variabilă nu este o intrare a funcțieigenerate_noise_mitigating_observable, o folosim pentru a controla câți termeni din dorim să măsurăm efectiv. Aici vom măsura doar primii 30 de termeni, care sunt termenii originali din observabilul nostru. Termenii au fost acum re-scalați astfel încât măsurarea lor are efectul de a atenua zgomotul de poartă învățat. Deși măsurăm doar 30 de termeni din , este adesea totuși util să îi lăsăm să crească, deoarece aceasta crește precizia factorilor de scalare ai termenilor principali.
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>]
