주 콘텐츠로 건너뛰기

기댓값 추정을 위한 연산자 역전파(OBP)

사용 시간 예상: Heron r3 프로세서에서 4분 (참고: 이는 추정치이며 실제 실행 시간은 다를 수 있습니다.)

학습 목표​

  • qiskit-addon-obp를 사용하여 회로 실행 횟수 증가를 감수하면서 양자 회로의 깊이를 줄이는 방법

  • qiskit-addon-utils를 사용하여 XYZ 해밀토니안과 그 시간 진화 회로를 구성하는 방법

사전 요구 사항​

  • 관측 가능량의 기댓값을 계산하기 위한 Estimator 프리미티브 사용법

배경​

연산자 역전파(Operator backpropagation)는 양자 회로의 끝에서 연산을 측정 대상 관측 가능량(observable)에 흡수하는 기법으로, 일반적으로 관측 가능량의 항이 늘어나는 대신 회로의 깊이를 줄일 수 있습니다. 목표는 관측 가능량이 지나치게 커지지 않는 범위에서 최대한 많은 회로를 역전파하는 것입니다. Qiskit 기반 구현은 OBP Qiskit 애드온에서 제공됩니다. 자세한 내용은 해당 문서를 참고하세요.

관측 가능량 O=∑PcPPO = \sum_P c_P P를 측정해야 하는 예시 회로를 생각해봅시다. 여기서 PP는 Pauli 연산자이고 cPc_P는 계수입니다. 회로를 단일 유니터리 UU로 나타내면 아래 그림과 같이 U=UCUQU = U_C U_Q로 논리적으로 분할할 수 있습니다.

Uq 다음에 Uc가 오는 회로 다이어그램

연산자 역전파는 유니터리 UCU_C를 O′=UC†OUC=∑PcPUC†PUCO' = U_C^{\dagger}OU_C = \sum_P c_P U_C^{\dagger}PU_C와 같이 관측 가능량에 진화시켜 흡수합니다. 즉, 계산의 일부가 관측 가능량을 OO에서 O′O'로 진화시키는 고전적 방법을 통해 수행됩니다. 이제 원래 문제는 유니터리가 UQU_Q인 더 낮은 깊이의 회로에서 관측 가능량 O′O'를 측정하는 것으로 재구성할 수 있습니다.

유니터리 UCU_C는 여러 슬라이스 UC=USUS−1...U2U1U_C = U_S U_{S-1}...U_2U_1으로 표현됩니다. 슬라이스를 정의하는 방법은 여러 가지가 있습니다. 예를 들어, 위 예시 회로에서 각 RzzR_{zz} 레이어와 각 RxR_x 게이트 레이어를 개별 슬라이스로 볼 수 있습니다. 역전파는 O′=Πs=1S∑PcPUs†PUsO' = \Pi_{s=1}^S \sum_P c_P U_s^{\dagger} P U_s를 고전적으로 계산하는 과정을 포함합니다. 각 슬라이스 UsU_s는 Us=exp(−iθsPs2)U_s = exp(\frac{-i\theta_s P_s}{2})로 표현할 수 있으며, 여기서 PsP_s는 nn-큐비트 Pauli이고 θs\theta_s는 스칼라입니다. 다음이 성립함을 쉽게 확인할 수 있습니다.

Us†PUs=Pif [P,Ps]=0,U_s^{\dagger} P U_s = P \qquad \text{if} ~[P,P_s] = 0, Us†PUs=cos(θs)P+isin(θs)PsPif {P,Ps}=0U_s^{\dagger} P U_s = \qquad cos(\theta_s)P + i sin(\theta_s)P_sP \qquad \text{if} ~\{P,P_s\} = 0

위 예시에서 {P,Ps}=0\{P,P_s\} = 0이면 기댓값을 계산하기 위해 하나가 아닌 두 개의 양자 회로를 실행해야 합니다. 따라서 역전파는 관측 가능량의 항 수를 늘려 더 많은 회로 실행이 필요할 수 있습니다. 연산자가 너무 커지지 않으면서 더 깊이 역전파하는 방법 중 하나는 연산자에 항을 추가하는 대신 계수가 작은 항을 잘라내는(truncate) 것입니다. 예를 들어, 위 예시에서 θs\theta_s가 충분히 작다면 PsPP_sP를 포함하는 항을 잘라낼 수 있습니다. 항을 잘라내면 실행해야 할 양자 회로 수가 줄어들지만, 그 대가로 잘라낸 항의 계수 크기에 비례하는 오차가 최종 기댓값 계산에 발생합니다.

요구 사항​

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

  • Qiskit SDK v2.0 이상, 시각화 지원 포함

  • Qiskit Runtime v0.22 이상 (pip install qiskit-ibm-runtime)

  • OBP Qiskit 애드온 0.3 이상 (pip install qiskit-addon-obp)

  • Qiskit 애드온 유틸리티 0.3 이상 (pip install qiskit-addon-utils)

설정​

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-addon-obp qiskit-addon-utils qiskit-ibm-runtime rustworkx
import numpy as np
import matplotlib.pyplot as plt

from qiskit.primitives import StatevectorEstimator
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler import CouplingMap
from qiskit.synthesis import LieTrotter

from qiskit_addon_utils.problem_generators import generate_xyz_hamiltonian
from qiskit_addon_utils.problem_generators import (
generate_time_evolution_circuit,
)
from qiskit_addon_utils.slicing import slice_by_depth, combine_slices
from qiskit_addon_obp.utils.simplify import OperatorBudget
from qiskit_addon_obp import backpropagate
from qiskit_addon_obp.utils.truncating import setup_budget

from rustworkx.visualization import graphviz_draw

from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import EstimatorV2, EstimatorOptions

소규모 시뮬레이터 예시​

이 튜토리얼은 OBP Qiskit 애드온을 사용하여 하이젠베르크 스핀 체인의 양자 동역학을 시뮬레이션하는 Qiskit 패턴을 구현합니다. 노이즈 없는 시뮬레이터에서는 역전파 유무에 관계없이 동일한 기댓값을 얻을 수 있습니다.

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

양자 하이젠베르크 모델의 시간 진화를 양자 실험으로 매핑하기​

먼저 qiskit-addon-utils의 generate_xyz_hamiltonian 함수를 사용하여 주어진 연결 그래프에서 하이젠베르크 유사 해밀토니안을 생성합니다. 이 그래프는 rustworkx.PyGraph 또는 CouplingMap일 수 있습니다. 다음에서는 10개의 큐비트로 이루어진 선형 체인 CouplingMap을 사용합니다.

num_qubits = 10
layout = [(i - 1, i) for i in range(1, num_qubits)]

# Instantiate a CouplingMap object
coupling_map = CouplingMap(layout)
graphviz_draw(coupling_map.graph, method="circo")

이전 코드 셀의 출력

다음으로, 하이젠베르크 XYZ 해밀토니안을 모델링하는 Pauli 연산자를 생성합니다.

H^XYZ=∑(j,k)∈E(Jxσjxσkx+Jyσjyσky+Jzσjzσkz)+∑j∈V(hxσjx+hyσjy+hzσjz),{\hat{\mathcal{H}}_{XYZ} = \sum_{(j,k)\in E} (J_{x} \sigma_j^{x} \sigma_{k}^{x} + J_{y} \sigma_j^{y} \sigma_{k}^{y} + J_{z} \sigma_j^{z} \sigma_{k}^{z}) + \sum_{j\in V} (h_{x} \sigma_j^{x} + h_{y} \sigma_j^{y} + h_{z} \sigma_j^{z}),}

여기서 G(V,E)G(V,E)는 커플링 맵의 그래프입니다. 이 튜토리얼에서는 Jx,Jy,JzJ_x, J_y, J_z를 각각 π8,π4,π2\frac{\pi}{8}, \frac{\pi}{4}, \frac{\pi}{2}로, hx,hy,hzh_x, h_y, h_z를 각각 π3,π6,π9\frac{\pi}{3}, \frac{\pi}{6}, \frac{\pi}{9}로 설정하였습니다.

# Get a qubit operator describing the Heisenberg XYZ model
hamiltonian = generate_xyz_hamiltonian(
coupling_map,
coupling_constants=(np.pi / 8, np.pi / 4, np.pi / 2),
ext_magnetic_field=(np.pi / 3, np.pi / 6, np.pi / 9),
)
print(hamiltonian)
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', 'IIIIIIIIIX', 'IIIIIIIIIY', 'IIIIIIIIIZ', 'IIIIIIIIXI', 'IIIIIIIIYI', 'IIIIIIIIZI', 'IIIIIIIXII', 'IIIIIIIYII', 'IIIIIIIZII', 'IIIIIIXIII', 'IIIIIIYIII', 'IIIIIIZIII', 'IIIIIXIIII', 'IIIIIYIIII', 'IIIIIZIIII', 'IIIIXIIIII', 'IIIIYIIIII', 'IIIIZIIIII', 'IIIXIIIIII', 'IIIYIIIIII', 'IIIZIIIIII', 'IIXIIIIIII', 'IIYIIIIIII', 'IIZIIIIIII', 'IXIIIIIIII', 'IYIIIIIIII', 'IZIIIIIIII', 'XIIIIIIIII', 'YIIIIIIIII', 'ZIIIIIIIII'],
coeffs=[0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j,
0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j,
1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j,
0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j,
0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j,
1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j,
0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 1.04719755+0.j,
0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,
0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j,
1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j,
0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,
0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j,
1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j,
0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,
0.34906585+0.j])

큐비트 연산자로부터 시간 진화를 모델링하는 양자 회로를 생성할 수 있습니다. generate_time_evolution_circuit을 Lie Trotter 분해와 함께 사용하여 시간 진화 회로를 구성하였습니다.

circuit = generate_time_evolution_circuit(
hamiltonian,
time=0.2,
synthesis=LieTrotter(reps=2),
)
circuit.draw("mpl", style="iqp", fold=-1)

이전 코드 셀의 출력

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

역전파할 회로 슬라이스 생성하기​

backpropagate 함수는 한 번에 전체 회로 슬라이스를 역전파합니다. 따라서 슬라이싱 방법의 선택은 주어진 문제에서 역전파 성능에 영향을 줄 수 있습니다. 여기서는 slice_by_depth 함수를 사용하여 같은 유형의 게이트를 슬라이스로 묶겠습니다.

회로 슬라이싱에 대한 더 자세한 논의는 qiskit-addon-utils 패키지의 사용 방법 가이드를 참고하세요.

slices = slice_by_depth(circuit, max_slice_depth=1)
print(f"Separated the circuit into {len(slices)} slices.")
Separated the circuit into 18 slices.

역전파 중 연산자 증가 크기 제한하기​

역전파 중 연산자의 항 수는 일반적으로 LL이 슬라이스 수일 때 2L2^L에 빠르게 근접합니다. 두 항이 큐비트 단위로 교환되지 않으면(non-commuting), 해당 항들의 기댓값을 얻기 위해 별도의 회로가 필요합니다. 예를 들어, 2-큐비트 관측 가능량 O=0.1XX+0.3IZ−0.5IXO = 0.1 XX + 0.3 IZ - 0.5 IX가 있다면, [XX,IX]=0[XX,IX] = 0이므로 이 두 항의 기댓값을 계산하는 데 단일 기저 측정으로 충분합니다. 하지만 IZIZ는 나머지 두 항과 반교환(anti-commute)하므로 IZIZ의 기댓값을 계산하려면 별도의 기저 측정이 필요합니다. 즉, ⟨O⟩\langle O \rangle을 계산하는 데 하나가 아닌 두 개의 회로가 필요합니다. 연산자의 항 수가 증가할수록 필요한 회로 실행 횟수도 증가할 가능성이 있습니다.

연산자의 크기는 backpropagate 함수의 operator_budget 키워드 인수를 지정하여 제한할 수 있으며, 이 인수는 OperatorBudget 인스턴스를 받습니다.

추가 리소스(회로 실행 횟수, 즉 필요한 QPU 시간) 할당량을 제어하기 위해, 역전파된 관측 가능량이 가질 수 있는 큐비트 단위 교환 Pauli 그룹의 최대 수를 제한합니다. 여기서는 연산자의 큐비트 단위 교환 Pauli 그룹 수가 여덟을 초과하면 역전파를 중단하도록 지정합니다.

op_budget = OperatorBudget(max_qwc_groups=8)

회로에서 슬라이스 역전파하기​

먼저 관측 가능량을 MZ=1N∑i=1N⟨Zi⟩M_Z = \frac{1}{N} \sum_{i=1}^N \langle Z_i \rangle로 지정합니다. 여기서 NN은 큐비트의 수입니다. 관측 가능량의 항들이 더 이상 여덟 개 이하의 큐비트 단위 교환 Pauli 그룹으로 결합될 수 없을 때까지 시간 진화 회로에서 슬라이스를 역전파합니다.

observable = SparsePauliOp.from_sparse_list(
[("Z", [i], 1 / num_qubits) for i in range(num_qubits)],
num_qubits=num_qubits,
)
observable
SparsePauliOp(['IIIIIIIIIZ', 'IIIIIIIIZI', 'IIIIIIIZII', 'IIIIIIZIII', 'IIIIIZIIII', 'IIIIZIIIII', 'IIIZIIIIII', 'IIZIIIIIII', 'IZIIIIIIII', 'ZIIIIIIIII'],
coeffs=[0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j,
0.1+0.j, 0.1+0.j])

아래에서 6개의 슬라이스가 역전파되었으며, 항들이 여덟이 아닌 여섯 개의 그룹으로 결합된 것을 볼 수 있습니다. 이는 한 슬라이스를 더 역전파하면 Pauli 그룹 수가 여덟을 초과한다는 것을 의미합니다. 반환된 메타데이터를 검사하여 이를 확인할 수 있습니다. 또한 이 부분에서 회로 변환은 정확합니다. 즉, 새로운 관측 가능량 O′O'의 어떤 항도 잘리지 않았습니다. 역전파된 회로와 역전파된 연산자는 원래 회로와 연산자와 정확히 동일한 결과를 제공합니다.

# Backpropagate slices onto the observable
bp_obs, remaining_slices, metadata = backpropagate(
observable, slices, operator_budget=op_budget
)
# Recombine the slices remaining after backpropagation
bp_circuit = combine_slices(remaining_slices)

print(f"Backpropagated {metadata.num_backpropagated_slices} slices.")
print(
f"New observable has {len(bp_obs.paulis)} terms, which can be combined into "
f"{len(bp_obs.group_commuting(qubit_wise=True))} groups."
)
print(
f"Note that backpropagating one more slice would result in "
f"{metadata.backpropagation_history[-1].num_paulis[0]} terms "
f"across {metadata.backpropagation_history[-1].num_qwc_groups} groups."
)
print("The remaining circuit after backpropagation looks as follows:")
bp_circuit.draw("mpl", fold=-1, scale=0.6)
Backpropagated 6 slices.
New observable has 60 terms, which can be combined into 6 groups.
Note that backpropagating one more slice would result in 114 terms across 12 groups.
The remaining circuit after backpropagation looks as follows:

이전 코드 셀의 출력

소규모 시뮬레이터 예시에서는 잘라내기를 사용하지 않겠습니다. 노이즈가 없는 환경에서는 역전파 유무에 관계없이 동일한 결과가 나오며, 잘라내기는 근사 오차를 추가하여 결과를 악화시키기 때문입니다.

기저 게이트 집합으로 회로 트랜스파일하기​

이제 원본 회로와 역전파된 회로를 모두 백엔드의 기저 게이트로 트랜스파일합니다. 소규모 인스턴스는 시뮬레이터에서 실행할 것이므로 실제 백엔드에서 트랜스파일할 필요는 없습니다.

service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=133
)
print(backend)
<IBMBackend('ibm_kingston')>
pm_basis = generate_preset_pass_manager(
optimization_level=3, basis_gates=backend.configuration().basis_gates
)
isa_circuit = pm_basis.run(circuit)
isa_bp_circuit = pm_basis.run(bp_circuit)

3단계: Qiskit 프리미티브를 사용하여 실행​

먼저 원본 회로와 역전파된 회로에 해당하는 두 개의 Primitive Unified Bloc (PUB)을 생성합니다. 그런 다음 이상적인 Estimator에서 PUB를 실행하여 기댓값을 얻습니다.

pubs = [(isa_circuit, observable), (isa_bp_circuit, bp_obs)]
rng = np.random.default_rng()
estimator = StatevectorEstimator(seed=rng)
job = estimator.run(pubs)

4단계: 후처리하여 원하는 고전 형식으로 결과 반환​

이제 원본 회로와 역전파된 회로의 기댓값을 구합니다.

primitive_result = job.result()
circuit_expval = primitive_result[0].data.evs.item()
bp_circuit_expval = primitive_result[1].data.evs.item()
methods = [
"No backpropagation",
"Backpropagation",
]
values = [circuit_expval, bp_circuit_expval]

ax = plt.gca()
plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
ax.set_ylim([0.6, 0.92])
ax.set_ylabel(r"$M_Z$", fontsize=12)
Text(0, 0.5, '$M_Z$')

이전 코드 셀의 출력

예상대로 두 기댓값은 일치합니다. 노이즈 없는 상태 벡터 시뮬레이터에서 실행하기 때문에, 역전파는 회로-관측량 쌍의 정확한 변환이므로 원본 워크플로와 역전파 워크플로는 동일한 MZM_Z 값을 산출해야 합니다. 역전파의 이점은 노이즈가 있는 하드웨어에서만 나타나며, 이 경우 더 짧은 역전파 회로는 오차가 적게 누적됩니다. 이는 아래의 대규모 하드웨어 예시에서 확인할 수 있습니다.

대규모 하드웨어 예시​

실험을 개발할 때는 시각화와 시뮬레이션을 쉽게 하기 위해 소규모 회로로 시작하는 것이 유용합니다. 이제 동일한 JJ, hh 파라미터와 동일한 관측량 MZM_Z를 사용하되, 4개의 Trotter 스텝으로 50큐비트 하이젠베르크 해밀토니안에 대한 연산자 역전파를 살펴봅니다. 이 규모에서의 이상적인 기댓값은 단순 계산으로는 구할 수 없으므로, 텐서 네트워크를 사용하여 이상적인 기댓값이 ≃0.89\simeq 0.89임을 구합니다.

역전파와 함께, 이 대규모 예시에서는 잘라내기를 적용한 역전파도 소개합니다. 이상적으로는 유효 회로의 깊이를 줄이기 위해 최대한 많이 역전파하고 싶습니다. 하지만 이는 업데이트된 관측량에 교환되지 않는 항의 수를 증가시켜 양자 오버헤드를 키울 수 있습니다. 따라서 계수가 작은 관측량 항을 제거하는 잘라내기(truncation)라는 기법을 사용할 수 있습니다. 잘라내기는 업데이트된 관측량의 항 수를 줄여 더 깊은 전파를 가능하게 하지만, 그 대가로 일부 근사가 도입됩니다. 따라서 더 깊은 역전파로 인한 노이즈 감소 효과가 잘라내기 근사 오차에 압도되지 않도록 잘라내기를 일정 범위 내로 제한하는 것이 필요합니다.

잘라내기 양을 제한하기 위해, setup_budget 함수를 사용하여 각 슬라이스와 전체 역전파 회로에 대한 오차 예산을 할당합니다. 이를 통해 슬라이스 단위 및 전체 회로에 걸쳐 잘라내기가 제어됩니다. 예산 할당의 다른 방법은 이 가이드를 참고하세요.

num_qubits = 50
layout = [(i - 1, i) for i in range(1, num_qubits)]

# Instantiate a CouplingMap object
coupling_map = CouplingMap(layout)

hamiltonian = generate_xyz_hamiltonian(
coupling_map,
coupling_constants=(np.pi / 8, np.pi / 4, np.pi / 2),
ext_magnetic_field=(np.pi / 3, np.pi / 6, np.pi / 9),
)

# Generate a time evolution circuit for the Hamiltonian
circuit = generate_time_evolution_circuit(
hamiltonian,
time=0.2,
synthesis=LieTrotter(reps=4),
)

# Define the observable to measure
observable = SparsePauliOp.from_sparse_list(
[("Z", [i], 1 / num_qubits) for i in range(num_qubits)],
num_qubits,
)

slices = slice_by_depth(circuit, max_slice_depth=1)

# Define the maximum number of qwc groups allowed in the
# backpropagated observable,
# and the truncation error budget
op_budget = OperatorBudget(max_qwc_groups=15)
truncation_error_budget = setup_budget(
max_error_total=0.03, max_error_per_slice=0.005
)

# First backpropagation without truncation
bp_obs, remaining_slices, metadata = backpropagate(
observable, slices, operator_budget=op_budget
)
bp_circuit = combine_slices(remaining_slices)

# Now backpropagate with truncation, using the same operator budget and
# the defined truncation error budget
bp_obs_trunc, remaining_slices_trunc, metadata = backpropagate(
observable,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
bp_circuit_trunc = combine_slices(
remaining_slices_trunc, include_barriers=False
)

# Now we transpile the original circuit and the two backpropagated circuits,
# and apply the layout to the corresponding observables
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)

isa_circuit = pm.run(circuit)
isa_bp_circuit = pm.run(bp_circuit)
isa_bp_circuit_trunc = pm.run(bp_circuit_trunc)

isa_observable = observable.apply_layout(isa_circuit.layout)
isa_bp_observable = bp_obs.apply_layout(isa_bp_circuit.layout)
isa_bp_observable_trunc = bp_obs_trunc.apply_layout(
isa_bp_circuit_trunc.layout
)

# Compare the 2-qubit depth of each transpiled circuit to see how much
# depth backpropagation saved
print(
f"2-qubit depth without backpropagation: "
f"{isa_circuit.depth(lambda x: x.operation.num_qubits == 2)}"
)
print(
f"2-qubit depth with backpropagation: "
f"{isa_bp_circuit.depth(lambda x: x.operation.num_qubits == 2)}"
)
print(
f"2-qubit depth with backpropagation and truncation: "
f"{isa_bp_circuit_trunc.depth(lambda x: x.operation.num_qubits == 2)}"
)

pubs = [
(isa_circuit, isa_observable),
(isa_bp_circuit, isa_bp_observable),
(isa_bp_circuit_trunc, isa_bp_observable_trunc),
]

# Now we instantiate the Estimator primitive for the hardware with
# ZNE and measurement error
# mitigation and compute the three circuits and observables
options = EstimatorOptions()
options.default_precision = 0.01
options.resilience_level = 2
options.resilience.zne.noise_factors = [1, 1.2, 1.4]
options.resilience.zne.extrapolator = ["linear"]
estimator = EstimatorV2(mode=backend, options=options)

estimator.options.environment.job_tags = ["TUT_OBP"]
job = estimator.run(pubs)

# Retrieve the results and the standard deviations
result_no_bp = job.result()[0].data.evs.item()
result_bp = job.result()[1].data.evs.item()
result_bp_trunc = job.result()[2].data.evs.item()

std_no_bp = job.result()[0].data.stds.item()
std_bp = job.result()[1].data.stds.item()
std_bp_trunc = job.result()[2].data.stds.item()
2-qubit depth without backpropagation: 24
2-qubit depth with backpropagation: 20
2-qubit depth with backpropagation and truncation: 18
print(f"Expectation value without backpropagation: {result_no_bp}")
print(f"Backpropagated expectation value: {result_bp}")
print(f"Backpropagated expectation value with truncation: {result_bp_trunc}")
Expectation value without backpropagation: 0.9543907942381811
Backpropagated expectation value: 0.9445337385406468
Backpropagated expectation value with truncation: 0.934050286970965
# Plot the results
methods = [
"No backpropagation",
"Backpropagation",
"Backpropagation w/ truncation",
]
values = [result_no_bp, result_bp, result_bp_trunc]
error_bars = [std_no_bp, std_bp, std_bp_trunc]

ax = plt.gca()
plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
plt.errorbar(methods, values, yerr=error_bars, fmt="o", color="r", capsize=5)
plt.axhline(0.89)
ax.set_ylim([0.8, 0.98])
plt.text(0.25, 0.895, "Exact result")
ax.set_ylabel(r"$M_Z$", fontsize=12)
Text(0, 0.5, '$M_Z$')

이전 코드 셀의 출력

다음 단계​

이 내용이 흥미로우셨다면 다음 자료를 참고해 보세요.

추천