簡単に量子プログラミングが始められるBlueqatライブラリ
を開発しています。
https://github.com/Blueqat/Blueqat
前回
Blueqatのバックエンドを作る 〜 その1では、OpenQASMの入力を受け入れられるシミュレータのバックエンドの作り方を説明しました。
今回は、そうでない一般の場合のバックエンドの作り方を説明します。
全部自分で作る方法
最低限、Backendを継承して、runメソッドを実装すれば、バックエンドが作れます。
また、バックエンドオブジェクト自体のコピーが、copy.deepcopyでは都合が悪い場合は、copyメソッドも再実装します。
runメソッドはrun(self, gates, n_qubits, *args, **kwargs)の形になっています。
gatesは、量子ゲートのオブジェクトのリスト、n_qubitsは、量子ビット数、*args, **kwargsには、ユーザがCircuit().run()を呼び出したときのrunの引数が渡されます。
これさえ実装すれば、バックエンドはできるのですが、さすがに開発者に丸投げしすぎているので、次のようなフローを用意しています。
ややこしそうに思えますが、フローに乗っかれば、実装をやや楽にすることができます。
また、runメソッドを再実装する際も、用意しているものを流用してもいいかもしれません。
用意しているフローに乗っかる方法
全体的な流れ
デフォルトでは、runメソッドを呼ぶと、以下のコードが呼び出されます。
    def _run(self, gates, n_qubits, args, kwargs):
        gates, ctx = self._preprocess_run(gates, n_qubits, args, kwargs)
        self._run_gates(gates, n_qubits, ctx)
        return self._postprocess_run(ctx)
    def _run_gates(self, gates, n_qubits, ctx):
        """Iterate gates and call backend's action for each gates"""
        for gate in gates:
            action = self._get_action(gate)
            if action is not None:
                ctx = action(gate, ctx)
            else:
                ctx = self._run_gates(gate.fallback(n_qubits), n_qubits, ctx)
        return ctx
バックエンド開発者が実装すべきものは
- 
_preprocess_runメソッド
- 各ゲートごとのaction
- 
_postprocess_runメソッド
です。
ctxについて
ここでctxと書かれている変数に注目してください。
この変数は、runが始まってから終わるまでの状態を保持するための変数です。(不要ならばNoneで構わないですが、全く不要となるケースは少ないと思います)
バックエンド自体も普通のオブジェクトなのでself.foo = ...などとすれば状態を持たせられますが、バグ等の原因になりますので、できるだけctxを使ってください。
ctxに保存するものの一例として、
- 量子ビット数
- 計算途中の状態ベクトル
- 
runメソッドの入力オプションや、それをパースしたもの
- その他、作るのに時間がかかるものや、状態を持っているもの
などがあります。
(量子ビット数をctxに残しておかないと、実行中には分からないことについては、注意が必要です)
また、ctxに注目して上のコードを見ると
- 
_preprocess_runでctxオブジェクトを作る
- 各ゲートごとのactionを呼び出す際、ctxオブジェクトが渡される。また、actionはctxオブジェクトを返すよう実装することが期待されている
- 
_postprocess_runはctxを受け取りrunの結果を返す
という流れになっています。
actionの定義
量子回路は、ゲートを並べることで作られています。
典型的なバックエンドの実装方法として、並んでいるゲートを順次適用していく、というものがあります。
ゲートを適用する操作をここではactionと言っています。
actionを実装するには、単にメソッドを追加すればよく、
def gate_{ゲート名}(self, gate, ctx):
    # なにか実装
    return ctx
のようにします。
Blueqatの特徴として、Circuit().h[:]のようなスライス表記がありましたが、スライス表記をバラすためのメソッドも用意していて、for idx in gate.target_iter(量子ビット数)のようにすると1量子ビットゲートのインデックスがとれます。また、CNOTなどの2量子ビットゲートのインデックスはfor c, t in gate.control_target_iter(量子ビット数)のようにします。
ここで量子ビット数が必要なのはCircuit().h[:]などのようにしたとき、何量子ビット目まで適用するかを知るためです。
定義が必要なaction
全部のゲートは実装する必要がありません。
例えば、TゲートやSゲートは、回転Zゲート(RZ)があれば作ることができるので、Blueqat側で、そういったゲートについては、実装されていなければ別のゲートを代わりに使うように面倒を見ています。
また、代わりのゲートがないと、もし使われた場合はエラーになりますが、使われなかった場合はエラーにならないので、一部のゲートのみをサポートしたバックエンドを作っても構いません。(たとえばXゲート、CXゲート、CCXゲートのみを実装すると、古典の論理回路に特化したバックエンドを作れます。ただのビット演算で実装できるので高速に動作するものが作成できます)
実装が必要なゲートは今後整理するかもしれませんが、現状は
X, Y, Z, H, CZ, CX, RX, RY, RZ, CCZ, U3と測定です。(測定も、ゲートと同じようにactionを定義します)
実装例を見る
やや変わり種のバックエンドにQasmOutputBackendがあります。
これは、シミュレータではなく、Blueqatの量子回路をOpenQASMに変換するためのバックエンドです。
ctxには、OpenQASMの行のリストと量子ビット数を保持します。
また、各actionでは、リストに行を追加していきます。
コード全体はこちら。
class QasmOutputBackend(Backend):
    """Backend for OpenQASM output."""
    def _preprocess_run(self, gates, n_qubits, args, kwargs):
        def _parse_run_args(output_prologue=True, **_kwargs):
            return { 'output_prologue': output_prologue }
        args = _parse_run_args(*args, **kwargs)
        if args['output_prologue']:
            qasmlist = [
                "OPENQASM 2.0;",
                'include "qelib1.inc";',
                f"qreg q[{n_qubits}];",
                f"creg c[{n_qubits}];",
            ]
        else:
            qasmlist = []
        return gates, (qasmlist, n_qubits)
    def _postprocess_run(self, ctx):
        return "\n".join(ctx[0])
    def _one_qubit_gate_noargs(self, gate, ctx):
        for idx in gate.target_iter(ctx[1]):
            ctx[0].append(f"{gate.lowername} q[{idx}];")
        return ctx
    gate_x = _one_qubit_gate_noargs
    gate_y = _one_qubit_gate_noargs
    gate_z = _one_qubit_gate_noargs
    gate_h = _one_qubit_gate_noargs
    gate_t = _one_qubit_gate_noargs
    gate_s = _one_qubit_gate_noargs
    def _two_qubit_gate_noargs(self, gate, ctx):
        for control, target in gate.control_target_iter(ctx[1]):
            ctx[0].append(f"{gate.lowername} q[{control}],q[{target}];")
        return ctx
    gate_cz = _two_qubit_gate_noargs
    gate_cx = _two_qubit_gate_noargs
    gate_cy = _two_qubit_gate_noargs
    gate_ch = _two_qubit_gate_noargs
    gate_swap = _two_qubit_gate_noargs
    def _one_qubit_gate_args_theta(self, gate, ctx):
        for idx in gate.target_iter(ctx[1]):
            ctx[0].append(f"{gate.lowername}({gate.theta}) q[{idx}];")
        return ctx
    gate_rx = _one_qubit_gate_args_theta
    gate_ry = _one_qubit_gate_args_theta
    gate_rz = _one_qubit_gate_args_theta
    def gate_i(self, gate, ctx):
        for idx in gate.target_iter(ctx[1]):
            ctx[0].append(f"id q[{idx}];")
        return ctx
    def gate_u1(self, gate, ctx):
        for idx in gate.target_iter(ctx[1]):
            ctx[0].append(f"{gate.lowername}({gate.lambd}) q[{idx}];")
        return ctx
    def gate_u2(self, gate, ctx):
        for idx in gate.target_iter(ctx[1]):
            ctx[0].append(f"{gate.lowername}({gate.phi},{gate.lambd}) q[{idx}];")
        return ctx
    def gate_u3(self, gate, ctx):
        for idx in gate.target_iter(ctx[1]):
            ctx[0].append(f"{gate.lowername}({gate.theta},{gate.phi},{gate.lambd}) q[{idx}];")
        return ctx
    def gate_cu1(self, gate, ctx):
        for c, t in gate.control_target_iter(ctx[1]):
            ctx[0].append(f"{gate.lowername}({gate.lambd}) q[{c}],q[{t}];")
        return ctx
    def gate_cu2(self, gate, ctx):
        for c, t in gate.control_target_iter(ctx[1]):
            ctx[0].append(f"{gate.lowername}({gate.phi},{gate.lambd}) q[{c}],q[{t}];")
        return ctx
    def gate_cu3(self, gate, ctx):
        for c, t in gate.control_target_iter(ctx[1]):
            ctx[0].append(f"{gate.lowername}({gate.theta},{gate.phi},{gate.lambd}) q[{c}],q[{t}];")
        return ctx
    def _three_qubit_gate_noargs(self, gate, ctx):
        c0, c1, t = gate.targets
        ctx[0].append(f"{gate.lowername} q[{c0}],q[{c1}],q[{t}];")
        return ctx
    gate_ccx = _three_qubit_gate_noargs
    gate_cswap = _three_qubit_gate_noargs
    def gate_measure(self, gate, ctx):
        for idx in gate.target_iter(ctx[1]):
            ctx[0].append(f"measure q[{idx}] -> c[{idx}];")
        return ctx
_preprocess_runを見る
    def _preprocess_run(self, gates, n_qubits, args, kwargs):
        def _parse_run_args(output_prologue=True, **_kwargs):
            return { 'output_prologue': output_prologue }
        args = _parse_run_args(*args, **kwargs)
        if args['output_prologue']:
            qasmlist = [
                "OPENQASM 2.0;",
                'include "qelib1.inc";',
                f"qreg q[{n_qubits}];",
                f"creg c[{n_qubits}];",
            ]
        else:
            qasmlist = []
        return gates, (qasmlist, n_qubits)
まずやっていることは、オプションの解析です。
Circuit().run(backend='qasm_output', output_prologue=False)のようにoutput_prologueオプションを付けて実行される場合があるので、オプションの解析を行っています。
このオプションはデフォルトではTrueですが、Falseが指定された場合、OpenQASMの冒頭に付け加えられる
OPENQASM 2.0;
include "qelib1.inc";
qreg q[ビット数];
creg c[ビット数];
を省略します。
続いて、ctxは、OpenQASMの行のリストと、量子ビット数のタプルとしています。
gatesとctxを返していますが、gatesは引数で渡されたものをそのまま返しています。
gatesを_preprocess_runで返すことにしているのは、最適化等でゲート列を操作したい場合などを考慮したためですが、特に行う必要がなければ、受け取った引数をそのまま返します。
actionを見る
    def _one_qubit_gate_noargs(self, gate, ctx):
        for idx in gate.target_iter(ctx[1]):
            ctx[0].append(f"{gate.lowername} q[{idx}];")
        return ctx
    gate_x = _one_qubit_gate_noargs
    gate_y = _one_qubit_gate_noargs
    gate_z = _one_qubit_gate_noargs
    # いっぱいあるので略
    def gate_cu3(self, gate, ctx):
        for c, t in gate.control_target_iter(ctx[1]):
            ctx[0].append(f"{gate.lowername}({gate.theta},{gate.phi},{gate.lambd}) q[{c}],q[{t}];")
        return ctx
こんな感じでやっていきます。x, y, zゲートなどは、一個一個作るのが面倒なので横着しています。
横着できない場合は、gate_cu3のように丁寧に実装します。
やっていることは、ctxが([行のリスト], 量子ビット数)でしたので、ctx[0].append(...)で行のリストに新たな行を追加しているだけです。
_postprocess_runを見る
    def _postprocess_run(self, ctx):
        return "\n".join(ctx[0])
単純に、行のリストを、改行区切りの文字列にして返しているだけです。
この結果が、runの結果になります。
まとめ
前回は、OpenQASMを読み込める処理系のためのバックエンド実装方法を見ていきましたが、今回は、より汎用的なバックエンドの実装方法を見ていきました。
Blueqatでは、ライブラリ側でできるだけ多く面倒を見ることと、開発者やユーザがやりたいことをできるようにすることの両立を目指していて、Blueqatが用意した仕組みに乗っかってバックエンドを簡単に作ることもでき、乗っからずにフルでrunメソッドを実装することもできます。
バックエンド自体は、誰でも作ることができ、みなさんの自作シミュレータをBlueqatのインタフェースで使うことも可能です。
ぜひ皆さん、バックエンドの実装をしてみてください。

