Skip to main content
IBM Quantum Platform

Verbesserung der Erwartungswerte durch propagierte Lärmabsorption (PNA)

Geschätzte Laufzeit: 10 Minuten auf einem Heron-Prozessor (HINWEIS: Dies ist lediglich eine Schätzung. (Die Laufzeit kann variieren.)


Lernziele

  • Was die propagierte Rauschabsorption (PNA) ist und wie sie das Rauschen von Zwei-Qubit-Gattern mindert, indem sie erlernte inverse Rauschkanäle in die gemessene Observable absorbiert
  • So verwenden Sie samplomatic zum Einrahmen und Kommentieren von Schaltungsebenen für das Drehen, Basisänderungen und die Rauscheinjektion
  • Wie man mit [NoiseLearnerV3]( /docs/guides/directed-execution-model#noiselearnerv3 ) Schichtenrauschen lernt und es in eine rauschmindernde Observable überführt mit qiskit-addon-pna
  • Wie man mit den Klassen Executor und QuantumProgram in randomisierte Schaltungen abtastet qiskit-ibm-runtimeund PNA mit TREX und einer Nachselektion kombiniert

Voraussetzungen


Hintergrund

In diesem Tutorial zeigen wir, wie man erweiterte Tools zur Fehlerminderung in Qiskit nutzen kann, um die Schätzung des Erwartungswerts bei verrauschten Quantenexperimenten zu verbessern.

Was ist propagierte Schallabsorption (PNA)?

Die propagierte Rauschabsorption ist eine Technik zur Minderung von Gatterfehlern, bei der die Observable durch den inversen Rauschkanal geleitet wird, der Zwei-Qubit-Gatter beeinflusst, wodurch eine rauschmindernde Observable entsteht.

Mithilfe der Quantenfehlerminderung können wir aus verrauschter Quantenhardware nützliche Erwartungswerte gewinnen, ohne dass eine vollständige Fehlertoleranz erforderlich ist. PNA konzentriert sich speziell darauf, Störeffekte in die Messgröße selbst zu integrieren, anstatt die Funktionsweise der Schaltung zu verändern.

Jedes rauschbehaftete Gatter in einer Quantenschaltung lässt sich als ideales Gatter modellieren, auf das ein Rauschkanal folgt. PNA erlernt oder charakterisiert diese Rauschkanäle und definiert deren Inverse. Anstatt die inversen Operationen in die Hardware-Ausführung einzubinden (was in der Regel nicht machbar ist), leitet PNA die inversen Rauschkanäle durch die Schaltung weiter und wendet sie auf die Observable an. Dieser Prozess wandelt den Observablen OO in einen neuen Operator O~\tilde{O} um, sodass die Messung von O~\tilde{O} an einer verrauschten Schaltung denselben Erwartungswert ergibt wie die Messung von OO an einer idealen, rauschfreien Schaltung.

Dies lässt sich wie folgt zusammenfassen:

  1. Modellieren Sie jedes verrauschte Gatter uiu_i als eine ideale Operation, auf die ein Rauschkanal Λi\Lambda_i folgt.
  2. Ermitteln oder schätzen Sie die einzelnen „ Λi\Lambda_i “ mithilfe von Tools zur Rauschcharakterisierung.
  3. Definieren und übertragen Sie die inversen Rauschkarten Λi−1\Lambda_i^{-1} mithilfe von Pauli-Transfer-Verfahren vorwärts durch die Schaltung.
  4. Integrieren Sie diese Inversen in die Observable, was zu einem rauschreduzierten Operator O~\tilde{O} führt.

Wird das Rauschen als Pauli-Kanal (oder allgemeiner als spärlicher Pauli-Lindblad-Kanal) beschrieben, lässt sich diese Ausbreitung mithilfe der Pauli-Ausbreitung effizient durchführen. Die Pauli-Ausbreitung bietet einen Rahmen, um zu approximieren, wie sich inverse Rauschkanäle beim Durchlaufen von Schichten aus Clifford- und Nicht-Clifford-Operationen transformieren, und gleichzeitig die Rechenkomplexität zu kontrollieren.

Indem die Kompensation in den beobachtbaren Bereich verlagert wird, vermeidet PNA den hohen Abtast-Overhead, der andernfalls durch das Einfügen physikalischer Korrekturoperationen in die Schaltung entstehen würde. Stattdessen wird die ursprüngliche, rauschbehaftete Schaltung ausgeführt, während die Observable in einen neuen Operator O~\tilde{O} umgewandelt wird, dessen Erwartungswert die Auswirkungen des Rauschens aufhebt.

Der PNA-Workflow

Der Prozess lässt sich anhand der folgenden konzeptionellen Phasen nachvollziehen. Die erste schematische Darstellung veranschaulicht ein typisches Experiment mit Störsignalen.

Lautes Experiment

Wenn wir das Rauschmodell trainieren, können wir dessen Inverse anwenden und das Rauschen auslöschen.

Experiment zur Rauschunterdrückung

Anstatt den inversen Rauschkanal durch Abtastung auf der QPU zu implementieren, wie dies bei der probabilistischen Fehlerkorrektur (PEC) der Fall ist, wenden wir ihn klassisch unter Verwendung der Pauli-Propagation auf die gemessene Observable an. Die daraus resultierende Observable mildert das gelernte Gate-Rauschen bei der Messung effektiv ab.

PNA-Übersicht

Modulare Fehlerbehebung mit Samplomatic und Executor

Der PNA-Ansatz baut auf einer modularen Architektur zur Fehlerminderung in Qiskit auf. Diese Architektur nutzt die samplomatic Bibliothek in Verbindung mit den QuantumProgram Klassen und Executor (die unter qiskit-ibm-runtimev0.47.0 zu Qiskit Runtime hinzugefügt wurden), um Techniken wie die propagierte Rauschabsorption und das Pauli-Twirling über verschiedene Experimente hinweg kombinierbar und wiederverwendbar zu machen.

ExecutorAnstatt die Logik zur Risikominderung in die Schaltungsdefinition selbst einzubetten, wird die Risikominderung deklarativ durch samplomatic Annotationen ausgedrückt und programmgesteuert über verwaltet, wodurch gesteuert wird, wie randomisierte Schaltungen generiert, ausgeführt und nachbearbeitet werden.

In diesem Tutorial implementieren wir ein Qiskit-Muster, um zu veranschaulichen, wie PNA inverse Pauli-Rauschkanäle weiterleiten und die Observable entsprechend anpassen kann, um die Schätzung von Erwartungswerten auf verrauschten QPUs zu verbessern.

Workflow-Übersicht

  • Schritt 1: Abbildung auf das Quantenproblem
    • Konstruieren Sie ein gespiegeltes, „Trotterisiertes“ Kicked-Ising-Modell und eine Zielgröße.
  • Schritt 2: Rauschen charakterisieren und verbreiten
    • Verwenden samplomatic Sie, um eindeutige Zwei-Qubit-Schichten und Messungen im Schaltkreis zu identifizieren und zu kennzeichnen.
    • Erfahren Sie mithilfe von NoiseLearnerV3…, welche Art von Rauschen die einzelnen Ebenen jeweils beeinflusst.
    • Ordne jede InjectNoise Anmerkung dem entsprechenden trainierten Rauschmodell zu.
    • Verwenden qiskit-addon-pna Sie, um die inversen Rauschkanäle durch die Schaltung weiterzuleiten und sie in die Zielobservable zu integrieren.
  • Schritt 3: Durchführung von Quantenexperimenten
    • ExecutorDefinieren Sie ein QuantumProgram , um eine zufällige Stichprobenziehung über samplex festzulegen, und führen Sie die Experimente im Backend mithilfe von durch.
  • Schritt 4: Rekonstruktion und Analyse der Ergebnisse
    • Vergleichen Sie die Strategien zur Risikominderung (PNA, PNA+TREX, PNA+PS, PNA+PS+TREX) und stellen Sie die Verbesserung gegenüber den Ergebnissen ohne Risikominderungsmaßnahmen dar.

Anforderungen

Bevor Sie mit dieser Anleitung beginnen, stellen Sie bitte sicher, dass Folgendes installiert ist:

  • Qiskit SDK v2.2 oder höher, mit Visual isierungsunterstützung
  • Qiskit Runtime v0.47 oder später (pip install qiskit-ibm-runtime)
  • Samplomatic v0.13 oder höher (pip install samplomatic)
  • PNA-Qiskit-Add-on (pip install qiskit-addon-pna)
  • Qiskit-Add-on „utils“ (pip install qiskit-addon-utils)

Einrichtung

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

Beispiel für einen Simulator im kleinen Maßstab

PNA mindert das physikalische Rauschen des Zwei-Qubit-Gates eines bestimmten Quantenprozessors. Der Arbeitsablauf hängt von zwei Hardware-Diensten ab, für die es in einem idealen Simulator keine sinnvolle Entsprechung gibt:

  • NoiseLearnerV3 charakterisiert experimentell den spärlichen Pauli-Lindblad-Rauschkanal, der mit jeder einzelnen Zwei-Qubit-Schicht der transpilierten Schaltung verbunden ist. Bei einem rauschfreien Simulator gibt es kein Rauschen, das man lernen könnte, und der propagierte inverse Kanal wäre die Identitätsfunktion.
  • Die Executor Primitive nimmt Proben der verdrehten, zufällig angeordneten Schaltkreise, die von samplomatic auf einem Backend generiert wurden.

Grundsätzlich könnte man ein synthetisches Rauschmodell verwenden. generate_noise_mitigating_observableSie könnten beispielsweise Anweisungen mit Qiskit Aer verknüpfen PauliLindbladError und die daraus resultierende Schaltung mit Rauschen direkt an weiterleiten. Dies dient jedoch lediglich dazu, die klassische Buchführung anhand des von Ihnen selbst eingefügten Rauschens zu validieren, und verschleiert den Sinn dieser Technik. Aus diesem Grund überspringen wir das Simulator-Beispiel und demonstrieren den vollständigen PNA-Workflow direkt auf der Hardware, wobei jeder Schritt des Qiskit-Musters im Folgenden einzeln erläutert wird.


Beispiel für groß angelegte Hardware

Wir führen nun den gesamten PNA-Workflow auf einem Ising-Modell mit 30 Knoten und „Kick“-Mechanismus durch, das auf einer „ IBM Quantum® “-Hardware ausgeführt wird, wobei wir die vier Schritte eines Qiskit-Musters befolgen.

Schritt 1: Abbildung auf ein Quantenproblem

Erzeuge die gespiegelte Trotter-Schaltung und die Observable

In diesem Experiment werden wir die zeitliche Dynamik eines Ising-Modells mit 30 Knoten und „Kick“-Effekt auf einer Spin-Kette vom Typ „ 1D “ untersuchen. Der betrachtete Hamilton-Operator lautet:

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

wobei J>0J>0 die Kopplung der Spins der nächsten Nachbarn beschreibt, i<ji<j, und das globale Transversalfeld, hh, auf π8\frac{\pi}{8} festgelegt ist. Je weiter hh von einem Clifford-Winkel entfernt ist (d. h. θ=nπ2,n∈Z\theta=n\frac{\pi}{2}, n \in \mathbb{Z} ), desto schwieriger wird es, die Anti-Rausch-Generatoren durch die Schaltung zu propagieren.

Als Observable wählen wir die durchschnittliche Magnetisierung an einem einzelnen Gitterpunkt, 1N∑i=1N⟨zi⟩\frac{1}{N} \sum_{i=1}^{N} \langle z_i \rangle, wobei NN die Anzahl der Gitterpunkte 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)

Output:

Output of the previous code cell

Schritt 2: Optimierung des Problems für die Ausführung auf der Hardware

Der nächste Schritt besteht darin, unsere gespiegelte Trotter-Schaltung für die Ausführung auf einer „Real- IBM Quantum “-Hardware vorzubereiten. Die Ausführung auf einer QPU erfordert mehr als nur den Aufbau der abstrakten Schaltung, da wir diese so optimieren müssen, dass:

  1. Es berücksichtigt die nativen Gate-Einstellungen und die Konnektivität des Backends. Bei der Transpilation wird die logische Schaltung in eine ISA-Schaltung abgebildet, die vom Ziel-Backend unterstützt wird. Dadurch wird sichergestellt, dass jede Interaktion zwischen einem Gatter und einem Qubit physikalisch realisierbar ist.

  2. Wir können das Rauschen auf der Ebene der Schaltungsschichten charakterisieren. PNA basiert auf dem Erlernen und der Weiterverbreitung inverser Rauschkanäle. Um dies effizient zu bewerkstelligen, zerlegen wir den transpilierten Schaltkreis in einzelne „boxed“-Schichten mit jeweils zwei Qubits. Mit diesen Feldern können wir jede Schicht des Netzwerks mit einem eigenen, trainierten Rauschmodell verknüpfen.

  3. Wir können realistische Rauschmodelle in PNA einspeisen. Sobald die Schaltung in eine Box verpackt wurde, nutzen wir den NoiseLearnerV3 Dienst, um experimentell die Pauli-Rauschkanäle zu ermitteln, die jede einzelne Zwei-Qubit-Schicht beeinflussen. Diese trainierten Modelle werden dann über die Annotationen von Samplomatic wieder mit der Schaltung verknüpft.

Insgesamt schließt dieser Schritt die Lücke zwischen einer idealisierten Spiegelungsschaltung und einer hardwarefähigen Schaltung mit erlernten Rauschmodellen. Mit dieser Konfiguration kann PNA inverse Rauschkanäle durch die Schaltung leiten und die Observable entsprechend anpassen.

Mit dem Backend verbinden und in eine ISA-Schaltung transpilieren

Zunächst initialisieren wir den „ IBM Quantum “-Compute-Service und wählen ein Backend aus. Melden Sie sich mit Ihrem eigenen Konto an, indem Sie den Anweisungen zum Speichern Ihrer Anmeldedaten folgen. Danach QiskitRuntimeService() werden diese automatisch abgerufen.

# 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

Als Nächstes wählen wir im Backend eine zusammenhängende Kette von Qubits aus und transpilieren die Schaltung darauf.

num_qubits Mithilfe des find_qubit_chain im Abschnitt „Einrichtung“ definierten Hilfsmittels wählen wir eine Reihe zusammenhängender physikalischer Qubits aus. initial_layoutAnschließend führen wir eine Transpilation mit optimization_level=0 durch und legen diese Kette als fest, wodurch die Struktur der Zwei-Qubit-Gatter-Ebene der gespiegelten Schaltung exakt erhalten bleibt. Auf dieser Struktur basieren die Schritte zum Boxen und zum Lärmlernen, daher muss hier eine höhere Optimierungsstufe (die die gespiegelten Gatter aufheben würde) vermieden werden.

# 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:

Output of the previous code cell

Drehe die Zwei-Qubit-Gatter-Schichten und die Messungen und finde eindeutige Schichten

Wir verwenden samplomatic zur Einrahmung der Schaltung und zur Identifizierung eindeutiger Zwei-Qubit-Schichten. Eine Box ist ein Konstrukt, das Anweisungen zusammenfasst, sodass bestimmte Absichten oder Anmerkungen später einheitlich auf alle Gatter innerhalb derselben Box angewendet werden können.

generate_boxing_pass_managerHier wenden wir die Methode an, die über die bloße Identifizierung von Zwei-Qubit-Schichten hinausgeht. Es erfüllt mehrere wichtige Aufgaben:

  • Gruppiert alle Zwei-Qubit-Schichten im Schaltkreis,
  • Wendet die Twirl Anmerkungen und ChangeBasis auf diese Ebenen an,
  • Gruppiert Messvorgänge in eigenen, eingerahmten Abschnitten und
  • Wendet die InjectNoise Annotation auf jede Zwei-Qubit-Schicht an.

Diese Anmerkungen legen fest, wie Rauschen, Basiswechsel und Twirling in der gesamten Schaltung behandelt werden. Außerdem legen sie die Struktur fest, die später zum Erlernen und zur Geräuschunterdrückung verwendet wird.

Die wichtigsten Konfigurationsoptionen sind:

  • enable_gates/enable_measures: True: Alle Zwei-Qubit-Gatter-Schichten und Endmessungen in Kästchen einfügen. Einzel-Qubit-Gatter sind innerhalb der Kästchen linksbündig ausgerichtet.
  • measure_annotations: all: Fügen Sie Twirl und ChangeBasis Anmerkungen auf dem Messrahmen ein.
  • twirling_strategy: active: Alle aktiven Qubits in jedem Feld, das Verschränkungsgatter enthält, drehen.
  • inject_noise_targets: gates: Füge InjectNoise allen Twirlmit „-“ gekennzeichneten Feldern, die Verflechtungsgatter enthalten, Anmerkungen hinzu.
  • inject_noise_strategy: uniform_modification: Alle Rauschschichten im gesamten Schaltkreis gleichmäßig skalieren.
# 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:

Output of the previous code cell

Generieren Sie die Vorlagenschaltung und samplex, die festlegen, wie die Schaltung abgetastet wird.

ExecutorHier fügen wir außerdem Zuschauer- und Nachauswahlmessungen hinzu, die erforderlich sind, um eine Nachauswahl an den vom … ausgegebenen Stichproben 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)

Output:

Output of the previous code cell

Lerne den Klang mithilfe von NoiseLearnerV3

Bevor wir PNA zur Fehlerminderung einsetzen können, müssen wir zunächst das Rauschen charakterisieren, das auf jede einzelne Zwei-Qubit-Schicht und die Messschicht in unserer Schaltung einwirkt. Dazu verwenden wir das NoiseLearnerV3 Programm, um für jede der zuvor identifizierten Schichten Rauschmodelle experimentell zu erlernen. Der Lernende führt Benchmarking-ähnliche Experimente durch, mit denen der Rauschkanal für jede Schicht geschätzt wird, und gibt ein Ergebnisobjekt zurück, das das gelernte Modell enthält.

Zunächst identifizieren wir die einzelnen Schichten in unserer Schaltung mithilfe von find_unique_box_instructions aus samplomatic. Dadurch wird sichergestellt, dass wir das Rauschen pro Schichttyp nur einmal trainieren, wodurch die Anzahl der Experimente und die Gesamtkosten minimiert werden. Die daraus resultierende Liste der Schichten wird an den Noise-Learner übergeben.

Es gibt einige wichtige Parameter, die bestimmen, wie das Rauschen gelernt wird:

  • num_randomizations: Anzahl der pro Lernkonfiguration verwendeten Zufallskreisläufe.
  • shots_per_randomization: Anzahl der Schüsse pro zufälligem Lernzyklus.
  • layer_pair_depths: Die Schaltungstiefen (gemessen in der Anzahl der Paare), die in den Lernexperimenten verwendet werden sollen.
  • post_selection: Ermöglicht eine flankenbasierte Nachauswahl mithilfe von rx Gatter, um Nachmessimpulse anzulegen.
# 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()

Visualisierung der gelernten Rauschraten

Nachdem wir die Rauschmodelle trainiert haben, können wir die Verteilung der geschätzten Fehlerraten sowohl für Ein-Qubit- als auch für Zwei-Qubit-Operationen untersuchen. Der folgende Code extrahiert die Pauli-Lindblad-Darstellungen aus den erlernten Rauschergebnissen und erfasst die entsprechenden Rauschraten.

Für jede trainierte Schicht:

  • Wir wandeln das Rauschmodell in eine spärliche Liste von (pstr, qubits, rate) Tupeln um, wobei pstr die auf die gegebenen Qubits wirkende Pauli-Kette und rate die zugehörige Fehlerrate ist.
  • Wir unterteilen die Raten in Ein-Qubit-Terme (len(pstr) == 1) und Zwei-Qubit-Terme (len(pstr) == 2).
  • Anschließend werden die Ratenlisten sortiert und ihre Medianwerte berechnet.

Wir stellen die Rauschratenverteilungen für ein Qubit (rot) und zwei Qubits (blau) auf einer logarithmischen 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 Raten für Ein- und Zwei-Qubit-Zustände hängt vom jeweiligen Gerät und von den konkret untersuchten Schichten ab; in diesem Durchlauf weisen die Generatoren für Ein-Qubit-Zustände ( weight-1 ) die höhere Medianrate auf.

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

Schaltkästen mit erlerntem Rauschen verknüpfen

Sobald wir für jede einzelne Zwei-Qubit-Schicht Rauschmodelle erhalten haben, müssen wir diese mit den entsprechenden InjectNoise Anmerkungen innerhalb des eingerahmten Schaltkreises verknüpfen.

Die InjectNoise Anweisung ist eine samplomatic Anmerkung, die die Einzel-Qubit-Dresser nutzt, um auf kontrollierte und konfigurierbare Weise Rauschen in die Schaltung einzuspielen. Es ermöglicht eine modulare Rauschmodellierung über verschiedene Ebenen hinweg.

Jede InjectNoise Anmerkung enthält:

  • InjectNoise.ref - eine eindeutige Kennung für die Anmerkung. Dies wird vom Objekt samplex verwendet, um das entsprechende Rauschmodell korrekt zuzuweisen.
  • InjectNoise.modifier_ref (optional) – eine sekundäre Referenz, die es ermöglicht, das zugewiesene Rauschmodell mit einem multiplikativen Faktor zu skalieren.

In diesem Schritt erstellen wir eine Zuordnung von jedem InjectNoise.ref zu seinem entsprechenden erlernten Rauschmodell (PauliLindbladMap). Diese Zuordnung stellt sicher, dass jede Verschränkungsgatter-Schicht im Schaltkreis mit dem passenden Rauschmodell gepaart wird, sodass Rauscheffekte während der Stichprobenentnahme und der anschließenden Rauschunterdrückungsschritte präzise 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()
        )

Das Observable durch das gelernte Anti-Rauschen propagieren

Wie oben bereits erläutert, erfolgt dies in zwei Schritten. Zunächst leiten wir einen Anti-Rausch-Generator bis zum Ende der Schaltung weiter. Anschließend wenden wir diesen weiterentwickelten Generator auf die Observable an. Dieser Vorgang wird für jeden Rauschunterdrückungsgenerator in der Schaltung wiederholt. Bei dieser Implementierung werden alle Generatoren einer bestimmten Schicht parallel bis zum Ende der Schaltung weitergeleitet. Zudem wird Multiprocessing nach dem „ Python “-Prinzip eingesetzt, um sowohl die Vorwärtsausbreitung des Anti-Rauschens als auch die Rückwärtsausbreitung der Observable parallel durchzuführen. Dadurch wird eine Anhäufung von weiterentwickelten Generatoren im Speicher verhindert und gleichzeitig die Rechenressourcen optimal genutzt.

Bei der Ausführung von PNA müssen Sie stets eine verrauschte Schaltung und eine Beobachtable angeben. Wenn es sich bei Ihrer „noisy circuit“-Schaltung um eine in einem Kasten dargestellte Schaltung mit InjectNoise Anmerkungen handelt, müssen Sie die Zuordnung angeben, die wir im obigen Schritt erstellt haben. qiskit-aerMan kann auch einen nicht-boxed Schaltkreis übergeben, der Befehle aus enthält PauliLindbladError . In diesem Fall muss nicht angegeben werden. refs_to_noise_models Berücksichtigen Sie zusätzlich zu den primären Eingaben auch Folgendes:

  • max_err_terms: Die Anzahl der Terme, die in jedem Anti-Rausch-Generator beibehalten werden sollen, während die Vorwärtsberechnung durchgeführt wird. Wenn man diesen Wert größer wählt, erhöht sich in der Regel die Genauigkeit, allerdings ist nicht garantiert, dass dieses Verhalten monoton verläuft.
  • max_obs_terms: Die Anzahl der Terme, die in der rauschmindernden Observable „ O~\tilde{O} “ beibehalten werden sollen, während diese durch das entwickelte Antirauschen rückwärtspropagiert wird. Höhere Werte erhöhen in der Regel die Genauigkeit, allerdings ist nicht garantiert, dass dies monoton verläuft.
  • num_processes: Die Anzahl der Kerne, die für den Prozess reserviert werden sollen. Denken Sie daran, dass die Generatoren vorwärts propagiert und parallel auf die Observable angewendet werden.
  • search_step: Beim Backpropagation-Schritt wird ein „greedy“-Verfahren verwendet, um zwei Operatoren in der Pauli-Basis näherungsweise zu konjugieren. search_stepDiese Methode lässt sich durch eine Erhöhung von beschleunigen. Weitere Informationen finden Sie unter pauli-prop Dokumente .
  • num_to_measure: Diese Variable ist zwar keine Eingabe für generate_noise_mitigating_observable, wir verwenden sie jedoch, um zu steuern, wie viele Terme aus O~\tilde{O} wir tatsächlich messen möchten. Hier erfassen wir nur die 30 wichtigsten Begriffe, bei denen es sich um die ursprünglichen Begriffe in unserer Beobachtungsgröße handelt. Die Terme wurden nun so neu skaliert, dass ihre Messung zu einer Abschwächung des erlernten Gate-Rauschens führt. Obwohl wir nur 30 Terme aus O~\tilde{O} messen, ist es oft dennoch sinnvoll, die Anzahl der Terme groß werden zu lassen, da dies die Genauigkeit 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]

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

Bringe die Maßbasen in die kanonische Form über

Als Nächstes suchen wir eine minimale Menge von Basen für die Messung, sodass wir jeden Pauli-Term in der gemessenen Observable vollständig abdecken können ( es können viele Observablen gleichzeitig gemessen werden, sofern sie qubitweise kommutieren ). Da wir lediglich die Terme in unserer ursprünglichen Observablen messen, die die Summe aller Einzel-PaulisZ ist, wird nur eine einzige Basis benötigt: die All-BasisZ .

Neben der Bestimmung eines Satzes von Pauli-Messbasen müssen wir diese Pauli-Terme auf die kanonische Form abbilden, die von der … erwartet wird Executor. Weitere Informationen zur kanonischen Qubit-Anordnung finden Sie 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: Durchführung von Quantenexperimenten

Legen Sie fest, wie die Stichprobenahme im QuantumProgram

Executor``QuantumProgramWir richten nun den ein, der als zentraler Container für alle Schaltungen und Abtastkonfigurationen dient, die vom ausgeführt werden. Dieses Objekt legt fest, wie zufällig generierte Schaltungsinstanzen erzeugt, gebündelt und ausgeführt werden, um die in PNA verwendeten Messergebnisse zu erzeugen.

Ein QuantumProgram kann mehrere Elemente enthalten, die jeweils aus einer Vorlagenschaltung und einem entsprechenden samplex Objekt bestehen, das festlegt, wie die Randomisierungen angewendet werden. Diese Abstraktion ermöglicht es Executor , den gesamten Arbeitsablauf als ein einziges, modulares Programm zu handhaben – von der zufälligen Schaltkreiserzeugung bis hin zur Erfassung und Zusammenführung der Aufnahmen.

In diesem Schritt erstellen wir ein QuantumProgram Programm, das unser PNA-Experiment unter Verwendung der Schaltungsvorlage und samplex der zuvor von uns erstellten Schaltung ausführt. Die Konfiguration umfasst folgende Elemente:

  • template_circuit: Die Schaltung, die alle Logikgatter enthält, die zur Umsetzung aller gewünschten Randomisierungen erforderlich sind (einschließlich Twirling-Randomisierungen, Parameter usw.).
  • samplex: Ein Objekt, das eine Wahrscheinlichkeitsverteilung über alle möglichen Schaltungsrandomisierungen definiert, aus denen Stichproben entnommen werden sollen.
  • samplex_arguments: Die Bindungen, die erforderlich sind, um die samplex
    • basis_changes: Hier legen wir einen Satz von Basen fest, die gemessen werden sollen und die alle Pauli-Terme in der gemessenen Observable abdecken.
    • noise_scales.ref: Wir haben die Skalierung jeder Rauschschicht auf 0.0 eingestellt, um zu verhindern, dass zusätzliches Rauschen in unsere Samples gelangt.
    • pauli_lindblad_maps: Erforderlich, wenn noise_scales übergeben werden. Hierbei werden lediglich die Lärmschichten dem zugehörigen Lärmmodell zugeordnet.
  • samplex_arguments``shape: Ein Form-Tupel zur Erweiterung der durch … definierten impliziten Form. Die durch diese Erweiterung eingeführten nichttrivialen Achsen zählen 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,),
)

Lesen Sie die Schaltung mit dem Executor

QuantumProgramNachdem wir nun unsere definiert haben, ist die Durchführung des Experiments unkompliziert. Wir instanziieren einfach das Executor Objekt, weisen ihm das Backend zu 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: Rekonstruktion und Analyse der Ergebnisse

Um einen fehlerkorrigierten Erwartungswert zu berechnen, gehen wir wie folgt vor:

  • Berechnen Sie die TREX-Skalierungsfaktoren auf der Grundlage des ermittelten Rauschens, das die Messungen beeinflusst,
  • Erzeuge eine Maske, um nur die nachträglich ausgewählten Samples beizubehalten, und
  • Verwenden Sie die executor_expectation_values Funktion aus qiskit-addon-utils , um alle Daten zu einem fehlerbereinigten Erwartungswert zusammenzufassen.
# 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,
)

Vergleich der Minderungsstrategien: PNA, PNA+TREX, PNA+PS, PNA+PS+TREX

Anhand der Executor Ergebnisse berechnen und visualisieren wir die Erwartungswerte für verschiedene Varianten der Risikominderung.

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:

Output of the previous code cell

Die Ergebnisse verdeutlichen die kumulativen Vorteile einer Kombination verschiedener Techniken zur Fehlerminderung. Bereits der einfache PNA-Ansatz bringt den Erwartungswert wieder nahe an den idealen Referenzwert heran, was darauf hindeutet, dass die Einbeziehung inverser Rauschkanäle in die Observable Fehler bei Zwei-Qubit-Gattern wirksam kompensiert.

  • Durch Hinzufügen der TREX-Gewichtungsanpassung (PNA+TREX) lässt sich die Schätzung leicht verbessern, indem das Ungleichgewicht bei der Stichprobenauswahl in den randomisierten Schaltkreisen korrigiert wird.
  • Die Nachselektion (PNA+PS) sorgt für eine deutlichere Verbesserung, indem sie inkonsistente Messergebnisse herausfiltert, die wahrscheinlich auf Restfehler zurückzuführen sind.
  • Schließlich liefert die Kombination aller drei Methoden (PNA+PS+TREX) das genaueste Ergebnis, das nahezu dem Idealwert entspricht, was zeigt, wie sich diese Strategien zur Fehlerminderung gegenseitig verstärken.

Insgesamt zeigt der Vergleich, dass PNA eine solide Grundlage für die rauschbewusste Erwartungswertschätzung bildet, während TREX und die Nachselektion ergänzende Verfeinerungen für weitere Genauigkeitsgewinne bieten.


Nächste Schritte

Wenn Sie diese Arbeit interessant fanden, könnten Sie auch an folgenden Materialien interessiert sein:

War diese Seite hilfreich?
Melden Sie auf GitHub einen Fehler, einen Tippfehler oder fordern Sie Inhalte an.