🧁

Qualtranでリソース推定してみる

に公開

この記事はJij Advent Calendar 2025の2日目の記事です.

この数年で随分とFTQCという言葉が身近に感じるようになってきました.すると,想定しているアルゴリズムを実行するのがどの程度大変か,いつ頃なら実行できそうかということが知りたくなります.誤り耐性量子計算では,実行しやすいゲートもあれば,するのが大変なゲートもあります.これまでの多くの理論的な解析では主に計算量に着目していましたが,計算量の陰に隠されたコストや実行しにくいゲートがどの程度入っているかということを把握しなければ実行時期の見積もりをすることはできません.このような計算コストの見積もりをリソース推定と呼んだりします.

このリソース推定のための機能が最近は様々なライブラリーに入ってきています.有名なところだと,Pennylaneにも最近入りました.この話についても別の機会にしたいですが,今回は,QualtranというGoogle Quantum AIが開発しているライブラリーの紹介をしたいと思います.

今回の記事は,少し試してみたというものなので説明には間違いもあると思いますし,ライブラリーも頻繁に更新されていきますので,詳細はドキュメントを確認いただければと思います.ちなみに,今回はGithubのmainブランチから最新版を使用しています.

Bloqを自作する

Qualtranで最も重要な構成要素はBloqです.Bloqとは量子回路におけるサブルーチンを表すものです.Qualtranでは,Bloqs Libraryの中に多様なBloqがすでに用意されているので,それを用いて自分が興味ある問題に適用することができます.ですが,ここでは機能の説明のためにあえて自分でBloqを作ってみましょう.

最初はこのBloqをどのように定義するかを見てみます.Qualtranの最初のチュートリアルにある例ですが,CNOTゲートを自分で書いてみましょう.CNOTゲートは制御ビットが0なら何もしないで,1ならばターゲットとなる量子ビットにXを作用するというものです.最初のビットを制御ビットとし.2つ目のビットをターゲットビットとすると作用は

CNOT\ket{x}\ket{y} = \ket{x}\ket{y\oplus x}

と書くことができます.

ここではBloqを定義するための最小の機能である,signatureだけ書いてみましょう.signatureは関数シグネチャと同じような意味で使われていて,このBloqに何が入って出ていくかを定義します.

import attrs
import qualtran as qlt

@attrs.frozen
class MyCNOT(qlt.Bloq):
    @property
    def signature(self):
        return qlt.Signature([
            qlt.Register('control', qlt.QBit()),
            qlt.Register('target', qlt.QBit()),
        ])

BloqのsignatureはSignatureを返さないといけないですが,これは単純にRegisterのリストとなっています.Registerは量子情報のinput/outputを表現しています.ここではQBit()を用いていますが,必ずしも量子ビットを直接定義する必要はなく,以下のように

qlt.Register('x', qlt.QFxp(bitsize=8, num_frac=6, signed=False), side=qlt.Side.LEFT)

浮動小数点も表現することができます.qlt.QFxp(bitsize=8, num_frac=6, signed=False)はビット幅が8で小数点のビット数が6の浮動小数点を表しており,量子ビット数としてはbitsize分の量子ビットが確保されます.このように,量子ビット以外にも様々な数値型がQualtranでは用意されています.

最低限このシグネチャのみを定義しておけばいいというのは,Qualtranの面白い特徴だと思います.この例だとCNOTがどのように動くかというのを知っているので,それを実装しないのかという点に疑問が湧くかもしれませんが,リソース推定ではBloqを何回使用しているか,という点が興味事項なので必ずしもBloqの動作の詳細を記述する必要はありません.

さて,CNOTの例では2つの量子ビットを受け取って返しています.Qualtranではこのような情報をグラフとして保持しており,show_bloqで可視化することができます.

from qualtran.drawing import show_bloq
cnot = MyCNOT()
show_bloq(cnot)

cnot

Qualtranの面白い機能として,量子計算の具体的な実装を書くだけでなく,古典的な実装のテストのためにある古典ビットが入った時にどういう古典ビットが返るのかという機能をBloqに実装することができます.ただ,これ自体は量子回路を実行しているわけではないので,ここに入ってきたものはこのように動作することを期待するというということを表しているだけであることに注意しましょう.

from qualtran.simulation.classical_sim import get_classical_truth_table, format_classical_truth_table

@attrs.frozen
class MyCNOT(qlt.Bloq):
    @property
    def signature(self):
        return qlt.Signature([
            qlt.Register('x', qlt.QBit()),
            qlt.Register('y', qlt.QBit()),
        ])
    
    def on_classical_vals(self, x, y):
        return {"x": x, "y": (x + y) % 2}


cnot = MyCNOT()
print(format_classical_truth_table(*get_classical_truth_table(cnot)))
x  y  |  x  y
--------------
0, 0 -> 0, 0
0, 1 -> 0, 1
1, 0 -> 1, 1
1, 1 -> 1, 0

確かに,CNOTで想定される挙動になっていることがわかります.

Bloqを接続していくことで,より大きなBloqを作っていくことができます.例えば,CNOTを3つくっつけてSWAPゲートを作ってみましょう.

class MySwap(qlt.Bloq):
    @property
    def signature(self):
        return qlt.Signature([
            qlt.Register('x', qlt.QBit()),
            qlt.Register('y', qlt.QBit()),
        ])

    def build_composite_bloq(self, bb: qlt.BloqBuilder, *, x, y):
        x, y = bb.add(MyCNOT(), x=x, y=y)
        y, x = bb.add(MyCNOT(), x=y, y=x)
        x, y = bb.add(MyCNOT(), x=x, y=y)
        return {'x': x, 'y': y}

現在あるBloqを組み合わせてアルゴリズムを作る場合には,build_composite_bloqを定義します.ここでは,signatureで定義したinputがどのように入ってきて,どのような操作を経て出ていくかを定義しています.SWAPゲートではCNOTを3つ接続するので,CNOTを3回bb.addしています.
そして,最後は出ていく量子ビットの情報をまとめて返しています.

正しく実装できているか古典的に調べてみましょう.MyCNOTは古典的な挙動を実装しているので,それを接続しただけのMySwapも同様のことを行うことができます.

swap = MySwap()
print(format_classical_truth_table(*get_classical_truth_table(swap)))
x  y  |  x  y
--------------
0, 0 -> 0, 0
0, 1 -> 1, 0
1, 0 -> 0, 1
1, 1 -> 1, 1

確かに,swapができていますね.ここで作ったMySwapを分解して構成要素を調べることもできます.これはdecompose_bloq()メソッドで行うことができます.

show_bloq(swap.decompose_bloq())

swap

量子位相推定を実装してみる.

簡単な実装の方法を見たので,次はもう少しリソースを推定したくなる例を組んでみましょう.ここでは例として量子位相推定(Quantum Phase Estimation, QPE)を実装してみます.

量子位相推定は,多くの量子アルゴリズムのサブルーチンとして用いられるアルゴリズムです.ユニタリー演算子Uとその固有状態\ket{u}が与えられた時に,その固有値U\ket{u} = e^{2\pi i \theta}\ket{u}という関係を持ちます.量子位相推定はこの時の位相\thetaを推定します.つまり,大まかには以下のようなことを行うアルゴリズムです.

\mathrm{QPE}(U)\ket{0}\ket{u} = \ket{\theta}\ket{u}

ここでは\mathrm{QPE}(U)はユニタリー演算子Uに対するQPEということを表しています.最初の量子ビット\ket{0}と準備した部分を位相レジスター,\ket{u}をターゲットレジスターとこの例では呼ぶことにします.

このアルゴリズムには色々な工夫が施された様々なバージョンが存在しますが,今回は教科書に載っているような以下の量子回路を考えてみましょう.
qpe_circ

位相レジスターに対してアダマールゲートを作用させた後に,制御ユニタリーを作用させていき,最後に量子逆フーリエ変換を作用させます.QPEでは,ターゲットレジスターのサイズは,入れられたユニタリー演算子の情報から決めるようにしましょう.また,位相レジスターのサイズによって精度が変化するので,これも指定できるようにしておきましょう.

from typing import Dict
import cirq
from qualtran.cirq_interop import CirqGateAsBloq
from qualtran.bloqs.basic_gates import Hadamard
from qualtran.bloqs.qft import QFTTextBook

@attrs.frozen
class MyQPE(qlt.Bloq):
    base_gate: cirq.Gate
    m_bits: int

    @property
    def _n_target(self) -> int:
        return cirq.num_qubits(self.base_gate)

    @property
    def signature(self) -> qlt.Signature:
        return qlt.Signature([
            qlt.Register("qpe_reg", qlt.QFxp(self.m_bits, self.m_bits), side=qlt.Side.RIGHT),
            qlt.Register("q", qlt.QBit(), shape=(self._n_target,), side=qlt.Side.THRU),
        ])

    def build_composite_bloq(self, bb: qlt.BloqBuilder, *, q : qlt.Register) -> Dict[str, qlt.Register]:

        qpe_reg = bb.allocate(n=self.m_bits, dtype=qlt.QFxp(self.m_bits, self.m_bits))
        phase_bits = list(bb.split(qpe_reg))
        for i in range(self.m_bits):
            phase_bits[i] = bb.add(Hadamard(), q=phase_bits[i])

        for i in range(self.m_bits):
            k = self.m_bits - 1 - i
            powered_gate = self.base_gate ** (2**k)
            c_gate = powered_gate.controlled(num_controls=1)
            cu_bloq = CirqGateAsBloq(c_gate)

            q_in = [phase_bits[i], *list(q)]
            q_out = bb.add(cu_bloq, q=q_in)

            phase_bits[i], q = q_out[0], q_out[1:]

        qpe_reg = bb.join(phase_bits, dtype=qlt.QFxp(self.m_bits, self.m_bits))

        iqft = QFTTextBook(self.m_bits).adjoint()
        qpe_reg = bb.add(iqft, q=qpe_reg)

        return {"qpe_reg": qpe_reg, "q": q}

ここでは,定義通りSignatureとして位相レジスターとターゲットレジスターの二つを用意しています.位相レジスターでは,最初は初期状態\ket{0}からスタートしているので,このQPEの中でこのレジスターを取ることとしています.これを表すのが,side = qlt.Side.RIGHTです.Registerのデフォルトは,量子ビットが入って出ていくということを表すqlt.Side.THRUですが,出ていくだけ(右側だけが空いている)ということを表すのはqlt.Side.RIGHTです.

今回はユニタリー演算子としては,cirqのゲートを受け取って,それを制御ユニタリーのBloqにしています.QFTも自分で実装した方が例としてはいいのですが,ここではQualtranの中に元々入っているqft.QFTTextBookを使っています.

さて,回路ができたので実際に動かしてみましょう.Qualtranで作った回路は(分解できるのならば)cirqで実行することができます.

ここでは一番簡単な例として,U = e^{-i\phi Z}というユニタリー演算子を考えます.この固有状態\ket{1}を使って\phiをQPEで推定しましょう.今回は,\sqrt{2}/10という値を\phiとして指定して,位相レジスターのサイズを変化させた時の精度を調べてみましょう.

import numpy as np
import matplotlib.pyplot as plt
from qualtran._infra.gate_with_registers import get_named_qubits

phi = np.sqrt(2) / 10

m_bits = 10

def run_qpe_simulation(phi, m_bits):
    U = cirq.ZPowGate(exponent=2 * phi)
    qpe = MyQPE(base_gate=U, m_bits=m_bits)
    cbloq = qpe.as_composite_bloq()

    in_quregs = get_named_qubits(cbloq.signature.lefts())
    frozen_circuit, quregs = cbloq.to_cirq_circuit_and_quregs(**in_quregs)

    target_qubits = np.asarray(in_quregs["q"], dtype=object).ravel().tolist()
    phase_qubits = np.asarray(quregs["qpe_reg"], dtype=object).ravel().tolist()

    c = cirq.Circuit(
        cirq.X.on_each(*target_qubits),
        frozen_circuit,
        cirq.measure(*phase_qubits, key="phase"),
    )
    sim = cirq.Simulator()
    result = sim.run(c, repetitions=500)

    bits = result.measurements["phase"]
    keys = [cirq.big_endian_bits_to_int(row) for row in bits]
    hist = {k: keys.count(k) for k in set(keys)}

    top = max(hist.items(), key=lambda kv: kv[1])
    phi_qpe = top[0] / (2**m_bits)
    return hist, phi_qpe

qpe_estimaions = []
for m_bits in range(5,12):
    _, phi_qpe = run_qpe_simulation(phi, m_bits)
    qpe_estimaions.append(phi_qpe)

plt.plot(range(5,12), qpe_estimaions, marker='o')
plt.hlines(y=phi,xmin=5,xmax=11, color='r', linestyle='--', label='True φ')
plt.xlabel('Number of bits (m)')
plt.ylabel('Estimated φ from QPE')

qpe

確かに,QPEの結果を格納する位相レジスターのサイズをおきくしていくことで,精度よく結果を求められるようになっていることがわかります.

しかし,このようにただ回路を実行させるだけでは普通の量子計算用のSDKと変わりません.一番やりたかったことは量子回路を実行することではなく,計算の実行に必要なリソースを推定推定することです.次は,リソースの推定をしてみましょう.

まずは,先ほどと同じ設定でリソースを調べてみましょう.基本的な情報はt_complexityメソッドからとることができます.T-Gateの数,Cliffordゲートの数,回転ゲートの数を取ることができます.さらに,t_incl_rotationsを使うことで,回転ゲートを分解した時のT-Gateの数を見積もることもできます.

m_bits = 10
U = cirq.ZPowGate(exponent=2 * phi)
myqpe = MyQPE(base_gate=U, m_bits=m_bits)

myqpe_t = myqpe.t_complexity()
print("T-Gates: ", myqpe_t.t)
print("Rotations: ", myqpe_t.rotations)
print("Cliffords: ", myqpe_t.clifford)
print("T-count (including rotations): ", myqpe_t.t_incl_rotations(eps=1e-11))
T-Gates:  188
Rotations:  38
Cliffords:  619
T-count (including rotations):  2164

この回路は古典計算機で実行できるので,リソースを測ってもそれほど面白くありません.

続いて,実際には実行できないような例を試してみましょう.本当はユニタリーゲートとして複雑なものをとってくるのがいいとは思いますが,ここでは位相レジスターのビット数を100として必要なリソースを調べてみます.

m_bits = 100
U = cirq.ZPowGate(exponent=2 * phi)
myqpe = MyQPE(base_gate=U, m_bits=m_bits)

myqpe_t = myqpe.t_complexity()
print("T-Gates: ", myqpe_t.t)
print("Rotations: ", myqpe_t.rotations)
print("Cliffords: ", myqpe_t.clifford)
print("T-count (including rotations): ", myqpe_t.t_incl_rotations(eps=1e-11))
T-Gates:  19898
Rotations:  2976
Cliffords:  66576
T-count (including rotations):  174650

このように実行ができない巨大な回路でも必要なリソースを調べることができます.

Bloqライブラリーを使う

さて,ここまではほぼ手で回路を組んでいたので,Qualtranの利点を感じることはなかったかもしれません.Qualtranの良いところはBloq Libraryが充実しているところです.実際,今回例に出したCNOT, SWAP, QPEも全てBloq Libraryの中に入っています.抽象度の高いBloqを組み合わせて自分の解きたい問題のリソースを推定できるというのが,Qualtranの良いところです.その上で,自分で実装の工夫したい部分のみを作り込んでいけばいいのです.なので,次にこのBloq Libraryを使ってQPEのリソースを推定するということを行なってみたいと思います.

せっかくなので,Qualtranの面白い機能をもう一つ紹介しましょう.先ほどの例ではリソースを推定するときに,使用する量子ビット数を指定してから,どれくらいのリソースが必要か見積もっていました.一方で,論文などでは量子ビットがmビット使っていたとすると,Tゲートのリソースはf(m)であるみたいな形で書かれることがあります.実際,必要なリソースは量子ビット数を増やしていくと,どのように増えていくのか知りたいという場面もあると思います.実は,Qualtranではこのような抽象的な形のリソース推定も行うことができます.

Bloqライブラリーには,いくつかQPEが入っていますがここではTextbookQPEを使用します.TextbookQPEはユニタリー演算子だけでなく,初期状態用意のユニタリー,QFTのアルゴリズムも指定することができます.ここでは,先ほどと同じ,アダマールゲートで初期状態を作り,教科書的な逆量子フーリエ変換を使用することにしましょう.

import sympy

from qualtran.bloqs.basic_gates import ZPowGate
from qualtran.bloqs.phase_estimation import RectangularWindowState, TextbookQPE
from qualtran.bloqs.qft import QFTTextBook

theta = sympy.Symbol('theta')
m_bits = sympy.Symbol('m')
textbook_qpe = TextbookQPE(
    unitary = ZPowGate(exponent=2 * theta), ctrl_state_prep= RectangularWindowState(m_bits), qft_inv= QFTTextBook(m_bits)
)

今回は具体的な量子ビット数ではなくsympyを用いて量子ビット数を表現しています.先ほどと同様にTゲート数を調べてみましょう,

textbook_qpe_t = textbook_qpe.t_complexity()
print("T-Gates: ", textbook_qpe_t.t)
print("Rotations: ", textbook_qpe_t.rotations)
print("Cliffords: ", textbook_qpe_t.clifford)

print("T-count (including rotations): ", textbook_qpe_t.t_incl_rotations(eps=1e-11))
T-Gates:  4*2**m + 4*(m - 1)*floor(m/2) - 4
Rotations:  2**m + (m - 1)*floor(m/2) - 1
Cliffords:  13*2**m + 2*m + 10*(m - 1)*floor(m/2) + (3*m - 3)*floor(m/2) + floor(m/2) - 13
T-count (including rotations):  ceiling(56*2**m + 56*(m - 1)*floor(m/2)) - 56

このように,Qualtranではsympyを使うことでリソースの依存を関数形として求めることができます.しかもこの数式はjupyter notebookではLaTeX表記もしてくれます.このmに具体的な値を代入をしたければsubsを使うことができます.

textbook_qpe_t.t.subs({"m": 10})
4272

確かに,数値としても得ることができました.

さて,QualtranのBloq Libraryを使って求めたリソースは,自作したQPEに比べて20倍以上大きい値となっています.これは一体どうしてでしょうか?実は,これは今回の問題設定とQPEの実装の仕方に起因するものです.通常QPEで最も重い計算は指数回のユニタリー演算子を用意する部分です.しかし,今回の問題ではユニタリー演算子としてZ回転を持ってきています.そのため,このユニタリー演算子の指数回の作用というのは

(e^{-i\phi Z})^n = e^{-in\phi Z}

のように書くことができます.そのため,結局一回の回転演算子で書くことができます.自作のQPEではcirqのZPowGateは冪乗の計算がこのように処理されるようになっています.このように指数の演算を簡単化できるものをfast-forwardableと言ったりします.この場合には計算コストが安く済みますが,ユニタリー演算子を指数回作用させないといけないとすると,大きなコストがかかってしまいます.QualtranのBloq Libraryを使用した場合には,ユニタリー演算がfast-forwardableではないとしてリソースが推定されている可能性が高いです.ひょっとしたら,fast-forwardableの設定がどこかでできる可能性がありますが,私はわからなかったので知っている方がいたら教えてください.

まとめ

今回はQualtranの基本的な使い方とQPEを使ったリソース推定についてまとめました.Qualtranにはsurface codeに関連する機能があったりテンソルネットワークを実行する機能があったりと紹介できなかった機能があるので,次の機会にはそれも調べてみたいです.また,もう少し複雑なアルゴリズムを実装してリソース推定もしてみたいです.

最近は,色々な量子計算用のSDKが出てきていて,ソフトウェア的にも非常に楽しいですね.Pennylaneのリソース推定機能や,guppyとかも紹介できればと思います.

最後に

Jijでは各ポジションを積極的に採用しています!
現在の募集職種は、以下リンクよりご覧いただけます。
カジュアル面談からのスタートも大歓迎ですので、お気軽にご連絡ください。
https://open.talentio.com/r/1/c/j-ij.com/homes/1900

GitHubで編集を提案

Discussion