Erwartungswerte mit Propagated Noise Absorption (PNA) verbessern
Geschätzte Nutzungsdauer: 10 Minuten auf einem Heron-Prozessor (HINWEIS: Dies ist nur eine Schätzung. Deine tatsächliche Laufzeit kann abweichen.)
Lernziele
-
Was propagated noise absorption (PNA) ist und wie es Zwei-Qubit-Gate-Rauschen mindert, indem gelernte inverse Rauschkanäle in das gemessene Observable absorbiert werden
-
Wie man
samplomaticverwendet, um Circuit-Layer für Twirling, Basiswechsel und Rauschinjektion zu boxen und zu annotieren -
Wie man Layer-Rauschen mit
[NoiseLearnerV3](/guides/directed-execution-model#noiselearnerv3) lernt und es mitqiskit-addon-pnain ein rauschminderndes Observable propagiert -
Wie man randomisierte Circuits mit den Klassen
QuantumProgramundExecutorinqiskit-ibm-runtimesampelt und PNA mit TREX und Post-Selection kombiniert
Voraussetzungen
-
Der Qiskit-Patterns-Workflow
-
Verwendung des Estimator-Primitives zur Berechnung von Erwartungswerten eines Observablen
-
Fehlerminderungstechniken wie Pauli-Twirling und TREX, behandelt in Fehlermitigationsoptionen mit der Estimator-Primitive kombinieren
Hintergrund
In diesem Tutorial zeigen wir, wie fortgeschrittene Fehlerminderungswerkzeuge in Qiskit genutzt werden können, um die Schätzung von Erwartungswerten in verrauschten Quantenexperimenten zu verbessern.
Was ist Propagated Noise Absorption (PNA)?
Propagated Noise Absorption ist eine Technik zur Minderung von Gate-Fehlern, bei der die Observable durch den inversen Rauschkanal propagiert wird, der Zwei-Qubit-Gates betrifft, wodurch eine rauschmindernde Observable entsteht.
Wir können Quantenfehlerminderung nutzen, um nützliche Erwartungswerte aus verrauschter Quantenhardware zu extrahieren, ohne dass volle Fehlertoleranz erforderlich ist. PNA konzentriert sich speziell darauf, Rauscheffekte in die Observable selbst zu absorbieren, anstatt die Schaltungsoperationen zu modifizieren.
Jedes verrauschte Gate in einer Quantenschaltung kann als ideales Gate modelliert werden, auf das ein Rauschkanal folgt. PNA lernt oder charakterisiert diese Rauschkanäle und definiert deren Inverse. Anstatt die inversen Operationen in die Hardware-Ausführung einzufügen (was in der Regel nicht durchführbar ist), propagiert PNA die inversen Rauschkanäle vorwärts durch die Schaltung und wendet sie auf die Observable an. Dieser Prozess transformiert die Observable in einen neuen Operator , sodass die Messung von auf der verrauschten Schaltung denselben Erwartungswert liefert wie die Messung von auf einer idealen, rauschfreien Schaltung.
Dies lässt sich wie folgt zusammenfassen:
- Modelliere jedes verrauschte Gate als ideale Operation, gefolgt von einem Rauschkanal .
- Lerne oder schätze jedes mithilfe von Rauschcharakterisierungswerkzeugen.
- Definiere und propagiere die inversen Rauschabbildungen mithilfe von Pauli-Transfer-Techniken vorwärts durch die Schaltung.
- Absorbiere diese Inversen in die Observable, wodurch ein rauschgeminderter Operator entsteht.
Wenn das Rauschen als Pauli-Kanal (oder allgemeiner als sparse Pauli-Lindblad-Kanal) beschrieben wird, kann diese Propagation effizient mithilfe von Pauli-Propagation durchgeführt werden. Pauli-Propagation bietet einen Rahmen zur Näherung, wie sich inverse Rauschkanäle beim Durchlaufen von Schichten aus Clifford- und Nicht-Clifford-Operationen transformieren, während die Rechenkomplexität kontrolliert wird.
Indem die Minderung in die Observable verlagert wird, vermeidet PNA den großen Sampling-Overhead, der sonst durch das Einfügen physischer Korrekturoperationen in die Schaltung entstehen würde. Stattdessen wird die ursprüngliche verrauschte Schaltung ausgeführt, während die Observable in einen neuen Operator transformiert wird, dessen Erwartungswert die Rauscheffekte aufhebt.
Der PNA-Workflow
Der Prozess lässt sich anhand der folgenden konzeptionellen Stufen verstehen. Das erste Schema veranschaulicht ein Standard-Rauschexperiment.
Wenn wir das Rauschmodell lernen, können wir dessen Inverse anwenden und das Rauschen aufheben.
Anstatt den inversen Rauschkanal wie bei probabilistic error cancellation (PEC) durch Sampling auf der QPU zu implementieren, wenden wir ihn klassisch mithilfe von Pauli-Propagation auf die gemessene Observable an. Die resultierende Observable mindert bei Messung effektiv das gelernte Gate-Rauschen.

Modulare Fehlerminderung mit Samplomatic und Executor
Der PNA-Ansatz baut auf einer modularen Fehlerminderungsarchitektur in Qiskit auf. Diese Architektur nutzt die Bibliothek samplomatic zusammen mit den Klassen QuantumProgram und Executor (die in qiskit-ibm-runtime v0.47.0 zu Qiskit Runtime hinzugefügt wurden), um Techniken wie Propagated Noise Absorption und Pauli-Twirling über verschiedene Experimente hinweg kombinierbar und wiederverwendbar zu machen.
Anstatt die Minderungslogik in die Schaltungsdefinition selbst einzubetten, wird die Minderung deklarativ durch samplomatic-Annotationen ausgedrückt und programmatisch über Executor verwaltet, der steuert, wie randomisierte Schaltungen erzeugt, ausgeführt und nachbearbeitet werden.
In diesem Tutorial implementieren wir ein Qiskit-Pattern, um zu zeigen, wie PNA inverse Pauli-Rauschkanäle propagieren und die Observable entsprechend modifizieren kann, um die Schätzung von Erwartungswerten auf verrauschten QPUs zu verbessern.
Workflow-Übersicht
-
Schritt 1: Auf das Quantenproblem abbilden
- Konstruiere ein gespiegeltes, trotterisiertes Kicked-Ising-Modell und eine Zielobservable.
-
Schritt 2: Rauschen charakterisieren und propagieren
-
Verwende
samplomatic, um eindeutige Zwei-Qubit-Schichten und Messungen in der Schaltung zu identifizieren und zu annotieren. -
Lerne mit
NoiseLearnerV3das Rauschen, das jede eindeutige Schicht betrifft. -
Ordne jede
InjectNoise-Annotation ihrem entsprechenden gelernten Rauschmodell zu. -
Verwende
qiskit-addon-pna, um die inversen Rauschkanäle vorwärts durch die Schaltung zu propagieren und sie in die Zielobservable zu absorbieren.
-
-
Schritt 3: Quantenexperimente ausführen
- Definiere ein
QuantumProgram, um randomisiertes Sampling übersamplexfestzulegen, und führe die Experimente mithilfe desExecutorauf dem Backend aus.
- Definiere ein
-
Schritt 4: Ergebnisse rekonstruieren und analysieren
- Vergleiche Minderungsstrategien (PNA, PNA+TREX, PNA+PS, PNA+PS+TREX) und visualisiere die Verbesserung gegenüber ungeminderten Ergebnissen.
Anforderungen
Stelle sicher, dass du Folgendes installiert hast, bevor du mit diesem Tutorial beginnst:
-
Qiskit SDK v2.2 oder höher, mit Visualisierungs-Unterstützung
-
Qiskit Runtime v0.47 oder höher (
pip install qiskit-ibm-runtime) -
Samplomatic v0.13 oder höher (
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
Kleinskaliges Beispiel auf dem Simulator
PNA mindert das physische Zwei-Qubit-Gate-Rauschen eines bestimmten Quantenprozessors. Der Workflow hängt von zwei Hardwarediensten ab, die auf einem idealen Simulator kein sinnvolles Analogon haben:
-
NoiseLearnerV3charakterisiert experimentell den sparse Pauli-Lindblad-Rauschkanal, der jeder eindeutigen Zwei-Qubit-Schicht der transpilierten Schaltung zugeordnet ist. Auf einem rauschfreien Simulator gibt es kein zu lernendes Rauschen, und der propagierte inverse Kanal wäre die Identität. -
Die
Executor-Primitive samplet die vonsamplomaticerzeugten, getwirlten, randomisierten Schaltungen auf einem Backend.
Im Prinzip könntest du ein synthetisches Rauschmodell einsetzen. Zum Beispiel könntest du mit Qiskit Aer PauliLindbladError-Instruktionen anhängen und die resultierende verrauschte Schaltung direkt an generate_noise_mitigating_observable übergeben. Dies validiert jedoch nur die klassische Buchführung gegen selbst eingespeistes Rauschen und verschleiert den Sinn der Technik. Aus diesem Grund überspringen wir das Simulatorbeispiel und demonstrieren den vollständigen PNA-Workflow direkt auf Hardware, wobei jeder Schritt des Qiskit-Patterns im Folgenden einzeln dargestellt wird.
Beispiel für großskalige Hardware
Wir führen nun den vollständigen PNA-Workflow für ein Kicked-Ising-Modell mit 30 Sites aus, das auf IBM Quantum®-Hardware ausgeführt wird, entlang der vier Schritte eines Qiskit-Patterns.
Schritt 1: Auf ein Quantenproblem abbilden
Die gespiegelte Trotter-Schaltung und Observable erzeugen
Für dieses Experiment untersuchen wir die Zeitdynamik eines Kicked-Ising-Modells mit 30 Sites auf einer 1D-Spinkette. Der betrachtete Hamilton-Operator lautet:
,
wobei die Kopplung benachbarter Spins beschreibt, , und das globale Transversalfeld auf gesetzt ist. Je weiter von einem Clifford-Winkel entfernt ist (das heißt ), desto schwieriger wird es, die Antirausch-Generatoren durch die Schaltung zu propagieren.
Als Observable betrachten wir die durchschnittliche Einzelplatz-Magnetisierung, , wobei die Anzahl der Sites ist.
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)

Schritt 2: Das Problem für die Hardware-Ausführung optimieren
Der nächste Schritt besteht darin, unsere gespiegelte Trotter-Schaltung für die Ausführung auf echter IBM-Quantum-Hardware vorzubereiten. Die Ausführung auf einer QPU erfordert mehr als nur das Konstruieren der abstrakten Schaltung, da wir sie so optimieren müssen, dass:
-
Sie den nativen Gate-Satz und die Konnektivität des Backends respektiert. Transpilation bildet die logische Schaltung auf eine vom Ziel-Backend unterstützte ISA-Schaltung ab. Dies stellt sicher, dass jedes Gate und jede Qubit-Interaktion physisch realisierbar ist.
-
Wir das Rauschen auf der Ebene der Schaltungsschichten charakterisieren können. PNA beruht auf dem Lernen und Propagieren inverser Rauschkanäle. Um dies effizient zu tun, zerlegen wir die transpilierte Schaltung in eindeutige, „geboxte“ Zwei-Qubit-Schichten. Diese Boxen ermöglichen es uns, jede Schicht der Schaltung mit ihrem eigenen gelernten Rauschmodell zu verknüpfen.
-
Wir realistische Rauschmodelle in PNA einspeisen können. Sobald die Schaltung geboxt wurde, nutzen wir den
NoiseLearnerV3-Dienst, um experimentell die Pauli-Rauschkanäle zu lernen, die jede eindeutige Zwei-Qubit-Schicht betreffen. Diese gelernten Modelle werden anschließend über die Annotationen von Samplomatic wieder mit der Schaltung verknüpft.
Insgesamt überbrückt dieser Schritt die Lücke zwischen einer idealisierten, gespiegelten Schaltung und einer hardwarebereiten Schaltung mit gelernten Rauschmodellen. Mit diesem Setup kann PNA inverse Rauschkanäle durch die Schaltung propagieren und die Observable entsprechend anpassen.
Mit dem Backend verbinden und in eine ISA-Schaltung transpilieren
Wir initialisieren zunächst den IBM Quantum Compute Service und wählen ein Backend aus. Authentifiziere dich mit deinem eigenen Konto, indem du die Anweisungen zum Speichern deiner Anmeldedaten befolgst; anschließend übernimmt QiskitRuntimeService() diese automatisch.
# 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
Als Nächstes wählen wir eine verbundene Kette von Qubits auf dem Backend aus und transpilieren die Schaltung darauf.
Mithilfe der im Abschnitt „Setup“ definierten Hilfsfunktion find_qubit_chain wählen wir eine Linie aus num_qubits verbundenen physischen Qubits aus. Anschließend transpilieren wir mit optimization_level=0 und legen diese Kette als initial_layout fest, wodurch die Zwei-Qubit-Gate-Schichtstruktur der gespiegelten Schaltung exakt erhalten bleibt. Auf diese Struktur stützen sich die Boxing- und Rauschlernschritte, weshalb eine höhere Optimierungsstufe (die die gespiegelten Gates aufheben würde) hier vermieden werden muss.
# 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)
Die Zwei-Qubit-Gate-Schichten und Messungen twirlen und eindeutige Schichten finden
Wir verwenden samplomatic, um die Schaltung zu boxen und eindeutige Zwei-Qubit-Schichten zu identifizieren. Eine Box ist ein Konstrukt, das Instruktionen gruppiert, sodass bestimmte Absichten oder Annotationen später einheitlich auf alle Gates innerhalb derselben Box angewendet werden können.
Hier rufen wir die Methode generate_boxing_pass_manager auf, die über das bloße Identifizieren von Zwei-Qubit-Schichten hinausgeht. Sie führt mehrere wichtige Aufgaben aus:
-
Gruppiert alle Zwei-Qubit-Schichten in der Schaltung,
-
Wendet die Annotationen
TwirlundChangeBasisauf diese Schichten an, -
Gruppiert Messoperationen in eigene geboxte Abschnitte, und
-
Wendet die Annotation
InjectNoiseauf jede Zwei-Qubit-Schicht an.
Diese Annotationen legen fest, wie Rauschen, Basiswechsel und Twirling in der gesamten Schaltung behandelt werden. Sie legen außerdem die Struktur fest, die später für das Lernen und Mindern von Rauschen verwendet wird.
Die wichtigsten Konfigurationsoptionen sind:
-
enable_gates/enable_measures: True: Boxt alle Zwei-Qubit-Gate-Schichten und terminalen Messungen. Einzel-Qubit-Gates werden innerhalb der Boxen links eingebettet (left-dressed). -
measure_annotations: all: Fügt die AnnotationenTwirlundChangeBasiszur Messbox hinzu. -
twirling_strategy: active: Twirlt alle aktiven Qubits in jeder Box, die verschränkende Gates enthält. -
inject_noise_targets: gates: FügtInjectNoise-Annotationen zu allen mitTwirlannotierten Boxen hinzu, die verschränkende Gates enthalten. -
inject_noise_strategy: uniform_modification: Skaliert alle Rauschschichten gleichmäßig über die gesamte Schaltung.
# 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)
Erzeuge die Vorlagenschaltung und den samplex, die festlegen, wie die Schaltung gesampelt wird.
Hier fügen wir außerdem Spectator- und Post-Selection-Messungen hinzu, die benötigt werden, um Post-Selection an den vom Executor ausgegebenen Samples durchzuführen.
# 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)

Das Rauschen mit NoiseLearnerV3 lernen
Bevor wir PNA zur Fehlerminderung anwenden können, müssen wir zunächst das Rauschen charakterisieren, das auf jede eindeutige Zwei-Qubit-Schicht und die Messschicht in unserer Schaltung wirkt. Dazu verwenden wir das NoiseLearnerV3-Programm, um experimentell Rauschmodelle für jede zuvor identifizierte Schicht zu lernen. Der Learner führt Experimente im Benchmarking-Stil durch, die den Rauschkanal schätzen, der jede Schicht betrifft, und gibt ein Ergebnisobjekt mit dem gelernten Modell zurück.
Wir beginnen damit, die eindeutigen Schichten in unserer Schaltung mithilfe von find_unique_box_instructions aus samplomatic zu identifizieren. Dies stellt sicher, dass wir das Rauschen nur einmal pro unterschiedlichem Schichttyp lernen, wodurch die Anzahl der Experimente und die gesamten Shot-Kosten minimiert werden. Die resultierende Liste von Schichten wird an den Noise Learner übergeben.
Es gibt einige wichtige Parameter, die steuern, wie das Rauschen gelernt wird:
-
num_randomizations: Anzahl der pro Lernkonfiguration verwendeten zufälligen Schaltungen. -
shots_per_randomization: Anzahl der Shots pro zufälliger Lernschaltung. -
layer_pair_depths: Die in den Lernexperimenten zu verwendenden Schaltungstiefen (gemessen in Anzahl der Paare). -
post_selection: Aktiviert kantenbasierte Post-Selection mithilfe vonrx-Gates, um Pulse nach der Messung anzuwenden.
# 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()
Die gelernten Rauschraten visualisieren
Nach dem Lernen der Rauschmodelle können wir die Verteilung der abgeleiteten Fehlerraten sowohl für Ein-Qubit- als auch für Zwei-Qubit-Operationen untersuchen. Der folgende Code extrahiert die Pauli-Lindblad-Darstellungen aus den gelernten Rauschergebnissen und sammelt die entsprechenden Rauschraten.
Für jede gelernte Schicht:
- Wir wandeln das Rauschmodell in eine dünnbesetzte Liste von
(pstr, qubits, rate)-Tupeln um, wobeipstrder auf die angegebenen Qubits wirkende Pauli-String undratedie zugehörige Fehlerrate ist. - Wir trennen die Raten in Ein-Qubit- (
len(pstr) == 1) und Zwei-Qubit-Terme (len(pstr) == 2). - Anschließend sortieren wir die Listen der Raten und berechnen ihre Medianwerte.
Wir stellen die Verteilungen der Ein-Qubit- (rot) und Zwei-Qubit-Rauschraten (blau) auf logarithmischer Skala dar, wobei ihre Medianwerte durch vertikale Linien markiert sind, sodass wir die relativen Größenordnungen der gelernten Pauli-Lindblad-Generatoren vergleichen können. Die Reihenfolge der Ein- und Zwei-Qubit-Raten hängt vom Gerät und von den jeweils charakterisierten Schichten ab; in diesem Durchlauf tragen die Einzel-Qubit-Generatoren (Gewicht 1) die größere Medianrate.
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>
Schaltungsboxen mit gelerntem Rauschen verknüpfen
Sobald wir Rauschmodelle für jede eindeutige Zwei-Qubit-Schicht erhalten haben, müssen wir sie mit den entsprechenden InjectNoise-Annotationen innerhalb der geboxten Schaltung verknüpfen.
Die InjectNoise-Direktive ist eine samplomatic-Annotation, die die Einzel-Qubit-Dresser verwendet, um Rauschen kontrolliert und konfigurierbar in die Schaltung einzuspeisen. Sie ermöglicht modulare Rauschmodellierung über verschiedene Schichten hinweg.
Jede InjectNoise-Annotation enthält:
InjectNoise.ref- eine eindeutige Kennung für die Annotation. Diese wird vomsamplex-Objekt verwendet, um das entsprechende Rauschmodell korrekt zuzuweisen.InjectNoise.modifier_ref(optional) - eine sekundäre Referenz, mit der das zugewiesene Rauschmodell um einen multiplikativen Faktor skaliert werden kann.
In diesem Schritt erstellen wir eine Zuordnung von jeder InjectNoise.ref zu ihrem entsprechenden gelernten Rauschmodell (PauliLindbladMap). Diese Zuordnung stellt sicher, dass jede verschränkende Gate-Schicht in der Schaltung mit dem passenden Rauschmodell gepaart wird, sodass Rauscheffekte während des Samplings und der anschließenden Rauschminderungsschritte korrekt angewendet werden.
# 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()
)
Die Observable durch das gelernte Antirauschen propagieren
Wie oben besprochen, geschieht dies in zwei Schritten. Zunächst propagieren wir einen Antirausch-Generator bis zum Ende der Schaltung. Danach propagieren wir die Observable durch diesen entwickelten Generator. Dieser Prozess wird für jeden Antirausch-Generator in der Schaltung wiederholt. In dieser Implementierung wird jeder Generator einer gegebenen Schicht parallel bis zum Ende der Schaltung propagiert. Zusätzlich wird Python-Multiprocessing verwendet, um sowohl die Vorwärtspropagation des Antirauschens als auch die Rückwärtspropagation der Observable parallel durchzuführen. Dies verhindert eine Anhäufung entwickelter Generatoren im Speicher und maximiert zudem die Rechenressourcen.
Beim Ausführen von PNA musst du immer eine verrauschte Schaltung und Observable bereitstellen. Wenn deine verrauschte Schaltung eine geboxte Schaltung mit InjectNoise-Annotationen ist, musst du die im vorherigen Schritt erstellte Zuordnung bereitstellen. Man kann auch eine nicht geboxte Schaltung mit PauliLindbladError-Instruktionen aus qiskit-aer übergeben. In diesem Fall muss refs_to_noise_models nicht bereitgestellt werden. Zusätzlich zu den primären Eingaben solltest du auch Folgendes berücksichtigen:
-
max_err_terms: Die Anzahl der Terme, die in jedem Antirausch-Generator während der Vorwärtspropagation beibehalten werden. Ein größerer Wert erhöht im Allgemeinen die Genauigkeit, dieses Verhalten ist jedoch nicht garantiert monoton. -
max_obs_terms: Die Anzahl der Terme, die in der rauschmindernden Observable während der Rückwärtspropagation durch das entwickelte Antirauschen beibehalten werden. Größere Werte erhöhen im Allgemeinen die Genauigkeit, jedoch nicht garantiert monoton. -
num_processes: Die Anzahl der dem Prozess zugewiesenen Kerne. Denke daran, dass die Generatoren parallel vorwärts propagiert und auf die Observable angewendet werden. -
search_step: Der Rückwärtspropagationsschritt verwendet eine gierige Methode, um zwei Operatoren in der Pauli-Basis näherungsweise zu konjugieren. Diese Methode kann durch Erhöhen vonsearch_stepbeschleunigt werden. Weitere Informationen findest du in derpauli-prop-Dokumentation. -
num_to_measure: Obwohl diese Variable keine Eingabe fürgenerate_noise_mitigating_observableist, verwenden wir sie, um zu steuern, wie viele Terme aus wir tatsächlich messen möchten. Hier messen wir nur die obersten 30 Terme, die den ursprünglichen Termen in unserer Observable entsprechen. Die Terme wurden nun so neu skaliert, dass ihre Messung die Wirkung hat, das gelernte Gate-Rauschen zu mindern. Obwohl wir nur 30 Terme aus messen, ist es oft dennoch sinnvoll, es groß werden zu lassen, da dies die Präzision der Skalierungsfaktoren der führenden Terme erhöht.
# 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>]
Die Messbasen in kanonische Form transformieren
Als Nächstes finden wir einen minimalen Satz von zu messenden Basen, sodass wir jeden Pauli-Term in der gemessenen Observable vollständig abdecken können (viele Observablen können gleichzeitig gemessen werden, wenn sie qubitweise kommutieren). Da wir nur die Terme in unserer ursprünglichen Observable messen, die die Summe aller Einzel-Z-Paulis ist, wird eine einzige Basis benötigt: die All-Z-Basis.
Zusätzlich zum Finden einer Menge von Pauli-Messbasen müssen wir diese Pauli-Terme in die kanonische Form abbilden, die vom Executor erwartet wird. Weitere Informationen zur kanonischen Qubit-Reihenfolge findest du in der Samplomatic-Dokumentation.
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
]
Schritt 3: Quantenexperimente ausführen
Festlegen, wie im QuantumProgram gesampelt wird
Wir richten nun das QuantumProgram ein, das als zentraler Container für alle Schaltungen und Sampling-Konfigurationen dient, die vom Executor ausgeführt werden. Dieses Objekt legt fest, wie randomisierte Schaltungsinstanzen erzeugt, gebündelt und ausgeführt werden, um die in PNA verwendeten Messergebnisse zu erzeugen.
Ein QuantumProgram kann mehrere Elemente enthalten, von denen jedes aus einer Vorlagenschaltung und einem zugehörigen samplex-Objekt besteht, das festlegt, wie die Randomisierungen angewendet werden. Diese Abstraktion ermöglicht es dem Executor, den gesamten Workflow als ein einziges, modulares Programm zu behandeln — von der randomisierten Schaltungserzeugung bis zur Shot-Sammlung und -Aggregation.
In diesem Schritt erstellen wir ein QuantumProgram, das unser PNA-Experiment mit der zuvor erstellten Vorlagenschaltung und dem zuvor erstellten samplex ausführt. Die Konfiguration umfasst die folgenden Elemente:
-
template_circuit: Die Schaltung, die alle Gates enthält, die zur Implementierung aller gewünschten Randomisierungen erforderlich sind (aus Twirling-Randomisierungen, Parametern und so weiter). -
samplex: Ein Objekt, das eine Wahrscheinlichkeitsverteilung über alle möglichen Schaltungsrandomisierungen definiert, aus denen gesampelt wird. -
samplex_arguments: Bindungen, die notwendig sind, um densamplexvollständig zu definieren-
basis_changes: Hier legen wir eine Menge von zu messenden Basen fest, die alle Pauli-Terme im gemessenen Observable abdecken. -
noise_scales.ref: Wir setzen die Skala jeder Rauschschicht auf0.0, um zu verhindern, dass zusätzliches Rauschen in unsere Samples eingebracht wird. -
pauli_lindblad_maps: Erforderlich, wennnoise_scalesübergeben werden. Dies ordnet lediglich Rauschschichten dem zugehörigen Rauschmodell zu.
-
-
shape: Ein Shape-Tupel, um die implizite Form zu erweitern, die durchsamplex_argumentsdefiniert wird. Nicht-triviale Achsen, die durch diese Erweiterung eingeführt werden, zählen die Randomisierungen auf.
# 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,),
)
Die Schaltung mit dem Executor sampeln
Nachdem wir unser QuantumProgram definiert haben, ist die Ausführung des Experiments unkompliziert. Wir instanziieren einfach das Executor-Objekt, versehen es mit dem Backend und führen das Programm aus.
# Execute (sample) the circuit
executor = Executor(backend)
job_exec = executor.run(program)
exec_results = job_exec.result()
Schritt 4: Ergebnisse rekonstruieren und analysieren
Um einen fehlergeminderten Erwartungswert zu berechnen, gehen wir wie folgt vor:
-
Berechne die TREX-Skalierungsfaktoren basierend auf dem gelernten Rauschen, das die Messungen beeinflusst,
-
Erzeuge eine Maske, um nur post-selektierte Samples zu behalten, und
-
Verwende die Funktion
executor_expectation_valuesausqiskit-addon-utils, um alle Daten zu einem fehlergeminderten Erwartungswert zusammenzuführen.
# 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,
)
Mitigationsstrategien vergleichen: PNA, PNA+TREX, PNA+PS, PNA+PS+TREX
Wir berechnen und visualisieren Erwartungswerte für mehrere Mitigationsvarianten anhand der Executor-Ergebnisse.
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()
Die Ergebnisse zeigen die kumulativen Vorteile der Kombination verschiedener Fehlermitigationstechniken. Der einfache PNA-Ansatz bringt den Erwartungswert bereits nahe an den idealen Benchmark zurück, was darauf hindeutet, dass die Propagation inverser Rauschkanäle in das Observable Zwei-Qubit-Gate-Fehler effektiv kompensiert.
-
Das Hinzufügen von TREX-Reweighting (PNA+TREX) verbessert die Schätzung leicht, indem es das Sampling-Ungleichgewicht in den randomisierten Schaltungen korrigiert.
-
Post-Selektion (PNA+PS) sorgt für eine deutlichere Verbesserung, indem inkonsistente Messergebnisse herausgefiltert werden, die wahrscheinlich auf Restfehler zurückzuführen sind.
-
Schließlich liefert die Kombination beider Methoden (PNA+PS+TREX) das genaueste Ergebnis, das nahezu dem Idealwert entspricht, was zeigt, wie sich diese Mitigationsstrategien gegenseitig verstärken.
Insgesamt zeigt der Vergleich, dass PNA eine robuste Grundlage für die rauschbewusste Schätzung von Erwartungswerten bildet, während TREX und Post-Selektion komplementäre Verfeinerungen für weitere Genauigkeitsgewinne bieten.
Nächste Schritte
Wenn du diese Arbeit interessant fandest, könnten dich folgende Materialien interessieren:
