Skip to main content
IBM Quantum Platform

Códigos de repetição

Estimativa de tempo de execução: menos de 10 segundos em um processador Heron (OBSERVAÇÃO: trata-se apenas de uma estimativa. (O tempo de execução pode variar.)


Resultados do aprendizado

  • Como implementar um código de correção de erros de inversão de bits utilizando circuitos dinâmicos
  • Como medir estabilizadores para detectar erros quânticos sem destruir as informações codificadas
  • Como avaliar o desempenho da correção de erros quânticos comparando resultados corrigidos e não corrigidos

Pré-requisitos


Segundo plano

Para permitir a correção de erros quânticos (QEC) em tempo real, você precisa ser capaz de controlar dinamicamente o fluxo do programa quântico durante a execução, de modo que as portas quânticas possam ser condicionadas aos resultados da medição. Este tutorial executa o código bit-flip, que é uma forma muito simples de QEC. Ele demonstra um circuito quântico dinâmico que pode proteger um qubit codificado de um único erro de inversão de bit e, em seguida, avalia o desempenho do código de inversão de bit.

Você pode explorar qubits ancilla adicionais e emaranhamento para medir estabilizadores que não transformam as informações quânticas codificadas e, ao mesmo tempo, informá-lo sobre algumas classes de erros que podem ter ocorrido. Um código estabilizador quântico codifica kk qubits lógicos em nn qubits físicos. Os códigos estabilizadores se concentram criticamente na correção de um conjunto de erros discretos com o apoio do grupo Pauli Πn\Pi^n.

Neste tutorial, demonstramos o código de inversão de bits por meio de um experimento simples com memória quântica. Prepararemos o qubit codificado no estado lógico ∣1ˉ⟩≡∣111⟩|\bar{1}\rangle \equiv |111\rangle e, em seguida, realizaremos vários ciclos de inatividade (para acumular erros), seguidos pela detecção e correção de erros. Em seguida, quantificamos a probabilidade de um erro lógico em função do número de ciclos (tempo), sendo que a probabilidade de erro lógico é a probabilidade de encontrar os qubits em um estado que não recupere o estado ∣1ˉ⟩|\bar{1}\rangle após a medição final (concretamente, os estados correspondentes a um erro são ∣000⟩|000\rangle, ∣001⟩|001\rangle, ∣010⟩|010\rangle e ∣100⟩|100\rangle ).

Compararemos a taxa de erros em função do tempo com as taxas de erros de qubits individuais não codificados e também com o uso do código de repetição, no qual detectamos e corrigimos erros apenas após a medição final, mas não dinamicamente durante o circuito.

Observação: o código de repetição permite apenas a correção de erros de inversão de bits e, portanto, não é um código completo de correção de erros. No entanto, devido à sua simplicidade, é um bom ponto de partida para ilustrar como implementar a correção de erros em um computador quântico. O experimento de memória abaixo testa apenas um único tipo de erro (um “ ∣1ˉ⟩|\bar{1}\rangle ” que se degrada para “ ∣0ˉ⟩|\bar{0}\rangle ”) e, tecnicamente, demonstra apenas a proteção da informação clássica.


Requisitos

Antes de iniciar este tutorial, verifique se você tem os seguintes itens instalados:

  • Qiskit SDK v2.0 ou posterior, com suporte para visualização
  • Qiskit Runtime v0.40 ou posterior (pip install qiskit-ibm-runtime)

Instalação

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

Exemplo de simulador em pequena escala

Vamos pular essa etapa, já que o objetivo deste experimento é medir o tempo de vida de um qubit lógico sob ruído real de hardware, como o amortecimento de amplitude durante os atrasos em estado inativo; um simulador sem ruído não apresentaria erros a serem detectados e corrigidos.


Exemplo de hardware em grande escala

Etapa 1: Mapeie entradas clássicas para um problema quântico

Escolha um backend

Para detectar erros durante o circuito, precisamos escolher um backend que tenha acesso ao método MidCircuitMeasure (consulte a documentação ).

# 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

Construa uma sequência de circuitos estabilizadores de inversão de bits que implementem várias rodadas de detecção e correção de erros

O código de inversão de bits está entre os exemplos mais simples de código estabilizador. Protege o estado contra um único erro de inversão de bit (X) em qualquer um dos qubits de codificação. Considere a ação do erro de inversão de bit XX, que mapeia ∣0⟩→∣1⟩|0\rangle \rightarrow |1\rangle e ∣1⟩→∣0⟩|1\rangle \rightarrow |0\rangle em qualquer um dos nossos qubits; assim, temos ϵ={E0,E1,E2}={IIX,IXI,XII}\epsilon = \{E_0, E_1, E_2 \} = \{IIX, IXI, XII\}. O código requer cinco qubits: três são usados para codificar o estado protegido (os “qubits de dados”), e os dois restantes são usados como ancillas de medição estabilizadoras.

A seguir, você construirá circuitos que (1) preparam os qubits de dados no estado lógico ∣1ˉ⟩|\bar{1} \rangle; em seguida, (2) executam vários ciclos de um atraso 5μs5 \mu s, seguidos de correção de erros (incluindo a reinicialização dos qubits de síndrome); e (3) leem o estado dos qubits de dados.

Também testaremos a duração do estado ∣1⟩|1\rangle sem utilizar correção de erros, incluindo três qubits de referência que preparamos no estado ∣1⟩|1\rangle, deixamos inativos e, em seguida, lemos.

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

Etapa 2: Otimizar o problema para execução em hardware quântico

Para reduzir o tempo total de execução da tarefa, o Qiskit primitives aceita apenas circuitos e observáveis que estejam em conformidade com as instruções e a conectividade suportadas pelo sistema de destino (conhecidos como circuitos e observáveis de arquitetura de conjunto de instruções (ISA)). Saiba mais sobre transpilação.

Gerar circuitos ISA

Começaremos definindo um layout inicial (ou seja, uma seleção de qubits físicos aos quais mapearemos nossos circuitos) ao compilar o mais longo dos nossos circuitos usando o gerenciador de passagens predefinido com nível de otimização 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]

Como qubits de referência, com os quais compararemos nossa memória quântica lógica, escolheremos os melhores qubits disponíveis em termos de tempo de coerência de amortecimento de 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

Etapa 3: Executar usando o comando 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'

Etapa 4: Realizar o pós-processamento e gerar o resultado no formato clássico desejado

Compararemos agora as taxas de erro em função do tempo entre a memória lógica que utiliza o código de repetição de 3 qubits, por um lado, e os qubits de referência individuais não codificados, por outro.

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

Observamos claramente um aumento na duração do estado ∣1ˉ⟩|\bar{1}\rangle, mesmo quando comparado aos melhores qubits físicos (não codificados). No entanto, tenha em mente que essa experiência corrige apenas um tipo de erro: as inversões de bits. É possível aumentar a vida útil do qubit lógico? Talvez seja interessante explorar a otimização do tempo de atraso, aumentar o tamanho do código de repetição para além de n=3n=3, etc.


Próximas etapas

Se você achou este trabalho interessante, talvez se interesse pelo material a seguir:

Recomendações
Esta página foi útil?
Relate um bug, erro de digitação ou solicite conteúdo no GitHub.