yohm's blog

プログラミングや研究などについて

OACISでどのようにPython APIを実現しているのか

なぜOACISにPython APIが導入されたか

OACISはウェブブラウザ上での操作がメインになるが、RubyまたはPythonでのスクリプト言語を使って操作を自動化することもできる。 スクリプト言語で特定の関数を呼ぶことでOACIS上の処理を実行できる。例えば、実行済の結果を参照したり、ParameterSetを作成したりといったことができ、それらのメソッドをOACISのAPIと呼んでいる。

github.com

これを使うことでパラメータの最適化、相図の探索、感度解析などを行うことができる。例えば、災害時の避難行動のエージェントシミュレーションを対象に、遺伝的アルゴリズムを用いて最適な避難計画の探索が行われたことがある。

さて、OACISはRuby on Rails上で開発されており、RubyAPIは内部で利用しているメソッドをそのまま呼ぶことで実現している。より具体的にはRailsのmodelのメソッドを直接呼んでOACIS上での処理を実現している。要するにRubyAPIは開発で使っている関数をそのまま利用しているだけである。

しかし、計算科学においてはRubyを使っている人は少数派で、Pythonをメイン言語にしている人が多い。Rubyはウェブ開発には非常に強みがあるが、計算科学においてはnumpyやscipyほどのメジャーなライブラリがなく、特に強いこだわりがなければPythonを使う人がほとんどだ。OACISは計算科学の人を対象にしたソフトウェアなのでPythonAPIを提供し、PythonからOACISを制御できる様にしたかった。

Python APIの実現方法

このようなAPIを実現するスタンダードな方法の一つは、Railsでweb APIを提供して、PythonからそのWeb APIを呼ぶ様にすることである。 しかし、この方法ではRaillsでweb APIを実装し、Pythonでそのweb APIを呼ぶメソッドを実装するという二重の手間が発生する。OACISの用途ではRailsが動いているホストから実行できれば良いだけなので、web APIだとオーバースペックである。

そこで今回はPythonからRubyのメソッドを呼ぶ方針を採用した。そのために、rb_callという「PythonからRubyの任意のメソッドを呼ぶための汎用的なライブラリ」を開発した。

github.com

このライブラリはOACIS以外にも汎用的に使えるもので、例えば以下の様なPythonコードを書いてRubyのメソッドを呼ぶことができる。

from rb_call import RubySession

rb = RubySession()                          # Execute a Ruby process
rb.require('./minimal_sample')              # load a Ruby library 'sample_class.rb'

MyClass = rb.const('MyClass')               # get a Class defined in 'sample_class.rb'
obj = MyClass()                             # create an instance of MyClass
print( obj.m1(), obj.m2(1,2), obj.m3(3,b=4) )
                                            #=> "m1", "m2 1 2", "m3 3 4"
proc = obj.m4('arg of proc')
print( proc() )                             #=> "m4 arg of proc"

e = obj.m5()                                # Not only a simple Array but an Enumerator is supported
for i in e:                                 # You can iterate using `for` syntax over an Enumerable
    print(i)                                #=> "5", "10"

実はこのライブラリはRubyPythonメタプログラミングをかなり巧妙に利用していて、全部で200行ほどしかない。 (200行ではあるが、ここまで洗練された実装にするのに三週間くらい使っている😅) このような汎用的な仕組みを構築すると、今後RubyAPIが増えたときにもPythonのコードに何も手を入れなくても動作するのでメンテナンスがだいぶ楽になっている。 rb_callが動作する仕組みについてはQiitaに記事を書いたので、メタプログラミングが好きな人はぜひ読んでみてほしい。

qiita.com

Pythonimport oacis をした時の挙動

Python内でimport oacisを実行すると、以下のコードがimportされる。 やっていることは、

  • rb_callをimport
  • OACISの環境をロードする(Rb.require_relative('../config/environment')
  • OACISのクラスをロードする(Simulator = Rb.const('Simulator')など)

である。これでPythonRubyAPIと同名のメソッドが使える様になり、PythonからOACISを制御できるようになった。

import sys
import os

rb_call_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../rb_call') )
sys.path.append( rb_call_path )

os.putenv('BUNDLE_GEMFILE', os.path.abspath(os.path.join(os.path.dirname(__file__),'../Gemfile')) )
from rb_call import RubySession

Rb = RubySession()
Rb.require_relative('../config/environment')
Rb.require_relative('../rb_call/patch/mongoid_patch')

# Defining classes of OACIS
Simulator = Rb.const('Simulator')
ParameterSet = Rb.const('ParameterSet')
Run = Rb.const('Run')
Analyzer = Rb.const('Analyzer')
Analysis = Rb.const('Analysis')
Host = Rb.const('Host')
HostGroup = Rb.const('HostGroup')

from .oacis_watcher import OacisWatcher

このようにOACISのPython APIは結構エレガントな仕組みで実装されていて、開発していてとても面白かった😀