2量子ビット`Move`命令として表現されるワイヤーカット
このチュートリアルでは、ワイヤーカットを使用して7量子ビットのCircuitを2つの4量子ビットのCircuitに分割することで、期待値を再構成します。
ここでは、以下のQiskitパターンの手順に従って進めます。
- ステップ1: 問題を量子Circuit・演算子にマッピングする:
- ハミルトニアンを量子Circuitにマッピングします。
- ステップ2: ターゲットハードウェア向けに最適化する [カッティング・アドオンを使用]:
- CircuitとObservableをカットします。
- サブ実験をハードウェア向けにトランスパイルします。
- ステップ3: ターゲットハードウェアで実行する:
- ステップ2で得られたサブ実験を
Samplerプリミティブで実行します。
- ステップ2で得られたサブ実験を
- ステップ4: 結果を後処理する [カッティング・アドオンを使用]:
- ステップ3の結果を組み合わせて、対象のObservableの期待値を再構成します。
ステップ1: マッピング
カットするCircuitを作成する
まず、arXiv:2302.03366v1の図1(a)にヒントを得たCircuitから始めます。
# 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")

Observableを指定する
from qiskit.quantum_info import SparsePauliOp
observable = SparsePauliOp(["ZIIIIII", "IIIZIII", "IIIIIIZ"])
ステップ2: 最適化
所望のカット位置にMove命令を配置した新しいCircuitを作成する
上記のCircuitに対して、中央のQubitラインに2箇所のワイヤーカットを配置し、Circuitをそれぞれ4量子ビットの2つのCircuitに分割したいとします。これを実現する方法の1つは、ある量子ビットワイヤーから別のワイヤーへ状態を移動させる2量子ビットのMove命令を手動で配置することです。Move命令は概念的には、第2量子ビットに対するリセット操作の後にSWAP Gateを適用することと等価です。この命令の効果は、第1(ソース)量子ビットの状態を第2(デスティネーション)量子ビットに転送し、第2量子ビットの入力状態を破棄することです。これが意図通りに機能するためには、第2(デスティネーション)量子ビットがシステムの残りの部分とエンタングルメントを共有していないことが重要です。そうでない場合、リセット操作によってシステムの残りの部分の状態が部分的に崩壊してしまいます。
ここでは、1つの追加QubitとMove操作を配置した新しいCircuitを構築します。この例では、Qubitの再利用が可能です。つまり、最初のMoveのソースQubitが、2番目のMove操作のデスティネーションQubitになります。
注意:Move命令を直接扱う代わりに、単一Qubitの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")

新しいCircuitに対応するObservableを作成する
このObservableはobservableに対応していますが、追加されたQubitワイヤーを正しく考慮する必要があります(すなわち、インデックス4に"I"を挿入します)。なお、Qiskitでは、文字列表現においてQubit-0は最も右側のパウリ文字に対応します。
observable_expanded = SparsePauliOp(["ZIIIIIII", "IIIIZIII", "IIIIIIIZ"])
CircuitとObservableを分割する
前のチュートリアルと同様に、共通のパーティションラベルを共有する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")

選択したカットのサンプリング・オーバーヘッドを計算する
ここでは2本のワイヤーをカットしており、結果としてサンプリング・オーバーヘッドは になります。
回路カットによるサンプリング・オーバーヘッドの詳細については、解説資料を参照してください。
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
Sampling overhead: 256.0
Backendで実行するサブ実験を生成する
generate_cutting_experimentsは、QubitパーティションラベルをそれぞれのサブCircuit・サブObservableにマッピングする辞書としてcircuits/observables引数を受け取ります。
完全なサイズのCircuitの期待値をシミュレートするために、分解されたGateの結合準確率分布から多数のサブ実験が生成され、1つまたは複数のBackendで実行されます。分布からのサンプル数はnum_samplesで制御され、一意なサンプルごとに1つの結合係数が与えられます。係数の計算方法の詳細については、解説資料を参照してください。
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をターゲットとしてサブ実験のCircuitをトランスパイルする必要があります。
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プリミティブでサブCircuitを実行する
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: 後処理
期待値を再構成する
各Observableの項について期待値を再構成し、それらを組み合わせて元のObservableの期待値を再構成します。
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)
再構成された期待値と元のCircuitおよびObservableから得られる厳密な期待値を比較する
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