Aller au contenu principal
Ce projet est en phase bêta. Tu as trouvé un bug ? Ouvrir un ticket GitHub. Une question ou une idée ? Pose ta question dans Discussions — tes retours sont les bienvenus !
Quelle est la qualité de la traduction sur cette page ?

Détection d'erreurs à faible surcoût avec des codes espace-temps

Dans ce tutoriel, nous allons implémenter un flux de travail pour échantillonner un Circuit de Clifford et utiliser le paquet qiskit-paulice afin d'améliorer la fidélité de la distribution mesurée en appliquant des vérifications de Pauli espace-temps tout au long du Circuit. Les vérifications de Pauli espace-temps sont similaires aux vérifications de Pauli cohérentes (CPC) en ce qu'un Circuit « charge utile » de Clifford est intriqué avec des Qubits ancillaires afin de « vérifier » certains invariants du Circuit. La mesure des ancillas produit un syndrome qui sert à déterminer si une erreur logique a été détectée pendant l'exécution du Circuit. Post-sélectionner uniquement les échantillons où aucune erreur n'a été détectée peut améliorer la fidélité des distributions échantillonnées, au prix d'une baisse des taux de post-sélection. La différence essentielle entre les CPC et les vérifications espace-temps est que la mesure des syndromes de CPC exige de mesurer des opérateurs de poids élevé, localisés dans le temps. Sur des topologies de Qubits à connectivité limitée, comme heavy hex, l'implémentation de ces vérifications nécessite de nombreuses Gates SWAP, ce qui rend souvent le Circuit trop profond pour être exécuté en pratique. Implémenter les vérifications de Pauli sous forme de codes espace-temps permet de les répartir dans tout le Circuit de charge utile, ce qui autorise un encodage de Qubits efficace pour le matériel et qui reste efficace pour détecter les erreurs logiques pendant l'exécution du Circuit. Le rôle principal du paquet qiskit-paulice est de trouver et d'insérer des vérifications de Pauli espace-temps valides à des emplacements du Circuit qui maximisent l'efficacité de détection d'erreurs tout en minimisant le surcoût en Qubits.

Ce tutoriel implémente un flux de travail pour améliorer la fidélité de la distribution échantillonnée à partir d'un Circuit quantique à l'aide de vérifications espace-temps. Comme nous utiliserons un Circuit de Clifford, nous pouvons estimer directement la fidélité à partir des valeurs moyennes, moyennées, des stabilisateurs du Circuit. Nous suivrons ces étapes :

  1. Créer un Circuit : Circuit de Clifford aléatoire peu profond, en briques 1D, utilisé pour visualiser les vérifications.

  2. Identifier les Qubits cibles des vérifications : Associer le Circuit aux Qubits physiques du QPU Heron ibm_boston et identifier les Qubits ayant des ancillas voisines.

  3. Transpiler le Circuit pour le Backend : Sélectionner une disposition de Qubits et traduire les Gates du Circuit dans le jeu de Gates natif de ibm_boston.

  4. Créer un modèle de bruit à partir des données du Backend : Générer un modèle de bruit approximatif à partir des données de benchmark du Backend. Ces données de bruit serviront à évaluer l'efficacité de chaque vérification supplémentaire.

  5. Visualiser les vérifications de Pauli : Ajouter quelques vérifications de Pauli au Circuit peu profond afin de visualiser comment elles sont ajoutées.

  6. Déterminer les stabilisateurs à mesurer : Augmenter la profondeur du Circuit pour accroître l'impact du bruit des Gates. Échantillonner aléatoirement un sous-ensemble de stabilisateurs du Circuit plus profond.

  7. Trouver de bonnes vérifications pour l'état entier : Trouver un ensemble de vérifications pour les stabilisateurs de l'état que prépare le Circuit. Ces vérifications sont valides quelle que soit la mesure effectuée ensuite, de sorte que le même Circuit vérifié sert pour chaque stabilisateur échantillonné.

  8. Échantillonner à partir du Circuit bruité : Mesurer les Circuits nu et vérifié dans la base de chaque stabilisateur échantillonné avec samplomatic, et les échantillonner avec la primitive Executor sur un simulateur bruité qui utilise le même bruit dépolarisant que celui employé pour trouver les vérifications.

  9. Post-sélectionner les échantillons du Circuit vérifié sans erreur détectée : Retirer les échantillons qui échouent à au moins une vérification. Calculer les valeurs moyennes pour le Circuit bruité nu et pour le Circuit contenant des vérifications.

  10. Comparer les fidélités des états bruités d'origine et post-sélectionnés : Montrer que post-sélectionner uniquement les échantillons sans erreur détectée améliore la fidélité estimée de l'état bruité.

1. Créer le Circuit​

Tout d'abord, nous allons créer un Circuit de Clifford peu profond dont les Gates intriquantes sont disposées selon une connectivité en briques. Nous utiliserons ce petit Circuit pour visualiser comment les vérifications sont ajoutées au 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)

Output of the previous code cell

2. Identifier les Qubits cibles des vérifications​

Ensuite, nous choisissons une disposition 1D de Qubits sur le QPU Heron r3, ibm_boston. Nous choisissons une disposition qui zigzague en diagonale à travers le graphe de couplage et qui maximise le nombre de Qubits ancillaires disponibles. Pour trouver les ancillas disponibles, nous utilisons la fonction get_check_qubits, qui renvoie des listes de paires de Qubits cible/ancilla. Une vérification de Pauli sur target_qubits[i] sera implémentée avec ancilla_qubits[i].

Dans le graphe de couplage ci-dessous, les Qubits verts sont les Qubits de charge utile, et les Qubits orange sont les ancillas que nous utiliserons pour implémenter les vérifications. Les Qubits 68, 69, 89, 91, 111, 113 et 133 ont des ancillas adjacentes ; nous utiliserons donc ces Qubits (correspondant aux Qubits virtuels 0, 1, 3, 5, 7, 9 et 11) comme Qubits cibles pour implémenter les vérifications. Remarque : le Qubit 134 est adjacent au Qubit 133 dans la charge utile mais n'est pas mis en évidence comme ancilla. C'est parce que 133 a déjà été choisi (arbitrairement) pour implémenter une vérification avec l'ancilla 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. Transpiler le Circuit pour le Backend​

Puisque le Backend a déjà été choisi, nous pouvons transpiler notre Circuit virtuel en un Circuit ISA. Ici, nous définissons simplement la disposition des Qubits et traduisons les Gates dans le jeu de Gates natif de 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. Créer un modèle de bruit à partir des données du Backend​

Maintenant que nous savons comment le Circuit sera disposé sur le QPU, nous pouvons modéliser la façon dont le bruit des Gates et de lecture du Backend affectera l'exécution du Circuit. Le modèle de bruit sert à déterminer les emplacements optimaux du Circuit où placer les vérifications de Pauli. Nous voulons placer les vérifications aux positions où elles capturent le maximum d'erreur. Bien qu'un modèle de bruit plus précis puisse améliorer les capacités de détection des vérifications, il n'est généralement pas nécessaire d'apprendre un modèle de bruit en échantillonnant le QPU. Ici, nous inférons un modèle de bruit de Gates dépolarisant uniforme en utilisant uniquement les données de benchmark du Backend issues de qiskit-ibm-runtime, et nous ignorons l'erreur de lecture qu'il rapporte également : les vérifications que nous trouvons à l'étape 7 ne lisent que leur ancilla, donc les erreurs de lecture de la charge utile leur sont invisibles et ne feraient qu'abaisser la fidélité post-sélectionnée à l'étape 10. Consulte la documentation de qiskit_paulice.NoiseModel pour plus d'informations sur la spécification du bruit des Gates et de lecture. Pour les utilisateurs qui souhaitent utiliser un modèle de bruit de Pauli-Lindblad appris, voir 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. Visualiser les vérifications de Pauli​

Ajoutons maintenant quelques vérifications au Circuit et voyons à quoi elles ressemblent. Nous pouvons utiliser la fonction qiskit_paulice.add_pauli_checks pour trouver un bon ensemble de vérifications valides. Nous fournissons en entrée le Circuit de charge utile de Clifford, ainsi qu'une liste de Qubits de charge utile sur lesquels ajouter des vérifications (Qubits cibles) et le modèle de bruit créé à l'étape précédente. Dans cet exemple, nous ajouterons des vérifications sur les 7 Qubits cibles disposant d'ancillas. Les vérifications sont ajoutées aux Qubits dans l'ordre où ils apparaissent dans target_qubits, ce qui permet de raisonner facilement sur la disposition finale du Circuit vérifié : final_layout = layout + ancilla_qubits. Si un utilisateur décide de préférer exécuter l'un des Circuits de sortie avec moins de vérifications (i), la disposition finale serait final_layout = layout + ancilla_qubits[:i+1].

La visualisation du Circuit montre que les vérifications ont été correctement implémentées avec les paires cible/ancilla spécifiées.

Détails sur qiskit_paulice.add_pauli_checks :

  • La première entrée doit être un Circuit de Clifford. Ici, il n'a pas de mesures et nous passons stabilizers="all", donc les vérifications sont trouvées pour les stabilisateurs de l'état que prépare le Circuit : une vérification est valide si son produit de Pauli espace-temps mesure un tel stabilisateur, si son syndrome est son seul bit d'ancilla, et si elle reste valide quelle que soit la base dans laquelle la charge utile est mesurée ensuite. C'est ce dont une estimation de fidélité a besoin, puisque chaque stabilisateur échantillonné est mesuré dans une base différente. (Omettre stabilizers permet au contraire de trouver des vérifications pour les mesures terminales d'un Circuit.)
  • Le deuxième argument positionnel est une liste d'indices de Qubits cibles. Chaque Qubit cible partagera des Gates intriquantes avec un Qubit ancilla pour former une vérification. Les vérifications seront trouvées et ajoutées au Circuit dans l'ordre où elles sont données dans cet argument.
  • Le dernier argument positionnel est un modèle de bruit représentant le bruit attendu pendant l'exécution du Circuit. Ce modèle sert à évaluer l'efficacité d'une vérification donnée.
  • ancilla_qubits : si le Circuit d'entrée possède une disposition, cet argument nommé indique à add_pauli_checks comment attribuer des Qubits physiques aux ancillas nouvellement ajoutées. ancilla_qubits doit avoir la même longueur que target_qubits (2e argument positionnel), et ancilla_qubits[i] doit servir à implémenter les vérifications sur target_qubits[i].
  • cost : la fonction de coût utilisée pour évaluer les vérifications. Peut être gamma ou LER (taux d'erreur logique), qui sont toutes des façons de décrire quelle part du canal de bruit reste non détectée par les vérifications. Avec stabilizers="all", une erreur est considérée comme logique si elle modifie l'état préparé, ce que mesure précisément la fidélité.
  • method : la méthode utilisée pour trouver des vérifications valides. Nous utilisons windowed, qui échantillonne aléatoirement de nombreux sous-ensembles en fenêtres spatiales du support (fils) d'un Qubit cible. Les autres valeurs valides sont genetic et windowed_genetic.
  • La sortie de add_pauli_checks contient des Circuits avec un nombre croissant de vérifications, d'un Circuit sans vérification à un Circuit avec des vérifications sur chaque Qubit cible (en supposant que des vérifications valides soient trouvées).

Voir les sections II à IV des informations supplémentaires pour des informations détaillées sur la recherche de bonnes vérifications de Pauli espace-temps

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. Déterminer les stabilisateurs à mesurer​

Pour tester les performances de la détection d'erreurs, nous allons estimer la fidélité de l'état stabilisateur, ∣ψ⟩=U∣0⟩⊗n|\psi\rangle = U|0\rangle^{\otimes n}, que le Circuit prépare idéalement, par rapport à l'état bruité, ρ\rho, que le matériel produit réellement. Le projecteur sur l'état stabilisateur pur, ∣ψ⟩|\psi\rangle, est défini comme la moyenne uniforme sur les 2n2^n éléments de son groupe stabilisateur S\mathcal{S} :

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

En injectant cela dans l'équation de la fidélité, nous voyons que la fidélité de l'état bruité, ρ\rho, est équivalente à la valeur moyenne, sur tous les stabilisateurs, G∈SG \in \mathcal{S}, de leurs valeurs moyennes par rapport à ρ\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

Pour des problèmes plus grands, mesurer (ou même énumérer) les 2n2^n stabilisateurs est irréalisable ; la fidélité est donc souvent estimée en mesurant un échantillon aléatoire de stabilisateurs. Échantillonner uniformément au hasard MM stabilisateurs G1,…,GMG_1, \ldots, G_M dans S\mathcal{S} donne un estimateur non biaisé de la fidélité, 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 Pour cette expérience, nous allons rendre le Circuit beaucoup plus profond afin d'amplifier l'effet du bruit des Gates, et nous ajouterons des vérifications sur chaque Qubit ayant un Qubit ancilla voisin. Bien qu'il existe des façons plus sophistiquées d'échantillonner les stabilisateurs pour estimer la fidélité, dans cet exemple nous échantillonnerons 100100 stabilisateurs avec une 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. Trouver de bonnes vérifications pour l'état entier​

Nous trouvons un ensemble de vérifications pour le Circuit profond exactement comme à l'étape 5. Comme les vérifications ne lisent que leurs ancillas, le même Circuit vérifié sert pour chaque stabilisateur échantillonné. Les Circuits renvoyés ne mesurent que les ancillas de vérification ; nous ajouterons donc les mesures de la charge utile à l'étape suivante.

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. Échantillonner à partir du Circuit bruité​

Nous échantillonnons chaque variante, de la charge utile nue au Circuit avec les 7 vérifications, pour chaque stabilisateur échantillonné. La variante sans vérification est la charge utile nue avec les mêmes mesures, et sert donc de référence bruitée. Au lieu d'ajouter la rotation de base de chaque stabilisateur à une copie de chaque variante, nous laissons samplomatic choisir la base de mesure au moment de l'échantillonnage. Pour chaque variante, nous :

  • Mesurons la charge utile et regroupons les mesures terminales dans une boîte avec la passe de boxing de samplomatic. La boîte porte une annotation ChangeBasis, de sorte que build renvoie un Circuit modèle dont les Gates de changement de base sont paramétrées, ainsi qu'un samplex qui calcule les paramètres pour une base demandée.

  • Ajoutons le modèle et le samplex à un QuantumProgram. Le samplex prend une base de mesure par Qubit de la boîte, dans l'ordre physique croissant, encodée par I=0I=0, Z=1Z=1, X=2X=2, Y=3Y=3. Passer un tableau de bases avec un premier axe de longueur 100 fait qu'un seul élément mesure les 100 stabilisateurs : la charge utile dans la base de chaque stabilisateur et les ancillas en ZZ. Un Qubit hors du support d'un stabilisateur est aussi mesuré en ZZ, et son bit est ignoré lors du calcul de la valeur moyenne du stabilisateur.

La primitive Executor de qiskit-ibm-runtime exécute l'ensemble du programme en un seul job. Si on lui donne un AerSimulator à la place d'un QPU, elle s'exécute localement sur ce simulateur ; nous lui en fournissons donc un avec le même bruit de Gates dépolarisant que celui utilisé pour trouver les vérifications, et nous utilisons la méthode stabilizer puisque tous les Circuits sont de Clifford. Qiskit Aer n'est nécessaire que pour construire ce simulateur : le mode local prend la graine depuis les options de Executor mais ignore leur noise_model, donc le bruit doit être défini sur le simulateur lui-même. Passer plutôt un Backend QPU exécuterait le même programme sur le matériel. Une fois les simulations terminées, nous calculerons le taux de post-sélection et les valeurs moyennes pour avoir une idée de l'efficacité de la détection d'erreurs avant de visualiser les données.

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. Post-sélectionner les échantillons du Circuit vérifié​

Dans ce paquet, une vérification est implémentée à l'aide de Gates intriquantes entre un Qubit ancilla et un Qubit cible. Chaque ancilla démarre dans ∣0⟩|0\rangle, donc ZancZ_\text{anc} stabilise son état d'entrée. Propager ZancZ_\text{anc} vers l'avant, depuis le début de l'ancilla à travers tout le Circuit vérifié, donne ZancZ_\text{anc} multiplié par un stabilisateur de l'état préparé ; la vérification est donc réussie si le bit de l'ancilla vaut 00. Un échantillon est conservé si toutes les vérifications réussissent.

Les Circuits vérifiés mesurent les ancillas dans le registre checks_c, donc un échantillon est conservé si chaque bit de ce registre vaut 0. En général, la méthode get_postselection_method des instances CheckedCircuit renvoyées par add_pauli_checks associe la chaîne de bits d'un shot au syndrome de chaque vérification à la fois ; pour les vérifications trouvées pour les stabilisateurs de l'état préparé, chaque syndrome est simplement son bit d'ancilla, donc nous lisons directement le registre.

Dans le graphique, nous voyons que l'ajout de vérifications fait baisser le taux de post-sélection. Si le but de la détection d'erreurs est d'attraper autant d'erreurs que possible, à mesure que le taux de post-sélection diminue, il peut falloir davantage de shots pour atteindre une précision cible. Si le taux de post-sélection devient trop bas, le nombre de shots nécessaires pour collecter un échantillon logique peut devenir prohibitif. Dans cet exemple, le taux de post-sélection semble converger, ce qui implique que les vérifications supplémentaires apportent de moins en moins de capacité de détection additionnelle. Bien qu'ajouter davantage de vérifications de Pauli offre généralement une meilleure capacité de détection d'erreurs, cela se paie par une profondeur de Circuit accrue et un coût d'échantillonnage plus élevé.

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. Comparer les fidélités des états bruités d'origine et post-sélectionnés​

Nous voyons que post-sélectionner uniquement les échantillons sans erreur détectée améliore fortement la valeur moyenne de chaque stabilisateur, et améliore donc la fidélité estimée de l'état stabilisateur. Bien que les valeurs post-sélectionnées aient été calculées uniquement à partir des échantillons ayant réussi toutes les vérifications, les valeurs moyennes sont beaucoup plus précises et la variance échantillonnée est plus faible. Nous pouvons aussi constater maintenant que le taux de post-sélection ci-dessus convergeait en fait vers une valeur proche de la fidélité du Circuit bruité. Le taux de post-sélection convergeait parce que les vérifications attrapaient la grande majorité des erreurs se produisant réellement pendant l'exécution du Circuit.

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

Comme un seul ensemble de vérifications sert pour chaque stabilisateur, il n'y a qu'une seule courbe gamma à examiner : la valeur de la fonction de coût après la validation de chaque vérification. Pour d'autres problèmes, sur un autre ensemble de Qubits physiques, et avec un modèle de bruit imparfait (rappelle-toi que, dans ce tutoriel, notre modèle de bruit modélisait exactement le bruit affectant les échantillons), l'ordre dans lequel les vérifications sont ajoutées aux Qubits cibles peut avoir un effet plus marqué sur la façon dont la courbe converge.

Cette courbe gamma qui converge vers 1.0 implique que des vérifications supplémentaires ne pourront pas détecter beaucoup plus d'erreur, car il reste très peu d'erreur modélisée non couverte par les vérifications. Cela renforce notre observation précédente selon laquelle la convergence du taux de post-sélection est due à l'absence d'erreur non couverte dans le 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()

Output of the previous code cell