逆伝播中のPauli項の切り捨て
このガイドでは、qiskit_addon_obp.utils.truncatingモジュールが提供するPauli項の切り捨て機構の設定方法を説明します。
演算子逆伝播(OBP)を使うと、オブザーバブルがより複雑になる代わりに、量子回路の深さを減らせます。OBPから有意な結果を得るには、通常、オブザーバブルが大きくなりすぎないよう、その項を切り捨てる必要があります。演算子が大きくなりすぎないようにしながら、回路のより深くまで逆伝播させる方法の1つは、係数の小さい項を演算子に加えずに切り捨てることです。項を切り捨てると実行する量子回路の数を減らせますが、最終的な期待値の計算に、切り捨てた項の係数の大きさに比例する誤差が生じます。
backpropagateメソッドは、オプションでTruncationErrorBudgetを受け取ります。これは、各スライスの逆伝播が成功するたびに、各オブザーバブルに対する低ウェイトのPauli項の切り捨てを設定します。 切り捨てられる項の数は、ユーザーが指定するさまざまな設定パラメータに依存します。 現時点で利用できる切り捨て戦略はtruncate_binary_searchメソッドのみです。 オブザーバブルと何らかの_予算_が与えられると、このメソッドはそのオブザーバブル内のPauli項と係数に対して二分探索を行い、最適なしきい値を見つけます。 このしきい値は、切り捨てられる係数の合計が最大でありながら予算を下回るようなものです。
注意: デフォルトでは、切り捨て誤差の評価と上界にL1ノルムが使われます。ただし、p_norm設定を使うと、使用するLpノルムを指定できます。
この設定の使い方について詳しくは、Pauli項の切り捨てに異なるLpノルムを使用するガイドを参照してください。
以下の例では、付属のsetup_budget関数を使って、TruncationErrorBudgetを構築するさまざまな方法を示します。
サンプル回路を構築する
このガイドでは、次の回路スライスを使用します。
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"matplotlib": "matplotlib", "qiskit": "qiskit", "qiskit_addon_obp": "qiskit-addon-obp", "qiskit_addon_utils": "qiskit-addon-utils", "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
# Generate a linear chain of 10 qubits
linear_chain = rustworkx.generators.path_graph(10)
# 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,
)
# 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)
# for visualization purposes, 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)

単純なオブザーバブルを1つ取り上げます。
from qiskit.quantum_info import SparsePauliOp
obs = SparsePauliOp("IIIIIZIIII")
最も単純なケース: スライスごとに固定の切り捨て予算
Pauli項の切り捨てに使える予算は、逆伝播の各ステップで変化させることができます。 その仕組みを理解するために、まず、ユーザーが指定する固定の切り捨て予算という最も単純なケースを見ていきます。
切り捨て予算を指定する最も簡単な方法は、max_error_per_slice引数を使うことです。
実際、これは演算子逆伝播による回路の深さの削減チュートリアルで行われていることです。max_error_per_sliceにfloatを設定すると、各スライスにその値に等しい予算が割り当てられます。
以下の例では、この値を0.001に設定します。これにより、18個のスライスすべてを逆伝播した場合でも、暗黙の切り捨て誤差は最大で0.018に抑えられることが保証されます。
スライスを逆伝播し、係数の小さい項を切り捨てた後に残った誤差予算は、常に次のスライスの誤差予算に加算されることに注意してください。
from qiskit_addon_obp.utils.truncating import setup_budget
truncation_error_budget = setup_budget(max_error_per_slice=0.001)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.001], max_error_total=inf, p_norm=1)
from qiskit_addon_obp import backpropagate
from qiskit_addon_obp.utils.simplify import OperatorBudget
op_budget = OperatorBudget(max_qwc_groups=10)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 11 circuit slices.
New observable contains 29 terms and 10 commuting groups.
次に、OBPMetadataインスタンスとvisualizationモジュールが提供するツールを使って、逆伝播の過程を可視化します。
-
左上のプロットは、3番目のスライスを逆伝播した後に、オブザーバブルの項を切り捨て始めるのに十分な予算があることを示しています。3番目のスライス以降は、逆伝播する各スライスの後に何らかの切り捨て誤差が生じているため、各スライスから少なくとも1つの項が切り捨てられていることがわかります。
-
右上のプロットは、3番目のスライスで誤差予算が
.003に増えることを示しています。残り予算が急激に減っており、これはオブザーバブルから項が切り捨てられたことを意味します。これは、左上のプロットから推測した内容と一致しています。 -
左下のプロットは、オブザーバブルから項を切り捨てるにつれて、全体の累積誤差が単調に増加していることを示しています。このグラフからは、3番目のスライスが逆伝播されるまで項が切り捨てられていないことも確認できます。
-
右下のプロットは、オブザーバブル内の可換なPauliグループの数が、指定した上限
10まで増加していることを示しています。このプロットからは、黒線と赤線が交差していることからもわかるように、もう1層逆伝播するとオブザーバブルが指定した上限を超えてしまうことも見て取れます。
これらすべてのプロットで、x軸は逆伝播されたスライスを順に並べていますが、OBPは回路の末尾から作用するため、slice 1が最後のスライス、slice 2がその1つ前のスライス、というように続くことに注意してください。
from matplotlib import pyplot as plt
from qiskit_addon_obp.utils.visualization import (
plot_accumulated_error,
plot_left_over_error_budget,
plot_num_qwc_groups,
plot_slice_errors,
)
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

スライスの予算を明示的に指定する
逆伝播の性能が最適になるように各スライスへ予算を割り当てる方法がわかっている場合は、各スライスに予算を明示的に割り当てるとよいでしょう。デモとして、最初の3つのスライスには予算を0とし、残りのスライスにはスライスごとに.001の予算を使います。
# Zero out the first 3 slices' budgets
max_error_per_slice = [0.0] * 3 + [0.001] * (len(slices) - 3)
truncation_error_budget = setup_budget(
max_error_per_slice=max_error_per_slice
)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.0, 0.0, 0.0, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001, 0.001], max_error_total=inf, p_norm=1)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 11 circuit slices.
New observable contains 32 terms and 10 commuting groups.
最初の3層から予算を取り除いた結果、4層目以降まで項は切り捨てられませんでした。これは、4つのプロットのうち3つで確認できます。やや意外なのは、4番目のスライスには繰り越された予算がなかったにもかかわらず、割り当てられた.001の予算を使って項が切り捨てられたことです。これは左上のプロットから明らかですが、右上のプロットでも、3番目と4番目のスライスの間で残り予算の曲線が平坦になることから確認できます。もう1つ注目すべき点は、この時点以降、前の例と同様に少なくとも1つの項が切り捨てられていることです。
重要なポイントは、2番目の例では一部のスライスの予算を0にしたことで切り捨て誤差が小さくなったものの、逆伝播できたスライスの数は同じで、オブザーバブルに含まれる可換なPauliグループの数も同じだったということです。左下のプロットを調べると、2番目の例の方が誤差の上界が小さいことを確認できます。
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

予算を周期的に指定する
Trotter回路のように繰り返しパターンのある回路では、その繰り返される部分のスライスに対して予算を指定し、その後に続くすべての繰り返しで同じ予算を使うように設定できます。
より具体的には、使用しているサンプル回路には、3回繰り返される6つのスライスがあり、合計で18個のスライスになります。1キュービット層とRYY層には任意に予算0を、各RXX層には.003の予算を割り当てます。予算を長さ6のシーケンスとして指定すると、その予算が18個のスライスすべてに周期的に適用される様子を確認します。
ここでも、スライスは逆順(つまり末尾から)に逆伝播されることに注意してください。したがって、周期的な予算の最初のエントリは実際には最後のスライスに、2番目のエントリはその1つ前のスライスに、というように使われます。
# Specify a length-6 per-slice budget.
# This will be cycled over three times to be applied to the 18 slices
max_error_per_slice = [0.0] * 4 + [0.003] * 2
truncation_error_budget = setup_budget(
max_error_per_slice=max_error_per_slice
)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.0, 0.0, 0.0, 0.0, 0.003, 0.003], max_error_total=inf, p_norm=1)
op_budget = OperatorBudget(max_qwc_groups=20)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 13 circuit slices.
New observable contains 49 terms and 14 commuting groups.
左上と左下の画像からわかるように、最初の4つのスライスには誤差予算が割り当てられていないため、切り捨ては行われませんでした。5番目と6番目のスライスでは、予算が利用可能になったため、一部の項が切り捨てられました。
左上の画像からわかるように、7番目のスライスは予算が割り当てられていないにもかかわらず、逆伝播後に比較的大きな誤差が生じています。これは、5番目と6番目のスライスに割り当てられた合計.006の予算のうち、使われたのは約.002だけだったためです。残りは7番目のスライスに繰り越され、右上の画像に見られるようにその大部分が消費されました。
左上と右上の画像は、わずかに残った予算が8〜10番目のスライスの間で使い切られ、予想どおり11番目のスライスで新しい予算が利用可能になることを示しています。左上、右上、左下の画像はいずれも、max_error_per_slice引数の長さがスライスの数より短い場合の周期的な動作を示しています。この周期的な動作はすべてのスライスにわたって続くはずでしたが、右下の画像にあるように、13個のスライスを逆伝播した後にmax_qwc_groupsの停止条件に達しました。
また興味深いのは、11番目のスライスで2巡目の予算が利用可能になった後に、Pauliグループの数が実際に減っていることです。これは、係数の小さいグループがあったものの、11番目のスライスを逆伝播した後に予算が増えるまで切り捨てられず、数回の反復にわたってオブザーバブルに蓄積されていたためです。この例は、アルゴリズムが終了するにはmax_qwc_groupsが_超過される_必要があることも示しています。
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

全体の誤差に上限を設ける
スライスごとの誤差予算の指定に加えて、切り捨てによって生じる誤差の最大量を指定することもできます。その上限に達すると、それ以上の切り捨ては行われません。ただし、逆伝播は、オブザーバブルが大きくなりすぎて停止条件のいずれかを満たすまで続きます。
次に、7番目のスライスを逆伝播した後に誤差予算が使い切られるようにmax_error_totalの上限を設定して、上記の実験を再実行します。
# Specify a length-6 per-slice budget.
# This will be cycled over 3 times to be applied to the 18 slices
max_error_per_slice = [0.0] * 4 + [0.003] * 2
truncation_error_budget = setup_budget(
max_error_per_slice=max_error_per_slice, max_error_total=0.006
)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.0, 0.0, 0.0, 0.0, 0.003, 0.003], max_error_total=0.006, p_norm=1)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 10 circuit slices.
New observable contains 67 terms and 20 commuting groups.
予想どおり、切り捨て誤差は.006で頭打ちになります(左下のグラフ)。今回の実行では11番目のスライスを逆伝播できなかった点が注目に値します。これは、項を切り捨てるための予算が足りず、可換なPauliグループの数が上限の20を超えてしまったためで、右下の画像で確認できます。
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

max_error_per_sliceを指定せずに、max_error_totalだけで全体の予算に上限を設けたい場合もあるでしょう。
truncation_error_budget = setup_budget(max_error_total=0.018)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.018], max_error_total=0.018, p_norm=1)
上のセルの出力は、per_slice_budgetがmax_error_totalに設定されているため、少し意外に思えるかもしれません。これは、利用可能な予算全体が貪欲に消費されることを示しています。
次のように考えるとよいでしょう。予算全体が各スライスで利用可能です(per_slice_budgetについてループするため)。ただし、すでに消費された予算は、アルゴリズムのその時点で利用可能な予算から差し引かれます。
op_budget = OperatorBudget(max_qwc_groups=10)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 9 circuit slices.
New observable contains 25 terms and 9 commuting groups.
右上の画像は、.018の誤差予算全体が最初のスライスで利用可能になる様子を示しています。3番目のスライスまでは切り捨てが行われないため、繰り越された予算は一定のままです。予算は、使い切られるまで、逆伝播される各スライスに全額が利用可能になるため、単調に減少します。
この実験では、このノートブックの最初の実験とほぼ同一であるにもかかわらず、逆伝播できたスライスが2つ少なかった点が注目に値します。これは、問題によっては、予算を均等に配分するのが最適な場合があることを示しています。他の問題では、スライスが貪欲に予算全体を消費できるようにする方が、よい性能が得られる可能性があります。
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

逆伝播するスライス数と全体の誤差の両方に上限を設ける
誤差予算を回路の最後まで分配したくなく、かといって貪欲に消費したくもない場合は、逆伝播するつもりのスライス数(num_slices)と、全体の誤差予算(max_error_total)を指定できます。これにより、誤差予算が入力スライス全体に(p_normに従って)均等に分配されます。
ここでは、逆伝播するスライス数の上限を12とし、全体の誤差予算は同じままにします。
num_slices = 12
truncation_error_budget = setup_budget(
max_error_total=0.018, num_slices=num_slices, p_norm=1
)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.0014999999999999998], max_error_total=0.018, p_norm=1)
次に、前のステップで予算を割り当てた12個のスライスを逆伝播してみます。これは、回路の最後の12個のスライスを渡すだけで行えます。p_norm=1の場合、12個のスライスそれぞれの利用可能な予算は0.018 / num_slices = 0.0015です。
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices[-num_slices:],
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
スライスの一部(コードslices[-num_slices:])をbackpropagateに渡したため、逆伝播後に残ったスライスと、逆伝播に送られなかったスライス(コードslices[:-num_slices])を結合する必要があります。
残りのスライスをすべて結合したら、combine_slicesを使って、深さが削減されたQuantumCircuitを生成できます。次に、逆伝播されたスライスの数と、オブザーバブルがどれだけ大きくなったかを調べます。
# Recombine the slices remaining after backprop with the rest of the original circuit
reduced_circuit = combine_slices(slices[:-num_slices] + remaining_slices)
print(f"Backpropagated {num_slices - len(remaining_slices)} circuit slices.")
print(
f"New observable contains {len(bp_obs)} terms and {len(bp_obs.group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 12 circuit slices.
New observable contains 29 terms and 9 commuting groups.
プロットは、オブザーバブルを10個未満の可換パウリ・グループに保ちながら、12個すべてのスライスを逆伝播できたことを示しています。また、num_slices と max_error_total を併用すると、予算がスライス間に分配され、未使用の予算が再び次のスライスに引き継がれることもわかります。これは 右上 のプロットで最も顕著で、逆伝播の過程で予算が消費され、また補充されています。
このノートブックの最初の実験や、直前の例と比べて、この誤差の分配方法が最も良い結果(より多くのスライスを逆伝播)をもたらしたことは注目に値します。これらの例はすべて .018 の誤差予算を割り当てていますが、その予算をどのように分配するかによって、逆伝播の結果は異なりました。
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])

複数のオブザーバブルを扱う
qiskit_addon_obp.backpropagate メソッドでは、オブザーバブルのシーケンスを渡すことができます。これにより、複数のターゲット・オブザーバブルを扱う際のワークフローが簡単になります。
ここでは、切り捨て戦略がこのようなケースをどのように扱うかを説明するため、あえて明示します。この例では、これまで使用してきたオブザーバブルに、もう1つオブザーバブルを追加します。
obs = [SparsePauliOp("IIIIIZIIII"), SparsePauliOp("IIIIIXIIII")]
最後に、このチュートリアルの最初の実験を繰り返しますが、今回は回路を逆伝播させる先のオブザーバブルを2つにします。
この例では、逆伝播できたスライスの数には影響しません。ただし、2つのオブザーバブルでパウリ項の数と可換グループの数が異なる結果になっていることがわかります。
truncation_error_budget = setup_budget(max_error_per_slice=0.001)
print(truncation_error_budget)
TruncationErrorBudget(per_slice_budget=[0.001], max_error_total=inf, p_norm=1)
bp_obs, remaining_slices, metadata = backpropagate(
obs,
slices,
operator_budget=op_budget,
truncation_error_budget=truncation_error_budget,
)
reduced_circuit = combine_slices(remaining_slices)
print(f"Backpropagated {len(slices) - len(remaining_slices)} circuit slices.")
print(
f"The new first observable contains {len(bp_obs[0])} terms and {len(bp_obs[0].group_commuting(qubit_wise=True))} commuting groups."
)
print(
f"The new second observable contains {len(bp_obs[1])} terms and {len(bp_obs[1].group_commuting(qubit_wise=True))} commuting groups."
)
Backpropagated 11 circuit slices.
The new first observable contains 29 terms and 10 commuting groups.
The new second observable contains 23 terms and 8 commuting groups.
以下のプロットは、逆伝播アルゴリズムが複数のオブザーバブルをどのように扱うかを説明するのに役立ちます。
まず、左上、右上、左下 のプロットは、項を切り捨てるための予算が各オブザーバブルに対して個別に設定されていることを示しています。言い換えると、どちらのオブザーバブルも、逆伝播したスライスごとに 0.001 の誤差を想定して項を切り捨てることができます。
オブザーバブルの性質が異なるため、予算の消費の仕方も異なります。この例では多くの部分が重なっていますが、常にそうなるとは限りません。
右下 のプロットは、max_qwc_groups が すべての オブザーバブルを考慮に入れることを示しています。つまり、すべてのオブザーバブルの項がまとめてグルーピングされ、量子ビット単位で可換なグループの最終的な数が1つ求められ、それが max_qwc_paulis と比較されます。max_paulis のしきい値(このノートブックでは扱いません)についても同様で、すべてのオブザーバブルにわたるパウリ項の数に上限を設定できます。
fig, axes = plt.subplots(2, 2, figsize=(20, 10))
plot_slice_errors(metadata, axes[(0, 0)])
plot_left_over_error_budget(metadata, axes[(0, 1)])
plot_accumulated_error(metadata, axes[(1, 0)])
plot_num_qwc_groups(metadata, axes[(1, 1)])
