OMNeT++による離散シミュレーション～SELECTノード編

#1.はじめに
　第4回目となる今回は教科書である「SLAMⅡによるシステム・シミュレーション入門」から例題2.6「トラック運送システム」に取り組みます．メインテーマはネットワークの合流（SELECT）です．

|#|例題|タイトル|テーマ|
|---|---|---|---|
|第１回|例題2.1|[２つの機械工程を持つ生産ライン](https://qiita.com/tsugulin/items/021bbc65094baaa56ee1)|QUEUEノード|
|第２回|例題2.3|[故障を伴う機械システムのシミュレーション](https://qiita.com/tsugulin/items/021bbc65094baaa56ee1)|RESOURCEブロック|
|第３回|例題2.5|[一車線通行の信号システム](https://qiita.com/tsugulin/items/a8c7e2408a8eb652a915)|GATEブロック|
|第４回|例題2.6|トラック運送システム（今回）|SELECTノード|
|第５回|例題2.7|病院内カルテ搬送システム|BATCHノード等|

#2.例題の概要
　例題2.6はトラック運送システムに関する問題です．ブルドーザーが１台，積込機が２台，トラックが４台あり，まずブルドーザが盛土を作り，次に積込機が盛土を２個づつトラックに積み込み，トラックが土を運搬して積み降した後に待機場所に戻るサイクルを繰り返します．なお積込作業を始めるには２個の盛土，積込機，トラックの３つの条件が揃っている必要があります．また積込機は空き時間がより長い方を使用します（両方使える場合）．各作業の所要時間は表の通りです．
![26-02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507413/814560a6-5302-295a-fc1e-a39d926a489a.png)

|重機の種類|作業内容|作業時間|
|---|---|---|
|ブルドーザ|盛土作り|平均4分の指数分布に従う２つの乱数の和で求められるアーラン分布|
|積込機1|盛土の積込み|平均14分の指数分布|
|積込機2|盛土の積込み|平均12分の指数分布|
|積込機|待機場所への戻り|5分|
|トラック|土の運搬|平均22分，標準偏差3分の正規分布|
|トラック|土の積降し|2～8分の一様分布|
|トラック|待機場所への戻り|平均18分，標準偏差3分の正規分布|

#3.例題のネットワーク図
　最初のcreateノードは平均４分の指数分布に従う２つの乱数の和で求められるアーラン分布に従う間隔にてメッセージを生成します．これはブルドーザが盛土を作る作業を表します．なおシミュレーション開始から480分経過時点にて当該処理は終了します．次のaccumulateノードは2個メッセージが到着するたびに新しいメッセージを次に送ります．これは積込作業に必要な盛土2個の準備が出来たことを表します．次のloadノードとtrackノードとworkerノードは盛土とトラックと積込機の待ち行列を表します．次のselectノードは積込作業を表し，loadノードとtrackノードとworkerノードの全ての待ち行列にメッセージがあれば取り出して作業を開始します．もし積込機が両方空いている場合は空き時間が長い方を選択します．作業時間は積込機１の場合が平均14分，積込機２の場合は平均12分の指数分布に従います．積込機は作業終了後5分間休憩してから待機場所であるworkerノードに戻ります．次のprocess1ノードに続くアクティビティはトラックによる運搬作業を表し，平均22分，標準偏差3分の正規分布に従う作業時間を要します．次のprocess2ノードに続くアクティビティはトラックの積み降し作業を表し，2～8分の一様分布に従う作業時間を要します．その後トラックは平均18分，標準偏差3分の正規分布に従う時間をかけて待機場所であるtrackノードに戻ります．
![26-03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507413/250dd06a-dc11-41d5-609a-2c711d7f9444.png)

#4.ソースコードの説明
メニューから「File」～「New」～「OMNeT++ Project」を選択して適当なプロジェクト名を付与して下さい（今回はSimSlam2.6としています）．プロジェクトを構成するソースファイルは表の通りですが，GitHub[（https://github.com/tsugulin/SimSlam2.6）](https://github.com/tsugulin/SimSlam2.6)から入手可能です．git cloneにてローカルにダウンロードし，Windowsのファイル・エクスプローラーからIDEのプロジェクト・エクスプローラーにドラッグ＆ドロップすることにより取り込み可能です．  

|フォルダ名|ファイル名|説明|
|---|---|---|
|simulations|omnetpp.ini|シミュレーションのパラメータ変数の定義|
|simulations|Simulation01.ned|ノードとモジュールの関連付け（＝ネットワーク）を定義|
|src|Create.ned|メッセージを生成するモジュールの宣言|
|src|Accumulate.ned|２個のメッセージを受け取りマージするモジュールの宣言|
|src|BlockingQueue.ned|待ち行列モジュールの宣言|
|src|Select.ned|前工程の全ての待ち行列にメッセージが届くことを待つモジュールの宣言|
|src|Process.ned|特に何もしないモジュール（ただし後続アクティビティがある）の宣言|
|src|Create.cc|Createモジュールの定義|
|src|Create.h|Createモジュールのクラス宣言|
|src|Accumulate.cc|Accumulateモジュールの定義|
|src|Accumulate.h|Accumulateモジュールのクラス宣言|
|src|BlockingQueue.cc|BlockingQueueモジュールの定義|
|src|BlockingQueue.h|BlockingQueueモジュールのクラス宣言|
|src|Select.cc|Selectモジュールの定義|
|src|Select.h|Selectモジュールのクラス宣言|
|src|Process.cc|Processモジュールの定義|
|src|Process.h|Processモジュールのクラス宣言|
|src|Wavg.cc|待ち行列長さの加重平均を計算するクラスの定義|
|src|Wavg.h|Wavgクラスの宣言|

　例題2.6のネットワーク定義はSimulation01.nedの内容の通りです．

```Simulation01.ned
network Simulation01
{
    submodules:
        create: Create {
            parameters:
                @display("p=50,150;i=block/source");
        }
        accumulate: Accumulate {
            parameters:
                @display("p=150,150;i=block/join");
        }
        track: BlockingQueue {
            parameters:
                @display("p=250,50;i=block/boundedqueue;q=queue");
        }
        load: BlockingQueue {
                parameters:
                @display("p=250,150;i=block/boundedqueue;q=queue");
        }
        worker: BlockingQueue {
                parameters:
                @display("p=250,250;i=block/boundedqueue;q=queue");
        }
        select: Select {
            parameters:
                @display("p=350,150;i=block/fork");
        }
        process1: Process {
            parameters:
                @display("p=450,150;i=block/process");
        }
        process2: Process {
            parameters:
                @display("p=550,150;i=block/process");
        }
        process3: Process {
            parameters:
                @display("p=650,150;i=block/process");
        }
	connections:
        create.out --> accumulate.in;
        accumulate.out --> load.in;
        load.out --> select.in1;
        track.out --> select.in2;
        worker.out --> select.in3;
        select.out --> process1.in;
        process1.out --> process2.in;
        process1.res --> worker.in;
        process2.out --> process3.in;
        process3.res --> track.in;
}
```

　各モジュールが使用する変数と入口及び出口は以下の通りです．Create.nedの変数timeLimitはシミュレーション時間を表します（今回は480）．変数intervalTimeはメッセージ生成の頻度を表します．Accumulate.nedの変数workUnitは何個のメッセージをマージするのかを表します（今回は2）．BlockingQueue.nedの変数qNameは待ち行列の名前を表します．変数initialQnumは待ち行列が初期状態で保持するメッセージの個数を表します．Select.nedの変数productionTime1は積込機１の所要時間を表します．変数productionTime2は積込機２の所要時間を表します．Process.nedの変数workNameは出口outの後続アクティビティ名を表します．変数subworkNameは出口resの後続アクティビティ名を表します．変数productionTime1は出口outの後続アクティビティの所要時間を表します．変数productionTime2は出口resの後続アクティビティの所要時間を表します．

|宣言ファイル名|変数|入口|出口|主な役割|
|---|---|---|---|---|
|Create.ned|timeLimit，intervalTime|なし|out|メッセージを作成する|
|Accumulate.ned|workUnit|in|out|２個のメッセージ受け取りをマージする|
|BlockingQueue.ned|qName，initialQnum|in|out|待ち行列を表す|
|Select.ned|productionTime1，productionTime2|in|out|工程の全ての待ち行列にメッセージが届くことを待つ|
|Process.ned|workName，subworkName，productionTime1，productionTime2|in|out|特に何もしないが後続アクティビティがある）|

　主要なモジュールであるSelectとBlokingQueueについて説明します．

　Selectモジュールの主な機能は前工程の３つの待ち行列にメッセージが全て揃うタイミングを待って積込作業を行うことです．変数before1，before2，before3に待ち行列BlockingQueueモジュールへの参照を表し，BlockingQueueのcheckメソッドを呼び出すことにより待ち行列の状態を確認します．変数onLoading1と2は積込機の使用状況を表します．Trueであれば使用中です．変数loader1と2は積込機の使用終了時刻を表します．変数stats1と2は積込機1と2の稼働時間を表します．変数lastTimeはrequestメソッドが最後に呼ばれたタイミングをセットしますが，最後のトラックが待機場所に戻ってきた時間を表し，稼働率を計算する際の分母の値として利用されます．
　initializeメソッドは各種変数の初期化を行い，handleMessageメソッドは到着メッセージの名前を見て積込機の種類を判断して使用を終了します．requestメソッドはBlockingQueueモジュールから呼び出され，前工程の全ての待ち行列にメッセージがあれば積込作業を行います．finishメソッドはモジュールを解放する際に統計情報を表示します．

```Select.h
#ifndef SELECT_H_
#define SELECT_H_

#include <omnetpp.h>
using namespace omnetpp;

class BlockingQueue;

class Select : public cSimpleModule {
private:
    BlockingQueue *before1;
    BlockingQueue *before2;
    BlockingQueue *before3;
    bool onLoading1;        // 1号機を利用中
    bool onLoading2;        // 2号機を利用中
    simtime_t loader1;      // 1号機の利用終了時刻
    simtime_t loader2;      // 2号機の利用終了時刻
    cStdDev stats1;
    cStdDev stats2;
    simtime_t lastTime;     // 最終シミュレーション時間の記録用

protected:
    virtual void initialize();
    virtual void handleMessage(cMessage *msg);
    virtual void finish();

public:
  virtual void request();
};

#endif /* SELECT_H_ */
```

```Select.cc
// 三つの待ち行列の準備が出来たらジョブを行う

#include "Select.h"
Define_Module(Select);
#include "BlockingQueue.h"

void Select::initialize()
{
    // 変数初期化
    onLoading1 = false;
    onLoading2 = false;
    loader1 = simTime();
    loader2 = simTime();

    // BlockingQueueノードのポインタを取得
    cModule *mod1 = gate("in1")->getPreviousGate()->getOwnerModule();
    cModule *mod2 = gate("in2")->getPreviousGate()->getOwnerModule();
    cModule *mod3 = gate("in3")->getPreviousGate()->getOwnerModule();
    before1 = check_and_cast<BlockingQueue *>(mod1);
    before2 = check_and_cast<BlockingQueue *>(mod2);
    before3 = check_and_cast<BlockingQueue *>(mod3);
}

// 積込アクティビティの終了
void Select::handleMessage(cMessage *msg)
{
    if (msg->isSelfMessage()) {
        if (strcmp(msg->getName(), "loading1") == 0) {
            onLoading1 = false;     // 積込機1の利用終了
            loader1 = simTime();    // 利用終了時刻
            stats1.collect(simTime() - msg->getCreationTime()); // 稼働時間を記録
        }
        else {
            onLoading2 = false;     // 積込機2の利用終了
            loader2 = simTime();    // 利用終了時刻
            stats2.collect(simTime() - msg->getCreationTime()); // 稼働時間を記録
        }
        send(msg, "out");
    }
}

// 全ての待ち行列の準備が出来ていれば，積込作業を行う
void Select::request()
{
    Enter_Method("request");

    // シミュレーション終了時刻を記録
    lastTime = simTime();

    // 全ての待ち行列の準備が出来ていることを確認
    if ( !before1->check() )  return;
    if ( !before2->check() )  return;
    if ( !before3->check() )  return;

    // 待ち行列の在庫を減らす
    before1->dispatch(lastTime);
    before2->dispatch(lastTime);
    before3->dispatch(lastTime);

    // 待ち時間が長い方の積込機を利用して積込作業を行う
    if (!onLoading1 && loader1 <= loader2) {     // 積込機1の待ち時間が長い
        onLoading1 = true;
        cMessage *msg = new cMessage("loading1");   // メッセージを生成
        scheduleAt(simTime() + par("productionTime1"), msg);
    }
    else if (!onLoading2) {         // もし積込機2が未使用ならば
        onLoading2 = true;
        cMessage *msg = new cMessage("loading2");   // メッセージを生成
        scheduleAt(simTime() + par("productionTime2"), msg);
    }
}

void Select::finish()
{
    EV << "Loader1 jobs Count:        " << stats1.getCount() << endl;
    EV << "Loader1 jobs Min leadtime: " << stats1.getMin() << endl;
    EV << "Loader1 jobs Avg leadtime: " << stats1.getMean() << endl;
    EV << "Loader1 jobs Max leadtime: " << stats1.getMax() << endl;
    EV << "Loader1 jobs SD:           " << stats1.getStddev() << endl;
    EV << "Loader1 Utilization:       " << stats1.getSum() / lastTime << endl;
    EV << "Loader2 jobs Count:        " << stats2.getCount() << endl;
    EV << "Loader2 jobs Min leadtime: " << stats2.getMin() << endl;
    EV << "Loader2 jobs Avg leadtime: " << stats2.getMean() << endl;
    EV << "Loader2 jobs Max leadtime: " << stats2.getMax() << endl;
    EV << "Loader2 jobs SD:           " << stats2.getStddev() << endl;
    EV << "Loader2 Utilization:       " << stats2.getSum() / lastTime << endl;
}
```

　BlockingQueueモジュールの主な機能は待ち行列の管理です．変数qNameは待ち行列の名前を表します．変数queueは待ち行列本体を表します．変数nextは次工程のSelectノードへの参照をです．変数lastTimeは稼働率を計算するための分母を表します．変数statsは統計情報を表します．変数qlenは待ち行列の平均長さを表します．
　initializeメソッドは各種変数の初期化を行います．初期値としてomnetpp.iniに指定された「initialQnum」の数のメッセージを待ち行列にセットします．handleMessageメソッドは到着したメッセージを待ち行列に入れてSelectノードに報告します．checkメソッドはSelectノードに待ち状態を返します．dispatchメソッドはSelectノードから呼ばれて待ち行列のメッセージを１つ削除します．finishメソッドはモジュールを解放する際に統計情報を表示します．

```BlockingQueue.h
#ifndef BLOCKINGQUEUE_H_
#define BLOCKINGQUEUE_H_

#include <omnetpp.h>
using namespace omnetpp;
#include "Wavg.h"
#define maxQnum 100

class Select;

class BlockingQueue : public cSimpleModule
{
  private:
    const char *qName;
    cQueue queue;
    Select *next;
    simtime_t lastTime;     // 最終シミュレーション時間の記録用
    cStdDev stats;
    Wavg qlen;

  protected:
    virtual void initialize();
    virtual void handleMessage(cMessage *msg);
    virtual void finish();

  public:
    virtual bool check();
    virtual void dispatch(simtime_t t);
};

#endif /* BLOCKINGQUEUE_H_ */
```

```BlockingQueue.cc
// 初期値のある待ち行列

#include "BlockingQueue.h"
Define_Module(BlockingQueue);
#include "Select.h"

void BlockingQueue::initialize()
{
    // キューの初期化．指定された名前のメッセージを指定個数だけキューに入れる
    queue.setName("queue");             // GUIに待ち行列長さを表示するための名前
    qName = par("qName");               // 待ち行列の名前
    int imax = par("initialQnum");
    for (int i = 0;  i < imax;  i++)
        queue.insert(new cMessage(qName));

    // 待ち行列の平均長さを計算するクラスを初期化
    qlen.init(simTime(), maxQnum);

    // Selectノードのポインタを取得
    cModule *mod = gate("out")->getNextGate()->getOwnerModule();
    next = check_and_cast<Select *>(mod);
}

void BlockingQueue::handleMessage(cMessage *msg)
{
    msg->setTimestamp(simTime());           // リードタイムの開始時間をセット
    qlen.set(simTime(), queue.getLength()); // 待ち行列長さを集計
    queue.insert(msg);                      // 待ち行列にメッセージを保管
    next->request();                        // 次工程を呼び出し
}

// 待ち行列があるならばTrueを返す
bool BlockingQueue::check()
{
    Enter_Method("check");
    return (queue.getLength() > 0) ? true: false;
}

// 待ち行列があるならば統計情報をとって削除する
void BlockingQueue::dispatch(simtime_t t)
{
    Enter_Method("dispatch");

    // 最終実行時刻の記録
    lastTime = t;

    if (queue.getLength() > 0) {    // 在庫が在れば
        qlen.set(simTime(), queue.getLength());                     // 待ち行列長さの加重平均値の計算
        cMessage *msg = check_and_cast<cMessage *>(queue.pop());    // 待ち行列先頭のメッセージを取り出す
        stats.collect(simTime() - msg->getTimestamp());             // 滞留時間の統計情報を保管
        delete msg;                 // メッセージは削除
    }
}

//　ノードの統計情報を表示
void BlockingQueue::finish()
{
    EV << qName << " AVG Length: " << qlen.get(lastTime) << endl;
    EV << qName << " Jobs Count: " << stats.getCount() << endl;
    EV << qName << " Min Time:   " << stats.getMin() << endl;
    EV << qName << " Avg Time:   " << stats.getMean() << endl;
    EV << qName << " Max Time:   " << stats.getMax() << endl;
    EV << qName << " SD:         " << stats.getStddev() << endl;
    EV << qName << " Utilization " << stats.getSum() / lastTime << endl;
}
```

#4.設定ファイル（omnetpp.ini）の準備
　omnetpp.iniの内容は以下の通りです．シミュレーション時間は1440秒としていますが，実際にはcreateノードの設定により480秒を少し回った時点で終了します．

```omnetpp.ini
[General]
network = Simulation01
record-eventlog = true

sim-time-limit = 1440s
cpu-time-limit = 1440s
total-stack = 7MiB  # increase if necessary
cmdenv-express-mode = true
cmdenv-event-banners = true
cmdenv-performance-display = false

[Config Run1]
*.create.timeLimit = 480.0s
*.create.intervalTime = erlang_k(2,8.0s)
*.accumulate.workUnit = 2
*.track.qName = "track"
*.track.initialQnum = 4
*.load.qName = "load"
*.load.initialQnum = 0
*.worker.qName = "worker"
*.worker.initialQnum = 2
*.select.productionTime1 = exponential(14.0s)
*.select.productionTime2 = exponential(12.0s)
*.process1.workName = "carry"
*.process1.subworkName = "return-worker"
*.process1.productionTime1 = normal(22.0s, 3.0s)
*.process1.productionTime2 = 5s
*.process2.workName = "unload"
*.process2.subworkName = "dummy"
*.process2.productionTime1 = uniform(2.0s, 8.0s)
*.process2.productionTime2 = 0s
*.process3.workName = "dummy"
*.process3.subworkName = "return-truck"
*.process3.productionTime1 = 0s
*.process3.productionTime2 = normal(18.0s, 3.0s)
```

#5.結果の確認
　実行結果は以下の通りです．実行環境が異なるため全く同じ結果にはなりませんが，ノード間のメッセージ交換状況を見る限り想定通りのシミュレーションを再現できたと考えます．

|項目|今回の結果|教科書|
|---|---|---|
|積込機1の処理件数|14|16|
|積込機2の処理件数|15|13|
|トラックの平均待ち行列長さ|0.762585|0.8554|
|盛土の平均待ち行列長さ|0.880421|0.6629|
|積込機の平均待ち行列長さ|0.563072|1.1249|
|積込機1の稼働率|0.253059|0.3014|
|積込機2の稼働率|0.31659|0.2830|

![26-04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507413/7368956b-958b-1b08-6556-cd1dade11d5a.png)

#6.参考資料
SLAMⅡによるシステム・シミュレーション入門（初版発行:1993年，著者:森戸晋，中野一夫，相沢りえ子，発行者:構造計画研究所発行，発売所:共立出版）

#7.改訂履歴
|改定日|改定内容|
|---|---|
|2021.4.3|初期登録|