tftanalyze.py ダウンロード/コピー
tftanalyze.py
tftanalyze.py
1"""
2概要:
3 TFT n-チャネルトランジスタの電気特性を解析するツールです。
4詳細説明:
5 このスクリプトは、TFT (Thin-Film Transistor) のn-チャネルデバイスにおける
6 I-V特性(伝達特性 (ID-VG) および出力特性 (ID-VD))データを読み込み、
7 解析し、結果をExcelレポートとPNGプロットとして出力します。
8 コマンドラインからの使用例は以下の通りです。
9 python tftanalyze.py --mode all --infile_vg transfer.csv --infile_vd output.csv
10
11主な機能:
12 CSV形式の測定データを自動的にエンコーディングを検出して読み込みます。
13 伝達特性データから閾値電圧 (Vth)、移動度 (Mobility)、サブスレッショルドスイング (S) などの
14 主要なデバイスパラメータを抽出します。
15 出力特性データから線形領域のコンダクタンス (gd) や移動度 (mu_lin, mu_eff) を評価します。
16 サビツキー・ゴレイフィルターを用いたデータの平滑化をサポートします。
17 解析結果をインタラクティブなグラフ表示とPNGファイルとして保存します。
18 全ての解析結果と生データ、平滑化データをExcelファイルに集約して出力します。
19関連リンク:
20 tftanalyze_usage
21"""
22import os
23import sys
24import argparse
25import csv
26import chardet
27from pathlib import Path
28import numpy as np
29from scipy import constants
30from scipy.signal import savgol_filter
31import pandas as pd
32import matplotlib.pyplot as plt
33from sklearn.linear_model import LinearRegression
34
35
36# --- 物理定数 ---
37EPS0 = constants.epsilon_0
38
39
40figsize_idvg_quad = (10, 8)
41
42def get_args():
43 """概要:
44 コマンドライン引数をパースします。
45 詳細説明:
46 この関数は、TFT解析スクリプトに必要な全てのコマンドライン引数を定義し、
47 ユーザーが指定した引数をパースして返します。
48 引数には、入力ファイルパス、解析モード、TFTデバイスの物理的寸法、
49 誘電体定数、電流の閾値、平滑化パラメータ、プロット表示・保存設定などが含まれます。
50 戻り値:
51 :returns: パースされた引数を含む argparse.Namespace オブジェクト。
52 :rtype: argparse.Namespace
53 """
54 parser = argparse.ArgumentParser(description='TFT n-channel Transfer/Output Analysis & Excel Tool (detail rev.)')
55 parser.add_argument('--infile_vg', default='TFT_Vg-Id_STD-ide3 [AS220518TFT-anneal(454) ; 2022_05_25 18_00_33].csv')
56 parser.add_argument('--infile_vd', default='TFT_Vd-Id-ide [AS220518TFT-anneal(455) ; 2022_05_25 18_01_11].csv')
57 parser.add_argument('--mode', choices=['read', 'analyze_idvg', 'analyze_idvd', 'all'], default='all')
58 parser.add_argument('--out_excel', default=None, help='Output Excel file. If omitted, a mode-dependent filename is used.')
59 parser.add_argument('--L', type=float, default=50.0, help='Channel length [um]')
60 parser.add_argument('--W', type=float, default=300.0, help='Channel width [um]')
61 parser.add_argument('--dg', type=float, default=150.0, help='Gate insulator thickness [nm]')
62 parser.add_argument('--epsg', type=float, default=3.9, help='Relative dielectric constant of gate insulator')
63 parser.add_argument('--ID_S', type=float, default=1e-9, help='Reference drain current for S extraction [A]')
64 parser.add_argument('--Imin', type=float, default=1e-15, help='Minimum current floor for log analysis [A]')
65 parser.add_argument('--smooth_npoints', type=int, default=5)
66 parser.add_argument('--lsq_order', type=int, default=2)
67 parser.add_argument('--show_plot', action='store_true', default=True, help='Show plots interactively (default: True)')
68 parser.add_argument('--no_show_plot', action='store_false', dest='show_plot', help='Do not show plots interactively')
69 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')
70 parser.add_argument('--save_plot', action='store_true', default=True, help='Save analysis plots as PNG files (default: True)')
71 parser.add_argument('--no_save_plot', action='store_false', dest='save_plot', help='Do not save analysis plots')
72 parser.add_argument('--plot_dir', default='tft_analysis_plots')
73 parser.add_argument('--reverse_vg', action='store_true', help='Reverse VG sign after loading data. Intended for p-channel data preprocessing.')
74 parser.add_argument('--idx_vg', type=int, default=0, help='Sweep segment index for VG-swept data. Incremented when VG sweep direction changes.')
75 parser.add_argument('--idx_vd', type=int, default=0, help='Sweep segment index for VD-swept data. Incremented when VD sweep direction changes.')
76 parser.add_argument('--read_smooth_domain', choices=['log', 'linear'], default='log',
77 help='Smoothing domain for mode=read. For output ID-VD preview, linear smoothing/linear-y plotting is forced.')
78 parser.add_argument('--read_keep_edge_raw', action='store_true', default=True,
79 help='In mode=read, keep edge points raw instead of applying Savitzky-Golay near endpoints (default: True).')
80 parser.add_argument('--read_smooth_edges', action='store_false', dest='read_keep_edge_raw',
81 help='Apply Savitzky-Golay smoothing also near endpoints.')
82 return parser.parse_args()
83
84
85def calculate_cox(dg_nm, epsg):
86 """概要:
87 ゲート酸化膜容量 (Cox) を計算します。
88 詳細説明:
89 ゲート絶縁膜の厚さ (ナノメートル単位) と比誘電率から、
90 単位面積あたりのゲート酸化膜容量 (F/cm^2) を計算します。
91 物理定数として真空の誘電率 (EPS0) を使用します。
92 引数:
93 :param dg_nm: ゲート絶縁膜の厚さ [nm]。
94 :type dg_nm: float
95 :param epsg: ゲート絶縁膜の比誘電率。
96 :type epsg: float
97 戻り値:
98 :returns: 単位面積あたりのゲート酸化膜容量 [F/cm^2]。
99 :rtype: float
100 """
101 dg_m = dg_nm * 1e-9
102 # F/m^2 -> F/cm^2
103 return (epsg * EPS0 / dg_m) * 1e-4
104
105
106def default_excel_name(mode):
107 """概要:
108 指定された解析モードに応じたデフォルトのExcelファイル名を返します。
109 詳細説明:
110 異なる解析モード('read', 'analyze_idvg', 'analyze_idvd', 'all')に対して、
111 それぞれ適切なデフォルトのExcelファイル名を決定します。
112 指定されたモードが辞書にない場合は、汎用的なレポートファイル名を返します。
113 引数:
114 :param mode: 解析モードを示す文字列。
115 :type mode: str
116 戻り値:
117 :returns: デフォルトのExcelファイル名。
118 :rtype: str
119 """
120 names = {
121 'read': 'tft_read_data.xlsx',
122 'analyze_idvg': 'tft_analysis_idvg.xlsx',
123 'analyze_idvd': 'tft_analysis_idvd.xlsx',
124 'all': 'tft_analysis_all.xlsx',
125 }
126 return names.get(mode, 'tft_analysis_report.xlsx')
127
128
129class Tee:
130 """概要:
131 stdout/stderr をコンソールとログファイルへ同時出力する簡単な Tee。
132 詳細説明:
133 このクラスは、複数のファイルライクオブジェクトに書き込み操作をミラーリングするために使用されます。
134 例えば、標準出力への書き込みと同時にログファイルへの書き込みを行う場合に便利です。
135 引数:
136 :param streams: データを書き込む対象となる一つ以上のファイルライクオブジェクト。
137 :type streams: file-like objects
138 """
139 def __init__(self, *streams):
140 """概要:
141 Teeオブジェクトを初期化します。
142 引数:
143 :param streams: データを書き込む対象となるファイルライクオブジェクトの可変長引数。
144 :type streams: file-like objects
145 戻り値:
146 :returns: なし
147 :rtype: None
148 """
149 self.streams = streams
150
151 def write(self, data):
152 """概要:
153 指定されたデータを全てのストリームに書き込みます。
154 詳細説明:
155 このメソッドは、コンストラクタで指定された各ストリームに対して、
156 入力されたデータを書き込み、その後すぐにストリームをフラッシュします。
157 引数:
158 :param data: 書き込むデータ。
159 :type data: str
160 戻り値:
161 :returns: なし
162 :rtype: None
163 """
164 for stream in self.streams:
165 stream.write(data)
166 stream.flush()
167
168 def flush(self):
169 """概要:
170 全てのストリームをフラッシュします。
171 詳細説明:
172 このメソッドは、コンストラクタで指定された各ストリームに対して、
173 バッファリングされている全てのデータを強制的に書き出します。
174 戻り値:
175 :returns: なし
176 :rtype: None
177 """
178 for stream in self.streams:
179 stream.flush()
180
181
182def _existing_or_first_path(*paths):
183 """概要:
184 出力名の基準にする入力ファイルを選びます。存在確認はゆるく行います。
185 詳細説明:
186 複数のファイルパスが与えられた場合、最初に空でないパス、
187 または提供されたパスのリストの最初のパスをPathオブジェクトとして返します。
188 ファイルシステムの存在チェックは行いません。
189 引数:
190 :param paths: 検査するファイルパスの可変引数。
191 :type paths: str
192 戻り値:
193 :returns: 基準として選択されたファイルパスのPathオブジェクト。
194 :rtype: pathlib.Path
195 """
196 for path in paths:
197 if path:
198 return Path(path)
199 return Path('tft_analysis')
200
201
202def prepare_output_paths(args):
203 """概要:
204 Excel/PNG/log の保存先を入力ファイルと同じディレクトリにそろえます。
205 詳細説明:
206 入力ファイルパス(args.infile_vg, args.infile_vd)を基準に、
207 Excelレポート、PNGプロット、ログファイルの出力ディレクトリとファイル名を決定し、
208 argsオブジェクトに設定します。これにより、全ての出力が関連する入力ファイルの近くに集約されます。
209 引数:
210 :param args: コマンドライン引数を含む argparse.Namespace オブジェクト。
211 infile_vg, infile_vd, mode, out_excel 属性を使用します。
212 :type args: argparse.Namespace
213 戻り値:
214 :returns: 出力パスが設定された argparse.Namespace オブジェクト。
215 :rtype: argparse.Namespace
216 """
217 if args.mode == 'analyze_idvd':
218 ref = _existing_or_first_path(args.infile_vd, args.infile_vg)
219 else:
220 ref = _existing_or_first_path(args.infile_vg, args.infile_vd)
221 ref = ref.expanduser()
222 out_dir = ref.parent if str(ref.parent) not in ('', '.') else Path.cwd()
223 out_dir = out_dir.resolve()
224 stem = ref.stem if ref.stem else 'tft_analysis'
225 args.output_dir = str(out_dir)
226 args.output_stem = stem
227 args.plot_dir = str(out_dir)
228
229 if args.out_excel is None:
230 args.out_excel = str(out_dir / f'{stem}_{args.mode}.xlsx')
231 else:
232 user_name = Path(args.out_excel).name
233 user_stem = Path(user_name).stem
234 user_suffix = Path(user_name).suffix or '.xlsx'
235 if stem not in user_stem:
236 user_name = f'{stem}_{user_stem}{user_suffix}'
237 args.out_excel = str(out_dir / user_name)
238
239 args.log_file = str(out_dir / f'{stem}_{args.mode}.log')
240 return args
241
242
243def plot_path(args, name):
244 """概要:
245 入力stemつきPNG保存パスを作成します。
246 詳細説明:
247 コマンドライン引数 (args) から取得した出力ステムとプロットディレクトリを基に、
248 指定された名前 (name) でPNGファイルの完全な保存パスを構築します。
249 引数:
250 :param args: コマンドライン引数を含む argparse.Namespace オブジェクト。
251 output_stem および plot_dir 属性を使用します。
252 :type args: argparse.Namespace
253 :param name: 保存するPNGファイルの名前(拡張子なし)。
254 :type name: str
255 戻り値:
256 :returns: 生成されたPNGファイルの完全なパス。
257 :rtype: str
258 """
259 stem = getattr(args, 'output_stem', 'tft_analysis')
260 out_dir = Path(getattr(args, 'plot_dir', '.'))
261 return str(out_dir / f'{stem}_{name}.png')
262
263def savgol_center_only(y, win, order, keep_edge_raw=True):
264 """概要:
265 サビツキー・ゴレイフィルターを適用し、オプションで端点付近の生データを保持します。
266 詳細説明:
267 scipy.signal.savgol_filter は端点付近を外挿/補間することがあります。
268 TFT出力曲線では、VD=0側の点数が少ないため、端点平滑化が系統的なずれのように見えることがあります。
269 keep_edge_raw=True の場合、完全な中心ウィンドウを持つ点のみが平滑化された値に置き換えられ、
270 端点付近のデータは元のクリップされたデータのまま維持されます。
271 データ長が短すぎる場合、またはウィンドウ長が不正な場合は、元のデータが返され、
272 平滑化が適用されなかったことを示すブール配列が返されます。
273 引数:
274 :param y: 平滑化するデータ配列。
275 :type y: numpy.ndarray or list
276 :param win: サビツキー・ゴレイフィルターのウィンドウ長。奇数である必要があります。
277 :type win: int
278 :param order: フィルターの多項式の次数。
279 :type order: int
280 :param keep_edge_raw: Trueの場合、完全な中心ウィンドウを持たない端点付近のデータを生データのまま保持します。
281 :type keep_edge_raw: bool
282 戻り値:
283 :returns: 平滑化されたデータ配列と、各点が平滑化に使用されたかを示すブール配列のタプル。
284 :rtype: tuple[numpy.ndarray, numpy.ndarray]
285 """
286 y = np.asarray(y, dtype=float)
287 if len(y) < 3 or not np.isfinite(win):
288 return y.copy(), np.zeros(len(y), dtype=bool)
289 win_i = int(win)
290 if win_i < 3 or win_i > len(y):
291 return y.copy(), np.zeros(len(y), dtype=bool)
292 order_i = min(int(order), win_i - 1)
293 ys = savgol_filter(y, win_i, order_i, mode='interp')
294 used = np.ones(len(y), dtype=bool)
295 if keep_edge_raw:
296 half = win_i // 2
297 used[:] = False
298 if len(y) > 2 * half:
299 used[half:len(y)-half] = True
300 out = y.copy()
301 out[used] = ys[used]
302 return out, used
303 return ys, used
304
305
306def add_read_columns_grouped(df, xcol, groupcol, args):
307 """概要:
308 データフレームに電流のクリッピングと平滑化された電流の列を追加します。
309 詳細説明:
310 この関数は、読み込み/プレビューモードのために、各グループ(またはデータ全体)に対して、
311 以下の列を追加します:
312 ID_abs_floor: IDの絶対値を args.Imin でクリッピングした値。
313 ID_smooth_linear: 線形スケールで平滑化されたID。
314 logID_smooth: 対数スケールで平滑化された log10(abs(ID))。
315 ID_smooth_log: logID_smooth を元に対数スケールで平滑化されたID。
316 ID_smooth: args.read_smooth_domain に応じて ID_smooth_linear または ID_smooth_log。
317 savgol_used_linear, savgol_used_log, savgol_used: Savitzky-Golay平滑化が適用された点を示すブールマスク。
318
319 重要な点:
320 伝達特性 (ID-VG) のプレビューは、デフォルトで log10(abs(ID)) の平滑化後に逆変換します。
321 出力特性 (ID-VD) のプレビューは、線形電流平滑化と線形Y軸プロットを強制します。
322 デフォルトでは、端点付近ではSavitzky-Golayフィルターの完全な中心ウィンドウがないため、
323 平滑化は無効化されます。これにより、VD=0付近の人工的なずれを回避します。
324 引数:
325 :param df: 処理対象のDataFrame。
326 :type df: pandas.DataFrame
327 :param xcol: データのX軸となる列名(例: 'VG', 'VD')。
328 :type xcol: str
329 :param groupcol: グループ化に使用する列名(例: 'VD', 'VG')。この列が存在しない場合、データ全体が単一のグループとして扱われます。
330 :type groupcol: str
331 :param args: コマンドライン引数を含むオブジェクト。Imin, lsq_order, smooth_npoints,
332 read_keep_edge_raw, read_smooth_domain 属性を使用します。
333 :type args: argparse.Namespace
334 戻り値:
335 :returns: クリップおよび平滑化された電流列が追加されたDataFrame。
336 :rtype: pandas.DataFrame
337 """
338
339 def _add_cols(tmp):
340 tmp = tmp.sort_values(xcol).copy()
341 tmp['ID_abs_floor'] = tmp['ID'].abs().clip(lower=args.Imin)
342 order = int(getattr(args, 'lsq_order', 2))
343 win = valid_savgol_window(len(tmp), args.smooth_npoints, order) if len(tmp) >= 3 else np.nan
344 keep_edge_raw = bool(getattr(args, 'read_keep_edge_raw', True))
345 if len(tmp) >= 3 and np.isfinite(win):
346 win_i = int(win)
347 lin, used_lin = savgol_center_only(tmp['ID_abs_floor'].to_numpy(), win_i, order, keep_edge_raw)
348 tmp['ID_smooth_linear'] = np.clip(lin, args.Imin, None)
349 log_id = np.log10(tmp['ID_abs_floor'].to_numpy())
350 log_s, used_log = savgol_center_only(log_id, win_i, order, keep_edge_raw)
351 tmp['logID_smooth'] = log_s
352 tmp['ID_smooth_log'] = np.clip(np.power(10.0, tmp['logID_smooth']), args.Imin, None)
353 tmp['savgol_used_linear'] = used_lin
354 tmp['savgol_used_log'] = used_log
355 else:
356 tmp['ID_smooth_linear'] = tmp['ID_abs_floor']
357 tmp['logID_smooth'] = np.log10(tmp['ID_abs_floor'])
358 tmp['ID_smooth_log'] = tmp['ID_abs_floor']
359 tmp['savgol_used_linear'] = False
360 tmp['savgol_used_log'] = False
361 if getattr(args, 'read_smooth_domain', 'log') == 'linear':
362 tmp['ID_smooth'] = tmp['ID_smooth_linear']
363 tmp['read_smooth_domain'] = 'linear current'
364 tmp['savgol_used'] = tmp['savgol_used_linear']
365 else:
366 tmp['ID_smooth'] = tmp['ID_smooth_log']
367 tmp['read_smooth_domain'] = 'log10 current'
368 tmp['savgol_used'] = tmp['savgol_used_log']
369 tmp['read_x'] = tmp[xcol]
370 tmp['savgol_window'] = win
371 tmp['savgol_order'] = order
372 tmp['edge_raw_kept'] = keep_edge_raw
373 return tmp
374
375 if groupcol not in df.columns:
376 tmp = _add_cols(df)
377 tmp['read_group'] = 'all'
378 return tmp
379
380 out = []
381 for gv in sorted(df[groupcol].dropna().unique()):
382 tmp = df[np.isclose(df[groupcol], gv, atol=1e-3)].copy()
383 if tmp.empty:
384 continue
385 tmp = _add_cols(tmp)
386 tmp['read_group'] = f'{groupcol}={gv:g}'
387 out.append(tmp)
388 return pd.concat(out, ignore_index=False) if out else pd.DataFrame()
389
390def plot_read_data(df_read, xcol, groupcol, title, args, fname_base, yscale='log'):
391 """概要:
392 読み込み/プレビューモードで平滑化されたデータをプロットします。
393 詳細説明:
394 生のクリップされたデータ (ID_abs_floor) と平滑化されたデータ (ID_smooth) を
395 X軸 (xcol) に対してプロットします。
396 groupcol が指定されている場合、データはグループごとにプロットされ、凡例に表示されます。
397 Y軸はオプションで対数スケールに設定できます。
398 生成されたプロットは、args.save_plot がTrueの場合、指定されたディレクトリにPNGファイルとして保存されます。
399 引数:
400 :param df_read: 読み込み/プレビュー用に処理されたデータを含むDataFrame。
401 :type df_read: pandas.DataFrame
402 :param xcol: X軸としてプロットする列名(例: 'VG', 'VD')。
403 :type xcol: str
404 :param groupcol: データをグループ化するための列名(例: 'VD', 'VG')。
405 この列が存在しない場合、データ全体が単一のグループとして扱われます。
406 :type groupcol: str
407 :param title: プロットのタイトル。
408 :type title: str
409 :param args: コマンドライン引数を含むオブジェクト。save_plot と plot_dir 属性を使用します。
410 :type args: argparse.Namespace
411 :param fname_base: 保存するPNGファイル名のベース。
412 :type fname_base: str
413 :param yscale: Y軸のスケール('log'または'linear')。Noneの場合、線形スケール。デフォルトは'log'。
414 :type yscale: str or None
415 戻り値:
416 :returns: 生成されたMatplotlibのFigureオブジェクト。
417 :rtype: matplotlib.figure.Figure
418 """
419 fig, ax = plt.subplots(1, 1, figsize=(8, 6))
420 if groupcol in df_read.columns:
421 groups = sorted(df_read[groupcol].dropna().unique())
422 for gv in groups:
423 tmp = df_read[np.isclose(df_read[groupcol], gv, atol=1e-3)].sort_values(xcol)
424 ax.plot(tmp[xcol], tmp['ID_abs_floor'], '.', ms=3, alpha=0.35)
425 ax.plot(tmp[xcol], tmp['ID_smooth'], '-', lw=1.6, label=f'{groupcol}={gv:g} ({tmp["read_smooth_domain"].iloc[0]})')
426 else:
427 tmp = df_read.sort_values(xcol)
428 ax.plot(tmp[xcol], tmp['ID_abs_floor'], '.', ms=3, alpha=0.35, label='clipped')
429 ax.plot(tmp[xcol], tmp['ID_smooth'], '-', lw=1.8, label=f'smoothed ({tmp["read_smooth_domain"].iloc[0]})')
430 if yscale is not None:
431 ax.set_yscale(yscale)
432 ax.set_xlabel(f'{xcol} [V]')
433 ax.set_ylabel(r'$|I_D|$ clipped/smoothed [A]')
434 ax.set_title(title)
435 ax.grid(True, alpha=0.15)
436 ax.legend(fontsize='x-small', ncol=2)
437 fig.tight_layout()
438 if args.save_plot:
439 os.makedirs(args.plot_dir, exist_ok=True)
440 fname = plot_path(args, fname_base)
441 fig.savefig(fname, dpi=200, bbox_inches='tight')
442 print(f' plot saved: {fname}')
443 return fig
444
445
446def run_read_mode(args):
447 """概要:
448 読み込み/プレビューモードの処理を実行します。
449 詳細説明:
450 指定された入力ファイル(伝達特性と出力特性)を読み込み、Imin で電流をクリッピングし、
451 Savitzky-Golayフィルターで平滑化します。その後、処理されたデータをプロットし、
452 結果の概要をコンソールに出力します。
453 伝達特性 (ID-VG) データは対数電流平滑化がデフォルトですが、
454 出力特性 (ID-VD) データは線形電流平滑化と線形Y軸プロットが強制されます。
455 処理されたデータフレームとサマリー情報は、Excelエクスポートのために返されます。
456 引数:
457 :param args: コマンドライン引数を含むオブジェクト。
458 infile_vg, infile_vd, reverse_vg, idx_vg, idx_vd, Imin,
459 read_smooth_domain, save_plot, plot_dir 属性を使用します。
460 :type args: argparse.Namespace
461 戻り値:
462 :returns:
463 MatplotlibのFigureオブジェクトのリスト。
464 読み込まれた/処理されたデータフレームをタグ('vg'または'vd')で格納した辞書。
465 読み込み処理のサマリー情報を含む辞書(各グループごと)のリスト。
466 :rtype: tuple[list[matplotlib.figure.Figure], dict[str, pandas.DataFrame], list[dict]]
467 """
468 figures = []
469 read_tables = {}
470 read_summary = []
471 targets = []
472 if args.infile_vg:
473 targets.append(('vg', args.infile_vg, 'VG', 'VD', 'Read preview: transfer-like ID-VG data'))
474 if args.infile_vd:
475 targets.append(('vd', args.infile_vd, 'VD', 'VG', 'Read preview: output ID-VD data'))
476 if not targets:
477 print('WARNING: no input file is specified. Use --infile_vg and/or --infile_vd.')
478 return figures, read_tables, read_summary
479 for tag, path, xcol, groupcol, title in targets:
480 df = detect_and_load(path, reverse_vg=args.reverse_vg)
481 if df is None:
482 continue
483 if xcol not in df.columns:
484 print(f'WARNING: {path} does not have required x column {xcol}; skipped.')
485 continue
486 idx_select = args.idx_vg if xcol == 'VG' else args.idx_vd
487 idx_label = 'idx_vg' if xcol == 'VG' else 'idx_vd'
488 selected_groups = []
489 if groupcol in df.columns:
490 for gv in sorted(df[groupcol].dropna().unique()):
491 gdf = df[np.isclose(df[groupcol], gv, atol=1e-3)].copy()
492 sg = select_sweep_segment(gdf, xcol, idx_select, idx_label, sort_after=True)
493 if not sg.empty:
494 selected_groups.append(sg)
495 df = pd.concat(selected_groups, ignore_index=False) if selected_groups else pd.DataFrame()
496 else:
497 df = select_sweep_segment(df, xcol, idx_select, idx_label, sort_after=True)
498 # Transfer ID-VG is best previewed with log-current smoothing.
499 # Output ID-VD should be previewed as a linear ID plot with linear-current smoothing.
500 print("xcol=", xcol)
501 if xcol == 'VD':
502 local_args = argparse.Namespace(**vars(args))
503 local_args.read_smooth_domain = 'linear'
504 df_read = add_read_columns_grouped(df, xcol, groupcol, local_args)
505 read_yscale = 'linear'
506 else:
507 df_read = add_read_columns_grouped(df, xcol, groupcol, args)
508 read_yscale = 'log'
509 if df_read.empty:
510 print(f'WARNING: no readable data after preprocessing: {path}')
511 continue
512 read_tables[tag] = df_read
513 print(f"\n{'='*20} READ/PREVIEW {tag.upper()} {'='*20}")
514 print(f'File: {path}')
515 print(f'Rows: {len(df_read)} | x={xcol} | group={groupcol if groupcol in df_read.columns else "none"}')
516 print(f'Imin clipping floor = {args.Imin:.4e} A')
517 print(f'read smoothing domain = {args.read_smooth_domain}')
518 if groupcol in df_read.columns:
519 for gv in sorted(df_read[groupcol].dropna().unique()):
520 tmp = df_read[np.isclose(df_read[groupcol], gv, atol=1e-3)]
521 read_summary.append({
522 'dataset': tag, 'file': path, 'xcol': xcol, 'groupcol': groupcol, 'group_value': gv,
523 'n_points': len(tmp), 'x_min': tmp[xcol].min(), 'x_max': tmp[xcol].max(),
524 'ID_abs_floor_min': tmp['ID_abs_floor'].min(), 'ID_abs_floor_max': tmp['ID_abs_floor'].max(),
525 'ID_smooth_min': tmp['ID_smooth'].min(), 'ID_smooth_max': tmp['ID_smooth'].max(),
526 'ID_smooth_log_min': tmp['ID_smooth_log'].min(), 'ID_smooth_log_max': tmp['ID_smooth_log'].max(),
527 'ID_smooth_linear_min': tmp['ID_smooth_linear'].min(), 'ID_smooth_linear_max': tmp['ID_smooth_linear'].max(),
528 'read_smooth_domain': tmp['read_smooth_domain'].iloc[0],
529 'savgol_window': tmp['savgol_window'].iloc[0],
530 'savgol_order': tmp['savgol_order'].iloc[0],
531 'n_savgol_used': int(tmp['savgol_used'].sum()),
532 'edge_raw_kept': bool(tmp['edge_raw_kept'].iloc[0]),
533 })
534 print(f' {groupcol}={gv:8.4g} | n={len(tmp):4d} | {xcol}=({tmp[xcol].min():.4g}, {tmp[xcol].max():.4g}) | '
535 f'ID_smooth=({tmp["ID_smooth"].min():.4e}, {tmp["ID_smooth"].max():.4e}) | '
536 f'win={tmp["savgol_window"].iloc[0]}, order={tmp["savgol_order"].iloc[0]}, '
537 f'smoothed points={int(tmp["savgol_used"].sum())}/{len(tmp)}, edge_raw={tmp["edge_raw_kept"].iloc[0]}')
538 print(" read_yscale=", read_yscale)
539 figures.append(plot_read_data(df_read, xcol, groupcol, title, args, f'read_{tag}', yscale=read_yscale))
540 return figures, read_tables, read_summary
541
542
543def _normalize_tft_column_name(name):
544 """概要:
545 測定CSVの列名を解析用の標準名へそろえます。
546 詳細説明:
547 例: VG(V) -> VG, Id -> ID。
548 既存の解析コードは VG, VD, ID などの大文字文字列名を仮定しているため、
549 読み込み直後にここで正規化します。
550 引数:
551 :param name: 正規化する列名。
552 :type name: str
553 戻り値:
554 :returns: 標準化された列名。
555 :rtype: str
556 """
557 s = str(name).strip().replace('\ufeff', '')
558 if '(' in s:
559 s = s.split('(', 1)[0]
560 if '[' in s:
561 s = s.split('[', 1)[0]
562 return s.strip().upper()
563
564
565def _to_numeric_dataframe(df):
566 """概要:
567 全列を可能な範囲で数値化します。
568 引数:
569 :param df: 数値化するDataFrame。
570 :type df: pandas.DataFrame
571 戻り値:
572 :returns: 全列が数値化されたDataFrame。
573 :rtype: pandas.DataFrame
574 """
575 out = df.copy()
576 for col in out.columns:
577 out[col] = pd.to_numeric(out[col], errors='coerce')
578 return out
579
580
581def _float_meta(metadata, key):
582 """概要:
583 4155/4156系CSVメタデータからfloat値を取り出します。
584 引数:
585 :param metadata: メタデータを含む辞書。
586 :type metadata: dict
587 :param key: 取得するメタデータのキー。
588 :type key: str
589 戻り値:
590 :returns: 取得されたfloat値。変換できない場合はNone。
591 :rtype: float or None
592 """
593 vals = metadata.get(key)
594 if not vals:
595 return None
596 try:
597 return float(str(vals[0]).strip())
598 except Exception:
599 return None
600
601
602def _int_meta(metadata, key):
603 """概要:
604 4155/4156系CSVメタデータからint値を取り出します。
605 引数:
606 :param metadata: メタデータを含む辞書。
607 :type metadata: dict
608 :param key: 取得するメタデータのキー。
609 :type key: str
610 戻り値:
611 :returns: 取得されたint値。変換できない場合や有効な数値でない場合はNone。
612 :rtype: int or None
613 """
614 val = _float_meta(metadata, key)
615 if val is None or not np.isfinite(val):
616 return None
617 return int(round(val))
618
619
620def _infer_smu_sweep_variable(metadata, sweep_role):
621 """概要:
622 Primary/Secondary掃引に対応する電圧名を推定します。
623 詳細説明:
624 Keysight/Agilent 4155系CSVでは、通常
625 Channel.VName = VD, VS, VG と Channel.Func = VAR2, CONST, VAR1
626 のようなメタデータがあり、VAR1がPrimary、VAR2がSecondaryに対応します。
627 引数:
628 :param metadata: メタデータを含む辞書。
629 :type metadata: dict
630 :param sweep_role: 掃引の役割('Primary'または'Secondary')。
631 :type sweep_role: str
632 戻り値:
633 :returns: 推定された電圧名(例: 'VG', 'VD')。推定できない場合はNone。
634 :rtype: str or None
635 """
636 target = {'Primary': 'VAR1', 'Secondary': 'VAR2'}.get(sweep_role)
637 if target is None:
638 return None
639 vnames = metadata.get('TestParameter.Channel.VName', [])
640 funcs = metadata.get('TestParameter.Channel.Func', [])
641 for vname, func in zip(vnames, funcs):
642 if str(func).strip().upper() == target:
643 return _normalize_tft_column_name(vname)
644 # 実データで一番多い組み合わせへのフォールバック
645 return 'VG' if sweep_role == 'Primary' else 'VD'
646
647
648def _infer_sweep_values(metadata, role):
649 """概要:
650 Primary/Secondary掃引の値リストをメタデータから推定します。
651 引数:
652 :param metadata: メタデータを含む辞書。
653 :type metadata: dict
654 :param role: 掃引の役割('Primary'または'Secondary')。
655 :type role: str
656 戻り値:
657 :returns: 推定された掃引値のリスト。
658 :rtype: list[float]
659 """
660 prefix = f'TestParameter.Measurement.{role}'
661 start = _float_meta(metadata, prefix + '.Start')
662 stop = _float_meta(metadata, prefix + '.Stop')
663 step = _float_meta(metadata, prefix + '.Step')
664 count = _int_meta(metadata, prefix + '.Count')
665
666 if role == 'Secondary':
667 if start is None or step is None or count is None or count <= 0:
668 return []
669 return [start + i * step for i in range(count)]
670
671 if start is None or stop is None or step is None or step == 0:
672 return []
673 n = int(round(abs((stop - start) / step))) + 1
674 if n <= 0:
675 return []
676 if stop >= start:
677 return [start + i * abs(step) for i in range(n)]
678 return [start - i * abs(step) for i in range(n)]
679
680
681def _infer_primary_branch_points(metadata):
682 """概要:
683 Primary掃引1枝あたりの点数をメタデータから推定します。
684 引数:
685 :param metadata: メタデータを含む辞書。
686 :type metadata: dict
687 戻り値:
688 :returns: Primary掃引1枝あたりの点数。推定できない場合はNone。
689 :rtype: int or None
690 """
691 vals = _infer_sweep_values(metadata, 'Primary')
692 return len(vals) if len(vals) > 1 else None
693
694
695def _is_double_primary_sweep(metadata):
696 """概要:
697 Primary掃引がダブル掃引(往復掃引)であるか判定します。
698 引数:
699 :param metadata: メタデータを含む辞書。
700 :type metadata: dict
701 戻り値:
702 :returns: ダブル掃引である場合はTrue、そうでない場合はFalse。
703 :rtype: bool
704 """
705 vals = metadata.get('TestParameter.Measurement.Primary.Locus', [])
706 return bool(vals) and str(vals[0]).strip().lower() == 'double'
707
708
709def _add_4155_inferred_columns(df, metadata):
710 """概要:
711 4155系DataName/DataValue CSVに不足しがちな掃引列を補います。
712 詳細説明:
713 DataName/DataValueブロックにはPrimary変数、ID、IGだけが保存され、
714 Secondary変数(今回の例ではVD)が各行に書かれない場合があります。
715 その場合、メタデータのSecondary Start/Step/Countから各行のVDを復元します。
716 引数:
717 :param df: 処理対象のDataFrame。
718 :type df: pandas.DataFrame
719 :param metadata: メタデータを含む辞書。
720 :type metadata: dict
721 戻り値:
722 :returns: 掃引列が補完されたDataFrame。
723 :rtype: pandas.DataFrame
724 """
725 out = df.copy()
726 if out.empty:
727 return out
728
729 primary_col = _infer_smu_sweep_variable(metadata, 'Primary')
730 secondary_col = _infer_smu_sweep_variable(metadata, 'Secondary')
731 half_n = _infer_primary_branch_points(metadata)
732 n = len(out)
733
734 if half_n is not None and half_n > 0:
735 sweep_id = np.arange(n) // half_n
736 elif primary_col in out.columns:
737 x = out[primary_col].to_numpy(dtype=float)
738 dx = np.diff(x)
739 sign = np.sign(dx)
740 for i in range(1, len(sign)):
741 if sign[i] == 0:
742 sign[i] = sign[i - 1]
743 change = np.where(sign[1:] * sign[:-1] < 0)[0] + 1
744 starts = [0] + (change + 1).tolist()
745 sweep_id = np.zeros(n, dtype=int)
746 for sid, a in enumerate(starts):
747 b = starts[sid + 1] if sid + 1 < len(starts) else n
748 sweep_id[a:b] = sid
749 else:
750 sweep_id = np.zeros(n, dtype=int)
751
752 out['sweep_id'] = sweep_id.astype(int)
753
754 directions = {}
755 if primary_col in out.columns:
756 for sid in sorted(out['sweep_id'].dropna().unique()):
757 sub = out[out['sweep_id'] == sid]
758 if sub.empty:
759 continue
760 x0 = sub[primary_col].iloc[0]
761 x1 = sub[primary_col].iloc[-1]
762 directions[sid] = 'forward' if x1 >= x0 else 'reverse'
763 out['sweep_direction'] = out['sweep_id'].map(directions)
764
765 secondary_values = _infer_sweep_values(metadata, 'Secondary')
766 branches_per_secondary = 2 if _is_double_primary_sweep(metadata) else 1
767 if secondary_col and secondary_values:
768 values_by_sweep = {}
769 for sid in sorted(out['sweep_id'].dropna().unique()):
770 idx = int(sid) // branches_per_secondary
771 values_by_sweep[int(sid)] = secondary_values[idx] if idx < len(secondary_values) else np.nan
772 inferred = out['sweep_id'].map(values_by_sweep)
773 # 列がない場合、またはDataName側の列が空の場合に補完。既存列がある場合はNaNのみ埋める。
774 if secondary_col not in out.columns:
775 out[secondary_col] = inferred
776 else:
777 out[secondary_col] = out[secondary_col].where(out[secondary_col].notna(), inferred)
778
779 return out
780
781
782def read_4155_dataname_datavalue_csv(filepath, encoding='utf-8-sig'):
783 """概要:
784 Keysight/Agilent 4155系の DataName/DataValue CSV を読み込みます。
785 詳細説明:
786 戻り値は (df, metadata) です。DataValue行が見つからない場合は
787 (None, metadata) を返し、通常CSV読み込みへフォールバックできるようにします。
788 引数:
789 :param filepath: 読み込むCSVファイルのパス。
790 :type filepath: str
791 :param encoding: ファイルの文字コード。デフォルトは'utf-8-sig'。
792 :type encoding: str
793 戻り値:
794 :returns: データを含むDataFrameとメタデータの辞書のタプル。
795 DataValue行が見つからない場合は (None, metadata)。
796 :rtype: tuple[pandas.DataFrame or None, dict]
797 """
798 metadata = {}
799 columns = None
800 data_rows = []
801
802 with open(filepath, 'r', encoding=encoding, errors='replace', newline='') as f:
803 reader = csv.reader(f)
804 for row in reader:
805 row = [str(v).strip() for v in row]
806 if not row or all(v == '' for v in row):
807 continue
808 tag = row[0]
809 if tag == 'DataName':
810 columns = [_normalize_tft_column_name(v) for v in row[1:]]
811 elif tag == 'DataValue':
812 if columns is None:
813 raise ValueError('DataValue appeared before DataName.')
814 vals = row[1:]
815 if len(vals) < len(columns):
816 vals = vals + [''] * (len(columns) - len(vals))
817 data_rows.append(vals[:len(columns)])
818 else:
819 if len(row) >= 2:
820 metadata[f'{row[0]}.{row[1]}'] = row[2:]
821
822 if columns is None or not data_rows:
823 return None, metadata
824
825 df = pd.DataFrame(data_rows, columns=columns)
826 df = _to_numeric_dataframe(df)
827 df = _add_4155_inferred_columns(df, metadata)
828 return df, metadata
829
830
831def _read_text_lines_with_detected_encoding(filepath):
832 """概要:
833 文字コードをゆるく推定してCSVを行単位で読み込みます。
834 引数:
835 :param filepath: 読み込むCSVファイルのパス。
836 :type filepath: str
837 戻り値:
838 :returns: ファイルの行リストと推定された文字コードのタプル。
839 :rtype: tuple[list[str], str]
840 """
841 with open(filepath, 'rb') as f:
842 rawdata = f.read(50000)
843 encoding = chardet.detect(rawdata)['encoding'] or 'utf-8-sig'
844 with open(filepath, 'r', encoding=encoding, errors='replace') as f:
845 lines = f.readlines()
846 return lines, encoding
847
848
849def detect_and_load(filepath, reverse_vg=False):
850 """概要:
851 CSVファイルからTFT解析用データを自動検出して読み込みます。
852 詳細説明:
853 対応形式:
854 1. 既存対応の「列ヘッダ行 + 数値データ行」形式
855 2. Keysight/Agilent 4155系の DataName / DataValue 形式
856
857 後者では、DataValueブロックにVDやVGなどのSecondary掃引列が明示されない場合でも、
858 メタデータから VD または VG を復元して既存解析コードへ渡します。
859 引数:
860 :param filepath: 読み込むCSVファイルのパス。
861 :type filepath: str
862 :param reverse_vg: Trueの場合、VGの符号を反転します。pチャネルデータの前処理を意図しています。
863 :type reverse_vg: bool
864 戻り値:
865 :returns: 読み込まれたデータを含むDataFrame。ファイルが見つからないかデータが読み込めない場合はNone。
866 :rtype: pandas.DataFrame or None
867 """
868 if not os.path.exists(filepath):
869 print(f'WARNING: file not found: {filepath}')
870 return None
871
872 lines, encoding = _read_text_lines_with_detected_encoding(filepath)
873
874 # まず4155/4156系 DataName/DataValue 形式として読み込む。
875 # この形式ではDataNameの次行がDataValueで始まるため、従来のヘッダ検出では拾えない。
876 try:
877 df_4155, metadata = read_4155_dataname_datavalue_csv(filepath, encoding=encoding)
878 except Exception as exc:
879 df_4155, metadata = None, {}
880 print(f'WARNING: failed to parse DataName/DataValue block as 4155-style CSV: {exc}')
881
882 if df_4155 is not None and {'VG', 'ID'}.issubset(df_4155.columns):
883 df = df_4155.dropna(subset=['VG', 'ID']).copy()
884 if reverse_vg and 'VG' in df.columns:
885 df['VG'] = -df['VG']
886 msg_cols = ', '.join(df.columns)
887 print(f'INFO: loaded 4155-style DataName/DataValue CSV: rows={len(df)}, columns={msg_cols}')
888 if 'VD' in df.columns:
889 vals = sorted(pd.Series(df['VD']).dropna().unique())
890 print('INFO: inferred/detected VD values: ' + ', '.join(f'{v:g}' for v in vals))
891 return df
892
893 # 従来形式: VG/IDを含むヘッダ行の直後から数値データが始まるCSV。
894 data_list, header, data_start_idx = [], None, -1
895 for i, line in enumerate(lines):
896 parts = [p.strip() for p in line.split(',')]
897 if not parts or parts[0] == '':
898 continue
899 upper_parts = [_normalize_tft_column_name(p) for p in parts]
900 if 'VG' in upper_parts and 'ID' in upper_parts:
901 if i + 1 < len(lines):
902 try:
903 float([p.strip() for p in lines[i + 1].split(',')][0])
904 header = upper_parts
905 data_start_idx = i + 1
906 break
907 except Exception:
908 continue
909
910 if data_start_idx != -1:
911 for line in lines[data_start_idx:]:
912 parts = [p.strip() for p in line.split(',')]
913 if parts and len(parts) >= len(header):
914 try:
915 float(parts[0])
916 data_list.append(parts[:len(header)])
917 except Exception:
918 continue
919
920 if header is None or not data_list:
921 print(f'WARNING: no VG/ID data block found: {filepath}')
922 return None
923
924 df = pd.DataFrame(data_list, columns=header).apply(pd.to_numeric, errors='coerce').dropna(subset=['VG', 'ID'])
925 if reverse_vg and 'VG' in df.columns:
926 df['VG'] = -df['VG']
927 return df
928
929
930def valid_savgol_window(n, requested, order):
931 """概要:
932 指定されたデータ点数に対して有効な奇数のSavitzky-Golayウィンドウ長を返します。
933 詳細説明:
934 サビツキー・ゴレイフィルターのウィンドウ長は、多項式の次数よりも大きく、
935 かつ奇数である必要があります。また、データ点数を超えることはできません。
936 この関数は、与えられた制約 (n, requested, order) に基づいて、
937 これらの条件を満たす最小かつ有効な奇数のウィンドウ長を計算して返します。
938 引数:
939 :param n: データ点の総数。
940 :type n: int
941 :param requested: 要求されたSavitzky-Golayウィンドウ長。
942 :type requested: int
943 :param order: Savitzky-Golayフィルターの多項式の次数。
944 :type order: int
945 戻り値:
946 :returns: 有効な奇数のSavitzky-Golayウィンドウ長。
947 :rtype: int
948 """
949 win = max(int(requested), order + 2)
950 if win % 2 == 0:
951 win += 1
952 if win > n:
953 win = n if n % 2 == 1 else n - 1
954 if win <= order:
955 win = order + 2 if (order + 2) % 2 == 1 else order + 3
956 return max(3, win)
957
958
959def nearest_row_by_current(df, target_id):
960 """概要:
961 データフレーム内で指定された目標電流 (target_id) に最も近いID_smoothを持つ行を検索します。
962 詳細説明:
963 データフレーム df の ID_smooth 列を基に、目標電流値 target_id との絶対差が最小となる行を特定します。
964 見つかった行のインデックスとその行全体のデータを返します。
965 データフレームが空の場合、または ID_smooth 列が存在しない場合は、Noneを返します。
966 引数:
967 :param df: 検索対象のDataFrame。ID_smooth列が必要です。
968 :type df: pandas.DataFrame
969 :param target_id: 検索する目標電流値 [A]。
970 :type target_id: float
971 戻り値:
972 :returns: 目標電流に最も近い行のインデックスと、その行のPandas Series。
973 データフレームが空の場合は (None, None) を返します。
974 :rtype: tuple[int or None, pandas.Series or None]
975 """
976 if df.empty:
977 return None, None
978 idx = (df['ID_smooth'] - target_id).abs().idxmin()
979 return idx, df.loc[idx]
980
981
982
983def add_sweep_index(df, col, idx_col):
984 """概要:
985 指定された列の掃引方向の変化に基づいて、掃引セグメントのインデックスを追加します。
986 詳細説明:
987 データフレームの指定された列 (col) の値の連続的な変化を分析し、
988 掃引方向が反転するたびに掃引セグメントのインデックスを1つ増やします。
989 これにより、多方向掃引データ(例: VGの往復掃引)を個別のセグメントに分割できます。
990 結果のインデックスは新しい列 (idx_col) としてデータフレームに追加されます。
991 引数:
992 :param df: 処理対象のDataFrame。
993 :type df: pandas.DataFrame
994 :param col: 掃引方向を検出する基準となる列名(例: 'VG', 'VD')。
995 :type col: str
996 :param idx_col: 生成される掃引インデックス列の名前。
997 :type idx_col: str
998 戻り値:
999 :returns: 掃引セグメントインデックス列が追加されたDataFrame。
1000 :rtype: pandas.DataFrame
1001 """
1002 out = df.copy()
1003 vals = out[col].to_numpy(dtype=float)
1004 seg = []
1005 current_seg = 0
1006 prev_sign = 0
1007 prev_val = np.nan
1008 for v in vals:
1009 sign = 0
1010 if np.isfinite(v) and np.isfinite(prev_val):
1011 dv = v - prev_val
1012 if abs(dv) > 1e-12:
1013 sign = 1 if dv > 0 else -1
1014 if sign != 0:
1015 if prev_sign != 0 and sign != prev_sign:
1016 current_seg += 1
1017 prev_sign = sign
1018 seg.append(current_seg)
1019 prev_val = v
1020 out[idx_col] = seg
1021 return out
1022
1023
1024def select_sweep_segment(df, col, idx, label, sort_after=True, verbose=False):
1025 """概要:
1026 掃引方向の変化インデックスに基づいて、DataFrameから特定の掃引セグメントを選択します。
1027 詳細説明:
1028 この関数は、add_sweep_index を使用して、指定された列 (col) の掃引セグメントインデックスを計算し、
1029 その後、要求されたインデックス (idx) に対応するセグメントのみをフィルタリングして返します。
1030 要求されたインデックスが存在しない場合、利用可能な最初のセグメントがフォールバックとして選択されます。
1031 オプションで、選択されたセグメントを col 列でソートできます。
1032 引数:
1033 :param df: 処理対象のDataFrame。
1034 :type df: pandas.DataFrame or None
1035 :param col: 掃引セグメントを識別する基準となる列名(例: 'VG', 'VD')。
1036 :type col: str
1037 :param idx: 選択する掃引セグメントのインデックス。
1038 :type idx: int
1039 :param label: 警告メッセージなどで使用するインデックスのラベル(例: 'idx_vg')。
1040 :type label: str
1041 :param sort_after: Trueの場合、選択後に col 列でDataFrameをソートします。デフォルトはTrue。
1042 :type sort_after: bool
1043 :param verbose: Trueの場合、選択されたセグメントに関する詳細情報を出力します。デフォルトはFalse。
1044 :type verbose: bool
1045 戻り値:
1046 :returns: 選択された掃引セグメントを含むDataFrame。元のDataFrameがNoneまたは空の場合、
1047 または col 列がない場合は、元のDataFrameまたはNoneを返します。
1048 :rtype: pandas.DataFrame or None
1049 """
1050 if df is None or df.empty or col not in df.columns:
1051 return df.copy() if df is not None else df
1052 idx_col = f'idx_{col.lower()}_sweep'
1053 work = add_sweep_index(df, col, idx_col)
1054 available = sorted(work[idx_col].dropna().unique())
1055 if idx not in available:
1056 print(f'WARNING: requested {label}={idx}, but available {label} values are {available}; fallback to first segment.')
1057 idx = available[0] if available else 0
1058 selected = work[work[idx_col] == idx].copy()
1059 if sort_after and not selected.empty:
1060 selected = selected.sort_values(col)
1061 if verbose and not selected.empty:
1062 print(f' selected {label}={idx}: n={len(selected)}, {col}=({selected[col].min():.4g}, {selected[col].max():.4g})')
1063 return selected
1064
1065def build_analysis_points(res):
1066 """概要:
1067 伝達特性解析の主要点をロングフォーマットで返します。
1068 詳細説明:
1069 res 辞書からVth, Smin, mu_max などの主要な解析ポイントを抽出し、
1070 Excelサマリーシートに適したリスト形式で提供します。
1071 各ポイントは、そのVG, ID, 導関数、移動度などの詳細な情報とともに辞書として格納されます。
1072 引数:
1073 :param res: 伝達特性解析結果を含む辞書。df (解析対象のDataFrame) および
1074 idx_ で始まるキー (各ポイントのインデックス) が必要です。
1075 :type res: dict
1076 戻り値:
1077 :returns: 各解析ポイントのデータを格納した辞書のリスト。
1078 :rtype: list[dict]
1079 """
1080 keys = [
1081 ('Vth_lin_ID_max_slope', res.get('idx_lin_slope_max'), 'Linear-region Vth from tangent at max d(ID)/dVG'),
1082 ('Vth_sat_sqrtID_max_slope', res.get('idx_sat_slope_max'), 'Saturation-region Vth from tangent at max d(sqrt(ID))/dVG'),
1083 ('Smin_max_log_slope', res.get('idx_smin'), 'Minimum S = inverse max dlog10(ID)/dVG in subthreshold region'),
1084 ('S_at_ID_S', res.get('idx_ids'), 'S at the current closest to ID_S'),
1085 ('mu_lin_max', res.get('idx_mu_lin_max'), 'Maximum linear-field-effect mobility muFE'),
1086 ('mu_sat_max', res.get('idx_mu_sat_max'), 'Maximum saturation mobility profile muSAT_prof'),
1087 ('Ioff', res.get('idx_ioff'), 'Off-current reference point'),
1088 ]
1089 rows = []
1090 df = res['df']
1091 for name, idx, note in keys:
1092 if idx is None or (isinstance(idx, float) and np.isnan(idx)) or idx not in df.index:
1093 continue
1094 r = df.loc[idx]
1095 rows.append({
1096 'VD': res['VD'],
1097 'point': name,
1098 'VG': r.get('VG', np.nan),
1099 'ID': r.get('ID', np.nan),
1100 'ID_smooth': r.get('ID_smooth', np.nan),
1101 'logID': r.get('logID', np.nan),
1102 'sqrtID': r.get('sqrtID', np.nan),
1103 'dlogID_dVG': r.get('dlogID', np.nan),
1104 'S_V_per_dec': r.get('S_val', np.nan),
1105 'gm_A_per_V': r.get('gm', np.nan),
1106 'dsqrtID_dVG': r.get('dsqrtID', np.nan),
1107 'mu_lin_cm2_Vs': r.get('muFE', np.nan),
1108 'mu_sat_cm2_Vs': r.get('muSAT_prof', np.nan),
1109 'note': note,
1110 })
1111 return rows
1112
1113
1114
1115
1116def region_check_saturation(vd, vg, vth, factor=3.0):
1117 """概要:
1118 飽和領域動作の条件をチェックします。
1119 詳細説明:
1120 トランジスタが飽和領域で動作しているかどうかを判断するために、
1121 指定されたVG、VD、Vth、および安全係数 (factor) を使用して
1122 VD >= factor * (VG - Vth) の条件を確認します。
1123 VG-Vthが正でない場合、または条件が満たされない場合は警告が生成されます。
1124 引数:
1125 :param vd: ドレイン電圧 [V]。
1126 :type vd: float
1127 :param vg: ゲート電圧 [V]。
1128 :type vg: float
1129 :param vth: 閾値電圧 [V]。
1130 :type vth: float
1131 :param factor: 飽和領域を保証するための安全係数。デフォルトは3.0。
1132 :type factor: float
1133 戻り値:
1134 :returns: 飽和領域チェックの結果を含む辞書。
1135 ok (bool), warning (str), VD_abs (float), VG_minus_Vth (float),
1136 ratio (float), criterion (str) を含みます。
1137 :rtype: dict
1138 """
1139 vd_eff = abs(float(vd))
1140 overdrive = float(vg) - float(vth)
1141 overdrive_pos = max(overdrive, 0.0)
1142 criterion = f"VD >= {factor:g}*(VG-Vth)"
1143 if overdrive_pos <= 0:
1144 return {'ok': False, 'warning': 'VG - Vth <= 0; mobility point is not clearly in the on-state.',
1145 'VD_abs': vd_eff, 'VG_minus_Vth': overdrive, 'ratio': np.nan, 'criterion': criterion}
1146 ratio = vd_eff / overdrive_pos
1147 ok = ratio >= factor
1148 warning = '' if ok else f"Possible non-saturation: VD/(VG-Vth)={ratio:.3g} < {factor:g}."
1149 return {'ok': ok, 'warning': warning, 'VD_abs': vd_eff, 'VG_minus_Vth': overdrive, 'ratio': ratio, 'criterion': criterion}
1150
1151
1152def region_check_linear(vd, vg, vth, factor=3.0):
1153 """概要:
1154 線形領域動作の条件をチェックします。
1155 詳細説明:
1156 トランジスタが線形領域で動作しているかどうかを判断するために、
1157 指定されたVG、VD、Vth、および安全係数 (factor) を使用して
1158 VG - Vth >= factor * VD の条件を確認します。
1159 VDが正でない場合、VG-Vthが負の場合、または条件が満たされない場合は警告が生成されます。
1160 引数:
1161 :param vd: ドレイン電圧 [V]。
1162 :type vd: float
1163 :param vg: ゲート電圧 [V]。
1164 :type vg: float
1165 :param vth: 閾値電圧 [V]。
1166 :type vth: float
1167 :param factor: 線形領域を保証するための安全係数。デフォルトは3.0。
1168 :type factor: float
1169 戻り値:
1170 :returns: 線形領域チェックの結果を含む辞書。
1171 ok (bool), warning (str), VD_abs (float), VG_minus_Vth (float),
1172 ratio (float), criterion (str) を含みます。
1173 :rtype: dict
1174 """
1175 vd_eff = abs(float(vd))
1176 overdrive = float(vg) - float(vth)
1177 criterion = f"VG-Vth >= {factor:g}*VD"
1178 if vd_eff <= 0:
1179 return {'ok': False, 'warning': 'VD <= 0; linear-region mobility is not meaningful.',
1180 'VD_abs': vd_eff, 'VG_minus_Vth': overdrive, 'ratio': np.nan, 'criterion': criterion}
1181 ratio = overdrive / vd_eff
1182 ok = (overdrive > 0) and (ratio >= factor)
1183 if overdrive <= 0:
1184 warning = 'VG - Vth <= 0; mobility point is below threshold.'
1185 elif not ok:
1186 warning = f"Possible non-linear-region point: (VG-Vth)/VD={ratio:.3g} < {factor:g}."
1187 else:
1188 warning = ''
1189 return {'ok': ok, 'warning': warning, 'VD_abs': vd_eff, 'VG_minus_Vth': overdrive, 'ratio': ratio, 'criterion': criterion}
1190
1191
1192
1193def classify_transfer_region(linear_check, saturation_check):
1194 """概要:
1195 線形/飽和/中間領域の推奨を返します。
1196 詳細説明:
1197 線形領域チェックと飽和領域チェックの結果に基づいて、
1198 現在解析中の動作点が線形、飽和、またはどちらでもない中間領域のどれに属するかを分類します。
1199 分類結果と、それに関連する推奨/警告メッセージを返します。
1200 引数:
1201 :param linear_check: region_check_linear 関数からの結果辞書。
1202 :type linear_check: dict
1203 :param saturation_check: region_check_saturation 関数からの結果辞書。
1204 :type saturation_check: dict
1205 戻り値:
1206 :returns: 推奨される領域タイプと、関連する警告/説明メッセージのタプル。
1207 :rtype: tuple[str, str]
1208 """
1209 lin_ok = bool(linear_check.get('ok', False))
1210 sat_ok = bool(saturation_check.get('ok', False))
1211 if lin_ok and not sat_ok:
1212 return 'linear', 'Use linear-region mobility/Vth. Condition VG-Vth >= factor*VD is satisfied.'
1213 if sat_ok and not lin_ok:
1214 return 'saturation', 'Use saturation-region mobility/Vth. Condition VD >= factor*(VG-Vth) is satisfied.'
1215 if lin_ok and sat_ok:
1216 return 'ambiguous', 'Both linear and saturation checks passed at their own extraction points; inspect plots.'
1217 return 'intermediate', 'WARNING: neither linear nor saturation condition is sufficiently satisfied.'
1218def analyze_vg_core(df_full, vd_val, args, cox):
1219 """概要:
1220 特定VDスライスのID-VGから、線形法と飽和法の両方でVth/移動度を抽出します。
1221 詳細説明:
1222 ID-VGデータに基づいて、線形領域(最大相互コンダクタンス gm から)と
1223 飽和領域(最大 d(sqrt(ID))/dVg から)の閾値電圧 (Vth) と移動度を抽出します。
1224 サブスレッショルドスイング (S) やオフ電流 (Ioff) も計算します。
1225 抽出されたポイントの動作領域チェックも行い、推奨される抽出方法を提示します。
1226 引数:
1227 :param df_full: 全てのVG-ID測定データを含むDataFrame。
1228 :type df_full: pandas.DataFrame
1229 :param vd_val: 解析対象のドレイン電圧 [V]。
1230 :type vd_val: float
1231 :param args: コマンドライン引数を含むオブジェクト。smooth_npoints, lsq_order, Imin,
1232 L, W, region_factor, ID_S 属性を使用します。
1233 :type args: argparse.Namespace
1234 :param cox: 単位面積あたりのゲート酸化膜容量 [F/cm^2]。
1235 :type cox: float
1236 戻り値:
1237 :returns: 特定VDスライスにおける詳細な解析結果を含む辞書。データが不十分な場合はNone。
1238 :rtype: dict or None
1239 """
1240 df_vd = df_full[np.isclose(df_full['VD'], vd_val, atol=1e-3)].copy()
1241 df_vd = select_sweep_segment(df_vd, 'VG', args.idx_vg, 'idx_vg', sort_after=True)
1242 if len(df_vd) < 5:
1243 return None
1244
1245 win = valid_savgol_window(len(df_vd), args.smooth_npoints, args.lsq_order)
1246 vg_step = df_vd['VG'].diff().dropna().median()
1247 if not np.isfinite(vg_step) or vg_step == 0:
1248 vg_step = 1.0
1249
1250 df_vd['ID_abs_floor'] = df_vd['ID'].abs().clip(lower=args.Imin)
1251 df_vd['ID_smooth'] = savgol_filter(df_vd['ID_abs_floor'], win, 1)
1252 df_vd['ID_smooth'] = np.clip(df_vd['ID_smooth'], args.Imin, None)
1253 df_vd['logID'] = np.log10(df_vd['ID_smooth'])
1254 df_vd['sqrtID'] = np.sqrt(df_vd['ID_smooth'])
1255 df_vd['dlogID'] = savgol_filter(df_vd['logID'], win, args.lsq_order, deriv=1, delta=vg_step)
1256 df_vd['gm'] = savgol_filter(df_vd['ID_smooth'], win, args.lsq_order, deriv=1, delta=vg_step)
1257 df_vd['dsqrtID'] = savgol_filter(df_vd['sqrtID'], win, args.lsq_order, deriv=1, delta=vg_step)
1258
1259 vd_eff = max(abs(vd_val), 1e-12)
1260 df_vd['muFE'] = (args.L / (args.W * cox * vd_eff)) * df_vd['gm']
1261 df_vd['muSAT_prof'] = (2 * args.L / (args.W * cox)) * (df_vd['dsqrtID'] ** 2)
1262 df_vd['S_val'] = np.where(df_vd['dlogID'] > 1e-6, 1.0 / df_vd['dlogID'], np.nan)
1263
1264 gm_valid = df_vd['gm'].replace([np.inf, -np.inf], np.nan).where(df_vd['gm'] > 0)
1265 idx_lin_slope_max = gm_valid.idxmax() if gm_valid.notna().any() else None
1266 if idx_lin_slope_max is not None:
1267 row_lin = df_vd.loc[idx_lin_slope_max]
1268 v_lin_slope = row_lin['VG']
1269 vth_lin = v_lin_slope - row_lin['ID_smooth'] / row_lin['gm'] if row_lin['gm'] > 0 else np.nan
1270 mu_lin_slope = row_lin['muFE']
1271 id_lin_slope = row_lin['ID_smooth']
1272 gm_max = row_lin['gm']
1273 else:
1274 v_lin_slope = vth_lin = mu_lin_slope = id_lin_slope = gm_max = np.nan
1275
1276 dsqrt_valid = df_vd['dsqrtID'].replace([np.inf, -np.inf], np.nan).where(df_vd['dsqrtID'] > 0)
1277 idx_sat_slope_max = dsqrt_valid.idxmax() if dsqrt_valid.notna().any() else None
1278 if idx_sat_slope_max is not None:
1279 row_sat = df_vd.loc[idx_sat_slope_max]
1280 v_sat_slope = row_sat['VG']
1281 vth_sat = v_sat_slope - row_sat['sqrtID'] / row_sat['dsqrtID'] if row_sat['dsqrtID'] > 0 else np.nan
1282 mu_sat_slope = row_sat['muSAT_prof']
1283 id_sat_slope = row_sat['ID_smooth']
1284 sqrtID_sat_slope = row_sat['sqrtID']
1285 dsqrtID_max = row_sat['dsqrtID']
1286 else:
1287 v_sat_slope = vth_sat = mu_sat_slope = id_sat_slope = sqrtID_sat_slope = dsqrtID_max = np.nan
1288
1289 idx_mu_lin_max = df_vd['muFE'].replace([np.inf, -np.inf], np.nan).idxmax()
1290 idx_mu_sat_max = df_vd['muSAT_prof'].replace([np.inf, -np.inf], np.nan).idxmax()
1291 mu_lin_max = df_vd.loc[idx_mu_lin_max, 'muFE']
1292 mu_sat_max = df_vd.loc[idx_mu_sat_max, 'muSAT_prof']
1293 VG_mu_lin_max = df_vd.loc[idx_mu_lin_max, 'VG']
1294 ID_mu_lin_max = df_vd.loc[idx_mu_lin_max, 'ID_smooth']
1295 VG_mu_sat_max = df_vd.loc[idx_mu_sat_max, 'VG']
1296 ID_mu_sat_max = df_vd.loc[idx_mu_sat_max, 'ID_smooth']
1297
1298 lin_region = region_check_linear(vd_eff, VG_mu_lin_max, vth_lin, args.region_factor)
1299 sat_region = region_check_saturation(vd_eff, VG_mu_sat_max, vth_sat, args.region_factor)
1300 recommended_method, recommended_warning = classify_transfer_region(lin_region, sat_region)
1301 if recommended_method == 'linear':
1302 vth_rec, mu_rec = vth_lin, mu_lin_max
1303 elif recommended_method == 'saturation':
1304 vth_rec, mu_rec = vth_sat, mu_sat_max
1305 else:
1306 vth_rec = np.nan
1307 mu_rec = np.nan
1308
1309 vth_for_subthreshold = vth_rec
1310 if not np.isfinite(vth_for_subthreshold):
1311 vth_for_subthreshold = vth_sat if np.isfinite(vth_sat) else vth_lin
1312
1313 off_mask = (df_vd['VG'] < vth_for_subthreshold - 5) & (df_vd['dlogID'].abs() < 0.1)
1314 if any(off_mask):
1315 ioff = df_vd[off_mask]['ID_smooth'].mean()
1316 idx_ioff = df_vd.loc[off_mask, 'ID_smooth'].idxmin()
1317 else:
1318 idx_ioff = df_vd['ID_smooth'].idxmin()
1319 ioff = df_vd.loc[idx_ioff, 'ID_smooth']
1320
1321 mask_s = (df_vd['VG'] < vth_for_subthreshold) & (df_vd['ID_smooth'] > ioff * 3) & (df_vd['dlogID'] > 0.05)
1322 df_s = df_vd[mask_s]
1323 s_min, vg_smin, id_smin, von, idx_smin = np.nan, np.nan, np.nan, np.nan, None
1324 if not df_s.empty:
1325 idx_smin = df_s['S_val'].idxmin()
1326 s_min = df_s.loc[idx_smin, 'S_val']
1327 vg_smin = df_s.loc[idx_smin, 'VG']
1328 id_smin = df_s.loc[idx_smin, 'ID_smooth']
1329 von = vg_smin + s_min * (np.log10(args.Imin) - np.log10(id_smin))
1330
1331 idx_ids, row_ids = nearest_row_by_current(df_vd, args.ID_S)
1332 legacy_vth = vth_rec if np.isfinite(vth_rec) else vth_sat
1333 legacy_mu = mu_rec if np.isfinite(mu_rec) else mu_sat_max
1334
1335 res = {
1336 'VD': vd_val,
1337 'Vth': legacy_vth,
1338 'mu_max': legacy_mu,
1339 'mu_type': recommended_method,
1340 'recommended_method': recommended_method,
1341 'recommended_warning': recommended_warning,
1342 'recommended_Vth': vth_rec,
1343 'recommended_mu': mu_rec,
1344 'Vth_lin': vth_lin,
1345 'v_lin_slope': v_lin_slope,
1346 'ID_lin_slope': id_lin_slope,
1347 'gm_max': gm_max,
1348 'mu_lin_slope': mu_lin_slope,
1349 'mu_lin_max': mu_lin_max,
1350 'VG_mu_lin_max': VG_mu_lin_max,
1351 'ID_mu_lin_max': ID_mu_lin_max,
1352 'lin_region_ok': lin_region['ok'],
1353 'lin_region_warning': lin_region['warning'],
1354 'lin_region_ratio_VGminusVth_over_VD': lin_region['ratio'],
1355 'lin_region_criterion': lin_region['criterion'],
1356 'lin_region_VG_minus_Vth': lin_region['VG_minus_Vth'],
1357 'Vth_sat': vth_sat,
1358 'v_sat_slope': v_sat_slope,
1359 'ID_sat_slope': id_sat_slope,
1360 'sqrtID_sat_slope': sqrtID_sat_slope,
1361 'dsqrtID_max': dsqrtID_max,
1362 'mu_sat_slope': mu_sat_slope,
1363 'mu_sat_max': mu_sat_max,
1364 'VG_mu_sat_max': VG_mu_sat_max,
1365 'ID_mu_sat_max': ID_mu_sat_max,
1366 'sat_region_ok': sat_region['ok'],
1367 'sat_region_warning': sat_region['warning'],
1368 'sat_region_ratio_VD_over_VGminusVth': sat_region['ratio'],
1369 'sat_region_criterion': sat_region['criterion'],
1370 'sat_region_VG_minus_Vth': sat_region['VG_minus_Vth'],
1371 'Ioff': ioff,
1372 'Smin': s_min,
1373 'VG_Smin': vg_smin,
1374 'id_smin': id_smin,
1375 'Von': von,
1376 'VG_ID_S': row_ids['VG'] if row_ids is not None else np.nan,
1377 'ID_S_target': args.ID_S,
1378 'Imin_target': args.Imin,
1379 'ID_S_val': row_ids['ID_smooth'] if row_ids is not None else np.nan,
1380 'S_ID_S': row_ids['S_val'] if row_ids is not None else np.nan,
1381 'savgol_window': win,
1382 'vg_step': vg_step,
1383 'idx_vg_selected': args.idx_vg,
1384 'idx_lin_slope_max': idx_lin_slope_max,
1385 'idx_sat_slope_max': idx_sat_slope_max,
1386 'idx_slope_max': idx_sat_slope_max,
1387 'idx_smin': idx_smin,
1388 'idx_ids': idx_ids,
1389 'idx_mu_lin_max': idx_mu_lin_max,
1390 'idx_mu_sat_max': idx_mu_sat_max,
1391 'idx_mu_max': idx_mu_lin_max if recommended_method == 'linear' else idx_mu_sat_max,
1392 'idx_ioff': idx_ioff,
1393 'df': df_vd,
1394 }
1395 res['analysis_points'] = build_analysis_points(res)
1396 return res
1397
1398
1399def annotate_vline(ax, x, label, ymin=None, ymax=None):
1400 """概要:
1401 Matplotlibのプロットに垂直線とテキストアノテーションを追加します。
1402 詳細説明:
1403 指定されたX座標 (x) に垂直線 (axvline) を引き、
1404 その線の近くにテキストラベル (label) を回転させて配置します。
1405 Y軸の範囲 (ymin, ymax) が指定されていない場合、現在のプロットのY軸範囲が使用されます。
1406 引数:
1407 :param ax: プロット対象のMatplotlib Axesオブジェクト。
1408 :type ax: matplotlib.axes.Axes
1409 :param x: 垂直線を引くX座標。
1410 :type x: float
1411 :param label: 垂直線の横に表示するテキストラベル。
1412 :type label: str
1413 :param ymin: 垂直線が描画されるY軸の下限。Noneの場合、現在のY軸の下限が使用されます。
1414 :type ymin: float or None
1415 :param ymax: 垂直線が描画されるY軸の上限。Noneの場合、現在のY軸の上限が使用されます。
1416 :type ymax: float or None
1417 戻り値:
1418 :returns: なし
1419 :rtype: None
1420 """
1421 ax.axvline(x, linestyle=':', linewidth=1, alpha=0.8)
1422 if ymin is None or ymax is None:
1423 ymin, ymax = ax.get_ylim()
1424 ax.text(x, ymax, label, rotation=90, va='top', ha='right', fontsize=8)
1425
1426
1427def plot_idvg_quad(res, args):
1428 """概要:
1429 伝達特性解析結果を 2x2 サブプロットとして可視化します。
1430 詳細説明:
1431 ID-VGデータに基づいて計算された様々なデバイス特性(ID-VG曲線、線形抽出、飽和抽出、移動度プロファイル)を
1432 2x2のサブプロットとして表示します。各プロットには、Vth、Smin、移動度最大値などの主要な解析ポイントが
1433 アノテーションとして表示されます。プロットはファイルに保存することも可能です。
1434 引数:
1435 :param res: analyze_vg_core 関数によって生成された、単一VDスライスの解析結果を含む辞書。
1436 :type res: dict
1437 :param args: コマンドライン引数を含むオブジェクト。save_plot および plot_dir 属性を使用します。
1438 :type args: argparse.Namespace
1439 戻り値:
1440 :returns: 生成されたMatplotlibのFigureオブジェクト。
1441 :rtype: matplotlib.figure.Figure
1442 """
1443 df = res['df']
1444 fig, axes = plt.subplots(2, 2, figsize=figsize_idvg_quad)
1445 fig.suptitle(f"n-ch Transfer Analysis (VD = {res['VD']} V)", fontsize=14)
1446
1447 ax = axes[0, 0]
1448 ax.plot(df['VG'], df['ID_smooth'], '-', lw=2, label='smoothed ID')
1449 ax.set_yscale('log')
1450 ax.set_xlabel(r'$V_G$ [V]')
1451 ax.set_ylabel(r'$I_D$ [A]')
1452 ax.set_title('ID-VG / subthreshold')
1453 ax.grid(True, alpha=0.15)
1454 if np.isfinite(res['Smin']):
1455 v_p = np.linspace(res['Von'], res['VG_Smin'], 30)
1456 id_p = 10 ** (np.log10(res['id_smin']) + (v_p - res['VG_Smin']) / res['Smin'])
1457 ax.plot(v_p, id_p, '--', label=f"Smin={res['Smin']:.3g} V/dec")
1458 ax.scatter(res['VG_Smin'], res['id_smin'], marker='o', s=55, label=f"Smin VG={res['VG_Smin']:.2f}")
1459 ax.scatter(res['Von'], args.Imin, marker='x', s=70, label=f"Von={res['Von']:.2f} V")
1460 annotate_vline(ax, res['VG_Smin'], 'Smin')
1461 if np.isfinite(res['VG_ID_S']):
1462 ax.scatter(res['VG_ID_S'], res['ID_S_val'], marker='s', s=50, label=f"ID_S S={res['S_ID_S']:.3g}")
1463 annotate_vline(ax, res['VG_ID_S'], 'ID_S')
1464 if np.isfinite(res['Vth_lin']):
1465 annotate_vline(ax, res['Vth_lin'], f"Vth_lin={res['Vth_lin']:.2f}")
1466 if np.isfinite(res['Vth_sat']):
1467 annotate_vline(ax, res['Vth_sat'], f"Vth_sat={res['Vth_sat']:.2f}")
1468 ax.legend(fontsize='x-small')
1469
1470 ax = axes[0, 1]
1471 ax.plot(df['VG'], df['ID_smooth'], '-', lw=2, label=r'$I_D$')
1472 if np.isfinite(res['Vth_lin']) and np.isfinite(res['gm_max']):
1473 vg_fit = np.linspace(min(res['Vth_lin'], df['VG'].min()), df['VG'].max(), 80)
1474 id_fit = res['gm_max'] * (vg_fit - res['Vth_lin'])
1475 ax.plot(vg_fit, id_fit, '--', label=f"linear tangent: Vth={res['Vth_lin']:.2f} V")
1476 ax.scatter(res['v_lin_slope'], res['ID_lin_slope'], marker='^', s=60,
1477 label=f"max gm VG={res['v_lin_slope']:.2f}")
1478 annotate_vline(ax, res['Vth_lin'], 'Vth_lin')
1479 annotate_vline(ax, res['v_lin_slope'], 'max gm')
1480 txt = (f"linear check at mu_lin max:\n"
1481 f"(VG-Vth)/VD={res['lin_region_ratio_VGminusVth_over_VD']:.3g}\n"
1482 f"criterion: {res['lin_region_criterion']}")
1483 if not res['lin_region_ok']:
1484 txt = 'WARNING\n' + txt
1485 ax.text(0.02, 0.98, txt, transform=ax.transAxes, va='top', ha='left', fontsize=8,
1486 bbox=dict(boxstyle='round', alpha=0.15))
1487 ax.set_xlabel(r'$V_G$ [V]')
1488 ax.set_ylabel(r'$I_D$ [A]')
1489 ax.set_title('Linear extraction: ID-VG tangent')
1490 ax.legend(fontsize='x-small')
1491 ax.grid(True, alpha=0.15)
1492
1493 ax = axes[1, 0]
1494 ax.plot(df['VG'], df['sqrtID'], '-', lw=2, label=r'$\sqrt{I_D}$')
1495 if np.isfinite(res['Vth_sat']) and np.isfinite(res['dsqrtID_max']):
1496 vg_fit = np.linspace(min(res['Vth_sat'], df['VG'].min()), df['VG'].max(), 80)
1497 ax.plot(vg_fit, res['dsqrtID_max'] * (vg_fit - res['Vth_sat']), '--',
1498 label=f"sat tangent: Vth={res['Vth_sat']:.2f} V")
1499 ax.scatter(res['v_sat_slope'], res['sqrtID_sat_slope'], marker='^', s=60,
1500 label=f"max slope VG={res['v_sat_slope']:.2f}")
1501 annotate_vline(ax, res['Vth_sat'], 'Vth_sat')
1502 annotate_vline(ax, res['v_sat_slope'], 'max slope')
1503 txt = (f"sat check at mu_sat max:\n"
1504 f"VD/(VG-Vth)={res['sat_region_ratio_VD_over_VGminusVth']:.3g}\n"
1505 f"criterion: {res['sat_region_criterion']}")
1506 if not res['sat_region_ok']:
1507 txt = 'WARNING\n' + txt
1508 ax.text(0.02, 0.98, txt, transform=ax.transAxes, va='top', ha='left', fontsize=8,
1509 bbox=dict(boxstyle='round', alpha=0.15))
1510 ax.set_xlabel(r'$V_G$ [V]')
1511 ax.set_ylabel(r'$\sqrt{I_D}$ [A$^{0.5}$]')
1512 ax.set_title('Saturation extraction: sqrt(ID)-VG tangent')
1513 ax.legend(fontsize='x-small')
1514 ax.grid(True, alpha=0.15)
1515
1516 ax = axes[1, 1]
1517 ax.plot(df['VG'], df['muFE'], '-', lw=2, label=r'$\mu_{lin}$ from $dI_D/dV_G$')
1518 ax.plot(df['VG'], df['muSAT_prof'], '--', lw=2, label=r'$\mu_{sat}$ from $d\sqrt{I_D}/dV_G$')
1519 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}")
1520 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}")
1521 if np.isfinite(res['v_lin_slope']):
1522 ax.scatter(res['v_lin_slope'], res['mu_lin_slope'], marker='^', s=55, label=f"mu_lin_slope={res['mu_lin_slope']:.3g}")
1523 if np.isfinite(res['v_sat_slope']):
1524 ax.scatter(res['v_sat_slope'], res['mu_sat_slope'], marker='v', s=55, label=f"mu_sat_slope={res['mu_sat_slope']:.3g}")
1525 if np.isfinite(res['Vth_lin']):
1526 annotate_vline(ax, res['Vth_lin'], 'Vth_lin')
1527 if np.isfinite(res['Vth_sat']):
1528 annotate_vline(ax, res['Vth_sat'], 'Vth_sat')
1529 txt = f"recommended: {res['recommended_method']}\n{res['recommended_warning']}"
1530 if res['recommended_method'] in ('intermediate', 'ambiguous'):
1531 txt = 'WARNING\n' + txt
1532 ax.text(0.02, 0.98, txt, transform=ax.transAxes, va='top', ha='left', fontsize=8,
1533 bbox=dict(boxstyle='round', alpha=0.15))
1534 ax.set_xlabel(r'$V_G$ [V]')
1535 ax.set_ylabel(r'Mobility [cm$^2$/Vs]')
1536 ax.set_title('Mobility profiles: linear and saturation formulas')
1537 ax.legend(fontsize='x-small')
1538 ax.grid(True, alpha=0.15)
1539
1540 fig.tight_layout()
1541 if args.save_plot:
1542 os.makedirs(args.plot_dir, exist_ok=True)
1543 fname = plot_path(args, f"idvg_VD_{res['VD']:g}".replace('.', 'p'))
1544 fig.savefig(fname, dpi=200, bbox_inches='tight')
1545 print(f' plot saved: {fname}')
1546 return fig
1547
1548
1549plot_idvg_triple = plot_idvg_quad
1550
1551
1552def print_transfer_report(res):
1553 """概要:
1554 伝達特性解析結果をコンソールに整形して出力します。
1555 詳細説明:
1556 analyze_vg_core 関数によって生成された伝達特性の解析結果辞書 (res) から、
1557 線形領域と飽和領域の閾値電圧 (Vth)、移動度 (mu)、サブスレッショルドスイング (Smin)、
1558 オフ電流 (Ioff) などの主要なデバイスパラメータを抽出し、
1559 コンソールに分かりやすい形式で詳細なレポートを出力します。
1560 また、各移動度抽出点における動作領域の適合性チェック結果も表示されます。
1561 引数:
1562 :param res: 伝達特性解析結果を含む辞書。
1563 :type res: dict
1564 戻り値:
1565 :returns: なし
1566 :rtype: None
1567 """
1568 print(f"VD = {res['VD']:8.3g} V (n-ch assumption)")
1569 print(" Linear-region extraction from ID-VG")
1570 print(f" Vth_lin : {res['Vth_lin']:10.4g} V")
1571 print(f" tangent point : VG={res['v_lin_slope']:10.4g} V, ID={res['ID_lin_slope']:10.4e} A, "
1572 f"gm={res['gm_max']:10.4e} A/V")
1573 print(f" mu_lin_slope : {res['mu_lin_slope']:10.4g} cm^2/Vs")
1574 print(f" mu_lin_max : {res['mu_lin_max']:10.4g} cm^2/Vs at VG={res['VG_mu_lin_max']:10.4g} V, "
1575 f"ID={res['ID_mu_lin_max']:10.4e} A")
1576 print(f" linear check : {res['lin_region_criterion']}; VG_mu_lin_max - Vth_lin = "
1577 f"{res['lin_region_VG_minus_Vth']:.4g} V, (VG-Vth)/VD = "
1578 f"{res['lin_region_ratio_VGminusVth_over_VD']:.4g}, OK={res['lin_region_ok']}")
1579 if not res['lin_region_ok']:
1580 print(f" WARNING : {res['lin_region_warning']}")
1581
1582 print(" Saturation-region extraction from sqrt(ID)-VG")
1583 print(f" Vth_sat : {res['Vth_sat']:10.4g} V")
1584 print(f" tangent point : VG={res['v_sat_slope']:10.4g} V, ID={res['ID_sat_slope']:10.4e} A, "
1585 f"sqrtID={res['sqrtID_sat_slope']:10.4e}, d sqrtID/dVG={res['dsqrtID_max']:10.4e}")
1586 print(f" mu_sat_slope : {res['mu_sat_slope']:10.4g} cm^2/Vs")
1587 print(f" mu_sat_max : {res['mu_sat_max']:10.4g} cm^2/Vs at VG={res['VG_mu_sat_max']:10.4g} V, "
1588 f"ID={res['ID_mu_sat_max']:10.4e} A")
1589 print(f" saturation check : {res['sat_region_criterion']}; VG_mu_sat_max - Vth_sat = "
1590 f"{res['sat_region_VG_minus_Vth']:.4g} V, VD/(VG-Vth) = "
1591 f"{res['sat_region_ratio_VD_over_VGminusVth']:.4g}, OK={res['sat_region_ok']}")
1592 if not res['sat_region_ok']:
1593 print(f" WARNING : {res['sat_region_warning']}")
1594
1595 print(f" Recommended method : {res['recommended_method']}")
1596 print(f" Recommendation : {res['recommended_warning']}")
1597 if res['recommended_method'] in ('intermediate', 'ambiguous'):
1598 print(" WARNING : mobility/Vth representative value is not recommended for this VD slice.")
1599 else:
1600 print(f" Recommended Vth : {res['recommended_Vth']:10.4g} V")
1601 print(f" Recommended mu : {res['recommended_mu']:10.4g} cm^2/Vs")
1602
1603 print(f" Ioff : {res['Ioff']:10.4e} A")
1604 if np.isfinite(res['Smin']):
1605 print(f" Smin : {res['Smin']:10.4g} V/dec at VG={res['VG_Smin']:10.4g} V, "
1606 f"ID={res['id_smin']:10.4e} A")
1607 print(f" Von from Smin : {res['Von']:10.4g} V at Imin={res['Imin_target']:10.4e} A")
1608 else:
1609 print(" Smin : not found")
1610 print(f" S at ID_S : {res['S_ID_S']:10.4g} V/dec at target ID={res['ID_S_target']:10.4e} A, "
1611 f"nearest VG={res['VG_ID_S']:10.4g} V, ID={res['ID_S_val']:10.4e} A")
1612 print(f" Savitzky-Golay : window={res['savgol_window']}")
1613 print('-' * 72)
1614
1615
1616
1617def interpolate_id_at_vd(df_vg, vd_target):
1618 """概要:
1619 単一のVG出力曲線内で、要求されたVD値におけるIDの絶対値を線形補間します。
1620 詳細説明:
1621 与えられたデータフレーム (df_vg) が特定のVGでのID-VDデータを含んでいると仮定し、
1622 vd_target における abs(ID) の絶対値を線形補間によって推定します。
1623 vd_target が測定範囲外の場合、またはデータが不足している場合は np.nan を返します。
1624 補間はVDとID列をソートした後に行われます。
1625 引数:
1626 :param df_vg: 単一のVG値におけるID-VDデータを含むDataFrame。VDとID列が必要です。
1627 :type df_vg: pandas.DataFrame
1628 :param vd_target: 補間したい目標ドレイン電圧 [V]。
1629 :type vd_target: float
1630 戻り値:
1631 :returns: vd_target における補間された abs(ID) の絶対値。データが利用できない場合や範囲外の場合は np.nan。
1632 :rtype: float
1633 """
1634 if df_vg.empty or not np.isfinite(vd_target):
1635 return np.nan
1636 tmp = df_vg[["VD", "ID"]].dropna().sort_values("VD")
1637 if tmp.empty:
1638 return np.nan
1639 vd = tmp["VD"].to_numpy(dtype=float)
1640 id_abs = np.abs(tmp["ID"].to_numpy(dtype=float))
1641 if vd_target < np.nanmin(vd) or vd_target > np.nanmax(vd):
1642 return np.nan
1643 return float(np.interp(vd_target, vd, id_abs))
1644
1645def analyze_idvd_logic(df, args, cox):
1646 """概要:
1647 TFTの出力特性 (ID-VD) を解析し、デバイスの線形領域特性と移動度を評価します。
1648 詳細説明:
1649 入力されたID-VDデータフレームから、以下の解析を実行します:
1650 1. 最大のVD値における伝達特性スライス (df_full) を用いて、参照閾値電圧 (vth_ref) を抽出します。
1651 2. 各VG値におけるID-VD曲線に対して、線形領域(低いVD)での伝達コンダクタンス (gd) を線形回帰で計算します。
1652 3. 低いVDスライスにおけるID-VGデータから、相互コンダクタンス (gm) とS値を計算します。
1653 4. 抽出されたgdとgm、および vth_ref を用いて、実効移動度 (mu_eff) と線形移動度 (mu_lin) を導出します。
1654 5. 各移動度抽出点に対して線形領域条件のチェック (region_check_linear) を行い、警告情報を記録します。
1655 6. 解析結果の概要をコンソールに出力し、出力曲線と移動度プロットを生成します。
1656 引数:
1657 :param df: ID-VDデータを含むDataFrame。VG, VD, ID列が必要です。
1658 :type df: pandas.DataFrame
1659 :param args: コマンドライン引数を含むオブジェクト。
1660 idx_vg, idx_vd, Imin, L, W, region_factor, save_plot, plot_dir 属性を使用します。
1661 :type args: argparse.Namespace
1662 :param cox: 単位面積あたりのゲート酸化膜容量 [F/cm^2]。
1663 :type cox: float
1664 戻り値:
1665 :returns: 各VGにおける解析結果の辞書リストと、生成されたMatplotlibのFigureオブジェクトのタプル。
1666 有効なデータがない場合は ([], None) を返します。
1667 :rtype: tuple[list[dict], matplotlib.figure.Figure or None]
1668 """
1669 max_vd = df['VD'].max()
1670 res_v_ref = analyze_vg_core(df, max_vd, args, cox)
1671 vth_ref = res_v_ref['Vth'] if res_v_ref else 0.0
1672 positive_vd = sorted(df[df['VD'] > 0]['VD'].unique())
1673 if not positive_vd:
1674 print('WARNING: no positive VD data found for n-ch output analysis.')
1675 return [], None
1676 low_vd = positive_vd[0]
1677 df_low = df[np.isclose(df['VD'], low_vd, atol=1e-3)].copy()
1678 df_low = select_sweep_segment(df_low, 'VG', args.idx_vg, 'idx_vg', sort_after=True)
1679 df_low['gm'] = df_low['ID'].abs().diff() / df_low['VG'].diff()
1680 df_low['logID'] = np.log10(df_low['ID'].abs().clip(lower=args.Imin))
1681 df_low['S_val'] = 1.0 / (df_low['logID'].diff() / df_low['VG'].diff())
1682
1683 summary, unique_vgs = [], sorted(df['VG'].unique())
1684 fig, axes = plt.subplots(1, 2, figsize=(15, 6))
1685 print(f"\n{'=' * 20} ID-VD OUTPUT ANALYSIS {'=' * 20}")
1686 print(f"Using Vth_ref = {vth_ref:.4g} V from VD={max_vd:.4g} V transfer-like slice")
1687 print(f"Low-VD slice for gm = {low_vd:.4g} V")
1688 print(f"Selected sweep segments: idx_vg={args.idx_vg}, idx_vd={args.idx_vd}")
1689
1690 for vg in unique_vgs:
1691 df_vg_all = df[np.isclose(df['VG'], vg, atol=1e-3)].copy()
1692 df_vg = select_sweep_segment(df_vg_all, 'VD', args.idx_vd, 'idx_vd', sort_after=True)
1693 if len(df_vg) < 3:
1694 continue
1695 fit_df = df_vg[df_vg['VD'] <= low_vd + 2]
1696 if len(fit_df) < 2:
1697 continue
1698 gd = LinearRegression().fit(fit_df['VD'].values.reshape(-1, 1), fit_df['ID'].abs().values).coef_[0]
1699 gm_row = df_low[np.isclose(df_low['VG'], vg, atol=1e-3)]
1700 gm = gm_row['gm'].values[0] if not gm_row.empty else 0.0
1701 denom = max(0.1, vg - vth_ref)
1702 mue = (gd * args.L) / (args.W * cox * denom)
1703 mul = (gm * args.L) / (args.W * cox * low_vd)
1704 s_val = gm_row['S_val'].values[0] if not gm_row.empty else np.nan
1705 lin_region = region_check_linear(low_vd, vg, vth_ref, args.region_factor)
1706 summary.append({'VG': vg, 'gd': gd, 'gm': gm, 'mu_eff': mue, 'mu_lin': mul, 'S_val': s_val,
1707 'Vth_ref': vth_ref, 'low_VD_for_gm': low_vd, 'fit_VD_max_for_gd': low_vd + 2,
1708 'linear_region_ok': lin_region['ok'],
1709 'linear_region_warning': lin_region['warning'],
1710 'linear_region_ratio_VGminusVth_over_VD': lin_region['ratio'],
1711 'linear_region_criterion': lin_region['criterion'],
1712 'VG_minus_Vth': lin_region['VG_minus_Vth'],
1713 'idx_vd_selected': args.idx_vd,
1714 'idx_vg_selected_for_gm': args.idx_vg})
1715 axes[0].plot(df_vg['VD'], df_vg['ID'].abs(), alpha=0.45)
1716 print(f" VG={vg:8.3g} V | gd={gd:10.4e} A/V | gm={gm:10.4e} A/V | "
1717 f"mu_eff={mue:10.4g} | mu_lin={mul:10.4g} | S={s_val:10.4g} | "
1718 f"linear OK={lin_region['ok']} ratio={(lin_region['ratio'] if np.isfinite(lin_region['ratio']) else np.nan):.4g}")
1719 if not lin_region['ok']:
1720 print(f" WARNING: {lin_region['warning']} criterion: {lin_region['criterion']}")
1721
1722 sdf = pd.DataFrame(summary)
1723 if not sdf.empty:
1724 # Saturation-boundary markers on output curves: VD_sat = VG - Vth_ref.
1725 vd_sat_list, id_sat_list = [], []
1726 for vg in sdf['VG'].to_numpy(dtype=float):
1727 vd_sat = vg - vth_ref
1728 df_vg_all = df[np.isclose(df['VG'], vg, atol=1e-3)].copy()
1729 df_vg = select_sweep_segment(df_vg_all, 'VD', args.idx_vd, 'idx_vd', sort_after=True)
1730 id_sat = interpolate_id_at_vd(df_vg, vd_sat)
1731 vd_sat_list.append(vd_sat)
1732 id_sat_list.append(id_sat)
1733 sdf['VD_sat_boundary'] = vd_sat_list
1734 sdf['ID_at_VD_sat_boundary'] = id_sat_list
1735 for i, row in sdf.iterrows():
1736 summary[i]['VD_sat_boundary'] = row['VD_sat_boundary']
1737 summary[i]['ID_at_VD_sat_boundary'] = row['ID_at_VD_sat_boundary']
1738
1739 good_boundary = sdf[np.isfinite(sdf['ID_at_VD_sat_boundary']) & (sdf['VD_sat_boundary'] >= 0)]
1740 if not good_boundary.empty:
1741 axes[0].plot(good_boundary['VD_sat_boundary'], good_boundary['ID_at_VD_sat_boundary'],
1742 '^--', lw=1.2, ms=6, label=r'saturation boundary $V_D=V_G-V_{th}$')
1743
1744 axes[0].set_xlabel(r'$V_D$ [V]')
1745 axes[0].set_ylabel(r'$|I_D|$ [A] (linear scale)')
1746 axes[0].set_title('Output curves')
1747 axes[0].legend(fontsize='x-small')
1748 axes[0].grid(True, alpha=0.15)
1749 axes[1].plot(sdf['VG'], sdf['mu_eff'], 'o-', label='mu_eff from gd')
1750 axes[1].plot(sdf['VG'], sdf['mu_lin'], 's--', label='mu_lin from gm')
1751 axes[1].axvline(vth_ref, linestyle=':', linewidth=1, label='Vth_ref')
1752 bad = sdf[~sdf['linear_region_ok']] if 'linear_region_ok' in sdf.columns else pd.DataFrame()
1753 if not bad.empty:
1754 axes[1].scatter(bad['VG'], bad['mu_lin'], marker='x', s=75, label='linear-region warning')
1755 axes[1].text(0.02, 0.98, f"linear check: VG-Vth >= {args.region_factor:g}*VD_low",
1756 transform=axes[1].transAxes, va='top', ha='left', fontsize=8,
1757 bbox=dict(boxstyle='round', alpha=0.15))
1758 axes[1].set_xlabel(r'$V_G$ [V]')
1759 axes[1].set_ylabel(r'Mobility [cm$^2$/Vs]')
1760 axes[1].legend()
1761 axes[1].grid(True, alpha=0.15)
1762 fig.tight_layout()
1763 if args.save_plot:
1764 os.makedirs(args.plot_dir, exist_ok=True)
1765 fname = plot_path(args, 'idvd_output_analysis')
1766 fig.savefig(fname, dpi=200, bbox_inches='tight')
1767 print(f' plot saved: {fname}')
1768 return summary, fig
1769
1770
1771def make_summary_dataframe(t_summary):
1772 """概要:
1773 伝達特性解析結果のリストから、Excel出力用のPandas DataFrameを作成します。
1774 詳細説明:
1775 t_summary リスト内の各解析結果辞書から、Excelサマリーシートに適した
1776 主要なパラメータを抽出します。
1777 元のデータフレーム (df) や詳細な分析ポイント (analysis_points)、
1778 および内部的なインデックス (idx_ で始まるキー) は除外されます。
1779 結果はPandas DataFrameとして整形され、Excelへのエクスポートに適した形式で提供されます。
1780 引数:
1781 :param t_summary: 各VDスライスでの伝達特性解析結果を含む辞書のリスト。
1782 :type t_summary: list[dict]
1783 戻り値:
1784 :returns: 主要な解析結果パラメータを含むPandas DataFrame。
1785 :rtype: pandas.DataFrame
1786 """
1787 rows = []
1788 for res in t_summary:
1789 row = {k: v for k, v in res.items() if k not in ('df', 'analysis_points') and not str(k).startswith('idx_')}
1790 rows.append(row)
1791 return pd.DataFrame(rows)
1792
1793
1794def run_analysis(args):
1795 """概要:
1796 スクリプトのメイン実行ロジックをカプセル化します。
1797 詳細説明:
1798 この関数は、コマンドライン引数をパースし、ゲート酸化膜容量 (Cox) を計算します。
1799 選択された解析モード (read, analyze_idvg, analyze_idvd, all) に応じて、
1800 対応するデータ読み込み、解析、プロット生成の関数を呼び出します。
1801 すべての結果(生データ、平滑化データ、解析サマリー、詳細分析ポイント)は集約され、
1802 最終的に単一のExcelレポートファイルとPNGプロットとして出力されます。
1803 プロットは --show_plot が指定されている場合、インタラクティブに表示されます。
1804 引数:
1805 :param args: コマンドライン引数を含む argparse.Namespace オブジェクト。
1806 :type args: argparse.Namespace
1807 戻り値:
1808 :returns: なし。解析結果はファイルシステムに出力されます。
1809 :rtype: None
1810 """
1811 # args is prepared by main(): output paths and logging are already configured.
1812 cox = calculate_cox(args.dg, args.epsg)
1813 t_summary, t_data, t_points, o_summary = [], [], [], []
1814 read_tables, read_summary = {}, []
1815 figures = []
1816
1817 print('TFT analysis mode: n-channel dedicated')
1818 print(f'Cox = {cox:.6e} F/cm^2 (dg={args.dg:g} nm, epsg={args.epsg:g})')
1819 if args.reverse_vg:
1820 print('VG sign reversal is enabled after data loading.')
1821
1822 if args.mode == 'read':
1823 figs, read_tables, read_summary = run_read_mode(args)
1824 figures.extend(figs)
1825
1826 if args.mode in ['analyze_idvg', 'all']:
1827 df_vg = detect_and_load(args.infile_vg, reverse_vg=args.reverse_vg)
1828 if df_vg is not None:
1829 print(f"\n{'=' * 20} DETAILED TRANSFER ANALYSIS {'=' * 20}")
1830 for vd in sorted(df_vg['VD'].unique()):
1831 if vd <= 0:
1832 print(f'Skip VD={vd:g} V because n-ch analysis expects positive VD.')
1833 continue
1834 res = analyze_vg_core(df_vg, vd, args, cox)
1835 if res:
1836 print_transfer_report(res)
1837 t_summary.append(res)
1838 res['df']['VD_label'] = vd
1839 t_data.append(res['df'])
1840 t_points.extend(res['analysis_points'])
1841 figures.append(plot_idvg_triple(res, args))
1842
1843 if args.mode in ['analyze_idvd', 'all']:
1844 df_vd = detect_and_load(args.infile_vd, reverse_vg=args.reverse_vg)
1845 if df_vd is not None:
1846 o_summary, fig = analyze_idvd_logic(df_vd, args, cox)
1847 if fig is not None:
1848 figures.append(fig)
1849
1850 with pd.ExcelWriter(args.out_excel) as writer:
1851 pd.DataFrame([{
1852 'analysis_assumption': 'n-channel TFT; positive VD; use --reverse_vg for p-channel preprocessing',
1853 'L_um': args.L,
1854 'W_um': args.W,
1855 'dg_nm': args.dg,
1856 'epsg': args.epsg,
1857 'Cox_F_per_cm2': cox,
1858 'ID_S_A': args.ID_S,
1859 'Imin_A': args.Imin,
1860 'smooth_npoints_requested': args.smooth_npoints,
1861 'lsq_order': args.lsq_order,
1862 'region_factor': args.region_factor,
1863 'idx_vg': args.idx_vg,
1864 'idx_vd': args.idx_vd,
1865 'plot_dir': args.plot_dir if args.save_plot else '',
1866 }]).to_excel(writer, sheet_name='Analysis_Settings', index=False)
1867 if read_summary:
1868 pd.DataFrame(read_summary).to_excel(writer, sheet_name='Read_Summary', index=False)
1869 for tag, rdf in read_tables.items():
1870 sheet = 'Read_Data_' + tag.upper()
1871 rdf.to_excel(writer, sheet_name=sheet[:31], index=False)
1872 if t_summary:
1873 make_summary_dataframe(t_summary).to_excel(writer, sheet_name='Summary_Transfer', index=False)
1874 if t_points:
1875 pd.DataFrame(t_points).to_excel(writer, sheet_name='Analysis_Points', index=False)
1876 if t_data:
1877 pd.concat(t_data).to_excel(writer, sheet_name='Data_Transfer_VG_Dep', index=False)
1878 if o_summary:
1879 pd.DataFrame(o_summary).to_excel(writer, sheet_name='Data_Output_VG_Dep', index=False)
1880 print(f'Report saved to {args.out_excel}')
1881
1882 # Excel and PNG files are saved before this interactive display.
1883 if args.show_plot and figures:
1884 plt.show()
1885 else:
1886 for fig in figures:
1887 plt.close(fig)
1888
1889
1890def main():
1891 """概要:
1892 プログラムのエントリポイントです。
1893 詳細説明:
1894 この関数は、コマンドライン引数をパースし、Excel、PNG、ログファイルの
1895 出力パスを準備します。標準出力と標準エラー出力をコンソールとログファイルの両方に
1896 出力するように設定した後、run_analysis 関数を呼び出して主要な解析ロジックを実行します。
1897 解析終了後、Tee 機能は解除されます。
1898 戻り値:
1899 :returns: なし
1900 :rtype: None
1901 """
1902 args = get_args()
1903 args = prepare_output_paths(args)
1904 os.makedirs(args.output_dir, exist_ok=True)
1905 original_stdout = sys.stdout
1906 original_stderr = sys.stderr
1907 with open(args.log_file, 'w', encoding='utf-8') as log_fp:
1908 sys.stdout = Tee(original_stdout, log_fp)
1909 sys.stderr = Tee(original_stderr, log_fp)
1910 try:
1911 print(f'Log file: {args.log_file}')
1912 print(f'Output directory: {args.output_dir}')
1913 print(f'Output stem: {args.output_stem}')
1914 run_analysis(args)
1915 finally:
1916 sys.stdout = original_stdout
1917 sys.stderr = original_stderr
1918
1919if __name__ == '__main__':
1920 main()