Samplexの入出力
概要
Samplex これは、samplomatic ライブラリの中核となる型です。 サンプレックスは、あるテンプレート回路のパラメータに関するパラメトリック確率分布を表すものであり、バインドされたテンプレート回路の実行から収集されたデータを後処理する際に使用する、その他の配列型のフィールドも備えています。 その中心となるインターフェースは、この分布から値を取り出して配列のコレクションを生成するメソッド sample() です。
このガイドでは、インターフェース sample() の使用方法、必要な入力の照会および指定方法、ならびに期待される出力の確認方法について説明します。 に渡し、およびから返される配列は、強型付けされています sample() 。 つまり、サンプレックスのインスタンスについては、サンプリングが行われる前に、その名前、型、形状が確定しており、それらを照会することができる。 サンプレックスに含まれる入力と出力は、多くの要因によって決まります。 例えば、測定時の回転があるかどうか、あるいはノイズの注入が必要かどうかなどです。 このため、一般的に、2つのsamplexインスタンスが入力と出力を共有することは想定されていません。
セットアップ
まず、ボックス化された回路を作成し、それをサンプレックスとテンプレートのペアに組み込みます。 これらは、以下の例で使用されます。 なお、回路を手動でボックス分けする場合、このガイドでは alpha、 beta、 ref1、 ref2、 mod_ref1、 mod_ref2、 mod_ref3、および という名称を使用しています conclude。 さらに、この回路には4つのオブジェクト Parameter があります。 このガイドでは、これらそれぞれがサンプレックスの入力および出力においてどのような役割を果たしているかを説明します。 回路の自動ボックス化に関する詳細については、 トランスパイラーガイドを参照してください。
import matplotlib.pyplot as plt
import numpy as np
from qiskit.circuit import ClassicalRegister, Parameter, QuantumCircuit, QuantumRegister
from qiskit.quantum_info import Operator, Pauli, PauliLindbladMap
from samplomatic import ChangeBasis, InjectNoise, Twirl, build
# our circuit has two classical registers named alpha and beta
circuit = QuantumCircuit(
QuantumRegister(4), alpha := ClassicalRegister(3, "alpha"), beta := ClassicalRegister(1, "beta")
)
# the first box is only twirled
with circuit.box([Twirl()]):
for idx in range(4):
circuit.rx(Parameter(f"a{idx}"), idx)
circuit.cz(0, 1)
circuit.cz(1, 2)
# the second box is twirled, and has noise injected
with circuit.box([Twirl(), InjectNoise(ref="ref1", modifier_ref="mod_ref1")]):
circuit.h(1)
circuit.cz(1, 2)
# the third box is twirled, and has different noise injected
with circuit.box([Twirl(), InjectNoise(ref="ref2", modifier_ref="mod_ref2")]):
circuit.rx(0.1, range(4))
circuit.cz(0, 1)
circuit.cz(1, 2)
# the fourth box is the same as the second, but with a different modifer ref
with circuit.box([Twirl(), InjectNoise(ref="ref1", modifier_ref="mod_ref3")]):
circuit.h(1)
circuit.cz(1, 2)
circuit.barrier()
# the final two boxes twirl, and add a basis change in one case
with circuit.box([Twirl(), ChangeBasis(ref="conclude")]):
circuit.measure(range(3), alpha)
with circuit.box([Twirl()]):
circuit.measure([3], beta)
circuit.draw("mpl")
次に、枠で囲まれた回路 build() に対して を呼び出し、テンプレートとサンプレックスのペアを作成します。 各ボックスが、ドレッシングを実現するための断片 rz-sx-rz-sx-rz を含む「サンドイッチ」状のバリアへと変換される様子をご覧いただけます。
template, samplex = build(circuit)
template.draw("mpl", fold=100)
samplex DAG を図にプロットし、ノードにカーソルを合わせると、以下の内容が表示されます:
- すべてのサンプリングノード(星)は、ツイリングのためのランダム化の生成、ノイズモデルからのサンプリング、あるいは基底の変更の注入を担当しています。
- すべての収集ノード(蝶ネクタイ)は、測定フリップまたはパラメータ角度である出力スライスのレンダリングを担当しています。
- すべての中間ノード(円)は、さまざまな種類のデータ操作を担当しています。
samplex.draw()必要な入力と期待される出力を確認する
必要な入力と期待される出力を確認する最も簡単な方法は、その Samplex オブジェクトを出力することです。 配列の各要素は「 '{name}' <{type}[{shape}...]>,」で区切られ、必須の入力項目がオプションの入力項目の前に配置されます。
print(samplex)Samplex(<58 nodes>)
Inputs:
- 'basis_changes.conclude' <uint8[3]>: Basis changing gates, in the symplectic ordering
I=0, Z=1, X=2, and Y=3.
- 'parameter_values' <float64[4]>: Input parameter values to use during sampling.
- 'pauli_lindblad_maps.ref1' <PauliLindbladMap>: A PauliLindblad map acting on 2 qubits,
with 'num_terms_ref1' terms.
- 'pauli_lindblad_maps.ref2' <PauliLindbladMap>: A PauliLindblad map acting on 4 qubits,
with 'num_terms_ref2' terms.
- 'local_scales.mod_ref1' <float64['num_terms_ref1']>: (Optional) An array of factors by
which to scale individual rates of a Pauli Lindblad map, where the order should match the
corresponding list of terms.
- 'local_scales.mod_ref2' <float64['num_terms_ref2']>: (Optional) An array of factors by
which to scale individual rates of a Pauli Lindblad map, where the order should match the
corresponding list of terms.
- 'local_scales.mod_ref3' <float64['num_terms_ref1']>: (Optional) An array of factors by
which to scale individual rates of a Pauli Lindblad map, where the order should match the
corresponding list of terms.
- 'noise_scales.mod_ref1' <float64[]>: (Optional) A scalar factor by which to scale rates
of a Pauli Lindblad map.
- 'noise_scales.mod_ref2' <float64[]>: (Optional) A scalar factor by which to scale rates
of a Pauli Lindblad map.
- 'noise_scales.mod_ref3' <float64[]>: (Optional) A scalar factor by which to scale rates
of a Pauli Lindblad map.
Outputs:
* 'measurement_flips.alpha' <bool['num_randomizations', 1, 3]>: Bit-flip corrections
for measurement twirling.
* 'measurement_flips.beta' <bool['num_randomizations', 1, 1]>: Bit-flip corrections for
measurement twirling.
* 'parameter_values' <float32['num_randomizations', 48]>: Parameter values valid for an
associated template circuit.
* 'pauli_signs' <bool['num_randomizations', 3]>: 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.
サンプレ outputs() ックスの および inputs() 、ならびにそれらの名前、形状、種類、説明といったより詳細な情報についても、プログラムから照会することができます。 これら2つのメソッドの戻り値の型は、 TensorInterface. です。 たとえば、次のコードは、入力インターフェースのすべての仕様を一覧表示しています:
samplex.inputs().specs[TensorSpecification('basis_changes.conclude', (3,), dtype('uint8'), 'Basis changing gates, in the symplectic ordering I=0, Z=1, X=2, and Y=3.'),
TensorSpecification('local_scales.mod_ref1', ('num_terms_ref1',), dtype('float64'), 'An array of factors by which to scale individual rates of a Pauli Lindblad map, where the order should match the corresponding list of terms.', optional=True),
TensorSpecification('local_scales.mod_ref2', ('num_terms_ref2',), dtype('float64'), 'An array of factors by which to scale individual rates of a Pauli Lindblad map, where the order should match the corresponding list of terms.', optional=True),
TensorSpecification('local_scales.mod_ref3', ('num_terms_ref1',), dtype('float64'), 'An array of factors by which to scale individual rates of a Pauli Lindblad map, where the order should match the corresponding list of terms.', optional=True),
TensorSpecification('noise_scales.mod_ref1', (), dtype('float64'), 'A scalar factor by which to scale rates of a Pauli Lindblad map.', optional=True),
TensorSpecification('noise_scales.mod_ref2', (), dtype('float64'), 'A scalar factor by which to scale rates of a Pauli Lindblad map.', optional=True),
TensorSpecification('noise_scales.mod_ref3', (), dtype('float64'), 'A scalar factor by which to scale rates of a Pauli Lindblad map.', optional=True),
TensorSpecification('parameter_values', (4,), dtype('float64'), 'Input parameter values to use during sampling.'),
PauliLindbladMapSpecification('pauli_lindblad_maps.ref1', num_qubits=2, num_terms=num_terms_ref1),
PauliLindbladMapSpecification('pauli_lindblad_maps.ref2', num_qubits=4, num_terms=num_terms_ref2)]
次の例では、名前に文字列「.」を含む出力指定子に関する情報をいくつか表示します "flips"。
for spec in samplex.outputs().get_specs("flips"):
print(spec.name, spec.shape, spec.dtype)measurement_flips.alpha ('num_randomizations', 1, 3) bool
measurement_flips.beta ('num_randomizations', 1, 1) bool
入力値をバインドしてサンプルを取得する
samplex には sample()、少なくとも「必須」とマークされている項 inputs() 目の値が必要です。 これらの値はまず入力インターフェースにバインドされ、それによって必要なすべての型チェック(場合によっては型の強制)および形状解析が行われるようにします。 型や形状に問題がある場合、または必須の値が欠落している場合は、詳細なエラーが発生します。
入力のバインド
次の例では、この sample() メソッドに必要な入力値が渡されています。 その後、これを使用して、サンプレックスから3つのランダム化を抽出します。 ドレッシングから求められる結合は、ノイズ注入のためのパウリ・リンドブラッド写像と、基底変換配列である。 さらに、元のボックス化された回路における4つのパラメータの値も必要となりますが、この場合は に設定されています np.linspace(0, 1, 4)。 サンプレックスはこれらのパラメータをドレッシングに組み込むため、事実上、サンプレックスの出力パラメータ値に対する変更となります。
inputs = samplex.inputs().bind(
# bind() concatenates nested dicts with a '.' so that we can
# set 'pauli_lindblad_maps.noise0/1' as follows. note that the
# names 'ref1' and 'ref2' derive from the names in the InjectNoise
# annotation.
pauli_lindblad_maps={
"ref1": PauliLindbladMap.identity(2),
"ref2": PauliLindbladMap.identity(4),
},
# likewise, we can set all basis changes as follows, where
# the name 'conclude' comes from our basis change annotation
basis_changes={"conclude": [0, 1, 2]},
# we must provide values for all parameters in the original circuit
parameter_values=np.linspace(0, 1, 4),
)
outputs = samplex.sample(inputs, num_randomizations=3)
print(outputs)SamplexOutput(<
- Dimension constraints: num_randomizations=3
* 'measurement_flips.alpha' <bool['num_randomizations', 1, 3]>: Bit-flip corrections for
measurement twirling.
* 'measurement_flips.beta' <bool['num_randomizations', 1, 1]>: Bit-flip corrections for
measurement twirling.
* 'parameter_values' <float32['num_randomizations', 48]>: Parameter values valid for an
associated template circuit.
* 'pauli_signs' <bool['num_randomizations', 3]>: 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.
>)
入力は、を複数回呼び出すか、または bind() 項目への直接の代入によって、インターフェースにバインドすることができます。 利便性を高める bind() ネストされた辞書形式を採用しているものの、これはフラットなマッピングオブジェクトであり、一般的なマッピング構文をすべてサポートしています。 必要な入力がすべて見つかった時点で、 は真 fully_bound となる。
inputs = (
samplex.inputs()
.bind(
pauli_lindblad_maps={
"ref1": PauliLindbladMap.identity(2),
"ref2": PauliLindbladMap.identity(4),
}
)
.bind(parameter_values=np.linspace(0, 1, 4))
)
# once the final requirement is bound, fully_bound becomes True
print("Fully bound?", inputs.fully_bound)
inputs["basis_changes.conclude"] = [0, 2, 3]
print("Fully bound?", inputs.fully_bound)
# setting optional values does not affect whether the interface is fully bound
inputs["noise_scales.mod_ref1"] = 3
print("Fully bound?", inputs.fully_bound)Fully bound? False
Fully bound? True
Fully bound? True
サブ SamplexOutput クラスを含むすべてのオブジェクト TensorInterface は、それらにバインドされたデータに対してオブジェクトをマッピングします。 つまり、たとえば、次のようにテンプレートパラメータの値を取得することができます。 この場合、3回のランダム化が要求されており、テンプレート回路には48個のパラメータがあるため (3, 48) 、その形状は となります。
print("(num_randomizations, num_template_params) =", outputs["parameter_values"].shape)
print(outputs["parameter_values"][0, :])(num_randomizations, num_template_params) = (3, 48)
[ 0.0000000e+00 0.0000000e+00 0.0000000e+00 1.5707964e+00
3.3333334e-01 1.5707964e+00 -1.5707964e+00 2.4749260e+00
-1.5707964e+00 1.5707964e+00 1.0000000e+00 1.5707964e+00
0.0000000e+00 1.5707964e+00 0.0000000e+00 3.1415927e+00
0.0000000e+00 0.0000000e+00 1.5707964e+00 3.0415926e+00
-1.5707964e+00 -1.5707964e+00 3.0415926e+00 -1.5707964e+00
-1.5707964e+00 1.0000000e-01 1.5707964e+00 1.5707964e+00
1.0000000e-01 1.5707964e+00 3.1415927e+00 1.5707964e+00
0.0000000e+00 0.0000000e+00 0.0000000e+00 0.0000000e+00
0.0000000e+00 3.1415927e+00 0.0000000e+00 3.1415927e+00
0.0000000e+00 0.0000000e+00 3.1415927e+00 1.5707964e+00
0.0000000e+00 4.4408921e-16 3.1415927e+00 0.0000000e+00]
入力を辞書として指定する
完全にバインドされたものを指定する代わりに TensorInterface、下のセルに示されているように、入力を標準の辞書として指定することもできます。 この場合、このメソッド sample() は内部的に辞書の項目をSamplexの入力インターフェースにバインドし、必要な入力がすべて存在し、互換性があることを検証します。
inputs = {
"pauli_lindblad_maps": {
"ref1": PauliLindbladMap.identity(2),
"ref2": PauliLindbladMap.identity(4),
},
"basis_changes": {"conclude": [0, 1, 2]},
"parameter_values": np.linspace(0, 1, 4),
}
outputs = samplex.sample(inputs, num_randomizations=3)入力タイプ
現在、インターフェースの値型には以下の2つがあります:
- 配列値:これらは形状とデータ型を持っています。 バインドされた値は、可能であればそのデータ型に変換されます。 どの寸法でも自由寸法にすることができます。
- パウリ・リンドブラッド写像:これらは 型でなければならず、
qiskit.quantum_info.PauliLindbladMapまた所定の数の量子ビットに対して作用しなければならない。 マップに含まれる項の数は、自由次元です。
# up to precision loss, these are equivalent via automatic type coersion of array inputs
samplex.inputs()["parameter_values"] = [1, 2, 8, 9]
samplex.inputs()["parameter_values"] = np.array([1, 2, 8, 9], dtype=np.float16)自由な寸法
このクラス TensorInterface には「 自由次元 」という概念があり、これらは整数サイズの名前で表されるもので、それらを制約するデータがインターフェースにバインドされるまでは、その値は未定となります。 データをバインドする際、同じ名前を持つすべての自由寸法は一貫したサイズに解決されなければならず、そうでない場合はエラーが発生します。 例えば、任意のサンプレ outputs() ックスの において、ランダム化に対応する配列の軸は、慣例 "num_randomizations" により という名前が付けられた自由パラメータである。 が呼び出 sample() されると、出力に値が紐付けられます
print("All free dimensions:", outputs.free_dimensions)
print("Current constraints:", outputs.bound_dimensions)All free dimensions: {'num_randomizations'}
Current constraints: {'num_randomizations': 3}
もう1つの一般的な自由次元シナリオとして、パウリ・リンドブラッド写像の仕様とその修飾子間で項の制約を共有する場合が挙げられる。 A は項の順序付き列であり qiskit.quantum_info.PauliLindbladMap 、各項には浮動小数点型のレートと疎なパウリ表現が含まれている。 この関 build() 数は、各アノテーション InjectNoise に対してこの "pauli_lindblad_maps.<ref>" 型の指定子を追加し、そのアノテーションに が含まれている場合は modifier_ref、指定 "local_scales.<modifier_ref>" 子も追加されます。 これら2つには、ノイズモデルにいくつのパウリ・リンドブラッド項が含まれるかを指定 "num_terms_<ref>" する自由次元が共通して存在します。なぜなら、後者は前者の発生率に対して「ジップ」を行って、その発生率をスケーリングする必要があるからです。
# both the PauliLindbladMap and the local scales imply 2 terms, so that the free dimension
# 'num_terms_noise2' is satisfied
inputs = samplex.inputs().bind(
pauli_lindblad_maps={"ref2": PauliLindbladMap.from_list([("XXYZ", 0.1), ("IIXX", 0.2)])},
local_scales={"mod_ref2": [1, 2]},
)
print("All free dimensions:", inputs.free_dimensions)
print("Current constraints:", inputs.bound_dimensions)All free dimensions: {'num_terms_ref1', 'num_terms_ref2'}
Current constraints: {'num_terms_ref2': 2}
量子ビットの順序付け規則
関数 build() によってボックス化された回路から構築されるサンプレックスには、配列値の入力が必要となる場合があります sample()。この場合、1つのインデックスは回路のボックス内の量子ビットに対応します。 このセクションでは、そのような軸に適用すべき順序の規則について説明します。 要約すると、ボックス化された回路の量子ビットの順序を使用し、そのボックスの量子ビットに限定する。
例えば、に対してパウリ・リンドブラッド写像をどのように指定すればよいかを明らかにしたいとしましょう "noise1"。 したがって、ノイズマップにおける量子ビットの順序は、これらの量子ビットを基準としたものとなります:
box_of_interest = circuit[1]
qubit_ordering_convention = [qubit for qubit in circuit.qubits if qubit in box_of_interest.qubits]
qubit_ordering_convention[<Qubit register=(4, "q0"), index=1>, <Qubit register=(4, "q0"), index=2>]
つまり、ノイズ X を使ってノイズを <Qubit register=(4, "q0"), index=2> 発生させたいのであれば、このようなパウリ・リンドブラッド写像を定義すべきです。なぜなら、これは 1 のインデックスにあるからです qubit_ordering_convention。
samplex.inputs().bind(
pauli_lindblad_maps={
"ref1": (noise1 := PauliLindbladMap.from_sparse_list([("X", [1], 0.12)], num_qubits=2))
}
)
noise1<PauliLindbladMap with 1 term on 2 qubits: (0.12)L(X_1)>
基底の変更についても同様です。 のクビット 0、1、2 に対して circuit、それぞれ演算子 X、Y、Z の +1 の固有状態へと回転させたい場合は、以下の配列を使用してください。なお、このライブラリ(および Qiskit)では、 、 、 、 という表記規則が採用されていることに留意してください。
samplex.inputs().bind(basis_changes={"conclude": (basis_change := [2, 3, 1])})
basis_change[2, 3, 1]
なお、量子ビットの順序付けの規則は、以下のいずれかによって定義される順序とは異なることに注意してください:
box_instruction.qubits, ここで はbox_instruction、そのqiskit.circuit.CircuitInstruction演算が である であるqiskit.circuit.BoxOp。というのも、この量子ビットのリストの順序は、ボックスコンテキストに命令が追加された順序に依存する可能性があるためであり、それは混乱を招く慣例となるからだ。 たとえば、 とwith box(): circuit.x([0, 1])では、順序が異なってしまいますwith box(): circuit.x([1, 0])。box_instruction.operation.body.qubitsなぜなら、これらの量子ビットは外部回路には現れない可能性さえあるからです。BoxOp命令本体(またはのようなその他の制御フロー操作IfElseOp)内の量子ビットについては、そのスコープ内でのみ一貫した意味を持つことが保証されています。 これは定義が曖昧な規約となり、一般的に使いにくいものになるでしょう。
テンプレート回路を用いたローカルテスト
関数 build() を使用して が構築される Samplex 場合、出力パラメータの値は、関連するテンプレート回路のパラメータと直接互換性がある必要があります。 たとえば、ランダム化のうちの1つについて、その値をテンプレートに紐付けて、ローカルで確認することができます。
inputs = samplex.inputs().bind(
pauli_lindblad_maps={
"ref1": PauliLindbladMap.identity(2),
"ref2": PauliLindbladMap.identity(4),
},
basis_changes={"conclude": [0, 0, 0]},
parameter_values=np.linspace(0, 1, 4),
)
outputs = samplex.sample(inputs, num_randomizations=3)
bound_template = template.assign_parameters(outputs["parameter_values"][2])
bound_template.draw("mpl", fold=1000)
これにより、サンプレックスが期待どおりのサンプルを出力していることを確認できます。 たとえば、次のコードでは、バインドされた回路を qiskit.quantum_info.Operator オブジェクトにキャストし、ビットフリップ・パウリ演算と組み合わせることで、3つのランダム化すべてが基本回路と論理的に同値であることを視覚的に確認しています。
from samplomatic.utils import unbox
# Operator requires that we unpack all of the boxes first
unboxed = unbox(circuit).assign_parameters(inputs["parameter_values"])
# We want to plot unitary matrix amplitudes, so we need to remove the non-unitary measurements
unboxed.remove_final_measurements()
unboxed_unitary = Operator(unboxed)
# Plot magnitudes of the unitary matrix represenation of the circuit
plt.subplot(1, 4, 1)
plt.imshow(np.abs(unboxed_unitary))
plt.title("Base Circuit")
# For each randomization we sampled, plot the unitary matrix representation
for idx in range(3):
bound_template = template.assign_parameters(outputs["parameter_values"][idx])
bound_template.remove_final_measurements()
# The example does measurement twirling by compiling random bitflip gates before the
# measurements, so we need to undo these at the operator level for each randomization.
# We do this by casting them to Paulis and composing with the bound circuit's unitary
alpha_flips = Pauli(([0, 0, 0], outputs["measurement_flips.alpha"][idx, 0]))
beta_flips = Pauli(([0], outputs["measurement_flips.beta"][idx, 0]))
flips = beta_flips ^ alpha_flips
bound_unitary = Operator(bound_template) & flips
# Plot magnitudes of the unitary matrix represenation of the circuit
plt.subplot(1, 4, idx + 2)
plt.imshow(np.abs(bound_unitary))
plt.title(f"Randomization {idx}")
plt.tight_layout()
同様に、以下のコードでは、出力パラメータの値とテンプレートをサンプラーに渡して、すべてのランダム化パターンについて10,000回のシミュレーションを行い、その結果得られた期待値(アルファレジスタのみ)を、基本回路のシミュレーション結果の期待値と比較しています。
from qiskit.primitives import StatevectorEstimator as Estimator
from qiskit.primitives import StatevectorSampler as Sampler
from qiskit.primitives.containers import BitArray
# simulate some expectation values direction from the base circuit
estimator_job = Estimator().run([(unboxed, ["IZII", "IIZI", "IIIZ"])])
evs = estimator_job.result()[0].data["evs"]
# get the sampler data from each randomization, and do bitflip correction
sampler_job = Sampler().run([(template, outputs["parameter_values"])], shots=10_000)
alpha_data = sampler_job.result()[0].data["alpha"]
alpha_data ^= BitArray.from_bool_array(outputs["measurement_flips.alpha"], "little")
# compare the results
print(" EVs from base circuit:", evs)
print("EVs from randomizations:", alpha_data.expectation_values(["ZII", "IZI", "IIZ"])) EVs from base circuit: [0.7819611 0.93553883 0.99500417]
EVs from randomizations: [0.789 0.9366 0.995 ]