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

# 本日は

タイトル通りPythonPlot.jl でプロットした結果の描画・表示での苦労話について話します．

https://qiita.com/SatoshiTerasaki/items/4be31630955b3dbe3bbc

にて行っている整備の延長で見つかったノウハウの共有です．

## 導入

プログラミング言語 Julia では Python の機能を呼び出せる仕組みが充実しています．例えば PythonPlot.jl は PythonCall.jl という Julia から Python を呼ぶパッケージ（ライブラリ）の上に構築された matplotlib の機能を呼ぶパッケージです． PythonPlot.jl/PythonCall.jl が出る前は PyCall.jl をベースに作られた PyPlot.jl が用いられてました． PythonPlot.jl は PyPlot.jl を置き換える目的で開発されています．

https://github.com/JuliaPy/PythonPlot.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 スクリプトがあるとします．

```python
from matplotlib import pyplot as plt

fig, ax = plt.subplots()
ax.plot([1,2,3],[2,3,1])
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135978/3fd8c314-dd43-2755-4694-1779a3db343c.png)

これを PythonPlot.jl を使った Julia のスクリプトとして次のように移植ができます．

```julia
using PythonPlot: pyplot as plt

fig, ax = plt.subplots()
ax.plot([1,2,3],[2,3,1])
```

つまり下記のように赤色の行を消して緑色の行に置き換えただけです．

```diff
- from matplotlib import pyplot as plt
+ using PythonPlot: pyplot as plt
```

カーネルとして IJulia.jl が提供するカーネル Julia 1.10.2 を使用します．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135978/95ac9997-8157-1164-0043-6f7525892615.png)

このように Python の資源を活用して Julia で仕事ができるようになります．

以下ではPythonPlot.jl でプロットした結果の描画・表示での苦労話について話します．それらに対応するための（アドホックではありますが）解決策を提示します．

## ハマった箇所（`pcolor`, `pcolormesh`）

二変数関数の出力を表示する際に `pcolor`, `pcolormesh` を使うことがあります． この時，プログラム実行後に描画が遅い問題あります． 現象を再現する最小限のコードを共有します．
 
```julia
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135978/045ac6c5-eff9-5a8d-e2d0-19d7148d65a0.png)

この記事を書いて気づきましたが，　Pluto Notebook の上で実行すると実行後にセルの編集をするととてももたつく現象も確認できました．

### 処方箋(`Figure` 型に変換して表示)

`PythonPlot.Figure(fig)` などによって PythonPlot の `Figure` 型のオブジェクトを描画するようにすると画面が固まる現象を回避できます．

```julia
julia> using PythonPlot: pyplot as plt
julia> fig, ax = plt.subplots()
```

ここで `typeof(fig)` を実行すると `PythonCall.Core.Py` が得られます．

https://github.com/JuliaPy/PythonCall.jl/blob/e99aa43b3eaf457fcac17ceb9f928f13583d0a13/src/Core/Py.jl#L40-L43

一方で `PythonPlot.Figure(fig)` をするとノートブック環境は PythonPlot.jl が提供する型のオブジェクトを表示しようとします．

https://github.com/JuliaPy/PythonPlot.jl/blob/fe588f4e3b084b9d0ada8641939c6331def77036/src/PythonPlot.jl#L80-L91

下記のコード

https://github.com/JuliaPy/PythonPlot.jl/blob/fe588f4e3b084b9d0ada8641939c6331def77036/src/PythonPlot.jl#L62-L93

を見る限り

```julia
fig.canvas.print_figure(io, format="png", bbox_inches="tight")
```

のようなことをしているのでこのやり方で io に書き込みをしているから問題ないのかもしれません．

ただ下記のコードでも問題なく動作するので `fig::Py` オブジェクトとから適切な MIME を見つけるのに時間がかかってるのかもしれないです（ここは想像で書いています）．

```julia
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135978/db4e0068-10a9-130a-647b-130f8e94a893.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135978/27ae9f87-3565-cdd9-9ae7-de8a252e471b.png)

などがあります．

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

### `isinteractive()` が `false` の場合

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135978/443d2356-807a-3a78-d252-acfc8dd6471e.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135978/0ce18b06-88ac-c05d-f8e6-17c887dd6acd.png)

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

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

追記: `fig` の型が `Py` であれば困らないはずですが，描画パフォーマンスの関係で `Figure(fig)` によって表示させる必要がある場合, `isinteractive()` の出力結果/カーネルの選択によって「二重に描画される」か「何も表示されない」という現象が起きて苦しむかもしれません．．．

### `isinteractive()` の有無を吸収する方法

色々紆余曲折があって下記のようになりました．

セルの最初に下記を実行します：

```julia
# セルの最初にこれを貼り付けておく
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` 関数を呼ぶと約束します．

```julia
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 を作りました．

https://github.com/julia-vscode/julia-vscode/issues/3602