Detectarea erorilor cu suprasarcină redusă folosind coduri spațiu-timp
Estimare de utilizare: 4 minute pe un procesor Heron (ibm_kingston sau echivalent) (NOTĂ: Aceasta este doar o estimare. Timpul tău de rulare poate varia.)
Rezultate ale învățării
-
Cum verificările Pauli spațiu-timp detectează erorile logice în circuitele Clifford și cum postselecția pe sindroamele lor crește fidelitatea unei distribuții eșantionate.
-
Cum să folosești pachetul
qiskit-paulicepentru a găsi și insera automat verificări eficiente pentru hardware cuget_check_qubits,NoiseModelșiadd_pauli_checks. -
Cum să estimezi fidelitatea unei stări stabilizatoare prin eșantionarea stabilizatorilor săi și postselecția pe sindroame de verificare.
-
Cum să rulezi întregul flux de lucru de detecție a erorilor pe hardware IBM Quantum® și să compari fidelitățile zgomotoase și postselectate.
Cerințe preliminare
-
Fundamentele hardware pentru calculul cuantic la scară utilă.
-
Formalismul Clifford și stabilizator, inclusiv modul în care un grup stabilizator descrie o stare stabilizatoare pură.
Context
Detectarea erorilor cu suprasarcină redusă folosind coduri spațiu-timp [1] de Simon Martiel și Ali Javadi-Abhari introduce o metodă de detectare a erorilor logice în circuite dominate de Clifford, care se situează între corecția completă a erorilor și atenuarea erorilor mai ușoară. Ideea se bazează pe verificările Pauli coerente (CPC) din Single-shot error mitigation by coherent Pauli checks [2] de van den Berg și alții. În ambele abordări, un circuit "payload" Clifford este încâlcit (entangled) cu qubiți ancilă pentru a verifica anumite invariante. Măsurarea ancilelor produce un sindrom care raportează dacă a fost detectată o eroare în timpul execuției. Păstrarea doar a eșantioanelor fără eroare detectată îmbunătățește fidelitatea distribuției eșantionate, cu costul unei rate de postselecție reduse.
Diferența cheie dintre verificările Pauli coerente și verificările spațiu-timp este constituită de operatorii pe care îi măsoară. Verificările Pauli coerente măsoară operatori localizați în timp, de pondere mare. Pe topologii de qubiți cu conectivitate limitată, cum ar fi heavy hex, aceste verificări necesită multe porți SWAP și adesea fac circuitul prea adânc pentru a rula în practică. Implementarea verificărilor ca și coduri spațiu-timp distribuie în schimb fiecare verificare pe circuitul payload în spațiu și timp. Acest lucru produce o codificare eficientă pentru hardware, care rămâne eficace la detectarea erorilor logice, menținând totodată joasă suprasarcina de qubiți și adâncime.
Ce face pachetul qiskit-paulice
Pachetul qiskit-paulice automatizează construcția acestor verificări, astfel încât nu trebuie să le construiești manual. Rolul său principal este să găsească și să insereze verificări Pauli spațiu-timp valide în locațiile dintr-un circuit care maximizează detectarea erorilor, minimizând în același timp suprasarcina de qubiți. O verificare este validă atunci când operatorii săi lasă acțiunea logică a circuitului payload neschimbată, de pondere mică atunci când folosește puține porți de încâlcire (entangling) și eficace atunci când detectează o mare parte din erori, relativ la zgomotul pe care verificarea însăși îl introduce. Pachetul evaluează verificările candidate în raport cu un model de zgomot și le adaugă pe cele mai bune la circuit. Acest tutorial folosește trei metode API:
-
get_check_qubitsinspectează harta de cuplaj a unui backend și returnează perechi de qubiți țintă și ancilă. O verificare petarget_qubits[i]foloseșteancilla_qubits[i]. -
NoiseModel.from_backendconstruiește un model de zgomot aproximativ din datele de benchmark ale backend-ului. Modelul evaluează verificările candidate, deci nu este necesar un model de zgomot exact, învățat. Pentru un model Pauli-Lindblad învățat, veziNoiseModel.from_pauli_lindblad_maps. -
add_pauli_checksgăsește și inserează verificări într-un circuit. Returnează o secvență de obiecteCheckedCircuitcu un număr crescător de verificări, iar fiecare obiect oferă oget_postselection_methodcare mapează un șir de biți măsurat la un vector de sindrom. Argumentulcostselectează funcția care evaluează o verificare (gamma, suprasarcina de eșantionare a canalului de zgomot invers postselectat, sauLER, rata de eroare logică). Argumentulmethodselectează strategia de căutare (windowed,geneticsauwindowed_genetic). Acest tutorial foloseștecost="gamma"șimethod="windowed", care împreună dau o selecție de verificări deterministă, reproductibilă.
Estimarea fidelității din eșantionarea stabilizatorilor
Pentru a măsura cât de bine funcționează detectarea erorilor, poți estima fidelitatea stării stabilizatoare pe care circuitul o pregătește ideal, față de starea zgomotoasă pe care hardware-ul o produce efectiv. Proiectorul pe o stare stabilizatoare pură este egal cu media uniformă peste cele elemente ale grupului său stabilizator :
Substituind aceasta în fidelitate, obținem fidelitatea lui ca media valorii de așteptare a fiecărui stabilizator în raport cu :
Pentru probleme mai mari, enumerarea tuturor celor stabilizatori este imposibilă, așa că poți estima fidelitatea dintr-un eșantion aleatoriu. Extragerea a stabilizatori uniform aleatoriu din dă o estimare nedeplasată:
Deoarece un circuit Clifford pregătește o stare stabilizatoare, poți estima fidelitatea sa direct din valorile de așteptare eșantionate ale stabilizatorilor săi. Acest tutorial parcurge mai întâi fluxul de lucru pe un simulator cu un circuit mic, apoi rulează același flux de lucru pe hardware cu un circuit mai mare și mai adânc. Pe măsură ce circuitele includ mai multe operații non-Clifford, numărul de verificări valide scade rapid, astfel încât metoda funcționează cel mai bine pentru circuite dominate de Clifford.
Cerințe
Înainte de a începe acest tutorial, asigură-te că ai instalate următoarele:
-
Qiskit SDK v2.0 sau o versiune ulterioară, cu suport pentru vizualizare
-
Qiskit Runtime v0.40 sau o versiune ulterioară (
pip install qiskit-ibm-runtime) -
Qiskit Aer v0.17 sau o versiune ulterioară (
pip install qiskit-aer) -
Qiskit Paulice (
pip install qiskit-paulice) -
tqdm (
pip install tqdm)
Configurare
Importă bibliotecile necesare și definește funcțiile ajutătoare care nu sunt disponibile ca importuri. Funcția random_clifford_circuit construiește un payload Clifford aleator de tip brickwork, find_check_layout caută în harta de conectare a unui backend un traseu de qubiți cu erori reduse și multe ancile disponibile, learned_noise_model transformă ieșirea NoiseLearner într-un model de zgomot qiskit-paulice, append_basis_rotation rotește un circuit astfel încât un stabilizator să fie măsurat în baza computațională, expectation calculează o valoare de expectanță a unui stabilizator din numărătorile eșantionate, iar cum_mean_sem urmărește estimarea curentă a fidelității.
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-aer qiskit-ibm-runtime qiskit-paulice tqdm
# 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
Exemplu de simulator la scară mică
Această secțiune parcurge întregul flux de lucru pe un simulator zgomotos. Utilizează date de referință ale backend-ului pentru a alege un layout de qubiți și un model de zgomot, găsește verificări automat și folosește postselecția pe distribuția eșantionată pentru a arăta îmbunătățirea fidelității.
Pasul 1: Maparea intrărilor clasice la o problemă cuantică
Circuitul payload este un circuit Clifford aleator, superficial, de tip brickwork unidimensional. Deoarece circuitul este Clifford, acesta pregătește o stare stabilizator a cărei fidelitate o poți estima direct din valorile de expectanță ale stabilizatorului eșantionate. Începe cu un circuit superficial pentru ca verificările să fie ușor de vizualizat în pasul următor.
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)
Pasul 2: Optimizare pentru execuția pe hardware cuantic
Maparea circuitului pe hardware stabilește layout-ul fizic al qubiților, modelul de zgomot care evaluează verificările candidate și verificările în sine.
Mai întâi, selectează un backend și caută în harta sa de conectare un layout de qubiți unidimensional cu ajutorul funcției find_check_layout definite în secțiunea Configurare. Funcția ajutătoare construiește plimbări aleatoare auto-evitante care evită porțile și citirile cu cele mai mari erori și păstrează traseul care oferă cele mai multe perechi de țintă și ancilă. Deoarece căutarea citește datele de conectivitate și de eroare direct din backend, același cod rulează pe orice QPU IBM Quantum. Funcția get_check_qubits returnează apoi perechile de țintă și ancilă, unde o verificare pe target_qubits[i] folosește ancilla_qubits[i].
În graful de conectare care urmează, qubiții verzi sunt qubiți payload, iar qubiții portocalii sunt ancilele care implementează verificările. Qubiții cu o ancilă adiacentă sunt folosiți ca qubiți țintă pentru verificări.
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,
)
Backend: ibm_boston
Target qubits: [105, 107, 108, 123, 125, 141, 143]
Ancilla qubits: [104, 97, 109, 122, 126, 140, 144]

Cu backend-ul și layout-ul alese, transpilează payload-ul într-un circuit de arhitectură a setului de instrucțiuni (ISA). Este necesar doar să setezi layout-ul și să traduci porțile în setul de porți nativ al backend-ului.
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)

În continuare, modelează modul în care zgomotul porților și al citirii de pe backend afectează execuția. Modelul de zgomot determină unde în circuit o verificare captează cea mai mare parte a erorii. Un model mai precis îmbunătățește detecția, dar de obicei nu este necesar să înveți unul prin eșantionarea QPU-ului. Modelul care urmează deduce un canal de depolarizare uniform pentru zgomotul porților și al citirii din datele de referință qiskit-ibm-runtime.
noise_model = NoiseModel.from_backend(
backend, layout, uniform_gate_noise=True
)
print(noise_model)
NoiseModel(gate_noise=0.001079865281450939, readout_noise=0.006001790364583333, idling_noise=None)
Acum adaugă verificări la circuit. Funcția add_pauli_checks primește payload-ul Clifford, lista de qubiți țintă și modelul de zgomot. Argumentul ancilla_qubits indică funcției care ancilă fizică să fie asociată cu fiecare țintă. Verificările sunt adăugate în ordinea în care apar qubiții țintă, astfel încât layout-ul final al circuitului verificat este layout + ancilla_qubits. Pentru a rula un circuit de ieșire cu mai puține (i) verificări, layout-ul final este layout + ancilla_qubits[:i].
Ieșirea funcției add_pauli_checks este o secvență de circuite cu un număr crescător de verificări, de la niciun fel de verificare până la o verificare pe fiecare qubit țintă. Vizualizarea confirmă că verificările folosesc perechile de țintă și ancilă specificate. Pentru detalii despre găsirea unor verificări bune, consultă secțiunile II-IV din informațiile suplimentare din referința [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)
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:

Pasul 3: Execuție folosind primitivele Qiskit
Pentru a face vizibil efectul zgomotului porților, crește adâncimea payload-ului și eșantionează un subset din stabilizatorii săi. Fiecare stabilizator, în general, nu comută pe qubiți cu ceilalți, astfel încât un singur set de verificări nu este valid pentru doi stabilizatori diferiți. În loc să grupezi stabilizatorii în seturi care comută, găsește un set bun de verificări pentru fiecare stabilizator în mod independent. Eșantionarea stabilizatorilor uniform aleator oferă o estimare nepărtinitoare a fidelității.
Construiește circuitul mai adânc și extrage un eșantion aleator din stabilizatorii săi.
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]}, ...}}"
)
Sampled 20 stabilizers of a 12-qubit circuit with two-qubit depth 24: {ZXIIXZYYXIZZ, XXXYIIZYXIII, ...}
Pentru fiecare stabilizator eșantionat, rotește circuitul astfel încât stabilizatorul să fie măsurat în baza computațională, transpilează-l pe backend și găsește un set bun de verificări. Perechile de țintă și ancilă sunt amestecate împreună pentru fiecare stabilizator astfel încât fiecare țintă să își păstreze ancila. Reține că verificările sunt fixate secvențial în ordinea în care sunt date qubiții țintă, iar o verificare fixată nu se schimbă pe măsură ce se adaugă mai multe verificări.
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."
)
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.
Eșantionează payload-ul brut și circuitele verificate cu Qiskit Aer. Simulatorul folosește același model de depolarizare care a evaluat verificările, astfel încât zgomotul vizat de verificări este zgomotul aplicat de simulator.
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))
100%|██████████| 20/20 [00:17<00:00, 1.13it/s]
Pasul 4: Post-procesare și returnarea rezultatului în formatul clasic dorit
Fiecare verificare folosește porți de întrețesere între o ancilă și o țintă. Ancila începe în , deci îi stabilizează intrarea. Propagarea înainte prin circuitul verificat produce un operator Pauli pe ieșire ale cărui termeni non-identitate definesc suportul verificării. O verificare trece atunci când biții din suportul său au paritate pară. Un eșantion este păstrat doar atunci când fiecare verificare trece.
Funcția get_postselection_method a fiecărui CheckedCircuit returnează o funcție care mapează un șir de biți măsurat la un vector de sindrom. Păstrează eșantioanele al căror sindrom este zero pentru fiecare verificare și elimină restul. Graficul care urmează arată că adăugarea mai multor verificări scade rata de postselecție. O rată de postselecție mai mică necesită mai multe fotografii (shots) pentru a atinge o precizie țintă, deci există un compromis între capacitatea de detecție și costul eșantionării. Rata pare să convergă, ceea ce indică faptul că verificările suplimentare contribuie cu o capacitate de detecție mai mică.
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()
Acum compară fidelitatea stării zgomotoase brute cu starea postselectată. Postselectarea doar a eșantioanelor fără erori detectate crește valoarea de expectanță a fiecărui stabilizator și, prin urmare, fidelitatea estimată. Valorile postselectate folosesc mai puține eșantioane decât valorile brute, totuși valorile de expectanță sunt mai precise, iar varianța eșantionată este mai mică. Observă de asemenea că rata medie de postselecție este apropiată de fidelitatea zgomotoasă. Aceasta este ceea ce te aștepți când verificările detectează aproape toate eșantioanele eronate: fracția de eșantioane care trec fiecare verificare se apropie de fracția de eșantioane fără erori, care este fidelitatea stării zgomotoase.
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()
ideal fidelity: 1.0
noisy fidelity: 0.7899
postselected fidelity: 0.9679
mean postselection rate: 0.780

Scorul gamma raportează cât de mult din canalul de zgomot modelat rămâne nedetectat de verificări. Reprezentarea grafică a scorului gamma în funcție de numărul de verificări fixate arată cum se îmbunătățește capacitatea de detecție pe măsură ce se adaugă fiecare verificare. O valoare de 1.0 înseamnă că verificările captează tot zgomotul modelat. Curbele scad spre 1.0 pe măsură ce se fixează mai multe verificări, ceea ce arată că fiecare verificare suplimentară captează o parte din eroarea rămasă nedetectată.
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()
Exemplu de hardware la scară mare
Același flux de lucru rulează pe hardware cu un payload mai mare și mai adânc. Această secțiune reutilizează backend-ul din exemplul cu simulator, dar construiește un nou layout de 20 de qubiți cu propriile perechi de țintă și ancilă și propriul pass manager, apoi trimite circuitele către QPU într-un singur job. La această dimensiune, majoritatea fotografiilor declanșează cel puțin o verificare, astfel încât rata de postselecție este mică, iar fiecare circuit are nevoie de un buget mare de fotografii pentru a avea suficiente eșantioane care să supraviețuiască. Exemplul își concentrează așadar bugetul pe câțiva stabilizatori eșantionați; aceasta rămâne o estimare nepărtinitoare a fidelității, dar este mai grosieră decât media pe mulți stabilizatori din exemplul cu simulator.
Un lucru se schimbă față de exemplul cu simulator: în loc să deducă un canal de depolarizare uniform din datele de calibrare, această secțiune învață modelul de zgomot cu NoiseLearner și construiește modelul qiskit-paulice din rezultat cu NoiseModel.from_pauli_lindblad_maps. Un model Pauli-Lindblad învățat captează structura spațială a zgomotului pe acest layout specific, în loc să presupună că fiecare muchie este la fel de zgomotoasă, astfel încât plasarea verificărilor este evaluată pe baza unui zgomot mai similar celui care afectează QPU-ul. Învățarea zgomotului necesită eșantionarea QPU-ului și ar trebui luată în considerare în orice buget general de eșantionare a QPU-ului.
Parametrii care urmează setează numărul de qubiți, adâncimea, numărul de stabilizatori și numărul de fotografii. Scalează hw_num_shots cu inversul ratei de postselecție: la o rată de 3%, 40.000 de fotografii lasă aproximativ 1.200 de eșantioane postselectate per circuit. Crește hw_num_stabilizers pentru o estimare mai precisă a fidelității, cu costul mai multor circuite per job, fiecare având nevoie de același buget de fotografii.
Pașii 1-4 (comprimați într-un singur bloc de cod)
Celula următoare rulează aceiași patru pași ca și exemplul cu simulator. Construiește payload-ul mai mare și eșantionează câțiva stabilizatori (pasul 1); alege un layout, învață modelul de zgomot pe acesta și găsește circuitul complet verificat pentru fiecare stabilizator (pasul 2); trimite un singur job Sampler care conține atât circuitele brute, cât și pe cele verificate (pasul 3); și postselectează numărătorile verificate pentru a compara estimările de fidelitate zgomotoasă și postselectată, per stabilizator și în medie (pasul 4). La această dimensiune, enumerarea întregului grup de stabilizatori, ca în exemplul cu simulator, nu este fezabilă, așa că celula subeșantionează un subset aleator de stabilizatori pentru a calcula o estimare a fidelității.
Observă că pasul 2 face mai mult aici decât în exemplul cu simulator: învățarea modelului de zgomot trimite propriul job NoiseLearner înainte de jobul Sampler, deci celula rulează două joburi în total. Acestea poartă etichetele TUT_ASPC_LEARN și TUT_ASPC astfel încât să le poți găsi mai târziu. Vezi Organizarea și căutarea după etichete de job pentru mai multe despre etichetarea joburilor.
# -------------------------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()
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
Pentru circuite de această dimensiune, majoritatea eșantioanelor conțin cel puțin o eroare detectată, astfel încât rata de postselecție este mică, iar postselecția elimină cele mai multe fotografii. Eșantioanele care trec fiecare verificare oferă o valoare de expectanță mult mai bună decât circuitul brut, iar valorile per stabilizator se separă clar de valoarea de referință zgomotoasă. Pentru a rafina estimarea fidelității, eșantionează mai mulți stabilizatori cu același buget de fotografii per circuit. Pentru a crește rata de postselecție, redu adâncimea circuitului sau fixează mai puține verificări; pentru a trece la payload-uri mai mari, scalează bugetul de fotografii cu inversul ratei de postselecție.
Pașii următori
Dacă acest studiu ți s-a părut interesant, s-ar putea să te intereseze următoarele materiale:
-
Tutorialul despre coduri de repetiție pentru o introducere în corecția erorilor cuantice.
-
Documentația
qiskit-paulicepentru API-ul complet de găsire a verificărilor și repozitoriul GitHub al pachetului pentru codul sursă. -
Articolul Detectarea erorilor cu costuri reduse folosind coduri spațio-temporale pentru teoria din spatele verificărilor.
Referințe
-
[1] Martiel, S., & Javadi-Abhari, A. (2025). Low-overhead error detection with spacetime codes. arXiv preprint arXiv:2504.15725.
-
[2] van den Berg, E., Bravyi, S., Gambetta, J. M., Jurcevic, P., Maslov, D., & Temme, K. (2023). Single-shot error mitigation by coherent Pauli checks. Physical Review Research, 5(3), 033193.