Skip to main content
IBM Quantum Platform

Fehlererkennung mit geringem Overhead mithilfe von Raum-Zeit-Codes

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


Lernziele

  • Wie Pauli-Prüfungen der Raumzeit logische Fehler in Clifford-Schaltkreisen aufspüren und wie die Postselektion anhand ihrer Syndrome die Genauigkeit einer Stichprobenverteilung erhöht.
  • So verwenden Sie das Paket qiskit-paulice , um mit get_check_qubits, NoiseModel, und automatisch hardwareeffiziente Prüfungen zu finden und einzufügen add_pauli_checks.
  • Wie lässt sich die Genauigkeit eines Stabilisatorzustands abschätzen, indem man seine Stabilisatoren abtastet und anhand von Prüfsyndromen nachselektiert?
  • So führen Sie den vollständigen Workflow zur Fehlererkennung auf der „ IBM Quantum® “-Hardware durch und vergleichen die Genauigkeiten bei verrauschten und nachselektierten Daten.

Voraussetzungen

  • Grundlagen der Hardware für Quantencomputer im industriellen Maßstab.
  • Der Clifford- und Stabilisator-Formalismus, einschließlich der Frage, wie eine Stabilisatorgruppe einen reinen Stabilisatorzustand beschreibt.

Hintergrund

Der Artikel „Low-overhead error detection with spacetime codes [1]“ von Simon Martiel und Ali Javadi-Abhari stellt eine Methode zur Erkennung logischer Fehler in Clifford-dominierten Schaltungen vor, die einen Mittelweg zwischen vollständiger Fehlerkorrektur und weniger aufwendiger Fehlerminderung darstellt. Das Konzept baut auf den kohärenten Pauli-Prüfungen (CPC) aus der Arbeit „Single-shot error mitigation by coherent Pauli checks“ [2] von van den Berg und anderen auf. Bei beiden Ansätzen wird eine Clifford-„Payload“-Schaltung mit Ancilla-Qubits verschränkt, um bestimmte Invarianten zu überprüfen. Die Auswertung der Ancillas führt zu einem Syndrom, das angibt, ob während der Ausführung ein Fehler festgestellt wurde. Wenn man nur die Stichproben beibehält, bei denen kein Fehler festgestellt wurde, verbessert sich die Genauigkeit der Stichprobenverteilung, allerdings auf Kosten einer geringeren Nachselektionsrate.

Der wesentliche Unterschied zwischen kohärenten Pauli-Prüfungen und Raumzeit-Prüfungen besteht in den Operatoren, die dabei gemessen werden. Kohärente Pauli-Prüfungen messen zeitlich lokalisierte Operatoren mit hohem Gewicht. Bei Qubit-Topologien mit begrenzter Konnektivität, wie beispielsweise „Heavy Hex“, erfordern diese Prüfungen viele SWAP-Gatter und führen oft dazu, dass die Schaltung zu tief wird, um in der Praxis ausgeführt werden zu können. Durch die Umsetzung der Prüfungen als Raum-Zeit-Codes wird stattdessen jede Prüfung räumlich und zeitlich über die Nutzlastschaltung verteilt. Dies führt zu einer hardwareeffizienten Kodierung, die bei der Erkennung logischer Fehler weiterhin effektiv ist und gleichzeitig den Overhead hinsichtlich Qubits und Tiefe gering hält.

Was das Paket qiskit-paulice leistet

Das Paket qiskit-paulice automatisiert die Erstellung dieser Prüfungen, sodass Sie diese nicht manuell erstellen müssen. Seine Hauptaufgabe besteht darin, gültige Raumzeit-Pauli-Prüfungen an den Stellen in einer Schaltung zu finden und einzufügen, an denen die Fehlererkennung maximiert und gleichzeitig der Qubit-Overhead minimiert wird. Eine Prüfschaltung gilt als gültig, wenn ihre Operatoren die logische Funktion der Nutzlastschaltung unverändert lassen, als ressourcenschonend, wenn sie nur wenige Verschränkungsgatter verwendet, und als effektiv, wenn sie einen großen Teil der Fehler erkennt – gemessen an dem Rauschen, das die Prüfschaltung selbst verursacht. Das Paket bewertet die Kandidatenprüfungen anhand eines Rauschmodells und übernimmt die besten davon in die Schaltung. In diesem Tutorial werden drei API-Methoden verwendet:

  • get_check_qubits überprüft eine Backend-Kopplungskarte und gibt Ziel- und Ancilla-Qubit-Paare zurück. Eine Überprüfung von verwendet target_qubits[i] ancilla_qubits[i].
  • NoiseModel.from_backend erstellt anhand von Backend-Benchmark-Daten ein grobes Rauschmodell. Das Modell bewertet die Kandidatenprüfungen, sodass kein exaktes, trainiertes Rauschmodell erforderlich ist. Ein trainiertes Pauli-Lindblad-Modell finden Sie unter NoiseModel.from_pauli_lindblad_maps.
  • add_pauli_checks sucht nach Fehlern und fügt Prüfungen in einen Schaltkreis ein. Es gibt eine Folge von CheckedCircuit Objekten mit einer steigenden Anzahl von Prüfwerten zurück, wobei jedes Objekt eine Funktion bereitstellt, get_postselection_method die eine gemessene Bitfolge auf einen Syndromvektor abbildet. Das Argument cost wählt die Funktion aus, die einen Check bewertet (gammaentweder den Abtast-Overhead des nachträglich ausgewählten inversen Rauschkanals LER oder die logische Fehlerrate). Das Argument method legt die Suchstrategie fest (windowed, genetic, oder windowed_genetic). In diesem Tutorial werden cost="gamma" und verwendet method="windowed", die zusammen eine deterministische, reproduzierbare Auswahl der Prüfungen ermöglichen.

Schätzung der Genauigkeit anhand von Stabilisator-Stichproben

Um zu messen, wie gut die Fehlererkennung funktioniert, kann man die Übereinstimmung zwischen dem Stabilisatorzustand ∣ψ⟩=U∣0⟩⊗n|\psi\rangle = U|0\rangle^{\otimes n}, den die Schaltung im Idealfall bereitstellt, und dem verrauschten Zustand ρ\rho, den die Hardware tatsächlich ausgibt, abschätzen. Die Projektion auf einen reinen Stabilisatorzustand ∣ψ⟩|\psi\rangle entspricht dem gleichmäßigen Mittelwert über die Elemente 2n2^n seiner Stabilisatorgruppe S\mathcal{S} :

∣ψ⟩⟨ψ∣=12n∑G∈SG.|\psi\rangle\langle\psi| = \frac{1}{2^n}\sum_{G \in \mathcal{S}} G.

Setzt man dies in die Fidelity ein, so ergibt sich die Fidelity von ρ\rho als der durchschnittliche Erwartungswert jedes Stabilisators G∈SG \in \mathcal{S} in Bezug auf ρ\rho :

F=Tr(ρ∣ψ⟩⟨ψ∣)=12n∑G∈STr(ρG)=12n∑G∈S⟨G⟩ρ.F = \mathrm{Tr}(\rho|\psi\rangle\langle\psi|) = \frac{1}{2^n} \sum_{G \in \mathcal{S}} \mathrm{Tr}(\rho G) = \frac{1}{2^n} \sum_{G \in \mathcal{S}} \langle G \rangle_\rho.

Bei größeren Problemen ist es nicht machbar, alle Stabilisatoren von 2n2^n aufzuzählen; daher kann man die Genauigkeit anhand einer Zufallsstichprobe abschätzen. Wenn man die Stabilisatoren MM G1,…,GMG_1, \ldots, G_M gleichmäßig zufällig aus S\mathcal{S} zieht, erhält man eine unverzerrte Schätzung:

F^M=1M∑i=1M⟨Gi⟩ρ.\hat F_M = \frac{1}{M} \sum_{i=1}^M \langle G_i \rangle_\rho.

Da ein Clifford-Schaltkreis einen Stabilisatorzustand erzeugt, lässt sich seine Fidelity direkt anhand der abgetasteten Erwartungswerte seiner Stabilisatoren abschätzen. In diesem Tutorial wird zunächst der Arbeitsablauf anhand einer kleinen Schaltung auf einem Simulator erläutert; anschließend wird derselbe Arbeitsablauf auf Hardware mit einer größeren, komplexeren Schaltung durchgeführt. Da Schaltkreise immer mehr Nicht-Clifford-Operationen enthalten, nimmt die Anzahl der gültigen Prüfungen rasch ab, sodass sich die Methode am besten für Clifford-dominierte Schaltkreise eignet.


Anforderungen

Bevor Sie mit dieser Anleitung beginnen, stellen Sie bitte sicher, dass folgende Komponenten installiert sind:

  • Qiskit SDK v2.0 oder höher, mit Visualisierungsunterstützung
  • Qiskit Runtime v0.40 oder später (pip install qiskit-ibm-runtime)
  • Qiskit Aer v0.17 oder höher (pip install qiskit-aer)
  • Qiskit Paulice (pip install qiskit-paulice)
  • tqdm (pip install tqdm)

Einrichtung

Importieren Sie die erforderlichen Bibliotheken und definieren Sie die Hilfsfunktionen, die nicht als Importe verfügbar sind. Die Funktion random_clifford_circuit generiert eine zufällige Clifford-Nutzlast nach dem „Brickwork“-Prinzip, find_check_layout durchsucht eine Backend-Kopplungskarte nach einem fehlerarmen Qubit-Pfad mit vielen verfügbaren Ancillas, learned_noise_model wandelt die Ausgabe NoiseLearner in ein qiskit-paulice Rauschmodell um, append_basis_rotation dreht eine Schaltung so, dass ein Stabilisator in der Rechenbasis gemessen wird, expectation berechnet einen Erwartungswert des Stabilisators aus den erfassten Zählwerten und cum_mean_sem verfolgt die aktuelle Fidelity-Schätzung.

# Standard library imports
import random
import time

# External libraries
import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm

# Qiskit
from qiskit import QuantumCircuit
from qiskit.quantum_info import Clifford, Pauli, PauliLindbladMap, PauliList
from qiskit.result import sampled_expectation_value
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.visualization import plot_coupling_map

# Qiskit Aer
from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel as AerNoiseModel
from qiskit_aer.noise import ReadoutError, depolarizing_error

# Qiskit IBM Runtime
from qiskit_ibm_runtime import NoiseLearner, QiskitRuntimeService
from qiskit_ibm_runtime import SamplerV2 as Sampler

# Qiskit Paulice
from qiskit_paulice import add_pauli_checks
from qiskit_paulice.layout import get_check_qubits
from qiskit_paulice.noise_models import NoiseModel
def random_clifford_circuit(
    num_qubits: int, depth: int, rng: np.random.Generator
) -> QuantumCircuit:
    """Brickwork random Clifford on `num_qubits`, with `depth` CZ layers."""
    qc = QuantumCircuit(num_qubits)
    qc.h(range(num_qubits))
    for d in range(depth):
        for i in range(d % 2, num_qubits - 1, 2):
            qc.cz(i, i + 1)
        for q in range(num_qubits):
            if rng.integers(0, 2):
                qc.sx(q)
            if rng.integers(0, 2):
                qc.s(q)
            if rng.integers(0, 2):
                qc.sx(q)
    return qc


def find_check_layout(
    backend,
    num_qubits: int,
    rng: np.random.Generator,
    num_trials: int = 200,
    max_gate_error: float = 0.03,
    max_readout_error: float = 0.2,
) -> list[int]:
    """Find a low-error path of `num_qubits` qubits with many available ancillas.

    Builds random self-avoiding walks on the coupling map, excluding the qubits
    and two-qubit gates whose reported errors exceed the thresholds, and keeps
    the path that offers the most target and ancilla pairs. Ties are broken by
    the lower average two-qubit gate error along the path.
    """
    target = backend.target
    gate_2q = next(
        name for name in ("cz", "ecr", "cx") if name in target.operation_names
    )

    # Collect per-edge gate errors and per-qubit readout errors
    edge_error = {}
    for qubits, props in target[gate_2q].items():
        edge = tuple(sorted(qubits))
        if props is not None and props.error is not None:
            edge_error[edge] = min(edge_error.get(edge, 1.0), props.error)
    readout_error = {
        qubit: target["measure"][(qubit,)].error
        for (qubit,) in target["measure"]
    }

    # Keep only the edges whose gate and readout errors are acceptable
    adjacency = {}
    for (q1, q2), error in edge_error.items():
        if (
            error <= max_gate_error
            and readout_error.get(q1, 1.0) <= max_readout_error
            and readout_error.get(q2, 1.0) <= max_readout_error
        ):
            adjacency.setdefault(q1, set()).add(q2)
            adjacency.setdefault(q2, set()).add(q1)

    # Random self-avoiding walks; keep the path with the most check pairs
    starts = sorted(adjacency)
    best_path = None
    best_score = (-1, float("inf"))
    for _ in range(num_trials):
        path = [starts[rng.integers(len(starts))]]
        while len(path) < num_qubits:
            options = sorted(adjacency[path[-1]] - set(path))
            if not options:
                break
            path.append(options[rng.integers(len(options))])
        if len(path) < num_qubits:
            continue
        num_pairs = len(get_check_qubits(backend.coupling_map, path)[0])
        mean_error = float(
            np.mean(
                [edge_error[tuple(sorted(e))] for e in zip(path, path[1:])]
            )
        )
        if num_pairs > best_score[0] or (
            num_pairs == best_score[0] and mean_error < best_score[1]
        ):
            best_path, best_score = path, (num_pairs, mean_error)

    if best_path is None:
        raise RuntimeError(
            "No connected low-error path found. Relax the error thresholds."
        )
    return best_path


def learned_noise_model(layer_errors, layout: list[int]) -> NoiseModel:
    """Build a `NoiseModel` from `NoiseLearner` results.

    `NoiseLearner` reports one `PauliLindbladError` per entangling layer, whose
    generators are indexed against that layer's own physical qubits, while
    `NoiseModel.from_pauli_lindblad_maps` expects `PauliLindbladMap`s indexed the
    way `NoiseModel.from_backend` indexes them: by position in `layout`. This
    translates between the two and drops generators that fall outside `layout`.
    """
    phys_to_virt = {phys: virt for virt, phys in enumerate(layout)}
    maps = []
    for layer in layer_errors:
        if layer.error is None:
            continue
        terms = []
        for pauli, rate in zip(
            layer.error.generators, layer.error.rates, strict=True
        ):
            label, indices = [], []
            for local, phys in enumerate(layer.qubits):
                x, z = bool(pauli.x[local]), bool(pauli.z[local])
                if not (x or z):
                    continue
                if phys not in phys_to_virt:
                    break  # generator reaches outside the layout, so skip it
                label.append("Y" if x and z else "X" if x else "Z")
                indices.append(phys_to_virt[phys])
            else:
                if label:
                    terms.append(
                        ("".join(label), tuple(indices), float(rate))
                    )
        # Each map needs a 2-qubit generator to define an entangling layer
        if any(len(t[1]) == 2 for t in terms):
            maps.append(
                PauliLindbladMap.from_sparse_list(
                    terms, num_qubits=len(layout)
                )
            )
    if not maps:
        raise RuntimeError(
            "No usable layer errors. Check that the learner ran on this layout."
        )
    return NoiseModel.from_pauli_lindblad_maps(maps)


def append_basis_rotation(
    circuit: QuantumCircuit, pauli: Pauli
) -> QuantumCircuit:
    """Strip measurements, append basis rotations for `pauli`, and re-measure."""
    out = circuit.remove_final_measurements(inplace=False)
    for q in range(pauli.num_qubits):
        if pauli.x[q]:
            if pauli.z[q]:
                out.sdg(q)
            out.h(q)
    out.measure_all()
    return out


def expectation(counts: dict, pauli: Pauli) -> float:
    """Expectation value of `pauli` from counts measured in the Z basis.

    Pads with identity on any qubits beyond the support of `pauli`, such as the
    check ancillas that appear in the postselected counts.
    """
    if not counts:
        return float("nan")
    n = pauli.num_qubits
    sign = -1 if int(pauli.phase) % 4 == 2 else 1
    total = len(next(iter(counts)))
    label = "".join(
        "Z" if q < n and (pauli.x[q] or pauli.z[q]) else "I"
        for q in range(total - 1, -1, -1)
    )
    return sign * sampled_expectation_value(counts, label)


def cum_mean_sem(values: np.ndarray):
    """Cumulative mean and standard error of the mean, ignoring NaNs."""
    valid = ~np.isnan(values)
    total = np.cumsum(np.where(valid, values, 0.0))
    total_sq = np.cumsum(np.where(valid, values**2, 0.0))
    count = np.maximum(np.cumsum(valid).astype(float), 1)
    mean = total / count
    sem = np.sqrt(np.maximum(total_sq / count - mean**2, 0) / count)
    return np.where(np.cumsum(valid) > 0, mean, np.nan), sem

Beispiel für einen Simulator im kleinen Maßstab

In diesem Abschnitt wird der gesamte Arbeitsablauf an einem Simulator mit Rauschen erläutert. Das Programm nutzt Backend-Benchmark-Daten, um ein Qubit-Layout und ein Rauschmodell auszuwählen, ermittelt automatisch Prüfungen und wendet eine Nachselektion auf die Stichprobenverteilung an, um die Verbesserung der Fidelity aufzuzeigen.

Schritt 1: Klassische Eingaben auf ein Quantenproblem abbilden

Die Nutzlastschaltung ist eine flache, eindimensionale, nach dem „Brickwork“-Prinzip aufgebaute zufällige Clifford-Schaltung. Da es sich um eine Clifford-Schaltung handelt, erzeugt sie einen Stabilisatorzustand, dessen Fidelity sich direkt aus den abgetasteten Erwartungswerten des Stabilisators abschätzen lässt. Beginnen Sie mit einer flachen Schaltung, damit sich die Überprüfungen im nächsten Schritt leicht veranschaulichen lassen.

num_qubits = 12
depth = 4
seed = 1764
rng = np.random.default_rng(seed)
np.random.seed(seed)

circuit = random_clifford_circuit(num_qubits, depth, rng)
circuit.measure_all()
circuit.draw("mpl", fold=-1, scale=0.6)

Output:

Output of the previous code cell

Schritt 2: Optimierung für die Ausführung auf Quantenhardware

Durch die Zuordnung der Schaltung zur Hardware werden die physikalische Anordnung der Qubits, das Rauschmodell zur Bewertung der möglichen Prüfungen sowie die Prüfungen selbst festgelegt.

Wählen Sie zunächst ein Backend aus und suchen Sie in dessen Kopplungskarte nach einem eindimensionalen Qubit-Layout mit dem im Abschnitt „Einrichtung“ definierten find_check_layout Hilfs-Qubit. Der Helfer generiert zufällige, selbstvermeidende Wege, die die Gatter und Auslesepunkte mit dem höchsten Fehler vermeiden, und wählt dabei den Pfad aus, der die meisten Ziel- und Ancilla-Paare bietet. Da die Suche die Verbindungs- und Fehlerdaten direkt aus dem Backend ausliest, läuft derselbe Code auf jeder beliebigen QPU von IBM Quantum. Die Funktion get_check_qubits gibt dann die Ziel- und Ancilla-Paare zurück, wobei bei der Überprüfung von target_qubits[i] verwendet wird ancilla_qubits[i].

Im folgenden Kopplungsdiagramm sind die grünen Qubits die Nutzlast-Qubits und die orangefarbenen Qubits die Hilfsqubits, die die Prüfungen durchführen. Qubits mit einem benachbarten Ancilla werden als Ziel-Qubits für Überprüfungen verwendet.

service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)

print(f"Backend: {backend.name}")

# Search for a low-error path, then pair each target qubit with a neighboring ancilla
layout = find_check_layout(backend, num_qubits, rng)
target_qubits, ancilla_qubits = get_check_qubits(backend, layout)
num_checks = len(target_qubits)

print(f"Target qubits:  {target_qubits}")
print(f"Ancilla qubits: {ancilla_qubits}")
plot_coupling_map(
    num_qubits=backend.num_qubits,
    qubit_coordinates=getattr(
        backend.configuration(), "qubit_coordinates", None
    ),
    coupling_map=backend.configuration().coupling_map,
    figsize=(12, 12),
    qubit_color=[
        "#4CAF50"
        if i in set(layout)
        else "#FF9800"
        if i in set(ancilla_qubits)
        else "#DDDDDD"
        for i in backend.coupling_map.graph.node_indices()
    ],
    qubit_size=220,
    line_width=2,
    font_size=90,
)

Output:

Backend: ibm_boston
Target qubits:  [105, 107, 108, 123, 125, 141, 143]
Ancilla qubits: [104, 97, 109, 122, 126, 140, 144]
Output of the previous code cell

Nachdem Backend und Layout ausgewählt wurden, transpilieren Sie die Nutzlast in eine Schaltung für eine Befehlssatzarchitektur (ISA). Es ist lediglich erforderlich, das Layout festzulegen und die Gatter in den nativen Gattersatz des Backends zu übersetzen.

pm = generate_preset_pass_manager(
    optimization_level=0, backend=backend, initial_layout=layout
)
circuit_isa = pm.run(circuit)
circuit_isa.draw("mpl", fold=-1, scale=0.6)

Output:

Output of the previous code cell

Modellieren Sie als Nächstes, wie sich das Gate- und das Ausleserauschen im Backend auf die Ausführung auswirken. Das Rauschmodell bestimmt, an welcher Stelle im Schaltkreis eine Prüfung die meisten Fehler erfasst. Ein präziseres Modell verbessert die Erkennung, doch in der Regel ist es nicht notwendig, ein solches Modell durch Stichproben auf der QPU zu trainieren. Das nachfolgende Modell leitet aus qiskit-ibm-runtime Benchmark-Daten einen gleichmäßigen depolarisierenden Kanal für Gate- und Ausleserauschen ab.

noise_model = NoiseModel.from_backend(
    backend, layout, uniform_gate_noise=True
)
print(noise_model)

Output:

NoiseModel(gate_noise=0.001079865281450939, readout_noise=0.006001790364583333, idling_noise=None)

Fügen Sie nun Sicherungen in den Stromkreis ein. Die Funktion add_pauli_checks nimmt die Clifford-Nutzlast, die Liste der Ziel-Qubits und das Rauschmodell entgegen. Das Argument ancilla_qubits gibt der Funktion vor, welches physikalische Ancilla mit dem jeweiligen Ziel gepaart werden soll. Die Prüfungen werden in der Reihenfolge hinzugefügt, in der die Ziel-Qubits auftreten, sodass das endgültige Layout der geprüften Schaltung wie folgt aussieht: layout + ancilla_qubits. Um eine Ausgangsschaltung mit weniger (i) Prüfungen zu betreiben, lautet das endgültige Layout layout + ancilla_qubits[:i].

Die Ausgabe von ist add_pauli_checks eine Folge von Schaltkreisen mit einer steigenden Anzahl von Prüfungen, angefangen bei gar keinen Prüfungen bis hin zu einer Prüfung pro Ziel-Qubit. Die Visualisierung bestätigt, dass bei den Prüfungen die angegebenen Ziel- und Ancilla-Paare verwendet werden. Einzelheiten zur Suche nach geeigneten Prüfungen finden Sie in den Abschnitten II bis IV der ergänzenden Informationen in [1].

checked = add_pauli_checks(
    circuit_isa,
    target_qubits,
    noise_model,
    ancilla_qubits=ancilla_qubits,
    cost="gamma",
    method="windowed",
    seed=seed,
)

print(f"Physical layout of payload and ancillas: {layout + ancilla_qubits}")
print("Checked circuit:")
checked[-1].circuit.draw("mpl", fold=-1, idle_wires=False)

Output:

Physical layout of payload and ancillas: [108, 107, 106, 105, 117, 125, 124, 123, 136, 143, 142, 141, 104, 97, 109, 122, 126, 140, 144]
Checked circuit:
Output of the previous code cell

Schritt 3: Mit „ Qiskit primitives “ ausführen

Um den Effekt des Gate-Rauschens sichtbar zu machen, erhöhen Sie die Tiefe der Nutzdaten und nehmen Sie eine Stichprobe aus deren Stabilisatoren. Jeder Stabilisator kommutiert im Allgemeinen nicht qubitweise mit den anderen, sodass ein einziger Satz von Überprüfungen nicht für zwei verschiedene Stabilisatoren gültig ist. Anstatt Stabilisatoren in Pendlermengen zu gruppieren, sollte man für jeden Stabilisator einzeln eine geeignete Reihe von Prüfungen finden. Eine gleichmäßige Zufallsauswahl von Stabilisatoren liefert eine unverzerrte Schätzung der Genauigkeit.

Erstellen Sie die tiefere Schaltung und wählen Sie eine Zufallsstichprobe ihrer Stabilisatoren aus.

depth = 24
num_stabilizers = 20
num_shots = 1_000

circuit = random_clifford_circuit(num_qubits, depth, rng)

# Build the full stabilizer group, then sample from it uniformly at random
circ_no_meas = circuit.remove_final_measurements(inplace=False)
stabilizer_group = PauliList([Pauli("I" * num_qubits)])
for generator in (
    Pauli(label) for label in Clifford(circ_no_meas).to_labels(mode="S")
):
    stabilizer_group = stabilizer_group + stabilizer_group.compose(generator)

keep = np.where(
    stabilizer_group.x.any(axis=1) | stabilizer_group.z.any(axis=1)
)[0]
chosen = np.random.default_rng(seed).choice(
    keep, size=min(num_stabilizers, len(keep)), replace=False
)
stabilizers = [stabilizer_group[int(i)] for i in chosen]

two_qubit_depth = circuit.depth(lambda x: x.operation.num_qubits == 2)
print(
    f"Sampled {len(stabilizers)} stabilizers of a {circuit.num_qubits}-qubit "
    f"circuit with two-qubit depth {two_qubit_depth}: "
    f"{{{stabilizers[0]}, {stabilizers[1]}, ...}}"
)

Output:

Sampled 20 stabilizers of a 12-qubit circuit with two-qubit depth 24: {ZXIIXZYYXIZZ, XXXYIIZYXIII, ...}

Für jeden untersuchten Stabilisator ist die Schaltung so zu drehen, dass der Stabilisator in der Rechenbasis gemessen wird, anschließend auf das Backend zu übertragen und eine geeignete Auswahl an Prüfungen zu ermitteln. Die Ziel- und Ancilla-Paare werden für jeden Stabilisator miteinander vermischt, sodass jedes Ziel seine Ancilla behält. Beachten Sie, dass Prüfungen nacheinander in der Reihenfolge der angegebenen Ziel-Qubits festgelegt werden und dass eine bereits festgelegte Prüfung nicht mehr geändert wird, wenn weitere Prüfungen hinzugefügt werden.

noisy_circuits = []
checked_circuits = []
depths_2q = []
t0 = time.time()
for i, pauli in enumerate(tqdm(stabilizers)):
    noisy_circuits.append(pm.run(append_basis_rotation(circuit, pauli)))
    # Shuffle target and ancilla pairs together so each target keeps its ancilla
    targets, ancillas = zip(
        *random.sample(
            list(zip(target_qubits, ancilla_qubits, strict=True)),
            k=len(target_qubits),
        ),
        strict=True,
    )
    checked_circuits.append(
        add_pauli_checks(
            noisy_circuits[-1],
            list(targets),
            noise_model,
            ancilla_qubits=list(ancillas),
            cost="gamma",
            method="windowed",
            seed=seed + 1 + i,
        )
    )
    depths_2q.append(
        checked_circuits[-1][-1].circuit.depth(lambda x: len(x.qubits) == 2)
    )

print(
    f"Added {num_checks} checks to {len(stabilizers)} circuits "
    f"in {(time.time() - t0):.0f}s."
)
print(
    f"On average, two-qubit depth increased from "
    f"{circuit.depth(lambda x: len(x.qubits) == 2)} to {int(np.mean(depths_2q))} "
    f"when adding {num_checks} checks."
)

Output:

100%|██████████| 20/20 [00:15<00:00,  1.29it/s]
Added 7 checks to 20 circuits in 15s.
On average, two-qubit depth increased from 24 to 33 when adding 7 checks.

Testen Sie die „Bare Payload“ und die geprüften Schaltkreise mit Qiskit Aer. Der Simulator verwendet dasselbe Depolarisationsmodell, mit dem die Prüfungen ausgewertet wurden, sodass das Rauschen, auf das die Prüfungen abzielen, genau das Rauschen ist, das der Simulator anwendet.

aer_nm = AerNoiseModel()
aer_nm.add_all_qubit_quantum_error(
    depolarizing_error(noise_model.gate_noise, 2), ["cz"]
)
p = noise_model.readout_noise
aer_nm.add_all_qubit_readout_error(ReadoutError([[1 - p, p], [p, 1 - p]]))
noisy_sim = AerSimulator(method="stabilizer", noise_model=aer_nm)

counts = []
for i, checked_circ_result in enumerate(tqdm(checked_circuits)):
    noisy_counts = (
        noisy_sim.run(
            noisy_circuits[i], shots=num_shots, seed_simulator=seed * i + 1
        )
        .result()
        .get_counts()
    )
    checked_counts_per_variant = []
    for k, ck in enumerate(checked_circ_result):
        variant_counts = (
            noisy_sim.run(
                ck.circuit, shots=num_shots, seed_simulator=seed * i + 2 + k
            )
            .result()
            .get_counts()
        )
        checked_counts_per_variant.append(variant_counts)
    counts.append((noisy_counts, checked_counts_per_variant))

Output:

100%|██████████| 20/20 [00:17<00:00,  1.13it/s]

Schritt 4: Nachbearbeitung und Ausgabe des Ergebnisses im gewünschten klassischen Format

Bei jeder Überprüfung kommen Verschränkungsgatter zwischen einem Ancilla und einem Ziel zum Einsatz. Die Ancilla startet unter ∣0⟩|0\rangle, daher stabilisiert ZancZ_\text{anc} ihre Eingabe. Die Weiterleitung von „ ZancZ_\text{anc} “ durch den geprüften Schaltkreis ergibt am Ausgang einen Pauli-Operator, dessen Nicht-Identitäts-Terme den Definitionsbereich der Prüfung definieren. Eine Prüfung gilt als bestanden, wenn die Bits in ihrem Datensatz eine gerade Parität aufweisen. Eine Probe wird nur dann aufbewahrt, wenn alle Prüfungen bestanden wurden.

Die get_postselection_method jeder liefert CheckedCircuit eine Funktion zurück, die eine gemessene Bitfolge auf einen Syndromvektor abbildet. Behalte die Stichproben, deren Syndrom bei jeder Überprüfung den Wert Null aufweist, und verwerfe den Rest. Die folgende Grafik zeigt, dass eine Erhöhung der Anzahl der Kontrollen die Nachauswahlquote senkt. Bei einer niedrigeren Nachauswahlrate sind mehr Messungen erforderlich, um eine bestimmte Genauigkeit zu erreichen; daher besteht ein Kompromiss zwischen Erkennungsfähigkeit und Stichprobenkosten. Die Rate scheint sich anzunähern, was darauf hindeutet, dass zusätzliche Kontrollen nur noch in geringem Maße zur Erkennungsfähigkeit beitragen.

rate_per_variant = []
kept_per_stab = []
for i, (_, checked_counts_per_variant) in enumerate(counts):
    rates = []
    kept_at_num_checks = None
    for k, variant_counts in enumerate(checked_counts_per_variant):
        ps_fn = checked_circuits[i][k].get_postselection_method()
        kept = {
            bs: n for bs, n in variant_counts.items() if not ps_fn(bs).any()
        }
        rates.append(sum(kept.values()) / num_shots)
        if k == num_checks:
            kept_at_num_checks = kept
    rate_per_variant.append(rates)
    kept_per_stab.append(kept_at_num_checks)

max_len = max(len(s) for s in rate_per_variant)
rates_arr = np.full((len(rate_per_variant), max_len), np.nan)
for i, s in enumerate(rate_per_variant):
    rates_arr[i, : len(s)] = s
ks = np.arange(max_len)

fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(ks, rates_arr.T, color="#ff8c00", alpha=0.15, linewidth=1)
ax.plot(
    ks,
    np.nanmedian(rates_arr, axis=0),
    color="black",
    linewidth=1,
    linestyle="--",
    label="median",
)
ax.set_xlabel("Checks committed")
ax.set_ylabel("Postselection rate")
ax.set_ylim((0, 1.05))
ax.set_title(
    f"Per-stabilizer postselection rate ({len(rates_arr)} stabilizers)"
)
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Output:

Output of the previous code cell

Vergleichen Sie nun die Genauigkeit des reinen, verrauschten Zustands mit der des nachselektierten Zustands. Die nachträgliche Auswahl nur der Proben, bei denen kein Fehler festgestellt wurde, erhöht den Erwartungswert jedes Stabilisators und damit die geschätzte Genauigkeit. Die nachselektierten Werte basieren auf einer geringeren Anzahl von Stichproben als die Rohwerte, dennoch sind die Erwartungswerte genauer und die Stichprobenvarianz geringer. Beachten Sie außerdem, dass die mittlere Postselektionsrate nahe an der verrauschten Genauigkeit liegt. Das ist genau das, was man erwartet, wenn die Prüfungen nahezu alle fehlerhaften Stichproben erkennen: Der Anteil der Stichproben, die alle Prüfungen bestehen, nähert sich dem Anteil der fehlerfreien Stichproben an, was der Genauigkeit des verrauschten Zustands entspricht.

results = []
for i, ((noisy_counts, _), kept) in enumerate(
    zip(counts, kept_per_stab, strict=True)
):
    results.append(
        (
            expectation(noisy_counts, stabilizers[i]),
            expectation(kept, stabilizers[i]),
            sum(kept.values()) / num_shots,
        )
    )

fidelity_noisy = float(np.nanmean([r[0] for r in results]))
fidelity_postsel = float(np.nanmean([r[1] for r in results]))
psr = float(np.mean([r[2] for r in results]))
print(
    f"ideal fidelity:          1.0\n"
    f"noisy fidelity:          {fidelity_noisy:.4f}\n"
    f"postselected fidelity:   {fidelity_postsel:.4f}\n"
    f"mean postselection rate: {psr:.3f}"
)

evs_ideal = np.ones(len(results))
evs_noisy = np.array([r[0] for r in results])
evs_post = np.array([r[1] for r in results])
idx = np.arange(len(results))


def strip(ax, ys, color, label):
    m, s = np.nanmean(ys), np.nanstd(ys)
    ax.axhspan(
        m - s, m + s, color=color, alpha=0.15, label=f"{label} mean and std"
    )
    ax.axhline(
        m, color=color, linewidth=1, linestyle="--", label=f"{label} fidelity"
    )


fig, ax = plt.subplots(figsize=(8, 4))
ax.axhline(np.nanmean(evs_ideal), color="black", linewidth=1.5, label="ideal")
strip(ax, evs_noisy, "red", "noisy")
strip(ax, evs_post, "green", "postselected")
ax.scatter(idx, evs_noisy, color="red", s=22, alpha=0.7, label="noisy EVs")
ax.scatter(
    idx,
    evs_post,
    color="green",
    s=22,
    alpha=0.7,
    label="postselected EVs",
)
ax.set_xlabel("stabilizer index")
ax.set_ylabel(r"$\langle G \rangle$")
ax.set_ylim((-0.1, 1.1))
ax.set_title("Per-stabilizer expectation values")
ax.legend(loc="lower left")
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

M = np.arange(1, len(results) + 1)
fig, ax = plt.subplots(figsize=(8, 4))
for ys, color, label in [
    (evs_ideal, "black", "ideal"),
    (evs_noisy, "red", "noisy"),
    (evs_post, "green", "postselected"),
]:
    cm, sem = cum_mean_sem(ys)
    ax.plot(M, cm, color=color, linewidth=1.5, label=label)
    ax.fill_between(M, cm - sem, cm + sem, color=color, alpha=0.15)
ax.set_xlabel("number of stabilizers averaged")
ax.set_ylabel("running fidelity estimate")
ax.set_title("Fidelity convergence versus number of stabilizers")
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Output:

ideal fidelity:          1.0
noisy fidelity:          0.7899
postselected fidelity:   0.9679
mean postselection rate: 0.780
Output of the previous code cell Output of the previous code cell

Der Gamma-Wert gibt an, wie viel des modellierten Rauschkanals von den Prüfungen unentdeckt bleibt. Stellt man den Gamma-Wert in Abhängigkeit von der Anzahl der durchgeführten Prüfungen grafisch dar, wird deutlich, wie sich die Erkennungsleistung mit jeder zusätzlichen Prüfung verbessert. Ein Wert von bedeutet 1.0 , dass die Prüfungen das gesamte modellierte Rauschen erfassen. Die Kurven fallen in Richtung ab, 1.0 je mehr Prüfungen durchgeführt werden, was zeigt, dass jede zusätzliche Prüfung einen Teil des verbleibenden, noch unentdeckten Fehlers aufdeckt.

stab_scores = [
    [variant.cost for variant in checked_circ_result]
    for checked_circ_result in checked_circuits
]
max_len = max(len(s) for s in stab_scores)
scores = np.full((len(stab_scores), max_len), np.nan)
for i, s in enumerate(stab_scores):
    scores[i, : len(s)] = s
ks = np.arange(max_len)

fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(ks, scores.T, color="#4682b4", alpha=0.15, linewidth=1)
ax.plot(
    ks,
    np.nanmedian(scores, axis=0),
    color="black",
    linewidth=1,
    linestyle="--",
    label="median",
)
ax.set_xlabel("Checks committed")
ax.set_ylabel("Gamma")
ax.set_yscale("log")
ax.set_title(f"Per-stabilizer gamma curves ({len(scores)} stabilizers)")
ax.legend()
ax.grid(True, alpha=0.3, which="both")
plt.tight_layout()
plt.show()

Output:

Output of the previous code cell

Beispiel für groß angelegte Hardware

Der gleiche Workflow läuft auf Hardware mit einer größeren und tieferen Nutzlast. Dieser Abschnitt nutzt das Backend aus dem Simulator-Beispiel, erstellt jedoch ein neues 20-Qubit-Layout mit eigenen Ziel- und Ancilla-Paaren sowie einem Pass-Manager und übermittelt die Schaltkreise anschließend in einem einzigen Auftrag an die QPU. Bei dieser Größe löst die meisten Schüsse mindestens eine Überprüfung aus, sodass die Nachauswahlrate gering ist und jede Schaltung ein großes Schussbudget benötigt, um genügend Stichproben für das Überleben zu erhalten. Das Beispiel konzentriert daher seine Ressourcen auf einige wenige zufällig ausgewählte Stabilisatoren; dies ist zwar immer noch eine unverzerrte Schätzung der Genauigkeit, jedoch grober als der im Simulatorbeispiel berechnete Durchschnittswert über viele Stabilisatoren hinweg.

Im Vergleich zum Simulatorbeispiel ändert sich ein Punkt: Anstatt aus den Kalibrierungsdaten einen gleichmäßigen depolarisierenden Kanal abzuleiten, wird in diesem Abschnitt das Rauschmodell mit gelernt NoiseLearner und das Modell qiskit-paulice anhand des Ergebnisses mit aufgebaut NoiseModel.from_pauli_lindblad_maps. Ein trainiertes Pauli-Lindblad-Modell erfasst die räumliche Struktur des Rauschens in diesem spezifischen Layout, anstatt davon auszugehen, dass jede Kante gleichermaßen verrauscht ist. Daher wird die Platzierung der Prüfzeichen anhand eines Rauschens bewertet, das demjenigen ähnlicher ist, das die QPU beeinflusst. Um das Rauschen zu ermitteln, muss die QPU abgetastet werden; dies sollte im Gesamtbudget für die QPU-Abtastung berücksichtigt werden.

Die folgenden Parameter legen die Anzahl der Qubits, die Tiefe, die Anzahl der Stabilisatoren und die Anzahl der Durchläufe fest. Skalierung mit hw_num_shots dem Kehrwert der Nachauswahlrate: Bei einer Rate von 3 % ergeben 40.000 Aufnahmen etwa 1.200 nachausgewählte Stichproben pro Schaltkreis. Erhöhen Sie den Wert, um hw_num_stabilizers eine genauere Genauigkeitsschätzung zu erhalten – allerdings auf Kosten einer höheren Anzahl an Schaltkreisen pro Auftrag, von denen jeder das gleiche Shot-Budget benötigt.

Schritte 1–4 (in einem einzigen Codeblock zusammengefasst)

Die folgende Zelle führt dieselben vier Schritte aus wie das Simulator-Beispiel. Es erstellt die größere Nutzlast und wählt einige Stabilisatoren aus (Schritt 1); wählt ein Layout aus, trainiert das Rauschmodell darauf und ermittelt für jeden Stabilisator die vollständig geprüfte Schaltung (Schritt 2); übermittelt einen Sampler-Auftrag, der sowohl die ungeschützten als auch die geprüften Schaltungen enthält (Schritt 3); und führt eine Nachauswahl der geprüften Zählwerte durch, um die rauschbehafteten und die nachausgewählten Fidelity-Schätzungen pro Stabilisator und im Durchschnitt zu vergleichen (Schritt 4). Bei dieser Größe ist es nicht machbar, die gesamte Stabilisatorgruppe wie im Simulatorbeispiel aufzuzählen; daher wählt die Zelle eine zufällige Teilmenge der Stabilisatoren aus, um eine Schätzung der Genauigkeit zu berechnen.

Beachten Sie, dass Schritt 2 hier mehr leistet als im Simulator-Beispiel: Beim Erlernen des Rauschmodells wird ein eigener NoiseLearner Job vor dem Sampler-Job eingereicht, sodass die Zelle insgesamt zwei Jobs ausführt. Sie tragen die Etiketten und TUT_ASPC_LEARN , TUT_ASPC damit Sie sie später wiederfinden können. Weitere Informationen zum Verschlagworten von Stellenangeboten finden Sie unter „Stellenangebote nach Tags organisieren und suchen “.

# -------------------------Step 1: build a larger payload and sample stabilizers-------------------------
hw_num_qubits = 20
hw_depth = 36
hw_num_stabilizers = 10
hw_num_shots = 40_000

hw_circuit = random_clifford_circuit(hw_num_qubits, hw_depth, rng)
hw_no_meas = hw_circuit.remove_final_measurements(inplace=False)

# Enumerating all 2^n stabilizers is infeasible at this size, so draw each
# stabilizer by composing a random subset of the group generators
hw_generators = [
    Pauli(label) for label in Clifford(hw_no_meas).to_labels(mode="S")
]
sample_rng = np.random.default_rng(seed)
hw_stabilizers = []
while len(hw_stabilizers) < hw_num_stabilizers:
    mask = sample_rng.integers(0, 2, hw_num_qubits).astype(bool)
    if not mask.any():
        continue  # skip the identity
    stabilizer = Pauli("I" * hw_num_qubits)
    for generator, chosen in zip(hw_generators, mask, strict=True):
        if chosen:
            stabilizer = stabilizer.compose(generator)
    hw_stabilizers.append(stabilizer)

# -------------------------Step 2: find a 20-qubit layout, learn its noise, and add checks-------------------------
# A single bad coupler or bad-readout qubit on the path drags every
# stabilizer down, so search harder and with tighter error thresholds
hw_layout = find_check_layout(
    backend,
    hw_num_qubits,
    rng,
    num_trials=500,
    max_gate_error=0.015,
    max_readout_error=0.05,
)
hw_target_qubits, hw_ancilla_qubits = get_check_qubits(backend, hw_layout)
hw_pm = generate_preset_pass_manager(
    optimization_level=0, backend=backend, initial_layout=hw_layout
)
print(f"Layout with {len(hw_target_qubits)} check pairs: {hw_layout}")

# ----- learn a Pauli-Lindblad noise model on this layout -----
# The simulator example scored checks against a uniform depolarizing channel
# inferred from calibration data. Here, learn the noise instead: NoiseLearner
# runs its own job on the QPU and returns a Pauli-Lindblad channel per unique
# entangling layer, so the checks are placed against the noise this layout
# actually has, including its spatial structure. All the sampled stabilizers
# share the same entangling layers and differ only in their final basis
# rotation, so learning on the bare payload covers all of them.
learner = NoiseLearner(
    mode=backend,
    options={
        "max_layers_to_learn": 4,
        "num_randomizations": 32,
        "shots_per_randomization": 128,
        "environment": {"job_tags": ["TUT_ASPC_LEARN"]},
    },
)
learner_job = learner.run([hw_pm.run(hw_circuit)])
print(f"Submitted noise-learner job {learner_job.job_id()}")
hw_layer_errors = learner_job.result().data

# To see how much the learned model helps, swap the next line for the
# simulator example's uniform model - a one-line change:
# hw_noise_model = NoiseModel.from_backend(backend, hw_layout, uniform_gate_noise=True)
hw_noise_model = learned_noise_model(hw_layer_errors, hw_layout)
# NoiseLearner characterizes gate noise only, so keep the readout estimate
# from calibration data rather than leaving it unset
hw_noise_model.readout_noise = NoiseModel.from_backend(
    backend, hw_layout, uniform_gate_noise=True
).readout_noise
print(
    f"Learned {len(hw_layer_errors)} layers; "
    f"readout noise {hw_noise_model.readout_noise:.5f}"
)

# ----- add the fully checked circuit per stabilizer -----
hw_noisy_circuits = []
hw_checked_circuits = []
for i, pauli in enumerate(tqdm(hw_stabilizers)):
    bare = hw_pm.run(append_basis_rotation(hw_circuit, pauli))
    hw_noisy_circuits.append(bare)
    variants = add_pauli_checks(
        bare,
        hw_target_qubits,
        hw_noise_model,
        ancilla_qubits=hw_ancilla_qubits,
        cost="gamma",
        method="windowed",
        seed=seed + 1 + i,
    )
    hw_checked_circuits.append(variants[-1])  # keep the fully checked circuit

# -------------------------Step 3: submit one Sampler job with the bare and checked circuits-------------------------
sampler = Sampler(mode=backend)
sampler.options.default_shots = hw_num_shots
sampler.options.environment.job_tags = ["TUT_ASPC"]

pubs = hw_noisy_circuits + [cc.circuit for cc in hw_checked_circuits]
job = sampler.run(pubs)
print(f"Submitted job {job.job_id()} with {len(pubs)} circuits")

# -------------------------Step 4: postselect and compare fidelity-------------------------
result = job.result()
n_stab = len(hw_stabilizers)

hw_results = []
for i in range(n_stab):
    noisy_counts = result[i].join_data().get_counts()
    checked_counts = result[n_stab + i].join_data().get_counts()
    ps_fn = hw_checked_circuits[i].get_postselection_method()
    kept = {bs: c for bs, c in checked_counts.items() if not ps_fn(bs).any()}
    hw_results.append(
        (
            expectation(noisy_counts, hw_stabilizers[i]),
            expectation(kept, hw_stabilizers[i]),
            sum(kept.values()) / sum(checked_counts.values()),
        )
    )

hw_fidelity_noisy = float(np.nanmean([r[0] for r in hw_results]))
hw_fidelity_postsel = float(np.nanmean([r[1] for r in hw_results]))
hw_psr = float(np.mean([r[2] for r in hw_results]))
print(
    f"noisy fidelity estimate:        {hw_fidelity_noisy:.4f}\n"
    f"postselected fidelity estimate: {hw_fidelity_postsel:.4f}\n"
    f"mean postselection rate:        {hw_psr:.4f} "
    f"(~{int(round(hw_psr * hw_num_shots))} kept shots per circuit)"
)

# Per-stabilizer breakdown. The postselection rate varies from stabilizer to
# stabilizer, so a stabilizer whose postselected value barely moves is usually
# one whose checks rejected little; the kept-shot count says how much of the
# gap is statistics rather than signal.
print("\nper-stabilizer results:")
print(
    f"{'idx':>3}  {'noisy':>8}  {'postsel':>8}  {'psr':>7}  {'kept shots':>10}"
)
for i, (noisy, post, psr_i) in enumerate(hw_results):
    print(
        f"{i:>3}  {noisy:>8.4f}  {post:>8.4f}  {psr_i:>7.4f}  "
        f"{int(round(psr_i * hw_num_shots)):>10}"
    )

hw_noisy = np.array([r[0] for r in hw_results])
hw_post = np.array([r[1] for r in hw_results])
idx = np.arange(n_stab)

fig, ax = plt.subplots(figsize=(9, 4))
ax.axhline(1.0, color="black", linewidth=1.5, label="ideal")
strip(ax, hw_noisy, "red", "noisy")
strip(ax, hw_post, "green", "postselected")
ax.scatter(idx, hw_noisy, color="red", s=22, alpha=0.7, label="noisy EVs")
ax.scatter(
    idx,
    hw_post,
    color="green",
    s=22,
    alpha=0.7,
    label="postselected EVs",
)
ax.set_xlabel("stabilizer index")
ax.set_ylabel(r"$\langle G \rangle$")
ax.set_ylim((-0.1, 1.1))
ax.set_xticks(idx)
ax.set_title("Per-stabilizer expectation values on hardware")
# Outside the axes so it cannot hide a data point
ax.legend(loc="center left", bbox_to_anchor=(1.02, 0.5), frameon=False)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Output:

Layout with 11 check pairs: [153, 152, 151, 138, 131, 130, 129, 118, 109, 110, 111, 98, 91, 90, 89, 78, 69, 70, 71, 58]
Submitted noise-learner job d9f4mncjeosc73fjfmkg
Learned 4 layers; readout noise 0.00470
100%|██████████| 10/10 [01:01<00:00,  6.18s/it]
Submitted job d9f4s04jeosc73fjftkg with 20 circuits
noisy fidelity estimate:        0.3685
postselected fidelity estimate: 0.6869
mean postselection rate:        0.2851 (~11404 kept shots per circuit)

per-stabilizer results:
idx     noisy   postsel      psr  kept shots
  0    0.3769    0.6918   0.3247       12987
  1    0.3745    0.6760   0.2999       11995
  2    0.3659    0.6389   0.3549       14196
  3    0.3821    0.7060   0.2660       10641
  4    0.3653    0.7475   0.2531       10124
  5    0.3752    0.7022   0.2698       10791
  6    0.3508    0.7144   0.2711       10842
  7    0.3485    0.7087   0.2381        9523
  8    0.3825    0.6289   0.2928       11711
  9    0.3630    0.6549   0.2808       11232
Output of the previous code cell

Bei Schaltungen dieser Größe weisen die meisten Proben mindestens einen erkannten Fehler auf, sodass die Postselektionsrate gering ist und bei der Postselektion die meisten Aufnahmen verworfen werden. Die Proben, die alle Prüfungen bestehen, liefern einen deutlich besseren Erwartungswert als die bloße Schaltung, und die Werte pro Stabilisator heben sich deutlich von der verrauschten Basislinie ab. Um die Genauigkeit der Schätzung zu verbessern, sollten mehr Stabilisatoren mit demselben Schussbudget pro Schaltung untersucht werden. Um die Nachselektionsrate zu erhöhen, sollte man die Schaltungstiefe verringern oder weniger Prüfungen durchführen; um größere Nutzlasten zu erreichen, sollte man das Shot-Budget proportional zum Kehrwert der Nachselektionsrate skalieren.


Nächste Schritte

Empfehlungen

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


Referenzen

  • [1] Martiel, S., & Javadi-Abhari, A. (2025). Fehlererkennung mit geringem Overhead mithilfe von Raum-Zeit-Codes. arXiv Vorabdruck arXiv:2504.15725.
  • [2] van den Berg, E., Bravyi, S., Gambetta, J. M., Jurcevic, P., Maslov, D., & Temme, K. (2023). Reduzierung von Einzelimpulsfehlern durch kohärente Pauli-Prüfungen. Physical Review Research, 5(3), 033193.
War diese Seite hilfreich?
Melden Sie auf GitHub einen Fehler, einen Tippfehler oder fordern Sie Inhalte an.