Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Qamomile v0.12.3

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] = aqubits[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]lohibindingsにあるケースなど)もサポートされます(#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参照(CastMetadataQFixedMetadataArrayRuntimeMetadataCastOperation.qubit_mapping)もまとめて書き換えます。独立したトレース実行から構築された構造的に同一な2つのBlockが、canonical form上では一致するようになり、content_hash(block)は安定なSHA-256のhex digestを返します。これはcontent-addressableなキャッシュやIR差分に使えます。表示専用フィールド(Block.nameBlock.output_namesValue.name)は意図的にハッシュから除外しているため、関数名だけが異なる構造同型な2つのカーネルは同じハッシュになります。スコープはBlockKind.AFFINEBlockKind.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パラメータマニフェスト

すべてのBlockparam_slots: tuple[ParamSlot, ...]を持つようになりました。古典的な値の引数ごとに1エントリで、(name, type, kind, ndim, default, bound_value, differentiable)を記録します。kindは、そのスロットがランタイムパラメータとしてパイプラインを通過する場合はRUNTIME_PARAMETERbindingsやPythonシグネチャのデフォルトで束縛された場合はCOMPILE_TIME_BOUNDになります。マニフェストは早期パイプライン(inlinesubstituteresolve_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 2

Why: partial_evalが束縛を定数に畳んだあと、IRだけでは「この定数は束縛由来か、ソース中のリテラルか」を区別できなくなります。同じコンパイル済み@qkernelを異なる束縛で繰り返し呼ぶ外部DSLにとって、この区別は必須で、param_slotsが無いと受け手は再バインドできません。qubitとVector[Qubit]の入力はマニフェストには入りません。それらは引き続きBlock.input_valuesで扱われます。

BlockのJSON / msgpackワイヤフォーマット

qamomile.circuit.ir.serializeからdump_json / load_jsondump_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を取り除いてBlockBlockKind.AFFINEまで進めます。これは上のcanonicalizeの入力契約でもあり、ここのワイヤフォーマットエンコーダの入力契約でもあります(kernel.blockHIERARCHICALの生トレースを返すため、シリアライズやハッシュの前に必ずinlineが必要です)。

Why: これは上のcanonical formとパラメータマニフェストが準備してきた、実際のワイヤフォーマットです。デコーダは動的クラス解決を一切行わず(すべての$typeタグはハードコードされたファクトリーマップを通る)、ロードは素のJSON / msgpackと同じ安全性を持ちます。pickleのような任意コード実行はありません。トップレベルのスコープはBlockKind.AFFINEBlockKind.ANALYZEDです。ControlledUOperation.blockCompositeGateOperation.implementation_blockに埋め込まれたネストしたHIERARCHICALは正当に通過できます。

バグ修正

ドキュメント

さらに詳しく