#!/usr/bin/env python3
"""Id-Vg ExcelファイルからTFTの伝達特性を分析します。

概要:
    このスクリプトは、線形領域と飽和領域の2つのId-Vg Excelファイルを読み込み、
    Vth、移動度、SS、Ionなどの代表的なTFTパラメータを計算します。
詳細説明:
    デフォルトの入出力ファイル名は元のスクリプトとの互換性を保っています。
    入力ファイルは IdVg-Vd0.1.xlsx (線形領域) と IdVg-Vd10.xlsx (飽和領域) です。
    デフォルトの出力ファイルは以下の通りです。

    IdVg-Vd10_analyze.csv
    IdVg-Vd0.1_analyze.csv
    output.csv
    output_log.csv
    rootIdVg.png
    IdVg_LIN.png
"""

from __future__ import annotations

import argparse
import logging
import os
import sys
from dataclasses import dataclass
from typing import Optional

import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
from sklearn.linear_model import LinearRegression


# -----------------------------------------------------------------------------
# Default analysis parameters kept from the original script
# -----------------------------------------------------------------------------
DEFAULT_COX = 2.24e-8
DEFAULT_L = 50.0e-6
DEFAULT_W = 300.0e-6
DEFAULT_TARGET_CURRENT = 1.0e-10
DEFAULT_IOFF_THRESHOLD = 1.0e-14
DEFAULT_ROLLING_WINDOW = 7


@dataclass(frozen=True)
class AnalysisConfig:
    """TFTパラメータ抽出に使用される設定値を格納します。

    属性:
        :param cox: 単位面積あたりのゲート容量。L、W、Id、Vdと単位を合わせる必要があります。元のスクリプトでは 2.24e-8でした。
        :type cox: float
        :param channel_length: チャネル長 L。
        :type channel_length: float
        :param channel_width: チャネル幅 W。
        :type channel_width: float
        :param target_current: SSおよびVonの評価に使用される電流しきい値。
        :type target_current: float
        :param ioff_threshold: Ioffの平均化に使用される上限電流しきい値。
        :type ioff_threshold: float
        :param rolling_window: 局所勾配の移動平均のウィンドウサイズ。
        :type rolling_window: int
    """

    cox: float = DEFAULT_COX
    channel_length: float = DEFAULT_L
    channel_width: float = DEFAULT_W
    target_current: float = DEFAULT_TARGET_CURRENT
    ioff_threshold: float = DEFAULT_IOFF_THRESHOLD
    rolling_window: int = DEFAULT_ROLLING_WINDOW


@dataclass
class IdVgResult:
    """Id-Vg曲線から計算された要約値を格納します。

    属性:
        :param dataframe: 処理されたデータを含むpandas DataFrame。
        :type dataframe: pandas.DataFrame
        :param vth: しきい値電圧。
        :type vth: float
        :param max_slope: 最大勾配。
        :type max_slope: float
        :param vg_at_max_slope: 最大勾配を示すVg値。
        :type vg_at_max_slope: float
        :param mobility: 移動度。
        :type mobility: float
        :param ss: サブスレッショルドスイング。
        :type ss: float
        :param ion: オン電流。
        :type ion: float
        :param ioff: オフ電流。推定できない場合はNone。
        :type ioff: Optional[float]
        :param ion_ioff_ratio: オン電流とオフ電流の比。IoffがNoneの場合はNone。
        :type ion_ioff_ratio: Optional[float]
        :param von: オン電圧。
        :type von: float
    """

    dataframe: pd.DataFrame
    vth: float
    max_slope: float
    vg_at_max_slope: float
    mobility: float
    ss: float
    ion: float
    ioff: Optional[float]
    ion_ioff_ratio: Optional[float]
    von: float


@dataclass
class IdVdResult:
    """Id-Vd曲線から計算された要約値を格納します。

    属性:
        :param dataframe: 処理されたデータを含むpandas DataFrame。
        :type dataframe: pandas.DataFrame
        :param mueff: 実効移動度。
        :type mueff: float
    """

    dataframe: pd.DataFrame
    mueff: float


# -----------------------------------------------------------------------------
# Input and validation
# -----------------------------------------------------------------------------
def read_excel_file(path: str, label: str) -> pd.DataFrame:
    """概要:
        Excelファイルを読み込み、失敗時に明確なエラーメッセージを追加します。
    引数:
        :param path: Excelファイルへのパス。
        :type path: str
        :param label: エラーメッセージで使用される人間が読める名前。
        :type label: str
    戻り値:
        :returns: Excelファイルから読み込まれたDataFrame。
        :rtype: pandas.DataFrame
    例外:
        :raises FileNotFoundError: ファイルが存在しない場合。
        :raises ValueError: pandasがファイルを読み込めない場合。
    """

    if not os.path.exists(path):
        raise FileNotFoundError(f"{label} file not found: {path}")

    try:
        return pd.read_excel(path)
    except Exception as exc:  # pandas may raise different exceptions by engine/version
        raise ValueError(f"Failed to read {label} Excel file: {path}") from exc


def validate_required_columns(df: pd.DataFrame, required_columns: list[str], label: str) -> None:
    """概要:
        DataFrameが必要な列を含んでいることを検証します。
    引数:
        :param df: 検証するDataFrame。
        :type df: pandas.DataFrame
        :param required_columns: 分析に必要な列名。
        :type required_columns: list[str]
        :param label: エラーメッセージで使用される人間が読める名前。
        :type label: str
    例外:
        :raises ValueError: 1つ以上の必須列が欠落している場合。
    """

    missing = [col for col in required_columns if col not in df.columns]
    if missing:
        raise ValueError(
            f"{label} data is missing required columns: {missing}. "
            f"Available columns: {list(df.columns)}"
        )


# -----------------------------------------------------------------------------
# Numerical helper functions
# -----------------------------------------------------------------------------
def calculate_local_slope_by_linear_fit(
    df: pd.DataFrame,
    x_series: pd.Series,
    y_series: pd.Series,
    half_window: int = 1,
) -> list[float]:
    """概要:
        各データポイント周辺で y = a*x + b をフィッティングして局所勾配を計算します。
    詳細説明:
        この関数は、元のスクリプトの slope3data および slope7data スタイルの関数を置き換えます。
        half_window=1 の場合、約3点を使用して、元の slope3data の動作に可能な限り一致させます。
    引数:
        :param df: 元となるDataFrame。その長さのみが使用されます。
        :type df: pandas.DataFrame
        :param x_series: X軸の値。
        :type x_series: pandas.Series
        :param y_series: Y軸の値。
        :type y_series: pandas.Series
        :param half_window: 各側の隣接ポイント数。
        :type half_window: int
    戻り値:
        :returns: 局所勾配のリスト。フィットできないポイントはNaNとして返されます。
        :rtype: list[float]
    """

    slopes: list[float] = []

    for index in range(len(df)):
        x_window = x_series[index - half_window : index + half_window + 1]
        y_window = y_series[index - half_window : index + half_window + 1]

        valid = pd.concat([x_window, y_window], axis=1).dropna()
        if len(valid) < 2:
            slopes.append(np.nan)
            continue

        try:
            x = valid.iloc[:, 0].to_numpy().reshape(-1, 1)
            y = valid.iloc[:, 1].to_numpy()
            model = LinearRegression().fit(x, y)
            slopes.append(float(model.coef_[0]))
        except ValueError as exc:
            logging.debug("Local slope fitting failed at index %s: %s", index, exc)
            slopes.append(np.nan)

    return slopes


def intercept_x(x: float, y: float, slope: float) -> float:
    """概要:
        直線 y_line = slope * (x_line - x0) のX切片を計算します。
    引数:
        :param x: 直線上の点のX座標。
        :type x: float
        :param y: 直線上の点のY座標。
        :type y: float
        :param slope: 直線の勾配。
        :type slope: float
    戻り値:
        :returns: X切片の値。
        :rtype: float
    """

    if pd.isna(slope) or slope == 0:
        return np.nan
    return float(x - y / slope)


def calculate_vth_by_max_slope(
    vg_series: pd.Series,
    y_series: pd.Series,
    slope_series: pd.Series,
) -> tuple[float, float, float]:
    """概要:
        最大勾配接線法を使用してしきい値電圧を計算します。
    引数:
        :param vg_series: ゲート電圧の値。
        :type vg_series: pandas.Series
        :param y_series: 分析モードに応じてIdまたはsqrt(Id)。
        :type y_series: pandas.Series
        :param slope_series: 局所勾配の値。
        :type slope_series: pandas.Series
    戻り値:
        :returns: (Vth, max_slope, Vg_at_max_slope) のタプル。
        :rtype: tuple
    例外:
        :raises ValueError: 有効な勾配が見つからない場合。
    """

    valid_slope = slope_series.replace([np.inf, -np.inf], np.nan).dropna()
    if valid_slope.empty:
        raise ValueError("No valid slope values are available for Vth calculation.")

    idx = valid_slope.idxmax()
    max_slope = float(valid_slope.loc[idx])
    x = float(vg_series.loc[idx])
    y = float(y_series.loc[idx])
    vth = intercept_x(x, y, max_slope)
    return vth, max_slope, x


def calculate_ion_ioff(id_series: pd.Series, ioff_threshold: float) -> tuple[float, Optional[float], Optional[float]]:
    """概要:
        Ion、Ioff、およびIon/Ioff比を計算します。
    引数:
        :param id_series: ドレイン電流の値。正の値が期待されます。
        :type id_series: pandas.Series
        :param ioff_threshold: 0 < Id < ioff_threshold を満たすデータポイントを平均してIoffを推定します。
        :type ioff_threshold: float
    戻り値:
        :returns: (Ion, Ioff, Ion/Ioff) のタプル。Ioffが推定できない場合、IoffとIon/IoffはNoneとして返されます。
        :rtype: tuple
    """

    valid_id = pd.to_numeric(id_series, errors="coerce").replace([np.inf, -np.inf], np.nan)
    ion = float(valid_id.max()) if not valid_id.dropna().empty else np.nan

    ioff_data = valid_id[(valid_id > 0) & (valid_id < ioff_threshold)].dropna()
    if ioff_data.empty:
        return ion, None, None

    ioff = float(ioff_data.mean())
    ratio = float(ion / ioff) if ioff != 0 else None
    return ion, ioff, ratio


def first_index_at_or_above(series: pd.Series, threshold: float) -> Optional[int]:
    """概要:
        シリーズ値がしきい値以上である最初のインデックスを返します。
    引数:
        :param series: 数値シリーズ。
        :type series: pandas.Series
        :param threshold: しきい値。
        :type threshold: float
    戻り値:
        :returns: 最初の一致するインデックス。条件を満たすポイントがない場合はNone。
        :rtype: Optional[int]
    """

    matched = series.index[series >= threshold]
    if len(matched) == 0:
        return None
    return matched.min()


def safe_inverse(value: float) -> float:
    """概要:
        1/value を返します。逆数が明確に定義されていない場合はNaNを返します。
    引数:
        :param value: 逆数を計算する数値。
        :type value: float
    戻り値:
        :returns: valueの逆数、またはNaN。
        :rtype: float
    """

    if pd.isna(value) or value == 0:
        return np.nan
    return float(1.0 / value)


# -----------------------------------------------------------------------------
# TFT analysis functions
# -----------------------------------------------------------------------------
def analyze_idvg_sat(df: pd.DataFrame, config: AnalysisConfig) -> IdVgResult:
    """概要:
        飽和領域Id-Vgデータを分析します。
    詳細説明:
        計算は元のスクリプトに従います。
        - 負/非正のIdをログおよび平方根演算のためにNaNに変換します。
        - log10(Id) および Id ** 0.5 (sqrt(Id)) を計算します。
        - 3点線形フィッティングにより局所勾配を計算します。
        - Id ** 0.5 (sqrt(Id)) 勾配を中心移動平均で平滑化します。
        - 平滑化された Id ** 0.5 (sqrt(Id)) 勾配から飽和移動度を計算します。
        - 最大勾配接線法によりVthを計算します。
        - Id >= target_current となる最初のポイントでSSを計算します。
    引数:
        :param df: 入力DataFrame。必須列: Vg および Id。
        :type df: pandas.DataFrame
        :param config: 分析設定。
        :type config: AnalysisConfig
    戻り値:
        :returns: 処理されたDataFrameと要約値を含むIdVgResult。
        :rtype: IdVgResult
    """

    validate_required_columns(df, ["Vg", "Id"], "SAT Id-Vg")
    df = df.copy()

    df["Id"] = pd.to_numeric(df["Id"], errors="coerce")
    df["Id_rm"] = np.where(df["Id"] > 0, df["Id"], np.nan)
    df["logId"] = np.log10(df["Id_rm"])
    df["rootId"] = df["Id_rm"] ** 0.5

    df["slope logId"] = calculate_local_slope_by_linear_fit(df, df["Vg"], df["logId"], half_window=1)
    df["slope rootId"] = calculate_local_slope_by_linear_fit(df, df["Vg"], df["rootId"], half_window=1)
    df["slope logId"] = df["slope logId"].replace([np.inf, -np.inf], np.nan)
    df["slope rootId"] = df["slope rootId"].replace([np.inf, -np.inf], np.nan)
    df["slope rootId smooth"] = df["slope rootId"].rolling(config.rolling_window, center=True).mean()

    df["muSAT"] = (
        2.0
        * config.channel_length
        / config.channel_width
        / config.cox
        * (df["slope rootId smooth"] ** 2)
    )
    df["muSAT"] = df["muSAT"].replace([np.inf, -np.inf], np.nan)

    vth, max_slope, vg_at_max_slope = calculate_vth_by_max_slope(
        df["Vg"], df["rootId"], df["slope rootId smooth"]
    )
    mobility = float(df["muSAT"].max()) if not df["muSAT"].dropna().empty else np.nan

    idx = first_index_at_or_above(df["Id_rm"], config.target_current)
    if idx is None:
        logging.warning("SAT: Id never reaches target current %.3e A. SS and Von are set to NaN.", config.target_current)
        ss = np.nan
        von = np.nan
    else:
        ss = safe_inverse(df.loc[idx, "slope logId"])
        von = float(df.loc[idx, "Vg"])

    ion, ioff, ratio = calculate_ion_ioff(df["Id_rm"], config.ioff_threshold)

    return IdVgResult(
        dataframe=df,
        vth=vth,
        max_slope=max_slope,
        vg_at_max_slope=vg_at_max_slope,
        mobility=mobility,
        ss=ss,
        ion=ion,
        ioff=ioff,
        ion_ioff_ratio=ratio,
        von=von,
    )


def analyze_idvg_lin(df: pd.DataFrame, config: AnalysisConfig) -> IdVgResult:
    """概要:
        線形領域Id-Vgデータを分析します。
    詳細説明:
        計算は元のスクリプトに従います。
        - 負/非正のIdをNaNに変換します。
        - Id および log10(Id) の局所勾配を計算します。
        - Id 勾配を中心移動平均で平滑化します。
        - 平滑化された Id 勾配から電界効果移動度を計算します。
        - 最大勾配接線法によりVthを計算します。
        - Id >= target_current となる最初のポイントでSSを計算します。
    引数:
        :param df: 入力DataFrame。必須列: Vg、Vd、および Id。
        :type df: pandas.DataFrame
        :param config: 分析設定。
        :type config: AnalysisConfig
    戻り値:
        :returns: 処理されたDataFrameと要約値を含むIdVgResult。
        :rtype: IdVgResult
    """

    validate_required_columns(df, ["Vg", "Vd", "Id"], "LIN Id-Vg")
    df = df.copy()

    df["Id"] = pd.to_numeric(df["Id"], errors="coerce")
    df["Id_rm"] = np.where(df["Id"] > 0, df["Id"], np.nan)
    df["logId"] = np.log10(df["Id_rm"])

    df["slope Id"] = calculate_local_slope_by_linear_fit(df, df["Vg"], df["Id_rm"], half_window=1)
    df["slope logId"] = calculate_local_slope_by_linear_fit(df, df["Vg"], df["logId"], half_window=1)
    df["slope Id"] = df["slope Id"].replace([np.inf, -np.inf], np.nan)
    df["slope Id smooth"] = df["slope Id"].rolling(config.rolling_window, center=True).mean()
    df["slope logId"] = df["slope logId"].replace([np.inf, -np.inf], np.nan)

    df["muFE"] = (
        config.channel_length
        / config.channel_width
        / config.cox
        / df["Vd"]
        * df["slope Id smooth"]
    )
    df["muFE"] = df["muFE"].replace([np.inf, -np.inf], np.nan)

    vth, max_slope, vg_at_max_slope = calculate_vth_by_max_slope(
        df["Vg"], df["Id_rm"], df["slope Id smooth"]
    )
    mobility = float(df["muFE"].max()) if not df["muFE"].dropna().empty else np.nan

    idx = first_index_at_or_above(df["Id_rm"], config.target_current)
    if idx is None:
        logging.warning("LIN: Id never reaches target current %.3e A. SS and Von are set to NaN.", config.target_current)
        ss = np.nan
        von = np.nan
    else:
        ss = safe_inverse(df.loc[idx, "slope logId"])
        von = float(df.loc[idx, "Vg"])

    ion, ioff, ratio = calculate_ion_ioff(df["Id_rm"], config.ioff_threshold)

    return IdVgResult(
        dataframe=df,
        vth=vth,
        max_slope=max_slope,
        vg_at_max_slope=vg_at_max_slope,
        mobility=mobility,
        ss=ss,
        ion=ion,
        ioff=ioff,
        ion_ioff_ratio=ratio,
        von=von,
    )


def analyze_idvd_lin(df: pd.DataFrame, vth: float, config: AnalysisConfig) -> IdVdResult:
    """概要:
        線形領域Id-Vdデータを分析します。
    詳細説明:
        この関数は、元のスクリプトに analyze_IdVd_LIN が含まれていたため保持されていますが、
        メインプロセスからは呼び出されません。
    引数:
        :param df: 入力DataFrame。必須列: Vd、Vg、および Id。
        :type df: pandas.DataFrame
        :param vth: mueff計算に使用されるしきい値電圧。
        :type vth: float
        :param config: 分析設定。
        :type config: AnalysisConfig
    戻り値:
        :returns: 処理されたDataFrameと最大mueffを含むIdVdResult。
        :rtype: IdVdResult
    """

    validate_required_columns(df, ["Vd", "Vg", "Id"], "LIN Id-Vd")
    df = df.copy()

    df["Id"] = pd.to_numeric(df["Id"], errors="coerce")
    df["Id_rm"] = np.where(df["Id"] > 0, df["Id"], np.nan)
    df = df.sort_values(by="Vd")
    df["slope Id"] = calculate_local_slope_by_linear_fit(df, df["Vd"], df["Id_rm"], half_window=1)
    df["mueff"] = (
        config.channel_length
        / config.channel_width
        / config.cox
        / (df["Vg"] - vth)
        * df["slope Id"]
    )
    df["mueff"] = df["mueff"].replace([np.inf, -np.inf], np.nan)
    mueff = float(df["mueff"].max()) if not df["mueff"].dropna().empty else np.nan

    return IdVdResult(dataframe=df, mueff=mueff)


# -----------------------------------------------------------------------------
# Plot functions
# -----------------------------------------------------------------------------
def plot_idvg_lin(df: pd.DataFrame, vth: float, max_slope: float, output_path: str) -> None:
    """概要:
        線形領域Id-Vgプロットを最大勾配接線とともに保存します。
    引数:
        :param df: プロットするデータを含むDataFrame。
        :type df: pandas.DataFrame
        :param vth: しきい値電圧。
        :type vth: float
        :param max_slope: 最大勾配。
        :type max_slope: float
        :param output_path: 出力画像の保存パス。
        :type output_path: str
    戻り値:
        :returns: なし
        :rtype: None
    """

    plt.figure()
    plt.scatter(df["Vg"], df["Id"], label="Vd=0.1V", s=4)
    df_line = max_slope * (df["Vg"] - vth)
    plt.plot(df["Vg"], df_line, color="red")
    plt.xlabel("Vg(V)")
    plt.ylabel("Id(A)")
    plt.ylim(0, 1.1 * df["Id"].max())
    plt.legend()
    plt.savefig(output_path)
    plt.close()


def plot_root_idvg(df: pd.DataFrame, vth: float, max_slope: float, output_path: str) -> None:
    """概要:
        飽和領域 Id ** 0.5 (sqrt(Id)) -Vg プロットを接線とともに保存します。
    引数:
        :param df: プロットするデータを含むDataFrame。
        :type df: pandas.DataFrame
        :param vth: しきい値電圧。
        :type vth: float
        :param max_slope: 最大勾配。
        :type max_slope: float
        :param output_path: 出力画像の保存パス。
        :type output_path: str
    戻り値:
        :returns: なし
        :rtype: None
    """

    plt.figure()
    plt.scatter(df["Vg"], df["rootId"], label="Vd=10V", s=4)
    df_line = max_slope * (df["Vg"] - vth)
    plt.plot(df["Vg"], df_line, color="red")
    plt.xlabel("Vg(V)")
    plt.ylabel("Id^0.5(A^0.5)")
    plt.ylim(0, 1.1 * df["rootId"].max())
    plt.legend()
    plt.savefig(output_path)
    plt.close()


# -----------------------------------------------------------------------------
# Output functions
# -----------------------------------------------------------------------------
def output_path(output_dir: str, filename: str) -> str:
    """概要:
        出力ディレクトリ内のパスを返します。
    引数:
        :param output_dir: 出力ディレクトリのパス。
        :type output_dir: str
        :param filename: ファイル名。
        :type filename: str
    戻り値:
        :returns: 出力ディレクトリ内のファイルの完全パス。
        :rtype: str
    """

    return os.path.join(output_dir, filename)


def write_summary_outputs(sat: IdVgResult, lin: IdVgResult, output_dir: str) -> None:
    """概要:
        元のスクリプトと互換性のある要約CSVファイルを書き込みます。
    詳細説明:
        この関数は意図的に、元の出力列名を互換性のために保持しています。
        これには、SSおよびIon値が列名を変更せずにlog10に変換される output_log.csv も含まれます。
    引数:
        :param sat: 飽和領域の分析結果。
        :type sat: IdVgResult
        :param lin: 線形領域の分析結果。
        :type lin: IdVgResult
        :param output_dir: 出力ディレクトリのパス。
        :type output_dir: str
    戻り値:
        :returns: なし
        :rtype: None
    """

    df_summary = pd.DataFrame()

    df_summary["Vth_SAT"] = [sat.vth]
    # df_summary["Von_SAT"] = [sat.von]
    df_summary["muSAT"] = [sat.mobility]
    # df_summary["Vgmaxslope_SAT"] = [sat.vg_at_max_slope]
    df_summary["SS_SAT"] = [sat.ss]
    df_summary["Ion_SAT"] = [sat.ion]

    df_summary["Vth_LIN"] = [lin.vth]
    # df_summary["Von_LIN"] = [lin.von]
    df_summary["muFE"] = [lin.mobility]
    # df_summary["Vgmaxslope_LIN"] = [lin.vg_at_max_slope]
    df_summary["SS_LIN"] = [lin.ss]
    df_summary["Ion_LIN"] = [lin.ion]

    df_summary.to_csv(output_path(output_dir, "output.csv"), index=False)

    df_log = df_summary.copy()
    for column in ["SS_SAT", "Ion_SAT", "SS_LIN", "Ion_LIN"]:
        df_log[column] = np.log10(df_log[column])
    df_log.to_csv(output_path(output_dir, "output_log.csv"), index=False)


# -----------------------------------------------------------------------------
# Command line interface
# -----------------------------------------------------------------------------
def parse_args(argv: Optional[list[str]] = None) -> argparse.Namespace:
    """概要:
        コマンドライン引数を解析します。
    引数:
        :param argv: コマンドライン引数のリスト。デフォルトはNoneで、sys.argvを使用します。
        :type argv: Optional[list[str]]
    戻り値:
        :returns: 解析された引数を格納したargparse.Namespaceオブジェクト。
        :rtype: argparse.Namespace
    """

    parser = argparse.ArgumentParser(
        description="Analyze TFT Id-Vg data and calculate Vth, mobility, SS, and Ion."
    )
    parser.add_argument(
        "--lin",
        default="IdVg-Vd0.1.xlsx",
        help="Excel file for linear-region Id-Vg data. Default: %(default)s",
    )
    parser.add_argument(
        "--sat",
        default="IdVg-Vd10.xlsx",
        help="Excel file for saturation-region Id-Vg data. Default: %(default)s",
    )
    parser.add_argument(
        "--outdir",
        default=".",
        help="Output directory. Default: current directory",
    )
    parser.add_argument(
        "--cox",
        type=float,
        default=DEFAULT_COX,
        help="Gate capacitance per unit area. Default: %(default).3e",
    )
    parser.add_argument(
        "--length",
        type=float,
        default=DEFAULT_L,
        help="Channel length L. Default: %(default).3e",
    )
    parser.add_argument(
        "--width",
        type=float,
        default=DEFAULT_W,
        help="Channel width W. Default: %(default).3e",
    )
    parser.add_argument(
        "--target-current",
        type=float,
        default=DEFAULT_TARGET_CURRENT,
        help="Current threshold used for SS and Von. Default: %(default).3e",
    )
    parser.add_argument(
        "--ioff-threshold",
        type=float,
        default=DEFAULT_IOFF_THRESHOLD,
        help="Upper current threshold used for Ioff averaging. Default: %(default).3e",
    )
    parser.add_argument(
        "--rolling-window",
        type=int,
        default=DEFAULT_ROLLING_WINDOW,
        help="Centered rolling-average window for slope smoothing. Default: %(default)s",
    )
    parser.add_argument(
        "--verbose",
        action="store_true",
        help="Show detailed traceback on error.",
    )
    return parser.parse_args(argv)


def validate_config(config: AnalysisConfig) -> None:
    """概要:
        コマンドライン設定値を検証します。
    引数:
        :param config: 検証する分析設定オブジェクト。
        :type config: AnalysisConfig
    戻り値:
        :returns: なし
        :rtype: None
    例外:
        :raises ValueError: 設定値が無効な場合。
    """

    if config.cox == 0:
        raise ValueError("cox must not be zero.")
    if config.channel_width == 0:
        raise ValueError("channel width must not be zero.")
    if config.rolling_window < 1:
        raise ValueError("rolling-window must be at least 1.")
    if config.target_current <= 0:
        raise ValueError("target-current must be positive.")
    if config.ioff_threshold <= 0:
        raise ValueError("ioff-threshold must be positive.")


def run(args: argparse.Namespace) -> None:
    """概要:
        完全な分析ワークフローを実行します。
    引数:
        :param args: コマンドライン引数を格納したNamespace。
        :type args: argparse.Namespace
    戻り値:
        :returns: なし
        :rtype: None
    """

    config = AnalysisConfig(
        cox=args.cox,
        channel_length=args.length,
        channel_width=args.width,
        target_current=args.target_current,
        ioff_threshold=args.ioff_threshold,
        rolling_window=args.rolling_window,
    )
    validate_config(config)

    os.makedirs(args.outdir, exist_ok=True)

    df_sat_input = read_excel_file(args.sat, "SAT Id-Vg")
    df_lin_input = read_excel_file(args.lin, "LIN Id-Vg")

    sat = analyze_idvg_sat(df_sat_input, config)
    sat.dataframe.to_csv(output_path(args.outdir, "IdVg-Vd10_analyze.csv"), index=False)
    plot_root_idvg(
        sat.dataframe,
        sat.vth,
        sat.max_slope,
        output_path(args.outdir, "rootIdVg.png"),
    )

    lin = analyze_idvg_lin(df_lin_input, config)
    lin.dataframe.to_csv(output_path(args.outdir, "IdVg-Vd0.1_analyze.csv"), index=False)
    plot_idvg_lin(
        lin.dataframe,
        lin.vth,
        lin.max_slope,
        output_path(args.outdir, "IdVg_LIN.png"),
    )

    write_summary_outputs(sat, lin, args.outdir)

    print("Analysis finished.")
    print(f"Output directory: {args.outdir}")
    print(f"Vth_SAT = {sat.vth}")
    print(f"muSAT   = {sat.mobility}")
    print(f"SS_SAT  = {sat.ss}")
    print(f"Ion_SAT = {sat.ion}")
    print(f"Vth_LIN = {lin.vth}")
    print(f"muFE    = {lin.mobility}")
    print(f"SS_LIN  = {lin.ss}")
    print(f"Ion_LIN = {lin.ion}")


def main(argv: Optional[list[str]] = None) -> int:
    """概要:
        コマンドライン実行のエントリポイントです。
    引数:
        :param argv: コマンドライン引数のリスト。デフォルトはNoneで、sys.argvを使用します。
        :type argv: Optional[list[str]]
    戻り値:
        :returns: 終了コード。成功時は0、エラー時は1。
        :rtype: int
    """

    args = parse_args(argv)
    logging.basicConfig(
        level=logging.DEBUG if args.verbose else logging.INFO,
        format="%(levelname)s: %(message)s",
    )

    try:
        run(args)
    except Exception as exc:
        print(f"Error: {exc}", file=sys.stderr)
        if args.verbose:
            raise
        return 1

    return 0


if __name__ == "__main__":
    raise SystemExit(main())