electrical.tftanalyze のソースコード

r"""TFT n-チャネルトランジスタの電気特性解析ツール

このスクリプトは、TFT (Thin-Film Transistor) のn-チャネルデバイスにおける
I-V特性(伝達特性 (ID-VG) および出力特性 (ID-VD))データを読み込み、
解析し、結果をExcelレポートとPNGプロットとして出力します。

主な機能:
- CSV形式の測定データを自動的にエンコーディングを検出して読み込みます。
- 伝達特性データから閾値電圧 (Vth)、移動度 (Mobility)、サブスレッショルドスイング (S) などの
  主要なデバイスパラメータを抽出します。
- 出力特性データから線形領域のコンダクタンス (gd) や移動度 (mu_lin, mu_eff) を評価します。
- サビツキー・ゴレイフィルターを用いたデータの平滑化をサポートします。
- 解析結果をインタラクティブなグラフ表示とPNGファイルとして保存します。
- 全ての解析結果と生データ、平滑化データをExcelファイルに集約して出力します。

Usage:
    python tftanalyze.py --mode all --infile_vg transfer.csv --infile_vd output.csv

:doc:`tftanalyze_usage`
"""
import os
import sys
import argparse
import chardet
from pathlib import Path
import numpy as np
from scipy import constants
from scipy.signal import savgol_filter
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression


# --- 物理定数 ---
EPS0 = constants.epsilon_0


figsize_idvg_quad = (10, 8)

[ドキュメント] def get_args(): """コマンドライン引数をパースします。 この関数は、TFT解析スクリプトに必要な全てのコマンドライン引数を定義し、 ユーザーが指定した引数をパースして返します。 引数には、入力ファイルパス、解析モード、TFTデバイスの物理的寸法、 誘電体定数、電流の閾値、平滑化パラメータ、プロット表示・保存設定などが含まれます。 :returns: パースされた引数を含む `argparse.Namespace` オブジェクト。 :rtype: argparse.Namespace """ parser = argparse.ArgumentParser(description='TFT n-channel Transfer/Output Analysis & Excel Tool (detail rev.)') parser.add_argument('--infile_vg', default='TFT_Vg-Id_STD-ide3 [AS220518TFT-anneal(454) ; 2022_05_25 18_00_33].csv') parser.add_argument('--infile_vd', default='TFT_Vd-Id-ide [AS220518TFT-anneal(455) ; 2022_05_25 18_01_11].csv') parser.add_argument('--mode', choices=['read', 'analyze_idvg', 'analyze_idvd', 'all'], default='all') parser.add_argument('--out_excel', default=None, help='Output Excel file. If omitted, a mode-dependent filename is used.') parser.add_argument('--L', type=float, default=50.0, help='Channel length [um]') parser.add_argument('--W', type=float, default=300.0, help='Channel width [um]') parser.add_argument('--dg', type=float, default=150.0, help='Gate insulator thickness [nm]') parser.add_argument('--epsg', type=float, default=3.9, help='Relative dielectric constant of gate insulator') parser.add_argument('--ID_S', type=float, default=1e-9, help='Reference drain current for S extraction [A]') parser.add_argument('--Imin', type=float, default=1e-15, help='Minimum current floor for log analysis [A]') parser.add_argument('--smooth_npoints', type=int, default=5) parser.add_argument('--lsq_order', type=int, default=2) parser.add_argument('--show_plot', action='store_true', default=True, help='Show plots interactively (default: True)') parser.add_argument('--no_show_plot', action='store_false', dest='show_plot', help='Do not show plots interactively') parser.add_argument('--region_factor', type=float, default=3.0, help='Safety factor for region checks: saturation requires VD >= factor*(VG-Vth); linear requires VG-Vth >= factor*VD') parser.add_argument('--save_plot', action='store_true', default=True, help='Save analysis plots as PNG files (default: True)') parser.add_argument('--no_save_plot', action='store_false', dest='save_plot', help='Do not save analysis plots') parser.add_argument('--plot_dir', default='tft_analysis_plots') parser.add_argument('--reverse_vg', action='store_true', help='Reverse VG sign after loading data. Intended for p-channel data preprocessing.') parser.add_argument('--idx_vg', type=int, default=0, help='Sweep segment index for VG-swept data. Incremented when VG sweep direction changes.') parser.add_argument('--idx_vd', type=int, default=0, help='Sweep segment index for VD-swept data. Incremented when VD sweep direction changes.') parser.add_argument('--read_smooth_domain', choices=['log', 'linear'], default='log', help='Smoothing domain for mode=read. For output ID-VD preview, linear smoothing/linear-y plotting is forced.') parser.add_argument('--read_keep_edge_raw', action='store_true', default=True, help='In mode=read, keep edge points raw instead of applying Savitzky-Golay near endpoints (default: True).') parser.add_argument('--read_smooth_edges', action='store_false', dest='read_keep_edge_raw', help='Apply Savitzky-Golay smoothing also near endpoints.') return parser.parse_args()
[ドキュメント] def calculate_cox(dg_nm, epsg): """ゲート酸化膜容量 (Cox) を計算します。 ゲート絶縁膜の厚さ (ナノメートル単位) と比誘電率から、 単位面積あたりのゲート酸化膜容量 (F/cm^2) を計算します。 物理定数として真空の誘電率 (EPS0) を使用します。 :param dg_nm: ゲート絶縁膜の厚さ [nm]。 :type dg_nm: float :param epsg: ゲート絶縁膜の比誘電率。 :type epsg: float :returns: 単位面積あたりのゲート酸化膜容量 [F/cm^2]。 :rtype: float """ dg_m = dg_nm * 1e-9 # F/m^2 -> F/cm^2 return (epsg * EPS0 / dg_m) * 1e-4
[ドキュメント] def default_excel_name(mode): """指定された解析モードに応じたデフォルトのExcelファイル名を返します。 異なる解析モード('read', 'analyze_idvg', 'analyze_idvd', 'all')に対して、 それぞれ適切なデフォルトのExcelファイル名を決定します。 指定されたモードが辞書にない場合は、汎用的なレポートファイル名を返します。 :param mode: 解析モードを示す文字列。 :type mode: str :returns: デフォルトのExcelファイル名。 :rtype: str """ names = { 'read': 'tft_read_data.xlsx', 'analyze_idvg': 'tft_analysis_idvg.xlsx', 'analyze_idvd': 'tft_analysis_idvd.xlsx', 'all': 'tft_analysis_all.xlsx', } return names.get(mode, 'tft_analysis_report.xlsx')
[ドキュメント] class Tee: """stdout/stderr をコンソールとログファイルへ同時出力する簡単な Tee。 このクラスは、複数のファイルライクオブジェクトに書き込み操作をミラーリングするために使用されます。 例えば、標準出力への書き込みと同時にログファイルへの書き込みを行う場合に便利です。 :param *streams: データを書き込む対象となる一つ以上のファイルライクオブジェクト。 :type *streams: file-like objects """ def __init__(self, *streams): self.streams = streams
[ドキュメント] def write(self, data): for stream in self.streams: stream.write(data) stream.flush()
[ドキュメント] def flush(self): for stream in self.streams: stream.flush()
def _existing_or_first_path(*paths): """出力名の基準にする入力ファイルを選びます。存在確認はゆるく行います。 複数のファイルパスが与えられた場合、最初に空でないパス、 または提供されたパスのリストの最初のパスを`Path`オブジェクトとして返します。 ファイルシステムの存在チェックは行いません。 :param *paths: 検査するファイルパスの可変引数。 :type *paths: str :returns: 基準として選択されたファイルパスの`Path`オブジェクト。 :rtype: pathlib.Path """ for path in paths: if path: return Path(path) return Path('tft_analysis')
[ドキュメント] def prepare_output_paths(args): """Excel/PNG/log の保存先を入力ファイルと同じディレクトリにそろえます。 入力ファイルパス(`infile_vg`, `infile_vd`)を基準に、 Excelレポート、PNGプロット、ログファイルの出力ディレクトリとファイル名を決定し、 `args`オブジェクトに設定します。これにより、全ての出力が関連する入力ファイルの近くに集約されます。 :param args: コマンドライン引数を含む `argparse.Namespace` オブジェクト。 `infile_vg`, `infile_vd`, `mode`, `out_excel` 属性を使用します。 :type args: argparse.Namespace :returns: 出力パスが設定された `argparse.Namespace` オブジェクト。 :rtype: argparse.Namespace """ if args.mode == 'analyze_idvd': ref = _existing_or_first_path(args.infile_vd, args.infile_vg) else: ref = _existing_or_first_path(args.infile_vg, args.infile_vd) ref = ref.expanduser() out_dir = ref.parent if str(ref.parent) not in ('', '.') else Path.cwd() out_dir = out_dir.resolve() stem = ref.stem if ref.stem else 'tft_analysis' args.output_dir = str(out_dir) args.output_stem = stem args.plot_dir = str(out_dir) if args.out_excel is None: args.out_excel = str(out_dir / f'{stem}_{args.mode}.xlsx') else: user_name = Path(args.out_excel).name user_stem = Path(user_name).stem user_suffix = Path(user_name).suffix or '.xlsx' if stem not in user_stem: user_name = f'{stem}_{user_stem}{user_suffix}' args.out_excel = str(out_dir / user_name) args.log_file = str(out_dir / f'{stem}_{args.mode}.log') return args
[ドキュメント] def plot_path(args, name): """入力stemつきPNG保存パスを作成します。 コマンドライン引数 (`args`) から取得した出力ステムとプロットディレクトリを基に、 指定された名前 (`name`) でPNGファイルの完全な保存パスを構築します。 :param args: コマンドライン引数を含む `argparse.Namespace` オブジェクト。 `output_stem` および `plot_dir` 属性を使用します。 :type args: argparse.Namespace :param name: 保存するPNGファイルの名前(拡張子なし)。 :type name: str :returns: 生成されたPNGファイルの完全なパス。 :rtype: str """ stem = getattr(args, 'output_stem', 'tft_analysis') out_dir = Path(getattr(args, 'plot_dir', '.')) return str(out_dir / f'{stem}_{name}.png')
[ドキュメント] def savgol_center_only(y, win, order, keep_edge_raw=True): """サビツキー・ゴレイフィルターを適用し、オプションで端点付近の生データを保持します。 `scipy.signal.savgol_filter` は端点付近を外挿/補間することがあります。 TFT出力曲線では、VD=0側の点数が少ないため、端点平滑化が系統的なずれのように見えることがあります。 `keep_edge_raw=True` の場合、完全な中心ウィンドウを持つ点のみが平滑化された値に置き換えられ、 端点付近のデータは元のクリップされたデータのまま維持されます。 データ長が短すぎる場合、またはウィンドウ長が不正な場合は、元のデータが返され、 平滑化が適用されなかったことを示すブール配列が返されます。 :param y: 平滑化するデータ配列。 :type y: numpy.ndarray or list :param win: サビツキー・ゴレイフィルターのウィンドウ長。奇数である必要があります。 :type win: int :param order: フィルターの多項式の次数。 :type order: int :param keep_edge_raw: Trueの場合、完全な中心ウィンドウを持たない端点付近のデータを生データのまま保持します。 :type keep_edge_raw: bool :returns: 平滑化されたデータ配列と、各点が平滑化に使用されたかを示すブール配列のタプル。 :rtype: tuple[numpy.ndarray, numpy.ndarray] """ y = np.asarray(y, dtype=float) if len(y) < 3 or not np.isfinite(win): return y.copy(), np.zeros(len(y), dtype=bool) win_i = int(win) if win_i < 3 or win_i > len(y): return y.copy(), np.zeros(len(y), dtype=bool) order_i = min(int(order), win_i - 1) ys = savgol_filter(y, win_i, order_i, mode='interp') used = np.ones(len(y), dtype=bool) if keep_edge_raw: half = win_i // 2 used[:] = False if len(y) > 2 * half: used[half:len(y)-half] = True out = y.copy() out[used] = ys[used] return out, used return ys, used
[ドキュメント] def add_read_columns_grouped(df, xcol, groupcol, args): """データフレームに電流のクリッピングと平滑化された電流の列を追加します。 この関数は、読み込み/プレビューモードのために、各グループ(またはデータ全体)に対して、 以下の列を追加します: - `ID_abs_floor`: `ID`の絶対値を`args.Imin`でクリッピングした値。 - `ID_smooth_linear`: 線形スケールで平滑化されたID。 - `logID_smooth`: 対数スケールで平滑化された`log10(|ID|)`。 - `ID_smooth_log`: `logID_smooth`を元に対数スケールで平滑化されたID。 - `ID_smooth`: `args.read_smooth_domain`に応じて`ID_smooth_linear`または`ID_smooth_log`。 - `savgol_used_linear`, `savgol_used_log`, `savgol_used`: Savitzky-Golay平滑化が適用された点を示すブールマスク。 重要な点: - 伝達特性 (ID-VG) のプレビューは、デフォルトで`log10(|ID|)`の平滑化後に逆変換します。 - 出力特性 (ID-VD) のプレビューは、線形電流平滑化と線形Y軸プロットを強制します。 - デフォルトでは、端点付近ではSavitzky-Golayフィルターの完全な中心ウィンドウがないため、 平滑化は無効化されます。これにより、VD=0付近の人工的なずれを回避します。 :param df: 処理対象のDataFrame。 :type df: pandas.DataFrame :param xcol: データのX軸となる列名(例: 'VG', 'VD')。 :type xcol: str :param groupcol: グループ化に使用する列名(例: 'VD', 'VG')。この列が存在しない場合、データ全体が単一のグループとして扱われます。 :type groupcol: str :param args: コマンドライン引数を含むオブジェクト。`Imin`, `lsq_order`, `smooth_npoints`, `read_keep_edge_raw`, `read_smooth_domain` 属性を使用します。 :type args: argparse.Namespace :returns: クリップおよび平滑化された電流列が追加されたDataFrame。 :rtype: pandas.DataFrame """ def _add_cols(tmp): tmp = tmp.sort_values(xcol).copy() tmp['ID_abs_floor'] = tmp['ID'].abs().clip(lower=args.Imin) order = int(getattr(args, 'lsq_order', 2)) win = valid_savgol_window(len(tmp), args.smooth_npoints, order) if len(tmp) >= 3 else np.nan keep_edge_raw = bool(getattr(args, 'read_keep_edge_raw', True)) if len(tmp) >= 3 and np.isfinite(win): win_i = int(win) lin, used_lin = savgol_center_only(tmp['ID_abs_floor'].to_numpy(), win_i, order, keep_edge_raw) tmp['ID_smooth_linear'] = np.clip(lin, args.Imin, None) log_id = np.log10(tmp['ID_abs_floor'].to_numpy()) log_s, used_log = savgol_center_only(log_id, win_i, order, keep_edge_raw) tmp['logID_smooth'] = log_s tmp['ID_smooth_log'] = np.clip(np.power(10.0, tmp['logID_smooth']), args.Imin, None) tmp['savgol_used_linear'] = used_lin tmp['savgol_used_log'] = used_log else: tmp['ID_smooth_linear'] = tmp['ID_abs_floor'] tmp['logID_smooth'] = np.log10(tmp['ID_abs_floor']) tmp['ID_smooth_log'] = tmp['ID_abs_floor'] tmp['savgol_used_linear'] = False tmp['savgol_used_log'] = False if getattr(args, 'read_smooth_domain', 'log') == 'linear': tmp['ID_smooth'] = tmp['ID_smooth_linear'] tmp['read_smooth_domain'] = 'linear current' tmp['savgol_used'] = tmp['savgol_used_linear'] else: tmp['ID_smooth'] = tmp['ID_smooth_log'] tmp['read_smooth_domain'] = 'log10 current' tmp['savgol_used'] = tmp['savgol_used_log'] tmp['read_x'] = tmp[xcol] tmp['savgol_window'] = win tmp['savgol_order'] = order tmp['edge_raw_kept'] = keep_edge_raw return tmp if groupcol not in df.columns: tmp = _add_cols(df) tmp['read_group'] = 'all' return tmp out = [] for gv in sorted(df[groupcol].dropna().unique()): tmp = df[np.isclose(df[groupcol], gv, atol=1e-3)].copy() if tmp.empty: continue tmp = _add_cols(tmp) tmp['read_group'] = f'{groupcol}={gv:g}' out.append(tmp) return pd.concat(out, ignore_index=False) if out else pd.DataFrame()
[ドキュメント] def plot_read_data(df_read, xcol, groupcol, title, args, fname_base, yscale='log'): """読み込み/プレビューモードで平滑化されたデータをプロットします。 生のクリップされたデータ (`ID_abs_floor`) と平滑化されたデータ (`ID_smooth`) を X軸 (`xcol`) に対してプロットします。 `groupcol`が指定されている場合、データはグループごとにプロットされ、凡例に表示されます。 Y軸はオプションで対数スケールに設定できます。 生成されたプロットは、`args.save_plot`がTrueの場合、指定されたディレクトリにPNGファイルとして保存されます。 :param df_read: 読み込み/プレビュー用に処理されたデータを含むDataFrame。 :type df_read: pandas.DataFrame :param xcol: X軸としてプロットする列名(例: 'VG', 'VD')。 :type xcol: str :param groupcol: データをグループ化するための列名(例: 'VD', 'VG')。 この列が存在しない場合、データ全体が単一のグループとして扱われます。 :type groupcol: str :param title: プロットのタイトル。 :type title: str :param args: コマンドライン引数を含むオブジェクト。`save_plot`と`plot_dir`属性を使用します。 :type args: argparse.Namespace :param fname_base: 保存するPNGファイル名のベース。 :type fname_base: str :param yscale: Y軸のスケール('log'または'linear')。Noneの場合、線形スケール。デフォルトは'log'。 :type yscale: str or None :returns: 生成されたMatplotlibのFigureオブジェクト。 :rtype: matplotlib.figure.Figure """ fig, ax = plt.subplots(1, 1, figsize=(8, 6)) if groupcol in df_read.columns: groups = sorted(df_read[groupcol].dropna().unique()) for gv in groups: tmp = df_read[np.isclose(df_read[groupcol], gv, atol=1e-3)].sort_values(xcol) ax.plot(tmp[xcol], tmp['ID_abs_floor'], '.', ms=3, alpha=0.35) ax.plot(tmp[xcol], tmp['ID_smooth'], '-', lw=1.6, label=f'{groupcol}={gv:g} ({tmp["read_smooth_domain"].iloc[0]})') else: tmp = df_read.sort_values(xcol) ax.plot(tmp[xcol], tmp['ID_abs_floor'], '.', ms=3, alpha=0.35, label='clipped') ax.plot(tmp[xcol], tmp['ID_smooth'], '-', lw=1.8, label=f'smoothed ({tmp["read_smooth_domain"].iloc[0]})') if yscale is not None: ax.set_yscale(yscale) ax.set_xlabel(f'{xcol} [V]') ax.set_ylabel(r'$|I_D|$ clipped/smoothed [A]') ax.set_title(title) ax.grid(True, alpha=0.15) ax.legend(fontsize='x-small', ncol=2) fig.tight_layout() if args.save_plot: os.makedirs(args.plot_dir, exist_ok=True) fname = plot_path(args, fname_base) fig.savefig(fname, dpi=200, bbox_inches='tight') print(f' plot saved: {fname}') return fig
[ドキュメント] def run_read_mode(args): """読み込み/プレビューモードの処理を実行します。 指定された入力ファイル(伝達特性と出力特性)を読み込み、`Imin`で電流をクリッピングし、 Savitzky-Golayフィルターで平滑化します。その後、処理されたデータをプロットし、 結果の概要をコンソールに出力します。 伝達特性 (ID-VG) データは対数電流平滑化がデフォルトですが、 出力特性 (ID-VD) データは線形電流平滑化と線形Y軸プロットが強制されます。 処理されたデータフレームとサマリー情報は、Excelエクスポートのために返されます。 :param args: コマンドライン引数を含むオブジェクト。 `infile_vg`, `infile_vd`, `reverse_vg`, `idx_vg`, `idx_vd`, `Imin`, `read_smooth_domain`, `save_plot`, `plot_dir` 属性を使用します。 :type args: argparse.Namespace :returns: MatplotlibのFigureオブジェクトのリスト。 読み込まれた/処理されたデータフレームをタグ('vg'または'vd')で格納した辞書。 読み込み処理のサマリー情報を含む辞書(各グループごと)のリスト。 :rtype: tuple[list[matplotlib.figure.Figure], dict[str, pandas.DataFrame], list[dict]] """ figures = [] read_tables = {} read_summary = [] targets = [] if args.infile_vg: targets.append(('vg', args.infile_vg, 'VG', 'VD', 'Read preview: transfer-like ID-VG data')) if args.infile_vd: targets.append(('vd', args.infile_vd, 'VD', 'VG', 'Read preview: output ID-VD data')) if not targets: print('WARNING: no input file is specified. Use --infile_vg and/or --infile_vd.') return figures, read_tables, read_summary for tag, path, xcol, groupcol, title in targets: df = detect_and_load(path, reverse_vg=args.reverse_vg) if df is None: continue if xcol not in df.columns: print(f'WARNING: {path} does not have required x column {xcol}; skipped.') continue idx_select = args.idx_vg if xcol == 'VG' else args.idx_vd idx_label = 'idx_vg' if xcol == 'VG' else 'idx_vd' selected_groups = [] if groupcol in df.columns: for gv in sorted(df[groupcol].dropna().unique()): gdf = df[np.isclose(df[groupcol], gv, atol=1e-3)].copy() sg = select_sweep_segment(gdf, xcol, idx_select, idx_label, sort_after=True) if not sg.empty: selected_groups.append(sg) df = pd.concat(selected_groups, ignore_index=False) if selected_groups else pd.DataFrame() else: df = select_sweep_segment(df, xcol, idx_select, idx_label, sort_after=True) # Transfer ID-VG is best previewed with log-current smoothing. # Output ID-VD should be previewed as a linear ID plot with linear-current smoothing. print("xcol=", xcol) if xcol == 'VD': local_args = argparse.Namespace(**vars(args)) local_args.read_smooth_domain = 'linear' df_read = add_read_columns_grouped(df, xcol, groupcol, local_args) read_yscale = 'linear' else: df_read = add_read_columns_grouped(df, xcol, groupcol, args) read_yscale = 'log' if df_read.empty: print(f'WARNING: no readable data after preprocessing: {path}') continue read_tables[tag] = df_read print(f"\n{'='*20} READ/PREVIEW {tag.upper()} {'='*20}") print(f'File: {path}') print(f'Rows: {len(df_read)} | x={xcol} | group={groupcol if groupcol in df_read.columns else "none"}') print(f'Imin clipping floor = {args.Imin:.4e} A') print(f'read smoothing domain = {args.read_smooth_domain}') if groupcol in df_read.columns: for gv in sorted(df_read[groupcol].dropna().unique()): tmp = df_read[np.isclose(df_read[groupcol], gv, atol=1e-3)] read_summary.append({ 'dataset': tag, 'file': path, 'xcol': xcol, 'groupcol': groupcol, 'group_value': gv, 'n_points': len(tmp), 'x_min': tmp[xcol].min(), 'x_max': tmp[xcol].max(), 'ID_abs_floor_min': tmp['ID_abs_floor'].min(), 'ID_abs_floor_max': tmp['ID_abs_floor'].max(), 'ID_smooth_min': tmp['ID_smooth'].min(), 'ID_smooth_max': tmp['ID_smooth'].max(), 'ID_smooth_log_min': tmp['ID_smooth_log'].min(), 'ID_smooth_log_max': tmp['ID_smooth_log'].max(), 'ID_smooth_linear_min': tmp['ID_smooth_linear'].min(), 'ID_smooth_linear_max': tmp['ID_smooth_linear'].max(), 'read_smooth_domain': tmp['read_smooth_domain'].iloc[0], 'savgol_window': tmp['savgol_window'].iloc[0], 'savgol_order': tmp['savgol_order'].iloc[0], 'n_savgol_used': int(tmp['savgol_used'].sum()), 'edge_raw_kept': bool(tmp['edge_raw_kept'].iloc[0]), }) print(f' {groupcol}={gv:8.4g} | n={len(tmp):4d} | {xcol}=({tmp[xcol].min():.4g}, {tmp[xcol].max():.4g}) | ' f'ID_smooth=({tmp["ID_smooth"].min():.4e}, {tmp["ID_smooth"].max():.4e}) | ' f'win={tmp["savgol_window"].iloc[0]}, order={tmp["savgol_order"].iloc[0]}, ' f'smoothed points={int(tmp["savgol_used"].sum())}/{len(tmp)}, edge_raw={tmp["edge_raw_kept"].iloc[0]}') print(" read_yscale=", read_yscale) figures.append(plot_read_data(df_read, xcol, groupcol, title, args, f'read_{tag}', yscale=read_yscale)) return figures, read_tables, read_summary
[ドキュメント] def detect_and_load(filepath, reverse_vg=False): """CSVファイルからVG-IDデータを自動検出して読み込みます。 指定されたファイルパスのCSVファイルを読み込み、`chardet`ライブラリを使用して文字コードを自動検出します。 ファイルの内容を解析し、`VG`と`ID`の列ヘッダーを持つデータブロックを探します。 データブロックが見つかった場合、そのデータはPandas DataFrameに変換されます。 `reverse_vg`がTrueの場合、読み込んだ`VG`列の符号を反転します(主にp-チャネルデバイスのデータ前処理用)。 ファイルが存在しない場合、必要なヘッダーが見つからない場合、またはデータが読み込めない場合はNoneを返します。 :param filepath: 読み込むCSVファイルのパス。 :type filepath: str :param reverse_vg: Trueの場合、VG列の符号を反転します(p-チャネルデータ用)。デフォルトはFalse。 :type reverse_vg: bool :returns: `VG`と`ID`データを含むDataFrame。データが見つからない場合やエラーが発生した場合はNone。 :rtype: pandas.DataFrame or None """ if not os.path.exists(filepath): print(f'WARNING: file not found: {filepath}') return None with open(filepath, 'rb') as f: rawdata = f.read(50000) encoding = chardet.detect(rawdata)['encoding'] or 'ascii' with open(filepath, 'r', encoding=encoding, errors='replace') as f: lines = f.readlines() data_list, header, data_start_idx = [], None, -1 for i, line in enumerate(lines): parts = [p.strip() for p in line.split(',')] if not parts or parts[0] == '': continue upper_parts = [p.upper() for p in parts] if 'VG' in upper_parts and 'ID' in upper_parts: if i + 1 < len(lines): try: float([p.strip() for p in lines[i + 1].split(',')][0]) header = [p.upper().split('(')[0] for p in parts] data_start_idx = i + 1 break except Exception: continue if data_start_idx != -1: for line in lines[data_start_idx:]: parts = [p.strip() for p in line.split(',')] if parts and len(parts) >= len(header): try: float(parts[0]) data_list.append(parts[:len(header)]) except Exception: continue if header is None or not data_list: print(f'WARNING: no VG/ID data block found: {filepath}') return None df = pd.DataFrame(data_list, columns=header).apply(pd.to_numeric, errors='coerce').dropna(subset=['VG', 'ID']) if reverse_vg and 'VG' in df.columns: df['VG'] = -df['VG'] return df
[ドキュメント] def valid_savgol_window(n, requested, order): """指定されたデータ点数に対して有効な奇数のSavitzky-Golayウィンドウ長を返します。 サビツキー・ゴレイフィルターのウィンドウ長は、多項式の次数よりも大きく、 かつ奇数である必要があります。また、データ点数を超えることはできません。 この関数は、与えられた制約 (`n`, `requested`, `order`) に基づいて、 これらの条件を満たす最小かつ有効な奇数のウィンドウ長を計算して返します。 :param n: データ点の総数。 :type n: int :param requested: 要求されたSavitzky-Golayウィンドウ長。 :type requested: int :param order: Savitzky-Golayフィルターの多項式の次数。 :type order: int :returns: 有効な奇数のSavitzky-Golayウィンドウ長。 :rtype: int """ win = max(int(requested), order + 2) if win % 2 == 0: win += 1 if win > n: win = n if n % 2 == 1 else n - 1 if win <= order: win = order + 2 if (order + 2) % 2 == 1 else order + 3 return max(3, win)
[ドキュメント] def nearest_row_by_current(df, target_id): """データフレーム内で指定された目標電流 (`target_id`) に最も近いID_smoothを持つ行を検索します。 データフレーム`df`の`ID_smooth`列を基に、目標電流値`target_id`との絶対差が最小となる行を特定します。 見つかった行のインデックスとその行全体のデータを返します。 データフレームが空の場合、または`ID_smooth`列が存在しない場合は、Noneを返します。 :param df: 検索対象のDataFrame。`ID_smooth`列が必要です。 :type df: pandas.DataFrame :param target_id: 検索する目標電流値 [A]。 :type target_id: float :returns: 目標電流に最も近い行のインデックスと、その行のPandas Series。 データフレームが空の場合は(None, None)を返します。 :rtype: tuple[int or None, pandas.Series or None] """ if df.empty: return None, None idx = (df['ID_smooth'] - target_id).abs().idxmin() return idx, df.loc[idx]
[ドキュメント] def add_sweep_index(df, col, idx_col): """指定された列の掃引方向の変化に基づいて、掃引セグメントのインデックスを追加します。 データフレームの指定された列 (`col`) の値の連続的な変化を分析し、 掃引方向が反転するたびに掃引セグメントのインデックスを1つ増やします。 これにより、多方向掃引データ(例: VGの往復掃引)を個別のセグメントに分割できます。 結果のインデックスは新しい列 (`idx_col`) としてデータフレームに追加されます。 :param df: 処理対象のDataFrame。 :type df: pandas.DataFrame :param col: 掃引方向を検出する基準となる列名(例: 'VG', 'VD')。 :type col: str :param idx_col: 生成される掃引インデックス列の名前。 :type idx_col: str :returns: 掃引セグメントインデックス列が追加されたDataFrame。 :rtype: pandas.DataFrame """ out = df.copy() vals = out[col].to_numpy(dtype=float) seg = [] current_seg = 0 prev_sign = 0 prev_val = np.nan for v in vals: sign = 0 if np.isfinite(v) and np.isfinite(prev_val): dv = v - prev_val if abs(dv) > 1e-12: sign = 1 if dv > 0 else -1 if sign != 0: if prev_sign != 0 and sign != prev_sign: current_seg += 1 prev_sign = sign seg.append(current_seg) prev_val = v out[idx_col] = seg return out
[ドキュメント] def select_sweep_segment(df, col, idx, label, sort_after=True, verbose=False): """掃引方向の変化インデックスに基づいて、DataFrameから特定の掃引セグメントを選択します。 この関数は、`add_sweep_index`を使用して、指定された列 (`col`) の掃引セグメントインデックスを計算し、 その後、要求されたインデックス (`idx`) に対応するセグメントのみをフィルタリングして返します。 要求されたインデックスが存在しない場合、利用可能な最初のセグメントがフォールバックとして選択されます。 オプションで、選択されたセグメントを`col`列でソートできます。 :param df: 処理対象のDataFrame。 :type df: pandas.DataFrame or None :param col: 掃引セグメントを識別する基準となる列名(例: 'VG', 'VD')。 :type col: str :param idx: 選択する掃引セグメントのインデックス。 :type idx: int :param label: 警告メッセージなどで使用するインデックスのラベル(例: 'idx_vg')。 :type label: str :param sort_after: Trueの場合、選択後に`col`列でDataFrameをソートします。デフォルトはTrue。 :type sort_after: bool :param verbose: Trueの場合、選択されたセグメントに関する詳細情報を出力します。デフォルトはFalse。 :type verbose: bool :returns: 選択された掃引セグメントを含むDataFrame。元のDataFrameがNoneまたは空の場合、 または`col`列がない場合は、元のDataFrameまたはNoneを返します。 :rtype: pandas.DataFrame or None """ if df is None or df.empty or col not in df.columns: return df.copy() if df is not None else df idx_col = f'idx_{col.lower()}_sweep' work = add_sweep_index(df, col, idx_col) available = sorted(work[idx_col].dropna().unique()) if idx not in available: print(f'WARNING: requested {label}={idx}, but available {label} values are {available}; fallback to first segment.') idx = available[0] if available else 0 selected = work[work[idx_col] == idx].copy() if sort_after and not selected.empty: selected = selected.sort_values(col) if verbose and not selected.empty: print(f' selected {label}={idx}: n={len(selected)}, {col}=({selected[col].min():.4g}, {selected[col].max():.4g})') return selected
[ドキュメント] def build_analysis_points(res): """伝達特性解析の主要点をロングフォーマットで返します。 `res`辞書からVth, Smin, mu_maxなどの主要な解析ポイントを抽出し、 Excelサマリーシートに適したリスト形式で提供します。 各ポイントは、そのVG, ID, 導関数、移動度などの詳細な情報とともに辞書として格納されます。 :param res: 伝達特性解析結果を含む辞書。`df` (解析対象のDataFrame) および `idx_`で始まるキー (各ポイントのインデックス) が必要です。 :type res: dict :returns: 各解析ポイントのデータを格納した辞書のリスト。 :rtype: list[dict] """ keys = [ ('Vth_lin_ID_max_slope', res.get('idx_lin_slope_max'), 'Linear-region Vth from tangent at max d(ID)/dVG'), ('Vth_sat_sqrtID_max_slope', res.get('idx_sat_slope_max'), 'Saturation-region Vth from tangent at max d(sqrt(ID))/dVG'), ('Smin_max_log_slope', res.get('idx_smin'), 'Minimum S = inverse max dlog10(ID)/dVG in subthreshold region'), ('S_at_ID_S', res.get('idx_ids'), 'S at the current closest to ID_S'), ('mu_lin_max', res.get('idx_mu_lin_max'), 'Maximum linear-field-effect mobility muFE'), ('mu_sat_max', res.get('idx_mu_sat_max'), 'Maximum saturation mobility profile muSAT_prof'), ('Ioff', res.get('idx_ioff'), 'Off-current reference point'), ] rows = [] df = res['df'] for name, idx, note in keys: if idx is None or (isinstance(idx, float) and np.isnan(idx)) or idx not in df.index: continue r = df.loc[idx] rows.append({ 'VD': res['VD'], 'point': name, 'VG': r.get('VG', np.nan), 'ID': r.get('ID', np.nan), 'ID_smooth': r.get('ID_smooth', np.nan), 'logID': r.get('logID', np.nan), 'sqrtID': r.get('sqrtID', np.nan), 'dlogID_dVG': r.get('dlogID', np.nan), 'S_V_per_dec': r.get('S_val', np.nan), 'gm_A_per_V': r.get('gm', np.nan), 'dsqrtID_dVG': r.get('dsqrtID', np.nan), 'mu_lin_cm2_Vs': r.get('muFE', np.nan), 'mu_sat_cm2_Vs': r.get('muSAT_prof', np.nan), 'note': note, }) return rows
[ドキュメント] def region_check_saturation(vd, vg, vth, factor=3.0): """飽和領域動作の条件をチェックします。 トランジスタが飽和領域で動作しているかどうかを判断するために、 指定されたVG、VD、Vth、および安全係数 (`factor`) を使用して `VD >= factor * (VG - Vth)` の条件を確認します。 VG-Vthが正でない場合、または条件が満たされない場合は警告が生成されます。 :param vd: ドレイン電圧 [V]。 :type vd: float :param vg: ゲート電圧 [V]。 :type vg: float :param vth: 閾値電圧 [V]。 :type vth: float :param factor: 飽和領域を保証するための安全係数。デフォルトは3.0。 :type factor: float :returns: 飽和領域チェックの結果を含む辞書。 `ok` (bool), `warning` (str), `VD_abs` (float), `VG_minus_Vth` (float), `ratio` (float), `criterion` (str) を含みます。 :rtype: dict """ vd_eff = abs(float(vd)) overdrive = float(vg) - float(vth) overdrive_pos = max(overdrive, 0.0) criterion = f"VD >= {factor:g}*(VG-Vth)" if overdrive_pos <= 0: return {'ok': False, 'warning': 'VG - Vth <= 0; mobility point is not clearly in the on-state.', 'VD_abs': vd_eff, 'VG_minus_Vth': overdrive, 'ratio': np.nan, 'criterion': criterion} ratio = vd_eff / overdrive_pos ok = ratio >= factor warning = '' if ok else f"Possible non-saturation: VD/(VG-Vth)={ratio:.3g} < {factor:g}." return {'ok': ok, 'warning': warning, 'VD_abs': vd_eff, 'VG_minus_Vth': overdrive, 'ratio': ratio, 'criterion': criterion}
[ドキュメント] def region_check_linear(vd, vg, vth, factor=3.0): """線形領域動作の条件をチェックします。 トランジスタが線形領域で動作しているかどうかを判断するために、 指定されたVG、VD、Vth、および安全係数 (`factor`) を使用して `VG - Vth >= factor * VD` の条件を確認します。 VDが正でない場合、VG-Vthが負の場合、または条件が満たされない場合は警告が生成されます。 :param vd: ドレイン電圧 [V]。 :type vd: float :param vg: ゲート電圧 [V]。 :type vg: float :param vth: 閾値電圧 [V]。 :type vth: float :param factor: 線形領域を保証するための安全係数。デフォルトは3.0。 :type factor: float :returns: 線形領域チェックの結果を含む辞書。 `ok` (bool), `warning` (str), `VD_abs` (float), `VG_minus_Vth` (float), `ratio` (float), `criterion` (str) を含みます。 :rtype: dict """ vd_eff = abs(float(vd)) overdrive = float(vg) - float(vth) criterion = f"VG-Vth >= {factor:g}*VD" if vd_eff <= 0: return {'ok': False, 'warning': 'VD <= 0; linear-region mobility is not meaningful.', 'VD_abs': vd_eff, 'VG_minus_Vth': overdrive, 'ratio': np.nan, 'criterion': criterion} ratio = overdrive / vd_eff ok = (overdrive > 0) and (ratio >= factor) if overdrive <= 0: warning = 'VG - Vth <= 0; mobility point is below threshold.' elif not ok: warning = f"Possible non-linear-region point: (VG-Vth)/VD={ratio:.3g} < {factor:g}." else: warning = '' return {'ok': ok, 'warning': warning, 'VD_abs': vd_eff, 'VG_minus_Vth': overdrive, 'ratio': ratio, 'criterion': criterion}
[ドキュメント] def classify_transfer_region(linear_check, saturation_check): """線形/飽和/中間領域の推奨を返します。 線形領域チェックと飽和領域チェックの結果に基づいて、 現在解析中の動作点が線形、飽和、またはどちらでもない中間領域のどれに属するかを分類します。 分類結果と、それに関連する推奨/警告メッセージを返します。 :param linear_check: `region_check_linear`関数からの結果辞書。 :type linear_check: dict :param saturation_check: `region_check_saturation`関数からの結果辞書。 :type saturation_check: dict :returns: 推奨される領域タイプと、関連する警告/説明メッセージのタプル。 :rtype: tuple[str, str] """ lin_ok = bool(linear_check.get('ok', False)) sat_ok = bool(saturation_check.get('ok', False)) if lin_ok and not sat_ok: return 'linear', 'Use linear-region mobility/Vth. Condition VG-Vth >= factor*VD is satisfied.' if sat_ok and not lin_ok: return 'saturation', 'Use saturation-region mobility/Vth. Condition VD >= factor*(VG-Vth) is satisfied.' if lin_ok and sat_ok: return 'ambiguous', 'Both linear and saturation checks passed at their own extraction points; inspect plots.' return 'intermediate', 'WARNING: neither linear nor saturation condition is sufficiently satisfied.'
[ドキュメント] def analyze_vg_core(df_full, vd_val, args, cox): """特定VDスライスのID-VGから、線形法と飽和法の両方でVth/移動度を抽出します。 ID-VGデータに基づいて、線形領域(最大相互コンダクタンス`gm`から)と 飽和領域(最大`d(sqrt(ID))/dVg`から)の閾値電圧 (Vth) と移動度を抽出します。 サブスレッショルドスイング (S) やオフ電流 (Ioff) も計算します。 抽出されたポイントの動作領域チェックも行い、推奨される抽出方法を提示します。 :param df_full: 全てのVG-ID測定データを含むDataFrame。 :type df_full: pandas.DataFrame :param vd_val: 解析対象のドレイン電圧 [V]。 :type vd_val: float :param args: コマンドライン引数を含むオブジェクト。`smooth_npoints`, `lsq_order`, `Imin`, `L`, `W`, `region_factor`, `ID_S` 属性を使用します。 :type args: argparse.Namespace :param cox: 単位面積あたりのゲート酸化膜容量 [F/cm^2]。 :type cox: float :returns: 特定VDスライスにおける詳細な解析結果を含む辞書。データが不十分な場合はNone。 :rtype: dict or None """ df_vd = df_full[np.isclose(df_full['VD'], vd_val, atol=1e-3)].copy() df_vd = select_sweep_segment(df_vd, 'VG', args.idx_vg, 'idx_vg', sort_after=True) if len(df_vd) < 5: return None win = valid_savgol_window(len(df_vd), args.smooth_npoints, args.lsq_order) vg_step = df_vd['VG'].diff().dropna().median() if not np.isfinite(vg_step) or vg_step == 0: vg_step = 1.0 df_vd['ID_abs_floor'] = df_vd['ID'].abs().clip(lower=args.Imin) df_vd['ID_smooth'] = savgol_filter(df_vd['ID_abs_floor'], win, 1) df_vd['ID_smooth'] = np.clip(df_vd['ID_smooth'], args.Imin, None) df_vd['logID'] = np.log10(df_vd['ID_smooth']) df_vd['sqrtID'] = np.sqrt(df_vd['ID_smooth']) df_vd['dlogID'] = savgol_filter(df_vd['logID'], win, args.lsq_order, deriv=1, delta=vg_step) df_vd['gm'] = savgol_filter(df_vd['ID_smooth'], win, args.lsq_order, deriv=1, delta=vg_step) df_vd['dsqrtID'] = savgol_filter(df_vd['sqrtID'], win, args.lsq_order, deriv=1, delta=vg_step) vd_eff = max(abs(vd_val), 1e-12) df_vd['muFE'] = (args.L / (args.W * cox * vd_eff)) * df_vd['gm'] df_vd['muSAT_prof'] = (2 * args.L / (args.W * cox)) * (df_vd['dsqrtID'] ** 2) df_vd['S_val'] = np.where(df_vd['dlogID'] > 1e-6, 1.0 / df_vd['dlogID'], np.nan) gm_valid = df_vd['gm'].replace([np.inf, -np.inf], np.nan).where(df_vd['gm'] > 0) idx_lin_slope_max = gm_valid.idxmax() if gm_valid.notna().any() else None if idx_lin_slope_max is not None: row_lin = df_vd.loc[idx_lin_slope_max] v_lin_slope = row_lin['VG'] vth_lin = v_lin_slope - row_lin['ID_smooth'] / row_lin['gm'] if row_lin['gm'] > 0 else np.nan mu_lin_slope = row_lin['muFE'] id_lin_slope = row_lin['ID_smooth'] gm_max = row_lin['gm'] else: v_lin_slope = vth_lin = mu_lin_slope = id_lin_slope = gm_max = np.nan dsqrt_valid = df_vd['dsqrtID'].replace([np.inf, -np.inf], np.nan).where(df_vd['dsqrtID'] > 0) idx_sat_slope_max = dsqrt_valid.idxmax() if dsqrt_valid.notna().any() else None if idx_sat_slope_max is not None: row_sat = df_vd.loc[idx_sat_slope_max] v_sat_slope = row_sat['VG'] vth_sat = v_sat_slope - row_sat['sqrtID'] / row_sat['dsqrtID'] if row_sat['dsqrtID'] > 0 else np.nan mu_sat_slope = row_sat['muSAT_prof'] id_sat_slope = row_sat['ID_smooth'] sqrtID_sat_slope = row_sat['sqrtID'] dsqrtID_max = row_sat['dsqrtID'] else: v_sat_slope = vth_sat = mu_sat_slope = id_sat_slope = sqrtID_sat_slope = dsqrtID_max = np.nan idx_mu_lin_max = df_vd['muFE'].replace([np.inf, -np.inf], np.nan).idxmax() idx_mu_sat_max = df_vd['muSAT_prof'].replace([np.inf, -np.inf], np.nan).idxmax() mu_lin_max = df_vd.loc[idx_mu_lin_max, 'muFE'] mu_sat_max = df_vd.loc[idx_mu_sat_max, 'muSAT_prof'] VG_mu_lin_max = df_vd.loc[idx_mu_lin_max, 'VG'] ID_mu_lin_max = df_vd.loc[idx_mu_lin_max, 'ID_smooth'] VG_mu_sat_max = df_vd.loc[idx_mu_sat_max, 'VG'] ID_mu_sat_max = df_vd.loc[idx_mu_sat_max, 'ID_smooth'] lin_region = region_check_linear(vd_eff, VG_mu_lin_max, vth_lin, args.region_factor) sat_region = region_check_saturation(vd_eff, VG_mu_sat_max, vth_sat, args.region_factor) recommended_method, recommended_warning = classify_transfer_region(lin_region, sat_region) if recommended_method == 'linear': vth_rec, mu_rec = vth_lin, mu_lin_max elif recommended_method == 'saturation': vth_rec, mu_rec = vth_sat, mu_sat_max else: vth_rec = np.nan mu_rec = np.nan vth_for_subthreshold = vth_rec if not np.isfinite(vth_for_subthreshold): vth_for_subthreshold = vth_sat if np.isfinite(vth_sat) else vth_lin off_mask = (df_vd['VG'] < vth_for_subthreshold - 5) & (df_vd['dlogID'].abs() < 0.1) if any(off_mask): ioff = df_vd[off_mask]['ID_smooth'].mean() idx_ioff = df_vd.loc[off_mask, 'ID_smooth'].idxmin() else: idx_ioff = df_vd['ID_smooth'].idxmin() ioff = df_vd.loc[idx_ioff, 'ID_smooth'] mask_s = (df_vd['VG'] < vth_for_subthreshold) & (df_vd['ID_smooth'] > ioff * 3) & (df_vd['dlogID'] > 0.05) df_s = df_vd[mask_s] s_min, vg_smin, id_smin, von, idx_smin = np.nan, np.nan, np.nan, np.nan, None if not df_s.empty: idx_smin = df_s['S_val'].idxmin() s_min = df_s.loc[idx_smin, 'S_val'] vg_smin = df_s.loc[idx_smin, 'VG'] id_smin = df_s.loc[idx_smin, 'ID_smooth'] von = vg_smin + s_min * (np.log10(args.Imin) - np.log10(id_smin)) idx_ids, row_ids = nearest_row_by_current(df_vd, args.ID_S) legacy_vth = vth_rec if np.isfinite(vth_rec) else vth_sat legacy_mu = mu_rec if np.isfinite(mu_rec) else mu_sat_max res = { 'VD': vd_val, 'Vth': legacy_vth, 'mu_max': legacy_mu, 'mu_type': recommended_method, 'recommended_method': recommended_method, 'recommended_warning': recommended_warning, 'recommended_Vth': vth_rec, 'recommended_mu': mu_rec, 'Vth_lin': vth_lin, 'v_lin_slope': v_lin_slope, 'ID_lin_slope': id_lin_slope, 'gm_max': gm_max, 'mu_lin_slope': mu_lin_slope, 'mu_lin_max': mu_lin_max, 'VG_mu_lin_max': VG_mu_lin_max, 'ID_mu_lin_max': ID_mu_lin_max, 'lin_region_ok': lin_region['ok'], 'lin_region_warning': lin_region['warning'], 'lin_region_ratio_VGminusVth_over_VD': lin_region['ratio'], 'lin_region_criterion': lin_region['criterion'], 'lin_region_VG_minus_Vth': lin_region['VG_minus_Vth'], 'Vth_sat': vth_sat, 'v_sat_slope': v_sat_slope, 'ID_sat_slope': id_sat_slope, 'sqrtID_sat_slope': sqrtID_sat_slope, 'dsqrtID_max': dsqrtID_max, 'mu_sat_slope': mu_sat_slope, 'mu_sat_max': mu_sat_max, 'VG_mu_sat_max': VG_mu_sat_max, 'ID_mu_sat_max': ID_mu_sat_max, 'sat_region_ok': sat_region['ok'], 'sat_region_warning': sat_region['warning'], 'sat_region_ratio_VD_over_VGminusVth': sat_region['ratio'], 'sat_region_criterion': sat_region['criterion'], 'sat_region_VG_minus_Vth': sat_region['VG_minus_Vth'], 'Ioff': ioff, 'Smin': s_min, 'VG_Smin': vg_smin, 'id_smin': id_smin, 'Von': von, 'VG_ID_S': row_ids['VG'] if row_ids is not None else np.nan, 'ID_S_target': args.ID_S, 'Imin_target': args.Imin, 'ID_S_val': row_ids['ID_smooth'] if row_ids is not None else np.nan, 'S_ID_S': row_ids['S_val'] if row_ids is not None else np.nan, 'savgol_window': win, 'vg_step': vg_step, 'idx_vg_selected': args.idx_vg, 'idx_lin_slope_max': idx_lin_slope_max, 'idx_sat_slope_max': idx_sat_slope_max, 'idx_slope_max': idx_sat_slope_max, 'idx_smin': idx_smin, 'idx_ids': idx_ids, 'idx_mu_lin_max': idx_mu_lin_max, 'idx_mu_sat_max': idx_mu_sat_max, 'idx_mu_max': idx_mu_lin_max if recommended_method == 'linear' else idx_mu_sat_max, 'idx_ioff': idx_ioff, 'df': df_vd, } res['analysis_points'] = build_analysis_points(res) return res
[ドキュメント] def annotate_vline(ax, x, label, ymin=None, ymax=None): """Matplotlibのプロットに垂直線とテキストアノテーションを追加します。 指定されたX座標 (`x`) に垂直線 (`axvline`) を引き、 その線の近くにテキストラベル (`label`) を回転させて配置します。 Y軸の範囲 (`ymin`, `ymax`) が指定されていない場合、現在のプロットのY軸範囲が使用されます。 :param ax: プロット対象のMatplotlib Axesオブジェクト。 :type ax: matplotlib.axes.Axes :param x: 垂直線を引くX座標。 :type x: float :param label: 垂直線の横に表示するテキストラベル。 :type label: str :param ymin: 垂直線が描画されるY軸の下限。Noneの場合、現在のY軸の下限が使用されます。 :type ymin: float or None :param ymax: 垂直線が描画されるY軸の上限。Noneの場合、現在のY軸の上限が使用されます。 :type ymax: float or None :returns: なし :rtype: None """ ax.axvline(x, linestyle=':', linewidth=1, alpha=0.8) if ymin is None or ymax is None: ymin, ymax = ax.get_ylim() ax.text(x, ymax, label, rotation=90, va='top', ha='right', fontsize=8)
[ドキュメント] def plot_idvg_quad(res, args): """伝達特性解析結果を 2x2 サブプロットとして可視化します。 ID-VGデータに基づいて計算された様々なデバイス特性(ID-VG曲線、線形抽出、飽和抽出、移動度プロファイル)を 2x2のサブプロットとして表示します。各プロットには、Vth、Smin、移動度最大値などの主要な解析ポイントが アノテーションとして表示されます。プロットはファイルに保存することも可能です。 :param res: `analyze_vg_core`関数によって生成された、単一VDスライスの解析結果を含む辞書。 :type res: dict :param args: コマンドライン引数を含むオブジェクト。`save_plot`および`plot_dir`属性を使用します。 :type args: argparse.Namespace :returns: 生成されたMatplotlibのFigureオブジェクト。 :rtype: matplotlib.figure.Figure """ df = res['df'] fig, axes = plt.subplots(2, 2, figsize=figsize_idvg_quad) fig.suptitle(f"n-ch Transfer Analysis (VD = {res['VD']} V)", fontsize=14) ax = axes[0, 0] ax.plot(df['VG'], df['ID_smooth'], '-', lw=2, label='smoothed ID') ax.set_yscale('log') ax.set_xlabel(r'$V_G$ [V]') ax.set_ylabel(r'$I_D$ [A]') ax.set_title('ID-VG / subthreshold') ax.grid(True, alpha=0.15) if np.isfinite(res['Smin']): v_p = np.linspace(res['Von'], res['VG_Smin'], 30) id_p = 10 ** (np.log10(res['id_smin']) + (v_p - res['VG_Smin']) / res['Smin']) ax.plot(v_p, id_p, '--', label=f"Smin={res['Smin']:.3g} V/dec") ax.scatter(res['VG_Smin'], res['id_smin'], marker='o', s=55, label=f"Smin VG={res['VG_Smin']:.2f}") ax.scatter(res['Von'], args.Imin, marker='x', s=70, label=f"Von={res['Von']:.2f} V") annotate_vline(ax, res['VG_Smin'], 'Smin') if np.isfinite(res['VG_ID_S']): ax.scatter(res['VG_ID_S'], res['ID_S_val'], marker='s', s=50, label=f"ID_S S={res['S_ID_S']:.3g}") annotate_vline(ax, res['VG_ID_S'], 'ID_S') if np.isfinite(res['Vth_lin']): annotate_vline(ax, res['Vth_lin'], f"Vth_lin={res['Vth_lin']:.2f}") if np.isfinite(res['Vth_sat']): annotate_vline(ax, res['Vth_sat'], f"Vth_sat={res['Vth_sat']:.2f}") ax.legend(fontsize='x-small') ax = axes[0, 1] ax.plot(df['VG'], df['ID_smooth'], '-', lw=2, label=r'$I_D$') if np.isfinite(res['Vth_lin']) and np.isfinite(res['gm_max']): vg_fit = np.linspace(min(res['Vth_lin'], df['VG'].min()), df['VG'].max(), 80) id_fit = res['gm_max'] * (vg_fit - res['Vth_lin']) ax.plot(vg_fit, id_fit, '--', label=f"linear tangent: Vth={res['Vth_lin']:.2f} V") ax.scatter(res['v_lin_slope'], res['ID_lin_slope'], marker='^', s=60, label=f"max gm VG={res['v_lin_slope']:.2f}") annotate_vline(ax, res['Vth_lin'], 'Vth_lin') annotate_vline(ax, res['v_lin_slope'], 'max gm') txt = (f"linear check at mu_lin max:\n" f"(VG-Vth)/VD={res['lin_region_ratio_VGminusVth_over_VD']:.3g}\n" f"criterion: {res['lin_region_criterion']}") if not res['lin_region_ok']: txt = 'WARNING\n' + txt ax.text(0.02, 0.98, txt, transform=ax.transAxes, va='top', ha='left', fontsize=8, bbox=dict(boxstyle='round', alpha=0.15)) ax.set_xlabel(r'$V_G$ [V]') ax.set_ylabel(r'$I_D$ [A]') ax.set_title('Linear extraction: ID-VG tangent') ax.legend(fontsize='x-small') ax.grid(True, alpha=0.15) ax = axes[1, 0] ax.plot(df['VG'], df['sqrtID'], '-', lw=2, label=r'$\sqrt{I_D}$') if np.isfinite(res['Vth_sat']) and np.isfinite(res['dsqrtID_max']): vg_fit = np.linspace(min(res['Vth_sat'], df['VG'].min()), df['VG'].max(), 80) ax.plot(vg_fit, res['dsqrtID_max'] * (vg_fit - res['Vth_sat']), '--', label=f"sat tangent: Vth={res['Vth_sat']:.2f} V") ax.scatter(res['v_sat_slope'], res['sqrtID_sat_slope'], marker='^', s=60, label=f"max slope VG={res['v_sat_slope']:.2f}") annotate_vline(ax, res['Vth_sat'], 'Vth_sat') annotate_vline(ax, res['v_sat_slope'], 'max slope') txt = (f"sat check at mu_sat max:\n" f"VD/(VG-Vth)={res['sat_region_ratio_VD_over_VGminusVth']:.3g}\n" f"criterion: {res['sat_region_criterion']}") if not res['sat_region_ok']: txt = 'WARNING\n' + txt ax.text(0.02, 0.98, txt, transform=ax.transAxes, va='top', ha='left', fontsize=8, bbox=dict(boxstyle='round', alpha=0.15)) ax.set_xlabel(r'$V_G$ [V]') ax.set_ylabel(r'$\sqrt{I_D}$ [A$^{0.5}$]') ax.set_title('Saturation extraction: sqrt(ID)-VG tangent') ax.legend(fontsize='x-small') ax.grid(True, alpha=0.15) ax = axes[1, 1] ax.plot(df['VG'], df['muFE'], '-', lw=2, label=r'$\mu_{lin}$ from $dI_D/dV_G$') ax.plot(df['VG'], df['muSAT_prof'], '--', lw=2, label=r'$\mu_{sat}$ from $d\sqrt{I_D}/dV_G$') ax.scatter(res['VG_mu_lin_max'], res['mu_lin_max'], marker='o', s=60, label=f"mu_lin max={res['mu_lin_max']:.3g}") ax.scatter(res['VG_mu_sat_max'], res['mu_sat_max'], marker='s', s=60, label=f"mu_sat max={res['mu_sat_max']:.3g}") if np.isfinite(res['v_lin_slope']): ax.scatter(res['v_lin_slope'], res['mu_lin_slope'], marker='^', s=55, label=f"mu_lin_slope={res['mu_lin_slope']:.3g}") if np.isfinite(res['v_sat_slope']): ax.scatter(res['v_sat_slope'], res['mu_sat_slope'], marker='v', s=55, label=f"mu_sat_slope={res['mu_sat_slope']:.3g}") if np.isfinite(res['Vth_lin']): annotate_vline(ax, res['Vth_lin'], 'Vth_lin') if np.isfinite(res['Vth_sat']): annotate_vline(ax, res['Vth_sat'], 'Vth_sat') txt = f"recommended: {res['recommended_method']}\n{res['recommended_warning']}" if res['recommended_method'] in ('intermediate', 'ambiguous'): txt = 'WARNING\n' + txt ax.text(0.02, 0.98, txt, transform=ax.transAxes, va='top', ha='left', fontsize=8, bbox=dict(boxstyle='round', alpha=0.15)) ax.set_xlabel(r'$V_G$ [V]') ax.set_ylabel(r'Mobility [cm$^2$/Vs]') ax.set_title('Mobility profiles: linear and saturation formulas') ax.legend(fontsize='x-small') ax.grid(True, alpha=0.15) fig.tight_layout() if args.save_plot: os.makedirs(args.plot_dir, exist_ok=True) fname = plot_path(args, f"idvg_VD_{res['VD']:g}".replace('.', 'p')) fig.savefig(fname, dpi=200, bbox_inches='tight') print(f' plot saved: {fname}') return fig
plot_idvg_triple = plot_idvg_quad
[ドキュメント] def interpolate_id_at_vd(df_vg, vd_target): """単一のVG出力曲線内で、要求されたVD値におけるIDの絶対値を線形補間します。 与えられたデータフレーム (`df_vg`) が特定のVGでのID-VDデータを含んでいると仮定し、 `vd_target`における`|ID|`の絶対値を線形補間によって推定します。 `vd_target`が測定範囲外の場合、またはデータが不足している場合は`np.nan`を返します。 補間は`VD`と`ID`列をソートした後に行われます。 :param df_vg: 単一のVG値におけるID-VDデータを含むDataFrame。`VD`と`ID`列が必要です。 :type df_vg: pandas.DataFrame :param vd_target: 補間したい目標ドレイン電圧 [V]。 :type vd_target: float :returns: `vd_target`における補間された`|ID|`の絶対値。データが利用できない場合や範囲外の場合は`np.nan`。 :rtype: float """ if df_vg.empty or not np.isfinite(vd_target): return np.nan tmp = df_vg[["VD", "ID"]].dropna().sort_values("VD") if tmp.empty: return np.nan vd = tmp["VD"].to_numpy(dtype=float) id_abs = np.abs(tmp["ID"].to_numpy(dtype=float)) if vd_target < np.nanmin(vd) or vd_target > np.nanmax(vd): return np.nan return float(np.interp(vd_target, vd, id_abs))
[ドキュメント] def analyze_idvd_logic(df, args, cox): """TFTの出力特性 (ID-VD) を解析し、デバイスの線形領域特性と移動度を評価します。 入力されたID-VDデータフレームから、以下の解析を実行します: 1. 最大のVD値における伝達特性スライス (`df_full`) を用いて、参照閾値電圧 (`vth_ref`) を抽出します。 2. 各VG値におけるID-VD曲線に対して、線形領域(低いVD)での伝達コンダクタンス (gd) を線形回帰で計算します。 3. 低いVDスライスにおけるID-VGデータから、相互コンダクタンス (gm) とS値を計算します。 4. 抽出されたgdとgm、および`vth_ref`を用いて、実効移動度 (mu_eff) と線形移動度 (mu_lin) を導出します。 5. 各移動度抽出点に対して線形領域条件のチェック (`region_check_linear`) を行い、警告情報を記録します。 6. 解析結果の概要をコンソールに出力し、出力曲線と移動度プロットを生成します。 :param df: ID-VDデータを含むDataFrame。`VG`, `VD`, `ID`列が必要です。 :type df: pandas.DataFrame :param args: コマンドライン引数を含むオブジェクト。 `idx_vg`, `idx_vd`, `Imin`, `L`, `W`, `region_factor`, `save_plot`, `plot_dir` 属性を使用します。 :type args: argparse.Namespace :param cox: 単位面積あたりのゲート酸化膜容量 [F/cm^2]。 :type cox: float :returns: 各VGにおける解析結果の辞書リストと、生成されたMatplotlibのFigureオブジェクトのタプル。 有効なデータがない場合は([], None)を返します。 :rtype: tuple[list[dict], matplotlib.figure.Figure or None] """ max_vd = df['VD'].max() res_v_ref = analyze_vg_core(df, max_vd, args, cox) vth_ref = res_v_ref['Vth'] if res_v_ref else 0.0 positive_vd = sorted(df[df['VD'] > 0]['VD'].unique()) if not positive_vd: print('WARNING: no positive VD data found for n-ch output analysis.') return [], None low_vd = positive_vd[0] df_low = df[np.isclose(df['VD'], low_vd, atol=1e-3)].copy() df_low = select_sweep_segment(df_low, 'VG', args.idx_vg, 'idx_vg', sort_after=True) df_low['gm'] = df_low['ID'].abs().diff() / df_low['VG'].diff() df_low['logID'] = np.log10(df_low['ID'].abs().clip(lower=args.Imin)) df_low['S_val'] = 1.0 / (df_low['logID'].diff() / df_low['VG'].diff()) summary, unique_vgs = [], sorted(df['VG'].unique()) fig, axes = plt.subplots(1, 2, figsize=(15, 6)) print(f"\n{'=' * 20} ID-VD OUTPUT ANALYSIS {'=' * 20}") print(f"Using Vth_ref = {vth_ref:.4g} V from VD={max_vd:.4g} V transfer-like slice") print(f"Low-VD slice for gm = {low_vd:.4g} V") print(f"Selected sweep segments: idx_vg={args.idx_vg}, idx_vd={args.idx_vd}") for vg in unique_vgs: df_vg_all = df[np.isclose(df['VG'], vg, atol=1e-3)].copy() df_vg = select_sweep_segment(df_vg_all, 'VD', args.idx_vd, 'idx_vd', sort_after=True) if len(df_vg) < 3: continue fit_df = df_vg[df_vg['VD'] <= low_vd + 2] if len(fit_df) < 2: continue gd = LinearRegression().fit(fit_df['VD'].values.reshape(-1, 1), fit_df['ID'].abs().values).coef_[0] gm_row = df_low[np.isclose(df_low['VG'], vg, atol=1e-3)] gm = gm_row['gm'].values[0] if not gm_row.empty else 0.0 denom = max(0.1, vg - vth_ref) mue = (gd * args.L) / (args.W * cox * denom) mul = (gm * args.L) / (args.W * cox * low_vd) s_val = gm_row['S_val'].values[0] if not gm_row.empty else np.nan lin_region = region_check_linear(low_vd, vg, vth_ref, args.region_factor) summary.append({'VG': vg, 'gd': gd, 'gm': gm, 'mu_eff': mue, 'mu_lin': mul, 'S_val': s_val, 'Vth_ref': vth_ref, 'low_VD_for_gm': low_vd, 'fit_VD_max_for_gd': low_vd + 2, 'linear_region_ok': lin_region['ok'], 'linear_region_warning': lin_region['warning'], 'linear_region_ratio_VGminusVth_over_VD': lin_region['ratio'], 'linear_region_criterion': lin_region['criterion'], 'VG_minus_Vth': lin_region['VG_minus_Vth'], 'idx_vd_selected': args.idx_vd, 'idx_vg_selected_for_gm': args.idx_vg}) axes[0].plot(df_vg['VD'], df_vg['ID'].abs(), alpha=0.45) print(f" VG={vg:8.3g} V | gd={gd:10.4e} A/V | gm={gm:10.4e} A/V | " f"mu_eff={mue:10.4g} | mu_lin={mul:10.4g} | S={s_val:10.4g} | " f"linear OK={lin_region['ok']} ratio={(lin_region['ratio'] if np.isfinite(lin_region['ratio']) else np.nan):.4g}") if not lin_region['ok']: print(f" WARNING: {lin_region['warning']} criterion: {lin_region['criterion']}") sdf = pd.DataFrame(summary) if not sdf.empty: # Saturation-boundary markers on output curves: VD_sat = VG - Vth_ref. vd_sat_list, id_sat_list = [], [] for vg in sdf['VG'].to_numpy(dtype=float): vd_sat = vg - vth_ref df_vg_all = df[np.isclose(df['VG'], vg, atol=1e-3)].copy() df_vg = select_sweep_segment(df_vg_all, 'VD', args.idx_vd, 'idx_vd', sort_after=True) id_sat = interpolate_id_at_vd(df_vg, vd_sat) vd_sat_list.append(vd_sat) id_sat_list.append(id_sat) sdf['VD_sat_boundary'] = vd_sat_list sdf['ID_at_VD_sat_boundary'] = id_sat_list for i, row in sdf.iterrows(): summary[i]['VD_sat_boundary'] = row['VD_sat_boundary'] summary[i]['ID_at_VD_sat_boundary'] = row['ID_at_VD_sat_boundary'] good_boundary = sdf[np.isfinite(sdf['ID_at_VD_sat_boundary']) & (sdf['VD_sat_boundary'] >= 0)] if not good_boundary.empty: axes[0].plot(good_boundary['VD_sat_boundary'], good_boundary['ID_at_VD_sat_boundary'], '^--', lw=1.2, ms=6, label=r'saturation boundary $V_D=V_G-V_{th}$') axes[0].set_xlabel(r'$V_D$ [V]') axes[0].set_ylabel(r'$|I_D|$ [A] (linear scale)') axes[0].set_title('Output curves') axes[0].legend(fontsize='x-small') axes[0].grid(True, alpha=0.15) axes[1].plot(sdf['VG'], sdf['mu_eff'], 'o-', label='mu_eff from gd') axes[1].plot(sdf['VG'], sdf['mu_lin'], 's--', label='mu_lin from gm') axes[1].axvline(vth_ref, linestyle=':', linewidth=1, label='Vth_ref') bad = sdf[~sdf['linear_region_ok']] if 'linear_region_ok' in sdf.columns else pd.DataFrame() if not bad.empty: axes[1].scatter(bad['VG'], bad['mu_lin'], marker='x', s=75, label='linear-region warning') axes[1].text(0.02, 0.98, f"linear check: VG-Vth >= {args.region_factor:g}*VD_low", transform=axes[1].transAxes, va='top', ha='left', fontsize=8, bbox=dict(boxstyle='round', alpha=0.15)) axes[1].set_xlabel(r'$V_G$ [V]') axes[1].set_ylabel(r'Mobility [cm$^2$/Vs]') axes[1].legend() axes[1].grid(True, alpha=0.15) fig.tight_layout() if args.save_plot: os.makedirs(args.plot_dir, exist_ok=True) fname = plot_path(args, 'idvd_output_analysis') fig.savefig(fname, dpi=200, bbox_inches='tight') print(f' plot saved: {fname}') return summary, fig
[ドキュメント] def make_summary_dataframe(t_summary): """伝達特性解析結果のリストから、Excel出力用のPandas DataFrameを作成します。 `t_summary`リスト内の各解析結果辞書から、Excelサマリーシートに適した 主要なパラメータを抽出します。 元のデータフレーム (`df`) や詳細な分析ポイント (`analysis_points`)、 および内部的なインデックス (`idx_`で始まるキー) は除外されます。 結果はPandas DataFrameとして整形され、Excelへのエクスポートに適した形式で提供されます。 :param t_summary: 各VDスライスでの伝達特性解析結果を含む辞書のリスト。 :type t_summary: list[dict] :returns: 主要な解析結果パラメータを含むPandas DataFrame。 :rtype: pandas.DataFrame """ rows = [] for res in t_summary: row = {k: v for k, v in res.items() if k not in ('df', 'analysis_points') and not str(k).startswith('idx_')} rows.append(row) return pd.DataFrame(rows)
[ドキュメント] def run_analysis(args): """スクリプトのメイン実行ロジックをカプセル化します。 この関数は、コマンドライン引数をパースし、ゲート酸化膜容量 (Cox) を計算します。 選択された解析モード (`read`, `analyze_idvg`, `analyze_idvd`, `all`) に応じて、 対応するデータ読み込み、解析、プロット生成の関数を呼び出します。 すべての結果(生データ、平滑化データ、解析サマリー、詳細分析ポイント)は集約され、 最終的に単一のExcelレポートファイルとPNGプロットとして出力されます。 プロットは`--show_plot`が指定されている場合、インタラクティブに表示されます。 :param args: コマンドライン引数を含む `argparse.Namespace` オブジェクト。 :type args: argparse.Namespace :returns: なし。解析結果はファイルシステムに出力されます。 :rtype: None """ # args is prepared by main(): output paths and logging are already configured. cox = calculate_cox(args.dg, args.epsg) t_summary, t_data, t_points, o_summary = [], [], [], [] read_tables, read_summary = {}, [] figures = [] print('TFT analysis mode: n-channel dedicated') print(f'Cox = {cox:.6e} F/cm^2 (dg={args.dg:g} nm, epsg={args.epsg:g})') if args.reverse_vg: print('VG sign reversal is enabled after data loading.') if args.mode == 'read': figs, read_tables, read_summary = run_read_mode(args) figures.extend(figs) if args.mode in ['analyze_idvg', 'all']: df_vg = detect_and_load(args.infile_vg, reverse_vg=args.reverse_vg) if df_vg is not None: print(f"\n{'=' * 20} DETAILED TRANSFER ANALYSIS {'=' * 20}") for vd in sorted(df_vg['VD'].unique()): if vd <= 0: print(f'Skip VD={vd:g} V because n-ch analysis expects positive VD.') continue res = analyze_vg_core(df_vg, vd, args, cox) if res: print_transfer_report(res) t_summary.append(res) res['df']['VD_label'] = vd t_data.append(res['df']) t_points.extend(res['analysis_points']) figures.append(plot_idvg_triple(res, args)) if args.mode in ['analyze_idvd', 'all']: df_vd = detect_and_load(args.infile_vd, reverse_vg=args.reverse_vg) if df_vd is not None: o_summary, fig = analyze_idvd_logic(df_vd, args, cox) if fig is not None: figures.append(fig) with pd.ExcelWriter(args.out_excel) as writer: pd.DataFrame([{ 'analysis_assumption': 'n-channel TFT; positive VD; use --reverse_vg for p-channel preprocessing', 'L_um': args.L, 'W_um': args.W, 'dg_nm': args.dg, 'epsg': args.epsg, 'Cox_F_per_cm2': cox, 'ID_S_A': args.ID_S, 'Imin_A': args.Imin, 'smooth_npoints_requested': args.smooth_npoints, 'lsq_order': args.lsq_order, 'region_factor': args.region_factor, 'idx_vg': args.idx_vg, 'idx_vd': args.idx_vd, 'plot_dir': args.plot_dir if args.save_plot else '', }]).to_excel(writer, sheet_name='Analysis_Settings', index=False) if read_summary: pd.DataFrame(read_summary).to_excel(writer, sheet_name='Read_Summary', index=False) for tag, rdf in read_tables.items(): sheet = 'Read_Data_' + tag.upper() rdf.to_excel(writer, sheet_name=sheet[:31], index=False) if t_summary: make_summary_dataframe(t_summary).to_excel(writer, sheet_name='Summary_Transfer', index=False) if t_points: pd.DataFrame(t_points).to_excel(writer, sheet_name='Analysis_Points', index=False) if t_data: pd.concat(t_data).to_excel(writer, sheet_name='Data_Transfer_VG_Dep', index=False) if o_summary: pd.DataFrame(o_summary).to_excel(writer, sheet_name='Data_Output_VG_Dep', index=False) print(f'Report saved to {args.out_excel}') # Excel and PNG files are saved before this interactive display. if args.show_plot and figures: plt.show() else: for fig in figures: plt.close(fig)
[ドキュメント] def main(): """プログラムのエントリポイントです。 この関数は、コマンドライン引数をパースし、Excel、PNG、ログファイルの 出力パスを準備します。標準出力と標準エラー出力をコンソールとログファイルの両方に 出力するように設定した後、`run_analysis`関数を呼び出して主要な解析ロジックを実行します。 解析終了後、Tee機能は解除されます。 :returns: なし :rtype: None """ args = get_args() args = prepare_output_paths(args) os.makedirs(args.output_dir, exist_ok=True) original_stdout = sys.stdout original_stderr = sys.stderr with open(args.log_file, 'w', encoding='utf-8') as log_fp: sys.stdout = Tee(original_stdout, log_fp) sys.stderr = Tee(original_stderr, log_fp) try: print(f'Log file: {args.log_file}') print(f'Output directory: {args.output_dir}') print(f'Output stem: {args.output_stem}') run_analysis(args) finally: sys.stdout = original_stdout sys.stderr = original_stderr
if __name__ == '__main__': main()