OR-Tools のルート最適化アプリを Bokeh + Heroku でデプロイしてみた
前回 Bokeh + Heroku でアプリをデプロイする記事を書きました。
もう少し工夫して、 OR-Tools でクリックした位置をドットで表示し、ドットをスライダーバーで指定した車両(アプリ中では背景に世界地図を使っているので飛行機と表現)の台数で巡回するルートを計算させるアプリを作成しました。
Bokeh は Streamlit や PyWebIO よりも取り回しが難しい分、クリック や ダブルクリックイベントを認識できる等、かなり自由度が高い印象です。
作成したアプリはこちら
コードはこちら
ファイル構成
Bokeh の実装は app.py に集約しています。
今回は 最適化の実装部分は optimize.py に集約していますが、その辺はお好みで。
|-- app.py # 主に Bokeh による UI を実装した実行ファイル
|-- optimize.py # 最適化部分のライブラリ
|-- Procfile
|-- runtime.txt
|-- requirements.txt
|-- static # 画像を保存するフォルダ
Procfile や runtime.txt 、requirements.txt については手前味噌ですが前回のブログをご参照ください。
クリック等のイベント処理
クリックした際の座標を集約するため ColumnDataSource でインスタンスを作成します。
callback(event)関数内にクリックした際実行したい内容、今回であれば list内にクリックした箇所の座標を追加し、その内容を先ほど作成した ColumnDataSource で作成したインスタンスに指定したりします。
ちょっと気持ち悪いですが、座標を格納する list、coord は callbak(event)内で global変数とするようです。
plot.on_event(bokehのメソッド, 作成した callback関数) とすることでイベントを認識させることができるようです。
クリックの場合は下記のように Tap、ダブルクリックの場合は DoubleTap、ボタンのクリックの場合は ButtonClick などを使います。
from bokeh.events import Tap
coord = []
source = ColumnDataSource(data=dict(x=[], y=[]))
def callback_click(event):
global coord_list
coord=(event.x,event.y) # クリックした位置の座標をタプルに保存
coord_list.append(coord) # 座標をリストに保存
source.data = dict( # 他の機能で共通して使えるよう座標を souse に保存
x=[i[0] for i in coord_list],
y=[i[1] for i in coord_list]
)
plot.on_event(Tap, callback_click)
スライダーバー
飛行機の台数を自由に変えられるよう、スライダーバーを使っています。
ラジオボタンやチェックボックス等、他のヴィジェットを使う場合は下記を参照ください。
from bokeh.models import Slider
slider = Slider(
title="飛行機の数", # タイトル
start=1, # スライダーバーの数値の最小値
end=10, # スライダーバーの数値の最大値
value=3, # スライダーバーの数値の初期値
step=1, # スライダーバーの数値の間隔
)
slider.js_on_change("value", CustomJS(code="""
console.log('slider: value=' + this.value, this.toString())
"""))
スライダーバーで決定した数値は slider.value で取得できます。
線
最適化実行ボタンを押すと最適化を実行し、その結果を矢印線で表示するよう、ボタンイベント用の callback 関数内に 線と矢印を表示する Bokeh のライブラリを仕込んでいます。
plot = figure(
title='任意のタイトル',
tools="tap",
height=xxx, # 任意の数値を指定
width=xxx, # 同上
x_range=(xxx, xxx), # 任意の数値の範囲をタプルで指定
y_range=(xxx, xxx), # 同上
)
def callback_button():
(略)
# 各ルートの軌跡をラインで表示。次の矢印でも軌跡は表示できるが、凡例を作成するために使用。
plot.line(
np.array(all_route_result_coodList[i])[:, 0],
np.array(all_route_result_coodList[i])[:, 1],
legend_label=str(i),
color=d3['Category10'][10][i]
)
# 各ルートの方向を矢印として表示
for j in range(len(all_route_result_coodList[i])-1):
plot.add_layout(
Arrow(
end=VeeHead(line_color="firebrick", line_width=0.1),
x_start=all_route_result_coodList[i][j][0],
y_start=all_route_result_coodList[i][j][1],
x_end=all_route_result_coodList[i][j+1][0],
y_end=all_route_result_coodList[i][j+1][1],
line_color=d3['Category10'][10][i],
)
)
plot.add_layout(plot.legend[0], 'right') # 凡例の追加
色
上記でも触れていますが、色は bokeh.palletes のメソッドで指定することができます。
from bokeh.palettes import d3
d3['Category10']
以下のように、出力したい色の数:(カラーコードのタプル)、という辞書型データとなっているようです。
出力:
{3: ('#1f77b4', '#ff7f0e', '#2ca02c'),
4: ('#1f77b4', '#ff7f0e', '#2ca02c', '#d62728'),
5: ('#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd'),
6: ('#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b'),
7: ('#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2'),
8: ('#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f'),
9: ('#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22'),
10: ('#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf')}
以下のように指定すれば、線や図形に指定した色を使うことができます。
パレットの大分類["パレットの小分類"][出力したいカラーコードの数][使いたいカラーコードのインデックス]
具体的には以下のように使用します。
plot.line(
(省略)
color=d3['Category10'][10][0]
)
背景画像の表示
シンプルに画像を表示する関数がないようで苦労しましたが、こちらのサイトを参考にさせていただきました。
# 画像ファイルの読込、PIL -> numpy への変換
img_path = "static/4564885_m.jpg"
img_file = Image.open(img_path)
img_file = np.array(img_file)
# 画像表示の設定
img = np.empty((img_file.shape[0], img_file.shape[1]), dtype=np.uint32)
view = img.view(dtype=np.uint8).reshape((img_file.shape[0], img_file.shape[1], 4))
view[:, :, 0:3] = np.flipud(img_file[:, :, 0:3])#上下反転あり
view[:, :, 3] = 255
# 画像を表示
plot.image_rgba(
image=[img],
x=0,
y=0,
dw=img_file.shape[1],
dh=img_file.shape[0],
global_alpha=0.3
)
ルート最適化についてはほぼ OR-Tools のサンプルコードと変わらないので下記リンクを載せるのみで詳細は割愛します。
以上になります、最後までお読みいただきありがとうございました。
Discussion