Estimator 프리미티브로 오류 완화 옵션 결합하기
사용량 추정: Heron r2 프로세서에서 약 7분 소요됩니다. (참고: 이는 추정치이며 실제 런타임은 다를 수 있습니다.)
학습 목표
이 튜토리얼을 진행하기 전에 다음 주제에 익숙할 것을 권장합니다:
- 이 가이드에 설명된 동적 디커플링, 측정 오류 완화, Gate twirling, 영잡음 외삽법의 기본 개념.
사전 요구 사항
이 튜토리얼을 마치면 다음을 이해할 수 있습니다:
- 앞서 언급한 오류 완화 기법이 하드웨어에서 선택적으로 어떻게 구현되는지.
- 이 기법들이 하드웨어 노이즈 완화 능력 면에서 서로 어떻게 비교되는지.
배경
이 튜토리얼에서는 Qiskit Runtime의 Estimator 프리미티브에서 사용할 수 있는 오류 억제 및 오류 완화 옵션을 살펴봅니다. 이 튜토리얼은 다음 각 방법을 개별적으로 구현하는 방법을 보여줍니다:
- Dynamical decoupling
- 측정 오류 완화
- Gate twirling
- 영잡음 외삽법 (ZNE)
이러한 기법을 개별적으로 구현하는 대신 복원력 수준을 사용하여 구현할 수도 있으며, 이 경우 resilience_level은 0, 1, 2 값을 가집니다:
- 0 : 완화 없음.
- 1 : 측정 오류 완화 적용.
- 2 : Gate twirling, 측정 오류 완화, ZNE 적용.
이 튜토리얼에서는 Circuit과 Observable을 구성하고, 다양한 오류 완화 설정의 조합을 사용하여 Estimator 프리미티브로 작업을 제출합니다. 그런 다음 결과를 플롯하여 다양한 설정의 효과를 관찰합니다. 튜토리얼의 대부분은 시각화를 쉽게 하기 위해 10-Qubit Circuit을 사용하며, 마지막에는 워크플로를 50 Qubit으로 확장합니다.
요구 사항
이 안내서를 시작하기 전에 다음이 설치되어 있는지 확인하세요:
- 시각화 지원이 포함된 Qiskit SDK v2.1 이상
- Qiskit Runtime v0.40 이상 (
pip install qiskit-ibm-runtime)
설정
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-ibm-runtime
import matplotlib.pyplot as plt
import numpy as np
from qiskit.circuit.library import efficient_su2, unitary_overlap
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import Batch, EstimatorV2 as Estimator
소규모 시뮬레이터 예제
시뮬레이터에서는 런타임 오류 완화가 지원되지 않으므로 이 단계는 건너뜁니다.
하드웨어 예제
1단계: 고전적 입력을 양자 문제로 변환하기
이 안내서에서는 고전적 문제가 이미 양자 문제로 변환되어 있다고 가정합니다. 먼저 측정할 Circuit과 Observable을 구성합니다. 여기서 사용하는 기술은 다양한 종류의 Circuit에 적용할 수 있지만, 간단함을 위해 Qiskit Circuit 라이브러리에 포함된 efficient_su2 Circuit을 사용합니다.
efficient_su2는 제한된 qubit 연결성을 가진 양자 하드웨어에서 효율적으로 실행될 수 있도록 설계된 매개변수화된 양자 Circuit으로, 최적화 및 화학과 같은 응용 분야의 문제를 해결하기에 충분한 표현력을 갖추고 있습니다. 선택한 반복 횟수만큼 매개변수화된 단일 qubit Gate 레이어와 고정된 패턴의 2-Qubit Gate 레이어를 번갈아 구성합니다. 2-Qubit Gate 패턴은 사용자가 지정할 수 있습니다. 여기서는 2-Qubit Gate를 최대한 밀집하게 배치하여 Circuit 깊이를 최소화하는 내장 pairwise 패턴을 사용합니다. 이 패턴은 선형 qubit 연결성만으로도 실행할 수 있습니다.
n_qubits = 10
reps = 1
circuit = efficient_su2(n_qubits, entanglement="pairwise", reps=reps)
circuit.decompose().draw("mpl", scale=0.7)

Observable로는 마지막 Qubit에 작용하는 Pauli 연산자 를 사용합니다. 마지막 Qubit이 이 문자열의 첫 번째 원소에 해당하는 이유는 Qiskit이 리틀 엔디언 표기법을 사용하기 때문입니다.
# Z on the last qubit (index -1) with coefficient 1.0
observable = SparsePauliOp.from_sparse_list(
[("Z", [-1], 1.0)], num_qubits=n_qubits
)
이 시점에서 Circuit을 실행하고 Observable을 측정할 수 있습니다. 그러나 양자 장치의 출력을 정확한 답—즉 오류 없이 Circuit이 실행되었을 때의 Observable 이론값—과 비교하고 싶기도 합니다. 소규모 양자 Circuit의 경우 고전 컴퓨터에서 시뮬레이션하여 이 값을 계산할 수 있지만, 더 큰 유틸리티 규모의 Circuit에서는 불가능합니다. 이 문제는 양자 장치의 성능을 벤치마킹하는 데 유용한 "미러 Circuit" 기법("계산-역계산"이라고도 함)으로 해결할 수 있습니다.
미러 Circuit
미러 Circuit 기법에서는 Circuit과 그 역 Circuit을 연결합니다. 역 Circuit은 Circuit의 각 Gate를 역순으로 반전시켜 만듭니다. 결과 Circuit은 단위 연산자를 구현하므로 간단히 시뮬레이션할 수 있습니다. 원래 Circuit의 구조가 미러 Circuit에 보존되어 있기 때문에, 미러 Circuit 실행은 원래 Circuit에서 양자 장치가 어떻게 동작하는지를 여전히 파악하는 데 도움이 됩니다.
다음 코드 셀은 Circuit에 무작위 매개변수를 할당하고, unitary_overlap 클래스를 사용하여 미러 Circuit을 구성합니다. Circuit을 미러링하기 전에, Transpiler가 배리어 양쪽의 두 Circuit 부분을 합쳐 Gate가 없는 트랜스파일된 Circuit이 생성되지 않도록 배리어 명령을 추가합니다.
# Generate random parameters
rng = np.random.default_rng(1234)
params = rng.uniform(-np.pi, np.pi, size=circuit.num_parameters)
# Assign the parameters to the circuit
assigned_circuit = circuit.assign_parameters(params)
# Add a barrier to prevent circuit optimization of mirrored operators
assigned_circuit.barrier()
# Construct mirror circuit
mirror_circuit = unitary_overlap(assigned_circuit, assigned_circuit)
mirror_circuit.decompose().draw("mpl", scale=0.7)

2단계: 양자 하드웨어 실행을 위한 문제 최적화
하드웨어에서 실행하기 전에 Circuit을 최적화해야 합니다. 이 과정에는 다음 단계가 포함됩니다:
- Circuit의 가상 Qubit을 하드웨어의 물리적 Qubit에 매핑하는 qubit 레이아웃 선택
- 연결되지 않은 qubit 간의 상호작용을 라우팅하기 위해 필요에 따라 swap Gate 삽입
- Circuit의 Gate를 하드웨어에서 직접 실행할 수 있는 명령어 집합 아키텍처 (ISA) 명령어로 변환
- Circuit 깊이와 Gate 수를 최소화하는 Circuit 최적화 수행
Qiskit에 내장된 Transpiler가 이 모든 단계를 자동으로 수행할 수 있습니다. 이 예제에서는 하드웨어 효율적인 Circuit을 사용하므로, Transpiler가 라우팅 상호작용을 위해 swap Gate를 삽입하지 않아도 되는 qubit 레이아웃을 선택할 수 있어야 합니다.
Circuit을 최적화하기 전에 사용할 하드웨어 장치를 선택해야 합니다. 다음 코드 셀은 최소 127 Qubit을 가진 가장 한가한 장치를 요청합니다.
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)
print(backend)
<IBMBackend('ibm_fez')>
패스 매니저를 생성하고 Circuit에 실행하여 선택한 Backend에 맞게 Circuit을 트랜스파일할 수 있습니다. 패스 매니저를 생성하는 쉬운 방법은 generate_preset_pass_manager 함수를 사용하는 것입니다. 패스 매니저를 사용한 트랜스파일에 대한 자세한 설명은 패스 매니저로 트랜스파일하기를 참조하세요.
pass_manager = generate_preset_pass_manager(
optimization_level=3, backend=backend, seed_transpiler=1234
)
isa_circuit = pass_manager.run(mirror_circuit)
isa_circuit.draw("mpl", idle_wires=False, scale=0.7, fold=-1)

트랜스파일된 Circuit에는 이제 ISA 명령어만 포함되어 있습니다. 모든 Gate는 Gate와 회전, 그리고 CZ Gate로 분해되었습니다.
트랜스파일 과정에서 Circuit의 가상 Qubit이 하드웨어의 물리적 Qubit에 매핑되었습니다. Qubit 레이아웃에 대한 정보는 트랜스파일된 Circuit의 layout 속성에 저장됩니다. Observable도 가상 qubit 기준으로 정의되었으므로, SparsePauliOp의 apply_layout 메서드를 사용하여 Observable에 이 레이아웃을 적용해야 합니다.
isa_observable = observable.apply_layout(isa_circuit.layout)
print("Original observable:")
print(observable)
print()
print("Observable with layout applied:")
print(isa_observable)
Original observable:
SparsePauliOp(['ZIIIIIIIII'],
coeffs=[1.+0.j])
Observable with layout applied:
SparsePauliOp(['IIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j])
3단계: Qiskit 프리미티브를 사용한 실행
이제 Estimator 프리미티브를 사용하여 Circuit을 실행할 준비가 되었습니다.
여기서는 오류 억제 또는 완화 없이 시작하여 Qiskit Runtime에서 사용 가능한 다양한 오류 억제 및 완화 옵션을 순차적으로 활성화하면서 5개의 별도 작업을 제출합니다. 옵션에 대한 자세한 내용은 다음 페이지를 참조하세요:
이 작업들은 서로 독립적으로 실행될 수 있으므로, 배치 모드를 사용하여 Qiskit Runtime이 실행 타이밍을 최적화할 수 있습니다.
pub = (isa_circuit, isa_observable)
jobs = []
with Batch(backend=backend) as batch:
estimator = Estimator(mode=batch)
estimator.options.environment.job_tags = [
"TUT_CEM_SS"
] # add tag for this small scale job
# Set number of shots
estimator.options.default_shots = 100_000
# Disable runtime compilation and error mitigation
estimator.options.resilience_level = 0
# Run job with no error mitigation
job0 = estimator.run([pub])
jobs.append(job0)
# Add dynamical decoupling (DD)
estimator.options.dynamical_decoupling.enable = True
estimator.options.dynamical_decoupling.sequence_type = "XpXm"
job1 = estimator.run([pub])
jobs.append(job1)
# Add readout error mitigation (DD + TREX)
estimator.options.resilience.measure_mitigation = True
job2 = estimator.run([pub])
jobs.append(job2)
# Add gate twirling (DD + TREX + Gate Twirling)
estimator.options.twirling.enable_gates = True
estimator.options.twirling.num_randomizations = "auto"
job3 = estimator.run([pub])
jobs.append(job3)
# Add zero-noise extrapolation (DD + TREX + Gate Twirling + ZNE)
estimator.options.resilience.zne_mitigation = True
estimator.options.resilience.zne.noise_factors = (1, 3, 5)
estimator.options.resilience.zne.extrapolator = ("exponential", "linear")
job4 = estimator.run([pub])
jobs.append(job4)
4단계: 후처리 및 원하는 고전적 형식으로 결과 반환
마지막으로 데이터를 분석합니다. 여기서는 작업 결과를 가져오고, 측정된 기댓값을 추출한 다음, 1 표준편차의 오차 막대를 포함하여 값을 플롯합니다.
# Retrieve the job results
results = [job.result() for job in jobs]
# Unpack the PUB results (there's only one PUB result in each job result)
pub_results = [result[0] for result in results]
# Unpack the expectation values and standard errors
expectation_vals = np.array(
[float(pub_result.data.evs) for pub_result in pub_results]
)
standard_errors = np.array(
[float(pub_result.data.stds) for pub_result in pub_results]
)
# Plot the expectation values
fig, ax = plt.subplots()
labels = ["No mitigation", "+ DD", "+ TREX", "+ Twirling", "+ ZNE"]
ax.bar(
range(len(labels)),
expectation_vals,
yerr=standard_errors,
label="experiment",
)
ax.axhline(y=1.0, color="gray", linestyle="--", label="ideal")
ax.set_xticks(range(len(labels)))
ax.set_xticklabels(labels)
ax.set_ylabel("Expectation value")
ax.legend(loc="upper left")
plt.show()
이 소규모에서는 대부분의 오류 완화 기법의 효과를 보기 어렵지만, 영잡음 외삽법(ZNE)은 눈에 띄는 개선을 보여줍니다. 단, 이 개선은 무료로 얻어지는 것이 아니며, ZNE 결과의 오차 막대도 더 크다는 점에 유의하세요.
대규모 하드웨어 예제
실험을 개발할 때는 시각화와 시뮬레이션을 쉽게 하기 위해 작은 Circuit으로 시작하는 것이 유용합니다. 10-Qubit Circuit으로 워크플로우를 개발하고 테스트했으니, 이제 50 Qubit으로 확장할 수 있습니다. 다음 코드 셀은 이 안내서의 모든 단계를 반복하되, 50-Qubit Circuit에 적용합니다.
n_qubits = 50
reps = 1
# Construct circuit and observable
circuit = efficient_su2(n_qubits, entanglement="pairwise", reps=reps)
observable = SparsePauliOp.from_sparse_list(
[("Z", [-1], 1.0)], num_qubits=n_qubits
)
# Assign parameters to circuit
params = rng.uniform(-np.pi, np.pi, size=circuit.num_parameters)
assigned_circuit = circuit.assign_parameters(params)
assigned_circuit.barrier()
# Construct mirror circuit
mirror_circuit = unitary_overlap(assigned_circuit, assigned_circuit)
# Transpile circuit and observable
isa_circuit = pass_manager.run(mirror_circuit)
isa_observable = observable.apply_layout(isa_circuit.layout)
# Run jobs
pub = (isa_circuit, isa_observable)
jobs = []
with Batch(backend=backend) as batch:
estimator = Estimator(mode=batch)
estimator.options.environment.job_tags = [
"TUT_CEM_LS"
] # add tag for this large scale job
# Set number of shots
estimator.options.default_shots = 100_000
# Disable runtime compilation and error mitigation
estimator.options.resilience_level = 0
# Run job with no error mitigation
job0 = estimator.run([pub])
jobs.append(job0)
# Add dynamical decoupling (DD)
estimator.options.dynamical_decoupling.enable = True
estimator.options.dynamical_decoupling.sequence_type = "XpXm"
job1 = estimator.run([pub])
jobs.append(job1)
# Add readout error mitigation (DD + TREX)
estimator.options.resilience.measure_mitigation = True
job2 = estimator.run([pub])
jobs.append(job2)
# Add gate twirling (DD + TREX + Gate Twirling)
estimator.options.twirling.enable_gates = True
estimator.options.twirling.num_randomizations = "auto"
job3 = estimator.run([pub])
jobs.append(job3)
# Add zero-noise extrapolation (DD + TREX + Gate Twirling + ZNE)
estimator.options.resilience.zne_mitigation = True
estimator.options.resilience.zne.noise_factors = (1, 3, 5)
estimator.options.resilience.zne.extrapolator = ("exponential", "linear")
job4 = estimator.run([pub])
jobs.append(job4)
# Retrieve the job results
results = [job.result() for job in jobs]
# Unpack the PUB results (there's only one PUB result in each job result)
pub_results = [result[0] for result in results]
# Unpack the expectation values and standard errors
expectation_vals = np.array(
[float(pub_result.data.evs) for pub_result in pub_results]
)
standard_errors = np.array(
[float(pub_result.data.stds) for pub_result in pub_results]
)
# Plot the expectation values
fig, ax = plt.subplots()
labels = ["No mitigation", "+ DD", "+ TREX", "+ Twirling", "+ ZNE"]
ax.bar(
range(len(labels)),
expectation_vals,
yerr=standard_errors,
label="experiment",
)
ax.axhline(y=1.0, color="gray", linestyle="--", label="ideal")
ax.set_xticks(range(len(labels)))
ax.set_xticklabels(labels)
ax.set_ylabel("Expectation value")
ax.legend(loc="upper left")
plt.show()
50-Qubit 결과를 앞서 얻은 10-Qubit 결과와 비교하면 다음과 같은 점을 발견할 수 있습니다(실행마다 결과가 다를 수 있습니다):
- 모든 실험에서 이상적인 값에 더 가까운 결과가 나오고 오차 막대도 모두 작아집니다.
- 동적 디커플링(Dynamical Decoupling)을 추가하면 완화 없음 경우에 비해 성능이 오히려 나빠질 수 있습니다. Circuit이 매우 밀집되어 있기 때문에 이는 놀라운 일이 아닙니다. 동적 디커플링은 Gate가 적용되지 않고 Qubit이 유휴 상태로 있는 큰 공백이 Circuit 내에 있을 때 주로 유용합니다. 이러한 공백이 없을 경우 동적 디커플링은 효과적이지 않으며, 동적 디커플링 펄스 자체의 오류로 인해 성능이 실제로 악화될 수 있습니다. 10-Qubit Circuit은 이 효과를 관찰하기에 너무 작았을 수 있습니다.
- 영잡음 외삽(ZNE)을 적용하면 결과가 이상적인 값에 매우 가까워집니다. 이는 ZNE 기법의 강력함을 보여줍니다.
다음 단계
이 내용이 흥미로우셨다면, 이 튜토리얼에서 다루지 않은 추가적인 오류 완화 및 오류 억제 기법에 관한 다음 자료도 살펴보세요: