OACISでどのようにPython APIを実現しているのか
なぜOACISにPython APIが導入されたか
OACISはウェブブラウザ上での操作がメインになるが、RubyまたはPythonでのスクリプト言語を使って操作を自動化することもできる。 スクリプト言語で特定の関数を呼ぶことでOACIS上の処理を実行できる。例えば、実行済の結果を参照したり、ParameterSetを作成したりといったことができ、それらのメソッドをOACISのAPIと呼んでいる。
これを使うことでパラメータの最適化、相図の探索、感度解析などを行うことができる。例えば、災害時の避難行動のエージェントシミュレーションを対象に、遺伝的アルゴリズムを用いて最適な避難計画の探索が行われたことがある。
さて、OACISはRuby on Rails上で開発されており、RubyのAPIは内部で利用しているメソッドをそのまま呼ぶことで実現している。より具体的にはRailsのmodelのメソッドを直接呼んでOACIS上での処理を実現している。要するにRubyのAPIは開発で使っている関数をそのまま利用しているだけである。
しかし、計算科学においてはRubyを使っている人は少数派で、Pythonをメイン言語にしている人が多い。Rubyはウェブ開発には非常に強みがあるが、計算科学においてはnumpyやscipyほどのメジャーなライブラリがなく、特に強いこだわりがなければPythonを使う人がほとんどだ。OACISは計算科学の人を対象にしたソフトウェアなのでPythonのAPIを提供し、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の任意のメソッドを呼ぶための汎用的なライブラリ」を開発した。
このライブラリは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"
実はこのライブラリはRubyとPythonのメタプログラミングをかなり巧妙に利用していて、全部で200行ほどしかない。 (200行ではあるが、ここまで洗練された実装にするのに三週間くらい使っている😅) このような汎用的な仕組みを構築すると、今後RubyのAPIが増えたときにもPythonのコードに何も手を入れなくても動作するのでメンテナンスがだいぶ楽になっている。 rb_callが動作する仕組みについてはQiitaに記事を書いたので、メタプログラミングが好きな人はぜひ読んでみてほしい。
Pythonで import oacis
をした時の挙動
Python内でimport oacis
を実行すると、以下のコードがimportされる。
やっていることは、
rb_call
をimport- OACISの環境をロードする(
Rb.require_relative('../config/environment')
- OACISのクラスをロードする(
Simulator = Rb.const('Simulator')
など)
である。これでPythonでRubyのAPIと同名のメソッドが使える様になり、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