1. エージェントシミュレーションとは
エージェントシミュレーションとは、複雑な社会現象を分析するために用いられるシミュレーションで、分析対象となる領域に、自律的に行動する主体(≒エージェント)を配置し、その挙動や相互作用を観察することで実現します。用途としては、道路の新設による混雑状況の変化を把握するために、交通網を対象として、自動車やトラックをエージェントとした交通流分析や、災害時の避難状況を把握するために建物を対象として、避難者をエージェントとした避難経路分析などがあります。
本資料では、倉庫におけるピッキング作業を、作業者をエージェントとし、作業者のピッキングルールや、棚の配置による効率の違いを分析することを目的としたシミュレーションの構築を目指します。ただし、本資料では、複雑な棚割りや、精緻なピッキングルールの実装は行わず、基本的な処理である、1)作業環境の構築、2)エージェントの配置、3)エージェントの移動、4)移動のカウントの方法について、説明します。
2. 想定する環境
本資料では以下のような環境を想定します。
- 領域:
- 5×5の格子状のセルからなる倉庫。左下を原点(0,0)とする。
- エージェント:
- 棚エージェント:商品が置かれた棚のエージェント。領域のセル単位で配置する。(1,4)、(2,4)、(3,4)に配置する。
= ピッカーエージェント:商品をピッキングするエージェント。領域のセル単位で上下左右に移動する。初期位置は、(2,0)とする。 - アイテムエージェント:ピッキング対象となる商品エージェント。棚の前に配置することで棚まで行くことを模擬する。位置は(3,3)とする。
- 移動カウントエージェント:移動量のカウントのためのエージェント。原点に配置するが、処理は行わない。
- 棚エージェント:商品が置かれた棚のエージェント。領域のセル単位で配置する。(1,4)、(2,4)、(3,4)に配置する。
図1 想定するシミュレーション環境
3. シミュレーション設計
3.1. ピッカーエージェント
本資料では、自己のルールに基づいて動作するエージェントは、ピッカーエージェントのみとなりますので、ここでは、ピッカーエージェントの動作について設計します。エージェントシミュレーションにおいては、任意の間隔で繰り返しの処理が行われるのですが、アイテムをピッキングするというエージェントの処理は単純に表現すると図 2のように表現できます。どのように進む方向を決定するかが工夫のしどころとなりますが、進み方向の決定方法については、実装のところで検討することとします。
図2 ピッカーエージェント処理
3.2. 他のエージェント
その他のエージェントは、対象とする領域(倉庫)に配置されるだけで、処理対象とはなりませんので、配置に必要なプロパティだけが必要になります。
4. 実装
4.1. ツールの準備(Repast Simphony)
Repast Simphony(RS)は、オープンソースのエージェントシミュレーションツールです。Javaを用いてシミュレーションを構築することができます。オープンソースプロジェクト から、実行ファイルをダウンロードして、インストールすることで、利用可能となります。RSはEclipseのライブラリとして提供されますが、この実行ファイルを利用することで、Eclipseごとインストールすることができます。この資料では、既存のEclipseに追加する方法ではなく、新規にEclipseごとインストールする方法を前提に説明を進めます。
4.2. Projectの作成
RSにおけるプロジェクトの作成は、通常のJavaプログラムと同様です。File>Newから、Otherを選択し、Repast Simphonyフォルダから、Repast Simphony Projectを選択してください。ここでは、プロジェクト名をPickSimとしておきます。
図3 Repast SimphonyでのProject作成
4.3. エージェント用のクラスの作成
シミュレーションのために、処理を記述する必要があるのですが、まずは、必要となるクラス用のJavaファイルを作成しておきます。作成は、EclipseのNavigatorにおいて、PickSimのプロジェクトを選択し、ソースファイル(src)フォルダ配下のpickSimフォルダを右クリックして、クラスを新規作成することで作成できます。ここでは、ピッカーエージェント(Picker.java)、棚エージェント(Shelf.java)、アイテムエージェント(Item.java)、移動カウントエージェント(Count.java)を作成しておきます。また、エージェントではないのですが、各エージェントで共通に使用する値を管理するクラスとして、Params.javaというクラスも作成しておきましょう。
図 4 エージェント用クラスの作成
作成したクラスには、ひとまず、エージェントが配置される領域であるGridをプロパティと、コンストラクタのみ追加しておきます。その際、これらのクラスが配置される領域(grid)を引数として定義しておきます。以下はPicker.javaの例ですが、他のエージェントでも同様にして記述しておきます。
public class Picker {
// プロパティ
public Grid<Object>grid;
// コンストラクタ
public Picker(Grid<Object>grid) {
this.grid = grid;
}
}
4.4. Contextの作成
各エージェントの処理は、後ほど行うとして、まずは、Contextを作成します。このContextで、シミュレーションで使用するエージェントを定義します。Contextの作成には、プロジェクト名+Builder.javaという名前のクラスを作成する必要があります。この場合ですと、PickSimBuilder.javaとなります。
図 5 PickSimBuilder.javaの作成画面
この時に、単にClassを作成するのではなく、Interfaceを追加します。具体的には、Addボタンを押下し、新たに開く画面で、ContextBuilderを選択して、OKを押下します。ContextBuilderが表示されていない場合には、上部の検索窓にContextBuilderと入力することで表示されます。
図 6 ContextBuilderの追加
作成されたクラスには二か所エラーがあります。ContextBuilderのところののところのエラーは、を
図 7 一つ目のエラー対処
もう一つのPickSimBuilderの下の赤線については、赤線の部分にカーソルを持っていき、そこで、右クリック、Source > Override/Implement Methods を選択し、開いた画面でOKを押下することで、対処します。
図 8 二つ目のエラー対処(1)
図 9 二つ目のエラー対処(2)
このようにすると、エラーは消えます。
ここからエージェントをContextに配置するのですが、その前に、Context名を付与しておきます。Context名は、setIdメソッドを使うと付与できます。ここでは、PickSimとつけておきます。また、戻り値もnullから、contextに変更しておきましょう。
図 10 Context名の設定
また、今回、エージェントをGridと呼ばれる格子状の領域に配置しますので、そのインスタンスの生成も行います。RSでのGridの生成は次のように実施します。これは、5×5の”grid”という名前の格子状の領域を生成し、格子の端は壁のようになっていてその先の領域はない、という意味になっています。クラスのところにエラーが出ると思いますが、適宜、必要なクラスをimportしておいてください。
// 格子状領域の作成
GridFactory gridFactory=GridFactoryFinder.createGridFactory(null);
Grid<Object>grid=gridFactory.createGrid("grid", context,
new GridBuilderParameters<Object>(new StrictBorders(),
new SimpleGridAdder<Object>(), true,5,5));
格子の端の条件としては、反対側につながっているとする条件や、ぶつかると跳ね返るといった条件もあります。
ここまでで、Context名と格子を設定しましたが、このContext名とGrid名は、context.xmlファイルでも指定する必要があります。Context.xmlは、「Navigator」のPickSim.rsフォルダ内にありますので、context名については、同じ名前になっているかを確認し、格子名については、projectionを追加しておいてください。
図 11 Context.xmlの変更
続いてエージェントをContextに追加する処理を追加します。基本的には、インスタンスの追加、Contextに追加、インスタンスを所定の位置に移動、で追加することができます。今回は以下のように設置します。
表 1 エージェントの配置
| エージェント | 設置場所 |
|---|---|
| ピッカーエージェント | (2, 0) |
| 棚エージェント | (1, 4)、(2, 4)、(3, 4) |
| アイテムエージェント | (3, 3) |
| 移動カウントエージェント | (0, 0) ただし、初期値としては設置しない。 |
これらの場所は、変数として、Params.javaに設定しておきます。
public class Params {
public static int[][] posPicker= {{2,0}}; // ピッカーエージェントの場所
public static int[][] posShelf = {{1,4}, {2, 4},{3, 4}}; // 棚エージェントの場所
public static int[][] posItem= {{3,3}}; // アイテムエージェントの場所
public static int[][] posCounter = {{0, 0}};// 移動カウントエージェントの場所
}
PickerSimBuilderには、以下のようなコードが入ります。
public Context build(Context<Object> context) {
// 変数
int[][] posPicker = Params.posPicker; // ピッカーエージェントの場所
int[][] posShelf = Params.posShelf; // 棚エージェントの場所
int[][] posItem = Params.posItem; // アイテムエージェントの場所
// Context名設定
context.setId("PickSim");
// 格子状領域の作成
GridFactory gridFactory=GridFactoryFinder.createGridFactory(null);
Grid<Object>grid=gridFactory.createGrid("grid", context,
new GridBuilderParameters<Object>(new StrictBorders(),
new SimpleGridAdder<Object>(), true,5,5));
// ピッカーエージェント追加
for(int i=0; i<posPicker.length; i++) {
Picker p = new Picker(grid); // インスタンス生成
context.add(p); // Contextに追加
grid.moveTo(p, posPicker[i][0], posPicker[i][1]); // 所定の位置に移動
}
// 棚エージェントの追加
for(int i=0; i<posShelf.length; i++) {
Shelf s = new Shelf(grid); // インスタンス生成
context.add(s); // Contextに追加
grid.moveTo(s, posShelf[i][0], posShelf[i][1]); // 所定の位置に移動
}
// 商品エージェントの追加
for(int i=0; i<posItem.length; i++) {
Item itm = new Item(grid); // インスタンス生成
context.add(itm); // Contextに追加
grid.moveTo(itm, posItem[i][0], posItem[i][1]); // 所定の位置に移動
}
return context;
}
4.5. 設定の確認(シミュレーションの実行)
プログラムの実行ですが、通常のEclipseなら、実行ボタンを押下すれば動かすことができるのですが、RSの場合には、Contextの指定と、描画をする場合には、Displayの設定が必要です。まず、シミュレーション実行のコンソール画面を表示させる方法ですが、メニューの緑色の三角印のアイコン横の黒い下向き参画をクリックします。開いたメニューからPickSim Modelを選択します。
図 12 PickSim Modelの選択
図 13 コンソール画面
ここからは、コンソール画面を使って設定を続けます。まず、Contextの指定ですが、これには、コンソール画面の「Scenario Tree」にある、「Data Loaders」を使用します。まず、作成した時に入っている、「XML & Model Init Context」を選択し、削除します。次に、Data Loadersを右クリックし、「Set Data Loader」を選びます。開いた画面から、一番上の「Custom ContextBuilder Implementation」を選択して、「Next」、「Finish」を押下します。
図 14 Data Sourceの指定
つづいて、同じく「Scenario Tree」から、「Displays」を選択し、「Add Display」を選択します。開いた画面の下方、左のウィンドウにgridと表示されていると思いますので、それを選択し、右のウィンドウに、中央にある矢印のアイコンを使って移動します。次に「Next」を押下します。
図 15 Data Sourceの指定
続いて、エージェントを指定する画面がありますが、ここでは、格子に表示するエージェントである、Picker、Item、Shelf、Countをそれぞれ選択し、画面右に矢印アイコンを使って移動します。つづいて、「Next」を押下します。
図 16 エージェントの指定
それぞれのエージェントは、画面上で、異なるアイコンとして表示することができます。ここでは、Pickerをデフォルトの青色丸印、Shelfをグレーの四角、Itemを赤色の星印、Countを黒色のバツ印として表示するようにしてみましょう。
図 17 エージェントの指定(Item.java)
「Next」を押下して、先に進むと格子のサイズを指定する画面になりますが、特に変更する必要はありません。その次の、スケジュールを変更する画面も同様に、そのままで「Finish」を押下して大丈夫です。これで設定は完了ですが、今後も繰り返し使用する設定ですので、コンソール画面上方のフロッピーディスクアイコンをクリックして、忘れないように、設定を保存しておきます。
実行するには、コンソール画面の情報のアイコンで、電源ボタンのようなアイコン(Initialize Run)をまず一回押下し、続いて、三角印のボタン(Start)を押下することで実行できます。
図 18 実行時に使用するボタン
今回は処理を書いていませんので、電源ボタンだけ押してみましょう。画面に5×5セルの格子と、配置したが右端に表示されたでしょうか。
図 19 セルの表示
5. ピッカーエージェント処理
5.1. 繰り返し処理
RSにおいて、エージェントに繰り返し処理をさせたい際には、ScheduledMethodを使用します。ScheduledMethodでは、開始(start)と繰り返し間隔(interval)を指定します。実際に繰り返し処理が行われる様子を確認するために、以下のように記述してみます(10行~14行)。こうすることで、コンソール画面に繰り返しのたびに文字が表示されます。実際に実行してみましょう。
public class Picker {
// プロパティ
public Grid<Object>grid;
// コンストラクタ
public Picker(Grid<Object>grid) {
this.grid = grid;
}
@ScheduledMethod(start = 0, interval = 1000)
public void picking() {
// ScheduledMethodの確認用
System.out.println("処理が繰り返されます");
}
}
5.2. 設計したフローチャートの実装
次に、に置いて設計したフローチャートを実装します。
5.2.1. 場所の確認
自分の場所は、GripPointクラスで表現できます。GridクラスのgetLocation()メソッドを用いて場所は取得でき、GridPointクラスのgetX()、getY()メソッドを使用することで、存在するセルの位置を取得できます。5行目は動作が確認出来たらコメントアウトしておきます。
@ScheduledMethod(start = 0, interval = 10000)
public void picking() {
// 場所の確認
GridPoint gpPicker = grid.getLocation(this);
System.out.printf("エージェントは、%d、%dにいます。\n", gpPicker.getX(), gpPicker.getY());
}
5.2.2. アイテムの場所を確認する処理
アイテムの場所の確認については、GripPointを戻り値とするメソッドで対応します。様々な方法が考えられますが、ここでは、直にセルの座標を返すメソッドとしておきます。
public GridPoint getItemLocation() {
GridPoint gp = new GridPoint();
// 直接値を入力する
Iterable<Object> obj = grid.getObjectsAt(3, 3);// セルにいるエージェントを探す
for(Object o: obj){ // セル内のエージェントをチェックする
if(o.getClass().getName().equals("pickSim.Item")){ // エージェントがItemであることを確認
Item itm = (Item)o;
gp = grid.getLocation(o);
break;
}
}
return gp;
}
呼び出し側は次にようになります。これについても動作を確認しておきましょう。3行目は動作が確認出来たらコメントアウトしておきます。
// アイテムの場所の確認
GridPoint gpItem = this.getItemLocation();
System.out.printf("アイテムは、%d、%dにいます。\n", gpItem.getX(), gpItem.getY());
5.2.3. 進む方向を決定する処理
進む方向の決定についても、方向を戻り値とするメソッドで対応することとします。壁などがあるかどうかを確認しながら進む方向を決定するなど、方向の決め方もたくさんあると思いますが、ここでは、単純に、アイテムの場所まで、横方向(X)、縦方向(Y)の順に進むという方法を実装します。引数は、自身とアイテムの場所のGridPoint、戻り値は、gridを外部から見た際の方角を示す文字列(上:n、右:e、下:s、左:w、移動無し:c)とします。
public String getDirection(GridPoint gpPicker, GridPoint gpItem) {
String direction = "";
if(gpPicker.getX() > gpItem.getX()) direction = "w"; // 右
else if(gpPicker.getX()<gpItem.getX())direction = "e"; // 左
else {
if(gpPicker.getY()>gpItem.getY())direction = "s"; // 下
else if(gpPicker.getY()<gpItem.getY())direction = "n"; // 上
else direction = "c"; // 移動無し
}
return direction;
}
メソッドは以下のように呼び出します。
// 進む方向の決定
String direction = this.getDirection(gpPicker, gpItem);
5.2.4. 進む処理
進むための処理は、GridクラスのmoveToメソッドを使用することで実現します。case文を用い、directionの値で処理を分岐します。
// 進む処理
switch(direction) {
case "n":
grid.moveTo(this, gpPicker.getX(), gpPicker.getY()+1); // 領域上方向
System.out.println("上");
break;
case "e":
grid.moveTo(this, gpPicker.getX()+1, gpPicker.getY()); // 領域右方向
System.out.println("右");
break;
case "s":
grid.moveTo(this, gpPicker.getX(), gpPicker.getY()-1); // 領域下方向
System.out.println("下");
break;
case "w":
grid.moveTo(this, gpPicker.getX()-1, gpPicker.getY()); // 領域左方向
System.out.println("下");
case "c":
System.out.println("ステイ");
break;
}
5.2.5. ピッキング処理
ピッキング処理は、Gridクラスのremoveメソッドを使用することで実現します。この処理もピッカーエージェントの位置を引数としたメソッドとして定義します。
public boolean isRemoveItem(GridPoint gp) {
boolean result = false;
Iterable<Object> obj = grid.getObjectsAt(gp.getX(), gp.getY());// エージェントを探す
for(Object o: obj){ // セル内のエージェントを一個ずつチェックする
if(o.getClass().getName().equals("pickSim.Item")){ // アイテムエージェントか判断
Item itm = (Item)o;
Context<Object>context = ContextUtils.getContext(itm);
context.remove(o); // Itemエージェントを消去
result = true; // 削除成功
break;
}
}
return result;
}
削除のメソッドは以下のように呼び出します。
// ピッキング処理
boolean result = this.isRemoveItem(gpPicker);
if(result)System.out.println("ピッキングしました");
実際に実行してみましょう。結果はどうなりましたか。このままでは、アイテムのピッキングまではうまく動きますが、その後エラーが出てしまうと思います。これは、アイテムが無くなったのに、アイテムが存在する前提で処理が書かれているためです。この問題に対処するために、フローチャートを次の方に変更してみます。
図 20 新しいピッカーエージェントの処理フロー
アイテムが残っているかどうかの真偽値として、isItemExistを定義し、アイテムが残っているかどうかのメソッドも定義します。
public boolean isItemExist() {
boolean result = false;
for(int i=0; i<posItem.length; i++) {
// 直接値を入力する
Iterable<Object> obj = grid.getObjectsAt(posItem[i][0], posItem[i][1]);// セルにいるエージェント
for(Object o: obj){ // セル内のエージェントをチェックする
if(o.getClass().getName().equals("pickSim.Item")){ // エージェントがItemなら
result = true;
break;
}
}
}
return result;
}
メソッドにおいて、アイテムエージェントの場所の情報も必要となりますので、Params.javaで定義したposItem変数をこのクラスでも定義します。これは、ピッカーがピッキングする商品のリストを手に持って歩きまわるイメージでしょうか。あとは、フローチャートに示したのと同様に、If文で処理を分岐し、isItemExistが真の場合には、これまで作成した処理を、そうでない場合には、ピッキングが終了した旨のメッセージを出力することとします。
@ScheduledMethod(start = 0, interval = 1000)
public void picking() {
// 場所の確認
GridPoint gpPicker = grid.getLocation(this);
// アイテムがあるか確認
isItemExist = this.isItemExist();
// アイテムの有無で処理を分岐
if(isItemExist) {
// アイテムの場所の確認
GridPoint gpItem = this.getItemLocation();
(途中省略)
// ピッキング処理
boolean result = this.isRemoveItem(gpPicker);
if(result)System.out.println("ピッキングしました");
}else {
System.out.println("ピッキングが終わりました");
}
}
では改めて、シミュレーションを実行してみましょう。今度は、エラーなくピッキングを終了できたと思います。
6. 移動距離のカウント
ここまでで倉庫の作業者(ピッカーエージェント)が商品(アイテムエージェント)をピッキングする様子をシミュレーションできました。ただ、棚のレイアウトやピッキング順の性能を評価するためには、ピッカーがどの程度歩いたのかを知る必要があります。そこで、ここでは、ピッカーが移動するたびに、ダミーのエージェントとして、移動カウントエージェントをgridに配置することにします。case文の分岐で移動することになっているものの箇所に書き入れる必要がありますので、ここでもメソッドとして定義しておきます。また、posCounterもParams.javaから取得しておきます。
public void placeCounter() {
Count cnt = new Count(grid);
Context<Object>context = ContextUtils.getContext(this);
context.add(cnt); // Contextに追加
grid.moveTo(cnt, posCounter[0][0], posCounter[0][1]); // 所定の位置に移動
}
上記のメソッドは以下のように呼び出します。
switch(direction) {
case "n":
grid.moveTo(this, gpPicker.getX(), gpPicker.getY()+1); // 領域上方向
System.out.println("上");
placeCounter(); // 移動カウントエージェント配置
break;
(途中省略)
}
case文の中にこのメソッドを呼び出す個所も追加して、実行してみましょう。(0, 0)の箇所に、カウントエージェントが配置されたと思います。ただ、同じ場所にエージェントが配置されてしまうので、このままでは、どの程度移動したかが分かりません。そこで、ここでは、RSの機能を使って、どの程度増えているかを可視化してみます。
RSのシミュレーション実行画面の「Scenario Tree」の「Data Sets」で、右クリックすると「Add Data Set」というメニューが現れますので、それを選択します。表示されたウィンドウにおいて、「Data Set Type」は「Aggregate」を選択し、「Next」を押下します。
図 21 Data Set追加(1)
図 22 Data Set追加(2)
次の画面では、「Add」ボタンを押して、Data Setに追加するデータを選択します。ここでは図にあるように、「Method Data Sources」タブを選択し、「Source Name」に「Move count」と書き、「Agent Type」は、「Count」を選択、「Aggregation Operation」については「Count」を選択しておきます。その後、「Next」「Finish」を押下して、設定を保存します。
図 23 Data Set追加(3)
次に、同じく「Scenario Tree」の「Charts」を右クリックし、メニューから「Add Time Series Chart」を選択し、設定画面を開きます。Data Setは先ほど作成した「A Data Set」を選択します。他の名前をつけた場合には、その名前を選択します。選択後、「Next」を押下して、表示した画面では、描画したいデータを選択します。選択後、さらに「Next」を押下します。
図 24 チャートの追加(1)
図 25 チャートの追加(2)
図 26 チャートの追加(3)
表示された画面で、チャートの「Title」と「Y-axis」を入力します。ここでは、それぞれ「Picker move count」と「Number of cells」としました。「Finish」を押下後、設定を保存しておきます。
図 27 チャートの追加(4)
チャートの設定は以上ですが、ここで、一定の回数ScheduledMethodを実行したらシミュレーションを停止する処理も追記しておきます。これは、PickSimBuilder.javaのreturn contextの直前に記述します。ライブラリも必要に応じてimportしておきます。
// プログラムを停止するまでのtickの回数
RunEnvironment.getInstance().endAt(10000);
では、シミュレーションを実行してみましょう。実行ボタンを押下後、画面タブから、先ほど作成した「Picker move counter」タブを選択します。ScheduledMethodのintervalの値と、シミュレーションの停止までのタイミングによっては、うまく表示されない場合もあります。以下は、停止までが10000Tick、intervalが1000の時の実行結果です。
図 28 実行結果
実行結果からは、ピッカーエージェントが4セル分移動してアイテムエージェントをピックしていることが分かります。この移動カウントエージェントの数は、バッチ処理をする際にも取り出すことができ、モンテカルロシミュレーションを実施する際には有効になります。
7. 演習課題
ここまでの演習で、ピッカーがアイテムをピッキングすることをシミュレーションしました。また、その際に、ピッカーエージェントがどの程度移動しているかを計測する方法も実装しました。しかし、このままでは不十分なこともあります。
- アイテムエージェントの場所確認
今回は、エージェントの場所の値を固定値として戻すメソッドとして実装しましたが、実際には、アイテムを探しながら、あるいは、アイテムを指定してピッキングをする必要があると考えられます。 - 移動方向の決定方法
今回は、単純に、gridの左右方向の移動後に、上下方向移動するという方法をとりましたが、場合によっては別の方法の方が、短距離で移動できるかもしれません。また、移動しようとして、移動先のセルが、棚や壁で移動できないケースもあるかもしれませんが、今回はそのような処理も含まれていません。 - ピッキング後のピッカーエージェント
今回は、ピッカーエージェントはピッキング終了後に、その場でとどまってしまいましたが、実際には、商品をピッキングしたのち、所定の場所に戻ることが求められます。
これらの課題は、この演習で説明した方法だけでは実現が難しいものもありますが、RSのAPIなども参考にして、実装してみてください。