psim言語講座(第13回)チュートリアル編(4)

NTTデータ数理システム MSIISM Conference 2023
  • HOME
  • psim言語講座(第13回)チュートリアル編(4)

本記事は当社が発行しているシミュレーションメールマガジンVol.13の記事です。
シミュレーションメールマガジンの詳細・購読申込はこちら のサポートページから

はじめに

前回は、psim の Facility 資源の様々な利用方法を説明しました。

今回は、Store の利用方法を説明していきます。

ストア

離散イベントシミュレーションでは有限の資源を利用する時に発生する待ち行列をシミュレートする事ができます。

その「資源」のひとつにファシリティがありますが、今回はストアという概念を説明します。

ストアは有限の離散量資源を貯えられるようなものを表します。取得と追加操作が可能で、双方に待ち行列が発生します。取得と追加操作は独立行われます。

例えば、「コンビニエンスストアの棚」などです。コンビニエンスストアの棚には複数の商品がありますが、店員は定期的に商品を入荷し配置し、定期的にお客がその商品を買います。棚が一杯の時は商品を配置出来す、棚が空の場合は商品を買う事は出来ません。

以下のようにして、容量 3 のストアを定義できます。

    s = Store(3, monitor = True)

ストアの基本

ストアの基本の使い方は以下のようになります。

以下は、ストア s に任意のオブジェクト item を追加します。容量に空きがあれば追加しますが、容量に空きがない場合は、追加できるまで待ちます。

    yield s.put1(item)

以下は、ストア s にからオブジェクトをひとつ取得します。ストア内にアイテムがあれば、即座に取得しますが、取得できるアイテムがない場合は、取得できるまで待ちます。

    result = yield s.get1(name = "get1")
    item = result["get1"]

簡単な例

    initialize()
    s = Store(3, monitor = True)

    def printStoreStatus():
        print(f"バッファ: {s.sizeBuffer()}/{s.capacity}, " +
              f"取得待ち: {s.sizeGetQueue()}, " +
              f"追加待ち: {s.sizePutQueue()}")

    def supplier():
        for item in ["商品1", "商品2", "商品3"]:
            yield pause(5)
            print(f"時間 {now()} {item} 追加待ち")
            printStoreStatus()
            yield s.put1(item)
            print(f"時間 {now()} {item} 追加終了")
            printStoreStatus()

    def consumer():
        while True:
            yield pause(3)
            print(f"時間 {now()} 取得待ち")
            printStoreStatus()
            result = yield s.get1(name = "get1")
            item = result["get1"]
            print(f"時間 {now()} {item} 取得終了")
            printStoreStatus()

    activate(supplier)()
    activate(consumer)()
    start()

このコードは、供給者(supplier)と消費者(consumer)が独立に動作しています。

供給者は、5 秒起きに、商品を追加していきます。この例では商品が追加できてから、5 秒待つようなコードになっています。

消費者は、3 秒起きに、商品を取得していきます。この例では商品が取得できてから、3 秒待つようなコードになっています。

printStoreStatus は動作確認用のコードで、ストアのバッファ数、追加待ち数、取得待ち数を表示します。

時間 3.0 取得待ち
バッファ: 0/3, 取得待ち: 0, 追加待ち: 0
時間 5.0 商品1 追加待ち
バッファ: 0/3, 取得待ち: 1, 追加待ち: 0
時間 5.0 商品1 追加終了
バッファ: 1/3, 取得待ち: 1, 追加待ち: 0
時間 5.0 商品1 取得終了
バッファ: 0/3, 取得待ち: 0, 追加待ち: 0
時間 8.0 取得待ち
バッファ: 0/3, 取得待ち: 0, 追加待ち: 0
時間 10.0 商品2 追加待ち
バッファ: 0/3, 取得待ち: 1, 追加待ち: 0
時間 10.0 商品2 追加終了
バッファ: 1/3, 取得待ち: 1, 追加待ち: 0
時間 10.0 商品2 取得終了
バッファ: 0/3, 取得待ち: 0, 追加待ち: 0
時間 13.0 取得待ち
バッファ: 0/3, 取得待ち: 0, 追加待ち: 0
時間 15.0 商品3 追加待ち
バッファ: 0/3, 取得待ち: 1, 追加待ち: 0
時間 15.0 商品3 追加終了
バッファ: 1/3, 取得待ち: 1, 追加待ち: 0
時間 15.0 商品3 取得終了
バッファ: 0/3, 取得待ち: 0, 追加待ち: 0
時間 18.0 取得待ち
バッファ: 0/3, 取得待ち: 0, 追加待ち: 0

供給間隔より、消費間隔の方が短いので、当然ながら、品不足状態になっていきます。

並列化

先の例では供給者、消費者ともに、ストアへの操作が完了してから、一定時間待っています。

しかし、現実世界のモデルを考えると、供給や消費のイベントは、ストアの状態とは無関係に発生します。例えば、商品が納入されたタイミングで店員は商品を補充しようとするかもしれません。また、消費者は、任意のタイミングで購入しようとします。

そのような並列モデルは以下のように書く事ができます。

    initialize()
    s = Store(3, monitor = True)

    def supply(item):
        print(f"時間 {now()} {item} 追加待ち")
        printStoreStatus()
        yield s.put1(item)
        print(f"時間 {now()} {item} 追加終了")
        printStoreStatus()

    def supplier():
        for item in ["商品1", "商品2", "商品3"]:
            yield pause(5)
            yield subactivate(supply)(item)
        yield alwaysFalse()

    def consume():
        print(f"時間 {now()} 取得待ち")
        printStoreStatus()
        result = yield s.get1(name = "get1")
        item = result["get1"]
        print(f"時間 {now()} {item} 取得終了")
        printStoreStatus()

    def consumer():
        for i in range(5):
            yield pause(3)
            yield subactivate(consume)()
        yield alwaysFalse()

    activate(supplier)()
    activate(consumer)()
    start()

supplier プロセスは、5 秒毎に、商品を追加するサブプロセスを起動しています。そのサブプロセスは、商品を追加しようとします。ストアに空きがあれば即座に追加しますが、空きがなかった場合は、追加できるまで待ちます。ここで、複数の追加要求があれば、待ち行列が構成されます。

consumer プロセスは、3 秒毎に、商品を取得するサブプロセスを起動しています。このサブプロセスは、商品を取得しようとします。ストアにアイテムがあれば即座に取得しますが、アイテムがなかった場合は、取得できるまで待ちます。ここで、複数の取得要求があれば、待ち行列が構成されます。

実行結果は以下のようになります。

時間 3.0 取得待ち
バッファ: 0/3, 取得待ち: 0, 追加待ち: 0
時間 5.0 商品1 追加待ち
バッファ: 0/3, 取得待ち: 1, 追加待ち: 0
時間 5.0 商品1 追加終了
バッファ: 1/3, 取得待ち: 1, 追加待ち: 0
時間 5.0 商品1 取得終了
バッファ: 0/3, 取得待ち: 0, 追加待ち: 0
時間 6.0 取得待ち
バッファ: 0/3, 取得待ち: 0, 追加待ち: 0
時間 9.0 取得待ち
バッファ: 0/3, 取得待ち: 1, 追加待ち: 0
時間 10.0 商品2 追加待ち
バッファ: 0/3, 取得待ち: 2, 追加待ち: 0
時間 10.0 商品2 追加終了
バッファ: 1/3, 取得待ち: 2, 追加待ち: 0
時間 10.0 商品2 取得終了
バッファ: 0/3, 取得待ち: 1, 追加待ち: 0
時間 12.0 取得待ち
バッファ: 0/3, 取得待ち: 1, 追加待ち: 0
時間 15.0 商品3 追加待ち
バッファ: 0/3, 取得待ち: 2, 追加待ち: 0
時間 15.0 取得待ち
バッファ: 0/3, 取得待ち: 2, 追加待ち: 0
時間 15.0 商品3 追加終了
バッファ: 1/3, 取得待ち: 2, 追加待ち: 0
時間 15.0 商品3 取得終了
バッファ: 0/3, 取得待ち: 1, 追加待ち: 0

供給より消費の方が多く、消費過多である事は予想できます。結果を見ると、取得待ち数が増えていっている事が分かります。

供給過多

次に、消費より供給の方が多い、供給過多の例を考えます。

供給は 1 秒毎に行い、消費は 5 秒毎に行う例は、以下になります。

    initialize()
    s = Store(3, monitor = True)

    def supply(item):
        print(f"時間 {now()} {item} 追加待ち")
        printStoreStatus()
        yield s.put1(item)
        print(f"時間 {now()} {item} 追加終了")
        printStoreStatus()

    def supplier():
        for item in ["商品1", "商品2", "商品3", "商品4", "商品5",
                     "商品6", "商品7"]:
            yield pause(1)
            yield subactivate(supply)(item)
        yield alwaysFalse()

    def consume():
        print(f"時間 {now()} 取得待ち")
        printStoreStatus()
        result = yield s.get1(name = "get1")
        item = result["get1"]
        print(f"時間 {now()} {item} 取得終了")
        printStoreStatus()

    def consumer():
        for i in range(5):
            yield pause(5)
            yield subactivate(consume)()
        yield alwaysFalse()

    activate(supplier)()
    activate(consumer)()
    start(until = 20)

実行結果は以下になりました。

時間 1.0 商品1 追加待ち
バッファ: 0/3, 取得待ち: 0, 追加待ち: 0
時間 1.0 商品1 追加終了
バッファ: 1/3, 取得待ち: 0, 追加待ち: 0
時間 2.0 商品2 追加待ち
バッファ: 1/3, 取得待ち: 0, 追加待ち: 0
時間 2.0 商品2 追加終了
バッファ: 2/3, 取得待ち: 0, 追加待ち: 0
時間 3.0 商品3 追加待ち
バッファ: 2/3, 取得待ち: 0, 追加待ち: 0
時間 3.0 商品3 追加終了
バッファ: 3/3, 取得待ち: 0, 追加待ち: 0
時間 4.0 商品4 追加待ち
バッファ: 3/3, 取得待ち: 0, 追加待ち: 0
時間 5.0 取得待ち
バッファ: 3/3, 取得待ち: 0, 追加待ち: 1
時間 5.0 商品5 追加待ち
バッファ: 3/3, 取得待ち: 0, 追加待ち: 1
時間 5.0 商品1 取得終了
バッファ: 2/3, 取得待ち: 0, 追加待ち: 1
時間 5.0 商品4 追加終了
バッファ: 3/3, 取得待ち: 0, 追加待ち: 0
時間 6.0 商品6 追加待ち
バッファ: 3/3, 取得待ち: 0, 追加待ち: 1
時間 7.0 商品7 追加待ち
バッファ: 3/3, 取得待ち: 0, 追加待ち: 2
時間 10.0 取得待ち
バッファ: 3/3, 取得待ち: 0, 追加待ち: 3
時間 10.0 商品2 取得終了
バッファ: 2/3, 取得待ち: 0, 追加待ち: 3
時間 10.0 商品5 追加終了
バッファ: 3/3, 取得待ち: 0, 追加待ち: 2
時間 15.0 取得待ち
バッファ: 3/3, 取得待ち: 0, 追加待ち: 2
時間 15.0 商品3 取得終了
バッファ: 2/3, 取得待ち: 0, 追加待ち: 2
時間 15.0 商品6 追加終了
バッファ: 3/3, 取得待ち: 0, 追加待ち: 1

ストアの容量 3 に到達すると、それ以上供給ができなくなります。その結果、追加待ち数が増加していっている事が分かります。

取得条件の指定

s.get1() は、ストア s から商品を 1 つ取得します。この時、棚にある商品は全て同じで、どれを取得するかは指定しておらず、商品の中からひとつを取得します。(実際には、古く追加されたものが先に取得される)

棚には複数の製品が置かれている場合もあるかもしれません。

そのようなケースでは、s.get1 に条件式を加える事ができます。

以下のように書いた場合、

        yield s.get1(lambda v: v == "商品3")

ストア s の中から、「商品3」を取得しようとします。商品3がある場合は即座に取得しますが、商品3がない場合は取得できるまで待ちます。

このように、取得したい製品を明示的に指定する例は以下になります。

    initialize()
    s = Store(False, monitor = True)

    def supply(item):
        print(f"時間 {now()} {item} 追加待ち")
        printStoreStatus()
        yield s.put1(item)
        print(f"時間 {now()} {item} 追加終了")
        printStoreStatus()

    def supplier():
        for item in ["商品1", "商品2", "商品3", "商品4", "商品5",
                     "商品6", "商品7"]:
            yield pause(1)
            yield subactivate(supply)(item)
        yield alwaysFalse()

    def consume(item):
        print(f"時間 {now()} {item} 取得待ち")
        printStoreStatus()
        result = yield s.get1(lambda v: v == item, name = "get1")
        item = result["get1"]
        print(f"時間 {now()} {item} 取得終了")
        printStoreStatus()

    def consumer():
        for item in ["商品4", "商品1", "商品2", "商品7", "商品5",
                     "商品6", "商品3", ]:
            yield pause(2)
            yield subactivate(consume)(item)
        yield alwaysFalse()

    activate(supplier)()
    activate(consumer)()
    start(until = 10)

この例では、ストア s の容量を無限大に設定しています。Store の定義時に容量に False を指定すると、容量が無限大になります。

供給者は、"商品1", "商品2", "商品3", "商品4", "商品5", "商品6", "商品7"の順に商品を追加します。

消費者は、"商品4", "商品1", "商品2", "商品7", "商品5", "商品6", "商品3"の順に商品を取得します。

実行結果は以下になります。

時間 1.0 商品1 追加待ち
バッファ: 0/False, 取得待ち: 0, 追加待ち: 0
時間 1.0 商品1 追加終了
バッファ: 1/False, 取得待ち: 0, 追加待ち: 0
時間 2.0 商品4 取得待ち
バッファ: 1/False, 取得待ち: 0, 追加待ち: 0

最初の消費者が、"商品4" を求めますが、在庫がないため、"商品4"が入荷されるまで待っています。その間、その後の消費者が渋滞してしまっていますが、供給は続いています。

時間 2.0 商品2 追加待ち
バッファ: 1/False, 取得待ち: 0, 追加待ち: 0
時間 2.0 商品2 追加終了
バッファ: 2/False, 取得待ち: 1, 追加待ち: 0
時間 3.0 商品3 追加待ち
バッファ: 2/False, 取得待ち: 1, 追加待ち: 0
時間 3.0 商品3 追加終了
バッファ: 3/False, 取得待ち: 1, 追加待ち: 0
時間 4.0 商品1 取得待ち
バッファ: 3/False, 取得待ち: 1, 追加待ち: 0
時間 4.0 商品4 追加待ち
バッファ: 3/False, 取得待ち: 1, 追加待ち: 0
時間 4.0 商品4 追加終了
バッファ: 4/False, 取得待ち: 2, 追加待ち: 0
時間 4.0 商品4 取得終了
バッファ: 3/False, 取得待ち: 1, 追加待ち: 0

次の消費者が "商品1" を求めており、"商品1" は在庫にあるのに、取得できていません。

"商品4" が入荷されると、最初の消費者が品物を取得し、次の消費者が即座に "商品1" を取得します。

その後の動作は以下のようになります。

時間 4.0 商品1 取得終了
バッファ: 2/False, 取得待ち: 0, 追加待ち: 0
時間 5.0 商品5 追加待ち
バッファ: 2/False, 取得待ち: 0, 追加待ち: 0
時間 5.0 商品5 追加終了
バッファ: 3/False, 取得待ち: 0, 追加待ち: 0
時間 6.0 商品2 取得待ち
バッファ: 3/False, 取得待ち: 0, 追加待ち: 0
時間 6.0 商品6 追加待ち
バッファ: 3/False, 取得待ち: 0, 追加待ち: 0
時間 6.0 商品2 取得終了
バッファ: 2/False, 取得待ち: 0, 追加待ち: 0
時間 6.0 商品6 追加終了
バッファ: 3/False, 取得待ち: 0, 追加待ち: 0
時間 7.0 商品7 追加待ち
バッファ: 3/False, 取得待ち: 0, 追加待ち: 0
時間 7.0 商品7 追加終了
バッファ: 4/False, 取得待ち: 0, 追加待ち: 0
時間 8.0 商品7 取得待ち
バッファ: 4/False, 取得待ち: 0, 追加待ち: 0
時間 8.0 商品7 取得終了
バッファ: 3/False, 取得待ち: 0, 追加待ち: 0

この動作は想定した通りではあります。しかしながら、現実の世界のモデルでは、あまりこのような待ち方はしないかもしれません。

実際のスーパーの商品棚などを想定すると、おそらく、商品が無い場合は、商品の入手をあきらめる事が多いのではないでしょうか。

今回はスーパーの商品棚を例に出しましたが、生産ラインのシミュレーション、コールセンターのシミュレーションなどでもストアは基本的な資源となります。より柔軟なストアの用例について、次回以降、説明していこうと思います。

まとめ

今回は、psim の Store 資源の基本的な利用方法を説明しました。

次回は、より詳しいストアの待ち受けについて説明しようと思います。

監修:株式会社NTTデータ数理システム 機械学習、統計解析、数理計画、シミュレーションなどの数理科学を 背景とした技術を活用し、業種・テーマを問わず幅広く仕事をしています。
http://www.msi.co.jp NTTデータ数理システムができること
「数理科学の基礎知識」e-book無料ダウンロードはこちら

関連記事