実験の設計

実験中には、測定値をグラフによって可視化したいことがあります。 これを実現するために、 ebilab.experiment.core モジュールを用いることができます。

簡易的な実装

簡易的には、 matplotlib を用いて、以下のようなコードで実現することができます。 これは、マルチメーターを用いて抵抗を測定してプロットするプログラムです。

 1from datetime import datetime
 2import pandas as pd
 3import matplotlib.pyplot as plt
 4from ebilab.experiment.devices import K34411A
 5
 6fig, ax = plt.subplots(1, 1)
 7plt.pause(0.01)
 8
 9multimeter = K34411A()
10data = []
11started_at = datetime.now()
12while True:
13    R = multimeter.measure_resistance()
14    t = (datetime.now() - started_at).total_seconds()
15    print(v)
16
17    # update plot
18    data.append({"t": t, "R": R})
19    df = pd.DataFrame(data)
20    ax.cla()
21    ax.plot(df["t"], df["R"])
22    ax.set_xlabel("Time")
23    ax.set_ylabel("Resistance")
24    ax.grid()
25    plt.pause(0.1)

実際にこのようなコードで運用するには、いくつか問題があります。

このコードにはいくつか問題があります。

  • 実験のロジックを定義するコードと可視化のためのコードが混在している

    • 外れ値をフィルタリングしてプロットしたい場合など、どこまでが実験のロジックでどこからが可視化のためのロジックなのかが分かりづらくなる。

  • matplotlibの描画によってデータの取得がブロッキングされ、データの取得速度に影響する。

  • プログラムファイルへのデータ保存やグラフでの可視化のコードに関して、プログラムを作成する際に同じようなコードを何度も書く必要がある。

ExperimentPlotter を利用した実装

ebilab.experiment.core モジュール に含まれる Experiment クラス、 Plotter クラスを用いることで、 これらの問題を解決することができます。

上記のコードは、以下のように書き変えることができます。

 1# sample of multi-thread measurement / plotting using Plotter / Experiment class
 2import matplotlib.pyplot as plt
 3from matplotlib.figure import Figure
 4
 5from ebilab.experiment.core import Plotter, Experiment
 6from ebilab.experiment.devices import K34411A
 7
 8class ContinuousResistanceMesurement(Experiment):
 9    columns = ["R"]
10    filename = "resistance"
11
12    def steps(self):
13        multimeter = K34411A()
14        while self.running:
15            r = multimeter.measure_resistance()
16            self.send_row({"R": r})
17
18class ResistancePlotter(Plotter):
19    _fig: Figure
20
21    def prepare(self):
22        self._fig, self._ax = plt.subplots(1, 1)
23
24    def update(self, df):
25        df = df.query("R < 1e20")
26
27        self._ax.cla()
28
29        self._ax.plot(df["t"], df["R"])
30        self._ax.set_xlabel("Time / s")
31        self._ax.set_ylabel("Resistance / Ohm")
32        self._ax.grid()
33
34if __name__ == "__main__":
35    experiment = ContinuousResistanceMesurement()
36    experiment.plotter = ResistancePlotter()
37    experiment.start()

Experiment クラス、 Plotter クラスを継承し、 実験のロジックと可視化のロジックをそれぞれ定義することで、実験を設計することができます。 これにより、ロジックを適切に分割し、読みやすいコードを実現することができます。 また、クラス単位で定義することにより、同じ実験で可視化の方法だけを変更したり、別の実験でも同一の可視化方法を用いるなど、 それぞれのコンポーネントを再利用しやすくなります。

ファイルへの保存やプロット用プログラムへのデータの受け渡しに関して考慮する必要はありません。 また、matplotlibの描画中にもデータの取得を継続するため、マルチスレッド処理を行なっていますが、その制御に関しては考慮する必要はありません。

実際の実装

ユーザーは以下のクラスを実装する必要があります。

Experient クラス

実験のロジックは、以下のように定義されます。

class ContinuousResistanceMesurement(Experiment):
    columns = ["R"]
    filename = "resistance"

    def steps(self):
        multimeter = K34411A()
        while self.running:
            r = multimeter.measure_resistance()
            self.send_row({"R": r})
  • クラスの columns プロパティを用いて、記録用のcsvファイルの列を指定します。

  • steps関数に、実際の実験の処理を定義します。

    • 実験を途中で中断できるようにするため、 self.running をチェックして False だった場合はスクリプトを終了してください。

    • self.send_row() メソッドを用いて、測定結果のデータを記録することができます。

      • メソッド実行1回あたり、csvファイル1行になります。省略された項目の列は空欄となります。

      • ファイルへの保存やタイムスタンプの挿入などは、自動で行なわれます。

  • スクリプト実行時のカレントディレクトリ配下に data ディレクトリが作成され、csvファイルが保存されます。 クラスの filename プロパティで指定した名前に、自動で日時が記録され、ファイル名となります。

Plotter クラス

可視化のロジックは、以下のように定義されます。

注釈

リアルタイムプロットが必要ない場合は実装を省略することができます。 その場合でも、ファイルへの自動保存などの恩恵を得ることができます。

class ResistancePlotter(Plotter):
    _fig: Figure

    def prepare(self):
        self._fig, self._ax = plt.subplots(1, 1)

    def update(self, df):
        df = df.query("R < 1e20")

        self._ax.cla()

        self._ax.plot(df["t"], df["R"])
        self._ax.set_xlabel("Time / s")
        self._ax.set_ylabel("Resistance / Ohm")
        self._ax.grid()
  • prepareコマンドは初回のみ実行されます。

  • updateコマンドは、定期的に実行されます。

実験の実行

定義したクラスを用いて、以下のように実験を実行できます。

if __name__ == "__main__":
    experiment = ContinuousResistanceMesurement()
    experiment.plotter = ResistancePlotter()
    experiment.start()

注釈

リアルタイムプロットが必要ない場合はplotterの指定は必要ありません。