1. 基本方針

1.1 plugin と manager の責務

tkfilter では、責務を次のように分けます。

要素

役割

filter plugin

自分が読めるファイルか判定し、ファイルを読み込んで inf 辞書を返す

tkFilterManager

filter を読み込み、優先順に check_file_type() を試し、最初に一致した filter を使う

アプリケーション

inf 辞書を受け取り、解析・可視化・保存を行う

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 側では、必要なければ appcparams は無視してかまいません。

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() は内部で次の処理を行います。

  1. filter plugin をロードする

  2. check_file_type() を順番に呼ぶ

  3. 最初に一致した filter の read_data() を呼ぶ

  4. convert=True かつ filter に convert() があれば呼ぶ

  5. 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_ver

plugin の目的とデータ形式バージョン

default_ext

標準拡張子

input_type

入力ファイル形式の識別子

output_type

出力データ形式の識別子

任意項目です。

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() はできるだけ軽くし、通常のフォーマット不一致では例外を出さない方針にします。

推奨方針です。

状況

推奨動作

拡張子が違う

None を返す

ヘッダを見たが自分の形式ではない

None を返す

ファイルが壊れていて自分の形式と断定できない

原則 None を返す

plugin の実装ミス

例外を出してよい

read_data() 呼び出し後の読み込み失敗

例外を出す

旧 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"]

nxnX の表記は過去に揺れがあります。新規 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

11. エラー処理方針

11.1 基本方針

新しい tkfilter では、次の方針を推奨します。

関数

通常の不一致

読み込み失敗

推奨

check_file_type()

None を返す

原則 None、または例外

例外は少なめ

read_data()

呼ばれない

例外を出す

raise 推奨

read_files()

対応filterなしならスキップ可能

raise_error に従う

バッチ処理では継続可能

11.2 raise_error

tkfilterread_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読み込み

主に tkApplication.load_modules()

tkfilter.tkreader.load_filters() または tkFilterManager

アプリ側の読み込み

filterを探して read_data() を直接呼ぶ

tkfilter.read_data() が探索から読み込みまで行う

優先順位

glob順、またはアプリ側実装に依存

plugin_order.txt で外部指定

print_data() / plot_data()

標準APIとして記載

単独テスト用の推奨補助関数

convert()

標準処理の一部

任意の後処理フック。convert=True なら自動呼び出し

エラー処理

None, "Error: ...", app.terminate() が混在

新規 plugin では read_data() 失敗時は raise 推奨

tkApplication

plugin管理も担当

現仕様のまま残す。新規コードは tkfilter へ移行


16. 推奨する開発手順

新しい filter を作るときの流れです。

  1. 既存の近い filter をコピーする

  2. plugin_ver, default_ext, input_type, output_type を修正する

  3. check_file_type() を軽く実装する

  4. read_data()inf 辞書を返す

  5. 必要なら convert() を実装する

  6. print_data()plot_data() で単独テストできるようにする

  7. plugin_order.txt に module 名を追加する

  8. 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 のキーを見て処理します。

よく使うキー名はできるだけ統一します。

  • filename

  • sample_name

  • data_list

  • labels

  • meta

  • nregion

  • nspectrum

  • ndata

  • xmin

  • xmax

  • xstep

  • yscale

特殊なデータは追加キーとして持たせます。


18. まとめ

tkfilter の基本思想は次の通りです。

plugin は読む
manager は探す
アプリは inf を使う

filter plugin の仕様は大きく変えず、外側に tkfilter を置くことで、既存 plugin を活かしながら、アプリケーション側のコードを簡潔にできます。

新規 plugin では、特に次の点を推奨します。

  • check_file_type() は不一致なら None

  • read_data() は成功なら inf、失敗なら raise

  • data_list_type = "[[x, y]]" を標準にする

  • print_data() / plot_data() は単独テスト用に用意する

  • filter の優先順位は plugin_order.txt で管理する