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