주 콘텐츠로 건너뛰기

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 ZZ 연산자 ZIIZ I \cdots I를 사용합니다. 마지막 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는 X\sqrt{X} Gate와 RzR_z 회전, 그리고 CZ Gate로 분해되었습니다.

트랜스파일 과정에서 Circuit의 가상 Qubit이 하드웨어의 물리적 Qubit에 매핑되었습니다. Qubit 레이아웃에 대한 정보는 트랜스파일된 Circuit의 layout 속성에 저장됩니다. Observable도 가상 qubit 기준으로 정의되었으므로, SparsePauliOpapply_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 기법의 강력함을 보여줍니다.

다음 단계

Recommendations

이 내용이 흥미로우셨다면, 이 튜토리얼에서 다루지 않은 추가적인 오류 완화 및 오류 억제 기법에 관한 다음 자료도 살펴보세요: