はじめてのOMNeT++～待ち行列が1つでサービスが2つの場合

# 1.はじめに

今回はサービスが２つで待ち行列が１つの場合のシミュレーションを紹介します。例題は[前回](https://qiita.com/ponse7/items/e2ed05e12c9af10aa0c9)および[前々回](https://qiita.com/ponse7/items/3620f2b2084b451c726e)と同様に平鍋氏の[「サルでもわかる待ち行列」](http://objectclub.jp/technicaldoc/monkey/s_wait/)からの引用です。
![例題イメージ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507413/6a7bbdc6-c381-3e50-5bdb-f5d89f2d6e6e.png)

# 2.シミュレーションモデルの説明
今回は待ち行列は「Ｍ／Ｍ／２」と言われるタイプです。待合室は１つであり、患者は空いた診療室に先着順に案内されます。
![ネットワークモデル](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507413/a7744133-4f6f-8502-c649-e0f1ed326a1c.png)

# 3.ソースコードの説明
ソースコードを[GitHub](https://github.com/tsugulin/SimLesson01C.git/)から入手可能ですので、[以前の説明](https://qiita.com/ponse7/items/3620f2b2084b451c726e)を参考にOMNeT++ IDEに取り込んで下さい。前々回（SimLesson01A）からの変更点について以下に説明します。
|ファイル名|内容|主な変更点|
|---|---|---|
|Simulation.ned|ノード及びノード間のネットワークを定義|追加したノードとノード間接続の定義|
|Queue.ned|待合室モジュールのパラメータやコネクションの定義|分岐に関するパラメータの追加|
|Queue.h|待合室モジュールのヘッダー|分岐に関するクラスプロパティやメソッドの追加|
|Queue.cc|待合室モジュールのソース|分岐に関するロジックの実装|

Simlation.nedの変更点は、(1)医師ノード(doctor)を配列に変更、(2)ノード追加に伴う経路追加の２点です。
```diff_c
network Simulation01
{
	(中略)
	submodules:
		(中略)
- 		doctor: Sink {中略}
+ 		doctor[2]: Sink {中略}	//---(1)
	connections:
		(中略)
-		wait.out --> doctor.in;
+		wait.out++ --> doctor[0].in;	//---(2)
+		wait.out++ --> doctor[1].in;	//---(2)
-		doctor.out --> wait.in++;
+		doctor[0].out --> wait.in++;	//---(2)
+		doctor[1].out --> wait.in++;	//---(2)
}
```
Queue.nedの変更点は、(1)最大分岐数を指定するためのforkNumberパラメータ追加、(2)outゲートを配列化の２点です。
```diff_c
simple Queue
{
	parameters:
+		int forkNumber = default(0);	//---(1)
		volatile double intervalTime @unit(s) = default(0);
	gates:
		input in[];
-		output out;
+		output out[];	//---(2)
}
```
Queue.hの変更点は、(1)最大分岐数を格納するmaxプロパティの追加、(2)診療室の空き状況を格納するnxtプロパティの追加、(3)患者を診療室に案内するforwardメソッド追加の３点です。
```diff_c
(前略)
class Queue : public cSimpleModule
private:
+	int max;	//---(1)
+	int ntx;	//---(2)
	cQueue queue;
	cLongHistogram waitTime;

protected:
	virtual void initialize();
	virtual void handleMessage(cMessage *msg);
+	virtual void forward(int);		//---(3)
	virtual void finish();
};
(後略)
```
Queue.ccの変更点は(1)各プロパティの初期化、(2)WATCHマクロによる監視設定、(3)患者が到着した際に診療室にrequestメッセージを送る処理を削除、(4)患者が到着した際に変数nxtの状況に応じてforwardメソッドを呼び出す処理を追加、(5)待合室からcallメッセージが届いた際に患者を案内する処理を削除、(6)待合室からcallメッセージが届いた際に変数nxtを更新してforwardメソッドを呼び出す処理を追加、(7)患者を診療室に案内するforwardメソッドの追加の７点です。
```diff_c
(前略)
void Queue::initialize()
{
	queue.setName("queue");
+	max = par("forkNumber").intValue();		//---(1)
+	nxt = 0;
+	WATCH(nxt);								//---(2)
	scheduleAt(simTime() + par("intervalTime"), new cMessage("beat"));
}

void Queue::handleMessage(cMessage *msg)
{
	if (strcmp(msg->getName(), "patient") == 0) {
		msg->setTimestamp(simTime());
		queue.insert(msg);
-		send(new cMessage("request"), "out");			//---(3)
+		if (nxt < 3) {									//---(4)
+			if (nxt == 0)  j = intuniform(0, max-1);
+			else           j = (nxt == 1) ? 1: 0;
+			forward(j);
+		}
	} else if (strcmp(msg->getName(), "call") == 0) {
-		if (queue.getLength() > 0) {					//---(5)
-			cMessage *patient = check_and_cast<cMessage *>(queue.pop());    
-			waitTime.collect(simTime() - patient->getTimestamp());          
-			send(patient, "out");
-		}
+		int j = (strcmp(msg->getArrivalGate()->getFullName(), "in[1]") == 0) ? 0: 1;	//---(6)
+		nxt ^= j + 1;
+		forward(j);
		delete msg;
	}
}

+ void Queue::forward(int doctor)			//---(7)
+ {
+	if (queue.getLength() > 0) {
+		cMessage *patient = check_and_cast<cMessage *>(queue.pop());    
+		waitTime.collect(simTime() - patient->getTimestamp());          
+		send(patient, "out", doctor);
+		nxt ^= doctor + 1;
+	}
+ }
```
なお変数nxtは0から3の値を取り、その意味は以下の通りです。
|値|意味|
|---|---|
|0|診療室は両方空いている|
|1|診療室0は使用中だが、診療室1は空いている|
|2|診療室0は空いているが、診療室1は使用中|
|3|診療室は両方使用中|

omnetpp.iniの変更点は、(1)分岐数の設定、(2)医師ノードの配列化、(3)乱数設定の変更の３点です。
```diff_c
[General]
(前略)
network = Simulation01
*.enter.intervalTime = exponential(10.0s)
+*.wait.forkNumber = 2								//---(1)
-*.doctor.serviceTime = exponential(8.0s)
+*.doctor[*].serviceTime = exponential(8.0s)		//---(2)
num-rngs = 1

-Config Run2]
-*.doctor.serviceTime = exponential(4.0s)

+[Config Run1]
+num-rngs = 2										//---(3)
+*.enter[*].rng-0 = 0
+*.doctor[*].rng-0 = 1
```

# 4.例題の実行
シミレーションのConfigurationを「General」として実行すると待ち行列の長さが1.21分となりました。公式（M/M/2）の解は1.5分なので約２割の誤差があります。このように結果が想定と異なる場合、原因としてはロジックの設計ミス、実装のバグ、パラメータが乱数に依拠することに起因する誤差等が考えられます。
![実行結果1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507413/04179bee-7320-a10d-f7cc-8f6ad9e8d3c1.png)

そこでConfigurationを「Run1」として乱数体系を変更して実行すると結果は1.49分となり、公式の解とほぼ同じ結果となりました。
![実行結果2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507413/e1eab0b1-d836-f6b0-b0d8-9f0bd6dfbe17.png)

# 5.Tips
シミュレーションの乱数体系はomnetpp.iniの設定により簡単に変更できます。Generalでは(1)にて乱数生成器が１つしか定義されていないので、全てのノードは同じ乱数生成器を使用します。いっぽうRun1では(2)にて乱数生成器を２つ定義しており、(3)にてenterノードが乱数生成器0、(4)にてdoctorノードが乱数生成器1を利用するよう設定しています。
```diff_c
[General]
(前略)
num-rngs = 1				//---(1)

[Config Run1]
num-rngs = 2				//---(2)
*.enter[*].rng-0 = 0		//---(3)
*.doctor[*].rng-0 = 1		//---(4)
```



# 6.改訂履歴
|改定日|改定前|改定後|
|---|---|---|
|2022.4.30|-|初期登録|