Wykrywanie błędów o niskim narzucie z użyciem kodów czasoprzestrzennych
W tym samouczku zaimplementujemy przepływ pracy służący do próbkowania z obwodu Cliffordowskiego i użyjemy pakietu qiskit-paulice, aby zwiększyć wierność zmierzonego rozkładu przez zastosowanie czasoprzestrzennych kontroli Pauliego w całym obwodzie. Czasoprzestrzenne kontrole Pauliego są podobne do koherentnych kontroli Pauliego (CPC) w tym, że obwód Cliffordowski „ładunku” jest splątywany z pomocniczymi kubitami w celu „sprawdzenia” pewnych niezmienników w obwodzie. Pomiar kubitów pomocniczych daje syndrom, na podstawie którego określa się, czy podczas wykonywania obwodu wykryto błąd logiczny. Postselekcja wyłącznie próbek, w których nie wykryto błędu, może poprawić wierność próbkowanych rozkładów kosztem zmniejszenia współczynników postselekcji. Kluczowa różnica między CPC a kontrolami czasoprzestrzennymi polega na tym, że pomiar syndromów CPC wymaga pomiaru zlokalizowanych w czasie operatorów o dużej wadze. Na topologiach kubitów o ograniczonej łączności, takich jak heavy hex, implementacja tych kontroli wymaga wielu bramek SWAP, co często sprawia, że obwód jest zbyt głęboki, by uruchomić go w praktyce. Implementacja kontroli Pauliego jako kodów czasoprzestrzennych pozwala rozmieścić je w całym obwodzie ładunku, umożliwiając wydajne sprzętowo kodowanie kubitów, które pozostaje skuteczne w wykrywaniu błędów logicznych podczas wykonywania obwodu. Główną rolą pakietu qiskit-paulice jest znajdowanie i wstawianie poprawnych czasoprzestrzennych kontroli Pauliego w miejsca obwodu, które maksymalizują skuteczność wykrywania błędów i minimalizują narzut kubitów.
Ten samouczek implementuje przepływ pracy zwiększający wierność rozkładu próbkowanego z obwodu kwantowego za pomocą kontroli czasoprzestrzennych. Ponieważ użyjemy obwodu Cliffordowskiego, możemy w prosty sposób oszacować wierność na podstawie uśrednionych wartości oczekiwanych stabilizatorów obwodu. Wykonamy następujące kroki:
-
Utwórz obwód: płytki jednowymiarowy losowy obwód Cliffordowski o układzie „brickwork”, używany do wizualizacji kontroli.
-
Wskaż kubity jako cele kontroli: odwzoruj obwód na fizyczne kubity QPU Heron
ibm_bostoni wskaż kubity z sąsiednimi kubitami pomocniczymi. -
Transpiluj obwód do Backendu: wybierz układ kubitów i przetłumacz bramki obwodu na natywny zestaw bramek
ibm_boston. -
Utwórz model szumu na podstawie danych Backendu: wygeneruj przybliżony model szumu na podstawie danych testów wydajności Backendu. Te dane o szumie posłużą do oceny skuteczności każdej dodatkowej kontroli.
-
Zwizualizuj kontrole Pauliego: dodaj kilka kontroli Pauliego do płytkiego obwodu, aby zobaczyć, jak kontrole są dodawane.
-
Określ stabilizatory do pomiaru: zwiększ głębokość obwodu, aby wzmocnić wpływ szumu bramek. Losowo wybierz podzbiór stabilizatorów głębszego obwodu.
-
Znajdź dobre kontrole dla całego stanu: znajdź jeden zestaw kontroli dla stabilizatorów stanu, który przygotowuje obwód. Takie kontrole są poprawne niezależnie od tego, co zostanie zmierzone później, więc ten sam obwód z kontrolami obsługuje każdy próbkowany stabilizator.
-
Spróbkuj z zaszumionego obwodu: zmierz obwody bez kontroli i z kontrolami w bazie każdego próbkowanego stabilizatora za pomocą samplomatic i spróbkuj je za pomocą prymitywu
Executorna zaszumionym symulatorze, który używa tego samego szumu depolaryzacyjnego, co przy szukaniu kontroli. -
Wykonaj postselekcję próbek obwodu z kontrolami bez wykrytych błędów: usuń próbki, które nie przechodzą co najmniej jednej kontroli. Oblicz wartości oczekiwane dla zaszumionego obwodu bez kontroli i obwodu zawierającego kontrole.
-
Porównaj wierności oryginalnych i poddanych postselekcji zaszumionych stanów: pokaż, że postselekcja wyłącznie próbek bez wykrytych błędów poprawia szacowaną wierność zaszumionego stanu.
1. Utwórz obwód
Najpierw utworzymy płytki obwód Cliffordowski, w którym bramki splątujące są ułożone w łączności typu brickwork. Użyjemy tego małego obwodu do zwizualizowania, jak kontrole są dodawane do obwodu.
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"matplotlib": "matplotlib", "numpy": "numpy", "qiskit": "qiskit", "qiskit_aer": "qiskit-aer", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "qiskit_paulice": "qiskit-paulice", "samplomatic": "samplomatic"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
import numpy as np
from qiskit import QuantumCircuit
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
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.draw("mpl", fold=-1, scale=0.6)

2. Wskaż kubity jako cele kontroli
Następnie wybieramy jednowymiarowy układ kubitów na QPU Heron r3, ibm_boston. Wybieramy układ, który zygzakiem biegnie po przekątnej grafu sprzężeń i maksymalizuje liczbę dostępnych kubitów pomocniczych. Aby znaleźć dostępne kubity pomocnicze, używamy funkcji get_check_qubits, która zwraca listy par kubit docelowy/kubit pomocniczy. Kontrola Pauliego na target_qubits[i] zostanie zaimplementowana przy użyciu ancilla_qubits[i].
Na poniższym grafie sprzężeń zielone kubity to kubity ładunku, a pomarańczowe kubity to kubity pomocnicze, których użyjemy do implementacji kontroli. Kubity 68, 69, 89, 91, 111, 113 i 133 mają sąsiednie kubity pomocnicze, więc użyjemy tych kubitów (odpowiadających kubitom wirtualnym 0, 1, 3, 5, 7, 9 i 11) jako kubitów docelowych do implementacji kontroli. Zauważ, że kubit 134 sąsiaduje z kubitem 133 w ładunku, ale nie jest wyróżniony jako kubit pomocniczy. Dzieje się tak, ponieważ 133 został już (arbitralnie) wybrany do implementacji kontroli z kubitem pomocniczym 132.
from qiskit.visualization import plot_coupling_map
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_paulice.layout import get_check_qubits
service = QiskitRuntimeService()
backend = service.backend("ibm_boston")
# Choose a layout and pair each available target qubit with a neighboring ancilla
layout = [68, 69, 78, 89, 90, 91, 98, 111, 112, 113, 119, 133]
target_qubits, ancilla_qubits = get_check_qubits(backend, layout)
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,
)
Target qubits: [68, 69, 89, 91, 111, 113, 133]
Ancilla qubits: [67, 70, 88, 92, 110, 114, 132]

3. Transpiluj obwód do Backendu
Ponieważ Backend został już wybrany, możemy przetranspilować nasz obwód wirtualny do obwodu ISA. Tutaj po prostu ustawiamy układ kubitów i tłumaczymy bramki na natywny zestaw bramek ibm_boston.
from qiskit.transpiler import generate_preset_pass_manager
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)

4. Utwórz model szumu na podstawie danych Backendu
Skoro wiemy już, jak obwód zostanie rozmieszczony na QPU, możemy zamodelować, jak szum bramek i odczytu w Backendzie wpłynie na wykonanie obwodu. Model szumu służy do wyznaczenia optymalnych miejsc w obwodzie na umieszczenie kontroli Pauliego. Chcemy umieszczać kontrole w pozycjach, w których wychwytują największą ilość błędu. Choć dokładniejszy model szumu może poprawić zdolność kontroli do wykrywania błędów, zwykle nie jest konieczne uczenie modelu szumu przez próbkowanie QPU. Tutaj wnioskujemy jednorodny depolaryzacyjny model szumu bramek wyłącznie na podstawie danych testów wydajności Backendu z qiskit-ibm-runtime i pomijamy zgłaszany przez nie błąd odczytu: kontrole, które znajdziemy w kroku 7, odczytują tylko swój kubit pomocniczy, więc błędy odczytu ładunku są dla nich niewidoczne i jedynie obniżyłyby wierność po postselekcji w kroku 10. Więcej informacji o określaniu szumu bramek i odczytu znajdziesz w dokumentacji qiskit_paulice.NoiseModel. Dla użytkowników, którzy chcą użyć wyuczonego modelu szumu Pauli-Lindblad, zobacz NoiseModel.from_pauli_lindblad_maps.
from dataclasses import replace
from qiskit_paulice.noise_models import NoiseModel
# Model the gate noise as a uniform depolarizing channel based on backend benchmark data
noise_model = replace(
NoiseModel.from_backend(backend, layout, uniform_gate_noise=True),
readout_noise=None,
)
print(noise_model)
NoiseModel(gate_noise=0.0015808382794757693, readout_noise=None, idling_noise=None)
5. Zwizualizuj kontrole Pauliego
Dodajmy teraz kilka kontroli do obwodu i zobaczmy, jak wyglądają. Możemy użyć funkcji qiskit_paulice.add_pauli_checks, aby znaleźć dobry zestaw poprawnych kontroli. Jako dane wejściowe podajemy obwód Cliffordowski ładunku, listę kubitów ładunku, do których dodajemy kontrole (kubity docelowe), oraz model szumu utworzony w poprzednim kroku. W tym przykładzie dodamy kontrole do wszystkich 7 kubitów docelowych z dostępnymi kubitami pomocniczymi. Kontrole są dodawane do kubitów w kolejności, w jakiej występują w target_qubits, co ułatwia wnioskowanie o końcowym układzie obwodu z kontrolami: final_layout = layout + ancilla_qubits. Jeśli użytkownik zdecyduje, że woli uruchomić jeden z obwodów wyjściowych z mniejszą liczbą (i) kontroli, końcowy układ będzie miał postać final_layout = layout + ancilla_qubits[:i+1].
Wizualizacja obwodu pokazuje, że kontrole zostały poprawnie zaimplementowane przy użyciu wskazanych par kubit docelowy/kubit pomocniczy.
Szczegóły dotyczące qiskit_paulice.add_pauli_checks:
- Pierwszym argumentem powinien być obwód Cliffordowski. Tutaj nie ma on pomiarów i przekazujemy
stabilizers="all", więc kontrole są znajdowane dla stabilizatorów stanu, który przygotowuje obwód: kontrola jest poprawna, jeśli jej czasoprzestrzenny iloczyn Pauliego mierzy taki stabilizator, jej syndromem jest wyłącznie bit jej kubitu pomocniczego i pozostaje poprawna niezależnie od tego, w jakiej bazie ładunek zostanie później zmierzony. Tego potrzebuje estymacja wierności, ponieważ każdy próbkowany stabilizator jest mierzony w innej bazie. (Pominięciestabilizerspowoduje natomiast znajdowanie kontroli dla końcowych pomiarów obwodu). - Drugim argumentem pozycyjnym jest lista indeksów kubitów docelowych. Każdy kubit docelowy będzie współdzielił bramki splątujące z kubitem pomocniczym, tworząc kontrolę. Kontrole będą znajdowane i dodawane do obwodu w kolejności, w jakiej podano je w tym argumencie.
- Ostatnim argumentem pozycyjnym jest model szumu reprezentujący szum, który ma wpływać na obwód podczas wykonania. Ten model służy do oceny skuteczności danej kontroli.
ancilla_qubits: jeśli obwód wejściowy ma układ, ten argument nazwany instruujeadd_pauli_checks, jak przypisać fizyczne kubity do nowo dodanych kubitów pomocniczych.ancilla_qubitspowinno mieć tę samą długość cotarget_qubits(2. argument pozycyjny), aancilla_qubits[i]powinno służyć do implementacji kontroli natarget_qubits[i].cost: funkcja kosztu używana do oceny kontroli. Może to byćgammalubLER(logiczna stopa błędów), które są sposobami opisania, jaka część kanału szumu pozostaje niewykryta przez kontrole. Przystabilizers="all"błąd jest uznawany za logiczny, jeśli zmienia przygotowany stan, co dokładnie mierzy wierność.method: metoda używana do znajdowania poprawnych kontroli. Używamywindowed, która losowo próbkuje wiele przestrzennych podzbiorów okien nośnika (przewodów) kubitu docelowego. Inne poprawne wartości togeneticiwindowed_genetic.- Wynik
add_pauli_checkszawiera obwody z rosnącą liczbą kontroli, od obwodu bez kontroli do obwodu z kontrolami na każdym kubicie docelowym (zakładając, że znaleziono poprawne kontrole).
Szczegółowe informacje o znajdowaniu dobrych czasoprzestrzennych kontroli Pauliego znajdziesz w sekcjach II-IV informacji uzupełniających
import matplotlib.pyplot as plt
from qiskit_paulice import add_pauli_checks
# Find checks on circuit
checked = add_pauli_checks(
circuit_isa,
target_qubits,
noise_model,
ancilla_qubits=ancilla_qubits,
cost="gamma",
method="windowed",
seed=seed,
stabilizers="all",
)
print(f"Physical layout of payload+ancillas: {layout + ancilla_qubits}")
print("Checked circuit:")
checked[-1].circuit.draw("mpl", fold=-1, idle_wires=False)
Physical layout of payload+ancillas: [68, 69, 78, 89, 90, 91, 98, 111, 112, 113, 119, 133, 67, 70, 88, 92, 110, 114, 132]
Checked circuit:

6. Określ stabilizatory do pomiaru
Aby przetestować skuteczność wykrywania błędów, oszacujemy wierność stanu stabilizatorowego, , który obwód idealnie przygotowuje, względem zaszumionego stanu, , który sprzęt faktycznie zwraca. Projektor na czysty stan stabilizatorowy, , jest zdefiniowany jako jednorodna średnia po elementach jego grupy stabilizatorów :
Podstawiając to do równania na wierność, widzimy, że wierność zaszumionego stanu, , jest równa średniej wartości oczekiwanej wszystkich stabilizatorów, , względem :
Dla większych problemów pomiar (a nawet wyliczenie) wszystkich stabilizatorów jest niewykonalny, więc wierność często szacuje się, mierząc losową próbkę stabilizatorów. Wylosowanie stabilizatorów jednostajnie losowo z daje nieobciążony estymator wierności, ,
W tym eksperymencie znacznie pogłębimy obwód, aby wzmocnić wpływ szumu bramek, i dodamy kontrole do każdego kubitu z sąsiednim kubitem pomocniczym. Choć istnieją bardziej wyrafinowane sposoby próbkowania stabilizatorów do estymacji wierności, w tym przykładzie wylosujemy stabilizatorów z jednakowym prawdopodobieństwem.
from qiskit.quantum_info import Clifford, Pauli, PauliList
num_stabilizers = 100
num_shots = 1_000
num_checks = len(target_qubits)
depth = 72
circuit = random_clifford_circuit(num_qubits, depth, rng)
# Create all 2^N stabilizers
stabilizers = PauliList([Pauli("I" * num_qubits)])
for generator in (
Pauli(label) for label in Clifford(circuit).to_labels(mode="S")
):
stabilizers = stabilizers + stabilizers.compose(generator)
# Choose some random stabilizers from the set
keep = np.where(stabilizers.x.any(axis=1) | stabilizers.z.any(axis=1))[0]
chosen = np.random.default_rng(seed).choice(
keep, size=min(num_stabilizers, len(keep)), replace=False
)
stabilizers = stabilizers[chosen]
depth = circuit.depth(lambda x: x.operation.num_qubits == 2)
print(
f"Sampled {len(stabilizers)} stabilizers of {circuit.num_qubits}-qubit circuit with two-qubit depth of {depth}: {{{stabilizers[0]}, {stabilizers[1]}, ...}}"
)
Sampled 100 stabilizers of 12-qubit circuit with two-qubit depth of 72: {-YIIYZZZIZIYY, -XIXYIIZXYIZX, ...}
7. Znajdź dobre kontrole dla całego stanu
Znajdujemy jeden zestaw kontroli dla głębokiego obwodu dokładnie tak jak w kroku 5. Ponieważ kontrole odczytują tylko swoje kubity pomocnicze, ten sam obwód z kontrolami obsługuje każdy próbkowany stabilizator. Zwrócone obwody mierzą tylko kubity pomocnicze kontroli, więc pomiary ładunku dodamy w następnym kroku.
import time
# Transpile the deep payload without measurements and find one set of checks for the whole state
payload_isa = pm.run(circuit)
t0 = time.time()
checked = add_pauli_checks(
payload_isa,
target_qubits,
noise_model,
ancilla_qubits=ancilla_qubits,
cost="gamma",
method="windowed",
seed=seed,
stabilizers="all",
)
print(
f"Added {len(checked[-1].check_qubits)} checks in {time.time() - t0:.0f}s."
)
print(
f"2-qubit depth increased from {payload_isa.depth(lambda x: len(x.qubits) == 2)} to "
f"{checked[-1].circuit.depth(lambda x: len(x.qubits) == 2)} when adding {num_checks} checks."
)
Added 7 checks in 3s.
2-qubit depth increased from 72 to 85 when adding 7 checks.
8. Spróbkuj z zaszumionego obwodu
Próbkujemy każdy wariant, od samego ładunku po obwód ze wszystkimi 7 kontrolami, dla każdego próbkowanego stabilizatora. Wariant z zerem kontroli to sam ładunek z tymi samymi pomiarami, więc służy jako zaszumiony punkt odniesienia. Zamiast dołączać rotację bazy każdego stabilizatora do kopii każdego wariantu, pozwalamy, aby samplomatic wybierał bazę pomiaru w czasie próbkowania. Dla każdego wariantu:
-
Zmierz ładunek i zgrupuj końcowe pomiary w pudełko za pomocą pass managera pudełkowania z samplomatic. Pudełko niesie adnotację
ChangeBasis, więcbuildzwraca obwód szablonowy, w którym bramki zmieniające bazę są sparametryzowane, wraz z samplexem, który oblicza parametry dla żądanej bazy. -
Dodaj szablon i samplex do
QuantumProgram. Samplex przyjmuje jedną bazę pomiaru na kubit w pudełku, w rosnącej kolejności fizycznej, zakodowaną jako , , , . Przekazanie tablicy baz z wiodącą osią o długości100sprawia, że pojedyncza pozycja mierzy wszystkie100stabilizatorów: ładunek w bazie każdego stabilizatora, a kubity pomocnicze w . Kubit spoza nośnika stabilizatora jest również mierzony w , a jego bit jest ignorowany przy obliczaniu wartości oczekiwanej stabilizatora.
Prymityw Executor z qiskit-ibm-runtime uruchamia cały program w jednym zadaniu. Gdy otrzyma AerSimulator zamiast QPU, uruchamia się lokalnie na tym symulatorze, więc przekazujemy mu symulator z tym samym depolaryzacyjnym szumem bramek, którego użyliśmy do znalezienia kontroli, i używamy metody stabilizatorowej, ponieważ wszystkie obwody są Cliffordowskie. Qiskit Aer jest potrzebny tylko do zbudowania tego symulatora: tryb lokalny bierze ziarno z opcji Executor, ale ignoruje ich noise_model, więc szum musi być ustawiony na samym symulatorze. Przekazanie zamiast tego Backendu QPU uruchomiłoby ten sam program na sprzęcie. Po zakończeniu symulacji obliczymy współczynnik postselekcji i wartości oczekiwane, aby zorientować się, jak dobrze zadziałało wykrywanie błędów, zanim zwizualizujemy dane.
from qiskit import ClassicalRegister
from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel as AerNoiseModel
from qiskit_aer.noise import depolarizing_error
from qiskit_ibm_runtime import Executor
from qiskit_ibm_runtime.quantum_program import QuantumProgram
from samplomatic.builders import build
from samplomatic.transpiler import generate_boxing_pass_manager
# Group the terminal measurements into a box whose measurement basis is chosen at sampling time
boxing_pm = generate_boxing_pass_manager(
enable_gates=False, measure_annotations="change_basis"
)
# bases[i, q]: basis in which physical qubit q is measured for stabilizer i (I=0, Z=1, X=2, Y=3)
bases = np.ones((len(stabilizers), backend.num_qubits), dtype=np.uint8)
bases[:, layout] = stabilizers.z + 2 * stabilizers.x
program = QuantumProgram(shots=num_shots)
for variant in checked:
circ = variant.circuit.copy()
circ.add_register(meas := ClassicalRegister(num_qubits, "meas"))
circ.measure(layout, meas)
template, samplex = build(boxing_pm.run(circ))
(basis_input,) = samplex.inputs().get_specs()
measured_qubits = sorted(layout + list(variant.check_qubits))
program.append_samplex_item(
template,
samplex=samplex,
samplex_arguments={basis_input.name: bases[:, measured_qubits]},
shape=(len(stabilizers),),
)
# Simulate the program with the same gate noise we used to find the checks
aer_nm = AerNoiseModel()
aer_nm.add_all_qubit_quantum_error(
depolarizing_error(noise_model.gate_noise, 2), ["cz"]
)
noisy_sim = AerSimulator(method="stabilizer", noise_model=aer_nm)
# outs[k][register]: bits of the variant with k checks, shape (stabilizers, shots, bits)
t0 = time.time()
outs = (
Executor(noisy_sim, options={"simulator": {"seed_simulator": seed}})
.run(program)
.result()
)
print(
f"Sampled {len(checked)} circuits for {len(stabilizers)} stabilizers in {time.time() - t0:.0f}s."
)
Sampled 8 circuits for 100 stabilizers in 77s.
9. Postselekcja próbek obwodu z kontrolami
W tym pakiecie kontrola jest implementowana za pomocą bramek splątujących między jednym kubitem pomocniczym a jednym kubitem docelowym. Każdy kubit pomocniczy zaczyna w , więc stabilizuje jego stan wejściowy. Propagacja w przód od początku kubitu pomocniczego przez cały obwód z kontrolami daje razy stabilizator przygotowanego stanu, więc kontrola przechodzi, jeśli bit kubitu pomocniczego wynosi . Próbka jest zachowywana, jeśli przechodzi każda kontrola.
Obwody z kontrolami mierzą kubity pomocnicze do rejestru checks_c, więc próbka jest zachowywana, jeśli każdy bit tego rejestru wynosi 0. Ogólnie metoda get_postselection_method instancji CheckedCircuit zwracanych przez add_pauli_checks odwzorowuje ciąg bitów pojedynczego strzału na syndrom każdej kontroli jednocześnie; dla kontroli znalezionych dla stabilizatorów przygotowanego stanu każdy syndrom jest po prostu bitem jego kubitu pomocniczego, więc odczytujemy rejestr bezpośrednio.
Na wykresie widać, że dodawanie kolejnych kontroli powoduje spadek współczynnika postselekcji. Choć celem wykrywania błędów jest wychwycenie jak największej liczby błędów, wraz ze spadkiem współczynnika postselekcji może być potrzebnych więcej strzałów, aby osiągnąć zadaną dokładność. Jeśli współczynnik postselekcji spadnie zbyt nisko, liczba strzałów potrzebnych do zebrania jednej próbki logicznej może stać się zaporowa. W tym przykładzie współczynnik postselekcji wydaje się zbiegać, co sugeruje, że dodatkowe kontrole wnoszą coraz mniejszą dodatkową zdolność wykrywania. Choć dodawanie kolejnych kontroli Pauliego zwykle zapewnia większą zdolność wykrywania błędów, odbywa się to kosztem zwiększonej głębokości obwodu i wyższego kosztu próbkowania.
def keep_mask(out):
"""Per-shot True/False: every ancilla read 0, so every Pauli check passed."""
if "checks_c" not in out: # the bare payload has no checks
return np.ones(out["meas"].shape[:-1], dtype=bool)
return ~out["checks_c"].any(axis=-1)
# masks[k][i]: kept shots of stabilizer i measured on the variant with k checks
masks = [keep_mask(out) for out in outs]
psrs_arr = np.array([mask.mean(axis=-1) for mask in masks]).T
ks = np.arange(psrs_arr.shape[1])
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(ks, psrs_arr.T, color="darkorange", alpha=0.15, linewidth=1)
ax.plot(
ks,
np.median(psrs_arr, axis=0),
color="black",
linewidth=1,
linestyle="--",
label="median",
)
ax.set(
xlabel="Checks committed",
ylabel="Post-selection rate",
ylim=(0, 1.05),
title=f"Per-stabilizer post-selection rate ({len(psrs_arr)} stabilizers)",
)
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

10. Porównaj wierności oryginalnych i poddanych postselekcji zaszumionych stanów
Widzimy, że postselekcja wyłącznie próbek bez wykrytych błędów znacznie poprawia wartość oczekiwaną każdego stabilizatora, a tym samym poprawia szacowaną wierność stanu stabilizatorowego. Choć wartości po postselekcji obliczono wyłącznie z próbek, które przeszły każdą kontrolę, wartości oczekiwane są znacznie dokładniejsze, a wariancja próbkowania jest mniejsza. Widzimy też teraz, że współczynnik postselekcji pokazany wyżej faktycznie zbiegał do wartości zbliżonej do wierności zaszumionego obwodu. Współczynnik postselekcji zbiegał, ponieważ kontrole wychwytywały zdecydowaną większość błędów faktycznie występujących podczas wykonywania obwodu.
def expectation_values(out, mask):
"""EV of each stabilizer from the shots of ``out`` kept by ``mask``.
Bit q of ``meas`` is virtual qubit q, so a stabilizer's eigenvalue in a shot is the parity of
the bits in its support, times its sign.
"""
support = stabilizers.x | stabilizers.z
parity = (out["meas"] & support[:, None, :]).sum(axis=-1) % 2
signs = np.where(stabilizers.phase == 2, -1.0, 1.0)
eigenvalues = np.where(parity, -1.0, 1.0) * signs[:, None]
kept = mask.sum(axis=-1)
return np.where(
kept > 0,
(eigenvalues * mask).sum(axis=-1) / np.maximum(kept, 1),
np.nan,
)
evs_noisy = expectation_values(outs[0], masks[0])
evs_post = expectation_values(outs[-1], masks[-1])
print(
f"ideal fidelity: 1.0\n"
f"noisy fidelity: {np.nanmean(evs_noisy):.4f}\n"
f"postselected fidelity: {np.nanmean(evs_post):.4f}\n"
f"mean post-sel rate: {masks[-1].mean():.3f}\n"
)
evs_ideal = np.ones(len(stabilizers))
idx = np.arange(len(stabilizers))
def cum_mean_sem(ys):
valid = ~np.isnan(ys)
s = np.cumsum(np.where(valid, ys, 0.0))
s2 = np.cumsum(np.where(valid, ys**2, 0.0))
n = np.maximum(np.cumsum(valid).astype(float), 1)
mean = s / n
sem = np.sqrt(np.maximum(s2 / n - mean**2, 0) / n)
return np.where(np.cumsum(valid) > 0, mean, np.nan), sem
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 ± std"
)
ax.axhline(m, color=color, linewidth=1, linestyle="--")
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", "post-sel")
ax.scatter(
idx, evs_noisy, color="red", s=22, alpha=0.7, zorder=3, label="noisy <G>"
)
ax.scatter(
idx,
evs_post,
color="green",
s=22,
alpha=0.7,
zorder=3,
label="post-sel <G>",
)
ax.set(
xlabel="stabilizer index",
ylabel=r"$\langle G \rangle$",
ylim=(-0.1, 1.1),
title="Per-stabilizer EVs",
)
ax.legend(loc="lower left", ncol=2)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
M = np.arange(1, len(stabilizers) + 1)
fig, ax = plt.subplots(figsize=(8, 4))
for ys, color, label in [
(evs_ideal, "black", "ideal"),
(evs_noisy, "red", "noisy"),
(evs_post, "green", "post-sel"),
]:
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",
ylabel="running fidelity estimate",
title="Fidelity convergence vs. number of stabilizers",
)
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
ideal fidelity: 1.0
noisy fidelity: 0.5582
postselected fidelity: 0.9747
mean post-sel rate: 0.535

Ponieważ jeden zestaw kontroli obsługuje każdy stabilizator, jest tylko jedna krzywa gamma do przeanalizowania: wartość funkcji kosztu po zatwierdzeniu każdej kontroli. Dla innych problemów, na innym zestawie fizycznych kubitów i przy niedoskonałym modelu szumu (pamiętaj, że nasz model szumu w tym samouczku dokładnie odwzorowywał szum wpływający na próbki), kolejność dodawania kontroli do kubitów docelowych może mieć wyraźniejszy wpływ na to, jak krzywa zbiega.
To, że ta krzywa gamma zbiega do 1.0, oznacza, że dodatkowe kontrole nie będą w stanie wykryć znacznej ilości dodatkowego błędu, ponieważ bardzo mało modelowanego błędu pozostaje nieobjęte przez kontrole. Potwierdza to naszą wcześniejszą obserwację, że zbieżność współczynnika postselekcji wynika z braku nieobjętego błędu w obwodzie.
costs = [variant.cost for variant in checked]
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(range(len(costs)), costs, color="steelblue", marker="o")
ax.set(
xlabel="Checks committed",
ylabel="Gamma",
yscale="log",
title="Gamma of the noise left uncovered by the checks",
)
ax.grid(True, alpha=0.3, which="both")
plt.tight_layout()
plt.show()