AKAGI Rails

鉄道模型シミュレーターで遊んでいたはずが、気づいたらPythonなども。

Pydoc-Markdown をお試し

Pythonのdocstringを自動でドキュメント化するときはSphinxが定番ですが,それに代わるPydoc-Markdownというのを見つけたので,備忘録がてら紹介します。

pypi.org

インストール

pipからインストールできます。pipxでのインストールが推奨されています。私は無視して普通にpipでインストールしましたが,言われたとおりにするとしたら

pip install pipx
pipx install pydoc-markdown

どうせ必要になるので,MkDocsも入れておきます。MkDocs-Materialも。

お試し

Pydoc-MarkdownのGithubからまるごとダウンロードしてきて,Pydoc-MarkdownでPydoc-Markdownのドキュメントを作ってみます。

pydoc-markdown-develop (pydoc-markdown.yamlとかreadme.mdがあるディレクトリ) でターミナルを開き,次のコマンドを実行すると,ブラウザが立ち上がりlocalhostでドキュメントが読めます。

pydoc-markdown  --watch-and-serve -o

もう少し詳しく

pydoc-markdown.yaml の中身を見てみると,ここでドキュメントの構造をはじめいろいろなオプションを設定していることが分かります。

pydoc-markdown.yamlのひな型は

pydoc-markdown --bootstrap-mkdocs

で生成できます。

ただしコイツの書き方の詳しいところが私もよく分かっていません。pydoc-markdown-develop/pydoc-markdown.yamlを雰囲気でマネするのがいいかも。

HTML化

./docs/build をさらにMkDocsでビルドすると,HTMLドキュメントにできます。手順はCaldia氏のMkDocsマニュアルが参考になりますのでご参照ください。

蛇足

VRMNXpy用の拡張パッケージを作れないかと思っていろいろ試していますが,マニュアル作りのことも考えておかないといけません。 しかしSphinxではコードを一通り実行してdocstringを整形するので,実行中にvrmapiのImportErrorが出ると止まってしまい,うまくいきません。

その点Pydoc-Markdownは,コードを実行せずにdocstringをパースして整形してくれるので,この問題を回避できる…と思われたのですが,謎のエラーに遭遇しておりこちらもうまくいっていません。

STATIONflowも気になっている

factorioはまだロケットが打ち上がっていませんが、一旦飽きが来ました(苦笑) コロナ騒ぎで再注目を浴びたPlague Inc.というゲームもやってみました。じっくり系のゲームではなく、テーブルゲームのようなゲームサイクルの感覚でした。

ちょっと前に教えてもらった、STATIONflowというゲームも気になっています。地下鉄の駅を作り、発生する乗客の流れを満足させていくゲームのようです。

各エージェントが視野を持っていて、案内サインを見つけてそれぞれ行動を決定するという、なかなか凝った作りになっています。

続きの話はゲームと関係なくなってくるので、暇で暇で仕方がなくパソコンを閉じても他にやることが本当に無い方だけお読み下さい。

交通流ネットワークの2つの切り口

交通流ネットワークの見方には、大きく2つのアプローチがあります。 ひとつはネットワークを「静的」なものとして捉え、一定の交通フローが定常的に存在する(または、1回きりの流れが発生する)ような状態を考える切り口。もう一つは、時々刻々と流れが変化していく「動的」なものとして捉える切り口です。

「静的」なモデルでは渋滞の再現が難しいなど解析できることに限りがあるため、近年では「動的」な切り口での話が多いと思います。STATIONflowもリアルタイムにエージェントがその時の周囲の状態に合わせて行動するというものなので、「動的」な交通流シミュレーションになっています。

ところが静的モデルも結構面白い話題があるので、自分の勉強がてら紹介したいと思います。前から興味があったのでいい機会ですw

静的モデルの基本

こういう系で最も分かりやすい基本的なものは、「最大流問題」ではないでしょうか。

あなた=奉行は、江戸から上田にできるだけたくさんの物資を運ばなくてはなりません。各街道(隣接した2地点間)の人足には限りがあるので中山道だけではなく別の裏街道を使ったルートも検討することになっていますが、物資を溢れさせずに運びきれる量はいくらで、どの道にどれだけ通すのがよいでしょうか。

f:id:AKAGI-vrmstation:20200507125330p:plain
適当な地図

グラフ理論の言葉では、各地点を頂点(ノード)、街道を枝(エッジ)といいます。そして江戸-熊谷-高崎-軽井沢-小諸-上田 が中山道のルートですね。このような一連の経路のことをパスといいます。

図には書いていませんが、各枝には、人足に応じて運べる物資の上限が決まっているものとします。

本筋とは逸れますが草津を通って草軽交通のルートで軽井沢に出るルートがあったり、菅平のほうを回って直接上田に出るルートもあるのがマニアックですねw 果たしてそのルートを使ってどれだけ輸送量を増やせるのやら(笑)

交通流ネットワークの利用者均衡条件

もう少し交通流っぽい状況を考えてみましょう。交通流の場合は、シンプルな問題と違って、

  • ネットワーク内にいろいろな種類のODペア(起終点=Origin→Destinationの組み合わせのこと)の流れが発生する
  • 流量が先に決まっている
  • 混雑すると余計に時間(≒コスト)がかかる
  • 多少遠回りでも空いている道なら、早く着くので通る人が現れる

そこで、 誰もが「比べたら一番早く着く経路」を選ぶ ときに全体としてどのような流れが出来上がるか、を考えます。 このとき、例えば江戸-上田間で、中山道経由と川越街道経由の流れができたとして、「どちらの経路も結局所要時間は同じ」で「他の経路の所要時間よりも短いか、せいぜい等しい」ということが結果的に成り立ちます。なぜなら、仮にどちらかのルートのほうが早かったとしたら、より時間の掛かるルートから人が流れてきて混雑度が増し、2つのルートの所要時間が等しくなるように均衡するからです。

この均衡状態が成り立つことをはWardropの第1原則(利用者均衡条件)といいます。

新しい道ができたのに遅くなる?

直感的には納得しづらいですが、Wardropの第1原則に基づいて調べると、「道を増やすと全体の所要時間の合計が悪化する」事態が起きることがあります。この現象は「Braessのパラドクス」と名前までついています。調べるとWikipediaにも記事がありますが、平易なものでは「高校数学の美しい物語」が分かりやすいです。

mathtrain.jp

極端な例ではありますが、Wikipediaによると、「うまくない道路」を閉鎖して交通を改善した例があるそうです。

交通流ネットワークのシステム最適化

Braessのパラドクスから、利用者それぞれが自分のことだけを考えて通る道を選んでいては、利用者一人ひとりも損をするようなケースがあることが分かります。逆に言えば、社会全体として所要時間を最小化するには、別の通り方(通し方)があるということです。

このとき、先ほどと同様に、仮に中山道経由と川越街道経由の流れが決まったとして、「どちらの経路の限界所要時間も同じ」で、「それ以外の使われていない経路の限界所要時間よりも短いかせいぜい等しい」という均衡条件が成り立ちます。こちらはWardropの第2原則と呼ばれます。限界所要時間って何よ?というと、「流量を1増やしたときに余分に増える所要時間」のことです。

仮に、限界所要時間がアンバランスな状態があったとしたら、お上の目線で考えて、「限界所要時間の大きい経路」の流量を減らして「限界所要時間の少ない経路」に振り替えれば、全体の所要時間合計を短縮できてしまいます。「全体の所要時間が最小」ということは、振り替えて所要時間合計を短縮できるような経路がないということなので、先ほどの均衡条件が成り立っていることが言えます。

(ここでいう限界というのは、経済学方面で、英語のmarginalに充てられている訳語です。)

システム最適フローを求めることはちょうど、リンクコスト=各枝の交通量に対して全体に発生している費用が下に凸の多品種最小費用流問題を解くことになります。(つまり、増えれば増えるほど混雑に対応する余分なコストがかかる状態を表します。)(よね?ちょっと自信がない…) ふつうの最小費用流問題問題に比べると解くのに手間がかかります。

静的な解析の限界

こんどはmarginalの意味ではなく、limitationsですかね。

  • Wardropの第2原則(全体最適化)は、理論的に考えることはできるものの、実現についてハードルが高い。
  • 第1原則も、各交通利用者がネットワーク上の交通量や混み具合について完全に知っていることを仮定しており、この仮定は現実に成り立っているとは言い難い。 (最近は高性能のカーナビが、リアルタイムの渋滞情報に基づいて経路を計算してくれますが…。)
  • 現実には、ある程度以上の交通需要がドバっと発生すると、詰まってしまって動かなくなり、渋滞が他の道に伝播する現象が起きるが、再現できていない

などの限界があります。

おわりに

  • 駅の人の流れというと、渋谷駅が槍玉に上がることが多い気がします。銀座線乗り場のリニューアルで改札周りが混むようになったとか、東横線から井の頭線への最短ルートはサインに出ておらず隠されているとか。原理的に仕方のない部分とか、敢えて情報を歪めて全体最適に近づけているとか、そういう事情があるものと思われます。
  • 正しくないことを書いていたらごめんなさい。優しく教えて下さい。
  • こっちの方面をマジメに勉強したい場合、その手の専門書はSTATIONflowよりも高いです。

vrmapi.LOG()に文句タラタラ

VRMNXでログ出力に使うvrmapi.LOG()ですが,いや,実はコレが文句タラタラでして・・・。

  • 型がstr, int, floatしか受け付けてくれない
  • したがってうっかりdictやlistを入れるとTypeErrorが出てくる
  • listやdictを使うシーンでのデバッグではstr()を噛まさないといけないけど面倒くさいし,忘れる
  • pythonオブジェクトのstr属性のことを何だと思っているんだ!

こういうものは,仕方がないので自分で何とかする。

def LOG(*objects, sep=' ', end=''):
    """すこし賢いログ出力。
    
    Python標準のprint()とほぼ同じ形式でVRMNXのスクリプトLOGに出力します。
    objectsのすべてにstr()を引っかけて,sepで区切りながらつなげて出力します。
    スクリプトLOGではString型と表示されますがobjectsの実体とは無関係です。
    sep, endを設定する場合キーワード引数で設定してください。
    
    Args:
        objects: LOGに出したいオブジェクト。str()した上で出力します。
        sep: objectどうしの区切り。
        end: 行末。
    """
    output = sep.join(map(str, objects)) + end
    vrmapi.LOG(output)

文句タラタラだけど,旧世代のVRMスクリプトよりは5000兆倍素晴らしいのでがんばってやっています。

でも実は,地味にすごい(?)機能もログウィンドウには付いています。

f:id:AKAGI-vrmstation:20200503141550p:plain
エラーを出したコードが書いてあるオブジェクトがどれだか分かる

エラーロガーとしては当たり前の機能なような気もしますが,VRMNXのPythonエンジンはどのオブジェクトに書いたコードも区別なく実行する…ように見えていたのに,VRMNX側(vrmapiの実体)には区別できるらしいです。

Advanced Trackのポイントのまくらぎが気になったので無理矢理直した

Advanced Trackは高精細な作りでレイアウトの見た目をぐっと引き締めてくれますが,見た目上どうしても気になる点があります。

それは, PCまくらぎが使われている 点です。

f:id:AKAGI-vrmstation:20200427202353j:plain
本来はポイントにPCまくらぎは使わない

PCまくらぎというのは,ネジを締め込むための穴を予め決まった場所に開けてあるので,ポイントのようにいろんな場所にクギを打ち込む場所には使わないみたいです。ということで,ポイントの部分だけはいまだに木まくらぎが使われている場所が多いです。

f:id:AKAGI-vrmstation:20200427203818j:plain
スラブポイントは合成まくらぎみたいなのが付いている

Advanced Trackはリアルな作りがウリなのに,まくらぎだけイマイチなので残念です。

そこで,「線路付帯設備」に入っている,単品のまくらぎを重ねて並べてみました。

f:id:AKAGI-vrmstation:20200427202350j:plain
ついでに転轍機も

線路に対して+3.6mmで,4mm間隔で並べると,ちょうどPCまくらぎの模様に重なります。(ただし,分岐側のまくらぎ模様が斜めになっている部分は下が見えてしまいます。ボルトの位置も合わない…w)

f:id:AKAGI-vrmstation:20200427204635p:plain
1936ポイント1基に104本の枕木(木製)が並べてある

左分岐を作って力尽きました。右は今は作りたくありませんw

Factorioのサプライチェーンネットワーク

Factorioの各アイテムについて、材料として何が必要かの関係性をとりあえず図示してみました。

  1. 各アイテムの材料をExcelにデータベースのように打ち込む(手打ちしたw)
  2. NetworkXというライブラリで、アイテムをノード、依存関係をアークとするようなグラフをExcelを元に作る
  3. NetworkX (というかmatplotlib.pyplot) で可視化

f:id:AKAGI-vrmstation:20200421235538p:plain
必要なモノ同士が矢印で結ばれている

ゲームの進み具合的に、まだ宇宙サイエンスパック(白フラスコ)を製造する必要がなさそうなので、それ以外のフラスコの製造に必要なアイテムのみ抽出してグラフにしてあります。

各ノードの位置関係についてはまだしっかりと作り込んでいませんが、このグラフに表された関係性が、各アイテムの必要数の解析や、施設配置最適化を考えていくための土台となります。

Factorioに血が騒ぐ

Factorioというゲームは前から知っていましたがなかなかやらずにいました。

f:id:AKAGI-vrmstation:20200415235925p:plain

ところがですね、やってみると意外にこれが面白い!そろそろ石油工場がオープンするあたりまで進んできました。

store.steampowered.com

工場(といっても、広大な大地に直接いろんな設備を置く)を作っていろいろなものを生産していくゲームです。燃料を採掘し、鉄鉱石を掘り、様々な加工・組立のプロセスを繰り返していきます。

ゲームをすすめていくと物流手段に「鉄道」が使えるようになるのもハマる要素の一つですが、 実は「経営工学」を専攻していたので、IE(インダストリアル・エンジニアリング)には一家言あるわけですワ~(ゲームをするのはヘタクソですが)。

各工程を担当させる加工機械をどのように配置するか、は工場内レイアウトを考えていることになります。そのためのIE的手法に、SLP (Systematic Layout Planning) というものがあります。

SLPをする上では工程(施設)どうしの関連性を調査する必要もありますが、そのためにはアローダイアグラムが使えます。アローダイアグラムは実際の工場の工程編成を決めるのにも使われていたはずです。

各工程では、「必要なものが 必要なときに 必要なだけ」届く必要があります。このくらいはJITとかカンバンとかいろいろな言葉で、社会的にも断片的に認知されているはずです。カンバン方式などプル型の生産システムではは、後工程から前工程に注文を伝播させていく形式で、下流工程が上流工程のことをあまり気にしなくてもよいように工夫されています。(が、各ポイントでのカンバンの枚数を決めるのはけっこう大変です)

しかしFactorioでは注文の概念が扱いづらい(回路部品というのを使えばできるかも、と思ったけど、できるのか?)ので、前工程から後工程にものを押し出していくプッシュ型の生産システムを構築するのが簡単そうです。ところが、製品Xと製品Yは共通の部品Pとか共通の原料Aを持っていたりなんかして、「何がどれだけ必要か」は上流にいくほど分かりにくくなります。これを勘定する手段にはBOM(部品展開表)というのがあります。どうやら欧州の自動車製造はBOMで回っているらしいです。

アローダイアグラムを使った分析手法のひとつにPERTがあります。本当は大量生産ではなくプロジェクトマネジメント(一回きりのものごと)の日程計画に使われるツールですが、計算をちょっと応用すると、加工機械の最適な設置台数の検討やボトルネック工程の予測がきくようになります。

ということで、BOM(もどき)とPERT(もどき)を組み合わせて、すてきな工場を作りたいなあと思っています。

VRMNXの時間系イベントが使いにくいなあと思っているアナタへ

実はそんな人いない説

はともかく。

まずはバージョン5以前の時間系イベントとVRMNXpyの時間系イベントの仕様の相違について,Afterイベントを例に見てみましょう。

バージョン5以前のイベントは,

//イベントを設定
SetEventAfter Target Method EventID Interval
// Target: 対象オブジェクト
// Method: 対象オブジェクトのメソッド
// EventID: イベントIDを受け取るグローバル変数(のポインタ)
// Interval: 時間間隔(ms)

BeginFunc Method
    // Intervalミリ秒後に実行する制御の中身
EndFunc

という仕様でした。一方でVRMNXpyでは

#イベントを設定
evid = target.SetEventAfter(Interval)
# target: 対象オブジェクト
# interval: 時間間隔(s)
# (返り値) evid: イベントID

#指定時間経過すると対象オブジェクトのイベントハンドラが呼び出される
def vrmevent_xx(obj,ev,param):
    if ev=='after':
        # Afterイベントが起きたときの制御の中身

聡明な読者諸兄には自明なことかと思いますが,VRMNXpyのイベントは,どのイベントIDであってもとりあえず同じコードを走らせてしまうという仕様になっています。イベントハンドラの中に様々な処理をベタ書きしているといつかバグの温床となるでしょう。

この問題を解決するひとつのアイデアとして,次のような実装を提案することができます。ただし,一部が理解のしやすさのため多少冗長な書き方になっています。

def SetEventAfterAKG(target, callback, interval):
    """ 実行対象の関数を明示するAfterイベントのラッパ
    * vrmapiのイベントハンドラに専用コードを仕込む必要がある *
    Parameters -----
    target: 対象オブジェクト
    callback: このAfterイベントで実行されるコールバック関数
    interval: 時間間隔(秒)
    Returns -----
    eventid

    callback関数はobj, ev, paramの3つのparameterを伴って呼び出されます。
    """
    # vrmapiにイベントを登録
    evid = target.SetEventAfter(interval)
    # 対象オブジェクトのdictにコールバック関数を登録
    d = target.GetDict()
    evkey = 'AKGCB{}'.format(evid)
    d[evkey] = callback
    return evid

# 今回Afterイベントで仕込みたい制御がこちらだとしましょう
def sample_cb(obj,ev,param):
    # 制御の中身(省略)
    pass

# イベントを設定する際のコード
evid = SetEventAfterAKG(target, sample_cb, interval=3)

# 対象オブジェクトのイベントハンドラの中身で・・・
def vrmevent_xx(obj,ev,param):
    if ev=='after':
        # 以下が専用コード
        d = obj.GetDict()
        evkey = 'AKGCB{}'.format(param['eventid'])
        try:
            callback = d[evkey]
        except KeyError:
            # ラッパ経由で登録しなかったAfterイベントにはDictにcallbackが入っていないのでエラーが送出される
            # お行儀は最悪だけどとりあえずシカトする
            pass
        else:
            # Dictからcallbackがエラーなく見つかったときだけコールバックを呼び出し
            callback(obj,ev,param)

これで一応,バージョン5互換(もどき)にAfterイベントのcallback処理を作り込んでいくことができます。イベントハンドラ(vrmevent_xx)の中に処理をベタ書きせず,外へ独立した書き方ができるのでいろいろ楽ちんであると思います。見た目的には,インデントを浅くする効果があり全体がPythonらしいスッキリとした仕上がりになるはずです。 実行時のことを考えると,ほどよく名前空間を切ることになるので不要なオブジェクトのメモリが早く開放されるのではないかと思います。Pythonの関数呼び出しは結構重たいことが知られています(注:リンク先のサンプルコードはPython2仕様である。)がframeイベントで何百個も関数を呼び出すとかでなければ大丈夫でしょう。

target.SetEventAfterAKG(...)ではなく,SetEventAfterAKG(target, ...)でイベントを登録するお作法に混乱するかもしれませんが,これはSetEventAfterAKGがvrmapiのSetEventAfterのように各オブジェクトのメソッドとしてでなく,グローバルな関数として定義することを想定しているからです。

このアプローチであればVRMNXpyにKillEventが存在しない問題も,割と簡単に解決します。KillEventのかわりに,Dictから当該のイベントのcallbackを保存しているメンバを取り除いてしまえばよいのです。

d = obj.GetDict()
d.pop('AKGCB{}'.format(evid_to_kill))