pvanalyze.py ダウンロード/コピー

pvanalyze.py をダウンロード

pvanalyze.py
pvanalyze.py
   1#!/usr/bin/env python3
   2# -*- coding: utf-8 -*-
   3
   4"""
   5PV特性評価ツールスクリプト。
   6
   7概要:
   8    太陽電池の電気的・光学的特性を評価するツールです。
   9詳細説明:
  10    mode=alpha または mode=make_alpha では、反射率と透過率のスペクトルから
  11    吸収スペクトルを計算し、プロットおよびExcelへの保存を行います。
  12    mode=analyze では、I-Vデータからパラメータ推定、発電特性解析、量子効率計算、
  13    および結果のプロットを行います。
  14主な機能:
  15    - 反射率/透過率から吸収スペクトルの計算と保存
  16    - I-Vデータからの太陽電池パラメータ(I0, ndiode, IPV, Rs, Rsh)推定
  17    - 発電特性(Voc, Jsc, FF, Pmaxなど)の解析
  18    - 量子効率(EQE, IQE)の計算
  19    - 解析結果のプロットとExcelファイルへの保存
  20関連リンク:
  21    pvanalyze_usage
  22"""
  23
  24import sys
  25import argparse
  26import builtins
  27from pathlib import Path
  28import traceback
  29import csv
  30import math
  31
  32import matplotlib.pyplot as plt
  33import numpy as np
  34from openpyxl import Workbook, load_workbook
  35from openpyxl.styles import Font, PatternFill
  36
  37
  38KB = 1.380649e-23
  39E_CHARGE = 1.602176634e-19
  40H = 6.62607015e-34
  41C = 2.99792458e8
  42
  43PARAM_NAMES = ["I0", "ndiode", "IPV", "Rs", "Rsh"]
  44EPS_I = 1.0e-15
  45
  46DPV_NM = 46.837
  47AREA_MM2 = 0.5 * 0.5 * math.pi
  48AREA_CM2 = AREA_MM2 * 0.01  # 1 mm^2 = 0.01 cm^2
  49PHOTON_FLUX = 1.95804e18  # cm^-2 s^-1
  50PHOTON_NM = 1363.0        # nm
  51PIN = ""                  # W/cm^2, if None and F0 is not None then compute from hc/lambda
  52
  53fontsize = 16
  54
  55DARK_IV_FILE = "I_V SweepTest SMU1 [(1) ; 2026_03_24 14_15_24]-SY251213-1-LD0.0V.csv"
  56PV_IV_FILE = "I_V SweepTest SMU1 [(10) ; 2026_03_24 14_18_37]-SY251213-1-LD1.8V.csv"
  57R_FILE = "SY251024-1-Bi4O6S21-STO001-225oC-10mJ_R(UDS).txt"
  58T_FILE = "SY251024-1-Bi4O6S21-STO001-225oC-10mJ_T(UDS).txt"
  59ALPHA_FILE = "alpha_Bi2OS2.xlsx"
  60
  61
  62def initialize():
  63    """
  64    概要:
  65        コマンドライン引数を解析し、プログラムの初期設定を行います。
  66    詳細説明:
  67        argparseモジュールを使用して、実行モード、ファイルパス、温度、膜厚などの
  68        パラメータを定義し、ユーザー入力から値をパースします。
  69    引数:
  70        なし
  71    戻り値:
  72        :returns: 解析された引数オブジェクトと引数パーサーオブジェクトのタプル。
  73        :rtype: tuple[argparse.Namespace, argparse.ArgumentParser]
  74    """
  75    parser = argparse.ArgumentParser(description="PV characterization tool with alpha/analyze modes.")
  76    parser.add_argument("--mode", default="analyze", choices=["alpha", "make_alpha", "analyze"],
  77                        help="Execution mode")
  78    parser.add_argument("--dark", default=DARK_IV_FILE, help="Dark IV CSV file")
  79    parser.add_argument("--light", default=PV_IV_FILE, help="Illuminated IV CSV file")
  80    parser.add_argument("--R", default=R_FILE, help="Reflectance spectrum file")
  81    # NOTE: --T is already requested for temperature, so transmittance file uses --Tr.
  82    parser.add_argument("--Tr", default=T_FILE, help="Transmittance spectrum file")
  83    parser.add_argument("--eq", default=None, help="Equivalent spectral irradiance file")
  84    parser.add_argument("--alpha", default=ALPHA_FILE, help="Excel file for absorption spectrum / alpha")
  85    parser.add_argument("--T", type=float, default=300.0, help="Temperature (K)")
  86    parser.add_argument("--d", type=float, default=DPV_NM, help="PV layer thickness (nm)")
  87    parser.add_argument("--sweep_dark", type=int, default=0, help="Sweep index for dark analysis")
  88    parser.add_argument("--sweep_light", type=int, default=0, help="Sweep index for light analysis")
  89    parser.add_argument("--F0", type=float, default=PHOTON_FLUX, help="Incident photon flux (cm^-2 s^-1)")
  90    parser.add_argument("--P0", type=str, default=PIN, help="Incident photon energy density (W/cm^2)")
  91    parser.add_argument("--E", type=float, default=0.0, help="Photon energy (eV). If 0, use lambda.")
  92    parser.add_argument("--lambda_nm", type=float, default=PHOTON_NM,
  93                        help="Photon wavelength (nm), used when E=0")
  94    parser.add_argument("--S", type=float, default=AREA_CM2, help="Electrode area (cm^2)")
  95    parser.add_argument("--outprefix", default="pvanalyze", help="Output file prefix")
  96    args = parser.parse_args()
  97    return args, parser
  98
  99
 100def print_args_and_derived(args, E_use=None, P0_use=None):
 101    """
 102    概要:
 103        解析されたコマンドライン引数と派生値を標準出力に表示します。
 104    引数:
 105        :param args: 解析された引数オブジェクト。
 106        :type args: argparse.Namespace
 107        :param E_use: 使用されるフォトンエネルギー (eV)。
 108        :type E_use: float or None
 109        :param P0_use: 使用される入射光パワー密度 (W/cm^2)。
 110        :type P0_use: float or None
 111    戻り値:
 112        なし
 113    例外:
 114        :raises ValueError: フォトン波長 lambda_nm が0以下の場合。
 115        :raises ValueError: フォトンエネルギー E が0未満の場合。
 116        :raises ValueError: 入射フォトンフラックス F0 が0以下の場合。
 117        :raises ValueError: 入射光パワー密度 P0 が0以下の場合。
 118        :raises ValueError: 温度 T が0以下の場合。
 119        :raises ValueError: 電極面積 S が0以下の場合。
 120        :raises ValueError: PV層の膜厚 d が0.1nm以下の場合。
 121        :raises ValueError: compute_p0 関数から None が返された場合。
 122    """
 123    print("=== Arguments ===")
 124    print(f"mode         = {args.mode}")
 125    if args.mode == "make_alpha":
 126        print(f"R            = {args.R}")
 127        print(f"Tr           = {args.Tr}")
 128    else:
 129        print(f"alpha        = {args.alpha}")
 130    print(f"d            = {args.d} nm")
 131    print(f"outprefix    = {args.outprefix}")
 132
 133    if "alpha" not in args.mode:
 134        print(f"dark         = {args.dark}")
 135        print(f"light        = {args.light}")
 136        print(f"T            = {args.T} K")
 137        print(f"sweep_dark   = {args.sweep_dark}")
 138        print(f"sweep_light  = {args.sweep_light}")
 139        if args.F0 is not None:
 140            print(f"F0           = {args.F0} cm^-2 s^-1")
 141        if args.P0 != "":
 142            print(f"P0           = {args.P0} W/cm^2")
 143#        print(f"eq           = {args.eq}")
 144        print(f"E            = {args.E} eV")
 145        print(f"lambda_nm    = {args.lambda_nm} nm")
 146        print(f"S            = {args.S} cm^2")
 147    print()
 148
 149    if args.mode == 'analyze':
 150        if args.P0 == "":
 151            if args.E is not None and args.E == 0.0:
 152                if args.lambda_nm is None or args.lambda_nm <= 0.0:
 153                    raise ValueError(f"lambda_nm must be > 0 nm, got {args.lambda_nm}")
 154            elif args.E is not None and args.E < 0.0:
 155                raise ValueError(f"E must be >= 0 eV, got {args.E}")
 156
 157            if args.F0 <= 0.0:
 158                raise ValueError(f"F0 must be > 0 cm^-2 s^-1, got {args.F0}")
 159
 160            args.E = compute_photon_energy_eV(args.E, args.lambda_nm)
 161            print(f"E_use  = {args.E} eV")
 162            args.P0 = compute_p0(args.F0, args.P0, args.E, args.lambda_nm)
 163            if args.P0 is None:
 164                raise ValueError(f"Got None from compute_p0() using F0={args.F0} and E={args.E}")
 165            print(f"P0_use = {args.P0} W/cm^2" if args.P0 is not None else "P0_use       = None")
 166        else:
 167            args.P0 = float(args.P0)
 168            if args.P0 <= 0.0:
 169                raise ValueError(f"P0 must be > 0 W/cm^2, got {args.P0}")
 170
 171        if args.T <= 0.0:
 172            raise ValueError(f"T must be > 0 K, got {args.T}")
 173        if args.S <= 0.0:
 174            raise ValueError(f"S must be > 0 cm^2, got {args.S}")
 175
 176        if args.d <= 0.1:
 177            raise ValueError(f"d must be > 0.1 nm, got {args.d}")
 178
 179
 180def read_data(infile, xmin=None, xmax=None, ndataskip=0):
 181    """
 182    概要:
 183        指定されたCSVファイルからI-Vデータを読み込みます。
 184    詳細説明:
 185        ファイル内のメタデータ(記録時間、データ名)を抽出し、電圧と電流のデータポイントをパースします。
 186        複数のスイープを検出し、それぞれをリストに分割します。
 187    引数:
 188        :param infile: 読み込むCSVファイルのパス。
 189        :type infile: str
 190        :param xmin: X軸(電圧)の最小値。これより小さい値はスキップされます。
 191        :type xmin: float or None
 192        :param xmax: X軸(電圧)の最大値。これより大きい値はスキップされます。
 193        :type xmax: float or None
 194        :param ndataskip: データポイントをスキップする間隔。0の場合、スキップしません。
 195        :type ndataskip: int
 196    戻り値:
 197        :returns: 各スイープのX軸(電圧)データのリスト、各スイープのY軸(電流)データのリスト、ファイルから抽出されたメタデータのタプル。
 198        :rtype: tuple[list[numpy.ndarray], list[numpy.ndarray], dict]
 199    例外:
 200        :raises ValueError: ファイルから有効なI-Vデータが見つからない場合。
 201    """
 202    print(f"[I/O] Read IV data: {infile}")
 203    raw_x = []
 204    raw_y = []
 205    inf = {"FileName": infile, "RecordTime": "Unknown", "DataName": ("V", "I")}
 206
 207    icount = 0
 208    data_started = False
 209    with open(infile, "r", encoding="utf-8-sig", newline="") as f:
 210        reader = csv.reader(f)
 211        for row in reader:
 212            if not row:
 213                continue
 214            cols = [c.strip() for c in row]
 215
 216            if len(cols) >= 3 and cols[0] == "MetaData" and "RecordTime" in cols[1]:
 217                inf["RecordTime"] = cols[2]
 218
 219            if cols[0] == "DataName":
 220                if len(cols) >= 3:
 221                    inf["DataName"] = (cols[1], cols[2])
 222                data_started = True
 223                continue
 224
 225            if data_started and cols[0] == "DataValue":
 226                if len(cols) < 3:
 227                    continue
 228                icount += 1
 229                if ndataskip > 0 and (icount - 1) % ndataskip != 0:
 230                    continue
 231                try:
 232                    x = float(cols[1])
 233                    y = float(cols[2])
 234                except ValueError:
 235                    continue
 236                if xmin is not None and x < xmin:
 237                    continue
 238                if xmax is not None and x > xmax:
 239                    continue
 240                raw_x.append(x)
 241                raw_y.append(y)
 242
 243    if not raw_x:
 244        raise ValueError(f"No valid IV data found in {infile}")
 245
 246    xs_list = []
 247    ys_list = []
 248
 249    cur_x = [raw_x[0]]
 250    cur_y = [raw_y[0]]
 251    prev_sign = 0
 252
 253    for i in range(1, len(raw_x)):
 254        dx = raw_x[i] - raw_x[i - 1]
 255        sign = 0 if abs(dx) < 1e-15 else (1 if dx > 0 else -1)
 256
 257        if prev_sign == 0:
 258            prev_sign = sign
 259
 260        if sign != 0 and prev_sign != 0 and sign != prev_sign:
 261            xs_list.append(np.asarray(cur_x, dtype=float))
 262            ys_list.append(np.asarray(cur_y, dtype=float))
 263            cur_x = [raw_x[i - 1], raw_x[i]]
 264            cur_y = [raw_y[i - 1], raw_y[i]]
 265            prev_sign = sign
 266        else:
 267            cur_x.append(raw_x[i])
 268            cur_y.append(raw_y[i])
 269            if sign != 0:
 270                prev_sign = sign
 271
 272    if cur_x:
 273        xs_list.append(np.asarray(cur_x, dtype=float))
 274        ys_list.append(np.asarray(cur_y, dtype=float))
 275
 276    inf["Points"] = sum(len(x) for x in xs_list)
 277    inf["NSweeps"] = len(xs_list)
 278    return xs_list, ys_list, inf
 279
 280
 281def read_alpha_from_excel(infile):
 282    """
 283    概要:
 284        Excelファイルから吸収スペクトル(alpha)データを読み込みます。
 285    詳細説明:
 286        save_alpha_to_excel関数によって保存された形式のExcelファイルを想定しています。
 287        alpha_spectrumシートから波長、R、Tr、A、alphaのデータを抽出します。
 288    引数:
 289        :param infile: 読み込むExcelファイルのパス。
 290        :type infile: str
 291    戻り値:
 292        :returns: 波長、反射率、透過率、吸収率、吸収係数を含む辞書。
 293        :rtype: dict
 294    例外:
 295        :raises ValueError: 指定されたExcelファイルに'alpha_spectrum'シートが見つからない場合。
 296        :raises ValueError: ファイルから数値の吸収スペクトルデータが見つからない場合。
 297    """
 298    print(f"[I/O] Read alpha Excel: {infile}")
 299    wb = load_workbook(infile, data_only=True)
 300    if "alpha_spectrum" not in wb.sheetnames:
 301        raise ValueError(f"Sheet 'alpha_spectrum' not found in {infile}")
 302    ws = wb["alpha_spectrum"]
 303
 304    wl = []
 305    R = []
 306    Tr = []
 307    A = []
 308    alpha = []
 309
 310    first = True
 311    for row in ws.iter_rows(values_only=True):
 312        if first:
 313            first = False
 314            continue
 315        if row is None or len(row) < 6:
 316            continue
 317        try:
 318            # columns: photon_energy_eV, wavelength_nm, R, Tr, A, alpha_cm^-1
 319            wl.append(float(row[1]))
 320            R.append(float(row[2]))
 321            Tr.append(float(row[3]))
 322            A.append(float(row[4]))
 323            alpha.append(float(row[5]))
 324        except (TypeError, ValueError):
 325            continue
 326
 327    if not wl:
 328        raise ValueError(f"No numeric alpha data found in {infile}")
 329
 330    return {
 331        "wl_nm": np.asarray(wl, dtype=float),
 332        "R": np.asarray(R, dtype=float),
 333        "Tr": np.asarray(Tr, dtype=float),
 334        "A": np.asarray(A, dtype=float),
 335        "alpha_cm^-1": np.asarray(alpha, dtype=float),
 336    }
 337
 338
 339def read_optical_spectrum(infile):
 340    """
 341    概要:
 342        指定されたテキストファイルから光学スペクトルデータ(反射率Rまたは透過率T)を読み込みます。
 343    詳細説明:
 344        ファイル内のタブ区切りまたはスペース区切りのデータをパースし、
 345        波長とスペクトル値のペアを抽出します。
 346        重複する波長を処理し、波長順にソートします。
 347    引数:
 348        :param infile: 読み込む光学スペクトルファイルのパス。
 349        :type infile: str
 350    戻り値:
 351        :returns: 波長 (nm)のnumpy.ndarray、スペクトル値 (RまたはTのパーセンテージ)のnumpy.ndarray、
 352                  ファイルから抽出されたメタデータを含む辞書のタプル。
 353        :rtype: tuple[numpy.ndarray, numpy.ndarray, dict]
 354    例外:
 355        :raises RuntimeError: ファイルの読み込みに失敗した場合。
 356        :raises ValueError: ファイルから数値のスペクトルデータが見つからない場合。
 357    """
 358    
 359    print(f"[I/O] Read optical spectrum: {infile}")
 360    info = {"FileName": infile, "Label": Path(infile).stem, "YUnit": "a.u."}
 361    lines = []
 362
 363    encodings = ["cp932", "utf-8-sig", "utf-8", "latin1"]
 364    last_err = None
 365    for enc in encodings:
 366        try:
 367            with open(infile, "r", encoding=enc, errors="replace") as f:
 368                lines = f.readlines()
 369            break
 370        except Exception as e:
 371            last_err = e
 372    if not lines:
 373        raise RuntimeError(f"Failed to read optical spectrum: {infile} ({last_err})")
 374
 375    wl = []
 376    val = []
 377
 378    for line in lines:
 379        s = line.strip()
 380        if not s:
 381            continue
 382
 383        if "\t" in s:
 384            parts = [p.strip() for p in s.split("\t")]
 385            if len(parts) >= 2:
 386                if "サンプル" in parts[0] or "サンプル" in parts[0]:
 387                    info["Label"] = parts[1]
 388                if "%T" in parts[1]:
 389                    info["YUnit"] = "%T"
 390                elif "%R" in parts[1]:
 391                    info["YUnit"] = "%R"
 392
 393        tokens = s.replace(",", " ").replace("\t", " ").split()
 394        nums = []
 395        for t in tokens:
 396            try:
 397                nums.append(float(t))
 398            except ValueError:
 399                pass
 400        if len(nums) >= 2:
 401            wl.append(nums[0])
 402            val.append(nums[1])
 403
 404    if not wl:
 405        raise ValueError(f"No numeric spectrum data found in {infile}")
 406
 407    wl = np.asarray(wl, dtype=float)
 408    val = np.asarray(val, dtype=float)
 409
 410    uu, idx = np.unique(wl, return_index=True)
 411    wl = wl[np.sort(idx)]
 412    val = val[np.sort(idx)]
 413
 414    order = np.argsort(wl)
 415    wl = wl[order]
 416    val = val[order]
 417    return wl, val, info
 418
 419
 420def choose_sweep(xs_list, ys_list, sweep_index=0):
 421    """
 422    概要:
 423        複数のスイープデータから指定されたインデックスの単一スイープを選択します。
 424    引数:
 425        :param xs_list: 各スイープのX軸(電圧)データのリスト。
 426        :type xs_list: list[numpy.ndarray]
 427        :param ys_list: 各スイープのY軸(電流)データのリスト。
 428        :type ys_list: list[numpy.ndarray]
 429        :param sweep_index: 選択するスイープのインデックス。
 430        :type sweep_index: int
 431    戻り値:
 432        :returns: 選択されたスイープのX軸データとY軸データ。
 433        :rtype: tuple[numpy.ndarray, numpy.ndarray]
 434    例外:
 435        :raises ValueError: スイープデータが利用できない場合。
 436        :raises IndexError: sweep_indexが範囲外の場合。
 437    """
 438    if len(xs_list) == 0:
 439        raise ValueError("No sweep data available.")
 440    idx = int(sweep_index)
 441    if idx < 0 or idx >= len(xs_list):
 442        raise IndexError(f"sweep_index={idx} is out of range (0..{len(xs_list)-1})")
 443    return np.asarray(xs_list[idx], dtype=float), np.asarray(ys_list[idx], dtype=float)
 444
 445
 446def consolidate_duplicate_x(x, y):
 447    """
 448    概要:
 449        X軸に重複する値がある場合、Y軸の対応する値を平均して重複を解消します。
 450    引数:
 451        :param x: X軸データ。
 452        :type x: numpy.ndarray
 453        :param y: Y軸データ。
 454        :type y: numpy.ndarray
 455    戻り値:
 456        :returns: 重複が解消されたX軸データと、重複が解消され平均化されたY軸データ。
 457        :rtype: tuple[numpy.ndarray, numpy.ndarray]
 458    """
 459    x = np.asarray(x, dtype=float)
 460    y = np.asarray(y, dtype=float)
 461    order = np.argsort(x)
 462    xs = x[order]
 463    ys = y[order]
 464
 465    ux = []
 466    uy = []
 467    i = 0
 468    n = len(xs)
 469    while i < n:
 470        xv = xs[i]
 471        vals = [ys[i]]
 472        j = i + 1
 473        while j < n and abs(xs[j] - xv) < 1e-15:
 474            vals.append(ys[j])
 475            j += 1
 476        ux.append(xv)
 477        uy.append(float(np.mean(vals)))
 478        i = j
 479    return np.asarray(ux), np.asarray(uy)
 480
 481
 482def smooth_polyfit(y, window_points=5, poly_order=3):
 483    """
 484    概要:
 485        多項式フィッティングを用いたSavitzky-Golay風の平滑化をデータに適用します。
 486    詳細説明:
 487        各データポイントを中心に指定されたウィンドウ内のデータに対して多項式フィッティングを行い、
 488        中心点の値を予測することで平滑化を行います。
 489    引数:
 490        :param y: 平滑化するY軸データ。
 491        :type y: numpy.ndarray
 492        :param window_points: フィッティングに使用するウィンドウ内のデータポイント数。奇数である必要があります。
 493        :type window_points: int
 494        :param poly_order: 多項式フィッティングの次数。
 495        :type poly_order: int
 496    戻り値:
 497        :returns: 平滑化されたY軸データ。
 498        :rtype: numpy.ndarray
 499    """
 500    y = np.asarray(y, dtype=float)
 501    n = len(y)
 502    if n < 3:
 503        return y.copy()
 504
 505    if window_points < 3:
 506        window_points = 3
 507    if window_points % 2 == 0:
 508        window_points += 1
 509    if window_points > n:
 510        window_points = n if (n % 2 == 1) else n - 1
 511    if poly_order >= window_points:
 512        poly_order = window_points - 1
 513    poly_order = max(poly_order, 1)
 514
 515    half = window_points // 2
 516    ys = np.empty(n, dtype=float)
 517
 518    for i in range(n):
 519        i0 = max(0, i - half)
 520        i1 = min(n, i + half + 1)
 521        while (i1 - i0) < window_points:
 522            if i0 > 0:
 523                i0 -= 1
 524            elif i1 < n:
 525                i1 += 1
 526            else:
 527                break
 528        idx = np.arange(i0, i1, dtype=float)
 529        yy = y[i0:i1]
 530        xloc = idx - i
 531        deg = min(poly_order, len(yy) - 1)
 532        coeff = np.polyfit(xloc, yy, deg)
 533        ys[i] = np.polyval(coeff, 0.0)
 534    return ys
 535
 536
 537def local_poly_value(x, y, x0, npts=7, order=3):
 538    """
 539    概要:
 540        指定されたX座標の周囲のデータポイントを使用して、局所的な多項式フィッティングを行い、
 541        x0におけるY値を推定します。
 542    引数:
 543        :param x: X軸データ。
 544        :type x: numpy.ndarray
 545        :param y: Y軸データ。
 546        :type y: numpy.ndarray
 547        :param x0: Y値を推定するX座標。
 548        :type x0: float
 549        :param npts: フィッティングに使用するデータポイント数。
 550        :type npts: int
 551        :param order: 多項式フィッティングの次数。
 552        :type order: int
 553    戻り値:
 554        :returns: x0における推定されたY値。
 555        :rtype: float
 556    """
 557    x = np.asarray(x, dtype=float)
 558    y = np.asarray(y, dtype=float)
 559    idx = np.argsort(np.abs(x - x0))[:max(2, npts)]
 560    xs = x[idx]
 561    ys = y[idx]
 562    sidx = np.argsort(xs)
 563    xs = xs[sidx]
 564    ys = ys[sidx]
 565    deg = min(order, len(xs) - 1)
 566    coeff = np.polyfit(xs, ys, deg)
 567    return float(np.polyval(coeff, x0))
 568
 569
 570def zero_crossing_x(x, y):
 571    """
 572    概要:
 573        Y値がゼロを横切るX座標を線形補間によって見つけます。
 574    詳細説明:
 575        2つの連続するデータポイントの間でY値の符号が変わる点、
 576        またはY値が最もゼロに近い点のX座標を返します。
 577    引数:
 578        :param x: X軸データ。
 579        :type x: numpy.ndarray
 580        :param y: Y軸データ。
 581        :type y: numpy.ndarray
 582    戻り値:
 583        :returns: Y値がゼロを横切るX座標。
 584        :rtype: float
 585    """
 586    x = np.asarray(x, dtype=float)
 587    y = np.asarray(y, dtype=float)
 588    order = np.argsort(x)
 589    x = x[order]
 590    y = y[order]
 591
 592    for i in range(len(x) - 1):
 593        y1, y2 = y[i], y[i + 1]
 594        if y1 == 0:
 595            return float(x[i])
 596        if y1 * y2 < 0:
 597            return float(x[i] + (0 - y1) * (x[i + 1] - x[i]) / (y2 - y1))
 598    return float(x[np.argmin(np.abs(y))])
 599
 600
 601def interpolate_to_common_wavelength(wl_ref, y_ref, wl_target):
 602    """
 603    概要:
 604        参照波長スケール上のデータをターゲット波長スケールに線形補間します。
 605    引数:
 606        :param wl_ref: 参照波長データ (nm)。
 607        :type wl_ref: numpy.ndarray
 608        :param y_ref: 参照Y軸データ。
 609        :type y_ref: numpy.ndarray
 610        :param wl_target: 補間対象のターゲット波長データ (nm)。
 611        :type wl_target: numpy.ndarray
 612    戻り値:
 613        :returns: ターゲット波長スケールに補間されたY軸データ。
 614        :rtype: numpy.ndarray
 615    """
 616    wl_ref = np.asarray(wl_ref, dtype=float)
 617    y_ref = np.asarray(y_ref, dtype=float)
 618    wl_target = np.asarray(wl_target, dtype=float)
 619    return np.interp(wl_target, wl_ref, y_ref, left=np.nan, right=np.nan)
 620
 621
 622def compute_photon_energy_eV(E_eV, lambda_nm):
 623    """
 624    概要:
 625        フォトンエネルギー(eV)を計算します。
 626    詳細説明:
 627        フォトンエネルギーが直接与えられている場合はそれを使用し、
 628        そうでない場合は波長から計算します。
 629    引数:
 630        :param E_eV: フォトンエネルギー (eV)。0より大きい場合はこれを使用。
 631        :type E_eV: float
 632        :param lambda_nm: フォトン波長 (nm)。E_eVが0の場合にこれを使用。
 633        :type lambda_nm: float
 634    戻り値:
 635        :returns: 計算されたフォトンエネルギー (eV)。
 636        :rtype: float
 637    """
 638    if E_eV is not None and E_eV > 0:
 639        return float(E_eV)
 640    return float(1239.841984 / lambda_nm)
 641
 642
 643def compute_p0(F0, P0, E_eV, lambda_nm):
 644    """
 645    概要:
 646        入射フォトンエネルギー密度(P0)を計算します。
 647    詳細説明:
 648        入射フォトンフラックス(F0)が与えられている場合はそれから計算し、
 649        そうでない場合は直接指定されたP0を使用します。
 650    引数:
 651        :param F0: 入射フォトンフラックス (cm^-2 s^-1)。Noneまたは0の場合はP0を使用します。
 652        :type F0: float or None
 653        :param P0: 入射フォトンエネルギー密度 (W/cm^2)。Noneの場合はF0から計算します。
 654        :type P0: str or None
 655        :param E_eV: フォトンエネルギー (eV)。F0からP0を計算する場合に使用。
 656        :type E_eV: float
 657        :param lambda_nm: フォトン波長 (nm)。F0からP0を計算する場合に使用。
 658        :type lambda_nm: float
 659    戻り値:
 660        :returns: 計算された入射フォトンエネルギー密度 (W/cm^2)。
 661        :rtype: float or None
 662    """
 663    if F0 is not None and F0 > 0:
 664        E_use = compute_photon_energy_eV(E_eV, lambda_nm)
 665        return float(F0 * E_use * E_CHARGE)
 666    return None if P0 is None else float(P0)
 667
 668
 669def pv_metrics_from_iv(V, I, S):
 670    """
 671    概要:
 672        I-Vデータから太陽電池の主要な性能指標(Voc, Jsc, FF, Pmaxなど)を計算します。
 673    引数:
 674        :param V: 電圧データ (V)。
 675        :type V: numpy.ndarray
 676        :param I: 電流データ (A)。
 677        :type I: numpy.ndarray
 678        :param S: 電極面積 (cm^2)。
 679        :type S: float
 680    戻り値:
 681        :returns: 以下の主要な太陽電池性能指標を含む辞書。
 682        :rtype: dict
 683            - Voc_V (float): 開放電圧 (V)。
 684            - Jsc_A_cm2 (float): 短絡電流密度 (A/cm^2)。
 685            - Jsc_mA_cm2 (float): 短絡電流密度 (mA/cm^2)。
 686            - Vop_V (float): 最大出力動作電圧 (V)。
 687            - Jop_A_cm2 (float): 最大出力動作電流密度 (A/cm^2)。
 688            - Jop_mA_cm2 (float): 最大出力動作電流密度 (mA/cm^2)。
 689            - Pmax_W_cm2 (float): 最大出力電力密度 (W/cm^2)。
 690            - Pmax_mW_cm2 (float): 最大出力電力密度 (mW/cm^2)。
 691            - FF (float): 曲線因子。
 692    """
 693    V = np.asarray(V, dtype=float)
 694    I = np.asarray(I, dtype=float)
 695    J = I / S
 696
 697    Jsc = local_poly_value(V, J, 0.0, npts=7, order=3)
 698    Voc = zero_crossing_x(V, I)
 699
 700    Pgen = -V * J
 701    idx = int(np.argmax(Pgen))
 702    Vop = float(V[idx])
 703    Jop = float(J[idx])
 704    Pmax = float(Pgen[idx])
 705
 706    denom = abs(Voc * Jsc)
 707    FF = float(Pmax / denom) if denom > 1e-30 else float("nan")
 708
 709    return {
 710        "Voc_V": float(Voc),
 711        "Jsc_A_cm2": float(Jsc),
 712        "Jsc_mA_cm2": float(Jsc * 1e3),
 713        "Vop_V": float(Vop),
 714        "Jop_A_cm2": float(Jop),
 715        "Jop_mA_cm2": float(Jop * 1e3),
 716        "Pmax_W_cm2": float(Pmax),
 717        "Pmax_mW_cm2": float(Pmax * 1e3),
 718        "FF": float(FF),
 719    }
 720
 721
 722def fit_local_line(x, y, center_idx, npts=7):
 723    """
 724    概要:
 725        指定された中心インデックスの周囲のデータポイントを用いて局所的な線形フィッティングを行います。
 726    引数:
 727        :param x: X軸データ。
 728        :type x: numpy.ndarray
 729        :param y: Y軸データ。
 730        :type y: numpy.ndarray
 731        :param center_idx: フィッティングの中心となるデータポイントのインデックス。
 732        :type center_idx: int
 733        :param npts: フィッティングに使用するデータポイント数。
 734        :type npts: int
 735    戻り値:
 736        :returns: フィットされた直線の傾き、Y切片、フィッティングに使用されたXデータ、
 737                  Yデータ、開始インデックス、終了インデックス(排他的)のタプル。
 738        :rtype: tuple[float, float, numpy.ndarray, numpy.ndarray, int, int]
 739    """
 740    x = np.asarray(x, dtype=float)
 741    y = np.asarray(y, dtype=float)
 742    n = len(x)
 743    npts = max(2, min(int(npts), n))
 744    half = npts // 2
 745    i0 = max(0, center_idx - half)
 746    i1 = min(n, i0 + npts)
 747    i0 = max(0, i1 - npts)
 748    xx = x[i0:i1]
 749    yy = y[i0:i1]
 750    a, b = np.polyfit(xx, yy, 1)
 751    return float(a), float(b), xx, yy, i0, i1
 752
 753
 754def estimate_rs_tangent_point(V, I):
 755    """
 756    概要:
 757        I-V曲線から直列抵抗(Rs)を推定するための接点と抵抗値を計算します。
 758    詳細説明:
 759        順方向バイアス領域でdI/dVが最大となる点を特定し、その点での接線からRsを計算します。
 760    引数:
 761        :param V: 電圧データ (V)。
 762        :type V: numpy.ndarray
 763        :param I: 電流データ (A)。
 764        :type I: numpy.ndarray
 765    戻り値:
 766        :returns: 直列抵抗推定に関する情報を含む辞書。
 767        :rtype: dict
 768            - v_rep (float): 接点電圧 (V)。
 769            - i_rep (float): 接点電流 (A)。
 770            - Is (float): 接点電流 (A) (互換性のため)。
 771            - Rs (float): 推定された直列抵抗 (Ω)。
 772            - slope (float): 接線の傾き (A/V)。
 773            - intercept (float): 接線のY切片 (A)。
 774            - xx (numpy.ndarray): 接線フィッティングに使用されたXデータ。
 775            - yy (numpy.ndarray): 接線フィッティングに使用されたYデータ。
 776            - idx (int): 接点のインデックス。
 777    """
 778    xu, yu = consolidate_duplicate_x(V, I)
 779    y_sm = smooth_polyfit(yu, window_points=5, poly_order=3)
 780    dydv = np.gradient(y_sm, xu)
 781
 782    pos_idx = np.where(xu > 0.0)[0]
 783    if len(pos_idx) == 0:
 784        idx_pos = int(np.argmax(dydv))
 785    else:
 786        pad = min(2, max(0, len(pos_idx) // 4))
 787        cand = pos_idx[pad:len(pos_idx)-pad] if len(pos_idx) - 2 * pad >= 1 else pos_idx
 788        idx_local = int(np.argmax(dydv[cand]))
 789        idx_pos = int(cand[idx_local])
 790
 791    a, b, xx, yy, i0, i1 = fit_local_line(xu, y_sm, idx_pos, npts=7)
 792    v_rep = float(xu[idx_pos])
 793    i_rep = float(a * v_rep + b)
 794    rs = float("inf") if abs(a) < 1e-30 else float(1.0 / a)
 795    return {"v_rep": v_rep, "i_rep": i_rep, "Is": i_rep, "Rs": rs,
 796            "slope": float(a), "intercept": float(b), "xx": xx, "yy": yy, "idx": idx_pos}
 797
 798
 799def estimate_rsh_tangent_point(V, I):
 800    """
 801    概要:
 802        I-V曲線から並列抵抗(Rsh)を推定するための接点と抵抗値を計算します。
 803    詳細説明:
 804        逆方向バイアス領域でdI/dVが最小となる点(絶対値)を特定し、
 805        その点での接線からRshを計算します。
 806    引数:
 807        :param V: 電圧データ (V)。
 808        :type V: numpy.ndarray
 809        :param I: 電流データ (A)。
 810        :type I: numpy.ndarray
 811    戻り値:
 812        :returns: 並列抵抗推定に関する情報を含む辞書。
 813        :rtype: dict
 814            - v_rep (float): 接点電圧 (V)。
 815            - i_rep (float): 接点電流 (A)。
 816            - Ish (float): 接点電流 (A) (互換性のため)。
 817            - Rsh (float): 推定された並列抵抗 (Ω)。
 818            - slope (float): 接線の傾きの絶対値 (A/V)。
 819            - intercept (float): 接線のY切片 (A)。
 820            - slope_fit_raw (float): フィットされた直線の元の傾き (A/V)。
 821            - intercept_fit_raw (float): フィットされた直線の元のY切片 (A)。
 822            - xx (numpy.ndarray): 接線フィッティングに使用されたXデータ。
 823            - yy (numpy.ndarray): 接線フィッティングに使用されたYデータ。
 824            - idx (int): 接点のインデックス。
 825    """
 826    xu, yu = consolidate_duplicate_x(V, I)
 827    y_sm = smooth_polyfit(yu, window_points=5, poly_order=3)
 828    dydv = np.gradient(y_sm, xu)
 829
 830    neg_mask = xu < 0.0
 831    if np.any(neg_mask):
 832        dneg = np.abs(dydv[neg_mask])
 833        idx_neg_local = int(np.argmin(dneg))
 834        idx_neg = np.where(neg_mask)[0][idx_neg_local]
 835    else:
 836        idx_neg = int(np.argmin(np.abs(dydv)))
 837
 838    a_fit, b_fit, xx, yy, i0, i1 = fit_local_line(xu, y_sm, idx_neg, npts=7)
 839    v_rep = float(xu[idx_neg])
 840    i_fit_ref = float(a_fit * v_rep + b_fit)
 841    a = abs(float(a_fit))
 842    b = float(i_fit_ref - a * v_rep)
 843    i_rep = float(a * v_rep + b)
 844    rsh = float("inf") if abs(a) < 1e-30 else float(1.0 / a)
 845    return {"v_rep": v_rep, "i_rep": i_rep, "Ish": i_rep, "Rsh": rsh,
 846            "slope": float(a), "intercept": float(b), "slope_fit_raw": float(a_fit),
 847            "intercept_fit_raw": float(b_fit), "xx": xx, "yy": yy, "idx": idx_neg}
 848
 849
 850def estimate_ndiode_representative_point(V, I, T=300.0, Ish=None):
 851    """
 852    概要:
 853        I-V曲線からダイオード因子(ndiode)を推定するための代表点と値を計算します。
 854    詳細説明:
 855        順方向バイアス領域でlog(abs(I))の二階微分が最小となる点を特定し、
 856        その点での一次微分からダイオード因子を計算します。
 857    引数:
 858        :param V: 電圧データ (V)。
 859        :type V: numpy.ndarray
 860        :param I: 電流データ (A)。
 861        :type I: numpy.ndarray
 862        :param T: 温度 (K)。
 863        :type T: float
 864        :param Ish: 並列抵抗に流れる電流の推定値。これを考慮してIを調整します。
 865        :type Ish: float or None
 866    戻り値:
 867        :returns: ダイオード因子推定に関する情報を含む辞書。計算できなかった場合はNone。
 868        :rtype: dict or None
 869            - v_rep (float): 代表点電圧 (V)。
 870            - ndiode (float): 推定されたダイオード因子。
 871            - slope_logI (float): 代表点でのlog(abs(I))の傾き (1/V)。
 872            - curvature_logI (float): 代表点でのlog(abs(I))の二階微分 (1/V^2)。
 873    """
 874    xu, yu = consolidate_duplicate_x(V, I)
 875    order = np.argsort(xu)
 876    xu = xu[order]
 877    yu = yu[order]
 878
 879    ish_abs = abs(Ish) if Ish is not None else 0.0
 880    mask = (xu > 0.0) & (np.abs(yu) > ish_abs)
 881    if np.count_nonzero(mask) < 5:
 882        return None
 883
 884    x = xu[mask]
 885    y = np.abs(yu[mask]) + EPS_I
 886
 887    logI = np.log(y)
 888    logI_sm = smooth_polyfit(logI, window_points=5, poly_order=3)
 889    d1 = np.gradient(logI_sm, x)
 890    d2 = np.gradient(d1, x)
 891
 892    good = np.isfinite(d1) & np.isfinite(d2) & (d1 > 0.0)
 893    if np.count_nonzero(good) < 3:
 894        return None
 895
 896    xg = x[good]
 897    d1g = d1[good]
 898    d2g = d2[good]
 899
 900    idx = int(np.argmin(np.abs(d2g)))
 901    v_rep = float(xg[idx])
 902    slope = float(d1g[idx])
 903    ndiode = float(E_CHARGE / (KB * T * slope)) if abs(slope) > 1e-30 else float("nan")
 904    return {"v_rep": v_rep, "ndiode": ndiode, "slope_logI": slope, "curvature_logI": float(d2g[idx])}
 905
 906
 907def estimate_initial_params(V, I_meas, T=300.0):
 908    """
 909    概要:
 910        I-Vデータから単一ダイオードモデルの初期パラメータ(I0, ndiode, IPV, Rs, Rsh)を推定します。
 911    詳細説明:
 912        estimate_rs_tangent_point、estimate_rsh_tangent_point、
 913        estimate_ndiode_representative_pointなどの補助関数を用いて、
 914        各パラメータの初期値を経験的に決定します。
 915    引数:
 916        :param V: 電圧データ (V)。
 917        :type V: numpy.ndarray
 918        :param I_meas: 測定電流データ (A)。
 919        :type I_meas: numpy.ndarray
 920        :param T: 温度 (K)。
 921        :type T: float
 922    戻り値:
 923        :returns: 推定された初期パラメータを含む辞書。
 924        :rtype: dict
 925            - I0 (float): 逆方向飽和電流 (A)。
 926            - ndiode (float): ダイオード因子。
 927            - IPV (float): 光電流 (A)。
 928            - Rs (float): 直列抵抗 (Ω)。
 929            - Rsh (float): 並列抵抗 (Ω)。
 930            - Ish (float): 並列抵抗に流れる電流 (A)。
 931            - Vsh (float): Rsh推定における代表点電圧 (V)。
 932            - Vnd (float): ndiode推定における代表点電圧 (V)。
 933    """
 934    xu, yu = consolidate_duplicate_x(V, I_meas)
 935    order = np.argsort(xu)
 936    xu = xu[order]
 937    yu = yu[order]
 938    y_sm = smooth_polyfit(yu, window_points=5, poly_order=3)
 939
 940    rs_info = estimate_rs_tangent_point(V, I_meas)
 941    Rs = abs(float(rs_info["Rs"]))
 942
 943    rsh_info = estimate_rsh_tangent_point(V, I_meas)
 944    Rsh = abs(float(rsh_info["Rsh"]))
 945    Ish = float(rsh_info["Ish"])
 946
 947    I0 = abs(Ish)
 948    IPV = -local_poly_value(xu, y_sm, 0.0, npts=7, order=3)
 949
 950    nd_info = estimate_ndiode_representative_point(V, I_meas, T=T, Ish=Ish)
 951    if nd_info is None or (not np.isfinite(nd_info["ndiode"])):
 952        print()
 953        print("########################################################################")
 954        print("  Warning!!!: Could not get valid ndiode estimation")
 955        print("     Use ndiode = 1000.0 instead but don't refer to this value")
 956        print("########################################################################")
 957        ndiode = 1000.0
 958        Vnd = float("nan")
 959    else:
 960        ndiode = float(nd_info["ndiode"])
 961        Vnd = float(nd_info["v_rep"])
 962    
 963    return {
 964        "I0": I0,
 965        "ndiode": ndiode,
 966        "IPV": IPV,
 967        "Rs": Rs,
 968        "Rsh": Rsh,
 969        "Ish": Ish,
 970        "Vsh": float(rsh_info["v_rep"]),
 971        "Vnd": Vnd,
 972    }
 973
 974
 975def analyze_optical(wl_R_nm, R_percent, wl_T_nm, T_percent, d_nm):
 976    """
 977    概要:
 978        反射率(R)と透過率(T)のスペクトルデータから吸収率(A)と吸収係数(α)を計算します。
 979    詳細説明:
 980        異なる波長スケールのRとTを共通の波長スケールに補間し、
 981        RとTのデータに基づいてAとαを計算します。
 982    引数:
 983        :param wl_R_nm: 反射率スペクトルの波長データ (nm)。
 984        :type wl_R_nm: numpy.ndarray
 985        :param R_percent: 反射率データ (%)。
 986        :type R_percent: numpy.ndarray
 987        :param wl_T_nm: 透過率スペクトルの波長データ (nm)。
 988        :type wl_T_nm: numpy.ndarray
 989        :param T_percent: 透過率データ (%)。
 990        :type T_percent: numpy.ndarray
 991        :param d_nm: PV層の膜厚 (nm)。
 992        :type d_nm: float
 993    戻り値:
 994        :returns: 計算された光学特性値を含む辞書。
 995        :rtype: dict
 996            - wl_nm (numpy.ndarray): 共通波長データ (nm)。
 997            - R (numpy.ndarray): 反射率 (0-1)。
 998            - Tr (numpy.ndarray): 透過率 (0-1)。
 999            - A (numpy.ndarray): 吸収率 (0-1)。
1000            - alpha_cm^-1 (numpy.ndarray): 吸収係数 (cm^-1)。
1001    """
1002    wl_common = np.unique(np.concatenate([wl_R_nm, wl_T_nm]))
1003    wl_common.sort()
1004
1005    R = interpolate_to_common_wavelength(wl_R_nm, R_percent, wl_common) / 100.0
1006    Tr = interpolate_to_common_wavelength(wl_T_nm, T_percent, wl_common) / 100.0
1007
1008    mask = np.isfinite(R) & np.isfinite(Tr)
1009    wl = wl_common[mask]
1010    R = np.clip(R[mask], 0.0, 1.0)
1011    Tr = np.clip(Tr[mask], 0.0, 1.0)
1012
1013    A = np.clip(1.0 - R - Tr, 0.0, 1.0)
1014
1015    d_cm = d_nm * 1e-7
1016    denom = np.clip(1.0 - R, 1e-12, None)
1017    alpha_cm = -np.log(np.clip(Tr / denom, 1e-12, None)) / max(d_cm, 1e-30)
1018
1019    return {"wl_nm": wl, "R": R, "Tr": Tr, "A": A, "alpha_cm^-1": alpha_cm}
1020
1021
1022def quantum_efficiencies(F0, A_abs, JPV_A_cm2, Jsc_A_cm2):
1023    """
1024    概要:
1025        量子効率(EQEとIQE)を計算します。
1026    詳細説明:
1027        入射フォトンフラックス、吸収率、光電流(JPV)、短絡電流密度(Jsc)に基づいて、
1028        生成効率と収集効率を評価します。
1029    引数:
1030        :param F0: 入射フォトンフラックス (cm^-2 s^-1)。Noneまたは0の場合はNaNを返します。
1031        :type F0: float or None
1032        :param A_abs: 特定のエネルギーにおける吸収率。NoneまたはNaNの場合はIQEをNaNとします。
1033        :type A_abs: float or None
1034        :param JPV_A_cm2: 光電流密度 (A/cm^2)。
1035        :type JPV_A_cm2: float
1036        :param Jsc_A_cm2: 短絡電流密度 (A/cm^2)。
1037        :type Jsc_A_cm2: float
1038    戻り値:
1039        :returns: 計算された量子効率を含む辞書。
1040        :rtype: dict
1041            - EQE_gen (float): 生成外部量子効率。
1042            - IQE_gen (float): 生成内部量子効率。
1043            - EQE (float): 外部量子効率。
1044            - IQE (float): 内部量子効率。
1045    """
1046    out = {"EQE_gen": float("nan"), "IQE_gen": float("nan"), "EQE": float("nan"), "IQE": float("nan")}
1047    if F0 is None or F0 <= 0:
1048        return out
1049
1050    denom_ext = E_CHARGE * F0
1051    out["EQE_gen"] = abs(JPV_A_cm2) / denom_ext
1052    out["EQE"] = abs(Jsc_A_cm2) / denom_ext
1053
1054    if A_abs is not None and np.isfinite(A_abs) and A_abs > 0:
1055        denom_int = E_CHARGE * F0 * A_abs
1056        out["IQE_gen"] = abs(JPV_A_cm2) / denom_int
1057        out["IQE"] = abs(Jsc_A_cm2) / denom_int
1058    return out
1059
1060
1061def alpha_at_energy(optical, E_eV):
1062    """
1063    概要:
1064        指定されたフォトンエネルギーにおける吸収係数(α)を光学スペクトルデータから補間して取得します。
1065    引数:
1066        :param optical: analyze_opticalまたはread_alpha_from_excelから返された光学データ辞書。
1067        :type optical: dict or None
1068        :param E_eV: 取得したいフォトンエネルギー (eV)。
1069        :type E_eV: float
1070    戻り値:
1071        :returns: 指定されたエネルギーにおける吸収係数 (cm^-1)。データがない場合はNaN。
1072        :rtype: float
1073    例外:
1074        :raises ValueError: 補間された吸収係数がNaNの場合。
1075    """
1076    if optical is None:
1077        return float("nan")
1078
1079    wl = np.asarray(optical["wl_nm"], dtype=float)
1080    alpha = np.asarray(optical["alpha_cm^-1"], dtype=float)
1081    energy = 1239.841984 / wl
1082    order = np.argsort(energy)
1083    energy = energy[order]
1084    alpha = alpha[order]
1085    
1086    alpha_interp = np.interp(E_eV, energy, alpha, left=np.nan, right=np.nan)
1087    if not np.isfinite(alpha_interp):
1088        raise ValueError(f"got nan for alpha_at_energy()")
1089
1090    return float(alpha_interp)
1091
1092def absorptance_from_alpha(alpha_cm, d_nm):
1093    """
1094    概要:
1095        吸収係数と膜厚から吸収率を計算します。
1096    詳細説明:
1097        A = 1 - exp(-alpha * d) の式を用いて計算します。
1098    引数:
1099        :param alpha_cm: 吸収係数 (cm^-1)。
1100        :type alpha_cm: float or None
1101        :param d_nm: PV層の膜厚 (nm)。
1102        :type d_nm: float
1103    戻り値:
1104        :returns: 計算された吸収率。
1105        :rtype: float
1106    例外:
1107        :raises ValueError: alpha_cmがNoneまたは無効な値の場合。
1108    """
1109    if alpha_cm is None or not np.isfinite(alpha_cm):
1110        raise ValueError(f"got None or invalid value for alpha_cm")
1111    d_cm = d_nm * 1e-7
1112    return float(1.0 - np.exp(-alpha_cm * d_cm))
1113
1114
1115def save_alpha_to_excel(optical, outfile_xlsx, meta=None):
1116    """
1117    概要:
1118        吸収スペクトルデータをExcelファイルに保存します。
1119    詳細説明:
1120        alpha_spectrumというシートにフォトンエネルギー、波長、R、Tr、A、alphaのデータを書き込みます。
1121        summaryシートには膜厚やファイル名、平均値などのメタデータを保存します。
1122    引数:
1123        :param optical: analyze_optical関数から返された光学データ辞書。
1124        :type optical: dict
1125        :param outfile_xlsx: 保存するExcelファイルのパス。
1126        :type outfile_xlsx: str
1127        :param meta: 保存する追加のメタデータ。
1128        :type meta: dict or None
1129    戻り値:
1130        なし
1131    """
1132    print(f"[I/O] Save alpha Excel: {outfile_xlsx}")
1133    wb = Workbook()
1134    ws = wb.active
1135    ws.title = "alpha_spectrum"
1136
1137    header_fill = PatternFill("solid", fgColor="1F4E78")
1138    header_font = Font(color="FFFFFF", bold=True)
1139
1140    headers = ["photon_energy_eV", "wavelength_nm", "R", "Tr", "A", "alpha_cm^-1"]
1141    ws.append(headers)
1142    for c in ws[1]:
1143        c.fill = header_fill
1144        c.font = header_font
1145
1146    hc_eVnm = 1239.841984
1147    energy_eV = hc_eVnm / optical["wl_nm"]
1148    order = np.argsort(energy_eV)
1149
1150    for i in order:
1151        ws.append([
1152            float(energy_eV[i]),
1153            float(optical["wl_nm"][i]),
1154            float(optical["R"][i]),
1155            float(optical["Tr"][i]),
1156            float(optical["A"][i]),
1157            float(optical["alpha_cm^-1"][i]),
1158        ])
1159
1160    for col in ["A", "B", "C", "D", "E", "F"]:
1161        ws.column_dimensions[col].width = 18
1162
1163    ws2 = wb.create_sheet("summary")
1164    ws2["A1"] = "d_nm"
1165    ws2["B1"] = meta.get("d_nm", "") if meta else ""
1166    ws2["A2"] = "R_file"
1167    ws2["B2"] = meta.get("R", "") if meta else ""
1168    ws2["A3"] = "Tr_file"
1169    ws2["B3"] = meta.get("Tr", "") if meta else ""
1170    ws2["A5"] = "R_mean"
1171    ws2["B5"] = float(np.mean(optical["R"]))
1172    ws2["A6"] = "Tr_mean"
1173    ws2["B6"] = float(np.mean(optical["Tr"]))
1174    ws2["A7"] = "A_mean"
1175    ws2["B7"] = float(np.mean(optical["A"]))
1176    ws2["A8"] = "alpha_mean_cm^-1"
1177    ws2["B8"] = float(np.mean(optical["alpha_cm^-1"]))
1178
1179    for col in ["A", "B"]:
1180        ws2.column_dimensions[col].width = 24
1181
1182    wb.save(outfile_xlsx)
1183    print(f"[I/O] Saved alpha Excel: {outfile_xlsx}")
1184
1185
1186def plot_alpha(optical, outfile=None, pause=False):
1187    """
1188    概要:
1189        吸収係数スペクトル(α vs フォトンエネルギー)をプロットします。
1190    引数:
1191        :param optical: analyze_optical関数から返された光学データ辞書。
1192        :type optical: dict
1193        :param outfile: プロットを保存するファイルパス。Noneの場合、画面に表示します。
1194        :type outfile: str or None
1195        :param pause: プロットを閉じるまで一時停止するかどうか。Trueの場合、plt.show()を呼び出します。
1196        :type pause: bool
1197    戻り値:
1198        :returns: 作成されたMatplotlib Figureオブジェクト。
1199        :rtype: matplotlib.figure.Figure
1200    """
1201    fig, ax = plt.subplots(1, 1, figsize=(6.2, 4.8))
1202    hc_eVnm = 1239.841984
1203    energy_eV = hc_eVnm / optical["wl_nm"]
1204
1205    order = np.argsort(energy_eV)
1206    x = energy_eV[order]
1207    y = optical["alpha_cm^-1"][order]
1208
1209    ax.plot(x, y, "-", linewidth=1.5)
1210    ax.tick_params(axis='x', labelsize=fontsize)
1211    ax.tick_params(axis='y', labelsize=fontsize)
1212    ax.set_xlabel("Photon energy / eV", fontsize=fontsize)
1213    ax.set_ylabel(r"$\alpha$ / cm$^{-1}$", fontsize=fontsize)
1214    ax.set_title(r"Absorption coefficient $\alpha(E)$", fontsize=fontsize)
1215    ax.grid(True)
1216
1217    fig.tight_layout()
1218    if outfile:
1219        print(f"[I/O] Save plot: {outfile}")
1220        fig.savefig(outfile, dpi=160)
1221
1222    if pause:
1223        plt.show()
1224    else:
1225        plt.pause(0.01)
1226    return fig
1227
1228
1229def estimate_ndiode_curve(V, I, T=300.0):
1230    """
1231    概要:
1232        I-Vデータから電圧に対するダイオード因子(ndiode)の曲線を推定します。
1233    詳細説明:
1234        log(abs(I))の一次微分を計算し、それに基づいて各電圧点におけるダイオード因子を導出します。
1235    引数:
1236        :param V: 電圧データ (V)。
1237        :type V: numpy.ndarray
1238        :param I: 電流データ (A)。
1239        :type I: numpy.ndarray
1240        :param T: 温度 (K)。
1241        :type T: float
1242    戻り値:
1243        :returns: ダイオード因子が計算された電圧データと、各電圧に対応する推定されたダイオード因子。
1244        :rtype: tuple[numpy.ndarray, numpy.ndarray]
1245    """
1246    V = np.asarray(V, dtype=float)
1247    I = np.asarray(I, dtype=float)
1248    xu, yu = consolidate_duplicate_x(V, I)
1249    order = np.argsort(xu)
1250    xu = xu[order]
1251    yu = yu[order]
1252
1253    mask = (xu > 0.0) & (yu > 0.0)
1254    if np.count_nonzero(mask) < 5:
1255        return np.array([]), np.array([])
1256
1257    x = xu[mask]
1258    y = yu[mask]
1259    logI = np.log(np.abs(y) + EPS_I)
1260    logI_sm = smooth_polyfit(logI, window_points=5, poly_order=3)
1261    dlogIdV = np.gradient(logI_sm, x)
1262
1263    with np.errstate(divide="ignore", invalid="ignore"):
1264        ndiode = E_CHARGE / (KB * T * dlogIdV)
1265    good = np.isfinite(ndiode) & (ndiode > 0)
1266    return x[good], ndiode[good]
1267
1268
1269def plot_iv_comparison(dark_sweeps, light_sweeps, S, light_metrics,
1270                       dark_params=None, light_params=None, outfile=None, T=300.0, pause=False):
1271    """
1272    概要:
1273        暗電流I-Vおよび光照射I-Vデータと、関連する推定値や解析結果を比較プロットします。
1274    詳細説明:
1275        以下の4つのサブプロットを作成します。
1276        - log_10(abs(I))-V曲線(暗電流と光電流)。
1277        - 線形J-V曲線(順方向と逆方向)。RsとRshの接線、Ish(0)点を表示。
1278        - ndiode-V曲線と代表点。
1279        - 光起電力出力(-J vs V)。MPP, Voc, Jsc点を表示。
1280    引数:
1281        :param dark_sweeps: 暗電流測定のI-Vスイープデータのリスト。
1282        :type dark_sweeps: list[tuple[numpy.ndarray, numpy.ndarray]]
1283        :param light_sweeps: 光照射測定のI-Vスイープデータのリスト。
1284        :type light_sweeps: list[tuple[numpy.ndarray, numpy.ndarray]]
1285        :param S: デバイスの面積 (cm^2)。
1286        :type S: float
1287        :param light_metrics: 光照射下のI-Vデータから計算された光起電力性能指標。
1288        :type light_metrics: dict
1289        :param dark_params: 暗電流I-Vデータから推定された初期パラメータ。Rs/Rshの接線表示に使用。
1290        :type dark_params: dict or None
1291        :param light_params: 光照射I-Vデータから推定された初期パラメータ。I0/IPVの表示に使用。
1292        :type light_params: dict or None
1293        :param outfile: プロットを保存するファイルパス。Noneの場合、画面に表示します。
1294        :type outfile: str or None
1295        :param T: 温度 (K)。ndiode曲線の計算に使用。
1296        :type T: float
1297        :param pause: プロットを閉じるまで一時停止するかどうか。Trueの場合、plt.show()を呼び出します。
1298        :type pause: bool
1299    戻り値:
1300        :returns: 作成されたMatplotlib Figureオブジェクト。
1301        :rtype: matplotlib.figure.Figure
1302    """
1303    fig = plt.figure(figsize=(13, 8))
1304    gs = fig.add_gridspec(2, 2, height_ratios=[3, 1.5])
1305
1306    ax_log = fig.add_subplot(gs[0, 0])
1307    ax_lin_f = fig.add_subplot(gs[0, 1])
1308    ax_nd = fig.add_subplot(gs[1, 0], sharex=ax_log)
1309    ax_pv = fig.add_subplot(gs[1, 1])
1310
1311    ax_lin_r = ax_lin_f.twinx()
1312
1313    def style_ax(ax, xlabel=None, ylabel=None, title=None):
1314        ax.tick_params(axis='x', labelsize=fontsize)
1315        ax.tick_params(axis='y', labelsize=fontsize)
1316        if xlabel is not None:
1317            ax.set_xlabel(xlabel, fontsize=fontsize)
1318        if ylabel is not None:
1319            ax.set_ylabel(ylabel, fontsize=fontsize)
1320        if title is not None:
1321            ax.set_title(title, fontsize=fontsize)
1322
1323    def plot_split_linear(ax_f, ax_r, sweeps, label_prefix, color=None):
1324        first_f = True
1325        first_r = True
1326        for V, I in sweeps:
1327            V = np.asarray(V, dtype=float)
1328            I = np.asarray(I, dtype=float)
1329
1330            mask_f = V >= 0.0
1331            mask_r = V <= 0.0
1332
1333            if np.any(mask_f):
1334                Jf = I[mask_f] / S * 1e3
1335                ax_f.plot(np.abs(V[mask_f]), Jf, "-", linewidth=1.2, color=color,
1336                          label=label_prefix if first_f else None)
1337                first_f = False
1338
1339            if np.any(mask_r):
1340                Jr = I[mask_r] / S * 1e3
1341                ax_r.plot(np.abs(V[mask_r]), Jr, "--", linewidth=1.2, color=color,
1342                          label=f"{label_prefix} (rev)" if first_r else None)
1343                first_r = False
1344
1345    plot_split_linear(ax_lin_f, ax_lin_r, dark_sweeps, "dark")
1346    plot_split_linear(ax_lin_f, ax_lin_r, light_sweeps, "light")
1347
1348    if dark_params is not None and len(dark_sweeps) > 0:
1349        Vd, Id = dark_sweeps[0]
1350        rs_info = estimate_rs_tangent_point(Vd, Id)
1351        a_rs = rs_info["slope"] / S * 1e3
1352        b_rs = rs_info["intercept"] / S * 1e3
1353        v_rep_rs = rs_info["v_rep"]
1354        i_rep_rs = rs_info["i_rep"] / S * 1e3
1355
1356        if abs(a_rs) > 1e-30:
1357            v_zero = float(-b_rs / a_rs)
1358            x0 = max(0.0, min(abs(v_rep_rs), abs(v_zero)))
1359            x1 = max(abs(v_rep_rs), abs(v_zero))
1360            x_tan_rs = np.array([x0, x1])
1361            y_tan_rs = a_rs * x_tan_rs + b_rs
1362            ax_lin_f.plot(x_tan_rs, y_tan_rs, "-", linewidth=0.9, label="Rs tangent")
1363            ax_lin_f.plot(abs(v_rep_rs), i_rep_rs, "o", markersize=5, label="Rs point")
1364
1365    if dark_params is not None and len(dark_sweeps) > 0:
1366        Vd, Id = dark_sweeps[0]
1367        tang = estimate_rsh_tangent_point(Vd, Id)
1368        a = tang["slope"] / S * 1e3
1369        b = tang["intercept"] / S * 1e3
1370        v_rep = tang["v_rep"]
1371        i_rep = tang["i_rep"] / S * 1e3
1372
1373        x_rev_all = []
1374        for VV, II in dark_sweeps:
1375            VV = np.asarray(VV, dtype=float)
1376            mask_r_all = VV <= 0.0
1377            if np.any(mask_r_all):
1378                x_rev_all.extend(list(np.abs(VV[mask_r_all])))
1379        if len(x_rev_all) == 0:
1380            x_tan = np.array([0.0, abs(v_rep)])
1381        else:
1382            x_tan = np.array([0.0, float(np.max(x_rev_all))])
1383
1384        y_tan = (-a) * x_tan + b
1385        ax_lin_r.plot(x_tan, y_tan, "-", linewidth=0.9, label="Rsh tangent", zorder=5)
1386        ax_lin_r.plot(abs(v_rep), i_rep, "o", markersize=5, label="Rsh point", zorder=6)
1387        ish0 = float(b)
1388        ax_lin_r.plot(0.0, ish0, "s", markersize=5, label="Ish(0)", zorder=6)
1389
1390    style_ax(ax_lin_f, xlabel="|V| / V", ylabel="J forward / mA cm$^{-2}$, V >= 0",
1391             title="Linear J-V (forward / reverse separated)")
1392    ax_lin_r.tick_params(axis='y', labelsize=fontsize)
1393    ax_lin_r.set_ylabel("J reverse / mA cm$^{-2}$, V <= 0", fontsize=fontsize)
1394    ax_lin_f.grid(True)
1395
1396    h1, l1 = ax_lin_f.get_legend_handles_labels()
1397    h2, l2 = ax_lin_r.get_legend_handles_labels()
1398    ax_lin_f.legend(h1 + h2, l1 + l2, fontsize=8, loc="best")
1399
1400    for i, (V, I) in enumerate(dark_sweeps):
1401        ax_log.plot(V, np.log10(np.abs(I) + EPS_I), "-", linewidth=1.2, label="dark" if i == 0 else None)
1402    for i, (V, I) in enumerate(light_sweeps):
1403        ax_log.plot(V, np.log10(np.abs(I) + EPS_I), "-", linewidth=1.2, label="light" if i == 0 else None)
1404    if light_params is not None:
1405        ax_log.axhline(np.log10(abs(light_params["I0"]) + EPS_I), linestyle=":", linewidth=1.0, label="-I0")
1406        ax_log.axhline(np.log10(abs(light_params["I0"] + light_params["IPV"]) + EPS_I),
1407                       linestyle=":", linewidth=1.0, label="-I0-IPV")
1408    style_ax(ax_log, xlabel="V / V", ylabel=r"log$_{10}$(|I|)", title="Dark / Illuminated log10(|I|)-V")
1409    ax_log.grid(True)
1410    ax_log.legend(fontsize=8)
1411
1412    Vdark0, Idark0 = dark_sweeps[0]
1413    Vn, nn = estimate_ndiode_curve(Vdark0, Idark0, T=T)
1414    if len(Vn) > 0:
1415        ax_nd.plot(Vn, nn, "-", linewidth=1.3, label="ndiode(V)")
1416
1417    ish_for_nd = dark_params.get("Ish", None) if dark_params is not None else None
1418    nd_rep = estimate_ndiode_representative_point(Vdark0, Idark0, T=T, Ish=ish_for_nd)
1419    if nd_rep is not None and np.isfinite(nd_rep["ndiode"]):
1420        ax_nd.plot(nd_rep["v_rep"], nd_rep["ndiode"], "o", markersize=6, label="ndiode point")
1421
1422    style_ax(ax_nd, xlabel="V / V", ylabel="ndiode", title="Estimated ndiode vs V")
1423    ax_nd.grid(True)
1424    ax_nd.legend(fontsize=8)
1425
1426    V0, I0 = light_sweeps[0]
1427    J0_mA = I0 / S * 1e3
1428    ypv = -J0_mA
1429    ax_pv.plot(V0, ypv, "-", linewidth=1.5, label="-J (light)")
1430    ax_pv.plot(light_metrics["Vop_V"], -light_metrics["Jop_mA_cm2"], "o", markersize=7, label="MPP")
1431    ax_pv.plot(light_metrics["Voc_V"], 0.0, "s", markersize=7, label="Voc")
1432    ax_pv.plot(0.0, -light_metrics["Jsc_mA_cm2"], "^", markersize=7, label="Jsc")
1433
1434    style_ax(ax_pv, xlabel="V / V", ylabel="-J / mA/cm$^2$", title="Photovoltaic output")
1435    ymax = max(float(np.max(ypv)), 0.0, float(-light_metrics["Jsc_mA_cm2"]))
1436    ax_pv.set_ylim(0.0, ymax if ymax > 0 else 1.0)
1437    ax_pv.grid(True)
1438    ax_pv.legend(fontsize=8)
1439
1440    fig.tight_layout()
1441    if outfile:
1442        print(f"[I/O] Save plot: {outfile}")
1443        fig.savefig(outfile, dpi=160)
1444
1445    if pause:
1446        plt.show()
1447    else:
1448        plt.pause(0.01)
1449    return fig
1450
1451
1452def exec_alpha(args):
1453    """
1454    概要:
1455        alphaまたはmake_alphaモードでプログラムを実行するメイン関数。
1456    詳細説明:
1457        光学スペクトルファイル(RとTr)を読み込み、吸収率と吸収係数を計算し、
1458        結果をプロットしてExcelファイルに保存します。
1459    引数:
1460        :param args: コマンドライン引数。
1461        :type args: argparse.Namespace
1462    戻り値:
1463        なし
1464    """
1465
1466#    E_use = compute_photon_energy_eV(args.E, args.lambda_nm)
1467#    P0_use = compute_p0(args.F0, args.P0, args.E, args.lambda_nm)
1468#    print_args_and_derived(args, E_use, P0_use)
1469    print_args_and_derived(args)
1470    
1471    wl_R, R_percent, info_R = read_optical_spectrum(args.R)
1472    wl_T, T_percent, info_T = read_optical_spectrum(args.Tr)
1473
1474    optical = analyze_optical(wl_R, R_percent, wl_T, T_percent, args.d)
1475
1476    print()
1477    print("=== make_alpha analysis ===")
1478    print(f"d_nm          : {args.d:.16g} nm")
1479    print(f"R_mean        : {np.mean(optical['R']):.16g}")
1480    print(f"Tr_mean       : {np.mean(optical['Tr']):.16g}")
1481    print(f"A_mean        : {np.mean(optical['A']):.16g}")
1482    print(f"alpha_mean    : {np.mean(optical['alpha_cm^-1']):.16g} cm^-1")
1483
1484    print()
1485    outxlsx = f"{args.outprefix}_alpha.xlsx"
1486    if args.mode == 'make_alpha':
1487        save_alpha_to_excel(optical, outxlsx, meta={"d_nm": args.d, "R": args.R, "Tr": args.Tr})
1488        print(f"Saving Excel  : {outxlsx}")
1489
1490    outplot = f"{args.outprefix}_alpha_E.png"
1491    print(f"Saving plot   : {outplot}")
1492
1493    plot_alpha(optical, outfile=outplot, pause = False)
1494    input("\nPress ENTER to terminate\n")
1495
1496
1497def exec_analyze(args):
1498    """
1499    概要:
1500        analyzeモードでプログラムを実行するメイン関数。
1501    詳細説明:
1502        暗電流および光照射I-Vファイルを読み込み、初期パラメータと光起電力性能指標を推定し、
1503        量子効率を計算します。光学データが利用可能な場合はそれも考慮に入れます。
1504        結果を標準出力に表示し、比較プロットを作成します。
1505    引数:
1506        :param args: コマンドライン引数。
1507        :type args: argparse.Namespace
1508    戻り値:
1509        なし
1510    例外:
1511        :raises ValueError: alpha_at_energy 関数から NaN が返された場合。
1512    """
1513    print()
1514    print("[MODE] analyze")
1515    P0_use = compute_p0(args.F0, args.P0, args.E, args.lambda_nm)
1516    E_use = compute_photon_energy_eV(args.E, args.lambda_nm)
1517    print_args_and_derived(args, E_use, P0_use)
1518
1519    dark_xs, dark_ys, dark_info = read_data(args.dark)
1520    light_xs, light_ys, light_info = read_data(args.light)
1521
1522    dark_sweeps = list(zip(dark_xs, dark_ys))
1523    light_sweeps = list(zip(light_xs, light_ys))
1524
1525    Vd, Id = choose_sweep(dark_xs, dark_ys, args.sweep_dark)
1526    Vl, Il = choose_sweep(light_xs, light_ys, args.sweep_light)
1527
1528    dark_params = estimate_initial_params(Vd, Id, T=args.T)
1529    dark_params["IPV"] = 0.0
1530    light_params = estimate_initial_params(Vl, Il, T=args.T)
1531
1532    dark_metrics = pv_metrics_from_iv(Vd, Id, args.S)
1533    light_metrics = pv_metrics_from_iv(Vl, Il, args.S)
1534
1535    optical = None
1536    alpha_at_E = float("nan")
1537    A_at_E = float("nan")
1538    if args.alpha:
1539        optical = read_alpha_from_excel(args.alpha)
1540
1541    if optical is not None:
1542        alpha_at_E = alpha_at_energy(optical, E_use)
1543        if alpha_at_E is None:
1544            raise ValueError(f"Got nan for alpha_at_E()")
1545
1546        A_at_E = absorptance_from_alpha(alpha_at_E, args.d)
1547
1548    eta = float("nan")
1549    if P0_use is not None and P0_use > 0:
1550        eta = light_metrics["Pmax_W_cm2"] / P0_use
1551
1552    JPV_A_cm2 = light_params["IPV"] / args.S
1553    qe = quantum_efficiencies(args.F0, A_at_E, JPV_A_cm2, light_metrics["Jsc_A_cm2"])
1554
1555    print("=== Device constants ===")
1556    print(f"d            = {args.d:.6f} nm")
1557    print(f"S            = {args.S:.8f} cm^2")
1558    print(f"T            = {args.T:.6f} K")
1559    print()
1560
1561    print("=== Photon input ===")
1562    print(f"lambda       = {args.lambda_nm:.16g} nm")
1563    print(f"E            = {E_use:.16g} eV")
1564    print(f"F0           = {args.F0:.16g} cm^-2 s^-1" if args.F0 is not None else "F0           = None")
1565    print(f"P0           = {P0_use*1e3:.16g} mW/cm^2" if P0_use is not None else "P0           = None")
1566    print()
1567
1568    print("=== Dark IV estimated parameters ===")
1569    for k in PARAM_NAMES:
1570        print(f"{k:8s} = {dark_params[k]:.16g}")
1571    rsh_info = estimate_rsh_tangent_point(Vd, Id)
1572    rs_info = estimate_rs_tangent_point(Vd, Id)
1573    print(f"{'Vs':8s} = {rs_info['v_rep']:.16g}")
1574    print(f"{'Is':8s} = {rs_info['i_rep']:.16g}")
1575    print(f"{'Vsh':8s} = {rsh_info['v_rep']:.16g}")
1576    print(f"{'Ish':8s} = {rsh_info['Ish']:.16g}")
1577    print(f"{'Ish(0)':8s} = {rsh_info['intercept']:.16g}")
1578    print()
1579
1580    print("=== Illuminated IV estimated parameters ===")
1581    for k in PARAM_NAMES:
1582        print(f"{k:8s} = {light_params[k]:.16g}")
1583    print()
1584
1585    print("=== Photovoltaic metrics ===")
1586    print(f"Voc        = {light_metrics['Voc_V']:.16g} V")
1587    print(f"Jsc        = {light_metrics['Jsc_mA_cm2']:.16g} mA/cm^2")
1588    print(f"Vop        = {light_metrics['Vop_V']:.16g} V")
1589    print(f"Jop        = {light_metrics['Jop_mA_cm2']:.16g} mA/cm^2")
1590    print(f"FF         = {light_metrics['FF']:.16g}")
1591    print(f"Pmax       = {light_metrics['Pmax_mW_cm2']:.16g} mW/cm^2")
1592    print(f"Efficiency = {eta:.16g}" if np.isfinite(eta) else "Efficiency = nan")
1593    print()
1594
1595    if optical is not None:
1596        print("=== Optical metrics ===")
1597        print(f"alpha(E)          = {alpha_at_E:.16g} cm^-1")
1598        print(f"absorption length = {args.d} nm")
1599        print(f"A(E)              = {A_at_E:.16g}")
1600        print()
1601
1602    print("=== Quantum efficiencies ===")
1603    print(f"JPV         = {JPV_A_cm2*1e3:.16g} mA/cm^2")
1604    print(f"EQE_gen     = {qe['EQE_gen']:.16g}")
1605    print(f"IQE_gen     = {qe['IQE_gen']:.16g}")
1606    print(f"EQE         = {qe['EQE']:.16g}")
1607    print(f"IQE         = {qe['IQE']:.16g}")
1608
1609    print()
1610    outplot = f"{args.outprefix}_iv.png"
1611    plot_iv_comparison(
1612        dark_sweeps, light_sweeps, args.S, light_metrics,
1613        dark_params=dark_params, light_params=light_params,
1614        outfile=outplot, T=args.T,
1615        pause=False
1616    )
1617    print(f"Saved plot: {outplot}")
1618    input("\nPress ENTER to terminate\n")
1619
1620
1621_original_print = None
1622_redirect_fp = None
1623def dual_print(*args, **kwargs):
1624    """
1625    概要:
1626        標準出力とログファイルの両方にメッセージを出力します。
1627    詳細説明:
1628        通常のprint関数の動作を模倣し、標準出力に加えて、
1629        _redirect_fpに設定されたファイルオブジェクトにも出力します。
1630    引数:
1631        :param args: printに渡される位置引数。
1632        :type args: tuple
1633        :param kwargs: printに渡されるキーワード引数。
1634        :type kwargs: dict
1635    戻り値:
1636        なし
1637    """
1638    _original_print(*args, **kwargs)
1639
1640    file_kwargs = dict(kwargs)
1641    file_kwargs.pop("file", None)   # file指定があっても無視してログファイルへ
1642    file_kwargs.setdefault("flush", True)
1643
1644    _original_print(*args, file=_redirect_fp, **file_kwargs)
1645
1646def main():
1647    """
1648    概要:
1649        プログラムのエントリーポイント。
1650    詳細説明:
1651        コマンドライン引数を初期化し、指定されたモードに基づいて
1652        対応する実行関数を呼び出します。
1653        実行ログは自動的にファイルにリダイレクトされます。
1654    引数:
1655        なし
1656    戻り値:
1657        なし
1658    """
1659
1660    global _original_print, _redirect_fp
1661
1662    print()
1663    args, parser = initialize()
1664
1665    _original_print = builtins.print
1666    if "alpha" in args.mode:
1667        logfile = f"{args.outprefix}_alpha.txt"
1668    else:
1669        logfile = f"{args.outprefix}_{args.mode}.txt"
1670    print(f"Open log file [{logfile}]")
1671    _redirect_fp = open(logfile, "w", encoding="utf-8")
1672    builtins.print = dual_print
1673
1674    if args.mode == "alpha" or args.mode == "make_alpha":
1675        exec_alpha(args)
1676    elif args.mode == "analyze":
1677        exec_analyze(args)
1678    else:
1679        parser.error(f"Unsupported mode: {args.mode}")
1680
1681
1682if __name__ == "__main__":
1683    try:
1684        main()
1685    except Exception:
1686        traceback.print_exc()
1687        sys.exit(1)
1688    finally:
1689        if _redirect_fp: _redirect_fp.close()