From 6a1196968c076059468058381538be3cd0c963f9 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 8 Apr 2025 14:09:58 -0400 Subject: [PATCH 01/17] add function --- pvlib/ivtools/sdm/__init__.py | 1 + pvlib/ivtools/sdm/pvsyst.py | 310 ++++++++++++++++++++++++++++++++++ 2 files changed, 311 insertions(+) diff --git a/pvlib/ivtools/sdm/__init__.py b/pvlib/ivtools/sdm/__init__.py index b45519fc16..d0b658c198 100644 --- a/pvlib/ivtools/sdm/__init__.py +++ b/pvlib/ivtools/sdm/__init__.py @@ -15,5 +15,6 @@ from pvlib.ivtools.sdm.pvsyst import ( # noqa: F401 fit_pvsyst_sandia, + fit_pvsyst_iec61853_sandia, pvsyst_temperature_coeff, ) diff --git a/pvlib/ivtools/sdm/pvsyst.py b/pvlib/ivtools/sdm/pvsyst.py index 6233c38c72..4724c0b55f 100644 --- a/pvlib/ivtools/sdm/pvsyst.py +++ b/pvlib/ivtools/sdm/pvsyst.py @@ -14,6 +14,9 @@ _extract_sdm_params, _initial_iv_params, _update_iv_params ) +from pvlib.pvsystem import ( + _pvsyst_Rsh, _pvsyst_IL, _pvsyst_Io, _pvsyst_nNsVth, _pvsyst_gamma +) CONSTANTS = {'E0': 1000.0, 'T0': 25.0, 'k': constants.k, 'q': constants.e} @@ -305,3 +308,310 @@ def maxp(temp_cell, irrad_ref, alpha_sc, gamma_ref, mu_gamma, I_L_ref, gamma_pdc = _first_order_centered_difference(maxp, x0=temp_ref, args=args) return gamma_pdc / pmp + + +def fit_pvsyst_iec61853_sandia(effective_irradiance, temp_cell, + i_sc, v_oc, i_mp, v_mp, + cells_in_series, EgRef=1.121, + alpha_sc=None, beta_mp=None, + r_sh_coeff=0.12, R_s=None, + min_Rsh_irradiance=None): + """ + Estimate parameters for the PVsyst module performance model using + IEC 61853-1 matrix measurements. + + Parameters + ---------- + effective_irradiance : array + Effective irradiance for each test condition [W/m²] + temp_cell : array + Cell temperature for each test condition [C] + i_sc : array + Short circuit current for each test condition [A] + v_oc : array + Open circuit voltage for each test condition [V] + i_mp : array + Current at maximum power point for each test condition [A] + v_mp : array + Voltage at maximum power point for each test condition [V] + cells_in_series : int + The number of cells connected in series. + EgRef : float, optional + The energy bandgap at reference temperature in units of eV. + 1.121 eV for crystalline silicon. EgRef must be >0. + alpha_sc : float, optional + Temperature coefficient of short circuit current. If not specified, + it will be estimated using the ``i_sc`` values at irradiance of + 1000 W/m2. [A/K] + beta_mp : float, optional + Temperature coefficient of maximum power voltage. If not specified, + it will be estimated using the ``v_mp`` values at irradiance of + 1000 W/m2. [1/K] + r_sh_coeff : float, default 0.12 + Shunt resistance fitting coefficient. The default value is taken + from [1]_. + R_s : float, optional + Series resistance value. If not provided, a value will be estimated + from the input measurements. [ohm] + min_Rsh_irradiance : float, optional + Irradiance threshold below which values are excluded when estimating + shunt resistance parameter values. May be useful for modules + with problematic low-light measurements. [W/m²] + + Returns + ------- + dict + alpha_sc : float + short circuit current temperature coefficient [A/K] + gamma_ref : float + diode (ideality) factor at STC [unitless] + mu_gamma : float + temperature coefficient for diode (ideality) factor [1/K] + I_L_ref : float + light current at STC [A] + I_o_ref : float + dark current at STC [A] + R_sh_ref : float + shunt resistance at STC [ohm] + R_sh_0 : float + shunt resistance at zero irradiance [ohm] + R_sh_exp : float + exponential factor defining decrease in shunt resistance with + increasing effective irradiance + R_s : float + series resistance at STC [ohm] + cells_in_series : int + number of cells in series + EgRef : float + effective band gap at STC [eV] + + See also + -------- + pvlib.pvsystem.calcparams_pvsyst + pvlib.ivtools.sdm.fit_pvsyst_sandia + + Notes + ----- + This method is non-iterative. In some cases, it may be desirable to + refine the estimated parameter values using a numerical optimizer like + those provided in ``scipy.optimize``. + + References + ---------- + .. [1] K. S. Anderson, C. W. Hansen, and M. Theristis, "A Noniterative + Method of Estimating Parameter Values for the PVsyst Version 6 + Single-Diode Model From IEC 61853-1 Matrix Measurements," IEEE Journal + of Photovoltaics, vol. 15, 3, 2025. :doi:`10.1109/JPHOTOV.2025.3554338` + """ + + try: + import statsmodels.api as sm + except ImportError: + raise ImportError('fit_pvsyst_iec61853_sandia requires statsmodels') + + if alpha_sc is None: + mu_i_sc = _fit_tempco(i_sc[effective_irradiance==1000], + temp_cell[effective_irradiance==1000]) + i_sc_ref = float(i_sc[(effective_irradiance==1000) & (temp_cell==25)]) + alpha_sc = mu_i_sc * i_sc_ref + + if beta_mp is None: + beta_mp = _fit_tempco(v_mp[effective_irradiance==1000], + temp_cell[effective_irradiance==1000]) + + R_sh_ref, R_sh_0, R_sh_exp = _fit_shunt_resistances( + i_sc, i_mp, v_mp, effective_irradiance, temp_cell, beta_mp, + coeff=r_sh_coeff, min_irradiance=min_Rsh_irradiance + ) + + if R_s is None: + R_s = _fit_series_resistance(sm, v_oc, i_mp, v_mp) + + filt = temp_cell == 25 + gamma_ref, mu_gamma = _fit_diode_ideality_factor( + sm, i_sc[filt], v_oc[filt], i_mp[filt], v_mp[filt], + effective_irradiance[filt], temp_cell[filt], + R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series + ) + + I_o_ref = _fit_saturation_current( + i_sc, v_oc, effective_irradiance, temp_cell, gamma_ref, mu_gamma, + R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series, EgRef + ) + + I_L_ref = _fit_photocurrent( + sm, i_sc, effective_irradiance, temp_cell, alpha_sc, + gamma_ref, mu_gamma, + I_o_ref, R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series, EgRef + ) + + gamma_ref, mu_gamma = \ + _fit_diode_ideality_factor_post(sm, i_mp, v_mp, effective_irradiance, + temp_cell, alpha_sc, I_L_ref, I_o_ref, + R_sh_ref, R_sh_0, R_sh_exp, R_s, + cells_in_series, EgRef) + + fitted_params = dict( + alpha_sc=alpha_sc, + gamma_ref=gamma_ref, + mu_gamma=mu_gamma, + I_L_ref=I_L_ref, + I_o_ref=I_o_ref, + R_sh_ref=R_sh_ref, + R_sh_0=R_sh_0, + R_sh_exp=R_sh_exp, + R_s=R_s, + cells_in_series=cells_in_series, + EgRef=EgRef, + ) + return fitted_params + + +def _fit_tempco(values, temp_cell, temp_cell_ref=25): + fit = np.polynomial.polynomial.Polynomial.fit(temp_cell, values, deg=1) + intercept, slope = fit.convert().coef + value_ref = intercept + slope*temp_cell_ref + return slope / value_ref + + +def _fit_shunt_resistances(i_sc, i_mp, v_mp, effective_irradiance, temp_cell, + beta_v_mp, coeff=0.2, min_irradiance=None): + if min_irradiance is None: + min_irradiance = 0 + + mask = effective_irradiance >= min_irradiance + i_sc = i_sc[mask] + i_mp = i_mp[mask] + v_mp = v_mp[mask] + effective_irradiance = effective_irradiance[mask] + temp_cell = temp_cell[mask] + + # Equation 10 + Rsh_est = ( + (v_mp / (1 + beta_v_mp * (temp_cell - 25))) + / (coeff * (i_sc - i_mp)) + ) + Rshexp = 5.5 + + # Eq 11 + y = Rsh_est + x = np.exp(-Rshexp * effective_irradiance / 1000) + + fit = np.polynomial.polynomial.Polynomial.fit(x, y, deg=1) + intercept, slope = fit.convert().coef + Rshbase = intercept + Rsh0 = slope + Rshbase + + # Eq 12 + expRshexp = np.exp(-Rshexp) + Rshref = Rshbase * (1 - expRshexp) + Rsh0 * expRshexp + + return Rshref, Rsh0, Rshexp + + +def _fit_series_resistance(sm, v_oc, i_mp, v_mp): + # Stein et al 2014, https://doi.org/10.1109/PVSC.2014.6925326 + + # Eq 13 + x = np.array([i_mp, np.log(i_mp), v_mp]).T + x = sm.add_constant(x) + y = v_oc + + results = sm.OLS(endog=y, exog=x).fit() + R_s = results.params['x1'] + return R_s + + +def _fit_diode_ideality_factor(sm, i_sc, v_oc, i_mp, v_mp, + effective_irradiance, temp_cell, + R_sh_ref, R_sh_0, R_sh_exp, R_s, + cells_in_series): + + NsVth = _pvsyst_nNsVth(temp_cell, gamma=1, cells_in_series=cells_in_series) + Rsh = _pvsyst_Rsh(effective_irradiance, R_sh_ref, R_sh_0, R_sh_exp) + term1 = (i_sc * (1 + R_s/Rsh) - v_oc / Rsh) # Eq 15 + term2 = (i_sc - i_mp) * (1 + R_s/Rsh) - v_mp / Rsh # Eq 16 + + # Eq 14 + x1 = NsVth * np.log(term2 / term1) + + x = np.array([x1]).T + y = v_mp + i_mp*R_s - v_oc + + results = sm.OLS(endog=y, exog=x).fit() + gamma_ref = results.params[0] + return gamma_ref, 0 + + +def _fit_saturation_current(i_sc, v_oc, effective_irradiance, temp_cell, + gamma_ref, mu_gamma, + R_sh_ref, R_sh_0, R_sh_exp, R_s, + cells_in_series, EgRef): + R_sh = _pvsyst_Rsh(effective_irradiance, R_sh_ref, R_sh_0, R_sh_exp) + gamma = _pvsyst_gamma(temp_cell, gamma_ref, mu_gamma) + nNsVth = _pvsyst_nNsVth(temp_cell, gamma, cells_in_series) + + # Eq 17 + I_o_est = (i_sc * (1 + R_s/R_sh) - v_oc/R_sh) / (np.expm1(v_oc / nNsVth)) + x = _pvsyst_Io(temp_cell, gamma, I_o_ref=1, EgRef=EgRef) + + # Eq 18 + log_I_o_ref = np.mean(np.log(I_o_est) - np.log(x)) + I_o_ref = np.exp(log_I_o_ref) + + return I_o_ref + + +def _fit_photocurrent(sm, i_sc, effective_irradiance, temp_cell, + alpha_sc, gamma_ref, mu_gamma, I_o_ref, + R_sh_ref, R_sh_0, R_sh_exp, R_s, + cells_in_series, EgRef): + R_sh = _pvsyst_Rsh(effective_irradiance, R_sh_ref, R_sh_0, R_sh_exp) + gamma = _pvsyst_gamma(temp_cell, gamma_ref, mu_gamma) + I_o = _pvsyst_Io(temp_cell, gamma, I_o_ref, EgRef) + nNsVth = _pvsyst_nNsVth(temp_cell, gamma, cells_in_series) + + # Eq 19 + I_L_est = i_sc + I_o * (np.expm1(i_sc * R_s / nNsVth)) + i_sc * R_s / R_sh + + # Eq 20 + x = np.array([effective_irradiance / 1000]).T + y = I_L_est - effective_irradiance / 1000 * alpha_sc * (temp_cell - 25) + results = sm.OLS(endog=y, exog=x).fit() + I_L_ref = results.params['x1'] + return I_L_ref + + +def _fit_diode_ideality_factor_post(sm, i_mp, v_mp, effective_irradiance, + temp_cell, alpha_sc, I_L_ref, I_o_ref, + R_sh_ref, R_sh_0, R_sh_exp, R_s, + cells_in_series, EgRef): + + Rsh = _pvsyst_Rsh(effective_irradiance, R_sh_ref, R_sh_0, R_sh_exp) + I_L = _pvsyst_IL(effective_irradiance, temp_cell, I_L_ref, alpha_sc) + NsVth = _pvsyst_nNsVth(temp_cell, gamma=1, cells_in_series=cells_in_series) + + Tref_K = 25 + 273.15 + Tcell_K = temp_cell + 273.15 + + # Eq 21 + k = constants.k # Boltzmann constant in J/K + q = constants.e # elementary charge in coulomb + numerator = ( + (q * EgRef / k) * (1/Tref_K - 1/Tcell_K) + + (v_mp + i_mp*R_s) / NsVth + ) + denominator = ( + np.log((I_L - i_mp - (v_mp+i_mp*R_s) / Rsh) / I_o_ref) + - 3 * np.log(Tcell_K / Tref_K) + ) + gamma_est = numerator / denominator + + # Eq 22 + x = np.array([temp_cell - 25]).T + x = sm.add_constant(x) + y = gamma_est + + results = sm.OLS(endog=y, exog=x).fit() + gamma_ref, mu_gamma = results.params + return gamma_ref, mu_gamma From 9cb218c18d56747d7974ee30d88455867fa9ceec Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 8 Apr 2025 14:10:13 -0400 Subject: [PATCH 02/17] add tests --- tests/ivtools/sdm/test_pvsyst.py | 106 +++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/tests/ivtools/sdm/test_pvsyst.py b/tests/ivtools/sdm/test_pvsyst.py index 6927a8c1d3..5d246904c6 100644 --- a/tests/ivtools/sdm/test_pvsyst.py +++ b/tests/ivtools/sdm/test_pvsyst.py @@ -7,6 +7,8 @@ from tests.conftest import requires_statsmodels, TESTS_DATA_DIR +import pytest + def _read_iv_curves_for_test(datafile, npts): """ read constants and npts IV curves from datafile """ @@ -198,3 +200,107 @@ def test_pvsyst_temperature_coeff(): params['I_L_ref'], params['I_o_ref'], params['R_sh_ref'], params['R_sh_0'], params['R_s'], params['cells_in_series']) assert_allclose(gamma_pdc, expected, rtol=0.0005) + + +@pytest.fixture +def pvsyst_iec61853_table3(): + # test cases in Table 3 of https://doi.org/10.1109/JPHOTOV.2025.3554338 + parameters = ['alpha_sc', 'I_L_ref', 'I_o_ref', 'gamma_ref', 'mu_gamma', + 'R_sh_ref', 'R_sh_0', 'R_sh_exp', 'R_s', 'cells_in_series', + 'EgRef'] + high_current = { + 'true': [1e-3, 10, 1e-11, 1.0, -1e-4, 300, 1500, 5.5, 0.2, 60, 1.121], + 'estimated': [9.993e-4, 9.997, 1.408e-10, 1.109, -1.666e-5, 543.7, + 6130, 5.5, 0.176, 60, 1.121] + } + high_voltage = { + 'true': [1e-3, 1.8, 1e-9, 1.5, 1e-4, 3000, 10000, 5.5, 5.0, 108, 1.4], + 'estimated': [9.983e-4, 1.799, 1.225e-8, 1.706, 2.662e-4, 4288, 47560, + 5.5, 4.292, 108, 1.4], + } + for d in [high_current, high_voltage]: + for key in d: + d[key] = dict(zip(parameters, np.array(d[key]))) + + return { + 'high current': high_current, 'high voltage': high_voltage + } + + +@pytest.fixture +def iec61853_conditions(): + ee = np.array([100, 100, 200, 200, 400, 400, 400, 600, 600, 600, 600, + 800, 800, 800, 800, 1000, 1000, 1000, 1000, 1100, 1100, + 1100], dtype=np.float64) + tc = np.array([15, 25, 15, 25, 15, 25, 50, 15, 25, 50, 75, + 15, 25, 50, 75, 15, 25, 50, 75, 25, 50, 75], + dtype=np.float64) + return ee, tc + + +@requires_statsmodels +def test_fit_pvsyst_iec61853_sandia(pvsyst_iec61853_table3, + iec61853_conditions): + ee, tc = iec61853_conditions + for _, case in pvsyst_iec61853_table3.items(): + true_params = case['true'] + expected_params = case['estimated'] + + sde_params = pvsystem.calcparams_pvsyst(ee, tc, **true_params) + iv = pvsystem.singlediode(*sde_params) + + fitted_params = sdm.fit_pvsyst_iec61853_sandia( + ee, tc, iv['i_sc'], iv['v_oc'], iv['i_mp'], iv['v_mp'], + true_params['cells_in_series'], EgRef=true_params['EgRef'], + ) + for key in expected_params.keys(): + assert np.isclose(fitted_params[key], expected_params[key], + atol=0, rtol=1e-3) + + +@requires_statsmodels +def test_fit_pvsyst_iec61853_sandia_optional(pvsyst_iec61853_table3, + iec61853_conditions): + # verify that modifying each optional parameter results in different output + ee, tc = iec61853_conditions + case = pvsyst_iec61853_table3['high current'] + true_params = case['true'] + expected = case['estimated'] + sde_params = pvsystem.calcparams_pvsyst(ee, tc, **true_params) + iv = pvsystem.singlediode(*sde_params) + + main_inputs = dict( + effective_irradiance=ee, temp_cell=tc, i_sc=iv['i_sc'], + v_oc=iv['v_oc'], i_mp=iv['i_mp'], v_mp=iv['v_mp'], + cells_in_series=true_params['cells_in_series'] + ) + + fitted_params = sdm.fit_pvsyst_iec61853_sandia(**main_inputs, + EgRef=1.0) + assert not np.isclose(fitted_params['I_o_ref'], expected['I_o_ref'], + atol=0, rtol=1e-3) + + fitted_params = sdm.fit_pvsyst_iec61853_sandia(**main_inputs, + alpha_sc=0.5e-3) + assert not np.isclose(fitted_params['alpha_sc'], expected['alpha_sc'], + atol=0, rtol=1e-3) + + fitted_params = sdm.fit_pvsyst_iec61853_sandia(**main_inputs, + beta_mp=-1e-4) + assert not np.isclose(fitted_params['R_sh_ref'], expected['R_sh_ref'], + atol=0, rtol=1e-3) + + fitted_params = sdm.fit_pvsyst_iec61853_sandia(**main_inputs, + r_sh_coeff=0.3) + assert not np.isclose(fitted_params['R_sh_ref'], expected['R_sh_ref'], + atol=0, rtol=1e-3) + + fitted_params = sdm.fit_pvsyst_iec61853_sandia(**main_inputs, + R_s=0.5) + assert not np.isclose(fitted_params['R_s'], expected['R_s'], + atol=0, rtol=1e-3) + + fitted_params = sdm.fit_pvsyst_iec61853_sandia(**main_inputs, + min_Rsh_irradiance=500) + assert not np.isclose(fitted_params['R_sh_ref'], expected['R_sh_ref'], + atol=0, rtol=1e-3) From f7682ae7ef14d3c09784c44deedba3d68c3cfed8 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 8 Apr 2025 14:10:25 -0400 Subject: [PATCH 03/17] add to API reference --- docs/sphinx/source/reference/pv_modeling/parameters.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sphinx/source/reference/pv_modeling/parameters.rst b/docs/sphinx/source/reference/pv_modeling/parameters.rst index 9b1817bd01..69e8ba942f 100644 --- a/docs/sphinx/source/reference/pv_modeling/parameters.rst +++ b/docs/sphinx/source/reference/pv_modeling/parameters.rst @@ -12,6 +12,7 @@ Functions for fitting single diode models ivtools.sdm.fit_cec_sam ivtools.sdm.fit_desoto ivtools.sdm.fit_pvsyst_sandia + ivtools.sdm.fit_pvsyst_iec61853_sandia ivtools.sdm.fit_desoto_sandia Functions for fitting the single diode equation From f48485c994cbfe6f35fa5bf24664254bcbfb6a5f Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 8 Apr 2025 14:10:32 -0400 Subject: [PATCH 04/17] whatsnew --- docs/sphinx/source/whatsnew/v0.12.1.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.12.1.rst b/docs/sphinx/source/whatsnew/v0.12.1.rst index f1c9b7e69d..a58d792471 100644 --- a/docs/sphinx/source/whatsnew/v0.12.1.rst +++ b/docs/sphinx/source/whatsnew/v0.12.1.rst @@ -14,8 +14,9 @@ Bug fixes Enhancements ~~~~~~~~~~~~ -* ``pvlib.ivtools.sdm`` is now a subpackage. (:issue:`2252`, :pull:`2256`) - +* :py:mod:`pvlib.ivtools.sdm` is now a subpackage. (:issue:`2252`, :pull:`2256`) +* Add a function for estimating PVsyst SDM parameters from IEC 61853-1 matrix + data (:py:func:`~pvlib.ivtools.sdm.fit_pvsyst_iec61853_sandia`). (:issue:`2185`, :pull:`2429`) Documentation ~~~~~~~~~~~~~ From 07540a1e9293380b7e48cd5a39a26cee6075165f Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 8 Apr 2025 14:32:46 -0400 Subject: [PATCH 05/17] lint --- pvlib/ivtools/sdm/pvsyst.py | 32 ++++++++++++++++---------------- tests/ivtools/sdm/test_pvsyst.py | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/pvlib/ivtools/sdm/pvsyst.py b/pvlib/ivtools/sdm/pvsyst.py index 4724c0b55f..78d066d6a8 100644 --- a/pvlib/ivtools/sdm/pvsyst.py +++ b/pvlib/ivtools/sdm/pvsyst.py @@ -409,15 +409,16 @@ def fit_pvsyst_iec61853_sandia(effective_irradiance, temp_cell, except ImportError: raise ImportError('fit_pvsyst_iec61853_sandia requires statsmodels') + is_g_stc = effective_irradiance == 1000 + is_t_stc = temp_cell == 25 + if alpha_sc is None: - mu_i_sc = _fit_tempco(i_sc[effective_irradiance==1000], - temp_cell[effective_irradiance==1000]) - i_sc_ref = float(i_sc[(effective_irradiance==1000) & (temp_cell==25)]) + mu_i_sc = _fit_tempco(i_sc[is_g_stc], temp_cell[is_g_stc]) + i_sc_ref = float(i_sc[is_g_stc & is_t_stc]) alpha_sc = mu_i_sc * i_sc_ref if beta_mp is None: - beta_mp = _fit_tempco(v_mp[effective_irradiance==1000], - temp_cell[effective_irradiance==1000]) + beta_mp = _fit_tempco(v_mp[is_g_stc], temp_cell[is_g_stc]) R_sh_ref, R_sh_0, R_sh_exp = _fit_shunt_resistances( i_sc, i_mp, v_mp, effective_irradiance, temp_cell, beta_mp, @@ -427,10 +428,9 @@ def fit_pvsyst_iec61853_sandia(effective_irradiance, temp_cell, if R_s is None: R_s = _fit_series_resistance(sm, v_oc, i_mp, v_mp) - filt = temp_cell == 25 gamma_ref, mu_gamma = _fit_diode_ideality_factor( - sm, i_sc[filt], v_oc[filt], i_mp[filt], v_mp[filt], - effective_irradiance[filt], temp_cell[filt], + sm, i_sc[is_t_stc], v_oc[is_t_stc], i_mp[is_t_stc], v_mp[is_t_stc], + effective_irradiance[is_t_stc], temp_cell[is_t_stc], R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series ) @@ -447,7 +447,7 @@ def fit_pvsyst_iec61853_sandia(effective_irradiance, temp_cell, gamma_ref, mu_gamma = \ _fit_diode_ideality_factor_post(sm, i_mp, v_mp, effective_irradiance, - temp_cell, alpha_sc, I_L_ref, I_o_ref, + temp_cell, alpha_sc, I_L_ref, I_o_ref, R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series, EgRef) @@ -485,18 +485,18 @@ def _fit_shunt_resistances(i_sc, i_mp, v_mp, effective_irradiance, temp_cell, v_mp = v_mp[mask] effective_irradiance = effective_irradiance[mask] temp_cell = temp_cell[mask] - + # Equation 10 Rsh_est = ( (v_mp / (1 + beta_v_mp * (temp_cell - 25))) / (coeff * (i_sc - i_mp)) ) Rshexp = 5.5 - + # Eq 11 y = Rsh_est x = np.exp(-Rshexp * effective_irradiance / 1000) - + fit = np.polynomial.polynomial.Polynomial.fit(x, y, deg=1) intercept, slope = fit.convert().coef Rshbase = intercept @@ -511,12 +511,12 @@ def _fit_shunt_resistances(i_sc, i_mp, v_mp, effective_irradiance, temp_cell, def _fit_series_resistance(sm, v_oc, i_mp, v_mp): # Stein et al 2014, https://doi.org/10.1109/PVSC.2014.6925326 - + # Eq 13 x = np.array([i_mp, np.log(i_mp), v_mp]).T x = sm.add_constant(x) y = v_oc - + results = sm.OLS(endog=y, exog=x).fit() R_s = results.params['x1'] return R_s @@ -563,7 +563,7 @@ def _fit_saturation_current(i_sc, v_oc, effective_irradiance, temp_cell, def _fit_photocurrent(sm, i_sc, effective_irradiance, temp_cell, - alpha_sc, gamma_ref, mu_gamma, I_o_ref, + alpha_sc, gamma_ref, mu_gamma, I_o_ref, R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series, EgRef): R_sh = _pvsyst_Rsh(effective_irradiance, R_sh_ref, R_sh_0, R_sh_exp) @@ -583,7 +583,7 @@ def _fit_photocurrent(sm, i_sc, effective_irradiance, temp_cell, def _fit_diode_ideality_factor_post(sm, i_mp, v_mp, effective_irradiance, - temp_cell, alpha_sc, I_L_ref, I_o_ref, + temp_cell, alpha_sc, I_L_ref, I_o_ref, R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series, EgRef): diff --git a/tests/ivtools/sdm/test_pvsyst.py b/tests/ivtools/sdm/test_pvsyst.py index 5d246904c6..178e68ab77 100644 --- a/tests/ivtools/sdm/test_pvsyst.py +++ b/tests/ivtools/sdm/test_pvsyst.py @@ -294,7 +294,7 @@ def test_fit_pvsyst_iec61853_sandia_optional(pvsyst_iec61853_table3, r_sh_coeff=0.3) assert not np.isclose(fitted_params['R_sh_ref'], expected['R_sh_ref'], atol=0, rtol=1e-3) - + fitted_params = sdm.fit_pvsyst_iec61853_sandia(**main_inputs, R_s=0.5) assert not np.isclose(fitted_params['R_s'], expected['R_s'], From f91884c304bb0681556c9061dc24e46bf6bcae2e Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 9 Apr 2025 09:48:56 -0400 Subject: [PATCH 06/17] add suffix to names of private functions --- pvlib/ivtools/sdm/pvsyst.py | 62 ++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/pvlib/ivtools/sdm/pvsyst.py b/pvlib/ivtools/sdm/pvsyst.py index 78d066d6a8..b5dd6175de 100644 --- a/pvlib/ivtools/sdm/pvsyst.py +++ b/pvlib/ivtools/sdm/pvsyst.py @@ -413,43 +413,45 @@ def fit_pvsyst_iec61853_sandia(effective_irradiance, temp_cell, is_t_stc = temp_cell == 25 if alpha_sc is None: - mu_i_sc = _fit_tempco(i_sc[is_g_stc], temp_cell[is_g_stc]) i_sc_ref = float(i_sc[is_g_stc & is_t_stc]) + mu_i_sc = _fit_tempco_pvsyst_iec61853_sandia(i_sc[is_g_stc], + temp_cell[is_g_stc]) alpha_sc = mu_i_sc * i_sc_ref if beta_mp is None: - beta_mp = _fit_tempco(v_mp[is_g_stc], temp_cell[is_g_stc]) + beta_mp = _fit_tempco_pvsyst_iec61853_sandia(v_mp[is_g_stc], + temp_cell[is_g_stc]) - R_sh_ref, R_sh_0, R_sh_exp = _fit_shunt_resistances( + R_sh_ref, R_sh_0, R_sh_exp = _fit_shunt_resistances_pvsyst_iec61853_sandia( i_sc, i_mp, v_mp, effective_irradiance, temp_cell, beta_mp, coeff=r_sh_coeff, min_irradiance=min_Rsh_irradiance ) if R_s is None: - R_s = _fit_series_resistance(sm, v_oc, i_mp, v_mp) + R_s = _fit_series_resistance_pvsyst_iec61853_sandia(sm, v_oc, + i_mp, v_mp) - gamma_ref, mu_gamma = _fit_diode_ideality_factor( + gamma_ref, mu_gamma = _fit_diode_ideality_factor_pvsyst_iec61853_sandia( sm, i_sc[is_t_stc], v_oc[is_t_stc], i_mp[is_t_stc], v_mp[is_t_stc], effective_irradiance[is_t_stc], temp_cell[is_t_stc], R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series ) - I_o_ref = _fit_saturation_current( + I_o_ref = _fit_saturation_current_pvsyst_iec61853_sandia( i_sc, v_oc, effective_irradiance, temp_cell, gamma_ref, mu_gamma, R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series, EgRef ) - I_L_ref = _fit_photocurrent( + I_L_ref = _fit_photocurrent_pvsyst_iec61853_sandia( sm, i_sc, effective_irradiance, temp_cell, alpha_sc, gamma_ref, mu_gamma, I_o_ref, R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series, EgRef ) gamma_ref, mu_gamma = \ - _fit_diode_ideality_factor_post(sm, i_mp, v_mp, effective_irradiance, - temp_cell, alpha_sc, I_L_ref, I_o_ref, - R_sh_ref, R_sh_0, R_sh_exp, R_s, - cells_in_series, EgRef) + _fit_diode_ideality_factor_post_pvsyst_iec61853_sandia( + sm, i_mp, v_mp, effective_irradiance, temp_cell, alpha_sc, I_L_ref, + I_o_ref, R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series, EgRef) fitted_params = dict( alpha_sc=alpha_sc, @@ -467,15 +469,16 @@ def fit_pvsyst_iec61853_sandia(effective_irradiance, temp_cell, return fitted_params -def _fit_tempco(values, temp_cell, temp_cell_ref=25): +def _fit_tempco_pvsyst_iec61853_sandia(values, temp_cell, temp_cell_ref=25): fit = np.polynomial.polynomial.Polynomial.fit(temp_cell, values, deg=1) intercept, slope = fit.convert().coef value_ref = intercept + slope*temp_cell_ref return slope / value_ref -def _fit_shunt_resistances(i_sc, i_mp, v_mp, effective_irradiance, temp_cell, - beta_v_mp, coeff=0.2, min_irradiance=None): +def _fit_shunt_resistances_pvsyst_iec61853_sandia( + i_sc, i_mp, v_mp, effective_irradiance, temp_cell, + beta_v_mp, coeff=0.2, min_irradiance=None): if min_irradiance is None: min_irradiance = 0 @@ -509,7 +512,7 @@ def _fit_shunt_resistances(i_sc, i_mp, v_mp, effective_irradiance, temp_cell, return Rshref, Rsh0, Rshexp -def _fit_series_resistance(sm, v_oc, i_mp, v_mp): +def _fit_series_resistance_pvsyst_iec61853_sandia(sm, v_oc, i_mp, v_mp): # Stein et al 2014, https://doi.org/10.1109/PVSC.2014.6925326 # Eq 13 @@ -522,10 +525,9 @@ def _fit_series_resistance(sm, v_oc, i_mp, v_mp): return R_s -def _fit_diode_ideality_factor(sm, i_sc, v_oc, i_mp, v_mp, - effective_irradiance, temp_cell, - R_sh_ref, R_sh_0, R_sh_exp, R_s, - cells_in_series): +def _fit_diode_ideality_factor_pvsyst_iec61853_sandia( + sm, i_sc, v_oc, i_mp, v_mp, effective_irradiance, temp_cell, + R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series): NsVth = _pvsyst_nNsVth(temp_cell, gamma=1, cells_in_series=cells_in_series) Rsh = _pvsyst_Rsh(effective_irradiance, R_sh_ref, R_sh_0, R_sh_exp) @@ -543,10 +545,9 @@ def _fit_diode_ideality_factor(sm, i_sc, v_oc, i_mp, v_mp, return gamma_ref, 0 -def _fit_saturation_current(i_sc, v_oc, effective_irradiance, temp_cell, - gamma_ref, mu_gamma, - R_sh_ref, R_sh_0, R_sh_exp, R_s, - cells_in_series, EgRef): +def _fit_saturation_current_pvsyst_iec61853_sandia( + i_sc, v_oc, effective_irradiance, temp_cell, gamma_ref, mu_gamma, + R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series, EgRef): R_sh = _pvsyst_Rsh(effective_irradiance, R_sh_ref, R_sh_0, R_sh_exp) gamma = _pvsyst_gamma(temp_cell, gamma_ref, mu_gamma) nNsVth = _pvsyst_nNsVth(temp_cell, gamma, cells_in_series) @@ -562,10 +563,10 @@ def _fit_saturation_current(i_sc, v_oc, effective_irradiance, temp_cell, return I_o_ref -def _fit_photocurrent(sm, i_sc, effective_irradiance, temp_cell, - alpha_sc, gamma_ref, mu_gamma, I_o_ref, - R_sh_ref, R_sh_0, R_sh_exp, R_s, - cells_in_series, EgRef): +def _fit_photocurrent_pvsyst_iec61853_sandia( + sm, i_sc, effective_irradiance, temp_cell, alpha_sc, gamma_ref, + mu_gamma, I_o_ref, R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series, + EgRef): R_sh = _pvsyst_Rsh(effective_irradiance, R_sh_ref, R_sh_0, R_sh_exp) gamma = _pvsyst_gamma(temp_cell, gamma_ref, mu_gamma) I_o = _pvsyst_Io(temp_cell, gamma, I_o_ref, EgRef) @@ -582,10 +583,9 @@ def _fit_photocurrent(sm, i_sc, effective_irradiance, temp_cell, return I_L_ref -def _fit_diode_ideality_factor_post(sm, i_mp, v_mp, effective_irradiance, - temp_cell, alpha_sc, I_L_ref, I_o_ref, - R_sh_ref, R_sh_0, R_sh_exp, R_s, - cells_in_series, EgRef): +def _fit_diode_ideality_factor_post_pvsyst_iec61853_sandia( + sm, i_mp, v_mp, effective_irradiance, temp_cell, alpha_sc, I_L_ref, + I_o_ref, R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series, EgRef): Rsh = _pvsyst_Rsh(effective_irradiance, R_sh_ref, R_sh_0, R_sh_exp) I_L = _pvsyst_IL(effective_irradiance, temp_cell, I_L_ref, alpha_sc) From df724bc23b1347e8232313536dc9d736aa794ada Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 9 Apr 2025 09:49:49 -0400 Subject: [PATCH 07/17] add irrad/temp tolerance parameters --- pvlib/ivtools/sdm/pvsyst.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/pvlib/ivtools/sdm/pvsyst.py b/pvlib/ivtools/sdm/pvsyst.py index b5dd6175de..7a3cdef412 100644 --- a/pvlib/ivtools/sdm/pvsyst.py +++ b/pvlib/ivtools/sdm/pvsyst.py @@ -315,7 +315,9 @@ def fit_pvsyst_iec61853_sandia(effective_irradiance, temp_cell, cells_in_series, EgRef=1.121, alpha_sc=None, beta_mp=None, r_sh_coeff=0.12, R_s=None, - min_Rsh_irradiance=None): + min_Rsh_irradiance=None, + irradiance_tolerance=20, + temperature_tolerance=1): """ Estimate parameters for the PVsyst module performance model using IEC 61853-1 matrix measurements. @@ -357,6 +359,14 @@ def fit_pvsyst_iec61853_sandia(effective_irradiance, temp_cell, Irradiance threshold below which values are excluded when estimating shunt resistance parameter values. May be useful for modules with problematic low-light measurements. [W/m²] + irradiance_tolerance : float, default 20 + Tolerance for irradiance variation around the STC value. + The default value corresponds to a +/- 2% interval around the STC + value of 1000 W/m². [W/m²] + temperature_tolerance : float, default 1 + Tolerance for temperature variation around the STC value. + The default value corresponds to a +/- 1 degree interval around the STC + value of 25 degrees. [C] Returns ------- @@ -409,8 +419,10 @@ def fit_pvsyst_iec61853_sandia(effective_irradiance, temp_cell, except ImportError: raise ImportError('fit_pvsyst_iec61853_sandia requires statsmodels') - is_g_stc = effective_irradiance == 1000 - is_t_stc = temp_cell == 25 + is_g_stc = np.isclose(effective_irradiance, 1000, rtol=0, + atol=irradiance_tolerance) + is_t_stc = np.isclose(temp_cell, 25, rtol=0, + atol=temperature_tolerance) if alpha_sc is None: i_sc_ref = float(i_sc[is_g_stc & is_t_stc]) From 283d80f5b97d140dd349db4ff2fa7715bf3f5d51 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 9 Apr 2025 09:49:56 -0400 Subject: [PATCH 08/17] add tolerance tests --- tests/ivtools/sdm/test_pvsyst.py | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/ivtools/sdm/test_pvsyst.py b/tests/ivtools/sdm/test_pvsyst.py index 178e68ab77..43421e0e38 100644 --- a/tests/ivtools/sdm/test_pvsyst.py +++ b/tests/ivtools/sdm/test_pvsyst.py @@ -304,3 +304,36 @@ def test_fit_pvsyst_iec61853_sandia_optional(pvsyst_iec61853_table3, min_Rsh_irradiance=500) assert not np.isclose(fitted_params['R_sh_ref'], expected['R_sh_ref'], atol=0, rtol=1e-3) + + +@requires_statsmodels +def test_fit_pvsyst_iec61853_sandia_tolerance(pvsyst_iec61853_table3, + iec61853_conditions): + # verify that the *_tolerance parameters allow non-"perfect" irradiance + # and temperature values + ee, tc = iec61853_conditions + ee[ee == 1000] = 999 + tc[tc == 25] = 25.1 + case = pvsyst_iec61853_table3['high current'] + true_params = case['true'] + expected = case['estimated'] + sde_params = pvsystem.calcparams_pvsyst(ee, tc, **true_params) + iv = pvsystem.singlediode(*sde_params) + + inputs = dict( + effective_irradiance=ee, temp_cell=tc, i_sc=iv['i_sc'], + v_oc=iv['v_oc'], i_mp=iv['i_mp'], v_mp=iv['v_mp'], + cells_in_series=true_params['cells_in_series'] + ) + fitted_params = sdm.fit_pvsyst_iec61853_sandia(**inputs) + # still get approximately the expected values + for key in expected.keys(): + assert np.isclose(fitted_params[key], expected[key], atol=0, rtol=1e-2) + + # but if the changes exceed the specified tolerance, then error: + with pytest.raises(ValueError, match='Coefficient array is empty'): + sdm.fit_pvsyst_iec61853_sandia(**inputs, irradiance_tolerance=0.1) + + with pytest.raises(ValueError, match='can only convert an array of size 1'): + sdm.fit_pvsyst_iec61853_sandia(**inputs, temperature_tolerance=0.1) + From 400f2c1ded7e3c28a284b1206dd0798b0c562aeb Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 9 Apr 2025 09:50:11 -0400 Subject: [PATCH 09/17] fix a couple warnings --- pvlib/ivtools/sdm/pvsyst.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/ivtools/sdm/pvsyst.py b/pvlib/ivtools/sdm/pvsyst.py index 7a3cdef412..d5e78e764b 100644 --- a/pvlib/ivtools/sdm/pvsyst.py +++ b/pvlib/ivtools/sdm/pvsyst.py @@ -425,9 +425,9 @@ def fit_pvsyst_iec61853_sandia(effective_irradiance, temp_cell, atol=temperature_tolerance) if alpha_sc is None: - i_sc_ref = float(i_sc[is_g_stc & is_t_stc]) mu_i_sc = _fit_tempco_pvsyst_iec61853_sandia(i_sc[is_g_stc], temp_cell[is_g_stc]) + i_sc_ref = float(i_sc[is_g_stc & is_t_stc].item()) alpha_sc = mu_i_sc * i_sc_ref if beta_mp is None: @@ -553,7 +553,7 @@ def _fit_diode_ideality_factor_pvsyst_iec61853_sandia( y = v_mp + i_mp*R_s - v_oc results = sm.OLS(endog=y, exog=x).fit() - gamma_ref = results.params[0] + gamma_ref = results.params['x1'] return gamma_ref, 0 From a143fa5e944b492cf264ac0d885f735ad8ae7e7b Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 9 Apr 2025 09:58:01 -0400 Subject: [PATCH 10/17] lint --- tests/ivtools/sdm/test_pvsyst.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ivtools/sdm/test_pvsyst.py b/tests/ivtools/sdm/test_pvsyst.py index 43421e0e38..19fc9ddbec 100644 --- a/tests/ivtools/sdm/test_pvsyst.py +++ b/tests/ivtools/sdm/test_pvsyst.py @@ -334,6 +334,6 @@ def test_fit_pvsyst_iec61853_sandia_tolerance(pvsyst_iec61853_table3, with pytest.raises(ValueError, match='Coefficient array is empty'): sdm.fit_pvsyst_iec61853_sandia(**inputs, irradiance_tolerance=0.1) - with pytest.raises(ValueError, match='can only convert an array of size 1'): + with pytest.raises(ValueError, + match='can only convert an array of size 1'): sdm.fit_pvsyst_iec61853_sandia(**inputs, temperature_tolerance=0.1) - From 8e353122d24b5434275f77fb36f1d2f27d8ffea9 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 10 Apr 2025 08:25:48 -0400 Subject: [PATCH 11/17] change order of optional parameters --- pvlib/ivtools/sdm/pvsyst.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pvlib/ivtools/sdm/pvsyst.py b/pvlib/ivtools/sdm/pvsyst.py index d5e78e764b..ef3173c0bf 100644 --- a/pvlib/ivtools/sdm/pvsyst.py +++ b/pvlib/ivtools/sdm/pvsyst.py @@ -314,7 +314,7 @@ def fit_pvsyst_iec61853_sandia(effective_irradiance, temp_cell, i_sc, v_oc, i_mp, v_mp, cells_in_series, EgRef=1.121, alpha_sc=None, beta_mp=None, - r_sh_coeff=0.12, R_s=None, + R_s=None, r_sh_coeff=0.12, min_Rsh_irradiance=None, irradiance_tolerance=20, temperature_tolerance=1): @@ -349,12 +349,12 @@ def fit_pvsyst_iec61853_sandia(effective_irradiance, temp_cell, Temperature coefficient of maximum power voltage. If not specified, it will be estimated using the ``v_mp`` values at irradiance of 1000 W/m2. [1/K] - r_sh_coeff : float, default 0.12 - Shunt resistance fitting coefficient. The default value is taken - from [1]_. R_s : float, optional Series resistance value. If not provided, a value will be estimated from the input measurements. [ohm] + r_sh_coeff : float, default 0.12 + Shunt resistance fitting coefficient. The default value is taken + from [1]_. min_Rsh_irradiance : float, optional Irradiance threshold below which values are excluded when estimating shunt resistance parameter values. May be useful for modules From bb86a53345fd7252036b2fcaea864dbac3b99882 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 10 Apr 2025 11:52:36 -0400 Subject: [PATCH 12/17] replace statsmodels with np.linalg.lstsq --- pvlib/ivtools/sdm/pvsyst.py | 44 +++++++++++++------------------- tests/ivtools/sdm/test_pvsyst.py | 3 --- 2 files changed, 18 insertions(+), 29 deletions(-) diff --git a/pvlib/ivtools/sdm/pvsyst.py b/pvlib/ivtools/sdm/pvsyst.py index ef3173c0bf..2399faf921 100644 --- a/pvlib/ivtools/sdm/pvsyst.py +++ b/pvlib/ivtools/sdm/pvsyst.py @@ -414,11 +414,6 @@ def fit_pvsyst_iec61853_sandia(effective_irradiance, temp_cell, of Photovoltaics, vol. 15, 3, 2025. :doi:`10.1109/JPHOTOV.2025.3554338` """ - try: - import statsmodels.api as sm - except ImportError: - raise ImportError('fit_pvsyst_iec61853_sandia requires statsmodels') - is_g_stc = np.isclose(effective_irradiance, 1000, rtol=0, atol=irradiance_tolerance) is_t_stc = np.isclose(temp_cell, 25, rtol=0, @@ -440,11 +435,10 @@ def fit_pvsyst_iec61853_sandia(effective_irradiance, temp_cell, ) if R_s is None: - R_s = _fit_series_resistance_pvsyst_iec61853_sandia(sm, v_oc, - i_mp, v_mp) + R_s = _fit_series_resistance_pvsyst_iec61853_sandia(v_oc, i_mp, v_mp) gamma_ref, mu_gamma = _fit_diode_ideality_factor_pvsyst_iec61853_sandia( - sm, i_sc[is_t_stc], v_oc[is_t_stc], i_mp[is_t_stc], v_mp[is_t_stc], + i_sc[is_t_stc], v_oc[is_t_stc], i_mp[is_t_stc], v_mp[is_t_stc], effective_irradiance[is_t_stc], temp_cell[is_t_stc], R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series ) @@ -455,14 +449,14 @@ def fit_pvsyst_iec61853_sandia(effective_irradiance, temp_cell, ) I_L_ref = _fit_photocurrent_pvsyst_iec61853_sandia( - sm, i_sc, effective_irradiance, temp_cell, alpha_sc, + i_sc, effective_irradiance, temp_cell, alpha_sc, gamma_ref, mu_gamma, I_o_ref, R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series, EgRef ) gamma_ref, mu_gamma = \ _fit_diode_ideality_factor_post_pvsyst_iec61853_sandia( - sm, i_mp, v_mp, effective_irradiance, temp_cell, alpha_sc, I_L_ref, + i_mp, v_mp, effective_irradiance, temp_cell, alpha_sc, I_L_ref, I_o_ref, R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series, EgRef) fitted_params = dict( @@ -524,21 +518,20 @@ def _fit_shunt_resistances_pvsyst_iec61853_sandia( return Rshref, Rsh0, Rshexp -def _fit_series_resistance_pvsyst_iec61853_sandia(sm, v_oc, i_mp, v_mp): +def _fit_series_resistance_pvsyst_iec61853_sandia(v_oc, i_mp, v_mp): # Stein et al 2014, https://doi.org/10.1109/PVSC.2014.6925326 # Eq 13 - x = np.array([i_mp, np.log(i_mp), v_mp]).T - x = sm.add_constant(x) + x = np.array([np.ones(len(i_mp)), i_mp, np.log(i_mp), v_mp]).T y = v_oc - results = sm.OLS(endog=y, exog=x).fit() - R_s = results.params['x1'] + coeff, _, _, _ = np.linalg.lstsq(x, y, rcond=None) + R_s = coeff[1] return R_s def _fit_diode_ideality_factor_pvsyst_iec61853_sandia( - sm, i_sc, v_oc, i_mp, v_mp, effective_irradiance, temp_cell, + i_sc, v_oc, i_mp, v_mp, effective_irradiance, temp_cell, R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series): NsVth = _pvsyst_nNsVth(temp_cell, gamma=1, cells_in_series=cells_in_series) @@ -552,8 +545,8 @@ def _fit_diode_ideality_factor_pvsyst_iec61853_sandia( x = np.array([x1]).T y = v_mp + i_mp*R_s - v_oc - results = sm.OLS(endog=y, exog=x).fit() - gamma_ref = results.params['x1'] + coeff, _, _, _ = np.linalg.lstsq(x, y, rcond=None) + gamma_ref = coeff[0] return gamma_ref, 0 @@ -576,7 +569,7 @@ def _fit_saturation_current_pvsyst_iec61853_sandia( def _fit_photocurrent_pvsyst_iec61853_sandia( - sm, i_sc, effective_irradiance, temp_cell, alpha_sc, gamma_ref, + i_sc, effective_irradiance, temp_cell, alpha_sc, gamma_ref, mu_gamma, I_o_ref, R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series, EgRef): R_sh = _pvsyst_Rsh(effective_irradiance, R_sh_ref, R_sh_0, R_sh_exp) @@ -590,13 +583,13 @@ def _fit_photocurrent_pvsyst_iec61853_sandia( # Eq 20 x = np.array([effective_irradiance / 1000]).T y = I_L_est - effective_irradiance / 1000 * alpha_sc * (temp_cell - 25) - results = sm.OLS(endog=y, exog=x).fit() - I_L_ref = results.params['x1'] + coeff, _, _, _ = np.linalg.lstsq(x, y, rcond=None) + I_L_ref = coeff[0] return I_L_ref def _fit_diode_ideality_factor_post_pvsyst_iec61853_sandia( - sm, i_mp, v_mp, effective_irradiance, temp_cell, alpha_sc, I_L_ref, + i_mp, v_mp, effective_irradiance, temp_cell, alpha_sc, I_L_ref, I_o_ref, R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series, EgRef): Rsh = _pvsyst_Rsh(effective_irradiance, R_sh_ref, R_sh_0, R_sh_exp) @@ -620,10 +613,9 @@ def _fit_diode_ideality_factor_post_pvsyst_iec61853_sandia( gamma_est = numerator / denominator # Eq 22 - x = np.array([temp_cell - 25]).T - x = sm.add_constant(x) + x = np.array([np.ones(len(i_mp)), temp_cell - 25]).T y = gamma_est - results = sm.OLS(endog=y, exog=x).fit() - gamma_ref, mu_gamma = results.params + coeff, _, _, _ = np.linalg.lstsq(x, y, rcond=None) + gamma_ref, mu_gamma = coeff return gamma_ref, mu_gamma diff --git a/tests/ivtools/sdm/test_pvsyst.py b/tests/ivtools/sdm/test_pvsyst.py index 19fc9ddbec..7d2d7797c9 100644 --- a/tests/ivtools/sdm/test_pvsyst.py +++ b/tests/ivtools/sdm/test_pvsyst.py @@ -238,7 +238,6 @@ def iec61853_conditions(): return ee, tc -@requires_statsmodels def test_fit_pvsyst_iec61853_sandia(pvsyst_iec61853_table3, iec61853_conditions): ee, tc = iec61853_conditions @@ -258,7 +257,6 @@ def test_fit_pvsyst_iec61853_sandia(pvsyst_iec61853_table3, atol=0, rtol=1e-3) -@requires_statsmodels def test_fit_pvsyst_iec61853_sandia_optional(pvsyst_iec61853_table3, iec61853_conditions): # verify that modifying each optional parameter results in different output @@ -306,7 +304,6 @@ def test_fit_pvsyst_iec61853_sandia_optional(pvsyst_iec61853_table3, atol=0, rtol=1e-3) -@requires_statsmodels def test_fit_pvsyst_iec61853_sandia_tolerance(pvsyst_iec61853_table3, iec61853_conditions): # verify that the *_tolerance parameters allow non-"perfect" irradiance From fe146b61132b4330ead5b3c3444d0ae9421fe996 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 10 Apr 2025 12:12:55 -0400 Subject: [PATCH 13/17] add _2025 to function name --- .../reference/pv_modeling/parameters.rst | 2 +- docs/sphinx/source/whatsnew/v0.12.1.rst | 2 +- pvlib/ivtools/sdm/__init__.py | 2 +- pvlib/ivtools/sdm/pvsyst.py | 64 ++++++++++--------- tests/ivtools/sdm/test_pvsyst.py | 45 ++++++------- 5 files changed, 60 insertions(+), 55 deletions(-) diff --git a/docs/sphinx/source/reference/pv_modeling/parameters.rst b/docs/sphinx/source/reference/pv_modeling/parameters.rst index 69e8ba942f..1f146eaccc 100644 --- a/docs/sphinx/source/reference/pv_modeling/parameters.rst +++ b/docs/sphinx/source/reference/pv_modeling/parameters.rst @@ -12,7 +12,7 @@ Functions for fitting single diode models ivtools.sdm.fit_cec_sam ivtools.sdm.fit_desoto ivtools.sdm.fit_pvsyst_sandia - ivtools.sdm.fit_pvsyst_iec61853_sandia + ivtools.sdm.fit_pvsyst_iec61853_sandia_2025 ivtools.sdm.fit_desoto_sandia Functions for fitting the single diode equation diff --git a/docs/sphinx/source/whatsnew/v0.12.1.rst b/docs/sphinx/source/whatsnew/v0.12.1.rst index a58d792471..b7fffd5996 100644 --- a/docs/sphinx/source/whatsnew/v0.12.1.rst +++ b/docs/sphinx/source/whatsnew/v0.12.1.rst @@ -16,7 +16,7 @@ Enhancements ~~~~~~~~~~~~ * :py:mod:`pvlib.ivtools.sdm` is now a subpackage. (:issue:`2252`, :pull:`2256`) * Add a function for estimating PVsyst SDM parameters from IEC 61853-1 matrix - data (:py:func:`~pvlib.ivtools.sdm.fit_pvsyst_iec61853_sandia`). (:issue:`2185`, :pull:`2429`) + data (:py:func:`~pvlib.ivtools.sdm.fit_pvsyst_iec61853_sandia_2025`). (:issue:`2185`, :pull:`2429`) Documentation ~~~~~~~~~~~~~ diff --git a/pvlib/ivtools/sdm/__init__.py b/pvlib/ivtools/sdm/__init__.py index d0b658c198..8535f1f5e6 100644 --- a/pvlib/ivtools/sdm/__init__.py +++ b/pvlib/ivtools/sdm/__init__.py @@ -15,6 +15,6 @@ from pvlib.ivtools.sdm.pvsyst import ( # noqa: F401 fit_pvsyst_sandia, - fit_pvsyst_iec61853_sandia, + fit_pvsyst_iec61853_sandia_2025, pvsyst_temperature_coeff, ) diff --git a/pvlib/ivtools/sdm/pvsyst.py b/pvlib/ivtools/sdm/pvsyst.py index 2399faf921..5be4654200 100644 --- a/pvlib/ivtools/sdm/pvsyst.py +++ b/pvlib/ivtools/sdm/pvsyst.py @@ -310,14 +310,14 @@ def maxp(temp_cell, irrad_ref, alpha_sc, gamma_ref, mu_gamma, I_L_ref, return gamma_pdc / pmp -def fit_pvsyst_iec61853_sandia(effective_irradiance, temp_cell, - i_sc, v_oc, i_mp, v_mp, - cells_in_series, EgRef=1.121, - alpha_sc=None, beta_mp=None, - R_s=None, r_sh_coeff=0.12, - min_Rsh_irradiance=None, - irradiance_tolerance=20, - temperature_tolerance=1): +def fit_pvsyst_iec61853_sandia_2025(effective_irradiance, temp_cell, + i_sc, v_oc, i_mp, v_mp, + cells_in_series, EgRef=1.121, + alpha_sc=None, beta_mp=None, + R_s=None, r_sh_coeff=0.12, + min_Rsh_irradiance=None, + irradiance_tolerance=20, + temperature_tolerance=1): """ Estimate parameters for the PVsyst module performance model using IEC 61853-1 matrix measurements. @@ -420,42 +420,45 @@ def fit_pvsyst_iec61853_sandia(effective_irradiance, temp_cell, atol=temperature_tolerance) if alpha_sc is None: - mu_i_sc = _fit_tempco_pvsyst_iec61853_sandia(i_sc[is_g_stc], - temp_cell[is_g_stc]) + mu_i_sc = _fit_tempco_pvsyst_iec61853_sandia_2025(i_sc[is_g_stc], + temp_cell[is_g_stc]) i_sc_ref = float(i_sc[is_g_stc & is_t_stc].item()) alpha_sc = mu_i_sc * i_sc_ref if beta_mp is None: - beta_mp = _fit_tempco_pvsyst_iec61853_sandia(v_mp[is_g_stc], - temp_cell[is_g_stc]) + beta_mp = _fit_tempco_pvsyst_iec61853_sandia_2025(v_mp[is_g_stc], + temp_cell[is_g_stc]) - R_sh_ref, R_sh_0, R_sh_exp = _fit_shunt_resistances_pvsyst_iec61853_sandia( - i_sc, i_mp, v_mp, effective_irradiance, temp_cell, beta_mp, - coeff=r_sh_coeff, min_irradiance=min_Rsh_irradiance + R_sh_ref, R_sh_0, R_sh_exp = \ + _fit_shunt_resistances_pvsyst_iec61853_sandia_2025( + i_sc, i_mp, v_mp, effective_irradiance, temp_cell, beta_mp, + coeff=r_sh_coeff, min_irradiance=min_Rsh_irradiance ) if R_s is None: - R_s = _fit_series_resistance_pvsyst_iec61853_sandia(v_oc, i_mp, v_mp) + R_s = _fit_series_resistance_pvsyst_iec61853_sandia_2025(v_oc, i_mp, + v_mp) - gamma_ref, mu_gamma = _fit_diode_ideality_factor_pvsyst_iec61853_sandia( - i_sc[is_t_stc], v_oc[is_t_stc], i_mp[is_t_stc], v_mp[is_t_stc], - effective_irradiance[is_t_stc], temp_cell[is_t_stc], - R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series + gamma_ref, mu_gamma = \ + _fit_diode_ideality_factor_pvsyst_iec61853_sandia_2025( + i_sc[is_t_stc], v_oc[is_t_stc], i_mp[is_t_stc], v_mp[is_t_stc], + effective_irradiance[is_t_stc], temp_cell[is_t_stc], + R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series ) - I_o_ref = _fit_saturation_current_pvsyst_iec61853_sandia( + I_o_ref = _fit_saturation_current_pvsyst_iec61853_sandia_2025( i_sc, v_oc, effective_irradiance, temp_cell, gamma_ref, mu_gamma, R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series, EgRef ) - I_L_ref = _fit_photocurrent_pvsyst_iec61853_sandia( + I_L_ref = _fit_photocurrent_pvsyst_iec61853_sandia_2025( i_sc, effective_irradiance, temp_cell, alpha_sc, gamma_ref, mu_gamma, I_o_ref, R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series, EgRef ) gamma_ref, mu_gamma = \ - _fit_diode_ideality_factor_post_pvsyst_iec61853_sandia( + _fit_diode_ideality_factor_post_pvsyst_iec61853_sandia_2025( i_mp, v_mp, effective_irradiance, temp_cell, alpha_sc, I_L_ref, I_o_ref, R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series, EgRef) @@ -475,14 +478,15 @@ def fit_pvsyst_iec61853_sandia(effective_irradiance, temp_cell, return fitted_params -def _fit_tempco_pvsyst_iec61853_sandia(values, temp_cell, temp_cell_ref=25): +def _fit_tempco_pvsyst_iec61853_sandia_2025(values, temp_cell, + temp_cell_ref=25): fit = np.polynomial.polynomial.Polynomial.fit(temp_cell, values, deg=1) intercept, slope = fit.convert().coef value_ref = intercept + slope*temp_cell_ref return slope / value_ref -def _fit_shunt_resistances_pvsyst_iec61853_sandia( +def _fit_shunt_resistances_pvsyst_iec61853_sandia_2025( i_sc, i_mp, v_mp, effective_irradiance, temp_cell, beta_v_mp, coeff=0.2, min_irradiance=None): if min_irradiance is None: @@ -518,7 +522,7 @@ def _fit_shunt_resistances_pvsyst_iec61853_sandia( return Rshref, Rsh0, Rshexp -def _fit_series_resistance_pvsyst_iec61853_sandia(v_oc, i_mp, v_mp): +def _fit_series_resistance_pvsyst_iec61853_sandia_2025(v_oc, i_mp, v_mp): # Stein et al 2014, https://doi.org/10.1109/PVSC.2014.6925326 # Eq 13 @@ -530,7 +534,7 @@ def _fit_series_resistance_pvsyst_iec61853_sandia(v_oc, i_mp, v_mp): return R_s -def _fit_diode_ideality_factor_pvsyst_iec61853_sandia( +def _fit_diode_ideality_factor_pvsyst_iec61853_sandia_2025( i_sc, v_oc, i_mp, v_mp, effective_irradiance, temp_cell, R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series): @@ -550,7 +554,7 @@ def _fit_diode_ideality_factor_pvsyst_iec61853_sandia( return gamma_ref, 0 -def _fit_saturation_current_pvsyst_iec61853_sandia( +def _fit_saturation_current_pvsyst_iec61853_sandia_2025( i_sc, v_oc, effective_irradiance, temp_cell, gamma_ref, mu_gamma, R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series, EgRef): R_sh = _pvsyst_Rsh(effective_irradiance, R_sh_ref, R_sh_0, R_sh_exp) @@ -568,7 +572,7 @@ def _fit_saturation_current_pvsyst_iec61853_sandia( return I_o_ref -def _fit_photocurrent_pvsyst_iec61853_sandia( +def _fit_photocurrent_pvsyst_iec61853_sandia_2025( i_sc, effective_irradiance, temp_cell, alpha_sc, gamma_ref, mu_gamma, I_o_ref, R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series, EgRef): @@ -588,7 +592,7 @@ def _fit_photocurrent_pvsyst_iec61853_sandia( return I_L_ref -def _fit_diode_ideality_factor_post_pvsyst_iec61853_sandia( +def _fit_diode_ideality_factor_post_pvsyst_iec61853_sandia_2025( i_mp, v_mp, effective_irradiance, temp_cell, alpha_sc, I_L_ref, I_o_ref, R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series, EgRef): diff --git a/tests/ivtools/sdm/test_pvsyst.py b/tests/ivtools/sdm/test_pvsyst.py index 7d2d7797c9..2a1793633b 100644 --- a/tests/ivtools/sdm/test_pvsyst.py +++ b/tests/ivtools/sdm/test_pvsyst.py @@ -238,8 +238,8 @@ def iec61853_conditions(): return ee, tc -def test_fit_pvsyst_iec61853_sandia(pvsyst_iec61853_table3, - iec61853_conditions): +def test_fit_pvsyst_iec61853_sandia_2025(pvsyst_iec61853_table3, + iec61853_conditions): ee, tc = iec61853_conditions for _, case in pvsyst_iec61853_table3.items(): true_params = case['true'] @@ -248,7 +248,7 @@ def test_fit_pvsyst_iec61853_sandia(pvsyst_iec61853_table3, sde_params = pvsystem.calcparams_pvsyst(ee, tc, **true_params) iv = pvsystem.singlediode(*sde_params) - fitted_params = sdm.fit_pvsyst_iec61853_sandia( + fitted_params = sdm.fit_pvsyst_iec61853_sandia_2025( ee, tc, iv['i_sc'], iv['v_oc'], iv['i_mp'], iv['v_mp'], true_params['cells_in_series'], EgRef=true_params['EgRef'], ) @@ -257,8 +257,8 @@ def test_fit_pvsyst_iec61853_sandia(pvsyst_iec61853_table3, atol=0, rtol=1e-3) -def test_fit_pvsyst_iec61853_sandia_optional(pvsyst_iec61853_table3, - iec61853_conditions): +def test_fit_pvsyst_iec61853_sandia_2025_optional(pvsyst_iec61853_table3, + iec61853_conditions): # verify that modifying each optional parameter results in different output ee, tc = iec61853_conditions case = pvsyst_iec61853_table3['high current'] @@ -273,39 +273,39 @@ def test_fit_pvsyst_iec61853_sandia_optional(pvsyst_iec61853_table3, cells_in_series=true_params['cells_in_series'] ) - fitted_params = sdm.fit_pvsyst_iec61853_sandia(**main_inputs, - EgRef=1.0) + fitted_params = sdm.fit_pvsyst_iec61853_sandia_2025(**main_inputs, + EgRef=1.0) assert not np.isclose(fitted_params['I_o_ref'], expected['I_o_ref'], atol=0, rtol=1e-3) - fitted_params = sdm.fit_pvsyst_iec61853_sandia(**main_inputs, - alpha_sc=0.5e-3) + fitted_params = sdm.fit_pvsyst_iec61853_sandia_2025(**main_inputs, + alpha_sc=0.5e-3) assert not np.isclose(fitted_params['alpha_sc'], expected['alpha_sc'], atol=0, rtol=1e-3) - fitted_params = sdm.fit_pvsyst_iec61853_sandia(**main_inputs, - beta_mp=-1e-4) + fitted_params = sdm.fit_pvsyst_iec61853_sandia_2025(**main_inputs, + beta_mp=-1e-4) assert not np.isclose(fitted_params['R_sh_ref'], expected['R_sh_ref'], atol=0, rtol=1e-3) - fitted_params = sdm.fit_pvsyst_iec61853_sandia(**main_inputs, - r_sh_coeff=0.3) + fitted_params = sdm.fit_pvsyst_iec61853_sandia_2025(**main_inputs, + r_sh_coeff=0.3) assert not np.isclose(fitted_params['R_sh_ref'], expected['R_sh_ref'], atol=0, rtol=1e-3) - fitted_params = sdm.fit_pvsyst_iec61853_sandia(**main_inputs, - R_s=0.5) + fitted_params = sdm.fit_pvsyst_iec61853_sandia_2025(**main_inputs, + R_s=0.5) assert not np.isclose(fitted_params['R_s'], expected['R_s'], atol=0, rtol=1e-3) - fitted_params = sdm.fit_pvsyst_iec61853_sandia(**main_inputs, - min_Rsh_irradiance=500) + fitted_params = sdm.fit_pvsyst_iec61853_sandia_2025(**main_inputs, + min_Rsh_irradiance=500) assert not np.isclose(fitted_params['R_sh_ref'], expected['R_sh_ref'], atol=0, rtol=1e-3) -def test_fit_pvsyst_iec61853_sandia_tolerance(pvsyst_iec61853_table3, - iec61853_conditions): +def test_fit_pvsyst_iec61853_sandia_2025_tolerance(pvsyst_iec61853_table3, + iec61853_conditions): # verify that the *_tolerance parameters allow non-"perfect" irradiance # and temperature values ee, tc = iec61853_conditions @@ -322,15 +322,16 @@ def test_fit_pvsyst_iec61853_sandia_tolerance(pvsyst_iec61853_table3, v_oc=iv['v_oc'], i_mp=iv['i_mp'], v_mp=iv['v_mp'], cells_in_series=true_params['cells_in_series'] ) - fitted_params = sdm.fit_pvsyst_iec61853_sandia(**inputs) + fitted_params = sdm.fit_pvsyst_iec61853_sandia_2025(**inputs) # still get approximately the expected values for key in expected.keys(): assert np.isclose(fitted_params[key], expected[key], atol=0, rtol=1e-2) # but if the changes exceed the specified tolerance, then error: with pytest.raises(ValueError, match='Coefficient array is empty'): - sdm.fit_pvsyst_iec61853_sandia(**inputs, irradiance_tolerance=0.1) + sdm.fit_pvsyst_iec61853_sandia_2025(**inputs, irradiance_tolerance=0.1) with pytest.raises(ValueError, match='can only convert an array of size 1'): - sdm.fit_pvsyst_iec61853_sandia(**inputs, temperature_tolerance=0.1) + sdm.fit_pvsyst_iec61853_sandia_2025(**inputs, + temperature_tolerance=0.1) From d34705cff28368c203858dabf4a13976f3e8d11c Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 10 Apr 2025 12:14:57 -0400 Subject: [PATCH 14/17] add note regarding array shape --- pvlib/ivtools/sdm/pvsyst.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pvlib/ivtools/sdm/pvsyst.py b/pvlib/ivtools/sdm/pvsyst.py index 5be4654200..d44c2d3795 100644 --- a/pvlib/ivtools/sdm/pvsyst.py +++ b/pvlib/ivtools/sdm/pvsyst.py @@ -402,6 +402,9 @@ def fit_pvsyst_iec61853_sandia_2025(effective_irradiance, temp_cell, Notes ----- + Input arrays of operating conditions and electrical measurements must be + 1-D with equal lengths. + This method is non-iterative. In some cases, it may be desirable to refine the estimated parameter values using a numerical optimizer like those provided in ``scipy.optimize``. From 6684b2ed43eaacf755e389aa0e68f4ce839cc0a6 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 10 Apr 2025 12:36:32 -0400 Subject: [PATCH 15/17] lint --- pvlib/ivtools/sdm/pvsyst.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pvlib/ivtools/sdm/pvsyst.py b/pvlib/ivtools/sdm/pvsyst.py index d44c2d3795..11886db44b 100644 --- a/pvlib/ivtools/sdm/pvsyst.py +++ b/pvlib/ivtools/sdm/pvsyst.py @@ -404,7 +404,7 @@ def fit_pvsyst_iec61853_sandia_2025(effective_irradiance, temp_cell, ----- Input arrays of operating conditions and electrical measurements must be 1-D with equal lengths. - + This method is non-iterative. In some cases, it may be desirable to refine the estimated parameter values using a numerical optimizer like those provided in ``scipy.optimize``. @@ -435,8 +435,7 @@ def fit_pvsyst_iec61853_sandia_2025(effective_irradiance, temp_cell, R_sh_ref, R_sh_0, R_sh_exp = \ _fit_shunt_resistances_pvsyst_iec61853_sandia_2025( i_sc, i_mp, v_mp, effective_irradiance, temp_cell, beta_mp, - coeff=r_sh_coeff, min_irradiance=min_Rsh_irradiance - ) + coeff=r_sh_coeff, min_irradiance=min_Rsh_irradiance) if R_s is None: R_s = _fit_series_resistance_pvsyst_iec61853_sandia_2025(v_oc, i_mp, @@ -446,8 +445,7 @@ def fit_pvsyst_iec61853_sandia_2025(effective_irradiance, temp_cell, _fit_diode_ideality_factor_pvsyst_iec61853_sandia_2025( i_sc[is_t_stc], v_oc[is_t_stc], i_mp[is_t_stc], v_mp[is_t_stc], effective_irradiance[is_t_stc], temp_cell[is_t_stc], - R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series - ) + R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series) I_o_ref = _fit_saturation_current_pvsyst_iec61853_sandia_2025( i_sc, v_oc, effective_irradiance, temp_cell, gamma_ref, mu_gamma, From 773e809aeb3441a619d19c92d6844778fa849720 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 10 Apr 2025 13:11:37 -0400 Subject: [PATCH 16/17] Update pvsyst.py --- pvlib/ivtools/sdm/pvsyst.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/ivtools/sdm/pvsyst.py b/pvlib/ivtools/sdm/pvsyst.py index 11886db44b..3e5a485fa8 100644 --- a/pvlib/ivtools/sdm/pvsyst.py +++ b/pvlib/ivtools/sdm/pvsyst.py @@ -406,8 +406,8 @@ def fit_pvsyst_iec61853_sandia_2025(effective_irradiance, temp_cell, 1-D with equal lengths. This method is non-iterative. In some cases, it may be desirable to - refine the estimated parameter values using a numerical optimizer like - those provided in ``scipy.optimize``. + refine the estimated parameter values using a numerical optimizer such as + the default method in ``scipy.optimize.minimize``. References ---------- From 7e64e8eead3cce187182a884199d2f5e6bcb9d62 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 11 Apr 2025 08:22:26 -0400 Subject: [PATCH 17/17] add note regarding temp coeff consistency --- pvlib/ivtools/sdm/pvsyst.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pvlib/ivtools/sdm/pvsyst.py b/pvlib/ivtools/sdm/pvsyst.py index 3e5a485fa8..7e1f8c71ab 100644 --- a/pvlib/ivtools/sdm/pvsyst.py +++ b/pvlib/ivtools/sdm/pvsyst.py @@ -405,6 +405,10 @@ def fit_pvsyst_iec61853_sandia_2025(effective_irradiance, temp_cell, Input arrays of operating conditions and electrical measurements must be 1-D with equal lengths. + Values supplied for ``alpha_sc``, ``beta_mp``, and ``R_s`` must be + consistent with the matrix data, as these values are used when estimating + other model parameters. + This method is non-iterative. In some cases, it may be desirable to refine the estimated parameter values using a numerical optimizer such as the default method in ``scipy.optimize.minimize``.