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.0

Qamomile v0.12.0は、組み込みのSuzuki–Trotter時間発展(order 1, 2、および任意の偶数order ≥ 4)、密なエルミート行列を厳密なパウリHamiltonianに変換する新しいqamomile.linalgモジュール、そしてこれらのアルゴリズムを自然なPythonとして表現するためのコンパイラ機能(自己再帰@qkernelVector[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

破壊的変更

新機能

組み込みアルゴリズムとしての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.HermitianMatrixHamiltonian

新しいqamomile.linalgモジュールは、密な2**n × 2**nのエルミートNumPy配列をFast Walsh–Hadamard変換によりO(n · 4**n)で厳密なパウリHamiltonianに変換します。結果はそのままpauli_evolveexpval、最適化系ヘルパーへ渡せます。対になる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コンバーター(QRAC21ConverterQRAC31ConverterQRAC32ConverterQRACSpaceEfficientConverter)の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-partitionチュートリアルを参照してください。

内部的な変更

上の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をパラメータ型として受け入れることがこの拡張の自然な完成形になりました。boolifの条件が最も素直に欲しがる型であり、二級市民のままにしておく理由はありません。

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))

バグ修正

ドキュメント

さらに詳しく