Interact.jlを動作させる + GraphPlot.jlと併用する

4 min read読了の目安(約4100字

背景

今回はインタラクティブなグラフ操作を実装する Interact.jl の動作環境を作成しつつ,これまで LightGraphs.jl を用いて操作してきたグラフを描画するために利用していた GraphPlot.jl と併用できる環境を作成します.

環境設定

大まかな流れとしては次のように作成しました.

  1. Python環境の用意 (ここではpyenvを利用してpython 3.9.1をインストールしました)
  2. Jupyter labのインストール (最新の3.0.xでは Interact.jl に必要な WebIO.jl の問題が解消出来なかったため,ここでは 2.3.0 をインストールしました)
  3. IJulia.jl, Interact.jl のインストール (ここでは WebIO.jl のドキュメントに従い,しばらく待機しました)

https://juliagizmos.github.io/WebIO.jl/latest/providers/ijulia/

Interact.jl の動作確認

描画環境としては普段からよく使っている Plots.jl + gr とします.

サイクロイド曲線

例として高校生が習うサイクロイド曲線の媒介変数表示を使います.パラメータ a,媒介変数を \theta として

x = a(\theta - \sin\theta), y = a(1 - \cos\theta)

となるような点 (x, y) を集めたものです.

https://examist.jp/mathematics/parameter-polar/cycloid-baikai/

様々なパラメータ a に対する,サイクロイド曲線の Interact.jl + Plots.jl を用いたインタラクティブな描画

媒介変数 \theta について,Juliaのベクトルを利用し,\cos,\sin を適用させることにします.ここで係数 a を変化させると,円弧の形が変化することになるため,これを Interact.jl を用いたスライダー (左右に動くUI) を利用して変化を観察してみます.

using WebIO, Interact, Plots
gr()

θ = 0:π/180:2*π;

@manipulate for a in 1:1:10
    x = a .* (θ .- sin.(θ))
    y = a .* (1 .- cos.(θ))
    plot(x, y, xlim=(0, 2π * 10), ylim=(0, 23), lw=3, color=:tomato, label="curve (a=$a)")
end

これを Jupyter lab 上で動作させたものが以下の通りです.うまく動作しましたね.

GraphPlot.jlとの併用と注意点

GraphPlot.jl は Compose.jl を利用した LigtGraphs.jl の可視化ライブラリの一つです.これまでの記事でも利用しています.例えば以下の記事で出力したグラフ構造は,GraphPlot.jl によるものでした.

https://zenn.dev/takilog/articles/89330b26de946c338dea

ここで Plot.jl と同じように GraphPlot.jl を利用したいのですが,これを単純に次のように描くと失敗します.ここでは Barabási–Albert model で作成した適当なサイズのグラフ構造を circular layout によって配置したグラフを描画させてみます.ここでは次のような操作を Interact.jl で描画しようとしています.

  1. グラフを作成する (頂点集合 V=\{1,\dots, n\})
  2. s=1 として,t\in V と変化させたとき,変化した t の値に応じた最短経路を GraphPlot.jl でインタラクティブに描画する
using Plots, Interact, WebIO
using GraphPlot, LightGraphs, Colors, Cairo

# グラフとノードの座標を作成
g = barabasi_albert(20, 3);
pos = circular_layout(g);
s = 1

# tを変化させる
ui = @manipulate for t in 2:nv(g)
    states = dijkstra_shortest_paths(g, s) # 最短経路状態
    pt = enumerate_paths(states)[t] # 状態 -> 経路
    path = Edge[]
    for i in 1:length(pt)-1 # ここの処理はゴミ(雑)
        push!(path, Edge(pt[i], pt[i + 1]))
        push!(path, Edge(pt[i + 1], pt[i]))
    end

    # 頂点と最短経路を塗る
    nc = [v in pt ? colorant"blue" : colorant"turquoise" for v in vertices(g)] 
    ec = [e in path ? colorant"tomato" : colorant"lightgray" for e in edges(g)]
    gplot(g, pos[1], pos[2], edgestrokec=ec, nodelabel=1:nv(g), nodefillc=nc)
end
display(ui)

動作させてみましょう.

動きました!だいたいいい感じですが,円形上のグラフのアスペクト比が GraphPlot.jl のデフォルトになって横に伸びてしまっているので,これを直したいですね.きれいな比にするには,ドキュメントなどには書いていないのですが,Compose.jl の方のデフォルトの画像サイズを修正すると良いことが分かります.この設定ですが,分からなすぎて泣きながら調べていたら,こちらの Qiita の記事に教えてもらいました.

https://qiita.com/yuifu/items/995935e91f4efbbf20cd

さてこの一行を追加して,(7cm, 7cm) ぐらいの1:1比で画像を作成しましょう.上のスクリプトの冒頭にこちらを追加します.

using Compose
set_default_graphic_size(7cm, 7cm)

うまく行った気持ちになるのですが,これで実行すると怒られます.というか次のような出力になってしまいます.

実はこの設定は微妙に曲者で,雑に利用するとこのようなことになることがあります.つまり Jupyter lab に画像を書き出しているところが壊れてしまい,うまく確認ができません.こうなると kernel を再起動するなどしないとうまく回復できません (できる方法を知っている人は教えていただけると助かります).

これの原因ですが,正直よく分かっていません (?????).おそらく using Compose と using Interact が何らかの不具合を起こすことがある,ような気がしています (ここまで調べたならちゃんと調べろという説もあるのですが,Interact.jl + Compose.jl(GraphPlot.jl) をhard useしている人が世の中にあまりいなくて…).

もう少し詳しい理由が分かったらどこかに書いておきたいと思います

memo: Juliaの端末で試すと分かりますが,Interact.jlからimportしてくると1.0cm,Compose.jlからimportしてくると10.0mmと出るので,このあたりに何か多重ディスパッチの齟齬がありそうな気がしています.

今の所,次の書き方で using の影響を薄めにしておくと,正しく動作することを確認しています

import Compose: set_default_graphic_size, cm
set_default_graphic_size(7cm, 7cm)

いずれにしても,この設定によってアスペクト比を直せるようになったので,再度動作確認した結果が次の図です.

動きました!今日はここで終わりです.