トランスパイラー
generate_boxing_pass_manager() 関数は、回路の命令をアノテーション付きのボックスにグループ化できる qiskit.transpiler.PassManager を構築するための、柔軟で便利なツールです。回路を手動でグループ化してアノテーションを付ける代わりの簡単な方法となり、ワークフローの自動化や、さまざまな回路ボックス化戦略のテストなどに役立ちます。
このガイドでは、generate_boxing_pass_manager() とその引数の使い方を説明します。関数の各引数の効果を際立たせるため、以降のセクションでは主に下の回路を参照します。
from qiskit.circuit import Parameter, QuantumCircuit
circuit = QuantumCircuit(4, 7)
circuit.h(1)
circuit.h(2)
circuit.cz(1, 2)
circuit.h(1)
circuit.cx(1, 0)
circuit.cx(2, 3)
circuit.measure(range(1, 4), range(3))
circuit.cx(0, 1)
circuit.cx(1, 2)
circuit.cx(2, 3)
for qubit in range(4):
circuit.rz(Parameter(f"th_{qubit}"), qubit)
circuit.rx(Parameter(f"phi_{qubit}"), qubit)
circuit.rz(Parameter(f"lam_{qubit}"), qubit)
circuit.measure(range(4), range(3, 7))
circuit.draw("mpl", scale=0.8)
演算をボックスにグループ化する
引数 enable_gates を True または False に設定すると、2量子ビット・ゲートをボックスにグループ化するかどうかを指定できます。同様に、enable_measures は測定をグループ化するかどうかを指定します。次のスニペットは、ゲートと測定の両方をグループ化する例を示しています。
from samplomatic.transpiler import generate_boxing_pass_manager
boxing_pass_manager = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=True,
)
transpiled_circuit = boxing_pass_manager.run(circuit)
transpiled_circuit.draw("mpl", scale=0.8)

図からわかるように、パス・マネージャーは2種類のボックスを作成します。1つは2量子ビット・ゲートの単一の層を含むもの、もう1つは測定の単一の層を含むものです。この分離は、通常は均質な演算の層を対象とするノイズ学習・緩和プロトコルの標準的な実践を反映しています。2量子ビット・ゲートと測定は、それらを収容できる最も左のボックスに配置され、すべての1量子ビット・ゲートは、その後に続く2量子ビット・ゲートまたは測定と同じボックスに配置されます。
次のコードは、enable_gates を False に設定した例を示しています。ご覧のとおり、2量子ビット・ゲートはボックスにグループ化されず、それらに先行する1量子ビット・ゲートもグループ化されません。
boxing_pass_manager = generate_boxing_pass_manager(
enable_gates=False,
enable_measures=True,
)
transpiled_circuit = boxing_pass_manager.run(circuit)
transpiled_circuit.draw("mpl", scale=0.8)
ボックスへのアノテーションの付け方を選ぶ
返される回路のすべての2量子ビット・ゲートと測定のボックスは、左ドレスのアノテーションを持ちます。特に、2量子ビット・ゲートを含むすべてのボックスには Twirl のアノテーションが付きます。測定ボックスについては、ユーザーは Twirl、BasisTranform(mode を "measure" に設定)、またはその両方から選択できます。次のコードは、すべてのボックスがツワールされ、測定ボックスにはさらに BasisTranform のアノテーションが付く回路を生成します。
boxing_pass_manager = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=True,
measure_annotations="all",
)
transpiled_circuit = boxing_pass_manager.run(circuit)
ノイズ注入のために回路を準備する
inject_noise_targets を使うと、どのボックスに InjectNoise アノテーションを付けるかを指定できます。たとえば、次のコードは、2量子ビット・ゲートのボックスには InjectNoise アノテーションが付き、測定ボックスには付かない回路を生成します。
boxing_pass_manager = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=True,
inject_noise_targets="gates",
)
transpiled_circuit = boxing_pass_manager.run(circuit)
回路に、1量子ビット・ゲートを除いて等価なボックスが2つ以上含まれている場合、それらすべてに同じ ref を持つ InjectNoise アノテーションが付きます。したがって、返される回路内の一意な ref の数は、一意なボックスの数と等しくなります。
inject_noise_strategy の値は InjectNoise アノテーションに影響します。次の値が指定できます。
-
modifier_ref='': ノイズ・マップからサンプリングする前に修正する必要がない場合に推奨されます。 -
modifier_ref=ref: すべてのノイズ・マップを同じ係数で一様にスケーリングする必要がある場合に推奨されます。 -
modifier_refの一意な値: ノイズ・マップごとに異なる係数でスケーリングする必要がある場合に推奨されます。
次のコードは、2量子ビット・ゲートのボックスが、一意な modifier_ref の値を持つ InjectNoise アノテーションを持つ回路を生成します。
boxing_pass_manager = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=True,
inject_noise_targets="gates",
inject_noise_strategy="individual_modification",
)
transpiled_circuit = boxing_pass_manager.run(circuit)
ツワーリング戦略を選択する
ボックス化パス・マネージャーは、まずどのエンタングリング・ゲートと測定命令を同じボックスに入れるかを選択します。これが完了すると、ボックスは一般に全幅にはならず、回路内でアクティブな量子ビットやバックエンドのターゲットに存在する量子ビットの一部のみを含む可能性が高くなります。ボックス化パス・マネージャーの典型的な構成では、ボックス命令が作用するすべての量子ビット(ボックス内のどの命令も作用しない量子ビットも含む)がツワールされるため、部分幅のボックスはツワーリングのノイズ調整効果に影響します。アイドル状態の量子ビットはツワールされず、たとえばボックスが適用されている間にコヒーレント・エラーが蓄積する可能性があります。twirling_strategy は、各ボックスが所有する量子ビットを拡張し、アイドル状態の量子ビットの数を変更するためのオプションを提供します。
-
"active"(デフォルト)は、ボックスをアイドル状態の量子ビットに拡張しません。 -
"active_accum"は、回路内の先行する命令によってすでに作用された量子ビットまで、すべてのボックスを拡張します。回路の開始時点の量子ビットを、アクティブになるまでツワールしないことで、できるだけ長く基底状態のままにしたい場合に便利です。 -
"active_circuit"は、回路内の任意の命令が作用する量子ビットまで、すべてのボックスを拡張します。エンタングリング層が繰り返される構造化された回路で、各ボックスごとにノイズを学習する必要があるため、一意なボックスの数を最小限にしたい場合に便利です。 -
"all"は、回路のすべての量子レジスターにあるすべての量子ビットまで、すべてのボックスを拡張します。QPU上のすべての量子ビットを常にツワールしたい場合の"active_circuit"の拡張として便利です。
次の例では、各戦略を基本回路に適用し、その結果を描画します。
circuit = QuantumCircuit(5, 2)
circuit.h(range(4))
circuit.cx(0, 1)
circuit.cx(1, 2)
circuit.cx(2, 3)
circuit.cx(0, 1)
circuit.cx(1, 2)
circuit.cx(2, 3)
circuit.barrier()
circuit.h(range(4))
circuit.measure([1, 2], [0, 1])
circuit.draw("mpl", scale=0.6)
import matplotlib.pyplot as plt
plt.figure(figsize=(9, 6))
for idx, strategy in enumerate(["active", "active_accum", "active_circuit", "all"]):
ax = plt.subplot(2, 2, idx + 1)
pm = generate_boxing_pass_manager(twirling_strategy=strategy, remove_barriers="never")
transpiled_circuit = pm.run(circuit)
fig = transpiled_circuit.draw("mpl", scale=0.8, ax=ax)
ax.set_title(f"twirling_strategy='{strategy}'")
plt.tight_layout()

バリアの扱いを指定する
デフォルトでは、バリアは、すべてのエンタングリング・ゲートと測定命令が指示どおりにボックス化された後(twirling_strategy の選択による拡張を含む)、ただし可能な場合に1量子ビット・ゲートが右隣のボックスに追加される前に、回路から削除されます。
たとえば、次の回路にはバリアが含まれています。
circuit_with_barrier = QuantumCircuit(4)
circuit_with_barrier.h(range(4))
circuit_with_barrier.cz(0, 1)
circuit_with_barrier.barrier()
circuit_with_barrier.cz(2, 3)
circuit_with_barrier.measure_all()
circuit_with_barrier.draw("mpl", scale=0.8)
デフォルトの動作では、2つのエンタングリング・ゲートが同じボックスに入ることはありませんが、バリアがあっても1量子ビット・ゲートは次に利用可能なボックスに吸収されます。
boxing_pass_manager = generate_boxing_pass_manager()
transpiled_circuit = boxing_pass_manager.run(circuit_with_barrier)
transpiled_circuit.draw("mpl", scale=0.8)
remove_barriers を 'never' に設定すると、バリアが保持され、1量子ビット・ゲートが制約されます。
boxing_pass_manager = generate_boxing_pass_manager(remove_barriers="never")
transpiled_circuit = boxing_pass_manager.run(circuit_with_barrier)
transpiled_circuit.draw("mpl", scale=0.8)
最後に、バリアをただちに削除すると、エンタングラーが別々のボックスに入るという制約がなくなります。
boxing_pass_manager = generate_boxing_pass_manager(remove_barriers="immediately")
transpiled_circuit = boxing_pass_manager.run(circuit_with_barrier)
transpiled_circuit.draw("mpl", scale=0.8)
ストラティフィケーションの仕組み
ボックス化パス・マネージャーは、2つの主要なパスを順に実行します。
-
GroupGatesIntoBoxes— エンタングリング・ゲートをボックスに層別化します。この段階では1量子ビット・ゲートは無視され、2量子ビット・ゲートのみがグループ化されます。 -
AbsorbSingleQubitGates— 各1量子ビット・ゲートを、その右にある最も近い適格なボックスに吸収します。
層別化の際、GroupGatesIntoBoxes は回路のDAGをトポロジカル順に走査し、各2量子ビット・ゲートを、その量子ビットがまだ占有されていない最も早いグループに配置します。3種類の命令はデリミターとして機能し、その量子ビットに対する現在のグループを確定させて、それらの量子ビット上の後続のゲートを新しいグループに入れさせます。
-
バリア
-
既存のボックス命令
-
Measurements
1量子ビット・ゲートはデリミターではありません。グループ化には影響せず、AbsorbSingleQubitGates による処理に任されます。現在、可換性の解析は行われず、グループ化は回路のトポロジカル順をそのまま尊重します。
なぜ remove_barriers="after_stratification" が推奨されるデフォルトなのか
デフォルトのモードが "after_stratification" なのは、次の2つの利点を兼ね備えているためです。
-
バリアは、エンタングリング・ゲートのグループ化を制約します(層別化の間、デリミターとして機能します)。
-
バリアは
AbsorbSingleQubitGatesが実行される前に削除されるため、1量子ビット・ゲートは以前のバリアの位置を越えて流れ、最も近いボックスに吸収されます。
対照的に:
-
"never"は常にバリアを保持するため、1量子ビット・ゲートがバリアとボックスの間に閉じ込められ、吸収が妨げられて、ツワールされない回転が回路に残ります。 -
"immediately"は層別化の前にバリアを削除するため、グループ化には影響しません。 -
"finally"は、出力回路にバリアを残したい場合(たとえば可視化のため)に便利ですが、バリアとボックスの間にある1量子ビット・ゲートは吸収されません。
バリアを使ったボックス化 ISA 回路
繰り返し構造を持つ回路(トロッター化されたハミルトニアン・シミュレーション、変分アンザッツ、エラー増幅シーケンスなど)は、通常プログラムで構築し、その後 ISA 形式にトランスパイルします。トランスパイル済みの回路に手作業でボックスを追加するのは面倒なため、自然なワークフローは次のようになります。
-
論理回路を構築し、ボックスの境界にしたいレイヤーの境界にバリアを挿入します。
-
ISA 回路にトランスパイルします。Qiskit のトランスパイラーは、ルーティングと最適化の間もバリアを保持します。
-
ISA 回路に対して
generate_boxing_pass_manager()を実行します。
あるレイヤーの末尾がすべての量子ビットをカバーしていない場合、バリアが必要です。バリアがないと、貪欲アルゴリズムは次のレイヤーのゲートを、それらの「空いている」量子ビット上で同じグループに配置してしまいます。具体例として、5 量子ビットのリング上での 2 ステップのハイゼンベルク ZZ シミュレーションを考えます。各トロッター・ステップには 3 つのボンド・レイヤーがあります。偶数ボンド (0,1), (2,3)、奇数ボンド (1,2), (3,4)、そしてラップ・ボンド (4,0) です。奇数ボンド・レイヤーの後、量子ビット 0 と 4 はどちらも空いています。バリアがないと、ラップ・ボンド (4,0) は、それらの空いている量子ビットを使う次のトロッター・ステップのボンドと貪欲にグループ化され、異なるステップのゲートが同じボックスに混在してしまいます。
from qiskit.circuit import Parameter, QuantumCircuit
n = 5 # 5-qubit ring: bonds (0,1), (1,2), (2,3), (3,4), (4,0)
dt = Parameter("dt")
heisenberg = QuantumCircuit(n)
for step in range(2):
# Layer 1: even bonds (0,1) and (2,3)
for idx in range(0, n - 1, 2):
heisenberg.rzz(dt, idx, idx + 1)
heisenberg.barrier()
# Layer 2: odd bonds (1,2) and (3,4) -- qubits 0 and 4 are now free
for idx in range(1, n - 1, 2):
heisenberg.rzz(dt, idx, idx + 1)
heisenberg.barrier()
# Layer 3: wrap bond (4,0) -- both qubits were free after layer 2
heisenberg.rzz(dt, n - 1, 0)
if step < 1:
heisenberg.barrier() # separates the two Trotter steps
heisenberg.measure_all()
heisenberg.draw("mpl", scale=0.7)
次のコードは、基盤となるデバイスがネイティブなエンタングラーとして CX を持つと仮定して、回路を ISA 形式にトランスパイルします。バリアはトランスパイル後も残り、出力回路でも確認できることに注意してください。
from qiskit.transpiler import generate_preset_pass_manager
preset_pm = generate_preset_pass_manager(
basis_gates=["rz", "sx", "cx"],
coupling_map=[[0, 1], [1, 2], [2, 3], [3, 4], [4, 0]],
optimization_level=1,
)
isa_circuit = preset_pm.run(heisenberg)
isa_circuit.draw("mpl", scale=0.5, fold=200)
次に、ボクシング・パスマネージャーを ISA 回路に適用します。3 つのボンド・レイヤーはそれぞれ、各トロッター・ステップ内で別々のボックスになります。
boxing_pm = generate_boxing_pass_manager()
boxed_isa = boxing_pm.run(isa_circuit)
boxed_isa.draw("mpl", scale=0.5, fold=100)
Samplomatic のパスマネージャーを Qiskit のプリセット・パスマネージャーに組み込む
以下のコードは、generate_boxing_pass_manager() を Qiskit のプリセット・パスマネージャーに組み込む方法を示しています。トランスパイルされた回路は ISA であり、ボックスを含みます。
from qiskit.transpiler import generate_preset_pass_manager
preset_pass_manager = generate_preset_pass_manager(
basis_gates=["rz", "sx", "cx"],
coupling_map=[[0, 1], [1, 2]],
optimization_level=0,
)
boxing_pass_manager = generate_boxing_pass_manager()
# Run the boxing pass manager after the scheduling stage
preset_pass_manager.post_scheduling = boxing_pass_manager
circuit = QuantumCircuit(3)
circuit.h(0)
circuit.cx(0, 1)
circuit.cx(0, 2)
circuit.measure_all()
transpiled_circuit = preset_pass_manager.run(circuit)
transpiled_circuit.draw("mpl", scale=0.8)
このパターンは、バリアベースの ISA ボクシング・ワークフローとも自然に組み合わせられます。ボクシング・パスマネージャーはスケジューリング後のステップとして実行され、元の論理回路のバリアがトランスパイル後も残っているため、期待どおりに層別化を導きます。
preset_pm = generate_preset_pass_manager(
basis_gates=["rz", "sx", "cx"],
coupling_map=[[0, 1], [1, 2], [2, 3], [3, 4], [4, 0]],
optimization_level=1,
)
preset_pm.post_scheduling = generate_boxing_pass_manager()
boxed_heisenberg = preset_pm.run(heisenberg)
boxed_heisenberg.draw("mpl", scale=0.5, fold=100)
アノテーションの確認と変更
自動ボクシングの後、特定のボックスのアノテーションを確認したり、調整したりしたい場合があります。たとえば、ノイズ注入の追加、アノテーションの削除、ツワーリング・グループの変更などです。Samplomatic は、この目的のために samplomatic.utils にユーティリティ関数群を提供しています。これらはすべて、元の回路を変更せずに新しい回路を返します。
extend_annotations() は、回路内のすべてのボックスに 1 つ以上のアノテーションを追加します。
from samplomatic import InjectNoise, Twirl
from samplomatic.utils import extend_annotations
boxed = generate_boxing_pass_manager().run(circuit_with_barrier)
# Add InjectNoise to every box
with_noise = extend_annotations(boxed, InjectNoise("my_noise_ref"))
with_noise.draw("mpl", scale=0.8)
filter_annotations() は、指定したタイプのアノテーションのみを残し、それ以外を削除します。replace_annotations() は、各アノテーションに呼び出し可能オブジェクトを適用します。この呼び出し可能オブジェクトは、置き換えるアノテーションのリストを返します(削除するには [] を、置換するには要素 1 つのリストを、展開するには複数要素のリストを返します)。
from samplomatic.utils import filter_annotations, replace_annotations
# Keep only Twirl annotations, drop everything else
twirl_only = filter_annotations(with_noise, Twirl)
# Replace the twirling group on every Twirl annotation
updated = replace_annotations(
twirl_only, lambda a: [Twirl(group="local_c1")] if isinstance(a, Twirl) else [a]
)
汎用の map_annotations() 関数は、アノテーションのリストを新しいリストに写像する任意の呼び出し可能オブジェクトを受け取り、ボックスごとの変換を完全に制御できます。strip_annotations() は、すべてのボックスからすべてのアノテーションを削除する便利なラッパーです。
すべてのゲート・ボックスに InjectNoise を追加するような一括変更には、generate_boxing_pass_manager() の inject_noise_targets パラメーターが最も簡潔な方法です。これらのユーティリティは、パスマネージャーのパラメーターではカバーできない、ボックスごとの選択的な編集に役立ちます。
回路をビルドする
generate_boxing_pass_manager() が生成するパスマネージャーは、どれも build() によってテンプレートと samplex のペアへ正常に変換できる回路を返します。たとえば、次のコードは、ボクシング・パスマネージャーが生成した回路に対して build() 関数を呼び出します。
from samplomatic import build
boxing_pass_manager = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=True,
)
transpiled_circuit = boxing_pass_manager.run(circuit)
template, samplex = build(transpiled_circuit)
どのようなトランスパイル済み回路でも正常にビルドできるように、パスマネージャーは必要に応じて追加のボックスを含める方法を知っています。たとえば、測定されていない量子ビット 0 で終わる以下の回路を考えます。
circuit_with_unmeasured_qubit = QuantumCircuit(4, 3)
circuit_with_unmeasured_qubit.cz(0, 1)
circuit_with_unmeasured_qubit.cz(2, 3)
for qubit in range(4):
circuit_with_unmeasured_qubit.rz(Parameter(f"th_{qubit}"), qubit)
circuit_with_unmeasured_qubit.rx(Parameter(f"phi_{qubit}"), qubit)
circuit_with_unmeasured_qubit.rz(Parameter(f"lam_{qubit}"), qubit)
circuit_with_unmeasured_qubit.measure(range(1, 4), range(3))
circuit_with_unmeasured_qubit.draw("mpl", scale=0.8)
ゲートと測定の周囲に左ドレスのボックスを描くと、量子ビット 0 上に回収されていない仮想ゲートが残る回路になり、この回路に対して build() を呼び出すとエラーになります。これを避けるため、generate_boxing_pass_manager() が返すパスマネージャーは、コレクターとして機能する右ドレスのボックスを追加できます。たとえば、次のコードでは、量子ビット 0 は、回収されていない仮想ゲートを拾う右ドレスのボックスで終端されます。量子ビット 0 に作用する 1 量子ビット・ゲートも、結果として得られる回路の深さを最小限に抑えるため、このボックスの内部に配置されます。
boxing_pass_manager = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=True,
)
transpiled_circuit = boxing_pass_manager.run(circuit_with_unmeasured_qubit)
transpiled_circuit.draw("mpl", scale=0.8)
別の例では、ボックス化されていない測定のために回収されないまま残ってしまう仮想ゲートを回収するため、右ドレスのボックスが追加されます。
boxing_pass_manager = generate_boxing_pass_manager(
enable_gates=True,
enable_measures=False,
)
transpiled_circuit = boxing_pass_manager.run(circuit_with_unmeasured_qubit)
transpiled_circuit.draw("mpl", scale=0.8)