Skip to main content
IBM Quantum Platform

트로터 오류를 줄이기 위한 다중 제품 공식

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


학습 성과

  • 다중 제품 공식(MPF)이 여러 개의 얕은 회로에서 얻은 기대값을 결합함으로써 해밀토니안 시뮬레이션에서 트로터 오차를 어떻게 줄이는가
  • MPF가 표준 상품 구성보다 유리한 경우와 그렇지 않은 경우
  • qiskit_addon_mpf 패키지를 사용하여 정적 및 동적 MPF 계수를 계산하는 방법
  • IBM Quantum® 하드웨어에서 트랜스파일링, 오류 완화 및 후처리를 포함한 MPF 워크플로를 처음부터 끝까지 실행하는 방법

전제조건


배경

다중 제품 포뮬러란 무엇인가요?

양자 컴퓨터에서 양자 시스템을 시뮬레이션할 때, 핵심 과제는 해밀토니안 HH 에 대한 시간 진화 연산자 eiHte^{-iHt} 를 근사화하는 것이다. 표준적인 접근 방식은 트로터-스즈키 분해라고도 알려진 곱 공식 (PF)을 사용하는 것이다. 이 방법은 H=a=1dFaH = \sum_{a=1}^d F_a 를 개별 유니터리 연산 eiFate^{-iF_a t} 을 효율적으로 구현할 수 있는 항들로 분해한 다음, 전체 진화 과정을 이러한 더 단순한 유니터리 연산들의 순서된 곱으로 근사화합니다.

1차 곱 공식(리-트로터)은 다음과 같습니다:

S1(t):=a=1deiFat,S_1(t) := \prod_{a=1}^d e^{-i F_a t},

이는 2차 오차를 초래합니다: S1(t)=eiHt+O(t2)S_1(t) = e^{-iHt} + \mathcal{O}(t^2). 고차 대칭 공식 S2χ(t)S_{2\chi}(t) (여기서 χ\chi 는 대칭 곱 공식의 차수를 나타냅니다 [1] 참조)은 eiHt+O(t2χ+1)e^{-iHt} + \mathcal{O}(t^{2\chi+1}) 로 더 빠르게 수렴하지만, 단계당 회로 깊이가 더 깊어지는 대가를 치릅니다.

고정 차수 χ\chi 에서의 오차를 줄이기 위해, 일반적으로 전체 진화 시간 ttkk 개의 작은 트로터 단계로 나눕니다. 각 단계에서는 eiHt/ke^{-iHt/k} 를 곱셈 공식으로 근사화하며, 이러한 단계들이 연결됩니다:

eiHt[S2χ(t/k)]k.e^{-iHt} \approx \left[S_{2\chi}(t/k)\right]^k.

2χ2\chi 차 대칭 공식의 경우, 잔여 트로터 오차는 O ⁣(t2χ+1/k2χ)\mathcal{O}\!\left(t^{2\chi+1} / k^{2\chi}\right) 의 비율로 증가합니다. 따라서 kk 를 증가시키면 트로터 오차를 빠르게 억제할 수 있지만, 동시에 회로 깊이가 선형적으로 증가하게 되며, 노이즈가 많은 하드웨어에서는 이는 게이트 노이즈의 누적량이 더 많아진다는 것을 의미합니다. 트로터 오차( kk 가 클수록 유리함)하드웨어 노이즈( kk 가 작을수록 유리함) 사이의 이러한 상충 관계야말로, 다중 제품 공식이 해결하기 위해 고안된 바로 그 문제입니다. MPF는 고정된 순서 χ\chikk 의 다양한 선택 결과를 결합하는 것에 관한 것이며, 기본 곱셈 공식의 순서를 변경하는 것은 아니라는 점에 유의하십시오.

다중 제품 공식(MPF) [1]은 각각 서로 다른 수의 트로터 단계 k1,k2,,krk_1, k_2, \ldots, k_r ( rr 단계 수 집합)를 사용하는 여러 개의 더 얕은 트로터 회로에서 얻은 기대값의 가중 선형 조합 을 구성합니다:

AMPF(t)=j=1rxjAkj(t),\langle A \rangle_{\text{MPF}}(t) = \sum_{j=1}^r x_j \, \langle A \rangle_{k_j}(t),

여기서 Akj(t)\langle A \rangle_{k_j}(t) 는 시간 tt 에서 관측량 AA 의 기대값으로, kjk_j 단계의 트로터 회로를 통해 추정된 것이며, 계수 {xj}j=1r\{x_j\}_{j=1}^r 는 조합에서 주요 트로터 오차 항들이 상쇄되도록 선택된다. 4단계 에서 이 식을 다시 살펴보면서, 트로터(Trotter)의 결과를 종합하기 위해 이를 명시적으로 계산할 것입니다. 실무상 가장 중요한 점은 MPF의 가장 깊은 회로에 필요한 단계 수가 kmaxk_{\max} 에 불과하다는 것인데, 이는 동일한 유효 트로터 오차를 직접 구하는 데 필요한 단일 kk 보다 훨씬 적은 수치입니다. 회로 깊이가 얕기 때문에 MPF 방식은 노이즈가 많은 하드웨어에 더 적합합니다.

계수는 어떻게 결정되나요?

MPF 계수에는 두 가지 계열이 있습니다:

정적 계수는 해밀토니안, 초기 상태 및 진화 시간과 무관하다. 이는 선행 트로터 오차 항들이 상쇄되도록 하는 선형 연립방정식 Ax=bAx = b 을 풀어서 구할 수 있다. 2χ2\chi 차 대칭 곱 공식과 함께 사용되는 일련의 트로터 단계 {kj}j=1r\{k_j\}_{j=1}^r 에 대해, kjk_j 의 역수 제곱으로 트로터 오차를 전개하면 다음과 같은 형태의 제약 방정식이 도출된다:

j=1rxj=1,j=1rxjkjηn=0(n=0,,r2),\sum_{j=1}^r x_j = 1, \quad \sum_{j=1}^r \frac{x_j}{k_j^{\eta_n}} = 0 \quad (n = 0, \ldots, r-2),

여기서 정수 지수 {ηn}\{\eta_n\} 는 선택된 곱셈 공식에 대한 연속적인 트로터 오차 항들의 차수를 나타낸다. 대칭적인2χ2\chi 차 PF의 경우, [S2χ(t/k)]k\left[S_{2\chi}(t/k)\right]^k 의 주 오차는 1/k2χ1/k^{2\chi} 의 비율을 따르며, 후속 보정은 1/k2χ+2,1/k2χ+4,1/k^{2\chi+2}, 1/k^{2\chi+4}, \ldots 의 비율을 따릅니다. 따라서 지수는 ηn=2χ+2n\eta_n = 2\chi + 2n 입니다. 비대칭 PF의 경우, 홀수 지수와 짝수 지수가 모두 기여하며 ηn=2χ+n\eta_n = 2\chi + n 입니다. 전체 도출 과정은 참고문헌 [1] 을 참조하십시오. 위 방정식계의 첫 번째 방정식은 편향 없음( kjk_j \to \infty 한 극한에서 MPF가 정확한 기대값을 재현함)을 보장하며, 나머지 r1r-1 방정식들은 첫 번째 r1r-1 트로터 오차 항들을 차례로 상쇄합니다. 결과로 얻어진 L1L_1 -노름 x1\|x\|_1 이 너무 클 경우(이는 표본 잡음을 증폭시킴), 대신 x1\|x\|_1 을 상한으로 설정하고 Axb\|Ax - b\| 을 최소화하는 근사 최적화 문제를 풀 수 있습니다.

동적 계수 [2], [3]은 또한 해밀토니안, 초기 상태 및 진화 시간 tt 에 따라 달라진다. 이 계수들은 실제 시간 진화 상태와 MPF 근사치 사이의 프로베니우스 노름 거리를 최소화한다:

ρ(t)μD(t)F2=1+i,jMij(t)xi(t)xj(t)2iLi(t)xi(t),\|\rho(t) - \mu^D(t)\|_F^2 = 1 + \sum_{i,j} M_{ij}(t)\, x_i(t)\, x_j(t) - 2\sum_i L_i(t)\, x_i(t),

여기서 Mij(t)=Tr[ρki(t)ρkj(t)]M_{ij}(t) = \mathrm{Tr}[\rho_{k_i}(t)\,\rho_{k_j}(t)] 는 서로 다른 단계 수 ki,kjk_i, k_j 에 대한 트로터 진화 상태들 간의 중첩을 나타내는 그램 행렬이며, Li(t)=Tr[ρ(t)ρki(t)]L_i(t) = \mathrm{Tr}[\rho(t)\,\rho_{k_i}(t)] 는 (근사적인) 정확한 상태와의 중첩을 측정한다. qiskit_addon_mpf이 튜토리얼에서는 텐서 네트워크 기법, 특히 의 TeNPy-based 백엔드를 사용하여 이러한 수치를 효율적으로 계산합니다.

MPF는 언제 사용해야 할까요?

MPF는 다음과 같은 경우에 가장 큰 이점을 제공합니다:

  • 회로 깊이가 병목 지점입니다. 하드웨어 노이즈로 인해 실행 가능한 깊이가 제한되는 경우, MPF를 사용하여 더 얕은 회로에서도 더 높은 유효 트로터 정확도를 달성하십시오.
  • 완전한 상태 준비가 아니라 정확한 기대값이 필요합니다. MPF는 기대값 수준에서 작동합니다. 즉, 양자 상태가 아닌 고전적인 수치를 결합합니다. 따라서 Estimator 프리미티브를 사용할 때 관측 가능한 추정값을 구하는 데 이상적입니다.
  • 트로터 스텝 횟수를 적당히 조합합니다. 일반적으로 r=3r = 355 의 서로 다른 단계 수 kjk_j 를 조합하면, x1\|x\|_1 를 관리 가능한 수준으로 유지하면서 몇 가지 주요 트로터 오차 항을 상쇄하는 데 충분합니다.

MPF가 도움이 되지 않을 수 있는 경우

  • 진화 시간이 매우 짧습니다. tt 가 충분히 작아서 단일 저차 트로터 공식만으로도 이미 정확도가 보장되는 경우, 여러 회로를 실행하는 데 드는 부하가 불필요해집니다.
  • 상태 준비 작업. MPF는 보정된 양자 상태가 아니라 보정된 기대값 을 산출합니다. 실제 시간 경과에 따른 상태(예를 들어, 다른 양자 서브루틴의 입력으로 사용하기 위한 경우)가 필요한 경우에는 MPF를 적용할 수 없습니다.
  • 수렴 조건을 위반하는 트로터 걸음 수. 정적 계수 도출 과정에서는 각 개별 [S2χ(t/kj)]kj\left[S_{2\chi}(t/k_j)\right]^{k_j}t/kjt/k_j 의 급수로 전개하는데, 이 전개는 t/kmin1t/k_{\min} \lesssim 1 일 때만 잘 수렴한다. 주어진 tt 에 비해 kmink_{\min} 를 너무 작게 선택하면, 가장 얕은 회로가 섭동 영역을 훨씬 벗어나게 되며, MPF가 상쇄하지 못한 고차 오차 항이 커지게 되고, 상쇄를 위해 큰 계수가 필요할 수 있다. L1L_1 -노름 x1\|x\|_1 은 실용적인 진단 기준이 됩니다. 즉, x11\|x\|_1 \gg 1 일 때, 샘플링 오버헤드 x12\propto \|x\|_1^2 가 Trotter 오차 감소 효과를 상쇄할 수 있습니다. 자세한 내용은 트로터 계단 선택 가이드를 참조하세요.

이 튜토리얼에서 다루는 내용

이 튜토리얼에서는 MPF 워크플로우의 전 과정을 두 단계에 걸쳐 단계별로 안내합니다. 먼저, 소규모 시뮬레이터 예제 (10-큐비트 하이젠베르크 사슬)를 통해 문제를 설정하고, 정적 및 동적 MPF 계수를 계산하며, 그 결과로 얻은 기대값을 정확한 대각화 결과와 비교하는 방법을 보여줍니다. 이어서, 대규모 하드웨어 예제 (50-큐비트 XXZ 체인)를 통해 트랜스파일링 방법, 오류 완화 기능을 갖춘 IBM Quantum 하드웨어에서 실행하는 방법, 그리고 MPF 계수를 사용하여 결과를 후처리하는 방법을 보여줍니다. 이 문서 전반에 걸쳐 표준 Qiskit 도구와 함께 이 qiskit_addon_mpf 패키지를 사용합니다.


요구사항

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

  • Qiskit SDK v2.0 또는 그 이후 버전, 시각화 기능 지원
  • Qiskit Runtime v0.22 이상 (pip install qiskit-ibm-runtime)
  • Qiskit Aer 시뮬레이터 (pip install qiskit-aer)
  • TeNPy 백엔드를 사용하는 MPF Qiskit 애드온 (pip install "qiskit-addon-mpf[tenpy]")
  • Qiskit 애드온 유틸리티 (pip install qiskit-addon-utils)
  • SciPy (pip install scipy)

설정

아래에서는 이 튜토리얼 전반에 걸쳐 사용된 모든 패키지 임포트 문들을 하나의 셀에 모아 놓았습니다. XXPlusYYGate또한 인접한 rxxryy 회전을 하나의 로 병합하는 트랜스파일러 패스를 CollectAndCollapse 정의합니다. 이 처리는 1단계에서 회로를 구성할 때(게이트 수를 적게 유지하기 위해)뿐만 아니라, 4단계에서 동적 MPF를 위한 계층적 구조를 추출할 때도 간접적으로 적용됩니다( TeNPy 는 융합되지 않은 회전 쌍이 아닌 2-큐비트 게이트를 기대하기 때문입니다).

import warnings

import numpy as np
import matplotlib.pyplot as plt
from functools import partial
from copy import deepcopy

from qiskit import QuantumCircuit
from qiskit.quantum_info import Pauli, SparsePauliOp, Statevector
from qiskit.synthesis import SuzukiTrotter
from qiskit.transpiler import CouplingMap, PassManager
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.circuit.library import XXPlusYYGate
from qiskit.transpiler.passes.optimization.collect_and_collapse import (
    CollectAndCollapse,
    collect_using_filter_function,
    collapse_to_operation,
)

from qiskit_aer import AerSimulator
from qiskit_ibm_runtime import EstimatorV2 as Estimator, QiskitRuntimeService

from qiskit_addon_utils.problem_generators import (
    generate_xyz_hamiltonian,
    generate_time_evolution_circuit,
)
from qiskit_addon_utils.slicing import slice_by_depth
from qiskit_addon_mpf.static import setup_static_lse
from qiskit_addon_mpf.dynamic import setup_dynamic_lse
from qiskit_addon_mpf.costs import (
    setup_exact_problem,
    setup_sum_of_squares_problem,
    setup_frobenius_problem,
)
from qiskit_addon_mpf.backends.tenpy_layers import (
    LayerModel,
    LayerwiseEvolver,
)
from qiskit_addon_mpf.backends.tenpy_tebd import MPOState, MPS_neel_state

from scipy.linalg import expm

# Suppress TeNPy's `unit_cell_width` future-API warning. The default
# (`unit_cell_width=len(sites)`) is correct for Chain lattices, which is what
# `CouplingMap.from_line(...)` produces here, so the warning is informational.
warnings.filterwarnings(
    "ignore",
    message=r".*unit_cell_width.*",
    category=UserWarning,
)


# --- Helper: collect XX + YY rotations into a single gate ---
def filter_function(node):
    return node.op.name in {"rxx", "ryy"}


collect_function = partial(
    collect_using_filter_function,
    filter_function=filter_function,
    split_blocks=True,
    min_block_size=1,
)


def collapse_to_xx_plus_yy(block):
    param = 0.0
    for node in block.data:
        param += node.operation.params[0]
    return XXPlusYYGate(param)


collapse_function = partial(
    collapse_to_operation,
    collapse_function=collapse_to_xx_plus_yy,
)

pm = PassManager()
pm.append(CollectAndCollapse(collect_function, collapse_function))

소규모 시뮬레이터 예시

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

먼저, 네엘 상태 010101\vert 0101\ldots01 \rangle 를 초기 상태로 삼아, 직선 상의 10-큐비트 하이젠베르크 모델을 고려합니다. 해밀토니안은 다음과 같습니다:

H^Heis=Ji=1L1(XiXi+1+YiYi+1+ZiZi+1),\hat{\mathcal{H}}_{\text{Heis}} = J \sum_{i=1}^{L-1} \left(X_i X_{i+1} + Y_i Y_{i+1} + Z_i Z_{i+1}\right),

여기서 JJ 는 최인접 이웃 결합 강도이다. 우리는 체인의 중간에 위치한 한 쌍의 큐비트에 대해 ZZ 상관함수 ZL/21ZL/2Z_{L/2-1} Z_{L/2} 를 측정하고, 2차 곱 공식 kj=[1,2,4]k_j = [1, 2, 4] 을 적용한 트로터 단계를 사용합니다.

L = 10

# Generate coupling map and Hamiltonian
coupling_map = CouplingMap.from_line(L, bidirectional=False)

hamiltonian = generate_xyz_hamiltonian(
    coupling_map,
    coupling_constants=(1.0, 1.0, 1.0),
    ext_magnetic_field=(0.0, 0.0, 0.0),
)
print(hamiltonian)

Output:

SparsePauliOp(['IIIIIIIXXI', 'IIIIIIIYYI', 'IIIIIIIZZI', 'IIIIIXXIII', 'IIIIIYYIII', 'IIIIIZZIII', 'IIIXXIIIII', 'IIIYYIIIII', 'IIIZZIIIII', 'IXXIIIIIII', 'IYYIIIIIII', 'IZZIIIIIII', 'IIIIIIIIXX', 'IIIIIIIIYY', 'IIIIIIIIZZ', 'IIIIIIXXII', 'IIIIIIYYII', 'IIIIIIZZII', 'IIIIXXIIII', 'IIIIYYIIII', 'IIIIZZIIII', 'IIXXIIIIII', 'IIYYIIIIII', 'IIZZIIIIII', 'XXIIIIIIII', 'YYIIIIIIII', 'ZZIIIIIIII'],
              coeffs=[1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,
 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j,
 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j])
# Observable: ZZ on the middle pair of qubits
observable = SparsePauliOp.from_sparse_list(
    [("ZZ", (L // 2 - 1, L // 2), 1.0)], num_qubits=L
)
print(observable)

Output:

SparsePauliOp(['IIIIZZIIII'],
              coeffs=[1.+0.j])
# MPF parameters
mpf_trotter_steps = [1, 2, 4]
order = 2
symmetric = False

trotter_times = np.arange(0.5, 1.55, 0.1)
exact_evolution_times = np.arange(trotter_times[0], 1.55, 0.05)

트로터 회로 만들기

우리는 각 시간점과 각 트로터 단계 수에 대해 근사 트로터 시간 진화를 구현하는 회로를 생성합니다. ‘설정’ 섹션에서 정의된 이 CollectAndCollapse 패스는 XX 및 YY 회전을 단일 XX+YY 게이트로 통합하여, 이후 더 효율적인 텐서 네트워크 시뮬레이션을 준비합니다.

# Initial Neel state preparation
initial_state_circ = QuantumCircuit(L)
initial_state_circ.x([i for i in range(L) if i % 2 != 0])


all_circs = []
for total_time in trotter_times:
    mpf_trotter_circs = [
        generate_time_evolution_circuit(
            hamiltonian,
            time=total_time,
            synthesis=SuzukiTrotter(reps=num_steps, order=order),
        )
        for num_steps in mpf_trotter_steps
    ]

    mpf_trotter_circs = pm.run(
        mpf_trotter_circs
    )  # Collect XX and YY into XX + YY

    mpf_circuits = [
        initial_state_circ.compose(circuit) for circuit in mpf_trotter_circs
    ]
    all_circs.append(mpf_circuits)
mpf_circuits[-1].draw("mpl", fold=-1)

Output:

Output of the previous code cell

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

소규모 예제로는 Aer 시뮬레이터를 다룰 것입니다. 회로가 실행 준비가 되기 전에 두 가지 변환이 이루어집니다:

  1. 해밀토니안 시뮬레이션 단계에서의 게이트 수집. XXPlusYYGate‘Setup’ 셀에서는 인접한 rxxryy 회전을 하나의 로 통합하는 패스를 CollectAndCollapse 구축했습니다. 우리는 1단계(호출 pm.run(...) )에서 트로터 회로를 구축할 때 이미 이 단계를 적용했습니다. 이를 통해 2-큐비트 게이트의 수를 줄일 수 있을 뿐만 아니라, 이후 동적 계수 계산을 위한 텐서 네트워크 시뮬레이션에 더 적합한 구조를 얻을 수 있다.

  2. 시뮬레이터의 ISA로 전환합니다. 아래에서는 Qiskit 프리셋 패스 매니저를 실행하여 optimization_level=3 각 Trotter 회로를 시뮬레이터의 명령어 집합 아키텍처(ISA) 수준으로 변환합니다.

aer_sim = AerSimulator()
pm_sim = generate_preset_pass_manager(backend=aer_sim, optimization_level=3)

isa_circs_all_times = [
    pm_sim.run([deepcopy(c) for c in mpf_circuits])
    for mpf_circuits in all_circs
]

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

소규모 예제의 경우, ISA로 변환된 트로터 회로를 Aer가 지원하는 프리미티브를 EstimatorV2 통해 실행합니다. 이렇게 하면 각 (kj,t)(k_j, t) 쌍에 대해 잡음이 없는 기준값을 얻을 수 있습니다. 이 값들이 바로 MPF가 4단계에서 결합할 Akj(t)\langle A \rangle_{k_j}(t) 값들입니다. 나중에 각 개별 제품 공식과 MPF의 전체 시계열 곡선을 그래프로 나타낼 수 있도록 진화 시점을 훑어봅니다.

estimator = Estimator(mode=aer_sim)

mpf_expvals_all_times, mpf_stds_all_times = [], []
for isa_circuits in isa_circs_all_times:
    result = estimator.run(
        [(circuit, observable) for circuit in isa_circuits], precision=0.005
    ).result()
    mpf_expvals_all_times.append([res.data.evs for res in result])
    mpf_stds_all_times.append([res.data.stds for res in result])

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

4단계에서는 MPF가 실제로 구축됩니다. 비록 여기서는 계수 xjx_j* 가* 계산되지만(동적 변형의 경우 이 계산이 상당한 연산 부하를 유발할 수 있음), 개념적으로 이는 3단계의 양자 측정 결과를 하나의 보정된 기대값으로 결합하는 고전적인 방법이기 때문에, 우리는 전체 계수 및 결합 작업 흐름을 후처리 단계로 간주합니다.

MPF가 실제 동역학을 얼마나 잘 재현하는지 평가하기 위해, 먼저 해밀토니안을 직접 지수 함수화하여 시간 경과에 따른 정확한 기대값을 계산한다. 이 문제가 해결 가능한 이유는 L=10L = 10 이기 때문이며, 아래의 대규모 하드웨어 예시에서는 대신 텐서 네트워크 추정값에 의존해야 할 것입니다.

exact_expvals = []
for t in exact_evolution_times:
    exp_H = expm(-1j * t * hamiltonian.to_matrix())
    initial_state = Statevector(initial_state_circ).data
    time_evolved_state = exp_H @ initial_state

    exact_obs = (
        time_evolved_state.conj()
        @ observable.to_matrix()
        @ time_evolved_state
    ).real
    exact_expvals.append(exact_obs)

정적 MPF 계수

정적 MPF는 진화 시간, 해밀토니안 및 초기 상태와 무관한 계수 xjx_j 를 사용합니다. 배경에서 설명한 선형 방정식 시스템 Ax=bAx = b 을 세우고, 계수를 구합니다. 행렬 AA 은 트로터 단계 수 kjk_j, 곱 공식의 차수 χ\chi, 그리고 공식이 대칭인지 여부(이는 지수 ηn\eta_n 를 결정함)에 의해 결정된다.

이 소규모 예제에서는 비대칭 차수( 2χ=22\chi=2 )의 스즈키-트로터 공식(따라서 χ=1\chi=1ηn=2+n\eta_n = 2 + n, 결과적으로 η0=2,η1=3\eta_0 = 2,\, \eta_1 = 3 )을 적용한 kj=[1,2,4]k_j = [1, 2, 4] 를 사용합니다. 이에 따라 시스템은 다음과 같이 표현됩니다:

A=[11111221421123143],b=[100].A = \begin{bmatrix} 1 & 1 & 1\\ 1 & \frac{1}{2^2} & \frac{1}{4^2} \\ 1 & \frac{1}{2^3} & \frac{1}{4^3} \\ \end{bmatrix}, \quad b = \begin{bmatrix} 1 \\ 0 \\ 0 \end{bmatrix}.

첫 번째 행은 편향 없음( jxj=1\sum_j x_j = 1 )을 보장하며, 두 번째와 세 번째 행은 각각 선행 트로터 오차 항 1/k21/k^2 과 차순위 트로터 오차 항 1/k31/k^3 을 상쇄합니다.

LSE 설정하기

우리는 에서 qiskit_addon_mpf.static 를 사용하여 setup_static_lse 앞서 설명한 행렬 AA 과 우변 벡터 bb 를 구성합니다. 행렬 AAkjk_j 뿐만 아니라, 곱의 공식 선택 — 특히 그 차수 χ\chi대칭 여부 — 에 따라서도 달라집니다. 이 symmetric 플래그는 지수 패턴 ηn\eta_n 을 제어합니다(대칭 식은 짝수 차수의 트로터 오차 항만을 생성합니다; 참고문헌 [1] 참조). 참고 문헌 [2] 에 제시된 바와 같이, 기본 PF가 대칭적일 때조차 를 설정하는 symmetric=True 것이 엄밀히 말해 반드시 필요한 것은 아니라는 점에 유의해야 한다. 비대칭 LSE는 여전히 유효하며(단, 불필요한 추가 제약 조건을 부과할 뿐이다).

이 예제에서는 1단계에서 이미 와 symmetric = False 를 설정해 order = 2 두었습니다.

lse = setup_static_lse(mpf_trotter_steps, order=order, symmetric=symmetric)

구성된 행렬 AA 과 벡터 bb 을 검토하여, 이들이 위에 기술된 시스템과 일치하는지 확인하십시오.

lse.A

Output:

array([[1.      , 1.      , 1.      ],
       [1.      , 0.25    , 0.0625  ],
       [1.      , 0.125   , 0.015625]])
lse.b

Output:

array([1., 0., 0.])

LSE를 바탕으로, ( x=A1bx = A^{-1}b 의 직접 해법)을 통해 lse.solve() 정적 계수 xjx_j 를 구합니다.

mpf_coeffs = lse.solve()
print(
    f"The static coefficients associated with the ansatze are: {mpf_coeffs}"
)

Output:

The static coefficients associated with the ansatze are: [ 0.04761905 -0.57142857  1.52380952]
정확한 모델을 사용하여 xx 에 최적화하십시오

x=A1bx = A^{-1}b 을 계산하는 대신, setup_exact_model을 사용하여 LSE를 제약 조건으로 삼고, 그 최적 해가 xx 을 산출하는 cvxpy.Problem 인스턴스를 생성할 수 있습니다.

model_exact, coeffs_exact = setup_exact_problem(lse)
model_exact.solve()
print(coeffs_exact.value)

Output:

[ 0.04761905 -0.57142857  1.52380952]
print(
    "L1 norm of the exact coefficients:",
    np.linalg.norm(coeffs_exact.value, ord=1),
)

Output:

L1 norm of the exact coefficients: 2.1428571428556378
xx 에 대한 근사 모델을 사용한 최적화

선택된 kjk_j 값 집합에 대한 L1L_1 규범이 너무 높다고 판단될 수도 있습니다. 만약 그런 상황이고 다른 kjk_j 값을 선택할 수 없다면, Axb\|Ax - b\| 을 최소화하면서 L1L_1 -노름을 선택한 임계값으로 제한하는 근사 해법을 사용할 수 있습니다. ‘근사 모델 사용 방법’ 가이드를 확인해 보세요.

model_approx, coeffs_approx = setup_sum_of_squares_problem(
    lse, max_l1_norm=1.5
)
model_approx.solve()
print(coeffs_approx.value)
print(
    "L1 norm of the approximate coefficients:",
    np.linalg.norm(coeffs_approx.value, ord=1),
)

Output:

[-1.10294118e-03 -2.48897059e-01  1.25000000e+00]
L1 norm of the approximate coefficients: 1.5

동적 MPF 계수

정적 MPF는 해밀토니안 및 상태에 구애받지 않는 방식으로 트로터 오차 항을 상쇄하므로, 주어진 해밀토니안과 초기 상태에 대해 반드시 가능한 한 가장 작은 근사 오차를 산출하는 것은 아니다. qiskit_addon_mpf반면, 동적 MPF(참고문헌 [2], [3] )는 각 시간 tt 에서 프로베니우스 노름 거리 ρ(t)μD(t)F2\|\rho(t) - \mu^D(t)\|_F^2 를 최소화하는 시간 의존적 계수 xi(t)x_i(t) 를 구합니다. ‘배경’ 절에서 설명한 바와 같이, 이를 위해서는 트로터 진화 상태들 간의 중첩 행렬 Mij(t)M_{ij}(t) 과 정확한 상태와의 중첩 Li(t)L_i(t) 이 필요하며, 이 두 가지 모두 본 논문에서 텐서 네트워크( TeNPy ) 백엔드를 사용하여 추정합니다.

동적 LSE를 설정하려면 다음 세 가지 요소가 필요합니다:

  1. 이 애드온이 각 kjk_j 에 대해 실행하여 ρkj(t)\rho_{k_j}(t) 를 MPS/MPO 형식으로 생성하는 근사적 에볼버 팩토리입니다. slice_by_depth우리는 순서- 22 트로터 회로의 층별 구조(각 층당 하나씩)를 바탕으로 이를 구축하며, 이를 TeNPy 의 절단 매개변수를 갖는 형태로 LayerwiseEvolver 감싸서 구성합니다.
  2. 고정밀도 기준 ρ(t)\rho(t) 를 생성하는 정확한 진화기 팩토리입니다. 우리는 정확한 진화를 대리하기 위해 작은 시간 단계의 4차 스즈키-트로터 회로(dt=0.1, order=4)를 사용합니다.
  3. TeNPy 시뮬레이션에 초기값을 제공하는 아이덴티티 팩토리와 초기 상태 MPS입니다.

아래 코드는 근사적 진화 알고리즘 생성기를 구현합니다.

# Create approximate time-evolution circuits
single_2nd_order_circ = generate_time_evolution_circuit(
    hamiltonian, time=1.0, synthesis=SuzukiTrotter(reps=1, order=order)
)
single_2nd_order_circ = pm.run(single_2nd_order_circ)  # collect XX and YY

# Find layers in the circuit
layers = slice_by_depth(single_2nd_order_circ, max_slice_depth=1)

# Create tensor network models
models = [
    LayerModel.from_quantum_circuit(layer, conserve="Sz") for layer in layers
]

# Create the time-evolution object
approx_factory = partial(
    LayerwiseEvolver,
    layers=models,
    options={
        "preserve_norm": False,
        "trunc_params": {
            "chi_max": 64,
            "svd_min": 1e-8,
            "trunc_cut": None,
        },
        "max_delta_t": 2,
    },
)
Warning

텐서 네트워크 시뮬레이션의 세부 사항을 결정하는 LayerwiseEvolver 의 옵션은 잘못 정의된 최적화 문제를 설정하지 않도록 신중하게 선택해야 합니다.

dt=0.1우리는 작은 시간 간격을 사용하여 4차 스즈키-트로터 공식을 통해 정확한 시간 진화 상태를 근사화한다. TeNPy 의 트런케이션 매개변수는 정확도에 영향을 미칠 수 있으므로, 다양한 값을 시도해 보는 것이 중요합니다.

single_4th_order_circ = generate_time_evolution_circuit(
    hamiltonian, time=1.0, synthesis=SuzukiTrotter(reps=1, order=4)
)
single_4th_order_circ = pm.run(single_4th_order_circ)
exact_model_layers = [
    LayerModel.from_quantum_circuit(layer, conserve="Sz")
    for layer in slice_by_depth(single_4th_order_circ, max_slice_depth=1)
]

exact_factory = partial(
    LayerwiseEvolver,
    layers=exact_model_layers,
    dt=0.1,
    options={
        "preserve_norm": False,
        "trunc_params": {
            "chi_max": 64,
            "svd_min": 1e-8,
            "trunc_cut": None,
        },
        "max_delta_t": 2,
    },
)

마지막으로, 초기 MPO 상태를 산출하는 를 identity_factory 정의하고, 층상 트로터 모델에서 사용하는 격자와 일치하는 MPS로서 네엘 초기 상태를 준비한다.

def identity_factory():
    return MPOState.initialize_from_lattice(models[0].lat, conserve=True)


mps_initial_state = MPS_neel_state(models[0].lat)

공장이 설정되었으므로, 이제 각 진화 시점에서 동적 계수를 계산합니다. 각 tt 에 대해, setup_dynamic_lse TeNPy, 를 통해 관련 중첩 행렬을 구축하고, setup_frobenius_problem 프로베니우스 노름 비용을 최소화하는 를 cvxpy.Problem 반환합니다. mpf_dynamic_coeffs_list해법기는 해당 시간에 맞춰 조정된 계수 xj(t)x_j(t) 를 반환하며, 우리는 이를 에 수집합니다. 주어진 tt 에 대해 솔버가 실패할 경우, 계수를 0으로 설정하여 루프가 계속 진행되도록 합니다.

mpf_dynamic_coeffs_list = []
for t in trotter_times:
    print(f"Computing dynamic coefficients for time={t}")
    lse = setup_dynamic_lse(
        mpf_trotter_steps,
        t,
        identity_factory,
        exact_factory,
        approx_factory,
        mps_initial_state,
    )
    problem, coeffs = setup_frobenius_problem(lse)
    try:
        problem.solve()
        mpf_dynamic_coeffs_list.append(coeffs.value)
    except Exception as error:
        mpf_dynamic_coeffs_list.append(np.zeros(len(mpf_trotter_steps)))
        print(error, "Calculation Failed for time", t)
    print("")

Output:

Computing dynamic coefficients for time=0.5

Computing dynamic coefficients for time=0.6

Computing dynamic coefficients for time=0.7

Computing dynamic coefficients for time=0.7999999999999999

Computing dynamic coefficients for time=0.8999999999999999

Computing dynamic coefficients for time=0.9999999999999999

Computing dynamic coefficients for time=1.0999999999999999

Computing dynamic coefficients for time=1.1999999999999997

Computing dynamic coefficients for time=1.2999999999999998

Computing dynamic coefficients for time=1.4

Computing dynamic coefficients for time=1.4999999999999998

트로터의 기대값과 MPF 계수를 결합한다

이제 각 계수 집합(정적-정확, 정적-근사, 동적)에 대해 AMPF(t)=jxjAkj(t)\langle A \rangle_{\text{MPF}}(t) = \sum_j x_j \, \langle A \rangle_{k_j}(t) 를 계산하고, 회로별 표준 오차를 전파한 뒤, 그 결과로 얻은 시계열을 정확한 대각화 곡선과 비교하여 그래프로 표시합니다.

sym = {1: "^", 2: "s", 4: "p"}
# Get expectation values at all times for each Trotter step
for k, step in enumerate(mpf_trotter_steps):
    trotter_curve, trotter_curve_error = [], []
    for trotter_expvals, trotter_stds in zip(
        mpf_expvals_all_times, mpf_stds_all_times
    ):
        trotter_curve.append(trotter_expvals[k])
        trotter_curve_error.append(trotter_stds[k])

    plt.errorbar(
        trotter_times,
        trotter_curve,
        yerr=trotter_curve_error,
        alpha=0.5,
        markersize=4,
        marker=sym[step],
        color="grey",
        label=f"{mpf_trotter_steps[k]} Trotter steps",
    )

# Get expectation values at all times for the static MPF with exact coeffs
exact_mpf_curve, exact_mpf_curve_error = [], []
for trotter_expvals, trotter_stds in zip(
    mpf_expvals_all_times, mpf_stds_all_times
):
    mpf_std = np.sqrt(
        sum(
            [
                (coeff**2) * (std**2)
                for coeff, std in zip(coeffs_exact.value, trotter_stds)
            ]
        )
    )
    exact_mpf_curve_error.append(mpf_std)
    exact_mpf_curve.append(trotter_expvals @ coeffs_exact.value)

plt.errorbar(
    trotter_times,
    exact_mpf_curve,
    yerr=exact_mpf_curve_error,
    markersize=4,
    marker="o",
    label="Static MPF - Exact",
    color="purple",
)


# Get expectation values at all times for the static MPF with approximate coeffs
approx_mpf_curve, approx_mpf_curve_error = [], []
for trotter_expvals, trotter_stds in zip(
    mpf_expvals_all_times, mpf_stds_all_times
):
    mpf_std = np.sqrt(
        sum(
            [
                (coeff**2) * (std**2)
                for coeff, std in zip(coeffs_approx.value, trotter_stds)
            ]
        )
    )
    approx_mpf_curve_error.append(mpf_std)
    approx_mpf_curve.append(trotter_expvals @ coeffs_approx.value)

plt.errorbar(
    trotter_times,
    approx_mpf_curve,
    yerr=approx_mpf_curve_error,
    markersize=4,
    marker="o",
    label="Static MPF - Approx",
    color="orange",
)


# Get expectation values at all times for the dynamic MPF
dynamic_mpf_curve, dynamic_mpf_curve_error = [], []
for trotter_expvals, trotter_stds, dynamic_coeffs in zip(
    mpf_expvals_all_times, mpf_stds_all_times, mpf_dynamic_coeffs_list
):
    mpf_std = np.sqrt(
        sum(
            [
                (coeff**2) * (std**2)
                for coeff, std in zip(dynamic_coeffs, trotter_stds)
            ]
        )
    )
    dynamic_mpf_curve_error.append(mpf_std)
    dynamic_mpf_curve.append(trotter_expvals @ dynamic_coeffs)

plt.errorbar(
    trotter_times,
    dynamic_mpf_curve,
    yerr=dynamic_mpf_curve_error,
    markersize=4,
    marker="o",
    label="Dynamic MPF",
    color="pink",
)


# Exact expectation values
plt.plot(
    exact_evolution_times,
    exact_expvals,
    color="red",
    linestyle="--",
    label="Exact time-evolution",
)

plt.title(f"$\\langle Z_{{{L//2-1}}} Z_{{{L//2}}} \\rangle$ vs time")
plt.xlabel("Time")
plt.ylabel("Expectation Value")
plt.legend(loc="upper center", bbox_to_anchor=(0.5, -0.2), ncol=2)
plt.grid(alpha=0.1)
plt.tight_layout()
plt.show()

Output:

Output of the previous code cell

위의 그래프는 트로터 오차와 표본 오차 간의 상호작용을 보여줍니다.

  • 트로터 오류. 시간이 지남에 따라 개별 제품의 공식(회색 마커)은 정확한 곡선에서 점점 더 멀어집니다. k=1k=1 회로는 편차가 가장 크고 가장 얕지만, 이미 t/k1t/k \gtrsim 1 인 영역에 속하므로, 주요 오차 항인 1/k21/k^{2} 의 값이 큽니다. MPF 조합(색상 표시기)은 이러한 선행 트로터 오차 항 중 몇 가지를 상쇄하므로, 어떤 단일 kjk_j 회로보다 훨씬 더 정확하게 곡선을 따라갑니다. 남아 있는 오차는 MPF가 상쇄 하지 못하는 고차 트로터 항을 반영합니다. 즉, 22, r=3r=3 정적 MPF는 오차의 첫 두 차수만 제거할 뿐이며, t/kmint/k_{\min} 값이 커지면 상쇄되지 않은 잔여 오차가 결국 지배적이게 됩니다. 따라서 MPF는 매우 얕은 회로가 임의의 시점에서 정확성을 유지한다고 보장하지 않습니다.

  • 표본 오차. MPF 곡선에서 오차 막대가 더 넓게 나타나는 것은 선형 조합의 직접적인 결과입니다. 회로별 독립 표준 오차 σkj\sigma_{k_j} 를 합산하면 총 분산 σMPF2=jxj2σkj2\sigma_{\text{MPF}}^2 = \sum_j x_j^2 \, \sigma_{k_j}^2 이 됩니다. 따라서 x2\|x\|_2 가 클수록(실제로는 우리가 제어하는 x1\|x\|_1 가 클수록), 주어진 목표 불확실도에 도달하기 위해 더 많은 측정 횟수가 필요합니다. 이것이 ‘Background’의 근사 해법기 옵션에 숨겨진 절충점입니다. 즉, 이 오버헤드를 관리 가능한 수준으로 유지하기 위해 x1\|x\|_1 에 상한을 설정합니다. 중요한 점은, 트로터 오차와 달리 표본 오차는 1/Nshots1/\sqrt{N_{\text{shots}}} 에 비례하여 줄어들기 때문에, 더 많은 샷을 사용함으로써 항상 이를 줄일 수 있다는 것이다.

아래의 대규모 하드웨어 예시에서, 하드웨어 노이즈는 각 Akj\langle A \rangle_{k_j} 에 추가적인 오차 원인으로 유입되며, 이 역시 MPF 계수에 의해 증폭됩니다. 해당 섹션에서는 오류 완화 기법이 MPF와 어떻게 상호작용하는지 살펴보겠습니다.


대규모 하드웨어 예시

이 절에서는 문제를 시뮬레이션으로 정확히 재현할 수 있는 범위를 넘어 확장해 보겠습니다. 우리는 시간 t=3t = 3 에서 50-큐비트 XXZ 사슬을 사용하여 문헌 [3] 에 제시된 결과 중 일부를 재현합니다. 소규모 예제와 동일한 4단계 워크플로를 따르되, 이번에는 오류 완화 기능이 적용된 실제 양자 하드웨어를 대상으로 합니다. 템플릿에서와 마찬가지로, 각 단계는 코드 내에 직접 표시되어 있으며, 중간 결과를 살펴볼 필요가 있는 경우 하나의 단계가 여러 셀에 걸쳐 표시될 수 있습니다.

이 매핑 과정은 소규모 예시와 유사합니다. 즉, 해밀토니안을 정의하고, 트로터 매개변수를 선택하며, MPF 계수(정적 및 동적)를 계산한 뒤, 회로를 구성합니다. 주요 차이점은 다음과 같습니다:

  • U(0.5,1.5)\mathcal{U}(0.5, 1.5) (참고문헌 [3] )에서 추출한 무작위 결합을 갖는 50개 사이트의 XXZ 해밀토니안.
  • kj=[3,4,6]k_j = [3, 4, 6]대칭 2차 트로터 공식 (따라서 χ=1\chi=1, symmetric=True).
  • 단일 고정 진화 시간 t=3t = 3. kmin=3k_{\min}=3 를 대입하면 t/kmin=1t/k_{\min}=1 가 되며, 이를 통해 얕은 구성 요소들이 MPF가 의존하는 선행 오차 모델이 유효한 트로터 수렴 영역 내에 유지된다.
  • 기준으로 삼기 위해, k=10k = 10 ’ 트로터 단계를 사용한 단일 회로 비교 실험을 추가로 수행하였다. 우리가 k=10k = 10 를 선택한 이유는, 이 회로의 하드웨어 상 2-큐비트 처리 깊이가 가장 깊은 MPF 구성 요소( kmax=6k_{\max}=6 )에 다중 MPF 회로 실행에 따른 오버헤드를 더한 값보다 더 깊기 때문입니다. 이 깊이는 노이즈 제한 영역에 해당할 만큼 충분히 깊으며, 이 영역에서 MPF 조합이 단일 회로 기준선보다 우수한 성능을 보일 것으로 예상됩니다. 이는 MPF 조합에 대한 “단일 심층 회로” 비교일 뿐, MPF의 유효 트로터 오차를 대상으로 하는 회로는 아닙니다(후자의 경우 훨씬 더 많은 단계가 필요하기 때문입니다).

여기서는 아직 1단계(매핑 및 회로 구성)에 있지만, 이 셀에서는 정적 계수뿐만 아니라 동적 계수도 미리 계산한다는 점에 유의하십시오. 동적 계수는 HHtt 에 따라 달라지지만 양자 측정 결과에는 영향을 받지 않으므로, 4단계 이전의 어느 시점에서나 계산할 수 있습니다. MPF와 관련된 모든 설정을 한곳에 모아두기 위해 지금 이렇게 하고 있습니다.

# -------------------------Step 1-------------------------
L = 50
coupling_map = CouplingMap.from_line(L, bidirectional=False)

# XXZ Hamiltonian with random couplings (Ref. [3])
np.random.seed(0)
even_edges = list(coupling_map.get_edges())[::2]
odd_edges = list(coupling_map.get_edges())[1::2]

Js = np.random.uniform(0.5, 1.5, size=L)
hamiltonian = SparsePauliOp(Pauli("I" * L))
for i, edge in enumerate(even_edges + odd_edges):
    hamiltonian += SparsePauliOp.from_sparse_list(
        [
            ("XX", (edge), 2 * Js[i]),
            ("YY", (edge), 2 * Js[i]),
            ("ZZ", (edge), 4 * Js[i]),
        ],
        num_qubits=L,
    )

observable = SparsePauliOp.from_sparse_list(
    [("ZZ", (L // 2 - 1, L // 2), 1.0)], num_qubits=L
)

total_time = 3
mpf_trotter_steps = [3, 4, 6]
order = 2
symmetric = True

# Static coefficients
lse = setup_static_lse(mpf_trotter_steps, order=order, symmetric=symmetric)
mpf_coeffs = lse.solve()
print(f"Static coefficients: {mpf_coeffs}")
print(f"L1 norm: {np.linalg.norm(mpf_coeffs, ord=1)}")

model_approx, coeffs_approx = setup_sum_of_squares_problem(
    lse, max_l1_norm=2.0
)
model_approx.solve()
print(f"Approximate coefficients: {coeffs_approx.value}")
print(f"L1 norm (approx): {np.linalg.norm(coeffs_approx.value, ord=1)}")

# -------------------------Dynamic coefficients-------------------------
single_2nd_order_circ = generate_time_evolution_circuit(
    hamiltonian, time=1.0, synthesis=SuzukiTrotter(reps=1, order=order)
)
single_2nd_order_circ = pm.run(single_2nd_order_circ)

layers = slice_by_depth(single_2nd_order_circ, max_slice_depth=1)
models = [
    LayerModel.from_quantum_circuit(layer, conserve="Sz") for layer in layers
]

approx_factory = partial(
    LayerwiseEvolver,
    layers=models,
    options={
        "preserve_norm": False,
        "trunc_params": {"chi_max": 64, "svd_min": 1e-8, "trunc_cut": None},
        "max_delta_t": 4,
    },
)

single_4th_order_circ = generate_time_evolution_circuit(
    hamiltonian, time=1.0, synthesis=SuzukiTrotter(reps=1, order=4)
)
single_4th_order_circ = pm.run(single_4th_order_circ)
exact_model_layers = [
    LayerModel.from_quantum_circuit(layer, conserve="Sz")
    for layer in slice_by_depth(single_4th_order_circ, max_slice_depth=1)
]

exact_factory = partial(
    LayerwiseEvolver,
    layers=exact_model_layers,
    dt=0.1,
    options={
        "preserve_norm": False,
        "trunc_params": {"chi_max": 64, "svd_min": 1e-8, "trunc_cut": None},
        "max_delta_t": 3,
    },
)


def identity_factory():
    return MPOState.initialize_from_lattice(models[0].lat, conserve=True)


mps_initial_state = MPS_neel_state(models[0].lat)

print(f"Computing dynamic coefficients for time={total_time}")
lse_dyn = setup_dynamic_lse(
    mpf_trotter_steps,
    total_time,
    identity_factory,
    exact_factory,
    approx_factory,
    mps_initial_state,
)
problem, coeffs_dyn = setup_frobenius_problem(lse_dyn)
try:
    problem.solve()
    mpf_dynamic_coeffs = coeffs_dyn.value
except Exception as error:
    mpf_dynamic_coeffs = np.zeros(len(mpf_trotter_steps))
    print(error, "Calculation Failed")

# -------------------------Step 1 (cont): Build circuits-------------------------
mpf_circuits = []
for k in mpf_trotter_steps:
    circuit = QuantumCircuit(L)
    circuit.x([i for i in range(L) if i % 2])
    trotter_circ = generate_time_evolution_circuit(
        hamiltonian,
        synthesis=SuzukiTrotter(reps=k, order=order),
        time=total_time,
    )
    circuit.compose(trotter_circ, qubits=range(L), inplace=True)
    mpf_circuits.append(circuit)

# Baseline "single deep circuit" comparison run with k=10 Trotter steps.
# Its two-qubit depth is deeper than the deepest MPF constituent (k_max=6) plus
# the overhead of running multiple circuits, pushing it into the noise-limited
# regime where MPF is expected to outperform. It does NOT target the MPF's effective
# Trotter error (which would require many more steps).
comp_circuit = QuantumCircuit(L)
comp_circuit.x([i for i in range(L) if i % 2])
trotter_circ = generate_time_evolution_circuit(
    hamiltonian,
    synthesis=SuzukiTrotter(reps=10, order=order),
    time=total_time,
)
comp_circuit.compose(trotter_circ, qubits=range(L), inplace=True)
mpf_circuits.append(comp_circuit)

Output:

Static coefficients: [ 0.42857143 -1.82857143  2.4       ]
L1 norm: 4.65714285714286
Approximate coefficients: [-0.4942491   0.40206845  1.09218065]
L1 norm (approx): 1.9884981979026675
Computing dynamic coefficients for time=3

이제 선택한 백엔드에 맞춰 회로를 최적화합니다. optimization_level=3우리는 Qiskit의 사전 설정 패스 매니저를 사용하며, 이 매니저는 적절한 물리적 큐비트 세트를 자동으로 선택하고 각 회로를 장치 토폴로지에 매핑합니다.

# -------------------------Step 2-------------------------
service = QiskitRuntimeService()
# backend = service.least_busy(operational=True, simulator=False, min_num_qubits=L)
backend = service.backend("ibm_fez")
print(backend)

transpiler = generate_preset_pass_manager(
    optimization_level=3, backend=backend
)
transpiled_circuits = [transpiler.run(circ) for circ in mpf_circuits]

isa_observables = [
    observable.apply_layout(circ.layout) for circ in transpiled_circuits
]

Output:

<IBMBackend('ibm_fez')>

실제 하드웨어에서 더 복잡한 회로를 구동하려면 강력한 오류 완화 조치가 필요합니다. 본 연구에서는 동적 분리, 게이트 및 측정 트위링, 측정 오차 완화, 제로 노이즈 외삽법(ZNE)을 구현합니다. 여기서 사용하는 ZNE 잡음 계수(1, 1.2, 1.4)는 얕은 회로 시나리오에서보다 작다는 점에 유의해야 합니다. 이는 더 깊은 MPF 구성 요소들이 이미 잡음 임계치에 근접해 있으며, 잡음이 크게 증폭될 경우 ZNE 외삽이 신뢰할 수 있는 범위를 벗어나게 되기 때문입니다.

우리는 4개의 회로 전체( kj=[3,4,6]k_j = [3, 4, 6] 의 MPF 구성 요소 3개와 k=10k = 10 의 기준선)를 하나의 Estimator 작업으로 제출합니다.

# -------------------------Step 3-------------------------
estimator = Estimator(mode=backend)
estimator.options.default_shots = 30000

# Error suppression/mitigation
estimator.options.dynamical_decoupling.enable = True
estimator.options.twirling.enable_gates = True
estimator.options.twirling.enable_measure = True
estimator.options.twirling.num_randomizations = "auto"
estimator.options.twirling.strategy = "active-accum"
estimator.options.resilience.measure_mitigation = True
estimator.options.experimental.execution_path = "gen3-turbo"

estimator.options.resilience.zne_mitigation = True
estimator.options.resilience.zne.noise_factors = (1, 1.2, 1.4)
estimator.options.resilience.zne.extrapolator = "linear"

estimator.options.environment.job_tags = ["TUT_MPF"]

job_50 = estimator.run(
    [
        (circ, observable)
        for circ, observable in zip(transpiled_circuits, isa_observables)
    ]
)

작업 결과에서 회로별 기대값과 표준편차를 추출한 다음, 소규모 예제와 정확히 동일한 방식으로 각 MPF 계수 집합과 결합합니다: AMPF=jxjAkj\langle A \rangle_{\text{MPF}} = \sum_j x_j \, \langle A \rangle_{k_j}, 전파된 분산은 σ2=jxj2σkj2\sigma^2 = \sum_j x_j^2 \sigma_{k_j}^2 입니다.

# -------------------------Step 4-------------------------
result = job_50.result()
evs = [res.data.evs for res in result]
std = [res.data.stds for res in result]

print(evs)
print(std)

Output:

[array(-0.07916195), array(-0.04479681), array(-0.2560756), array(-0.06045848)]
[array(0.04605538), array(0.10056336), array(0.14426151), array(0.04059092)]
exact_mpf_std = np.sqrt(
    sum([(coeff**2) * (std**2) for coeff, std in zip(mpf_coeffs, std[:3])])
)
print(
    "Exact static MPF expectation value: ",
    evs[:3] @ mpf_coeffs,
    "+-",
    exact_mpf_std,
)
approx_mpf_std = np.sqrt(
    sum(
        [
            (coeff**2) * (std**2)
            for coeff, std in zip(coeffs_approx.value, std[:3])
        ]
    )
)
print(
    "Approximate static MPF expectation value: ",
    evs[:3] @ coeffs_approx.value,
    "+-",
    approx_mpf_std,
)
dynamic_mpf_std = np.sqrt(
    sum(
        [
            (coeff**2) * (std**2)
            for coeff, std in zip(mpf_dynamic_coeffs, std[:3])
        ]
    )
)
print(
    "Dynamic MPF expectation value: ",
    evs[:3] @ mpf_dynamic_coeffs,
    "+-",
    dynamic_mpf_std,
)

Output:

Exact static MPF expectation value:  -0.5665938395816946 +- 0.3925273058119915
Approximate static MPF expectation value:  -0.25856647611537903 +- 0.164249927266166
Dynamic MPF expectation value:  -0.12667812062949296 +- 0.06059471006973169
sym = {3: "^", 4: "s", 6: "p"}
for k, step in enumerate(mpf_trotter_steps):
    plt.errorbar(
        k,
        evs[k],
        yerr=std[k],
        alpha=0.5,
        markersize=4,
        marker=sym[step],
        color="grey",
        label=f"{mpf_trotter_steps[k]} Trotter steps",
    )

plt.errorbar(
    3,
    evs[-1],
    yerr=std[-1],
    alpha=0.5,
    markersize=8,
    marker="x",
    color="blue",
    label="10 Trotter steps",
)

plt.errorbar(
    4,
    evs[:3] @ mpf_coeffs,
    yerr=exact_mpf_std,
    markersize=4,
    marker="o",
    color="purple",
    label="Static MPF",
)

plt.errorbar(
    5,
    evs[:3] @ coeffs_approx.value,
    yerr=approx_mpf_std,
    markersize=4,
    marker="o",
    color="orange",
    label="Approximate static MPF",
)

plt.errorbar(
    6,
    evs[:3] @ mpf_dynamic_coeffs,
    yerr=dynamic_mpf_std,
    markersize=4,
    marker="o",
    color="pink",
    label="Dynamic MPF",
)

exact_obs = -0.24384471447172074  # Calculated via Tensor Network calculation
plt.axhline(
    y=exact_obs, linestyle="--", color="red", label="Exact time-evolution"
)

plt.title(
    f"$\\langle Z_{{{L//2-1}}} Z_{{{L//2}}} \\rangle$ at time {total_time} for the different methods"
)
plt.xlabel("Method")
plt.ylabel("Expectation Value")
plt.legend(loc="upper center", bbox_to_anchor=(0.5, -0.2), ncol=2)
plt.grid(alpha=0.1)
plt.tight_layout()
plt.show()

Output:

Output of the previous code cell

위의 하드웨어 결과에 대해 몇 가지 관찰 사항을 말씀드리자면:

  • 하드웨어 측면에서 더 깊이 파고드는 데는 비용이 듭니다. 단일 회로 기준선은 상황을 명확히 보여줍니다. k=6k = 6 회로는 사실상 정확합니다( 0.256-0.256 대 기준값 0.244-0.244 ). 반면, 더 깊은 k=10k = 10 기준선은 나쁘며( 0.061-0.061, 0.18\sim 0.18 의 오차), 더 나은 것은 아닙니다. 트로터 오차가 이미 작을 경우, 단계를 추가하면 주로 회로가 더 깊어지면서 게이트 노이즈와 비결합 현상이 더 많이 누적될 뿐이다. 이것이 바로 MPF가 설계된 목적입니다. 즉, 얕은 구성 요소만을 사용하여 깊은 회로 수준의 정확도를 달성하는 것입니다.

  • 소노름 MPF가 딥 싱글 서킷보다 성능이 더 우수하다. 대략적인 정적 MPF( x12\|x\|_1 \approx 2 로 상한 설정)는 0.259-0.259 에 도달하며, 이는 기준치보다 0.015\sim 0.015 정도 낮은 수치로, k=10k = 10 의 기준치보다는 훨씬 더 가까운 수준입니다. 동적 MPF( 0.127-0.127 ) 역시 그 기준치를 여유 있게 상회합니다. 두 방법 모두 얕은 kj=[3,4,6]k_j = [3, 4, 6] 회로만을 결합하지만, 깊은 단일 회로로는 도출할 수 없었던 답을 찾아냅니다.

  • 계수 노름은 수학적 최적성보다 더 중요합니다. 정확한 정적 MPF는 x1=4.66\|x\|_1 = 4.66 를 가지며, 모든 추정기 중 가장 성능이 떨어집니다( 0.567-0.567, 오차 범위가 0.30.3 보다 큽니다). 계수의 노름이 크기 때문에 각 Akj\langle A \rangle_{k_j} 에 대한 잔류 게이트 노이즈, 비고전성, ZNE 오차가 대략 동일한 배수로 증폭되어, 이를 통해 얻는 트로터 오차 상쇄 효과를 압도해 버립니다. 노름(대략적 정적 해법, x12\|x\|_1 \approx 2 )에 상한을 설정하면 이러한 과부하 현상이 해소되고 최상의 추정값을 얻을 수 있다. 비록 그 계수들이 더 이상 주요 트로터 오차를 정확히 상쇄하지는 못하더라도 말이다.

  • 개별 얕은 코스도 여전히 경쟁력을 갖출 수 있다. 이곳에서 유일한 k=6k = 6 구성 요소( 0.256-0.256 )는 그 자체로 본질적으로 정확한 값을 나타냅니다. 이번 실행에서는 근사 정적 MPF보다 심지어 아주 약간 더 가까운 값을 보여줍니다. 문제는 “수렴했으나 아직 잡음에 의해 제한되지는 않은” 최적의 지점에 어떤 kk 가 위치하는지 미리 알 수 없다는 점이며, 트로터 수렴을 보장하기 위해 단순히 더 깊은 곳( k=10k = 10 )으로 가는 것처럼 안전해 보이는 선택이 바로 실패하는 선택이라는 것입니다. MPF는 적절한 깊이를 추측할 필요가 없는, 얕은 회로들의 원리에 입각한 조합을 제공합니다.

실무적으로 얻을 수 있는 교훈은, 하드웨어 환경에서 MPF를 사용할 때는 각 개별 Akj\langle A \rangle_{k_j} 에 대해 강력한 오차 완화 기법을 병행해야 하며, 계수 L1L_1 -노름은 적정 수준으로 유지해야 하고(근사 해법기 또는 동적 MPF 사용), 트로터 단계 kjk_jt/kmin1t/k_{\min} \lesssim 1 가 성립하도록 선택해야 한다는 점입니다. 여기서 t=3t = 3kmin=3k_{\min} = 3 를 적용하면 t/kmin=1t/k_{\min} = 1 가 되며, 이를 통해 정적 MPF가 의존하는 선행 오차 모델이 유효한 수렴 영역 내에 구성 요소들을 유지할 수 있습니다. 이러한 선택에 따라, 여기에서 소노름 MPF는 수렴된 단일 회로와 동등한 성능을 보이는 반면, 단순히 “깊이만 늘리는” 방식의 기준 모델은 그렇지 못하여, 문헌 [3] 에서 제시된 ‘깊이 대 정확도’의 이점을 재현해 냅니다. 또한 개별 실행 결과에는 잡음이 존재한다는 점에 유의해야 합니다. 동일한 작업을 다른 방식으로 제출하거나(또는 다른 백엔드에서 실행할 경우) 정확한 순위가 달라질 수 있습니다. 그러나 확고한 경향은 다음과 같습니다. small- x1\|x\|_1 의 MPF는 좋은 성능을 보이며, large- x1\|x\|_1 의 exact-static MPF는 하드웨어 잡음의 영향을 더 크게 받으며, over-deep 단일 회로는 잡음에 의해 성능이 제한됩니다.


다음 단계

권장사항

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


참조

[1] 바스케스, A. C., Egger, D. J., Ochsner, D., & Woerner, S. 하드웨어 친화적인 해밀토니안 시뮬레이션을 위한 잘 조건화된 다중 산출식. Quantum, 제7권, 1067쪽 (2023)

[2] Zhuk, S., Robertson, N. F., & Bravyi, S. 해밀토니안 시뮬레이션을 위한 트로터 오차 상한 및 동적 다중 곱 공식. Physical Review Research, 6(3), 033309 (2024)

[3] 로버트슨, N. F., 등 텐서 네트워크를 활용한 동적 다중곱 공식. arXiv:2407.17405 (2024)

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