Qamomile v0.12.3では、@qkernelの中でVector[Qubit]にPython風のスライシング(q[1::2]、q[lo:hi]など)を適用できるようになりました。返ってくるVectorViewはゲート、測定、ヘルパーカーネルへの受け渡しなど、通常のVector[Qubit]と同じように扱えます。あわせて、Pauli文字列Hamiltonian同士のcommutator(a, b)と、ランタイムbitsからの状態準備を可能にするcomputational_basis_stateという2つのアルゴリズム部品も追加されました。また、コンパイル済み@qkernelを外部DSLの計算グラフ上のサブグラフとして扱うための基盤として、IRにも内部的な更新が入っています。
pip install qamomile==0.12.3破壊的変更¶
borrow競合用の例外QubitBorrowConflictErrorを追加¶
QubitBorrowConflictError(AffineTypeErrorのサブクラス)が新たに導入され、別の生存中のハンドルにスロットがborrowされているためにqubitスロットにアクセスできない場合に送出されます。未返却のスライスview、未返却の要素borrowなどがこれに該当します。QubitConsumedErrorとの違いは可逆性で、borrowはq[0:3] = aやqubits[0] = q0のように返却すればアクセスが復元しますが、consumeされたスロットは復元できません。v0.12.2から例外クラスが変わるのはArrayBase._get_elementの「要素は既にborrow済み」チェック1箇所のみで、ここは従来QubitConsumedErrorを送出していましたが、これからはQubitBorrowConflictErrorを送出します。残り4箇所(ArrayBase内2箇所、VectorView._wrap内2箇所)は今回新たに追加されたスライスview競合用のraiseサイトです。要素既borrow済みのケースを明示的にQubitConsumedErrorでcatchしていた呼び出し側は、QubitBorrowConflictErrorを明示的にcatchするか、共通基底のAffineTypeErrorをcatchするように切り替える必要があります(#395)。
新機能¶
Python風のVectorスライシング¶
Vector[Qubit]がPythonのスライス構文(q[lo:hi]、q[lo:hi:step]、q[::step])を@qkernelの中で直接受け取れるようになりました。結果はVectorViewで、IRには第一級のSliceArrayOperationとして降りていき、Qiskit / QURI Parts / CUDA-Qのいずれでも正しく実行されます。スライス境界はPythonリテラル、スカラーハンドル(UInt)、それらの算術式のいずれでも構いません。bindingsを適用したあとでないと解決できない境界(q[lo:hi]でlo、hiがbindingsにあるケースなど)もサポートされます(#357)。
import qamomile.circuit as qmc
from qamomile.qiskit import QiskitTranspiler
@qmc.qkernel
def alternating() -> qmc.Vector[qmc.Bit]:
q = qmc.qubit_array(6, name="q")
evens = q[0::2]
odds = q[1::2]
for i in qmc.range(evens.shape[0]):
evens[i] = qmc.h(evens[i])
evens[i], odds[i] = qmc.cx(evens[i], odds[i])
q[0::2] = evens
q[1::2] = odds
return qmc.measure(q)
exe = QiskitTranspiler().transpile(alternating)transpileパイプラインに追加されたSliceBorrowCheckPassがスライス関連のアフィン型エラーを検知します。measure(q[1::2])のあとにq[1]へアクセスするとQubitConsumedError、最初のviewを返さずに重なるviewを取ろうとするとQubitBorrowConflictError、親レジスタの長さを越える境界は自動でクランプされます。負のstepと負のindex(q[::-1]、q[-2:])はサポートしていません。
エンドツーエンドの解説は新設のチュートリアルベクトルスライシングを参照してください。
Pauli文字列Hamiltonian同士のcommutator(a, b)¶
qamomile.observable.commutator(a, b)は2つのPauli文字列Hamiltonianに対する交換子[A, B] = A B - B Aを返します。実装はqubitパリティ規則(2つのPauli文字列が反交換するのは、互いに異なる非恒等Pauliを持つqubitの個数が奇数のときに限る)を使って、交換するPauliペアをすべて事前に落としてから積を計算します。a * b - b * aを展開してから打ち消しを行うよりも1ペアあたりのコストが低く、結果は完全に簡略化されたHamiltonianなので、そのまま検査や解析値との比較に使えます(#394)。
import qamomile.observable as qm_o
omega, Omega = 1.0, 0.3
Hz = 0.5 * omega * qm_o.Z(0)
Hx = 0.5 * Omega * qm_o.X(0)
comm = qm_o.commutator(Hz, Hx)
print(comm)Hamiltonian((Y0,): 0.15j)ハミルトニアンシミュレーションチュートリアルには、教科書通りの[H_z, H_x] = i ω Ω / 2 · Yをすでに扱っているTrotter分解の動機付けとして導出するセクションを追加しました。
computational_basis_stateアルゴリズムヘルパー¶
qamomile.circuit.algorithm.computational_basis_state(q, bits)はRx(π · bits[i])を各qubitに適用することで、|0⟩^⊗nから計算基底状態|bits⟩を準備します。bits[i] == 0なら恒等、bits[i] == 1なら(グローバル位相を除いて)Xに一致します。重要な性質は、bitsをトランスパイル時にランタイムパラメータとして残せることです。ランタイムのif bits[i]: x(...)はサポート対象のSDK向けエミッタで発行できませんが、パラメータ付きの回転は発行できます。これはQeMCMCのように初期ビット列だけが毎反復で変わるハイブリッドループで、回路を再コンパイルせずにbitsだけを差し替えるための部品です(#361)。
import qamomile.circuit as qmc
from qamomile.circuit.algorithm import computational_basis_state
from qamomile.qiskit import QiskitTranspiler
@qmc.qkernel
def prepare_basis(
n: qmc.UInt,
bits: qmc.Vector[qmc.UInt],
) -> qmc.Vector[qmc.Bit]:
q = qmc.qubit_array(n, name="q")
q = computational_basis_state(q, bits)
return qmc.measure(q)
exe = QiskitTranspiler().transpile(
prepare_basis, bindings={"n": 3}, parameters=["bits"]
)nをコンパイル時に束縛することでcomputational_basis_state内部のfor i in qmc.range(n)本体のゲート数が確定し、bitsはランタイムパラメータとして残るので、実行時にパラメータとして渡せます。
エンドツーエンドのユースケースは新設のQuantum-enhanced MCMCチュートリアルを参照してください(trotterized_time_evolutionを提案分布とする古典Metropolis–Hastings法のハイブリッド構成で、各反復ごとに基底状態を再バインドします)。
内部的な変更¶
IRには、コンパイル済み@qkernelを外部DSLの計算グラフ上のサブグラフとして扱うための基盤プリミティブが3つ加わりました。それぞれ独立に使えます。
Canonical formとcontent hash¶
qamomile.circuit.ir.canonicalize(block)は、Block内のすべてのValue.uuid / Value.logical_idを決定的なカウンターで番号付け直し、operationやvalueメタデータに埋め込まれたUUID参照(CastMetadata、QFixedMetadata、ArrayRuntimeMetadata、CastOperation.qubit_mapping)もまとめて書き換えます。独立したトレース実行から構築された構造的に同一な2つのBlockが、canonical form上では一致するようになり、content_hash(block)は安定なSHA-256のhex digestを返します。これはcontent-addressableなキャッシュやIR差分に使えます。表示専用フィールド(Block.name、Block.output_names、Value.name)は意図的にハッシュから除外しているため、関数名だけが異なる構造同型な2つのカーネルは同じハッシュになります。スコープはBlockKind.AFFINEとBlockKind.ANALYZEDで、HIERARCHICALはinline済みである必要があります(#389)。
import qamomile.circuit as qmc
from qamomile.circuit.ir import canonicalize, content_hash
from qamomile.qiskit import QiskitTranspiler
@qmc.qkernel
def bell() -> qmc.Vector[qmc.Bit]:
q = qmc.qubit_array(2, name="q")
q[0] = qmc.h(q[0])
q[0], q[1] = qmc.cx(q[0], q[1])
return qmc.measure(q)
affine = QiskitTranspiler().inline(bell.block)
print(content_hash(canonicalize(affine)))Why: これは後続の「ポータブルなサブグラフ」関連機能が依存するIRレベルの同一性プリミティブです。canonical formがあることで、受け渡しされるIRはビルドに依存しない名前を持てます。これが無いと、ハイブリッドランナーやIRキャッシュは、独立に構築された同じカーネルの2コピーが等価であることを判定できません。
Block.param_slotsパラメータマニフェスト¶
すべてのBlockがparam_slots: tuple[ParamSlot, ...]を持つようになりました。古典的な値の引数ごとに1エントリで、(name, type, kind, ndim, default, bound_value, differentiable)を記録します。kindは、そのスロットがランタイムパラメータとしてパイプラインを通過する場合はRUNTIME_PARAMETER、bindingsやPythonシグネチャのデフォルトで束縛された場合はCOMPILE_TIME_BOUNDになります。マニフェストは早期パイプライン(inline、substitute、resolve_parameter_shapes)を通過するため、下流の読み手はカーネルの古典契約をIRだけから復元できます。Python側の外部メタデータは不要です(#390)。
import qamomile.circuit as qmc
@qmc.qkernel
def vqe_layer(theta: qmc.Float, depth: qmc.UInt = 2) -> qmc.Vector[qmc.Bit]:
q = qmc.qubit_array(2, name="q")
for _ in qmc.range(depth):
q = qmc.ry(q, theta)
return qmc.measure(q)
block = vqe_layer.build(parameters=["theta"])
for slot in block.param_slots:
print(slot.name, slot.kind.value, slot.ndim, slot.bound_value)theta runtime_parameter 0 None
depth compile_time_bound 0 2Why: partial_evalが束縛を定数に畳んだあと、IRだけでは「この定数は束縛由来か、ソース中のリテラルか」を区別できなくなります。同じコンパイル済み@qkernelを異なる束縛で繰り返し呼ぶ外部DSLにとって、この区別は必須で、param_slotsが無いと受け手は再バインドできません。qubitとVector[Qubit]の入力はマニフェストには入りません。それらは引き続きBlock.input_valuesで扱われます。
BlockのJSON / msgpackワイヤフォーマット¶
qamomile.circuit.ir.serializeからdump_json / load_jsonとdump_msgpack / load_msgpackが公開されました(ツール向けにto_dict / from_dictもあります)。どちらのエンコーダも{"schema_version": <int>, "block": <block dict>}というトップレベルエンベロープを書き出します。現在のバージョンはSCHEMA_VERSION = 1です。numpyペイロード(例: ParamSlot.bound_valueの配列)はdtype許可リスト付きでラップされます。msgpackはバイトをそのまま通すため、numpyを多く含むIRではJSONよりコンパクトになる傾向があります(#391)。
import qamomile.circuit as qmc
from qamomile.circuit.ir.serialize import dump_msgpack, load_msgpack
from qamomile.qiskit import QiskitTranspiler
@qmc.qkernel
def superposition() -> qmc.Vector[qmc.Bit]:
q = qmc.qubit_array(2, name="q")
q = qmc.h(q)
return qmc.measure(q)
affine = QiskitTranspiler().inline(superposition.block)
blob = dump_msgpack(affine)
restored = load_msgpack(blob)Transpiler.inline()はCallBlockOperationを取り除いてBlockをBlockKind.AFFINEまで進めます。これは上のcanonicalizeの入力契約でもあり、ここのワイヤフォーマットエンコーダの入力契約でもあります(kernel.blockはHIERARCHICALの生トレースを返すため、シリアライズやハッシュの前に必ずinlineが必要です)。
Why: これは上のcanonical formとパラメータマニフェストが準備してきた、実際のワイヤフォーマットです。デコーダは動的クラス解決を一切行わず(すべての$typeタグはハードコードされたファクトリーマップを通る)、ロードは素のJSON / msgpackと同じ安全性を持ちます。pickleのような任意コード実行はありません。トップレベルのスコープはBlockKind.AFFINEとBlockKind.ANALYZEDです。ControlledUOperation.blockやCompositeGateOperation.implementation_blockに埋め込まれたネストしたHIERARCHICALは正当に通過できます。
バグ修正¶
Hamiltonian.__add__/__mul__/__sub__がmax(num_qubits)を保つようになりました(#394)。Hamiltonian同士の二項演算子は、これまで左オペランドが宣言するレジスタしか引き継がず、右オペランドのqubit幅を黙って捨てていました。例えばHamiltonian.identity(1, num_qubits=2) * Hamiltonian.identity(1, num_qubits=5)はnum_qubits == 5ではなくnum_qubits == 2を返していました。この修正で、H = Z(0) + 0.0 * Z(1) # 2qubit幅にパディングのような回避パターンは不要になります。スカラー演算は影響ありません。SymbolicControlledU → ConcreteControlledU昇格時に制御Vectorが正しく展開されるようになりました(#396)。ConstantFoldingPassがnum_controlsを具体的なintに畳んだあと、昇格後のオペレーションは従来も対称的なベクトルオペランド配置[ctrl_vector, tgt, ...]を保持しており、qubit単位の配置[ctrl_0, ..., ctrl_{nc-1}, tgt, ...]へ展開されていませんでした。その結果、下流のエミットでターゲット / パラメータのスロットが制御スライスに紛れ込み、引数数不足の制御ゲートが生成されていました。可視化のスライスview向け修正(#395)。
inline=Trueがコールイ側のv[i]を架空の連続ワイヤ上にレンダリングする問題、inline=Falseがスライス被演算子に対してnum_qubitsを越える幽霊ワイヤを確保する問題、互い違いのスライスに対する2つの独立したインラインブロック(例:h_all(q[0::2])とx_all(q[1::2]))で破線ボックスが重なる問題、いずれも修正されました。
ドキュメント¶
新設のチュートリアルベクトルスライシングは、
VectorViewの基本、インラインのブロードキャスト省略形、ネストしたスライス、ヘルパー@qkernelへのviewの受け渡し(部分レンジへのqmc.qftを含む)、1本のレジスタを互いに素なviewへ分割するパターン、そして新コンパイラ診断が検知するアフィンエラーのパターンを通します。新設のQuantum-enhanced MCMC(QeMCMC)チュートリアルは、まず古典Metropolis–Hastings MCMCを扱い、その上でQamomileの
trotterized_time_evolutionを提案分布として用いるQeMCMCを実装し、量子古典ハイブリッドループ全体をエンドツーエンドで通します。新設の量子カーネル分類チュートリアルは、
@qkernelの上に量子カーネルSVMを組み立て、リソース推定までの流れを扱います(#324)。ハミルトニアンシミュレーションチュートリアルに、新しい
commutatorヘルパーを使って[H_z, H_x] = i ω Ω / 2 · Yを導出するセクションが加わりました。すでに同チュートリアルで扱っているTrotter分解の教科書的な動機付けです(#394)。チュートリアル03–08は04–09に番号付け直し: ベクトルスライシングのチュートリアルを03番に差し込むため、従来の03–08は1つずつ後ろにずれました。例えば旧Tutorial 03(Resource Estimation)はTutorial 04、旧Tutorial 08(コンパイル&トランスパイル)はTutorial 09になっています。v0.12.3以前のチュートリアルファイルパスを指す外部ブックマークは更新が必要です(#395)。