Qamomile v0.12.0は、組み込みのSuzuki–Trotter時間発展(order 1, 2、および任意の偶数order ≥ 4)、密なエルミート行列を厳密なパウリHamiltonianに変換する新しいqamomile.linalgモジュール、そしてこれらのアルゴリズムを自然なPythonとして表現するためのコンパイラ機能(自己再帰@qkernel、Vector[Observable]パラメータ、boolパラメータ)を提供します。量子最適化向けのモジュールではMathematicalProblemConverter.decode()が入力に応じて戻り値型を切り替えるようになり、ommx.v1.Instanceから構築したコンバーターはommx.v1.SampleSetを返すため、実行可能性・元の目的関数値・制約ごとの違反量をOMMXのAPIから直接扱えます。新しい3つのチュートリアルがこれらの機能をエンドツーエンドでカバーするほか、量子誤り訂正のチュートリアル(Shor 9-qubit符号とSteane [[7,1,3]]符号)が2本追加されました。
pip install qamomile==0.12.0破壊的変更¶
qamomile.optimization.utilsを削除しました。 唯一のヘルパーis_close_zeroは、量子最適化向けのモジュールの外(例えばqamomile.linalg)からも再利用できるようにパッケージ内プライベートなqamomile._utilsへ移動しました。直接importしていた場合は、ローカルな等価表現(math.isclose(value, 0.0, abs_tol=1e-15))に切り替えてください。qamomile._utilsは意図的にプライベートであり、サポート対象のAPIではありません。MathematicalProblemConverter.decode()の戻り値型が入力に応じて変わるようになりました(#353)。ommx.v1.Instanceから構築したコンバーター(QAOAConverter/FQAOAConverter/QRACConverterなど)のdecode()は、これまでBinarySampleSetを返していましたが、v0.12.0からはommx.v1.SampleSetを返します。BinarySampleSetが必要な場合は、新しく公開されたdecode_to_binary_sampleset()を使ってください。BinaryModelから構築したコンバーターの戻り値(BinarySampleSet)は変わりません。MathematicalProblemConverter/FQAOAConverterに渡したommx.v1.Instanceを破壊的に変更しなくなりました(#353)。Instance.to_qubo()は呼び出されたインスタンスを書き換えます(非バイナリ変数のためのslack決定変数の追加、ペナルティ法による制約の目的関数への吸収)。これまではコンバーターが呼び出し側のインスタンスに対して直接これを行っていたため、同じインスタンスを別用途に使うと挙動が暗黙に変わっていました。v0.12.0ではコンストラクタが入口でbytes経由のディープコピーを取り、コピーに対してto_qubo()を実行するため、呼び出し側のInstanceはそのまま保たれます。
新機能¶
組み込みアルゴリズムとしてのSuzuki–Trotter時間発展¶
qamomile.circuit.algorithm.trotterized_time_evolutionは、H = sum_k hamiltonian[k]に対してexp(-i · gamma · H)をレジスタへ適用します。order引数で公式を選択でき、1はLie–Trotter、2はStrang、任意の正の偶数はSuzukiのフラクタル再帰を意味します。公開ヘルパーは入力を検証し、内部の@qkernelビルディングブロックへ処理を委譲します。具体的なorder bindingの下では、自己再帰するSuzukiステップが展開されるため、出力プログラムに再帰呼び出しは残りません。一方で、外側のTrotter stepループのような通常のループ構造は、バックエンドが対応している場合にはバックエンドのループとして出力されることがあります(#337、#342)。
import qamomile.circuit as qmc
import qamomile.observable as qm_o
from qamomile.circuit.algorithm import trotterized_time_evolution
from qamomile.qiskit import QiskitTranspiler
@qmc.qkernel
def rabi(
Hs: qmc.Vector[qmc.Observable],
gamma: qmc.Float,
order: qmc.UInt,
step: qmc.UInt,
) -> qmc.Vector[qmc.Bit]:
qreg = qmc.qubit_array(1, "q")
qreg = trotterized_time_evolution(qreg, Hs, order, gamma, step)
return qmc.measure(qreg)
# H = (omega/2) Z + (Omega/2) X — 非可換な2項なのでTrotter誤差が観測できる
Hs = [0.5 * 1.2 * qm_o.Z(0), 0.5 * 0.8 * qm_o.X(0)]
transpiler = QiskitTranspiler()
exe = transpiler.transpile(
rabi, bindings={"Hs": Hs, "order": 4, "step": 8, "gamma": 1.0}
)Rabi振動を題材にS_kの収束次数(Δt^{2k})まで検証するエンドツーエンドの解説はチュートリアル07を参照してください。
qamomile.linalg.HermitianMatrix → Hamiltonian¶
新しいqamomile.linalgモジュールは、密な2**n × 2**nのエルミートNumPy配列をFast Walsh–Hadamard変換によりO(n · 4**n)で厳密なパウリHamiltonianに変換します。結果はそのままpauli_evolve、expval、最適化系ヘルパーへ渡せます。対になるHamiltonian.to_numpy()で密行列に戻すこともできます(#336)。
import numpy as np
from qamomile.linalg import HermitianMatrix
X = np.array([[0, 1], [1, 0]], dtype=complex)
Z = np.array([[1, 0], [0, -1]], dtype=complex)
I2 = np.eye(2, dtype=complex)
# 2サイト横磁場Ising: -Z0 Z1 - 0.7 (X0 + X1)
M = -np.kron(Z, Z) - 0.7 * (np.kron(I2, X) + np.kron(X, I2))
H = HermitianMatrix(M).to_hamiltonian()HermitianMatrixは構築時に検証(2次元の正方行列、次元が2の冪、np.allclose(matrix, matrix.conj().T, atol=atol)に従うエルミート性)を行い、通常の+ / - / スカラー* / /の算術をサポートします。qubit 0は計算基底インデックスの最下位ビットに対応し、これはQiskitの内部表現と一致します。エルミート性の判定にはNumPyのデフォルト相対許容も使われるため、行列要素が非常に大きい場合は、atolより大きい非対称性でも相対的に小さければ許容されることがあります。ただし、to_hamiltonian()は非無視な虚数パウリ係数を生成する分解を拒否します。詳しくはチュートリアル08を参照してください。
OMMXラウンドトリップに対応する多態的なdecode()¶
MathematicalProblemConverter.decode()は、コンバーターがどう構築されたかに応じた型を返すようになりました。ommx.v1.Instanceから構築したコンバーターは、元の(ペナルティ未適用の)インスタンスに対して評価されたommx.v1.SampleSetを返します。これにより、実行可能性、目的関数値、制約ごとの違反量をOMMX自身のAPI(.summary / .summary_with_constraints / .best_feasible / .feasible / .objectives)からそのまま扱えます。BinaryModelから構築したコンバーターはこれまで通りBinarySampleSetを返します(#353)。
新しいdecode_to_binary_sampleset()は、QUBOドメインのBinarySampleSetを必要とする呼び出し側のための公開エスケープハッチです。例えば、ペナルティ込みのenergyを古典オプティマイザのコストとして使いたい場合に利用します。decode()は内部的にこのメソッドを呼び、ommx.v1.Instanceから構築したコンバーターでは結果をOMMXのevaluate_samplesを通して返します。
具体的なQRACコンバーター(QRAC21Converter、QRAC31Converter、QRAC32Converter、QRACSpaceEfficientConverter)のdecode(rounded_spins_list)も同じ規約に従います。QRACで丸めたスピン割り当ては合成されたSampleResultに詰め直され、基底クラスの多態的なdecodeを経由するため、QAOA / FQAOA / QRAOは同じ後段APIを共有します。ommx.v1.Instanceから構築したコンバーターは繰り返し呼び出しても安全になりました — Instanceはコンストラクタでディープコピーされる(破壊的変更を参照)ため、呼び出し側のインスタンスはそのまま保たれます。
from qamomile.optimization.qaoa import QAOAConverter
from qamomile.qiskit import QiskitTranspiler
# ommx_instance、gammas、betasはチュートリアルと同様に準備済みとします。
transpiler = QiskitTranspiler()
converter = QAOAConverter(ommx_instance) # ommx.v1.Instanceから構築
exe = converter.transpile(transpiler, p=2)
# sample_resultはexe.sample(...).result()で得る前提(チュートリアル参照)
sample_set = converter.decode(sample_result) # ommx.v1.SampleSet
print(sample_set.best_feasible.objective)
# 古典オプティマイザ向けにQUBOドメインのenergyが欲しい場合は
binary_ss = converter.decode_to_binary_sampleset(sample_result)両方の戻り値を使うエンドツーエンドな例は、更新されたgraph
内部的な変更¶
上のTrotter機能は、コンパイラ・フロントエンドに加えた一連の改善の動機になりました。trotterized_time_evolutionを自然なPythonとして書くために直接必要だったものもあれば、同じコンパイル時制御フローの経路を堅牢にする中で入ったものもあります。いずれも単独で利用できます。
自己再帰@qkernel¶
@qkernelが自分自身を直接呼び出せるようになりました。トランスパイラは、与えられたbindingsの下でinline + partial-evalパスを反復することで自己再帰呼び出しを解決するため、再帰を駆動する条件がbase caseへ畳み込まれた後の出力プログラムには、再帰的なCallBlock構造が残りません(#337)。
import qamomile.circuit as qmc
import qamomile.observable as qm_o
from qamomile.qiskit import QiskitTranspiler
@qmc.qkernel
def suzuki_step(
q: qmc.Vector[qmc.Qubit],
hamiltonian: qmc.Vector[qmc.Observable],
order: qmc.UInt,
dt: qmc.Float,
) -> qmc.Vector[qmc.Qubit]:
if order == 2:
# Strangのベースケース: Hamiltonian項に対する回文的な掃引
last = hamiltonian.shape[0] - 1
half_dt = 0.5 * dt
for i in qmc.range(last):
q = qmc.pauli_evolve(q, hamiltonian[i], half_dt)
q = qmc.pauli_evolve(q, hamiltonian[last], dt)
for j in qmc.range(last):
rev = last - 1 - j
q = qmc.pauli_evolve(q, hamiltonian[rev], half_dt)
else:
# Suzuki再帰: S_{2k}は5つのS_{2k-2}ブロックから構成
p = 1.0 / (4.0 - 4.0 ** (1.0 / (order - 1)))
w = 1.0 - 4.0 * p
q = suzuki_step(q, hamiltonian, order - 2, p * dt)
q = suzuki_step(q, hamiltonian, order - 2, p * dt)
q = suzuki_step(q, hamiltonian, order - 2, w * dt)
q = suzuki_step(q, hamiltonian, order - 2, p * dt)
q = suzuki_step(q, hamiltonian, order - 2, p * dt)
return q
@qmc.qkernel
def suzuki_demo(
hamiltonian: qmc.Vector[qmc.Observable],
order: qmc.UInt,
dt: qmc.Float,
) -> qmc.Vector[qmc.Bit]:
# suzuki_stepはレジスタを受け取るので、レジスタごと測定して返す
qreg = qmc.qubit_array(1, "q")
qreg = suzuki_step(qreg, hamiltonian, order, dt)
return qmc.measure(qreg)
Hs = [qm_o.Z(0), qm_o.X(0)]
transpiler = QiskitTranspiler()
exe = transpiler.transpile(
suzuki_demo, bindings={"hamiltonian": Hs, "order": 4, "dt": 0.3}
)Why: SuzukiのフラクタルS_{2k}(Δt) = S_{2k-2}(p_k Δt)² · S_{2k-2}((1-4p_k)Δt) · S_{2k-2}(p_k Δt)²は、order - 2で自分自身を呼び出すカーネルとして書くのが最も自然です。展開ループがなければ、この再帰はコンパイル時に終了できません。
order(あるいは再帰深さを決める他のパラメータ)はトランスパイル時にコンパイル時定数へバインドされている必要があります。そうでないとベースケースのifが畳み込めず、展開ループが停止条件を持てません。
Vector[Observable]パラメータ型¶
pauli_evolveがobservableパラメータを受け取れるようになり、@qkernelはトランスパイル時にバインドされるVector[qmc.Observable]をパラメータ型として受け付けるようになりました(#340)。
import qamomile.circuit as qmc
import qamomile.observable as qm_o
from qamomile.qiskit import QiskitTranspiler
@qmc.qkernel
def step(
Hs: qmc.Vector[qmc.Observable],
dt: qmc.Float,
) -> qmc.Vector[qmc.Bit]:
qreg = qmc.qubit_array(1, "q")
for i in qmc.range(Hs.shape[0]):
qreg = qmc.pauli_evolve(qreg, Hs[i], dt)
return qmc.measure(qreg)
# 項リストをトランスパイル時にバインド
QiskitTranspiler().transpile(
step, bindings={"Hs": [qm_o.Z(0), qm_o.X(0)], "dt": 0.3}
)Why: TrotterステップはH = sum_k H_kの各部分Hamiltonian H_kを順に処理する必要があるため、項リストはカーネルにパラメータとして入る必要があります。Vector[Observable]がない場合、特定のHをカーネルのソースに直接埋め込み、Hamiltonianごとに再トレースしなければなりません。
boolパラメータ型¶
@qkernelは既存のスカラー型ハンドルと並んでboolパラメータも受け付けるようになりました(#346)。
import qamomile.circuit as qmc
from qamomile.qiskit import QiskitTranspiler
@qmc.qkernel
def maybe_h(q: qmc.Qubit, do_it: bool) -> qmc.Qubit:
if do_it:
q = qmc.h(q)
return q
@qmc.qkernel
def maybe_h_entry(do_it: bool) -> qmc.Bit:
q = qmc.qubit(name="q")
q = maybe_h(q, do_it)
return qmc.measure(q)
# boolフラグをトランスパイル時にバインド
QiskitTranspiler().transpile(maybe_h_entry, bindings={"do_it": True})Why: Trotterの作業を通じて、@qkernel内のコンパイル時に解決可能な条件を持つif文の扱いを強化しました。ifが安定したことで、boolをパラメータ型として受け入れることがこの拡張の自然な完成形になりました。boolはifの条件が最も素直に欲しがる型であり、二級市民のままにしておく理由はありません。
MLIRスタイルのIR pretty-printer¶
qamomile.circuit.ir.pretty_print_blockは、パイプラインの任意の段階(HIERARCHICAL / AFFINE / ANALYZED)でBlockを人間が読める形式でダンプします。トランスパイラのデバッグに有用です。上の項目とは異なり、これはTrotter機能の一部ではありません — 新しいコンパイルチュートリアル(チュートリアル09)と同時にリリースされ、そこではパイプラインを読者に見せるために使われています。出力は人間が読むためのもので、リリース間で変わる可能性があります。
import qamomile.circuit as qmc
from qamomile.circuit.ir import pretty_print_block
from qamomile.qiskit import QiskitTranspiler
@qmc.qkernel
def ghz(n: qmc.UInt) -> qmc.Vector[qmc.Bit]:
q = qmc.qubit_array(n, "q")
q[0] = qmc.h(q[0])
for i in qmc.range(1, n):
q[0], q[i] = qmc.cx(q[0], q[i])
return qmc.measure(q)
block = QiskitTranspiler().to_block(ghz, bindings={"n": 3})
print(pretty_print_block(block))バグ修正¶
pauli_evolveがSSAのlogical_idを保持するようになりました(#354)。pauli_evolveは結果として新しいArrayValueを作っており、他のゲート操作のようにnext_version()で入力レジスタのSSAバージョンをインクリメントしていなかったため、logical_idの連鎖が切れていました。ユーザー側の症状としては、pauli_evolveを内部で使う@qkernel(例えばtrotterized_time_evolution)を別の@qkernelから呼び出し、外側でqmc.measure(qreg)を実行すると、測定が静かに落ちてsample()の結果が[(None, count), ...]、clbit_mapが空、という状態になっていました。回避策としてqmc.measure(qreg[0])のように単一要素を取り出す書き方が知られていましたが、これは不要になり、上の例も自然なqmc.measure(qreg)形式で書けるようになっています。あわせてinlineパス側でも、callee側のshape-dim UUIDを外側のvalue_mapに伝播するように修正されたため、リソースアロケータが呼び出し後のレジスタサイズを正しく解決できます。Transpiler.transpile()がbindingsとparametersの重複を拒否するようになりました(#359)。同じ名前をbindingsとparametersの両方に渡すのは曖昧(プレースホルダなのかランタイム記号なのか)であり、パラメータ配列の要素に依存する制御フロー述語をサイレントに誤コンパイルすることがありました。トランスパイラはこの重複を入口でValueErrorとして弾きます。内部的には、BinOp/CompOp/CondOp/NotOpを畳み込む3つの箇所が共通のeval_utils.fold_classical_opヘルパーを使うようになり、ランタイムパラメータのガードが呼び出し側ごとにずれることがなくなりました。可視化の改善: Toffoli (CCX)はCXスタイルの制御点とターゲットXで描画されます。畳み込まれたループブロックの参加ワイヤーは制御点スタイルのドットでマークされ、
Sdg/TdgにはTeXラベルが付くようになりました。CallBlockラベル中のBinOp引数が展開され、IR内部のプレースホルダ名(?、tmp名)が描画ラベルに漏れることがなくなりました。ループ影響解析がネストした制御フローを正しく扱うようになり、配列を
CallBlockに渡すforループでも全てのqubitが検出されるようになりました。Hamiltonian.to_numpy()の高速化: パウリ行列をモジュールレベルにホイストすることで、呼び出しごとの再構築を避けるようになりました。
ドキュメント¶
新規チュートリアル07 — ハミルトニアンシミュレーション(Rabi模型へのSuzuki–Trotter)。
新規チュートリアル08 — エルミート分解(
HermitianMatrix.to_hamiltonian()をエンドツーエンドで解説)。新規チュートリアル09 — コンパイルとトランスパイル。パイプラインをより深く解説し、QFixed測定のlowering事例も含みます。
新規チュートリアル10 — 量子誤り訂正(Shor 9-qubit符号、syndrome測定)(#352)。
新規チュートリアル11 — Steane符号(
[[7,1,3]]CSS符号、transversalなHadamard)(#352)。新規水素分子に対するVQEチュートリアルを
vqa/に追加(#318)。MaxCutチュートリアルをスピン変数を起点とする構成に書き直し、
AerSimulatorとNumPyにシードを与えて再現性に関する注意書きも明示するように改善しました(#356)。インデックスページにカモミールの由来と発音ガイドを追記しました。