モデルの再現性
概要
FlexSimでは、シミュレーション実行でのばらつきが統計分布の使用を通して許容されます。このようなランダムに生成される値からは、静的なデータセットよりも正確な結果が得られます。実験ツールを使用すれば、異なる結果が得られるさまざまなシナリオの複数の複製を実行できます。これらの結果を比較して分析することにより、シミュレートするシステムの理解を深めたり、信頼区間を構築したりできます。
ただし、モデルを構築して結果を検証したり、モデル内のエラーを修正したりする場合は、このような変わりやすい結果は望ましくありません。このような場合では、モデルに再現性があると便利です。
このトピックでは、モデルを再現可能にするすべての方法を紹介します。
ランダムストリームを繰り返す
このオプションは、統計メニューにあります。オンにすると、モデルをリセットして実行するたびに、乱数ジェネレータによって同じ数列が生成されます。これにより、すべてのシミュレーション実行でモデルの結果を同じにすることができます。再現可能な結果を構築する場合は、このオプションを常時オンにする必要があります。
真に再現可能なモデルを構築するには、以下の条件を満たす必要があります。
タスク実行者リセット位置
タスク実行者がリセット位置を持っていない場合は、モデルをリセットして実行すると、それらのタスク実行者は最後のシミュレーション実行の終了時点から開始することになります。これは、タスク実行者が実行ごとに異なる移動時間を持つことになり、結果に影響することを意味します。
リセット位置を設定するには、[タスク実行者]を右クリックして、[編集]>[オブジェクトのリセット位置を設定]を選択します。または、複数のタスク実行者のリセット位置を一度に設定するには、タスク実行者のグループをShiftキーを押しながら選択またはCtrlキーを押しながらクリックしてから、選択されたオブジェクトのいずれかを強調表示して、[クイックプロパティ]の[ビュー]>[選択したオブジェクトを編集]セクションで[リセット時の位置]の[セット]ボタンをクリックします。
カスタム描画コード
オブジェクトのいずれかに「カスタム描画」トリガー内のコードが含まれている場合は、そのコードに統計分布などの乱数ジェネレータを使用する関数への呼び出しが含まれていないことを確認します。
アイテムの場所
コードのいずれかでアイテムの場所を問い合わせている場合は、問い合わせる前にそのアイテムまたはそのアイテムを含む固定リソースオブジェクトに対する関数updatelocations()を呼び出す必要があります。updatelocations()が呼び出されなかった場合は、アイテムが最後に描画された位置が返されます。描画レートは実行速度によって異なるため、updatelocations()が呼び出されなかった場合は、モデルを低速で実行したか、高速で実行したか、別のハードウェアで実行したかによって結果が異なります。
(たとえば、コンベヤなどを使用して)アイテムを移動する際のロード/アンロードでは、その位置を問い合わせる必要があることに注意してください。そのため、アイテムを移動位置に対してロード/アンロードする直前にupdatelocations()を呼び出します。
変数の初期化
カスタムコードで宣言された変数は、必ず、何らかのデフォルト値に初期化します。初期化しなかった場合、変数にランダムデータが含まれることがあります。明示的に設定される前の変数へのアクセスが想定される状況では、その変数内のランダムデータによって、モデル内で再現不可能な動作が引き起こされる恐れがあります。
int myNumber; //the myNumber variable will have random data
int myValidData = 0; //by initializing to a value that I can account for in my code, I guarantee that no random behavior will be introduced by uninitialized variables
treenode myObject; //because I did not initialize this value, there is no telling what it is referencing. It is not NULL by default!
treenode myNode = NULL; //much better to specify a NULL treenode, or some other treenode, as the default value, rather than assume (or hope) that an uninitialized variable will be NULL.
データのリセット
モデルの実行中に永続的データの値が変更された場合、モデルをリセットしてもその永続的データは元の状態に戻りません。永続的データの例には、グローバルテーブルやオブジェクトラベルが含まれます。必ず、データを元の状態に戻すコードをOnResetトリガーに追加してください。
オブジェクトラベルの場合は、オブジェクトの[ラベル]ページにある[自動的にラベルをリセット]をオンにすることでデータを元の状態に戻せるようになります。
再現不可能なデータ
ロジックのいずれかが、常に変化するデータに基づいている場合、モデルは絶対に再現可能にはなりません。このようなロジックの部分は、決定または条件が再現不可能なデータに基づかないように再設計する必要があります。
再現不可能なデータの例には、メモリアドレス(tonum()を使用したツリーノードのメモリアドレスの取得。tonum()は廃止予定のため、今後は使用を控えるべき)とリアルタイム(realtime()関数を使用した現実世界のタイムスタンプの取得)があります。
すべてのモデリングエラーを解消する
[システムコンソール]または[コンパイラーコンソール]のエラーを引き起こす問題を追跡して修正します。これらのエラーを無視しないでください。これらのエラーは、モデル内の何かに問題があり、修正する必要があることを示しています。
その他の問題としては、モデル内のエラーが予測不能な動作を引き起こし、モデルを再現不可能にする恐れがあります。
再現不可能なモデルのデバッグ
上記の提案を適用してもモデルが再現可能な結果を示さない場合に、モデルをデバッグする最善の方法は、[デバッグ]メニューの下にあるイベントログを調査することです。
- テスト要件に基づいてモデルをセットアップします。
- イベントログが開き、[ロギングを有効化]チェックボックスがオンになっている
- 開始時間と終了時間の値が設定されている
- さまざまなインターフェイスウィンドウが開いたり閉じたりしている(3Dビューやグローバルテーブルなど)
- など
- モデルを保存します。
- FlexSimを閉じます。
- モデルをダブルクリックし、開いたモデルでFlexSimを開きます。
- [リセット]を1回押して、[実行]を1回押します。
- 実行が完了するか、[イベントログ]ウィンドウの必要な[停止時間]が経過したら、[エクスポート]を押して、ログをCSVファイルとして保存します。
- [リセット]を1回押して、[実行]を1回押します。
- 2回目の実行が完了したら、新しいイベントログを別のファイル名でエクスポートします。
- これで、2つのイベントログができたため、それらをWinMergeなどのテキスト比較ツールを使用して比較することができます。
- 2つのログで時間が異なる最初のイベントを探します。
- そのイベントに関与したオブジェクト、つまり、そのイベントを作成したオブジェクトを検査して、2つの実行の相違点を確認します。
- その相違点の直前までモデルを再実行してから、相違するイベントまでイベントをステップスルーします。デバッグツールを使用して、必要なコードをステップスルーします。
モデルによっては、イベントログの[イベント]ヘッダーをクリックして、特定のイベントを除去することによって時間を節約できる場合があります。たとえば、メッセージパラメータの1つとしてメモリアドレスを含むメッセージを送信する場合は、それらのアドレスがシミュレーション実行ごとに異なるため、エクスポートしたログファイルでその他の違いを見つけるのが難しくなります。このようなことが想定される場合は、ログファイルから該当するイベントを削除することで解消することがあります。
モデルを開いただけではリセットされないメインまたはビューツリー内のアプリケーションの状態や変数に対して、モデルが何らかの操作を行うことがあります。このような場合、FlexSimを閉じて開き直すことでリセットできます。これにより、後続の実行が同じでない場合でも、各テストの1回目の実行とその次のテストの1回目の実行を一致させることができます。モデルがFlexSimを閉じない単純なリセット/実行後に再現可能にならなかった場合でも、ステップ3~6によって同じ実行を生成するはずです。
追加のメモリのアドレス指定に関する考慮事項
オブジェクトのメモリアドレス(tonum()コマンドを使用して取得、tonum()は廃止予定のため、今後は使用を控えるべき)がモデリングロジックで使用されている場合は、doubleとして保存する必要があります。それらを整数にキャストすると、問題が発生します。intは符号付きのデータタイプですが、メモリアドレスは符号なしです。そのため、整数として保存されたメモリアドレスに対してtonode()を呼び出すと、その関数が負数になる可能性があり、実際のノードを返す代わりにNULLを返すことになります。
64ビットシステムではこの問題が悪化します。整数は32ビットですが、64ビットシステムのメモリアドレスは64ビットです。これは、intより大きいメモリアドレスを持つ可能性があり、そのアドレスをintにキャストすると切り捨てられることを意味します。tonode()を使用してノードを参照しようとすると、メモリ内の別の場所が参照されます。必ず、メモリアドレスはdoubleとして保存してください。
より適切な例としては、参照を直接使用するか、参照をできるだけ速やかにツリーノードにキャストする方法があります。可能な場合はいつでも、メモリアドレスを宣言された変数として保存しないようにしてください。たとえば、msgparam(3)がメモリアドレスを返す場合は、次のようになります。
treenode qtde = tonode(msgparam(3)); //convert to a treenode as soon as possible
int qtde = msgparam(3); //don't do this! always convert to treenode as soon as possible or, if necessary, store the memory address as a double
まとめ:
- メモリアドレスはできるだけ速やかにtonode()を使用してツリーノードに変換してください。
- 数字を介してメモリアドレスを渡す必要がある場合は、intではなく、doubleデータタイプを使用してください。
- doubleは64ビットを格納し、その値に対してtonode()が呼び出されたときに正しくツリーノードに戻します。