Miglioramento dei valori attesi mediante l'assorbimento del rumore propagato (PNA)
Stima del tempo di esecuzione: 10 minuti su un processore Heron (NOTA: si tratta solo di una stima. (La durata potrebbe variare.)
Risultati di apprendimento
Dopo aver seguito questo tutorial, gli utenti dovrebbero aver compreso:
- Che cos’è l’assorbimento del rumore propagato (PNA) e in che modo mitiga il rumore dei gate a due qubit assorbendo i canali di rumore inversi appresi nell’osservabile misurato
- Come utilizzare
samplomaticper racchiudere in riquadri e annotare i livelli dei circuiti in vista di rotazioni, cambi di base e iniezione di rumore - Come ricavare il rumore di livello con
[NoiseLearnerV3]( /docs/guides/directed-execution-model#noiselearnerv3 ) e trasformarlo in un osservabile di mitigazione del rumore conqiskit-addon-pna - Come campionare circuiti randomizzati con le classi
ExecutoreQuantumPrograminqiskit-ibm-runtime, e combinare PNA con TREX e la post-selezione
Prerequisiti
Consigliamo agli utenti di acquisire familiarità con i seguenti argomenti prima di seguire questo tutorial:
- Il flusso di lavoro dei modelli di Qiskit
- Utilizzo della primitiva "Estimator" per calcolare i valori attesi di una grandezza osservabile
- Tecniche di mitigazione degli errori quali il “Pauli twirling” e il TREX, illustrate nella sezione “Combinare le opzioni di mitigazione degli errori con la primitiva Estimator”
Sfondo
In questo tutorial mostriamo come sfruttare gli strumenti avanzati di mitigazione degli errori disponibili in Qiskit per migliorare la stima del valore atteso negli esperimenti quantistici soggetti a rumore.
Che cos’è l’assorbimento del rumore propagato (PNA)?
L'assorbimento del rumore propagato è una tecnica volta a mitigare gli errori dei gate, che consiste nel propagare l'osservabile attraverso il canale di rumore inverso che influenza i gate a due qubit, ottenendo così un osservabile in grado di mitigare il rumore.
Possiamo ricorrere alla mitigazione degli errori quantistici per ricavare valori attesi utili da hardware quantistico soggetto a rumore senza richiedere una tolleranza completa ai guasti. La PNA si concentra specificamente sull'assorbimento degli effetti del rumore all'interno dell'osservabile stesso, piuttosto che sulla modifica del funzionamento del circuito.
Ogni gate soggetto a rumore in un circuito quantistico può essere modellato come un gate ideale seguito da un canale di rumore. Il PNA apprende o caratterizza questi canali di rumore e ne definisce le inverse. Anziché inserire le operazioni inverse nell'esecuzione hardware (cosa che in genere non è fattibile), il PNA propaga i canali di rumore inversi in avanti attraverso il circuito e li applica all'osservabile. Questo processo trasforma l'operatore osservabile in un nuovo operatore , in modo tale che la misurazione di su un circuito soggetto a rumore fornisca lo stesso valore atteso della misurazione di su un circuito ideale, privo di rumore.
Ciò può essere sintetizzato come segue:
- Modellare ogni porta soggetta a rumore come un'operazione ideale seguita da un canale di rumore .
- Determinare o stimare ogni utilizzando strumenti di caratterizzazione del rumore.
- Definire e propagare le mappe inverse del rumore in avanti attraverso il circuito utilizzando le tecniche di trasferimento di Pauli.
- Assorbire questi inversi nell'osservabile, ottenendo così un operatore con rumore attenuato .
Quando il rumore viene descritto come un canale di Pauli (o, più in generale, come un canale di Pauli-Lindblad sparsificato), tale propagazione può essere effettuata in modo efficiente utilizzando la propagazione di Pauli. La propagazione di Pauli fornisce un quadro di riferimento per approssimare il modo in cui i canali di rumore inversi si trasformano man mano che attraversano strati di operazioni Clifford e non Clifford, controllando al contempo la complessità computazionale.
Trasferendo la mitigazione nell'ambito dell'osservabile, il PNA evita l'elevato sovraccarico di campionamento che deriverebbe altrimenti dall'inserimento di operazioni di correzione fisica nel circuito. Viene invece eseguito il circuito rumoroso originale, mentre l'osservabile viene trasformato in un nuovo operatore il cui valore atteso annulla gli effetti del rumore.
Il flusso di lavoro PNA
Il processo può essere compreso attraverso le seguenti fasi concettuali. Il primo schema illustra un esperimento standard in presenza di rumore.
Se apprendiamo il modello di rumore, possiamo applicarne l'inverso ed eliminare il rumore.
Anziché implementare il canale di rumore inverso campionandolo sulla QPU, come avviene nella cancellazione probabilistica dell'errore (PEC), lo applichiamo in modo classico all'osservabile misurato utilizzando la propagazione di Pauli. L'osservabile risultante attenua efficacemente il rumore del gate appreso al momento della misurazione.
Mitigazione modulare degli errori con Samplomatic ed Executor
L'approccio PNA si basa su un'architettura modulare di mitigazione degli errori presente in Qiskit. Questa architettura utilizza la samplomatic libreria insieme alle classi QuantumProgram e Executor (aggiunte a Qiskit Runtime in qiskit-ibm-runtimev0.47.0 ) per rendere tecniche quali l'assorbimento del rumore propagato e il "Pauli twirling" componibili e riutilizzabili in diversi esperimenti.
Anziché incorporare la logica di mitigazione all'interno della definizione stessa del circuito, la mitigazione viene espressa in modo dichiarativo tramite samplomatic annotazioni e gestita a livello di programmazione tramite Executor, che controlla il modo in cui i circuiti randomizzati vengono generati, eseguiti e sottoposti a post-elaborazione.
In questo tutorial implementiamo un modello Qiskit per dimostrare come il PNA possa propagare i canali di rumore di Pauli inversi e modificare di conseguenza l'osservabile, al fine di migliorare la stima dei valori attesi su QPU soggette a rumore.
Panoramica del flusso di lavoro
- Fase 1: Trasposizione al problema quantistico
- Costruire un modello di Ising con kick "Trotterizzato" speculare e un osservabile di riferimento.
- Fase 2: Caratterizzazione e propagazione del rumore
- Utilizzare
samplomaticper identificare e annotare gli strati e le misurazioni univoci a due qubit presenti nel circuito. - Scopri quali rumori influenzano ogni singolo livello utilizzando
NoiseLearnerV3. - Associa ogni
InjectNoiseannotazione al modello di rumore appreso corrispondente. - Utilizzare
qiskit-addon-pnaper propagare i canali di rumore inversi in avanti attraverso il circuito e incorporarli nell'osservabile di destinazione.
- Utilizzare
- Fase 3: Eseguire esperimenti quantistici
- Definire un
QuantumProgramper specificare un campionamento casuale tramitesamplexed eseguire gli esperimenti sul backend utilizzando ilExecutor.
- Definire un
- Fase 4: Ricostruzione e analisi dei risultati
- Confrontare le strategie di mitigazione (PNA, PNA+TREX, PNA+PS, PNA+PS+TREX) e visualizzare il miglioramento rispetto ai risultati senza mitigazione.
Requisiti
Prima di iniziare questo tutorial, assicurati di avere installato quanto segue:
- Qiskit SDK v2.2 o versioni successive, con supporto alla visualizzazione
- Qiskit Runtime v0.47 o versioni successive (
pip install qiskit-ibm-runtime) - Samplomatic v0.13 o o versioni successive (
pip install samplomatic) - Componente aggiuntivo PNA per Qiskit (
pip install qiskit-addon-pna) - Utilità aggiuntive di Qiskit (
pip install qiskit-addon-utils)
Configura
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_pathEsempio di simulatore su piccola scala
Il PNA riduce il rumore fisico dei gate a due qubit di uno specifico processore quantistico. Il flusso di lavoro dipende da due servizi hardware che non hanno un equivalente significativo in un simulatore ideale:
NoiseLearnerV3caratterizza sperimentalmente il canale di rumore sparso di Pauli-Lindblad associato a ciascun singolo strato a due qubit del circuito transpiled. Su un simulatore silenzioso non c'è alcun rumore da apprendere, e il canale inverso propagato sarebbe quello identitario.- La
Executorfunzione "primitive" campiona i circuiti intrecciati e casuali generati dasamplomaticsu un backend.
In linea di principio, si potrebbe ricorrere a un modello di rumore sintetico. Ad esempio, potresti allegare PauliLindbladError delle istruzioni con Qiskit Aer e passare il circuito rumoroso risultante direttamente a generate_noise_mitigating_observable. Tuttavia, questo non fa altro che convalidare la contabilità classica rispetto al rumore che hai inserito tu stesso, e oscura il senso della tecnica. Per questo motivo tralasciamo l'esempio con il simulatore e illustriamo il flusso di lavoro completo del PNA direttamente sull'hardware, con ogni fase del modello Qiskit descritta in dettaglio qui di seguito.
Esempio di hardware su larga scala
Ora eseguiamo il flusso di lavoro PNA completo su un modello di Ising a 30 siti con effetto "kicked", eseguito su un hardware d IBM Quantum®, seguendo le quattro fasi di un modello Qiskit.
Fase 1: Trasposizione a un problema quantistico
Generare il circuito di Trotter speculare e l'osservabile
In questo esperimento studieremo la dinamica temporale di un modello di Ising a 30 siti con kick su una catena di spin di tipo “ 1D ”. L'hamiltoniano considerato è:
,
dove descrive l'accoppiamento tra gli spin più vicini, , mentre il campo trasversale globale, , è impostato su . Quanto più si allontana da un angolo di Clifford (ovvero ), tanto più difficile diventa propagare i generatori di antirumore attraverso il circuito.
Per quanto riguarda la scelta dell’osservabile, considereremo la magnetizzazione media per singolo sito, , dove è 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:
Fase 2: Ottimizzare il problema per l'esecuzione su hardware
Il passo successivo consiste nel preparare il nostro circuito Trotter specchiato per l'esecuzione su un hardware reale IBM Quantum. L'esecuzione su una QPU non si limita alla semplice costruzione del circuito astratto, poiché è necessario ottimizzarlo in modo che:
-
Rispetta l'insieme di porte e la connettività nativi del backend. La transpilazione mappa il circuito logico in un circuito ISA supportato dal backend di destinazione. Ciò garantisce che ogni interazione tra gate e qubit sia fisicamente realizzabile.
-
Possiamo caratterizzare il rumore a livello dei livelli circuitali. Il PNA si basa sull'apprendimento e sulla propagazione dei canali di rumore inversi. Per farlo in modo efficiente, suddividiamo il circuito transpilato in livelli “incapsulati” unici da due qubit. Queste caselle ci consentono di associare a ciascun strato del circuito il proprio modello di rumore appreso.
-
Possiamo inserire modelli di rumore realistici nel PNA. Una volta che il circuito è stato incapsulato, utilizziamo il
NoiseLearnerV3servizio per individuare sperimentalmente i canali di rumore di Pauli che influenzano ogni singolo strato a due qubit. Questi modelli appresi vengono poi ricollegati al circuito tramite le annotazioni di Samplomatic.
Nel complesso, questa fase colma il divario tra un circuito speculare idealizzato e un circuito pronto per l'implementazione hardware con modelli di rumore appresi. Con questa configurazione, il PNA è in grado di propagare canali di rumore inversi attraverso il circuito e di regolare di conseguenza l'osservabile.
Connettersi al backend e traspilare in un circuito ISA
Per prima cosa inizializziamo un servizio di comput IBM Quantum e e selezioniamo un backend. Effettua l'autenticazione con il tuo account seguendo le istruzioni per salvare le tue credenziali; in seguito, il sistema le QiskitRuntimeService() 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}")Output:
Selected backend: ibm_fez
Successivamente, scegliamo una catena connessa di qubit sul backend e trasponiamo il circuito su di essa.
Utilizzando l'helper find_qubit_chain definito nella sezione "Setup", selezioniamo una linea di num_qubits qubit fisici collegati. initial_layoutEseguiamo quindi la transpilazione con optimization_level=0 e fissiamo questa catena come, che preserva esattamente la struttura a livelli di porte a due qubit del circuito speculare. Tale struttura è alla base delle fasi di “boxing” e di “noise-learning”, pertanto in questo caso è necessario evitare un livello di ottimizzazione più elevato (che annullerebbe i gate specchiati).
# 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:
Ruota gli strati dei gate a due qubit e delle misurazioni, e individua gli strati univoci
Utilizziamo samplomatic per incapsulare il circuito e identificare livelli univoci a due qubit. Un box è un costrutto che raggruppa una serie di istruzioni in modo che determinati intenti o annotazioni possano essere successivamente applicati in modo uniforme a tutti i gate all’interno dello stesso box.
In questo caso, chiamiamo il metodo generate_boxing_pass_manager, che va oltre la semplice identificazione di strati a due qubit. Svolge diverse funzioni fondamentali:
- Raggruppa tutti gli strati a due qubit presenti nel circuito,
- Applica le
Twirlannotazioni eChangeBasisa quei livelli, - raggruppa le operazioni di misurazione in sezioni a sé stanti, e
- Applica l'annotazione
InjectNoisea ciascun strato di due qubit.
Queste annotazioni definiscono come vengono gestiti il rumore, i cambiamenti di base e il twirling lungo tutto il circuito. Inoltre, definiscono la struttura che verrà utilizzata in seguito per lo studio e la riduzione del rumore.
Le opzioni di configurazione principali sono:
enable_gates/enable_measures: True: Inserire in un riquadro tutti gli strati di gate a due qubit e le misurazioni terminali. All'interno delle caselle, le porte a singolo qubit sono rappresentate con orientamento a sinistra.measure_annotations: all: IncludereTwirlle annotazioni eChangeBasissulla casella di misura.twirling_strategy: active: Ruota tutti i qubit attivi in ogni casella contenente porte di entanglement.Twirl``inject_noise_targets: gates: AggiungiInjectNoiseannotazioni a tutte le caselle annotate contenenti porte di entanglement.inject_noise_strategy: uniform_modification: Regolare in modo uniforme tutti i livelli di rumore in tutto 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:
Generare il circuito modello e samplex, che definiscono le modalità di campionamento del circuito.
Qui aggiungiamo anche le misurazioni dello spettatore e quelle post-selezione, necessarie per eseguire la post-selezione sui campioni generati dal 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:
Impara a riconoscere il rumore utilizzando NoiseLearnerV3
Prima di poter applicare il PNA per la mitigazione degli errori, dobbiamo innanzitutto caratterizzare il rumore che agisce su ciascuno dei singoli livelli a due qubit e sul livello di misurazione del nostro circuito. A tal fine, utilizziamo il NoiseLearnerV3 programma per apprendere sperimentalmente i modelli di rumore per ciascun strato identificato in precedenza. Il sistema di apprendimento esegue esperimenti di tipo benchmarking che stimano il rumore del canale che influenza ciascun strato e restituisce un oggetto risultato contenente il modello appreso.
Iniziamo identificando gli strati univoci del nostro circuito utilizzando find_unique_box_instructions da samplomatic. In questo modo ci assicuriamo di apprendere il rumore una sola volta per ogni tipo di strato distinto, riducendo al minimo il numero di esperimenti e il costo totale degli shot. L'elenco dei livelli così ottenuto viene trasmesso al modello di apprendimento del rumore.
Esistono alcuni parametri chiave che determinano il modo in cui viene appreso il rumore:
num_randomizations: Numero di circuiti casuali utilizzati per ciascuna configurazione di apprendimento.shots_per_randomization: Numero di tentativi effettuati per ogni ciclo di apprendimento casuale.layer_pair_depths: Le profondità del circuito (misurate in numero di coppie) da utilizzare negli esperimenti di apprendimento.post_selection: Consente la post-selezione basata sui fronti utilizzandorxi gate 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()Visualizza i tassi di rumore appresi
Dopo aver appreso i modelli di rumore, possiamo esaminare la distribuzione dei tassi di errore dedotti sia per le operazioni su un qubit che per quelle su due qubit. Il codice riportato di seguito estrae le rappresentazioni di Pauli-Lindblad dai risultati relativi al rumore appreso e raccoglie i corrispondenti tassi di rumore.
Per ogni strato addestrato:
- Convertiamo il modello di rumore in un elenco sparso di
(pstr, qubits, rate)tuple, dovepstrè la stringa di Pauli che agisce sui qubit dati erateè il tasso di errore associato. - Suddividiamo le frequenze in termini relativi a un qubit (
len(pstr) == 1) e a due qubit (len(pstr) == 2). - Gli elenchi delle tariffe vengono quindi ordinati e se ne calcolano i valori mediani.
Rappresentiamo graficamente le distribuzioni dei tassi di rumore per un qubit (rosso) e due qubit (blu) su una scala logaritmica, con i rispettivi valori mediani indicati da linee verticali, in modo da poter confrontare le grandezze relative dei generatori di Pauli-Lindblad appresi. L'ordine dei tassi relativi ai qubit singoli e ai qubit doppi dipende dal dispositivo e dagli strati specifici sottoposti a caratterizzazione; in questa serie di esperimenti, i generatori a qubit singolo ( weight-1 ) presentano 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()Output:
<matplotlib.legend.Legend at 0x125336120>
Associare le scatole di derivazione al rumore appreso
Una volta ottenuti i modelli di rumore per ogni singolo strato a due qubit, dobbiamo collegarli alle corrispondenti InjectNoise annotazioni all’interno del circuito racchiuso nel riquadro.
La InjectNoise direttiva è un'annotazione samplomatic che utilizza i "dresser" a singolo qubit per iniettare rumore nel circuito in modo controllato e configurabile. Consente una modellizzazione modulare del rumore su diversi livelli.
Ogni InjectNoise annotazione comprende:
InjectNoise.ref- un identificatore univoco per l'annotazione. Viene utilizzato dall'oggettosamplexper assegnare correttamente il modello di rumore corrispondente.InjectNoise.modifier_ref(facoltativo) - un riferimento secondario che consente di ridimensionare il modello di rumore assegnato mediante un fattore moltiplicativo.
In questa fase, definiamo una corrispondenza tra ciascun InjectNoise.ref e il relativo modello di rumore appreso (PauliLindbladMap). Tale corrispondenza garantisce che ogni strato di gate di entanglement nel circuito sia abbinato al modello di rumore appropriato, in modo che gli effetti del rumore vengano applicati con precisione 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 già detto, questa operazione si svolge in due fasi. Per prima cosa, estendiamo un generatore di antirumore fino alla fine del circuito. Successivamente, propaghiamo l’osservabile attraverso quel generatore evoluto. Questo processo viene ripetuto per ciascun generatore anti-rumore presente nel circuito. In questa implementazione, ogni generatore di un determinato livello viene propagato in parallelo fino alla fine del circuito. Inoltre, si ricorre al multiprocessing Python per eseguire in parallelo sia la propagazione in avanti dell’anti-rumore sia la propagazione all’indietro dell’osservabile. Ciò impedisce l'accumulo di generatori evoluti in memoria e ottimizza al massimo le risorse di calcolo.
Quando si esegue PNA, è sempre necessario specificare un circuito soggetto a rumore e una grandezza osservabile. Se il circuito rumoroso è un circuito racchiuso in un riquadro con InjectNoise delle annotazioni, è necessario fornire la mappatura che abbiamo creato nel passaggio precedente. È anche possibile passare un circuito non boxed contenente PauliLindbladError istruzioni provenienti da qiskit-aer. In tal caso, refs_to_noise_models non è necessario fornirlo. Oltre agli input principali, si prega di tenere in considerazione anche quanto segue:
max_err_terms: Il numero di termini da mantenere in ciascun generatore anti-rumore man mano che la propagazione procede in avanti. Aumentare questo valore migliora in genere la precisione, ma non è garantito che tale andamento sia monotono.max_obs_terms: Il numero di termini da mantenere nell'osservabile di mitigazione del rumore, , man mano che viene propagato a ritroso attraverso l'anti-rumore evoluto. Valori più elevati aumentano generalmente la precisione, ma non è garantito che ciò avvenga in modo monotono.num_processes: Il numero di core da dedicare al processo. Ricordate che i generatori vengono propagati in avanti e applicati all'osservabile in parallelo.search_step: La fase di retropropagazione utilizza un metodo avido per coniugare approssimativamente due operatori nella base di Pauli. È possibile velocizzare questo metodo aumentandosearch_step. Vedi ilpauli-propConsultare la documentazione per ulteriori informazioni.num_to_measure: Sebbene questa variabile non sia un parametro di input pergenerate_noise_mitigating_observable, la utilizziamo per controllare quanti termini di vogliamo effettivamente misurare. In questo caso prendiamo in considerazione solo i primi 30 termini, che corrispondono ai termini originali presenti nel nostro osservabile. I termini sono stati ora ridimensionati in modo tale che la loro misurazione abbia l'effetto di attenuare il rumore del gate appreso. Sebbene si misurino solo 30 termini da , spesso è comunque utile lasciare che il numero cresca, 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]Output:
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), ".")Output:
[<matplotlib.lines.Line2D at 0x124b0a120>]
Trasformare le basi di misura in forma canonica
Successivamente, individuiamo un insieme minimo di basi di misura tale da coprire interamente ogni termine di Pauli nell'osservabile misurato ( è possibile misurare contemporaneamente più osservabili, purché commutino a livello di qubit ). Poiché stiamo misurando solo i termini della nostra grandezza osservabile originale, che è la somma di tutti i Pauli singoliZ , è necessaria una sola base: la base "all-Z ".
Oltre a individuare un insieme di basi di misura di Pauli, dobbiamo mappare questi termini di Pauli nella forma canonica prevista dalla Executor. Per ulteriori informazioni sull'ordinamento canonico dei qubit, consulta 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 esperimenti quantistici
Specificare le modalità di campionamento nel QuantumProgram
Ora configuriamo il QuantumProgram, che funge da contenitore centrale per tutti i circuiti e le configurazioni di campionamento che verranno eseguiti dal Executor. Questo oggetto definisce le modalità con cui le istanze di circuito generate in modo casuale vengono create, raggruppate in batch ed eseguite per produrre i risultati di misurazione utilizzati in PNA.
Un QuantumProgram può contenere più elementi, ciascuno dei quali è costituito da un circuito modello e da un oggetto corrispondente samplex che definisce le modalità di applicazione delle randomizzazioni. Questa astrazione consente al Executor di gestire l'intero flusso di lavoro come un unico programma modulare — dalla generazione casuale dei circuiti alla raccolta e all'aggregazione degli scatti.
In questa fase, creiamo un programma QuantumProgram che esegue il nostro esperimento PNA utilizzando il circuito modello e samplex il codice che abbiamo realizzato in precedenza. La configurazione comprende i seguenti elementi:
template_circuit: Il circuito contenente tutte le porte logiche necessarie per implementare tutte le randomizzazioni desiderate (tra cui randomizzazioni di tipo “twirling”, parametri e così via).samplex: Un oggetto che definisce una distribuzione di probabilità su tutte le possibili randomizzazioni del circuito da cui effettuare il campionamento.samplex_arguments: I vincoli necessari per definire completamente ilsamplexbasis_changes: È qui che specifichiamo un insieme di basi di misura che copra tutti i termini di Pauli presenti nell'osservabile misurato.noise_scales.ref: Abbiamo impostato la scala di ciascun livello di rumore su0.0per evitare che venga introdotto ulteriore rumore nei nostri campioni.pauli_lindblad_maps: Obbligatorio senoise_scalesvengono passati. Questo non fa altro che associare i livelli di rumore al modello di rumore corrispondente.
shape: Una tupla di forma per estendere la forma implicita definita dasamplex_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,),
)Provare il circuito utilizzando il Executor
Ora che abbiamo definito il nostro QuantumProgram, eseguire l'esperimento è semplicissimo. Basta creare un'istanza Executor dell'oggetto, 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: Ricostruzione e analisi dei risultati
Per calcolare un valore atteso corretto per l'errore, procediamo come segue:
- Calcolare i fattori di scala TREX in base al rumore appreso che influenza le misurazioni,
- Generare una maschera per mantenere solo i campioni selezionati in seguito, e
- Utilizzare la
executor_expectation_valuesfunzione daqiskit-addon-utilsper combinare tutti i dati in un valore atteso con errore ridotto.
# 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,
)Confronto tra le strategie di mitigazione: PNA, PNA+TREX, PNA+PS, PNA+PS+TREX
Calcoliamo e visualizziamo i valori attesi per diverse varianti di mitigazione utilizzando i Executor risultati.
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:
I risultati dimostrano i vantaggi cumulativi derivanti dalla combinazione di diverse tecniche di mitigazione degli errori. Il semplice approccio PNA riporta già il valore atteso vicino al benchmark ideale, indicando che la propagazione dei canali di rumore inversi nell’osservabile compensa efficacemente gli errori dei gate a due qubit.
- L'aggiunta della riponderazione TREX (PNA+TREX) migliora leggermente la stima, correggendo lo squilibrio di campionamento nei circuiti randomizzati.
- La post-selezione (PNA+PS) offre un miglioramento più evidente, filtrando i risultati di misurazione incoerenti che probabilmente derivano da errori residui.
- Infine, la combinazione di tutte e tre (PNA+PS+TREX) produce il risultato più accurato, che si avvicina quasi al valore ideale, dimostrando come queste strategie di mitigazione si rafforzino a vicenda.
Nel complesso, il confronto evidenzia che il PNA costituisce una solida base per la stima del valore atteso che tiene conto del rumore, mentre il TREX e la post-selezione offrono perfezionamenti complementari per ottenere ulteriori miglioramenti in termini di accuratezza.
Passi successivi
Se questo lavoro ti è sembrato interessante, potrebbero interessarti i seguenti contenuti: