Sari la conținutul principal

Î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 samplomatic pentru 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 NoiseLearnerV3 pentru a învăța modelul de erori care afectează porțile cu 2 Qubiți și măsurătorile.
  • Folosim qiskit-addon-pna pentru a genera un observabil de reducere a zgomotului
  • Folosim primitivul qiskit-ibm-runtime.Executor pentru a genera eșantioanele brute QPU reflectând fiecare shot pentru fiecare randomizare de răsucire și bază măsurată
  • Folosim qiskit-addon-utils pentru 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. Noisy experiment Dacă învățăm modelul de zgomot, putem aplica inversul acestuia și anula zgomotul. Noise-mitigated experiment Î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. PNA overview

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:

H=Ji,jZiZj+hiXiH = -J\sum\limits_{\langle i,j \rangle} Z_iZ_j + h\sum\limits_iX_i,

unde J>0J>0 descrie cuplajul spinilor vecini apropiați, i<ji<j, iar câmpul transversal global, hh, este setat la π8\frac{\pi}{8}. Cu cât hh este mai departe de un unghi Clifford (adică θ=nπ2,nZ\theta=n\frac{\pi}{2}, n \in \mathbb{Z}), 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, 1Ni=1Nzi\frac{1}{N} \sum_{i=1}^{N} \langle z_i \rangle, unde NN 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)

Quantum circuit diagram

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.

Quantum circuit diagram

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: all Include adnotările Twirl și ChangeBasis pe cutia de măsurare
  • twirling_strategy: active: Răsucește toți Qubiții activi din fiecare cutie care conține porți cu entanglare
  • inject_noise_targets: gates: Adnotările InjectNoise trebuie adăugate la toate cutiile adnotate cu Twirl care conțin porți cu entanglare
  • inject_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)

Quantum circuit diagram

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)

Quantum circuit diagram

Î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ățare
  • shots_per_randomization: Numărul total de shot-uri folosite per circuit de învățare aleator
  • layer_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ți rx pentru 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>

Plot output

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, O~\tilde{O}, 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 valorii search_step. Consultă documentația pauli-prop pentru mai multe informații.
  • num_to_measure: Deși această variabilă nu este o intrare a funcției generate_noise_mitigating_observable, o folosim pentru a controla câți termeni din O~\tilde{O} 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 O~\tilde{O}, 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>]

Plot output

Transformarea bazelor de măsurare în formă canonică

Urmează să găsim un set minim de baze de măsurare astfel încât să putem acoperi complet fiecare termen Pauli din observabila măsurată (multe observabile pot fi măsurate simultan dacă comutează qubit cu qubit). Deoarece măsurăm doar termenii din observabila noastră originală, care este suma tuturor Paulilor de tip Z individual, este nevoie de o singură bază -- baza Z completă.

Pe lângă găsirea unui set de baze de măsurare Pauli, trebuie să mapăm acești termeni Pauli la forma canonică așteptată de primitivul Executor. Pentru mai multe informații despre ordonarea canonică a qubiților, consultă documentația 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
]

Specificarea modului de eșantionare în QuantumProgram

QuantumProgram este locul unde specificăm cum să eșantionăm experimentul:

  • template_circuit: Circuit-ul care conține toate porțile necesare pentru a implementa toate randomizările dorite (din randomizări de twirling, parametri etc.).
  • samplex: Un obiect care definește o distribuție de probabilitate peste toate randomizările posibile ale circuitului din care să se eșantioneze.
  • samplex_arguments: Legăturile necesare pentru a defini complet samplex-ul
    • basis_changes: Aici specificăm un set de baze de măsurare care vor acoperi toți termenii Pauli din observabila măsurată.
    • noise_scales.ref: Setăm scala fiecărui strat de zgomot la 0.0 pentru a preveni injectarea de zgomot suplimentar în eșantioanele noastre
    • pauli_lindblad_maps: Necesar dacă se transmit noise_scales. Acesta mapează straturile de zgomot la modelul de zgomot asociat.
  • shape: Un tuplu de formă pentru a extinde forma implicită definită de samplex_arguments. Axele non-triviale introduse de această extensie enumeră randomizările.
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),
)

Eșantionarea Circuit-ului cu primitivul prototip Executor

Acum că am definit QuantumProgram-ul nostru, executarea experimentului este simplă. Instanțiem pur și simplu obiectul Executor, îi furnizăm Backend-ul și rulăm programul.

from qiskit_ibm_runtime import Executor

# Execute (sample) the circuit
executor = Executor(backend)
job_exec = executor.run(program)
exec_results = job_exec.result()

Postprocesarea eșantioanelor pentru calcularea unei valori de așteptare cu mitigarea erorilor

Pentru a calcula o valoare de așteptare cu mitigarea erorilor, vom:

  • Calcula factorii de scalare TREX pe baza zgomotului învățat care afectează măsurătorile
  • Genera o mască pentru păstrarea doar a eșantioanelor selectate prin post-selecție
  • Folosi funcția executor_expectation_values din qiskit-addon-utils pentru a combina toate datele într-o valoare de așteptare cu mitigarea erorilor.
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()

Plot output