1.はじめに
今回はサービスが2つある待ち行列のシミュレーションを紹介します。例題は前回同様に平鍋氏の「サルでもわかる待ち行列」からの引用です。
2.シミュレーションモデルの説明
今回のモデルはサービスが2つあり(医者が2人)、窓口から待合室にかけてネットワークが分岐します。このとき2種類のロジックにてシミュレーションが可能であり、1つ目はランダムに待合室を選択する方法、もう1つは待ち行列の短い待合室を選択する方法です。
3.ソースコードの説明
ソースコードをGitHubから入手可能ですので、前回説明を参考にOMNeT++ IDEに取り込んで下さい。主な変更点について以下に説明します。
ファイル名 | 内容 | 主な変更点 |
---|---|---|
Simulation.ned | ノード及びノード間のネットワークを定義 | 追加したノードとノード間接続の定義 |
Create.ned | 窓口モジュールのパラメータやコネクションの定義 | 分岐に関するパラメータの追加 |
Create.h | 窓口モジュールのヘッダー | 分岐に関するクラスプロパティやメソッドの追加 |
Create.cc | 窓口モジュールのソース | 分岐に関するロジックの実装 |
Queue.cc | 待合室モジュールのソース | 待合室から窓口に待ち状況を連絡 |
Simlation.nedの変更点は(1)窓口ノード(enter)に待ち状況を表示するため@displayプロパティを変更、(2)待合室ノード(wait)と医師ノード(doctor)を配列に変更、(3)ノード追加に起因する経路追加、(4)待合室ノードから窓口ノードに待ち状況を連絡するための経路追加の4点です。
network Simulation01
{
(中略)
submodules:
enter: Create {
parameters:
- @display("p=74,88;i=block/source");
+ @display("p=74,88;i=block/source;t"); //(1)textタグを追加
}
- wait: Queue {中略}
+ wait[2]: Queue {中略} //(2)配列に変更
- doctor: Sink {中略}
+ doctor[2]: Sink {中略} //(2)配列に変更
connections:
- enter.out --> wait.in++;
+ enter.out++ --> wait[0].in++; //(3)窓口から待合室0の経路
+ enter.out++ --> wait[1].in++; //(3)窓口から待合室1の経路
+ wait[0].out2 --> enter.in++; //(4)待合室0から窓口への経路
+ wait[1].out2 --> enter.in++; //(4)待合室1から窓口への経路
- wait.out --> doctor.in;
+ wait[0].out --> doctor[0].in; //(3)待合室0から医師0への経路
+ wait[1].out --> doctor[1].in; //(3)待合室1から医師1への経路
- doctor.out --> wait.in++;
+ doctor[0].out --> wait[0].in++; //(3)医師0から待合室0への経路
+ doctor[1].out --> wait[1].in++; //(3)医師1から待合室1への経路
}
Create.nedの変更点は(1)窓口の分岐ロジックを選択するためのmodeパラメータ追加、(2)窓口の分岐数を指定するためのforkNumberパラメータ追加、(3)待合室から待ち状況を受信するためinゲートを定義、(4)複数の待合室に送信するためoutゲートを配列化の4点です。
simple Create
{
parameters:
+ string mode; //(1)窓口の分岐ロジック用
+ int forkNumber = default(0); //(2)窓口の分岐数指定用
volatile double intervalTime @unit(s) = default(0);
gates:
+ input in[]; //(3)待合室からの受信用。待合室が複数なので配列
- output out;
+ output out[]; //(4)待合室への送信用。待合室が複数なので配列
}
Create.hの変更点は(1)は分岐ロジックのタイプを格納するmodeプロパティの定義、(2)最大分岐数を格納するmaxプロパティの定義、(3)は各待合室の待ち行列長さを格納するnxtプロパティの定義、(4)はenterノードに各待合室の待ち行列長さを表示するメソッド宣言の4点です。
(前略)
+ #include <map> //(3)std::mapを使用するため
class Create : public cSimpleModule {
protected:
+ std::string mode; //(1)分岐ロジック種類の保管用
+ int max; //(2)分岐数の保管用
+ std::map<long, long> nxt; //(3)各待合室の待ち状況
virtual void initialize();
virtual void handleMessage(cMessage *msg);
+ virtual void showGUI(int, int); //(4)窓口ノードに待ち状況を表示
};
(後略)
Create.ccの変更点は(1)各プロパティの初期化、(2)WATCHマクロによる監視設定、(3)窓口に患者が到着した際の処理、(4)待合室から待ち状況が届いた際の処理、(5)シミュレーション実行画面に待ち状況を表示の5点です。
(前略)
void Create::initialize()
{
+ mode = par("mode").stringValue(); //---(1)
+ max = par("forkNumber").intValue(); //---(1)
+ for (int i = 0; i < max; i++) nxt[i] = 0; //---(1)
+ WATCH_MAP(nxt); //---(2)
scheduleAt(simTime() + par("intervalTime"), new cMessage("beat"));
}
void Create::handleMessage(cMessage *msg)
{
+ if ( msg->isSelfMessage() ) { //---(3)
+ int j = intuniform(0, max-1); //---(3)
+ if (mode == "minimum") //---(3)
+ j = (nxt[0] <= nxt[1]) ? 0: 1; //---(3)
+ nxt[j]++; //---(3)
+ showGUI(nxt[0], nxt[1]); //---(3)
send(new cMessage("patient"), "out", j);
scheduleAt(simTime() + par("intervalTime"), msg);
+ } else { //---(4)
+ int j = (strcmp(msg->getArrivalGate()->getFullName(), "in[0]") == 0) ? 0: 1; //---(6)
+ nxt[j] = msg->getKind(); //---(4)
+ showGUI(nxt[0], nxt[1]); //---(4)
+ delete msg; //---(4)
+ }
}
+ void Create::showGUI(int a1, int a2) //---(5)
+ {
+ std::string str = "Room0:" + std::to_string(a1) + " ,Room1:" + std::to_string(a2);
+ getDisplayString().setTagArg("t", 0, str.c_str());
+ }
Queue.ccの変更点は、(1)待ち状況を窓口に連絡する処理の追加の1点です。
(前略)
void Queue::handleMessage(cMessage *msg)
{
if (strcmp(msg->getName(), "patient") == 0) {
msg->setTimestamp(simTime());
queue.insert(msg);
send(new cMessage("request"), "out");
} else if (strcmp(msg->getName(), "call") == 0) {
if (queue.getLength() > 0) {
cMessage *patient = check_and_cast<cMessage *>(queue.pop());
waitTime.collect(simTime() - patient->getTimestamp());
send(patient, "out");
+ cMessage* info = new cMessage("status"); //---(1)
+ info->setKind(queue.getLength()); //---(1)
+ send(info,"out2"); //---(1)
}
delete msg;
}
}
(後略)
なお、Sink.ccに変更はありません。
4.例題の実行
シミレーションをGeneralにて実行します。この場合は行き先の待合室がランダムに決まります。
結果はwait0ノードの平均待ち時間が5.10736分、wait1ノードが5.53126分、加重平均値が5.32分となりました。公式(M/M/1)の解は5.3分なので想定通りの結果と言えるでしょう。
次にシミレーションをRun1にて実行します。この場合、患者は待ち人数の少ない待合室に案内されます。
結果は加重平均値が3.7分(wait0ノードが3.98062分、wait1ノードが2.95929分)となり待ち時間が約3割改善されました。このようにロジックを変更した場合の効果を簡単に確認できる点がシミュレーションにて問題を解く理由の1つです。
5.Tips
デバッグのためにシミュレーション実行時の変数値を確認する方法を3つ紹介します。1つ目はEVマクロを利用してログに出力する方法です。使い方はC++のストリームと同様です。
EV << "Queue Jobs Count: " << waitTime.getCount() << endl;
2つ目はsetTagArgメソッドを利用してシミュレーション実行時のノードに値を表示する方法です。まずSimulation.nedにて当該ノードの@displayタグにtextタグを追加します。
@display("p=74,88;i=block/source;t");
次に当該ノードのモジュールからsetTagArgメソッドによりtextタグに値をセットします。
std::string str = "Room0:" + std::to_string(a1) + " ,Room1:" + std::to_string(a2);
getDisplayString().setTagArg("t", 0, str.c_str());
シミュケーション実行時に画面右上のGUIの当該ノードに値が表示されます。
最後はWATCHマクロを利用する方法です。まずモジュールのクラスに監視するプロパティを定義します。
std::map<long, long> nxt;
次にモジュールのinitializeメソッドにて監視するプロパティをWATCHマクロにて指定します。WATCHマクロの種類はWATCH()、WATCH_VECTOR()、WATCH_LIST()、WATCH_OBJ()など12種類ありますが、以下の場合は監視対象がstd::mapなのでWATCH_MAPを使います。
WATCH_MAP(nxt);
シミュレーション実行時に画面左下のインスペクタウィンドウに値が表示されます。
6.次回予告
次回は「サルでもわかる待ち行列」から、待ち行列が1つで医者が2人のケースのシミュレーションを紹介します。
7.改訂履歴
改定日 | 改定前 | 改定後 |
---|---|---|
2022.4.22 | - | 初期登録 |