実験の制御と可視化
実験中には、測定値をグラフによって可視化したいことがあります。 これを実現するために、ebilabを用いることができます。
Simple implementation
You can implement code like below with matplotlib. This program measures resistance by digital multimeter and plot it to a chart.
1from datetime import datetime
2import pandas as pd
3import matplotlib.pyplot as plt
4from ebilab.visa 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(R)
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)
This code has some problems.
This code includes both a part which describe a logic of the experiment and a part for visualization.
外れ値をフィルタリングしてプロットしたい場合など、どこまでが実験のロジックでどこからが可視化のためのロジックなのかが分かりづらくなる。
Data acquision rate is lowered becaues the rendering of matplotlib blocks data acquision.
You have to write similar code many times like saving data or visualizing.
ebilabを利用した実装
ebilabを用いることで、 これらの問題を解決することができます。
The preceding code can be refactored as below,
1# sample of GUI app
2import asyncio
3import random
4
5from ebilab.api import BaseExperiment, BasePlotter, FloatField, SelectField
6from ebilab.gui.controller import launch_gui
7
8
9# class to decide steps of experiment
10class RandomWalkExperiment(BaseExperiment):
11 """
12 Random Walk
13
14 This is example experiment.
15 The interval is 0.2 sec.
16 """
17
18 columns = ["v", "v2"] # please specify columns to write csv file
19 name = "random-walk" # filename is suffixed by datetime
20
21 initial = FloatField(default=2.0)
22 step = SelectField(choices=[1, 2, 4], default_index=1)
23
24 async def setup(self):
25 self.v = self.initial
26 self.logger.info(f"Random walk experiment started with initial value {self.v}.")
27
28 async def steps(self):
29 self.logger.info("Starting random walk experiment.")
30 while True:
31 # you can use self.logger to log messages
32 self.logger.debug(f"log: {self.v}")
33
34 # random walk logic
35 step_value = self.step if random.random() < 0.5 else -self.step
36 self.v += step_value
37
38 # The value you yield will be sent to GUI and saved to CSV file
39 yield {"v": self.v, "v2": self.v * 2}
40
41 # use asyncio.sleep instead of time.sleep
42 await asyncio.sleep(0.2)
43
44 async def cleanup(self):
45 self.logger.info("Random walk experiment finished.")
46
47
48# class to decide how to plot during experiment
49@RandomWalkExperiment.register_plotter
50class TransientPlotter(BasePlotter):
51 name = "transient"
52 window_length = 10
53
54 def setup(self):
55 # this method is executed before starting experiment
56 # e.g. adding Axes to Figure
57 # figure is stored in `self.fig`
58 if self.fig:
59 self._ax = self.fig.add_subplot(111)
60
61 def update(self, df):
62 # this method is executed many times during experiment
63 # df is pandas.DataFrame which has experiment data
64 if hasattr(self, "_ax") and not df.empty:
65 self._ax.clear()
66
67 self._ax.plot(df["t"], df["v"])
68 self._ax.set_xlabel("Time")
69 self._ax.set_ylabel("Voltage")
70 self._ax.grid(True)
71 self._ax.text(0.05, 0.95, f"step={self.experiment.step}", transform=self._ax.transAxes, verticalalignment='top')
72
73
74# class to decide how to plot during experiment
75@RandomWalkExperiment.register_plotter
76class HistgramPlotter(BasePlotter):
77 name = "histgram"
78 bins = FloatField(default=10.0)
79
80 def setup(self):
81 # this method is executed before starting experiment
82 # e.g. adding Axes to Figure
83 # figure is stored in `self.fig`
84 if self.fig:
85 self._ax = self.fig.add_subplot(111)
86
87 def update(self, df):
88 # this method is executed many times during experiment
89 # df is pandas.DataFrame which has experiment data
90 if hasattr(self, "_ax") and not df.empty and "v" in df.columns:
91 self._ax.clear()
92
93 self._ax.hist(df["v"], bins=int(self.bins))
94 self._ax.set_xlabel("Value")
95 self._ax.set_ylabel("Count")
96
97 # 実験インスタンスにアクセスしてパラメータを表示
98 if self.experiment:
99 title = f"Histogram (step={self.experiment.step}, initial={self.experiment.initial})"
100 self._ax.set_title(title)
101 # ログに出力してテスト
102 self.experiment.logger.info(f"Plotter accessed experiment parameters: step={self.experiment.step}")
103
104 self._ax.grid(True)
105
106
107if __name__ == "__main__":
108 # This is a sample code to run the experiment
109 # You can run this file directly to see the experiment in action
110 launch_gui([RandomWalkExperiment])
BaseExperiment クラス、 BasePlotter クラスを継承し、
実験のロジックと可視化のロジックをそれぞれ実装することで、実験を設計することができます。
これにより、ロジックを適切に分割し、読みやすいコードを実現することができます。
また、クラス単位で定義することにより、同じ実験で可視化の方法だけを変更したり、別の実験でも同一の可視化方法を用いるなど、
それぞれのコンポーネントを再利用しやすくなります。
ファイルへの保存やプロット用プログラムへのデータの受け渡しに関して考慮する必要はありません。 また、matplotlibの描画中にもデータの取得を継続するため、マルチスレッド処理を行なっていますが、その制御に関しては考慮する必要はありません。
実際の実装
ユーザーは以下のクラスを実装する必要があります。
Experiment クラス
実験のロジックは、BaseExperiment を継承して定義します。
class RandomWalkExperiment(BaseExperiment):
"""
Random Walk
This is example experiment.
The interval is 0.2 sec.
"""
columns = ["v", "v2"] # please specify columns to write csv file
name = "random-walk" # filename is suffixed by datetime
initial = FloatField(default=2.0)
step = SelectField(choices=[1, 2, 4], default_index=1)
async def setup(self):
self.v = self.initial
self.logger.info(f"Random walk experiment started with initial value {self.v}.")
async def steps(self):
self.logger.info("Starting random walk experiment.")
while True:
# you can use self.logger to log messages
self.logger.debug(f"log: {self.v}")
# random walk logic
step_value = self.step if random.random() < 0.5 else -self.step
self.v += step_value
# The value you yield will be sent to GUI and saved to CSV file
yield {"v": self.v, "v2": self.v * 2}
# use asyncio.sleep instead of time.sleep
await asyncio.sleep(0.2)
async def cleanup(self):
self.logger.info("Random walk experiment finished.")
基本的な構造
name: 実験の名前。csvファイル名のベースになります。columns: 記録用のcsvファイルの列名を指定します。パラメータ定義:
FloatField,SelectField,IntField,StrField,BoolFieldなどのフィールドクラスを用いて、GUIから設定できるパラメータを定義します。
ライフサイクルメソッド
実験は以下の3つの async メソッドで構成されます。
async def setup(): 実験の準備。デバイスへの接続などを行います。async def steps(): データをyieldするメインループ。async def cleanup(): 正常終了・中断・エラー時に必ず呼ばれる後処理。
steps() メソッドでは、 yield を使用して測定データを記録します。
yield で返された辞書は、CSVファイルに保存され、GUIで可視化されます。
Note
time.sleep() の代わりに await asyncio.sleep() を使用してください。
これにより、スリープ中でも実験を中断できます。
ロギング
実験中のログは self.logger を通じて出力できます。
出力したログは、GUIのログビューアに表示される他、ログファイルにも保存されます(デバッグモードでは保存されない)。
self.logger.info("情報メッセージ")
self.logger.debug("デバッグメッセージ")
self.logger.warning("警告メッセージ")
self.logger.error("エラーメッセージ")
Plotter クラス
可視化のロジックは、BasePlotter を継承して定義します。
@Experiment.register_plotter デコレータで実験クラスに関連付けます。
# class to decide how to plot during experiment
@RandomWalkExperiment.register_plotter
class TransientPlotter(BasePlotter):
name = "transient"
window_length = 10
def setup(self):
# this method is executed before starting experiment
# e.g. adding Axes to Figure
# figure is stored in `self.fig`
if self.fig:
self._ax = self.fig.add_subplot(111)
def update(self, df):
# this method is executed many times during experiment
# df is pandas.DataFrame which has experiment data
if hasattr(self, "_ax") and not df.empty:
self._ax.clear()
self._ax.plot(df["t"], df["v"])
self._ax.set_xlabel("Time")
self._ax.set_ylabel("Voltage")
self._ax.grid(True)
name: プロッターの識別名。GUIのドロップダウンに表示されます。setup(): プロットの初期設定。プロッターがアクティブになった際に一度だけ呼ばれます。update(df): データが更新されるたびに呼ばれます。dfはpandas.DataFrameで、全ての実験データが含まれます。
Plotterのパラメータ
Plotter にもフィールドを定義できます。これらはGUIから実験中でも変更できます。
@RandomWalkExperiment.register_plotter
class HistgramPlotter(BasePlotter):
name = "histgram"
bins = FloatField(default=10.0) # GUIから変更可能なパラメータ
def update(self, df):
self._ax.hist(df["v"], bins=int(self.bins))
Experimentへのアクセス
self.experiment を通じて、実験インスタンスのパラメータにアクセスできます。
def update(self, df):
# 実験パラメータを参照
title = f"step={self.experiment.step}"
self._ax.set_title(title)
また、 self.experiment.is_running で実験が実行中かどうかを確認できます。
これを使って、実行中と履歴表示でプロットの挙動を変えることができます。
実験の実行
定義したクラスを用いて、以下のように実験を実行できます。
if __name__ == "__main__":
# This is a sample code to run the experiment
# You can run this file directly to see the experiment in action
launch_gui([RandomWalkExperiment])
Note
複数の実験クラスを指定することができます。GUIのドロップダウンから選択できます。
フィールドの種類
パラメータ定義に使用できるフィールドクラスは以下の通りです。
フィールド |
用途 |
パラメータ |
|---|---|---|
|
浮動小数点数 |
|
|
整数 |
|
|
文字列 |
|
|
真偽値 |
|
|
選択肢 |
|
使用例:
class MyExperiment(BaseExperiment):
# 浮動小数点数パラメータ(範囲指定あり)
voltage = FloatField(default=1.0, min=0.0, max=10.0)
# 整数パラメータ
count = IntField(default=100, min=1)
# 文字列パラメータ
sample_name = StrField(default="sample1")
# 真偽値パラメータ
auto_save = BoolField(default=True)
# 選択肢パラメータ
mode = SelectField(choices=["fast", "normal", "slow"], default_index=1)
データの保存
スクリプト実行時のカレントディレクトリ配下に
dataディレクトリが作成され、csvファイルが保存されます。ファイル名は
nameプロパティで指定した名前に日時が付加されます。自動的に
t列(実験開始からの経過秒数)とsync_t列(syncボタンからの経過秒数)が追加されます。
ショートカットキー
GUIでは以下のショートカットキーが使用できます。
F5: 実験開始F6: デバッグ実行(データを保存しない)F9: 実験終了F12: sync(タイムスタンプを記録)