期待値推定のための演算子バックプロパゲーション(OBP)
推定所要時間:Heron r3 プロセッサで 4 分(注:これはあくまで目安です。 (実行時間は異なる場合があります。)
学習成果
このチュートリアルを終えた後、ユーザーは以下の点を理解できるようになります:
- 回路の実行回数を増やす代わりに、量子回路の深さを減らす方法
qiskit-addon-obp - XYZハミルトニアンとその時間発展回路を構築する方法
qiskit-addon-utils
前提条件
このチュートリアルを進める前に、以下の内容についてあらかじめ理解しておいていただくことをお勧めします:
- Estimatorプリミティブを使用して観測変数の期待値を計算する
背景
演算子のバックプロパゲーションとは、量子回路の末尾にある演算を測定される観測量に吸収させる手法であり、一般に、観測量に追加項が生じる代償として、回路の深さを浅くすることができる。 目標は、観測可能な変数が過度に大きくなるのを防ぎつつ、回路の可能な限り多くの部分をバックプロパゲーションさせることである。 OBP Qiskit アドオンには、Qiskit ベースの実装が含まれています。 詳細については、関連するドキュメントをご参照ください。
観測量 を測定する回路の例を考えてみよう。ここで、 はパウリ演算子であり、 は係数である。 この回路を単一のユニタリー と表すことにする。これは、下図に示すように、 へと論理的に分割することができる。
演算子のバックプロパゲーションでは、ユニタリー演算 を観測量に吸収し、 として進化させる。言い換えれば、計算の一部は、観測量が から へと進化する過程を通じて、古典的に実行される。これにより、元の問題は、ユニタリー演算が である新しい低深さの回路について、観測量 を測定するという問題として再定式化できる。
単一の は、複数のスライス として表現されます。スライスを定義する方法はいくつかあります。 例えば、上記の回路例において、 の各層および ゲートの各層は、それぞれ独立したスライスと見なすことができます。 バックプロパゲーションでは、従来、 の計算が行われます。 各スライス は、 と表すことができる。ここで、 は -量子ビットのパウリ演算子であり、 はスカラーである。 これは簡単に確認できます
上記の例において、 である場合、期待値を計算するには、1つではなく2つの量子回路を実行する必要があります。 したがって、バックプロパゲーションによって観測可能関数の項の数が増加し、その結果、回路の実行回数が増える可能性がある。 演算子の規模が過度に大きくなるのを防ぎつつ、回路へのバックプロパゲーションをより深く行うための方法の一つは、係数の小さい項を演算子に加えるのではなく、切り捨てることである。 例えば、上記の例では、 が十分に小さいという条件の下で、 を含む項を切り捨てることもできる。 項を切り捨てると、実行すべき量子回路の数が減るが、その代償として、切り捨てられた項の係数の大きさに比例した誤差が、最終的な期待値の計算に生じる。
要件
このチュートリアルを始める前に、以下のものがインストールされていることを確認してください:
- Qiskit SDK v2.0 以降、 可視化機能を搭載
- Qiskit Runtime v0.22 以降 (
pip install qiskit-ibm-runtime) - OBP Qiskit アドオン 0.3 以降 (
pip install qiskit-addon-obp) - Qiskit アドオン utils 0.3 以降 (
pip install qiskit-addon-utils)
セットアップ
import numpy as np
import matplotlib.pyplot as plt
from qiskit.primitives import StatevectorEstimator
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler import CouplingMap
from qiskit.synthesis import LieTrotter
from qiskit_addon_utils.problem_generators import generate_xyz_hamiltonian
from qiskit_addon_utils.problem_generators import (
generate_time_evolution_circuit,
)
from qiskit_addon_utils.slicing import slice_by_depth, combine_slices
from qiskit_addon_obp.utils.simplify import OperatorBudget
from qiskit_addon_obp import backpropagate
from qiskit_addon_obp.utils.truncating import setup_budget
from rustworkx.visualization import graphviz_draw
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import EstimatorV2, EstimatorOptions小規模シミュレータの例
このチュートリアルでは、OBP Qiskitアドオンを使用して、ハイゼンベルグスピン鎖の量子ダイナミクスをシミュレートするQiskit パターンを実装します。 なお、ノイズのないシミュレータでは、バックプロパゲーションを行う場合と行わない場合で得られる期待値は同じになります。
ステップ1:古典的な入力を量子問題にマッピングする
量子ハイゼンベルクモデルの時間発展を量子実験に写像する
まず、から qiskit-addon-utils 関数を generate_xyz_hamiltonian 用いて、与えられた接続グラフ上でハイゼンベルク型のハミルトニアンを生成する。 このグラフは、 rustworkx.PyGraph または CouplingMap のいずれかです。 以下では、10個の量子ビットからなる線形チェーン CouplingMap を使用する。
num_qubits = 10
layout = [(i - 1, i) for i in range(1, num_qubits)]
# Instantiate a CouplingMap object
coupling_map = CouplingMap(layout)
graphviz_draw(coupling_map.graph, method="circo")Output:
次に、ハイゼンベルクXYZハミルトニアンをモデル化するパウリ演算子を生成します:
ここで、 は結合写像のグラフである。 このチュートリアルでは、 を 、 を として使用しています。
# Get a qubit operator describing the Heisenberg XYZ model
hamiltonian = generate_xyz_hamiltonian(
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),
)
print(hamiltonian)Output:
SparsePauliOp(['IIIIIIIXXI', 'IIIIIIIYYI', 'IIIIIIIZZI', 'IIIIIXXIII', 'IIIIIYYIII', 'IIIIIZZIII', 'IIIXXIIIII', 'IIIYYIIIII', 'IIIZZIIIII', 'IXXIIIIIII', 'IYYIIIIIII', 'IZZIIIIIII', 'IIIIIIIIXX', 'IIIIIIIIYY', 'IIIIIIIIZZ', 'IIIIIIXXII', 'IIIIIIYYII', 'IIIIIIZZII', 'IIIIXXIIII', 'IIIIYYIIII', 'IIIIZZIIII', 'IIXXIIIIII', 'IIYYIIIIII', 'IIZZIIIIII', 'XXIIIIIIII', 'YYIIIIIIII', 'ZZIIIIIIII', 'IIIIIIIIIX', 'IIIIIIIIIY', 'IIIIIIIIIZ', 'IIIIIIIIXI', 'IIIIIIIIYI', 'IIIIIIIIZI', 'IIIIIIIXII', 'IIIIIIIYII', 'IIIIIIIZII', 'IIIIIIXIII', 'IIIIIIYIII', 'IIIIIIZIII', 'IIIIIXIIII', 'IIIIIYIIII', 'IIIIIZIIII', 'IIIIXIIIII', 'IIIIYIIIII', 'IIIIZIIIII', 'IIIXIIIIII', 'IIIYIIIIII', 'IIIZIIIIII', 'IIXIIIIIII', 'IIYIIIIIII', 'IIZIIIIIII', 'IXIIIIIIII', 'IYIIIIIIII', 'IZIIIIIIII', 'XIIIIIIIII', 'YIIIIIIIII', 'ZIIIIIIIII'],
coeffs=[0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j,
0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j,
1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j,
0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j,
0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j,
1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j,
0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 1.04719755+0.j,
0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,
0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j,
1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j,
0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,
0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j,
1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j,
0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,
0.34906585+0.j])
量子ビット演算子から、その時間発展をモデル化する量子回路を生成することができる。 我々は、リー・トロッター分解を用いて時間発展回路を構築した generate_time_evolution_circuit 。
circuit = generate_time_evolution_circuit(
hamiltonian,
time=0.2,
synthesis=LieTrotter(reps=2),
)
circuit.draw("mpl", style="iqp", fold=-1)Output:
ステップ2:量子ハードウェア実行に向けた問題の最適化
バックプロパゲーションを行うための回路スライスを作成する
この backpropagate 関数は、回路のスライスを一度にまとめてバックプロパゲーションを行います。 したがって、スライシングの選択は、特定の問題においてバックプロパゲーションの性能にどのような影響を与えるかに関わってくる。 ここでは、関数 slice_by_depth を使用して、同じタイプのゲートをスライスごとにグループ化します。
サーキットスライシングについてさらに詳しく知りたい方は、こちらのパッケージ qiskit-addon-utils のハウツーガイドをご覧ください。
slices = slice_by_depth(circuit, max_slice_depth=1)
print(f"Separated the circuit into {len(slices)} slices.")Output:
Separated the circuit into 18 slices.
バックプロパゲーション中にオペレータのサイズがどれほど大きくなるかを制限する
バックプロパゲーション中、演算子の項の数は、一般的に に急速に近づきます。ここで、 はスライスの数です。 演算子内の2つの項が量子ビットごとに可換でない場合、それらに対応する期待値を求めるには、それぞれ別の回路が必要となる。 例えば、2量子ビットの観測量 がある場合、 であるため、単一の基底での測定を行うだけで、これら2つの項の期待値を計算するのに十分である。 しかし、 は他の2つの項と反交換するため、 の期待値を計算するには、別の基底測定が必要となる。つまり、 を計算するには、1つではなく2つの回路が必要となる。演算子の項の数が増えるにつれて、必要な回路の実行回数も増える可能性がある。
演算子のサイズは、 backpropagate 関数の operator_budget kwarg を指定することで制限できる。 OperatorBudget インスタンスを受け取る。
割り当てられる追加リソースの量(回路の実行回数、ひいては必要なQPU時間)を制御するため、バックプロパゲーションされた観測量が持つことができるクビットごとの可換パウリ群の最大数を制限する。 ここでは、演算子に含まれるクビットごとの可換パウリ群の数が8を超えた時点で、バックプロパゲーションを停止させることを規定する。
op_budget = OperatorBudget(max_qwc_groups=8)回路からのスライスをバックプロパゲートする
、 は量子ビットの数である。 時間発展回路からのスライスをバックプロパゲートし、観測可能な項が8量子ビット以下のパウリ群にまとめられなくなるまでバックプロパゲートする。
observable = SparsePauliOp.from_sparse_list(
[("Z", [i], 1 / num_qubits) for i in range(num_qubits)],
num_qubits=num_qubits,
)
observableOutput:
SparsePauliOp(['IIIIIIIIIZ', 'IIIIIIIIZI', 'IIIIIIIZII', 'IIIIIIZIII', 'IIIIIZIIII', 'IIIIZIIIII', 'IIIZIIIIII', 'IIZIIIIIII', 'IZIIIIIIII', 'ZIIIIIIIII'],
coeffs=[0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j,
0.1+0.j, 0.1+0.j])
以下に示すように、6つのスライスに対してバックプロパゲーションを行い、項は8つではなく6つのグループにまとめられました。 これは、スライスをもう1つバックプロパゲーションすると、パウリ群の数が8を超えることになることを意味する。 返されたメタデータを調べれば、これが事実であることを確認できます。 なお、この部分では回路の変換が厳密である点にも留意されたい。 つまり、新しい観測量 の項は、いずれも切り捨てられていない。 バックプロパゲーションされた回路と演算子は、元の回路および演算子とまったく同じ結果を与える。
# Backpropagate slices onto the observable
bp_obs, remaining_slices, metadata = backpropagate(
observable, slices, operator_budget=op_budget
)
# Recombine the slices remaining after backpropagation
bp_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {metadata.num_backpropagated_slices} slices.")
print(
f"New observable has {len(bp_obs.paulis)} terms, which can be combined into "
f"{len(bp_obs.group_commuting(qubit_wise=True))} groups."
)
print(
f"Note that backpropagating one more slice would result in "
f"{metadata.backpropagation_history[-1].num_paulis[0]} terms "
f"across {metadata.backpropagation_history[-1].num_qwc_groups} groups."
)
print("The remaining circuit after backpropagation looks as follows:")
bp_circuit.draw("mpl", fold=-1, scale=0.6)Output:
Backpropagated 6 slices.
New observable has 60 terms, which can be combined into 6 groups.
Note that backpropagating one more slice would result in 114 terms across 12 groups.
The remaining circuit after backpropagation looks as follows:
シミュレータでの小規模な例については、切り捨ては行いません。 これは、ノイズがない場合、バックプロパゲーションを行う回路と行わない回路では同じ結果が得られるためであり、切り捨てを行うと近似が加わることで結果が悪化するためです。
回路を基本ゲートセットに変換する
ここで、元の回路とバックプロパゲーションされた回路の両方を、バックエンドのベースゲートにトランスパイルします。 小規模なインスタンスではシミュレータ上で実行するため、実際のバックエンドでトランスパイルを行う必要はありません。
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=133
)
print(backend)Output:
<IBMBackend('ibm_kingston')>
pm_basis = generate_preset_pass_manager(
optimization_level=3, basis_gates=backend.configuration().basis_gates
)
isa_circuit = pm_basis.run(circuit)
isa_bp_circuit = pm_basis.run(bp_circuit)ステップ3: Qiskit primitivesを使用して実行する
まず、元の回路とバックプロパゲーションされた回路に対応する 2 つのプリミティブ・ユニファイド・ブロック(PUB)を作成します。 次に、理想的な推定器を用いて確率分布を評価し、期待値を求めます。
pubs = [(isa_circuit, observable), (isa_bp_circuit, bp_obs)]rng = np.random.default_rng()
estimator = StatevectorEstimator(seed=rng)
job = estimator.run(pubs)ステップ4:後処理を行い、希望する従来の形式で結果を返す
ここで、元の回路とバックプロパゲーションを施した回路の期待値を求めます。
primitive_result = job.result()
circuit_expval = primitive_result[0].data.evs.item()
bp_circuit_expval = primitive_result[1].data.evs.item()methods = [
"No backpropagation",
"Backpropagation",
]
values = [circuit_expval, bp_circuit_expval]
ax = plt.gca()
plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
ax.set_ylim([0.6, 0.92])
ax.set_ylabel(r"$M_Z$", fontsize=12)Output:
Text(0, 0.5, '$M_Z$')
予想通り、2つの期待値は一致している。 ノイズのない状態ベクトルシミュレータ上で実行しているため、バックプロパゲーションは回路・観測値ペアに対する厳密な変換となります。したがって、元のワークフローとバックプロパゲーションを適用したワークフローは、 に対して同じ値を算出する必要があります。バックプロパゲーションの利点は、ノイズのあるハードウェア上で初めて明らかになります。そこでは、バックプロパゲーションを適用した回路の方が短いため、蓄積される誤差が少なくなります。これは、以下の大規模ハードウェアの例で示されている通りです。
大規模なハードウェアの例
実験を設計する際は、可視化やシミュレーションを容易にするために、まずは簡単な回路から始めるのが有効です。 ここでは、 および パラメータの値、ならびに観測量 を同じに設定した50キュービットのハイゼンベルク・ハミルトニアンについて、4回のトロッター法を用いた演算子のバックプロパゲーションを検討する。 このスケールにおける理想的な期待値は、力ずくの計算では求められないため、テンソルネットワークを用いて計算した結果、理想的な期待値は であることが得られた。
この大規模な例では、バックプロパゲーションに加え、切り捨て付きバックプロパゲーションについても紹介します。 理想的には、有効回路の深さを減らすために、可能な限りバックプロパゲーションを行いたい。 しかし、その結果、更新後の観測可能量に非通勤項が大量に含まれることになり、量子オーバーヘッドが増大してしまうことがよくある。 したがって、係数が小さい項については、「切り捨て」と呼ばれる手法を用いて除外することができます。 切り捨てを行うと、更新後の観測量の項の数が減るため、伝播範囲は広がるが、その一方で、ある程度の近似が生じる。 したがって、より深いバックプロパゲーションによって得られるノイズ低減効果が、近似誤差によって相殺されないよう、切り捨てを一定の範囲内に制限する必要がある。
切り捨ての量を制限するため、関数 setup_budget を使用して、各スライスごとにエラー予算を割り当てるとともに、バックプロパゲーションされた回路全体に対する総エラー予算を設定する。 これにより、回路全体だけでなく、各スライスごとに切り捨てが適切に制御されるようになります。 予算の配分方法については、こちらのガイドもご参照ください。
num_qubits = 50
layout = [(i - 1, i) for i in range(1, num_qubits)]
# Instantiate a CouplingMap object
coupling_map = CouplingMap(layout)
hamiltonian = generate_xyz_hamiltonian(
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),
)
# Generate a time evolution circuit for the Hamiltonian
circuit = generate_time_evolution_circuit(
hamiltonian,
time=0.2,
synthesis=LieTrotter(reps=4),
)
# Define the observable to measure
observable = SparsePauliOp.from_sparse_list(
[("Z", [i], 1 / num_qubits) for i in range(num_qubits)],
num_qubits,
)
slices = slice_by_depth(circuit, max_slice_depth=1)
# Define the maximum number of qwc groups allowed in the
# backpropagated observable,
# and the truncation error budget
op_budget = OperatorBudget(max_qwc_groups=15)
truncation_error_budget = setup_budget(
max_error_total=0.03, max_error_per_slice=0.005
)
# First backpropagation without truncation
bp_obs, remaining_slices, metadata = backpropagate(
observable, slices, operator_budget=op_budget
)
bp_circuit = combine_slices(remaining_slices)
# Now backpropagate with truncation, using the same operator budget and
# the defined truncation error budget
bp_obs_trunc, remaining_slices_trunc, metadata = backpropagate(
observable,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
bp_circuit_trunc = combine_slices(
remaining_slices_trunc, include_barriers=False
)
# Now we transpile the original circuit and the two backpropagated circuits,
# and apply the layout to the corresponding observables
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)
isa_circuit = pm.run(circuit)
isa_bp_circuit = pm.run(bp_circuit)
isa_bp_circuit_trunc = pm.run(bp_circuit_trunc)
isa_observable = observable.apply_layout(isa_circuit.layout)
isa_bp_observable = bp_obs.apply_layout(isa_bp_circuit.layout)
isa_bp_observable_trunc = bp_obs_trunc.apply_layout(
isa_bp_circuit_trunc.layout
)
# Compare the 2-qubit depth of each transpiled circuit to see how much
# depth backpropagation saved
print(
f"2-qubit depth without backpropagation: "
f"{isa_circuit.depth(lambda x: x.operation.num_qubits == 2)}"
)
print(
f"2-qubit depth with backpropagation: "
f"{isa_bp_circuit.depth(lambda x: x.operation.num_qubits == 2)}"
)
print(
f"2-qubit depth with backpropagation and truncation: "
f"{isa_bp_circuit_trunc.depth(lambda x: x.operation.num_qubits == 2)}"
)
pubs = [
(isa_circuit, isa_observable),
(isa_bp_circuit, isa_bp_observable),
(isa_bp_circuit_trunc, isa_bp_observable_trunc),
]
# Now we instantiate the Estimator primitive for the hardware with
# ZNE and measurement error
# mitigation and compute the three circuits and observables
options = EstimatorOptions()
options.default_precision = 0.01
options.resilience_level = 2
options.resilience.zne.noise_factors = [1, 1.2, 1.4]
options.resilience.zne.extrapolator = ["linear"]
estimator = EstimatorV2(mode=backend, options=options)
estimator.options.environment.job_tags = ["TUT_OBP"]
job = estimator.run(pubs)
# Retrieve the results and the standard deviations
result_no_bp = job.result()[0].data.evs.item()
result_bp = job.result()[1].data.evs.item()
result_bp_trunc = job.result()[2].data.evs.item()
std_no_bp = job.result()[0].data.stds.item()
std_bp = job.result()[1].data.stds.item()
std_bp_trunc = job.result()[2].data.stds.item()Output:
2-qubit depth without backpropagation: 24
2-qubit depth with backpropagation: 20
2-qubit depth with backpropagation and truncation: 18
print(f"Expectation value without backpropagation: {result_no_bp}")
print(f"Backpropagated expectation value: {result_bp}")
print(f"Backpropagated expectation value with truncation: {result_bp_trunc}")Output:
Expectation value without backpropagation: 0.9543907942381811
Backpropagated expectation value: 0.9445337385406468
Backpropagated expectation value with truncation: 0.934050286970965
# Plot the results
methods = [
"No backpropagation",
"Backpropagation",
"Backpropagation w/ truncation",
]
values = [result_no_bp, result_bp, result_bp_trunc]
error_bars = [std_no_bp, std_bp, std_bp_trunc]
ax = plt.gca()
plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
plt.errorbar(methods, values, yerr=error_bars, fmt="o", color="r", capsize=5)
plt.axhline(0.89)
ax.set_ylim([0.8, 0.98])
plt.text(0.25, 0.895, "Exact result")
ax.set_ylabel(r"$M_Z$", fontsize=12)Output:
Text(0, 0.5, '$M_Z$')
次のステップ
この作品が興味深かった方は、以下の資料もご参照ください:
- 時間発展回路のための近似量子コンパイル
- トロッター誤差を低減するための多製品対応式
pauli-prop、パウリ伝播のためのRustで高速化されたパッケージ。OBP、古典的な期待値推定、ノイズを含むシミュレーションに関するチュートリアルが用意されている