Bokehでスミスチャート

インピーダンス・アナライザやネットワーク・アナライザで測定したインピーダンスをスミスチャート上で見てみたいとき、私は今までVEEを使っていたのですが、これだとプロットの特定の1点についての周波数などの情報がわからず、不便に感じていました。

先日、Bokehを使ってスミスチャートを描いてみたところ、下図のようにマウスオーバーで周波数はもちろん、Rs, Csなど任意の情報を表示させることができ、またズームや移動もマウスで操作でき、非常に使いやすかったので紹介します。

![bokeh.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280874/41e1575d-b471-2af1-c909-34ac34dfe38c.gif)


使いまわししやすいように

1. 測定器などから取り込んだトレースファイルを読込んで、マウスオーバーで表示したいパラメーターを求め、1つのDataFrameにまとめる
2. Bokehチャートの標準の目盛り線を消して、スミスチャートのグリッドを表示する

の2つをそれぞれ関数として準備し、これらを使ってスミスチャートを描画します。

# 1. トレースファイルをDataFrameにまとめる

トレースファイルを読込んで、必要なパラメーターとともにDataFrameにまとめる部分を1つの関数にしておきます。

下記の例では、トレースファイルのヘッダー部分を除いた7行目から取込み、1, 2列目がそれぞれインピーダンス、位相となっています。
周波数はこの関数内で作成していますが、トレースファイルに記述されていればそれを取込んだほうがいいと思います。その辺はお使いの測定機に合わせて変更してください。

一旦、複素数のインピーダンスを求めてから、マウスオーバーで表示したいRsやCsを計算しています。その他に、Rp, CpやX、Lなど、マウスオーバーで表示したいものがあればここで追加してください。

ただし、DataFrameにgamma_realとgamma_imag列がありますが、これは反射係数の実部と虚部で、プロットに使用するので、マウスオーバーで表示する、しないに関わらず必須です。

ここでの計算は複素数の配列の計算になりますが、Numpyの関数を使えば配列丸ごと1行のコードで計算できるので極力Numpyを使用します。

```Python
import pandas as pd
import numpy as np

def get_wfm(filename):
    # 周波数軸を作る
    freq = np.linspace(500e3, 9.5e6, 401)
    
    # トレースファイルを読み込む
    wfm = pd.read_csv(filename, header=6)
    
    # ndarrayで値を取得する
    z_abs = wfm.iloc[:, 0].values
    z_phase = wfm.iloc[:, 1].values
    
    # 表示に必要なパラメーターを計算してDataFrameにまとめる
    zs = z_abs * (np.cos(np.radians(z_phase)) + 1j * np.sin(np.radians(z_phase)))
    gamma = (zs - 50) / (zs + 50)
    
    df = pd.DataFrame({"f_MHz": freq / 1e6,
                       "gamma_real": gamma.real,
                       "gamma_imag": gamma.imag,
                       "z_abs": z_abs,
                       "z_phase": z_phase,
                       "rs": zs.real,
                       "cs_pF": -1 / (2 * np.pi * freq * zs.imag) * 1e12,
                      })    
    return df 
```

# 2. スミスチャートのグリッド表示を描画する

スミスチャートのあの独特のグリッドを作成します。通常の目盛り線ではなくデータのプロットを薄いグレイでそれらしい見栄えにして表示しているだけです。
本当は目盛り線として引いて、スケールをズームしたときなんかに一緒に目盛り間隔が変わってくれると嬉しいのですが、やり方がわからないのでこれで我慢です。どなたかご存じでしたら教えてください。

下記のように1つの関数にまとめておきます。

```Python
def draw_smith_grid(p):
    grid_color = 'gray'
    grid_linewidth = 0.2

    # Bokehの標準グリッドを非表示にする
    p.xgrid.grid_line_color = None
    p.ygrid.grid_line_color = None

    # SmithChartの外周を描く
    rads = np.radians(np.arange(0, 361))
    p.line(x=np.cos(rads), y=np.sin(rads), color=grid_color, line_width=grid_linewidth)
    
    # 純抵抗のラインを引く
    p.line(x=[-1, 1], y=[0, 0], color=grid_color, line_width=grid_linewidth*2)

    # 定抵抗円を描く
    rs = [0.75, 0.5, 0.25]
    for r in rs:
        p.line(x=r*np.cos(rads) + 1 - r, y=r*np.sin(rads), color=grid_color, line_width=grid_linewidth, legend_label='impedance')
        p.line(x=r*np.cos(rads) - 1 + r, y=r*np.sin(rads), color=grid_color, line_width=grid_linewidth, legend_label='immittance', visible=False)

    # 定リアクタンス円を描く
    rs = np.logspace(-6, 6, 300)
    xs = [0.5, 1, 2]
    for x in xs:
        z = rs + 1j*x
        y = 1 / z
        gamma = (z - 1) / (z + 1)
        p.line(x=gamma.real, y=gamma.imag, color=grid_color, line_width=grid_linewidth, legend_label='impedance')
        p.line(x=gamma.real, y=-gamma.imag, color=grid_color, line_width=grid_linewidth, legend_label='impedance')
        gamma = (y - 1) / (y + 1)
        p.line(x=gamma.real, y=gamma.imag, color=grid_color, line_width=grid_linewidth, legend_label='immittance', visible=False)
        p.line(x=gamma.real, y=-gamma.imag, color=grid_color, line_width=grid_linewidth, legend_label='immittance', visible=False)

```

# 3. Bokehチャートを準備する 

マウスオーバーで表示したいパラメーターを指定するリストを準備します。
`(マウスオーバーで表示したい名前, @DataFrameの列名)`という形で指定します。

```Python
TOOLTIPS = [
        ("index", "$index"),
        ("(x,y)", "($x, $y)"),
        ("f_MHz", "@f_MHz"),
        ("z_abs", "@z_abs"),
        ("z_phase", "@z_phase"),
        ("rs", "@rs"),
        ("cs_pF", "@cs_pF"),
]
```

このTOOLTIPSをチャートを作るときに指定します。

```Python
from bokeh.plotting import figure, output_file, show, reset_output, output_notebook

output_notebook()
p = figure(width=500, height=500, 
           tooltips=TOOLTIPS,
           x_range=(-1.05, 1.2), y_range=(-1.05, 1.2),
           active_scroll="wheel_zoom")

# SmithChartのグリッドを描く
draw_smith_grid(p)
```

スミスチャートが楕円ではなく円で表示されるよう、チャートのサイズと表示レンジはうまく調整する必要があります。
チャートを生成したら、`draw_smith_grid()`を呼び出して目盛りをスミスチャートのグリッドに変更しておきます。

# 4. トレースデータを読込んでスミスチャートを描く

次にトレースデータを読込んでスミスチャートを描いていきます。下記の例ではwfmフォルダに保存したファイルをすべて描画しています。

スミスチャートは普通のX-Y座標に反射係数の実部と虚部をそれぞれプロットすればいいだけなので、先ほど作った`get_wfm()`でDataFrameを取得し、gamma_realとgamma_imag列の値を使用してプロットします。

マウスオーバーするときに、データポイントがわかりやすいようにlineにmarkerを設定したいのですが、そのような機能はないようなので、同じデータをcircleでもプロットしています。同じデータを2本プロットすることになってしまいますが、legend_labelの値が同じなら複数のプロットも1つの凡例にまとまってくれるようですので、実用上これで問題ないです。

最後に`legend.click_policy`を設定しておくことで、凡例をクリックするとその系列を表示 / 非表示に切り替えることができます。

```Python
import glob
import os
from bokeh.palettes import Blues4

files = glob.glob('wfm/*.csv')

for i, file in enumerate(files):
    df = get_wfm(file)
    p.line(x="gamma_real", y="gamma_imag", source=df, color=Blues4[i], legend_label=os.path.basename(file))
    p.circle(x="gamma_real", y="gamma_imag", source=df, color=Blues4[i], legend_label=os.path.basename(file))

p.legend.click_policy = "hide" 
show(p)
```

以上を実行すると下図のようになります。

![bokeh_plot (7).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280874/196289a3-8627-5f8a-2237-bd7e88aeaedf.png)