Probabilistic error cancellation mit shaded lightcones
Geschätzte Nutzungsdauer: 10 Minuten auf einem Heron-Prozessor (HINWEIS: Dies ist nur eine Schätzung. Deine tatsächliche Laufzeit kann abweichen.)
Lernziele
-
Was probabilistic error cancellation (PEC) ist und warum ihr Sampling-Overhead exponentiell mit dem gesamten auf den Circuit wirkenden Rauschen wächst
-
Wie shaded lightcones (SLC) den Beitrag jedes Rauschterms zum Ziel-Observable begrenzen, sodass du das Minderungsbudget dort einsetzen kannst, wo es wirklich zählt — und wie die Shading-Methode dir über die bereits durch ein rein geometrisches Lightcone erzielte Overhead-Reduktion ohne Bias hinaus erlaubt, einen begrenzten Rest-Bias gegen einen noch geringeren Sampling-Overhead einzutauschen
-
Wie man Layer-Rauschen mit
NoiseLearnerV3lernt und Anti-Rauschen übersamplomaticund dieExecutor-Primitive injiziert -
Wie man PEC und PEC+SLC mit TREX und Postselection kombiniert, um einen Erwartungswert auf Hardware zu schätzen
Voraussetzungen
-
Der Qiskit-Patterns-Workflow
-
Verwendung des Estimator-Primitives zur Berechnung von Erwartungswerten eines Observablen
-
Fehlerminderungstechniken wie Pauli Twirling und TREX, behandelt in Fehlerminderungsoptionen mit dem Estimator-Primitive kombinieren
Hintergrund
Dieses Tutorial zeigt, wie man Fehler mit dem shaded lightcone (SLC)-Addon mindert. Dieses Addon ist eine Weiterentwicklung der probabilistic error cancellation (PEC)-Technik, bei der das Rauschen eindeutiger Layer in einem Circuit gelernt und anschließend durch Anwenden von Einzel-Qubit-Gates und Nachbearbeitungstechniken aufgehoben wird. Im Vergleich zu anderen Methoden bietet PEC robustere Schranken für den Bias des gemilderten Ergebnisses, leidet aber tendenziell unter einem höheren Overhead in Bezug auf QPU-Zeit. Um die Abschwächung des Erwartungswerts durch Rauschen auszugleichen, wird das durchschnittliche Ergebnis bei PEC mit einem Faktor neu skaliert, wobei die gelernte Rauschrate des Fehler-Pauli bei Layer im Circuit ist. Diese Neuskalierung erhöht die Varianz um den Faktor und vervielfacht damit auch die Anzahl der auf der QPU benötigten Circuit-Ausführungen um — wir nennen dies die Sampling-Kosten oder den Sampling-Overhead. Da exponentiell wächst, ist PEC oft auf flache Circuits oder solche mit wenigen Qubits beschränkt. Mehr über PEC erfährst du in Probabilistic error cancellation with sparse Pauli-Lindblad models on noisy quantum processors.
Wenn wir Fehler identifizieren können, die nicht gemindert werden müssen, können wir diese Sampling-Kosten exponentiell senken. Ein erster Schritt in diese Richtung ist die Implementierung einer lokal bewussten Fehlerminderung, die ein schnell berechenbares konventionelles „Lightcone“ verwendet, um den PEC-Overhead zu reduzieren, indem die Empfindlichkeit eines Observable gegenüber Fehlern im gesamten Circuit begrenzt wird — dies erweitert die Machbarkeit von PEC bei manchen Problemen auf größere Skalen. Fehler außerhalb dieses Lightcones können das gemessene Ergebnis nicht beeinflussen und können daher vom Fehleraufhebungsverfahren ausgeschlossen werden. Dieser Ausschluss verringert den Sampling-Overhead, in manchen Fällen erheblich, ohne zusätzlichen Bias einzuführen. Insbesondere pendelt sich beim Messen eines lokalen Observable eines Circuits fester Tiefe der benötigte Sampling-Overhead schließlich ein, wenn man die Anzahl der Qubits im Circuit skaliert (siehe Abb. 2b in Locality and error mitigation of quantum circuits).
Shaded lightcones (SLC) gehen noch weiter, indem sie klassische Simulationen nutzen, um die Empfindlichkeit gegenüber Fehlern im gesamten Circuit enger zu begrenzen. Dies tauscht etwas QPU-Zeit gegen CPU-Zeit und verringert den Sampling-Overhead, der zur Renormierung des Bias benötigt wird. Statt eines harten Cutoffs erhält jeder mögliche Fehler im Circuit eine abgestufte „Shade“, die die Empfindlichkeit des Erwartungswerts des Observable gegenüber diesem Fehler nach oben begrenzt. (Bei den weiter unten eingeführten rückwärts propagierten Schranken hängt diese Empfindlichkeit auch vom Anfangszustand des Circuits ab — dem All-Null-Zustand in typischen Workflows und auch in diesem Tutorial.) Diese verfeinerte Charakterisierung ermöglicht effizientere, gezieltere Anwendungen von PEC mit reduzierter Varianz und gibt dem Nutzer gleichzeitig die Möglichkeit, den Bias bei der Observable-Schätzung kontrolliert einzustellen. Weitere Details findest du in Lightcone shading for classically accelerated quantum error mitigation.
Unser Workflow für das SLC-Addon nutzt die samplomatic-Bibliothek zusammen mit den Klassen QuantumProgram und Executor, die in qiskit-ibm-runtime v0.47.0 zu Qiskit Runtime hinzugefügt wurden, und ermöglicht Nutzern eine modularere Kontrolle über Ausführungseinstellungen für Fehlerunterdrückung und -minderung bei gleichzeitig einfacher Bedienbarkeit. Mehr dazu findest du im Guide directed execution model.
SLC-Fehlerminderungs-Workflow auf einen Blick
Zur Modellierung des Rauschens der QPU verwenden wir ein sparse Pauli-Lindblad-Rauschmodell mit Ein- und Zwei-Qubit-Pauli-Fehlerraten, die lokal auf jedem Qubit und jeder Kante des Geräts erzeugt werden. Mit dieser Wahl sieht der in diesem Tutorial vorgestellte SLC-Fehlerminderungs-Workflow wie folgt aus:
a. CPU — Begrenzung der Auswirkung einzelner Fehler von Ein- und Zwei-Qubit-Pauli-Fehlern
-
Vorwärtspropagation (Begrenzung der Auswirkung auf das Observable). Propagiere jeden Fehler bis zum Ende des Circuits und berechne seinen Kommutator mit dem Observable.
-
Kürze Operatorterme während der Evolution, um die Berechnung handhabbar zu halten.
-
Verschärfe diese Schranken zusätzlich durch eine lose Rückwärtspropagation des Observable auf Basis von Quantum-Speed-Limits.
-
-
Rückwärtspropagation (Begrenzung der Auswirkung auf den Anfangszustand). Propagiere jeden Fehler bis zum Anfang des Circuits und berechne seinen Kommutator mit dem Anfangszustand.
b. QPU — Rauschraten lernen. Verwende NoiseLearnerV3, um die Raten des Pauli-Lindblad-Rauschmodells zu schätzen.
c. CPU — Minderung priorisieren
-
Aktualisiere die zusammengeführten Schranken mit den gelernten Rauschraten. Kombiniere die zuvor berechneten Vorwärts- und Rückwärtsschranken und aktualisiere sie mit den gelernten Rauschraten.
-
Ordne die zu mindernden Rauschkomponenten anhand der berechneten Schranken und gelernten Raten nach Rang. Priorisiere jeden möglichen Rauschfehler basierend auf seiner geschätzten Auswirkung auf den Bias und dem damit verbundenen Aufwand zur Korrektur.
d. QPU — Anti-Rauschen einfügen und ausführen. Führe den betreffenden Circuit mit Anti-Rauschen (inversem Rauschen) aus, das mittels Box-Annotationen festgelegt wird.
e. CPU — Observable schätzen. Berechne den Erwartungswert und wende dabei messungsbasierte Postselection an, um die Auswirkung von nicht-markowschem Rauschen zu verringern.
Überblick über das Rauschlernen
Rauschlernen ist ein gängiger Schritt in mehreren Fehlerminderungsmethoden, der vom Noise Learner durchgeführt wird; er kommt auch im Tutorial zur probabilistic error amplification vor. In NoiseLearnerV3 kannst du die zu lernenden Rausch-Layer gezielt als CircuitInstruction-Objekte identifizieren, sodass du die gewünschten SLC-Rauschschranken für jeden Layer wie oben beschrieben berechnen kannst. Das gelernte Pauli-Lindblad-Modell liefert Koeffizienten, die bei der PEC+SLC-Priorisierung verwendet werden. Wie die Gates zu Layern zusammengefasst werden, lässt sich mit den Convenience-Funktionen generate_boxing_pass_manager und find_unique_box_instructions bestimmen; das Ergebnis wird anschließend, wie in Schritt 2 unten beschrieben, in die SLC-Hilfsfunktion generate_noise_model_paulis eingespeist.
| Teil 1 | Teil 2 | Teil 3 |
|---|---|---|
| Zwei-Qubit-Gate-Layer per Pauli-Twirling verwirbeln | Identitätspaare von Layern wiederholen und Rauschen lernen | Eine Fidelity ableiten (Fehler für jeden Rauschkanal) |
![]() | ![]() |
Überblick über die Nachbearbeitung
Nach der Ausführung auf Quanten-Hardware mithilfe des samplomatic- und Executor-Frameworks wandeln wir unsere Bitstring-Messungen in den gewünschten Observable-Wert um. In diesem Tutorial demonstrieren wir den Workflow an einem gespiegelten Ising-Circuit (konstruiert in Schritt 1 unten), für den der ideale Erwartungswert des gemessenen Observable 1 beträgt, weil die zweite Hälfte des Circuits die erste rückgängig macht. Bei der Berechnung des Observable-Werts mit der Funktion executor_expectation_values wenden wir einige Nachbearbeitungstechniken an, die die Rauschauswirkung verringern. Dazu gehören das Entfernen von Shots, die von nicht-markowschem Rauschen betroffen sind, Readout-Fehlerminderung sowie die Berücksichtigung von Details unserer PEC-Implementierung. Details werden in Schritt 4 unten besprochen.
Anforderungen
Bevor du mit diesem Tutorial beginnst, stelle sicher, dass Folgendes installiert ist:
-
Qiskit SDK v2.2 oder neuer, mit Unterstützung für Visualisierung
-
Qiskit Runtime v0.47 oder neuer (
pip install qiskit-ibm-runtime) -
Shaded-lightcones-Qiskit-Addon v0.1 oder neuer (
pip install qiskit-addon-slc) -
Qiskit Addon Utils v0.3 oder neuer (
pip install qiskit-addon-utils) -
Samplomatic v0.13 oder neuer (
pip install samplomatic)
Setup
Importiere zunächst die Pakete und Funktionen, die zum Ausführen dieses Notebooks benötigt werden.
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-addon-slc qiskit-addon-utils qiskit-ibm-runtime samplomatic
from multiprocessing import set_start_method
# Setting this value prevents itertools.starmap deadlock on UNIX systems
set_start_method("spawn")
# Needed to prevent PySCF from parallelizing internally (SLC only)
%set_env OMP_NUM_THREADS=1
env: OMP_NUM_THREADS=1
import numpy as np
from matplotlib import pyplot as plt
from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler import generate_preset_pass_manager, PassManager
from qiskit_ibm_runtime import (
QiskitRuntimeService,
QuantumProgram,
Executor,
NoiseLearnerV3,
)
import samplomatic
from samplomatic.utils import find_unique_box_instructions
from samplomatic.transpiler import generate_boxing_pass_manager
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 (
gamma_from_noisy_boxes,
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_slc.bounds import (
compute_backward_bounds,
compute_forward_bounds,
compute_local_scales,
merge_bounds,
tighten_with_speed_limit,
)
from qiskit_addon_slc.utils import (
generate_noise_model_paulis,
map_modifier_ref_to_ref,
)
from qiskit_addon_slc.visualization import draw_shaded_lightcone
Kleines Simulator-Beispiel
Wie andere lernbasierte Fehlerminderungsmethoden mindert PEC mit shaded lightcones das physikalische Rauschen eines bestimmten Quantenprozessors und ist daher auf Hardware-Dienste angewiesen, für die es auf einem idealen Simulator kein sinnvolles Analogon gibt:
-
NoiseLearnerV3charakterisiert den sparse Pauli-Lindblad-Rauschkanal experimentell auf jedem eindeutigen Zwei-Qubit-Layer. Auf einem rauschfreien Simulator gibt es kein Rauschen aufzuheben. -
Die
Executor-Primitive samplet die getwirlten, mit Anti-Rauschen versehenen Circuits, die vonsamplomaticerzeugt werden, auf einem Backend.
Die Berechnung der shaded-lightcone-Schranken ist klassisch, aber sie ist nur relativ zu den gelernten Hardware-Rauschraten aussagekräftig, die das Minderungsbudget und den Sampling-Overhead festlegen. Aus diesen Gründen überspringen wir das kleine Simulator-Beispiel und demonstrieren den vollständigen PEC+SLC-Workflow direkt auf Hardware, wobei jeder Schritt des Qiskit-Patterns im Folgenden einzeln aufgeschlüsselt wird.
Großskaliges Hardware-Beispiel
Wir führen den vollständigen PEC+SLC-Workflow auf einem gespiegelten 20-Qubit-Ising-Circuit aus, der auf IBM Quantum®-Hardware ausgeführt wird, und folgen dabei den vier Schritten eines Qiskit-Patterns.
Schritt 1: Das Problem abbilden
Unsere Demonstration ist wie folgt aufgebaut:
-
Wir entscheiden uns, eine Zeitentwicklung unter einem 1D-Ising-Ketten-Hamiltonian durchzuführen. Die 1D-Ising-Kette liefert eine schön dichte Circuit-Struktur, die sich gut eignet, um PEC-Implementierungen zu demonstrieren.
-
Wir entscheiden uns, das Observable zu messen, das eine - und eine -Komponente enthält. Das eignet sich gut, um zu zeigen, wie die SLC-Schranken verschiedene Fehlertypen unterschiedlich behandeln.
-
Wir möchten, dass der ideale Erwartungswert dieses Observable genau 1 ist, damit die Qualität der gemilderten Schätzungen leicht beurteilt werden kann. Das Spiegeln des Circuits bringt uns dabei fast ans Ziel: Für jedes Gate in der zweiten Hälfte des Circuits gibt es ein inverses Gate in der ersten Hälfte, sodass der ideale Circuit wie die Identität wirkt.
-
Schließlich erfordert das Erreichen des idealen Erwartungswerts von 1, dass der Circuit in einem +1-Eigenzustand des Observable beginnt — und dank der Spiegelung im Idealfall auch endet. Da das Observable eine Nicht--Komponente enthält und die
Executor-Primitive die gewünschte Messbasis am Ende des Circuits berücksichtigt, stellen wir die entsprechenden Basis-Präparations-Gates am Anfang des Spiegel-Circuits mit einerprepare_basis-Funktion voran. Wir verwenden die Funktionget_measurement_bases, um zu bestimmen, welche Gates benötigt werden und wo sie platziert werden müssen, wobei wir die Feinheiten der Qubit-Indizierung im Auge behalten, die sich aus denbox-Annotationskonventionen ergeben, wie im Abschnitt zur Vorbereitung kanonischer Basismessungen besprochen.
# Width of the Ising chain (a free parameter of this demonstration)
num_qubits = 20
# The observable to measure: one X component (on qubit 6) and one Z component
# (on qubit 13), as motivated in "Step 1: Map the problem" above
target_obs_sparse = [("XZ", [6, 13], 1.0)]
observable = SparsePauliOp.from_sparse_list(
target_obs_sparse, num_qubits=num_qubits
)
# Determine the measurement basis each qubit requires for this observable.
# The reverser is used during post-processing in Step 4.
bases_virt, reverser_virt = get_measurement_bases(observable)
# Free parameters of the demonstration circuit: the number of Trotter steps
# sets the circuit depth, and rx_angle is the transverse-field rotation angle
# applied in each step (pi/4 makes the dynamics non-Clifford)
num_trotter_steps = 10
rx_angle = np.pi / 4
def construct_ising_circuit(
num_qubits: int,
num_trotter_steps: int,
rx_angle: float,
) -> QuantumCircuit:
circuit = QuantumCircuit(num_qubits)
for _ 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)
return circuit
def prepare_basis(
circuit: QuantumCircuit, basis: list[int]
) -> QuantumCircuit:
# basis is a list of integer values from 0 to 3. These map to the basis measurement as:
# 0 = I; 1 = Z; 2 = X; 3 = Y
assert len(basis) == circuit.num_qubits
out_circ = circuit.copy_empty_like()
for qb, bas in enumerate(basis):
if bas == 2:
out_circ.h(qb)
elif bas == 3:
out_circ.rx(-np.pi / 2, qb)
out_circ.barrier()
out_circ.compose(circuit, inplace=True)
return out_circ
def mirror_circuit(circuit: QuantumCircuit) -> QuantumCircuit:
mirror_circ = circuit.copy_empty_like()
mirror_circ.compose(circuit.inverse(), inplace=True)
mirror_circ.barrier()
mirror_circ.compose(circuit, inplace=True)
mirror_circ.measure_active()
return mirror_circ
# Instantiate the mirrored circuit and prepend the basis-preparation gates
circuit = construct_ising_circuit(num_qubits, num_trotter_steps, rx_angle)
mirrored_circuit = mirror_circuit(circuit)
mirrored_circuit = prepare_basis(mirrored_circuit, bases_virt[0])
mirrored_circuit.draw(
"mpl", fold=-1, scale=0.3, idle_wires=False, measure_arrows=False
)

Schritt 2: Optimieren
Wir optimieren Details im Zusammenhang mit dem auszuführenden Circuit, dem zu messenden Observable und den Rauschlern-Parametern. Als Ausgangspunkt wählen wir den am wenigsten ausgelasteten Heron-Prozessor und instanziieren ihn mit aktivierten Fractional Gates. Diese Fractional Gates ermöglichen eine höhere Empfindlichkeit bei einem Teil unserer Postselection-Filterung.
# Initialize IBM Quantum Compute Service using your saved credentials
service = QiskitRuntimeService()
# Select the least busy Heron device. This tutorial targets Heron processors:
# the usage estimate and the non-Markovian-noise post-selection discussed in
# Step 4 are specific to this processor family.
backend = service.least_busy(
operational=True,
simulator=False,
filters=lambda b: b.processor_type["family"] == "Heron",
)
# 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_kingston
Zunächst transpilieren wir unseren Circuit in ISA-Instruktionen, wie es für die Ausführung auf unseren QPUs erforderlich ist. Die Wahl der physischen Qubits, auf denen ausgeführt wird, hat einen großen Einfluss auf die Qualität der Ergebnisse, da Kohärenzzeiten und Gate-Fidelitäten über ein Gerät hinweg variieren und sich mit der Zeit verändern. Für ein sorgfältiges Experiment kann es sich lohnen, eine Kette von Qubits von Hand auszuwählen, informiert durch schnelle Benchmarking-Experimente (siehe zum Beispiel das Tutorial Real-time benchmarking for qubit selection). Für die Zwecke dieses Tutorials verlassen wir uns auf den Qiskit-Transpiler, um ein Layout basierend auf der Coupling Map des Backends und dessen aktuellsten Kalibrierungsdaten auszuwählen.
isa_pm = generate_preset_pass_manager(backend=backend, optimization_level=1)
isa_circuit = isa_pm.run(mirrored_circuit)
# The chain of physical qubits selected by the transpiler
layout = isa_circuit.layout.final_index_layout()
print(f"Selected layout: {layout}")
isa_observable = observable.apply_layout(
layout, num_qubits=isa_circuit.num_qubits
)
Selected layout: [71, 58, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 36, 21, 22, 23, 16, 3, 2]
wire_order = layout + [
q for q in range(isa_circuit.num_qubits) if q not in layout
]
isa_circuit.draw(
"mpl",
fold=-1,
scale=0.3,
idle_wires=False,
wire_order=wire_order,
measure_arrows=False,
)

Den Circuit boxen
Der Einfachheit halber verwenden wir den Transpilations-Pass generate_boxing_pass_manager, der die Circuit-Instruktionen in annotierte Boxen einteilt. Diese Boxen zeigen im Fall von PEC klar an, wo Anti-Rauschen in den Circuit injiziert werden soll. Details zu den Einstellungen findest du in der Samplomatic-Dokumentation.
Beachte, dass der SLC-Workflow später im Prozess die Verwendung von inject_noise_strategy="individual_modification" erfordert, da dies es uns ermöglicht, die InjectNoise-Annotationen der BoxOp-Instruktionen im Circuit eindeutig zu identifizieren. (Boxen ohne InjectNoise-Annotation können auf diese Weise nicht identifiziert werden.)
Die SLC-Schrankenberechnungen gehen derzeit davon aus, dass die InjectNoise-Annotationen mit site="after" platziert werden (siehe qiskit-addon-slc#33). Da sich der Standardwert dieser Einstellung in neueren Versionen von samplomatic geändert hat (siehe samplomatic#372), setzen wir unten inject_noise_site="after" explizit.
Die Funktion find_unique_box_instructions durchläuft den bereitgestellten geboxten Circuit und identifiziert diejenigen, die eindeutige Zwei-Qubit(2Q)-Layer oder Messungen aufweisen, zum Zweck des Rauschlernens und der Rauschinjektion.
# Box circuit with Twirl and InjectNoise annotations
boxes_pm = generate_boxing_pass_manager(
twirling_strategy="active",
inject_noise_strategy="individual_modification",
inject_noise_site="after",
inject_noise_targets="gates",
measure_annotations="all",
)
boxed_circuit = boxes_pm.run(isa_circuit)
# Find the unique instructions (layers) from boxed circuit
unique_2q_instructions = find_unique_box_instructions(
boxed_circuit, normalize_annotations=None, undress_boxes=True
)
boxed_circuit.draw(
"mpl",
fold=-1,
scale=0.3,
idle_wires=False,
wire_order=wire_order,
measure_arrows=False,
)

Kanonische Basismessungen vorbereiten
Beachte, dass wir besonders sorgfältig auf die Qubit-Reihenfolge achten müssen. Im Folgenden führen wir den Begriff canonical_qubits ein, um die Qubit-Reihenfolge beim Übergeben an Executor entsprechend zu aktualisieren, aufgrund der Art und Weise, wie die Qubit-Reihenfolge beim Boxen von Circuits und beim Auffinden eindeutiger Instruktionen erfasst wird. Details findest du in der Dokumentation zur Qubit-Reihenfolgekonvention.
# Determine the canonical qubits order
meas_box = boxed_circuit.data[-1]
canonical_qubits = [
idx
for idx, qubit in enumerate(boxed_circuit.qubits)
if qubit in meas_box.qubits
]
# map canonical qubit to physical (isa) qubit
c_2_p = {c: p for c, p in enumerate(canonical_qubits)}
# map physical (isa) qubit to virtual qubit (index in original circuit)
p_2_v = {p: v for v, p in enumerate(layout)}
# compute map between virtual and canonical qubit indices.
c_2_v = {c: p_2_v[p] for c, p in c_2_p.items()}
assert len(c_2_v) == num_qubits
bases_canon = [
np.array([base_i[c_2_v[c]] for c in range(num_qubits)], dtype=np.uint8)
for base_i in bases_virt
]
Workflow für Lightcone-Shading, Rauschlernen und Anti-Rauschinjektion
In diesem Tutorial führen wir die SLC-Schrankenberechnungen aus, bevor das Rauschlernen abgeschlossen ist, sodass der zu mindernde Circuit zeitlich so nah wie möglich am gelernten Rauschmodell ausgeführt wird. Im Prinzip kann dieser Workflow weiter parallelisiert werden: Ein Rauschlern-Job kann laufen, während parallel die Rausch-Schranken geschätzt werden. Für einen beliebigen Quanten-Circuit kann die Berechnung der Rausch-Schranken mit einer schwach exponentiellen Abhängigkeit skalieren, sodass die Parallelisierung der Schrankenberechnung (zum Beispiel über viele CPU-Kerne hinweg) engere Schranken für ein gegebenes Rechenzeitbudget liefert, und die QPU-Ausführungen und Schrankenberechnungen können selbst für den effizientesten Workflow parallelisiert werden.
Zu lernende Rauschmodell-Paulis vorhersagen
Bevor wir Rauschlernen auf Hardware durchführen, zählen wir zunächst auf, welche Pauli-Fehlerterme unser Rauschmodell enthalten wird. Erinnere dich daran, dass wir das Rauschen jedes eindeutigen Layers als sparse Pauli-Lindblad-Modell mit einer Fehlerrate für jeden Gewicht-eins-Pauli (, oder ) auf jedem aktiven Qubit sowie einer für jeden Gewicht-zwei-Pauli auf jedem verbundenen Paar aktiver Qubits modellieren. Die Funktion generate_noise_model_paulis durchläuft jeden eindeutigen geboxten Layer des bereitgestellten Circuits und erzeugt genau diese Liste von Pauli-Termen unter Berücksichtigung der Qubit-Konnektivität des Circuits.
Mit anderen Worten: Dieser Schritt sagt die Struktur des Rauschmodells voraus, die NoiseLearnerV3 später mit gelernten Raten füllen wird. Dass wir diese Struktur schon im Voraus kennen, ermöglicht es uns, für jeden potenziellen Fehler eine Vorwärts- und eine Rückwärtsschranke zu berechnen, bevor das Rauschlernen überhaupt ausgeführt wurde.
noise_model_paulis = generate_noise_model_paulis(
unique_2q_instructions, backend.coupling_map, boxed_circuit
)
noise_model_rates = {ref: None for ref in noise_model_paulis}
a. Vorwärtsschranken berechnen
Die Funktion compute_forward_bounds wertet die Kommutierungsbeziehungen zwischen den Gates in jedem Layer und den oben erzeugten Pauli-Termen im Hinblick darauf aus, wie sich vorwärts propagierte Fehler auf das gewünschte Observable auswirken. Für Gates, die mit den Pauli-Termen kommutieren, wird nichts getan. Clifford-Gates werden in Richtung Anfang des Circuits verschoben. Bei Nicht-Clifford-Gates approximieren wir ihren Einfluss auf die Ziel-Observablen, um sie später (nachdem alle Schranken zusammengeführt wurden) für die Rauschaufhebung zu priorisieren. Diese Schranke wird erreicht, indem zunächst die L2-Norm angewendet wird (nämlich die Quadratwurzel der Summe der Quadrate der relevanten Pauli-Term-Koeffizienten). Wenn zu viele Qubit-Terme beteiligt sind, greifen wir auf eine losere Schranke zurück, die die Dreiecksungleichung verwendet.
Die Parameter der Schrankenberechnung festlegen
Die folgenden Einstellungen steuern die klassischen Schrankenberechnungen:
-
slc_atol: die absolute Toleranz der Berechnungen. Operatorterme mit Koeffizienten unterhalb dieses Schwellenwerts werden als vernachlässigbar behandelt und verworfen. -
slc_eigval_max_qubits: die maximale Anzahl an Qubits, für die die engere, eigenwertbasierte (L2-Norm-)Schranke ausgewertet wird. Operatoren, deren Träger mehr Qubits umfasst, fallen auf die oben beschriebene lockerere Dreiecksungleichungs-Schranke zurück. -
slc_evolution_max_terms: die maximale Anzahl an Pauli-Termen, die beim Propagieren von Operatoren durch den Circuit beibehalten werden. Das Kürzen der kleinsten Terme hält die Berechnung handhabbar, auf Kosten geringfügig loserer Schranken. -
slc_num_processes: die Anzahl der CPU-Prozesse, über die die Schrankenberechnung parallelisiert wird. -
slc_timeout: das Zeitbudget (in Sekunden) für die Schrankenberechnung. Alle Layer, die innerhalb dieses Budgets nicht erreicht werden, erhalten die triviale Schranke von 2.
In diesem Tutorial wird der Timeout absichtlich erreicht: Wir halten slc_timeout bewusst klein, damit das Notebook schnell ausgeführt wird, und um zu zeigen, dass das Erreichen des Timeouts sicher ist. Die berechneten Schattierungen sind in jeder Phase obere Schranken, sodass das Erreichen des Timeouts die Schranken für die nicht erreichten Layer nur lockerer (trivial) macht; es unterschätzt niemals die Auswirkung eines Fehlers. Dies zeigt sich in den folgenden Visualisierungen dadurch, dass die am weitesten links liegenden Layer vollständig mit der trivialen Schranke gefüllt sind. Eine Erhöhung des Timeouts (oder der Anzahl der Prozesse) verengt die Schranken und reduziert den Sampling-Overhead weiter.
slc_atol = 1e-8
slc_eigval_max_qubits = 18
slc_evolution_max_terms = 1000
slc_num_processes = 8
slc_timeout = 60
forward_bounds = compute_forward_bounds(
boxed_circuit,
noise_model_paulis,
isa_observable,
evolution_max_terms=slc_evolution_max_terms,
eigval_max_qubits=slc_eigval_max_qubits,
atol=slc_atol,
num_processes=slc_num_processes,
timeout=slc_timeout,
)
Bounds computation timed out.
Die SLC zur manuellen Überprüfung visualisieren
Du kannst das Verhalten der shaded bounds interpretieren, indem du untersuchst, wie die Messungen und Pauli-Terme mit den lokalen Fehlern interagieren. Diese Muster sind charakteristisch für dieses Zeitentwicklungsproblem mit einem kicked Ising Hamiltonian und treten auch im Paper Lightcone Shading for Classically Accelerated Quantum Error Mitigation auf, mit mehreren verräterischen Merkmalen:
-
Wir können die beiden Kegel, die von den beiden Nicht-Identitäts-Paulis im Observable herrühren, deutlich unterscheiden.
-
Wir sehen, dass die X-Messung auf Qubit 6 mit dem X-Fehler im äußersten rechten Layer kommutiert.
-
Wir sehen, dass der Z-Pauli auf Qubit 13 mit dem Z-Fehler im äußersten rechten Layer kommutiert.
-
Wenn wir das oben angegebene Timeout erreichen, werden die verbleibenden Layer links davon vollständig mit trivialen Schranken von zwei gefüllt.
for p in "XYZ":
display(
draw_shaded_lightcone(
boxed_circuit,
forward_bounds,
noise_model_paulis,
pauli_filter=p,
scale=0.15,
fold=-1,
idle_wires=False,
wire_order=wire_order,
measure_arrows=False,
)
)



b. Vorwärtsschranken mit Quantum-Speed-Limits verschärfen
Als Nächstes verschärfen wir die Schranken mit der Funktion tighten_with_speed_limit. Die geometrische Ausbreitung des Lightcones des Observable ist bereits vollständig in den oben berechneten Vorwärtsschranken berücksichtigt. Was diese Funktion hinzufügt, ist ein Selbstkonsistenz-Durchlauf, der die begrenzte Geschwindigkeit nutzt, mit der sich Information durch den Circuit ausbreiten kann — der Lightcone wächst pro Layer von Zwei-Qubit-Gates um höchstens ein Qubit —, um die zuvor berechneten Schranken gegeneinander zu verschärfen. Die Information fließt in beide Richtungen:
-
Von den Kommutator-Schranken zu den Schranken auf dem rückwärts propagierten Observable: Wenn das Observable auf einem Qubit mit einem -Fehler kommutiert, kann es dort keine große - oder -Komponente haben.
-
Von den Schranken auf dem rückwärts propagierten Observable zu den Kommutator-Schranken: Wenn das Observable auf einem Qubit nur wenig - und -Komponente hat, muss es dort fast mit kommutieren.
forward_bounds_tighter = tighten_with_speed_limit(
forward_bounds, boxed_circuit, noise_model_paulis, isa_observable
)
Die SLC zur manuellen Überprüfung visualisieren
Im Prinzip sorgt diese Verschärfung für einen glatteren Übergang zwischen den berechneten Schranken und den trivialen Schranken, die den Layern jenseits des Timeouts zugewiesen werden. In diesem Beispiel ist der Effekt kaum sichtbar, da sich die Lightcones bis zum Erreichen des Timeouts bereits bis an die Ränder des Circuits ausgebreitet haben.
for p in "XYZ":
display(
draw_shaded_lightcone(
boxed_circuit,
forward_bounds_tighter,
noise_model_paulis,
pauli_filter=p,
scale=0.15,
fold=-1,
idle_wires=False,
wire_order=wire_order,
measure_arrows=False,
)
)



c. Rückwärtsschranken berechnen
Dieser Teil der Rauschvorhersage bewertet, wie sich ein Fehler bei einem bestimmten Layer auf den Eingangszustand auswirken kann. Die Funktion compute_backward_bounds invertiert zunächst den Circuit und entfernt Messungs-Gates, und führt dann eine ähnliche Analyse durch wie bei den Vorwärtsschranken-Berechnungen.
backward_bounds = compute_backward_bounds(
boxed_circuit,
noise_model_paulis,
evolution_max_terms=slc_evolution_max_terms,
num_processes=slc_num_processes,
timeout=slc_timeout,
)
Die SLC zur manuellen Überprüfung visualisieren
Anhand der Berechnung der Rückwärtsschranken können wir sehen, wie die Struktur des Anfangszustands das frühe Verhalten der Fehlerausbreitung bestimmt:
-
Wir können deutlich sehen, wie die Z-Fehler anfangs mit dem Anfangszustand |0⟩ kommutieren.
-
Nur auf Qubit 6, wo wir den +1-Eigenzustand der X-Basis initialisieren, kommutiert ein Z-Fehler nicht, während ein X-Fehler kommutiert.
for p in "XYZ":
display(
draw_shaded_lightcone(
boxed_circuit,
backward_bounds,
noise_model_paulis,
pauli_filter=p,
scale=0.15,
fold=-1,
idle_wires=False,
wire_order=wire_order,
measure_arrows=False,
)
)



Vorschau der zusammengeführten Schranken ohne gelernte Rauschraten
Die Funktion merged_bounds bestimmt die Stelle im Circuit, an der der Wechsel von Rückwärts- zu Vorwärtsschranken den geschätzten Gesamt-Bias auf das gewünschte Observable minimiert. Dieser Bias wird als Summe der Rückwärtsschranken-Beiträge für alle Rauschstellen vor dieser Stelle plus der Vorwärtsschranken-Beiträge für alle Rauschstellen danach berechnet. Aktuell geschieht dies einheitlich für alle Qubits.
Der optimale Punkt, an dem von Rückwärts- zu Vorwärtsschranken gewechselt wird, hängt von den gelernten Rauschraten ab. In diesem Stadium haben wir noch keine Raten gelernt: noise_model_rates wurde für jeden Layer mit None initialisiert, was merge_bounds als einheitliche Rauschraten behandelt. Die hier gezeigten zusammengeführten Schranken sind daher nur eine Vorschau; wir berechnen sie in Schritt 3 mit den tatsächlich gelernten Raten neu.
merged_bounds = merge_bounds(
boxed_circuit,
forward_bounds_tighter,
backward_bounds,
noise_model_rates,
)
Missing noise rates. Partitioning backward/forward commutator bounds by assuming uniform error rates.
Optimal spacetime partitioning not implemented!Just partitioning list of noisy boxes.
Die SLC zur manuellen Überprüfung visualisieren
Nach dem Zusammenführen der Rückwärts- und der verschärften Vorwärtsschranken wird das Verhalten der kombinierten SLCs deutlich:
-
Die obige Funktion sagt uns, dass eine Partition gewählt wird, an der der Wechsel von Rückwärts- zu verschärften Vorwärtsschranken stattfindet.
-
Wir sehen unten, dass die SLCs nun teilweise Rückwärts- und teilweise verschärfte Vorwärtsschranken enthalten.
for p in "XYZ":
display(
draw_shaded_lightcone(
boxed_circuit,
merged_bounds,
noise_model_paulis,
pauli_filter=p,
scale=0.15,
fold=-1,
idle_wires=False,
wire_order=wire_order,
measure_arrows=False,
)
)



Schritt 3: Ausführen
In diesem Abschnitt beginnen wir mit dem Teil des Workflows, der ein echtes Quantengerät verwendet. Für diese lernbasierte Fehlerminderungsmethode gibt es zwei Schritte:
-
Lerne das Rauschen mit
NoiseLearnerV3. -
Führe einen Fehlerminderungs-Circuit mit dem
samplomatic- undExecutor-Framework aus.
Mit den begrenzten Fehlern aus unserem Quanten-Circuit lernen wir die zugehörigen Rauschraten, um unser Fehlerbudget zu priorisieren, den Sampling-Overhead zu bestimmen und auf einer QPU auszuführen.
a. Die Rauschraten lernen
Der Noise Learner charakterisiert die Rauschprozesse, die die Gates in einem oder mehreren betreffenden Circuits beeinflussen, basierend auf dem sparse Pauli-Lindblad-Rauschmodell. Die Methode run() startet einen Rauschlern-Job für die bereitgestellten eindeutigen Zwei-Qubit-Layer, wobei die in der Noise-Learner-Konfiguration angegebenen Optionen verwendet werden. Diese Optionen steuern die Pauli-Twirling-Strategie, die Anzahl der Randomisierungen und Shots, die Lerntiefen und die Postselection.
Außerdem wählen wir die Lerntiefen bewusst aus. Ein praktischer Befund für lernbasierte Fehlerminderung mit samplomatic ist, dass es sehr vorteilhaft ist, wenn die tiefste Lerntiefe der Tiefe des Circuits entspricht, der gemindert werden soll. Da NLv3-layer_pair_depths in Layer-Paaren gemessen werden (ein Layer plus seine Inverse), setzen wir den tiefsten Wert auf die Hälfte der Zwei-Qubit-Layer-Tiefe des Circuits.
post_selection_enabled = True
# Match the deepest noise-learning depth to the depth of the circuit being
# mitigated. NLv3 ``layer_pair_depths`` are measured in layer pairs (a layer
# plus its inverse), so the deepest value is half the circuit's two-qubit-layer
# depth. Learning to this depth markedly improves the quality of the mitigation.
#
# We measure the two-qubit-layer depth on the pre-boxed ISA circuit: after
# boxing, every two-qubit gate is hidden inside a full-width ``BoxOp``, so a
# ``num_qubits == 2`` filter on ``boxed_circuit`` matches nothing (and
# ``QuantumCircuit.depth`` does not recurse into boxes).
depth_2q = isa_circuit.depth(lambda instr: instr.operation.num_qubits == 2)
max_layer_pair_depth = depth_2q // 2 # dividing by 2 since we want pairs
# 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}
)
noise_learner_options = {
"num_randomizations": 64,
"shots_per_randomization": 128,
"layer_pair_depths": layer_pair_depths,
"post_selection": {
"enable": post_selection_enabled,
"strategy": "edge",
"x_pulse_type": "rx",
},
"environment": {"job_tags": ["TUT_SLC"]},
}
noise_learner = NoiseLearnerV3(backend, noise_learner_options)
noise_learner_job = noise_learner.run(unique_2q_instructions)
noise_learner_result = noise_learner_job.result()
if post_selection_enabled:
print(
"Minimum fraction of shots kept for noise learning experiments: ",
end="",
)
print(
f"{min([min(d.values()) for d in [nlr.metadata['post_selection']['fraction_kept'] for nlr in noise_learner_result[:2]]]):.2f}"
)
Minimum fraction of shots kept for noise learning experiments: 0.71
# Get a dict mapping each InjectNoise.ref to its learned PauliLindbladMap
refs_2_plm = noise_learner_result.to_dict(
unique_2q_instructions, require_refs=False
)
b.i. Zusammengeführte Schranken mit tatsächlich gelernten Rauschraten aktualisieren
Nachdem das spezifische Rauschmodell gelernt wurde, können wir die gelernten Rauschraten auf die vorhergesagten Rauschschranken anwenden und eine endgültige Bestimmung erhalten, welche Schranken den größten Einfluss auf die Minimierung des Bias haben.
merged_bounds = merge_bounds(
boxed_circuit,
forward_bounds_tighter,
backward_bounds,
refs_2_plm,
)
Optimal spacetime partitioning not implemented!Just partitioning list of noisy boxes.
b.ii. Die local_scales für die Hardware-Ausführung berechnen
compute_local_scales betrachtet jeden möglichen Rauschfehler im Circuit und schätzt ab, wie stark dieser Fehler die endgültige Messung verzerren könnte sowie wie teuer es wäre, ihn zu korrigieren. Anschließend werden die Fehler danach eingestuft, wie lohnend ihre Minderung ist, und es wird die Teilmenge ausgewählt, die den Bias so weit wie möglich reduziert, während das zulässige Sampling-Kostenbudget eingehalten wird (oder eine gewünschte Genauigkeit erreicht wird). Das Ergebnis ist ein Satz von Skalierungsfaktoren, der angibt, welche Fehler aktiv gemindert werden und welche ungemindert bleiben (local_scales), zusammen mit dem vorhergesagten gesamten Sampling-Kosten-Overhead (sampling_costs) und dem verbleibenden Bias (residual_bias_bound).
Die Fähigkeit, den gewünschten verbleibenden Bias zu steuern, ist ein entscheidendes Merkmal der SLC-Implementierung von PEC. Während in der ursprünglichen Implementierung der Sampling-Overhead immer auf einen Bias von null abzielte, können wir den erforderlichen Sampling-Overhead mit einem Kompromiss beim erwarteten verbleibenden Bias anpassen. Dies hilft dem Nutzer, innerhalb eines festen Sampling-Budgets zu bleiben, was besonders beim anfänglichen Prototyping eines Workflows nützlich sein kann.
id_map = map_modifier_ref_to_ref(boxed_circuit)
summed_rates = 0.0
for box_id, noise_id in id_map.items():
learned_plm = refs_2_plm[noise_id]
summed_rates += np.sum(learned_plm.rates)
# print(f"{box_id}:\tgamma = {np.exp(2 * summed_rates):1.6e}\tsampling cost = {np.exp(4 * summed_rates):1.6e}")
total_gamma = np.exp(2 * summed_rates)
print(
f"Full PEC gamma={total_gamma}, sampling cost (gamma^2) = {total_gamma**2}"
)
Full PEC gamma=12.750876317157042, sampling cost (gamma^2) = 162.58484685543633
biases = []
costs = []
for bias in [0.0] + np.arange(0.001, 0.102, 0.01).tolist():
_, cost_, bias_ = compute_local_scales(
boxed_circuit,
merged_bounds,
refs_2_plm,
sampling_cost_budget=np.inf,
bias_tolerance=bias,
)
biases.append(bias_)
costs.append(cost_)
Sampling-Overhead gegen verbleibenden Bias abwägen
xticks = np.arange(0, 11)
fig, ax = plt.subplots()
ax.scatter(
[0], [total_gamma**2], marker="D", c="tab:orange", label="full PEC"
)
ax.plot(
100 * np.array(biases),
np.array(costs),
"o-",
c="tab:blue",
label="PEC+SLC",
)
ax.set_yscale("log")
ax.set_xticks(xticks, [f"{x:.1f}" for x in xticks])
ax.set_xlabel("Remaining bias [%]")
ax.set_ylabel(r"Sampling overhead, $\gamma^2$")
ax.grid()
ax.legend()
fig.suptitle("PEC sampling overhead reduction due to SLC")
Text(0.5, 0.98, 'PEC sampling overhead reduction due to SLC')
chosen_bias_thres = 0.1
local_scales, sampling_cost, residual_bias_bound = compute_local_scales(
boxed_circuit,
merged_bounds,
refs_2_plm,
sampling_cost_budget=np.inf,
bias_tolerance=chosen_bias_thres,
)
print(
f"PEC+SLC sampling cost (gamma^2) = {sampling_cost} "
f"w/ remaining bias = {100 * residual_bias_bound:.1f}%"
)
PEC+SLC sampling cost (gamma^2) = 48.13646509622307 w/ remaining bias = 10.0%
c. Den betreffenden Circuit mit Antirauschen ausführen
c.i. Template-Circuit mithilfe von samplex vorbereiten
Der samplex ist eine Ausgabe der build-Methode von Samplomatic, die alle Informationen kodiert, die zur Erzeugung randomisierter Parameter für template_circuit benötigt werden. Diese werden dann verwendet, um die QuantumProgram-Objekte einzurichten, die wiederum mit der Executor-Primitive auf einer QPU ausgeführt werden. Jedes QuantumProgram kann mehrere Elemente enthalten, die man sich als ein Paar aus template und samplex vorstellen kann.
Weitere Details findest du im Tutorial Hello samplomatic.
# Build template circuit and samplex for later use with the "Executor"
template_circuit, samplex = samplomatic.build(boxed_circuit)
# Set up postselection if it's been enabled
if post_selection_enabled:
# Set up post selection PM (to add PS instructions)
post_selection_pm = PassManager(
[
AddSpectatorMeasures(backend.coupling_map),
AddPostSelectionMeasures(x_pulse_type="rx"),
]
)
final_template_circuit = post_selection_pm.run(template_circuit)
else:
final_template_circuit = template_circuit
c.ii. Das QuantumProgram einrichten
num_randomizations = 4096
shots_per_randomization = 64
chunk_size = 256
# Set up QuantumProgram
program = QuantumProgram(shots=shots_per_randomization, noise_maps=refs_2_plm)
# no EM
# Collect up a dict of the other arguments that need to be bound to samplex_inputs
samplex_inputs = {
f"noise_scales.{ref}": float(0) for ref in local_scales.keys()
}
samplex_inputs |= {"basis_changes": {"basis0": bases_canon[0]}}
# Convert samplex_inputs into a dict to pass to QuantumProgram
samplex_arguments = (
samplex.inputs().bind(**samplex_inputs).make_broadcastable()
)
program.append_samplex_item(
circuit=final_template_circuit,
samplex=samplex,
samplex_arguments=samplex_arguments,
shape=(num_randomizations,),
chunk_size=chunk_size,
)
# plain PEC
# Collect a dict of the other arguments that need to be bound to samplex_inputs
samplex_inputs = {
f"noise_scales.{ref}": float(-1) for ref in local_scales.keys()
}
samplex_inputs |= {"basis_changes": {"basis0": bases_canon[0]}}
# Convert samplex_inputs into a dict to pass to QuantumProgram
samplex_arguments = (
samplex.inputs().bind(**samplex_inputs).make_broadcastable()
)
program.append_samplex_item(
circuit=final_template_circuit,
samplex=samplex,
samplex_arguments=samplex_arguments,
shape=(num_randomizations,),
chunk_size=chunk_size,
)
# PEC+SLC
# Collect a dict of the other arguments that need to be bound to samplex_inputs
samplex_inputs = {
f"noise_scales.{ref}": float(-1) for ref in local_scales.keys()
}
samplex_inputs |= {"basis_changes": {"basis0": bases_canon[0]}}
samplex_inputs |= {"local_scales": local_scales}
# Convert samplex_inputs into a dict to pass to QuantumProgram
samplex_arguments = (
samplex.inputs().bind(**samplex_inputs).make_broadcastable()
)
program.append_samplex_item(
circuit=final_template_circuit,
samplex=samplex,
samplex_arguments=samplex_arguments,
shape=(num_randomizations,),
chunk_size=chunk_size,
)
c.iii. Programm mit der Executor-Primitive ausführen
executor = Executor(backend)
job_exec = executor.run(program)
results_exec = job_exec.result()
Schritt 4: Nachbearbeitung
Während wir den finalen Erwartungswert von Interesse mithilfe von executor_expectation_values berechnen, implementieren wir einige Nachbearbeitungstechniken, um sicherzustellen, dass wir Ergebnisse von möglichst hoher Qualität erhalten. Zunächst wenden wir unsere Twirled Readout Error Extinction (TREX) an, die Fehler berücksichtigt, die während des Readout-Prozesses auftreten. Anschließend beheben wir Fehler durch nicht-markovsches Rauschen auf unseren Heron-Backends mithilfe einer Post-Selection-Methode. Diese Methode misst aktive Qubits und Spectator-Qubits, wendet dann eine langsame Rotation auf jedes Qubit an und misst erneut. In Fällen, in denen die beiden Messungen ein erwartetes gekipptes Qubit nicht bestätigen, werden diese Shots durch Anwendung einer mask aus dem PostSelector verworfen. Bei der Berechnung der Maske kann eine bestimmte Strategie festgelegt werden, um basierend auf Einzel-Qubit-Knoten oder benachbarten Spectator-Kanten zu filtern, was sowohl die Anzahl der herausgefilterten Shots als auch die Qualität der Ergebnisse beeinflussen kann.
measurement_noise_map = noise_learner_result[2].to_pauli_lindblad_map()
trex_scale_factors = trex_factors(measurement_noise_map, reverser_virt)
post_selection_strategy = "node"
def post_process_conv(datum, steps=16, gamma=None, ps=False, trex=False):
meas = datum["meas"]
flips = datum["measurement_flips.meas"]
signs = datum.get("pauli_signs", None)
meas_basis_axis = None
avg_axis = 0
mask = None
if ps and post_selection_enabled:
# Post-select the results
post_selector = PostSelector.from_circuit(
circuit=final_template_circuit, coupling_map=backend.coupling_map
)
# Compute the ps mask for filtering results
mask = post_selector.compute_mask(
datum, strategy=post_selection_strategy
)
# Compute fraction of shots kept from post selection
total_num_shots = num_randomizations * shots_per_randomization
ps_ratio = np.sum(mask) * 100 / total_num_shots / len(bases_canon)
print(
f"With {post_selection_strategy}-based post selection ({ps_ratio:.1f}% of shots kept):"
)
results = []
for i in range(steps, num_randomizations + 1, steps):
# Compute mitigated expvals w/out post-selection
res = executor_expectation_values(
meas[:i],
reverser_virt,
meas_basis_axis,
avg_axis=avg_axis,
measurement_flips=flips[:i],
pauli_signs=signs[:i] if signs is not None else None,
postselect_mask=mask[:i] if mask is not None else None,
rescale_factors=trex_scale_factors if trex else None,
gamma_factor=gamma,
)
results.append(res[0])
return results
gamma_pec = gamma_from_noisy_boxes(refs_2_plm, id_map)
gamma_slc = gamma_from_noisy_boxes(refs_2_plm, id_map, local_scales)
steps = 16
results = {}
for label, result_idx, gamma, use_ps, use_trex in [
("PEC", 1, gamma_pec, True, True),
("PEC+SLC", 2, gamma_slc, True, True),
("Unmitigated", 0, None, False, False),
]:
res = post_process_conv(
results_exec[result_idx],
steps=steps,
gamma=gamma,
ps=use_ps,
trex=use_trex,
)
results[label] = res
With node-based post selection (24.1% of shots kept):
With node-based post selection (24.2% of shots kept):
Anhand der Untersuchung der experimentellen Ergebnisse können wir das Verhalten der verschiedenen Ansätze direkt vergleichen: PEC, PEC kombiniert mit SLC und die ungeminderte Baseline. Einige spezifische Details, die hervorzuheben sind:
-
Das ungeminderte Ergebnis liegt weit außerhalb des 10-%-Bias-Bands (nahe 0,35) und wird von der Anzahl der Randomisierungen nicht beeinflusst.
-
Auf diesem Gerät hat das vollständige PEC einen Sampling-Overhead von . Nach großen anfänglichen Schwankungen erholt sich die reine PEC-Schätzung und pendelt sich bei etwa 1,1 ein, wobei sie den exakten Wert leicht überschießt. Beachte, dass das 10-%-Bias-Band nur für den PEC+SLC-Schätzer gilt: Reines PEC ist im Prinzip unverzerrt, sodass diese Restabweichung nicht durch eine Bias-Garantie abgedeckt ist, sondern stattdessen statistische Schwankungen und Unvollkommenheiten im gelernten Rauschmodell widerspiegelt, was bei dieser Problemgröße angemessen ist.
-
SLC reduziert den Overhead um einen weiteren Faktor von ~3,4 (auf , für eine Restbias-Grenze von etwa 10 %). PEC+SLC schwankt bei wenigen Randomisierungen ebenfalls stark und reicht zunächst sogar in den unphysikalischen Bereich, doch die Schätzung konvergiert stetig zum exakten Wert und pendelt sich nahe 1,0 gut innerhalb des Bands ein. Es erreicht oder übertrifft die Genauigkeit von reinem PEC bei etwa einem Drittel der Sampling-Kosten und demonstriert damit den Vorteil des Lightcone-Shadings.
-
Die Fehlerbalken beider geminderten Schätzungen verengen sich, wenn sich die Randomisierungen häufen; bei einer festen Anzahl von Randomisierungen weist der PEC+SLC-Schätzer die kleinere statistische Varianz auf, was mit seinem kleineren übereinstimmt.
Beachte, dass die gelernten Rauschraten (und damit die Overheads und geminderten Schätzungen) vom Backend und seiner Kalibrierung zum Zeitpunkt der Ausführung abhängen, sodass ein erneutes Ausführen dieses Notebooks quantitativ unterschiedliche Werte liefern kann.
fig, ax = plt.subplots(1, 1, figsize=(12, 6))
ax.axhline(1.0, color="black", label="Exact")
ax.fill_between(
[-50, 4100], -10, 0, color="grey", alpha=0.25, label="Unphysical"
)
ax.fill_between([-50, 4100], 1, 10, color="grey", alpha=0.25)
ax.fill_between(
[-50, 4100], 0.9, 1.1, color="red", alpha=0.25, label="10% bias"
)
for label, res in results.items():
ax.errorbar(
list(range(steps, num_randomizations + 1, steps)),
[r[0] for r in res],
yerr=[r[1] for r in res],
alpha=0.75,
marker="o",
linestyle="",
markerfacecolor="none",
label=label,
)
ax.set_ylabel(r"$\langle X_{6}Z_{13}\rangle$")
ax.set_xlabel("# randomizations")
ax.grid()
ax.legend(ncols=2)
ax.set_ylim([-0.1, 2.0])
ax.set_xlim([-50, 4100])
(-50.0, 4100.0)

Nächste Schritte
Wenn du diese Arbeit interessant fandest, könnten dich folgende Materialien interessieren:

