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

  • HOME
  • psim言語講座(第10回)チュートリアル編(1)

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

はじめに

今までは現実的な問題の実装例をサンプルとしてご紹介してきました。

今回からは、psim の基本的な API や部品などを一つずつ説明していこうと思います。

まずは、psim の最小のサンプルコードを見てみます。

基本構造

psim 言語による離散イベントシミュレーション記述の基本構造は以下のようになります。

# 最小のサンプルコード

# psim ライブラリのインポート
from psim import *

# psim の初期化
initialize()

# プロセスの定義
def proc():
    yield pause(0)
    print("Hello World!")

# proc をスケジューラに登録する。
activate(proc)()

# スケジューラを起動する。
start()

先頭に必ず「from psim import *」が必要です。これが書かれていると、psim の API を利用する事ができます。次の「initialize()」も必須です。これは、psim の内部変数を初期化します。

次にプロセスを定義します。これを説明するために、少々背景を説明します。

psim 言語は、離散イベントシミュレーションを記述するための言語です。離散イベントシミュレーションとは、イベントを起因として状態が変化するようなシステムのシミュレーションの事です。psim 言語はプロセス試行で離散イベントシミュレーションを簡単に定義する仕組みを提供しています。

プロセスとは、イベントを起因として状態が変化するような一連の操作の事を指します。

psim ではプロセスは、Python のジェネレータとして定義します。「イベントを起因として状態が変化する」部分を yield 文を使って記述します。この例では、「yield pause(0)」がそれを指します。

yield の後のコマンド pause(0) が psim の API であり、待ち受け式と呼ばれます。pause(0) というのは、0 秒後に発火する待ち受け式です。ご想像の通り、「yield pause(0)」は、一瞬で発火するので見掛け上は何も変化はありません。ただし、内部的には一度 psim のスケジューラに処理が返り、すぐに戻ってくるという動作になります。戻ってきたら次の行の「print("Hello World!")」を実行します。

この例では、並行した別のプロセスがないため、非常に単純な動作になります。しかし、もし別の並行プロセスがある場合、psim のスケジューラは、適切な別のプロセスに処理を切り替えます。一般的な離散イベントシミュレーションは複数のプロセスが複雑に組み合わさり、お互いに影響しあいながらシミュレーションが進みます。そのようなスケジュール管理を行うのがスケジューラの役目です。どんなに複雑になろうとも、プロセスの定義者は、自身の状態変化のみに着目して動作を定義していけばよいことになります。

psim のプロセスは、Python のジェネレータとして定義しなくてはなりません。Python の仕様で、yield 文がひとつも含まれない定義はジェネレータではなく、通常の関数定義になってしまうので、コード内の「yield pause(0)」は必須です。この行を削除するとエラーになってしまいます。

「activate(proc)()」は先に定義したプロセス proc を、スケジューラに登録しています。

少々慣れないと読み難いのは、activate(proc) の後の () かと思います。これについては後程説明します。

「start()」は、psim のスケジューラを起動します。つまりシミュレーションが開始されます。全てのプロセスが終了するまでシミュレーションを実行します。

プロセスとは

複数の処理

プロセス定義は、ジェネレータとして定義する必要があると説明しました。

ジェネレータと聞くと難しく感じてしまう方もいるかもしれませんが、最初は yield がひとつ以上含まれる関数のようなものだと理解していただければ十分です。ただ、プロセスの中にひとつ以上の yield 文を入れる事だけは忘れずに注意下さい。

以下のコードはどう動くか、想像してみて下さい。

from psim import *

# psim の初期化
initialize()

# プロセスの定義
def proc():
    yield pause(10.5)
    print(f"シミュレーション開始から {now()} 秒経過しました。")
    yield pause(99999.999)
    print(f"シミュレーション開始から {now()} 秒経過しました。")

activate(proc)()

start()

ご想像の通り、以下のような出力となります。

シミュレーション開始から 10.5 秒経過しました。
シミュレーション開始から 100010.499 秒経過しました。

ただ、実際のシミュレーション時間は、100010.499 秒かかるわけではありません。

このような仕組みが離散イベントシミュレーションと呼ばれます。

制御構造

psim 言語はプロセス試行で離散イベントシミュレーションを簡単に定義する仕組みを提供しています。

これは、「イベントを起因として状態が変化する」部分を yield 文を使って記述しますが、それ以外の部分は、Python の制御構文がそのまま記述可能であるという意味しています。

例えば、以下のコードはどう動くか、考えてみて下さい。

def proc():

    for i in range(5):
        yield pause(i)
        print(f"シミュレーション開始から {now()} 秒経過しました。")

    g = exponentialDistribution(10)

    while True:
        yield pause(next(g))
        print(f"シミュレーション開始から {now()} 秒経過しました。")

        if now() > 100:
            break

(乱数の部分は毎回変化しますが)、以下のような出力となります。

シミュレーション開始から 0.0 秒経過しました。
シミュレーション開始から 1.0 秒経過しました。
シミュレーション開始から 3.0 秒経過しました。
シミュレーション開始から 6.0 秒経過しました。
シミュレーション開始から 10.0 秒経過しました。
シミュレーション開始から 14.812456338050854 秒経過しました。
シミュレーション開始から 26.686596475084023 秒経過しました。
シミュレーション開始から 31.77890272018562 秒経過しました。
シミュレーション開始から 64.65120746708594 秒経過しました。
シミュレーション開始から 105.06452831932417 秒経過しました。

Python プログラミングに慣れている方なら、もしかすると、ものすごく当たり前に見えるかもしれません。

そのようにご理解いただいても問題ありません。ただ、psim で activate されたプロセスは、内部的には通常のPythonプロセスと同じように実行されているわけではありません。普通のPythonの関数を書くように離散イベントシミュレーションのプロセスを定義できるように psim は設計されている、というのが正しい説明になります。

さいごに

今回は、プロセスのごく基本的な事しか説明できませんでした。次回は、複数のプロセスが並行に走るような例を説明していこうと思います。

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

関連記事