analyze_TFT_refactored.py ダウンロード/コピー

analyze_TFT_refactored.py をダウンロード

analyze_TFT_refactored.py
analyze_TFT_refactored.py
  1#!/usr/bin/env python3
  2"""Id-Vg ExcelファイルからTFTの伝達特性を分析します。
  3
  4概要:
  5    このスクリプトは、線形領域と飽和領域の2つのId-Vg Excelファイルを読み込み、
  6    Vth、移動度、SS、Ionなどの代表的なTFTパラメータを計算します。
  7詳細説明:
  8    デフォルトの入出力ファイル名は元のスクリプトとの互換性を保っています。
  9    入力ファイルは IdVg-Vd0.1.xlsx (線形領域) と IdVg-Vd10.xlsx (飽和領域) です。
 10    デフォルトの出力ファイルは以下の通りです。
 11
 12    IdVg-Vd10_analyze.csv
 13    IdVg-Vd0.1_analyze.csv
 14    output.csv
 15    output_log.csv
 16    rootIdVg.png
 17    IdVg_LIN.png
 18"""
 19
 20from __future__ import annotations
 21
 22import argparse
 23import logging
 24import os
 25import sys
 26from dataclasses import dataclass
 27from typing import Optional
 28
 29import numpy as np
 30import pandas as pd
 31from matplotlib import pyplot as plt
 32from sklearn.linear_model import LinearRegression
 33
 34
 35# -----------------------------------------------------------------------------
 36# Default analysis parameters kept from the original script
 37# -----------------------------------------------------------------------------
 38DEFAULT_COX = 2.24e-8
 39DEFAULT_L = 50.0e-6
 40DEFAULT_W = 300.0e-6
 41DEFAULT_TARGET_CURRENT = 1.0e-10
 42DEFAULT_IOFF_THRESHOLD = 1.0e-14
 43DEFAULT_ROLLING_WINDOW = 7
 44
 45
 46@dataclass(frozen=True)
 47class AnalysisConfig:
 48    """TFTパラメータ抽出に使用される設定値を格納します。
 49
 50    属性:
 51        :param cox: 単位面積あたりのゲート容量。L、W、Id、Vdと単位を合わせる必要があります。元のスクリプトでは 2.24e-8でした。
 52        :type cox: float
 53        :param channel_length: チャネル長 L。
 54        :type channel_length: float
 55        :param channel_width: チャネル幅 W。
 56        :type channel_width: float
 57        :param target_current: SSおよびVonの評価に使用される電流しきい値。
 58        :type target_current: float
 59        :param ioff_threshold: Ioffの平均化に使用される上限電流しきい値。
 60        :type ioff_threshold: float
 61        :param rolling_window: 局所勾配の移動平均のウィンドウサイズ。
 62        :type rolling_window: int
 63    """
 64
 65    cox: float = DEFAULT_COX
 66    channel_length: float = DEFAULT_L
 67    channel_width: float = DEFAULT_W
 68    target_current: float = DEFAULT_TARGET_CURRENT
 69    ioff_threshold: float = DEFAULT_IOFF_THRESHOLD
 70    rolling_window: int = DEFAULT_ROLLING_WINDOW
 71
 72
 73@dataclass
 74class IdVgResult:
 75    """Id-Vg曲線から計算された要約値を格納します。
 76
 77    属性:
 78        :param dataframe: 処理されたデータを含むpandas DataFrame。
 79        :type dataframe: pandas.DataFrame
 80        :param vth: しきい値電圧。
 81        :type vth: float
 82        :param max_slope: 最大勾配。
 83        :type max_slope: float
 84        :param vg_at_max_slope: 最大勾配を示すVg値。
 85        :type vg_at_max_slope: float
 86        :param mobility: 移動度。
 87        :type mobility: float
 88        :param ss: サブスレッショルドスイング。
 89        :type ss: float
 90        :param ion: オン電流。
 91        :type ion: float
 92        :param ioff: オフ電流。推定できない場合はNone。
 93        :type ioff: Optional[float]
 94        :param ion_ioff_ratio: オン電流とオフ電流の比。IoffがNoneの場合はNone。
 95        :type ion_ioff_ratio: Optional[float]
 96        :param von: オン電圧。
 97        :type von: float
 98    """
 99
100    dataframe: pd.DataFrame
101    vth: float
102    max_slope: float
103    vg_at_max_slope: float
104    mobility: float
105    ss: float
106    ion: float
107    ioff: Optional[float]
108    ion_ioff_ratio: Optional[float]
109    von: float
110
111
112@dataclass
113class IdVdResult:
114    """Id-Vd曲線から計算された要約値を格納します。
115
116    属性:
117        :param dataframe: 処理されたデータを含むpandas DataFrame。
118        :type dataframe: pandas.DataFrame
119        :param mueff: 実効移動度。
120        :type mueff: float
121    """
122
123    dataframe: pd.DataFrame
124    mueff: float
125
126
127# -----------------------------------------------------------------------------
128# Input and validation
129# -----------------------------------------------------------------------------
130def read_excel_file(path: str, label: str) -> pd.DataFrame:
131    """概要:
132        Excelファイルを読み込み、失敗時に明確なエラーメッセージを追加します。
133    引数:
134        :param path: Excelファイルへのパス。
135        :type path: str
136        :param label: エラーメッセージで使用される人間が読める名前。
137        :type label: str
138    戻り値:
139        :returns: Excelファイルから読み込まれたDataFrame。
140        :rtype: pandas.DataFrame
141    例外:
142        :raises FileNotFoundError: ファイルが存在しない場合。
143        :raises ValueError: pandasがファイルを読み込めない場合。
144    """
145
146    if not os.path.exists(path):
147        raise FileNotFoundError(f"{label} file not found: {path}")
148
149    try:
150        return pd.read_excel(path)
151    except Exception as exc:  # pandas may raise different exceptions by engine/version
152        raise ValueError(f"Failed to read {label} Excel file: {path}") from exc
153
154
155def validate_required_columns(df: pd.DataFrame, required_columns: list[str], label: str) -> None:
156    """概要:
157        DataFrameが必要な列を含んでいることを検証します。
158    引数:
159        :param df: 検証するDataFrame。
160        :type df: pandas.DataFrame
161        :param required_columns: 分析に必要な列名。
162        :type required_columns: list[str]
163        :param label: エラーメッセージで使用される人間が読める名前。
164        :type label: str
165    例外:
166        :raises ValueError: 1つ以上の必須列が欠落している場合。
167    """
168
169    missing = [col for col in required_columns if col not in df.columns]
170    if missing:
171        raise ValueError(
172            f"{label} data is missing required columns: {missing}. "
173            f"Available columns: {list(df.columns)}"
174        )
175
176
177# -----------------------------------------------------------------------------
178# Numerical helper functions
179# -----------------------------------------------------------------------------
180def calculate_local_slope_by_linear_fit(
181    df: pd.DataFrame,
182    x_series: pd.Series,
183    y_series: pd.Series,
184    half_window: int = 1,
185) -> list[float]:
186    """概要:
187        各データポイント周辺で y = a*x + b をフィッティングして局所勾配を計算します。
188    詳細説明:
189        この関数は、元のスクリプトの slope3data および slope7data スタイルの関数を置き換えます。
190        half_window=1 の場合、約3点を使用して、元の slope3data の動作に可能な限り一致させます。
191    引数:
192        :param df: 元となるDataFrame。その長さのみが使用されます。
193        :type df: pandas.DataFrame
194        :param x_series: X軸の値。
195        :type x_series: pandas.Series
196        :param y_series: Y軸の値。
197        :type y_series: pandas.Series
198        :param half_window: 各側の隣接ポイント数。
199        :type half_window: int
200    戻り値:
201        :returns: 局所勾配のリスト。フィットできないポイントはNaNとして返されます。
202        :rtype: list[float]
203    """
204
205    slopes: list[float] = []
206
207    for index in range(len(df)):
208        x_window = x_series[index - half_window : index + half_window + 1]
209        y_window = y_series[index - half_window : index + half_window + 1]
210
211        valid = pd.concat([x_window, y_window], axis=1).dropna()
212        if len(valid) < 2:
213            slopes.append(np.nan)
214            continue
215
216        try:
217            x = valid.iloc[:, 0].to_numpy().reshape(-1, 1)
218            y = valid.iloc[:, 1].to_numpy()
219            model = LinearRegression().fit(x, y)
220            slopes.append(float(model.coef_[0]))
221        except ValueError as exc:
222            logging.debug("Local slope fitting failed at index %s: %s", index, exc)
223            slopes.append(np.nan)
224
225    return slopes
226
227
228def intercept_x(x: float, y: float, slope: float) -> float:
229    """概要:
230        直線 y_line = slope * (x_line - x0) のX切片を計算します。
231    引数:
232        :param x: 直線上の点のX座標。
233        :type x: float
234        :param y: 直線上の点のY座標。
235        :type y: float
236        :param slope: 直線の勾配。
237        :type slope: float
238    戻り値:
239        :returns: X切片の値。
240        :rtype: float
241    """
242
243    if pd.isna(slope) or slope == 0:
244        return np.nan
245    return float(x - y / slope)
246
247
248def calculate_vth_by_max_slope(
249    vg_series: pd.Series,
250    y_series: pd.Series,
251    slope_series: pd.Series,
252) -> tuple[float, float, float]:
253    """概要:
254        最大勾配接線法を使用してしきい値電圧を計算します。
255    引数:
256        :param vg_series: ゲート電圧の値。
257        :type vg_series: pandas.Series
258        :param y_series: 分析モードに応じてIdまたはsqrt(Id)。
259        :type y_series: pandas.Series
260        :param slope_series: 局所勾配の値。
261        :type slope_series: pandas.Series
262    戻り値:
263        :returns: (Vth, max_slope, Vg_at_max_slope) のタプル。
264        :rtype: tuple
265    例外:
266        :raises ValueError: 有効な勾配が見つからない場合。
267    """
268
269    valid_slope = slope_series.replace([np.inf, -np.inf], np.nan).dropna()
270    if valid_slope.empty:
271        raise ValueError("No valid slope values are available for Vth calculation.")
272
273    idx = valid_slope.idxmax()
274    max_slope = float(valid_slope.loc[idx])
275    x = float(vg_series.loc[idx])
276    y = float(y_series.loc[idx])
277    vth = intercept_x(x, y, max_slope)
278    return vth, max_slope, x
279
280
281def calculate_ion_ioff(id_series: pd.Series, ioff_threshold: float) -> tuple[float, Optional[float], Optional[float]]:
282    """概要:
283        Ion、Ioff、およびIon/Ioff比を計算します。
284    引数:
285        :param id_series: ドレイン電流の値。正の値が期待されます。
286        :type id_series: pandas.Series
287        :param ioff_threshold: 0 < Id < ioff_threshold を満たすデータポイントを平均してIoffを推定します。
288        :type ioff_threshold: float
289    戻り値:
290        :returns: (Ion, Ioff, Ion/Ioff) のタプル。Ioffが推定できない場合、IoffとIon/IoffはNoneとして返されます。
291        :rtype: tuple
292    """
293
294    valid_id = pd.to_numeric(id_series, errors="coerce").replace([np.inf, -np.inf], np.nan)
295    ion = float(valid_id.max()) if not valid_id.dropna().empty else np.nan
296
297    ioff_data = valid_id[(valid_id > 0) & (valid_id < ioff_threshold)].dropna()
298    if ioff_data.empty:
299        return ion, None, None
300
301    ioff = float(ioff_data.mean())
302    ratio = float(ion / ioff) if ioff != 0 else None
303    return ion, ioff, ratio
304
305
306def first_index_at_or_above(series: pd.Series, threshold: float) -> Optional[int]:
307    """概要:
308        シリーズ値がしきい値以上である最初のインデックスを返します。
309    引数:
310        :param series: 数値シリーズ。
311        :type series: pandas.Series
312        :param threshold: しきい値。
313        :type threshold: float
314    戻り値:
315        :returns: 最初の一致するインデックス。条件を満たすポイントがない場合はNone。
316        :rtype: Optional[int]
317    """
318
319    matched = series.index[series >= threshold]
320    if len(matched) == 0:
321        return None
322    return matched.min()
323
324
325def safe_inverse(value: float) -> float:
326    """概要:
327        1/value を返します。逆数が明確に定義されていない場合はNaNを返します。
328    引数:
329        :param value: 逆数を計算する数値。
330        :type value: float
331    戻り値:
332        :returns: valueの逆数、またはNaN。
333        :rtype: float
334    """
335
336    if pd.isna(value) or value == 0:
337        return np.nan
338    return float(1.0 / value)
339
340
341# -----------------------------------------------------------------------------
342# TFT analysis functions
343# -----------------------------------------------------------------------------
344def analyze_idvg_sat(df: pd.DataFrame, config: AnalysisConfig) -> IdVgResult:
345    """概要:
346        飽和領域Id-Vgデータを分析します。
347    詳細説明:
348        計算は元のスクリプトに従います。
349        - 負/非正のIdをログおよび平方根演算のためにNaNに変換します。
350        - log10(Id) および Id ** 0.5 (sqrt(Id)) を計算します。
351        - 3点線形フィッティングにより局所勾配を計算します。
352        - Id ** 0.5 (sqrt(Id)) 勾配を中心移動平均で平滑化します。
353        - 平滑化された Id ** 0.5 (sqrt(Id)) 勾配から飽和移動度を計算します。
354        - 最大勾配接線法によりVthを計算します。
355        - Id >= target_current となる最初のポイントでSSを計算します。
356    引数:
357        :param df: 入力DataFrame。必須列: Vg および Id。
358        :type df: pandas.DataFrame
359        :param config: 分析設定。
360        :type config: AnalysisConfig
361    戻り値:
362        :returns: 処理されたDataFrameと要約値を含むIdVgResult。
363        :rtype: IdVgResult
364    """
365
366    validate_required_columns(df, ["Vg", "Id"], "SAT Id-Vg")
367    df = df.copy()
368
369    df["Id"] = pd.to_numeric(df["Id"], errors="coerce")
370    df["Id_rm"] = np.where(df["Id"] > 0, df["Id"], np.nan)
371    df["logId"] = np.log10(df["Id_rm"])
372    df["rootId"] = df["Id_rm"] ** 0.5
373
374    df["slope logId"] = calculate_local_slope_by_linear_fit(df, df["Vg"], df["logId"], half_window=1)
375    df["slope rootId"] = calculate_local_slope_by_linear_fit(df, df["Vg"], df["rootId"], half_window=1)
376    df["slope logId"] = df["slope logId"].replace([np.inf, -np.inf], np.nan)
377    df["slope rootId"] = df["slope rootId"].replace([np.inf, -np.inf], np.nan)
378    df["slope rootId smooth"] = df["slope rootId"].rolling(config.rolling_window, center=True).mean()
379
380    df["muSAT"] = (
381        2.0
382        * config.channel_length
383        / config.channel_width
384        / config.cox
385        * (df["slope rootId smooth"] ** 2)
386    )
387    df["muSAT"] = df["muSAT"].replace([np.inf, -np.inf], np.nan)
388
389    vth, max_slope, vg_at_max_slope = calculate_vth_by_max_slope(
390        df["Vg"], df["rootId"], df["slope rootId smooth"]
391    )
392    mobility = float(df["muSAT"].max()) if not df["muSAT"].dropna().empty else np.nan
393
394    idx = first_index_at_or_above(df["Id_rm"], config.target_current)
395    if idx is None:
396        logging.warning("SAT: Id never reaches target current %.3e A. SS and Von are set to NaN.", config.target_current)
397        ss = np.nan
398        von = np.nan
399    else:
400        ss = safe_inverse(df.loc[idx, "slope logId"])
401        von = float(df.loc[idx, "Vg"])
402
403    ion, ioff, ratio = calculate_ion_ioff(df["Id_rm"], config.ioff_threshold)
404
405    return IdVgResult(
406        dataframe=df,
407        vth=vth,
408        max_slope=max_slope,
409        vg_at_max_slope=vg_at_max_slope,
410        mobility=mobility,
411        ss=ss,
412        ion=ion,
413        ioff=ioff,
414        ion_ioff_ratio=ratio,
415        von=von,
416    )
417
418
419def analyze_idvg_lin(df: pd.DataFrame, config: AnalysisConfig) -> IdVgResult:
420    """概要:
421        線形領域Id-Vgデータを分析します。
422    詳細説明:
423        計算は元のスクリプトに従います。
424        - 負/非正のIdをNaNに変換します。
425        - Id および log10(Id) の局所勾配を計算します。
426        - Id 勾配を中心移動平均で平滑化します。
427        - 平滑化された Id 勾配から電界効果移動度を計算します。
428        - 最大勾配接線法によりVthを計算します。
429        - Id >= target_current となる最初のポイントでSSを計算します。
430    引数:
431        :param df: 入力DataFrame。必須列: Vg、Vd、および Id。
432        :type df: pandas.DataFrame
433        :param config: 分析設定。
434        :type config: AnalysisConfig
435    戻り値:
436        :returns: 処理されたDataFrameと要約値を含むIdVgResult。
437        :rtype: IdVgResult
438    """
439
440    validate_required_columns(df, ["Vg", "Vd", "Id"], "LIN Id-Vg")
441    df = df.copy()
442
443    df["Id"] = pd.to_numeric(df["Id"], errors="coerce")
444    df["Id_rm"] = np.where(df["Id"] > 0, df["Id"], np.nan)
445    df["logId"] = np.log10(df["Id_rm"])
446
447    df["slope Id"] = calculate_local_slope_by_linear_fit(df, df["Vg"], df["Id_rm"], half_window=1)
448    df["slope logId"] = calculate_local_slope_by_linear_fit(df, df["Vg"], df["logId"], half_window=1)
449    df["slope Id"] = df["slope Id"].replace([np.inf, -np.inf], np.nan)
450    df["slope Id smooth"] = df["slope Id"].rolling(config.rolling_window, center=True).mean()
451    df["slope logId"] = df["slope logId"].replace([np.inf, -np.inf], np.nan)
452
453    df["muFE"] = (
454        config.channel_length
455        / config.channel_width
456        / config.cox
457        / df["Vd"]
458        * df["slope Id smooth"]
459    )
460    df["muFE"] = df["muFE"].replace([np.inf, -np.inf], np.nan)
461
462    vth, max_slope, vg_at_max_slope = calculate_vth_by_max_slope(
463        df["Vg"], df["Id_rm"], df["slope Id smooth"]
464    )
465    mobility = float(df["muFE"].max()) if not df["muFE"].dropna().empty else np.nan
466
467    idx = first_index_at_or_above(df["Id_rm"], config.target_current)
468    if idx is None:
469        logging.warning("LIN: Id never reaches target current %.3e A. SS and Von are set to NaN.", config.target_current)
470        ss = np.nan
471        von = np.nan
472    else:
473        ss = safe_inverse(df.loc[idx, "slope logId"])
474        von = float(df.loc[idx, "Vg"])
475
476    ion, ioff, ratio = calculate_ion_ioff(df["Id_rm"], config.ioff_threshold)
477
478    return IdVgResult(
479        dataframe=df,
480        vth=vth,
481        max_slope=max_slope,
482        vg_at_max_slope=vg_at_max_slope,
483        mobility=mobility,
484        ss=ss,
485        ion=ion,
486        ioff=ioff,
487        ion_ioff_ratio=ratio,
488        von=von,
489    )
490
491
492def analyze_idvd_lin(df: pd.DataFrame, vth: float, config: AnalysisConfig) -> IdVdResult:
493    """概要:
494        線形領域Id-Vdデータを分析します。
495    詳細説明:
496        この関数は、元のスクリプトに analyze_IdVd_LIN が含まれていたため保持されていますが、
497        メインプロセスからは呼び出されません。
498    引数:
499        :param df: 入力DataFrame。必須列: Vd、Vg、および Id。
500        :type df: pandas.DataFrame
501        :param vth: mueff計算に使用されるしきい値電圧。
502        :type vth: float
503        :param config: 分析設定。
504        :type config: AnalysisConfig
505    戻り値:
506        :returns: 処理されたDataFrameと最大mueffを含むIdVdResult。
507        :rtype: IdVdResult
508    """
509
510    validate_required_columns(df, ["Vd", "Vg", "Id"], "LIN Id-Vd")
511    df = df.copy()
512
513    df["Id"] = pd.to_numeric(df["Id"], errors="coerce")
514    df["Id_rm"] = np.where(df["Id"] > 0, df["Id"], np.nan)
515    df = df.sort_values(by="Vd")
516    df["slope Id"] = calculate_local_slope_by_linear_fit(df, df["Vd"], df["Id_rm"], half_window=1)
517    df["mueff"] = (
518        config.channel_length
519        / config.channel_width
520        / config.cox
521        / (df["Vg"] - vth)
522        * df["slope Id"]
523    )
524    df["mueff"] = df["mueff"].replace([np.inf, -np.inf], np.nan)
525    mueff = float(df["mueff"].max()) if not df["mueff"].dropna().empty else np.nan
526
527    return IdVdResult(dataframe=df, mueff=mueff)
528
529
530# -----------------------------------------------------------------------------
531# Plot functions
532# -----------------------------------------------------------------------------
533def plot_idvg_lin(df: pd.DataFrame, vth: float, max_slope: float, output_path: str) -> None:
534    """概要:
535        線形領域Id-Vgプロットを最大勾配接線とともに保存します。
536    引数:
537        :param df: プロットするデータを含むDataFrame。
538        :type df: pandas.DataFrame
539        :param vth: しきい値電圧。
540        :type vth: float
541        :param max_slope: 最大勾配。
542        :type max_slope: float
543        :param output_path: 出力画像の保存パス。
544        :type output_path: str
545    戻り値:
546        :returns: なし
547        :rtype: None
548    """
549
550    plt.figure()
551    plt.scatter(df["Vg"], df["Id"], label="Vd=0.1V", s=4)
552    df_line = max_slope * (df["Vg"] - vth)
553    plt.plot(df["Vg"], df_line, color="red")
554    plt.xlabel("Vg(V)")
555    plt.ylabel("Id(A)")
556    plt.ylim(0, 1.1 * df["Id"].max())
557    plt.legend()
558    plt.savefig(output_path)
559    plt.close()
560
561
562def plot_root_idvg(df: pd.DataFrame, vth: float, max_slope: float, output_path: str) -> None:
563    """概要:
564        飽和領域 Id ** 0.5 (sqrt(Id)) -Vg プロットを接線とともに保存します。
565    引数:
566        :param df: プロットするデータを含むDataFrame。
567        :type df: pandas.DataFrame
568        :param vth: しきい値電圧。
569        :type vth: float
570        :param max_slope: 最大勾配。
571        :type max_slope: float
572        :param output_path: 出力画像の保存パス。
573        :type output_path: str
574    戻り値:
575        :returns: なし
576        :rtype: None
577    """
578
579    plt.figure()
580    plt.scatter(df["Vg"], df["rootId"], label="Vd=10V", s=4)
581    df_line = max_slope * (df["Vg"] - vth)
582    plt.plot(df["Vg"], df_line, color="red")
583    plt.xlabel("Vg(V)")
584    plt.ylabel("Id^0.5(A^0.5)")
585    plt.ylim(0, 1.1 * df["rootId"].max())
586    plt.legend()
587    plt.savefig(output_path)
588    plt.close()
589
590
591# -----------------------------------------------------------------------------
592# Output functions
593# -----------------------------------------------------------------------------
594def output_path(output_dir: str, filename: str) -> str:
595    """概要:
596        出力ディレクトリ内のパスを返します。
597    引数:
598        :param output_dir: 出力ディレクトリのパス。
599        :type output_dir: str
600        :param filename: ファイル名。
601        :type filename: str
602    戻り値:
603        :returns: 出力ディレクトリ内のファイルの完全パス。
604        :rtype: str
605    """
606
607    return os.path.join(output_dir, filename)
608
609
610def write_summary_outputs(sat: IdVgResult, lin: IdVgResult, output_dir: str) -> None:
611    """概要:
612        元のスクリプトと互換性のある要約CSVファイルを書き込みます。
613    詳細説明:
614        この関数は意図的に、元の出力列名を互換性のために保持しています。
615        これには、SSおよびIon値が列名を変更せずにlog10に変換される output_log.csv も含まれます。
616    引数:
617        :param sat: 飽和領域の分析結果。
618        :type sat: IdVgResult
619        :param lin: 線形領域の分析結果。
620        :type lin: IdVgResult
621        :param output_dir: 出力ディレクトリのパス。
622        :type output_dir: str
623    戻り値:
624        :returns: なし
625        :rtype: None
626    """
627
628    df_summary = pd.DataFrame()
629
630    df_summary["Vth_SAT"] = [sat.vth]
631    # df_summary["Von_SAT"] = [sat.von]
632    df_summary["muSAT"] = [sat.mobility]
633    # df_summary["Vgmaxslope_SAT"] = [sat.vg_at_max_slope]
634    df_summary["SS_SAT"] = [sat.ss]
635    df_summary["Ion_SAT"] = [sat.ion]
636
637    df_summary["Vth_LIN"] = [lin.vth]
638    # df_summary["Von_LIN"] = [lin.von]
639    df_summary["muFE"] = [lin.mobility]
640    # df_summary["Vgmaxslope_LIN"] = [lin.vg_at_max_slope]
641    df_summary["SS_LIN"] = [lin.ss]
642    df_summary["Ion_LIN"] = [lin.ion]
643
644    df_summary.to_csv(output_path(output_dir, "output.csv"), index=False)
645
646    df_log = df_summary.copy()
647    for column in ["SS_SAT", "Ion_SAT", "SS_LIN", "Ion_LIN"]:
648        df_log[column] = np.log10(df_log[column])
649    df_log.to_csv(output_path(output_dir, "output_log.csv"), index=False)
650
651
652# -----------------------------------------------------------------------------
653# Command line interface
654# -----------------------------------------------------------------------------
655def parse_args(argv: Optional[list[str]] = None) -> argparse.Namespace:
656    """概要:
657        コマンドライン引数を解析します。
658    引数:
659        :param argv: コマンドライン引数のリスト。デフォルトはNoneで、sys.argvを使用します。
660        :type argv: Optional[list[str]]
661    戻り値:
662        :returns: 解析された引数を格納したargparse.Namespaceオブジェクト。
663        :rtype: argparse.Namespace
664    """
665
666    parser = argparse.ArgumentParser(
667        description="Analyze TFT Id-Vg data and calculate Vth, mobility, SS, and Ion."
668    )
669    parser.add_argument(
670        "--lin",
671        default="IdVg-Vd0.1.xlsx",
672        help="Excel file for linear-region Id-Vg data. Default: %(default)s",
673    )
674    parser.add_argument(
675        "--sat",
676        default="IdVg-Vd10.xlsx",
677        help="Excel file for saturation-region Id-Vg data. Default: %(default)s",
678    )
679    parser.add_argument(
680        "--outdir",
681        default=".",
682        help="Output directory. Default: current directory",
683    )
684    parser.add_argument(
685        "--cox",
686        type=float,
687        default=DEFAULT_COX,
688        help="Gate capacitance per unit area. Default: %(default).3e",
689    )
690    parser.add_argument(
691        "--length",
692        type=float,
693        default=DEFAULT_L,
694        help="Channel length L. Default: %(default).3e",
695    )
696    parser.add_argument(
697        "--width",
698        type=float,
699        default=DEFAULT_W,
700        help="Channel width W. Default: %(default).3e",
701    )
702    parser.add_argument(
703        "--target-current",
704        type=float,
705        default=DEFAULT_TARGET_CURRENT,
706        help="Current threshold used for SS and Von. Default: %(default).3e",
707    )
708    parser.add_argument(
709        "--ioff-threshold",
710        type=float,
711        default=DEFAULT_IOFF_THRESHOLD,
712        help="Upper current threshold used for Ioff averaging. Default: %(default).3e",
713    )
714    parser.add_argument(
715        "--rolling-window",
716        type=int,
717        default=DEFAULT_ROLLING_WINDOW,
718        help="Centered rolling-average window for slope smoothing. Default: %(default)s",
719    )
720    parser.add_argument(
721        "--verbose",
722        action="store_true",
723        help="Show detailed traceback on error.",
724    )
725    return parser.parse_args(argv)
726
727
728def validate_config(config: AnalysisConfig) -> None:
729    """概要:
730        コマンドライン設定値を検証します。
731    引数:
732        :param config: 検証する分析設定オブジェクト。
733        :type config: AnalysisConfig
734    戻り値:
735        :returns: なし
736        :rtype: None
737    例外:
738        :raises ValueError: 設定値が無効な場合。
739    """
740
741    if config.cox == 0:
742        raise ValueError("cox must not be zero.")
743    if config.channel_width == 0:
744        raise ValueError("channel width must not be zero.")
745    if config.rolling_window < 1:
746        raise ValueError("rolling-window must be at least 1.")
747    if config.target_current <= 0:
748        raise ValueError("target-current must be positive.")
749    if config.ioff_threshold <= 0:
750        raise ValueError("ioff-threshold must be positive.")
751
752
753def run(args: argparse.Namespace) -> None:
754    """概要:
755        完全な分析ワークフローを実行します。
756    引数:
757        :param args: コマンドライン引数を格納したNamespace。
758        :type args: argparse.Namespace
759    戻り値:
760        :returns: なし
761        :rtype: None
762    """
763
764    config = AnalysisConfig(
765        cox=args.cox,
766        channel_length=args.length,
767        channel_width=args.width,
768        target_current=args.target_current,
769        ioff_threshold=args.ioff_threshold,
770        rolling_window=args.rolling_window,
771    )
772    validate_config(config)
773
774    os.makedirs(args.outdir, exist_ok=True)
775
776    df_sat_input = read_excel_file(args.sat, "SAT Id-Vg")
777    df_lin_input = read_excel_file(args.lin, "LIN Id-Vg")
778
779    sat = analyze_idvg_sat(df_sat_input, config)
780    sat.dataframe.to_csv(output_path(args.outdir, "IdVg-Vd10_analyze.csv"), index=False)
781    plot_root_idvg(
782        sat.dataframe,
783        sat.vth,
784        sat.max_slope,
785        output_path(args.outdir, "rootIdVg.png"),
786    )
787
788    lin = analyze_idvg_lin(df_lin_input, config)
789    lin.dataframe.to_csv(output_path(args.outdir, "IdVg-Vd0.1_analyze.csv"), index=False)
790    plot_idvg_lin(
791        lin.dataframe,
792        lin.vth,
793        lin.max_slope,
794        output_path(args.outdir, "IdVg_LIN.png"),
795    )
796
797    write_summary_outputs(sat, lin, args.outdir)
798
799    print("Analysis finished.")
800    print(f"Output directory: {args.outdir}")
801    print(f"Vth_SAT = {sat.vth}")
802    print(f"muSAT   = {sat.mobility}")
803    print(f"SS_SAT  = {sat.ss}")
804    print(f"Ion_SAT = {sat.ion}")
805    print(f"Vth_LIN = {lin.vth}")
806    print(f"muFE    = {lin.mobility}")
807    print(f"SS_LIN  = {lin.ss}")
808    print(f"Ion_LIN = {lin.ion}")
809
810
811def main(argv: Optional[list[str]] = None) -> int:
812    """概要:
813        コマンドライン実行のエントリポイントです。
814    引数:
815        :param argv: コマンドライン引数のリスト。デフォルトはNoneで、sys.argvを使用します。
816        :type argv: Optional[list[str]]
817    戻り値:
818        :returns: 終了コード。成功時は0、エラー時は1。
819        :rtype: int
820    """
821
822    args = parse_args(argv)
823    logging.basicConfig(
824        level=logging.DEBUG if args.verbose else logging.INFO,
825        format="%(levelname)s: %(message)s",
826    )
827
828    try:
829        run(args)
830    except Exception as exc:
831        print(f"Error: {exc}", file=sys.stderr)
832        if args.verbose:
833            raise
834        return 1
835
836    return 0
837
838
839if __name__ == "__main__":
840    raise SystemExit(main())