はじめに
SimPyというPythonの離散事象シミュレーション用のパッケージを見つけて試してみたら気に入ったので自分用の備忘録も兼ねて使い方をまとめていく.
- SimPyのドキュメント
- SimPyのソース
- 前回の記事(はじめの一歩)
第2回目の今回は,前回触れられなかった「resources関連のモジュール群」で提供されているリソースについてみていこう.この使い方としては,シミュレーションの対象システムのモデルの部品に使うというようなイメージだと思う.
どんな部品でも,SimPyに用意されているリソースを使わずに実装することはできるが,使った方が便利ならそれに越したことはない.このとき,デフォルトのリソースをそのまま使うのではなく,少しカスタマイズしたくなることもあるかもしれない.ちょっとしたカスタマイズならできそうだと思えるぐらいまで理解することを目指そう.
モジュール群の構成
「resources関連のモジュール群」をまとめたフォルダには,container.py,store.py,resource.py,base.pyの4つのモジュールが含まれている.
container.pyには,なんらかのオブジェクトを蓄えておく容器のような役割を果たすリソース(Containerクラス)が定義されている.蓄えられるオブジェクトは,1つ,2つと数えられるものでもいいし,液体などの連続的なものでもよい.ただし,離散的なオブジェクトの場合でもオブジェクトはどれも同一で,個体間の区別はできない.
store.pyに定義されているリソース(Store,PriorityStore,FilterStoreの3クラス)もなんらかのオブジェクトを蓄えておくものである.これらのリソースに蓄えられるものは離散的なオブジェクトのみで,個体間の区別が可能な点がContainerとの違いである.
resource.pyには,プロセスが何かの処理を進めるために使用する道具,機械,担当者などのリソースが定義されている(Resource,PriorityResource,PreemptiveResourceの3クラス).このリソースは数が限られていて,複数のプロセスが同時にそれを必要とした場合には取り合いが生じることがある.
base.pyには,上の3つのリソースの基盤となるBaseResourceクラス(とそれが利用する事象PutとGet)が定義されている.上のリソースはいずれもこのBaseResourceを継承したサブクラスになっているので,最初にbase.pyモジュールからみていこう.
ただし,「BaseResourceの仕組み」の節は少しテクニカルなので,もしリソースをデフォルトのまま,あるいは微修正して使えれば十分という場合はスキップしてもいいと思う.
BaseResourceの仕組み
BaseResourceはプロセスからの依頼に対応して何かを受け入れたり,払い出したりする装置としてモデル化されている.受け入れたことを表すシグナルをGet,払い出したことを表すシグナルをPutと考えよう.PutとGetはどちらも事象の一種で,Eventクラスを継承したサブクラスとして実装されている.そして,いずれも依頼元のプロセス(proc
)と依頼先のリソース(resource
)への参照をもつ.
BaseResourceクラスは,環境env
への参照,容量を表す変数(capacity
),Put/Getを格納するリスト(put_queue
/get_queue
)をもち,put()
/get()
,_do_put()
/_do_get()
,_trigger_put()
/_trigger_get()
という6つのメソッドを備えている.
put()
/get()
メソッドは,Put/Getクラスのインスタンス事象p
/g
を生成し,それをリストput_queue
/get_queue
に追加する.そして,事象p
/g
のコールバック関数のリストに_trigger_get()
/_trigger_put()
を追加してから_trigger_put()
/_trigger_get()
を呼ぶ.
ここに,_trigger_put()
/_trigger_get()
メソッドは,対応するリストput_queue
/get_queue
を「先頭から順に」走査していく.そして,各要素についてその事象に対応する受入れ処理もしくは払出し処理(_do_put()
/_do_get()
メソッド)を呼ぶ.なお,これらのメソッドからFalse
が返ってきた場合は,リストの途中であってもそこでリストの走査を抜ける.
BaseResourceクラスには_do_put()
/_do_get()
メソッドのスケルトンが用意されているのみで,それらの具体的な実装はサブクラスで行うようになっている.例えば,対象の要素(事象Put/Get)が所定の条件(例えば,Putなら容量にまだ余裕があるかどうかなど)を満たしていれば,それをok=True
でトリガーする.条件を満たしていなければFalse
を返す,といった実装が考えられる.
慣れていないとわかりにくいと思うので,この場合の流れを簡単に説明しよう.例えば,あるPut事象p
が生成されたとしよう.これは受入れ処理のリクエストが届いたことに該当する.この場合,事象p
はput_queue
の末尾に追加され,p.callbacks
に_trigger_get()
を入れた後,_trigger_put()
が呼ばれる.
このとき仮に,事象p
が届く前にはput_queue
にもリソースの中にも何も入っていなかったとしよう.この場合,今はput_queue
にはp
だけが入っていることになる._trigger_put()
は,このp
を取り出して_do_put()
を呼ぶ._do_put()
は,リソースの容量にまだ余裕がある(今は空だから)ことを確認して,p.succeed()
を呼ぶ(p.ok=True
でp
をトリガーする).これで,p
に対応する受入れ処理のリクエストは無事処理された.
その後,イベントカレンダからp
が取り出されその生起処理が実行される(すなわち,p.callbacks
に入っているコールバック関数が順に呼ばれる).この際,_trigger_get()
も呼ばれ,もしget_queue
が空でなければ,_do_get()
が呼ばれる.すると,上記のp
によって受け入れられたオブジェクト(しかリソースの中には存在しないから,それが)が払い出される,というような流れになる.
また,補足として,事象Put/Getは,リストput_queue
/get_queue
内で待っている間に依頼元プロセスから取り消しが入った場合(OR結合の事象を待っていて別の事象が先に生起した場合やInterrupt
例外を受け取った場合など),cancel()
処理を呼んでリストから取り除く必要がある.ただし,プロセス側でyield
する際に,with
ブロックでコンテキスト化しておくと必要に応じてcancel()
が自動的に呼ばれるようになる(cancel()
せずに再度同じ事象をyield
して待ち続けることもできるが,その場合はプロセス側のコードにそれを明示すること).
リソースのフレームワークとしてうまく設計されているなと思う.個人的に1つだけ限界を感じたのは,_trigger_put()
/_trigger_get()
メソッドがリストを先頭から順に走査して,False
でブレークするように実装されている点.デフォルトだとput_queue
/get_queue
はFIFOキューになるが,後で見るように,適切なキーでリストがソートされるようにしておけば単純なディスパッチングルールなどは簡単に表現できる.しかし,ダイナミックなロジックを組み込みたいときは,_trigger_put()
/_trigger_get()
メソッド自体を本格的にオーバーライドする必要が出てくるかもしれない.
Containerとその使用例
最初にContainerクラスが上のBaseResourceクラスをどのように継承しているかを確認しておこう.まず,Put/Getの代わりにそれらのサブクラスContainerPut/ContainerGetが用いられている.違いは,これらのサブクラスに,受入れ・払出しの要求量を表す変数(amount
,負数はNG)が追加されている点である.
Containerクラス自体には,リソース内に蓄えられているオブジェクトの総数,もしくは総量を表す変数(level
)が追加されている.また,_do_put()
メソッドは,capacity-level
がamount
以上であればトリガー,そうでなければFalse
を返すという形で,_do_get()
メソッドは,level
がamount
以上であればトリガー,そうでなければFalse
を返すという形で,それぞれ実装されている.
Containerクラスのインスタンスは,
simpy.resources.container.Container(env, capacity=float('inf'), init=0)
simpy.Container(env, capacity=float('inf'), init=0)
のいずれかで生成できる(2つ目はショートカット).生成時に特に指定しないと,capacity
は無限に,level
の初期値は0にそれぞれ設定される.
標準的な使い方
一例として,前回の在庫管理の例を微修正したモデルをContainerを用いて作成してみた.コードの後に解説があるので,それを参照しながら眺めてみてほしい.
import random
import simpy
def manager(env):
env.model.ordered = False # no back order to receive
env.stocktake = env.event() # create the first signal (event)
while True:
yield env.stocktake
report(env)
if not env.model.ordered and env.model.level <= 10:
# only when no back order to receive
# reorder point = 10
env.process(deliverer(env)) # activate deliverer
env.model.ordered = True # back order will be received
env.stocktake = env.event() # create the next signal (event)
def deliverer(env):
yield env.timeout(5) # delivery lead time = 5
env.model.put(20) # back order is recieved
env.model.ordered = False # no back order to receive
def customer(env):
while True:
time_to = random.expovariate(1)
yield env.timeout(time_to)
how_many = random.randint(1, 3)
env.model.get(how_many)
env.stocktake.succeed() # signal for stocktaking (event)
def report(env):
print('[{}] current level: {}, orderd: {}, queue length: {} '.format(round(env.now), env.model.level, env.model.ordered, len(env.model.get_queue)))
def main():
env = simpy.Environment()
env.model = simpy.Container(env, init=10) # model is marely a Container
env.process(manager(env))
env.process(customer(env))
env.run(until=200)
if __name__ == "__main__":
main()
前回のコードと比べると,Modelクラスの定義がなくなっていることがわかる.main()
の中を見てみるとわかるように,Containerのインスタンスをそのままenv.model
として利用しているからである.
デフォルトのContainerでは,バックオーダのリストを保持しにくいので,同時に高々1個のオーダしか出せないように微修正を加えた.また,Containerの特徴を確認するために,顧客が買っていく品物の数は1個には限定せず,3個以下の乱数で個数を定めるようにしてある.それにあわせて配送のリードタイムも5に短縮した.結果表示のメソッドは関数に変更して,プロセスmanager()
から直接呼び出している.
カスタマイズの例
最後に,興味のある人向けにContainerクラスを少しカスタマイズした例を示しておこう.
import random
import simpy
class CustomContainer(simpy.Container):
def __init__(self, env, capacity=float('inf'), init=0):
self.env = env
super(CustomContainer, self).__init__(env, capacity, init)
def _trigger_get(self, put_event):
if len(self.get_queue) > 0:
e = self.get_queue[len(self.get_queue) -1]
if not hasattr(e, 'now'):
e.now = self.env.now
super(CustomContainer, self)._trigger_get(put_event)
def _do_get(self, event):
if self._level >= event.amount:
print('I waited for {} time units.'.format(round(self.env.now -event.now)))
super(CustomContainer, self)._do_get(event)
def report(self):
print('[{}] current level: {}, ordered: {}, queue length: {} '.format(round(self.env.now), self.level, self.ordered, len(self.get_queue)))
def manager(env):
env.model.ordered = False # no back order to receive
env.stocktake = env.event() # create the first signal (event)
while True:
yield env.stocktake
env.model.report()
if not env.model.ordered and env.model.level <= 10:
# only when no back order to receive
# reorder point = 10
env.process(deliverer(env)) # activate deliverer
env.model.ordered = True # back order will be received
env.stocktake = env.event() # create the next signal (event)
def deliverer(env):
yield env.timeout(5) # delivery lead time = 5
yield env.model.put(20) # back order is received
env.model.ordered = False # no back order to receive
def customer(env):
while True:
time_to = random.expovariate(1)
yield env.timeout(time_to)
how_many = random.randint(1, 3)
yield env.model.get(how_many)
env.stocktake.succeed() # signal for stocktaking (event)
def main():
env = simpy.Environment()
env.model = CustomContainer(env, init=10)
env.process(manager(env))
env.process(customer(env))
env.run(until=200)
if __name__ == "__main__":
main()
CustomContainerクラスは,Containerを継承したサブクラスになっている.それ以降の部分は,上の例とほぼ同じである.
CustomContainerクラスの定義を見てみると,もともと独立した関数だった画面表示のためのreport()
をメソッドとして取り込んでいることがわかる.ただ,この変更はシミュレーションの機能自体にはまったく影響を与えない,単に好みの問題だ.
もう1つの変更点は,Get事象がトリガーされたときに,それに対応する各顧客の待ち時間を画面に表示させるようにしたことである._do_get()
メソッドをオーバーライドして実現していることがわかる.なお,待ち始めた時刻をGet事象に属性now
としてもたせるために,_trigger_get()
メソッドにもちょっとしたトリックを仕込んだ.
まとめ
今回はSimPyの「resources関連のモジュール群」のうち,base.pyとcontainer.pyの内容についてまとめた.Conteinerクラスの標準的な使い方と簡単なカスタマイズの具体例も紹介したので,機会があればぜひ試してみてほしい.残りの2つのモジュールについても,次回以降にまとめたいと思う.