1. 基本方針
1.1 plugin と manager の責務
tkfilter では、責務を次のように分けます。
要素 |
役割 |
|---|---|
filter plugin |
自分が読めるファイルか判定し、ファイルを読み込んで |
|
filter を読み込み、優先順に |
アプリケーション |
|
filter plugin は、基本的には Python module です。クラス化は必須ではありません。
filter/
plugin_order.txt
miniflex_ras2xrd.py
txt2xrd.py
generic_csv.py
1.2 tkApplication への依存
tkfilter 本体は tkApplication に依存しません。
ただし、既存 filter との互換性のため、各関数には従来通り
app=None
cparams=None
を渡せるようにします。
filter 側では、必要なければ app と cparams は無視してかまいません。
1.3 最小API
新しい filter plugin で必須に近い関数は次の2つです。
def check_file_type(infile, inf=None, app=None, cparams=None):
...
def read_data(infile, app=None, cparams=None, is_print=False):
...
その他の関数は任意です。
def convert(inf, app=None, cparams=None):
return inf
def get_input_type(inf=None, app=None, cparams=None):
...
def get_output_type(inf=None, app=None, cparams=None):
...
def print_data(inf, app=None, cparams=None):
...
def plot_data(inf, app=None, cparams=None):
...
def save_data(outfiles, inf, app=None, cparams=None, is_print=False):
...
print_data() や plot_data() は、plugin を単独でテストできるようにするための標準実装として有用ですが、tkfilter から読み込むための必須APIではありません。
convert() も必須ではありません。用途に応じた変換例や後処理フックとして使います。
2. パッケージ構成
標準的な構成例です。
project/
tkfilter/
__init__.py
tkfilter.py
tkfiltermanager.py
tkreader.py
tkfilterutils.py
filter/
plugin_order.txt
miniflex_ras2xrd.py
txt2xrd.py
generic_csv.py
test_tkfilter.py
主な import は次の通りです。
from tkfilter.tkreader import read_data, read_files, load_filters
from tkfilter.tkfiltermanager import tkFilterManager
from tkfilter.tkfilter import tkFilter
3. 基本的な使い方
3.1 1ファイルを読む
from tkfilter.tkreader import read_data
inf = read_data(
"sample.ras",
plugin_dir="filter",
manifest="filter/plugin_order.txt",
)
read_data() は内部で次の処理を行います。
filter plugin をロードする
check_file_type()を順番に呼ぶ最初に一致した filter の
read_data()を呼ぶconvert=Trueかつ filter にconvert()があれば呼ぶinf辞書を返す
3.2 複数ファイルを読む
from tkfilter.tkreader import read_files
inf_list = read_files(
"data/*.*",
plugin_dir="filter",
manifest="filter/plugin_order.txt",
)
read_files() は、標準では一部の一時ファイルや出力ファイルをスキップします。
~で始まるファイル-out.を含むファイル
必要なら次のように変更できます。
inf_list = read_files(
"data/*.*",
plugin_dir="filter",
skip_temporary=False,
skip_output=False,
)
3.3 manager を使って繰り返し読む
大量のファイルを読む場合は、毎回 filter をロードしないように tkFilterManager を使います。
from tkfilter.tkreader import load_filters
manager = load_filters(
plugin_dir="filter",
manifest="filter/plugin_order.txt",
)
inf1 = manager.read_data("sample1.ras")
inf2 = manager.read_data("sample2.ras")
4. filter の読み込み順序
4.1 manifest / order file
filter の優先順位は、plugin 内部ではなく、外部の order file に書きます。
例: filter/plugin_order.txt
# Rigaku / vendor-specific XRD readers
miniflex_ras2xrd.py
smartlab_txt.py
xrdml_reader.py
# CIF / calculated pattern readers
cif2xrd.py
xrd_excel.py
# Generic fallbacks should be near the end
generic_xy_txt.py
generic_csv.py
generic_excel.py
ルールは次の通りです。
1行に1つの module 名または
.pyファイル名を書く空行は無視する
#以降はコメントとして無視する.pyあり・なしの両方を許す重複した module は最初だけ採用する
order file にない plugin は、標準では読まない
4.2 未掲載 plugin も読み込む
order file に書かれていない plugin を最後に追加したい場合は、include_unlisted=True を指定します。
manager = load_filters(
plugin_dir="filter",
manifest="filter/plugin_order.txt",
include_unlisted=True,
)
4.3 manifest 未指定時
manifest または order_file を指定しない場合は、
plugin_dir/*.py
をファイル名順で読み込みます。
manager = load_filters(plugin_dir="filter")
5. filter plugin の標準仕様
5.1 global 変数
filter module には、次のような global 変数を置くことを推奨します。
plugin_ver = "data:0.2"
default_ext = ".ras"
input_type = "MiniFlex .ras"
output_type = "pXRD"
推奨項目です。
変数 |
意味 |
|---|---|
|
plugin の目的とデータ形式バージョン |
|
標準拡張子 |
|
入力ファイル形式の識別子 |
|
出力データ形式の識別子 |
任意項目です。
written_by = "Toshio Kamiya"
copyright = ""
extensions = [".ras"]
extensions は、複数拡張子を受け付ける場合に使えます。
6. check_file_type()
6.1 役割
check_file_type() は、入力ファイルがその filter で読めるかどうかを判定します。
def check_file_type(infile, inf=None, app=None, cparams=None):
...
6.2 戻り値
推奨する戻り値は次の通りです。
# 対応している
return {"file_type": input_type}
# 対応していない
return None
check_file_type() は、複数 filter を順番に試すときに呼ばれます。そのため、通常の不一致では例外を出さず、None を返すことを推奨します。
6.3 例外処理
新しい filter では、check_file_type() はできるだけ軽くし、通常のフォーマット不一致では例外を出さない方針にします。
推奨方針です。
状況 |
推奨動作 |
|---|---|
拡張子が違う |
|
ヘッダを見たが自分の形式ではない |
|
ファイルが壊れていて自分の形式と断定できない |
原則 |
plugin の実装ミス |
例外を出してよい |
|
例外を出す |
旧 filter では "Error: ..." 文字列を返す実装があります。tkfilter は互換のため、文字列に "Error" または "error" を含む戻り値をエラー扱いとして解釈します。ただし、新規 plugin ではこの方式は推奨しません。
7. read_data()
7.1 役割
read_data() は、ファイルを読み込み、標準化された inf 辞書を返します。
def read_data(infile, app=None, cparams=None, is_print=False):
...
7.2 戻り値
成功した場合は inf 辞書を返します。
return inf
read_data() は、原則として check_file_type() が一致した後に呼ばれます。そのため、読み込み失敗は例外として扱う方が自然です。
raise FileNotFoundError(infile)
raise ValueError("Invalid data format")
None を返す方式は、旧仕様との互換や特別な用途では使えますが、新規 plugin では原則として避けます。
7.3 is_print
is_print=True の場合、読み込み中の進行状況やファイル名をコンソールに表示してよいです。
if is_print:
print(f"Read [{infile}]")
古い plugin では print_level を使っているものがあります。新規 plugin では is_print を標準にします。必要なら print_level を追加で受け取ってもかまいません。
8. inf 辞書の標準構造
8.1 標準スペクトルデータ
標準形式は plugin_ver = "data:0.2"、data_list_type = "[[x, y]]" です。
単一 region、単一 spectrum の例です。
inf = {
"filename": infile,
"sample_name": sample_name,
"data_list_type": "[[x, y]]",
"meta": meta,
"labels": [[r"2$\theta$ ($\degree$)", "Intensity"]],
"data_list": [[x, y]],
"nregion": 1,
"nspectrum": 1,
"ndata": len(x),
"xmin": min(x),
"xmax": max(x),
"xstep": x[1] - x[0],
"yscale": "linear",
}
8.2 複数 spectrum
同じ x 軸に対して複数の y データを持つ場合です。
inf["labels"] = [["x", "y1", "y2", "y3"]]
inf["data_list"] = [[x, y1, y2, y3]]
inf["nregion"] = 1
inf["nspectrum"] = 3
8.3 複数 region
x 軸範囲や測定条件が異なる複数 region を持つ場合です。
inf["labels"] = [
["x_a", "y_a1", "y_a2"],
["x_b", "y_b1", "y_b2"],
]
inf["data_list"] = [
[x_a, y_a1, y_a2],
[x_b, y_b1, y_b2],
]
inf["nregion"] = 2
inf["nspectrum"] = 2
8.4 x 変数が複数ある場合
TFT の VG, VD - ID データなど、x 変数が複数ある場合は、nx または nX を使います。
inf["data_list"] = [[VG, VD, ID]]
inf["labels"] = [["VG", "VD", "ID"]]
inf["nregion"] = 1
inf["nx"] = 2
inf["nspectrum"] = len(inf["data_list"][0]) - inf["nx"]
nx と nX の表記は過去に揺れがあります。新規 plugin では Python の一般的な名前として nx を推奨します。既存データとの互換が必要な場合は、両方を入れてもかまいません。
inf["nx"] = 2
inf["nX"] = 2
8.5 任意の追加データ
data_list に入れにくいデータは、inf の追加キーとして保存します。
例: XRD 回折線情報
inf["diffractions"] = {
"source": source,
"Q2": Q2,
"dhkl": dhkl,
"hkl": hkl,
"intensity": intensity,
}
例: 畳み込み済みXRDプロファイル
inf["conv_data"] = [xQ2, xrd_cal]
アプリケーション固有のデータは、キー名を明確にして追加します。
9. convert()
9.1 役割
convert() は、read_data() で読み込んだ inf を、目的に応じた形式へ変換するための任意関数です。
def convert(inf, app=None, cparams=None):
return inf
例です。
CIF構造から回折線リストを計算する
回折線リストから畳み込み済みXRDプロファイルを作る
生データを標準
data_list形式へ整えるpymatgen, ASE などのオブジェクトへ変換する
9.2 tkfilter からの呼び出し
tkfilter では、標準で convert=True になっています。
inf = read_data("sample.cif", plugin_dir="filter", convert=True)
filter に convert() があれば、read_data() の後に自動で呼ばれます。
convert() が None を返した場合、tkfilter は元の inf をそのまま使います。変換後の inf を返す実装を推奨します。
def convert(inf, app=None, cparams=None):
inf["converted"] = True
return inf
10. print_data(), plot_data(), save_data()
これらは、plugin を単独でテストするための標準的な補助関数です。
10.1 print_data()
def print_data(inf, app=None, cparams=None):
...
読み込んだ inf の概要や先頭データを表示します。
10.2 plot_data()
def plot_data(inf, app=None, cparams=None):
...
グラフを描画します。
単独テストでは便利ですが、アプリケーション側では独自の描画関数を使ってもかまいません。
10.3 save_data()
def save_data(outfiles, inf, app=None, cparams=None, is_print=False):
...
outfiles はリストにします。
outfiles = ["sample-out.xlsx"]
save_data(outfiles, inf)
メタデータファイルなど複数ファイルを保存する場合にも対応できます。
11. エラー処理方針
11.1 基本方針
新しい tkfilter では、次の方針を推奨します。
関数 |
通常の不一致 |
読み込み失敗 |
推奨 |
|---|---|---|---|
|
|
原則 |
例外は少なめ |
|
呼ばれない |
例外を出す |
|
|
対応filterなしならスキップ可能 |
|
バッチ処理では継続可能 |
11.2 raise_error
tkfilter の read_data() は標準で raise_error=True です。
inf = read_data("sample.ras", raise_error=True)
対応する filter がない場合は例外を出します。
RuntimeError: No filter matched [sample.ras]
一方、read_files() は標準で raise_error=False です。複数ファイルを読むときは、読めないファイルをスキップして処理を続ける方が便利なためです。
inf_list = read_files("data/*.*", raise_error=False)
11.3 app.terminate() について
既存 filter では、エラー時に app.terminate() を呼ぶ実装があります。
新規 plugin では、ライブラリとしての再利用性を高めるため、app.terminate() よりも例外を推奨します。
if not os.path.isfile(infile):
raise FileNotFoundError(infile)
ただし、plugin を単独アプリとして動かす main() 内では、例外を捕まえて app.terminate() してもかまいません。
12. 最小 filter plugin の例
以下は、2列の x y テキストファイルを読む最小例です。
# xy2data.py
import os
plugin_ver = "data:0.2"
default_ext = ".xy"
input_type = "Two-column XY text"
output_type = "spectrum"
def check_file_type(infile, inf=None, app=None, cparams=None):
if not os.path.isfile(infile):
return None
root, ext = os.path.splitext(infile)
if ext.lower() != default_ext:
return None
try:
with open(infile, "r", encoding="utf-8") as fp:
for line in fp:
line = line.strip()
if not line or line.startswith("#"):
continue
cols = line.replace(",", " ").split()
if len(cols) < 2:
return None
float(cols[0])
float(cols[1])
break
except Exception:
return None
return {"file_type": input_type}
def get_input_type(inf=None, app=None, cparams=None):
return {"file_type": input_type}
def get_output_type(inf=None, app=None, cparams=None):
return {"file_type": output_type}
def read_data(infile, app=None, cparams=None, is_print=False):
if is_print:
print(f"Read [{infile}]")
if not os.path.isfile(infile):
raise FileNotFoundError(infile)
x = []
y = []
with open(infile, "r", encoding="utf-8") as fp:
for line in fp:
line = line.strip()
if not line or line.startswith("#"):
continue
cols = line.replace(",", " ").split()
if len(cols) < 2:
continue
x.append(float(cols[0]))
y.append(float(cols[1]))
if len(x) == 0:
raise ValueError(f"No numeric data was found in [{infile}]")
sample_name = os.path.basename(infile)
inf = {
"filename": infile,
"sample_name": sample_name,
"data_list_type": "[[x, y]]",
"meta": {},
"labels": [["x", "y"]],
"data_list": [[x, y]],
"nregion": 1,
"nspectrum": 1,
"ndata": len(x),
"xmin": min(x),
"xmax": max(x),
"yscale": "linear",
}
if len(x) >= 2:
inf["xstep"] = x[1] - x[0]
return inf
def convert(inf, app=None, cparams=None):
return inf
def print_data(inf, app=None, cparams=None):
print("")
print(f"filename : {inf.get('filename')}")
print(f"sample : {inf.get('sample_name')}")
print(f"ndata : {inf.get('ndata')}")
print(f"labels : {inf.get('labels')}")
def plot_data(inf, app=None, cparams=None):
from matplotlib import pyplot as plt
x, y = inf["data_list"][0]
labels = inf["labels"][0]
plt.plot(x, y)
plt.xlabel(labels[0])
plt.ylabel(labels[1])
plt.tight_layout()
plt.show()
def main():
import sys
if len(sys.argv) < 2:
print(f"Usage: python {sys.argv[0]} input.xy")
return
infile = sys.argv[1]
file_type = check_file_type(infile)
if file_type is None:
print(f"Error: invalid file type [{infile}]")
return
inf = read_data(infile, is_print=True)
inf = convert(inf)
print_data(inf)
plot_data(inf)
if __name__ == "__main__":
main()
13. test_tkfilter.py
test_tkfilter.py は、filter plugin をまとめて読み込み、引数で指定したファイルを表示・プロットするための確認用プログラムです。
python test_tkfilter.py sample.ras --plugin-dir=filter
order file を使う場合です。
python test_tkfilter.py sample.ras --plugin-dir=filter --manifest=filter/plugin_order.txt
グラフを表示しない場合です。
python test_tkfilter.py sample.ras --plugin-dir=filter --show=0
画像に保存する場合です。
python test_tkfilter.py sample.ras --plugin-dir=filter --show=0 --savefig=sample.png
14. Sphinx / MyST での利用
このファイルを、たとえば
docs/tkfilter.md
として保存します。
Sphinx の index.md または index.rst の toctree に追加します。
MyST Markdown の例です。
```{toctree}
:maxdepth: 2
tkfilter
```
conf.py で MyST を使う場合は、次のようにします。
extensions = [
"myst_parser",
]
15. 旧仕様からの主な変更点
項目 |
旧仕様 |
新仕様 |
|---|---|---|
filter読み込み |
主に |
|
アプリ側の読み込み |
filterを探して |
|
優先順位 |
glob順、またはアプリ側実装に依存 |
|
|
標準APIとして記載 |
単独テスト用の推奨補助関数 |
|
標準処理の一部 |
任意の後処理フック。 |
エラー処理 |
|
新規 plugin では |
|
plugin管理も担当 |
現仕様のまま残す。新規コードは |
16. 推奨する開発手順
新しい filter を作るときの流れです。
既存の近い filter をコピーする
plugin_ver,default_ext,input_type,output_typeを修正するcheck_file_type()を軽く実装するread_data()でinf辞書を返す必要なら
convert()を実装するprint_data()とplot_data()で単独テストできるようにするplugin_order.txtに module 名を追加するtest_tkfilter.pyで確認する
確認コマンド例です。
python test_tkfilter.py sample.dat --plugin-dir=filter --manifest=filter/plugin_order.txt
17. 注意点
17.1 汎用 filter は最後に置く
generic_txt.py, generic_csv.py, generic_excel.py のような filter は、多くのファイルを読めてしまいます。
そのため、order file では必ず後ろに置きます。
specific_format.py
vendor_format.py
lab_format.py
generic_txt.py
generic_csv.py
generic_excel.py
17.2 check_file_type() を厳しめにする
拡張子だけで判定すると、汎用 filter が先に一致してしまう可能性があります。
可能なら、ヘッダやデータ列数も軽く確認します。
if ext.lower() != ".ras":
return None
# header check
...
return {"file_type": input_type}
17.3 inf のキー名を安定させる
上位アプリは inf のキーを見て処理します。
よく使うキー名はできるだけ統一します。
filenamesample_namedata_listlabelsmetanregionnspectrumndataxminxmaxxstepyscale
特殊なデータは追加キーとして持たせます。
18. まとめ
tkfilter の基本思想は次の通りです。
plugin は読む
manager は探す
アプリは inf を使う
filter plugin の仕様は大きく変えず、外側に tkfilter を置くことで、既存 plugin を活かしながら、アプリケーション側のコードを簡潔にできます。
新規 plugin では、特に次の点を推奨します。
check_file_type()は不一致ならNoneread_data()は成功ならinf、失敗ならraisedata_list_type = "[[x, y]]"を標準にするprint_data()/plot_data()は単独テスト用に用意するfilter の優先順位は
plugin_order.txtで管理する