SimPyで離散事象シミュレーション(2) リソースを理解しよう：Container編

# はじめに

SimPyというPythonの離散事象シミュレーション用のパッケージを見つけて試してみたら気に入ったので自分用の備忘録も兼ねて使い方をまとめていく．

* [SimPyのドキュメント](https://simpy.readthedocs.io/en/latest/)
* [SimPyのソース](https://bitbucket.org/simpy/simpy/src/default/)
* [前回の記事](https://qiita.com/j54854/items/687f8842be1d3e212384)（はじめの一歩）

第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クラスのインスタンスは，

```python
simpy.resources.container.Container(env, capacity=float('inf'), init=0)
simpy.Container(env, capacity=float('inf'), init=0)
```

のいずれかで生成できる（2つ目はショートカット）．生成時に特に指定しないと，`capacity`は無限に，`level`の初期値は0にそれぞれ設定される．

## 標準的な使い方

一例として，前回の在庫管理の例を微修正したモデルをContainerを用いて作成してみた．コードの後に解説があるので，それを参照しながら眺めてみてほしい．

```python
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クラスを少しカスタマイズした例を示しておこう．

```python
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()`をメソッドとして取り込んでいることがわかる．ただ，この変更はシミュレーションの機能自体にはまったく影響を与えない，単に好みの問題だ．

もう１つの変更点は，Get事象がトリガーされたときに，それに対応する各顧客の待ち時間を画面に表示させるようにしたことである．`_do_get()`メソッドをオーバーライドして実現していることがわかる．なお，待ち始めた時刻をGet事象に属性`now`としてもたせるために，`_trigger_get()`メソッドにもちょっとしたトリックを仕込んだ．

# まとめ

今回はSimPyの「resources関連のモジュール群」のうち，base.pyとcontainer.pyの内容についてまとめた．Conteinerクラスの標準的な使い方と簡単なカスタマイズの具体例も紹介したので，機会があればぜひ試してみてほしい．残りの2つのモジュールについても，次回以降にまとめたいと思う．

* [第3回へのリンク](https://qiita.com/j54854/items/2bcc189acb0cd9667857)
* [この記事のプログラムのソース（GitHub）](https://github.com/j54854/mySimPy)