#1.はじめに
第5回目となる今回は教科書である「SLAMⅡによるシステム・シミュレーション入門」から例題2.7「病院内カルテ搬送システム」に取り組みます.メインテーマはメッセージのバッチ化(BATCH)と到着待ち(MATCH)です.
# | 例題 | タイトル | テーマ |
---|---|---|---|
第1回 | 例題2.1 | 2つの機械工程を持つ生産ライン | QUEUEノード |
第2回 | 例題2.3 | 故障を伴う機械システムのシミュレーション | RESOURCEブロック |
第3回 | 例題2.5 | 一車線通行の信号システム | GATEブロック |
第4回 | 例題2.6 | トラック運送システム | SELECTノード |
第5回 | 例題2.7 | 病院内カルテ搬送システム(今回) | BATCHノード等 |
#2.例題の概要
例題2.7は病院のカルテ搬送システムの問題です.病院に外来窓口が3つあり,それそれ平均10分,15分,17分の指数分布の到着間隔にて患者が到着します.受付が終わると患者は診察室に向かいますが,患者のうち30%は事前に検査を受けます.診察室の前には待合室があり,カルテの到着を待ってから診察室に入ります.カルテは受付から連絡があるとカルテ保管室から搬送車に積み込まれ,3つ溜まると搬送車によって一度に診察室に搬送されます.搬送車の台数は1台のみです.各作業の所要時間は表の通りです.
場所 | 作業内容 | 作業時間 |
---|---|---|
外来受付 | 受付 | 1~3分の一様分布 |
検査室 | 検査を受けてから待合室に移動 | 平均8分,標準偏差1分の正規分布 |
廊下 | 受付から待合室まで直接移動 | 平均3分,標準偏差0.5分の正規分布 |
カルテ保管室 | カルテを探して搬送車に積込み | 1分 |
搬送車ルート | カルテ保管室から診察室まで | 2分 |
搬送車ルート | 診察室からカルテ保管室まで | 3分 |
#3.例題のネットワーク図
create1~3の各ノードは平均10分,平均15分,平均17分の指数分布に従う間隔にてメッセージを生成しますが,これは外来患者の到着を表します.次のqueue1~3ノードは受付手続きの待ち行列を表します.次のassignノードは到着した患者に連番を付与します.この連番は後ほどmatchノードにて患者とカルテをマッチングするために必要です.またassignノードはメッセージを複製して,患者ネットワークとカルテネットワークの両方に送り出します.
患者ネットワークを通過するメッセージのうち30%は平均8分,標準偏差1分の正規分布に従う所要時間にてqueue4に届きます.いっぽう残り70%は平均3分,標準偏差0.5分の正規分布に従う所要時間にてqueue4に届きます.前者は検査室を経由する患者の流れ,後者は直接待合室に向かう患者の流れを表します.queue4ノードは診察室前にある患者待合室を表します.
カルテネットワークでは先ずbatchノードが届いたメッセージをaddObjectメソッドにより新しいメッセージに付与し,3つ溜まったら次工程に送ります.次のawaitノードはresourceブロックに搬送車のリソースを要求し,獲得出来れば所用時間2分にてメッセージを次工程に送ります.これは搬送車の診察室への往路を表します.次のprocessノードは次工程にメッセージを転送してから3分後にリソースを開放しますが,これはカルテ保管室に搬送車を戻す処理を表します.次のunbatchノードはメッセージに付随する3つのメッセージをgetObjectメソッドにて取り出してqueue5ノードに送ります.
最後にmatchノードがqueue4ノードやqueue5ノードに届いたメッセージを比較して連番が一致すれば待ち行列から取り出し,terminateノードに送ります.これは患者の待合室から診察室への移動を表します.
#4.ソースコードの説明
メニューから「File」~「New」~「OMNeT++ Project」を選択して適当なプロジェクト名を付与して下さい(今回はSimSlam2.7としています).プロジェクトを構成するソースファイルは表の通りですが,GitHub(https://github.com/tsugulin/SimSlam2.7)から入手可能です.git cloneにてローカルにダウンロードし,Windowsのファイル・エクスプローラーからIDEのプロジェクト・エクスプローラーにドラッグ&ドロップすることにより取り込み可能です.
フォルダ名 | ファイル名 | 説明 |
---|---|---|
simulations | omnetpp.ini | シミュレーションのパラメータ変数の定義 |
simulations | Simulation01.ned | ノードとモジュールの関連付け(=ネットワーク)を定義 |
src | Create.ned | メッセージを生成するモジュールの宣言 |
src | BlockingQueue.ned | 外来患者の待ち行列を表すモジュールの宣言 |
src | Assign.ned | メッセージに連番を付与するモジュールの宣言 |
src | Array.ned | 診察室前の患者とカルテの待ち行列を表すモジュールの宣言 |
src | Match.ned | 患者とカルテのマッチングを行うモジュールの宣言 |
src | Batch.ned | 複数メッセージをバッチ化して新しいメッセージを作成するモジュールの宣言 |
src | Unbatch.ned | バッチ化したメッセージから複数のメッセージを取り出すモジュールの宣言 |
src | Resource.ned | 搬送車のリソースを管理するブロックの宣言 |
src | Lockup.ned | 搬送車リソースの獲得を要求するモジュールの宣言 |
src | Process.ned | 搬送車リソースの開放を行うモジュールの宣言 |
src | Create.cc | Createモジュールの定義 |
src | Create.h | Createモジュールのクラス宣言 |
src | BlockingQueue.cc | BlockingQueueモジュールの定義 |
src | BlockingQueue.h | BlockingQueueモジュールのクラス宣言 |
src | Assign.cc | Assignモジュールの定義 |
src | Assign.h | Assignモジュールのクラス宣言 |
src | Array.cc | Arrayモジュールの定義 |
src | Array.h | Arrayモジュールのクラス宣言 |
src | Match.cc | Matchモジュールの定義 |
src | Match.h | Matchモジュールのクラス宣言 |
src | Batch.cc | Batchモジュールの定義 |
src | Batch.h | Batchモジュールのクラス宣言 |
src | Unbatch.cc | Unbatchモジュールの定義 |
src | Unbatch.h | Unbatchモジュールのクラス宣言 |
src | Resource.cc | Resourceブロックの定義 |
src | Resource.h | Resourceブロックのクラス宣言 |
src | Await.cc | Lockupモジュールの定義 |
src | Await.h | Lockupモジュールのクラス宣言 |
src | Process.cc | Processモジュールの定義 |
src | Process.h | Processモジュールのクラス宣言 |
src | Wavg.cc | 待ち行列長さの加重平均を計算するクラスの定義 |
src | Wavg.h | Wavgクラスの宣言 |
例題2.7のネットワーク定義はSimulation01.nedの内容の通りです.新しいテクニックとして診察室前で待っている患者やカルテの連番情報を表示するため,queue4とqueue5の@displayプロパティにtパラメータを使用しています.表示内容はモジュールを定義するプログラム中からgetDisplayString().setTagArgメソッドにて指定することが出来ます.
network Simulation01
{
submodules:
create1: Create {
parameters:
@display("p=50,50;i=block/source");
}
create2: Create {
parameters:
@display("p=50,150;i=block/source");
}
create3: Create {
parameters:
@display("p=50,250;i=block/source");
}
queue1: BlockingQueue {
parameters:
@display("p=150,50;i=block/boundedqueue;q=queue");
}
queue2: BlockingQueue {
parameters:
@display("p=150,150;i=block/boundedqueue;q=queue");
}
queue3: BlockingQueue {
parameters:
@display("p=150,250;i=block/boundedqueue;q=queue");
}
assign: Assign {
parameters:
@display("p=250,150;i=block/control");
}
queue4: Array {
parameters:
@display("p=750,150;i=block/boundedqueue;t");
}
batch: Batch {
parameters:
@display("p=350,250;i=block/join");
}
await: Lockup {
parameters:
@display("p=450,250;i=block/circle;q=queue");
}
process: Process {
parameters:
@display("p=550,250;i=block/process");
}
unbatch: Unbatch {
parameters:
@display("p=650,250;i=block/fork");
}
queue5: Array {
parameters:
@display("p=750,250;i=block/boundedqueue;t");
}
match: Match {
parameters:
@display("p=850,150;i=block/switch");
}
terminate: Terminate {
parameters:
@display("p=950,150;i=block/sink");
}
resource: Resource {
parameters:
@display("p=500,350;i=block/table;t");
}
connections:
create1.out --> queue1.in;
create2.out --> queue2.in;
create3.out --> queue3.in;
queue1.out --> assign.in1;
queue2.out --> assign.in2;
queue3.out --> assign.in3;
assign.out1 --> queue4.in;
queue4.out --> match.in1;
assign.out2 --> batch.in;
batch.out --> await.in;
await.out --> process.in;
process.out --> unbatch.in;
unbatch.out --> queue5.in;
queue5.out --> match.in2;
match.out --> terminate.in;
process.res --> resource.from_release;
await.res --> resource.from_await;
}
各モジュールが使用する変数と入口及び出口は下記表の通りです.
宣言ファイル名 | 変数(内容) | 入口 | 出口 | 主な役割 |
---|---|---|---|---|
Create.ned | intervalTime(メッセージ生成の頻度) | なし | out | メッセージを作成する |
BlockingQueue.ned | qName(待ち行列の名前), workTime(受付待ち時間) | in | out | 外来患者の待ち行列 |
Assign.ned | workTime1(検査室経由で診察室に行く場合の所要時間), workTime2(直接診察室に行く場合の所要時間), workTime3(患者のカルテを探すのにかかる時間) | in1(外来受付1),in2(外来受付2),in3(外来受付3) | out1(患者),out2(カルテ) | 連番を付与 |
Array.ned | qName(待ち行列の名前) | in | out | 診察室前の待ち行列 |
Match.ned | なし | in1(患者待ち行列), in2(カルテ待ち行列) | out | 患者とカルテのマッチング |
Batch.ned | workUnit(搬送車に積み込むカルテの数) | in | out | workUnit個のメッセージを1つのメッセージにまとめる |
Unbatch.ned | なし | in | out | メッセージに付随する複数のメッセージを取り出す |
Resoucre.ned | なし | from_await(リソース要求用), from_release(リソース解放用) | なし | 搬送車リソースの管理 |
Lockup.ned | workTime(カルテ保管室から診察室までの所要時間) | in | out, res(リソース要求用) | リソースの要求と獲得 |
Process.ned | workTime(診察室からカルテ保管室までの所要時間) | in | out, res(リソース解放用) | workTime秒後にリソースを開放 |
Terminate.ned | なし | in | なし | メッセージの消滅 |
主要なモジュールであるBatch/Unbatch/Array/Matchについて説明します.
Batchモジュールはカルテの搬送車への積込作業の実装であり,到着メッセージを指定個数まとめて次工程に送ることが主な役割です.変数imaxはバッチとしてまとめるメッセージの収容個数を表します.変数idxは現在滞留中のメッセージ個数を表します.変数msgsはメッセージを保管する待ち行列の参照です.変数batchは次工程に送るバッチ・メッセージの参照です.変数statsは統計情報を表します.
initializeメソッドはomnetpp.iniからパラメータを読み込み変数imaxに指定個数をセットします.handleMessageメソッドは到着したメッセージを待ち行列msgsにinsertして,滞留個数を変数idxにてカウントします.変数idxが指定個数imaxを超えると待ち行列msgsをbatchメッセージにaddObjectして次工程にメッセージを送ります.finishメソッドはモジュールを解放する際に処理件数等の統計情報を表示します.
#ifndef BATCH_H_
#define BATCH_H_
#include <omnetpp.h>
using namespace omnetpp;
class Batch : public cSimpleModule {
private:
long imax; // インデックスの最大値
long idx; // メッセージ配列のインデックス
cQueue *msgs;
cMessage *batch;
cStdDev stats;
protected:
virtual void initialize();
virtual void handleMessage(cMessage *msg);
virtual void finish();
};
#endif /* BATCH_H_ */
#include "Batch.h"
Define_Module(Batch);
void Batch::initialize()
{
imax = par("workUnit"); // バッチにて集約する個数
idx = 0; // 連番を初期化
msgs = new cQueue("msgs"); // オブジェクトを生成
batch = new cMessage("batch"); // バッチメッセージの作成
}
void Batch::handleMessage(cMessage *msg)
{
// 前工程から届いたメッセージの場合,アクティビティを開始
msgs->insert(msg); // バッチにメッセージを追加
if (++idx >= imax) {
// 必要個数溜まった場合
batch->addObject(msgs); // メッセージに待ち行列を追加
send(batch, "out"); // 次工程にバッチメッセージを送信
idx = 0; // 連番を初期化
msgs = new cQueue("msgs"); // オブジェクトを生成
batch = new cMessage("batch"); // バッチメッセージの作成
stats.collect(0); // 送信個数を記録
}
}
// 処理件数を表示
void Batch::finish()
{
EV << "Batch jobs Count: " << stats.getCount() << endl;
delete msgs;
delete batch;
}
Unbatchモジュールは搬送車に積まれたカルテの搬出作業の実装であり,バッチ化されたメッセージの復元が主な役割です.クラス変数は統計情報を保管するstatsのみです.
handleMessageメソッドは到着したmsgオブジェクト(クラスはcMessage)からgetObjectメソッドによりmsgsオブジェクトを取り出し,msgsオブジェクトからpopメソッドにより各メッセージを取り出して次工程に送ります.finishメソッドはモジュールを解放する際に処理件数を表示します.
#ifndef UNBATCH_H_
#define UNBATCH_H_
#include <omnetpp.h>
using namespace omnetpp;
class Unbatch : public cSimpleModule
{
private:
cStdDev stats;
protected:
virtual void handleMessage(cMessage *msg);
virtual void finish();
};
#endif /* UNBATCH_H_ */
#include "Unbatch.h"
Define_Module(Unbatch);
void Unbatch::handleMessage(cMessage *msg)
{
if (msg->hasObject("msgs")) {
// メッセージがオブジェクトを含んでいる場合
cObject *obj = msg->getObject("msgs"); // メッセージの待ち行列を取り出し
cQueue *msgs = check_and_cast<cQueue *>(obj);
while (!msgs->isEmpty()) {
send(check_and_cast<cMessage *>(msgs->pop()), "out"); // キューに溜まっているメッセージを次工程に送出
stats.collect(0); // 送信個数を記録
}
}
delete msg;
}
// 処理件数を表示
void Unbatch::finish()
{
EV << "Unbatch jobs Count: " << stats.getCount() << endl;
}
Arrayモジュールは診察室前の待ち行列の実装であり,Matchモジュールと連携して患者とカルテのマッチング処理を行います.まずinitializeメソッドがomnetpp.iniファイルを読み込み変数qNameに待ち行列の名前(Queue4またはQueue5)をセットします.また変数nextにMatchノードへの参照をセットします.次のhandleMessageメソッドは到着したメッセージのKind属性をキーにMatchノードのcheckメソッドを呼び出してペアとなる要素を探します(例えば患者の待ち行列Queue4に2番の患者が到着した際にカルテの待ち行列Queue5から2番の患者のカルテを探す).もし見つかった場合は次工程のMatchノードにメッセージを送りますが,存在しない場合はaddAtメソッドにてKind属性をキーとしてArrayオブジェクトに保管します.checkメソッドはMatchノードから呼び出され,指定されたキーの患者やカルテの存在有無を返します.またremoveメソッドはMatchノードから呼び出され,指定されたキーの患者やカルテを削除します.displayArrayメソッドはシミュレーション実行画面に配列中の要素を一覧表示します.finishメソッドはモジュールを解放する際に平均待ち行列長さ等の統計情報を表示します.
#ifndef ARRAY_H_
#define ARRAY_H_
#include <omnetpp.h>
using namespace omnetpp;
#include "Wavg.h"
#define maxQnum 100
class Match;
class Array : public cSimpleModule
{
private:
const char *qName;
cArray ary;
long length; // 保管しているメッセージ数
Match *next;
Wavg qlen;
cStdDev stats;
protected:
virtual void initialize();
virtual void handleMessage(cMessage *msg);
virtual void finish();
virtual void displayArray();
public:
virtual bool check(long);
virtual void remove(long);
};
#endif /* ARRAY_H_ */
#include "Array.h"
Define_Module(Array);
#include "Match.h"
void Array::initialize()
{
// 変数初期化
ary.setName("queue"); // GUIに待ち行列長さを表示するための名前
qName = par("qName"); // 待ち行列の名前
qlen.init(simTime(), maxQnum); // 待ち行列の平均長さを計算するクラスを初期化
length = 0; // 待ち行列数を初期化
// 次工程のマッチノードを取得
cModule *mod = gate("out")->getNextGate()->getOwnerModule();
next = check_and_cast<Match *>(mod);
}
void Array::handleMessage(cMessage *msg)
{
long idx = msg->getKind(); // 患者やカルテの番号をセット
if (next->check(qName, idx)) {
// マッチング先のカルテまたは患者がある場合
send(msg, "out"); // 次工程に送信
stats.collect(0);
}
else {
// マッチング先のペアがない場合
qlen.set(simTime(), length); // 待ち行列長さの加重平均値の計算
length++; // 要素数を増やす
msg->setTimestamp(simTime()); // リードタイム開始時間をセット
ary.addAt(idx, msg); // メッセージを配列に保管
displayArray(); // 配列の状況を表示
}
}
// マッチノードから呼び出され,同じ番号の要素が存在すればTrueを返す
bool Array::check(long idx)
{
Enter_Method("check");
return ary.exist(idx);
}
// マッチノードから呼び出され,同じ番号の要素が削除する
void Array::remove(long idx)
{
Enter_Method("remove");
if (ary.exist(idx)) {
qlen.set(simTime(), length); // 待ち行列長さの加重平均値の計算
length--; // 要素数を減らす
cMessage *msg = check_and_cast<cMessage *>(ary[idx]); // メッセージ取り出し
stats.collect(simTime() - msg->getTimestamp()); // キュー滞留時間を保管
delete ary.remove(idx); // 配列とメッセージを削除
displayArray(); // 配列の状況を表示
}
}
// シミュレーション実行画面のアイコンに待ち状況を表示する
void Array::displayArray()
{
std::string str; // 空の文字列を作成
for (cArray::Iterator it(ary); !it.end(); it++) {
cMessage *msg = check_and_cast<cMessage *>(*it);
str += "," + std::to_string(msg->getKind()); // 患者やカルテの連番を列記
}
str.erase(0,1); // 先頭文字を削除
getDisplayString().setTagArg("t", 0, str.c_str()); // シミュレーション画面に表示
}
// ノードの統計情報を表示
void Array::finish()
{
EV << qName << " AVG Length: " << qlen.get(simTime()) << endl;
EV << qName << " Jobs Count: " << stats.getCount() << endl;
EV << qName << " Min WaitTime: " << stats.getMin() << endl;
EV << qName << " Avg WaitTime: " << stats.getMean() << endl;
EV << qName << " Max WaitTime: " << stats.getMax() << endl;
EV << qName << " Standard deviation: " << stats.getStddev() << endl;
}
MatchモジュールはArrayモジュールと連携して診察室前の患者とカルテのマッチング処理を行います.変数mod1とmod2は前工程のArrayモジュール・オブジェクトの参照です.また変数before1とbefore2はArrayモジュールの参照です.変数statsは統計情報を表します.
initializeメソッドは各種変数の初期化を行い,モジュールの入口in1及びin2に接続する前工程モジュールへの参照をmod1,mod2,before1及びbefore2にセットします.handleMessageメソッドは前工程からメッセージが届くと次工程に転送しつつ,ペアとなる待ち行列の要素を削除します.例えばカルテ待ち行列から2番のメッセージが届いた場合は患者待ち行列から2番の要素を削除します.checkメソッドは前工程のArrayモジュールから呼び出され,ペアとなるメッセージの存在有無を返します.例えばQueue4から2番の問合せがあった場合にQueue5に2番の要素が存在すればTrueを返します.finishメソッドはモジュールを解放する際に統計情報を表示します.
#ifndef MATCH_H_
#define MATCH_H_
#include <omnetpp.h>
using namespace omnetpp;
class Array;
class Match : public cSimpleModule
{
private:
cModule *mod1;
cModule *mod2;
Array *before1;
Array *before2;
cStdDev stats;
protected:
virtual void initialize();
virtual void handleMessage(cMessage *msg);
virtual void finish();
public:
virtual bool check(long);
};
#endif /* MATCH_H_ */
#include "Match.h"
Define_Module(Match);
#include "Array.h"
void Match::initialize()
{
// 前工程のポインタを取得
mod1 = gate("in1")->getPreviousGate()->getOwnerModule();
mod2 = gate("in2")->getPreviousGate()->getOwnerModule();
before1 = check_and_cast<Array *>(mod1);
before2 = check_and_cast<Array *>(mod2);
}
void Match::handleMessage(cMessage *msg)
{
if (msg->getSenderModule() == mod1)
before2->remove(msg->getKind());
else
before1->remove(msg->getKind());
send(msg, "out");
}
// もう片方のノードに一致する待ち行列があれば次に送る
bool Match::check(const char *name, long idx)
{
Enter_Method("check");
if (strcmp(name, "Queue4") == 0 && before2->check(idx))
return true;
else if (strcmp(name, "Queue5") == 0 && before1->check(idx))
return true;
else
return false;
}
void Match::finish()
{
EV << "Match jobs Count: " << stats.getCount() << endl;
}
#4.設定ファイル(omnetpp.ini)の準備
omnetpp.iniの内容は以下の通りです.オリジナルでは単位が分ですが,omnetの都合上として単位を秒としています.
[General]
network = Simulation01
record-eventlog = true
sim-time-limit = 480s
cpu-time-limit = 480s
total-stack = 7MiB # increase if necessary
cmdenv-express-mode = true
cmdenv-event-banners = true
cmdenv-performance-display = false
[Config Run1]
*.create1.intervalTime = exponential(10.0s)
*.create2.intervalTime = exponential(15.0s)
*.create3.intervalTime = exponential(17.0s)
*.queue1.workTime = uniform(1.0s, 3.0s)
*.queue2.workTime = uniform(1.0s, 3.0s)
*.queue3.workTime = uniform(1.0s, 3.0s)
*.assign.workTime1 = normal(8.0s, 1.0s)
*.assign.workTime2 = normal(3.0s, 0.5s)
*.assign.workTime3 = 1.0s
*.batch.workUnit = 3
*.await.workTime = 2.0s
*.process.workTime = 3.0s
*.queue1.qName = "Queue1"
*.queue2.qName = "Queue2"
*.queue3.qName = "Queue3"
*.queue4.qName = "Queue4"
*.queue5.qName = "Queue5"
#5.結果の確認
実行結果は以下の通りです.実行環境が異なるため全く同じ結果にはなりませんが,待ち時間の状況やシミュレーション画面の状況を見ると想定通りの結果を得られたと考えます.
項目 | 今回の結果 | 教科書 |
---|---|---|
処理人数 | 114人 | 117人 |
平均処理時間 | 10.2001分 | 10.45分 |
外来受付1の平均待ち行列長さ | 0.0345644 | 0.0191 |
外来受付2の平均待ち行列長さ | 0.0090269 | 0.0054 |
外来受付2の平均待ち行列長さ | 0.000472091 | 0.0202 |
診察室前の患者の平均待ち行列長さ | 0.866895 | 0.8991 |
診察室前のカルテの平均待ち行列長さ | 0.193547 | 0.2338 |
外来受付1の平均待ち時間 | 0.307239 | 0.1954 |
外来受付2の平均待ち時間 | 0.1313 | 0.0742 |
外来受付2の平均待ち時間 | 0.00781392 | 0.2687 |
診察室前の患者の平均時間 | 3.65009 | 3.6887 |
診察室前のカルテの平均時間 | 0.814935 | 0.9594 |
リソース使用率 | 0.395833 | 0.5967 |
#6.今後の予定
基本的なOMMet++のテクニックは習得できましたので,次回以降は教科書の例題を離れて問題作りも含めたシミュレーションに取り組みたいと考えます.
#7.参考資料
SLAMⅡによるシステム・シミュレーション入門(初版発行:1993年,著者:森戸晋,中野一夫,相沢りえ子,発行者:構造計画研究所発行,発売所:共立出版)
#8.改訂履歴
改定日 | 改定内容 |
---|---|
2021.5.1 | 初期登録 |