デバッグとトレース
build() 関数は、注釈付きの回路をテンプレート回路とサンプレックスのペアに変換します。複雑な回路では、元の回路のどのボックスが、テンプレート内のどのバリアや、サンプレックス DAG のどのノードに対応するのかを判別するのが難しい場合があります。
Samplomatic は、相補的な2つのトレース・ツールを提供します。
-
バリア・ラベル: テンプレート回路内のすべてのバリアは、その由来を符号化したラベルを持ちます。これらは
debug=Trueが設定されているかどうかにかかわらず、常に存在します。 -
サンプレックス・ノード上のトレース情報:
build()をdebug=Trueで呼び出すと、すべてのサンプレックス・ノードが、それを生成したボックスへ結び付けるTraceInfoオブジェクトを持ちます。
どちらのツールも、ボックスに Tag 注釈が付いているときに最も役立ちます。この注釈は、ボックスに ref 文字列を付加します。あるいは、InjectNoise の参照も同様に付加されます。
ボックスにタグを付ける
Tag 注釈は、ボックスに ref 文字列を付加します。この ref は、バリア・ラベルと、サンプレックス・ノード上のトレース情報に表示されます。
次の回路例には3つのボックスがあります。互いに素な量子ビットペア上にタグ付きの CX ボックスが2つと、タグなしの右側ドレス済み測定ボックスが1つです。2つ目の CX ボックスには InjectNoise 注釈も付いているため、その tag の ref とノイズの ref の両方がバリア・ラベルに表示されます。2つの CX ボックスは異なる量子ビットを覆っているので、サンプレックス・オプティマイザーは、それらの伝播ノードを共有ノードにマージできます。これについてはトレース情報のセクションで示します。
from qiskit.circuit import QuantumCircuit
from samplomatic import InjectNoise, Tag, Twirl, build
circuit = QuantumCircuit(4, 4)
with circuit.box([Twirl(), Tag("cx_ab")]):
circuit.cx(0, 1)
with circuit.box([Twirl(), InjectNoise("cx_noise"), Tag("cx_cd")]):
circuit.cx(2, 3)
with circuit.box([Twirl(), Tag("meas_box")]):
circuit.measure(range(4), range(4))
circuit.draw("mpl")
テンプレート回路内のバリア・ラベル
上の回路に対して build() を呼び出すと、識別用のラベルを持つバリアを備えたテンプレートが生成されます。各ラベルの形式は {side}{scope}@{key=value&...} です。
-
サイド:
L= 左ドレッシングの境界、M= ボックス内部コンテンツの境界、R= 右ドレッシングの境界。 -
スコープ: 同じネスト・レベルにある複数のボックスを区別する整数インデックス(ネストされたボックスの場合はアンダースコア区切りのリスト)。
-
注釈: ボックスの注釈から導出される、
@で始まり&で区切られたkey=valueペアのリスト。tagキーはTag注釈に、inject_noiseはInjectNoise注釈に由来します。
タグのないボックスのバリアには、サイドとスコープのみが付き(例: L2)、@ の接尾辞は付きません。
template, samplex = build(circuit)
template.draw("mpl", fold=100)
バリア・ラベルは、プログラムで抽出することもできます。
barrier_labels = [instr.operation.label for instr in template if instr.operation.name == "barrier"]
barrier_labels
['L0@tag=cx_ab',
'M0@tag=cx_ab',
'R0@tag=cx_ab',
'L1@inject_noise=cx_noise&tag=cx_cd',
'M1@inject_noise=cx_noise&tag=cx_cd',
'R1@inject_noise=cx_noise&tag=cx_cd',
'L2@tag=meas_box',
'M2@tag=meas_box',
'R2@tag=meas_box']
サンプレックス・ノード上のトレース情報
build() に debug=True を渡すと、すべてのサンプレックス・ノードにトレース情報が付加されます。各ノードの trace_info 属性は TraceInfo オブジェクトで、その trace_refs ディクショナリーは、注釈キー(例: "tag" や "inject_noise")を ref 文字列の集合にマップします。対応するボックス注釈のないノードでは trace_info=None になります。
デバッグ・ビルドされたサンプレックスに対して draw() を呼び出すと、インタラクティブなグラフ上の任意のノードにカーソルを合わせたときに、ホバー・ツールチップ内にその trace_refs が表示されます。
template, samplex = build(circuit, debug=True)
samplex.draw()
トレース情報は、プログラムで調べることもできます。一部のノードが 'cx_ab' と 'cx_cd' の両方から参照を持っていることに注目してください。2つのボックスが互いに素な量子ビットを覆っており、右側ドレス済み測定ボックスの放出から来る共通の先行ノードを共有しているため、サンプレックス・オプティマイザーはそれらの並列な伝播ノードをマージしました。
for node in samplex.graph.nodes():
if node.trace_info is not None:
tags = node.trace_info.trace_refs.get("tag", set())
merged = " ← merged" if len(tags) > 1 else ""
print(f"{type(node).__name__:40s} tags={tags}{merged}")
TwirlSamplingNode tags={'meas_box'}
InjectNoiseNode tags={'cx_cd'}
CollectZ2ToOutputNode tags={'cx_cd'}
TwirlSamplingNode tags={'cx_cd'}
CombineRegistersNode tags={'cx_cd'}
CollectTemplateValues tags={'cx_cd'}
TwirlSamplingNode tags={'cx_ab'}
CombineRegistersNode tags={'cx_ab', 'cx_cd'} ← merged
CollectTemplateValues tags={'cx_ab', 'cx_cd'} ← merged
特定のボックスに由来するすべてのノードを見つけるには、"tag" キーでフィルターします。
tag_ref = "cx_ab"
matching_nodes = [
node
for node in samplex.graph.nodes()
if node.trace_info is not None and tag_ref in node.trace_info.trace_refs.get("tag", set())
]
print(f"Nodes originating from box '{tag_ref}':")
for node in matching_nodes:
print(f" {type(node).__name__}")
Nodes originating from box 'cx_ab':
TwirlSamplingNode
CombineRegistersNode
CollectTemplateValues
サンプレックス DAG
build() が返すサンプレックスは有向非巡回グラフ(DAG)であり、エッジはレジスターの依存関係を表します。つまり、ノード A からノード B へのエッジは、B が動作する前に、A が共有された仮想レジスターに作用し終えるのを B が待たなければならないことを意味します。これは量子回路の DAG のような時間的な順序ではありません。代わりに、このグラフはランダム化を生成するノードから、それらを出力として合成するノードへと流れます。
3種類のノードがあり、インタラクティブなプロットではそれぞれ異なる見た目で表示されます。
| 形状 | 色 | 型 | 役割 |
|---|---|---|---|
| 星 | 赤 | SamplingNode | 分布または入力から新しい仮想レジスターを生成する |
| 円 | 緑 | EvaluationNode | 仮想レジスターを変換、結合、または伝播する |
| 蝶ネクタイ | 青 / 紫 | CollectionNode | レジスターを読み取り、sample() の出力に書き込む |
実行は3つのフェーズで進みます。
-
まず、すべてのサンプリング・ノードが(並列に)実行されます。
-
次に、評価ノードがトポロジカル順序で実行されます(各世代内では並列)。
-
最後に、コレクション・ノードが(並列に)実行されます。
graphviz のレイアウトはこれを反映しています。サンプリング・ノードは上部に、コレクション・ノードは下部に表示されます。
ホバー・ツールチップ
インタラクティブな可視化では、どのノードにもカーソルを合わせるとツールチップが表示されます。ツールチップには次の情報が含まれます。
-
ノードのクラス名と、その整数のグラフ・インデックス。
-
レジスター・マニフェスト。ノードがどのレジスターを生成し、読み取り、書き込み、削除するかを示します。これらから、ノード間でデータがどのように流れるかがわかります。
-
ノード固有の詳細。サンプリング・ノードの分布の型、乗算ノードのオペランド、コレクション・ノードのテンプレート・パラメーターのインデックスなどが含まれます。
-
トレース参照(
debug=Trueの場合のみ)。ノードを、その由来となるボックスへ結び付ける注釈キーと ref 文字列です。
プロットをクリックしてから個々のノードにカーソルを合わせるのが、ソースコードを読まずに、あるノードが何をするのかを理解する最も速い方法です。
サンプレックスの概要
可視化を調べる前に、print(samplex) を使うと、ノード数、必要な入力、保証される出力の簡単なテキスト概要が得られます。
template, samplex = build(circuit)
print(samplex)
Samplex(<17 nodes>)
Inputs:
- 'pauli_lindblad_maps.cx_noise' <PauliLindbladMap>: A PauliLindblad map acting on 2
qubits, with 'num_terms_cx_noise' terms.
Outputs:
* 'measurement_flips.c' <bool['num_randomizations', 1, 4]>: Bit-flip corrections for
measurement twirling.
* 'parameter_values' <float32['num_randomizations', 24]>: Parameter values valid for an
associated template circuit.
* 'pauli_signs' <bool['num_randomizations', 1]>: Signs from sampled Pauli Lindblad
maps, where boolean values represent the parity of the number of non-trivial factors in the
sampled error that arise from negative rates. In other words, in order to implement basic
PEC, the sign used to correct expectation values should be ``(-1)**bool_value``. The order
matches the iteration order of boxes in the original circuit with noise injection
annotations.
レジスターを調べる
sample() に keep_registers=True を渡すと、サンプリング終了時に有効な中間の VirtualRegister オブジェクトが保持され、outputs.metadata["registers"] に格納されます。各レジスターは、仮想群要素の2次元配列(形状は (num_subsystems, num_randomizations) で、末尾にゲート形状の次元が付く場合があります)です。
これは、仮想ゲートが正しく結合されたことを確認したり、回転角に合成される前の生のパウリまたはユニタリーのサンプルを調べたりするのに便利です。
from qiskit.quantum_info import PauliLindbladMap
outputs = samplex.sample(
{"pauli_lindblad_maps.cx_noise": PauliLindbladMap.identity(2)},
num_randomizations=3,
keep_registers=True,
)
for name, reg in outputs.metadata["registers"].items():
print(f"{name}: type={reg.TYPE.value}, shape={reg.virtual_gates.shape}")
lhs_0: type=pauli, shape=(4, 3)
rhs_0: type=pauli, shape=(4, 3)
lhs_3: type=pauli, shape=(2, 3)
rhs_3: type=pauli, shape=(2, 3)
inject_noise_1: type=pauli, shape=(2, 3)
sign_1: type=z2, shape=(1, 3)
lhs_7: type=pauli, shape=(2, 3)
rhs_7: type=pauli, shape=(2, 3)
meas_prop_6: type=pauli, shape=(4, 3)
collect_10: type=u2, shape=(2, 3, 2, 2)
meas_prop_z2a_6: type=z2, shape=(4, 3)
collect_9: type=u2, shape=(4, 3, 2, 2)
collect_5: type=u2, shape=(2, 3, 2, 2)
特定のレジスターの最終状態の内容を表示できます。次の例では、レジスターの型は PauliRegister です。ストレージ形式の詳細については、API documentation(/apidocs/index)__ をご覧ください。
print("lhs_0:", outputs.metadata["registers"]["lhs_0"])
outputs.metadata["registers"]["lhs_0"].virtual_gates
lhs_0: PauliRegister(<4, 3>)
array([[0, 0, 1],
[3, 1, 1],
[3, 0, 0],
[1, 0, 0]], dtype=uint8)
サンプレックス・ノードとテンプレート・パラメーターの対応
sample() が返す outputs["parameter_values"] 配列の形状は (num_randomizations, N) で、N は len(template.parameters) に一致します。各列はテンプレート回路の1つのパラメーターに対応し、i 番目の列が template.parameters[i] を埋めます。
CollectTemplateValues コレクション・ノードは、仮想レジスターとテンプレート・パラメーターを結ぶ橋渡しです。そのようなノードはそれぞれ、出力配列のどの列に書き込むかを記録するインデックス情報を保持しています。これにより、どの仮想ゲートのサブシステムがどのテンプレート・パラメーターを駆動するかを追跡できます。
サンプリングしたパラメーター値をテンプレート回路にバインドして実験を実行する方法については、{Samplex inputs and outputs}samplex-io ガイドをご覧ください。
from samplomatic.samplex.nodes import CollectTemplateValues
template, samplex = build(circuit, debug=True)
for node in samplex.graph.nodes():
if isinstance(node, CollectTemplateValues):
tags = node.trace_info.trace_refs.get("tag", set()) if node.trace_info else set()
print(f"tags={tags} → template param indices: {node.template_idxs.tolist()}")
tags={'cx_cd'} → template param indices: [[6, 7, 8], [9, 10, 11]]
tags={'cx_ab', 'cx_cd'} → template param indices: [[12, 13, 14], [15, 16, 17], [18, 19, 20], [21, 22, 23]]
tags=set() → template param indices: [[0, 1, 2], [3, 4, 5]]
自動タグ付け
generate_boxing_pass_manager() を使用する場合、add_tags パラメーターが、すべてのボックスに Tag 注釈を自動的に追加します。3つのモードが利用できます。
-
"unique_instance": 回路の順序で、ボックスに連番のref(t0、t1など)を割り当てます。構造に関係なく、すべてのボックスに異なるrefが付きます。 -
"unique_box": 各ボックスの内容の構造的ハッシュを計算し、構造的に等価なすべてのボックスに同じrefを割り当てます。位置ではなくタイプごとにボックスをグループ化する場合に便利です。 -
"noise_ref": 各ボックスのInjectNoiseアノテーションからrefをコピーし、アノテーションを持つボックスにのみタグを付けます。意味のあるノイズ ref がすでに存在する場合に便利です。
次の例では、2つのCXボックスが構造的に等価な回路に対して "unique_instance" と "unique_box" を適用します。"unique_instance" では、それぞれに個別の ref が割り当てられますが、"unique_box" では、1つを共有します。
from samplomatic.transpiler import generate_boxing_pass_manager
base_circuit = QuantumCircuit(3)
base_circuit.cx(0, 1)
base_circuit.cx(1, 2)
base_circuit.measure_all()
# unique_instance: every box gets a distinct tag ref
pm = generate_boxing_pass_manager(add_tags="unique_instance")
boxed = pm.run(base_circuit)
template, _ = build(boxed)
print("unique_instance barrier labels:")
for instr in template:
if instr.operation.name == "barrier" and instr.operation.label:
print(f" {instr.operation.label}")
unique_instance barrier labels:
L0@tag=t0
M0@tag=t0
R0@tag=t0
L1@tag=t1
M1@tag=t1
R1@tag=t1
L2@tag=t2
M2@tag=t2
R2@tag=t2
