Qulacs を例に Julia から Python ライブラリを呼べるようにする話
本日は
Qulacs を Julia から Pythonista にとって自然な形で利用できるようにした Kyulacs.jl を(私が作ったので)紹介します. 現時点では野良パッケージです.
Qulacs 自体は Qulacs にある説明を引用します:
Qulacsは、高速な量子回路シミュレータであり、大きな量子回路やノイズがあったり、パラメトリックな量子回路にも対応しております。 Qulacsは、C / C ++で実装されており、Pythonインターフェイスもあるため、高速回路シミュレーションと高い操作性の両立しました。
もっと知りたい場合は Quantum Native Dojo の3章 を見ると良いです. 量子コンピュータを初めて学ぶ際も 2x2 の行列ができる程度の知識とクロネッカー積を知っていれば 1, 2 章を読めば雰囲気を掴めると思います.
Julia からも使いたい
Qulacs を Julia から使いたい場合下記のような選択肢があります
- C/C++ API を Clang.jl や CxxWrap.jl を介して呼び出す
- Python API を PyCall.jl を介して呼び出す
- コードを読んで作る
今回は Python API を使う方法を採用しました. 「だってPython便利なんだもーん」 という本音と
中身は pybind11 を介して C/C++ のコードが走るっぽいので利便性を重視する方向に傾きました.
ハイハイ,どうせ using PyCall; qulacs=pyimport("qulacs")
するんでしょ?知ってる知ってる(・ω・`). まぁまぁもう少しお付き合いください.
呼び出しましょう(愚直な方法)
Qulacs の Readme にあるサンプルコードを試してみましょう.
# readme_example.py
from qulacs import Observable, QuantumCircuit, QuantumState
from qulacs.gate import Y, CNOT, merge
state = QuantumState(3)
seed = 0 # set random seed
state.set_Haar_random_state(seed)
circuit = QuantumCircuit(3)
circuit.add_X_gate(0)
merged_gate = merge(CNOT(0, 1), Y(1))
circuit.add_gate(merged_gate)
circuit.add_RX_gate(1, 0.5)
circuit.update_quantum_state(state)
observable = Observable(3)
observable.add_operator(2.0, "X 2 Y 1 Z 0")
observable.add_operator(-3.0, "Z 2")
value = observable.get_expectation_value(state)
# 0.2835596510287872
print(value)
PyCall.jl
を使うと pyimport
関数によって下記のように同じことができます.
# readme_example.jl
using PyCall
qulacs = pyimport("qulacs")
QuantumState = qulacs.QuantumState
QuantumCircuit = qulacs.QuantumCircuit
Observable = qulacs.Observable
gate = pyimport("qulacs.gate")
Y = gate.Y
CNOT = gate.CNOT
merge = gate.merge
state = QuantumState(3)
seed = 0 # set random seed
state.set_Haar_random_state(seed)
circuit = QuantumCircuit(3)
circuit.add_X_gate(0)
merged_gate = merge(CNOT(0, 1), Y(1))
circuit.add_gate(merged_gate)
circuit.add_RX_gate(1, 0.5)
circuit.update_quantum_state(state)
observable = Observable(3)
observable.add_operator(2.0, "X 2 Y 1 Z 0")
observable.add_operator(-3.0, "Z 2")
value = observable.get_expectation_value(state)
# 0.2835596510287872
println(value)
PyCall.jl は偉大なのでたいていのPythonライブラリを呼び出せます.上記のコードはもちろん動作します.ですが,qulacs
直下にアトリビュートとして得られるクラスや qulacs.gate
以下にあるものを呼び出すために
qulacs.xxx
や qulacs.gate.xxx
のようなコードを書くのがちょっとだるいんですよね.
元々のPythonのコードと同様にもう少し簡単に QuantumState
やゲートを呼び出せるようにできないものか? Kyulacs.jl がそれを解決します.
呼び出しましょう(Kyulacs.jl による方法)
Kyulacs.jl を使うと下記のようにできます.
using Kyulacs: Observable, QuantumCircuit, QuantumState
using Kyulacs.Gate: CNOT, Y, merge
state = QuantumState(3)
seed = 0 # set random seed
state.set_Haar_random_state(seed)
circuit = QuantumCircuit(3)
circuit.add_X_gate(0)
merged_gate = merge(CNOT(0, 1), Y(1))
circuit.add_gate(merged_gate)
circuit.add_RX_gate(1, 0.5)
circuit.update_quantum_state(state)
observable = Observable(3)
observable.add_operator(2.0, "X 2 Y 1 Z 0")
observable.add_operator(-3.0, "Z 2")
value = observable.get_expectation_value(state)
# 0.2835596510287872
println(value)
Python の from qulacs import ...
の部分を using Kyulacs: ...
と読み替えることであとはほぼ全て同じコードが使いまわせます.
細かい補足
値を表示する Pythonの print
は Julia では println
に相当します. Julia の print
は println
の改行をしない版になります.
julia> print(1); print(2); print(3)
123
julia> println(1); println(2); println(3)
1
2
3
単に Kyulacs.jl の紹介だとここでおしまいですが,Kyulacs.jl で使っているテクニックを紹介していきます.
pyimport("qulacs") と Kyulacs.jl の違い
単純に pyimport
するだけの違いを説明していきます.
Julia の型として見えていること
using Kyulacs
をすると qulacs
や QuantumState
, QuantumCircuit
, Observable
などを即時に使うことができます. これらの識別子は Kyulacs module の内部で export
をすると宣言しているからです. qulacs
は pyimport("qulacs")
によって得られた PyObject
を型にもつオブジェクトです.
julia> using Kyulacs
julia> qulacs |> typeof
PyCall.PyObject
julia> qulacs.QuantumState |> typeof
PyCall.PyObject # せやろな
julia> QuantumState |> typeof
DataType # おやぁ?
QuantumState
は単に PyObject
を型にもつ qulacs.QuantumState
ではなく Julia の構造体として Kyulacs から export されていることがわかります.
せっかくなので2量子状態を表すインスタンスを作ってみましょう. 例えばここを読んでみる
julia> using Kyulacs
julia> state = QuantumState(2)
QuantumState(PyObject *** Quantum State ***
* Qubit Count : 2
* Dimension : 4
* State vector :
(1,0)
(0,0)
(0,0)
(0,0)
)
julia> state.get_vector()
4-element Vector{ComplexF64}:
1.0 + 0.0im
0.0 + 0.0im
0.0 + 0.0im
0.0 + 0.0im
ケットベクトルによる表記で言えば ⊗
関数を使って確認できます.
julia> using Kronecker
julia> ψ = ComplexF64[1, 0]
2-element Vector{ComplexF64}:
1.0 + 0.0im
0.0 + 0.0im
julia> ψ ⊗ ψ
4×1 Kronecker.KroneckerProduct{ComplexF64, Matrix{ComplexF64}, Matrix{ComplexF64}}:
1.0 + 0.0im
0.0 + 0.0im
0.0 + 0.0im
0.0 + 0.0im
さて, state = QuantumState(2)
によって state という Julia オブジェクトが作られました.
julia> state |> typeof
QuantumState
Julia の QuantumState
構造体は Python のインスタンスを格納するフィールドを持っている構造体として定義されています.
using PyCall
qulacs = pyimport("qulacs")
struct QuantumState
pyobj::PyObject
QuantumState(n) = new(qulacs.QuantumState(n))
end
実際,state.pyobj
が実行できます.
julia> using Kyulacs
julia> state = QuantumState(2);
julia> state.pyobj
PyObject *** Quantum State ***
* Qubit Count : 2
* Dimension : 4
* State vector :
(1,0)
(0,0)
(0,0)
(0,0)
つまり PyObject を型にする Pythonオブジェクトを包み込んでいるだけです.
ラップしたPythonオブジェクトに紐づいているメソッドを直に呼び出せる
一方で,get_vector
という
julia> state.get_vector() # Julia のオブジェクトなのにメソッドが呼び出せる
4-element Vector{ComplexF64}:
1.0 + 0.0im
0.0 + 0.0im
0.0 + 0.0im
0.0 + 0.0im
真面目に考えると下記のように pyobj
を経由する必要がある必要があるはずです.
julia> state.pyobj.get_vector()
4-element Vector{ComplexF64}:
1.0 + 0.0im
0.0 + 0.0im
0.0 + 0.0im
0.0 + 0.0im
なぜでしょう? 🧐 例えばJuliaで標準に使える複素数型の場合で試すと下記のようなエラーが出てきます:
julia> c = ComplexF64(1, 2)
1.0 + 2.0im
julia> c.re
1.0
julia> c.get_vector
ERROR: type Complex has no field get_vector
Stacktrace:
[1] getproperty(x::ComplexF64, f::Symbol)
@ Base ./Base.jl:42
[2] top-level scope
@ REPL[15]:1
Base.getproperty
を改造している.
実は 上記のエラーメッセージをよくみると getproperty
というのが出ています.実はプログラマーが記述した state.get_object
は Julia 内部では
getproperty(state, :get_object)
という関数の呼び出しの形に変換されます. :get_object
は Symbolを型にもつ Julia のオブジェクトです.
Base.jl: getproperty を読むと getfield
関数に帰着されJuliaの構造体のフィールドにアクセスします.複素数の例だと re
というフィールドがあるから c.re
が意味を持ち c.get_vector()
というのはエラーを出すわけです.
一般論の説明はこれまでにして QuantumState の実装に戻ります. 実は Kyulacs.jl 内部では
QuantumState
に対する getproperty
関数を次のように実装しています:
function Base.getproperty(t::QuantumState, s::Symbol)
if s ∈ fieldnames(QuantumState)
return getfield(t, s)
else
return getproperty(getfield(t, :pyobj), s)
end
end
これによって state.pyobj.get_vector
というまどろっこしい書き方をせずに state.get_vector
という形で Python ライブラリ qulacs のメソッドを呼び出すことができます. :get_vector
は Julia 構造体 QuantumState
のフィールドではないので
getproperty(getfield(state, :pyobj), :get_vector)
というのが実行されます.
state.<tab>
の補完は Python の dir
の結果が得られる.
単純に pyobj
を包み込むだけだと state.
のタブ補完は state.pyobj
が得られてしまいます. get_vector
を見つけるには ``state.pyobj.<タブ>のようにやはり
pyobj` を経由する必要が有ります. ですが Kyulacs の実装だと下記のようになります.
julia> using Kyulacs; state = QuantumState(2)
julia> state. # ここでタブを連打
__class__ __ne__ get_marginal_probability
__delattr__ __new__ get_qubit_count
__dir__ __reduce__ get_squared_norm
__doc__ __reduce_ex__ get_vector
__eq__ __repr__ get_zero_probability
__format__ __setattr__ load
__ge__ __sizeof__ multiply_coef
__getattribute__ __str__ multiply_elementwise_function
__gt__ __subclasshook__ normalize
__hash__ add_state sampling
__init__ allocate_buffer set_Haar_random_state
__init_subclass__ copy set_classical_value
__le__ get_classical_value set_computational_basis
__lt__ get_device_name set_zero_state
__module__ get_entropy to_string
これは Base.propertynames
を次のように改造しているからです:
Base.propertynames(t::QuantumState)) = propertynames(getfield(t, :pyobj))
どうやら tab キーを押した時の候補は propertynames の戻り値が得られるようです. 上記の改造によって pyobj
以下の候補を列挙されるようになります.
Python メソッドの入力は Python オブジェクトの値を渡す
次のようなコードを考えます
using Kyulacs
state = QuantumState(2)
circuit = QuantumCircuit(2)
circuit.update_quantum_state(state)
circuit.update_quantum_state(state)
は注意深く観察すると <Pythonのメソッド>
に対してJulia のオブジェクト state
をつっこんでいます. state
は我々が作った Julia 構造体なのでそれをそのまま渡すとエラーが起きてしまいます. そこで下記コードによって Julia のオブジェクトをPyObjectする変換規則を与えてやります.そうすると PyCall の力で Julia のオブジェクトは包んでいる pyobj
の方を update_quantum_state
の引数に渡しているかのような処理を行えます.
using PyCall
PyObject(t::QuantumState) = t.pyobj
ここまでのまとめ
今までの話をまとめると下記に相当するコードが Kyulacs.jl に入り込んでいることが想像できます.
struct QuantumState
pyobj::PyObject
QuantumState(args...) = new(qulacs.gate.QuantumState(args...))
end
PyObject(t::QuantumState) = t.pyobj
function Base.propertynames(t::QuantumState)
propertynames(getfield(t, :pyobj))
end
function Base.getproperty(t::QuantumState, s::Symbol)
if s ∈ fieldnames(QuantumState)
return getfield(t, s)
else
return getproperty(getfield(t, :pyobj), s)
end
end
export QuantumState
マクロを使って賄う
さて,実際は↑のようなものが qulacs の QuantumState
だけでなく QuantumCircuit
などのさまざまなオブジェクトにも適用される必要があります. 各々の型の定義に対して同じような実装をつらつら書くのは辛いです. そこで Kyulacs.jl ではマクロを使って上記の処理を各々の qulacs のクラスに対して行っています.
for class in [:QuantumState, :QuantumCircuit] # ここは実はいっぱいある
@eval begin
struct $class
pyobj::PyObject
$(class)(args...) = new(qulacs.$(class)(args...))
end
PyObject(t::$(class)) = t.pyobj
function Base.propertynames(t::$(class))
propertynames(getfield(t, :pyobj))
end
function Base.getproperty(t::$(class), s::Symbol)
if s ∈ fieldnames($(class))
return getfield(t, s)
else
return getproperty(getfield(t, :pyobj), s)
end
end
export $(class)
end
end
@eval
マクロによってループ内にあるテンプレートに命が吹き込まれ Pythonのクラスと同名の Julia の構造体が定義されていきます(そして export もされる).
qulacs.gate
以下にあるクラスも同様な手法で変換されます. 例えば CNOT
は using Kyulacs.Gate
とすることで使えます.
Julia のマクロすごいでしょ?この機構のおかげで私は調査も含めて 1日で Kyulacs.jl をリリースすることができました.
まとめ
Julia インターフェース Kyulacs.jl を使うことで qulacs の機能を Python ユーザーにとって自然な表記で利用できることがわかりました. Pythonクラスと同名の型を定義することで他のパッケージの抽象型のサブタイプとして活用できる余地も作ることができました.qulacs に限らず他の Python ライブラリにも応用できるテクニックですのでぜひ使ってみてください.
Discussion
Kyulacs.jl を
https://github.com/AtelierArith/Gallery.git
レジストリに登録したので下記の手順で Docker のコンテナ内で実行できます.