Detectarea erorilor cu cost suplimentar redus folosind coduri spațiu-timp
În acest tutorial vom implementa un flux de lucru pentru a eșantiona dintr-un circuit Clifford și vom folosi pachetul qiskit-paulice pentru a crește fidelitatea distribuției măsurate aplicând verificări Pauli spațiu-timp pe parcursul circuitului. Verificările Pauli spațiu-timp sunt similare cu verificările Pauli coerente (CPC) prin faptul că un circuit Clifford „sarcină utilă” este încâlcit cu câțiva qubiți auxiliari pentru a „verifica” anumiți invarianți ai circuitului. Măsurarea qubiților auxiliari produce un sindrom care este folosit pentru a determina dacă o eroare logică a fost detectată în timpul execuției circuitului. Postselecția doar a eșantioanelor în care nu a fost detectată nicio eroare poate îmbunătăți fidelitatea distribuțiilor eșantionate, cu prețul scăderii ratelor de postselecție. Diferența cheie dintre CPC și verificările spațiu-timp este că măsurarea sindroamelor CPC necesită măsurarea unor operatori localizați în timp, cu pondere mare. Pe topologii de qubiți cu conectivitate limitată, cum ar fi heavy hex, implementarea acestor verificări necesită multe porți SWAP, făcând adesea circuitul prea adânc pentru a fi rulat în practică. Implementarea verificărilor Pauli ca coduri spațiu-timp permite distribuirea lor pe tot circuitul sarcinii utile, permițând o codificare a qubiților eficientă din punct de vedere hardware, care rămâne eficace în detectarea erorilor logice în timpul execuției circuitului. Rolul principal al pachetului qiskit-paulice este de a găsi și de a insera verificări Pauli spațiu-timp valide în locuri din circuit care maximizează eficacitatea detectării erorilor și minimizează costul suplimentar în qubiți.
Acest tutorial implementează un flux de lucru pentru creșterea fidelității distribuției eșantionate dintr-un circuit cuantic folosind verificări spațiu-timp. Deoarece vom folosi un circuit Clifford, putem estima fidelitatea direct din valorile de așteptare medii ale stabilizatorilor circuitului. Vom urma acești pași:
-
Crearea unui circuit: un circuit Clifford aleatoriu de tip brickwork 1D, puțin adânc, folosit pentru a vizualiza verificările.
-
Identificarea qubiților țintă pentru verificări: mapează circuitul pe qubiți fizici ai QPU-ului Heron
ibm_bostonși identifică qubiții cu qubiți auxiliari vecini. -
Transpilarea circuitului pentru backend: alege o dispunere a qubiților și traduce porțile circuitului în setul de porți nativ al
ibm_boston. -
Crearea unui model de zgomot din datele backend-ului: generează un model de zgomot aproximativ pe baza datelor de benchmark ale backend-ului. Aceste date de zgomot vor fi folosite pentru a evalua eficacitatea fiecărei verificări suplimentare.
-
Vizualizarea verificărilor Pauli: adaugă câteva verificări Pauli circuitului puțin adânc, ca să putem vizualiza cum sunt adăugate verificările.
-
Determinarea stabilizatorilor de măsurat: crește adâncimea circuitului pentru a mări impactul zgomotului porților. Eșantionează aleatoriu un subset de stabilizatori ai circuitului mai adânc.
-
Găsirea unor verificări bune pentru întreaga stare: găsește un set de verificări pentru stabilizatorii stării pe care o pregătește circuitul. Astfel de verificări sunt valide indiferent de ce se măsoară ulterior, deci același circuit verificat servește fiecărui stabilizator eșantionat.
-
Eșantionarea din circuitul zgomotos: măsoară circuitele simplu și cel verificat în baza fiecărui stabilizator eșantionat cu samplomatic și eșantionează-le cu primitiva
Executorpe un simulator zgomotos care folosește același zgomot depolarizant care a fost folosit pentru a găsi verificările. -
Postselecția eșantioanelor circuitului verificat fără erori detectate: elimină eșantioanele care eșuează la cel puțin o verificare. Calculează valorile de așteptare pentru circuitul simplu zgomotos și pentru circuitul care conține verificări.
-
Compararea fidelităților stărilor zgomotoase originale și postselectate: arată că postselecția doar a eșantioanelor fără erori detectate îmbunătățește fidelitatea estimată a stării zgomotoase.
1. Crearea circuitului
Mai întâi, vom crea un circuit Clifford puțin adânc, cu porțile de încâlcire dispuse într-o conectivitate de tip brickwork. Vom folosi acest circuit mic pentru a vizualiza cum sunt adăugate verificările în circuit.
# 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. Identificarea qubiților țintă pentru verificări
În continuare alegem o dispunere 1D a qubiților pe QPU-ul Heron r3, ibm_boston. Alegem o dispunere care merge în zigzag pe diagonală prin graful de cuplare și maximizează numărul de qubiți auxiliari disponibili. Pentru a găsi qubiții auxiliari disponibili, folosim funcția get_check_qubits, care returnează liste de perechi qubit țintă/auxiliar. O verificare Pauli pe target_qubits[i] va fi implementată folosind ancilla_qubits[i].
În graful de cuplare de mai jos, qubiții verzi sunt qubiții sarcinii utile, iar cei portocalii sunt qubiții auxiliari pe care îi vom folosi pentru a implementa verificările. Qubiții 68, 69, 89, 91, 111, 113 și 133 au qubiți auxiliari adiacenți, așa că vom folosi acești qubiți (corespunzători qubiților virtuali 0, 1, 3, 5, 7, 9 și 11) ca qubiți țintă pentru implementarea verificărilor. Reține că qubitul 134 este adiacent qubitului 133 în sarcina utilă, dar nu este evidențiat ca auxiliar. Aceasta se întâmplă deoarece 133 a fost deja ales (în mod arbitrar) pentru a implementa o verificare cu qubitul auxiliar 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. Transpilarea circuitului pentru backend
Deoarece backend-ul a fost deja ales, putem transpila circuitul nostru virtual într-un circuit ISA. Aici doar stabilim dispunerea qubiților și traducem porțile în setul de porți nativ al 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. Crearea unui model de zgomot din datele backend-ului
Acum că știm cum va fi dispus circuitul pe QPU, putem modela cum va afecta zgomotul porților și al citirii de pe backend execuția circuitului. Modelul de zgomot este folosit pentru a determina locurile optime din circuit unde să plasăm verificările Pauli. Dorim să plasăm verificările în poziții în care captează cea mai mare cantitate de eroare. Deși un model de zgomot mai precis poate îmbunătăți capacitățile de detectare ale verificărilor, de obicei nu este necesar să înveți un model de zgomot prin eșantionarea QPU-ului. Aici deducem un model uniform de zgomot depolarizant al porților folosind doar datele de benchmark ale backend-ului din qiskit-ibm-runtime și renunțăm la eroarea de citire pe care o raportează și ea: verificările pe care le găsim la pasul 7 citesc doar qubitul lor auxiliar, deci erorile de citire ale sarcinii utile sunt invizibile pentru ele și ar scădea doar fidelitatea postselectată de la pasul 10. Consultă documentația qiskit_paulice.NoiseModel pentru mai multe informații despre specificarea zgomotului porților și al citirii. Pentru utilizatorii care doresc să folosească un model de zgomot Pauli-Lindblad învățat, consultă 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. Vizualizarea verificărilor Pauli
Acum să adăugăm câteva verificări circuitului și să vedem cum arată. Putem folosi funcția qiskit_paulice.add_pauli_checks pentru a găsi un set bun de verificări valide. Furnizăm ca intrare circuitul Clifford al sarcinii utile, împreună cu o listă de qubiți ai sarcinii utile pe care se adaugă verificări (qubiții țintă) și modelul de zgomot pe care l-am creat la pasul anterior. În acest exemplu, vom adăuga verificări pe toți cei 7 qubiți țintă cu qubiți auxiliari disponibili. Verificările sunt adăugate qubiților în ordinea în care apar în target_qubits, ceea ce face ușor de înțeles dispunerea finală a circuitului verificat: final_layout = layout + ancilla_qubits. Dacă un utilizator decide că preferă să ruleze unul dintre circuitele de ieșire cu mai puține (i) verificări, dispunerea finală ar fi final_layout = layout + ancilla_qubits[:i+1].
Vizualizarea circuitului arată că verificările au fost implementate corect folosind perechile țintă/auxiliar specificate.
Detalii despre qiskit_paulice.add_pauli_checks:
- Prima intrare trebuie să fie un circuit Clifford. Aici nu are măsurători și transmitem
stabilizers="all", deci verificările sunt găsite pentru stabilizatorii stării pe care o pregătește circuitul: o verificare este validă dacă produsul ei Pauli spațiu-timp măsoară un astfel de stabilizator, sindromul ei este doar bitul qubitului auxiliar și rămâne valid indiferent în ce bază este măsurată ulterior sarcina utilă. Acest lucru este necesar pentru estimarea fidelității, deoarece fiecare stabilizator eșantionat este măsurat într-o bază diferită. (Omitereastabilizersgăsește în schimb verificări pentru măsurătorile terminale ale unui circuit.) - Al doilea argument pozițional este o listă de indici ai qubiților țintă. Fiecare qubit țintă va partaja porți de încâlcire cu un qubit auxiliar pentru a forma o verificare. Verificările vor fi găsite și adăugate circuitului în ordinea în care sunt date în acest argument.
- Ultimul argument pozițional este un model de zgomot care reprezintă zgomotul așteptat să afecteze circuitul în timpul execuției. Acest model este folosit pentru a evalua eficacitatea unei verificări date.
ancilla_qubits: dacă circuitul de intrare are o dispunere, acest argument cu nume îi indică luiadd_pauli_checkscum să atribuie qubiți fizici qubiților auxiliari nou adăugați.ancilla_qubitstrebuie să aibă aceeași lungime catarget_qubits(al 2-lea argument pozițional), iarancilla_qubits[i]trebuie folosit pentru a implementa verificările petarget_qubits[i].cost: funcția de cost folosită pentru a evalua verificările. Poate figammasauLER(rata erorilor logice), care sunt toate moduri de a descrie cât din canalul de zgomot rămâne nedetectat de verificări. Custabilizers="all"o eroare contează ca logică dacă schimbă starea pregătită, ceea ce măsoară exact fidelitatea.method: metoda folosită pentru a găsi verificări valide. Folosimwindowed, care eșantionează aleatoriu multe subseturi de ferestre spațiale ale suportului (firelor) unui qubit țintă. Alte valori valide suntgeneticșiwindowed_genetic.- Ieșirea lui
add_pauli_checksconține circuite cu un număr tot mai mare de verificări, de la un circuit fără verificări până la un circuit cu verificări pe fiecare qubit țintă (presupunând că se găsesc verificări valide).
Consultă secțiunile II-IV din informațiile suplimentare pentru informații detaliate despre găsirea unor verificări Pauli spațiu-timp bune
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. Determinarea stabilizatorilor de măsurat
Pentru a testa performanța detectării erorilor, vom estima fidelitatea stării stabilizator, , pe care circuitul o pregătește ideal, în raport cu starea zgomotoasă, , pe care o produce de fapt hardware-ul. Proiectorul pe starea stabilizator pură, , este definit ca media uniformă pe cele elemente ale grupului său de stabilizatori :
Înlocuind aceasta în ecuația fidelității, vedem că fidelitatea stării zgomotoase, , este echivalentă cu valoarea de așteptare medie a tuturor stabilizatorilor, , în raport cu :
Pentru probleme mai mari, măsurarea (sau chiar enumerarea) tuturor celor stabilizatori este nefezabilă, așa că fidelitatea este adesea estimată măsurând un eșantion aleatoriu de stabilizatori. Eșantionarea a stabilizatori uniform aleatoriu din dă un estimator nedeplasat al fidelității, ,
Pentru acest experiment vom face circuitul mult mai adânc pentru a spori efectul zgomotului porților și vom adăuga verificări pe fiecare qubit cu un qubit auxiliar vecin. Deși există modalități mai sofisticate de a eșantiona stabilizatori pentru a estima fidelitatea, în acest exemplu vom eșantiona de stabilizatori cu probabilitate uniformă.
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. Găsirea unor verificări bune pentru întreaga stare
Găsim un set de verificări pentru circuitul adânc exact ca la pasul 5. Deoarece verificările citesc doar qubiții lor auxiliari, același circuit verificat servește fiecărui stabilizator eșantionat. Circuitele returnate măsoară doar qubiții auxiliari de verificare, așa că vom adăuga măsurătorile sarcinii utile la pasul următor.
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. Eșantionarea din circuitul zgomotos
Eșantionăm fiecare variantă, de la sarcina utilă simplă până la circuitul cu toate cele 7 verificări, pentru fiecare stabilizator eșantionat. Varianta cu zero verificări este sarcina utilă simplă cu aceleași măsurători, deci servește drept referință zgomotoasă. În loc să adăugăm rotația de bază a fiecărui stabilizator la o copie a fiecărei variante, lăsăm samplomatic să aleagă baza de măsurare în momentul eșantionării. Pentru fiecare variantă:
-
Măsurăm sarcina utilă și grupăm măsurătorile terminale într-o casetă cu pass manager-ul de încadrare al samplomatic. Caseta poartă o adnotare
ChangeBasis, decibuildreturnează un circuit șablon ale cărui porți de schimbare a bazei sunt parametrizate, împreună cu un samplex care calculează parametrii pentru o bază solicitată. -
Adăugăm șablonul și samplex-ul într-un
QuantumProgram. Samplex-ul primește o bază de măsurare pentru fiecare qubit din casetă, în ordine fizică crescătoare, codificată ca , , , . Transmiterea unui tablou de baze cu o axă inițială de lungime100face ca un singur element să măsoare toți cei100de stabilizatori: sarcina utilă în baza fiecărui stabilizator și qubiții auxiliari în . Un qubit din afara suportului unui stabilizator este de asemenea măsurat în , iar bitul său este ignorat când se calculează valoarea de așteptare a stabilizatorului.
Primitiva Executor din qiskit-ibm-runtime rulează întregul program într-un singur job. Dacă i se dă un AerSimulator în locul unui QPU, rulează local pe acel simulator, așa că îi dăm unul cu același zgomot depolarizant al porților pe care l-am folosit pentru a găsi verificările și folosim metoda stabilizator, deoarece toate circuitele sunt Clifford. Qiskit Aer este necesar doar pentru a construi acest simulator: modul local preia seed-ul din opțiunile Executor, dar ignoră noise_model din ele, așa că zgomotul trebuie setat pe simulatorul însuși. Transmiterea în schimb a unui backend QPU ar rula același program pe hardware. După finalizarea simulărilor, vom calcula rata de postselecție și valorile de așteptare pentru a ne face o idee despre cât de bine a funcționat detectarea erorilor, înainte de a vizualiza datele.
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. Postselecția eșantioanelor circuitului verificat
În acest pachet, o verificare este implementată folosind porți de încâlcire între un qubit auxiliar și un qubit țintă. Fiecare qubit auxiliar pornește în , deci stabilizează starea sa de intrare. Propagarea înainte a lui de la începutul qubitului auxiliar prin întregul circuit verificat dă înmulțit cu un stabilizator al stării pregătite, deci verificarea trece dacă bitul auxiliar este . Un eșantion este păstrat dacă toate verificările trec.
Circuitele verificate măsoară qubiții auxiliari în registrul checks_c, deci un eșantion este păstrat dacă fiecare bit al acelui registru este 0. În general, metoda get_postselection_method a instanțelor CheckedCircuit returnate de add_pauli_checks mapează șirul de biți al unui shot la sindromul fiecărei verificări deodată; pentru verificările găsite pentru stabilizatorii stării pregătite, fiecare sindrom este doar bitul său auxiliar, așa că citim direct registrul.
În grafic vedem că adăugarea mai multor verificări face ca rata de postselecție să scadă. Deși scopul detectării erorilor este de a prinde cât mai multe erori, pe măsură ce rata de postselecție scade pot fi necesare mai multe shot-uri pentru a atinge o anumită acuratețe țintă. Dacă rata de postselecție scade prea mult, numărul de shot-uri necesare pentru a colecta un eșantion logic poate deveni prohibitiv. În acest exemplu, rata de postselecție pare să conveargă, ceea ce implică faptul că verificările suplimentare contribuie cu tot mai puțină capacitate suplimentară de detectare. Deși adăugarea mai multor verificări Pauli oferă în general o capacitate mai mare de detectare a erorilor, vine cu prețul unei adâncimi mai mari a circuitului și al unui cost de eșantionare mai mare.
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. Compararea fidelităților stărilor zgomotoase originale și postselectate
Vedem că postselecția doar a eșantioanelor fără erori detectate îmbunătățește considerabil valoarea de așteptare pentru fiecare stabilizator și, astfel, îmbunătățește fidelitatea estimată a stării stabilizator. Deși valorile postselectate au fost calculate folosind doar eșantioanele care au trecut toate verificările, valorile de așteptare sunt mult mai precise, iar varianța eșantionată este mai mică. Putem vedea acum și că rata de postselecție de mai sus converga de fapt către o valoare similară cu fidelitatea circuitului zgomotos. Rata de postselecție converga deoarece verificările prindeau marea majoritate a erorilor care apăreau efectiv în timpul execuției circuitului.
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

Deoarece un singur set de verificări servește fiecărui stabilizator, există o singură curbă gamma de urmărit: valoarea funcției de cost după fiecare verificare adăugată. Pentru alte probleme, pe un set diferit de qubiți fizici și cu un model de zgomot imperfect (ține minte, modelul nostru de zgomot din acest tutorial a modelat exact zgomotul care afectează eșantioanele), ordinea în care sunt adăugate verificările qubiților țintă poate avea un efect mai pronunțat asupra modului în care converge curba.
Această curbă gamma care converge către 1.0 implică faptul că verificările suplimentare nu vor putea detecta o cantitate mare de eroare în plus, deoarece există foarte puțină eroare modelată pe care verificările să nu o acopere. Acest lucru întărește observația noastră anterioară conform căreia convergența ratei de postselecție se datorează lipsei de eroare neacoperită în circuit.
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()