2
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Jupyter Notebook 環境で PythonPlot.jl の描画・表示周りで苦労した話

Last updated at Posted at 2024-04-24

本日は

タイトル通り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])

image.png

これを 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 を使用します.

image.png

このように 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 分ほど待たされます.

image.png

この記事を書いて気づきましたが, 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 ユーザなら一度は目にする現象だと思います.

image.png

回避策として (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 で導入されるカーネル)

image.png

などがあります.

さて,これで終われば苦労はしません.どういうことかというとノートブック的な環境で isinteractive()false を出す環境があるからです.

isinteractive()false の場合

そんな環境あるのか?というと実はあって

  • julia sample.jl のように実行した場合(それはそう)
  • Pluto.jl のノートブック(へー...)
  • VS Code + Jupyter Notebook の環境でカーネルとして juliaup が提供するものを選んだ場合(下図参照)

image.png

Julia 1.10 channel のカーネルを選んだ場合,下記のように fig を最後につけても二重に表示されません.そして isinteractive()false となる.

image.png

これはこれで便利ではありますが,複数人で開発を行う際に使うカーネルが IJulia が提供するものか juliaup が提供するものかをあまり意識したくない(させたくない)でしょう.

JupyterBook によって教材を作成するシチュエーションでは,Julia は isinteractive()true で動作します.普段 VS Code の juliaup が提供するもので教材作成・デバッグをして,いざとなった時にグラフが二重に表示される,表示されない.という問題に気づきます.:innocent:

追記: 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() の出力で二重に表示されるとか出力が抑制されるというズレを気にする必要がなくなります.また,figPythonCall.Py の場合,描画の速度の低下を抑えるために事前に Figure(fig) へ変換するという作法を _display 関数が担うようにしています.将来 isinteractive() 周りの問題が仮に解決した場合は _display に関する部分だけを変更すればいいので先を見越したメンテナンスがしやすくなります.

ややアドホックですが解決策を提示しました.

Issue として出した方が良いなということで重い腰を上げて Issue を作りました.

2
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?