Skip to main content
IBM Quantum Platform

반복 코드

예상 소요 시간: 헤론 프로세서에서 10초 미만 (참고: 이는 단지 추정치일 뿐입니다.) (실행 시간은 다를 수 있습니다.)


학습 성과

  • 동적 회로를 사용하여 비트 플립 오류 정정 코드를 구현하는 방법
  • 인코딩된 정보를 손상시키지 않고 양자 오류를 감지하기 위해 안정화기를 측정하는 방법
  • 수정된 결과와 수정되지 않은 결과를 비교하여 양자 오류 정정 성능을 평가하는 방법

전제조건


배경

실시간 양자 오류 수정(QEC)을 사용하려면 실행 중에 양자 프로그램 흐름을 동적으로 제어하여 양자 게이트가 측정 결과에 따라 조절될 수 있도록 해야 합니다. 이 튜토리얼에서는 매우 간단한 형태의 QEC인 비트 플립 코드를 실행합니다. 인코딩된 큐비트를 단일 비트 플립 오류로부터 보호할 수 있는 동적 양자 회로를 시연하고 비트 플립 코드 성능을 평가합니다.

인코딩된 양자 정보를 변환하지 않는 안정기를 측정하기 위해 추가적인 안실라 큐비트와 얽힘을 활용하면서 발생할 수 있는 일부 오류 클래스에 대해 알려줄 수 있습니다. 양자 안정화 코드는 kk 논리적 큐비트를 nn 물리적 큐비트로 인코딩합니다. 안정화 코드는 폴리 그룹( Πn\Pi^n )의 지원을 받아 이산 오류 세트를 수정하는 데 중점을 둡니다.

이 튜토리얼에서는 간단한 양자 메모리 실험을 통해 비트 플립 코드를 시연합니다. 우리는 논리 상태 ∣1ˉ⟩≡∣111⟩|\bar{1}\rangle \equiv |111\rangle 에 있는 인코딩된 큐비트를 준비한 다음, (오류를 축적하기 위해) 여러 번의 유휴 시간을 거친 후 오류 검출 및 정정 과정을 수행할 것입니다. 그런 다음, 논리 오류 확률을 사이클 수(시간)의 함수로 정량화하는데, 여기서 논리 오류 확률이란 최종 측정 후 양자 오류 상태( ∣1ˉ⟩|\bar{1}\rangle )를 회복하지 못하는 상태에 있는 큐비트를 발견할 확률을 의미한다(구체적으로, 오류에 해당하는 상태는 ∣000⟩|000\rangle, ∣001⟩|001\rangle, ∣010⟩|010\rangle 및 ∣100⟩|100\rangle 이다).

우리는 오류 발생률과 시간의 관계를, 인코딩되지 않은 개별 큐비트의 오류 발생률과 비교할 것이며, 또한 회로 작동 중에는 동적으로 오류를 감지하거나 수정하지 않고 최종 측정 후에만 오류를 감지하고 수정하는 반복 코드의 사용 사례와도 비교할 것입니다.

참고: 이 반복 코드는 비트 반전 오류만 수정할 수 있으므로 완전한 오류 정정 코드는 아닙니다. 그러나 이 방법이 간단하기 때문에, 양자 컴퓨터에서 오류 보정을 구현하는 방법을 설명하는 데 좋은 출발점이 됩니다. 아래의 기억 실험은 단 한 가지 유형의 오류( ∣1ˉ⟩|\bar{1}\rangle 에서 ∣0ˉ⟩|\bar{0}\rangle 로 변하는 경우)만을 테스트하며, 엄밀히 말해 고전적 정보의 보호만을 입증할 뿐입니다.


요구사항

이 튜토리얼을 시작하기 전에 다음이 설치되어 있는지 확인하세요:

  • Qiskit SDK v2.0 또는 이후 버전, 시각화 지원 기능 포함
  • Qiskit Runtime v0.40 이상 (pip install qiskit-ibm-runtime)

설정

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

소규모 시뮬레이터 예시

이 실험의 목적은 유휴 지연 시간 동안 발생하는 진폭 감쇠와 같은 실제 하드웨어 노이즈 하에서 논리 큐비트의 수명을 측정하는 것이므로, 이 단계는 생략하겠습니다. 노이즈가 없는 시뮬레이터에서는 감지하고 수정해야 할 오류가 나타나지 않을 것이기 때문입니다.


대규모 하드웨어 예시

1단계: 고전적 입력을 양자 문제에 매핑하기

백엔드 선택

회로 실행 중 오류를 감지하려면 해당 메서드를 MidCircuitMeasure 사용할 수 있는 백엔드를 선택해야 합니다( 문서 참조).

# 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

여러 단계의 오류 검출 및 정정 기능을 구현하는 비트 플립 안정화 회로 시퀀스를 구축하십시오

비트 플립 코드는 안정화 코드의 가장 간단한 예 중 하나이다. 이는 인코딩 큐비트 중 어느 하나에서 발생하는 단일 비트 플립(X) 오류로부터 상태를 보호합니다. 비트 플립 오류 XX 의 작용을 고려해 보자. 이 오류는 어떤 큐비트에 대해 ∣0⟩→∣1⟩|0\rangle \rightarrow |1\rangle 와 ∣1⟩→∣0⟩|1\rangle \rightarrow |0\rangle 를 매핑하므로, ϵ={E0,E1,E2}={IIX,IXI,XII}\epsilon = \{E_0, E_1, E_2 \} = \{IIX, IXI, XII\} 가 된다. 이 코드에는 5개의 큐비트가 필요하며, 그중 3개는 보호 상태를 인코딩하는 데 사용되고(“데이터 큐비트”), 나머지 2개는 안정화 측정 보조 큐비트로 사용된다.

아래에서는 (1) 데이터 큐비트를 논리적 ∣1ˉ⟩|\bar{1} \rangle 상태로 준비한 다음, (2) 5μs5 \mu s 지연 과정을 여러 번 반복하고 오류 정정(증후군 큐비트의 재설정 포함)을 수행한 후, (3) 데이터 큐비트의 상태를 읽어내는 회로를 구성하게 됩니다.

또한, 오류 정정 기법을 사용하지 않은 상태에서 ∣1⟩|1\rangle 상태의 수명을 테스트하기 위해, ∣1⟩|1\rangle 상태로 준비한 후 아무런 연산을 가하지 않고 그대로 두었다가 읽기 측정을 수행하는 세 개의 기준 큐비트를 포함시킬 것입니다.

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

2단계: 양자 하드웨어 실행을 위해 문제를 최적화한다

작업의 총 실행 시간을 줄이기 위해, Qiskit primitives 는 대상 시스템에서 지원하는 명령어 및 연결 방식을 준수하는 회로와 관측값만 허용합니다(이를 명령어 집합 아키텍처(ISA) 회로 및 관측값이라고 합니다). 트랜스파일레이션에 대해 자세히 알아보세요.

ISA 회로 생성

먼저, 사전 설정된 패스 매니저를 사용하여 최적화 수준 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]

우리의 논리 양자 메모리와 비교할 기준 큐비트로는, 진폭 감쇠 코히어런스 시간 측면에서 현재 이용 가능한 최상의 큐비트를 선정할 것이다( 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

3단계: 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'

4단계: 후처리를 수행하고 원하는 기존 형식으로 결과를 반환합니다

이제 3-큐비트 반복 코드를 사용한 논리 메모리와, 코딩되지 않은 개별 기준 큐비트 간의 오류율과 시간의 관계를 비교해 보겠습니다.

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

∣1ˉ⟩|\bar{1}\rangle 상태의 수명이, (인코딩되지 않은) 최고의 물리적 큐비트와 비교해도 분명히 향상된 것을 확인할 수 있습니다. 다만, 이 실험은 비트 반전과 같은 한 가지 유형의 오류만을 수정한다는 점을 명심해야 합니다. 논리적 큐비트의 수명을 늘릴 수 있나요? 지연 시간을 최적화하거나, 반복 코드의 크기를 n=3n=3 이상으로 늘리는 등의 방법을 고려해 볼 수 있습니다.


다음 단계

이 글이 흥미로웠다면, 다음 자료도 참고해 보시기 바랍니다:

권장사항
이 페이지가 도움이 되었습니까?
GitHub에서 버그, 오타를 보고하거나 컨텐츠를 요청하십시오.