Vai al contenuto principale
Questo progetto è in fase beta. Hai trovato un bug? Apri una issue su GitHub. Hai una domanda o un'idea? Chiedi su GitHub Discussions — apprezziamo il tuo feedback!
Com'è la qualità della traduzione su questa pagina?

Migliora i valori di aspettazione con la propagated noise absorption (PNA)

Stima di utilizzo: 10 minuti su un processore Heron (NOTA: questa è solo una stima. Il tempo effettivo potrebbe variare.)

Risultati di apprendimento​

  • Cos'è l'assorbimento di rumore propagato (PNA) e come mitiga il rumore dei gate a due qubit assorbendo canali di rumore inverso appresi nell'osservabile misurato

  • Come usare samplomatic per racchiudere e annotare i layer del circuito per twirling, cambi di base e iniezione di rumore

  • Come apprendere il rumore dei layer con [NoiseLearnerV3](/guides/directed-execution-model#noiselearnerv3) e propagarlo in un osservabile che mitiga il rumore con qiskit-addon-pna

  • Come campionare circuiti randomizzati con le classi QuantumProgram e Executor in qiskit-ibm-runtime, e combinare PNA con TREX e post-selection

Prerequisites​

Background​

In questo tutorial, dimostriamo come sfruttare strumenti avanzati di mitigazione degli errori in Qiskit per migliorare la stima dei valori di aspettazione in esperimenti quantistici rumorosi.

Cos'è la propagated noise absorption (PNA)?​

La propagated noise absorption è una tecnica per mitigare gli errori dei gate propagando l'osservabile attraverso il canale di rumore inverso che colpisce i gate a due qubit, ottenendo un osservabile che mitiga il rumore.

Possiamo usare la mitigazione degli errori quantistici per estrarre valori di aspettazione utili da hardware quantistico rumoroso senza richiedere piena tolleranza ai guasti. La PNA si concentra specificamente sull'assorbire gli effetti del rumore nell'osservabile stesso piuttosto che modificare le operazioni del circuito.

Ogni gate rumoroso in un circuito quantistico può essere modellato come un gate ideale seguito da un canale di rumore. La PNA apprende o caratterizza questi canali di rumore e ne definisce gli inversi. Invece di inserire le operazioni inverse nell'esecuzione hardware (il che è tipicamente infattibile), la PNA propaga i canali di rumore inverso in avanti attraverso il circuito e li applica all'osservabile. Questo processo trasforma l'osservabile OO in un nuovo operatore O~\tilde{O}, tale che misurare O~\tilde{O} sul circuito rumoroso produce lo stesso valore di aspettazione della misura di OO su un circuito ideale, privo di rumore.

Questo può essere riassunto come segue:

  1. Modella ogni gate rumoroso uiu_i come un'operazione ideale seguita da un canale di rumore Λi\Lambda_i.
  2. Apprendi o stima ciascun Λi\Lambda_i usando strumenti di caratterizzazione del rumore.
  3. Definisci e propaga le mappe di rumore inverso Λi−1\Lambda_i^{-1} in avanti attraverso il circuito usando tecniche di trasferimento di Pauli.
  4. Assorbi questi inversi nell'osservabile, ottenendo un operatore mitigato dal rumore O~\tilde{O}.

Quando il rumore è descritto come un canale di Pauli (o più in generale come un canale sparso di Pauli-Lindblad), questa propagazione può essere eseguita in modo efficiente usando la propagazione di Pauli. La propagazione di Pauli fornisce un framework per approssimare come i canali di rumore inverso si trasformano quando attraversano layer di operazioni Clifford e non-Clifford, mantenendo sotto controllo la complessità computazionale.

Spostando la mitigazione nell'osservabile, la PNA evita il grande overhead di campionamento che altrimenti deriverebbe dall'inserire operazioni di correzione fisiche nel circuito. Invece, viene eseguito il circuito rumoroso originale, mentre l'osservabile viene trasformato in un nuovo operatore O~\tilde{O} il cui valore di aspettazione annulla gli effetti del rumore.

Il workflow della PNA​

Il processo può essere compreso attraverso le seguenti fasi concettuali. Il primo schema illustra un esperimento rumoroso standard.

Noisy experiment

Se apprendiamo il modello di rumore, possiamo applicarne l'inverso e annullare il rumore.

Noise-mitigated experiment

Invece di implementare il canale di rumore inverso campionandolo sulla QPU come nella probabilistic error cancellation (PEC), lo applichiamo classicamente all'osservabile misurato usando la propagazione di Pauli. L'osservabile risultante mitiga efficacemente il rumore del gate appreso quando viene misurato.

PNA overview

Mitigazione modulare degli errori con Samplomatic ed Executor​

L'approccio PNA si basa su un'architettura modulare di mitigazione degli errori in Qiskit. Questa architettura usa la libreria samplomatic insieme alle classi QuantumProgram e Executor (aggiunte a Qiskit Runtime in qiskit-ibm-runtime v0.47.0) per rendere componibili e riutilizzabili in diversi esperimenti tecniche come la propagated noise absorption e il Pauli twirling.

Piuttosto che incorporare la logica di mitigazione all'interno della definizione stessa del circuito, la mitigazione viene espressa dichiarativamente tramite annotazioni samplomatic e gestita programmaticamente tramite Executor, che controlla come i circuiti randomizzati vengono generati, eseguiti e post-elaborati.

In questo tutorial implementiamo un Qiskit pattern per dimostrare come la PNA possa propagare canali di rumore di Pauli inverso e modificare l'osservabile di conseguenza, per migliorare la stima dei valori di aspettazione su QPU rumorose.

Panoramica del workflow​

  • Fase 1: Mappare il problema quantistico

    • Costruisci un modello Trotterizzato kicked-Ising a specchio e un osservabile target.
  • Fase 2: Caratterizzare e propagare il rumore

    • Usa samplomatic per identificare e annotare i layer unici a due qubit e le misure nel circuito.

    • Apprendi il rumore che colpisce ciascun layer unico usando NoiseLearnerV3.

    • Mappa ogni annotazione InjectNoise al suo modello di rumore appreso corrispondente.

    • Usa qiskit-addon-pna per propagare i canali di rumore inverso in avanti attraverso il circuito e assorbirli nell'osservabile target.

  • Fase 3: Eseguire gli esperimenti quantistici

    • Definisci un QuantumProgram per specificare il campionamento randomizzato tramite samplex ed esegui gli esperimenti sul backend usando l'Executor.
  • Fase 4: Ricostruire e analizzare i risultati

    • Confronta le strategie di mitigazione (PNA, PNA+TREX, PNA+PS, PNA+PS+TREX) e visualizza il miglioramento rispetto ai risultati non mitigati.

Requirements​

Prima di iniziare questo tutorial, assicurati di avere installato quanto segue:

  • Qiskit SDK v2.2 o successivo, con supporto per la visualizzazione

  • Qiskit Runtime v0.47 o successivo (pip install qiskit-ibm-runtime)

  • Samplomatic v0.13 o successivo (pip install samplomatic)

  • PNA Qiskit addon (pip install qiskit-addon-pna)

  • Qiskit addon utils (pip install qiskit-addon-utils)

Setup​

# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util

_needed = {"matplotlib": "matplotlib", "numpy": "numpy", "qiskit": "qiskit", "qiskit_addon_pna": "qiskit-addon-pna", "qiskit_addon_utils": "qiskit-addon-utils", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "samplomatic": "samplomatic"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
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_path

Esempio su simulatore su piccola scala​

La PNA mitiga il rumore fisico dei gate a due qubit di un processore quantistico specifico. Il workflow dipende da due servizi hardware che non hanno un analogo significativo su un simulatore ideale:

  • NoiseLearnerV3 caratterizza sperimentalmente il canale di rumore sparso di Pauli-Lindblad associato a ogni layer unico a due qubit del circuito transpilato. Su un simulatore privo di rumore non c'è rumore da apprendere, e il canale inverso propagato sarebbe l'identità.

  • La primitiva Executor campiona i circuiti twirled e randomizzati generati da samplomatic su un backend.

In linea di principio potresti sostituire un modello di rumore sintetico. Ad esempio, potresti collegare istruzioni PauliLindbladError con Qiskit Aer e passare il circuito rumoroso risultante direttamente a generate_noise_mitigating_observable. Tuttavia, questo convalida solo la contabilità classica rispetto al rumore che hai iniettato tu stesso, e oscura lo scopo della tecnica. Per questo motivo saltiamo l'esempio del simulatore e dimostriamo l'intero workflow PNA direttamente su hardware, con ogni fase del Qiskit pattern illustrata di seguito.

Esempio su hardware su larga scala​

Ora eseguiamo il workflow PNA completo su un modello kicked Ising a 30 siti eseguito su hardware IBM Quantum®, seguendo le quattro fasi di un Qiskit pattern.

Fase 1: Mappare un problema quantistico​

Genera il circuito Trotter a specchio e l'osservabile

Per questo esperimento, studieremo le dinamiche temporali di un modello kicked Ising a 30 siti su una catena di spin 1D. L'Hamiltoniana considerata è:

H=−J∑⟨i,j⟩ZiZj+h∑iXiH = -J\sum\limits_{\langle i,j \rangle} Z_iZ_j + h\sum\limits_iX_i,

dove J>0J>0 descrive l'accoppiamento degli spin dei primi vicini, i<ji<j, e il campo trasverso globale, hh, è impostato a π8\frac{\pi}{8}. Più hh è lontano da un angolo Clifford (cioè θ=nπ2,n∈Z\theta=n\frac{\pi}{2}, n \in \mathbb{Z}), più diventa difficile propagare i generatori di anti-rumore attraverso il circuito.

Per la scelta dell'osservabile, considereremo la magnetizzazione media per singolo sito, 1N∑i=1N⟨zi⟩\frac{1}{N} \sum_{i=1}^{N} \langle z_i \rangle, dove NN è il numero di siti.

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 of the previous code cell

Fase 2: Ottimizzare il problema per l'esecuzione su hardware​

Il passo successivo è preparare il nostro circuito Trotter a specchio per l'esecuzione su hardware IBM Quantum reale. Eseguire su una QPU richiede più della semplice costruzione del circuito astratto, poiché dobbiamo ottimizzarlo in modo che:

  1. Rispetti il set di gate nativo e la connettività del backend. La transpilazione mappa il circuito logico in un circuito ISA supportato dal backend target. Questo garantisce che ogni gate e interazione tra qubit sia fisicamente realizzabile.

  2. Possiamo caratterizzare il rumore a livello di layer del circuito. La PNA si basa sull'apprendere e propagare canali di rumore inverso. Per farlo in modo efficiente, suddividiamo il circuito transpilato in layer unici a due qubit "racchiusi in box". Questi box ci permettono di associare ogni layer del circuito con il proprio modello di rumore appreso.

  3. Possiamo alimentare la PNA con modelli di rumore realistici. Una volta racchiuso il circuito nei box, usiamo il servizio NoiseLearnerV3 per apprendere sperimentalmente i canali di rumore di Pauli che colpiscono ogni layer unico a due qubit. Questi modelli appresi vengono quindi ricollegati al circuito tramite le annotazioni di Samplomatic.

Nel complesso, questa fase colma il divario tra un circuito a specchio idealizzato e un circuito pronto per l'hardware con modelli di rumore appresi. Con questa configurazione, la PNA può propagare i canali di rumore inverso attraverso il circuito e adattare l'osservabile di conseguenza.

Connettersi al backend e transpilare in un circuito ISA​

Per prima cosa inizializziamo l'IBM Quantum Compute Service e selezioniamo un backend. Autenticati con il tuo account seguendo le istruzioni per salvare le tue credenziali, dopodiché QiskitRuntimeService() le rileverà automaticamente.

# Initialize IBM Quantum Compute 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}")
Selected backend: ibm_fez

Successivamente, scegliamo una catena connessa di qubit sul backend e transpiliamo il circuito su di essa.

Usando l'helper find_qubit_chain definito nella sezione Setup, selezioniamo una linea di num_qubits qubit fisici connessi. Poi transpiliamo con optimization_level=0 e fissiamo questa catena come initial_layout, che preserva esattamente la struttura a layer dei gate a due qubit del circuito a specchio. Questa struttura è ciò su cui si basano le fasi di boxing e apprendimento del rumore, quindi un livello di ottimizzazione più alto (che annullerebbe i gate a specchio) deve essere evitato qui.

# 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 of the previous code cell

Twirl dei layer di gate a due qubit e delle misure, e ricerca dei layer unici​

Usiamo samplomatic per racchiudere in box il circuito e identificare i layer unici a due qubit. Un box è un costrutto che raggruppa le istruzioni in modo che intenti o annotazioni specifiche possano poi essere applicate uniformemente a tutti i gate all'interno dello stesso box.

Qui, chiamiamo il metodo generate_boxing_pass_manager, che va oltre la semplice identificazione dei layer a due qubit. Esegue diversi compiti chiave:

  • Raggruppa tutti i layer a due qubit nel circuito,

  • Applica le annotazioni Twirl e ChangeBasis a quei layer,

  • Raggruppa le operazioni di misura nelle proprie sezioni racchiuse in box, e

  • Applica l'annotazione InjectNoise a ogni layer a due qubit.

Queste annotazioni definiscono come vengono gestiti il rumore, i cambi di base e il twirling in tutto il circuito. Stabiliscono anche la struttura usata successivamente per apprendere e mitigare il rumore.

Le opzioni di configurazione chiave sono:

  • enable_gates/enable_measures: True: racchiude in box tutti i layer di gate a due qubit e le misure terminali. All'interno dei box, i gate a singolo qubit vengono inglobati nel dressing a sinistra ("left-dressed").

  • measure_annotations: all: include le annotazioni Twirl e ChangeBasis sul box di misura.

  • twirling_strategy: active: applica il twirl a tutti i qubit attivi in ogni box contenente gate entangling.

  • inject_noise_targets: gates: aggiunge annotazioni InjectNoise a tutti i box annotati con Twirl che contengono gate entangling.

  • inject_noise_strategy: uniform_modification: scala tutti i layer di rumore in modo equivalente lungo il 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 of the previous code cell

Genera il circuito template e il samplex, che definiscono come il circuito verrà campionato.

Qui aggiungiamo anche misure di spettatore ("spectator") e di post-selection, necessarie per eseguire la post-selection sui campioni in output dall'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 of the previous code cell

Apprendere il rumore usando NoiseLearnerV3​

Prima di poter applicare la PNA per la mitigazione degli errori, dobbiamo prima caratterizzare il rumore che agisce su ogni layer unico a due qubit e sul layer di misura nel nostro circuito. Per farlo, usiamo il programma NoiseLearnerV3 per apprendere sperimentalmente i modelli di rumore per ogni layer identificato in precedenza. Il learner esegue esperimenti di tipo benchmarking che stimano il canale di rumore che colpisce ogni layer e restituisce un oggetto risultato contenente il modello appreso.

Iniziamo identificando i layer unici nel nostro circuito usando find_unique_box_instructions da samplomatic. Questo garantisce che apprendiamo il rumore una sola volta per ogni tipo distinto di layer, minimizzando il numero di esperimenti e il costo totale degli shot. L'elenco risultante di layer viene passato al noise learner.

Ci sono alcuni parametri chiave che controllano come viene appreso il rumore:

  • num_randomizations: numero di circuiti casuali usati per configurazione di apprendimento.

  • shots_per_randomization: numero di shot presi per circuito di apprendimento casuale.

  • layer_pair_depths: le profondità del circuito (misurate in numero di coppie) da usare negli esperimenti di apprendimento.

  • post_selection: abilita la post-selection basata sui bordi ("edge-based") usando gate rx per applicare impulsi post-misura.

# 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()

Visualizzare i tassi di rumore appresi​

Dopo aver appreso i modelli di rumore, possiamo esaminare la distribuzione dei tassi di errore inferiti sia per le operazioni a un qubit che per quelle a due qubit. Il codice seguente estrae le rappresentazioni di Pauli-Lindblad dai risultati di rumore appresi e raccoglie i tassi di rumore corrispondenti.

Per ogni layer appreso:

  • Convertiamo il modello di rumore in un elenco sparso di tuple (pstr, qubits, rate), dove pstr è la stringa di Pauli che agisce sui qubit indicati e rate è il tasso di errore associato.
  • Separiamo i tassi in termini a un qubit (len(pstr) == 1) e a due qubit (len(pstr) == 2).
  • Gli elenchi dei tassi vengono quindi ordinati e vengono calcolati i loro valori mediani.

Tracciamo le distribuzioni dei tassi di rumore a un qubit (rosso) e a due qubit (blu) su scala logaritmica, con i rispettivi valori mediani indicati da linee verticali, così da poter confrontare le grandezze relative dei generatori di Pauli-Lindblad appresi. L'ordine dei tassi a uno e due qubit dipende dal dispositivo e dai layer specifici caratterizzati; in questa esecuzione, i generatori a singolo qubit (peso 1) hanno il tasso mediano più elevato.

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 0x125336120>

Output of the previous code cell

Associare i box del circuito al rumore appreso​

Una volta ottenuti i modelli di rumore per ogni layer unico a due qubit, dobbiamo collegarli alle annotazioni InjectNoise corrispondenti all'interno del circuito racchiuso in box.

La direttiva InjectNoise è un'annotazione samplomatic che usa i dresser a singolo qubit per iniettare rumore nel circuito in modo controllato e configurabile. Consente la modellazione modulare del rumore attraverso diversi layer.

Ogni annotazione InjectNoise include:

  • InjectNoise.ref - un identificatore univoco per l'annotazione. Viene usato dall'oggetto samplex per assegnare correttamente il modello di rumore corrispondente.
  • InjectNoise.modifier_ref (opzionale) - un riferimento secondario che consente di scalare il modello di rumore assegnato tramite un fattore moltiplicativo.

In questa fase, costruiamo una mappatura da ogni InjectNoise.ref al suo modello di rumore appreso corrispondente (PauliLindbladMap). Questa mappatura garantisce che ogni layer di gate entangling nel circuito sia associato al modello di rumore appropriato, in modo che gli effetti del rumore vengano applicati accuratamente durante il campionamento e le successive fasi di mitigazione del rumore.

# 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()
)

Propagare l'osservabile attraverso l'anti-rumore appreso​

Come discusso sopra, questo viene fatto in due fasi. Prima, propaghiamo un generatore di anti-rumore fino alla fine del circuito. Dopodiché, propaghiamo l'osservabile attraverso quel generatore evoluto. Questo processo viene ripetuto per ogni generatore di anti-rumore nel circuito. In questa implementazione, ogni generatore in un dato layer viene propagato fino alla fine del circuito in parallelo. Inoltre, il multiprocessing di Python viene usato per eseguire in parallelo sia la propagazione in avanti dell'anti-rumore sia la retropropagazione dell'osservabile. Questo evita un accumulo di generatori evoluti in memoria e massimizza anche le risorse di calcolo.

Quando esegui la PNA, devi sempre fornire un circuito rumoroso e un osservabile. Se il tuo circuito rumoroso è un circuito racchiuso in box con annotazioni InjectNoise, devi fornire la mappatura creata nella fase precedente. È anche possibile passare un circuito non racchiuso in box contenente istruzioni PauliLindbladError da qiskit-aer. In tal caso, non è necessario fornire refs_to_noise_models. Oltre agli input principali, considera anche quanto segue:

  • max_err_terms: il numero di termini da mantenere in ciascun generatore di anti-rumore man mano che viene propagato in avanti. Consentire che questo valore sia più grande generalmente aumenta l'accuratezza, ma questo comportamento non è garantito essere monotono.

  • max_obs_terms: il numero di termini da mantenere nell'osservabile che mitiga il rumore, O~\tilde{O}, mentre viene retropropagato attraverso l'anti-rumore evoluto. Valori più grandi generalmente aumentano l'accuratezza, ma non è garantito che lo facciano in modo monotono.

  • num_processes: Il numero di core da dedicare al processo. Ricorda, i generatori vengono propagati in avanti e applicati all'osservabile in parallelo.

  • search_step: Il passo di retropropagazione usa un metodo greedy per coniugare approssimativamente due operatori nella base di Pauli. Questo metodo può essere velocizzato aumentando search_step. Consulta la documentazione di pauli-prop per maggiori informazioni.

  • num_to_measure: Sebbene questa variabile non sia un input di generate_noise_mitigating_observable, la usiamo per controllare quanti termini di O~\tilde{O} vogliamo effettivamente misurare. Qui misuriamo solo i primi 30 termini, che sono i termini originali del nostro osservabile. I termini sono ora stati riscalati in modo tale che misurarli abbia l'effetto di mitigare il rumore dei gate appreso. Sebbene misuriamo solo 30 termini di O~\tilde{O}, è spesso comunque utile lasciarlo crescere molto, poiché ciò aumenta la precisione dei fattori di scala dei termini principali.

# 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! 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), ".")
[<matplotlib.lines.Line2D at 0x124b0a120>]

Output of the previous code cell

Trasformare le basi di misura in forma canonica​

Successivamente, troviamo un insieme minimo di basi da misurare in modo da coprire completamente ogni termine di Pauli nell'osservabile misurato (molti osservabili possono essere misurati simultaneamente se commutano qubit per qubit). Poiché stiamo misurando solo i termini nel nostro osservabile originale, che è la somma di tutti i singoli Pauli Z, è necessaria una sola base: la base tutto-Z.

Oltre a trovare un insieme di basi di misura di Pauli, dobbiamo mappare questi termini di Pauli nella forma canonica attesa dall'Executor. Per maggiori informazioni sull'ordinamento canonico dei qubit, visita la documentazione di 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
]

Fase 3: Eseguire gli esperimenti quantistici​

Specificare come campionare nel QuantumProgram​

Ora impostiamo il QuantumProgram, che funge da contenitore centrale per tutti i circuiti e le configurazioni di campionamento che saranno eseguiti dall'Executor. Questo oggetto definisce come vengono generate, raggruppate ed eseguite le istanze di circuito randomizzate per produrre i risultati di misura usati in PNA.

Un QuantumProgram può contenere più elementi, ciascuno costituito da un circuito template e un corrispondente oggetto samplex che definisce come vengono applicate le randomizzazioni. Questa astrazione permette all'Executor di gestire l'intero flusso di lavoro come un unico programma modulare — dalla generazione di circuiti randomizzati alla raccolta e aggregazione degli shot.

In questo passo, creiamo un QuantumProgram che esegue il nostro esperimento PNA usando il circuito template e il samplex costruiti in precedenza. La configurazione include i seguenti elementi:

  • template_circuit: Il circuito contenente tutti i gate necessari per implementare tutte le randomizzazioni desiderate (da randomizzazioni di twirling, parametri, e così via).

  • samplex: Un oggetto che definisce una distribuzione di probabilità su tutte le possibili randomizzazioni di circuito da cui campionare.

  • samplex_arguments: Associazioni necessarie per definire completamente il samplex

    • basis_changes: Qui specifichiamo un insieme di basi da misurare che coprirà tutti i termini di Pauli nell'osservabile misurato.

    • noise_scales.ref: Impostiamo la scala di ogni layer di rumore a 0.0 per evitare che venga iniettato rumore aggiuntivo nei nostri campioni.

    • pauli_lindblad_maps: Necessario se vengono passati noise_scales. Questo mappa semplicemente i layer di rumore al modello di rumore associato.

  • shape: Una tupla di forma per estendere la forma implicita definita da samplex_arguments. Gli assi non banali introdotti da questa estensione enumerano le randomizzazioni.

# 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,),
)

Campionare il circuito usando l'Executor​

Ora che abbiamo definito il nostro QuantumProgram, eseguire l'esperimento è semplice. Basta istanziare l'oggetto Executor, fornirgli il backend ed eseguire il programma.

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

Fase 4: Ricostruire e analizzare i risultati​

Per calcolare un valore di aspettazione mitigato dagli errori, facciamo quanto segue:

  • Calcolare i fattori di scala TREX in base al rumore appreso che influisce sulle misure,

  • Generare una maschera per mantenere solo i campioni post-selezionati, e

  • Usare la funzione executor_expectation_values da qiskit-addon-utils per combinare tutti i dati in un valore di aspettazione mitigato dagli errori.

# 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,
)

Confrontare strategie di mitigazione: PNA, PNA+TREX, PNA+PS, PNA+PS+TREX​

Calcoliamo e visualizziamo i valori di aspettazione per diverse varianti di mitigazione usando i risultati dell'Executor.

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 of the previous code cell

I risultati dimostrano i benefici cumulativi della combinazione di diverse tecniche di mitigazione degli errori. La sola PNA riporta già il valore di aspettazione vicino al benchmark ideale, indicando che propagare canali di rumore inverso nell'osservabile compensa efficacemente gli errori dei gate a due qubit.

  • Aggiungere la riponderazione TREX (PNA+TREX) migliora leggermente la stima correggendo lo squilibrio di campionamento nei circuiti randomizzati.

  • La post-selezione (PNA+PS) fornisce un miglioramento più evidente filtrando i risultati di misura incoerenti che probabilmente derivano da errori residui.

  • Infine, combinare entrambe (PNA+PS+TREX) produce il risultato più accurato, quasi corrispondente al valore ideale, mostrando come queste strategie di mitigazione si rafforzino a vicenda.

In generale, il confronto evidenzia che PNA costituisce una base solida per la stima del valore di aspettazione consapevole del rumore, mentre TREX e la post-selezione offrono raffinamenti complementari per ulteriori guadagni di accuratezza.

Prossimi passi​

Se hai trovato interessante questo lavoro, potresti essere interessato al seguente materiale: