設定オプションの調整方法
バウンドの計算は原理的には厳密ですが、実際には近似的です。Pauli エラー項を回路を通して発展させると指数関数的に多くの項を持つ演算子が生成され、その結果得られる交換子のスペクトルノルムを求めること自体も高コストです。以下のオプションはこのコストを制御します。 オプションは次の 3 つのグループに分類されます。
-
精度オプションは、バウンドの数値そのものを変化させ、厳密さとコストのトレードオフを調整します。
-
スコープは、どのバウンドを計算するかを決定します。
-
実行オプションは、計算をどのように実行するかを変更します(何を計算するかは変わりません)。
| オプション | デフォルト | 制御対象 | 結果への影響 |
|---|---|---|---|
| 精度 | |||
evolution_max_terms | 1_000_000 | 発展後に残る演算子項の数 | 小さくする: 低コスト、緩いバウンド |
atol_simplify | 1e-8 | 無視できるとして破棄される交換子項 | 大きくする: 低コスト、緩いバウンド |
eigval_max_qubits | 14 | スペクトルノルムを計算する交換子の最大幅 | 小さくする: 低コスト、緩いバウンド |
atol_eigenvalue | 1e-10 | 反復固有値ソルバーが収束したとみなされる条件 | 大きくする: 低コスト、精度の低いバウンド |
| スコープ | |||
max_num_boxes | None | バウンドを計算するボックスの数 | 上限を超えたボックスは自明なバウンド 2.0 のままになります |
| 実行 | |||
timeout | None | 計算全体に対する実時間の制限 | 残りの作業は自明なバウンド 2.0 のままになります |
num_processes | 1 | バウンドを並列計算するワーカープロセスの数 | 単独では影響なし |
バウンドが有用なのは、有効である場合のみです。つまり、エラー項がオブザーバブルに与える影響を決して過小評価してはいけません。atol_eigenvalue 以外のこれらのオプションを緩めると、バウンドは大きく(より保守的に)なるだけで、小さくなることはありません。したがって、緩い設定のコストはサンプリングオーバーヘッドの増大であり、期待値の誤りではありません。atol_eigenvalue は例外です。
回路の定義
以下の実験では、クイックスタートガイドと同じ Trotter 化された Ising 回路とオブザーバブルを使用します。このサイズでは、各オプションの効果が目に見えて現れる一方、各計算は 1 秒未満で完了します。
# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util
_needed = {"numpy": "numpy", "qiskit": "qiskit", "qiskit_addon_slc": "qiskit-addon-slc", "samplomatic": "samplomatic"}
_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 numpy as np
from qiskit import QuantumCircuit
from qiskit.quantum_info import Pauli
from qiskit_addon_slc.utils import generate_noise_model_paulis
from samplomatic.transpiler import generate_boxing_pass_manager
from samplomatic.utils import find_unique_box_instructions
def trotter_ising_circuit(num_qubits, num_steps, rx_angle, rzz_angle):
"""Trotterized transverse-field Ising evolution on a 1D chain."""
circuit = QuantumCircuit(num_qubits)
for _ in range(num_steps):
circuit.rx(rx_angle, range(num_qubits))
circuit.barrier()
for start in (0, 1): # even then odd bonds
for i in range(start, num_qubits - 1, 2):
circuit.rzz(rzz_angle, i, i + 1)
circuit.barrier()
return circuit
num_qubits = 10
circuit = trotter_ising_circuit(
num_qubits, num_steps=4, rx_angle=np.pi / 16, rzz_angle=-np.pi / 3
)
observable = Pauli("I" * num_qubits).compose("Z", [num_qubits // 2])
boxing_pass = generate_boxing_pass_manager(
inject_noise_targets="all",
inject_noise_strategy="individual_modification",
inject_noise_site="after",
twirling_strategy="active",
remove_barriers="never",
)
boxed_circuit = boxing_pass.run(circuit)
noise_model_paulis = generate_noise_model_paulis(
find_unique_box_instructions(boxed_circuit)
)
print(f"noisy layers: {len(noise_model_paulis)}")
print(
f"Pauli error terms: {sum(len(p) for p in noise_model_paulis.values())}"
)
noisy layers: 2
Pauli error terms: 198
小さなヘルパー関数が各バウンドの集合を 1 行にまとめるため、オプションの効果が配列の羅列ではなく、いくつかの数値として表れます。
すべてのバウンドは 0 から自明なバウンド 2.0 の間にあります。サマリーには、バウンドの平均値と標準偏差が表示され、続いて各項が次のグループにどう分かれるかが示されます。
- 発展後のオブザーバブルと可換であるため、バウンドがちょうど 0 になる項
- 0 から 2.0 の間のバウンドが計算された項
- 自明な 2.0 のままの項
平均が低いほど厳密であり、厳密であるほど後のサンプリングオーバーヘッドが小さくなることに注意してください。
1 つのオプションに対するスイープでは、オプションのデフォルト値に * が付きます。
import inspect
import time
from qiskit_addon_slc.bounds import compute_forward_bounds
def summarize(rates, label, elapsed):
"""Prints a one-line summary of a set of bounds."""
# The bounds are floating-point results, so a term counts as bounded by 0, or as left at the
# trivial 2.0, when it is within rounding error of that value.
exact = np.isclose(rates, 0.0, rtol=0.0, atol=1e-12)
trivial = np.isclose(rates, 2.0, rtol=0.0, atol=1e-12)
computed = ~exact & ~trivial
print(
f"{label:28} mean {rates.mean():.4f} ± {rates.std():.3f} │ "
f"at 0: {exact.mean():4.0%} computed: {computed.mean():4.0%} "
f"at 2.0: {trivial.mean():4.0%} │ "
f"{elapsed:4.2f}s"
)
def forward(label, **kwargs):
"""Computes the forward bounds, summarizes them and returns all their rates."""
start = time.perf_counter()
bounds = compute_forward_bounds(
boxed_circuit, noise_model_paulis, observable, **kwargs
)
elapsed = time.perf_counter() - start
rates = np.concatenate([bound.rates for bound in bounds.values()])
summarize(rates, label, elapsed)
return rates
def sweep(option, values):
"""Computes the forward bounds for each value of one option, marking the default with `*`."""
default = (
inspect.signature(compute_forward_bounds).parameters[option].default
)
for value in values:
marker = "*" if value == default else " "
forward(f"{marker} {option}={value:g}", **{option: value})
baseline = forward(" defaults")
defaults mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.29s
精度
evolution_max_terms
Pauli エラー項を非 Clifford ゲートを通して発展させると Pauli の和になり、その和は回路の深さとともに増大します。evolution_max_terms は、保持するそれらの項の数に上限を設けます。破棄された分の 1-ノルムはバウンドに加算されるため、何も暗黙のうちに失われることはありません。
スイープは、デフォルトより厳密な値から極端な削減までの範囲で実行されます。上限を下げると、計算は低コストになり、バウンドは緩くなります。
sweep("evolution_max_terms", (10_000_000, 1_000_000, 1000, 100, 10, 2))
evolution_max_terms=1e+07 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.38s
* evolution_max_terms=1e+06 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.31s
evolution_max_terms=1000 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.30s
evolution_max_terms=100 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.29s
evolution_max_terms=10 mean 0.3927 ± 0.711 │ at 0: 59% computed: 31% at 2.0: 9% │ 0.28s
evolution_max_terms=2 mean 0.5245 ± 0.792 │ at 0: 59% computed: 23% at 2.0: 18% │ 0.21s
上限をデフォルトより大きくしても、ここでは何も変わりません。このサイズの回路では、デフォルトですでにすべての項が保持されるため、バウンドはこのオプションで達成できる最も厳密な状態にあります。これが典型的な状況です。精度オプションを厳しくする意味があるのは、そのデフォルトが実際に制約になっている場合だけであり、それを調べる方法が上記の実験です。より厳しい値で再実行し、バウンドが動くかどうかを確認してください。
警告: 基盤となる伝播ルーチンは、evolution_max_terms を、チェックすべき上限ではなく事前に確保する容量として扱います。numpy.iinfo(numpy.uint).max のように名目上無制限の値を渡すと、アロケーターに数テラバイトのメモリを要求し、プロセスが異常終了します。現実的な数値にとどめてください。デフォルトの 100 万でもすでに十分に大きな値です。
atol_simplify
交換子が形成された後、係数が atol_simplify を下回る項は破棄されます。発展の上限と同様に、この方法で失われた 1-ノルムはバウンドに加算されます。
sweep("atol_simplify", (0.0, 1e-8, 1e-4, 1e-2, 1e-1))
atol_simplify=0 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.32s
* atol_simplify=1e-08 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.31s
atol_simplify=0.0001 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.33s
atol_simplify=0.01 mean 0.3656 ± 0.685 │ at 0: 59% computed: 35% at 2.0: 6% │ 0.31s
atol_simplify=0.1 mean 0.3837 ± 0.697 │ at 0: 59% computed: 31% at 2.0: 10% │ 0.31s
eigval_max_qubits
最も厳密なバウンドは交換子のスペクトルノルムですが、そのためには固有値計算が必要です。交換子の幅が広がるとこれは高コストになるため、eigval_max_qubits は、それを超えると試行を断念して三角不等式を使用する幅を設定します。そのバウンド、すなわち交換子の係数 に対する
は有効ですが、より緩くなります。
これをゼロに設定すると、あらゆる場所でフォールバックが強制されるため、固有値ソルバーの利点が 1 つの数値で示されます。
sweep("eigval_max_qubits", (20, 14, 8, 4, 2, 0))
eigval_max_qubits=20 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.31s
* eigval_max_qubits=14 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.30s
eigval_max_qubits=8 mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.32s
eigval_max_qubits=4 mean 0.3742 ± 0.694 │ at 0: 59% computed: 35% at 2.0: 6% │ 0.30s
eigval_max_qubits=2 mean 0.4983 ± 0.809 │ at 0: 59% computed: 23% at 2.0: 18% │ 0.27s
eigval_max_qubits=0 mean 0.5081 ± 0.810 │ at 0: 59% computed: 21% at 2.0: 19% │ 0.27s
固有値ソルバーをオフにしても、平均は自明な 2.0 を大きく下回ったままです。バウンドが 0 の項の割合は変わりません。発展後のオブザーバブルとちょうど可換な項は交換子がゼロであり、これは固有値計算が検討される前に認識されるためです。それ以外の項では、三角不等式のバウンドが 2.0 に達して頭打ちになるものもありますが、多くの項ではそれでも小さいため、2.0 未満の計算されたバウンドが維持されます。
atol_eigenvalue
スペクトルノルムが計算される場合、atol_eigenvalue は反復固有値ソルバーの収束許容値です。ただし、ほとんどの交換子はこのソルバーには到達しません。
-
高々 4 量子ビットに作用する交換子は、直接、厳密に対角化されます。
-
それより広い交換子は、まず Pauli 項が必要とする最小の量子ビット数に縮約され、その縮約後の形が高々 8 量子ビットに作用する場合は、厳密に対角化されます。
この縮約の後でもなお 8 量子ビットを超える数が必要な交換子だけが反復ソルバーに渡され、この許容値が適用されるのはそこだけです。このガイドで使用するサイズの回路はそこまで到達しないため、このオプションのスイープではどの値でも同一のバウンドが得られます。このオプションが重要になるのは、大規模なデバイス上の広いライトコーンの場合です。
他の精度オプションとは異なり、atol_eigenvalue を緩めることは一方向について安全ではありません。反復固有値ソルバーはスペクトルの内側から極値固有値に近づくため、完全に収束していない推定値はスペクトルノルムを過小評価し、したがってバウンドも過小評価します。デフォルトが意図的に厳しく設定されているのはこのためです。ソルバーが収束しない場合は、三角不等式によるフォールバックが働き、これは再び有効になります。
スコープ
max_num_boxes
有用な制限が精度ではなく深さである場合もあります。max_num_boxes は、指定した数のボックスを超えるとバウンド計算の生成を停止し、それ以降のすべてのレイヤーには代わりに自明なバウンド 2.0 が割り当てられます。
この制限は、たとえばゲート数で数えた深さではなく、ノイズを持つ BoxOp 命令(InjectNoise アノテーションを持つもの)の数を数えます。これにより、回路のユーザーから見える構造に結び付けられ、トランスパイルの実行ごとに変わりうる自動生成されたアノテーション参照との結合を避けられます。
これは、2 つの半分がそれぞれ前方バウンドと後方バウンドで扱われる回路にとって自然な選択です。それぞれ、実際に使用される深さまでだけを計算します。
from qiskit.circuit import BoxOp
from samplomatic import InjectNoise
# The limit counts the noisy boxes encountered while iterating the circuit, not the unique ones:
# a repeated layer is one noise model but several boxes. Boxes without an `InjectNoise`
# annotation carry no error terms and do not count.
num_boxes = sum(
1
for inst in boxed_circuit.data
if isinstance(inst.operation, BoxOp)
and any(
isinstance(annotation, InjectNoise)
for annotation in inst.operation.annotations
)
)
print(
f"the circuit has {num_boxes} noisy boxes drawn from "
f"{len(noise_model_paulis)} distinct noise models\n"
)
for limit in (None, 6, 4, 2, 1):
marker = "*" if limit is None else " "
forward(f"{marker} max_num_boxes={limit}", max_num_boxes=limit)
the circuit has 9 noisy boxes drawn from 2 distinct noise models
* max_num_boxes=None mean 0.3639 ± 0.684 │ at 0: 59% computed: 36% at 2.0: 5% │ 0.33s
max_num_boxes=6 mean 0.7486 ± 0.935 │ at 0: 53% computed: 18% at 2.0: 30% │ 0.18s
max_num_boxes=4 mean 1.1506 ± 0.980 │ at 0: 40% computed: 5% at 2.0: 55% │ 0.11s
max_num_boxes=2 mean 1.5707 ± 0.821 │ at 0: 21% computed: 0% at 2.0: 79% │ 0.05s
max_num_boxes=1 mean 1.8157 ± 0.579 │ at 0: 9% computed: 0% at 2.0: 91% │ 0.03s
実行
timeout
timeout は秒単位の実時間の制限です。これが切れると、それまでに計算されたバウンドは保持され、未処理のものはすべて自明なバウンド 2.0 のままになるため、結果は常に使用可能ですが、早く打ち切るほど緩くなります。
これは厳密な期限ではありません。すでに実行中のタスクは完了することが許されるため、呼び出しは 1 つのタスクの所要時間の分だけ制限を超過する場合があります。
num_processes
異なるエラー項のバウンドは互いに独立しているため、num_processes はそれらをワーカープロセスに分散します。これ単独では、計算にかかる時間にのみ影響し、結果には影響しません。
注意: timeout と num_processes を組み合わせると、結果がタイミングに依存するようになります。制限時間が切れる前にどのタスクが完了するかは決定的ではありません。バウンドは有効なままですが、実行ごとに再現できなくなります。
どこから始めるか
デフォルトは、それが可能なサイズの回路で厳密なバウンドが得られるように選ばれています。計算が遅すぎる場合は、おおよそ次の順序でオプションを適用してみてください。
-
num_processes: 精度のコストなしで得られる無償の高速化です。 -
max_num_boxes: 回路のどの深さまでを重視するかが事前にわかっている場合に使用します。 -
eigval_max_qubits: 最も幅の広い交換子に対する最もコストの高いステップをスキップします。これらは通常、項全体のごく一部です。 -
evolution_max_termsとatol_simplify: これらは全体にわたって精度に影響するため、最後に調整し、上記のようなスイープで確認してバウンドがどれだけ動くかを判断してください。 -
timeout: 調整手段ではなく強制停止であり、品質にかかわらず、計算を期限までに終える必要がある場合に使用します。
バウンドが緩いほど、指定されたバイアス許容値に対して compute_local_scales が報告するサンプリングオーバーヘッドは大きくなります。調整中に注目すべきはこのオーバーヘッドであり、近似が実際にどれだけのコストを伴うかを教えてくれます。