Skip to main content
IBM Quantum Platform

Codici di ripetizione

Stima del tempo di esecuzione: meno di 10 secondi su un processore Heron (NOTA: si tratta solo di una stima. (La durata potrebbe variare.)


Risultati di apprendimento

  • Come implementare un codice di correzione degli errori di inversione di bit utilizzando circuiti dinamici
  • Come misurare gli stabilizzatori per rilevare errori quantistici senza distruggere le informazioni codificate
  • Come valutare le prestazioni della correzione quantistica degli errori confrontando i risultati corretti con quelli non corretti

Prerequisiti


Sfondo

Per consentire la correzione degli errori quantistici (QEC) in tempo reale, è necessario poter controllare dinamicamente il flusso del programma quantistico durante l'esecuzione, in modo che le porte quantistiche possano essere condizionate dai risultati delle misure. Questa esercitazione esegue il codice bit-flip, che è una forma molto semplice di QEC. Dimostra un circuito quantistico dinamico in grado di proteggere un qubit codificato da un singolo errore di bit-flip e valuta le prestazioni del codice bit-flip.

È possibile sfruttare qubit ancilla aggiuntivi e l'entanglement per misurare stabilizzatori che non trasformano le informazioni quantistiche codificate, informando comunque su alcune classi di errori che potrebbero essersi verificati. Un codice stabilizzatore quantistico codifica kk qubit logici in nn qubit fisici. I codici stabilizzatori si concentrano criticamente sulla correzione di un insieme di errori discreti con il supporto del gruppo di Pauli Πn\Pi^n.

In questo tutorial illustriamo il codice “bit-flip” attraverso un semplice esperimento sulla memoria quantistica. Prepareremo il qubit codificato nello stato logico ∣1ˉ⟩≡∣111⟩|\bar{1}\rangle \equiv |111\rangle e quindi eseguiremo più cicli di inattività (per generare errori), seguiti dal rilevamento e dalla correzione degli errori. Quantifichiamo quindi la probabilità di un errore logico in funzione del numero di cicli (tempo), dove la probabilità di errore logico è la probabilità di trovare i qubit in uno stato che non ripristini l’ ∣1ˉ⟩|\bar{1}\rangle e dopo la misurazione finale (in particolare, gli stati corrispondenti a un errore sono ∣000⟩|000\rangle, ∣001⟩|001\rangle, ∣010⟩|010\rangle e ∣100⟩|100\rangle ).

Confronteremo il tasso di errore in funzione del tempo con i tassi di errore dei singoli qubit non codificati, nonché con l’uso del codice di ripetizione, in cui gli errori vengono rilevati e corretti solo dopo la misurazione finale, ma non dinamicamente durante il funzionamento del circuito.

Nota: il codice di ripetizione consente solo la correzione degli errori di inversione di bit e, pertanto, non è un codice di correzione degli errori completo. Tuttavia, proprio grazie alla sua semplicità, rappresenta un buon punto di partenza per illustrare come implementare la correzione degli errori su un computer quantistico. L'esperimento sulla memoria riportato di seguito verifica solo un unico tipo di errore (un " ∣1ˉ⟩|\bar{1}\rangle " che si riduce a " ∣0ˉ⟩|\bar{0}\rangle ") e, tecnicamente, dimostra solo la protezione delle informazioni classiche.


Requisiti

Prima di iniziare questa esercitazione, assicuratevi di aver installato quanto segue:

  • Qiskit SDK v2.0 o versioni successive, con supporto alla visualizzazione
  • Qiskit Runtime v0.40 o più tardi (pip install qiskit-ibm-runtime)

Configura

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

Esempio di simulatore su piccola scala

Tralasceremo questo passaggio poiché l'obiettivo di questo esperimento è misurare la durata di vita di un qubit logico in presenza di rumore hardware reale, come lo smorzamento di ampiezza durante i ritardi di inattività; un simulatore privo di rumore non mostrerebbe errori da rilevare e correggere.


Esempio di hardware su larga scala

Fase 1: Mappare gli input classici su un problema quantistico

Scegli un backend

Per individuare eventuali errori durante l'esecuzione del circuito, è necessario scegliere un backend che abbia accesso al metodo MidCircuitMeasure (vedere la documentazione ).

# 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

Realizzare una sequenza di circuiti stabilizzatori con inversione di bit che implementino più cicli di rilevamento e correzione degli errori

Il codice bit-flip è uno degli esempi più semplici di codice stabilizzatore. Protegge lo stato da un singolo errore di inversione di bit (X) su uno qualsiasi dei qubit di codifica. Consideriamo l’azione dell’errore di inversione di bit XX, che mappa ∣0⟩→∣1⟩|0\rangle \rightarrow |1\rangle e ∣1⟩→∣0⟩|1\rangle \rightarrow |0\rangle su uno qualsiasi dei nostri qubit; in tal caso, si ottiene ϵ={E0,E1,E2}={IIX,IXI,XII}\epsilon = \{E_0, E_1, E_2 \} = \{IIX, IXI, XII\}. Il codice richiede cinque qubit: tre vengono utilizzati per codificare lo stato protetto (i “qubit di dati”), mentre i restanti due fungono da ancilla per la misurazione degli stabilizzatori.

Di seguito realizzerete dei circuiti che (1) preparano i qubit di dati nello stato logico ∣1ˉ⟩|\bar{1} \rangle, poi (2) eseguono più cicli di un ritardo 5μs5 \mu s seguiti dalla correzione degli errori (compreso il reset dei qubit di sindrome) e (3) leggono lo stato dei qubit di dati.

Verificheremo inoltre la durata dello stato “ ∣1⟩|1\rangle ” senza ricorrere alla correzione degli errori, includendo tre qubit di riferimento che prepareremo nello stato “ ∣1⟩|1\rangle ”, lasceremo inattivi e poi leggeremo.

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

Fase 2: Ottimizzare il problema per l'esecuzione su hardware quantistico

Per ridurre il tempo totale di esecuzione del processo, l’ Qiskit primitives e accetta solo circuiti e osservabili conformi alle istruzioni e alla connettività supportate dal sistema di destinazione (denominati “circuiti e osservabili dell’architettura del set di istruzioni” (ISA)). Scopri di più sulla transpilazione.

Generare circuiti ISA

Inizieremo individuando una configurazione iniziale (ovvero una selezione di qubit fisici a cui mappare i nostri circuiti) mediante la transpilazione del circuito più lungo tra quelli a nostra disposizione, utilizzando il gestore di passaggi preimpostato con livello di ottimizzazione 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]

Come qubit di riferimento con cui confronteremo la nostra memoria quantistica logica, sceglieremo i migliori qubit disponibili in termini di tempo di coerenza con smorzamento di ampiezza ( 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

Passaggio 3: Eseguire il comando utilizzando 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'

Fase 4: Elaborazione successiva e restituzione del risultato nel formato classico desiderato

Confronteremo ora i tassi di errore in funzione del tempo tra la memoria logica che utilizza il codice di ripetizione a 3 qubit, da un lato, e i singoli qubit di riferimento non codificati, dall’altro.

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

Si osserva chiaramente un aumento della durata dello stato " ∣1ˉ⟩|\bar{1}\rangle ", anche rispetto ai migliori qubit fisici (non codificati). Tuttavia, tieni presente che questo esperimento corregge solo un tipo di errore, ovvero i bit-flip. È possibile aumentare la durata di vita del qubit logico? Potresti valutare la possibilità di ottimizzare il tempo di ritardo, aumentare la dimensione del codice di ripetizione oltre n=3n=3, ecc.


Passi successivi

Se questo lavoro ti è sembrato interessante, potrebbero interessarti anche i seguenti materiali:

Suggerimenti
Questa pagina è stata utile?
Segnala un bug, un errore di battitura o richiedi contenuti su GitHub.