#簡単に量子プログラミングが始められる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のインタフェースで使うことも可能です。
ぜひ皆さん、バックエンドの実装をしてみてください。