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 ?

Codes de répétition

Estimation d'utilisation : moins de 10 secondes sur un processeur Heron (REMARQUE : il s'agit uniquement d'une estimation. Votre temps d'exécution peut varier.)

Résultats d'apprentissage​

  • Comment implémenter un code de correction d'erreur bit-flip à l'aide de circuits dynamiques

  • Comment mesurer des stabilisateurs pour détecter des erreurs quantiques sans détruire l'information encodée

  • Comment évaluer la performance de la correction d'erreur quantique en comparant les résultats corrigés et non corrigés

Prérequis​

Contexte​

Pour permettre la correction d'erreur quantique (QEC) en temps réel, tu dois être en mesure de contrôler dynamiquement le flux d'un programme quantique pendant son exécution, afin que les portes quantiques puissent être conditionnées par les résultats de mesure. Ce tutoriel exécute le code de retournement de bit (bit-flip code), qui est une forme très simple de QEC. Il présente un circuit quantique dynamique capable de protéger un qubit encodé contre une erreur de retournement de bit unique, puis évalue les performances du code de retournement de bit.

Tu peux exploiter des qubits ancillaires supplémentaires et l'intrication pour mesurer des stabilisateurs qui ne transforment pas l'information quantique encodée, tout en t'informant de certaines classes d'erreurs susceptibles de s'être produites. Un code stabilisateur quantique encode kk qubits logiques dans nn qubits physiques. Les codes stabilisateurs visent avant tout à corriger un ensemble discret d'erreurs dont le support est dans le groupe de Pauli Πn\Pi^n.

Dans ce tutoriel, nous démontrons le code de retournement de bit à l'aide d'une expérience simple de mémoire quantique. Nous allons préparer le qubit encodé dans l'état logique ∣1ˉ⟩≡∣111⟩|\bar{1}\rangle \equiv |111\rangle, puis mettre en œuvre plusieurs cycles de temps d'inactivité (pour accumuler des erreurs) suivis de détection et de correction d'erreur. Nous quantifions ensuite la probabilité d'erreur logique en fonction du nombre de cycles (temps), où la probabilité d'erreur logique est la probabilité de trouver les qubits dans un état qui ne récupère pas ∣1ˉ⟩|\bar{1}\rangle après la mesure finale (concrètement, les états correspondant à une erreur sont ∣000⟩|000\rangle, ∣001⟩|001\rangle, ∣010⟩|010\rangle et ∣100⟩|100\rangle).

Nous allons comparer le taux d'erreur en fonction du temps aux taux d'erreur de qubits individuels non encodés, et également à l'utilisation du code de répétition où nous ne détectons et ne corrigeons les erreurs qu'après la mesure finale, mais pas dynamiquement pendant le circuit.

Remarque : le code de répétition ne permet que de corriger les erreurs de retournement de bit et n'est donc pas un code de correction d'erreur complet. Cependant, en raison de sa simplicité, c'est un bon point de départ pour illustrer comment implémenter la correction d'erreur sur un ordinateur quantique. L'expérience de mémoire ci-dessous ne teste qu'un seul type d'erreur (une décroissance de ∣1ˉ⟩|\bar{1}\rangle vers ∣0ˉ⟩|\bar{0}\rangle) et démontre techniquement uniquement la protection de l'information classique.

Prérequis​

Avant de commencer ce tutoriel, assure-toi d'avoir installé les éléments suivants :

  • Qiskit SDK v2.0 ou ultérieur, avec le support de la visualisation

  • Qiskit Runtime v0.40 ou ultérieur (pip install qiskit-ibm-runtime)

Configuration​

# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util

_needed = {"matplotlib": "matplotlib", "numpy": "numpy", "qiskit": "qiskit", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "scipy": "scipy", "seaborn": "seaborn"}
_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
import seaborn as sns
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit

sns.set()
plt.rc("xtick", labelsize=20)
plt.rc("ytick", labelsize=20)
plt.rc("lines", linewidth=3)
plt.rc("font", size=20)
plt.rc("legend", fontsize="large")
plt.rc("axes", labelsize=20)
plt.rcParams["figure.figsize"] = 15, 6
plt.rcParams["legend.title_fontsize"] = 25
# Qiskit imports
from qiskit import (
QuantumCircuit,
QuantumRegister,
ClassicalRegister,
)

# qiskit-ibm-runtime
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler
from qiskit_ibm_runtime.circuit import MidCircuitMeasure

service = QiskitRuntimeService()

Exemple de simulateur à petite échelle​

Nous allons renoncer à cette étape puisque l'objectif de cette expérience est de mesurer la durée de vie d'un qubit logique sous le bruit réel du matériel, comme l'amortissement d'amplitude pendant les délais d'inactivité ; un simulateur sans bruit ne montrerait aucune erreur à détecter et à corriger.

Exemple de matériel à grande échelle​

Étape 1 : Transposer les entrées classiques en un problème quantique​

Choisir un backend​

Pour détecter les erreurs pendant le circuit, nous devons choisir un backend qui a accès à la méthode MidCircuitMeasure (voir la documentation).

# You can see all backends that support mid-circuit measurements by running the following code.
service.backends(filters=lambda b: "measure_2" in b.supported_instructions)
[<IBMBackend('ibm_pittsburgh')>,
<IBMBackend('ibm_boston')>,
<IBMBackend('ibm_kingston')>]
# Choose the least busy backend that supports mid-circuit measurements (`measure_2`).

backend = service.least_busy(
filters=lambda b: "measure_2" in b.supported_instructions,
operational=True,
simulator=False,
dynamic_circuits=True,
)
# backend = service.backend(backend_name) # alternatively, you could choose a specific backend
print(backend.name)
ibm_boston

Construire une séquence de circuits stabilisateurs de retournement de bit mettant en œuvre plusieurs cycles de détection et de correction d'erreur​

Le code de retournement de bit est l'un des exemples les plus simples de code stabilisateur. Il protège l'état contre une erreur de retournement de bit (X) unique sur l'un des qubits d'encodage. En considérant l'action de l'erreur de retournement de bit XX, qui effectue la transformation ∣0⟩→∣1⟩|0\rangle \rightarrow |1\rangle et ∣1⟩→∣0⟩|1\rangle \rightarrow |0\rangle sur n'importe lequel de nos qubits, nous avons ϵ={E0,E1,E2}={IIX,IXI,XII}\epsilon = \{E_0, E_1, E_2 \} = \{IIX, IXI, XII\}. Le code nécessite cinq qubits : trois sont utilisés pour encoder l'état protégé (les « qubits de données »), et les deux restants servent d'ancillas pour la mesure des stabilisateurs.

Ci-dessous, tu vas construire des circuits qui (1) préparent les qubits de données dans l'état logique ∣1ˉ⟩|\bar{1} \rangle, puis (2) exécutent plusieurs cycles d'un délai de 5μs5 \mu s suivi d'une correction d'erreur (y compris la réinitialisation des qubits de syndrome), et (3) lisent l'état des qubits de données.

Nous testerons également la durée de vie de l'état ∣1⟩|1\rangle sans utiliser la correction d'erreur en incluant trois qubits de référence que nous préparons dans l'état ∣1⟩|1\rangle, laissons inactifs, puis lisons.

def build_qc(
qreg_data,
qreg_syndrome,
creg_data,
creg_syndrome,
qreg_ref=None,
creg_ref=None,
):
"""Build a typical error correction circuit"""
if qreg_ref:
return QuantumCircuit(
qreg_data,
qreg_syndrome,
creg_data,
creg_syndrome,
qreg_ref,
creg_ref,
)
else:
return QuantumCircuit(
qreg_data, qreg_syndrome, creg_data, creg_syndrome
)

def encode_bit_flip(circuit, qreg_data, qreg_ref=None) -> QuantumCircuit:
"""Encode bit-flip. This is done by simply adding a cx"""

for q in qreg_data:
circuit.x(q)

if qreg_ref:
for q in qreg_ref:
circuit.x(q)

circuit.barrier()

return circuit

def measure_syndrome_bit(
circuit, qreg_data, qreg_syndrome, creg_syndrome, qreg_ref=None
):
"""
Measure the syndrome by measuring the parity.
We reset our ancilla qubits after measuring the stabilizer
so we can reuse them for repeated stabilizer measurements.
Because we have already observed the state of the qubit,
we can write the conditional reset protocol directly to
avoid another round of qubit measurement if we used
the `reset` instruction.
"""
circuit.cx(qreg_data[0], qreg_syndrome[0])
circuit.cx(qreg_data[1], qreg_syndrome[0])
circuit.cx(qreg_data[0], qreg_syndrome[1])
circuit.cx(qreg_data[2], qreg_syndrome[1])
circuit.barrier()

for q_measure, c_measure in zip(qreg_syndrome, creg_syndrome):
circuit.append(MidCircuitMeasure(), [q_measure], [c_measure])

with circuit.if_test((creg_syndrome[0], 1)):
circuit.x(qreg_syndrome[0])
with circuit.if_test((creg_syndrome[1], 1)):
circuit.x(qreg_syndrome[1])

circuit.barrier()
return circuit

def apply_correction_bit(circuit, qreg_data, creg_syndrome):
"""We can detect where an error occurred and correct our state"""
with circuit.if_test((creg_syndrome, 3)):
circuit.x(qreg_data[0])
with circuit.if_test((creg_syndrome, 1)):
circuit.x(qreg_data[1])
with circuit.if_test((creg_syndrome, 2)):
circuit.x(qreg_data[2])
circuit.barrier()
return circuit

def apply_final_readout(
circuit, qreg_data, creg_data, qreg_ref=None, creg_ref=None
):
"""Read out the final measurements"""

circuit.barrier()
if qreg_ref:
circuit.measure(qreg_ref, creg_ref)
circuit.measure(qreg_data, creg_data)

return circuit
def build_error_correction_sequence(
num_cycles, cycles_per_circuit, nq_ref=3, delay=None
) -> QuantumCircuit:
circuits = []
reps = []

qreg_data = QuantumRegister(3, name="qdata")
qreg_syndrome = QuantumRegister(2, name="qsyndrome")
creg_data = ClassicalRegister(3, name="cdata")
creg_syndrome = ClassicalRegister(2, name="csyndrome")
qreg_ref = QuantumRegister(nq_ref, name="qreference")
creg_ref = ClassicalRegister(nq_ref, name="creference")

circuit = build_qc(
qreg_data,
qreg_syndrome,
creg_data,
creg_syndrome,
qreg_ref=qreg_ref,
creg_ref=creg_ref,
)
circuit = encode_bit_flip(circuit, qreg_data, qreg_ref=qreg_ref)

circuit_n = circuit.copy()
circuit_n = apply_final_readout(
circuit_n, qreg_data, creg_data, qreg_ref=qreg_ref, creg_ref=creg_ref
)
circuits.append(circuit_n)
reps.append(0)

for i in range(1, num_cycles + 1):
if delay:
circuit.delay(delay, unit="us")
circuit.barrier()
circuit = measure_syndrome_bit(
circuit,
qreg_data,
qreg_syndrome,
creg_syndrome,
qreg_ref=qreg_ref,
)
circuit = apply_correction_bit(circuit, qreg_data, creg_syndrome)
circuit_n = circuit.copy()
if i % cycles_per_circuit == 0:
circuit_n = apply_final_readout(
circuit_n,
qreg_data,
creg_data,
qreg_ref=qreg_ref,
creg_ref=creg_ref,
)
circuits.append(circuit_n)
reps.append(i)

return circuits, np.array(reps)

num_cycles = 40
cycles_per_circuit = 4
nq_ref = 3
circuits, rep_array = build_error_correction_sequence(
num_cycles, cycles_per_circuit, nq_ref=3, delay=5
)
circuits[1].draw(output="mpl", cregbundle=False, fold=50)

Output of the previous code cell

Étape 2 : Optimiser le problème pour l'exécution quantique matérielle​

Pour réduire le temps total d'exécution des tâches, les primitives Qiskit n'acceptent que des circuits et des observables conformes aux instructions et à la connectivité prises en charge par le système cible (appelés circuits et observables ISA, pour « instruction set architecture »). En savoir plus sur la transpilation.

Générer des circuits ISA​

Nous commencerons par trouver une disposition initiale (c'est-à-dire, une sélection de qubits physiques sur lesquels mapper nos circuits) en transpilant le plus long de nos circuits à l'aide du gestionnaire de passes prédéfini avec le niveau d'optimisation 3.

from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager

pm = generate_preset_pass_manager(backend=backend, optimization_level=3)
isa_circuit_ref = pm.run(circuits[-1])
init_layout = isa_circuit_ref.layout.initial_index_layout(
filter_ancillas=True
)
print(init_layout)
[56, 44, 62, 43, 63, 22, 48, 67]

Pour les qubits de référence auxquels nous comparerons notre mémoire quantique logique, nous choisirons les meilleurs qubits disponibles en termes de temps de cohérence d'amortissement d'amplitude (T1T_1).

# get all qubits ordered by T1
t1_data = []
for i in range(backend.num_qubits):
try:
t1_us = backend.properties().t1(i) * 1e6
except Exception:
t1_us = 0.0
t1_data.append((i, t1_us))

t1_data_sorted = sorted(t1_data, key=lambda x: x[1], reverse=True)

# exclude the qubits we have already mapped the error correcting code to
t1_data_sorted = [
t1_data for t1_data in t1_data_sorted if t1_data[0] not in init_layout[:5]
]

# use the best qubits in terms of T1 for the reference qubits
init_layout = init_layout[:5] + [t1_data[0] for t1_data in t1_data_sorted[:3]]
print(init_layout)
[56, 44, 62, 43, 63, 143, 131, 31]
# These are the resulting T1 times
properties = backend.properties()
print("Amplitude damping decoherence times for code data qubits:")
for q in init_layout[:3]:
t1 = properties.t1(q)
print(f"qubit {q}: T1 = {t1 * 1e6:.0f} mus")

print("\nAmplitude damping decoherence times for reference qubits:")
for q in init_layout[-3:]:
t1 = properties.t1(q)
print(f"qubit {q}: T1 = {t1 * 1e6:.0f} mus")
Amplitude damping decoherence times code data qubits:
qubit 56: T1 = 322 mus
qubit 44: T1 = 263 mus
qubit 62: T1 = 290 mus

Amplitude damping decoherence times reference qubits:
qubit 143: T1 = 442 mus
qubit 131: T1 = 410 mus
qubit 31: T1 = 401 mus
# now we transpile all circuits to this initial layout; this way each circuit is run on the same qubits and we can make a fair comparison
pm = generate_preset_pass_manager(
backend=backend,
optimization_level=3,
initial_layout=init_layout,
)
isa_circuits = pm.run(circuits)
isa_circuits[1].draw("mpl", cregbundle=False, fold=50)

Output of the previous code cell

Étape 3 : Exécuter à l'aide des primitives Qiskit​

sampler = Sampler(mode=backend)

sampler.options.environment.job_tags = ["TUT-REPCODE"]
sampler.options.max_execution_time = 600 # this workload is expected to be under 10s, but it is generally a good habit to set a max execution time (here 600s = 10m)
job = sampler.run(isa_circuits, shots=1000)
print(job.job_id())
job.status()
'DONE'

Étape 4 : post-traitement et renvoi du résultat au format classique souhaité​

Nous allons maintenant comparer les taux d'erreur en fonction du temps entre la mémoire logique utilisant le code de répétition à 3 qubits d'une part, et les qubits de référence individuels non encodés d'autre part.

results = job.result()
def correct_counts(counts_dict):
"""
Corrects the measured logical qubit encoded in the repetition code using majority vote
"""

result = {"000": 0, "111": 0}
for bitstring, count in counts_dict.items():
key = "111" if bitstring.count("1") > 1 else "000"
result[key] += count
return result

accuracy = [] # logical qubit
accuracies_ref = np.zeros(
(len(results), nq_ref)
) # accuracies on individual reference qubits

for n, pub_result in enumerate(results):
# logical accuracy (one minus error probability) for active error correction with repetition code
counts = pub_result.data.cdata.get_counts()
shots = sum(counts.values())
counts_corrected = correct_counts(counts)
accuracy.append(counts_corrected.get("111", 0) / shots)

# accuracy for individual physical reference qubits without any error correction
for i in range(nq_ref):
counts = pub_result.data.creference.slice_bits(i).get_counts()
accuracies_ref[n, i] = counts.get("1", 0) / shots

accuracy = np.array(accuracy)
def error_proba_t1(N, t1):
"""
Exponential fitting function for amplitude damping vs. number of cycles
"""

t_cycle = 7.3e-6 # approximate time per cycle = 5 mus delay + 2.3 mus for error correction
return 1 - np.exp(-t_cycle * N / t1)

fig, ax = plt.subplots(1, 1, figsize=(15, 5))
ax.plot(
rep_array, (1.0 - accuracy) * 100.0, "ko-", linewidth=3, label="rep code"
)

for i in range(nq_ref):
accuracy_1q = accuracies_ref[:, i]
if i == 0:
ax.plot(
rep_array,
(1.0 - accuracy_1q) * 100.0,
"go-",
linewidth=1,
label="1q reference",
)
else:
ax.plot(rep_array, (1.0 - accuracy_1q) * 100.0, "go-", linewidth=1)

params_bf, pcov = curve_fit(
error_proba_t1, rep_array, 1.0 - accuracy, bounds=([0, 5e-3])
)
t1_bf = params_bf[0]
print(f"Best-fit effective T1 = {t1_bf * 1e6:.0f} us")
error_prob_bf = error_proba_t1(
rep_array, t1_bf
) # np.array([1 - np.exp(-t_cycle * nt/t1) for nt in rep_array])
ax.plot(rep_array, error_prob_bf * 100.0, "k--", linewidth=1)

ax.set_xlabel("error correction rounds")
ax.set_ylabel("error [%]")
ax.set_ylim(bottom=0)
ax.legend(fontsize=15);
Best-fit effective T1 = 1397 us

Output of the previous code cell

Nous constatons clairement une durée de vie améliorée de l'état ∣1ˉ⟩|\bar{1}\rangle, même par rapport aux meilleurs qubits physiques (non encodés). Cependant, garde à l'esprit que cette expérience ne corrige qu'un seul type d'erreur, à savoir les retournements de bit. Peux-tu améliorer la durée de vie du qubit logique ? Tu pourrais essayer d'optimiser la durée d'attente, d'augmenter la taille du code de répétition au-delà de n=3n=3, etc.

Prochaines étapes​

Si tu as trouvé ce travail intéressant, voici quelques ressources supplémentaires qui pourraient t'intéresser :

Recommandations