本日は
タイトル通りPythonPlot.jl でプロットした結果の描画・表示での苦労話について話します.
にて行っている整備の延長で見つかったノウハウの共有です.
導入
プログラミング言語 Julia では Python の機能を呼び出せる仕組みが充実しています.例えば PythonPlot.jl は PythonCall.jl という Julia から Python を呼ぶパッケージ(ライブラリ)の上に構築された matplotlib の機能を呼ぶパッケージです. PythonPlot.jl/PythonCall.jl が出る前は PyCall.jl をベースに作られた PyPlot.jl が用いられてました. PythonPlot.jl は PyPlot.jl を置き換える目的で開発されています.
PythonPlot uses the Julia PythonCall.jl package to call Matplotlib directly from Julia with little or no overhead (arrays are passed without making a copy). It is based on a fork of the PyPlot.jl package, which uses the older PyCall.jl interface to Python, and is intended to function as a mostly drop-in replacement for PyPlot.jl.
(PythonPlot.jl の README.md から一部抜粋)
例えば次のような Python スクリプトがあるとします.
from matplotlib import pyplot as plt
fig, ax = plt.subplots()
ax.plot([1,2,3],[2,3,1])
これを PythonPlot.jl を使った Julia のスクリプトとして次のように移植ができます.
using PythonPlot: pyplot as plt
fig, ax = plt.subplots()
ax.plot([1,2,3],[2,3,1])
つまり下記のように赤色の行を消して緑色の行に置き換えただけです.
- from matplotlib import pyplot as plt
+ using PythonPlot: pyplot as plt
カーネルとして IJulia.jl が提供するカーネル Julia 1.10.2 を使用します.
このように Python の資源を活用して Julia で仕事ができるようになります.
以下ではPythonPlot.jl でプロットした結果の描画・表示での苦労話について話します.それらに対応するための(アドホックではありますが)解決策を提示します.
ハマった箇所(pcolor
, pcolormesh
)
二変数関数の出力を表示する際に pcolor
, pcolormesh
を使うことがあります. この時,プログラム実行後に描画が遅い問題あります. 現象を再現する最小限のコードを共有します.
using PythonPlot: pyplot as plt
fig, axis = plt.subplots()
data = zeros(513, 513)
c = axis.pcolormesh(data, cmap="RdBu_r", vmin=-1, vmax=1)
fig.colorbar(c, ax=axis)
fig
Julia の REPL で動作させる分には問題ないですが, Jupyter NoteBook, VS Code, Pluto Notebook などで動作させると描画にとても時間がかかります. VS Code の場合ですと下記の添付の画像のように 1 分ほど待たされます.
この記事を書いて気づきましたが, Pluto Notebook の上で実行すると実行後にセルの編集をするととてももたつく現象も確認できました.
処方箋(Figure
型に変換して表示)
PythonPlot.Figure(fig)
などによって PythonPlot の Figure
型のオブジェクトを描画するようにすると画面が固まる現象を回避できます.
julia> using PythonPlot: pyplot as plt
julia> fig, ax = plt.subplots()
ここで typeof(fig)
を実行すると PythonCall.Core.Py
が得られます.
一方で PythonPlot.Figure(fig)
をするとノートブック環境は PythonPlot.jl が提供する型のオブジェクトを表示しようとします.
下記のコード
を見る限り
fig.canvas.print_figure(io, format="png", bbox_inches="tight")
のようなことをしているのでこのやり方で io に書き込みをしているから問題ないのかもしれません.
ただ下記のコードでも問題なく動作するので fig::Py
オブジェクトとから適切な MIME を見つけるのに時間がかかってるのかもしれないです(ここは想像で書いています).
begin
using FileIO
import ImageMagick, ImageShow
io = IOBuffer()
show(io, MIME("image/png"), fig)
load(Stream{format"PNG"}(io))
end
ひとまず fig::Figure
にして表示させると良いでしょう.ちなみに PythonPlot.gcf()
でも現在の figure object を表示させることもできますが,この戻り値は PythonCall.Py
ではなく PythonPlot.Figure
の型として得られます.
ハマった箇所(二重に描画される)
matplotlib + Jupyter ユーザなら一度は目にする現象だと思います.
回避策として (isinteractive()==true
の場合)
-
fig
を書かずにaxis.plot(xs, sin.(xs))
の行で出力をさせる -
fig;
と記述する.すなわち,セミコロンをつけて表示を抑える -
display(fig)
を使用する
という方法があります.ただし,このテクニックが有効なのは isinteractive()
が true
となるセッションでJuliaを動かしている環境です.isinteractive()
が true
になるのは
- Julia の REPL
- IJulia.jl で導入した Jupyter のカーネルを選んだ場合(
jupyter notebook
などのコマンドでノートブックを起動する) - VS Code で下図のカーネルを選んだ場合(IJulia.jl で導入されるカーネル)
などがあります.
さて,これで終われば苦労はしません.どういうことかというとノートブック的な環境で isinteractive()
が false
を出す環境があるからです.
isinteractive()
が false
の場合
そんな環境あるのか?というと実はあって
-
julia sample.jl
のように実行した場合(それはそう) - Pluto.jl のノートブック(へー...)
- VS Code + Jupyter Notebook の環境でカーネルとして juliaup が提供するものを選んだ場合(下図参照)
Julia 1.10 channel
のカーネルを選んだ場合,下記のように fig
を最後につけても二重に表示されません.そして isinteractive()
は false
となる.
これはこれで便利ではありますが,複数人で開発を行う際に使うカーネルが IJulia が提供するものか juliaup が提供するものかをあまり意識したくない(させたくない)でしょう.
JupyterBook によって教材を作成するシチュエーションでは,Julia は isinteractive()
が true
で動作します.普段 VS Code の juliaup が提供するもので教材作成・デバッグをして,いざとなった時にグラフが二重に表示される,表示されない.という問題に気づきます.
追記: fig
の型が Py
であれば困らないはずですが,描画パフォーマンスの関係で Figure(fig)
によって表示させる必要がある場合, isinteractive()
の出力結果/カーネルの選択によって「二重に描画される」か「何も表示されない」という現象が起きて苦しむかもしれません...
isinteractive()
の有無を吸収する方法
色々紆余曲折があって下記のようになりました.
セルの最初に下記を実行します:
# セルの最初にこれを貼り付けておく
using PythonCall: PythonCall
using PythonPlot: pyplot as plt, Figure
# Displays the matplotlib figure object `fig` and avoids duplicate plots.
_display(fig::Figure) = isinteractive() ? (fig; plt.show(); nothing) : Base.display(fig)
_display(fig::PythonCall.Py) = _display(Figure(fig))
これにより _display
という自前の関数を定義しました.次にプロットするセルでは _display
関数を呼ぶと約束します.
using PythonPlot: pyplot as plt
fig, axis = plt.subplots()
xs = -3:0.01:3
axis.plot(xs, sin.(xs))
_display(fig)
こうすると Jupyter Notebook 周りのエコシステムでは isinteractive()
の出力で二重に表示されるとか出力が抑制されるというズレを気にする必要がなくなります.また,fig
が PythonCall.Py
の場合,描画の速度の低下を抑えるために事前に Figure(fig)
へ変換するという作法を _display
関数が担うようにしています.将来 isinteractive()
周りの問題が仮に解決した場合は _display
に関する部分だけを変更すればいいので先を見越したメンテナンスがしやすくなります.
ややアドホックですが解決策を提示しました.
Issue として出した方が良いなということで重い腰を上げて Issue を作りました.