Améliorer les valeurs d'espérance : Absorption du bruit propagé (PNA)
Dans ce tutoriel, tu vas apprendre à exploiter les derniers outils de l'écosystème Qiskit pour mettre en œuvre un workflow entièrement personnalisable avec atténuation des erreurs. Nous allons présenter la technique PNA et l'utiliser pour atténuer les erreurs de Gate. Nous utiliserons également TREX pour atténuer les erreurs de lecture et la post-sélection pour atténuer les erreurs non capturées dans le modèle de bruit appris.
Plan
- Donner un bref aperçu de
PNA - Créer un Circuit quantique Trotterisé et un observable. Le transpiler vers le Backend et inclure des mesures de post-sélection.
- Utiliser
samplomaticpour tournoyer des couches de portes 2Q et des mesures. Trouver les couches 2Q uniques pour réduire le coût d'apprentissage du bruit. - Utiliser
NoiseLearnerV3pour apprendre le modèle d'erreur affectant les portes 2Q et les mesures. - Utiliser
qiskit-addon-pnapour générer un observable atténuant le bruit - Utiliser la primitive
qiskit-ibm-runtime.Executorpour générer les échantillons bruts du QPU reflétant chaque shot pour chaque randomisation de tournoiement et chaque base mesurée - Utiliser
qiskit-addon-utilspour post-traiter les données en une valeur d'espérance atténuée.
Qu'est-ce que l'absorption du bruit propagé (PNA) ?
Une technique pour atténuer les erreurs de Gate en propagant l'observable à travers le canal de bruit inverse affectant les portes à 2 qubits, aboutissant à un observable atténuant le bruit.
Les portes 2Q dans l'expérience que nous voulons exécuter seront affectées par un bruit substantiel.
Si nous apprenons le modèle de bruit, nous pouvons appliquer son inverse et annuler le bruit.
Au lieu d'implémenter le canal de bruit inverse en l'échantillonnant sur le QPU comme dans PEC, nous pouvons l'implémenter classiquement dans l'observable mesuré en utilisant la propagation de Pauli. Cela donne un observable plus complexe qui, lorsqu'il est mesuré, a pour effet d'atténuer le bruit de Gate appris.

Générer le Circuit Trotter en miroir et l'observable
Pour cette expérience, nous allons étudier la dynamique temporelle d'un modèle d'Ising frappé à 30 sites sur une chaîne de spins 1D. L'Hamiltonien considéré est :
,
où décrit le couplage des spins voisins les plus proches, , et le champ transverse global, , est fixé à . Plus est éloigné d'un angle de Clifford (c'est-à-dire ), plus il devient difficile de propager les générateurs d'anti-bruit à travers le Circuit.
Pour le choix de l'observable, nous considérons l'aimantation moyenne par site, , où est le nombre de sites.
# 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)

Ensuite, nous allons choisir une chaîne de Qubits sur ibm_kingston qui affichent de faibles taux d'erreur et transpiler le Circuit vers le 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.

Tournoyer les couches de portes 2 Qubits et les mesures, et trouver les couches uniques
Ici, nous nous assurons que le pass manager annote les boîtes avec les annotations Twirl et InjectNoise, ce qui nous permet d'apprendre le bruit qui affectera notre Circuit et d'associer ce bruit à la couche de Circuit correspondante.
enable_gates/enable_measure: True: Encadrer toutes les couches de portes 2q et les mesures terminales. Les portes à un Qubit seront habillées à gauche à l'intérieur des boîtes.measure_annotations: allInclure les annotationsTwirletChangeBasissur la boîte de mesuretwirling_strategy: active: Tournoyer tous les Qubits actifs dans chaque boîte contenant des portes intriquéesinject_noise_targets: gates: Les annotationsInjectNoisedoivent être ajoutées à toutes les boîtes annotées avecTwirlcontenant des portes intriquéesinject_noise_strategy: uniform_modification: Toutes les couches de bruit doivent être mises à l'échelle de manière équivalente.
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)

Générer le Circuit modèle et le samplex, définir la façon dont le Circuit sera échantillonné
Ici, nous ajoutons également des mesures spectatrices et de post-sélection, qui sont nécessaires pour effectuer la post-sélection sur les échantillons produits par 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)

Apprendre le bruit
Avant de lancer les expériences, on apprend le modèle de bruit qui affecte les portes d'intrication et les mesures dans le circuit. Disposer d'un modèle de bruit précis est nécessaire pour atténuer efficacement les erreurs. Apprendre le bruit juste avant d'exécuter les expériences donne les meilleures chances que le modèle de bruit décrive fidèlement le bruit réel affectant les portes lors de l'exécution.
Avant d'apprendre le bruit, on doit trouver les couches 2-Qubit uniques dans notre circuit afin de minimiser le nombre de shots nécessaires pour apprendre le bruit de l'ensemble du circuit. On utilise find_unique_box_instructions de samplomatic pour obtenir les couches uniques du circuit encadré, y compris la couche de mesure. Ce sont ces couches qu'on passe au noise learner.
Une fois les couches identifiées, on peut apprendre le bruit. Il y a quelques paramètres à considérer :
num_randomizations: Le nombre de circuits aléatoires à utiliser par configuration de circuit d'apprentissageshots_per_randomization: Nombre total de shots à utiliser par circuit d'apprentissage aléatoirelayer_pair_depths: Les profondeurs de circuit (mesurées en nombre de paires) à utiliser dans les expériences d'apprentissage.post_selection: On utilisera une post-sélection basée sur les arêtes lors de l'apprentissage en utilisant des portesrxpour implémenter les pulses post-mesure
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>
