メインコンテンツへスキップ

OBP を使ってみる

Package versions

このページのコードは、以下の要件を使って開発されました。 これらのバージョン以降を使用することをお勧めします。

qiskit[all]~=2.3.0
qiskit-ibm-runtime~=0.43.1
qiskit-addon-utils~=0.3.0
qiskit-addon-obp~=0.3.0

演算子バックプロパゲーション (OBP) を使って量子ワークロードを準備する際は、まず「回路スライス」を選択し、次にバックプロパゲーションされた演算子の小さな係数を持つ項を除去するためのトランケーション閾値(「エラーバジェット」)を指定し、バックプロパゲーションされた演算子全体のサイズの上限を設定する必要があります。バックプロパゲーション中、NN-qubit 回路の演算子の項数は最悪の場合、急速に 4N4^N に近づきます。このガイドでは、量子ワークロードに OBP を適用する手順を説明します。

qiskit-addons-obp パッケージの主要コンポーネントは backpropagate() 関数です。この関数は、再構成する最終オブザーバブル、古典的に計算する回路スライスのセット、およびオプションで TruncationErrorBudget または OperatorBudget を引数として受け取り、トランケーションに対する制約を提供します。これらを指定すると、古典的に計算されたバックプロパゲーション演算子 OO' が各スライス ss からのゲートを次の方法で適用することで反復的に計算されます:

O(s)=USs+1O(s1)USs+1O'^{(s)} = \mathcal{U}_{S-s+1}^\dagger O'^{(s-1)} \mathcal{U}_{S-s+1}

ここで SS はスライスの総数、Us\mathcal{U}_{s} は回路の単一スライスを表します。この例では、qiskit-addons-utils パッケージを使用して回路スライスを準備し、サンプル回路を生成します。

まず、Heisenberg XYZ 鎖の時間発展を考えます。このハミルトニアンは次の形式をとります:

H^=(j,k)(JxXjXk+JyYjYk+JzZjZk)+j(hxXj+hyYj+hzZj) \hat{H} = \sum_{(j,k)} \left( J_xX_jX_k + J_yY_jY_k + J_z Z_jZ_k \right) + \sum_{j} \left(h_xX_j + h_yY_j + h_zZ_j\right)

測定する期待値は Z0\langle Z_0 \rangle です。

次のコードスニペットは、qiskit_addons_utils.problem_generators モジュールと CouplingMap を使用して SparsePauliOp の形式でハミルトニアンを生成します。結合定数を Jx=π/8J_x=\pi/8Jy=π/4J_y=\pi/4Jz=π/2J_z=\pi/2 に、外部磁場を hx=π/3h_x=\pi/3hy=π/6h_y=\pi/6hz=π/9h_z=\pi/9 に設定し、その時間発展をモデル化する回路を生成します。

# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-addon-obp qiskit-addon-utils qiskit-ibm-runtime
import numpy as np
from qiskit.transpiler import CouplingMap
from qiskit.synthesis import LieTrotter
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService, EstimatorV2
from qiskit_ibm_runtime.fake_provider import FakeMelbourneV2
from qiskit.primitives import StatevectorEstimator
from qiskit.quantum_info import SparsePauliOp
from qiskit_addon_utils.problem_generators import (
generate_time_evolution_circuit,
generate_xyz_hamiltonian,
)
from qiskit_addon_utils.slicing import slice_by_gate_types
from qiskit_addon_obp.utils.simplify import OperatorBudget
from qiskit_addon_obp.utils.truncating import setup_budget
from qiskit_addon_obp import backpropagate
from qiskit_addon_utils.slicing import combine_slices

coupling_map = CouplingMap.from_heavy_hex(3, bidirectional=False)

# Choose a 10-qubit linear chain on this coupling map
reduced_coupling_map = coupling_map.reduce(
[0, 13, 1, 14, 10, 16, 5, 12, 8, 18]
)

# Get a qubit operator describing the Heisenberg XYZ model
hamiltonian = generate_xyz_hamiltonian(
reduced_coupling_map,
coupling_constants=(np.pi / 8, np.pi / 4, np.pi / 2),
ext_magnetic_field=(np.pi / 3, np.pi / 6, np.pi / 9),
)

# we evolve for some time
circuit = generate_time_evolution_circuit(
hamiltonian, synthesis=LieTrotter(reps=2), time=0.2
)

circuit.draw("mpl")

Output of the previous code cell

バックプロパゲーションの入力を準備する

次に、バックプロパゲーション用の回路スライスを生成します。一般に、スライス方法の選択は、特定の問題に対してバックプロパゲーションがどれだけ効果的に機能するかに影響を与える可能性があります。ここでは、qiskit_addons_utils.slice_by_gate_types 関数を使用して、同じタイプのゲートをスライスにグループ化します。

slices = slice_by_gate_types(circuit)
print(f"Separated the circuit into {len(slices)} slices.")
Separated the circuit into 18 slices.

スライスが生成されたら、OperatorBudget を指定して backpropagate() 関数に演算子のバックプロパゲーションを停止する条件を与え、古典的なオーバーヘッドがさらに増大するのを防ぎます。また、各スライスに対してトランケーションエラーバジェットを指定することもできます。このバジェットが満たされるまで、小さな係数を持つ Pauli 項が各スライスからトランケーションされます。残ったバジェットは次のスライスのバジェットに追加されます。

ここでは、演算子内の量子ビット単位で可換な Pauli グループの数が 88 を超えたときにバックプロパゲーションを停止し、各スライスに 0.0050.005 のエラーバジェットを割り当てるよう指定します。

op_budget = OperatorBudget(max_qwc_groups=8)
truncation_error_budget = setup_budget(max_error_per_slice=0.005)

スライスをバックプロパゲーションする

このステップでは、測定する最終オブザーバブルを定義し、各スライスにわたってバックプロパゲーションを実行します。backpropagate() 関数は 3 つの出力を返します:バックプロパゲーションされたオブザーバブル、バックプロパゲーションされなかった残りの回路スライス(量子ハードウェア上で実行する必要があります)、およびバックプロパゲーションに関するメタデータです。

OperatorBudgetTruncationErrorBudget はいずれも backpropagate() メソッドのオプションパラメーターであることに注意してください。一般に、両方の最適な選択はヒューリスティックに決定する必要があり、ある程度の実験が必要です。この例では、TruncationErrorBudget ありとなしの両方でバックプロパゲーションを行います。

デフォルトでは、backpropagate() はトランケーションされた係数の L1L_1 ノルムを使用してトランケーションから生じる総誤差を制限しますが、トランケーション誤差の計算方法を変更したい場合は他の LpL_p ノルムを使用することもできます。

# Specify a single-qubit observable
observable = SparsePauliOp("IIIIIIIIIZ")

# Backpropagate without the truncation error budget
backpropagated_observable, remaining_slices, metadata = backpropagate(
observable,
slices,
operator_budget=op_budget,
)

# Recombine the slices remaining after backpropagation
bp_circuit = combine_slices(remaining_slices, include_barriers=True)

print(f"Backpropagated {metadata.num_backpropagated_slices} slices.")
print(
f"New observable has {len(backpropagated_observable.paulis)} terms, which can be combined into "
f"{len(backpropagated_observable.group_commuting(qubit_wise=True))} groups.\n"
f"After truncation, the error in our observable is bounded by {metadata.accumulated_error(0):.3e}"
)
print(
f"Note that backpropagating one more slice would result in {metadata.backpropagation_history[-1].num_paulis[0]} terms "
f"across {metadata.backpropagation_history[-1].num_qwc_groups} groups."
)
Backpropagated 7 slices.
New observable has 18 terms, which can be combined into 8 groups.
After truncation, the error in our observable is bounded by 0.000e+00
Note that backpropagating one more slice would result in 27 terms across 12 groups.
print(
"The remaining circuit after backpropagation without truncation looks as follows:"
)
bp_circuit.draw("mpl", scale=0.6)
The remaining circuit after backpropagation without truncation looks as follows:

Output of the previous code cell

以下のコードスニペットは、トランケーションエラーバジェットありで回路をバックプロパゲーションします。

# Backpropagate *with* the truncation error budget
backpropagated_observable_trunc, remaining_slices_trunc, metadata_trunc = (
backpropagate(
observable,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
)

# Recombine the slices remaining after backpropagation
bp_circuit_trunc = combine_slices(
remaining_slices_trunc, include_barriers=True
)

print(f"Backpropagated {metadata_trunc.num_backpropagated_slices} slices.")
print(
f"New observable has {len(backpropagated_observable_trunc.paulis)} terms, which can be combined into "
f"{len(backpropagated_observable_trunc.group_commuting(qubit_wise=True))} groups.\n"
f"After truncation, the error in our observable is bounded by {metadata_trunc.accumulated_error(0):.3e}"
)
print(
f"Note that backpropagating one more slice would result in {metadata_trunc.backpropagation_history[-1].num_paulis[0]} terms "
f"across {metadata_trunc.backpropagation_history[-1].num_qwc_groups} groups."
)
Backpropagated 10 slices.
New observable has 19 terms, which can be combined into 8 groups.
After truncation, the error in our observable is bounded by 4.933e-02
Note that backpropagating one more slice would result in 27 terms across 13 groups.
print(
"The remaining circuit after backpropagation with truncation looks as follows:"
)
bp_circuit_trunc.draw("mpl", scale=0.6)
The remaining circuit after backpropagation with truncation looks as follows:

Output of the previous code cell

量子ワークロードをトランスパイルして実行する

演算子をバックプロパゲーションしたので、残りの回路部分を QPU 上で実行できます。Estimator を使用する量子ワークロードには bp_circuit_trunc 回路を含め、バックプロパゲーションされた演算子 backpropagated_observable を測定する必要があります。

OBP 単独の効果を実証するために、次のコードスニペットはオリジナルとバックプロパゲーションされた回路(トランケーションありとなし)の両方をトランスパイルし、StatevectorEstimator を使用して回路を古典的にシミュレーションします。

# Specify a backend and a pass manager for transpilation
backend = FakeMelbourneV2()
# pm = generate_preset_pass_manager(backend=backend, optimization_level=1)

pm = generate_preset_pass_manager(backend=backend, optimization_level=3)

# Transpile original experiment
circuit_isa = pm.run(circuit)
observable_isa = observable.apply_layout(circuit_isa.layout)

# Transpile backpropagated experiment without truncation
bp_circuit_isa = pm.run(bp_circuit)
bp_obs_isa = backpropagated_observable.apply_layout(bp_circuit_isa.layout)

# Transpile the backpropagated experiment with truncated observable terms
bp_circuit_trunc_isa = pm.run(bp_circuit_trunc)
bp_obs_trunc_isa = backpropagated_observable_trunc.apply_layout(
bp_circuit_trunc_isa.layout
)

estimator = StatevectorEstimator()

# Run the experiments using the exact statevector estimator
result_exact = (
estimator.run([(circuit, observable)]).result()[0].data.evs.item()
)

result_bp = (
estimator.run([(bp_circuit_isa, bp_obs_isa)]).result()[0].data.evs.item()
)
result_bp_trunc = (
estimator.run([(bp_circuit_trunc_isa, bp_obs_trunc_isa)])
.result()[0]
.data.evs.item()
)

print(f"Exact expectation value: {result_exact}")
print(f"Backpropagated expectation value without truncation: {result_bp}")
print(f"Backpropagated expectation value with truncation: {result_bp_trunc}")
print(
f" - Expected Error for truncated observable: {metadata_trunc.accumulated_error(0):.3e}"
)
print(
f" - Observed Error for truncated observable: {abs(result_exact - result_bp_trunc):.3e}"
)
Exact expectation value: 0.8854160687717517
Backpropagated expectation value without truncation: 0.8854160687717533
Backpropagated expectation value with truncation: 0.8850236647156081
- Expected Error for truncated observable: 4.933e-02
- Observed Error for truncated observable: 3.924e-04

最後に、次のコードスニペットはバックプロパゲーションされた回路を QPU 上でトランスパイルして実行します(トランケーションありとなしの両方)。

# Specify a backend and a pass manager for transpilation
service = QiskitRuntimeService()
backend = service.least_busy()
pm = generate_preset_pass_manager(backend=backend, optimization_level=3)

# Transpile backpropagated experiment without truncation
bp_circuit_isa = pm.run(bp_circuit)
bp_obs_isa = backpropagated_observable.apply_layout(bp_circuit_isa.layout)

# Transpile the backpropagated experiment with truncated observable terms
bp_circuit_trunc_isa = pm.run(bp_circuit_trunc)
bp_obs_trunc_isa = backpropagated_observable_trunc.apply_layout(
bp_circuit_trunc_isa.layout
)

# Run the experiments using Estimator primitive
estimator = EstimatorV2(mode=backend)

result_bp_qpu = (
estimator.run([(bp_circuit_isa, bp_obs_isa)]).result()[0].data.evs.item()
)

result_bp_trunc_qpu = (
estimator.run([(bp_circuit_trunc_isa, bp_obs_trunc_isa)])
.result()[0]
.data.evs.item()
)

print(f"Exact expectation value: {result_exact}")
print(f"Backpropagated expectation value without truncation: {result_bp_qpu}")
print(
f"Backpropagated expectation value with truncation: {result_bp_trunc_qpu}"
)
print(
f" - Observed Error for observable without truncation: {abs(result_exact - result_bp_qpu):.3e}"
)
print(
f" - Observed Error for truncated observable: {abs(result_exact - result_bp_trunc_qpu):.3e}"
)
Exact expectation value: 0.8854160687717517
Backpropagated expectation value without truncation: 0.8790435084647706
Backpropagated expectation value with truncation: 0.8759838342768448
- Observed Error for observable without truncation: 6.373e-03
- Observed Error for truncated observable: 9.432e-03

次のステップ

推奨事項