Skip to main content
IBM Quantum Platform

Q-CTRL의 퀀텀 제어 시스템( Qiskit Functions )을 활용한 양자 위상 추정

예상 사용 시간: Heron r2 프로세서에서 40초. (참고: 이는 추정치일 뿐입니다. 런타임은 다를 수 있습니다.)


배경

양자 위상 추정(QPE)은 양자 컴퓨팅의 기본 알고리즘으로, 쇼의 알고리즘, 양자 화학 기저 상태 에너지 추정, 고유값 문제 등 여러 중요한 애플리케이션의 기초를 형성합니다. QPE는 단일 연산자의 고유 상태와 관련된 위상 φ\varphi 을 추정하며, 다음과 같은 관계로 인코딩됩니다

Uφ=e2πiφφ,U \lvert \varphi \rangle = e^{2\pi i \varphi} \lvert \varphi \rangle,

를 계산하고 mm 큐비트 계산 [1] 을 사용하여 ϵ=O(1/2m)\epsilon = O(1/2^m) 의 정밀도로 결정합니다. 이러한 큐비트를 중첩하여 준비하고 UU 의 제어된 거듭제곱을 적용한 다음 역 양자 푸리에 변환(QFT)을 사용하여 위상을 이진 인코딩된 측정 결과로 추출함으로써 QPE는 이진 분수가 대략 φ\varphi 에 가까운 비트스트링에서 정점을 이루는 확률 분포를 생성합니다. 이상적인 경우, 가장 가능성이 높은 측정 결과는 위상의 이진 확장과 직접적으로 일치하는 반면, 다른 결과의 확률은 카운팅 큐비트 수에 따라 급격히 감소합니다. 그러나 하드웨어에서 딥 QPE 회로를 실행하는 데는 많은 수의 큐비트와 얽히고설킨 연산으로 인해 알고리즘이 디코히어런스와 게이트 오류에 매우 민감하게 반응한다는 문제가 있습니다. 이로 인해 비트스트링의 분포가 넓어지고 이동되어 실제 고유 위상이 가려집니다. 결과적으로 가장 높은 확률을 가진 비트 문자열은 더 이상 φ\varphi 의 올바른 이진 확장에 해당하지 않을 수 있습니다.

이 튜토리얼에서는 키스킷 함수로 제공되는 Q-CTRL의 Fire Opal 오류 억제 및 성능 관리 도구를 사용하여 QPE 알고리즘을 구현하는 방법을 소개합니다( Fire Opal 문서 참조). Fire Opal은 동적 디커플링, 큐비트 레이아웃 개선, 오류 억제 기법 등 고급 최적화를 자동으로 적용하여 더욱 완성도 높은 결과물을 제공합니다. 이러한 개선으로 하드웨어 비트스트링 분포가 노이즈 없는 시뮬레이션에서 얻은 분포에 가까워져 노이즈의 영향 하에서도 정확한 고유 위상을 안정적으로 식별할 수 있습니다.


요구사항

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

  • Qiskit SDK v1.4 또는 이후 버전, 시각화 지원 기능 포함
  • Qiskit Runtime v0.40 이상 (pip install qiskit-ibm-runtime)
  • Qiskit Functions Catalog v0.9.0 (pip install qiskit-ibm-catalog)
  • Fire Opal SDK v9.0.2 이상 (pip install fire-opal)
  • Q-CTRL 비주얼라이저 v8.0.2 이상 (pip install qctrl-visualizer)

설정

먼저, IBM Quantum API 키 를 사용하여 인증하십시오. 그런 다음 다음과 같이 Qiskit 함수를 선택합니다. (이 코드는 사용자가 이미 계정을 로컬 환경에 저장해 두었다고 가정합니다.)

from qiskit import QuantumCircuit

import numpy as np
import matplotlib.pyplot as plt
import qiskit
from qiskit import qasm2
from qiskit_aer import AerSimulator
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import SamplerV2 as Sampler
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
import qctrlvisualizer as qv
from qiskit_ibm_catalog import QiskitFunctionsCatalog

plt.style.use(qv.get_qctrl_style())
catalog = QiskitFunctionsCatalog(channel="ibm_quantum_platform")

# Access Function
perf_mgmt = catalog.load("q-ctrl/performance-management")

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

이 튜토리얼에서는 알려진 단일 큐비트 유니티의 고유 위상을 복구하는 QPE를 설명합니다. 위상을 추정하고자 하는 단일 큐비트의 위상은 타겟 큐비트에 적용된 단일 큐비트 위상 게이트입니다:

U(θ)=(100eiθ)=eiθ1 ⁣1.U(\theta)= \begin{pmatrix} 1 & 0\\[2pt] 0 & e^{i\theta} \end{pmatrix} = e^{i\theta\,|1\rangle\!\langle 1|}.

우리는 그 고유상태 ψ=1|\psi\rangle=|1\rangle 를 준비합니다. 1|1\rangleU(θ)U(\theta) 의 고유 벡터이며 고유값은 eiθe^{i\theta} 이므로 추정할 고유 위상은 다음과 같습니다:

φ=θ2π(mod1)\varphi = \frac{\theta}{2\pi} \pmod{1}

θ=162π\theta=\tfrac{1}{6}\cdot 2\pi 을 설정하므로 실측 위상은 φ=1/6\varphi=1/6 입니다. QPE 회로는 제어된 위상 회전을 각도 θ2k\theta\cdot2^k 로 적용하여 제어된 전력 U2kU^{2^k} 을 구현한 다음 역 QFT를 카운팅 레지스터에 적용하고 측정합니다. 결과 비트스트링은 1/61/6 의 이진 표현에 집중되어 있습니다.

이 회로는 mm 카운팅 큐비트(추정 정밀도를 설정하기 위해)와 목표 큐비트 하나를 사용합니다. 먼저 QPE 구현에 필요한 빌딩 블록인 양자 푸리에 변환(QFT)과 그 역, 고유상의 10진수와 2진수를 매핑하는 유틸리티 함수, 시뮬레이션과 하드웨어 결과를 비교하기 위해 원시 수를 확률로 정규화하는 헬퍼를 정의하는 것으로 시작합니다.

def inverse_quantum_fourier_transform(quantum_circuit, number_of_qubits):
    """
    Apply an inverse Quantum Fourier Transform the first `number_of_qubits` qubits in the
    `quantum_circuit`.
    """
    for qubit in range(number_of_qubits // 2):
        quantum_circuit.swap(qubit, number_of_qubits - qubit - 1)
    for j in range(number_of_qubits):
        for m in range(j):
            quantum_circuit.cp(-np.pi / float(2 ** (j - m)), m, j)
        quantum_circuit.h(j)
    return quantum_circuit
def bitstring_count_to_probabilities(data, shot_count):
    """
    This function turns an unsorted dictionary of bitstring counts into a sorted dictionary
    of probabilities.
    """
    # Turn the bitstring counts into probabilities.
    probabilities = {
        bitstring: bitstring_count / shot_count
        for bitstring, bitstring_count in data.items()
    }

    sorted_probabilities = dict(
        sorted(probabilities.items(), key=lambda x: x[1], reverse=True)
    )

    return sorted_probabilities

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

카운팅 큐비트를 중첩하여 준비하고, 제어된 위상 회전을 적용하여 목표 고유 위상을 인코딩하고, 측정 전에 역 QFT로 마무리하는 방식으로 QPE 회로를 구축합니다.

def quantum_phase_estimation_benchmark_circuit(
    number_of_counting_qubits, phase
):
    """
    Create the circuit for quantum phase estimation.

    Parameters
    ----------
    number_of_counting_qubits : The number of qubits in the circuit.
    phase : The desired phase.

    Returns
    -------
    QuantumCircuit
        The quantum phase estimation circuit for `number_of_counting_qubits` qubits.
    """
    qc = QuantumCircuit(
        number_of_counting_qubits + 1, number_of_counting_qubits
    )
    target = number_of_counting_qubits

    # |1> eigenstate for the single-qubit phase gate
    qc.x(target)

    # Hadamards on counting register
    for q in range(number_of_counting_qubits):
        qc.h(q)

    # ONE controlled phase per counting qubit: cp(phase * 2**k)
    for k in range(number_of_counting_qubits):
        qc.cp(phase * (1 << k), k, target)

    qc.barrier()

    # Inverse QFT on counting register
    inverse_quantum_fourier_transform(qc, number_of_counting_qubits)

    qc.barrier()
    for q in range(number_of_counting_qubits):
        qc.measure(q, q)
    return qc

3단계: Qiskit primitives 명령어로 실행합니다

실험을 위한 샷 수와 큐비트 수를 설정하고 mm 이진 숫자를 사용하여 목표 단계 φ=1/6\varphi = 1/6 를 인코딩합니다. 이러한 파라미터를 사용하여 시뮬레이션, 기본 하드웨어 및 Fire Opal 강화 백엔드에서 실행될 QPE 회로를 구축합니다.

shot_count = 10000
num_qubits = 35
phase = (1 / 6) * 2 * np.pi
circuits_quantum_phase_estimation = (
    quantum_phase_estimation_benchmark_circuit(
        number_of_counting_qubits=num_qubits, phase=phase
    )
)

MPS 시뮬레이션 실행

먼저 matrix_product_state 시뮬레이터를 사용하여 참조 분포를 생성하고 나중에 하드웨어 결과와 비교할 수 있도록 개수를 정규화된 확률로 변환합니다.

# Run the algorithm on the IBM Aer simulator.
aer_simulator = AerSimulator(method="matrix_product_state")

# Transpile the circuits for the simulator.
transpiled_circuits = qiskit.transpile(
    circuits_quantum_phase_estimation, aer_simulator
)
simulated_result = (
    aer_simulator.run(transpiled_circuits, shots=shot_count)
    .result()
    .get_counts()
)
simulated_result_probabilities = []

simulated_result_probabilities.append(
    bitstring_count_to_probabilities(
        simulated_result,
        shot_count=shot_count,
    )
)

하드웨어에서 실행

service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)

pm = generate_preset_pass_manager(backend=backend, optimization_level=3)
isa_circuits = pm.run(circuits_quantum_phase_estimation)
# Run the algorithm with IBM default.
sampler = Sampler(backend)

# Run all circuits using IBM Quantum Compute Service.
ibm_default_job = sampler.run([isa_circuits], shots=shot_count)

Fire Opal이 탑재된 하드웨어에서 실행

# Run the circuit using Sampler
fire_opal_job = perf_mgmt.run(
    primitive="sampler",
    pubs=[qasm2.dumps(circuits_quantum_phase_estimation)],
    backend_name=backend.name,
    options={"default_shots": shot_count},
)

4단계: 후처리 수행 및 원하는 클래식 형식으로 결과 반환

# Retrieve results.
ibm_default_result = ibm_default_job.result()
ibm_default_probabilities = []

for idx, pub_result in enumerate(ibm_default_result):
    ibm_default_probabilities.append(
        bitstring_count_to_probabilities(
            pub_result.data.c0.get_counts(),
            shot_count=shot_count,
        )
    )
fire_opal_result = fire_opal_job.result()

fire_opal_probabilities = []
for idx, pub_result in enumerate(fire_opal_result):
    fire_opal_probabilities.append(
        bitstring_count_to_probabilities(
            pub_result.data.c0.get_counts(),
            shot_count=shot_count,
        )
    )
data = {
    "simulation": simulated_result_probabilities,
    "default": ibm_default_probabilities,
    "fire_opal": fire_opal_probabilities,
}
def plot_distributions(
    data,
    number_of_counting_qubits,
    top_k=None,
    by="prob",
    shot_count=None,
):
    def nrm(d):
        s = sum(d.values())
        return {k: (v / s if s else 0.0) for k, v in d.items()}

    def as_float(d):
        return {k: float(v) for k, v in d.items()}

    def to_space(d):
        if by == "prob":
            return nrm(as_float(d))
        else:
            if shot_count and 0.99 <= sum(d.values()) <= 1.01:
                return {
                    k: v * float(shot_count) for k, v in as_float(d).items()
                }
            else:
                return as_float(d)

    def topk(d, k):
        items = sorted(d.items(), key=lambda kv: kv[1], reverse=True)
        return items[: (k or len(d))]

    phase = "1/6"

    sim = to_space(data["simulation"])
    dft = to_space(data["default"])
    qct = to_space(data["fire_opal"])

    correct = max(sim, key=sim.get) if sim else None
    print("Correct result:", correct)

    sim_items = topk(sim, top_k)
    dft_items = topk(dft, top_k)
    qct_items = topk(qct, top_k)

    sim_keys, y_sim = zip(*sim_items) if sim_items else ([], [])
    dft_keys, y_dft = zip(*dft_items) if dft_items else ([], [])
    qct_keys, y_qct = zip(*qct_items) if qct_items else ([], [])

    fig, axes = plt.subplots(3, 1, layout="constrained")
    ylab = "Probabilities"

    def panel(ax, keys, ys, title, color):
        x = np.arange(len(keys))
        bars = ax.bar(x, ys, color=color)
        ax.set_title(title)
        ax.set_ylabel(ylab)
        ax.set_xticks(x)
        ax.set_xticklabels(keys, rotation=90)
        ax.set_xlabel("Bitstrings")
        if correct in keys:
            i = keys.index(correct)
            bars[i].set_edgecolor("black")
            bars[i].set_linewidth(2)
        return max(ys, default=0.0)

    c_sim, c_dft, c_qct = (
        qv.QCTRL_STYLE_COLORS[5],
        qv.QCTRL_STYLE_COLORS[1],
        qv.QCTRL_STYLE_COLORS[0],
    )
    m1 = panel(axes[0], list(sim_keys), list(y_sim), "Simulation", c_sim)
    m2 = panel(axes[1], list(dft_keys), list(y_dft), "Default", c_dft)
    m3 = panel(axes[2], list(qct_keys), list(y_qct), "Q-CTRL", c_qct)

    for ax, m in zip(axes, (m1, m2, m3)):
        ax.set_ylim(0, 1.05 * (m or 1.0))

    for ax in axes:
        ax.label_outer()
    fig.suptitle(
        rf"{number_of_counting_qubits} counting qubits, $2\pi\varphi$={phase}"
    )
    fig.set_size_inches(20, 10)
    plt.show()
experiment_index = 0
phase_index = 0

distributions = {
    "simulation": data["simulation"][phase_index],
    "default": data["default"][phase_index],
    "fire_opal": data["fire_opal"][phase_index],
}

plot_distributions(
    distributions, num_qubits, top_k=100, by="prob", shot_count=shot_count
)

Output:

Correct result: 00101010101010101010101010101010101
Output of the previous code cell

시뮬레이션은 올바른 고유 위상을 위한 기준선을 설정합니다. 기본 하드웨어 실행은 노이즈가 많은 잘못된 비트 문자열에 확률을 분산시키기 때문에 이 결과를 가리는 노이즈가 표시됩니다. Q-CTRL 성과 관리를 사용하면 분포가 더 선명해지고 정확한 결과가 복구되어 이 규모에서 신뢰할 수 있는 QPE가 가능해집니다.


참조

[1] 강의 7: 위상 추정 및 인수분해. IBM Quantum Learning - 양자 알고리즘의 기초. 검색된 날짜: 2025년 10월 3일.


튜토리얼 설문조사

잠시 시간을 내어 이 튜토리얼에 대한 피드백을 제공해 주세요. 여러분의 인사이트는 콘텐츠 제공과 사용자 경험을 개선하는 데 도움이 됩니다.

설문조사 링크

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