2큐비트 `Move` 명령으로 표현되는 와이어 커팅
이 튜토리얼에서는 와이어 커팅을 사용하여 7큐비트 회로를 두 개의 4큐비트 회로로 분할함으로써 기댓값을 재구성합니다.
다음은 이 Qiskit 패턴에서 진행할 단계들입니다.
- 1단계: 문제를 양자 회로와 연산자로 매핑:
- 해밀토니안을 양자 회로에 매핑합니다.
- 2단계: 타겟 하드웨어에 맞게 최적화 [커팅 애드온 사용]:
- 회로와 관측가능량을 커팅합니다.
- 하드웨어에 맞게 서브실험을 트랜스파일합니다.
- 3단계: 타겟 하드웨어에서 실행:
- 2단계에서 얻은 서브실험을
Sampler프리미티브를 사용하여 실행합니다.
- 2단계에서 얻은 서브실험을
- 4단계: 결과 후처리 [커팅 애드온 사용]:
- 3단계의 결과를 결합하여 대상 관측가능량의 기댓값을 재구성합니다.
1단계: 매핑
커팅할 회로 생성
먼저, arXiv:2302.03366v1의 Fig. 1(a)에서 영감을 받은 회로로 시작합니다.
# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-addon-cutting qiskit-aer qiskit-ibm-runtime
import numpy as np
from qiskit import QuantumCircuit
qc_0 = QuantumCircuit(7)
for i in range(7):
qc_0.rx(np.pi / 4, i)
qc_0.cx(0, 3)
qc_0.cx(1, 3)
qc_0.cx(2, 3)
qc_0.cx(3, 4)
qc_0.cx(3, 5)
qc_0.cx(3, 6)
qc_0.cx(0, 3)
qc_0.cx(1, 3)
qc_0.cx(2, 3)
<qiskit.circuit.instructionset.InstructionSet at 0x7f16ab191a80>
qc_0.draw("mpl")

관측가능량 지정
from qiskit.quantum_info import SparsePauliOp
observable = SparsePauliOp(["ZIIIIII", "IIIZIII", "IIIIIIZ"])
2단계: 최적화
원하는 커팅 위치에 Move 명령을 배치한 새 회로 생성
위 회로에서 가운데 Qubit 라인에 두 개의 와이어 커팅을 배치하여 회로를 각각 4큐비트로 구성된 두 개의 회로로 분리하고자 합니다. 이를 수행하는 한 가지 방법은 한 Qubit 와이어에서 다른 와이어로 상태를 이동시키는 2큐비트 Move 명령을 수동으로 배치하는 것입니다. Move 명령은 개념적으로 두 번째 Qubit에 대한 리셋 연산 후 SWAP Gate를 적용하는 것과 동등합니다. 이 명령의 효과는 첫 번째(소스) Qubit의 상태를 두 번째(목적지) Qubit으로 전송하면서, 두 번째 Qubit의 들어오는 상태는 버리는 것입니다. 의도한 대로 작동하려면 두 번째(목적지) Qubit이 시스템의 나머지 부분과 얽힘을 공유하지 않아야 합니다. 그렇지 않으면 리셋 연산이 시스템의 나머지 부분의 상태를 부분적으로 붕괴시키게 됩니다.
여기서는 Qubit이 하나 더 추가되고 Move 연산이 배치된 새 회로를 구성합니다. 이 예시에서는 Qubit을 재사용할 수 있습니다. 첫 번째 Move의 소스 Qubit이 두 번째 Move 연산의 목적지 Qubit이 됩니다.
참고: Move 명령을 직접 사용하는 대신, 단일 큐비트 CutWire 명령을 사용하여 와이어 커팅을 표시하는 방법도 있습니다. cut_wires 함수는 CutWire를 새로 할당된 Qubit들에 대한 Move 명령으로 변환하기 위해 존재합니다. 그러나 수동 방식과 달리, 이 자동 방식은 Qubit 와이어의 재사용을 허용하지 않습니다. 자세한 내용은 CutWire 사용 방법 가이드를 참고해 주세요.
from qiskit_addon_cutting.instructions import Move
qc_1 = QuantumCircuit(8)
for i in [*range(4), *range(5, 8)]:
qc_1.rx(np.pi / 4, i)
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)
qc_1.append(Move(), [3, 4])
qc_1.cx(4, 5)
qc_1.cx(4, 6)
qc_1.cx(4, 7)
qc_1.append(Move(), [4, 3])
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)
qc_1.draw("mpl")

새 회로에 맞는 관측가능량 생성
이 관측가능량은 observable에 대응하지만, 추가된 Qubit 와이어를 올바르게 반영해야 합니다(즉, 인덱스 4에 "I"를 삽입합니다). 참고로 Qiskit에서는 문자열 표현의 qubit-0이 가장 오른쪽 Pauli 문자에 해당합니다.
observable_expanded = SparsePauliOp(["ZIIIIIII", "IIIIZIII", "IIIIIIIZ"])
회로와 관측가능량 분리
앞서 살펴본 튜토리얼과 마찬가지로, 공통된 분할 레이블을 공유하는 Qubit들은 함께 그룹화되며, 여러 분할에 걸쳐 있는 비로컬 Gate는 커팅됩니다.
from qiskit_addon_cutting import partition_problem
partitioned_problem = partition_problem(
circuit=qc_1, partition_labels="AAAABBBB", observables=observable_expanded.paulis
)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
bases = partitioned_problem.bases
분해된 문제 시각화
subobservables
{'A': PauliList(['IIII', 'ZIII', 'IIIZ']),
'B': PauliList(['ZIII', 'IIII', 'IIII'])}
subcircuits["A"].draw("mpl")

subcircuits["B"].draw("mpl")

선택된 커팅에 대한 샘플링 오버헤드 계산
여기서는 두 개의 와이어를 커팅하여 의 샘플링 오버헤드가 발생합니다.
회로 커팅에서 발생하는 샘플링 오버헤드에 대한 자세한 내용은 설명 자료를 참고해 주세요.
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
Sampling overhead: 256.0
Backend에서 실행할 서브실험 생성
generate_cutting_experiments는 circuits/observables 인자를 Qubit 분할 레이블과 각각의 subcircuit/subobservables를 매핑하는 딕셔너리로 받습니다.
전체 크기 회로의 기댓값을 시뮬레이션하기 위해, 분해된 게이트들의 결합 준확률 분포로부터 다수의 서브실험이 생성되고 하나 이상의 Backend에서 실행됩니다. 분포에서 추출되는 샘플의 수는 num_samples로 제어되며, 각 고유 샘플마다 결합된 하나의 계수가 주어집니다. 계수가 계산되는 방식에 대한 자세한 내용은 설명 자료를 참고해 주세요.
from qiskit_addon_cutting import generate_cutting_experiments
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits, observables=subobservables, num_samples=np.inf
)
Backend 선택
여기서는 가짜 Backend를 사용하며, 그 결과 Qiskit Runtime이 로컬 모드(즉, 로컬 시뮬레이터)에서 실행됩 니다.
from qiskit_ibm_runtime.fake_provider import FakeManilaV2
backend = FakeManilaV2()
Backend용 서브실험 준비
Qiskit Runtime에 제출하기 전에 Backend를 타겟으로 하여 회로를 트랜스파일해야 합니다.
from qiskit.transpiler import generate_preset_pass_manager
# Transpile the subexperiments to ISA circuits
pass_manager = generate_preset_pass_manager(optimization_level=1, backend=backend)
isa_subexperiments = {
label: pass_manager.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}
3단계: 실행
Qiskit Runtime Sampler 프리미티브를 사용하여 서브실험 실행
from qiskit_ibm_runtime import SamplerV2, Batch
# Submit each partition's subexperiments to the Qiskit Runtime Sampler
# primitive, in a single batch so that the jobs will run back-to-back.
with Batch(backend=backend) as batch:
sampler = SamplerV2(mode=batch)
jobs = {
label: sampler.run(subsystem_subexpts, shots=2**12)
for label, subsystem_subexpts in isa_subexperiments.items()
}
/home/garrison/Qiskit/qiskit-ibm-runtime/qiskit_ibm_runtime/session.py:157: UserWarning: Session is not supported in local testing mode or when using a simulator.
warnings.warn(
# Retrieve results
results = {label: job.result() for label, job in jobs.items()}
4단계: 후처리
기댓값 재구성
각 관측가능량 항에 대한 기댓값을 재구성하고 이를 결합하여 원래 관측가능량의 기댓값을 재구성합니다.
from qiskit_addon_cutting import reconstruct_expectation_values
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
reconstructed_expval = np.dot(reconstructed_expval_terms, observable.coeffs)
재구성된 기댓값과 원래 회로 및 관측가능량의 정확한 기댓값 비교
from qiskit_aer.primitives import EstimatorV2
estimator = EstimatorV2()
exact_expval = estimator.run([(qc_0, observable)]).result()[0].data.evs
print(f"Reconstructed expectation value: {np.real(np.round(reconstructed_expval, 8))}")
print(f"Exact expectation value: {np.round(exact_expval, 8)}")
print(f"Error in estimation: {np.real(np.round(reconstructed_expval-exact_expval, 8))}")
print(
f"Relative error in estimation: {np.real(np.round((reconstructed_expval-exact_expval) / exact_expval, 8))}"
)
Reconstructed expectation value: 1.51319069
Exact expectation value: 1.59099026
Error in estimation: -0.07779957
Relative error in estimation: -0.04890009