주 콘텐츠로 건너뛰기

Estimator 프리미티브로 오류 완화 옵션 결합하기

사용량 추정: Heron r2 프로세서에서 약 7분 소요됩니다. (참고: 이는 추정치이며 실제 런타임은 다를 수 있습니다.)

배경

이 안내서에서는 Qiskit Runtime의 Estimator 프리미티브에서 사용할 수 있는 오류 억제 및 오류 완화 옵션을 살펴봅니다. Circuit과 Observable을 구성하고, 다양한 오류 완화 설정의 조합을 사용하여 Estimator 프리미티브로 작업을 제출합니다. 그런 다음 결과를 플롯하여 다양한 설정의 효과를 관찰합니다. 대부분의 예제는 시각화를 쉽게 하기 위해 10-Qubit Circuit을 사용하며, 마지막에는 워크플로를 50 Qubit으로 확장할 수 있습니다.

사용할 오류 억제 및 완화 옵션은 다음과 같습니다:

  • Dynamical decoupling
  • 측정 오류 완화
  • Gate twirling
  • 영잡음 외삽법 (ZNE)

요구 사항

이 안내서를 시작하기 전에 다음이 설치되어 있는지 확인하세요:

  • 시각화 지원이 포함된 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를 사용합니다.

# 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 부분을 합치지 못하도록 배리어 명령을 추가합니다. 배리어가 없으면 Transpiler가 원래 Circuit과 역 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
)

패스 매니저를 생성하고 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 명령어만 포함되어 있습니다. 단일 Qubit Gate는 X\sqrt{X} Gate와 RzR_z 회전으로 분해되었으며, CX Gate는 ECR Gate와 단일 Qubit 회전으로 분해되었습니다.

트랜스파일 과정에서 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(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
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)
# 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)
# 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 결과와 비교하면 다음과 같은 점을 발견할 수 있습니다(실행마다 결과가 다를 수 있습니다):

  • 오류 완화 없이 실행한 결과가 더 나쁩니다. 더 큰 Circuit을 실행하면 더 많은 Gate를 실행해야 하므로 오류가 쌓일 기회가 더 많아집니다.
  • 동적 디커플링(Dynamical Decoupling)을 추가하면 성능이 오히려 나빠질 수 있습니다. Circuit이 매우 밀집되어 있기 때문에 이는 놀라운 일이 아닙니다. 동적 디커플링은 Gate가 적용되지 않고 Qubit이 유휴 상태로 있는 큰 공백이 Circuit 내에 있을 때 주로 유용합니다. 이러한 공백이 없을 경우 동적 디커플링은 효과적이지 않으며, 동적 디커플링 펄스 자체의 오류로 인해 성능이 실제로 악화될 수 있습니다. 10-Qubit Circuit은 이 효과를 관찰하기에 너무 작았을 수 있습니다.
  • 영잡음 외삽(ZNE)을 적용하면 오차 막대가 훨씬 크더라도 결과가 10-Qubit 결과만큼, 또는 거의 그만큼 좋아집니다. 이는 ZNE 기법의 강력함을 보여줍니다!

결론

이 안내서에서는 Qiskit Runtime Estimator 프리미티브에서 사용할 수 있는 다양한 오류 완화 옵션을 살펴보았습니다. 10-Qubit Circuit을 사용하여 워크플로우를 개발한 후 50 Qubit으로 확장했습니다. 더 많은 오류 억제 및 완화 옵션을 활성화한다고 해서 항상 성능이 향상되는 것은 아니라는 점(특히 이 경우 동적 디커플링 활성화)을 관찰했을 수 있습니다. 대부분의 옵션은 추가 설정을 허용하므로, 여러분의 작업에서 직접 테스트해 볼 수 있습니다!