From 0b147e83f07fcf01bcbcd5b6f5f88b5fc6e485d3 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Mon, 16 Sep 2024 10:57:40 -0700 Subject: [PATCH 01/25] functions and tests --- pvlib/ivtools/sdm.py | 290 +++++++++++++++++++++++++++++++- pvlib/tests/ivtools/test_sdm.py | 47 +++++- 2 files changed, 335 insertions(+), 2 deletions(-) diff --git a/pvlib/ivtools/sdm.py b/pvlib/ivtools/sdm.py index 07bd6e2396..643d261586 100644 --- a/pvlib/ivtools/sdm.py +++ b/pvlib/ivtools/sdm.py @@ -7,12 +7,14 @@ """ import numpy as np +import pandas as pd from scipy import constants from scipy import optimize from scipy.special import lambertw -from pvlib.pvsystem import calcparams_pvsyst, singlediode, v_from_i +from pvlib.pvsystem import (calcparams_pvsyst, calcparams_cec, singlediode, + v_from_i) from pvlib.singlediode import bishop88_mpp from pvlib.ivtools.utils import rectify_iv_curve, _numdiff @@ -24,6 +26,17 @@ CONSTANTS = {'E0': 1000.0, 'T0': 25.0, 'k': constants.k, 'q': constants.e} +IEC61853 = pd.DataFrame( + columns=['effective_irradiance', 'temp_cell'], + data = np.array( + [[100, 100, 100, 100, 200, 200, 200, 200, 400, 400, 400, 400, + 600, 600, 600, 600, 800, 800, 800, 800, 1000, 1000, 1000, 1000, + 1100, 1100, 1100, 1100], + [15, 25, 50, 75, 15, 25, 50, 75, 15, 25, 50, 75, 15, 25, 50, 75, + 15, 25, 50, 75, 15, 25, 50, 75, 15, 25, 50, 75]]).T, + dtype=np.float64) + + def fit_cec_sam(celltype, v_mp, i_mp, v_oc, i_sc, alpha_sc, beta_voc, gamma_pmp, cells_in_series, temp_ref=25): """ @@ -1354,3 +1367,278 @@ 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 _pvsyst_objfun(pvs_mod, cec_ivs, ee, tc, cs): + + # translate the guess into named args that are used in the functions + # order : [alpha_sc, gamma_ref, mu_gamma, I_L_ref, I_o_ref, + # R_sh_mult, R_sh_ref, R_s] + # cec_ivs : DataFrame with columns i_sc, v_oc, i_mp, v_mp, p_mp + # ee : effective irradiance + # tc : cell temperature + # cs : cells in series + alpha_sc = pvs_mod[0] + gamma_ref = pvs_mod[1] + mu_gamma = pvs_mod[2] + I_L_ref = pvs_mod[3] + I_o_ref = pvs_mod[4] + R_sh_mult = pvs_mod[5] + R_sh_ref = pvs_mod[6] + R_s = pvs_mod[7] + + R_sh_0 = R_sh_ref * R_sh_mult + + pvs_params = calcparams_pvsyst( + ee, tc, alpha_sc, gamma_ref, mu_gamma, I_L_ref, I_o_ref, R_sh_ref, + R_sh_0, R_s, cs) + + pvsyst_ivs = singlediode(*pvs_params) + + isc_diff = np.abs((pvsyst_ivs['i_sc'] - cec_ivs['i_sc']) / + cec_ivs['i_sc']).mean() + imp_diff = np.abs((pvsyst_ivs['i_mp'] - cec_ivs['i_mp']) / + cec_ivs['i_mp']).mean() + voc_diff = np.abs((pvsyst_ivs['v_oc'] - cec_ivs['v_oc']) / + cec_ivs['v_oc']).mean() + vmp_diff = np.abs((pvsyst_ivs['v_mp'] - cec_ivs['v_mp']) / + cec_ivs['v_mp']).mean() + pmp_diff = np.abs((pvsyst_ivs['p_mp'] - cec_ivs['p_mp']) / + cec_ivs['p_mp']).mean() + + mean_abs_diff = (isc_diff + imp_diff + voc_diff + vmp_diff + pmp_diff) / 5 + + return mean_abs_diff + + +def convert_cec_pvsyst(cec_model, cells_in_series): + r""" + Convert a CEC model to a PVsyst model. + + Uses optimization to fit the PVsyst model to :math:`I_{sc}`, + :math:`V_{oc}`, :math:`V_{mp}`, :math:`I_{mp}`, and :math:`P_{mp}`, + calculated using the input CEC model at the IEC 61853-3 conditions [2]_. + + Parameters + ---------- + cec_model : dict or DataFrame + Must include keys: 'alpha_sc', 'a_ref', 'I_L_ref', 'I_o_ref', + 'R_sh_ref', 'R_s', 'Adjust' + cell_in_series : int + Number of cells in series. + + Returns + ------- + dict with the following elements: + alpha_sc : float + Short-circuit current temperature coefficient [A/C] . + I_L_ref : float + The light-generated current (or photocurrent) at reference + conditions [A]. + I_o_ref : float + The dark or diode reverse saturation current at reference + conditions [A]. + EgRef : float + The energy bandgap at reference temperature [eV]. + R_s : float + The series resistance at reference conditions [ohm]. + R_sh_ref : float + The shunt resistance at reference conditions [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 [unitless]. + gamma_ref : float + Diode (ideality) factor at reference conditions [unitless]. + mu_gamma : float + Temperature coefficient for diode (ideality) factor at reference + conditions [1/K]. + cells_in_series : int + Number of cells in series. + + Notes + ----- + Reference conditions are irradiance of 1000 W/m⁻² and cell temperature of + 25 °C. + + References + ---------- + .. [1] L. Deville et al., "Parameter Translation for Photovoltaic Single + Diode Models", submitted. 2024 + + .. [2] "IEC 61853-3 Photovoltaic (PV) module performance testing and energy + rating - Part 3: Energy rating of PV modules". IEC, Geneva, 2018. + """ + + # calculate target IV curve values + cec_params = calcparams_cec( + IEC61853['effective_irradiance'], + IEC61853['temp_cell'], + **cec_model) + cec_ivs = singlediode(*cec_params) + + # initial guess at PVsyst parameters + # Order in list is alpha_sc, gamma_ref, mu_gamma, I_L_ref, I_o_ref, + # Rsh_mult = R_sh_0 / R_sh_ref, R_sh_ref, R_s + initial = [0, 1.2, 0.001, cec_model['I_L_ref'], cec_model['I_o_ref'], + 12, 1000, cec_model['R_s']] + + # bounds for PVsyst parameters + b_alpha = (-1, 1) + b_gamma = (1, 2) + b_mu = (-1, 1) + b_IL = (1e-12, 100) + b_Io = (1e-24, 0.1) + b_Rmult = (1, 20) + b_Rsh = (100, 1e6) + b_Rs = (1e-12, 10) + bounds = [b_alpha, b_gamma, b_mu, b_IL, b_Io, b_Rmult, b_Rsh, b_Rs] + + # optimization to find PVsyst parameters + result = optimize.minimize( + _pvsyst_objfun, initial, + args=(cec_ivs, IEC61853['effective_irradiance'], + IEC61853['temp_cell'], cells_in_series), + method='Nelder-Mead', bounds=bounds, + options={'maxiter': 5000, 'maxfev': 5000, 'xatol': 0.001}) + alpha_sc, gamma, mu_gamma, I_L_ref, I_o_ref, Rsh_mult, R_sh_ref, R_s = \ + result.x + + R_sh_0 = Rsh_mult * R_sh_ref + R_sh_exp = 5.5 + EgRef = 1.121 # default for all modules in the CEC model + return {'alpha_sc': alpha_sc, + 'I_L_ref': I_L_ref, 'I_o_ref': I_o_ref, 'EgRef': EgRef, 'R_s': R_s, + 'R_sh_ref': R_sh_ref, 'R_sh_0': R_sh_0, 'R_sh_exp': R_sh_exp, + 'gamma_ref': gamma, 'mu_gamma': mu_gamma, + 'cells_in_series': cells_in_series, + } + + +def _cec_objfun(cec_mod, pvs_ivs, ee, tc, alpha_sc): + # translate the guess into named args that are used in the functions + # order : [I_L_ref, I_o_ref, a_ref, R_sh_ref, R_s, alpha_sc, Adjust] + # pvs_ivs : DataFrame with columns i_sc, v_oc, i_mp, v_mp, p_mp + # ee : effective irradiance + # tc : cell temperature + # alpha_sc : temperature coefficient for Isc + I_L_ref = cec_mod[0] + I_o_ref = cec_mod[1] + a_ref = cec_mod[2] + R_sh_ref = cec_mod[3] + R_s = cec_mod[4] + Adjust = cec_mod[5] + alpha_sc = alpha_sc + + cec_params = calcparams_cec( + ee, tc, alpha_sc, a_ref, I_L_ref, I_o_ref, R_sh_ref, R_s, Adjust) + cec_ivs = singlediode(*cec_params) + + isc_rss = np.sqrt(sum((cec_ivs['i_sc'] - pvs_ivs['i_sc'])**2)) + imp_rss = np.sqrt(sum((cec_ivs['i_mp'] - pvs_ivs['i_mp'])**2)) + voc_rss = np.sqrt(sum((cec_ivs['v_oc'] - pvs_ivs['v_oc'])**2)) + vmp_rss = np.sqrt(sum((cec_ivs['v_mp'] - pvs_ivs['v_mp'])**2)) + pmp_rss = np.sqrt(sum((cec_ivs['p_mp'] - pvs_ivs['p_mp'])**2)) + + mean_diff = (isc_rss+imp_rss+voc_rss+vmp_rss+pmp_rss) / 5 + + return mean_diff + + +def convert_pvsyst_cec(pvsyst_model): + r""" + Convert a PVsyst model to a CEC model. + + Uses optimization to fit the CEC model to :math:`I_{sc}`, + :math:`V_{oc}`, :math:`V_{mp}`, :math:`I_{mp}`, and :math:`P_{mp}`, + calculated using the input PVsyst model at the IEC 61853-3 conditions [2]_. + + Parameters + ---------- + cec_model : dict or DataFrame + Must include keys: 'alpha_sc', 'I_L_ref', 'I_o_ref', 'EgRef', 'R_s', + 'R_sh_ref', 'R_sh_0', 'R_sh_exp', 'gamma_ref', 'mu_gamma', + 'cells_in_series' + + Returns + ------- + dict with the following elements: + I_L_ref : float + The light-generated current (or photocurrent) at reference + conditions [A]. + I_o_ref : float + The dark or diode reverse saturation current at reference + conditions [A]. + R_s : float + The series resistance at reference conditions [ohm]. + R_sh_ref : float + The shunt resistance at reference conditions [ohm]. + a_ref : float + The product of the usual diode ideality factor ``n`` (unitless), + number of cells in series ``Ns``, and cell thermal voltage at + reference conditions [V]. + Adjust : float + The adjustment to the temperature coefficient for short circuit + current, in percent. + EgRef : float + The energy bandgap at reference temperature [eV]. + dEgdT : float + The temperature dependence of the energy bandgap at reference + conditions [1/K]. + + Notes + ----- + Reference conditions are irradiance of 1000 W/m⁻² and cell temperature of + 25 °C. + + References + ---------- + .. [1] L. Deville et al., "Parameter Translation for Photovoltaic Single + Diode Models", submitted. 2024. + + .. [2] "IEC 61853-3 Photovoltaic (PV) module performance testing and energy + rating - Part 3: Energy rating of PV modules". IEC, Geneva, 2018. + """ + # calculate target IV curve values + pvs_params = calcparams_pvsyst( + IEC61853['effective_irradiance'], + IEC61853['temp_cell'], + **pvsyst_model) + pvsyst_ivs = singlediode(*pvs_params) + + # set EgRef and dEgdT to CEC defaults + EgRef = 1.121 + dEgdT = -0.0002677 + + # initial guess + # order must match _pvsyst_objfun + # order : [I_L_ref, I_o_ref, a_ref, R_sh_ref, R_s, alpha_sc, Adjust] + nNsVth = pvsyst_model['gamma_ref'] * pvsyst_model['cells_in_series'] \ + * 0.025 + initial = [pvsyst_model['I_L_ref'], pvsyst_model['I_o_ref'], + nNsVth, pvsyst_model['R_sh_ref'], pvsyst_model['R_s'], + 0] + + # bounds for PVsyst parameters + b_IL = (1e-12, 100) + b_Io = (1e-24, 0.1) + b_aref = (1e-12, 1000) + b_Rsh = (100, 1e6) + b_Rs = (1e-12, 10) + b_Adjust = (-100, 100) + bounds = [b_IL, b_Io, b_aref, b_Rsh, b_Rs, b_Adjust] + + result = optimize.minimize( + _cec_objfun, initial, + args=(pvsyst_ivs, IEC61853['effective_irradiance'], + IEC61853['temp_cell'], pvsyst_model['alpha_sc']), + method='Nelder-Mead', bounds=bounds, + options={'maxiter': 5000, 'maxfev': 5000, 'xatol': 0.001}) + I_L_ref, I_o_ref, a_ref, R_sh_ref, R_s, Adjust = result.x + + return {'alpha_sc': pvsyst_model['alpha_sc'], + 'a_ref': a_ref, 'I_L_ref': I_L_ref, 'I_o_ref': I_o_ref, + 'R_sh_ref': R_sh_ref, 'R_s': R_s, 'Adjust': Adjust, + 'EgRef': EgRef, 'dEgdT': dEgdT + } diff --git a/pvlib/tests/ivtools/test_sdm.py b/pvlib/tests/ivtools/test_sdm.py index d4cc7db141..ae28a0045d 100644 --- a/pvlib/tests/ivtools/test_sdm.py +++ b/pvlib/tests/ivtools/test_sdm.py @@ -6,7 +6,6 @@ from pvlib.ivtools import sdm from pvlib import pvsystem -from pvlib._deprecation import pvlibDeprecationWarning from pvlib.tests.conftest import requires_pysam, requires_statsmodels @@ -405,3 +404,49 @@ 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) + + +def test_convert_cec_pvsyst(): + cells_in_series = 66 + trina660_cec = {'I_L_ref': 18.4759, 'I_o_ref': 5.31e-12, + 'EgRef': 1.121, 'dEgdT': -0.0002677, + 'R_s': 0.159916, 'R_sh_ref': 113.991, 'a_ref': 1.59068, + 'Adjust': 6.42247, 'alpha_sc': 0.00629} + trina660_pvsyst_est = sdm.convert_cec_pvsyst(trina660_cec, + cells_in_series) + pvsyst_expected = {'alpha_sc': 0.007478218748188788, + 'I_L_ref': 18.227679597516214, + 'I_o_ref': 2.7418999402908e-11, + 'EgRef': 1.121, + 'R_s': 0.16331908293164496, + 'R_sh_ref': 5267.928954454954, + 'R_sh_0': 60171.206687871425, + 'R_sh_exp': 5.5, + 'gamma_ref': 1.0, + 'mu_gamma': -6.349173477135307e-05, + 'cells_in_series': 66} + + assert np.all([np.isclose(trina660_pvsyst_est[k], pvsyst_expected[k], + rtol=1e-3) + for k in pvsyst_expected]) + + +def test_convert_pvsyst_cec(): + trina660_pvsyst = {'alpha_sc': 0.0074, 'I_o_ref': 3.3e-11, 'EgRef': 1.121, + 'R_s': 0.156, 'R_sh_ref': 200, 'R_sh_0': 800, + 'R_sh_exp': 5.5, 'gamma_ref': 1.002, 'mu_gamma': 1e-3, + 'cells_in_series': 66} + trina660_cec_est = sdm.convert_pvsyst_cec(trina660_pvsyst) + cec_expected = {'alpha_sc': 0.0074, + 'I_L_ref': 18.05154226834071, + 'I_o_ref': 2.6863417875143392e-14, + 'EgRef': 1.121, + 'dEgdT': -0.0002677, + 'R_s': 0.09436341848926795, + 'a_ref': 1.2954800250731866, + 'Adjust': 0.0011675969492410047, + 'cells_in_series': 66} + + assert np.all([np.isclose(trina660_cec_est[k], cec_expected[k], + rtol=1e-3) + for k in cec_expected]) From 480c01bbb9224e29bbccdc06fba374da718e5d78 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 19 Sep 2024 11:18:19 -0700 Subject: [PATCH 02/25] fix test --- pvlib/ivtools/sdm.py | 4 ++-- pvlib/tests/ivtools/test_sdm.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pvlib/ivtools/sdm.py b/pvlib/ivtools/sdm.py index 643d261586..3a857d599c 100644 --- a/pvlib/ivtools/sdm.py +++ b/pvlib/ivtools/sdm.py @@ -28,7 +28,7 @@ IEC61853 = pd.DataFrame( columns=['effective_irradiance', 'temp_cell'], - data = np.array( + data=np.array( [[100, 100, 100, 100, 200, 200, 200, 200, 400, 400, 400, 400, 600, 600, 600, 600, 800, 800, 800, 800, 1000, 1000, 1000, 1000, 1100, 1100, 1100, 1100], @@ -1556,7 +1556,7 @@ def convert_pvsyst_cec(pvsyst_model): Parameters ---------- - cec_model : dict or DataFrame + pvsyst_model : dict or DataFrame Must include keys: 'alpha_sc', 'I_L_ref', 'I_o_ref', 'EgRef', 'R_s', 'R_sh_ref', 'R_sh_0', 'R_sh_exp', 'gamma_ref', 'mu_gamma', 'cells_in_series' diff --git a/pvlib/tests/ivtools/test_sdm.py b/pvlib/tests/ivtools/test_sdm.py index ae28a0045d..b4fd8ef556 100644 --- a/pvlib/tests/ivtools/test_sdm.py +++ b/pvlib/tests/ivtools/test_sdm.py @@ -432,7 +432,8 @@ def test_convert_cec_pvsyst(): def test_convert_pvsyst_cec(): - trina660_pvsyst = {'alpha_sc': 0.0074, 'I_o_ref': 3.3e-11, 'EgRef': 1.121, + trina660_pvsyst = {'alpha_sc': 0.0074, 'I_L_ref': 18.464391, + 'I_o_ref': 3.3e-11, 'EgRef': 1.121, 'R_s': 0.156, 'R_sh_ref': 200, 'R_sh_0': 800, 'R_sh_exp': 5.5, 'gamma_ref': 1.002, 'mu_gamma': 1e-3, 'cells_in_series': 66} From 22d51ee628b43d9fa1823f34624f3008088abac7 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 19 Sep 2024 11:34:36 -0700 Subject: [PATCH 03/25] build doc pages --- docs/sphinx/source/reference/pv_modeling/parameters.rst | 8 ++++++++ pvlib/ivtools/sdm.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/docs/sphinx/source/reference/pv_modeling/parameters.rst b/docs/sphinx/source/reference/pv_modeling/parameters.rst index 9b1817bd01..6b54cfedfd 100644 --- a/docs/sphinx/source/reference/pv_modeling/parameters.rst +++ b/docs/sphinx/source/reference/pv_modeling/parameters.rst @@ -21,6 +21,14 @@ Functions for fitting the single diode equation ivtools.sde.fit_sandia_simple +Functions for converting between single diode models + +.. autosummary:: + :toctree: ../generated/ + + ivtools.sdm.convert_cec_pvsyst + ivtools.sdm.convert_pvsyst_cec + Utilities for working with IV curve data .. autosummary:: diff --git a/pvlib/ivtools/sdm.py b/pvlib/ivtools/sdm.py index 3a857d599c..8a9a3b247e 100644 --- a/pvlib/ivtools/sdm.py +++ b/pvlib/ivtools/sdm.py @@ -1462,6 +1462,10 @@ def convert_cec_pvsyst(cec_model, cells_in_series): Reference conditions are irradiance of 1000 W/m⁻² and cell temperature of 25 °C. + See Also + -------- + pvlib.ivtools.sdm.convert_pvsyst_cec + References ---------- .. [1] L. Deville et al., "Parameter Translation for Photovoltaic Single @@ -1592,6 +1596,10 @@ def convert_pvsyst_cec(pvsyst_model): Reference conditions are irradiance of 1000 W/m⁻² and cell temperature of 25 °C. + See Also + -------- + pvlib.ivtools.sdm.convert_cec_pvsyst + References ---------- .. [1] L. Deville et al., "Parameter Translation for Photovoltaic Single From d65b57f049d1395b16ca706d7131c090597cae89 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 19 Sep 2024 11:47:25 -0700 Subject: [PATCH 04/25] really fix test --- pvlib/tests/ivtools/test_sdm.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pvlib/tests/ivtools/test_sdm.py b/pvlib/tests/ivtools/test_sdm.py index b4fd8ef556..5c0b4091d8 100644 --- a/pvlib/tests/ivtools/test_sdm.py +++ b/pvlib/tests/ivtools/test_sdm.py @@ -445,8 +445,7 @@ def test_convert_pvsyst_cec(): 'dEgdT': -0.0002677, 'R_s': 0.09436341848926795, 'a_ref': 1.2954800250731866, - 'Adjust': 0.0011675969492410047, - 'cells_in_series': 66} + 'Adjust': 0.0011675969492410047} assert np.all([np.isclose(trina660_cec_est[k], cec_expected[k], rtol=1e-3) From bc01aa78c6a46f04f520a47241408e64becbb143 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Mon, 23 Sep 2024 09:22:26 -0700 Subject: [PATCH 05/25] add kwargs --- pvlib/ivtools/sdm.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/pvlib/ivtools/sdm.py b/pvlib/ivtools/sdm.py index 8a9a3b247e..ac76fbf968 100644 --- a/pvlib/ivtools/sdm.py +++ b/pvlib/ivtools/sdm.py @@ -1411,7 +1411,8 @@ def _pvsyst_objfun(pvs_mod, cec_ivs, ee, tc, cs): return mean_abs_diff -def convert_cec_pvsyst(cec_model, cells_in_series): +def convert_cec_pvsyst(cec_model, cells_in_series, method='Nelder-Mead', + options=None): r""" Convert a CEC model to a PVsyst model. @@ -1426,6 +1427,10 @@ def convert_cec_pvsyst(cec_model, cells_in_series): 'R_sh_ref', 'R_s', 'Adjust' cell_in_series : int Number of cells in series. + method : str, default 'Nelder-Mead' + Method for scipy.optimize.minimize. + options : dict, optional + Solver options passed to scipy.optimize.minimize Returns ------- @@ -1474,6 +1479,8 @@ def convert_cec_pvsyst(cec_model, cells_in_series): .. [2] "IEC 61853-3 Photovoltaic (PV) module performance testing and energy rating - Part 3: Energy rating of PV modules". IEC, Geneva, 2018. """ + if options==None: + options = {'maxiter': 5000, 'maxfev': 5000, 'xatol': 0.001} # calculate target IV curve values cec_params = calcparams_cec( @@ -1504,8 +1511,10 @@ def convert_cec_pvsyst(cec_model, cells_in_series): _pvsyst_objfun, initial, args=(cec_ivs, IEC61853['effective_irradiance'], IEC61853['temp_cell'], cells_in_series), - method='Nelder-Mead', bounds=bounds, - options={'maxiter': 5000, 'maxfev': 5000, 'xatol': 0.001}) + method='Nelder-Mead', + bounds=bounds, + options=options) + alpha_sc, gamma, mu_gamma, I_L_ref, I_o_ref, Rsh_mult, R_sh_ref, R_s = \ result.x @@ -1550,7 +1559,7 @@ def _cec_objfun(cec_mod, pvs_ivs, ee, tc, alpha_sc): return mean_diff -def convert_pvsyst_cec(pvsyst_model): +def convert_pvsyst_cec(pvsyst_model, method='Nelder-Mead', options=None): r""" Convert a PVsyst model to a CEC model. @@ -1564,6 +1573,10 @@ def convert_pvsyst_cec(pvsyst_model): Must include keys: 'alpha_sc', 'I_L_ref', 'I_o_ref', 'EgRef', 'R_s', 'R_sh_ref', 'R_sh_0', 'R_sh_exp', 'gamma_ref', 'mu_gamma', 'cells_in_series' + method : str, default 'Nelder-Mead' + Method for scipy.optimize.minimize. + options : dict, optional + Solver options passed to scipy.optimize.minimize Returns ------- @@ -1608,6 +1621,10 @@ def convert_pvsyst_cec(pvsyst_model): .. [2] "IEC 61853-3 Photovoltaic (PV) module performance testing and energy rating - Part 3: Energy rating of PV modules". IEC, Geneva, 2018. """ + + if options==None: + options = {'maxiter': 5000, 'maxfev': 5000, 'xatol': 0.001} + # calculate target IV curve values pvs_params = calcparams_pvsyst( IEC61853['effective_irradiance'], @@ -1641,8 +1658,10 @@ def convert_pvsyst_cec(pvsyst_model): _cec_objfun, initial, args=(pvsyst_ivs, IEC61853['effective_irradiance'], IEC61853['temp_cell'], pvsyst_model['alpha_sc']), - method='Nelder-Mead', bounds=bounds, - options={'maxiter': 5000, 'maxfev': 5000, 'xatol': 0.001}) + method='Nelder-Mead', + bounds=bounds, + options=options) + I_L_ref, I_o_ref, a_ref, R_sh_ref, R_s, Adjust = result.x return {'alpha_sc': pvsyst_model['alpha_sc'], From ae4346bbb3951f2cd19377af71c3f2faa3cf1cad Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Mon, 23 Sep 2024 10:14:04 -0700 Subject: [PATCH 06/25] docstring edits --- pvlib/ivtools/sdm.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/pvlib/ivtools/sdm.py b/pvlib/ivtools/sdm.py index ac76fbf968..2e85e6b2c2 100644 --- a/pvlib/ivtools/sdm.py +++ b/pvlib/ivtools/sdm.py @@ -1414,11 +1414,13 @@ def _pvsyst_objfun(pvs_mod, cec_ivs, ee, tc, cs): def convert_cec_pvsyst(cec_model, cells_in_series, method='Nelder-Mead', options=None): r""" - Convert a CEC model to a PVsyst model. + Convert a set of CEC model parameters to an equivalent set of PVsyst model + parameters. - Uses optimization to fit the PVsyst model to :math:`I_{sc}`, - :math:`V_{oc}`, :math:`V_{mp}`, :math:`I_{mp}`, and :math:`P_{mp}`, - calculated using the input CEC model at the IEC 61853-3 conditions [2]_. + Parameter conversion uses optimization as described in [1]_ to fit the + PVsyst model to :math:`I_{sc}`, :math:`V_{oc}`, :math:`V_{mp}`, + :math:`I_{mp}`, and :math:`P_{mp}`, calculated using the input CEC model + at the IEC 61853-3 conditions [2]_. Parameters ---------- @@ -1430,7 +1432,7 @@ def convert_cec_pvsyst(cec_model, cells_in_series, method='Nelder-Mead', method : str, default 'Nelder-Mead' Method for scipy.optimize.minimize. options : dict, optional - Solver options passed to scipy.optimize.minimize + Solver options passed to scipy.optimize.minimize. Returns ------- @@ -1479,7 +1481,7 @@ def convert_cec_pvsyst(cec_model, cells_in_series, method='Nelder-Mead', .. [2] "IEC 61853-3 Photovoltaic (PV) module performance testing and energy rating - Part 3: Energy rating of PV modules". IEC, Geneva, 2018. """ - if options==None: + if options is None: options = {'maxiter': 5000, 'maxfev': 5000, 'xatol': 0.001} # calculate target IV curve values @@ -1561,11 +1563,13 @@ def _cec_objfun(cec_mod, pvs_ivs, ee, tc, alpha_sc): def convert_pvsyst_cec(pvsyst_model, method='Nelder-Mead', options=None): r""" - Convert a PVsyst model to a CEC model. + Convert a set of PVsyst model parameters to an equivalent set of CEC model + parameters. - Uses optimization to fit the CEC model to :math:`I_{sc}`, - :math:`V_{oc}`, :math:`V_{mp}`, :math:`I_{mp}`, and :math:`P_{mp}`, - calculated using the input PVsyst model at the IEC 61853-3 conditions [2]_. + Parameter conversion uses optimization as described in [1]_ to fit the + CEC model to :math:`I_{sc}`, :math:`V_{oc}`, :math:`V_{mp}`, + :math:`I_{mp}`, and :math:`P_{mp}`, calculated using the input PVsyst model + at the IEC 61853-3 conditions [2]_. Parameters ---------- @@ -1576,7 +1580,7 @@ def convert_pvsyst_cec(pvsyst_model, method='Nelder-Mead', options=None): method : str, default 'Nelder-Mead' Method for scipy.optimize.minimize. options : dict, optional - Solver options passed to scipy.optimize.minimize + Solver options passed to scipy.optimize.minimize. Returns ------- @@ -1622,7 +1626,7 @@ def convert_pvsyst_cec(pvsyst_model, method='Nelder-Mead', options=None): rating - Part 3: Energy rating of PV modules". IEC, Geneva, 2018. """ - if options==None: + if options is None: options = {'maxiter': 5000, 'maxfev': 5000, 'xatol': 0.001} # calculate target IV curve values From ef87465f3038402b9f4a07775d278066702d4f1a Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Tue, 30 Sep 2025 16:34:19 -0700 Subject: [PATCH 07/25] adapt to new organization --- pvlib/ivtools/convert.py | 336 ++++++++++++++++++++++++++++++++++ tests/ivtools/test_convert.py | 48 +++++ 2 files changed, 384 insertions(+) create mode 100644 pvlib/ivtools/convert.py create mode 100644 tests/ivtools/test_convert.py diff --git a/pvlib/ivtools/convert.py b/pvlib/ivtools/convert.py new file mode 100644 index 0000000000..b18c70e601 --- /dev/null +++ b/pvlib/ivtools/convert.py @@ -0,0 +1,336 @@ +""" +The ``sdm`` module contains functions to fit single diode models. + +Function names should follow the pattern "fit_" + name of model + "_" + + fitting method. + +""" + +import numpy as np +import pandas as pd + +from scipy import constants +from scipy import optimize + +from pvlib.pvsystem import (calcparams_pvsyst, calcparams_cec, singlediode) + + +CONSTANTS = {'E0': 1000.0, 'T0': 25.0, 'k': constants.k, 'q': constants.e} + + +IEC61853 = pd.DataFrame( + columns=['effective_irradiance', 'temp_cell'], + data=np.array( + [[100, 100, 100, 100, 200, 200, 200, 200, 400, 400, 400, 400, + 600, 600, 600, 600, 800, 800, 800, 800, 1000, 1000, 1000, 1000, + 1100, 1100, 1100, 1100], + [15, 25, 50, 75, 15, 25, 50, 75, 15, 25, 50, 75, 15, 25, 50, 75, + 15, 25, 50, 75, 15, 25, 50, 75, 15, 25, 50, 75]]).T, + dtype=np.float64) + + + +def _pvsyst_objfun(pvs_mod, cec_ivs, ee, tc, cs): + + # translate the guess into named args that are used in the functions + # order : [alpha_sc, gamma_ref, mu_gamma, I_L_ref, I_o_ref, + # R_sh_mult, R_sh_ref, R_s] + # cec_ivs : DataFrame with columns i_sc, v_oc, i_mp, v_mp, p_mp + # ee : effective irradiance + # tc : cell temperature + # cs : cells in series + alpha_sc = pvs_mod[0] + gamma_ref = pvs_mod[1] + mu_gamma = pvs_mod[2] + I_L_ref = pvs_mod[3] + I_o_ref = pvs_mod[4] + R_sh_mult = pvs_mod[5] + R_sh_ref = pvs_mod[6] + R_s = pvs_mod[7] + + R_sh_0 = R_sh_ref * R_sh_mult + + pvs_params = calcparams_pvsyst( + ee, tc, alpha_sc, gamma_ref, mu_gamma, I_L_ref, I_o_ref, R_sh_ref, + R_sh_0, R_s, cs) + + pvsyst_ivs = singlediode(*pvs_params) + + isc_diff = np.abs((pvsyst_ivs['i_sc'] - cec_ivs['i_sc']) / + cec_ivs['i_sc']).mean() + imp_diff = np.abs((pvsyst_ivs['i_mp'] - cec_ivs['i_mp']) / + cec_ivs['i_mp']).mean() + voc_diff = np.abs((pvsyst_ivs['v_oc'] - cec_ivs['v_oc']) / + cec_ivs['v_oc']).mean() + vmp_diff = np.abs((pvsyst_ivs['v_mp'] - cec_ivs['v_mp']) / + cec_ivs['v_mp']).mean() + pmp_diff = np.abs((pvsyst_ivs['p_mp'] - cec_ivs['p_mp']) / + cec_ivs['p_mp']).mean() + + mean_abs_diff = (isc_diff + imp_diff + voc_diff + vmp_diff + pmp_diff) / 5 + + return mean_abs_diff + + +def convert_cec_pvsyst(cec_model, cells_in_series, method='Nelder-Mead', + options=None): + r""" + Convert a set of CEC model parameters to an equivalent set of PVsyst model + parameters. + + Parameter conversion uses optimization as described in [1]_ to fit the + PVsyst model to :math:`I_{sc}`, :math:`V_{oc}`, :math:`V_{mp}`, + :math:`I_{mp}`, and :math:`P_{mp}`, calculated using the input CEC model + at the IEC 61853-3 conditions [2]_. + + Parameters + ---------- + cec_model : dict or DataFrame + Must include keys: 'alpha_sc', 'a_ref', 'I_L_ref', 'I_o_ref', + 'R_sh_ref', 'R_s', 'Adjust' + cell_in_series : int + Number of cells in series. + method : str, default 'Nelder-Mead' + Method for scipy.optimize.minimize. + options : dict, optional + Solver options passed to scipy.optimize.minimize. + + Returns + ------- + dict with the following elements: + alpha_sc : float + Short-circuit current temperature coefficient [A/C] . + I_L_ref : float + The light-generated current (or photocurrent) at reference + conditions [A]. + I_o_ref : float + The dark or diode reverse saturation current at reference + conditions [A]. + EgRef : float + The energy bandgap at reference temperature [eV]. + R_s : float + The series resistance at reference conditions [ohm]. + R_sh_ref : float + The shunt resistance at reference conditions [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 [unitless]. + gamma_ref : float + Diode (ideality) factor at reference conditions [unitless]. + mu_gamma : float + Temperature coefficient for diode (ideality) factor at reference + conditions [1/K]. + cells_in_series : int + Number of cells in series. + + Notes + ----- + Reference conditions are irradiance of 1000 W/m⁻² and cell temperature of + 25 °C. + + See Also + -------- + pvlib.ivtools.sdm.convert_pvsyst_cec + + References + ---------- + .. [1] L. Deville et al., "Parameter Translation for Photovoltaic Single + Diode Models", submitted. 2024 + + .. [2] "IEC 61853-3 Photovoltaic (PV) module performance testing and energy + rating - Part 3: Energy rating of PV modules". IEC, Geneva, 2018. + """ + if options is None: + options = {'maxiter': 5000, 'maxfev': 5000, 'xatol': 0.001} + + # calculate target IV curve values + cec_params = calcparams_cec( + IEC61853['effective_irradiance'], + IEC61853['temp_cell'], + **cec_model) + cec_ivs = singlediode(*cec_params) + + # initial guess at PVsyst parameters + # Order in list is alpha_sc, gamma_ref, mu_gamma, I_L_ref, I_o_ref, + # Rsh_mult = R_sh_0 / R_sh_ref, R_sh_ref, R_s + initial = [0, 1.2, 0.001, cec_model['I_L_ref'], cec_model['I_o_ref'], + 12, 1000, cec_model['R_s']] + + # bounds for PVsyst parameters + b_alpha = (-1, 1) + b_gamma = (1, 2) + b_mu = (-1, 1) + b_IL = (1e-12, 100) + b_Io = (1e-24, 0.1) + b_Rmult = (1, 20) + b_Rsh = (100, 1e6) + b_Rs = (1e-12, 10) + bounds = [b_alpha, b_gamma, b_mu, b_IL, b_Io, b_Rmult, b_Rsh, b_Rs] + + # optimization to find PVsyst parameters + result = optimize.minimize( + _pvsyst_objfun, initial, + args=(cec_ivs, IEC61853['effective_irradiance'], + IEC61853['temp_cell'], cells_in_series), + method='Nelder-Mead', + bounds=bounds, + options=options) + + alpha_sc, gamma, mu_gamma, I_L_ref, I_o_ref, Rsh_mult, R_sh_ref, R_s = \ + result.x + + R_sh_0 = Rsh_mult * R_sh_ref + R_sh_exp = 5.5 + EgRef = 1.121 # default for all modules in the CEC model + return {'alpha_sc': alpha_sc, + 'I_L_ref': I_L_ref, 'I_o_ref': I_o_ref, 'EgRef': EgRef, 'R_s': R_s, + 'R_sh_ref': R_sh_ref, 'R_sh_0': R_sh_0, 'R_sh_exp': R_sh_exp, + 'gamma_ref': gamma, 'mu_gamma': mu_gamma, + 'cells_in_series': cells_in_series, + } + + +def _cec_objfun(cec_mod, pvs_ivs, ee, tc, alpha_sc): + # translate the guess into named args that are used in the functions + # order : [I_L_ref, I_o_ref, a_ref, R_sh_ref, R_s, alpha_sc, Adjust] + # pvs_ivs : DataFrame with columns i_sc, v_oc, i_mp, v_mp, p_mp + # ee : effective irradiance + # tc : cell temperature + # alpha_sc : temperature coefficient for Isc + I_L_ref = cec_mod[0] + I_o_ref = cec_mod[1] + a_ref = cec_mod[2] + R_sh_ref = cec_mod[3] + R_s = cec_mod[4] + Adjust = cec_mod[5] + alpha_sc = alpha_sc + + cec_params = calcparams_cec( + ee, tc, alpha_sc, a_ref, I_L_ref, I_o_ref, R_sh_ref, R_s, Adjust) + cec_ivs = singlediode(*cec_params) + + isc_rss = np.sqrt(sum((cec_ivs['i_sc'] - pvs_ivs['i_sc'])**2)) + imp_rss = np.sqrt(sum((cec_ivs['i_mp'] - pvs_ivs['i_mp'])**2)) + voc_rss = np.sqrt(sum((cec_ivs['v_oc'] - pvs_ivs['v_oc'])**2)) + vmp_rss = np.sqrt(sum((cec_ivs['v_mp'] - pvs_ivs['v_mp'])**2)) + pmp_rss = np.sqrt(sum((cec_ivs['p_mp'] - pvs_ivs['p_mp'])**2)) + + mean_diff = (isc_rss+imp_rss+voc_rss+vmp_rss+pmp_rss) / 5 + + return mean_diff + + +def convert_pvsyst_cec(pvsyst_model, method='Nelder-Mead', options=None): + r""" + Convert a set of PVsyst model parameters to an equivalent set of CEC model + parameters. + + Parameter conversion uses optimization as described in [1]_ to fit the + CEC model to :math:`I_{sc}`, :math:`V_{oc}`, :math:`V_{mp}`, + :math:`I_{mp}`, and :math:`P_{mp}`, calculated using the input PVsyst model + at the IEC 61853-3 conditions [2]_. + + Parameters + ---------- + pvsyst_model : dict or DataFrame + Must include keys: 'alpha_sc', 'I_L_ref', 'I_o_ref', 'EgRef', 'R_s', + 'R_sh_ref', 'R_sh_0', 'R_sh_exp', 'gamma_ref', 'mu_gamma', + 'cells_in_series' + method : str, default 'Nelder-Mead' + Method for scipy.optimize.minimize. + options : dict, optional + Solver options passed to scipy.optimize.minimize. + + Returns + ------- + dict with the following elements: + I_L_ref : float + The light-generated current (or photocurrent) at reference + conditions [A]. + I_o_ref : float + The dark or diode reverse saturation current at reference + conditions [A]. + R_s : float + The series resistance at reference conditions [ohm]. + R_sh_ref : float + The shunt resistance at reference conditions [ohm]. + a_ref : float + The product of the usual diode ideality factor ``n`` (unitless), + number of cells in series ``Ns``, and cell thermal voltage at + reference conditions [V]. + Adjust : float + The adjustment to the temperature coefficient for short circuit + current, in percent. + EgRef : float + The energy bandgap at reference temperature [eV]. + dEgdT : float + The temperature dependence of the energy bandgap at reference + conditions [1/K]. + + Notes + ----- + Reference conditions are irradiance of 1000 W/m⁻² and cell temperature of + 25 °C. + + See Also + -------- + pvlib.ivtools.sdm.convert_cec_pvsyst + + References + ---------- + .. [1] L. Deville et al., "Parameter Translation for Photovoltaic Single + Diode Models", submitted. 2024. + + .. [2] "IEC 61853-3 Photovoltaic (PV) module performance testing and energy + rating - Part 3: Energy rating of PV modules". IEC, Geneva, 2018. + """ + + if options is None: + options = {'maxiter': 5000, 'maxfev': 5000, 'xatol': 0.001} + + # calculate target IV curve values + pvs_params = calcparams_pvsyst( + IEC61853['effective_irradiance'], + IEC61853['temp_cell'], + **pvsyst_model) + pvsyst_ivs = singlediode(*pvs_params) + + # set EgRef and dEgdT to CEC defaults + EgRef = 1.121 + dEgdT = -0.0002677 + + # initial guess + # order must match _pvsyst_objfun + # order : [I_L_ref, I_o_ref, a_ref, R_sh_ref, R_s, alpha_sc, Adjust] + nNsVth = pvsyst_model['gamma_ref'] * pvsyst_model['cells_in_series'] \ + * 0.025 + initial = [pvsyst_model['I_L_ref'], pvsyst_model['I_o_ref'], + nNsVth, pvsyst_model['R_sh_ref'], pvsyst_model['R_s'], + 0] + + # bounds for PVsyst parameters + b_IL = (1e-12, 100) + b_Io = (1e-24, 0.1) + b_aref = (1e-12, 1000) + b_Rsh = (100, 1e6) + b_Rs = (1e-12, 10) + b_Adjust = (-100, 100) + bounds = [b_IL, b_Io, b_aref, b_Rsh, b_Rs, b_Adjust] + + result = optimize.minimize( + _cec_objfun, initial, + args=(pvsyst_ivs, IEC61853['effective_irradiance'], + IEC61853['temp_cell'], pvsyst_model['alpha_sc']), + method='Nelder-Mead', + bounds=bounds, + options=options) + + I_L_ref, I_o_ref, a_ref, R_sh_ref, R_s, Adjust = result.x + + return {'alpha_sc': pvsyst_model['alpha_sc'], + 'a_ref': a_ref, 'I_L_ref': I_L_ref, 'I_o_ref': I_o_ref, + 'R_sh_ref': R_sh_ref, 'R_s': R_s, 'Adjust': Adjust, + 'EgRef': EgRef, 'dEgdT': dEgdT + } diff --git a/tests/ivtools/test_convert.py b/tests/ivtools/test_convert.py new file mode 100644 index 0000000000..899c0f26b7 --- /dev/null +++ b/tests/ivtools/test_convert.py @@ -0,0 +1,48 @@ +import numpy as np +from pvlib.ivtools import sdm + + +def test_convert_cec_pvsyst(): + cells_in_series = 66 + trina660_cec = {'I_L_ref': 18.4759, 'I_o_ref': 5.31e-12, + 'EgRef': 1.121, 'dEgdT': -0.0002677, + 'R_s': 0.159916, 'R_sh_ref': 113.991, 'a_ref': 1.59068, + 'Adjust': 6.42247, 'alpha_sc': 0.00629} + trina660_pvsyst_est = sdm.convert_cec_pvsyst(trina660_cec, + cells_in_series) + pvsyst_expected = {'alpha_sc': 0.007478218748188788, + 'I_L_ref': 18.227679597516214, + 'I_o_ref': 2.7418999402908e-11, + 'EgRef': 1.121, + 'R_s': 0.16331908293164496, + 'R_sh_ref': 5267.928954454954, + 'R_sh_0': 60171.206687871425, + 'R_sh_exp': 5.5, + 'gamma_ref': 1.0, + 'mu_gamma': -6.349173477135307e-05, + 'cells_in_series': 66} + + assert np.all([np.isclose(trina660_pvsyst_est[k], pvsyst_expected[k], + rtol=1e-3) + for k in pvsyst_expected]) + + +def test_convert_pvsyst_cec(): + trina660_pvsyst = {'alpha_sc': 0.0074, 'I_o_ref': 3.3e-11, 'EgRef': 1.121, + 'R_s': 0.156, 'R_sh_ref': 200, 'R_sh_0': 800, + 'R_sh_exp': 5.5, 'gamma_ref': 1.002, 'mu_gamma': 1e-3, + 'cells_in_series': 66} + trina660_cec_est = sdm.convert_pvsyst_cec(trina660_pvsyst) + cec_expected = {'alpha_sc': 0.0074, + 'I_L_ref': 18.05154226834071, + 'I_o_ref': 2.6863417875143392e-14, + 'EgRef': 1.121, + 'dEgdT': -0.0002677, + 'R_s': 0.09436341848926795, + 'a_ref': 1.2954800250731866, + 'Adjust': 0.0011675969492410047, + 'cells_in_series': 66} + + assert np.all([np.isclose(trina660_cec_est[k], cec_expected[k], + rtol=1e-3) + for k in cec_expected]) From 5188ee99ca0f0ac3e5a71a2e710469e4622107eb Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Tue, 30 Sep 2025 16:46:55 -0700 Subject: [PATCH 08/25] paths, names, reference --- pvlib/ivtools/convert.py | 17 ++++++++++------- tests/ivtools/test_convert.py | 6 +++--- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/pvlib/ivtools/convert.py b/pvlib/ivtools/convert.py index b18c70e601..5d7170ae69 100644 --- a/pvlib/ivtools/convert.py +++ b/pvlib/ivtools/convert.py @@ -1,8 +1,9 @@ """ -The ``sdm`` module contains functions to fit single diode models. +The ``convert`` module contains functions to convert between single diode +models. -Function names should follow the pattern "fit_" + name of model + "_" + - fitting method. +Function names should follow the pattern "convert_" + name of source model + + "_" + name of target method + "_" + name of conversion method. """ @@ -132,12 +133,13 @@ def convert_cec_pvsyst(cec_model, cells_in_series, method='Nelder-Mead', See Also -------- - pvlib.ivtools.sdm.convert_pvsyst_cec + pvlib.ivtools.convert.convert_pvsyst_cec References ---------- .. [1] L. Deville et al., "Parameter Translation for Photovoltaic Single - Diode Models", submitted. 2024 + Diode Models", Journal of Photovoltaics, vol. 15(3), pp. 451-457, + May 2025. :doi:`10.1109/jphotov.2025.3539319` .. [2] "IEC 61853-3 Photovoltaic (PV) module performance testing and energy rating - Part 3: Energy rating of PV modules". IEC, Geneva, 2018. @@ -276,12 +278,13 @@ def convert_pvsyst_cec(pvsyst_model, method='Nelder-Mead', options=None): See Also -------- - pvlib.ivtools.sdm.convert_cec_pvsyst + pvlib.ivtools.convert.convert_cec_pvsyst References ---------- .. [1] L. Deville et al., "Parameter Translation for Photovoltaic Single - Diode Models", submitted. 2024. + Diode Models", Journal of Photovoltaics, vol. 15(3), pp. 451-457, + May 2025. :doi:`10.1109/jphotov.2025.3539319` .. [2] "IEC 61853-3 Photovoltaic (PV) module performance testing and energy rating - Part 3: Energy rating of PV modules". IEC, Geneva, 2018. diff --git a/tests/ivtools/test_convert.py b/tests/ivtools/test_convert.py index 899c0f26b7..a5a82af11a 100644 --- a/tests/ivtools/test_convert.py +++ b/tests/ivtools/test_convert.py @@ -1,5 +1,5 @@ import numpy as np -from pvlib.ivtools import sdm +from pvlib.ivtools import convert def test_convert_cec_pvsyst(): @@ -8,7 +8,7 @@ def test_convert_cec_pvsyst(): 'EgRef': 1.121, 'dEgdT': -0.0002677, 'R_s': 0.159916, 'R_sh_ref': 113.991, 'a_ref': 1.59068, 'Adjust': 6.42247, 'alpha_sc': 0.00629} - trina660_pvsyst_est = sdm.convert_cec_pvsyst(trina660_cec, + trina660_pvsyst_est = convert.convert_cec_pvsyst(trina660_cec, cells_in_series) pvsyst_expected = {'alpha_sc': 0.007478218748188788, 'I_L_ref': 18.227679597516214, @@ -32,7 +32,7 @@ def test_convert_pvsyst_cec(): 'R_s': 0.156, 'R_sh_ref': 200, 'R_sh_0': 800, 'R_sh_exp': 5.5, 'gamma_ref': 1.002, 'mu_gamma': 1e-3, 'cells_in_series': 66} - trina660_cec_est = sdm.convert_pvsyst_cec(trina660_pvsyst) + trina660_cec_est = convert.convert_pvsyst_cec(trina660_pvsyst) cec_expected = {'alpha_sc': 0.0074, 'I_L_ref': 18.05154226834071, 'I_o_ref': 2.6863417875143392e-14, From a181438a8ee264fb5c74839877749ecfcaa6d4b1 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 1 Oct 2025 07:28:48 -0700 Subject: [PATCH 09/25] fixes --- pvlib/ivtools/convert.py | 1 - tests/ivtools/test_convert.py | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pvlib/ivtools/convert.py b/pvlib/ivtools/convert.py index 5d7170ae69..f6c553a038 100644 --- a/pvlib/ivtools/convert.py +++ b/pvlib/ivtools/convert.py @@ -30,7 +30,6 @@ dtype=np.float64) - def _pvsyst_objfun(pvs_mod, cec_ivs, ee, tc, cs): # translate the guess into named args that are used in the functions diff --git a/tests/ivtools/test_convert.py b/tests/ivtools/test_convert.py index a5a82af11a..3f7b3cb206 100644 --- a/tests/ivtools/test_convert.py +++ b/tests/ivtools/test_convert.py @@ -9,7 +9,7 @@ def test_convert_cec_pvsyst(): 'R_s': 0.159916, 'R_sh_ref': 113.991, 'a_ref': 1.59068, 'Adjust': 6.42247, 'alpha_sc': 0.00629} trina660_pvsyst_est = convert.convert_cec_pvsyst(trina660_cec, - cells_in_series) + cells_in_series) pvsyst_expected = {'alpha_sc': 0.007478218748188788, 'I_L_ref': 18.227679597516214, 'I_o_ref': 2.7418999402908e-11, @@ -28,7 +28,8 @@ def test_convert_cec_pvsyst(): def test_convert_pvsyst_cec(): - trina660_pvsyst = {'alpha_sc': 0.0074, 'I_o_ref': 3.3e-11, 'EgRef': 1.121, + trina660_pvsyst = {'alpha_sc': 0.0074, 'I_L_ref': 18.464391, + 'I_o_ref': 3.3e-11, 'EgRef': 1.121, 'R_s': 0.156, 'R_sh_ref': 200, 'R_sh_0': 800, 'R_sh_exp': 5.5, 'gamma_ref': 1.002, 'mu_gamma': 1e-3, 'cells_in_series': 66} From 365729865738eef0ea2670ec8a61309fdc6e486f Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 1 Oct 2025 08:06:56 -0700 Subject: [PATCH 10/25] why does cells_in_series have to be special --- tests/ivtools/test_convert.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/ivtools/test_convert.py b/tests/ivtools/test_convert.py index 3f7b3cb206..71aa2fd5ca 100644 --- a/tests/ivtools/test_convert.py +++ b/tests/ivtools/test_convert.py @@ -41,8 +41,7 @@ def test_convert_pvsyst_cec(): 'dEgdT': -0.0002677, 'R_s': 0.09436341848926795, 'a_ref': 1.2954800250731866, - 'Adjust': 0.0011675969492410047, - 'cells_in_series': 66} + 'Adjust': 0.0011675969492410047} assert np.all([np.isclose(trina660_cec_est[k], cec_expected[k], rtol=1e-3) From 11ce6017597ff2a9a6b508fada228e5e30ca7867 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 1 Oct 2025 10:14:08 -0700 Subject: [PATCH 11/25] correct IEC61853 matrix, add comments --- pvlib/ivtools/convert.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/pvlib/ivtools/convert.py b/pvlib/ivtools/convert.py index f6c553a038..d23daec571 100644 --- a/pvlib/ivtools/convert.py +++ b/pvlib/ivtools/convert.py @@ -19,7 +19,7 @@ CONSTANTS = {'E0': 1000.0, 'T0': 25.0, 'k': constants.k, 'q': constants.e} -IEC61853 = pd.DataFrame( +IEC61853_plus = pd.DataFrame( columns=['effective_irradiance', 'temp_cell'], data=np.array( [[100, 100, 100, 100, 200, 200, 200, 200, 400, 400, 400, 400, @@ -30,11 +30,24 @@ dtype=np.float64) -def _pvsyst_objfun(pvs_mod, cec_ivs, ee, tc, cs): +IEC61853 = pd.DataFrame( + columns=['effective_irradiance', 'temp_cell'], + data=np.array( + [[100, 100, 200, 200, 400, 400, 400, + 600, 600, 600, 600, 800, 800, 800, 800, 1000, 1000, 1000, 1000, + 1100, 1100, 1100], + [15, 25, 15, 25, 15, 25, 50, + 15, 25, 50, 75, 15, 25, 50, 75, 15, 25, 50, 75, + 25, 50, 75]]).T, + dtype=np.float64) + +def _pvsyst_objfun(pvs_mod, cec_ivs, ee, tc, cs): + # objective function for converting CEC to PVsyst model # translate the guess into named args that are used in the functions - # order : [alpha_sc, gamma_ref, mu_gamma, I_L_ref, I_o_ref, - # R_sh_mult, R_sh_ref, R_s] + # order of variables in pvs_mod: + # [alpha_sc, gamma_ref, mu_gamma, I_L_ref, I_o_ref, + # R_sh_mult, R_sh_ref, R_s] # cec_ivs : DataFrame with columns i_sc, v_oc, i_mp, v_mp, p_mp # ee : effective irradiance # tc : cell temperature @@ -56,6 +69,9 @@ def _pvsyst_objfun(pvs_mod, cec_ivs, ee, tc, cs): pvsyst_ivs = singlediode(*pvs_params) + + # calculate error metric, mean absolute relative error for PVsyst model as + # the target isc_diff = np.abs((pvsyst_ivs['i_sc'] - cec_ivs['i_sc']) / cec_ivs['i_sc']).mean() imp_diff = np.abs((pvsyst_ivs['i_mp'] - cec_ivs['i_mp']) / @@ -194,8 +210,10 @@ def convert_cec_pvsyst(cec_model, cells_in_series, method='Nelder-Mead', def _cec_objfun(cec_mod, pvs_ivs, ee, tc, alpha_sc): + # objective function for converting PVsyst to CEC model # translate the guess into named args that are used in the functions - # order : [I_L_ref, I_o_ref, a_ref, R_sh_ref, R_s, alpha_sc, Adjust] + # order of variables in cec_mod: + # [I_L_ref, I_o_ref, a_ref, R_sh_ref, R_s, alpha_sc, Adjust] # pvs_ivs : DataFrame with columns i_sc, v_oc, i_mp, v_mp, p_mp # ee : effective irradiance # tc : cell temperature @@ -212,6 +230,7 @@ def _cec_objfun(cec_mod, pvs_ivs, ee, tc, alpha_sc): ee, tc, alpha_sc, a_ref, I_L_ref, I_o_ref, R_sh_ref, R_s, Adjust) cec_ivs = singlediode(*cec_params) + # calculate error metric, root sum of squares for CEC model as the target isc_rss = np.sqrt(sum((cec_ivs['i_sc'] - pvs_ivs['i_sc'])**2)) imp_rss = np.sqrt(sum((cec_ivs['i_mp'] - pvs_ivs['i_mp'])**2)) voc_rss = np.sqrt(sum((cec_ivs['v_oc'] - pvs_ivs['v_oc'])**2)) From c62e6d1b329c2a4086b05ea7c6e3c0d1eb8f5e2f Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 3 Oct 2025 11:06:29 -0700 Subject: [PATCH 12/25] use find_minimum in lambertw for mpp --- pvlib/singlediode.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index 43be522437..6c92ae8a45 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -913,8 +913,28 @@ def _lambertw(photocurrent, saturation_current, resistance_series, v_oc = 0. # Find the voltage, v_mp, where the power is maximized. - # Start the golden section search at v_oc * 1.14 - p_mp, v_mp = _golden_sect_DataFrame(params, 0., v_oc * 1.14, _pwr_optfcn) + # use scipy.elementwise if available + use_gs = False + try: + from scipy.optimize.elementwise import find_minimum + init = (0., 0.8*v_oc, 1.01*v_oc) + res = find_minimum(_vmp_opt, init, + args=(params['photocurrent'], + params['saturation_current'], + params['resistance_series'], + params['resistance_shunt'], + params['nNsVth'],)) + if res.success.all(): + v_mp = res.x + p_mp = -1.*res.f_x + else: + use_gs = True + except ModuleNotFoundError: + use_gs = True + + if use_gs: + # gracefully switch to old golden section method + p_mp, v_mp = _golden_sect_DataFrame(params, 0., v_oc * 1.14, _pwr_optfcn) # Find Imp using Lambert W i_mp = _lambertw_i_from_v(v_mp, **params) @@ -938,6 +958,15 @@ def _lambertw(photocurrent, saturation_current, resistance_series, return out +def _vmp_opt(v, iph, io, rs, rsh, nNsVth): + ''' + Function to find power from ``i_from_v``. + ''' + current = _lambertw_i_from_v(v, iph, io, rs, rsh, nNsVth) + + return -v * current + + def _pwr_optfcn(df, loc): ''' Function to find power from ``i_from_v``. From ac500d53b7bc700ad4f0f359b9e202e674195a3e Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 3 Oct 2025 14:03:44 -0700 Subject: [PATCH 13/25] remove switch between find_minimum and golden mean --- pvlib/singlediode.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index 6c92ae8a45..a0b1c39a73 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -914,7 +914,6 @@ def _lambertw(photocurrent, saturation_current, resistance_series, # Find the voltage, v_mp, where the power is maximized. # use scipy.elementwise if available - use_gs = False try: from scipy.optimize.elementwise import find_minimum init = (0., 0.8*v_oc, 1.01*v_oc) @@ -924,16 +923,10 @@ def _lambertw(photocurrent, saturation_current, resistance_series, params['resistance_series'], params['resistance_shunt'], params['nNsVth'],)) - if res.success.all(): - v_mp = res.x - p_mp = -1.*res.f_x - else: - use_gs = True + v_mp = res.x + p_mp = -1.*res.f_x except ModuleNotFoundError: - use_gs = True - - if use_gs: - # gracefully switch to old golden section method + # switch to old golden section method p_mp, v_mp = _golden_sect_DataFrame(params, 0., v_oc * 1.14, _pwr_optfcn) # Find Imp using Lambert W @@ -960,7 +953,7 @@ def _lambertw(photocurrent, saturation_current, resistance_series, def _vmp_opt(v, iph, io, rs, rsh, nNsVth): ''' - Function to find power from ``i_from_v``. + Function to find negative of power from ``i_from_v``. ''' current = _lambertw_i_from_v(v, iph, io, rs, rsh, nNsVth) From 4827334dbf1887c9cb99c88963795de49d5b2b7f Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 3 Oct 2025 14:07:59 -0700 Subject: [PATCH 14/25] add removal comment --- pvlib/singlediode.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index a0b1c39a73..2f9c9d04c7 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -914,6 +914,7 @@ def _lambertw(photocurrent, saturation_current, resistance_series, # Find the voltage, v_mp, where the power is maximized. # use scipy.elementwise if available + # remove try/except when scipy>=1.15, and golden mean is retired try: from scipy.optimize.elementwise import find_minimum init = (0., 0.8*v_oc, 1.01*v_oc) From b36b297b65deaa4f2f8f050f3c5a7d28b0986291 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 3 Oct 2025 14:12:03 -0700 Subject: [PATCH 15/25] whatsnew --- docs/sphinx/source/whatsnew/v0.13.2.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.13.2.rst b/docs/sphinx/source/whatsnew/v0.13.2.rst index 3177f7a022..9916c28ce6 100644 --- a/docs/sphinx/source/whatsnew/v0.13.2.rst +++ b/docs/sphinx/source/whatsnew/v0.13.2.rst @@ -27,7 +27,9 @@ Enhancements :py:func:`~pvlib.singlediode.bishop88_mpp`, :py:func:`~pvlib.singlediode.bishop88_v_from_i`, and :py:func:`~pvlib.singlediode.bishop88_i_from_v`. (:issue:`2497`, :pull:`2498`) - +* Add capability for :py:func:`~pvlib.singlediode._lambertw` to use scipy + find_minimum to get the maximum power point instead of pvlib's golden + mean search. (:issue:`2497`, :pull:`2567`) Documentation @@ -52,4 +54,4 @@ Maintenance Contributors ~~~~~~~~~~~~ - +* Cliff Hansen (:ghuser:`cwhanse`) From edb553527cdb62c1eb9d116a01aecfd5a66fd02f Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 3 Oct 2025 14:13:23 -0700 Subject: [PATCH 16/25] remove multiplier on voc for initial interval --- pvlib/singlediode.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index 2f9c9d04c7..08c8e8edb0 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -917,7 +917,7 @@ def _lambertw(photocurrent, saturation_current, resistance_series, # remove try/except when scipy>=1.15, and golden mean is retired try: from scipy.optimize.elementwise import find_minimum - init = (0., 0.8*v_oc, 1.01*v_oc) + init = (0., 0.5*v_oc, v_oc) res = find_minimum(_vmp_opt, init, args=(params['photocurrent'], params['saturation_current'], @@ -928,7 +928,8 @@ def _lambertw(photocurrent, saturation_current, resistance_series, p_mp = -1.*res.f_x except ModuleNotFoundError: # switch to old golden section method - p_mp, v_mp = _golden_sect_DataFrame(params, 0., v_oc * 1.14, _pwr_optfcn) + p_mp, v_mp = _golden_sect_DataFrame(params, 0., v_oc * 1.14, + _pwr_optfcn) # Find Imp using Lambert W i_mp = _lambertw_i_from_v(v_mp, **params) From 5c7969687cef49f391adee54336bdad43b10fff3 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 3 Oct 2025 14:13:53 -0700 Subject: [PATCH 17/25] adjust multiplier for initial guess --- pvlib/singlediode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index 08c8e8edb0..0f79079b80 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -917,7 +917,7 @@ def _lambertw(photocurrent, saturation_current, resistance_series, # remove try/except when scipy>=1.15, and golden mean is retired try: from scipy.optimize.elementwise import find_minimum - init = (0., 0.5*v_oc, v_oc) + init = (0., 0.8*v_oc, v_oc) res = find_minimum(_vmp_opt, init, args=(params['photocurrent'], params['saturation_current'], From 2bdea49e692787979ecf84c371df41925e4116cd Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Mon, 6 Oct 2025 08:42:57 -0700 Subject: [PATCH 18/25] remake expected test output, add kwargs --- pvlib/ivtools/convert.py | 137 ++++++++++++++++++++-------------- tests/ivtools/test_convert.py | 40 +++++----- 2 files changed, 100 insertions(+), 77 deletions(-) diff --git a/pvlib/ivtools/convert.py b/pvlib/ivtools/convert.py index d23daec571..8627c40fcd 100644 --- a/pvlib/ivtools/convert.py +++ b/pvlib/ivtools/convert.py @@ -19,17 +19,6 @@ CONSTANTS = {'E0': 1000.0, 'T0': 25.0, 'k': constants.k, 'q': constants.e} -IEC61853_plus = pd.DataFrame( - columns=['effective_irradiance', 'temp_cell'], - data=np.array( - [[100, 100, 100, 100, 200, 200, 200, 200, 400, 400, 400, 400, - 600, 600, 600, 600, 800, 800, 800, 800, 1000, 1000, 1000, 1000, - 1100, 1100, 1100, 1100], - [15, 25, 50, 75, 15, 25, 50, 75, 15, 25, 50, 75, 15, 25, 50, 75, - 15, 25, 50, 75, 15, 25, 50, 75, 15, 25, 50, 75]]).T, - dtype=np.float64) - - IEC61853 = pd.DataFrame( columns=['effective_irradiance', 'temp_cell'], data=np.array( @@ -88,8 +77,10 @@ def _pvsyst_objfun(pvs_mod, cec_ivs, ee, tc, cs): return mean_abs_diff -def convert_cec_pvsyst(cec_model, cells_in_series, method='Nelder-Mead', - options=None): + +def convert_cec_pvsyst(cec_model, cells_in_series, initial=None, + method='Nelder-Mead', + bounds=None, options=None): r""" Convert a set of CEC model parameters to an equivalent set of PVsyst model parameters. @@ -106,8 +97,13 @@ def convert_cec_pvsyst(cec_model, cells_in_series, method='Nelder-Mead', 'R_sh_ref', 'R_s', 'Adjust' cell_in_series : int Number of cells in series. + initial : ndarray, optional + Initial guess for CEC model parameters. See Notes for parameter order. method : str, default 'Nelder-Mead' Method for scipy.optimize.minimize. + bounds : sequence or Bounds, optional + Initial guess for CEC model parameters. See Notes for parameter order. + See documentation for scipy.optimize.minimize for details. options : dict, optional Solver options passed to scipy.optimize.minimize. @@ -115,7 +111,7 @@ def convert_cec_pvsyst(cec_model, cells_in_series, method='Nelder-Mead', ------- dict with the following elements: alpha_sc : float - Short-circuit current temperature coefficient [A/C] . + Short-circuit current temperature coefficient [A/C]. I_L_ref : float The light-generated current (or photocurrent) at reference conditions [A]. @@ -159,6 +155,27 @@ def convert_cec_pvsyst(cec_model, cells_in_series, method='Nelder-Mead', .. [2] "IEC 61853-3 Photovoltaic (PV) module performance testing and energy rating - Part 3: Energy rating of PV modules". IEC, Geneva, 2018. """ + if initial is None: + # initial guess at PVsyst parameters + # Order in list is alpha_sc, gamma_ref, mu_gamma, I_L_ref, I_o_ref, + # Rsh_mult = R_sh_0 / R_sh_ref, R_sh_ref, R_s + gamma_ref = cec_model['a_ref'] / (cells_in_series * 0.025) + initial = [cec_model['alpha_sc'], gamma_ref, 0.001, + cec_model['I_L_ref'], cec_model['I_o_ref'], + 12, 1000, cec_model['R_s']] + + if bounds is None: + # bounds for PVsyst parameters + b_alpha = (-1, 1) + b_gamma = (1, 2) + b_mu = (-1, 1) + b_IL = (1e-12, 100) + b_Io = (1e-24, 0.1) + b_Rmult = (1, 20) + b_Rsh = (100, 1e6) + b_Rs = (1e-12, 10) + bounds = [b_alpha, b_gamma, b_mu, b_IL, b_Io, b_Rmult, b_Rsh, b_Rs] + if options is None: options = {'maxiter': 5000, 'maxfev': 5000, 'xatol': 0.001} @@ -169,23 +186,6 @@ def convert_cec_pvsyst(cec_model, cells_in_series, method='Nelder-Mead', **cec_model) cec_ivs = singlediode(*cec_params) - # initial guess at PVsyst parameters - # Order in list is alpha_sc, gamma_ref, mu_gamma, I_L_ref, I_o_ref, - # Rsh_mult = R_sh_0 / R_sh_ref, R_sh_ref, R_s - initial = [0, 1.2, 0.001, cec_model['I_L_ref'], cec_model['I_o_ref'], - 12, 1000, cec_model['R_s']] - - # bounds for PVsyst parameters - b_alpha = (-1, 1) - b_gamma = (1, 2) - b_mu = (-1, 1) - b_IL = (1e-12, 100) - b_Io = (1e-24, 0.1) - b_Rmult = (1, 20) - b_Rsh = (100, 1e6) - b_Rs = (1e-12, 10) - bounds = [b_alpha, b_gamma, b_mu, b_IL, b_Io, b_Rmult, b_Rsh, b_Rs] - # optimization to find PVsyst parameters result = optimize.minimize( _pvsyst_objfun, initial, @@ -242,7 +242,9 @@ def _cec_objfun(cec_mod, pvs_ivs, ee, tc, alpha_sc): return mean_diff -def convert_pvsyst_cec(pvsyst_model, method='Nelder-Mead', options=None): +def convert_pvsyst_cec(pvsyst_model, initial=None, method='Nelder-Mead', + bounds=None, options=None, + EgRef=1.121, dEgdT=-0.0002677): r""" Convert a set of PVsyst model parameters to an equivalent set of CEC model parameters. @@ -258,14 +260,31 @@ def convert_pvsyst_cec(pvsyst_model, method='Nelder-Mead', options=None): Must include keys: 'alpha_sc', 'I_L_ref', 'I_o_ref', 'EgRef', 'R_s', 'R_sh_ref', 'R_sh_0', 'R_sh_exp', 'gamma_ref', 'mu_gamma', 'cells_in_series' - method : str, default 'Nelder-Mead' + initial : ndarray, optional + Initial guess for CEC model parameters. See Notes for parameter order. + method : str or callable, default 'Nelder-Mead' Method for scipy.optimize.minimize. + bounds : sequence or Bounds, optional + Initial guess for CEC model parameters. See Notes for parameter order. + See documentation for scipy.optimize.minimize for details. options : dict, optional Solver options passed to scipy.optimize.minimize. + EgRef : float, default 1.121 + The energy bandgap at reference temperature [eV]. + 1.121 eV for crystalline silicon. EgRef=1.121 is implicit for all + cell types in the SAM CEC module database, and is imposed by the + CEC parameter estimation algorithm in SAM. + dEgdT : float, default -0.0002677 + The temperature dependence of the energy bandgap at reference + conditions [1/K]. dEgdT=-0.0002677 is implicit for all cell + types in the SAM CEC module database, and is imposed by the + CEC parameter estimation algorithm in SAM. Returns ------- dict with the following elements: + alpha_sc : float + The input short-circuit current temperature coefficient [A/C]. I_L_ref : float The light-generated current (or photocurrent) at reference conditions [A]. @@ -284,9 +303,9 @@ def convert_pvsyst_cec(pvsyst_model, method='Nelder-Mead', options=None): The adjustment to the temperature coefficient for short circuit current, in percent. EgRef : float - The energy bandgap at reference temperature [eV]. + The input energy bandgap at reference temperature [eV]. dEgdT : float - The temperature dependence of the energy bandgap at reference + The input temperature dependence of the energy bandgap at reference conditions [1/K]. Notes @@ -294,6 +313,11 @@ def convert_pvsyst_cec(pvsyst_model, method='Nelder-Mead', options=None): Reference conditions are irradiance of 1000 W/m⁻² and cell temperature of 25 °C. + Notes + ----- + The order of the parameters in the initial guess and bounds: + [I_L_ref, I_o_ref, a_ref, R_sh_ref, R_s, Adjust] + See Also -------- pvlib.ivtools.convert.convert_cec_pvsyst @@ -307,6 +331,25 @@ def convert_pvsyst_cec(pvsyst_model, method='Nelder-Mead', options=None): .. [2] "IEC 61853-3 Photovoltaic (PV) module performance testing and energy rating - Part 3: Energy rating of PV modules". IEC, Geneva, 2018. """ + # initial guess + # order must match arguments of _cec_objfun + # order : [I_L_ref, I_o_ref, a_ref, R_sh_ref, R_s, alpha_sc, Adjust] + + if initial is None: + nNsVth = pvsyst_model['gamma_ref'] * pvsyst_model['cells_in_series'] \ + * 0.025 + initial = [pvsyst_model['I_L_ref'], pvsyst_model['I_o_ref'], + nNsVth, pvsyst_model['R_sh_ref'], pvsyst_model['R_s'], 0] + + if bounds is None: + # bounds for CEC parameters + b_IL = (1e-12, 100) + b_Io = (1e-24, 0.1) + b_aref = (1e-12, 1000) + b_Rsh = (100, 1e6) + b_Rs = (1e-12, 10) + b_Adjust = (-100, 100) + bounds = [b_IL, b_Io, b_aref, b_Rsh, b_Rs, b_Adjust] if options is None: options = {'maxiter': 5000, 'maxfev': 5000, 'xatol': 0.001} @@ -318,33 +361,11 @@ def convert_pvsyst_cec(pvsyst_model, method='Nelder-Mead', options=None): **pvsyst_model) pvsyst_ivs = singlediode(*pvs_params) - # set EgRef and dEgdT to CEC defaults - EgRef = 1.121 - dEgdT = -0.0002677 - - # initial guess - # order must match _pvsyst_objfun - # order : [I_L_ref, I_o_ref, a_ref, R_sh_ref, R_s, alpha_sc, Adjust] - nNsVth = pvsyst_model['gamma_ref'] * pvsyst_model['cells_in_series'] \ - * 0.025 - initial = [pvsyst_model['I_L_ref'], pvsyst_model['I_o_ref'], - nNsVth, pvsyst_model['R_sh_ref'], pvsyst_model['R_s'], - 0] - - # bounds for PVsyst parameters - b_IL = (1e-12, 100) - b_Io = (1e-24, 0.1) - b_aref = (1e-12, 1000) - b_Rsh = (100, 1e6) - b_Rs = (1e-12, 10) - b_Adjust = (-100, 100) - bounds = [b_IL, b_Io, b_aref, b_Rsh, b_Rs, b_Adjust] - result = optimize.minimize( _cec_objfun, initial, args=(pvsyst_ivs, IEC61853['effective_irradiance'], IEC61853['temp_cell'], pvsyst_model['alpha_sc']), - method='Nelder-Mead', + method=method, bounds=bounds, options=options) diff --git a/tests/ivtools/test_convert.py b/tests/ivtools/test_convert.py index 71aa2fd5ca..8b5d3e7f5a 100644 --- a/tests/ivtools/test_convert.py +++ b/tests/ivtools/test_convert.py @@ -6,24 +6,24 @@ def test_convert_cec_pvsyst(): cells_in_series = 66 trina660_cec = {'I_L_ref': 18.4759, 'I_o_ref': 5.31e-12, 'EgRef': 1.121, 'dEgdT': -0.0002677, - 'R_s': 0.159916, 'R_sh_ref': 113.991, 'a_ref': 1.59068, + 'R_s': 0.159916, 'R_sh_ref': 113.991, 'a_ref': 1.8, 'Adjust': 6.42247, 'alpha_sc': 0.00629} trina660_pvsyst_est = convert.convert_cec_pvsyst(trina660_cec, cells_in_series) - pvsyst_expected = {'alpha_sc': 0.007478218748188788, - 'I_L_ref': 18.227679597516214, - 'I_o_ref': 2.7418999402908e-11, - 'EgRef': 1.121, - 'R_s': 0.16331908293164496, - 'R_sh_ref': 5267.928954454954, - 'R_sh_0': 60171.206687871425, - 'R_sh_exp': 5.5, - 'gamma_ref': 1.0, - 'mu_gamma': -6.349173477135307e-05, - 'cells_in_series': 66} + pvsyst_expected = {'alpha_sc': 0.0096671, + 'I_L_ref': 18.19305, + 'I_o_ref': 6.94494e-12, + 'EgRef': 1.121, + 'R_s': 0.16318, + 'R_sh_ref': 1000.947, + 'R_sh_0': 8593.35, + 'R_sh_exp': 5.5, + 'gamma_ref': 1.0724, + 'mu_gamma': -0.00074595, + 'cells_in_series': 66} assert np.all([np.isclose(trina660_pvsyst_est[k], pvsyst_expected[k], - rtol=1e-3) + rtol=1e-4, atol=0.) for k in pvsyst_expected]) @@ -34,15 +34,17 @@ def test_convert_pvsyst_cec(): 'R_sh_exp': 5.5, 'gamma_ref': 1.002, 'mu_gamma': 1e-3, 'cells_in_series': 66} trina660_cec_est = convert.convert_pvsyst_cec(trina660_pvsyst) + cec_expected = {'alpha_sc': 0.0074, - 'I_L_ref': 18.05154226834071, - 'I_o_ref': 2.6863417875143392e-14, + 'I_L_ref': 18.09421, + 'I_o_ref': 2.46522e-14, 'EgRef': 1.121, 'dEgdT': -0.0002677, - 'R_s': 0.09436341848926795, - 'a_ref': 1.2954800250731866, - 'Adjust': 0.0011675969492410047} + 'R_s': 0.098563, + 'R_sh_ref': 268.508, + 'a_ref': 1.2934, + 'Adjust': 0.0065145} assert np.all([np.isclose(trina660_cec_est[k], cec_expected[k], - rtol=1e-3) + rtol=1e-4, atol=0.) for k in cec_expected]) From facdd7e616306412e49349f0c7684f9ba55abf45 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Mon, 6 Oct 2025 08:56:43 -0700 Subject: [PATCH 19/25] indents --- tests/ivtools/test_convert.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/ivtools/test_convert.py b/tests/ivtools/test_convert.py index 8b5d3e7f5a..c20576825b 100644 --- a/tests/ivtools/test_convert.py +++ b/tests/ivtools/test_convert.py @@ -11,16 +11,16 @@ def test_convert_cec_pvsyst(): trina660_pvsyst_est = convert.convert_cec_pvsyst(trina660_cec, cells_in_series) pvsyst_expected = {'alpha_sc': 0.0096671, - 'I_L_ref': 18.19305, - 'I_o_ref': 6.94494e-12, - 'EgRef': 1.121, - 'R_s': 0.16318, - 'R_sh_ref': 1000.947, - 'R_sh_0': 8593.35, - 'R_sh_exp': 5.5, - 'gamma_ref': 1.0724, - 'mu_gamma': -0.00074595, - 'cells_in_series': 66} + 'I_L_ref': 18.19305, + 'I_o_ref': 6.94494e-12, + 'EgRef': 1.121, + 'R_s': 0.16318, + 'R_sh_ref': 1000.947, + 'R_sh_0': 8593.35, + 'R_sh_exp': 5.5, + 'gamma_ref': 1.0724, + 'mu_gamma': -0.00074595, + 'cells_in_series': 66} assert np.all([np.isclose(trina660_pvsyst_est[k], pvsyst_expected[k], rtol=1e-4, atol=0.) From ff20bef227b05a4e547e655e4345d63910f76cd4 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Mon, 6 Oct 2025 08:59:38 -0700 Subject: [PATCH 20/25] format --- pvlib/ivtools/convert.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pvlib/ivtools/convert.py b/pvlib/ivtools/convert.py index 8627c40fcd..96a318b766 100644 --- a/pvlib/ivtools/convert.py +++ b/pvlib/ivtools/convert.py @@ -58,7 +58,6 @@ def _pvsyst_objfun(pvs_mod, cec_ivs, ee, tc, cs): pvsyst_ivs = singlediode(*pvs_params) - # calculate error metric, mean absolute relative error for PVsyst model as # the target isc_diff = np.abs((pvsyst_ivs['i_sc'] - cec_ivs['i_sc']) / @@ -77,7 +76,6 @@ def _pvsyst_objfun(pvs_mod, cec_ivs, ee, tc, cs): return mean_abs_diff - def convert_cec_pvsyst(cec_model, cells_in_series, initial=None, method='Nelder-Mead', bounds=None, options=None): @@ -242,7 +240,7 @@ def _cec_objfun(cec_mod, pvs_ivs, ee, tc, alpha_sc): return mean_diff -def convert_pvsyst_cec(pvsyst_model, initial=None, method='Nelder-Mead', +def convert_pvsyst_cec(pvsyst_model, initial=None, method='Nelder-Mead', bounds=None, options=None, EgRef=1.121, dEgdT=-0.0002677): r""" From 92dcb4dd230281d300315c6341cb8262b1ae235f Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Mon, 6 Oct 2025 11:18:45 -0700 Subject: [PATCH 21/25] add some print statements to debug test failure --- tests/ivtools/test_convert.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/ivtools/test_convert.py b/tests/ivtools/test_convert.py index c20576825b..0afff807d4 100644 --- a/tests/ivtools/test_convert.py +++ b/tests/ivtools/test_convert.py @@ -22,6 +22,10 @@ def test_convert_cec_pvsyst(): 'mu_gamma': -0.00074595, 'cells_in_series': 66} + print(pvsyst_expected) + print("-----------") + print(trina660_pvsyst_est) + assert np.all([np.isclose(trina660_pvsyst_est[k], pvsyst_expected[k], rtol=1e-4, atol=0.) for k in pvsyst_expected]) From 1d4d40c73683040b5d5876d67a8f1fa710077a74 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Mon, 6 Oct 2025 11:59:53 -0700 Subject: [PATCH 22/25] rtol by parameter --- tests/ivtools/test_convert.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/ivtools/test_convert.py b/tests/ivtools/test_convert.py index 0afff807d4..2d3e44e6fa 100644 --- a/tests/ivtools/test_convert.py +++ b/tests/ivtools/test_convert.py @@ -22,12 +22,22 @@ def test_convert_cec_pvsyst(): 'mu_gamma': -0.00074595, 'cells_in_series': 66} - print(pvsyst_expected) - print("-----------") - print(trina660_pvsyst_est) + # set up dict of rtol, because some parameters are more sensitive to + # optimization process than others + rtol_d = {'alpha_sc': 1e-4, + 'I_L_ref': 1e-4, + 'I_o_ref': 1e-4, + 'EgRef': 1e-4, + 'R_s': 1e-4, + 'R_sh_ref': 1e-4, + 'R_sh_0': 1e-1, + 'R_sh_exp': 1e-3, + 'gamma_ref': 1e-4, + 'mu_gamma': 1e-4, + 'cells_in_series': 1e-8} assert np.all([np.isclose(trina660_pvsyst_est[k], pvsyst_expected[k], - rtol=1e-4, atol=0.) + rtol=rtol_d[k], atol=0.) for k in pvsyst_expected]) From 75a55f7c09070e0f7176702f0aaafa0d0f141bbf Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Mon, 6 Oct 2025 12:06:56 -0700 Subject: [PATCH 23/25] format --- tests/ivtools/test_convert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ivtools/test_convert.py b/tests/ivtools/test_convert.py index 2d3e44e6fa..e25c332b0e 100644 --- a/tests/ivtools/test_convert.py +++ b/tests/ivtools/test_convert.py @@ -35,7 +35,7 @@ def test_convert_cec_pvsyst(): 'gamma_ref': 1e-4, 'mu_gamma': 1e-4, 'cells_in_series': 1e-8} - + assert np.all([np.isclose(trina660_pvsyst_est[k], pvsyst_expected[k], rtol=rtol_d[k], atol=0.) for k in pvsyst_expected]) From a754adf280f829319e0b308b9ed15f07cf6eec20 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 9 Oct 2025 14:41:09 -0700 Subject: [PATCH 24/25] Update docs/sphinx/source/whatsnew/v0.13.2.rst Co-authored-by: Kevin Anderson --- docs/sphinx/source/whatsnew/v0.13.2.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.13.2.rst b/docs/sphinx/source/whatsnew/v0.13.2.rst index 9916c28ce6..175bde3269 100644 --- a/docs/sphinx/source/whatsnew/v0.13.2.rst +++ b/docs/sphinx/source/whatsnew/v0.13.2.rst @@ -27,9 +27,7 @@ Enhancements :py:func:`~pvlib.singlediode.bishop88_mpp`, :py:func:`~pvlib.singlediode.bishop88_v_from_i`, and :py:func:`~pvlib.singlediode.bishop88_i_from_v`. (:issue:`2497`, :pull:`2498`) -* Add capability for :py:func:`~pvlib.singlediode._lambertw` to use scipy - find_minimum to get the maximum power point instead of pvlib's golden - mean search. (:issue:`2497`, :pull:`2567`) +* Accelerate :py:func:`~pvlib.pvsystem.singlediode` when scipy>=1.15 is installed. (:issue:`2497`, :pull:`2567`) Documentation From 9ff3970de2a57dcc4b9001f9c1557aa9b8d65f3c Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Fri, 10 Oct 2025 08:18:34 -0700 Subject: [PATCH 25/25] undo bad pull --- docs/sphinx/source/whatsnew/v0.13.2.rst | 3 ++- pvlib/singlediode.py | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.13.2.rst b/docs/sphinx/source/whatsnew/v0.13.2.rst index cc8b96ac0d..bd7687d323 100644 --- a/docs/sphinx/source/whatsnew/v0.13.2.rst +++ b/docs/sphinx/source/whatsnew/v0.13.2.rst @@ -27,7 +27,8 @@ Enhancements :py:func:`~pvlib.singlediode.bishop88_mpp`, :py:func:`~pvlib.singlediode.bishop88_v_from_i`, and :py:func:`~pvlib.singlediode.bishop88_i_from_v`. (:issue:`2497`, :pull:`2498`) -* Accelerate :py:func:`~pvlib.pvsystem.singlediode` when scipy>=1.15 is installed. (:issue:`2497`, :pull:`2567`) +* Accelerate :py:func:`~pvlib.pvsystem.singlediode` when scipy>=1.15 is + installed. (:issue:`2497`, :pull:`2567`) Documentation diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index 0f79079b80..ad2d4b3243 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -930,9 +930,7 @@ def _lambertw(photocurrent, saturation_current, resistance_series, # switch to old golden section method p_mp, v_mp = _golden_sect_DataFrame(params, 0., v_oc * 1.14, _pwr_optfcn) - - # Find Imp using Lambert W - i_mp = _lambertw_i_from_v(v_mp, **params) + i_mp = p_mp / v_mp # Find Ix and Ixx using Lambert W i_x = _lambertw_i_from_v(0.5 * v_oc, **params)