メインコンテンツへスキップ
このプロジェクトはベータ版です。バグを見つけましたか?GitHub Issueを開く。質問やアイデアがありますか?Discussionsで質問する — フィードバックをお待ちしています!
このページの翻訳品質はいかがですか?

カスタムバウンドの計算方法

compute_forward_bounds と compute_backward_bounds は便利なラッパーです。どちらも、公開 API が提供する同じ 3 つの部品から構築されています。

  • ノルム関数: 発展した 1 つの Pauli エラー項に対するバウンドを与えます

  • ライトコーン: エラー項をどこに向けて発展させるかの起点を設定します

  • compute_bounds: 回路を反復処理し、項ごとの作業を分散します

これらを自分で組み立てると、ラッパーが対象としないものに対するバウンドを求められます。このガイドでは、そうする価値のある 1 つのケースを取り上げます。通常の後方バウンドが不必要に緩くなるミラー回路です。

ラッパーが実際に行っていること​

2 つのラッパーは、上に挙げた 3 つの点で異なります。

compute_forward_boundscompute_backward_bounds
回路指定どおりremove_measure(circuit).inverse()
ノルム関数time_evolved_norm_forward、オブザーバブルに対してtime_evolved_norm_backward、初期状態に対して
ライトコーンLightCone.initialize_from_pauli、オブザーバブルから設定LightCone.initialize_from_measurements、すべてのアクティブな量子ビットから設定
backwardsFalseTrue

前方バウンドは*「このエラーは、これから測定するオブザーバブルをどれだけ変化させうるか」という問いに、交換子のスペクトルノルムで答えます。後方バウンドは「このエラーは、開始時の状態をどれだけ変化させうるか」*という問いに、その状態をすべてのアクティブな量子ビット上の全ゼロ状態とみなして、核ノルムで答えます。これらは異なる問いであり、それぞれが回路のそれぞれの半分にあるエラーにとって適切な問いです。

ミラー回路のケース​

ミラー回路は、あるユニタリを適用してからそれを元に戻します: U†UU^\dagger U。これらは、理想的な結果を正確に知る必要がある場合(一般には Loschmidt エコー、ランダム化ベンチマーク、検証実験)に現れます。

ミラー回路には、一般的な後方バウンドが利用できない性質があります。後半が前半の逆であるため、エラーを初期状態まで後方に発展させることは、それをオブザーバブルまで前方に発展させることと同じゲートを通ります。2 つの問いは一致するため、より鋭い前方型のバウンドが後方向でも有効になります。

# 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
# The circuit of the quickstart guide...
forward_half = trotter_ising_circuit(
num_qubits, num_steps=4, rx_angle=np.pi / 16, rzz_angle=-np.pi / 3
)
# ...followed by its inverse. The ideal result is the initial state.
circuit = forward_half.compose(forward_half.inverse())

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"mirror circuit: {num_qubits} qubits, depth {circuit.depth()}")
print(f"noisy layers: {len(noise_model_paulis)}")
mirror circuit: 10 qubits, depth 24
noisy layers: 4

カスタムバウンドを組み立てる​

このレシピでは、compute_backward_bounds が回路に対して行うこと(測定を取り除き、反転し、backwards=True で反復処理する)を維持し、他の 2 つの部品を置き換えます。すなわち、前方ノルム関数と、compute_forward_bounds が自身を設定するのとまったく同じように LightCone.initialize_from_pauli でオブザーバブルから設定したライトコーンです。それぞれの置き換えは 1 行です。

from functools import partial

from qiskit_addon_slc.bounds import (
LightCone,
compute_bounds,
time_evolved_norm_forward,
)
from qiskit_addon_slc.utils import remove_measure

def compute_mirror_backward_bounds(
circuit,
noise_model_paulis,
/,
observable,
*,
evolution_max_terms=1_000_000,
**kwargs,
):
"""Bounds the backward direction of a mirror circuit with a forward-style bound.

Valid only for circuits whose second half inverts the first: the backward evolution then
traverses the same gates as the forward one, and reproduces the forward bounds.
"""
# As `compute_backward_bounds` does: strip measurements and invert, so that walking the circuit
# backwards from its end becomes walking the inverted circuit forwards.
circuit = remove_measure(circuit).inverse()

# The substitution: the forward norm function, bounding against the observable rather than
# against the initial state.
norm_fn = partial(
time_evolved_norm_forward,
observable=observable,
evolution_max_terms=evolution_max_terms,
)

# Seed the light cone from the observable, not from every active qubit.
light_cone = LightCone.initialize_from_pauli(circuit, observable)

return compute_bounds(
circuit,
noise_model_paulis,
light_cone,
norm_fn,
backwards=True,
**kwargs,
)

どれだけ改善するか​

同じ回路での標準の後方バウンドと比較し、設定ガイドと同じ方法でまとめます。すなわち、バウンドの平均値と標準偏差、続いて、バウンドが 0 の項、その間の計算されたバウンドを持つ項、自明な 2.0 のままの項の割合です。

from qiskit_addon_slc.bounds import (
compute_backward_bounds,
compute_forward_bounds,
)

forward_bounds = compute_forward_bounds(
boxed_circuit, noise_model_paulis, observable
)
stock_backward = compute_backward_bounds(boxed_circuit, noise_model_paulis)
custom_backward = compute_mirror_backward_bounds(
boxed_circuit, noise_model_paulis, observable
)

def summarize(label, bounds):
"""Prints a one-line summary of a set of bounds."""
rates = np.concatenate([bound.rates for bound in bounds.values()])
# 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:18} 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%}"
)

summarize("forward", forward_bounds)
summarize("backward (stock)", stock_backward)
summarize("backward (custom)", custom_backward)
forward mean 0.4323 ± 0.679 │ at 0: 38% computed: 61% at 2.0: 1%
backward (stock) mean 1.7218 ± 0.592 │ at 0: 0% computed: 95% at 2.0: 5%
backward (custom) mean 0.4323 ± 0.679 │ at 0: 38% computed: 61% at 2.0: 1%

標準の後方バウンドは自明な値 2.0 に近く、ほとんど情報を持ちません。ミラー回路では、あらゆるエラーがこのバウンドが測る意味で「初期状態に近い」ため、項を区別できません。カスタムバウンドは数倍厳密で、多くの項のバウンドを 0 にします。

実装を確認するために、2 つをボックスごとに比較できます。すべてのボックスで、カスタム後方バウンドは、丸め誤差を除いて、同じボックスの前方バウンドと同一になります。どちらも回路内の同じ地点における同じエラーを抑えており、ミラー回路ではその地点以降のゲートがそれ以前のゲートの逆に打ち消されるため、2 つの計算は反対側から同じ量を評価していることになります。

matches = sum(
np.allclose(forward_bounds[box_id].rates, custom_backward[box_id].rates)
for box_id in forward_bounds
)
print(f"{matches} of {len(forward_bounds)} boxes match the forward bounds")

worst = max(
np.abs(forward_bounds[box_id].rates - custom_backward[box_id].rates).max()
for box_id in forward_bounds
)
print(f"largest disagreement across all terms: {worst:.2e}")
16 of 16 boxes match the forward bounds
largest disagreement across all terms: 6.00e-15

注意: この等価性はミラー構造の性質であり、この手法の性質ではありません。自己逆でない回路でも同じ置き換えは実行できますが、後方向に対して有効なバウンドは計算されなくなります。その問いが当てはまらない回路の半分に対して、前方の問いに答えてしまうからです。回路の後半が前半の逆になっている場合にのみ使用してください。

残りのワークフローに組み込む​

結果は通常の Bounds 辞書なので、以降のすべては変わりません。

ミラー回路では、回路全体に対してどちらのバウンドも計算する理由もありません。前方バウンドは、末尾から数えた U†U^\dagger のボックスにのみ必要であり、後方バウンドは、先頭から数えた UU のボックスにのみ必要です。そのため max_num_boxes により、それぞれをノイズを持つボックスの半分に制限し、merge_bounds が 2 つの半分を結合します。

これが実際のバウンドの使い方であり、上記のボックスごとの比較はもはや行えないことを意味します。比較にはすべてのボックスについて両方のバウンドが必要だからです。これはこのガイドのための実装の確認であり、ワークフローのステップではありません。

from qiskit.quantum_info import PauliLindbladMap, QubitSparsePauliList
from qiskit_addon_slc.bounds import compute_local_scales, merge_bounds

# Stand-in for rates that a noise-learning experiment would measure on hardware.
rng = np.random.default_rng(42)
noise_rates = {
layer: PauliLindbladMap.from_components(
rng.random(len(terms)) * 5e-3,
QubitSparsePauliList.from_sparse_list(
terms.to_sparse_list(), terms.num_qubits
),
)
for layer, terms in noise_model_paulis.items()
}

# Compute each bound for its own half of the mirror circuit only. `forward_bounds` has an entry
# for every noisy box, which are the boxes `max_num_boxes` counts.
half = len(forward_bounds) // 2
half_forward = compute_forward_bounds(
boxed_circuit, noise_model_paulis, observable, max_num_boxes=half
)
half_stock = compute_backward_bounds(
boxed_circuit, noise_model_paulis, max_num_boxes=half
)
half_custom = compute_mirror_backward_bounds(
boxed_circuit, noise_model_paulis, observable, max_num_boxes=half
)

print(
f"each bound computed for {half} of {len(forward_bounds)} noisy boxes\n"
)
print(f"{'bounds':34} {'overhead':>12} {'residual bias':>14}")
for label, bounds in (
(
"forward + stock backward",
merge_bounds(boxed_circuit, half_forward, half_stock, noise_rates),
),
(
"forward + custom backward",
merge_bounds(boxed_circuit, half_forward, half_custom, noise_rates),
),
("forward only, whole circuit", forward_bounds),
):
_, cost, bias = compute_local_scales(
boxed_circuit, bounds, noise_rates, bias_tolerance=0.05
)
print(f"{label:34} {cost:12.4f} {bias:14.4f}")
Optimal spacetime partitioning not implemented! Just partitioning list of noisy boxes.
Optimal spacetime partitioning not implemented! Just partitioning list of noisy boxes.
each bound computed for 8 of 16 noisy boxes

bounds overhead residual bias
forward + stock backward 29848.0583 0.0498
forward + custom backward 465.4028 0.0498
forward only, whole circuit 465.4028 0.0498

標準の後方バウンドでは、サンプリングオーバーヘッドははるかに大きくなります。merge_bounds は、回路内の 1 つの切り替え点までは後方バウンドを、そこから先は前方バウンドを使用し、総バイアスを最小化する点を選びます。それぞれのバウンドを半分ずつ計算した場合、その点は回路の中央に来るため、UU のボックスは後方バウンドだけでカバーされます。標準の後方バウンドはそこでは自明な 2.0 に近く、オーバーヘッドを押し上げます。カスタムバウンドは前方バウンドと同じくらい厳密であるため、オーバーヘッドは完全な前方バウンドのものと一致します。

そうなると、完全な前方バウンドだけで同じ結果が得られるのに、なぜわざわざ行うのかという当然の疑問が生じます。答えはコストです。回路の序盤にあるボックスの前方バウンドは、そのエラーを UU の残りと U†U^\dagger の全体を通して発展させるため、回路中で最も長く、最も広い発展になります。発展した演算子は、通過する非 Clifford ゲートの数に対して指数関数的に大きくなりえ、ライトコーンが広がるにつれて交換子のスペクトルノルムの計算は指数関数的に高コストになります。半分ずつのアプローチでは、どのエラーも回路の半分を超えて発展させられることはありません。

したがって、節約の効果は回路の深さとともに大きくなります。さらに重要なのは、完全な前方バウンドは、半分のバウンドよりずっと前に計算不能になることです。序盤のボックスの発展が evolution_max_terms や eigval_max_qubits を超えると、そのバウンドは切り詰められるか三角不等式にフォールバックし、やがて計算コストが高すぎるようになります。その点がどこにあっても、半分ずつのアプローチでは、それが回路の前半のどこかにあればよいだけです。前方バウンドは中央より先に到達する必要がなく、ミラー回路では、UU のボックスの後方バウンドは、対応する U†U^\dagger のボックスの前方バウンドが終端まで通るのと同じ範囲の回路を通ってエラーを先頭まで発展させるため、同じ深さで限界に達します。

注意: U†U^\dagger のボックスの前方バウンドが同じ数値を持つように見えるのに、なぜ UU のボックスの後方バウンドを計算するのでしょうか。それは、ボックスが互いの鏡像ではないからです。各ボックスはゲートの片側にノイズを注入し、U†U^\dagger の逆レイヤーは固有のノイズモデルを持つ別のボックスであるため、UU のボックスに対応するボックスは、回路内の異なる地点でそのエラーを見ます。したがって、それらのバウンドは部分的にしか一致せず、UU のボックスのバウンドはそれらのボックスに対して計算する必要があります。

その他のカスタムバウンドの作成​

同じ 3 つの置き換えで、他のケースも扱えます。

  • 異なるノルム。 time_evolved_norm_forward は comm_norm_order を受け付けます。2 以外を渡すと固有値ソルバーをスキップし、そのノルムを直接計算します。

  • 異なるライトコーン。 LightCone.initialize_from_pauli は任意の Pauli からライトコーンを設定し、LightCone コンストラクターは追跡する量子ビットとそれを設定する演算を受け取るため、そのように表現できる任意の演算子に対してバウンドを計算できます。

  • まったく異なるノルム関数。 compute_bounds が必要とするのは呼び出し可能オブジェクトだけであり、Pauli と RotationGates を受け取って CommutatorBounds を返します。そのシグネチャを満たすものであれば何でも動作し、回路ファミリーに固有の閉形式のバウンドも含まれます。

何を置き換える場合でも、結果が真の上界であることを証明する必要があります。エラー項の影響を過小評価しても、目に見える失敗は生じません。その項が緩和されたノイズモデルから外れてしまうため、静かに誤った期待値が生成されます。