モデリングシミュレーションは、現実の世界の問題をコンピュータ上でモデル化して、複雑なシステムをより良く理解することで解決策を導き出すという方法論です。モデル化の代表的な手法としてはシステムダイナミクス、ディスクリートイベント、エージェントベースの3つがあり、この3つの手法全てに対応可能なローコードの汎用シミュレータとして開発されたのがAnyLogicというアプリケーションソフトです。本章で取上げるのは、このAnyLogicを使ったAMRのシミュレーションになります。AnyLogic自体はJavaで構築されており、ユーザが自身のモデルにコードを埋込む際にもJavaを使用します。
駄待ち狐がAnyLogicに興味を持ったのは2021年秋で、完全リタイア後も2023年春までモデリングを続けていました。実は「VII. ROSによるAMRのシミュレーション」の「3. 最初のROS1プログラム」と「4. ROS1パッケージの修正」の間には2年弱の空白期間があり、この間に取組んでいたのがAnyLogicです。「3. 最初のROS1プログラム」では、ROSの重さに疲れました。重いというのは何百行もあるプログラムがたくさん複雑な依存関係で絡み合っているという点に加え、シミュレーションに時間がかかるということもあります。レッツノートで「ドライブする4台の中で1台がナビ」を実行すると、現実の時間に対して3倍の時間がかかります。
ROSとAnyLogicはいずれもシミュレーション結果が3Dアニメーションで表示されますが、そこに至る過程は全く別物です。ROSではロボットを実際に動作させることが可能なプログラムを使って、ロボットに搭載されたSBCのプログラムをシミュレータ上に再現したロボットモデルを操作し、現実と同じ状況を作り出します。いわゆるデジタルツインです。一方、AnyLogicではロボットの動作を例えば秒速何メートルで移動するといった形でモデル化します。では、AnyLogicは(少なくともロボットシミュレーションに関しては)手抜きのバッタものなのかというと、決してそんなことはありません。軽さには軽いが故の大きなメリットがあります。
例えば、顧客から「搬送ロボットを使ってこんな倉庫システムを作りたい」という相談があったとします。開発担当者はExcel等を使った簡易計算で粗方の動きを出し、顧客に提示します。このアプローチでは、顧客は搬送ロボットがどんな動きをするのか全く実感が湧きません。Excelの代りにAnyLogicを使ったらどうでしょう。概念設計段階で顧客に3Dアニメーションで倉庫のイメージを見せることができます。駄待ち狐がこういう話をすると、開発担当者は必ず「最終的なシステムが完成すればデジタルツインも同時に出来上って、倉庫の動きは一目瞭然になる」と反論します。でも、それでは遅いのです。システムを受注できないのです。
すると次に来る反論が「そんな営業ツールは営業部門で作ればいい」です。ところがどっこいです。AnyLogicによるシミュレーションは、実際にシステムを構築していく際に概念を固めるという役割も果せます。顧客に3Dアニメーションを見てもらえば、「いや、ここはこんな風にするんじゃなくて・・・」という話ができ、開発の手戻りがなくせます。今回はいきなり「駄待ち狐あるある」になりますが、この話は口が酸っぱくなるほど力説したにも拘らず結局受入れて貰えませんでした。ですから本章のシミュレーションも業務に役立つことはなく、趣味のプログラミングに終っています。
本サイトの目的はAnyLogicの使い方を解説することではなく、駄待ち狐が創意工夫した点を述べることなので、いきなり実例に入ります。AnyLogicはビジネス向けソフトなのでそのライセンスは結構なお値段ですが、個人の学習目的であれば機能限定版を無償で使えます。また、AnyLogic社のサイトでは、多くの第三者が作成したあまたのモデルが公開されています。その中に駄待ち狐作もあり、"roll box pallet"で検索すると出てきます。"roll box pallet"(かご台車)は駄待ち狐が仕込んだ検索ワードで、2025年2月25日時点ではこのワードで検索されるのは駄待ち狐(このサイト上ではKenny Matson)の5作品だけです。
本節では、5作品の中からまず"CarryingRBPs_Exchange"を紹介します。上記ページの上段右端のものをクリック(本サイト上ではなくAnyLogicのサイト上で)すると、以下のページが開きます。左にあるプレイボタンをクリック(同上)すると、ただの動画ではなくAnyLogic上でこのモデルを動作させた時と同じウィンドウが開きます。AnyLogicサイト上ではAnyLogicアプリがなくてもモデルが実行できるのです。一方、右側の"Model sorce files"をクリック(同上)すると、AnyLogicアプリでそのまま使えるモデルファイルがダウンロードできます。このようにデモ動作もソースのダウンロードも可能だという前提で、以下の話を進めます。
AnyLogicサイトからCarryingRBPs_Exchange-Version 4.zipをダウンロードして、解凍したフォルダにあるRollBox_1.alpをAnyLogicアプリで開くと、ウィンドウ内の左にあるメニューからMainが自動的に選択されて中央部に表示されます。茶色の枠線で区切られた1ページ目はモデルを実行した時に最初に表示される画像で、2ページ目に以下の図があります。これはROSのworldに相当する環境モデルになります。AMRが走行するフロアのレイアウトに加え、AMR走行時の経路Pathと停止位置Nodeも設定されています。黄色の荷物が載ったかご台車は、荷物もかご台車本体も3D Objectになっていて、プログラムで表示・非表示を切換えられます。
環境モデルの右上にあるマークは3D表示する際のカメラの初期位置ですが、カメラ位置はシミュレーション中に自由に動かすことができます。左のメニューでAmrCartをダブルクリックすると新しいタブが開き、原点付近に小さくAMR+かご台車が表示されます。これも3D Objectで、プログラムでかご台車を表示・非表示にしてPathに沿って移動させます。そのプログラムが3ページ目にあるロジック(下図)です。「小学生のプログラミング教室じゃないんだから、ブロックフローチャートは止めてよ」と思われるかもしれませんが、各ブロックはJavaでプログラミングされたクラスであり、Javaのコードをビジュアル化したものになっています。
濃淡ブルーのブロックは予め用意されているクラスですが、そのパラメータはユーザが設定します。一方、黄色のブロックSidleLoop(sidleは横歩きの意)は新たに作成したクラスで、左のメニューでSidleLoopをダブルクリックすればその中身が表示されます。左下に並んでいるVariableとParameterはインスタンス変数で、クリックすればウィンドウの右側のPropertiesに型宣言や初期値設定が表示されます。Parameterは初期値を上位クラスで設定するので、引数を変数に代入するイメージです。SidleLoopに出てくる緑のブロックも新たに作成したクラスですが、黄色は一連の動き、緑は単一の動きという感じで(駄待ち狐が勝手に)使い分けています。
Mainロジックでは、まず左上にあるSourceクラスのインスタンスsrcAmrとDelayのslctBufferによって、2台のAmrCartクラスAgentを異なる初期位置に生成します。Agentもインスタンスですが、シミュレーションの主役として動作させる対象です。SourceはAgentを生成するクラスで、Delayはwait/sleepに相当します。条件分岐であるSelectOutputのdvdAmrで2台は分離されて、別の方向へ移動します。かご台車列の前でのAgentの動きは手前側も奥側も同じなので、どちらもSidleLoopクラスによって動作させます。奥のインスタンスがloop1、手前がloop2で、ノードや留置かご台車はインスタンス毎に異なるので個別に設定します。
Source、Delay、SelectOutput、SidleLoopのインスタンス毎の設定は、そのブロックをクリックした時にPropertiesとして表示されます。loop1とloop2はMoveToのmvTo23とmvTo11を介してループを構成していますが、mvTo23、mvTo11はnode23、node11への移動です。最後に残ったjdgEnd1からの分岐は終了時の処理です。2台のAgentを初期位置に戻して停止させます。2台とも戻ったらendStop1もしくはendStop2に埋込まれた条件判断よってプログラムは終了するので、SinkのsinkAmrに到達してAgentが消滅することはありません。Sourceで始りSinkで終るというフローチャートの体裁を整えるためにsinkAmrを入れてあります。
Mainロジック→SidleLoopクラス→Pushpullクラス→Rotationクラスという流れを全て逐行解説ならぬ逐ブロック解説していくと却って分りにくいので、まずはPushpullを例に取ってブルーのブロックにどのようにJavaコードを埋込んでロジックをカスタマイズするかについて説明します。Pushpullはかご台車の前にAMRが停った後、かご台車を挿抜する動きをさせるものです。下図でロジックの上段にあるIconはPushpullのインスタンスを上位のロジックにはめ込む際のアイコンで、その両端から出ているConnector(ブロックの論理的繋がりを示す線)の先はPushpullの中身のロジックの始点と終点になります。
90°旋回→留置位置に移動→かご台車と結合/分離→経路上に移動→隣に移動→留置位置に移動→かご台車と結合/分離→経路上に移動→90°旋回という動きを「隣に移動」の前後で分けて、どちらもPushpullで制御します。SidleLoopによって前半のPushpull→隣に移動→後半のPushpullという流れにするのです。前半のPushpullでは最初に旋回、後半のPushpullでは最後に旋回となるので、ロジック上は両方に旋回を入れて条件分岐でスキップするようにしています。その条件分岐がslct1とslct2ですが、slct1には条件分岐以外のコードも埋込まれています。上図はslct1をクリックして選択した状態で、PropertiesのCondition:は分岐の条件になります。
一方、ActionsにあるOn enter:はこのブロックにAgentが入った時に実行されるコードで、条件分岐とは関係ありません。On exit (true):とOn exit (false):はここではブランクですが、ブロックを出る時の処理になります。どちらに分岐するかで異なる処理を設定できます。On enter:の最初にある"rectangle.setFillColor(lightSalmon);"は、シミュレーション実行時にAgentがここで処理されている間、Logic画面で矩形が薄鮭色になるという小細工です。rectangleはIconの緑の矩形に付けた名前です。ブルーのブロックには処理中は色が濃くなるという機能があるので、自作のクラスでも色が変るようにしたという訳です。
この後のコードはenterLot、enterRotation、exitRotationの各ブロックに対する条件設定をしています。条件設定は対象ブロックの前であればどこに置いてもいいのですが、可読性を高めるためにクラスの最初のブロックでまとめて設定しています。enterLotに対してはAMRが留置位置に移動する際の目標座標(cartX, cartY)を算出します。agent.stopRotDirは留置位置がAMRの奥側にあるか手前側にあるかというパラメータです。本モデルでは奥側(= 1)の場合しかありませんが、この後に紹介する"CarryingRBPs_Go-Round"では手前側の場合もあります。
carts[]というのは留置されたかご台車オブジェクトcart101等の配列で、loop1、loop2の各々に設定されるインスタンス変数です。それがそのままpushpullのインスタンス変数に代入されるので、carts[].getX/Y()が留置かご台車の(x, y)座標になります。stopRotDirによって異なるオフセット量を加算すれば(cartX, cartY)が得られます。enterRotation、exitRotationでは旋回の向きrotDirを指定します。rotDirはRotationクラスのrotStrtブロックをクリックするとOn enter:に出てくる10種類になります。連続的に旋回させることもないので、Delayブロック終了時に45°ずつ旋回した中間姿勢、最終姿勢を表示するようにしています。
enterLotブロックには以下のコードが埋込まれています。MoveToクラスとしてのDestination:は上記の(cartX, cartY)ですが、On Exit:には「かご台車と結合/分離」のトリックが仕込んであります。AMRとかご台車を結合する際には、"agent.cart.setVisible(true);"でAmrCartのかご台車を非表示→表示にして、loop1の場合は"agent.load.setVisible(true);"でAmrCartの荷物も非表示→表示にします。同時に、"carts[...].setVisible(false);"と"loads[...].setVisible(false);"で留置かご台車を荷物ごと表示→非表示にすれば、留置かご台車はAMRに結合されます。この逆のことをすれば、かご台車はAMRから分離された留置かご台車となります。
AnyLogicのTutorialsに"Job Shop"という題材があり、駄待ち狐はまずこれを勉強しました。トラックで運ばれてきた部材を作業員がフォークリフトで加工場所に運び、加工した製品をまたフォークリフトで運んでトラックに積込むというものです。このモデルでは対向して進んできたフォークリフトが、相手が透明人間であるかのように交錯して進行します。作業員が乗ったフォークリフトなので、実際にはハンドル操作で相手をかわしてすれ違うけども、そんな細かいところは省略して生産能力をモデリングするという趣旨でしょう。
ただ、AMRのシミュレーションとしてはこれはいただけません。顧客にこんなものを見せたのでは「おたくのシステムでは衝突事故が起りまくりですね!!」となります。モデリングシミュレーションとはいえ、衝突回避は必須の要件です。Pushpullクラスの上位クラスであるSidleLoopのロジックは下図のようになっており、衝突回避のためにviaOcps[]という変数を導入しています。下図の挿入図にあるようにloop1のインスタンス変数として配列via[]が設定されており、その要素はloop1に含まれるノードです。そのノードがAgentによって占有されているかどうか(非占有なら0、占有なら1のint)を要素とする配列がviaOcps[]になります。
SidleLoopのjdgRetryブロックでは、次に進むノードが他のAgentによって占有されているかどうかを判断します。非占有であればmvToNextブロックに入ってAgentは進行し、占有されていればwaitRetryに戻って0.2秒待ちます。SidleLoopのノード間隔は狭いので2つ先のノードまで空いている必要があるため、jdgRetryのCondition:は複雑になってます。AgentがSidleLoopに侵入、横移動、退出の各場合で2つ先に対応する添字が[agent.nextX + 1]か[agent.nextX - 1]になります。この場合分けのために導入したのがagent.rtrgrdNum(retro/go-round number、retroは逆行の意)です。
次に紹介する作品は"CarryingRBPs_Go-Round"です。この作品では左端のプールにあるかご台車と田の字型通路を有するフロアに並んだかご台車をAMRが入替えていきます。プールのかご台車は端から順番に選択され、フロアのかご台車はランダムに選択されます。冒頭に表示される画面でAMRの台数と動作モードを選択できます。"Dual Mode"を選択しなければ、1台のAMRがプールから運んだかご台車を一旦通路上に置いて、入替えたいかご台車を通路に引出した後、空いたスペースに運んできたかご台車を入れます。デュアルモードでは空荷のAMRがフロアのかご台車を運び出し、その後から来るAMRがプールのかご台車をフロアに置きます。
前述の"Exchange"も今回の"Go-Round"も、元々はかご台車の入替えを想定して作ったものではありません。搬送ロボの部隊は2022年3月に発展的解消となり、駄待ち狐は会社勤めの最終年をまた別の部署で過すことになりました。メンバーは搬送ロボのハードやソフトを開発していた人達なので違和感はなかったのですが、開発対象は全く別物でした。ここには書けませんが、その開発対象を想定して作ったのが"Exchange"と"Go-Round"です。この後紹介する"Elevator"だけは本当にかご台車の運搬を想定しており、搬送ロボの部隊で開発しようとした倉庫システムからヒントを得ています。
下図に示すMainロジックで、CartPoolクラスは左端のプールにおけるかご台車の挿抜を制御します。AisleRun(aisleは両側に在庫品がある通路の意)クラスはフロアに並んだかご台車の入替えを制御しますが、aisleRun1~6の6つのインスタンスは田の字横線の左上、左中、左下、右上、右中、右下の通路に対応しています。初期位置(田の字の左端中段)から出発したAMRはslct1ブロックで分岐して、上半分にあるかご台車を入れ替えるのであれば上に、下半分を入れ替えるのであれば下に進みます。左側の入替えの場合は田の字の左右真ん中で折返して初期位置に戻ってきますが、右側の入替えの場合は田の字の右端まで行きます。
このモデルでは初期位置nodeInitにAMRが戻って来ると次の搬送指令が出されるという建付けにはなっておらず、搬送指令はAMRの実際の動きとは無関係なタイミングで出されます。AMRがいない時に出された指令はキューに保存されて戻って来たAMRに順次付与され、指令キューが空の時は戻って来たAMRが指令を待ちます。このロジックを実装する方法は色々あると思いますが、ここでは搬送指令が出されるとその指令をインスタンス変数として持った非表示のAgentを生成し、初期位置に待機させています。仮想のAgentに指令を保存していくのです。
仮想Agentを待機させるのは、CartPoolクラス内にあるwaitReturnです。waitReturnは阻止と非阻止の2つの状態を取るHoldクラスです。阻止状態ではwaitReturnの手前でAgentを待機させ、非阻止状態では通過させます。この阻止/非阻止を制御するのは、Mainロジックの2段目右端から2つ目のブロックmvifToInitです。mvifToInitは次項で説明するMvifToクラスのインスタンスですが、ここでの動作を説明します。mvifToInitにAgentが入る(AMRが初期位置に戻って来る)とwaitReturnを非阻止状態にして、戻って来たAgent自身はMvifToクラスにあるwait(Delayクラス)で待機します。
waitReturnで阻止されていた仮想Agentがあれば(搬送指令が保存されていれば)、非阻止になって通過する時に表示状態のリアルAgentとなり、戻って来た方のAgentのDelayを解除します。すると、戻って来たAgentはwaitReturnを阻止状態にした後、sinkAmrに入って消滅するのです。MvifToのwaitに戻って来たAgentがいなければ仮想AgentはwaitReturnで阻止されたままですし、仮想AgentがwaitReturnを通過しなければ戻って来たAgentはwaitに留ります。DelayクラスだけではAgentが入って来た時と出て行く時しか状態を判断しませんが、Holdクラスの阻止/非阻止を使うことでwaitの出入りを記憶させています。