Edited at

pythonのsimpyでシミュレーションをしてみた その2

More than 1 year has passed since last update.

simpyはsympyと検索エンジンに間違われるし、pythonの2系用と3系用があって情報も混ざっているし、日本語の情報も少なくて英語だらけだしウダウダグダグダ。。。

っとすみません脱線してしまいました。今回は前回の予告の通りsimpyのsimpy.Resourceクラスとsimpy.Containerクラスでしたね。

ResourceとCotainerをそれぞれ簡単に説明するとResourceはクライアント(利用者)が共通に利用可能なサーバー(サービスをしてくれる場所)のことでContainerはクライアントが利用可能な設定された環境に共通な資源のことです。ガソリンスタンドを例にして説明するとResourceが給油機でContainerがガソリンの量ですね。


Resource

まず簡単に先に挙げたガソリンスタンドを例にソースを紹介します。simpyのチュートリアルのサイトにもありますがそれとは違うものになります。


resource.py

import simpy

import random

NUM_GASSTATION = 2
GAS_MAX = 20

class Car:
def __init__(self, name, env, gasstation):
self.name = name
self.gas_amount = random.randint(1, GAS_MAX)
self.res = gasstation
print("%sが%dリットルのガソリンで運転開始" % (self.name, self.gas_amount))
env.process(self.car_run(env))

def car_run(self, env):
yield env.timeout(self.gas_amount) # 0だけのタイムアウトはすることができない
with self.res.request() as req:
yield req
print("%sが%.1fにガソリンスタンドに到着しました。" % (self.name, env.now))
amount = GAS_MAX
yield env.timeout(amount)
print("%sが%.1fにガソリンスタンドを出発しました。" % (self.name, env.now))

def carfuctory(env, gasstation, N):
for i in range(N):
Car("car"+str(i), env, gasstation)

env = simpy.Environment()
gasstation = simpy.Resource(env, capacity=NUM_GASSTATION) # NUM_GASSTATION個だけのガソリンスタンドの準備
carfuctory(env, gasstation, 10) # 10台分のシミュレーション
env.run() # ジェネレータ関数がなくなるまで


前回と違ってクラスでまとめてみました。今回はCarクラスを作成したときに初期化関数の中でenv.processで追加していますね。pythonは参照代入なのでクラスの中でenvを引数としてもうまくいくんですね。今回の説明で重要なところはResourceクラスのインスタンスを引数に受け取る

with self.res.request() as req:

yield req
print("%sが%.1fにガソリンスタンドに到着しました。" % (self.name, env.now))
amount = GAS_MAX
yield env.timeout(amount)
print("%sが%.1fにガソリンスタンドを出発しました。" % (self.name, env.now))

ここの部分です。with文はこの要求はここからここまでですよ、ここの部分だけでResourceクラスのインスタンスのself.resの中身をつかいますよという宣言です。そしてyield reqはself.resの中身を要求しますという宣言です。この部分ではself.resの初期化時にcapacityで宣言された個数のうち使われていないものの一つを使えるまで次に進みません。もちろん要求した瞬間に通ればそのまま次に進んでいきます。


Container

今度はsimpyのチュートリアルのこちらで説明をしたいと思います。こちらに和訳ではないですがわかりやすく日本語にしたソースを載せます。


GasStationRefueling.py

import itertools  # 特別なイテレータのライブラリ

import random

import simpy

RANDOM_SEED = 42
GAS_STATION_SIZE = 200 # ガソリンスタンドが保有できるガソリンの量
THRESHOLD = 10 # ガソリンを運ぶトラックを呼ぶときのガソリンの全体に対する割合
FUEL_TANK_SIZE = 50 # ガソリンスタンドが蓄えることができるガソリンの最大量
FUEL_TANK_LEVEL = [5, 25] # 車が持つガソリンの初期化時に必要な乱数の範囲[Min, Max]の表記で単位はリッター
REFUELING_SPEED = 2 # 環境の単位時間あたりに補充できるガソリンの量
TANK_TRUCK_TIME = 300 # トラックがガソリンを運ぶのにかかる時間
T_INTER = [30, 300] # 車を作る間隔を指定するために必要な乱数の範囲で[min, max]の表記
SIM_TIME = 1000 # シミュレーションする時間

def car(name, env, gas_station, fuel_pump):
"""
ガソリンを使って走る車のクラス
"""

fuel_tank_level = random.randint(*FUEL_TANK_LEVEL)
print('%sが%.1fにガソリンスタンドに到着しました。' % (name, env.now))
with gas_station.request() as req:
start = env.now
# ガソリンスタンドを要求
yield req

# その車のガソリンが満タンになるように容量を設定して車にガソリンを補充
# ただし燃料がガソリンスタンドにない場合はガソリンが手に入るまで待つ。
liters_required = FUEL_TANK_SIZE - fuel_tank_level
yield fuel_pump.get(liters_required)

# ガソリンの補充する速度にのっとってガソリンを補充
yield env.timeout(liters_required / REFUELING_SPEED)

print('%sは%.1fかかってガソリンを満タンにしました。' % (name,
env.now - start))

def gas_station_control(env, fuel_pump):
"""
ガソリンスタンドがガソリンを積んだトラックを呼ぶための一連の流れを記した関数
"""

while True:
if fuel_pump.level / fuel_pump.capacity * 100 < THRESHOLD:
print('%dにトラックを呼びました。' % env.now)
# タンクが到着するプロセスを返す
yield env.process(tank_truck(env, fuel_pump))

yield env.timeout(10) # 10単位時間分待つ

def tank_truck(env, fuel_pump):
"""ガソリンを積んだトラックの一連の流れ"""
yield env.timeout(TANK_TRUCK_TIME)
print('%dにトラックが到着しました' % env.now)
amount = fuel_pump.capacity - fuel_pump.level
print('トラックが%.1fリットルのガソリンを補充し始めました。' % amount)
yield fuel_pump.put(amount)

def car_generator(env, gas_station, fuel_pump):
"""車を作る工場"""
# 0からひとつずつカウントを増やしていくイテレータでループを回す。
for i in itertools.count():
yield env.timeout(random.randint(*T_INTER))
env.process(car('Car %d' % i, env, gas_station, fuel_pump))

# ここからシミュレーションの準備とその実行
print('Gas Station refuelling')
random.seed(RANDOM_SEED)

# シミュレーションの準備
env = simpy.Environment()
# ガソリンスタンドの利用できる給油口の定義
gas_station = simpy.Resource(env, 2)
# ガソリンスタンドが保有するガソリンの定義
fuel_pump = simpy.Container(env, GAS_STATION_SIZE, init=GAS_STATION_SIZE)

env.process(gas_station_control(env, fuel_pump))
env.process(car_generator(env, gas_station, fuel_pump))

# シミュレーションの実行
env.run(until=SIM_TIME)


このシミュレーションは次々に車が入ってくるガソリンスタンドのガソリンを積んだタンクを読んで補充してもらうというシミュレーションです。Containerクラスで重要なことは

container = simpy.Container(env, amount, init=init_amount)

yield container.put(amount)
yield container.get(amount)

の関数群です。上から順に変数宣言、containerへの補充、containerからの取得です。注意しないといけないのはcontainerの限界を超える注入はエラーをはかずにスルーされ、amount = 0の時はエラーをはいてしまうことです。

ちなみにsimpyはenv環境で作ったContainerのインスタンスはそのenv環境では共通に使うことができるのですがresourceとは何一つ結びついてないので注意して下さい。今回では

fuel_pump = simpy.Container(env, GAS_STATION_SIZE, init=GAS_STATION_SIZE)

で限界値がGAS_STATIONで初期値がGAS_STATION_SIZEのガソリンを蓄えることができる燃料タンクを作りますよと宣言しています。

そしてこのコードではtank_truckでfuel_pump.put関数でガソリンを補充して

        # その車のガソリンが満タンになるように容量を設定して車にガソリンを補充

# ただし燃料がガソリンスタンドにない場合はガソリンが手に入るまで待つ。
liters_required = FUEL_TANK_SIZE - fuel_tank_level
yield fuel_pump.get(liters_required)

の部分でガソリンを取得しています。この部分でのyield fuel_pump.get関数のふるまいはガソリンが取得できるまで待ってからガソリンを取得します。簡単ですね。

このようにsimpyは簡単に補充したり取得したりすることができます。


Containerクラスの追記

重要な変数の説明を忘れていたのでここに追記します。

Containerクラスのインスタンスはlevel変数とcapacity変数を持ちます。この変数は順にインスタンスが保有する量、保有できる限界の量を表します。今回の2つ目のプログラムでもこの変数で場合分けをしてトラックを呼んだりしているのがわかると思います。

実はResourceクラスでも今どのサーバーを使っているかの番号を変数として保持しているのですがよく挙動がわかってないのでここでは説明しません。


次回予告

これまでの雑な解説の総集編として自作したシミュレーションを解説します。

ここまで読んで理解できた人はすぐに理解できると思います。

その3ができました。