주 콘텐츠로 건너뛰기
이 프로젝트는 베타 단계입니다. 버그를 발견하셨나요? GitHub 이슈 만들기. 질문이나 아이디어가 있으신가요? Discussions에서 질문하기 — 피드백을 기다립니다!
이 페이지의 번역 품질은 어떻습니까?

OBP로 회로를 고전적으로 시뮬레이션하기

이 가이드에서는 OBP만으로 기댓값을 추정하기 위해 QuantumCircuit 인스턴스를 고전적으로 시뮬레이션하는 방법을 알아봅니다.

OBP는 관측량을 받아 주어진 회로를 통해 역전파하므로, 회로의 "시뮬레이션"은 이 회로에 대한 목표 관측량의 기댓값을 계산하는 것과 같습니다. 나중에 보시겠지만, qiskit-addon-obp 패키지는 간단한 노이즈 모델도 처리할 수 있어 노이즈가 있는 기댓값도 계산할 수 있습니다!

예제 회로 구성하기​

이 가이드에서는 Pauli term truncation guide와 같은 예제 회로를 사용합니다.

# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util

_needed = {"numpy": "numpy", "qiskit": "qiskit", "qiskit_addon_obp": "qiskit-addon-obp", "qiskit_addon_utils": "qiskit-addon-utils", "qiskit_ibm_runtime": "qiskit-ibm-runtime", "rustworkx": "rustworkx"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
import rustworkx.generators
from qiskit.synthesis import LieTrotter
from qiskit_addon_utils.problem_generators import (
PauliOrderStrategy,
generate_time_evolution_circuit,
generate_xyz_hamiltonian,
)
from qiskit_addon_utils.slicing import combine_slices, slice_by_gate_types

# we generate a linear chain of 10 qubits
num_qubits = 10
linear_chain = rustworkx.generators.path_graph(num_qubits)

# we use an arbitrary XY model
hamiltonian = generate_xyz_hamiltonian(
linear_chain,
coupling_constants=(0.05, 0.02, 0.0),
ext_magnetic_field=(0.02, 0.08, 0.0),
pauli_order_strategy=PauliOrderStrategy.InteractionThenColor,
)
# we evolve for some time
circuit = generate_time_evolution_circuit(
hamiltonian, synthesis=LieTrotter(reps=3), time=2.0
)
# slice the circuit by gate type
slices = slice_by_gate_types(circuit)

그러나 위의 회로는 선택한 해밀토니안에 따른 시간 발전만을 나타냅니다. 관측량의 기댓값을 계산할 기준이 되는 초기 상태도 필요합니다.

물론 초기 상태로 전부 0인(또는 진공) 상태를 선택할 수도 있지만, 자신만의 초기 상태를 삽입하는 방법을 보이기 위해 아래에서는 다른 상태를 선택합니다.

한 가지 방법은 위의 시간 발전 회로 앞에 초기 상태를 붙이는 것입니다: circuit.compose(initial_state, front=True). 하지만 이미 circuit을 슬라이스했으므로, 아래와 같이 초기 상태를 첫 번째 슬라이스로 삽입하는 것이 더 간단합니다.

이렇게 하면 나중에 초기 상태를 바꾸고 싶을 때 슬라이스를 다시 계산하지 않고도 첫 번째 슬라이스를 다른 초기 상태로 간단히 교체할 수 있습니다.

from qiskit.circuit import QuantumCircuit

initial_state = QuantumCircuit(num_qubits)
for i in range(0, num_qubits, 2):
initial_state.x(i)

slices.insert(0, initial_state)
# for visualization purposes only, we recombine the slices with barriers between them and draw the resulting circuit
combine_slices(slices, include_barriers=True).draw("mpl", fold=50, scale=0.6)

Output of the previous code cell

노이즈 없는 기댓값 시뮬레이션하기​

목표 관측량으로 중앙 큐비트들에 대한 ZZ 관측량을 선택합니다.

from qiskit.quantum_info import SparsePauliOp

obs = SparsePauliOp("IIIIZZIIII")

이제 OBP를 사용하여 기댓값을 고전적으로 시뮬레이션할 준비가 되었습니다. 이를 위해 다음과 같이 모든 슬라이스를 backpropagate 메서드에 전달하면 됩니다.

from qiskit_addon_obp import backpropagate

vacuum_state_obs, _, metadata = backpropagate(obs, slices)

이제 목표 관측량 obs를 (slices[0]에 놓은 initial_state를 포함하여) 전체 회로를 통해 역전파했으며, 그 결과 새로운 SparsePauliOp를 얻었습니다. 이 연산자의 기댓값은 진공 상태(|00...00>)에 사영하여 구할 수 있습니다.

이는 계산 기저로 정의된 모든 Pauli 항의 계수를 합하는 것으로 간단히 구할 수 있습니다.

vacuum_state_obs.coeffs[~vacuum_state_obs.paulis.x.any(axis=1)].sum()
np.complex128(-0.8285688012239535+4.9487770271457865e-20j)

정상 동작 확인(그리고 이것이 실제로 작동함을 보이기) 위해 결과를 Qiskit의 Statevector와 비교할 수 있습니다.

from qiskit.quantum_info import Statevector

Statevector(combine_slices(slices)).expectation_value(obs)
np.complex128(-0.8285687255430366+0j)

성능에 대한 몇 가지 참고 사항​

위의 backpropagate 호출의 계산 효율은 다음을 포함한 여러 요소에 크게 좌우됩니다.

  • circuit의 구조
  • 회로를 슬라이스하는 방법
  • 목표 관측량
  • 절단 매개변수

backpropagate 메서드는 각 _슬라이스_가 적용될 때마다 관측량을 단순화하므로, 슬라이스에 포함된 게이트 수가 계산 부담에 크게 영향을 줄 수 있습니다. 연산자 단순화 측면에서 가장 적극적인 전략은 회로를 개별 게이트 단위의 슬라이스로 나누는 것입니다.

또한 backpropagate 메서드에 내장된 모든 절단 메커니즘을 활용할 수 있습니다. 위에서는 사용하지 않았기 때문에 사실상 정확한 기댓값이 나왔지만, 사용 방법은 Pauli term truncation guide에서 알아볼 수 있습니다.

노이즈 있는 기댓값 시뮬레이션하기​

qiskit-addon-obp 패키지는 PauliLindbladError 형태의 노이즈 모델 처리도 지원합니다. 이는 예를 들어 NoiseLearner를 사용하여 회로의 2-큐비트 레이어에 대한 노이즈 모델을 특성화한 경우 특히 유용합니다.

이 섹션에서는 NoiseLearner가 반환하는 LayerError 객체를 사용하여 OBP로 노이즈 있는 기댓값을 계산하는 방법을 살펴봅니다.

노이즈 모델 얻기​

일반적으로는 NoiseLearner를 실행하여 특정 회로의 노이즈 모델을 얻습니다. 이 튜토리얼에서는 복잡성(과 무작위성)을 피하기 위해 그렇게 하지 않고, 아래에서 회로에 대한 노이즈 모델을 직접 코드에 지정합니다.

다만 데이터의 구조는 NoiseLearnerResult의 구조와 일치하도록 합니다. 현재(트랜스파일되지 않은) 형태에서 회로에는 고유한 2-큐비트 게이트 레이어가 4개 있습니다.

  • slices[1]: 모든 홀수 큐비트 쌍에 작용하는 Rxx 게이트
  • slices[2]: 모든 짝수 큐비트 쌍에 작용하는 Rxx 게이트
  • slices[3]: 모든 홀수 큐비트 쌍에 작용하는 Ryy 게이트
  • slices[4]: 모든 짝수 큐비트 쌍에 작용하는 Ryy 게이트

아래 셀에서는 이 레이어 각각에 대해 무작위 오류율을 가진 LayerError 인스턴스 4개를 직접 구성합니다.

import numpy as np
from qiskit.quantum_info import PauliList
from qiskit_ibm_runtime.utils.noise_learner_result import (
LayerError,
PauliLindbladError,
)

# fmt: off
pauli_errors_even = ['IIIIIIIIIX', 'IIIIIIIIIY', 'IIIIIIIIIZ', 'IIIIIIIIXI', 'IIIIIIIIXX', 'IIIIIIIIXY', 'IIIIIIIIXZ', 'IIIIIIIIYI', 'IIIIIIIIYX', 'IIIIIIIIYY', 'IIIIIIIIYZ', 'IIIIIIIIZI', 'IIIIIIIIZX', 'IIIIIIIIZY', 'IIIIIIIIZZ', 'IIIIIIIXII', 'IIIIIIIXXI', 'IIIIIIIXYI', 'IIIIIIIXZI', 'IIIIIIIYII', 'IIIIIIIYXI', 'IIIIIIIYYI', 'IIIIIIIYZI', 'IIIIIIIZII', 'IIIIIIIZXI', 'IIIIIIIZYI', 'IIIIIIIZZI', 'IIIIIIXIII', 'IIIIIIXXII', 'IIIIIIXYII', 'IIIIIIXZII', 'IIIIIIYIII', 'IIIIIIYXII', 'IIIIIIYYII', 'IIIIIIYZII', 'IIIIIIZIII', 'IIIIIIZXII', 'IIIIIIZYII', 'IIIIIIZZII', 'IIIIIXIIII', 'IIIIIXXIII', 'IIIIIXYIII', 'IIIIIXZIII', 'IIIIIYIIII', 'IIIIIYXIII', 'IIIIIYYIII', 'IIIIIYZIII', 'IIIIIZIIII', 'IIIIIZXIII', 'IIIIIZYIII', 'IIIIIZZIII', 'IIIIXIIIII', 'IIIIXXIIII', 'IIIIXYIIII', 'IIIIXZIIII', 'IIIIYIIIII', 'IIIIYXIIII', 'IIIIYYIIII', 'IIIIYZIIII', 'IIIIZIIIII', 'IIIIZXIIII', 'IIIIZYIIII', 'IIIIZZIIII', 'IIIXIIIIII', 'IIIXXIIIII', 'IIIXYIIIII', 'IIIXZIIIII', 'IIIYIIIIII', 'IIIYXIIIII', 'IIIYYIIIII', 'IIIYZIIIII', 'IIIZIIIIII', 'IIIZXIIIII', 'IIIZYIIIII', 'IIIZZIIIII', 'IIXIIIIIII', 'IIXXIIIIII', 'IIXYIIIIII', 'IIXZIIIIII', 'IIYIIIIIII', 'IIYXIIIIII', 'IIYYIIIIII', 'IIYZIIIIII', 'IIZIIIIIII', 'IIZXIIIIII', 'IIZYIIIIII', 'IIZZIIIIII', 'IXIIIIIIII', 'IXXIIIIIII', 'IXYIIIIIII', 'IXZIIIIIII', 'IYIIIIIIII', 'IYXIIIIIII', 'IYYIIIIIII', 'IYZIIIIIII', 'IZIIIIIIII', 'IZXIIIIIII', 'IZYIIIIIII', 'IZZIIIIIII', 'XIIIIIIIII', 'XXIIIIIIII', 'XYIIIIIIII', 'XZIIIIIIII', 'YIIIIIIIII', 'YXIIIIIIII', 'YYIIIIIIII', 'YZIIIIIIII', 'ZIIIIIIIII', 'ZXIIIIIIII', 'ZYIIIIIIII', 'ZZIIIIIIII']
pauli_errors_odd = ['IIIIIIIIXI', 'IIIIIIIIYI', 'IIIIIIIIZI', 'IIIIIIIXII', 'IIIIIIIXXI', 'IIIIIIIXYI', 'IIIIIIIXZI', 'IIIIIIIYII', 'IIIIIIIYXI', 'IIIIIIIYYI', 'IIIIIIIYZI', 'IIIIIIIZII', 'IIIIIIIZXI', 'IIIIIIIZYI', 'IIIIIIIZZI', 'IIIIIIXIII', 'IIIIIIXXII', 'IIIIIIXYII', 'IIIIIIXZII', 'IIIIIIYIII', 'IIIIIIYXII', 'IIIIIIYYII', 'IIIIIIYZII', 'IIIIIIZIII', 'IIIIIIZXII', 'IIIIIIZYII', 'IIIIIIZZII', 'IIIIIXIIII', 'IIIIIXXIII', 'IIIIIXYIII', 'IIIIIXZIII', 'IIIIIYIIII', 'IIIIIYXIII', 'IIIIIYYIII', 'IIIIIYZIII', 'IIIIIZIIII', 'IIIIIZXIII', 'IIIIIZYIII', 'IIIIIZZIII', 'IIIIXIIIII', 'IIIIXXIIII', 'IIIIXYIIII', 'IIIIXZIIII', 'IIIIYIIIII', 'IIIIYXIIII', 'IIIIYYIIII', 'IIIIYZIIII', 'IIIIZIIIII', 'IIIIZXIIII', 'IIIIZYIIII', 'IIIIZZIIII', 'IIIXIIIIII', 'IIIXXIIIII', 'IIIXYIIIII', 'IIIXZIIIII', 'IIIYIIIIII', 'IIIYXIIIII', 'IIIYYIIIII', 'IIIYZIIIII', 'IIIZIIIIII', 'IIIZXIIIII', 'IIIZYIIIII', 'IIIZZIIIII', 'IIXIIIIIII', 'IIXXIIIIII', 'IIXYIIIIII', 'IIXZIIIIII', 'IIYIIIIIII', 'IIYXIIIIII', 'IIYYIIIIII', 'IIYZIIIIII', 'IIZIIIIIII', 'IIZXIIIIII', 'IIZYIIIIII', 'IIZZIIIIII', 'IXIIIIIIII', 'IXXIIIIIII', 'IXYIIIIIII', 'IXZIIIIIII', 'IYIIIIIIII', 'IYXIIIIIII', 'IYYIIIIIII', 'IYZIIIIIII', 'IZIIIIIIII', 'IZXIIIIIII', 'IZYIIIIIII', 'IZZIIIIIII']
# fmt: on

np.random.seed(42)

layer_error_odd_xx = LayerError(
circuit=slices[1],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_odd),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_odd)),
),
)

layer_error_even_xx = LayerError(
circuit=slices[2],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_even),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_even)),
),
)

layer_error_odd_yy = LayerError(
circuit=slices[3],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_odd),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_odd)),
),
)

layer_error_even_yy = LayerError(
circuit=slices[4],
qubits=list(range(num_qubits)),
error=PauliLindbladError(
PauliList(pauli_errors_even),
0.0001 + 0.0004 * np.random.rand(len(pauli_errors_even)),
),
)

NoiseLearner를 사용하여 회로의 고유한 2-큐비트 게이트 레이어를 식별하고 그 노이즈를 특성화했다면 NoiseLearnerResult 객체를 얻었을 것입니다. 이 결과에는 위에서 직접 구성한 것과 같은 LayerError 객체의 목록이 들어 있습니다.

고유한 2-큐비트 레이어마다 LayerError에는 다음이 포함됩니다.

  • 해당 2-큐비트 게이트 레이어를 나타내는 QuantumCircuit
  • 이 회로가 작용하는 큐비트 인덱스
  • 이 레이어의 특성화된 노이즈 모델을 나타내는 PauliLindbladError

PauliLindbladError에는 두 개의 객체가 들어 있습니다.

  • 특성화된 Pauli 오류의 목록
  • 각 Pauli 오류에 대응하는 오류율

일반적으로 Pauli 오류 목록은 희소합니다. 구체적으로는 게이트가 작용하는 모든 큐비트에 대한 단일 큐비트 Pauli 오류와, 연결된 모든 큐비트에 대한 2-큐비트 Pauli 오류를 포함합니다.

노이즈 레이어를 회로에 삽입하기​

이전 섹션에서는 알려진 고유한 2-큐비트 게이트 레이어 각각에 대해 LayerError를 하나씩 구성했습니다. 즉, 어떤 LayerError가 어떤 슬라이스와 정확히 일치하는지 알고 있습니다.

일반적으로 LayerError를 사용할 때는 고유한 2-큐비트 게이트 레이어가 무엇이며 회로 안 어디에 나타나는지 알아내야 합니다. 그런 다음 circuit 및/또는 slices를 조정하여 LayerError를 알맞게 삽입해야 합니다. 일반적인 경우에 이를 수행하는 방법은 이 사용법 가이드의 범위를 벗어납니다. TODO: link to external documentation, once it exists! 여기서는 LayerError가 어느 슬라이스에 해당하는지 알고 있으므로 더 간단합니다. 따라서 노이즈를 나타내는 새 슬라이스를 삽입하기만 하면 됩니다.

LayerError.error의 각 PauliLindbladError는 유효한 QuantumCircuit 명령이 되도록 PauliLindbladErrorInstruction으로 감싸야 합니다.

from qiskit_addon_obp.utils.noise import PauliLindbladErrorInstruction

noisy_slices = []
for slice_ in slices:
if slice_ == layer_error_even_xx.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_even_xx.error
),
slice_.qubits,
)
]
)
)
elif slice_ == layer_error_odd_xx.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_odd_xx.error
),
slice_.qubits,
)
]
)
)
elif slice_ == layer_error_even_yy.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_even_yy.error
),
slice_.qubits,
)
]
)
)
elif slice_ == layer_error_odd_yy.circuit:
noisy_slices.append(
QuantumCircuit.from_instructions(
[
(
PauliLindbladErrorInstruction(
layer_error_odd_yy.error
),
slice_.qubits,
)
]
)
)
noisy_slices.append(slice_)

아래에서 회로를 그려 작업을 확인할 수 있습니다.

combine_slices(noisy_slices, include_barriers=True).draw(
"mpl", fold=100, scale=0.8
)

Output of the previous code cell

노이즈 있는 기댓값 시뮬레이션하기​

이 시점에서 기댓값을 고전적으로 시뮬레이션하는 방법은 이전과 완전히 같습니다. 다만

vacuum_state_noisy_obs, _, metadata = backpropagate(obs, noisy_slices)
vacuum_state_noisy_obs.coeffs[
~vacuum_state_noisy_obs.paulis.x.any(axis=1)
].sum()
np.complex128(-0.7230801696448901+7.082755280463563e-19j)

여러 성능 문제를 고려해야 한다는 점을 다시 강조합니다. 위의 해당 섹션으로 돌아가 확인하세요.