pvfit.py ダウンロード/コピー
pvfit.py
pvfit.py
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3"""
4概要: 拡張一ダイオードモデル(SDM)IVフィッティングツール
5
6詳細説明:
7 このモジュールは、太陽電池や他の半導体デバイスの電流-電圧(IV)特性を、
8 拡張一ダイオードモデル(SDM)を用いてフィッティング、初期値推定、またはシミュレーションするためのツールを提供します。
9 複数の輸送メカニズム(ダイオード、トンネル誘起フォワードエミッション (TFE)、
10 非理想フォワード電流 (FN)、空間電荷制限電流 (SCLC))を組み合わせて利用できます。
11
12主な機能:
13 フォワードバイアスとリバースバイアスのモデルを独立に選択可能。
14 ダイオード、TFE、FN、SCLCの各メカニズムを任意に組み合わせ可能。
15 数値解法は接合電圧 Vd を未知数とする根探しで安定化。
16 モデルに未使用のパラメータは自動的に固定。
17 forwardIV と reverseIV が同一の場合、再計算を回避して効率化。
18 最適化途中のIVカーブをアニメーション表示(常に有効)。
19 線形近似に基づくパラメータ誤差推定。
20 モデル電流の標準偏差から信頼性区間を水色領域で表示。
21
22mode=init の初期値推定の概要:
23 入力IVを電圧昇順にソート。
24 I-Vを局所多項式で平滑化。
25 dI/dV から Rs, Rsh を推定。
26 ISC = I(V=0) を V=0 近傍の多項式で推定。
27 IPV = -ISC。
28 VOC = I(V)=0 の多項式根で推定。
29 Ish = V<0 の代表点 Vsh における電流。
30 I0 = ISC - Ish (シャント電流は差し引かない)。
31 TFE 初期値: V<0 の十分負側で ln(abs(I))-V を一次近似し、E00 = 1/abs(slope)、
32 A_tfe = abs(I) * exp(+V/E00) を代表点から推定。
33 ndiode は固定初期値 1.5。
34 FN / SCLC の初期値は結果に影響しない極小値。
35 mode=init の初期値は fit 用の頑健な初期値です。
36 物理的に厳密な意味づけは mode=fit で精密化してください。
37
38関連リンク:
39 pvfit_usage
40"""
41
42import os
43import sys
44import argparse
45import builtins
46from pathlib import Path
47import traceback
48import csv
49from openpyxl import Workbook
50from openpyxl.styles import Font, PatternFill
51
52import matplotlib.pyplot as plt
53import numpy as np
54from scipy.optimize import brentq, minimize
55
56KB = 1.380649e-23
57E_CHARGE = 1.602176634e-19
58
59PARAM_NAMES = [
60 "I0", "ndiode", "IPV", "Rs", "Rsh",
61 "A_tfe", "E00",
62 "A_fn", "B_fn",
63 "K_sclc",
64]
65LOG_PARAMS = {
66 "I0", "IPV", "Rs", "Rsh",
67 "A_tfe", "E00",
68 "A_fn", "B_fn",
69 "K_sclc",
70}
71
72EPS_I = 1.0e-15
73EPS_R = 1.0e-12
74
75DEFAULT_PARAMETERS = {
76 "I0": 1.0e-10,
77 "ndiode": 1.5,
78 "IPV": 1.0e-6,
79 "Rs": 1.0,
80 "Rsh": 1.0e9,
81 "A_tfe": 1.0e-30,
82 "E00": 1.0e-3,
83 "A_fn": 1.0e-30,
84 "B_fn": 10.0,
85 "K_sclc": 1.0e-30,
86 "VOC_est": 0.5,
87 "ISC_est": -1.0e-6,
88 "Vsh_est": -0.1,
89 "Ish_est": -2.0e-6,
90 "Vtfe_est": -0.2,
91 "Itfe_est": 1.0e-12,
92 }
93
94fontsize = 16
95
96
97def initialize():
98 """
99 概要: コマンドライン引数パーサーを初期化し、引数を解析します。
100
101 詳細説明:
102 各種実行モード (analyze, init, fit, sim)、最適化手法、
103 入力ファイル、温度、データクリッピング範囲、スキップ間隔、
104 フォワード/リバースIVモデル、ブレンド電圧幅、プロット/プリント間隔、
105 最大イテレーション数、許容誤差、固定パラメータなどの引数を定義します。
106
107 引数:
108 なし
109
110 戻り値:
111 :returns: args (解析された引数を含むオブジェクト), parser (ArgumentParserオブジェクト)
112 :rtype: tuple[argparse.Namespace, argparse.ArgumentParser]
113 """
114 parser = argparse.ArgumentParser(description="Extended SDM IV fitting tool with diode/TFE/FN/SCLC.")
115 parser.add_argument("--mode", choices=["analyze", "init", "fit", "sim"], default="fit",
116 help="Execution mode (init: initial parameter estimation, fit: fit parameters to data, sim: simulate IV curve)")
117 parser.add_argument("--method", default="Nelder-Mead",
118 help="scipy.optimize.minimize method (e.g., 'Nelder-Mead', 'Powell', 'BFGS', 'L-BFGS-B')")
119 parser.add_argument("--infile", help="Input IV CSV file path")
120 parser.add_argument("--temperature", type=float, default=300.0,
121 help="Temperature in K for the model")
122 parser.add_argument("--xmin", type=float, help="Minimum voltage (V) for data clipping, fitting, and plotting")
123 parser.add_argument("--xmax", type=float, help="Maximum voltage (V) for data clipping, fitting, and plotting")
124 parser.add_argument("--ndataskip", type=int, default=0, help="# of data to skip reading")
125 parser.add_argument("--forwardIV", default="diode",
126 help="Transport mechanisms for forward-bias side, e.g., 'diode' or 'diode+tfe+sclc'")
127 parser.add_argument("--reverseIV", default="diode+tfe",
128 help="Transport mechanisms for reverse-bias side, e.g., 'diode+tfe' or 'diode+fn'")
129 parser.add_argument("--dv_blend", type=float, default=0.05,
130 help="Sigmoid transition width (V) between reverse and forward models for junction voltage Vd")
131 parser.add_argument("--ninterval_print", type=int, default=10,
132 help="Console print interval during fitting iterations")
133 parser.add_argument("--ninterval_plot", type=int, default=10,
134 help="Animation update interval during fitting iterations")
135 parser.add_argument("--nmaxiter", type=int, default=1000,
136 help="Maximum optimizer iterations")
137 parser.add_argument("--tol", type=float, default=1.0e-7,
138 help="Optimizer tolerance (xatol and fatol)")
139 parser.add_argument("--fix", nargs="*", default=[],
140 help="Space-separated list of parameter names to fix during fitting (e.g., 'I0 ndiode')")
141 parser.add_argument("--outprefix", default="pvfit", help="Output file prefix")
142
143 for p in PARAM_NAMES:
144 parser.add_argument(f"--{p}", type=float, help=f"Override initial value for {p}")
145
146 args = parser.parse_args()
147 args.animate_fit = True # 最適化中のアニメーション表示は常に有効
148
149 return args, parser
150
151# =============================================================================
152# argument repeat / validation
153# =============================================================================
154
155def print_args_and_derived(args):
156 """
157 概要: コマンドライン引数とその派生値をコンソールに出力します。
158
159 詳細説明:
160 ユーザーが指定した入力値と、プログラム内部で決定される追加の設定(例:使用されるパラメータ、自動固定されるパラメータ)
161 を一覧表示し、設定の確認を容易にします。
162
163 引数:
164 :param args: 解析されたコマンドライン引数を含むオブジェクト。
165 :type args: argparse.Namespace
166
167 戻り値:
168 :returns: なし
169 :rtype: None
170 """
171 print("=== Arguments ===")
172 print(f"mode = {args.mode}")
173 print(f"infile = {args.infile}")
174 print(f"temperature = {args.temperature} K")
175 print(f"xmin = {args.xmin} V" if args.xmin is not None else "xmin = None")
176 print(f"xmax = {args.xmax} V" if args.xmax is not None else "xmax = None")
177 print(f"ndataskip = {args.ndataskip}")
178 print(f"forwardIV = {args.forwardIV}")
179 print(f"reverseIV = {args.reverseIV}")
180 print(f"dv_blend = {args.dv_blend} V")
181 print(f"ninterval_print = {args.ninterval_print}")
182 print(f"ninterval_plot = {args.ninterval_plot}")
183 print(f"nmaxiter = {args.nmaxiter}")
184 print(f"tol = {args.tol}")
185 print(f"fix = {' '.join(args.fix) if args.fix else '(none)'}")
186 print(f"outprefix = {args.outprefix}")
187
188 for name in PARAM_NAMES:
189 val = getattr(args, name, None)
190 print(f"{name:12s} = {val}" if val is not None else f"{name:12s} = None")
191 print()
192
193 print("=== Derived values ===")
194 used = used_params_from_modes(args.forwardIV, args.reverseIV)
195 auto_fixed = set(PARAM_NAMES) - used
196 print(f"used_params = {' '.join(sorted(used)) if used else '(none)'}")
197 print(f"auto_fixed = {' '.join(sorted(auto_fixed)) if auto_fixed else '(none)'}")
198 print()
199
200
201def validate_args(args):
202 """
203 概要: コマンドライン引数の有効性を検証します。
204
205 詳細説明:
206 モードごとの必須引数、数値引数の範囲、パラメータ名の整合性などをチェックし、
207 無効な引数が存在する場合は ValueError を発生させます。
208
209 引数:
210 :param args: 解析されたコマンドライン引数を含むオブジェクト。
211 :type args: argparse.Namespace
212
213 戻り値:
214 :returns: なし
215 :rtype: None
216
217 例外:
218 :raises ValueError: 無効な引数が見つかった場合。
219 """
220 if args.mode in ("init", "fit", "analyze"):
221 if not args.infile:
222 raise ValueError(f"--infile is required for mode={args.mode}")
223
224 if args.ndataskip < 0:
225 raise ValueError(f"ndataskip must be >= 0, got {args.ndataskip}")
226
227 if args.xmin is not None and args.xmax is not None and args.xmin > args.xmax:
228 raise ValueError(f"xmin must be <= xmax, got xmin={args.xmin}, xmax={args.xmax}")
229
230 if args.temperature <= 0.0:
231 raise ValueError(f"temperature must be > 0 K, got {args.temperature}")
232
233 if args.dv_blend < 0.0:
234 raise ValueError(f"dv_blend must be >= 0 V, got {args.dv_blend}")
235
236 if args.ninterval_print <= 0:
237 raise ValueError(f"ninterval_print must be > 0, got {args.ninterval_print}")
238
239 if args.ninterval_plot <= 0:
240 raise ValueError(f"ninterval_plot must be > 0, got {args.ninterval_plot}")
241
242 if args.nmaxiter <= 0:
243 raise ValueError(f"nmaxiter must be > 0, got {args.nmaxiter}")
244
245 if args.tol <= 0.0:
246 raise ValueError(f"tol must be > 0, got {args.tol}")
247
248 unknown_fix = [x for x in args.fix if x not in PARAM_NAMES]
249 if unknown_fix:
250 raise ValueError(f"Unknown parameter name(s) in --fix: {unknown_fix}")
251
252 for name in LOG_PARAMS:
253 val = getattr(args, name, None)
254 if val is not None and val <= 0.0:
255 raise ValueError(f"{name} must be > 0 because it is log-scaled, got {val}")
256
257 # 非logパラメータ側
258 if getattr(args, "ndiode", None) is not None and args.ndiode <= 0.0:
259 raise ValueError(f"ndiode must be > 0, got {args.ndiode}")
260
261 if getattr(args, "B_fn", None) is not None and args.B_fn <= 0.0:
262 raise ValueError(f"B_fn must be > 0, got {args.B_fn}")
263
264
265# =============================================================================
266# data I/O
267# =============================================================================
268
269def read_data(infile, xmin=None, xmax=None, ndataskip=0):
270 """
271 概要: 指定されたCSVファイルからIVデータ(電圧と電流)を読み込みます。
272
273 詳細説明:
274 ファイルからメタデータを抽出し、電圧昇順にデータをソートし、
275 指定された電圧範囲でクリッピングし、データスキップ間隔を適用します。
276
277 引数:
278 :param infile: 入力IV CSVファイルのパス。
279 :type infile: str
280 :param xmin: データをクリップする最小電圧 (V)。Noneの場合、下限なし。
281 :type xmin: float or None
282 :param xmax: データをクリップする最大電圧 (V)。Noneの場合、上限なし。
283 :type xmax: float or None
284 :param ndataskip: 読み込み時にスキップするデータ点の数(n+1点ごとにデータを保持)。
285 :type ndataskip: int
286
287 戻り値:
288 :returns: V (読み込まれた電圧データのNumPy配列), I (読み込まれた電流データのNumPy配列), info (ファイル名と記録時間を含む辞書)
289 :rtype: tuple[numpy.ndarray, numpy.ndarray, dict]
290
291 例外:
292 :raises ValueError: 有効なデータが見つからない場合、またはクリッピング後にデータ点が残らない場合。
293 """
294 data_started = False
295 raw_v = []
296 raw_i = []
297 info = {"FileName": infile, "RecordTime": "Unknown"}
298
299 with open(infile, "r", encoding="utf-8-sig", newline="") as f:
300 reader = csv.reader(f)
301 for row in reader:
302 if not row:
303 continue
304
305 cols = [c.strip() for c in row]
306
307 if len(cols) >= 3 and cols[0] == "MetaData" and "RecordTime" in cols[1]:
308 info["RecordTime"] = cols[2]
309
310 if cols[0] == "DataName":
311 data_started = True
312 continue
313
314 if data_started and cols[0] == "DataValue":
315 if len(cols) < 3:
316 continue
317
318 v = float(cols[1])
319 i = float(cols[2])
320 if not np.isfinite(v) or not np.isfinite(i):
321 continue
322 raw_v.append(v)
323 raw_i.append(i)
324
325 if not raw_v:
326 raise ValueError(f"No valid data found in {infile}")
327
328 V = np.asarray(raw_v, dtype=float)
329 I = np.asarray(raw_i, dtype=float)
330
331 # まず V でソート
332 idx = np.argsort(V)
333 V = V[idx]
334 I = I[idx]
335
336 # 次に xmin/xmax でクリップ
337 mask = np.ones(len(V), dtype=bool)
338 if xmin is not None:
339 mask &= (V >= xmin)
340 if xmax is not None:
341 mask &= (V <= xmax)
342
343 V = V[mask]
344 I = I[mask]
345
346 if len(V) == 0:
347 raise ValueError("No data points remain after xmin/xmax clipping.")
348
349 # 最後に ndataskip を適用
350 if ndataskip > 0:
351 keep = np.zeros(len(V), dtype=bool)
352 keep[::(ndataskip + 1)] = True
353 V = V[keep]
354 I = I[keep]
355
356 print(f"File Name : {info['FileName']}")
357 print(f"Record Time : {info['RecordTime']}")
358 print(f"Points : {len(V)}")
359 print(f"V range : {V.min():.6g} to {V.max():.6g} V")
360
361 return V, I, info
362
363
364def load_param_csv(csv_path):
365 """
366 概要: パラメータ設定CSVファイルからパラメータ値と固定設定を読み込みます。
367
368 詳細説明:
369 CSVファイルには 'varname', 'value', 'optid' の列が含まれると想定されます。
370 'optid' が '0' の場合、そのパラメータは固定されます。
371
372 引数:
373 :param csv_path: 読み込むCSVファイルのパス。
374 :type csv_path: str
375
376 戻り値:
377 :returns: params (読み込まれたパラメータ名と値の辞書), fix_set (固定されるパラメータ名のセット)
378 :rtype: tuple[dict, set]
379 """
380 params = {}
381 fix_set = set()
382
383 if os.path.exists(csv_path):
384 with open(csv_path, "r", encoding="utf-8") as f:
385 reader = csv.DictReader(f)
386 for row in reader:
387 name = row.get("varname", "").strip()
388 if not name:
389 continue
390 try:
391 val = float(row.get("value", ""))
392 except (TypeError, ValueError):
393 continue
394
395 params[name] = val
396 try:
397 if int(row.get("optid", 1)) == 0 and name in PARAM_NAMES:
398 fix_set.add(name)
399 except (TypeError, ValueError):
400 pass
401
402 return params, fix_set
403
404
405def save_param_csv(csv_path, params, fix_set, errors=None, rss=None):
406 """
407 概要: 現在のパラメータ値、固定設定、推定誤差、およびRSSをCSVファイルに保存します。
408
409 詳細説明:
410 PARAM_NAMES にリストされている全てのパラメータと、追加の診断情報(VOC_estなど)が保存されます。
411
412 引数:
413 :param csv_path: 保存するCSVファイルのパス。
414 :type csv_path: str
415 :param params: 保存するパラメータ名と値の辞書。
416 :type params: dict
417 :param fix_set: 固定されたパラメータ名のセット。
418 :type fix_set: set
419 :param errors: 各パラメータの推定誤差を含む辞書。Noneの場合、誤差は0.0として保存。
420 :type errors: dict or None
421 :param rss: 残差平方和 (Residual Sum of Squares)。Noneの場合、保存しない。
422 :type rss: float or None
423
424 戻り値:
425 :returns: なし
426 :rtype: None
427 """
428 if errors is None:
429 errors = {}
430
431 with open(csv_path, "w", encoding="utf-8", newline="") as f:
432 writer = csv.writer(f)
433 writer.writerow(["varname", "value", "optid", "error"])
434 for name in PARAM_NAMES:
435 writer.writerow([
436 name,
437 params[name],
438 0 if name in fix_set else 1,
439 errors.get(name, 0.0),
440 ])
441 for extra in [
442 "VOC_est", "ISC_est", "Vsh_est", "Ish_est",
443 "Vtfe_est", "Itfe_est",
444 ]:
445 if extra in params:
446 writer.writerow([extra, params[extra], 0, 0.0])
447 if rss is not None:
448 writer.writerow(["RSS_logy", rss, 0, 0.0])
449
450def save_iv_to_excel(outfile_xlsx, V, I_input, I_init, I_fit, info,
451 params_init=None, params_final=None, rss=None, errors=None):
452 """
453 概要: IVデータ、初期パラメータ、最終パラメータ、およびサマリー情報をExcelファイルに保存します。
454
455 詳細説明:
456 測定データ、初期モデル電流、フィッティング後のモデル電流が線形および対数絶対値スケールで保存されます。
457 パラメータシートには初期値と最終値、および推定誤差が含まれます。
458
459 引数:
460 :param outfile_xlsx: 出力するExcelファイルのパス。
461 :type outfile_xlsx: str
462 :param V: 電圧データのNumPy配列。
463 :type V: numpy.ndarray
464 :param I_input: 測定された電流データのNumPy配列。Noneの場合、保存しない。
465 :type I_input: numpy.ndarray or None
466 :param I_init: 初期モデルによって計算された電流データのNumPy配列。Noneの場合、保存しない。
467 :type I_init: numpy.ndarray or None
468 :param I_fit: フィッティング後のモデルによって計算された電流データのNumPy配列。Noneの場合、保存しない。
469 :type I_fit: numpy.ndarray or None
470 :param info: ファイル名や記録時間などの情報を含む辞書。
471 :type info: dict
472 :param params_init: 初期パラメータ値を含む辞書。Noneの場合、保存しない。
473 :type params_init: dict or None
474 :param params_final: フィッティング後の最終パラメータ値を含む辞書。Noneの場合、保存しない。
475 :type params_final: dict or None
476 :param rss: 残差平方和 (Residual Sum of Squares)。Noneの場合、保存しない。
477 :type rss: float or None
478 :param errors: 各パラメータの推定誤差を含む辞書。Noneの場合、保存しない。
479 :type errors: dict or None
480
481 戻り値:
482 :returns: なし
483 :rtype: None
484 """
485 print(f"[I/O] Save IV Excel: {outfile_xlsx}")
486
487 wb = Workbook()
488
489 header_fill = PatternFill("solid", fgColor="1F4E78")
490 header_font = Font(color="FFFFFF", bold=True)
491
492 def log_abs_current(x):
493 if x is None:
494 return None
495 return np.log10(np.abs(x) + EPS_I)
496
497 # ------------------------------------------------------------
498 # sheet 1: iv_data
499 # ------------------------------------------------------------
500 ws = wb.active
501 ws.title = "iv_data"
502
503 headers = [
504 "V",
505 "I_input", "I_init", "I_fit",
506 "abs_I_input", "abs_I_init", "abs_I_fit",
507 "log10_abs_I_input", "log10_abs_I_init", "log10_abs_I_fit",
508 ]
509 ws.append(headers)
510 for c in ws[1]:
511 c.fill = header_fill
512 c.font = header_font
513
514 n = len(V)
515 for i in range(n):
516 ii = None if I_input is None else float(I_input[i])
517 i0 = None if I_init is None else float(I_init[i])
518 ifit = None if I_fit is None else float(I_fit[i])
519
520 ws.append([
521 float(V[i]),
522 ii, i0, ifit,
523 None if ii is None else abs(ii),
524 None if i0 is None else abs(i0),
525 None if ifit is None else abs(ifit),
526 None if ii is None else float(log_abs_current(ii)),
527 None if i0 is None else float(log_abs_current(i0)),
528 None if ifit is None else float(log_abs_current(ifit)),
529 ])
530
531 for col in ["A","B","C","D","E","F","G","H","I","J"]:
532 ws.column_dimensions[col].width = 18
533
534 # ------------------------------------------------------------
535 # sheet 2: init_params
536 # ------------------------------------------------------------
537 if params_init is not None:
538 ws2 = wb.create_sheet("init_params")
539 ws2.append(["name", "value"])
540 for c in ws2[1]:
541 c.fill = header_fill
542 c.font = header_font
543 for name in PARAM_NAMES:
544 ws2.append([name, float(params_init.get(name, np.nan))])
545 for extra in ["VOC_est", "ISC_est", "Vsh_est", "Ish_est", "Vtfe_est", "Itfe_est"]:
546 if extra in params_init:
547 ws2.append([extra, float(params_init[extra])])
548
549 # ------------------------------------------------------------
550 # sheet 3: final_params
551 # ------------------------------------------------------------
552 if params_final is not None:
553 ws3 = wb.create_sheet("final_params")
554 ws3.append(["name", "value", "error"])
555 for c in ws3[1]:
556 c.fill = header_fill
557 c.font = header_font
558 if errors is None:
559 errors = {}
560 for name in PARAM_NAMES:
561 ws3.append([
562 name,
563 float(params_final.get(name, np.nan)),
564 float(errors.get(name, np.nan)),
565 ])
566
567 # ------------------------------------------------------------
568 # sheet 4: summary
569 # ------------------------------------------------------------
570 ws4 = wb.create_sheet("summary")
571 ws4.append(["item", "value"])
572 for c in ws4[1]:
573 c.fill = header_fill
574 c.font = header_font
575
576 ws4.append(["FileName", info.get("FileName", "")])
577 ws4.append(["RecordTime", info.get("RecordTime", "")])
578 ws4.append(["Npoints", int(len(V))])
579 if rss is not None:
580 ws4.append(["RSS_logy", float(rss)])
581
582 wb.save(outfile_xlsx)
583 print(f"[I/O] Saved IV Excel: {outfile_xlsx}")
584
585
586# =============================================================================
587# helper math
588# =============================================================================
589
590def solve_root_poly(x, y, target_y=0.0, order=3):
591 """
592 概要: データ点 (x, y) に多項式をフィッティングし、指定された target_y に対応する x の根を近似的に見つけます。
593
594 詳細説明:
595 データ点数が少ない場合は、線形補間または多項式の次数を調整して対処します。
596 複数の実根がある場合は、入力 x の平均に最も近い根を選択します。
597
598 引数:
599 :param x: 独立変数データ点の配列。
600 :type x: numpy.ndarray
601 :param y: 従属変数データ点の配列。
602 :type y: numpy.ndarray
603 :param target_y: 根を見つけたい目標のy値。
604 :type target_y: float
605 :param order: 多項式フィッティングの次数。
606 :type order: int
607
608 戻り値:
609 :returns: target_y に対応する x の推定根。
610 :rtype: float
611 """
612 if len(x) < 2:
613 return float(x[0]) if len(x) else 0.0
614 order = min(order, len(x) - 1)
615 if order < 1:
616 return float(np.interp(target_y, y, x))
617 z = np.polyfit(x, y - target_y, order)
618 roots = np.roots(z)
619 real_roots = roots[np.isreal(roots)].real
620 if len(real_roots) == 0:
621 return float(np.interp(target_y, y, x))
622 return float(real_roots[np.argmin(np.abs(real_roots - np.mean(x)))])
623
624
625def local_poly_value(x, y, x0, npts=7, order=3):
626 """
627 概要: 指定された点 x0 の近傍のデータ点に局所多項式をフィッティングし、x0 におけるy値を推定します。
628
629 詳細説明:
630 npts で指定された数の最も近いデータ点を使用します。
631
632 引数:
633 :param x: 独立変数データ点の配列。
634 :type x: numpy.ndarray
635 :param y: 従属変数データ点の配列。
636 :type y: numpy.ndarray
637 :param x0: y値を推定したい目標のx値。
638 :type x0: float
639 :param npts: 局所フィッティングに使用するデータ点の数。
640 :type npts: int
641 :param order: 多項式フィッティングの次数。
642 :type order: int
643
644 戻り値:
645 :returns: x0 における推定y値。
646 :rtype: float
647 """
648 x = np.asarray(x, dtype=float)
649 y = np.asarray(y, dtype=float)
650 idx = np.argsort(np.abs(x - x0))[:max(2, npts)]
651 xs = x[idx]
652 ys = y[idx]
653 sidx = np.argsort(xs)
654 xs = xs[sidx]
655 ys = ys[sidx]
656 deg = min(order, len(xs) - 1)
657 if deg < 1: # データ点が1点のみの場合、その点のy値を返す
658 if len(xs) == 1:
659 return float(ys[0])
660 else: # データ点が2点の場合、線形補間
661 return float(np.interp(x0, xs, ys))
662
663 coef = np.polyfit(xs, ys, deg)
664 return float(np.polyval(coef, x0))
665
666
667def smooth_polyfit(y, window_points=5, poly_order=3):
668 """
669 概要: Savitzky-Golayフィルターに似た局所多項式フィッティングにより、配列 y を平滑化します。
670
671 詳細説明:
672 各データ点に対して、その点と近傍の window_points 数(奇数)のデータ点に
673 poly_order 次の多項式をフィッティングし、中央の点の値を推定します。
674
675 引数:
676 :param y: 平滑化するデータ点の配列。
677 :type y: numpy.ndarray
678 :param window_points: 各局所フィッティングに使用するウィンドウの点の数(奇数であるべき)。
679 :type window_points: int
680 :param poly_order: 各局所フィッティングで使用する多項式の次数。
681 :type poly_order: int
682
683 戻り値:
684 :returns: 平滑化されたデータ点の配列。
685 :rtype: numpy.ndarray
686 """
687 y = np.asarray(y, dtype=float)
688 n = len(y)
689
690 if n < 3:
691 return y.copy()
692
693 if window_points < 3:
694 window_points = 3
695 if window_points % 2 == 0:
696 window_points += 1
697 if window_points > n:
698 window_points = n if (n % 2 == 1) else n - 1
699 if window_points < 3: # ここに到達したら、平滑化不能なのでコピーを返す
700 return y.copy()
701
702 if poly_order >= window_points:
703 poly_order = window_points - 1
704 if poly_order < 1:
705 poly_order = 1
706
707 half = window_points // 2
708 ys = np.empty(n, dtype=float)
709
710 for i in range(n):
711 i0 = max(0, i - half)
712 i1 = min(n, i + half + 1)
713 while (i1 - i0) < window_points:
714 if i0 > 0:
715 i0 -= 1
716 elif i1 < n:
717 i1 += 1
718 else:
719 break
720
721 idx = np.arange(i0, i1, dtype=float)
722 yy = y[i0:i1]
723 xloc = idx - i # 中心を0とする相対座標
724
725 deg = min(poly_order, len(yy) - 1)
726 if deg < 1:
727 ys[i] = y[i]
728 continue
729
730 coeff = np.polyfit(xloc, yy, deg)
731 ys[i] = np.polyval(coeff, 0.0)
732
733 return ys
734
735
736def sigmoid_blend(x, dV):
737 """
738 概要: シグモイド関数を用いて、2つのモデル間のブレンド係数を計算します。
739
740 詳細説明:
741 x が0.0の場合に0.5、x が正で十分に大きい場合に1.0、
742 x が負で十分に小さい場合に0.0に近づく値を返します。
743 dV はブレンドの遷移幅を決定します。dV が非常に小さい場合、ステップ関数に近づきます。
744
745 引数:
746 :param x: ブレンドの基準となる電圧または汎用的な値。
747 :type x: numpy.ndarray or float
748 :param dV: シグモイド遷移の幅を制御するパラメータ。
749 :type dV: float
750
751 戻り値:
752 :returns: x に応じたブレンド係数。0から1の間の値を取ります。
753 :rtype: numpy.ndarray or float
754 """
755 x = np.asarray(x, dtype=float)
756 if dV < 1.0e-6:
757 return (x >= 0.0).astype(float)
758 z = np.clip(-x / dV, -700.0, 700.0) # オーバーフロー防止のためのクリッピング
759 return 1.0 / (1.0 + np.exp(z))
760
761
762# =============================================================================
763# current components
764# =============================================================================
765
766def j_diode(Vd, I0, ndiode, temperature):
767 """
768 概要: 理想ダイオードモデル(Shockley方程式)の電流成分を計算します。
769
770 引数:
771 :param Vd: ダイオード接合にかかる電圧。
772 :type Vd: numpy.ndarray or float
773 :param I0: 逆飽和電流。
774 :type I0: float
775 :param ndiode: ダイオード理想因子(エミッタ係数)。
776 :type ndiode: float
777 :param temperature: 温度 (K)。
778 :type temperature: float
779
780 戻り値:
781 :returns: ダイオード電流。
782 :rtype: numpy.ndarray or float
783 """
784 vt = ndiode * KB * temperature / E_CHARGE
785 arg = np.clip(Vd / vt, -700.0, 700.0) # オーバーフロー防止のためのクリッピング
786 return I0 * (np.exp(arg) - 1.0)
787
788
789def j_tfe_forward(Vd, A_tfe, E00):
790 """
791 概要: 熱アシストトンネルフィールドエミッション (TFE) 電流のフォワードバイアス成分を計算します。
792
793 詳細説明:
794 TFEは通常、低温や欠陥の多い材料で支配的になるメカニズムです。
795 この関数は、Vd > 0 の領域でTFEによる電流を計算します。
796
797 引数:
798 :param Vd: ダイオード接合にかかる電圧。
799 :type Vd: numpy.ndarray or float
800 :param A_tfe: TFE電流の振幅係数。
801 :type A_tfe: float
802 :param E00: TFEの特性エネルギーまたは係数。
803 :type E00: float
804
805 戻り値:
806 :returns: TFEフォワード電流。
807 :rtype: numpy.ndarray or float
808 """
809 e00 = max(E00, 1.0e-12) # ゼロ除算防止
810 arg = np.clip(Vd / e00, -700.0, 700.0) # オーバーフロー防止のためのクリッピング
811 return A_tfe * (np.exp(arg) - 1.0)
812
813
814def j_tfe_reverse(Vd, A_tfe, E00):
815 """
816 概要: 熱アシストトンネルフィールドエミッション (TFE) 電流のリバースバイアス成分を計算します。
817
818 詳細説明:
819 この関数は、Vd < 0 の領域でTFEによる電流を計算します。
820
821 引数:
822 :param Vd: ダイオード接合にかかる電圧。
823 :type Vd: numpy.ndarray or float
824 :param A_tfe: TFE電流の振幅係数。
825 :type A_tfe: float
826 :param E00: TFEの特性エネルギーまたは係数。
827 :type E00: float
828
829 戻り値:
830 :returns: TFEリバース電流。
831 :rtype: numpy.ndarray or float
832 """
833 e00 = max(E00, 1.0e-12) # ゼロ除算防止
834 arg = np.clip(-Vd / e00, -700.0, 700.0) # オーバーフロー防止のためのクリッピング
835 return -A_tfe * (np.exp(arg) - 1.0)
836
837
838def j_fn_forward(Vd, A_fn, B_fn):
839 """
840 概要: Fowler-Nordheimトンネル電流 (FN) のフォワードバイアス成分を計算します。
841
842 詳細説明:
843 このメカニズムは、高注入レベルや特定の欠陥メカニズムによって引き起こされる電流をモデル化するために使用できます。
844 Vd > 0 の領域で定義されます。
845
846 引数:
847 :param Vd: ダイオード接合にかかる電圧。
848 :type Vd: numpy.ndarray or float
849 :param A_fn: FN電流の振幅係数。
850 :type A_fn: float
851 :param B_fn: FN電流の指数関数的電圧依存性を制御する係数。
852 :type B_fn: float
853
854 戻り値:
855 :returns: FNフォワード電流。
856 :rtype: numpy.ndarray or float
857 """
858 if np.ndim(Vd) == 0:
859 if Vd <= 1.0e-12:
860 return 0.0
861 arg = np.clip(-B_fn / Vd, -700.0, 700.0) # オーバーフロー防止のためのクリッピング
862 return A_fn * (Vd ** 2) * np.exp(arg)
863
864 arr = np.asarray(Vd, dtype=float)
865 out = np.zeros_like(arr)
866 mask = arr > 1.0e-12
867 arg = np.clip(-B_fn / arr[mask], -700.0, 700.0) # オーバーフロー防止のためのクリッピング
868 out[mask] = A_fn * (arr[mask] ** 2) * np.exp(arg)
869 return out
870
871
872def j_fn_reverse(Vd, A_fn, B_fn):
873 """
874 概要: Fowler-Nordheimトンネル電流 (FN) のリバースバイアス成分を計算します。
875
876 詳細説明:
877 このメカニズムは、高注入レベルや特定の欠陥メカニズムによって引き起こされる電流をモデル化するために使用できます。
878 Vd < 0 の領域で定義されます。
879
880 引数:
881 :param Vd: ダイオード接合にかかる電圧。
882 :type Vd: numpy.ndarray or float
883 :param A_fn: FN電流の振幅係数。
884 :type A_fn: float
885 :param B_fn: FN電流の指数関数的電圧依存性を制御する係数。
886 :type B_fn: float
887
888 戻り値:
889 :returns: FNリバース電流。
890 :rtype: numpy.ndarray or float
891 """
892 if np.ndim(Vd) == 0:
893 if Vd >= -1.0e-12:
894 return 0.0
895 vabs = -Vd
896 arg = np.clip(-B_fn / vabs, -700.0, 700.0) # オーバーフロー防止のためのクリッピング
897 return -A_fn * (vabs ** 2) * np.exp(arg)
898
899 arr = np.asarray(Vd, dtype=float)
900 out = np.zeros_like(arr)
901 mask = arr < -1.0e-12
902 vabs = -arr[mask]
903 arg = np.clip(-B_fn / vabs, -700.0, 700.0) # オーバーフロー防止のためのクリッピング
904 out[mask] = -A_fn * (vabs ** 2) * np.exp(arg)
905 return out
906
907
908def j_sclc_trap_transition(Vd, K_sclc, m_sclc, Vtfl, dV=1e-3):
909 """
910 概要: トラップ制限空間電荷制限電流(SCLC)とトラップフリーSCLCの間の遷移をモデル化する電流成分を計算します。
911
912 詳細説明:
913 このモデルは、電圧に応じてトラップが満たされることで、電流が異なる電圧依存性を示す領域を表現します。
914 具体的には、トラップ制限領域では Vd^m_sclc に、トラップフリー領域では Vd^2 に従います。
915 シグモイド関数を用いて両者間の滑らかな遷移を実現します。
916
917 引数:
918 :param Vd: ダイオード接合にかかる電圧。
919 :type Vd: numpy.ndarray or float
920 :param K_sclc: SCLCの振幅係数(トラップ制限領域の係数)。
921 :type K_sclc: float
922 :param m_sclc: トラップ制限領域でのSCLCの電圧依存性指数。
923 :type m_sclc: float
924 :param Vtfl: トラップが満たされる閾値電圧。
925 :type Vtfl: float
926 :param dV: 遷移の幅を制御する電圧(シグモイド関数の傾き)。
927 :type dV: float
928
929 戻り値:
930 :returns: トラップ遷移を考慮したSCLC電流。
931 :rtype: numpy.ndarray or float
932 """
933 vabs = np.abs(Vd)
934 K_tf = K_sclc * (max(Vtfl, 1.0e-30) ** (m_sclc - 2.0))
935
936 if dV < 1.0e-12:
937 w = (vabs >= Vtfl).astype(float) if np.ndim(vabs) else float(vabs >= Vtfl)
938 else:
939 z = np.clip(-(vabs - Vtfl) / dV, -700.0, 700.0)
940 w = 1.0 / (1.0 + np.exp(z))
941
942 i_low = K_sclc * (vabs ** m_sclc) # trap-limited
943 i_high = K_tf * (vabs ** 2.0) # trap-free
944 imag = (1.0 - w) * i_low + w * i_high
945 return np.sign(Vd) * imag
946
947def j_sclc(Vd, K_sclc):
948 """
949 概要: 空間電荷制限電流 (SCLC) を計算します。
950
951 詳細説明:
952 SCLCは、高抵抗材料におけるキャリア注入が空間電荷によって制限されるときに発生します。
953 この電流は電圧の2乗に比例し、電圧の符号によって方向が決まります。
954
955 引数:
956 :param Vd: ダイオード接合にかかる電圧。
957 :type Vd: numpy.ndarray or float
958 :param K_sclc: SCLCの振幅係数。
959 :type K_sclc: float
960
961 戻り値:
962 :returns: SCLC電流。
963 :rtype: numpy.ndarray or float
964 """
965 return np.sign(Vd) * K_sclc * (np.abs(Vd) ** 2)
966
967
968def parse_mech_mode(mode_str):
969 """
970 概要: 輸送メカニズムのモード文字列を解析し、使用するメカニズムのセットを返します。
971
972 詳細説明:
973 モード文字列はカンマ (,) またはプラス (+) で区切られたメカニズム名(例: diode+tfe)を
974 含みます。無効なメカニズム名が指定された場合はValueErrorを発生させます。
975
976 引数:
977 :param mode_str: 解析するモード文字列(例: diode, diode+tfe, none)。
978 :type mode_str: str or None
979
980 戻り値:
981 :returns: 使用するメカニズム名のセット。
982 :rtype: set
983
984 例外:
985 :raises ValueError: 未知の輸送メカニズム名が指定された場合。
986 """
987 if mode_str is None:
988 return set()
989 txt = mode_str.lower().replace(" ", "")
990 if txt in ("", "none", "off"):
991 return set()
992 parts = [p for p in txt.replace(",", "+").split("+") if p]
993 norm = set()
994 for p in parts:
995 if p in ("diode", "tfe", "fn", "sclc"):
996 norm.add(p)
997 else:
998 raise ValueError(f"Unknown transport mechanism: {p}")
999 return norm
1000
1001
1002def used_params_from_modes(forwardIV, reverseIV):
1003 """
1004 概要: 指定されたフォワードおよびリバースIVモデルで使用されるパラメータ名を決定します。
1005
1006 詳細説明:
1007 共通のパラメータ (Rs, Rsh, IPV) に加えて、
1008 有効な輸送メカニズム (diode, tfe, fn, sclc) に関連するパラメータを特定します。
1009
1010 引数:
1011 :param forwardIV: フォワードバイアスモデルのメカニズムを示す文字列(例: diode, diode+tfe)。
1012 :type forwardIV: str
1013 :param reverseIV: リバースバイアスモデルのメカニズムを示す文字列(例: diode+tfe, diode+fn)。
1014 :type reverseIV: str
1015
1016 戻り値:
1017 :returns: 使用されるパラメータ名のセット。
1018 :rtype: set
1019 """
1020 used = {"Rs", "Rsh", "IPV"}
1021 all_modes = parse_mech_mode(forwardIV) | parse_mech_mode(reverseIV)
1022 if "diode" in all_modes:
1023 used |= {"I0", "ndiode"}
1024 if "tfe" in all_modes:
1025 used |= {"A_tfe", "E00"}
1026 if "fn" in all_modes:
1027 used |= {"A_fn", "B_fn"}
1028 if "sclc" in all_modes:
1029 used |= {"K_sclc"}
1030 return used
1031
1032
1033def junction_current_forward(Vd, p, mode_set, temperature):
1034 """
1035 概要: フォワードバイアス条件における総接合電流を計算します。
1036
1037 詳細説明:
1038 mode_set で指定されたメカニズムに基づいて、対応する電流成分(ダイオード、TFE、FN、SCLC)を合計します。
1039
1040 引数:
1041 :param Vd: ダイオード接合にかかる電圧。
1042 :type Vd: numpy.ndarray or float
1043 :param p: パラメータ値を含む辞書。
1044 :type p: dict
1045 :param mode_set: フォワードバイアスで使用するメカニズムのセット。
1046 :type mode_set: set
1047 :param temperature: 温度 (K)。
1048 :type temperature: float
1049
1050 戻り値:
1051 :returns: 総接合電流。
1052 :rtype: numpy.ndarray or float
1053 """
1054 j = 0.0
1055 if "diode" in mode_set:
1056 j += j_diode(Vd, p["I0"], p["ndiode"], temperature)
1057 if "tfe" in mode_set:
1058 j += j_tfe_forward(Vd, p["A_tfe"], p["E00"])
1059 if "fn" in mode_set:
1060 j += j_fn_forward(Vd, p["A_fn"], p["B_fn"])
1061 if "sclc" in mode_set:
1062 j += j_sclc(Vd, p["K_sclc"])
1063 return j
1064
1065
1066def junction_current_reverse(Vd, p, mode_set, temperature):
1067 """
1068 概要: リバースバイアス条件における総接合電流を計算します。
1069
1070 詳細説明:
1071 mode_set で指定されたメカニズムに基づいて、対応する電流成分(ダイオード、TFE、FN、SCLC)を合計します。
1072
1073 引数:
1074 :param Vd: ダイオード接合にかかる電圧。
1075 :type Vd: numpy.ndarray or float
1076 :param p: パラメータ値を含む辞書。
1077 :type p: dict
1078 :param mode_set: リバースバイアスで使用するメカニズムのセット。
1079 :type mode_set: set
1080 :param temperature: 温度 (K)。
1081 :type temperature: float
1082
1083 戻り値:
1084 :returns: 総接合電流。
1085 :rtype: numpy.ndarray or float
1086 """
1087 j = 0.0
1088 if "diode" in mode_set:
1089 j += j_diode(Vd, p["I0"], p["ndiode"], temperature)
1090 if "tfe" in mode_set:
1091 j += j_tfe_reverse(Vd, p["A_tfe"], p["E00"])
1092 if "fn" in mode_set:
1093 j += j_fn_reverse(Vd, p["A_fn"], p["B_fn"])
1094 if "sclc" in mode_set:
1095 j += j_sclc(Vd, p["K_sclc"])
1096 return j
1097
1098
1099# =============================================================================
1100# model
1101# =============================================================================
1102
1103def same_sign(a, b):
1104 """
1105 概要: 2つの数値が同じ符号を持つかどうかを判定します。
1106
1107 詳細説明:
1108 両方が正または両方が負の場合にTrueを返します。
1109 片方または両方がゼロの場合の動作は、厳密には考慮されていません。
1110
1111 引数:
1112 :param a: 1つ目の数値。
1113 :type a: float
1114 :param b: 2つ目の数値。
1115 :type b: float
1116
1117 戻り値:
1118 :returns: a と b が同じ符号を持つ場合はTrue、そうでない場合はFalse。
1119 :rtype: bool
1120 """
1121 return (a > 0.0 and b > 0.0) or (a < 0.0 and b < 0.0)
1122
1123def model(V, p, forwardIV="diode", reverseIV="diode+tfe", dV=0.05, temperature=300.0):
1124 """
1125 概要: 拡張一ダイオードモデル(SDM)に基づいて電流-電圧(IV)カーブを計算します。
1126
1127 詳細説明:
1128 与えられた外部電圧 V、パラメータ p、および選択された輸送メカニズム (forwardIV, reverseIV) を使用して
1129 対応する電流 I を計算します。接合電圧 Vd を未知数とする根探しアルゴリズム(Brentのメソッド)を
1130 用いて安定的にモデル電流を求めます。
1131 フォワードとリバースで異なるモデルが指定された場合、シグモイド関数 (sigmoid_blend) を使って
1132 Vd が0V付近でスムーズにブレンドされます。
1133
1134 引数:
1135 :param V: 外部印加電圧の配列。
1136 :type V: numpy.ndarray
1137 :param p: モデルパラメータを含む辞書。主要なキーは I0, ndiode, IPV, Rs, Rsh, A_tfe, E00, A_fn, B_fn, K_sclc。
1138 :type p: dict
1139 :param forwardIV: フォワードバイアス領域で使用する輸送メカニズムを示す文字列。例: diode, diode+tfe+sclc。
1140 :type forwardIV: str
1141 :param reverseIV: リバースバイアス領域で使用する輸送メカニズムを示す文字列。例: diode+tfe, diode+fn。
1142 :type reverseIV: str
1143 :param dV: フォワードモデルとリバースモデルをブレンドする際の電圧幅。Vd=0V付近での遷移の鋭さを制御します。
1144 :type dV: float
1145 :param temperature: シミュレーションの温度 (K)。
1146 :type temperature: float
1147
1148 戻り値:
1149 :returns: 各外部電圧 V に対応する総電流の配列。
1150 :rtype: numpy.ndarray
1151 """
1152 fset = parse_mech_mode(forwardIV)
1153 rset = parse_mech_mode(reverseIV)
1154 same_model = (fset == rset)
1155
1156 V = np.atleast_1d(np.asarray(V, dtype=float))
1157 Itotal = np.empty_like(V)
1158
1159 Rs = max(float(p["Rs"]), EPS_R)
1160 Rsh = max(float(p["Rsh"]), EPS_R)
1161 IPV = float(p["IPV"])
1162
1163 def residual_vd(vd_trial, v_target):
1164 """
1165 概要: 接合電圧 vd_trial の残差関数を計算します。
1166 この関数は brentq によって vd_trial の根を見つけるために使用されます。
1167 """
1168 if same_model:
1169 ij = junction_current_forward(vd_trial, p, fset, temperature)
1170 else:
1171 w = float(sigmoid_blend(vd_trial, dV))
1172 ij_f = junction_current_forward(vd_trial, p, fset, temperature)
1173 ij_r = junction_current_reverse(vd_trial, p, rset, temperature)
1174 ij = w * ij_f + (1.0 - w) * ij_r
1175
1176 ibranch = ij + vd_trial / Rsh
1177 v_calc = vd_trial + Rs * ibranch
1178 return v_calc - v_target
1179
1180 for i, v_val in enumerate(V):
1181 try:
1182 # 根探しの初期探索範囲を設定。VdはVよりRs*I落ちるため、広めに設定。
1183 width = max(5.0, abs(v_val) + Rs * (abs(IPV) + abs(v_val) / Rsh + 1.0))
1184 lo = v_val - width
1185 hi = v_val + width
1186
1187 # BrentQ のための探索範囲 [lo, hi] を確保。f(lo) と f(hi) が異なる符号を持つように調整。
1188 flo = residual_vd(lo, v_val)
1189 fhi = residual_vd(hi, v_val)
1190 ntry = 0
1191 while same_sign(flo, fhi) and ntry < 25:
1192 width *= 2.0
1193 lo = v_val - width
1194 hi = v_val + width
1195 flo = residual_vd(lo, v_val)
1196 fhi = residual_vd(hi, v_val)
1197 ntry += 1
1198
1199 if same_sign(flo, fhi): # 符号が異ならない場合、根が見つからなかったとみなし、近似値を返す
1200 vd_root = v_val
1201 else:
1202 vd_root = brentq(
1203 residual_vd, lo, hi,
1204 args=(v_val,),
1205 maxiter=300, xtol=1.0e-12, rtol=1.0e-10
1206 )
1207
1208 # 根が見つかったVdから電流を再計算
1209 if same_model:
1210 ij = junction_current_forward(vd_root, p, fset, temperature)
1211 else:
1212 w = float(sigmoid_blend(vd_root, dV))
1213 ij_f = junction_current_forward(vd_root, p, fset, temperature)
1214 ij_r = junction_current_reverse(vd_root, p, rset, temperature)
1215 ij = w * ij_f + (1.0 - w) * ij_r
1216
1217 ibranch = ij + vd_root / Rsh
1218 Itotal[i] = ibranch - IPV
1219 except Exception:
1220 # 根探しに失敗した場合のフォールバック(例:V = 0 で Rs + Rsh がゼロに近い場合など)
1221 Itotal[i] = v_val / max(Rs + Rsh, EPS_R) - IPV
1222
1223 return Itotal
1224
1225
1226# =============================================================================
1227# initial parameter estimation
1228# =============================================================================
1229
1230def estimate_voc_local_quadratic(V, I, nlsq_points=5):
1231 """
1232 概要: IVデータから開回路電圧(VOC)を局所二次多項式フィッティングで推定します。
1233
1234 詳細説明:
1235 電流がゼロに最も近い点の近傍データを使用して、二次多項式をフィッティングし、
1236 その多項式の根からVOCを求めます。データ点が少ない場合は、線形補間を使用します。
1237
1238 引数:
1239 :param V: 電圧データの配列。
1240 :type V: numpy.ndarray
1241 :param I: 電流データの配列。
1242 :type I: numpy.ndarray
1243 :param nlsq_points: 局所フィッティングに使用するデータ点の数。
1244 :type nlsq_points: int
1245
1246 戻り値:
1247 :returns: 推定された開回路電圧VOC。
1248 :rtype: float
1249 """
1250 V = np.asarray(V, dtype=float)
1251 I = np.asarray(I, dtype=float)
1252
1253 if len(V) < 3:
1254 return float(V[np.argmin(np.abs(I))])
1255
1256 nlsq_points = max(3, int(nlsq_points))
1257 nlsq_points = min(nlsq_points, len(V))
1258
1259 ic = int(np.argmin(np.abs(I))) # abs(I) 最小点
1260 half = nlsq_points // 2
1261
1262 i0 = max(0, ic - half)
1263 i1 = min(len(V), i0 + nlsq_points)
1264 i0 = max(0, i1 - nlsq_points)
1265
1266 x = V[i0:i1]
1267 y = I[i0:i1]
1268
1269 deg = min(2, len(x) - 1)
1270 coef = np.polyfit(x, y, deg)
1271
1272 if deg == 1:
1273 a, b = coef
1274 if abs(a) < 1.0e-30:
1275 return float(V[ic])
1276 return float(-b / a)
1277
1278 a, b, c = coef
1279 if abs(a) < 1.0e-30:
1280 if abs(b) < 1.0e-30:
1281 return float(V[ic])
1282 return float(-c / b)
1283
1284 roots = np.roots([a, b, c])
1285 real_roots = roots[np.isreal(roots)].real
1286
1287 if len(real_roots) == 0:
1288 return float(V[ic])
1289
1290 # 近傍窓の中心に最も近い根を採用
1291 x_center = V[ic]
1292 return float(real_roots[np.argmin(np.abs(real_roots - x_center))])
1293
1294def estimate_initial_params(V, I_meas, temperature):
1295 """
1296 概要: IVデータからSDM(拡張一ダイオードモデル)の初期パラメータを推定します。
1297
1298 詳細説明:
1299 データ平滑化、dI/dV解析、多項式根探し、負電圧領域でのTFE近似を用いて、
1300 最適化に適した頑健な初期値を導出します。
1301
1302 引数:
1303 :param V: 測定された電圧データ点の配列。
1304 :type V: numpy.ndarray
1305 :param I_meas: 測定された電流データ点の配列。
1306 :type I_meas: numpy.ndarray
1307 :param temperature: 測定時の温度 (K)。
1308 :type temperature: float
1309
1310 戻り値:
1311 :returns: 推定された初期パラメータを含む辞書。
1312 :rtype: dict
1313 """
1314 print("[Process] Estimating initial diode/TFE parameters from data...")
1315
1316 # 重複するV値を処理
1317 uV, idx = np.unique(V, return_index=True)
1318 uI = I_meas[idx]
1319
1320 # データ平滑化と微分
1321 y_sm = smooth_polyfit(uI, window_points=5, poly_order=3)
1322 dydv = np.gradient(y_sm, uV)
1323
1324 # Rsの推定: V>0 で dI/dV が最大となる点から
1325 pos_mask = uV > 0.0
1326 if np.any(pos_mask):
1327 dpos = dydv[pos_mask]
1328 xpos = uV[pos_mask]
1329 idx_pos_local = int(np.argmax(dpos))
1330 V_rs = float(xpos[idx_pos_local])
1331 slope_pos = float(dpos[idx_pos_local])
1332 else: # V>0 のデータがない場合、全体の最大勾配
1333 V_rs = float(uV[-1])
1334 slope_pos = float(np.max(dydv))
1335 Rs_est = 1.0 / abs(slope_pos)
1336
1337 # Rshの推定: V<0 で dI/dV が最小(最も負)となる点から
1338 neg_mask = uV < 0.0
1339 if np.any(neg_mask):
1340 dneg = dydv[neg_mask]
1341 xneg = uV[neg_mask]
1342 idx_neg_local = int(np.argmin(dneg))
1343 V_sh = float(xneg[idx_neg_local])
1344 slope_neg = float(dneg[idx_neg_local])
1345 else: # V<0 のデータがない場合、全体の最小勾配
1346 V_sh = float(uV[0])
1347 slope_neg = float(np.min(dydv))
1348 Rsh_est = 1.0 / max(abs(slope_neg), 1.0e-12)
1349
1350 # ISC, IPV, VOC の推定
1351 ISC_est = local_poly_value(uV, y_sm, 0.0, npts=7, order=3)
1352 IPV_est = max(-ISC_est, EPS_I)
1353 VOC_est = solve_root_poly(uV, y_sm, target_y=0.0, order=3)
1354 VOC_est = estimate_voc_local_quadratic(uV, y_sm, nlsq_points=5)
1355
1356 # Ish, I0 の推定
1357 Ish_est = local_poly_value(uV, y_sm, V_sh, npts=7, order=3)
1358 I0_est = max(ISC_est - Ish_est, 1.0e-12) # I0は通常正の値
1359
1360 # TFE 初期値推定 (V<0 の十分負側で ln(abs(I))-V を一次近似)
1361 Vtfe_est = np.nan
1362 Itfe_est = np.nan
1363 A_tfe_est = 1.0e-30
1364 E00_est = 1.0e-3
1365
1366 neg_idx = np.where(uV < 0.0)[0]
1367 if len(neg_idx) >= 5: # 負電圧領域に十分なデータ点がある場合
1368 nneg = len(neg_idx)
1369 ntail = max(5, int(np.ceil(0.4 * nneg))) # 最も負側の40%または5点以上を使用
1370 tail_idx = neg_idx[:ntail] # 最も負側の領域
1371
1372 V_tail = uV[tail_idx]
1373 I_tail = np.abs(y_sm[tail_idx])
1374
1375 mask_good = np.isfinite(V_tail) & np.isfinite(I_tail) & (I_tail > EPS_I)
1376 V_tail = V_tail[mask_good]
1377 I_tail = I_tail[mask_good]
1378
1379 if len(V_tail) >= 2: # 少なくとも2点あれば直線フィッティング可能
1380 lnI = np.log(I_tail)
1381 slope_tfe, intercept_tfe = np.polyfit(V_tail, lnI, 1)
1382
1383 if abs(slope_tfe) > 1.0e-12:
1384 E00_est = 1.0 / abs(slope_tfe)
1385 else:
1386 E00_est = 1.0e-3 # デフォルト値
1387
1388 irep = int(np.argmin(V_tail)) # 最も負の電圧点
1389 Vtfe_est = float(V_tail[irep])
1390 Itfe_est = float(I_tail[irep])
1391
1392 # ln|I| = ln(A_tfe) + (-V)/E00 for reverse-TFE-like growth
1393 # |I| = A_tfe * exp(-V/E00) より
1394 A_tfe_est = Itfe_est * np.exp(Vtfe_est / max(E00_est, 1.0e-12))
1395
1396 A_tfe_est = max(float(A_tfe_est), 1.0e-30)
1397 E00_est = max(float(E00_est), 1.0e-6)
1398
1399 # 推定されたパラメータとデフォルト値を結合
1400 return {
1401 "I0": I0_est,
1402 "ndiode": 1.5,
1403 "IPV": IPV_est,
1404 "Rs": max(Rs_est, 1.0e-6), # Rsはゼロにならないようにする
1405 "Rsh": max(Rsh_est, 1.0e3), # Rshは十分に大きい値から始める
1406 "A_tfe": A_tfe_est,
1407 "E00": E00_est,
1408 "A_fn": 1.0e-30, # FN/SCLCは初期値推定でカバーされないため極小値
1409 "B_fn": 10.0,
1410 "K_sclc": 1.0e-30,
1411 "VOC_est": VOC_est,
1412 "ISC_est": ISC_est,
1413 "Vsh_est": V_sh,
1414 "Ish_est": Ish_est,
1415 "Vtfe_est": Vtfe_est,
1416 "Itfe_est": Itfe_est,
1417 "dIdV_pos_rep": slope_pos,
1418 "dIdV_neg_rep": slope_neg,
1419 }
1420
1421
1422# =============================================================================
1423# fit support
1424# =============================================================================
1425
1426def pack_free_params(params, fix_set):
1427 """
1428 概要: 辞書形式の全パラメータから、最適化対象の自由パラメータのみをNumPy配列にパックします。
1429
1430 詳細説明:
1431 fix_set に含まれるパラメータは除外されます。
1432 LOG_PARAMS に含まれるパラメータは log10 変換されて配列に格納されます。
1433
1434 引数:
1435 :param params: 全パラメータ(固定および自由)を含む辞書。
1436 :type params: dict
1437 :param fix_set: 固定されるパラメータ名のセット。
1438 :type fix_set: set
1439
1440 戻り値:
1441 :returns: 最適化に使用される自由パラメータのNumPy配列。
1442 :rtype: numpy.ndarray
1443 """
1444 p0 = []
1445 for name in PARAM_NAMES:
1446 if name in fix_set:
1447 continue
1448 val = float(params[name])
1449 if name in LOG_PARAMS:
1450 p0.append(np.log10(max(val, 1.0e-30))) # ゼロや負の値を避ける
1451 else:
1452 p0.append(val)
1453 return np.asarray(p0, dtype=float)
1454
1455
1456def unpack_free_params(p_free, base_params, fix_set):
1457 """
1458 概要: 自由パラメータのNumPy配列を、固定パラメータと組み合わせて辞書形式の全パラメータにアンパックします。
1459
1460 詳細説明:
1461 base_params は固定パラメータの値を決定するために使用されます。
1462 LOG_PARAMS に含まれるパラメータは 10** 変換されて辞書に格納されます。
1463
1464 引数:
1465 :param p_free: 最適化中の自由パラメータのNumPy配列。
1466 :type p_free: numpy.ndarray
1467 :param base_params: 固定パラメータの基準値を含む辞書。
1468 :type base_params: dict
1469 :param fix_set: 固定されたパラメータ名のセット。
1470 :type fix_set: set
1471
1472 戻り値:
1473 :returns: 更新された全パラメータを含む辞書。
1474 :rtype: dict
1475 """
1476 curr = dict(base_params) # base_params をコピーして、固定されていないパラメータを更新
1477 idx = 0
1478 for name in PARAM_NAMES:
1479 if name in fix_set:
1480 continue
1481 val = float(p_free[idx])
1482 if name in LOG_PARAMS:
1483 curr[name] = 10.0 ** val
1484 else:
1485 curr[name] = val
1486 idx += 1
1487 return curr
1488
1489
1490
1491def signed_log10_current(I, eps=EPS_I):
1492 """
1493 概要: 符号を保持したまま電流を圧縮する補助関数。
1494
1495 詳細説明:
1496 sign(I) * log10(abs(I)+eps) を返します。
1497 電流値がゼロに近い場合に計算が不安定になることを防ぐため、abs(I) に eps を加えます。
1498
1499 引数:
1500 :param I: 電流値の配列または単一値。
1501 :type I: numpy.ndarray or float
1502 :param eps: 絶対値計算時に加算される微小量。ゼロ除算や対数関数の引数が負になるのを防ぐ。
1503 :type eps: float
1504
1505 戻り値:
1506 :returns: 符号を保持したログスケールの電流値。
1507 :rtype: numpy.ndarray or float
1508 """
1509 I = np.asarray(I, dtype=float)
1510 return np.sign(I) * np.log10(np.abs(I) + eps)
1511
1512
1513def objective(p_free, V, I_meas, args, base_params, fix_set):
1514 """
1515 概要: 最適化のための目的関数(残差平方和)を計算します。
1516
1517 詳細説明:
1518 測定電流とモデル電流の符号を保持した圧縮表現
1519 sign(I) * log10(abs(I)+EPS_I) の残差平方和を使用します。
1520
1521 引数:
1522 :param p_free: 最適化中の自由パラメータのNumPy配列。
1523 :type p_free: numpy.ndarray
1524 :param V: 測定された電圧データ点の配列。
1525 :type V: numpy.ndarray
1526 :param I_meas: 測定された電流データ点の配列。
1527 :type I_meas: numpy.ndarray
1528 :param args: コマンドライン引数パーサーから得られた設定を含むオブジェクト(例: forwardIV, reverseIV)。
1529 :type args: argparse.Namespace
1530 :param base_params: 固定パラメータの基準値を含む辞書。
1531 :type base_params: dict
1532 :param fix_set: 固定されたパラメータ名のセット。
1533 :type fix_set: set
1534
1535 戻り値:
1536 :returns: 目的関数の値(残差平方和)。
1537 :rtype: float
1538 """
1539 curr = unpack_free_params(p_free, base_params, fix_set)
1540 I_calc = model(
1541 V, curr,
1542 forwardIV=args.forwardIV,
1543 reverseIV=args.reverseIV,
1544 dV=args.dv_blend,
1545 temperature=args.temperature,
1546 )
1547 # 符号付き log 圧縮スケールでの残差平方和を計算。
1548 y_meas = signed_log10_current(I_meas, EPS_I)
1549 y_calc = signed_log10_current(I_calc, EPS_I)
1550 return float(np.sum((y_meas - y_calc) ** 2))
1551
1552
1553# =============================================================================
1554# error estimation
1555# =============================================================================
1556
1557def get_jacobian(p_free, V, temperature, args, base_params, fix_set):
1558 """
1559 概要: モデルのヤコビ行列を数値的に計算します。
1560
1561 詳細説明:
1562 各自由パラメータを微小量 eps だけ摂動させ、log10(abs(I)) の変化を計算して
1563 偏微分(ヤコビ行列の要素)を近似します。
1564
1565 引数:
1566 :param p_free: 最適化された自由パラメータのNumPy配列。
1567 :type p_free: numpy.ndarray
1568 :param V: 測定された電圧データ点の配列。
1569 :type V: numpy.ndarray
1570 :param temperature: シミュレーションの温度 (K)。
1571 :type temperature: float
1572 :param args: コマンドライン引数パーサーから得られた設定を含むオブジェクト。
1573 :type args: argparse.Namespace
1574 :param base_params: 固定パラメータの基準値を含む辞書。
1575 :type base_params: dict
1576 :param fix_set: 固定されたパラメータ名のセット。
1577 :type fix_set: set
1578
1579 戻り値:
1580 :returns: 各データ点に対する各自由パラメータの偏微分からなるヤコビ行列。
1581 :rtype: numpy.ndarray
1582 """
1583 eps = 1.0e-5 # 微小摂動量
1584 n_params = len(p_free)
1585 n_points = len(V)
1586 J = np.zeros((n_points, n_params), dtype=float)
1587
1588 def get_log_model(pv):
1589 """モデル電流の符号付きlog圧縮表現を計算するヘルパー関数。"""
1590 curr = unpack_free_params(pv, base_params, fix_set)
1591 I_calc = model(
1592 V, curr,
1593 forwardIV=args.forwardIV,
1594 reverseIV=args.reverseIV,
1595 dV=args.dv_blend,
1596 temperature=temperature,
1597 )
1598 return signed_log10_current(I_calc, EPS_I)
1599
1600 f0 = get_log_model(p_free) # 現在のパラメータでのモデル出力
1601
1602 # 各自由パラメータに対して数値微分を実行
1603 for i in range(n_params):
1604 pp = np.copy(p_free)
1605 pp[i] += eps # i番目のパラメータを微小量だけ変更
1606 fi = get_log_model(pp)
1607 J[:, i] = (fi - f0) / eps # 偏微分を近似
1608
1609 return J
1610
1611
1612def estimate_errors(res, V, I_meas, temperature, args, base_params, fix_set):
1613 """
1614 概要: フィッティング結果からパラメータの誤差とモデル電流の信頼区間を推定します。
1615
1616 詳細説明:
1617 ヤコビ行列と残差平方和を用いて共分散行列を計算し、誤差伝播によって
1618 各パラメータの誤差とモデル電流の log10 スケールでの標準偏差を算出します。
1619
1620 引数:
1621 :param res: scipy.optimize.Minimize の結果オブジェクト。最適化されたパラメータ (res.x) と目的関数の値 (res.fun) を含む。
1622 :type res: scipy.optimize.OptimizeResult
1623 :param V: 測定された電圧データ点の配列。
1624 :type V: numpy.ndarray
1625 :param I_meas: 測定された電流データ点の配列。
1626 :type I_meas: numpy.ndarray
1627 :param temperature: シミュレーションの温度 (K)。
1628 :type temperature: float
1629 :param args: コマンドライン引数パーサーから得られた設定を含むオブジェクト。
1630 :type args: argparse.Namespace
1631 :param base_params: 固定パラメータの基準値を含む辞書。
1632 :type base_params: dict
1633 :param fix_set: 固定されたパラメータ名のセット。
1634 :type fix_set: set
1635
1636 戻り値:
1637 :returns: errors_dict (各パラメータ名とその推定誤差の辞書), sigma_log_I (モデル電流のlog10値の標準偏差の配列)
1638 :rtype: tuple[dict, numpy.ndarray]
1639 """
1640 p_opt = np.asarray(res.x, dtype=float) # 最適化された自由パラメータ
1641 n = len(I_meas) # データ点数
1642 p = len(p_opt) # 自由パラメータ数
1643
1644 if p == 0 or n <= p: # 自由パラメータがない、またはデータ点数が少なすぎる場合
1645 return {}, np.zeros(len(V), dtype=float)
1646
1647 J = get_jacobian(p_opt, V, temperature, args, base_params, fix_set) # ヤコビ行列
1648 sigma2_hat = float(res.fun) / max(n - p, 1) # 残差平方和から残差分散を推定
1649
1650 try:
1651 # 共分散行列の計算: (J.T @ J)^-1 * sigma_hat^2
1652 # np.linalg.pinv は特異行列の場合も対応できる擬似逆行列
1653 cov = sigma2_hat * np.linalg.pinv(J.T @ J)
1654 # 自由パラメータ自体の誤差(logスケールの場合)
1655 p_errors_internal = np.sqrt(np.maximum(np.diag(cov), 0.0))
1656 # モデル電流のlog10値の標準偏差
1657 sigma_log_I = np.sqrt(np.maximum(np.diag(J @ cov @ J.T), 0.0))
1658
1659 errors_dict = {}
1660 idx = 0
1661 for name in PARAM_NAMES:
1662 if name in fix_set:
1663 continue
1664 if name in LOG_PARAMS:
1665 # log10変換されたパラメータの誤差を元のスケールに戻す
1666 val = 10.0 ** p_opt[idx]
1667 errors_dict[name] = val * np.log(10.0) * p_errors_internal[idx]
1668 else:
1669 errors_dict[name] = p_errors_internal[idx]
1670 idx += 1
1671
1672 return errors_dict, sigma_log_I
1673
1674 except Exception as e:
1675 print(f"Warning: Error estimation failed ({e})")
1676 return {}, np.zeros(len(V), dtype=float)
1677
1678
1679# =============================================================================
1680# plotting
1681# =============================================================================
1682
1683def initialize_plot(V, I_meas, I_sim, title = None):
1684 """
1685 概要: 最適化プロセスのための初期プロットを設定し、アニメーションオブジェクトを返します。
1686
1687 詳細説明:
1688 リアルタイムでのフィッティング進捗状況を表示するために使用されます。
1689 線形スケールと対数絶対値スケールの2つのサブプロットが作成されます。
1690
1691 引数:
1692 :param V: 電圧データの配列。
1693 :type V: numpy.ndarray
1694 :param I_meas: 測定された電流データの配列。
1695 :type I_meas: numpy.ndarray
1696 :param I_sim: 初期シミュレーション電流データの配列。
1697 :type I_sim: numpy.ndarray
1698 :param title: プロットのメインタイトル。Noneの場合、タイトルは設定されない。
1699 :type title: str or None
1700
1701 戻り値:
1702 :returns: fig (matplotlib.figure.Figure), axes (matplotlib.axes.Axesの配列), anim (アニメーションに使うオブジェクトの辞書)
1703 :rtype: tuple[matplotlib.figure.Figure, numpy.ndarray, dict]
1704 """
1705 anim = {}
1706
1707 plt.ion() # インタラクティブモードをオン
1708 fig, axes = plt.subplots(1, 2, figsize=(11, 5))
1709 ax1, ax2 = axes
1710 if title: ax1.set_title(title)
1711
1712 ax1.plot(V, I_meas, "o", color="black", alpha=0.3, markersize=4, label="data")
1713 line0, = ax1.plot(V, I_sim, "-", color="blue", lw=0.5, label="initial")
1714 line1, = ax1.plot(V, I_sim, "-", color="red", lw=1, label="sim")
1715 band1 = ax1.fill_between(V, I_sim, I_sim, color="lightblue", alpha=0.5) # 初期状態では幅ゼロ
1716 ax1.axhline(0.0, color="k", lw=0.5)
1717 ax1.axvline(0.0, color="k", lw=0.5)
1718 ax1.tick_params(axis='x', labelsize=fontsize) # x軸の目盛りフォントサイズ
1719 ax1.tick_params(axis='y', labelsize=fontsize) # y軸の目盛りフォントサイズ
1720 ax1.set_xlabel("V / V", fontsize = fontsize)
1721 ax1.set_ylabel("I / A", fontsize = fontsize)
1722 ax1.grid(True)
1723 ax1.legend()
1724
1725 ax2.set_title("Log abs(I)")
1726 ax2.semilogy(V, np.abs(I_meas) + EPS_I, "o", color="black", alpha=0.3, markersize=4)
1727 line02, = ax2.semilogy(V, np.abs(I_sim) + EPS_I, "-", color="blue", lw=0.5)
1728 line2, = ax2.semilogy(V, np.abs(I_sim) + EPS_I, "-", color="red", lw=1)
1729 band2 = ax2.fill_between(V, np.abs(I_sim) + EPS_I, np.abs(I_sim) + EPS_I, color="lightblue", alpha=0.5) # 初期状態では幅ゼロ
1730
1731 ax2.tick_params(axis='x', labelsize=fontsize) # x軸の目盛りフォントサイズ
1732 ax2.tick_params(axis='y', labelsize=fontsize) # y軸の目盛りフォントサイズ
1733 ax2.set_xlabel("V / V", fontsize = fontsize)
1734 ax2.set_ylabel("I / A", fontsize = fontsize)
1735 ax2.grid(True, which="both")
1736
1737 plt.tight_layout()
1738 anim = {
1739 "fig": fig,
1740 "ax1": ax1,
1741 "ax2": ax2,
1742 "line1": line1,
1743 "line2": line2,
1744 "band1": band1,
1745 "band2": band2,
1746 }
1747 return fig, axes, anim
1748
1749def plot_iv(fig, axes, V, I_meas, I_start, I_final, info, mode, sigma_log=None, mode_label = "", outfile=None, pause=False):
1750 """
1751 概要: 測定データとモデルIVカーブをプロットします。
1752
1753 詳細説明:
1754 線形スケールと対数絶対値スケールの2つのサブプロットを作成します。
1755 sigma_log が提供された場合、モデル電流の信頼区間を水色領域で表示します。
1756
1757 引数:
1758 :param fig: プロットに使うmatplotlib.figure.Figureオブジェクト。Noneの場合、新しいFigureを作成。
1759 :type fig: matplotlib.figure.Figure or None
1760 :param axes: プロットに使うmatplotlib.axes.Axesオブジェクトの配列。Noneの場合、新しいAxesを作成。
1761 :type axes: numpy.ndarray or None
1762 :param V: プロットする電圧データの配列。
1763 :type V: numpy.ndarray
1764 :param I_meas: 測定された電流データの配列。Noneの場合、測定データはプロットされない。
1765 :type I_meas: numpy.ndarray or None
1766 :param I_start: 初期モデルによって計算された電流データの配列。Noneの場合、初期モデルはプロットされない。
1767 :type I_start: numpy.ndarray or None
1768 :param I_final: 最終モデルによって計算された電流データの配列。
1769 :type I_final: numpy.ndarray
1770 :param info: ファイル名やレコード時間などの情報を含む辞書。プロットのタイトルに使用。
1771 :type info: dict
1772 :param mode: 現在の実行モード(例: init, fit, sim)を示す文字列。プロットのタイトルに使用。
1773 :type mode: str
1774 :param sigma_log: モデル電流の log10 値の標準偏差の配列。信頼区間を表示するために使用。Noneの場合、信頼区間は表示されない。
1775 :type sigma_log: numpy.ndarray or None
1776 :param mode_label: プロットのタイトルに追加するモードのラベル。
1777 :type mode_label: str
1778 :param outfile: プロットを保存するファイルパス。Noneの場合、保存しない。
1779 :type outfile: str or None
1780 :param pause: プロットを表示後に一時停止するかどうか。Trueの場合、ユーザーのキー入力を待つ。
1781 :type pause: bool
1782
1783 戻り値:
1784 :returns: なし
1785 :rtype: None
1786 """
1787
1788 if fig is None:
1789 fig, axes = plt.subplots(1, 2, figsize=(11, 5))
1790
1791 ax = axes[0]
1792 ax.set_title(f"{info['FileName']} ({mode_label})")
1793 if I_meas is not None:
1794 ax.plot(V, I_meas, "o", color="black", alpha=0.3, markersize=4, label="data")
1795 if I_start is not None:
1796 ax.plot(V, I_start, "-", color="blue", lw=0.5, label="initial")
1797 ax.plot(V, I_final, "-", color="red", lw=1, label="final")
1798
1799 if sigma_log is not None:
1800 log_abs = np.log10(np.abs(I_final) + EPS_I)
1801 upper = 10.0 ** (log_abs + sigma_log)
1802 lower = 10.0 ** (log_abs - sigma_log)
1803 sign = np.sign(I_final) # 電流の符号を保持
1804 ax.fill_between(V, sign * lower, sign * upper, color="lightblue", alpha=0.5)
1805
1806 ax.axhline(0.0, color="k", lw=0.5)
1807 ax.axvline(0.0, color="k", lw=0.5)
1808 ax.tick_params(axis='x', labelsize=fontsize) # x軸の目盛りフォントサイズ
1809 ax.tick_params(axis='y', labelsize=fontsize) # y軸の目盛りフォントサイズ
1810 ax.set_xlabel("V / V", fontsize = fontsize)
1811 ax.set_ylabel("I / A", fontsize = fontsize)
1812 ax.grid(True)
1813 ax.legend()
1814
1815 ax = axes[1]
1816 ax.set_title("Log abs(I)")
1817 if I_meas is not None:
1818 ax.semilogy(V, np.abs(I_meas) + EPS_I, "o", color="black", alpha=0.3, markersize=4)
1819 ax.semilogy(V, np.abs(I_final) + EPS_I, "-", color="red", lw=1)
1820
1821 if sigma_log is not None:
1822 log_abs = np.log10(np.abs(I_final) + EPS_I)
1823 upper = 10.0 ** (log_abs + sigma_log)
1824 lower = 10.0 ** (log_abs - sigma_log)
1825 ax.fill_between(V, lower, upper, color="lightblue", alpha=0.5)
1826
1827 ax.tick_params(axis='x', labelsize=fontsize) # x軸の目盛りフォントサイズ
1828 ax.tick_params(axis='y', labelsize=fontsize) # y軸の目盛りフォントサイズ
1829 ax.set_xlabel("V / V", fontsize = fontsize)
1830 ax.set_ylabel("I / A", fontsize = fontsize)
1831 ax.grid(True, which="both")
1832
1833 fig.tight_layout()
1834 if outfile:
1835 print(f"[I/O] Save plot: {outfile}")
1836 fig.savefig(outfile, dpi=160)
1837 if pause:
1838 plt.show()
1839 else:
1840 plt.pause(0.01)
1841
1842
1843def setup_parameters(params, args, csv_fix, args_fix, forwardIV, reverseIV):
1844 """
1845 概要: パラメータ辞書を最終的に設定し、固定されるパラメータを決定します。
1846
1847 詳細説明:
1848 コマンドライン引数で上書きされた値、CSVファイルから読み込まれた値、
1849 デフォルト値、および未使用のモデルメカニズムに基づいて自動的に固定されるパラメータを統合します。
1850
1851 引数:
1852 :param params: 現在のパラメータ値を含む辞書。
1853 :type params: dict
1854 :param args: 解析されたコマンドライン引数を含むオブジェクト。
1855 :type args: argparse.Namespace
1856 :param csv_fix: CSVファイルで固定されたパラメータ名のセット。
1857 :type csv_fix: set
1858 :param args_fix: コマンドライン引数で固定されたパラメータ名のリスト。
1859 :type args_fix: list
1860 :param forwardIV: フォワードバイアスモデルのメカニズムを示す文字列。
1861 :type forwardIV: str
1862 :param reverseIV: リバースバイアスモデルのメカニズムを示す文字列。
1863 :type reverseIV: str
1864
1865 戻り値:
1866 :returns: params (最終的なパラメータ値の辞書), fix_set (最終的に固定されるパラメータ名のセット), used (使用されるパラメータ名のセット), auto_fixed (自動的に固定されたパラメータ名のセット)
1867 :rtype: tuple[dict, set, set, set]
1868
1869 例外:
1870 :raises ValueError: 不明な固定パラメータ名が指定された場合。
1871 """
1872 # コマンドライン引数によるパラメータ上書き
1873 for name in PARAM_NAMES:
1874 val = getattr(args, name)
1875 if val is not None:
1876 params[name] = val
1877
1878 # 不足しているパラメータにデフォルト値を設定
1879 defaults = {
1880 "I0": 1.0e-10,
1881 "ndiode": 1.5,
1882 "IPV": 1.0e-6,
1883 "Rs": 1.0,
1884 "Rsh": 1.0e9,
1885 "A_tfe": 1.0e-30,
1886 "E00": 1.0e-3,
1887 "A_fn": 1.0e-30,
1888 "B_fn": 10.0,
1889 "K_sclc": 1.0e-30,
1890 }
1891 for k, v in defaults.items():
1892 params.setdefault(k, v)
1893
1894 # 固定パラメータの決定
1895 fix_set = set(csv_fix) | set(args_fix) # CSVとコマンドライン引数の両方で指定されたものを結合
1896
1897 # 使用されていないモデルパラメータを自動的に固定
1898 used = used_params_from_modes(forwardIV, reverseIV)
1899 auto_fixed = set(PARAM_NAMES) - used
1900 fix_set |= auto_fixed
1901
1902 # 不明な固定パラメータ名のチェック
1903 unknown_fix = [x for x in fix_set if x not in PARAM_NAMES]
1904 if unknown_fix:
1905 raise ValueError(f"Unknown parameter name(s) in --fix: {unknown_fix}")
1906
1907 print(f"Used parameters : {' '.join(sorted(used))}")
1908 print(f"Auto-fixed unused : {' '.join(sorted(auto_fixed)) if auto_fixed else '(none)'}")
1909
1910 return params, fix_set, used, auto_fixed
1911
1912
1913def print_parameter_repeat(params, fix_set, used, auto_fixed, title=""):
1914 """
1915 概要: 現在のモデルパラメータとその固定状態をコンソールに出力します。
1916
1917 詳細説明:
1918 これは、print_args_and_derived の「Derived values」セクションに似ていますが、
1919 最終的に決定された各パラメータの具体的な値と、それが固定されているかどうかの状態を詳細に示します。
1920
1921 引数:
1922 :param params: 最終的なパラメータ値を含む辞書。
1923 :type params: dict
1924 :param fix_set: 固定されるパラメータ名のセット。
1925 :type fix_set: set
1926 :param used: モデルで使用されるパラメータ名のセット。
1927 :type used: set
1928 :param auto_fixed: モデルによって自動的に固定されたパラメータ名のセット。
1929 :type auto_fixed: set
1930 :param title: 出力の前に表示されるタイトル文字列。
1931 :type title: str
1932
1933 戻り値:
1934 :returns: なし
1935 :rtype: None
1936 """
1937 if title:
1938 print(title)
1939 for name in PARAM_NAMES:
1940 status = ""
1941 if name in fix_set:
1942 status = "(Fixed)"
1943 if name in auto_fixed:
1944 status = "(Auto-Fixed)"
1945
1946 print(f" {name:8s}: {params.get(name, 'N/A'):.6e} {status}")
1947 print()
1948
1949
1950# =============================================================================
1951# fit execution
1952# =============================================================================
1953
1954def exec_fit(V, I_meas, info, csv_path, params, fix_set, args):
1955 """
1956 概要: fit モードの本体処理。最適化、誤差推定、保存、最終プロットまでをまとめて実行する。
1957
1958 詳細説明:
1959 この関数は、scipy.optimize.minimize を用いてSDMパラメータを測定データにフィッティングします。
1960 フィッティング中には、コンソールへの進捗表示とリアルタイムのIVカーブアニメーションが行われます。
1961 最適化後、パラメータの誤差が推定され、結果がCSVファイルに保存され、最終的なプロットが表示されます。
1962
1963 引数:
1964 :param V: 測定された電圧データ点の配列。
1965 :type V: numpy.ndarray
1966 :param I_meas: 測定された電流データ点の配列。
1967 :type I_meas: numpy.ndarray
1968 :param info: ファイル名やレコード時間などの情報を含む辞書。プロットのタイトルに使用。
1969 :type info: dict
1970 :param csv_path: パラメータを保存するCSVファイルのパス。
1971 :type csv_path: pathlib.Path
1972 :param params: 初期パラメータ値を含む辞書。
1973 :type params: dict
1974 :param fix_set: 固定されるパラメータ名のセット。
1975 :type fix_set: set
1976 :param args: コマンドライン引数パーサーから得られた設定を含むオブジェクト。
1977 :type args: argparse.Namespace
1978
1979 戻り値:
1980 :returns: params (最適化後のパラメータ値の辞書), rss (残差平方和), errors (各パラメータの推定誤差の辞書), sigma_log_curve (モデル電流のlog10値の標準偏差の配列)
1981 :rtype: tuple[dict, float, dict, numpy.ndarray]
1982 """
1983
1984 print(f"Execute {args.mode} mode.")
1985 params_init = dict(params)
1986 I_start = model(V, params, args.forwardIV, args.reverseIV, args.dv_blend, args.temperature)
1987 fig, axes, anim = initialize_plot(V, I_meas, I_start, title="Optimization progress")
1988
1989 p0 = pack_free_params(params, fix_set)
1990 state = {"count": 0}
1991
1992 def callback(xk):
1993 state["count"] += 1
1994 curr = unpack_free_params(xk, params, fix_set)
1995
1996 if state["count"] % max(args.ninterval_print, 1) == 0:
1997 rss = objective(xk, V, I_meas, args, params, fix_set)
1998 pstr = " ".join(
1999 f"{n}={curr[n]:.3e}" for n in PARAM_NAMES if n not in fix_set
2000 )
2001 print(f"{state['count']:4d}: RSS={rss:.6e} {pstr}")
2002
2003 if state["count"] % max(args.ninterval_plot, 1) == 0:
2004 I_anim = model(
2005 V, curr,
2006 forwardIV=args.forwardIV,
2007 reverseIV=args.reverseIV,
2008 dV=args.dv_blend,
2009 temperature=args.temperature,
2010 )
2011 anim["line1"].set_ydata(I_anim)
2012 anim["line2"].set_ydata(np.abs(I_anim) + EPS_I)
2013 anim["ax1"].relim()
2014 anim["ax1"].autoscale_view()
2015 anim["ax2"].relim()
2016 anim["ax2"].autoscale_view()
2017 anim["fig"].canvas.draw()
2018 anim["fig"].canvas.flush_events()
2019 plt.pause(0.001)
2020
2021 if len(p0) == 0:
2022 print("All parameters are fixed. Skipping optimization.")
2023 class DummyResult:
2024 x = np.array([], dtype=float)
2025 fun = objective(np.array([], dtype=float), V, I_meas, args, params, fix_set)
2026 res = DummyResult()
2027 rss = float(res.fun)
2028 else:
2029 res = minimize(
2030 objective,
2031 p0,
2032 args=(V, I_meas, args, params, fix_set),
2033 method=args.method,
2034 callback=callback,
2035 options={
2036 "maxiter": int(args.nmaxiter),
2037 "xatol": float(args.tol),
2038 "fatol": float(args.tol),
2039 "disp": False,
2040 },
2041 )
2042 params = unpack_free_params(res.x, params, fix_set)
2043 rss = float(res.fun)
2044
2045 errors, sigma_log_curve = estimate_errors(
2046 res, V, I_meas, args.temperature, args, params, fix_set
2047 )
2048
2049 save_param_csv(csv_path, params, fix_set, errors=errors, rss=rss)
2050
2051 plt.ioff()
2052
2053 print("\n--- Fit Results ---")
2054 print(f"RSS_logy : {rss:.6e}")
2055 for n in PARAM_NAMES:
2056 if n in errors:
2057 print(f" {n:8s}: {params[n]:.6e} +/- {errors[n]:.2e}")
2058 else:
2059 print(f" {n:8s}: {params[n]:.6e}" + (" (Fixed)" if n in fix_set else ""))
2060
2061 print()
2062 print("Parameters estimated from input IV data (Use if the fitting accurary is poor)")
2063 est = estimate_initial_params(V, I_meas, args.temperature)
2064 print(f" Rs_est : {est['Rs']:.6f} ohm")
2065 print(f" Rsh_est : {est['Rsh']:.6f} ohm")
2066 print(f" ISC_est : {est['ISC_est']:.6e} A")
2067 print(f" VOC_est : {est['VOC_est']:.6f} V")
2068
2069 I_final = model(
2070 V, params,
2071 forwardIV=args.forwardIV,
2072 reverseIV=args.reverseIV,
2073 dV=args.dv_blend,
2074 temperature=args.temperature,
2075 )
2076
2077 # Excel保存
2078 outfile_xlsx = f"{args.outprefix}-fitted.xlsx"
2079 save_iv_to_excel(
2080 outfile_xlsx,
2081 V=V,
2082 I_input=I_meas,
2083 I_init=I_start,
2084 I_fit=I_final,
2085 info=info,
2086 params_init=params_init,
2087 params_final=params,
2088 rss=rss,
2089 errors=errors,
2090 )
2091
2092 # 画像保存してから表示
2093 outfile_iv = f"{args.outprefix}-iv.png"
2094 plot_iv(
2095 fig, axes, V, I_meas, I_start, I_final, info, args.mode,
2096 sigma_log=sigma_log_curve,
2097 outfile=outfile_iv,
2098 pause=False
2099 )
2100 input("\nPress ENTER to terminate\n")
2101
2102 return params, rss, errors, sigma_log_curve
2103
2104
2105# =============================================================================
2106# main
2107# =============================================================================
2108
2109_original_print = None
2110_redirect_fp = None
2111def dual_print(*args, **kwargs):
2112 """
2113 概要: 標準出力とログファイルの両方にメッセージを出力します。
2114
2115 詳細説明:
2116 組み込みの print 関数をオーバーライドするために使用され、
2117 すべての出力がコンソールと指定されたログファイルの両方に書き込まれます。
2118
2119 引数:
2120 :param args: print関数に渡される可変長引数。
2121 :type args: tuple
2122 :param kwargs: print関数に渡されるキーワード引数。
2123 :type kwargs: dict
2124
2125 戻り値:
2126 :returns: なし
2127 :rtype: None
2128 """
2129 _original_print(*args, **kwargs)
2130
2131 file_kwargs = dict(kwargs)
2132 file_kwargs.pop("file", None) # file指定があっても無視してログファイルへ
2133 file_kwargs.setdefault("flush", True)
2134
2135 _original_print(*args, file=_redirect_fp, **file_kwargs)
2136
2137
2138def main():
2139 """
2140 概要: スクリプトのエントリーポイント。コマンドライン引数を解析し、IVフィッティングツールを実行します。
2141
2142 詳細説明:
2143 この関数は、初期値推定 (init)、モデルフィッティング (fit)、またはシミュレーション (sim) の
2144 いずれかのモードで動作します。データ読み込み、パラメータ管理、最適化、結果のプロットを行います。
2145
2146 引数:
2147 なし
2148
2149 戻り値:
2150 :returns: なし
2151 :rtype: None
2152 """
2153
2154 global _original_print, _redirect_fp
2155
2156 print()
2157 print("Read commandline arguments")
2158 args, parser = initialize()
2159
2160 validate_args(args)
2161
2162 _original_print = builtins.print
2163 logfile = f"{args.outprefix}_{args.mode}.txt"
2164 print(f"Open log file [{logfile}]")
2165 _redirect_fp = open(logfile, "w", encoding="utf-8")
2166 builtins.print = dual_print
2167
2168 print_args_and_derived(args)
2169 print(f"forwardIV model : {args.forwardIV}")
2170 print(f"reverseIV model : {args.reverseIV}")
2171
2172 # データ入力処理
2173 print()
2174 if args.infile:
2175 print(f"Read data from [{args.infile}]")
2176 print(f" x range: {args.xmin} - {args.xmax}, every {args.ndataskip} data")
2177 V, I_meas, info = read_data(args.infile, xmin=args.xmin, xmax=args.xmax, ndataskip=args.ndataskip)
2178 csv_path = Path(args.infile).stem + "-parameters.csv"
2179 else:
2180 if args.mode != "sim":
2181 raise ValueError("--infile is required for mode init/fit/analyze.")
2182
2183 print(f"Create simulation data")
2184 # シミュレーションモードの場合、ダミーデータを生成
2185 V = np.linspace(-0.2, 0.8, 500)
2186 I_meas = None
2187 info = {"FileName": "Simulation", "RecordTime": "N/A"}
2188 csv_path = "sim-parameters.csv"
2189
2190 # パラメータの読み込みと初期値設定
2191 print()
2192 if os.path.exists(csv_path):
2193 print(f"Read parameters from [{csv_path}]")
2194 params, csv_fix = load_param_csv(csv_path)
2195 else:
2196 print(f"Parameter file [{csv_path}] does not exist. Use default.")
2197 params = {}
2198 csv_fix = set()
2199
2200 if (args.mode == "init" or args.mode == "analyze") or not any(name in params for name in PARAM_NAMES):
2201 if I_meas is not None:
2202 print()
2203 print(f"Estimate initial parameters at T={args.temperature}")
2204 est = estimate_initial_params(V, I_meas, args.temperature)
2205 else:
2206 print()
2207 print(f"Use default initial parameters at T={args.temperature}")
2208 est = DEFAULT_PARAMETERS
2209 params.update(est)
2210
2211 print()
2212 print("Final setup model parameters from paramter file and commandline arguments")
2213 params, fix_set, used, auto_fixed = setup_parameters(params, args, csv_fix, args.fix, args.forwardIV, args.reverseIV)
2214 print_parameter_repeat(params, fix_set, used, auto_fixed, title="=== Final model parameters ===")
2215
2216 print()
2217 if args.mode == "analyze" or args.mode == "init":
2218 print(f"Execute {args.mode} mode.")
2219 print(" Extracted parameters")
2220 print(f" ISC_est : {params.get('ISC_est', np.nan):.6e} A")
2221 print(f" VOC_est : {params.get('VOC_est', np.nan):.6f} V")
2222 print(f" Vsh_est : {params.get('Vsh_est', np.nan):.6f} V")
2223 print(f" Ish_est : {params.get('Ish_est', np.nan):.6e} A")
2224 print(f" Vtfe_est: {params.get('Vtfe_est', np.nan):.6f} V")
2225 print(f" Itfe_est: {params.get('Itfe_est', np.nan):.6e} A")
2226
2227 I_model = model(
2228 V, params,
2229 forwardIV=args.forwardIV,
2230 reverseIV=args.reverseIV,
2231 dV=args.dv_blend,
2232 temperature=args.temperature,
2233 )
2234
2235 if args.mode == "init":
2236 save_param_csv(csv_path, params, fix_set)
2237 outfile_xlsx = f"{args.outprefix}-fitted.xlsx"
2238 save_iv_to_excel(
2239 outfile_xlsx,
2240 V=V,
2241 I_input=I_meas,
2242 I_init=None,
2243 I_fit=I_model,
2244 info=info,
2245 params_init=params,
2246 params_final=None,
2247 rss=None,
2248 errors=None,
2249 )
2250
2251 plot_iv(
2252 None, None, V, I_meas, None, I_model, info, args.mode,
2253 sigma_log=None,
2254 outfile=f"{args.outprefix}-iv.png",
2255 pause=False,
2256 )
2257 input("\nPress ENTER to terminate\n")
2258
2259 elif args.mode == "fit":
2260 params, rss, errors, sigma_log_curve = exec_fit(
2261 V, I_meas, info, csv_path, params, fix_set, args
2262 )
2263
2264 elif args.mode == "sim":
2265 print(f"Execute {args.mode} mode.")
2266 I_model = model(
2267 V, params,
2268 forwardIV=args.forwardIV,
2269 reverseIV=args.reverseIV,
2270 dV=args.dv_blend,
2271 temperature=args.temperature,
2272 )
2273
2274 outfile_xlsx = f"{args.outprefix}-fitted.xlsx"
2275 save_iv_to_excel(
2276 outfile_xlsx,
2277 V=V,
2278 I_input=None,
2279 I_init=None,
2280 I_fit=I_model,
2281 info=info,
2282 params_init=params,
2283 params_final=None,
2284 rss=None,
2285 errors=None,
2286 )
2287
2288 plot_iv(
2289 None, None, V, I_meas, None, I_model, info, args.mode,
2290 sigma_log=None,
2291 outfile=f"{args.outprefix}-iv.png",
2292 pause=False,
2293 )
2294 input("\nPress ENTER to terminate\n")
2295
2296
2297if __name__ == "__main__":
2298 try:
2299 main()
2300 except Exception:
2301 traceback.print_exc()
2302 sys.exit(1)
2303 finally:
2304 if _redirect_fp: _redirect_fp.close()