Vai al contenuto principale
Questo progetto è in fase beta. Hai trovato un bug? Apri una issue su GitHub. Hai una domanda o un'idea? Chiedi su GitHub Discussions — apprezziamo il tuo feedback!
Com'è la qualità della traduzione su questa pagina?

Rilevamento degli errori a basso overhead con codici spazio-temporali

In questo tutorial implementeremo un flusso di lavoro per campionare da un circuito Clifford e usare il pacchetto qiskit-paulice per aumentare la fidelity della distribuzione misurata applicando check di Pauli spazio-temporali lungo tutto il circuito. I check di Pauli spazio-temporali sono simili ai check di Pauli coerenti (CPC) in quanto un circuito "payload" Clifford viene entangled con alcuni qubit ausiliari per "controllare" determinati invarianti nel circuito. La misura degli ausiliari produce una sindrome usata per determinare se è stato rilevato un errore logico durante l'esecuzione del circuito. La postselezione dei soli campioni in cui non è stato rilevato alcun errore può migliorare la fidelity delle distribuzioni campionate a scapito di tassi di postselezione inferiori. La differenza chiave tra CPC e check spazio-temporali è che misurare le sindromi dei CPC richiede di misurare operatori localizzati nel tempo e di peso elevato. Su topologie di qubit con connettività limitata, come la heavy hex, implementare questi check richiede molti gate SWAP, rendendo spesso il circuito troppo profondo da eseguire in pratica. Implementare i check di Pauli come codici spazio-temporali permette di distribuirli lungo il circuito payload, consentendo una codifica dei qubit efficiente dal punto di vista hardware che rimane efficace nel rilevare errori logici durante l'esecuzione del circuito. Il ruolo principale del pacchetto qiskit-paulice è trovare e inserire check di Pauli spazio-temporali validi in posizioni del circuito che massimizzino l'efficacia del rilevamento degli errori e minimizzino l'overhead di qubit.

Questo tutorial implementa un flusso di lavoro per aumentare la fidelity della distribuzione campionata da un circuito quantistico usando check spazio-temporali. Poiché useremo un circuito Clifford, possiamo stimare la fidelity in modo semplice a partire dai valori di aspettazione medi degli stabilizzatori del circuito. Seguiremo questi passi:

  1. Creare un circuito: circuito Clifford casuale brickwork 1D poco profondo, usato per visualizzare i check.

  2. Identificare i qubit come target dei check: mappare il circuito sui qubit fisici del QPU Heron ibm_boston e identificare i qubit con ausiliari vicini.

  3. Eseguire il transpile del circuito per il backend: selezionare un layout dei qubit e tradurre i gate del circuito nel set di gate nativo di ibm_boston.

  4. Creare un modello di rumore dai dati del backend: generare un modello di rumore approssimativo basato sui dati di benchmark del backend. Questi dati di rumore verranno usati per valutare l'efficacia di ogni check aggiuntivo.

  5. Visualizzare i check di Pauli: aggiungere alcuni check di Pauli al circuito poco profondo, per poter visualizzare come vengono aggiunti.

  6. Determinare gli stabilizzatori da misurare: aumentare la profondità del circuito per accrescere l'impatto del rumore dei gate. Campionare casualmente un sottoinsieme di stabilizzatori del circuito più profondo.

  7. Trovare buoni check per l'intero stato: trovare un unico insieme di check per gli stabilizzatori dello stato che il circuito prepara. Tali check sono validi qualunque cosa venga misurata in seguito, quindi lo stesso circuito con check serve per ogni stabilizzatore campionato.

  8. Campionare dal circuito rumoroso: misurare i circuiti senza check e con check nella base di ciascuno stabilizzatore campionato con samplomatic, e campionarli con la primitive Executor su un simulatore rumoroso che usa lo stesso rumore depolarizzante usato per trovare i check.

  9. Postselezionare i campioni del circuito con check senza errori rilevati: rimuovere i campioni che falliscono almeno un check. Calcolare i valori di aspettazione per il circuito rumoroso senza check e per il circuito che contiene i check.

  10. Confrontare le fidelity degli stati rumorosi originale e postselezionato: mostrare che la postselezione dei soli campioni senza errori rilevati migliora la fidelity stimata dello stato rumoroso.

1. Creare il circuito​

Per prima cosa, creeremo un circuito Clifford poco profondo con i gate di entanglement disposti in una connettività brickwork. Useremo questo piccolo circuito per visualizzare come vengono aggiunti i check al circuito.

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

Output of the previous code cell

2. Identificare i qubit come target dei check​

Ora scegliamo un layout 1D di qubit sul QPU Heron r3, ibm_boston. Scegliamo un layout che procede a zig-zag in diagonale attraverso il grafo di accoppiamento e massimizza il numero di qubit ausiliari disponibili. Per trovare gli ausiliari disponibili, usiamo la funzione get_check_qubits, che restituisce liste di coppie di qubit target/ausiliario. Un check di Pauli su target_qubits[i] verrà implementato usando ancilla_qubits[i].

Nel grafo di accoppiamento seguente, i qubit verdi sono i qubit payload e i qubit arancioni sono gli ausiliari che useremo per implementare i check. I qubit 68, 69, 89, 91, 111, 113 e 133 hanno ausiliari adiacenti, quindi useremo quei qubit (corrispondenti ai qubit virtuali 0, 1, 3, 5, 7, 9 e 11) come qubit target per implementare i check. Nota che il qubit 134 è adiacente al qubit 133 nel payload ma non è evidenziato come ausiliario. Questo perché 133 era già stato scelto (arbitrariamente) per implementare un check con l'ausiliario 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]

Output of the previous code cell

3. Eseguire il transpile del circuito per il backend​

Poiché il backend è già stato scelto, possiamo procedere al transpile del nostro circuito virtuale in un circuito ISA. Qui impostiamo semplicemente il layout dei qubit e traduciamo i gate nel set di gate nativo di 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)

Output of the previous code cell

4. Creare un modello di rumore dai dati del backend​

Ora che sappiamo come il circuito sarà disposto sul QPU, possiamo modellare come il rumore dei gate e di lettura sul backend influenzerà l'esecuzione del circuito. Il modello di rumore viene usato per determinare le posizioni ottimali nel circuito in cui collocare i check di Pauli. Vogliamo collocare i check nelle posizioni in cui catturano la massima quantità di errore. Sebbene un modello di rumore più accurato possa migliorare la capacità di rilevamento dei check, di solito non è necessario apprendere un modello di rumore campionando il QPU. Qui deduciamo un modello di rumore uniforme di gate depolarizzante usando solo i dati di benchmark del backend di qiskit-ibm-runtime, e scartiamo l'errore di lettura che esso riporta: i check che troviamo nel passo 7 leggono solo il proprio ausiliario, quindi gli errori di lettura del payload sono invisibili per loro e abbasserebbero soltanto la fidelity postselezionata nel passo 10. Consulta la documentazione di qiskit_paulice.NoiseModel per maggiori informazioni su come specificare il rumore dei gate e di lettura. Per chi desidera usare un modello di rumore di Pauli-Lindblad appreso, vedi 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. Visualizzare i check di Pauli​

Ora aggiungiamo alcuni check al circuito e vediamo come appaiono. Possiamo usare la funzione qiskit_paulice.add_pauli_checks per trovare un buon insieme di check validi. Forniamo in input il circuito payload Clifford, insieme a una lista di qubit payload su cui aggiungere i check (qubit target) e al modello di rumore creato nel passo precedente. In questo esempio, aggiungeremo check a tutti i 7 qubit target con ausiliari disponibili. I check vengono aggiunti ai qubit nell'ordine in cui compaiono in target_qubits, il che rende facile ragionare sul layout finale del circuito con check: final_layout = layout + ancilla_qubits. Se si decide di eseguire uno dei circuiti di output con meno check (i), il layout finale sarebbe final_layout = layout + ancilla_qubits[:i+1].

La visualizzazione del circuito mostra che i check sono stati implementati correttamente usando le coppie target/ausiliario specificate.

Dettagli su qiskit_paulice.add_pauli_checks:

  • Il primo input deve essere un circuito Clifford. Qui non ha misure e passiamo stabilizers="all", quindi i check vengono trovati per gli stabilizzatori dello stato che il circuito prepara: un check è valido se il suo prodotto di Pauli spazio-temporale misura uno stabilizzatore di questo tipo, la sua sindrome è il solo bit del suo ausiliario, e rimane valido qualunque sia la base in cui il payload viene misurato in seguito. Questo è ciò che serve per la stima della fidelity, poiché ogni stabilizzatore campionato viene misurato in una base diversa. (Omettendo stabilizers si trovano invece check per le misure terminali di un circuito.)
  • Il secondo argomento posizionale è una lista di indici di qubit target. Ogni qubit target condividerà gate di entanglement con un qubit ausiliario per formare un check. I check verranno trovati e aggiunti al circuito nell'ordine in cui sono indicati in questo argomento.
  • L'ultimo argomento posizionale è un modello di rumore che rappresenta il rumore previsto durante l'esecuzione del circuito. Questo modello viene usato per valutare l'efficacia di un dato check.
  • ancilla_qubits: se il circuito di input ha un layout, questo kwarg indica ad add_pauli_checks come assegnare i qubit fisici agli ausiliari appena aggiunti. ancilla_qubits deve avere la stessa lunghezza di target_qubits (2º argomento posizionale), e ancilla_qubits[i] deve essere usato per implementare i check su target_qubits[i].
  • cost: la funzione di costo usata per valutare i check. Può essere gamma o LER (logical error rate, tasso di errore logico), che sono modi per descrivere quanta parte del canale di rumore rimane non rilevata dai check. Con stabilizers="all" un errore conta come logico se modifica lo stato preparato, che è esattamente ciò che la fidelity misura.
  • method: il metodo usato per trovare check validi. Usiamo windowed, che campiona casualmente molti sottoinsiemi di finestre spaziali del supporto (wire) di un qubit target. Altri valori validi sono genetic e windowed_genetic.
  • L'output di add_pauli_checks contiene circuiti con numeri crescenti di check, da un circuito senza check a un circuito con check su ogni qubit target (supponendo che vengano trovati check validi).

Vedi le Sezioni II-IV delle informazioni supplementari per informazioni dettagliate su come trovare buoni check di Pauli spazio-temporali

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:

Output of the previous code cell

6. Determinare gli stabilizzatori da misurare​

Per testare le prestazioni del rilevamento degli errori stimeremo la fidelity dello stato stabilizzatore, ∣ψ⟩=U∣0⟩⊗n|\psi\rangle = U|0\rangle^{\otimes n}, che il circuito prepara idealmente rispetto allo stato rumoroso, ρ\rho, che l'hardware produce effettivamente. Il proiettore sullo stato stabilizzatore puro, ∣ψ⟩|\psi\rangle, è definito come la media uniforme sui 2n2^n elementi del suo gruppo stabilizzatore S\mathcal{S}:

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

Inserendo questo nell'equazione della fidelity, vediamo che la fidelity dello stato rumoroso, ρ\rho, equivale al valore di aspettazione medio di tutti gli stabilizzatori, G∈SG \in \mathcal{S}, rispetto a ρ\rho:

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

Per problemi più grandi, misurare (o anche solo enumerare) tutti i 2n2^n stabilizzatori è impraticabile, quindi la fidelity viene spesso stimata misurando un campione casuale di stabilizzatori. Campionare MM stabilizzatori G1,…,GMG_1, \ldots, G_M uniformemente a caso da S\mathcal{S} fornisce uno stimatore non distorto della fidelity, F^M\hat F_M,

F^M=1M∑i=1M⟨Gi⟩ρ\hat F_M = \frac{1}{M} \sum_{i=1}^M \langle G_i \rangle_\rho Per questo esperimento renderemo il circuito molto più profondo per accentuare l'effetto del rumore dei gate, e aggiungeremo check a ogni qubit con un qubit ausiliario vicino. Sebbene esistano modi più sofisticati per campionare gli stabilizzatori e stimare la fidelity, in questo esempio campioneremo 100100 stabilizzatori con probabilità uniforme.

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. Trovare buoni check per l'intero stato​

Troviamo un unico insieme di check per il circuito profondo esattamente come nel passo 5. Poiché i check leggono solo i propri ausiliari, lo stesso circuito con check serve per ogni stabilizzatore campionato. I circuiti restituiti misurano solo gli ausiliari dei check, quindi aggiungeremo le misure del payload nel passo successivo.

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. Campionare dal circuito rumoroso​

Campioniamo ogni variante, dal payload senza check al circuito con tutti i 7 check, per ogni stabilizzatore campionato. La variante con zero check è il payload senza check con le stesse misure, quindi serve come riferimento rumoroso. Invece di aggiungere la rotazione di base di ogni stabilizzatore a una copia di ogni variante, lasciamo che samplomatic scelga la base di misura al momento del campionamento. Per ogni variante:

  • Misuriamo il payload e raggruppiamo le misure terminali in un box con il boxing pass manager di samplomatic. Il box porta un'annotazione ChangeBasis, quindi build restituisce un circuito template i cui gate di cambio di base sono parametrizzati, insieme a un samplex che calcola i parametri per una base richiesta.

  • Aggiungiamo il template e il samplex a un QuantumProgram. Il samplex accetta una base di misura per ogni qubit nel box, in ordine fisico crescente, codificata come I=0I=0, Z=1Z=1, X=2X=2, Y=3Y=3. Passare un array di basi con un asse iniziale di lunghezza 100 fa sì che un singolo item misuri tutti i 100 stabilizzatori: il payload nella base di ciascuno stabilizzatore e gli ausiliari in ZZ. Anche un qubit al di fuori del supporto di uno stabilizzatore viene misurato in ZZ, e il suo bit viene ignorato quando si calcola il valore di aspettazione dello stabilizzatore.

La primitive Executor di qiskit-ibm-runtime esegue l'intero programma in un unico job. Se le viene fornito un AerSimulator al posto di un QPU, viene eseguita in locale su quel simulatore, quindi gliene passiamo uno con lo stesso rumore depolarizzante dei gate che abbiamo usato per trovare i check, e usiamo il metodo stabilizer poiché tutti i circuiti sono Clifford. Qiskit Aer serve solo per costruire questo simulatore: la modalità locale prende il seed dalle opzioni di Executor ma ignora il loro noise_model, quindi il rumore deve essere impostato sul simulatore stesso. Passando invece un backend QPU, lo stesso programma verrebbe eseguito su hardware. Una volta completate le simulazioni, calcoleremo il tasso di postselezione e i valori di aspettazione per farci un'idea di quanto abbia funzionato bene il rilevamento degli errori prima di visualizzare i dati.

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. Postselezionare i campioni del circuito con check​

In questo pacchetto un check viene implementato usando gate di entanglement tra un qubit ausiliario e un qubit target. Ogni ausiliario parte in ∣0⟩|0\rangle, quindi ZancZ_\text{anc} stabilizza il suo stato di input. Propagando in avanti ZancZ_\text{anc} dall'inizio dell'ausiliario attraverso l'intero circuito con check si ottiene ZancZ_\text{anc} per uno stabilizzatore dello stato preparato, quindi il check è superato se il bit dell'ausiliario è 00. Un campione viene mantenuto se ogni check è superato.

I circuiti con check misurano gli ausiliari nel registro checks_c, quindi un campione viene mantenuto se ogni bit di quel registro è 0. In generale, il metodo get_postselection_method delle istanze di CheckedCircuit restituite da add_pauli_checks mappa la bitstring di uno shot alla sindrome di ogni check in una volta; per i check trovati per gli stabilizzatori dello stato preparato, ogni sindrome è semplicemente il bit del proprio ausiliario, quindi leggiamo direttamente il registro.

Nel grafico vediamo che aggiungere più check fa diminuire il tasso di postselezione. Sebbene l'obiettivo del rilevamento degli errori sia catturare il maggior numero possibile di errori, al diminuire del tasso di postselezione possono essere necessari più shot per raggiungere una certa accuratezza target. Se il tasso di postselezione scende troppo, il numero di shot necessari per raccogliere un campione logico può diventare proibitivo. In questo esempio, il tasso di postselezione sembra convergere, il che implica che i check aggiuntivi contribuiscono con una capacità di rilevamento aggiuntiva sempre minore. Sebbene aggiungere più check di Pauli offra in genere una maggiore capacità di rilevamento degli errori, ciò comporta un costo in termini di maggiore profondità del circuito e di maggiore costo di campionamento.

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()

Output of the previous code cell

10. Confrontare le fidelity degli stati rumorosi originale e postselezionato​

Vediamo che la postselezione dei soli campioni senza errori rilevati migliora notevolmente il valore di aspettazione di ogni stabilizzatore, e quindi migliora la fidelity stimata dello stato stabilizzatore. Sebbene i valori postselezionati siano stati calcolati usando solo i campioni che hanno superato ogni check, i valori di aspettazione sono molto più accurati e la varianza campionaria è inferiore. Possiamo anche vedere che il tasso di postselezione visto sopra stava in realtà convergendo a un valore simile alla fidelity del circuito rumoroso. Il tasso di postselezione stava convergendo perché i check catturavano la grande maggioranza degli errori che si verificano effettivamente durante l'esecuzione del circuito.

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

Output of the previous code cell

Output of the previous code cell

Poiché un unico insieme di check serve per ogni stabilizzatore, c'è una sola curva gamma da osservare: il valore della funzione di costo dopo che ciascun check è stato consolidato. Per altri problemi su un diverso insieme di qubit fisici, e con un modello di rumore imperfetto (ricorda, il nostro modello di rumore in questo tutorial modellava esattamente il rumore che influenza i campioni), l'ordine in cui i check vengono aggiunti ai qubit target può avere un effetto più marcato su come la curva converge.

Il fatto che questa curva gamma converga a 1.0 implica che i check aggiuntivi non saranno in grado di rilevare una grande quantità di errore in più, poiché c'è pochissimo errore modellato che non sia coperto dai check. Questo rafforza la nostra osservazione precedente secondo cui la convergenza del tasso di postselezione è dovuta alla mancanza di errore non coperto nel circuito.

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()

Output of the previous code cell