Source code for psc_sim.cell_metrics

"""Cell metrics, SR curves, and Rs extraction from the implicit diode model."""

from __future__ import annotations

import math

import numpy as np

from psc_sim.iv_solver import terminal_voc
from psc_sim.parameters import EXP_CLIP, CellMetrics, CellParameters
from psc_sim.simulation.backends import IVBackend, PythonIVBackend


def _current_at_v(
    v: float,
    p: CellParameters,
    rs: float,
    backend: IVBackend,
    i_hint: float | None,
) -> float:
    return backend.solve_point(v, p, rs=rs, i_hint=i_hint)


[docs] def normalized_sr_at_bias(v: float, i: float, p: CellParameters, rs: float) -> tuple[float, float]: """Return (SR, SRinv) = (dI/dIs, (dI/dIs)^-1) at one bias point.""" vt = p.vt() vd = v + i * rs ev = min(vd / vt, EXP_CLIP) exp_term = p.I0 * math.exp(ev) denom = 1.0 + exp_term * rs / vt + rs / p.Rsh sr = 1.0 / denom return sr, denom
def sr_vs_bias( p: CellParameters, v0: float = 0.0, v1: float = 0.9, step: float = 0.01, rs: float | None = None, *, iv_backend: IVBackend | None = None, ) -> tuple[np.ndarray, np.ndarray]: backend = iv_backend or PythonIVBackend() rs_use = p.Rs if rs is None else rs V = np.arange(v0, v1 + step * 0.5, step, dtype=float) SR = np.empty_like(V) prev: float | None = None for idx, v in enumerate(V): cur = _current_at_v(float(v), p, rs_use, backend, prev) prev = cur sr, _ = normalized_sr_at_bias(float(v), cur, p, rs_use) SR[idx] = sr return V, SR
[docs] def rs_extraction_series( p: CellParameters, v0: float = 0.0, v1: float = 0.8, step: float = 0.02, rs: float | None = None, *, iv_backend: IVBackend | None = None, ) -> tuple[np.ndarray, np.ndarray]: """Returns I_mA and SRinv = (dI/dIs)^-1.""" backend = iv_backend or PythonIVBackend() rs_use = p.Rs if rs is None else rs V = np.arange(v0, v1 + step * 0.5, step, dtype=float) I_mA = np.empty_like(V) SRinv = np.empty_like(V) prev: float | None = None for idx, v in enumerate(V): cur = _current_at_v(float(v), p, rs_use, backend, prev) prev = cur _, srinv = normalized_sr_at_bias(float(v), cur, p, rs_use) I_mA[idx] = cur * 1000.0 SRinv[idx] = srinv return I_mA, SRinv
[docs] def extract_rs_from_sr_inv( p: CellParameters, i_mA: np.ndarray | None = None, sr_inv: np.ndarray | None = None, mask: slice | np.ndarray | None = None, rs: float | None = None, *, iv_backend: IVBackend | None = None, ) -> float: """Linear regression SRinv vs I_mA; Rs = slope * vt * 1000.""" if i_mA is None or sr_inv is None: i_mA, sr_inv = rs_extraction_series(p, rs=rs, iv_backend=iv_backend) if mask is None: mask = slice(None) x = np.asarray(i_mA, dtype=float)[mask] y = np.asarray(sr_inv, dtype=float)[mask] ok = np.isfinite(x) & np.isfinite(y) x, y = x[ok], y[ok] n = len(x) if n < 2: return float("nan") mx = float(x.mean()) my = float(y.mean()) num = float(((x - mx) * (y - my)).sum()) den = float(((x - mx) ** 2).sum()) if den == 0.0: return float("nan") slope = num / den vt = p.vt() return slope * vt * 1000.0
[docs] def compute_metrics( p: CellParameters, pin_w_cm2: float = 0.1, *, iv_backend: IVBackend | None = None, ) -> CellMetrics: """Compute Voc/Isc/FF/PCE metrics using the same sampling as the HTML app.""" backend = iv_backend or PythonIVBackend() rs = p.Rs isc = _current_at_v(0.0, p, rs, backend, None) voc = terminal_voc(p, rs, v_hi=1.5) or 0.0 pmax = 0.0 vmp = 0.0 imp = 0.0 v = 0.0 i_prev = float(isc) while v <= voc + 1e-12: vq = round(float(v), 6) cur = _current_at_v(vq, p, rs, backend, i_prev) if not math.isfinite(cur): break i_prev = cur power = -vq * cur if power > pmax: pmax, vmp, imp = power, vq, cur v += 0.002 ff = min(1.0, max(0.0, pmax / (voc * abs(isc)))) if voc > 0 and isc != 0 else float("nan") pce = pmax / pin_w_cm2 * 100.0 if pin_w_cm2 > 0 else float("nan") rs_x = extract_rs_from_sr_inv(p, iv_backend=backend) return CellMetrics( voc=voc, isc=isc, ff=ff, pce=pce, rs_extracted=rs_x, pmax=pmax, vmp=vmp, imp=imp, )