Skip to main content
IBM Quantum Platform

잡음이 있는 양자 프로세서에서 관찰된 견고하고 일관된 비아벨 하드론 역학

예상 소요 시간: Heron 프로세서(ibm_boston 또는 이에 상응하는 프로세서)에서 6분 (참고: 이는 단지 추정치일 뿐입니다.) (실행 시간은 다를 수 있습니다.)


학습 성과

  • 비아벨 격자 게이지 이론(특히 SU(2))을 루프-스트링-하드론(LSH) 프레임워크를 활용하여 효율적인 양자 시뮬레이션을 위해 어떻게 재구성할 수 있는가
  • 근사적 SU(2) 게이지 이론 해밀토니안을 위한 트로터화 시간 진화 회로를 구성하고 이를 큐비트에 매핑하는 방법
  • IBM Quantum® 하드웨어에서 Qiskit Estimator 프리미티브를 사용하여 판독 오차 완화 기능을 적용한 이 회로를 실행하는 방법

전제조건


배경

둥기 부여

강력(strong force)에 대한 SU(3) 게이지 이론인 양자 색역학(QCD)은 쿼크를 하드론으로 결합시키고, 갇힘 현상과 끈 파열을 지배한다. 고전 격자 QCD 기법은 정적 특성을 분석하는 데 탁월하지만, 부호 문제로 인해 실시간 동역학을 시뮬레이션할 수는 없다. 양자 컴퓨터는 게이지장의 자유도를 큐비트에 직접 인코딩함으로써 이러한 장벽을 우회할 수 있는 방법을 제시한다.

이 튜토리얼에서는 이러한 시뮬레이션을 시연합니다. 즉, IBM Quantum 하드웨어를 사용하여 (1+1)차원 SU(2) 격자 게이지 이론에서 하드론의 실시간 전파를 시뮬레이션합니다. 이 이론은 가장 단순한 비아벨 게이지 이론이자 완전한 QCD로 나아가는 디딤돌입니다.

코구트-수스킨드 해밀토니안

이 이론은 1D 공간 격자 위에서, 격자점에는 엇갈리게 배열된 페르미온(물질)이, 연결선에는 SU(2) 게이지장이 배치된 형태로 정립되었다. 무차원 형태로 변환하면 해밀토니안은 다음과 같다:

W=HE(KS)+μHM+xHI(KS),W = H_E^{\text{(KS)}} + \mu H_M + x H_I^{\text{(KS)}},

여기서 HEH_E 는 색전기장 에너지, HMH_M 는 스태거드 질량 항, HIH_I 는 물질-게이지 상호작용(호핑) 항, μ=2mgx\mu = 2\frac{m}{g}\sqrt{x} 는 페르미온 질량을 나타내며, x=1g2a2x = \frac{1}{g^2 a^2} 는 상호작용 강도이다. 이 이론의 연속체 극한은 NN \to \inftyxx \to \infty 에 위치한다.

루프-스트링-하드론(LSH) 프레임워크

주요 과제 중 하나는 각 링크에 대한 게이지장의 힐베르트 공간이 무한차원이라는 점이다. 루프-스트링-하드론(LSH) 프레임워크는 플럭스의 루프, 분리된 전하를 연결하는 스트링, 그리고 하드론(사이트에 위치한 게이지 싱글렛 페르미온 쌍)과 같은 게이지 불변 변수들을 통해 이론을 재구성함으로써 이 문제를 해결합니다. LSH 기저에서는 가우스 법칙이 기저의 정의상 자동으로 성립하므로, 모든 기저 상태가 물리적으로 타당합니다. 각 격자 점은 루프 수 (nl,ni,no)(n_l, n_i, n_o), 유입 스트링, 유출 스트링을 나타내는 세 개의 양자수로 특징지어지며, 여기서 ni,no{0,1}n_i, n_o \in \{0,1\} 는 페르미온적이며 nl0n_l \geq 0 는 보손적이다. 이러한 값을 바탕으로, 짝수 사이트의 경우 nf(r)=ni(r)+no(r)n_f(r) = n_i(r) + n_o(r), 홀수 사이트의 경우 nf(r)=2[ni(r)+no(r)]n_f(r) = 2 - [n_i(r) + n_o(r)] 로 국소 페르미온 수가 정의된다.

전체 해밀토니안에서 양자 회로까지: 세 가지 핵심 근사법

이 양자 회로는 전체 SU(2) 해밀토니안을 정확히 시뮬레이션 하지는 않는다. 대신, 이 모델은 약한 결합 영역( x1x \gg 1 )에서 유효한 일련의 통제된 근사법을 적용합니다. 무엇이 근사되고 무엇이 근사되지 않는지를 이해하는 것이 필수적입니다:

근사 1 — HIH_I 의 약결합 극한: 전체 상호작용 해밀토니안 HI(LSH)H_I^{\text{(LSH)}} (식 [1] 의 식 (16)에는 1/nl+11/\sqrt{n_l+1} 와 같은 항을 통해 보손 양자수 nln_l 에 의존하는 선계수가 포함되어 있다. 약한 결합 영역( x1x \gg 1 )에서는 HEH_E 라는 전기 항이 역학을 지배하며, 이는 nln_l 가 큰 상태를 선호한다. nl1n_l \gg 1 일 때, 비율 nl/(nl+1)1n_l/(n_l+1) \to 1 와 이 모든 선계수는 1로 단순화된다. 그러면 상호작용 해밀토니안은 순전히 국소적인 가장 가까운 이웃 간 이동으로 환원된다:

HIapprox=r[σ(r)σ+(r+1)+σ+(r)σ(r+1)],H_I^{\text{approx}} = -\sum_r \left[\sigma^-(r)\sigma^+(r+1) + \sigma^+(r)\sigma^-(r+1)\right],

이는 nln_l 와 무관하며, 페르미온성 (ni,no)(n_i, n_o) 큐비트에만 작용합니다.

근사법 2 — HEH_E 의 전역 평균 플럭스: 전기 에너지는 각 링크의 nln_l 에 따라 달라진다. 약결합 진공 상태에서는 nln_l 가 크며 대략 균일합니다. 사이트별 nln_l 값을 단일 전역 평균값 nˉl\bar{n}_l 로 대체하여, HEH_E 가 각 사이트의 페르미온 배열에 비례하는 대각 위상이 되도록 한다:

HEapprox=NhE0+{r}(nˉl2+34)H_E^{\text{approx}} = N h_E^0 + \sum_{\{r'\}} \left(\frac{\bar{n}_l}{2} + \frac{3}{4}\right)

여기서 {r}\{r'\} 는 페르미온 구성 (ni=0,no=1)(n_i=0, n_o=1) 에 해당하는 사이트들에 대해 합을 취한 것이며, hE0h_E^0 는 무시해도 되는 전역 위상이다.

근사법 3 — 트로터화: 지속 시간이 δτ\delta_\tau 인 한 단계에 대한 시간 진화 연산자는 다음과 같이 분해된다:

eiδτWeim~HMeiδτHEapproxeicHIapproxe^{-i\delta_\tau W} \approx e^{-i\tilde{m} H_M} \, e^{-i\delta_\tau H_E^{\text{approx}}} \, e^{-ic H_I^{\text{approx}}}

여기서 c=δτxc = \delta_\tau x, m~=δτμ\tilde{m} = \delta_\tau \mu, θ=δτ(nˉl/2+3/4)\theta = -\delta_\tau(\bar{n}_l/2 + 3/4) 이다. 이 1차 트로터 분해는 δτ0\delta_\tau \to 0 일 때 사라지는 오차를 유발한다. 우리는 전체적으로 δτ=0.0015\delta_\tau = 0.0015 를 고정한다.

이 세 가지 근사법의 결과로, 사이트당 두 개의 페르미온 큐비트 (ni,no)(n_i, n_o) 만이 동역학적 성질을 가지게 되며, 보손 nln_l 의 자유도는 유효 매개변수로 흡수되었다. 이를 통해 NN 개의 격자 사이트에 대해 2N2N 개의 큐비트를 갖는 간결한 회로가 도출되며, 여기서 각 트로터 단계는 일정한 2-큐비트 게이트 깊이(단계당 13개)를 갖는다.

이 튜토리얼에서 시뮬레이션하는 내용

이 튜토리얼은 하드론의 전파 과정을 시뮬레이션합니다. 강한 결합 진공 상태(곱 상태)에서 시작하여, 격자 중심에 메손을 배치한 뒤 시간이 지남에 따라 변화를 관찰합니다. 차분 측정 프로토콜 — 중심 메손이 있는 경우와 없는 경우 모두에서 회로를 구동한 뒤 그 값을 서로 뺀다 — 는 하드웨어 노이즈와 경계 효과 모두로부터 코히어런트 하드론 신호를 분리해 낸다. 그 결과,閉じ込められた 메손의 호흡 모드에 특징적인 페르미온 밀도 진동의 광원뿔 패턴이 나타난다.


요구사항

이 튜토리얼을 시작하기 전에 다음을 설치하십시오:

  • Qiskit SDK v2.0 또는 그 이후 버전이며, 시각화 기능을 지원합니다
  • Qiskit Runtime v0.22 또는 그 이후 (pip install qiskit-ibm-runtime)
  • Pauli 전파 패키지 (pip install pauli-prop)
  • NumPy (pip install numpy)
  • Matplotlib (pip install matplotlib)

설정

먼저 필요한 라이브러리를 불러오고, LSH 시간 진화를 위한 양자 회로를 구성하는 헬퍼 함수를 정의하는 것으로 시작합니다. 회로 구성에는 세 가지 핵심 기능이 있습니다:

  1. pair_hamiltonian_circuit: 인접한 사이트 간의 근사 상호작용 해밀토니안에 대해 2-큐비트 유니터리 연산 UIU_I 을 구현합니다. 게이트 분해식은 다음과 같습니다. CNOTHRz(c)CNOTRz(c)CNOTHCNOT\text{CNOT} \to H \to R_z(-c) \to \text{CNOT} \to R_z(c) \to \text{CNOT} \to H \to \text{CNOT}.

  2. electric_hamiltonian_circuit: 각 사이트의 근사 전기장 에너지를 계산하기 위해 2-큐비트 유니터리 연산 UEU_E 을 구현합니다. 게이트 분해식은 다음과 같습니다. XRz(θ/2)CNOTRz(θ/2)CNOTRz(θ/2)XX \to R_z(\theta/2) \to \text{CNOT} \to R_z(-\theta/2) \to \text{CNOT} \to R_z(\theta/2) \to X.

  3. construct_circuit: 큐비트 연결을 관리하기 위해 SWAP 게이트를 사용하여 상호작용 항, 전기 항, 질량 항을 층층이 쌓아 완전한 트로터화 회로를 구성합니다.

# Import libraries

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import TwoSlopeNorm
from qiskit.circuit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from typing import Optional

import warnings

warnings.filterwarnings("ignore")
def pair_hamiltonian_circuit(c: float) -> QuantumCircuit:
    """Two-qubit unitary for the approximate interaction Hamiltonian H_I.

    Implements exp(-i * c * H_I^approx) for one pair of neighboring sites,
    where c = delta_tau * x.
    """
    qc_temp = QuantumCircuit(2)
    qc_temp.cx(1, 0)
    qc_temp.h(1)
    qc_temp.rz(-c, 1)
    qc_temp.cx(0, 1)
    qc_temp.rz(c, 1)
    qc_temp.cx(0, 1)
    qc_temp.h(1)
    qc_temp.cx(1, 0)
    return qc_temp


def electric_hamiltonian_circuit(theta: float) -> QuantumCircuit:
    """Two-qubit unitary for the approximate electric field Hamiltonian H_E.

    Implements exp(-i * theta * H_E^approx) for one lattice site,
    where theta = -delta_tau * (n_bar_l / 2 + 3/4).
    """
    qc_temp = QuantumCircuit(2)
    qc_temp.x(0)
    qc_temp.rz(theta / 2, 0)
    qc_temp.cx(0, 1)
    qc_temp.rz(-theta / 2, 1)
    qc_temp.cx(0, 1)
    qc_temp.rz(theta / 2, 1)
    qc_temp.x(0)
    return qc_temp


def construct_circuit(
    num_lattice_point: int,
    num_trotter_steps: int,
    c: float,
    theta: float,
    m: float,
    theory: Optional[int] = 2,
    barriers: Optional[bool] = False,
    measurement: Optional[bool] = False,
    add_init_state: Optional[bool] = True,
    inverse_mid: Optional[bool] = False,
) -> QuantumCircuit:
    """Construct the full Trotterized time-evolution circuit.

    Builds a circuit implementing n Trotter steps of the approximate SU(2)
    LSH Hamiltonian evolution. The qubit layout uses a zigzag ordering:
    n_i(0), n_i(1), n_o(0), n_o(1), n_i(2), n_i(3), n_o(2), n_o(3), ...
    which minimizes the number of SWAP layers needed.

    Args:
        num_lattice_point: Number of lattice sites
        (num_qubits = 2 * num_lattice_point).
        num_trotter_steps: Number of Trotter steps.
        c: Interaction parameter (delta_tau * x).
        theta: Electric field phase parameter.
        m: Mass parameter (m_tilde = delta_tau * mu).
        theory: 1 for single chain, 2 for SU(2). Default 2.
        barriers: Insert barriers between Trotter layers for
        visualization.
        measurement: Append measurements at the end.
        add_init_state: Prepare the half-filled (strong-coupling vacuum)
        initial state.
        inverse_mid: Swap the central sites
        (for differential measurement protocol).
    """
    num_qubits = theory * num_lattice_point
    qc = QuantumCircuit(num_qubits)

    if num_trotter_steps <= 0:
        return qc

    # --- Initial state preparation ---
    if add_init_state:
        i = 1
        while i < num_lattice_point:
            for j in range(theory):
                qc.x(i + j * num_lattice_point)
            i = i + 2
        if inverse_mid:
            mid_lattice_qubits = [num_qubits // 2 - 1, num_qubits // 2]
            qc.x(mid_lattice_qubits)
    else:
        i = 1
        while i < num_qubits - 1:
            qc.swap(i, i + 1)
            i = i + 4

    # --- Trotter steps ---
    for step in range(num_trotter_steps):
        if barriers:
            qc.barrier()

        # First SWAP layer (skipped at step 0 — absorbed into initial state mapping)
        if step > 0:
            i = 1
            while i < num_qubits - 1:
                qc.swap(i, i + 1)
                i = i + 4

        # First layer of pair interactions
        j = 0
        while j < num_qubits - 2:
            circ = pair_hamiltonian_circuit(c)
            qc.compose(circ, [j, j + 1], inplace=True)
            j = j + 2
        if num_lattice_point % 2 == 0:
            circ = pair_hamiltonian_circuit(c)
            qc.compose(circ, [j, j + 1], inplace=True)

        # Second SWAP layer
        i = 1
        while i < num_qubits - 1:
            qc.swap(i, i + 1)
            i = i + theory

        # Second layer of pair interactions
        j = 2
        while j < num_qubits - 3:
            circ = pair_hamiltonian_circuit(c)
            qc.compose(circ, [j, j + 1], inplace=True)
            j = j + 2
        if num_lattice_point % 2 != 0:
            circ = pair_hamiltonian_circuit(c)
            qc.compose(circ, [j, j + 1], inplace=True)

        # Third SWAP layer
        i = 3
        while i < num_qubits - 1:
            qc.swap(i, i + 1)
            i = i + 2 * theory

        # Electric field term
        if theta != 0:
            e_circ = electric_hamiltonian_circuit(theta)
            for j in range(num_lattice_point):
                qc.compose(e_circ, [2 * j, 2 * j + 1], inplace=True)

        # Mass term: Rz(-m_tilde) for even sites, Rz(m_tilde) for odd sites
        for q in range(num_qubits):
            if q % 2 == 0:
                qc.rz(-1 * m, q)
            else:
                qc.rz(m, q)

    if measurement:
        qc.measure_all()

    return qc
def get_probabilities(expval: float):
    """Convert a Z-expectation value to site occupation probability.

    Since <Z> = p(0) - p(1), the occupation probability is p(1) = (1 - <Z>) / 2.
    """
    p1 = round((1 - expval) / 2, 3)
    return p1


def get_number(expval_data, num_lattice_point):
    """Convert raw Z-expectation values to staggered fermion number n_f at each site.

    n_f(r) = n_i(r) + n_o(r)           for even r
    n_f(r) = 2 - [n_i(r) + n_o(r)]     for odd r

    The two qubits per site encode (n_i, n_o), and occupation probabilities
    give us <n_i> and <n_o>.
    """
    N = []
    for expvals in expval_data:
        Pstep = [get_probabilities(expval) for expval in expvals]
        Nstep = []
        for k in range(num_lattice_point):
            val = Pstep[2 * k] + Pstep[2 * k + 1]
            a = 2 * (k % 2) + (1 - 2 * (k % 2)) * val
            Nstep.append(float(a))
        N.append(Nstep)
    return N


def calculate_difference(N, N_mid, num_lattice_point):
    """Differential measurement protocol: |n_f(meson) - n_f(vacuum)|.

    Subtracting the vacuum (SCV) evolution from the meson evolution
    isolates the coherent hadron signal from symmetric noise and boundary effects.
    """
    N_diff = []
    for i in range(len(N)):
        Nstep_diff = []
        for j in range(num_lattice_point):
            Nstep_diff.append(abs(N[i][j] - N_mid[i][j]))
        N_diff.append(Nstep_diff)
    return N_diff

소규모 시뮬레이터 예시

먼저, 6개 사이트로 구성된 격자(12 큐비트)를 사용하여 소규모로 워크플로를 시연함으로써, 하드웨어에서 실행하기 전에 회로 구성을 검증하고 물리적 관측량을 파악할 수 있도록 하십시오.

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

해당 논문( x=100x = 100, m/g=1m/g = 1 )에서 연구된 약결합 영역에 해당하는 물리적 매개변수를 정의하십시오. 도출된 회로 매개변수는 다음과 같습니다:

  • c=δτx=0.15c = \delta_\tau \cdot x = 0.15 (상호작용 매개변수)
  • θ=δτ(nˉl/2+3/4)=0.01\theta = -\delta_\tau (\bar{n}_l/2 + 3/4) = 0.01 (전기장 위상)
  • m~=δτμ=0.03\tilde{m} = \delta_\tau \cdot \mu = 0.03 (질량 매개변수)

트로터 단계 수마다 두 가지 회로를 구성합니다. 하나는 중심에서 메손을 초기화하는 회로(inverse_mid=True)이고, 다른 하나는 강결합 진공을 준비하는 회로(inverse_mid=False)입니다. 미분 측정 프로토콜은 진공의 진화 과정을 차감하여 하드론 신호를 분리합니다.

# Physical / circuit parameters
num_lattice_point = 6  # 6 lattice sites -> 12 qubits for SU(2)
num_qubits = 2 * num_lattice_point
c = 0.15  # delta_tau * x
theta = 0.01  # electric field phase
m = 0.03  # m_tilde = delta_tau * mu
trotter_steps = range(1, 11)  # 10 Trotter steps

print(f"Lattice sites: {num_lattice_point}, Qubits: {num_qubits}")
print(f"Parameters: c={c}, theta={theta}, m_tilde={m}")

Output:

Lattice sites: 6, Qubits: 12
Parameters: c=0.15, theta=0.01, m_tilde=0.03
# Build circuits: meson initial state and vacuum (SCV) initial state
circuits_mid = [
    construct_circuit(
        num_lattice_point,
        d,
        c,
        theta,
        m,
        barriers=False,
        measurement=False,
        add_init_state=True,
        inverse_mid=True,
    )
    for d in trotter_steps
]

circuits = [
    construct_circuit(
        num_lattice_point,
        d,
        c,
        theta,
        m,
        barriers=False,
        measurement=False,
        add_init_state=True,
        inverse_mid=False,
    )
    for d in trotter_steps
]

# Visualize a single Trotter step
print(
    f"Circuit for 1 Trotter step: {circuits[0].num_qubits} qubits, depth {circuits[0].depth()}"
)
circuits[0].draw("mpl", fold=-1)

Output:

Circuit for 1 Trotter step: 12 qubits, depth 26
Output of the previous code cell

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

관측 가능한 양을 정의합니다: 모든 큐비트에 대한 단일 큐비트 ZZ 측정. Z\langle Z \rangle 에서 각 격자 점 rr 에 대한 입직 확률을 추출한 다음, 단계적 페르미온 수 nf(r)n_f(r) 를 구할 수 있습니다.

# Z observable on each qubit
observables = [
    SparsePauliOp("I" * i + "Z" + "I" * (num_qubits - i - 1))
    for i in range(num_qubits)
]

print(f"Number of observables: {len(observables)}")

Output:

Number of observables: 12

3단계: Qiskit primitives를 사용하여 실행하기

소규모에서 잡음이 없는 정확한 시뮬레이션을 수행하려면 를 사용하십시오 StatevectorEstimator .

from qiskit.primitives import StatevectorEstimator

estimator = StatevectorEstimator()

# Run meson circuits
pubs_mid = [(circuit, observables) for circuit in circuits_mid]
result_mid = estimator.run(pubs_mid).result()

# Run vacuum (SCV) circuits
pubs = [(circuit, observables) for circuit in circuits]
result = estimator.run(pubs).result()

# Extract expectation values
raw_expvals_mid = [
    result_mid[i].data.evs[::-1] for i in range(len(circuits_mid))
]
raw_expvals = [result[i].data.evs[::-1] for i in range(len(circuits))]

print(f"Computed expectation values for {len(raw_expvals)} Trotter steps")

Output:

Computed expectation values for 10 Trotter steps

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

기대값을 엇갈린 페르미온 수 nf(r,t)n_f(r, t) 로 변환하고, 미분 측정 프로토콜(메손 - 진공)을 적용하여 하드론 전파 히트맵을 생성합니다. 이는 참고 문헌의 그림 3의 구조를 재현한 것으로, x축에는 격자 점 rr, y축에는 트로터 단계(시간) tt, 색상 척도에는 nf(r,t)n_f(r,t) 가 표시되어 있습니다.

# Compute fermion numbers
N_mid_sim = get_number(raw_expvals_mid, num_lattice_point)
N_sim = get_number(raw_expvals, num_lattice_point)
N_diff_sim = calculate_difference(N_mid_sim, N_sim, num_lattice_point)

# --- Reproduce Figure 3 style: Staggered Fermionic Occupation Number Dynamics ---
fig, axes = plt.subplots(1, 3, figsize=(18, 5))

# Convert to numpy arrays for plotting
N_mid_arr = np.array(N_mid_sim)
N_arr = np.array(N_sim)
N_diff_arr = np.array(N_diff_sim)

# Color scheme
vmax = max(max(sublist) for sublist in N_arr)
vmin = -vmax

# Meson evolution
norm1 = TwoSlopeNorm(vmin=vmin, vcenter=0, vmax=vmax)
im0 = axes[0].imshow(
    N_mid_arr,
    aspect="auto",
    origin="lower",
    cmap="RdBu_r",
    norm=norm1,
    extent=[0, num_lattice_point, 0.5, len(trotter_steps) + 0.5],
)
axes[0].set_xlabel("Lattice site $r$", fontsize=12)
axes[0].set_ylabel("Trotter step $t$", fontsize=12)
axes[0].set_title("$n_f(r,t)$ — Meson initial state", fontsize=12)
plt.colorbar(im0, ax=axes[0], label="$n_f(r,t)$")

# Vacuum (SCV) evolution
im1 = axes[1].imshow(
    N_arr,
    aspect="auto",
    origin="lower",
    cmap="RdBu_r",
    norm=norm1,
    extent=[0, num_lattice_point, 0.5, len(trotter_steps) + 0.5],
)
axes[1].set_xlabel("Lattice site $r$", fontsize=12)
axes[1].set_ylabel("Trotter step $t$", fontsize=12)
axes[1].set_title("$n_f(r,t)$ — Vacuum (SCV)", fontsize=12)
plt.colorbar(im1, ax=axes[1], label="$n_f(r,t)$")

# Differential: meson - vacuum
norm2 = TwoSlopeNorm(vmin=vmin, vcenter=0, vmax=vmax)
im2 = axes[2].imshow(
    N_diff_arr,
    aspect="auto",
    origin="lower",
    cmap="RdBu_r",
    norm=norm2,
    extent=[0, num_lattice_point, 0.5, len(trotter_steps) + 0.5],
)
axes[2].set_xlabel("Lattice site $r$", fontsize=12)
axes[2].set_ylabel("Trotter step $t$", fontsize=12)
axes[2].set_title(
    "Staggered Fermionic Occupation Number Dynamics\n$|n_f^{\\mathrm{meson}} - n_f^{\\mathrm{vacuum}}|$",
    fontsize=12,
)
plt.colorbar(im2, ax=axes[2], label="$n_f(r,t)$")

plt.suptitle(
    f"Hadron propagation — {num_lattice_point}-site lattice (StatevectorEstimator)",
    fontsize=14,
    y=1.02,
)
plt.tight_layout()
plt.show()

Output:

Output of the previous code cell

대규모 하드웨어 예시

이제 IBM Quantum 하드웨어에서 30개 사이트 격자(60 큐비트)로 규모를 확장합니다. 이 규모에서, 10 트로터 단계로 구성된 회로는 3,400개 이상의 2-큐비트 게이트와 14,000개의 단일 큐비트 게이트로 이루어져 있습니다.

1~4단계 (단일 코드 블록으로 통합됨)

하드웨어 워크플로의 주요 측면:

  • 메손 및 진공 회로를 위한 10개의 트로터 단계 (드리프트를 최소화하기 위해 인터리브 처리됨)
  • —를 이용한 optimization_level=1 트랜스파일레이션: 회로 레이아웃은 이미 소자 토폴로지(선형 체인)와 동형이기 때문에, 라우팅 SWAP이 필요하지 않습니다. 이 트랜스파일러는 물리적 큐비트의 노이즈가 적은 체인을 선택하고, 게이트를 기본 게이트 집합으로 분해하는 데만 사용됩니다.
  • EstimatorV2 TREX 판독 오류 완화 및 파울리 회전 기법을 활용하여
  • Batch 모든 작업을 한꺼번에 제출하는 세션
# -------------------------Step 1: Define parameters & build circuits-------------------------

from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import EstimatorV2, Batch
from qiskit_ibm_runtime.options import (
    EstimatorOptions,
    ResilienceOptionsV2,
    TwirlingOptions,
    DynamicalDecouplingOptions,
)

service = QiskitRuntimeService()

num_lattice_point_hw = 30
num_qubits_hw = 2 * num_lattice_point_hw  # 60 qubits
c_hw = 0.15
theta_hw = 0.01
m_hw = 0.03
trotter_steps_hw = range(1, 11)  # 10 Trotter steps

# Build meson and vacuum circuits
circuits_mid_hw = [
    construct_circuit(
        num_lattice_point_hw,
        d,
        c_hw,
        theta_hw,
        m_hw,
        barriers=False,
        measurement=False,
        add_init_state=True,
        inverse_mid=True,
    )
    for d in trotter_steps_hw
]

circuits_hw = [
    construct_circuit(
        num_lattice_point_hw,
        d,
        c_hw,
        theta_hw,
        m_hw,
        barriers=False,
        measurement=False,
        add_init_state=True,
        inverse_mid=False,
    )
    for d in trotter_steps_hw
]

print(f"Built {len(circuits_hw)} circuit pairs for {num_qubits_hw} qubits")

# -------------------------Step 2: Transpile for hardware-------------------------
# The circuit topology is a linear chain, isomorphic to the device topology.
# We use optimization_level=1 since no routing SWAPs are needed — the transpiler
# only needs to select a low-noise qubit chain and decompose to native gates.

backend = service.backend("ibm_boston")

layout = [
    140,
    141,
    142,
    143,
    136,
    123,
    122,
    121,
    116,
    101,
    102,
    103,
    96,
    83,
    82,
    81,
    76,
    61,
    62,
    63,
    64,
    65,
    66,
    67,
    68,
    69,
    78,
    89,
    88,
    87,
    97,
    107,
    106,
    105,
    117,
    125,
    126,
    127,
    137,
    147,
    148,
    149,
    150,
    151,
    152,
    153,
    154,
    155,
    139,
    135,
    134,
    133,
    132,
    131,
    130,
    129,
    118,
    109,
    110,
    111,
]


pm = generate_preset_pass_manager(
    optimization_level=1, backend=backend, initial_layout=layout
)

isa_circuits_mid = pm.run(circuits_mid_hw)
isa_circuits = pm.run(circuits_hw)

print(f"Transpiled circuits. Example depth: {isa_circuits[0].depth()}")

# Define and layout-map observables
observables_hw = [
    SparsePauliOp("I" * i + "Z" + "I" * (num_qubits_hw - i - 1))
    for i in range(num_qubits_hw)
]

isa_observables_mid = [
    [obs.apply_layout(isa_circuits_mid[i].layout) for obs in observables_hw]
    for i in range(len(isa_circuits_mid))
]
isa_observables = [
    [obs.apply_layout(isa_circuits[i].layout) for obs in observables_hw]
    for i in range(len(isa_circuits))
]

# Build PUBs — interleave meson and vacuum for each Trotter step
isa_pubs_mid = [
    (circ, obs) for circ, obs in zip(isa_circuits_mid, isa_observables_mid)
]
isa_pubs = [(circ, obs) for circ, obs in zip(isa_circuits, isa_observables)]

pubs_to_execute = [
    [isa_pubs_mid[i], isa_pubs[i]] for i in range(len(isa_pubs))
]

# -------------------------Step 3: Execute on hardware-------------------------

twirling_options = TwirlingOptions(
    enable_gates=True,
    enable_measure=True,
    shots_per_randomization="auto",
    strategy="active-circuit",
)

resilience_options = ResilienceOptionsV2(
    measure_mitigation=True,  # TREX readout error mitigation
    zne_mitigation=False,  # ZNE turned off
)

dd_options = DynamicalDecouplingOptions(
    enable=False  # Circuit is sufficiently dense
)

options = EstimatorOptions(
    resilience=resilience_options,
    twirling=twirling_options,
    dynamical_decoupling=dd_options,
    default_shots=10_000,
)

ids = []
with Batch(backend=backend) as batch:
    for idx, pub in enumerate(pubs_to_execute):
        print(f"Submitting job for Trotter step {idx + 1}")
        estimator = EstimatorV2(mode=batch, options=options)
        estimator.skip_transpilation = True
        job = estimator.run(pub)
        ids.append(job.job_id())
    batch_id = batch.session_id

job_info = {"ids": ids, "batch_id": batch_id}
print(f"Submitted {len(ids)} jobs. Batch ID: {batch_id}")
print(ids)
# -------------------------Step 4: Post-process results-------------------------

jobs = [service.job(job_id) for job_id in ids]
results = [job.result() for job in jobs]

# Extract expectation values (index 0 = meson, index 1 = vacuum)
raw_expvals_mid_hw = [result[0].data.evs[::-1] for result in results]
raw_expvals_hw = [result[1].data.evs[::-1] for result in results]

# Compute fermion numbers and differential
N_mid_hw = get_number(raw_expvals_mid_hw, num_lattice_point_hw)
N_hw = get_number(raw_expvals_hw, num_lattice_point_hw)
N_diff_hw = calculate_difference(N_mid_hw, N_hw, num_lattice_point_hw)
N_diff_hw_arr = np.array(N_diff_hw)

fig, ax = plt.subplots(figsize=(10, 6))
vmax = np.abs(N_hw).max()
vmin = -vmax
norm = TwoSlopeNorm(vmin=vmin, vcenter=0, vmax=vmax)
im = ax.imshow(
    N_diff_hw_arr,
    aspect="auto",
    origin="lower",
    cmap="RdBu_r",
    norm=norm,
    extent=[0, 8, 0.5, len(trotter_steps_hw) + 0.5],
)
ax.set_xlabel("Lattice site $r$", fontsize=13)
ax.set_ylabel("Trotter step $t$", fontsize=13)
ax.set_title(
    "Staggered Fermionic Occupation Number Dynamics\nQuantum Simulation on IBM Hardware — 30-site lattice (60 qubits)",
    fontsize=13,
)
cbar = plt.colorbar(im, ax=ax)
cbar.set_label("$n_f(r,t)$", fontsize=12)
plt.tight_layout()
plt.show()

Output:

Output of the previous code cell

파울리 전파를 이용한 고전적 벤치마킹

파울리 전파법(PPM)은 하이젠베르크 모델에서 측정된 관측량을 회로 전체로 역전파함으로써, 양자 회로에 대한 잡음이 없는 고전적 시뮬레이션을 제공한다. 클리포드 층(CNOT, H, S, X 게이트)에서는 파울리 연산자가 항의 개수를 늘리지 않고 다른 파울리 연산자로 매핑됩니다. 비클리포드 층(회로 내의 RzR_z 게이트)은 분기를 유발할 수 있으며, 최악의 경우 항의 수가 두 배로 늘어날 수 있지만, 많은 분기들은 계수가 작아 생략할 수 있습니다.

...를 사용한 pauli-prop 워크플로는 다음과 같습니다:

  1. evolve_through_cliffords...를 사용하여 회로를 클리포드 부분과 비클리포드 부분으로 나누십시오.
  2. atol``propagate_through_circuit각 관측량을 를 사용하여 비클리포드 부분에 대해 전파하되, 최대 개의 max_terms 파울리 항까지 유지하고, 계수가 절단 임계값 보다 작은 항은 제외한다.
  3. Qiskit에 내장된 클리포드 연산 기능을 사용하여 클리포드 연산을 통해 결과를 도출합니다.
  4. 대각선 파울리 항( IIZZ 만 포함)의 계수를 합산하여 기대값을 구한다.

절단 임계값

propagate_through_circuit 매개변수는 atol 작은 파울리 분기를 얼마나 적극적으로 제거할지 결정합니다. 매우 엄격한 임계값(예를 들어, 1e-12)을 적용하면 거의 모든 분기를 유지하여 정확한 결과를 얻을 수 있지만, 회로 깊이가 깊어질수록 시뮬레이션 시간이 급격히 증가합니다. 논문에서 제시된 120-큐비트 시뮬레이션은 기본 설정에서 약 8.5 시간이 소요되었습니다. 1e-3임계값을 높이면(예를 들어, 또는 로 1e-6 ), 계수가 해당 값보다 낮은 항들이 제외되어 추적되는 항의 수가 대폭 줄어들고 계산 속도가 빨라집니다. 그 대가로 발생하는 것은 작고 제어 가능한 근사 오차이며, 이는 서로 다른 임계값에서 얻은 결과를 비교함으로써 검증할 수 있습니다.

import time
from pauli_prop import evolve_through_cliffords, propagate_through_circuit

# ── PPM Configuration ──
# Truncation threshold: controls the speed/accuracy trade-off.
PPM_THRESHOLD = 1e-3

# Maximum Pauli terms to track per observable (hard cap on memory/time)
PPM_MAX_TERMS = 66_000

print(f"PPM settings: atol={PPM_THRESHOLD}, max_terms={PPM_MAX_TERMS}")

# We propagate each single-qubit Z observable through each circuit.
# For PPM, we work with the un-transpiled circuits (ideal noiseless simulation).

observables_pp = [
    SparsePauliOp("I" * i + "Z" + "I" * (num_qubits_hw - i - 1))
    for i in range(num_qubits_hw)
]


def ppm_expectation_values(
    circuit, observables, max_terms=PPM_MAX_TERMS, atol=PPM_THRESHOLD
):
    """Compute expectation values of single-qubit Z observables
    via Pauli propagation.

    Args:
        circuit: The quantum circuit to simulate.
        observables: List of single-qubit Z observables.
        max_terms: Maximum number of Pauli terms to retain (hard cap).
        atol: Absolute tolerance — Pauli terms with coefficients below this
              value are discarded during propagation. Larger values give
              faster simulation at the cost of approximation accuracy.
    """
    circuit = circuit.decompose(["swap"])  # decompose SWAPs into 3 CX gates
    cliff, non_cliff = evolve_through_cliffords(circuit)

    evs = []
    for obs in observables:
        evolved_obs = propagate_through_circuit(
            obs, non_cliff, max_terms=max_terms, atol=atol, frame="h"
        )[0]
        evolved_obs.paulis = evolved_obs.paulis.evolve(cliff, frame="h")
        diagonal_mask = ~evolved_obs.paulis.x.any(axis=1)
        ev = float(evolved_obs.coeffs[diagonal_mask].sum().real)
        evs.append(ev)
    return np.array(evs)


# Run PPM for each Trotter step and record wall-clock time
pp_expvals_mid = []
pp_expvals = []
pp_times = []

for idx, d in enumerate(trotter_steps_hw):
    t_start = time.perf_counter()

    # Meson circuit
    evs_mid = ppm_expectation_values(circuits_mid_hw[idx], observables_pp)

    # Vacuum circuit
    evs_vac = ppm_expectation_values(circuits_hw[idx], observables_pp)

    elapsed = time.perf_counter() - t_start
    pp_times.append(elapsed)

    pp_expvals_mid.append(evs_mid[::-1])
    pp_expvals.append(evs_vac[::-1])

    print(f"Trotter step {d:2d}: {elapsed:.1f} s")

print(f"\nTotal PPM simulation time: {sum(pp_times):.1f} s")
print(f"Truncation threshold used: {PPM_THRESHOLD}")

Output:

PPM settings: atol=0.001, max_terms=66000
Trotter step  1: 5.0 s
Trotter step  2: 7.5 s
Trotter step  3: 11.2 s
Trotter step  4: 14.7 s
Trotter step  5: 18.3 s
Trotter step  6: 22.1 s
Trotter step  7: 25.6 s
Trotter step  8: 29.4 s
Trotter step  9: 33.2 s
Trotter step 10: 36.6 s

Total PPM simulation time: 203.6 s
Truncation threshold used: 0.001
# --- PPM simulation time vs. Trotter steps ---
fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(
    list(trotter_steps_hw),
    pp_times,
    "o-",
    color="tab:blue",
    linewidth=2,
    markersize=6,
)
ax.set_xlabel("Trotter step", fontsize=13)
ax.set_ylabel("Wall-clock time (s)", fontsize=13)
ax.set_title(
    "Pauli Propagation simulation time vs. Trotter steps\n(30-site lattice, 60 qubits)",
    fontsize=13,
)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Output:

Output of the previous code cell
# --- PPM heatmap and comparison with hardware ---
N_mid_pp = get_number(pp_expvals_mid, num_lattice_point_hw)
N_pp = get_number(pp_expvals, num_lattice_point_hw)
N_diff_pp = calculate_difference(N_mid_pp, N_pp, num_lattice_point_hw)

N_diff_pp_arr = np.array(N_diff_pp)

fig, axes = plt.subplots(1, 2, figsize=(18, 6))
vmax = np.abs(N_hw).max()
vmin = -vmax
norm = TwoSlopeNorm(vmin=vmin, vcenter=0, vmax=vmax)

# PPM result
im0 = axes[0].imshow(
    N_diff_pp_arr,
    aspect="auto",
    origin="lower",
    cmap="RdBu_r",
    norm=norm,
    extent=[0, num_lattice_point_hw, 0.5, len(trotter_steps_hw) + 0.5],
)
axes[0].set_xlabel("Lattice site $r$", fontsize=12)
axes[0].set_ylabel("Trotter step $t$", fontsize=12)
axes[0].set_title(
    "Pauli Propagation\n(classical noiseless simulation)", fontsize=12
)
plt.colorbar(im0, ax=axes[0], label="$n_f(r,t)$")

# Hardware result
im1 = axes[1].imshow(
    N_diff_hw_arr,
    aspect="auto",
    origin="lower",
    cmap="RdBu_r",
    norm=norm,
    extent=[0, num_lattice_point_hw, 0.5, len(trotter_steps_hw) + 0.5],
)
axes[1].set_xlabel("Lattice site $r$", fontsize=12)
axes[1].set_ylabel("Trotter step $t$", fontsize=12)
axes[1].set_title(
    "Quantum Simulation\n(IBM Hardware, readout error mitigation only)",
    fontsize=12,
)
plt.colorbar(im1, ax=axes[1], label="$n_f(r,t)$")

plt.suptitle(
    "Staggered Fermionic Occupation Number Dynamics — 30-site lattice",
    fontsize=14,
    y=1.02,
)
plt.tight_layout()
plt.show()

Output:

Output of the previous code cell

다음 단계

이 글이 흥미로웠다면, 다음 자료를 살펴보시는 것도 좋습니다:

권장사항

참조

[1] 원문 논문: Ilčić, Majumdar, Mathew 외. “잡음이 있는 양자 프로세서에서 관찰된 견고하고 일관된 비아벨 하드론 역학” arXiv:2602.18080 (2026)

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