pvanalyze のソースコード

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
:doc:`pvanalyze_usage`

PV特性評価ツール

mode=alpha
    反射率/透過率ファイルと膜厚から吸収スペクトルを計算し、
    プロットとExcel保存を行います。

mode=analyze
    I-Vデータからパラメータ推定、発電特性解析、量子効率計算、プロットを行います。
"""

import sys
import argparse
import builtins
from pathlib import Path
import traceback
import csv
import math

import matplotlib.pyplot as plt
import numpy as np
from openpyxl import Workbook, load_workbook
from openpyxl.styles import Font, PatternFill


KB = 1.380649e-23
E_CHARGE = 1.602176634e-19
H = 6.62607015e-34
C = 2.99792458e8

PARAM_NAMES = ["I0", "ndiode", "IPV", "Rs", "Rsh"]
EPS_I = 1.0e-15

DPV_NM = 46.837
AREA_MM2 = 0.5 * 0.5 * math.pi
AREA_CM2 = AREA_MM2 * 0.01  # 1 mm^2 = 0.01 cm^2
PHOTON_FLUX = 1.95804e18  # cm^-2 s^-1
PHOTON_NM = 1363.0        # nm
PIN = ""                  # W/cm^2, if None and F0 is not None then compute from hc/lambda

fontsize = 16

DARK_IV_FILE = "I_V SweepTest SMU1 [(1) ; 2026_03_24 14_15_24]-SY251213-1-LD0.0V.csv"
PV_IV_FILE = "I_V SweepTest SMU1 [(10) ; 2026_03_24 14_18_37]-SY251213-1-LD1.8V.csv"
R_FILE = "SY251024-1-Bi4O6S21-STO001-225oC-10mJ_R(UDS).txt"
T_FILE = "SY251024-1-Bi4O6S21-STO001-225oC-10mJ_T(UDS).txt"
ALPHA_FILE = "alpha_Bi2OS2.xlsx"


[ドキュメント] def initialize(): """ コマンドライン引数を解析し、プログラムの初期設定を行います。 詳細説明: `argparse`モジュールを使用して、実行モード、ファイルパス、温度、膜厚などの パラメータを定義し、ユーザー入力から値をパースします。 :returns: `args` (argparse.Namespace): 解析された引数オブジェクト。 `parser` (argparse.ArgumentParser): 引数パーサーオブジェクト。 """ parser = argparse.ArgumentParser(description="PV characterization tool with alpha/analyze modes.") parser.add_argument("--mode", default="analyze", choices=["alpha", "make_alpha", "analyze"], help="Execution mode") parser.add_argument("--dark", default=DARK_IV_FILE, help="Dark IV CSV file") parser.add_argument("--light", default=PV_IV_FILE, help="Illuminated IV CSV file") parser.add_argument("--R", default=R_FILE, help="Reflectance spectrum file") # NOTE: --T is already requested for temperature, so transmittance file uses --Tr. parser.add_argument("--Tr", default=T_FILE, help="Transmittance spectrum file") parser.add_argument("--eq", default=None, help="Equivalent spectral irradiance file") parser.add_argument("--alpha", default=ALPHA_FILE, help="Excel file for absorption spectrum / alpha") parser.add_argument("--T", type=float, default=300.0, help="Temperature (K)") parser.add_argument("--d", type=float, default=DPV_NM, help="PV layer thickness (nm)") parser.add_argument("--sweep_dark", type=int, default=0, help="Sweep index for dark analysis") parser.add_argument("--sweep_light", type=int, default=0, help="Sweep index for light analysis") parser.add_argument("--F0", type=float, default=PHOTON_FLUX, help="Incident photon flux (cm^-2 s^-1)") parser.add_argument("--P0", type=str, default=PIN, help="Incident photon energy density (W/cm^2)") parser.add_argument("--E", type=float, default=0.0, help="Photon energy (eV). If 0, use lambda.") parser.add_argument("--lambda_nm", type=float, default=PHOTON_NM, help="Photon wavelength (nm), used when E=0") parser.add_argument("--S", type=float, default=AREA_CM2, help="Electrode area (cm^2)") parser.add_argument("--outprefix", default="pvanalyze", help="Output file prefix") args = parser.parse_args() return args, parser
[ドキュメント] def read_data(infile, xmin=None, xmax=None, ndataskip=0): """ 指定されたCSVファイルからI-Vデータを読み込みます。 詳細説明: ファイル内のメタデータ(記録時間、データ名)を抽出し、電圧と電流のデータポイントをパースします。 複数のスイープを検出し、それぞれをリストに分割します。 :param infile: (str): 読み込むCSVファイルのパス。 :param xmin: (float, optional): X軸(電圧)の最小値。これより小さい値はスキップされます。デフォルトはNone。 :param xmax: (float, optional): X軸(電圧)の最大値。これより大きい値はスキップされます。デフォルトはNone。 :param ndataskip: (int, optional): データポイントをスキップする間隔。0の場合、スキップしません。デフォルトは0。 :returns: `xs_list` (list[numpy.ndarray]): 各スイープのX軸(電圧)データのリスト。 `ys_list` (list[numpy.ndarray]): 各スイープのY軸(電流)データのリスト。 `inf` (dict): ファイルから抽出されたメタデータ。 """ print(f"[I/O] Read IV data: {infile}") raw_x = [] raw_y = [] inf = {"FileName": infile, "RecordTime": "Unknown", "DataName": ("V", "I")} icount = 0 data_started = False with open(infile, "r", encoding="utf-8-sig", newline="") as f: reader = csv.reader(f) for row in reader: if not row: continue cols = [c.strip() for c in row] if len(cols) >= 3 and cols[0] == "MetaData" and "RecordTime" in cols[1]: inf["RecordTime"] = cols[2] if cols[0] == "DataName": if len(cols) >= 3: inf["DataName"] = (cols[1], cols[2]) data_started = True continue if data_started and cols[0] == "DataValue": if len(cols) < 3: continue icount += 1 if ndataskip > 0 and (icount - 1) % ndataskip != 0: continue try: x = float(cols[1]) y = float(cols[2]) except ValueError: continue if xmin is not None and x < xmin: continue if xmax is not None and x > xmax: continue raw_x.append(x) raw_y.append(y) if not raw_x: raise ValueError(f"No valid IV data found in {infile}") xs_list = [] ys_list = [] cur_x = [raw_x[0]] cur_y = [raw_y[0]] prev_sign = 0 for i in range(1, len(raw_x)): dx = raw_x[i] - raw_x[i - 1] sign = 0 if abs(dx) < 1e-15 else (1 if dx > 0 else -1) if prev_sign == 0: prev_sign = sign if sign != 0 and prev_sign != 0 and sign != prev_sign: xs_list.append(np.asarray(cur_x, dtype=float)) ys_list.append(np.asarray(cur_y, dtype=float)) cur_x = [raw_x[i - 1], raw_x[i]] cur_y = [raw_y[i - 1], raw_y[i]] prev_sign = sign else: cur_x.append(raw_x[i]) cur_y.append(raw_y[i]) if sign != 0: prev_sign = sign if cur_x: xs_list.append(np.asarray(cur_x, dtype=float)) ys_list.append(np.asarray(cur_y, dtype=float)) inf["Points"] = sum(len(x) for x in xs_list) inf["NSweeps"] = len(xs_list) return xs_list, ys_list, inf
[ドキュメント] def read_alpha_from_excel(infile): """ Excelファイルから吸収スペクトル(alpha)データを読み込みます。 詳細説明: `save_alpha_to_excel`関数によって保存された形式のExcelファイルを想定しています。 `alpha_spectrum`シートから波長、R、Tr、A、alphaのデータを抽出します。 :param infile: (str): 読み込むExcelファイルのパス。 :returns: `dict`: 波長、反射率、透過率、吸収率、吸収係数を含む辞書。 - `"wl_nm"` (numpy.ndarray): 波長 (nm)。 - `"R"` (numpy.ndarray): 反射率。 - `"Tr"` (numpy.ndarray): 透過率。 - `"A"` (numpy.ndarray): 吸収率。 - `"alpha_cm^-1"` (numpy.ndarray): 吸収係数 (cm^-1)。 """ print(f"[I/O] Read alpha Excel: {infile}") wb = load_workbook(infile, data_only=True) if "alpha_spectrum" not in wb.sheetnames: raise ValueError(f"Sheet 'alpha_spectrum' not found in {infile}") ws = wb["alpha_spectrum"] wl = [] R = [] Tr = [] A = [] alpha = [] first = True for row in ws.iter_rows(values_only=True): if first: first = False continue if row is None or len(row) < 6: continue try: # columns: photon_energy_eV, wavelength_nm, R, Tr, A, alpha_cm^-1 wl.append(float(row[1])) R.append(float(row[2])) Tr.append(float(row[3])) A.append(float(row[4])) alpha.append(float(row[5])) except (TypeError, ValueError): continue if not wl: raise ValueError(f"No numeric alpha data found in {infile}") return { "wl_nm": np.asarray(wl, dtype=float), "R": np.asarray(R, dtype=float), "Tr": np.asarray(Tr, dtype=float), "A": np.asarray(A, dtype=float), "alpha_cm^-1": np.asarray(alpha, dtype=float), }
[ドキュメント] def read_optical_spectrum(infile): """ 指定されたテキストファイルから光学スペクトルデータ(反射率Rまたは透過率T)を読み込みます。 詳細説明: ファイル内のタブ区切りまたはスペース区切りのデータをパースし、 波長とスペクトル値のペアを抽出します。 重複する波長を処理し、波長順にソートします。 :param infile: (str): 読み込む光学スペクトルファイルのパス。 :returns: `wl` (numpy.ndarray): 波長 (nm)。 `val` (numpy.ndarray): スペクトル値 (RまたはTのパーセンテージ)。 `info` (dict): ファイルから抽出されたメタデータ(ファイル名、ラベル、Y軸単位など)。 """ print(f"[I/O] Read optical spectrum: {infile}") info = {"FileName": infile, "Label": Path(infile).stem, "YUnit": "a.u."} lines = [] encodings = ["cp932", "utf-8-sig", "utf-8", "latin1"] last_err = None for enc in encodings: try: with open(infile, "r", encoding=enc, errors="replace") as f: lines = f.readlines() break except Exception as e: last_err = e if not lines: raise RuntimeError(f"Failed to read optical spectrum: {infile} ({last_err})") wl = [] val = [] for line in lines: s = line.strip() if not s: continue if "\t" in s: parts = [p.strip() for p in s.split("\t")] if len(parts) >= 2: if "サンプル" in parts[0] or "サンプル" in parts[0]: info["Label"] = parts[1] if "%T" in parts[1]: info["YUnit"] = "%T" elif "%R" in parts[1]: info["YUnit"] = "%R" tokens = s.replace(",", " ").replace("\t", " ").split() nums = [] for t in tokens: try: nums.append(float(t)) except ValueError: pass if len(nums) >= 2: wl.append(nums[0]) val.append(nums[1]) if not wl: raise ValueError(f"No numeric spectrum data found in {infile}") wl = np.asarray(wl, dtype=float) val = np.asarray(val, dtype=float) uu, idx = np.unique(wl, return_index=True) wl = wl[np.sort(idx)] val = val[np.sort(idx)] order = np.argsort(wl) wl = wl[order] val = val[order] return wl, val, info
[ドキュメント] def choose_sweep(xs_list, ys_list, sweep_index=0): """ 複数のスイープデータから指定されたインデックスの単一スイープを選択します。 :param xs_list: (list[numpy.ndarray]): 各スイープのX軸(電圧)データのリスト。 :param ys_list: (list[numpy.ndarray]): 各スイープのY軸(電流)データのリスト。 :param sweep_index: (int, optional): 選択するスイープのインデックス。デフォルトは0。 :returns: `x` (numpy.ndarray): 選択されたスイープのX軸データ。 `y` (numpy.ndarray): 選択されたスイープのY軸データ。 """ if len(xs_list) == 0: raise ValueError("No sweep data available.") idx = int(sweep_index) if idx < 0 or idx >= len(xs_list): raise IndexError(f"sweep_index={idx} is out of range (0..{len(xs_list)-1})") return np.asarray(xs_list[idx], dtype=float), np.asarray(ys_list[idx], dtype=float)
[ドキュメント] def consolidate_duplicate_x(x, y): """ X軸に重複する値がある場合、Y軸の対応する値を平均して重複を解消します。 :param x: (numpy.ndarray): X軸データ。 :param y: (numpy.ndarray): Y軸データ。 :returns: `ux` (numpy.ndarray): 重複が解消されたX軸データ。 `uy` (numpy.ndarray): 重複が解消され、平均化されたY軸データ。 """ x = np.asarray(x, dtype=float) y = np.asarray(y, dtype=float) order = np.argsort(x) xs = x[order] ys = y[order] ux = [] uy = [] i = 0 n = len(xs) while i < n: xv = xs[i] vals = [ys[i]] j = i + 1 while j < n and abs(xs[j] - xv) < 1e-15: vals.append(ys[j]) j += 1 ux.append(xv) uy.append(float(np.mean(vals))) i = j return np.asarray(ux), np.asarray(uy)
[ドキュメント] def smooth_polyfit(y, window_points=5, poly_order=3): """ 多項式フィッティングを用いたSavitzky-Golay風の平滑化をデータに適用します。 詳細説明: 各データポイントを中心に指定されたウィンドウ内のデータに対して多項式フィッティングを行い、 中心点の値を予測することで平滑化を行います。 :param y: (numpy.ndarray): 平滑化するY軸データ。 :param window_points: (int, optional): フィッティングに使用するウィンドウ内のデータポイント数。奇数である必要があります。デフォルトは5。 :param poly_order: (int, optional): 多項式フィッティングの次数。デフォルトは3。 :returns: `ys` (numpy.ndarray): 平滑化されたY軸データ。 """ y = np.asarray(y, dtype=float) n = len(y) if n < 3: return y.copy() if window_points < 3: window_points = 3 if window_points % 2 == 0: window_points += 1 if window_points > n: window_points = n if (n % 2 == 1) else n - 1 if poly_order >= window_points: poly_order = window_points - 1 poly_order = max(poly_order, 1) half = window_points // 2 ys = np.empty(n, dtype=float) for i in range(n): i0 = max(0, i - half) i1 = min(n, i + half + 1) while (i1 - i0) < window_points: if i0 > 0: i0 -= 1 elif i1 < n: i1 += 1 else: break idx = np.arange(i0, i1, dtype=float) yy = y[i0:i1] xloc = idx - i deg = min(poly_order, len(yy) - 1) coeff = np.polyfit(xloc, yy, deg) ys[i] = np.polyval(coeff, 0.0) return ys
[ドキュメント] def local_poly_value(x, y, x0, npts=7, order=3): """ 指定されたX座標の周囲のデータポイントを使用して、局所的な多項式フィッティングを行い、 `x0`におけるY値を推定します。 :param x: (numpy.ndarray): X軸データ。 :param y: (numpy.ndarray): Y軸データ。 :param x0: (float): Y値を推定するX座標。 :param npts: (int, optional): フィッティングに使用するデータポイント数。デフォルトは7。 :param order: (int, optional): 多項式フィッティングの次数。デフォルトは3。 :returns: `float`: `x0`における推定されたY値。 """ x = np.asarray(x, dtype=float) y = np.asarray(y, dtype=float) idx = np.argsort(np.abs(x - x0))[:max(2, npts)] xs = x[idx] ys = y[idx] sidx = np.argsort(xs) xs = xs[sidx] ys = ys[sidx] deg = min(order, len(xs) - 1) coeff = np.polyfit(xs, ys, deg) return float(np.polyval(coeff, x0))
[ドキュメント] def zero_crossing_x(x, y): """ Y値がゼロを横切るX座標を線形補間によって見つけます。 詳細説明: 2つの連続するデータポイントの間でY値の符号が変わる点、 またはY値が最もゼロに近い点のX座標を返します。 :param x: (numpy.ndarray): X軸データ。 :param y: (numpy.ndarray): Y軸データ。 :returns: `float`: Y値がゼロを横切るX座標。 """ x = np.asarray(x, dtype=float) y = np.asarray(y, dtype=float) order = np.argsort(x) x = x[order] y = y[order] for i in range(len(x) - 1): y1, y2 = y[i], y[i + 1] if y1 == 0: return float(x[i]) if y1 * y2 < 0: return float(x[i] + (0 - y1) * (x[i + 1] - x[i]) / (y2 - y1)) return float(x[np.argmin(np.abs(y))])
[ドキュメント] def interpolate_to_common_wavelength(wl_ref, y_ref, wl_target): """ 参照波長スケール上のデータをターゲット波長スケールに線形補間します。 :param wl_ref: (numpy.ndarray): 参照波長データ (nm)。 :param y_ref: (numpy.ndarray): 参照Y軸データ。 :param wl_target: (numpy.ndarray): 補間対象のターゲット波長データ (nm)。 :returns: `numpy.ndarray`: ターゲット波長スケールに補間されたY軸データ。 """ wl_ref = np.asarray(wl_ref, dtype=float) y_ref = np.asarray(y_ref, dtype=float) wl_target = np.asarray(wl_target, dtype=float) return np.interp(wl_target, wl_ref, y_ref, left=np.nan, right=np.nan)
[ドキュメント] def compute_photon_energy_eV(E_eV, lambda_nm): """ フォトンエネルギー(eV)を計算します。 詳細説明: フォトンエネルギーが直接与えられている場合はそれを使用し、 そうでない場合は波長から計算します。 :param E_eV: (float): フォトンエネルギー (eV)。0より大きい場合はこれを使用。 :param lambda_nm: (float): フォトン波長 (nm)。`E_eV`が0の場合にこれを使用。 :returns: `float`: 計算されたフォトンエネルギー (eV)。 """ if E_eV is not None and E_eV > 0: return float(E_eV) return float(1239.841984 / lambda_nm)
[ドキュメント] def compute_p0(F0, P0, E_eV, lambda_nm): """ 入射フォトンエネルギー密度(P0)を計算します。 詳細説明: 入射フォトンフラックス(F0)が与えられている場合はそれから計算し、 そうでない場合は直接指定されたP0を使用します。 :param F0: (float or None): 入射フォトンフラックス (cm^-2 s^-1)。Noneの場合はP0を使用。 :param P0: (float or None): 入射フォトンエネルギー密度 (W/cm^2)。Noneの場合はF0から計算。 :param E_eV: (float): フォトンエネルギー (eV)。F0からP0を計算する場合に使用。 :param lambda_nm: (float): フォトン波長 (nm)。F0からP0を計算する場合に使用。 :returns: `float or None`: 計算された入射フォトンエネルギー密度 (W/cm^2)。 """ if F0 is not None: E_use = compute_photon_energy_eV(E_eV, lambda_nm) return float(F0 * E_use * E_CHARGE) return None if P0 is None else float(P0)
[ドキュメント] def pv_metrics_from_iv(V, I, S): """ I-Vデータから太陽電池の主要な性能指標(Voc, Jsc, FF, Pmaxなど)を計算します。 :param V: (numpy.ndarray): 電圧データ (V)。 :param I: (numpy.ndarray): 電流データ (A)。 :param S: (float): 電極面積 (cm^2)。 :returns: `dict`: 以下の主要な太陽電池性能指標を含む辞書。 - `"Voc_V"` (float): 開放電圧 (V)。 - `"Jsc_A_cm2"` (float): 短絡電流密度 (A/cm^2)。 - `"Jsc_mA_cm2"` (float): 短絡電流密度 (mA/cm^2)。 - `"Vop_V"` (float): 最大出力動作電圧 (V)。 - `"Jop_A_cm2"` (float): 最大出力動作電流密度 (A/cm^2)。 - `"Jop_mA_cm2"` (float): 最大出力動作電流密度 (mA/cm^2)。 - `"Pmax_W_cm2"` (float): 最大出力電力密度 (W/cm^2)。 - `"Pmax_mW_cm2"` (float): 最大出力電力密度 (mW/cm^2)。 - `"FF"` (float): 曲線因子。 """ V = np.asarray(V, dtype=float) I = np.asarray(I, dtype=float) J = I / S Jsc = local_poly_value(V, J, 0.0, npts=7, order=3) Voc = zero_crossing_x(V, I) Pgen = -V * J idx = int(np.argmax(Pgen)) Vop = float(V[idx]) Jop = float(J[idx]) Pmax = float(Pgen[idx]) denom = abs(Voc * Jsc) FF = float(Pmax / denom) if denom > 1e-30 else float("nan") return { "Voc_V": float(Voc), "Jsc_A_cm2": float(Jsc), "Jsc_mA_cm2": float(Jsc * 1e3), "Vop_V": float(Vop), "Jop_A_cm2": float(Jop), "Jop_mA_cm2": float(Jop * 1e3), "Pmax_W_cm2": float(Pmax), "Pmax_mW_cm2": float(Pmax * 1e3), "FF": float(FF), }
[ドキュメント] def fit_local_line(x, y, center_idx, npts=7): """ 指定された中心インデックスの周囲のデータポイントを用いて局所的な線形フィッティングを行います。 :param x: (numpy.ndarray): X軸データ。 :param y: (numpy.ndarray): Y軸データ。 :param center_idx: (int): フィッティングの中心となるデータポイントのインデックス。 :param npts: (int, optional): フィッティングに使用するデータポイント数。デフォルトは7。 :returns: `a` (float): フィットされた直線の傾き。 `b` (float): フィットされた直線のY切片。 `xx` (numpy.ndarray): フィッティングに使用されたXデータ。 `yy` (numpy.ndarray): フィッティングに使用されたYデータ。 `i0` (int): フィッティングに使用されたデータの開始インデックス。 `i1` (int): フィッティングに使用されたデータの終了インデックス(排他的)。 """ x = np.asarray(x, dtype=float) y = np.asarray(y, dtype=float) n = len(x) npts = max(2, min(int(npts), n)) half = npts // 2 i0 = max(0, center_idx - half) i1 = min(n, i0 + npts) i0 = max(0, i1 - npts) xx = x[i0:i1] yy = y[i0:i1] a, b = np.polyfit(xx, yy, 1) return float(a), float(b), xx, yy, i0, i1
[ドキュメント] def estimate_rs_tangent_point(V, I): """ I-V曲線から直列抵抗(Rs)を推定するための接点と抵抗値を計算します。 詳細説明: 順方向バイアス領域でdI/dVが最大となる点を特定し、その点での接線からRsを計算します。 :param V: (numpy.ndarray): 電圧データ (V)。 :param I: (numpy.ndarray): 電流データ (A)。 :returns: `dict`: 直列抵抗推定に関する情報。 - `"v_rep"` (float): 接点電圧 (V)。 - `"i_rep"` (float): 接点電流 (A)。 - `"Is"` (float): 接点電流 (A) (互換性のため)。 - `"Rs"` (float): 推定された直列抵抗 (Ω)。 - `"slope"` (float): 接線の傾き (A/V)。 - `"intercept"` (float): 接線のY切片 (A)。 - `"xx"` (numpy.ndarray): 接線フィッティングに使用されたXデータ。 - `"yy"` (numpy.ndarray): 接線フィッティングに使用されたYデータ。 - `"idx"` (int): 接点のインデックス。 """ xu, yu = consolidate_duplicate_x(V, I) y_sm = smooth_polyfit(yu, window_points=5, poly_order=3) dydv = np.gradient(y_sm, xu) pos_idx = np.where(xu > 0.0)[0] if len(pos_idx) == 0: idx_pos = int(np.argmax(dydv)) else: pad = min(2, max(0, len(pos_idx) // 4)) cand = pos_idx[pad:len(pos_idx)-pad] if len(pos_idx) - 2 * pad >= 1 else pos_idx idx_local = int(np.argmax(dydv[cand])) idx_pos = int(cand[idx_local]) a, b, xx, yy, i0, i1 = fit_local_line(xu, y_sm, idx_pos, npts=7) v_rep = float(xu[idx_pos]) i_rep = float(a * v_rep + b) rs = float("inf") if abs(a) < 1e-30 else float(1.0 / a) return {"v_rep": v_rep, "i_rep": i_rep, "Is": i_rep, "Rs": rs, "slope": float(a), "intercept": float(b), "xx": xx, "yy": yy, "idx": idx_pos}
[ドキュメント] def estimate_rsh_tangent_point(V, I): """ I-V曲線から並列抵抗(Rsh)を推定するための接点と抵抗値を計算します。 詳細説明: 逆方向バイアス領域でdI/dVが最小となる点(絶対値)を特定し、 その点での接線からRshを計算します。 :param V: (numpy.ndarray): 電圧データ (V)。 :param I: (numpy.ndarray): 電流データ (A)。 :returns: `dict`: 並列抵抗推定に関する情報。 - `"v_rep"` (float): 接点電圧 (V)。 - `"i_rep"` (float): 接点電流 (A)。 - `"Ish"` (float): 接点電流 (A) (互換性のため)。 - `"Rsh"` (float): 推定された並列抵抗 (Ω)。 - `"slope"` (float): 接線の傾きの絶対値 (A/V)。 - `"intercept"` (float): 接線のY切片 (A)。 - `"slope_fit_raw"` (float): フィットされた直線の元の傾き (A/V)。 - `"intercept_fit_raw"` (float): フィットされた直線の元のY切片 (A)。 - `"xx"` (numpy.ndarray): 接線フィッティングに使用されたXデータ。 - `"yy"` (numpy.ndarray): 接線フィッティングに使用されたYデータ。 - `"idx"` (int): 接点のインデックス。 """ xu, yu = consolidate_duplicate_x(V, I) y_sm = smooth_polyfit(yu, window_points=5, poly_order=3) dydv = np.gradient(y_sm, xu) neg_mask = xu < 0.0 if np.any(neg_mask): dneg = np.abs(dydv[neg_mask]) idx_neg_local = int(np.argmin(dneg)) idx_neg = np.where(neg_mask)[0][idx_neg_local] else: idx_neg = int(np.argmin(np.abs(dydv))) a_fit, b_fit, xx, yy, i0, i1 = fit_local_line(xu, y_sm, idx_neg, npts=7) v_rep = float(xu[idx_neg]) i_fit_ref = float(a_fit * v_rep + b_fit) a = abs(float(a_fit)) b = float(i_fit_ref - a * v_rep) i_rep = float(a * v_rep + b) rsh = float("inf") if abs(a) < 1e-30 else float(1.0 / a) return {"v_rep": v_rep, "i_rep": i_rep, "Ish": i_rep, "Rsh": rsh, "slope": float(a), "intercept": float(b), "slope_fit_raw": float(a_fit), "intercept_fit_raw": float(b_fit), "xx": xx, "yy": yy, "idx": idx_neg}
[ドキュメント] def estimate_ndiode_representative_point(V, I, T=300.0, Ish=None): """ I-V曲線からダイオード因子(ndiode)を推定するための代表点と値を計算します。 詳細説明: 順方向バイアス領域でlog(I)の二階微分が最小となる点を特定し、 その点での一次微分からダイオード因子を計算します。 :param V: (numpy.ndarray): 電圧データ (V)。 :param I: (numpy.ndarray): 電流データ (A)。 :param T: (float, optional): 温度 (K)。デフォルトは300.0。 :param Ish: (float, optional): 並列抵抗に流れる電流の推定値。これを考慮してIを調整します。デフォルトはNone。 :returns: `dict or None`: ダイオード因子推定に関する情報。計算できなかった場合はNone。 - `"v_rep"` (float): 代表点電圧 (V)。 - `"ndiode"` (float): 推定されたダイオード因子。 - `"slope_logI"` (float): 代表点でのlog(I)の傾き (1/V)。 - `"curvature_logI"` (float): 代表点でのlog(I)の二階微分 (1/V^2)。 """ xu, yu = consolidate_duplicate_x(V, I) order = np.argsort(xu) xu = xu[order] yu = yu[order] ish_abs = abs(Ish) if Ish is not None else 0.0 mask = (xu > 0.0) & (np.abs(yu) > ish_abs) if np.count_nonzero(mask) < 5: return None x = xu[mask] y = np.abs(yu[mask]) + EPS_I logI = np.log(y) logI_sm = smooth_polyfit(logI, window_points=5, poly_order=3) d1 = np.gradient(logI_sm, x) d2 = np.gradient(d1, x) good = np.isfinite(d1) & np.isfinite(d2) & (d1 > 0.0) if np.count_nonzero(good) < 3: return None xg = x[good] d1g = d1[good] d2g = d2[good] idx = int(np.argmin(np.abs(d2g))) v_rep = float(xg[idx]) slope = float(d1g[idx]) ndiode = float(E_CHARGE / (KB * T * slope)) if abs(slope) > 1e-30 else float("nan") return {"v_rep": v_rep, "ndiode": ndiode, "slope_logI": slope, "curvature_logI": float(d2g[idx])}
[ドキュメント] def estimate_initial_params(V, I_meas, T=300.0): """ I-Vデータから単一ダイオードモデルの初期パラメータ(I0, ndiode, IPV, Rs, Rsh)を推定します。 詳細説明: `estimate_rs_tangent_point`, `estimate_rsh_tangent_point`, `estimate_ndiode_representative_point`などの補助関数を用いて、 各パラメータの初期値を経験的に決定します。 :param V: (numpy.ndarray): 電圧データ (V)。 :param I_meas: (numpy.ndarray): 測定電流データ (A)。 :param T: (float, optional): 温度 (K)。デフォルトは300.0。 :returns: `dict`: 推定された初期パラメータ。 - `"I0"` (float): 逆方向飽和電流 (A)。 - `"ndiode"` (float): ダイオード因子。 - `"IPV"` (float): 光電流 (A)。 - `"Rs"` (float): 直列抵抗 (Ω)。 - `"Rsh"` (float): 並列抵抗 (Ω)。 - `"Ish"` (float): 並列抵抗に流れる電流 (A)。 - `"Vsh"` (float): Rsh推定における代表点電圧 (V)。 - `"Vnd"` (float): ndiode推定における代表点電圧 (V)。 """ xu, yu = consolidate_duplicate_x(V, I_meas) y_sm = smooth_polyfit(yu, window_points=5, poly_order=3) rs_info = estimate_rs_tangent_point(V, I_meas) Rs = abs(float(rs_info["Rs"])) rsh_info = estimate_rsh_tangent_point(V, I_meas) Rsh = abs(float(rsh_info["Rsh"])) Ish = float(rsh_info["Ish"]) I0 = abs(Ish) IPV = -local_poly_value(xu, y_sm, 0.0, npts=7, order=3) nd_info = estimate_ndiode_representative_point(V, I_meas, T=T, Ish=Ish) if nd_info is None or (not np.isfinite(nd_info["ndiode"])): print() print("########################################################################") print(" Warning!!!: Could not get valid ndiode estimation") print(" Use ndiode = 1000.0 instead but don't refer to this value") print("########################################################################") ndiode = 1000.0 Vnd = float("nan") else: ndiode = float(nd_info["ndiode"]) Vnd = float(nd_info["v_rep"]) return { "I0": I0, "ndiode": ndiode, "IPV": IPV, "Rs": Rs, "Rsh": Rsh, "Ish": Ish, "Vsh": float(rsh_info["v_rep"]), "Vnd": Vnd, }
[ドキュメント] def analyze_optical(wl_R_nm, R_percent, wl_T_nm, T_percent, d_nm): """ 反射率(R)と透過率(T)のスペクトルデータから吸収率(A)と吸収係数(α)を計算します。 詳細説明: 異なる波長スケールのRとTを共通の波長スケールに補間し、 RとTのデータに基づいてAとαを計算します。 :param wl_R_nm: (numpy.ndarray): 反射率スペクトルの波長データ (nm)。 :param R_percent: (numpy.ndarray): 反射率データ (%)。 :param wl_T_nm: (numpy.ndarray): 透過率スペクトルの波長データ (nm)。 :param T_percent: (numpy.ndarray): 透過率データ (%)。 :param d_nm: (float): PV層の膜厚 (nm)。 :returns: `dict`: 計算された光学特性値。 - `"wl_nm"` (numpy.ndarray): 共通波長データ (nm)。 - `"R"` (numpy.ndarray): 反射率 (0-1)。 - `"Tr"` (numpy.ndarray): 透過率 (0-1)。 - `"A"` (numpy.ndarray): 吸収率 (0-1)。 - `"alpha_cm^-1"` (numpy.ndarray): 吸収係数 (cm^-1)。 """ wl_common = np.unique(np.concatenate([wl_R_nm, wl_T_nm])) wl_common.sort() R = interpolate_to_common_wavelength(wl_R_nm, R_percent, wl_common) / 100.0 Tr = interpolate_to_common_wavelength(wl_T_nm, T_percent, wl_common) / 100.0 mask = np.isfinite(R) & np.isfinite(Tr) wl = wl_common[mask] R = np.clip(R[mask], 0.0, 1.0) Tr = np.clip(Tr[mask], 0.0, 1.0) A = np.clip(1.0 - R - Tr, 0.0, 1.0) d_cm = d_nm * 1e-7 denom = np.clip(1.0 - R, 1e-12, None) alpha_cm = -np.log(np.clip(Tr / denom, 1e-12, None)) / max(d_cm, 1e-30) return {"wl_nm": wl, "R": R, "Tr": Tr, "A": A, "alpha_cm^-1": alpha_cm}
[ドキュメント] def quantum_efficiencies(F0, A_abs, JPV_A_cm2, Jsc_A_cm2): """ 量子効率(EQEとIQE)を計算します。 詳細説明: 入射フォトンフラックス、吸収率、光電流(JPV)、短絡電流密度(Jsc)に基づいて、 生成効率と収集効率を評価します。 :param F0: (float or None): 入射フォトンフラックス (cm^-2 s^-1)。Noneまたは0の場合はNaNを返します。 :param A_abs: (float or None): 特定のエネルギーにおける吸収率。NoneまたはNaNの場合はIQEをNaNとします。 :param JPV_A_cm2: (float): 光電流密度 (A/cm^2)。 :param Jsc_A_cm2: (float): 短絡電流密度 (A/cm^2)。 :returns: `dict`: 計算された量子効率。 - `"EQE_gen"` (float): 生成外部量子効率。 - `"IQE_gen"` (float): 生成内部量子効率。 - `"EQE"` (float): 外部量子効率。 - `"IQE"` (float): 内部量子効率。 """ out = {"EQE_gen": float("nan"), "IQE_gen": float("nan"), "EQE": float("nan"), "IQE": float("nan")} if F0 is None or F0 <= 0: return out denom_ext = E_CHARGE * F0 out["EQE_gen"] = abs(JPV_A_cm2) / denom_ext out["EQE"] = abs(Jsc_A_cm2) / denom_ext if A_abs is not None and np.isfinite(A_abs) and A_abs > 0: denom_int = E_CHARGE * F0 * A_abs out["IQE_gen"] = abs(JPV_A_cm2) / denom_int out["IQE"] = abs(Jsc_A_cm2) / denom_int return out
[ドキュメント] def alpha_at_energy(optical, E_eV): """ 指定されたフォトンエネルギーにおける吸収係数(α)を光学スペクトルデータから補間して取得します。 :param optical: (dict or None): `analyze_optical`または`read_alpha_from_excel`から返された光学データ辞書。 :param E_eV: (float): 取得したいフォトンエネルギー (eV)。 :returns: `float`: 指定されたエネルギーにおける吸収係数 (cm^-1)。データがない場合はNaN。 """ if optical is None: return float("nan") wl = np.asarray(optical["wl_nm"], dtype=float) alpha = np.asarray(optical["alpha_cm^-1"], dtype=float) energy = 1239.841984 / wl order = np.argsort(energy) energy = energy[order] alpha = alpha[order] alpha_interp = np.interp(E_eV, energy, alpha, left=np.nan, right=np.nan) if not np.isfinite(alpha_interp): raise ValueError(f"got nan for alpha_at_energy()") return float(alpha_interp)
[ドキュメント] def absorptance_from_alpha(alpha_cm, d_nm): """ 吸収係数と膜厚から吸収率を計算します。 詳細説明: `A = 1 - exp(-alpha * d)`の式を用いて計算します。 :param alpha_cm: (float or None): 吸収係数 (cm^-1)。 :param d_nm: (float): PV層の膜厚 (nm)。 :returns: `float`: 計算された吸収率。 """ if alpha_cm is None or not np.isfinite(alpha_cm): raise ValueError(f"got None or invalid value for alpha_cm") d_cm = d_nm * 1e-7 return float(1.0 - np.exp(-alpha_cm * d_cm))
[ドキュメント] def save_alpha_to_excel(optical, outfile_xlsx, meta=None): """ 吸収スペクトルデータをExcelファイルに保存します。 詳細説明: `alpha_spectrum`というシートにフォトンエネルギー、波長、R、Tr、A、alphaのデータを書き込みます。 `summary`シートには膜厚やファイル名、平均値などのメタデータを保存します。 :param optical: (dict): `analyze_optical`関数から返された光学データ辞書。 :param outfile_xlsx: (str): 保存するExcelファイルのパス。 :param meta: (dict, optional): 保存する追加のメタデータ。デフォルトはNone。 :returns: なし """ print(f"[I/O] Save alpha Excel: {outfile_xlsx}") wb = Workbook() ws = wb.active ws.title = "alpha_spectrum" header_fill = PatternFill("solid", fgColor="1F4E78") header_font = Font(color="FFFFFF", bold=True) headers = ["photon_energy_eV", "wavelength_nm", "R", "Tr", "A", "alpha_cm^-1"] ws.append(headers) for c in ws[1]: c.fill = header_fill c.font = header_font hc_eVnm = 1239.841984 energy_eV = hc_eVnm / optical["wl_nm"] order = np.argsort(energy_eV) for i in order: ws.append([ float(energy_eV[i]), float(optical["wl_nm"][i]), float(optical["R"][i]), float(optical["Tr"][i]), float(optical["A"][i]), float(optical["alpha_cm^-1"][i]), ]) for col in ["A", "B", "C", "D", "E", "F"]: ws.column_dimensions[col].width = 18 ws2 = wb.create_sheet("summary") ws2["A1"] = "d_nm" ws2["B1"] = meta.get("d_nm", "") if meta else "" ws2["A2"] = "R_file" ws2["B2"] = meta.get("R", "") if meta else "" ws2["A3"] = "Tr_file" ws2["B3"] = meta.get("Tr", "") if meta else "" ws2["A5"] = "R_mean" ws2["B5"] = float(np.mean(optical["R"])) ws2["A6"] = "Tr_mean" ws2["B6"] = float(np.mean(optical["Tr"])) ws2["A7"] = "A_mean" ws2["B7"] = float(np.mean(optical["A"])) ws2["A8"] = "alpha_mean_cm^-1" ws2["B8"] = float(np.mean(optical["alpha_cm^-1"])) for col in ["A", "B"]: ws2.column_dimensions[col].width = 24 wb.save(outfile_xlsx) print(f"[I/O] Saved alpha Excel: {outfile_xlsx}")
[ドキュメント] def plot_alpha(optical, outfile=None, pause=False): """ 吸収係数スペクトル(α vs フォトンエネルギー)をプロットします。 :param optical: (dict): `analyze_optical`関数から返された光学データ辞書。 :param outfile: (str, optional): プロットを保存するファイルパス。Noneの場合、画面に表示します。デフォルトはNone。 :returns: `matplotlib.figure.Figure`: 作成されたMatplotlib Figureオブジェクト。 """ fig, ax = plt.subplots(1, 1, figsize=(6.2, 4.8)) hc_eVnm = 1239.841984 energy_eV = hc_eVnm / optical["wl_nm"] order = np.argsort(energy_eV) x = energy_eV[order] y = optical["alpha_cm^-1"][order] ax.plot(x, y, "-", linewidth=1.5) ax.tick_params(axis='x', labelsize=fontsize) ax.tick_params(axis='y', labelsize=fontsize) ax.set_xlabel("Photon energy / eV", fontsize=fontsize) ax.set_ylabel(r"$\alpha$ / cm$^{-1}$", fontsize=fontsize) ax.set_title(r"Absorption coefficient $\alpha(E)$", fontsize=fontsize) ax.grid(True) fig.tight_layout() if outfile: print(f"[I/O] Save plot: {outfile}") fig.savefig(outfile, dpi=160) if pause: plt.show() else: plt.pause(0.01) return fig
[ドキュメント] def estimate_ndiode_curve(V, I, T=300.0): """ I-Vデータから電圧に対するダイオード因子(ndiode)の曲線を推定します。 詳細説明: log(I)の一次微分を計算し、それに基づいて各電圧点におけるダイオード因子を導出します。 :param V: (numpy.ndarray): 電圧データ (V)。 :param I: (numpy.ndarray): 電流データ (A)。 :param T: (float, optional): 温度 (K)。デフォルトは300.0。 :returns: `Vn` (numpy.ndarray): ダイオード因子が計算された電圧データ。 `nn` (numpy.ndarray): 各電圧に対応する推定されたダイオード因子。 """ V = np.asarray(V, dtype=float) I = np.asarray(I, dtype=float) xu, yu = consolidate_duplicate_x(V, I) order = np.argsort(xu) xu = xu[order] yu = yu[order] mask = (xu > 0.0) & (yu > 0.0) if np.count_nonzero(mask) < 5: return np.array([]), np.array([]) x = xu[mask] y = yu[mask] logI = np.log(np.abs(y) + EPS_I) logI_sm = smooth_polyfit(logI, window_points=5, poly_order=3) dlogIdV = np.gradient(logI_sm, x) with np.errstate(divide="ignore", invalid="ignore"): ndiode = E_CHARGE / (KB * T * dlogIdV) good = np.isfinite(ndiode) & (ndiode > 0) return x[good], ndiode[good]
[ドキュメント] def plot_iv_comparison(dark_sweeps, light_sweeps, S, light_metrics, dark_params=None, light_params=None, outfile=None, T=300.0, pause=False): """ 暗電流I-Vおよび光照射I-Vデータと、関連する推定値や解析結果を比較プロットします。 詳細説明: 以下の4つのサブプロットを作成します。 - log(|I|)-V曲線(暗電流と光電流)。 - 線形J-V曲線(順方向と逆方向)。RsとRshの接線、Ish(0)点を表示。 - ndiode-V曲線と代表点。 - 光起電力出力(-J vs V)。MPP, Voc, Jsc点を表示。 :param dark_sweeps: (list[tuple[numpy.ndarray, numpy.ndarray]]): 暗電流測定のI-Vスイープデータのリスト。 :param light_sweeps: (list[tuple[numpy.ndarray, numpy.ndarray]]): 光照射測定のI-Vスイープデータのリスト。 :param S: (float): デバイスの面積 (cm^2)。 :param light_metrics: (dict): 光照射下のI-Vデータから計算された光起電力性能指標。 :param dark_params: (dict, optional): 暗電流I-Vデータから推定された初期パラメータ。Rs/Rshの接線表示に使用。デフォルトはNone。 :param light_params: (dict, optional): 光照射I-Vデータから推定された初期パラメータ。I0/IPVの表示に使用。デフォルトはNone。 :param outfile: (str, optional): プロットを保存するファイルパス。Noneの場合、画面に表示します。デフォルトはNone。 :param T: (float, optional): 温度 (K)。ndiode曲線の計算に使用。デフォルトは300.0。 :returns: `matplotlib.figure.Figure`: 作成されたMatplotlib Figureオブジェクト。 """ fig = plt.figure(figsize=(13, 8)) gs = fig.add_gridspec(2, 2, height_ratios=[3, 1.5]) ax_log = fig.add_subplot(gs[0, 0]) ax_lin_f = fig.add_subplot(gs[0, 1]) ax_nd = fig.add_subplot(gs[1, 0], sharex=ax_log) ax_pv = fig.add_subplot(gs[1, 1]) ax_lin_r = ax_lin_f.twinx() def style_ax(ax, xlabel=None, ylabel=None, title=None): ax.tick_params(axis='x', labelsize=fontsize) ax.tick_params(axis='y', labelsize=fontsize) if xlabel is not None: ax.set_xlabel(xlabel, fontsize=fontsize) if ylabel is not None: ax.set_ylabel(ylabel, fontsize=fontsize) if title is not None: ax.set_title(title, fontsize=fontsize) def plot_split_linear(ax_f, ax_r, sweeps, label_prefix, color=None): first_f = True first_r = True for V, I in sweeps: V = np.asarray(V, dtype=float) I = np.asarray(I, dtype=float) mask_f = V >= 0.0 mask_r = V <= 0.0 if np.any(mask_f): Jf = I[mask_f] / S * 1e3 ax_f.plot(np.abs(V[mask_f]), Jf, "-", linewidth=1.2, color=color, label=label_prefix if first_f else None) first_f = False if np.any(mask_r): Jr = I[mask_r] / S * 1e3 ax_r.plot(np.abs(V[mask_r]), Jr, "--", linewidth=1.2, color=color, label=f"{label_prefix} (rev)" if first_r else None) first_r = False plot_split_linear(ax_lin_f, ax_lin_r, dark_sweeps, "dark") plot_split_linear(ax_lin_f, ax_lin_r, light_sweeps, "light") if dark_params is not None and len(dark_sweeps) > 0: Vd, Id = dark_sweeps[0] rs_info = estimate_rs_tangent_point(Vd, Id) a_rs = rs_info["slope"] / S * 1e3 b_rs = rs_info["intercept"] / S * 1e3 v_rep_rs = rs_info["v_rep"] i_rep_rs = rs_info["i_rep"] / S * 1e3 if abs(a_rs) > 1e-30: v_zero = float(-b_rs / a_rs) x0 = max(0.0, min(abs(v_rep_rs), abs(v_zero))) x1 = max(abs(v_rep_rs), abs(v_zero)) x_tan_rs = np.array([x0, x1]) y_tan_rs = a_rs * x_tan_rs + b_rs ax_lin_f.plot(x_tan_rs, y_tan_rs, "-", linewidth=0.9, label="Rs tangent") ax_lin_f.plot(abs(v_rep_rs), i_rep_rs, "o", markersize=5, label="Rs point") if dark_params is not None and len(dark_sweeps) > 0: Vd, Id = dark_sweeps[0] tang = estimate_rsh_tangent_point(Vd, Id) a = tang["slope"] / S * 1e3 b = tang["intercept"] / S * 1e3 v_rep = tang["v_rep"] i_rep = tang["i_rep"] / S * 1e3 x_rev_all = [] for VV, II in dark_sweeps: VV = np.asarray(VV, dtype=float) mask_r_all = VV <= 0.0 if np.any(mask_r_all): x_rev_all.extend(list(np.abs(VV[mask_r_all]))) if len(x_rev_all) == 0: x_tan = np.array([0.0, abs(v_rep)]) else: x_tan = np.array([0.0, float(np.max(x_rev_all))]) y_tan = (-a) * x_tan + b ax_lin_r.plot(x_tan, y_tan, "-", linewidth=0.9, label="Rsh tangent", zorder=5) ax_lin_r.plot(abs(v_rep), i_rep, "o", markersize=5, label="Rsh point", zorder=6) ish0 = float(b) ax_lin_r.plot(0.0, ish0, "s", markersize=5, label="Ish(0)", zorder=6) style_ax(ax_lin_f, xlabel="|V| / V", ylabel="J forward / mA cm$^{-2}$, V >= 0", title="Linear J-V (forward / reverse separated)") ax_lin_r.tick_params(axis='y', labelsize=fontsize) ax_lin_r.set_ylabel("J reverse / mA cm$^{-2}$, V <= 0", fontsize=fontsize) ax_lin_f.grid(True) h1, l1 = ax_lin_f.get_legend_handles_labels() h2, l2 = ax_lin_r.get_legend_handles_labels() ax_lin_f.legend(h1 + h2, l1 + l2, fontsize=8, loc="best") for i, (V, I) in enumerate(dark_sweeps): ax_log.plot(V, np.log10(np.abs(I) + EPS_I), "-", linewidth=1.2, label="dark" if i == 0 else None) for i, (V, I) in enumerate(light_sweeps): ax_log.plot(V, np.log10(np.abs(I) + EPS_I), "-", linewidth=1.2, label="light" if i == 0 else None) if light_params is not None: ax_log.axhline(np.log10(abs(light_params["I0"]) + EPS_I), linestyle=":", linewidth=1.0, label="-I0") ax_log.axhline(np.log10(abs(light_params["I0"] + light_params["IPV"]) + EPS_I), linestyle=":", linewidth=1.0, label="-I0-IPV") style_ax(ax_log, xlabel="V / V", ylabel=r"log$_{10}$(|I|)", title="Dark / Illuminated log10(|I|)-V") ax_log.grid(True) ax_log.legend(fontsize=8) Vdark0, Idark0 = dark_sweeps[0] Vn, nn = estimate_ndiode_curve(Vdark0, Idark0, T=T) if len(Vn) > 0: ax_nd.plot(Vn, nn, "-", linewidth=1.3, label="ndiode(V)") ish_for_nd = dark_params.get("Ish", None) if dark_params is not None else None nd_rep = estimate_ndiode_representative_point(Vdark0, Idark0, T=T, Ish=ish_for_nd) if nd_rep is not None and np.isfinite(nd_rep["ndiode"]): ax_nd.plot(nd_rep["v_rep"], nd_rep["ndiode"], "o", markersize=6, label="ndiode point") style_ax(ax_nd, xlabel="V / V", ylabel="ndiode", title="Estimated ndiode vs V") ax_nd.grid(True) ax_nd.legend(fontsize=8) V0, I0 = light_sweeps[0] J0_mA = I0 / S * 1e3 ypv = -J0_mA ax_pv.plot(V0, ypv, "-", linewidth=1.5, label="-J (light)") ax_pv.plot(light_metrics["Vop_V"], -light_metrics["Jop_mA_cm2"], "o", markersize=7, label="MPP") ax_pv.plot(light_metrics["Voc_V"], 0.0, "s", markersize=7, label="Voc") ax_pv.plot(0.0, -light_metrics["Jsc_mA_cm2"], "^", markersize=7, label="Jsc") style_ax(ax_pv, xlabel="V / V", ylabel="-J / mA/cm$^2$", title="Photovoltaic output") ymax = max(float(np.max(ypv)), 0.0, float(-light_metrics["Jsc_mA_cm2"])) ax_pv.set_ylim(0.0, ymax if ymax > 0 else 1.0) ax_pv.grid(True) ax_pv.legend(fontsize=8) fig.tight_layout() if outfile: print(f"[I/O] Save plot: {outfile}") fig.savefig(outfile, dpi=160) if pause: plt.show() else: plt.pause(0.01) return fig
[ドキュメント] def exec_alpha(args): """ `alpha`または`make_alpha`モードでプログラムを実行するメイン関数。 詳細説明: 光学スペクトルファイル(RとT)を読み込み、吸収率と吸収係数を計算し、 結果をプロットしてExcelファイルに保存します。 :param args: (argparse.Namespace): コマンドライン引数。 :returns: なし """ # E_use = compute_photon_energy_eV(args.E, args.lambda_nm) # P0_use = compute_p0(args.F0, args.P0, args.E, args.lambda_nm) # print_args_and_derived(args, E_use, P0_use) print_args_and_derived(args) wl_R, R_percent, info_R = read_optical_spectrum(args.R) wl_T, T_percent, info_T = read_optical_spectrum(args.Tr) optical = analyze_optical(wl_R, R_percent, wl_T, T_percent, args.d) print() print("=== make_alpha analysis ===") print(f"d_nm : {args.d:.16g} nm") print(f"R_mean : {np.mean(optical['R']):.16g}") print(f"Tr_mean : {np.mean(optical['Tr']):.16g}") print(f"A_mean : {np.mean(optical['A']):.16g}") print(f"alpha_mean : {np.mean(optical['alpha_cm^-1']):.16g} cm^-1") print() outxlsx = f"{args.outprefix}_alpha.xlsx" if args.mode == 'make_alpha': save_alpha_to_excel(optical, outxlsx, meta={"d_nm": args.d, "R": args.R, "Tr": args.Tr}) print(f"Saving Excel : {outxlsx}") outplot = f"{args.outprefix}_alpha_E.png" print(f"Saving plot : {outplot}") plot_alpha(optical, outfile=outplot, pause = False) input("\nPress ENTER to terminate\n")
[ドキュメント] def exec_analyze(args): """ `analyze`モードでプログラムを実行するメイン関数。 詳細説明: 暗電流および光照射I-Vファイルを読み込み、初期パラメータと光起電力性能指標を推定し、 量子効率を計算します。光学データが利用可能な場合はそれも考慮に入れます。 結果を標準出力に表示し、比較プロットを作成します。 :param args: (argparse.Namespace): コマンドライン引数。 :returns: なし """ print() print("[MODE] analyze") P0_use = compute_p0(args.F0, args.P0, args.E, args.lambda_nm) E_use = compute_photon_energy_eV(args.E, args.lambda_nm) print_args_and_derived(args, E_use, P0_use) dark_xs, dark_ys, dark_info = read_data(args.dark) light_xs, light_ys, light_info = read_data(args.light) dark_sweeps = list(zip(dark_xs, dark_ys)) light_sweeps = list(zip(light_xs, light_ys)) Vd, Id = choose_sweep(dark_xs, dark_ys, args.sweep_dark) Vl, Il = choose_sweep(light_xs, light_ys, args.sweep_light) dark_params = estimate_initial_params(Vd, Id, T=args.T) dark_params["IPV"] = 0.0 light_params = estimate_initial_params(Vl, Il, T=args.T) dark_metrics = pv_metrics_from_iv(Vd, Id, args.S) light_metrics = pv_metrics_from_iv(Vl, Il, args.S) optical = None alpha_at_E = float("nan") A_at_E = float("nan") if args.alpha: optical = read_alpha_from_excel(args.alpha) if optical is not None: alpha_at_E = alpha_at_energy(optical, E_use) if alpha_at_E is None: raise ValueError(f"Got nan for alpha_at_E()") A_at_E = absorptance_from_alpha(alpha_at_E, args.d) eta = float("nan") if P0_use is not None and P0_use > 0: eta = light_metrics["Pmax_W_cm2"] / P0_use JPV_A_cm2 = light_params["IPV"] / args.S qe = quantum_efficiencies(args.F0, A_at_E, JPV_A_cm2, light_metrics["Jsc_A_cm2"]) print("=== Device constants ===") print(f"d = {args.d:.6f} nm") print(f"S = {args.S:.8f} cm^2") print(f"T = {args.T:.6f} K") print() print("=== Photon input ===") print(f"lambda = {args.lambda_nm:.16g} nm") print(f"E = {E_use:.16g} eV") print(f"F0 = {args.F0:.16g} cm^-2 s^-1" if args.F0 is not None else "F0 = None") print(f"P0 = {P0_use*1e3:.16g} mW/cm^2" if P0_use is not None else "P0 = None") print() print("=== Dark IV estimated parameters ===") for k in PARAM_NAMES: print(f"{k:8s} = {dark_params[k]:.16g}") rsh_info = estimate_rsh_tangent_point(Vd, Id) rs_info = estimate_rs_tangent_point(Vd, Id) print(f"{'Vs':8s} = {rs_info['v_rep']:.16g}") print(f"{'Is':8s} = {rs_info['i_rep']:.16g}") print(f"{'Vsh':8s} = {rsh_info['v_rep']:.16g}") print(f"{'Ish':8s} = {rsh_info['Ish']:.16g}") print(f"{'Ish(0)':8s} = {rsh_info['intercept']:.16g}") print() print("=== Illuminated IV estimated parameters ===") for k in PARAM_NAMES: print(f"{k:8s} = {light_params[k]:.16g}") print() print("=== Photovoltaic metrics ===") print(f"Voc = {light_metrics['Voc_V']:.16g} V") print(f"Jsc = {light_metrics['Jsc_mA_cm2']:.16g} mA/cm^2") print(f"Vop = {light_metrics['Vop_V']:.16g} V") print(f"Jop = {light_metrics['Jop_mA_cm2']:.16g} mA/cm^2") print(f"FF = {light_metrics['FF']:.16g}") print(f"Pmax = {light_metrics['Pmax_mW_cm2']:.16g} mW/cm^2") print(f"Efficiency = {eta:.16g}" if np.isfinite(eta) else "Efficiency = nan") print() if optical is not None: print("=== Optical metrics ===") print(f"alpha(E) = {alpha_at_E:.16g} cm^-1") print(f"absorption length = {args.d} nm") print(f"A(E) = {A_at_E:.16g}") print() print("=== Quantum efficiencies ===") print(f"JPV = {JPV_A_cm2*1e3:.16g} mA/cm^2") print(f"EQE_gen = {qe['EQE_gen']:.16g}") print(f"IQE_gen = {qe['IQE_gen']:.16g}") print(f"EQE = {qe['EQE']:.16g}") print(f"IQE = {qe['IQE']:.16g}") print() outplot = f"{args.outprefix}_iv.png" plot_iv_comparison( dark_sweeps, light_sweeps, args.S, light_metrics, dark_params=dark_params, light_params=light_params, outfile=outplot, T=args.T, pause=False ) print(f"Saved plot: {outplot}") input("\nPress ENTER to terminate\n")
_original_print = None _redirect_fp = None
[ドキュメント] def dual_print(*args, **kwargs): _original_print(*args, **kwargs) file_kwargs = dict(kwargs) file_kwargs.pop("file", None) # file指定があっても無視してログファイルへ file_kwargs.setdefault("flush", True) _original_print(*args, file=_redirect_fp, **file_kwargs)
[ドキュメント] def main(): """ プログラムのエントリーポイント。 詳細説明: コマンドライン引数を初期化し、指定されたモードに基づいて 対応する実行関数を呼び出します。 :returns: なし """ global _original_print, _redirect_fp print() args, parser = initialize() _original_print = builtins.print if "alpha" in args.mode: logfile = f"{args.outprefix}_alpha.txt" else: logfile = f"{args.outprefix}_{args.mode}.txt" print(f"Open log file [{logfile}]") _redirect_fp = open(logfile, "w", encoding="utf-8") builtins.print = dual_print if args.mode == "alpha" or args.mode == "make_alpha": exec_alpha(args) elif args.mode == "analyze": exec_analyze(args) else: parser.error(f"Unsupported mode: {args.mode}")
if __name__ == "__main__": try: main() except Exception: traceback.print_exc() sys.exit(1) finally: if _redirect_fp: _redirect_fp.close()