Skip to main content
IBM Quantum Platform

Codes de répétition

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


Acquis d'apprentissage

  • Comment mettre en œuvre un code de correction d'erreurs de type « bit-flip » à l'aide de circuits dynamiques
  • Comment mesurer les stabilisateurs pour détecter les erreurs quantiques sans détruire les informations codées
  • Comment évaluer les performances de la correction d'erreurs quantiques en comparant les résultats corrigés et non corrigés

Prérequis


Arrière-plan

Pour permettre la correction d'erreurs quantiques (QEC) en temps réel, il faut pouvoir contrôler dynamiquement le flux du programme quantique pendant l'exécution, de sorte que les portes quantiques puissent être conditionnées par les résultats des mesures. Ce didacticiel utilise le code bit-flip, qui est une forme très simple de QEC. Il démontre un circuit quantique dynamique capable de protéger un qubit codé contre une erreur de basculement d'un seul bit, puis évalue les performances du code de basculement de bit.

Vous pouvez exploiter des qubits ancillaires supplémentaires et l'enchevêtrement pour mesurer des stabilisateurs qui ne transforment pas l'information quantique encodée, tout en vous informant de certaines catégories d'erreurs qui ont pu se produire. Un code stabilisateur quantique encode kk qubits logiques en nn qubits physiques. Les codes stabilisateurs se concentrent sur la correction d'un ensemble d'erreurs discrètes avec le soutien du groupe de Pauli Πn\Pi^n.

Dans ce tutoriel, nous présentons le code de permutation de bits à l'aide d'une expérience simple sur la mémoire quantique. Nous allons préparer le qubit codé dans l'état logique ∣1ˉ⟩≡∣111⟩|\bar{1}\rangle \equiv |111\rangle, puis effectuer plusieurs cycles de temps d'inactivité (afin de générer des erreurs), suivis d'une détection et d'une correction des erreurs. Nous quantifions ensuite la probabilité d’une erreur logique en fonction du nombre de cycles (temps), où la probabilité d’erreur logique correspond à la probabilité de trouver les qubits dans un état qui ne permet pas de retrouver l’ ∣1ˉ⟩|\bar{1}\rangle e 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 comparerons le taux d'erreur en fonction du temps à celui observé pour des qubits individuels non codés, ainsi qu'à l'utilisation d'un code de répétition, dans lequel nous ne détectons et ne corrigeons les erreurs qu'après la mesure finale, et non de manière dynamique pendant le fonctionnement du circuit.

Remarque : le code de répétition ne permet de corriger que les erreurs de permutation de bits; il ne s'agit donc pas d'un code de correction d'erreurs complet. Cependant, en raison de sa simplicité, il constitue un bon point de départ pour illustrer comment mettre en œuvre la correction d'erreurs sur un ordinateur quantique. L'expérience sur la mémoire présentée ci-dessous ne teste qu'un seul type d'erreur (une « ∣1ˉ⟩|\bar{1}\rangle » se transformant en « ∣0ˉ⟩|\bar{0}\rangle ») et, techniquement, ne démontre que la protection de l'information classique.


Exigences

Avant de commencer ce tutoriel, assurez-vous que les éléments suivants sont installés :

  • Qiskit SDK v2.0 ou plus tard, avec prise en charge de la visualisation
  • Qiskit Runtime v0.40 ou plus tard (pip install qiskit-ibm-runtime)

Configuration

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 faire l'impasse sur cette étape, car l'objectif de cette expérience est de mesurer la durée de vie d'un qubit logique en présence de bruit matériel réel, tel que l'atténuation d'amplitude pendant les délais d'inactivité; un simulateur sans bruit ne présenterait en effet aucune erreur à détecter et à corriger.


Exemple de matériel à grande échelle

Étape 1 : Mettre en correspondance les entrées classiques avec un problème quantique

Choisissez un backend

Pour détecter les erreurs au cours du circuit, nous devons choisir un backend ayant 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)

Output:

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

Output:

ibm_boston

Concevoir une série de circuits stabilisateurs à inversion de bits mettant en œuvre plusieurs cycles de détection et de correction d'erreurs

Le code « bit-flip » compte parmi les exemples les plus simples de code stabilisateur. Elle protège l'état contre une erreur de type « bit-flip » (X) sur n'importe lequel des qubits de codage. Considérons l’action de l’erreur de inversion de bit XX, qui mappe ∣0⟩→∣1⟩|0\rangle \rightarrow |1\rangle et ∣1⟩→∣0⟩|1\rangle \rightarrow |0\rangle sur n’importe lequel de nos qubits, ce qui donne alors ϵ={E0,E1,E2}={IIX,IXI,XII}\epsilon = \{E_0, E_1, E_2 \} = \{IIX, IXI, XII\}. Ce code nécessite cinq qubits : trois servent à coder l’état protégé (les « qubits de données »), et les deux autres sont utilisés comme ancilles de mesure stabilisateurs.

Vous allez réaliser ci-dessous 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 comprenant un délai de type « 5μs5 \mu s » suivi d'une correction d'erreurs (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 recourir à la correction d'erreurs, en incluant trois qubits de référence que nous préparerons dans l'état « ∣1⟩|1\rangle », que nous laisserons inactifs, puis dont nous lirons les valeurs.

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:

Output of the previous code cell

Étape 2 : Optimiser le problème en vue de son exécution sur du matériel quantique

Afin de réduire la durée totale d'exécution d'une tâche, les « Qiskit primitives » n'acceptent que les circuits et les observables conformes aux instructions et à la connectivité prises en charge par le système cible (appelés « circuits et observables de l'architecture de jeu d'instructions » (ISA)). En savoir plus sur la transpilation.

Générer des circuits ISA

Nous commencerons par déterminer une configuration 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 un niveau d'optimisation de 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)

Output:

[56, 44, 62, 43, 63, 22, 48, 67]

En ce qui concerne 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 à 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)

Output:

[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")

Output:

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:

Output of the previous code cell

Étape 3 : Exécutez la commande à l'aide d' Qiskit primitives

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

Output:

'DONE'

Étape 4 : Traitement ultérieur et restitution du résultat dans le 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 codé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);

Output:

Best-fit effective T1 = 1397 us
Output of the previous code cell

On constate clairement une amélioration de la durée de vie de l'état « ∣1ˉ⟩|\bar{1}\rangle », même par rapport aux meilleurs qubits physiques (non codés). Cependant, gardez à l'esprit que cette expérience ne corrige qu'un seul type d'erreur, à savoir les inversions de bits. Est-il possible d'augmenter la durée de vie du qubit logique? Vous pourriez envisager d'optimiser le temps de délai, d'augmenter la taille du code de répétition au-delà de n=3n=3, etc.


Etapes suivantes

Si ce travail vous a paru intéressant, les documents suivants pourraient vous intéresser :

Recommandations
Cette page a-t-elle été utile ?
Signaler un bogue, une coquille ou proposer du contenu sur GitHub.