From d2d4d91dcd28df9612c67712bdf6add003095bfa Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 1 Oct 2025 16:08:41 -0400 Subject: [PATCH 01/15] add function --- pvlib/pvarray.py | 91 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/pvlib/pvarray.py b/pvlib/pvarray.py index b156baa757..22a2e9e120 100644 --- a/pvlib/pvarray.py +++ b/pvlib/pvarray.py @@ -10,7 +10,7 @@ import numpy as np from scipy.optimize import curve_fit -from scipy.special import exp10 +from scipy.special import exp10, lambertw def pvefficiency_adr(effective_irradiance, temp_cell, @@ -394,3 +394,92 @@ def huld(effective_irradiance, temp_mod, pdc0, k=None, cell_type=None, k[5] * tprime**2 ) return pdc + + +def batzelis(effective_irradiance, temp_cell, + isc0, voc0, imp0, vmp0, alpha_sc, beta_voc): + """ + Compute maximum power point, open circuit, and short circuit + values using the Batzelis method. + + Batzelis's method [1]_ is a fast method of computing the maximum + power current and voltage. The calculations are rooted in the + single-diode equation, but only typical datasheet information + is required. + + Parameters + ---------- + effective_irradiance : numeric, non-negative + Effective irradiance incident on the PV module. [Wm⁻²] + temp_cell : numeric + PV module operating temperature. [°C] + isc0 : float + Short-circuit current at STC. [A] + voc0 : float + Open-circuit voltage at STC. [V] + imp0 : float + Maximum power point current at STC. [A] + vmp0 : float + Maximum power point voltage at STC. [V] + alpha_sc : float + Short-circuit current temperature coefficient at STC. [1/K] + beta_voc : float + Open-circuit voltage temperature coefficient at STC. [1/K] + + Returns + ------- + dict + The returned dict-like object always contains the keys/columns: + + * i_sc - short circuit current in amperes. + * v_oc - open circuit voltage in volts. + * i_mp - current at maximum power point in amperes. + * v_mp - voltage at maximum power point in volts. + * p_mp - power at maximum power point in watts. + + Notes + ----- + The ``alpha_sc`` and ``beta_voc`` temperature coefficient parameters + must be given as normalized values. + + References + ---------- + .. [1] E. I. Batzelis, "Simple PV Performance Equations Theoretically Well + Founded on the Single-Diode Model," Journal of Photovoltaics vol. 7, + no. 5, pp. 1400-1409, Sep 2017, :doi:`10.1109/JPHOTOV.2017.2711431` + """ + + t0 = 298.15 + delT = temp_cell - (t0 - 273.15) + lamT = (temp_cell + 273.15) / t0 + g = effective_irradiance / 1000 + lnG = np.log(g) + + # Eq 9-10 + del0 = (1 - beta_voc * t0) / (50.1 - alpha_sc * t0) + w0 = lambertw(np.exp(1/del0 + 1)).real + + # Eqs 27-28 + alpha_imp = alpha_sc + (beta_voc - 1/t0) / (w0 - 1) + beta_vmp = (voc0 / vmp0) * ( + beta_voc / (1 + del0) + + (del0 * (w0 - 1) - 1/(1 + del0)) / t0 + ) + + # Eq 26 + eps0 = (del0 / (1 + del0)) * (voc0 / vmp0) + eps1 = del0 * (w0 - 1) * (voc0 / vmp0) - 1 + + # Eqs 22-25 + isc = g * isc0 * (1 + alpha_sc * delT) + voc = voc0 * (1 + del0 * lamT * lnG + beta_voc * delT) + imp = g * imp0 * (1 + alpha_imp * delT) + vmp = vmp0 * (1 + eps0 * lamT * lnG + eps1 * (1 - g) + beta_vmp * delT) + + return { + 'p_mp': vmp * imp, + 'i_mp': imp, + 'v_mp': vmp, + 'i_sc': isc, + 'v_oc': voc, + } From c674f1b733a98411f2c14b4714baa7dc7a5c9a6d Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 2 Oct 2025 09:06:16 -0400 Subject: [PATCH 02/15] docs --- .../reference/pv_modeling/system_models.rst | 8 ++++ pvlib/pvarray.py | 41 ++++++++++++++----- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/docs/sphinx/source/reference/pv_modeling/system_models.rst b/docs/sphinx/source/reference/pv_modeling/system_models.rst index fb637ee8ed..3b8f29d0b2 100644 --- a/docs/sphinx/source/reference/pv_modeling/system_models.rst +++ b/docs/sphinx/source/reference/pv_modeling/system_models.rst @@ -55,3 +55,11 @@ PVGIS model :toctree: ../generated/ pvarray.huld + +Other +^^^^^ + +.. autosummary:: + :toctree: ../generated/ + + pvarray.batzelis diff --git a/pvlib/pvarray.py b/pvlib/pvarray.py index 22a2e9e120..61b3e21a16 100644 --- a/pvlib/pvarray.py +++ b/pvlib/pvarray.py @@ -400,12 +400,12 @@ def batzelis(effective_irradiance, temp_cell, isc0, voc0, imp0, vmp0, alpha_sc, beta_voc): """ Compute maximum power point, open circuit, and short circuit - values using the Batzelis method. + values using Batzelis's method. - Batzelis's method [1]_ is a fast method of computing the maximum - power current and voltage. The calculations are rooted in the - single-diode equation, but only typical datasheet information - is required. + Batzelis's method (described in Section III of [1]_) is a fast method + of computing the maximum power current and voltage. The calculations + are rooted in the De Soto single-diode model, but require only typical + datasheet information. Parameters ---------- @@ -431,22 +431,43 @@ def batzelis(effective_irradiance, temp_cell, dict The returned dict-like object always contains the keys/columns: - * i_sc - short circuit current in amperes. - * v_oc - open circuit voltage in volts. - * i_mp - current at maximum power point in amperes. - * v_mp - voltage at maximum power point in volts. - * p_mp - power at maximum power point in watts. + * p_mp - power at maximum power point. [W] + * i_mp - current at maximum power point. [A] + * v_mp - voltage at maximum power point. [V] + * i_sc - short circuit current. [A] + * v_oc - open circuit voltage. [V] Notes ----- + This method is the combination of three sub-methods for: + + 1. estimating single-diode model parameters from datasheet information + 2. translating SDM parameters from STC to operating conditions + (taken from the De Soto model) + 3. estimating the MPP, OC, and SC points on the resulting I-V curve. + The ``alpha_sc`` and ``beta_voc`` temperature coefficient parameters must be given as normalized values. + At extremely low irradiance (e.g. 1e-10 Wm⁻²), this model can produce + negative voltages. This function clips any negative voltages to zero. + References ---------- .. [1] E. I. Batzelis, "Simple PV Performance Equations Theoretically Well Founded on the Single-Diode Model," Journal of Photovoltaics vol. 7, no. 5, pp. 1400-1409, Sep 2017, :doi:`10.1109/JPHOTOV.2017.2711431` + + Examples + -------- + >>> params = {'isc0': 15.98, 'voc0': 50.26, 'imp0': 15.27, 'vmp0': 42.57, + ... 'alpha_sc': 0.00046, 'beta_voc': -0.0024} + >>> batzelis(np.array([1000, 800]), np.array([25, 30]), **params) + {'p_mp': array([650.0439 , 512.99195952]), + 'i_mp': array([15.27 , 12.23049227]), + 'v_mp': array([42.57 , 41.94368864]), + 'i_sc': array([15.98 , 12.8134032]), + 'v_oc': array([50.26 , 49.26532905])} """ t0 = 298.15 From d8988c668c3fda9a7bf776ef498adc4ee8b8b99d Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 2 Oct 2025 09:06:52 -0400 Subject: [PATCH 03/15] tests --- pvlib/pvarray.py | 10 +++++++++- tests/test_pvarray.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/pvlib/pvarray.py b/pvlib/pvarray.py index 61b3e21a16..dd8f6d54ed 100644 --- a/pvlib/pvarray.py +++ b/pvlib/pvarray.py @@ -474,7 +474,11 @@ def batzelis(effective_irradiance, temp_cell, delT = temp_cell - (t0 - 273.15) lamT = (temp_cell + 273.15) / t0 g = effective_irradiance / 1000 - lnG = np.log(g) + # for zero/negative irradiance, use lnG=large negative number so that + # computed voltages are negative and then clipped to zero + with np.errstate(divide='ignore'): # needed for pandas for some reason + lnG = np.log(g, out=np.full_like(g, -9e9), where=g>0) + lnG = np.where(np.isfinite(g), lnG, np.nan) # also preserve nans # Eq 9-10 del0 = (1 - beta_voc * t0) / (50.1 - alpha_sc * t0) @@ -497,6 +501,10 @@ def batzelis(effective_irradiance, temp_cell, imp = g * imp0 * (1 + alpha_imp * delT) vmp = vmp0 * (1 + eps0 * lamT * lnG + eps1 * (1 - g) + beta_vmp * delT) + # handle negative voltages from zero and extremely small irradiance + vmp = np.clip(vmp, a_min=0, a_max=None) + voc = np.clip(voc, a_min=0, a_max=None) + return { 'p_mp': vmp * imp, 'i_mp': imp, diff --git a/tests/test_pvarray.py b/tests/test_pvarray.py index a1fc1c4ac3..df73e8fe24 100644 --- a/tests/test_pvarray.py +++ b/tests/test_pvarray.py @@ -1,4 +1,5 @@ import numpy as np +from numpy import nan import pandas as pd from numpy.testing import assert_allclose from .conftest import assert_series_equal @@ -114,3 +115,40 @@ def test_huld_errors(): pvarray.huld( eff_irr, temp_mod, pdc0, cell_type='csi', k_version='2021' ) + + +def test_batzelis(): + params = {'isc0': 15.98, 'voc0': 50.26, 'imp0': 15.27, 'vmp0': 42.57, + 'alpha_sc': 0.00046, 'beta_voc': -0.0024} + g = np.array([1000, 500, 1200, 500, 1200, 0, nan, 1000]) + t = np.array([25, 20, 20, 50, 50, 25, 0, nan]) + expected = { # these values were computed using the function itself + 'p_mp': [650.044, 328.599, 789.136, 300.079, 723.401, 0, nan, nan], + 'i_mp': [ 15.270, 7.626, 18.302, 7.680, 18.433, 0, nan, nan], + 'v_mp': [ 42.570, 43.090, 43.117, 39.071, 39.246, 0, nan, nan], + 'i_sc': [ 15.980, 7.972, 19.132, 8.082, 19.397, 0, nan, nan], + 'v_oc': [ 50.260, 49.687, 51.172, 45.948, 47.585, 0, nan, nan], + } + + # numpy array + actual = pvarray.batzelis(g, t, **params) + for key, exp in expected.items(): + np.testing.assert_allclose(actual[key], exp, atol=1e-3) + + # pandas series + actual = pvarray.batzelis(pd.Series(g), pd.Series(t), **params) + for key, exp in expected.items(): + pd.testing.assert_series_equal(actual[key], pd.Series(exp), atol=1e-3) + + # scalar + actual = pvarray.batzelis(g[1], t[1], **params) + for key, exp in expected.items(): + assert pytest.approx(exp[1], abs=1e-3) == actual[key] + + +def test_batzelis_negative_voltage(): + params = {'isc0': 15.98, 'voc0': 50.26, 'imp0': 15.27, 'vmp0': 42.57, + 'alpha_sc': 0.00046, 'beta_voc': -0.0024} + actual = pvarray.batzelis(1e-10, 25, **params) + assert actual['v_mp'] == 0 + assert actual['v_oc'] == 0 From e24e62ffdcf43bfc42ffeb6551454b16b89b409f Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 2 Oct 2025 12:24:26 -0400 Subject: [PATCH 04/15] add SDM parameter estimation and key point functions --- .../source/reference/pv_modeling/sdm.rst | 2 + pvlib/ivtools/sdm/__init__.py | 4 ++ pvlib/ivtools/sdm/batzelis.py | 68 ++++++++++++++++++ pvlib/pvarray.py | 13 ++-- pvlib/singlediode.py | 70 +++++++++++++++++++ 5 files changed, 151 insertions(+), 6 deletions(-) create mode 100644 pvlib/ivtools/sdm/batzelis.py diff --git a/docs/sphinx/source/reference/pv_modeling/sdm.rst b/docs/sphinx/source/reference/pv_modeling/sdm.rst index bfd5103ebe..2aff477883 100644 --- a/docs/sphinx/source/reference/pv_modeling/sdm.rst +++ b/docs/sphinx/source/reference/pv_modeling/sdm.rst @@ -17,6 +17,7 @@ Functions relevant for single diode models. pvsystem.v_from_i pvsystem.max_power_point ivtools.sdm.pvsyst_temperature_coeff + singlediode.batzelis_keypoints Low-level functions for solving the single diode equation. @@ -37,3 +38,4 @@ Functions for fitting diode models ivtools.sde.fit_sandia_simple ivtools.sdm.fit_cec_sam ivtools.sdm.fit_desoto + ivtools.sdm.fit_desoto_batzelis diff --git a/pvlib/ivtools/sdm/__init__.py b/pvlib/ivtools/sdm/__init__.py index 8535f1f5e6..536eed80be 100644 --- a/pvlib/ivtools/sdm/__init__.py +++ b/pvlib/ivtools/sdm/__init__.py @@ -4,6 +4,10 @@ fitting method. """ +from pvlib.ivtools.sdm.batzelis import ( # noqa: F401 + fit_desoto_batzelis, +) + from pvlib.ivtools.sdm.cec import ( # noqa: F401 fit_cec_sam, ) diff --git a/pvlib/ivtools/sdm/batzelis.py b/pvlib/ivtools/sdm/batzelis.py new file mode 100644 index 0000000000..1c6cd44ee4 --- /dev/null +++ b/pvlib/ivtools/sdm/batzelis.py @@ -0,0 +1,68 @@ +""" +Batzelis's method for estimating single-diode model parameters from +datasheet values. +""" + +import numpy as np +from scipy.special import lambertw + + +def fit_desoto_batzelis(isc0, voc0, imp0, vmp0, alpha_sc, beta_voc): + """ + Determine De Soto single-diode model parameters from datasheet values + using Batzelis's method. + + This method is described in Section II.C of [1]_. + + Parameters + ---------- + isc0 : float + Short-circuit current at STC. [A] + voc0 : float + Open-circuit voltage at STC. [V] + imp0 : float + Maximum power point current at STC. [A] + vmp0 : float + Maximum power point voltage at STC. [V] + alpha_sc : float + Short-circuit current temperature coefficient at STC. [1/K] + beta_voc : float + Open-circuit voltage temperature coefficient at STC. [1/K] + + Returns + ------- + dict + The returned dict contains the keys: + + * ``alpha_sc`` [A/K] + * ``a_ref`` [V] + * ``I_L_ref`` [A] + * ``I_o_ref`` [A] + * ``R_sh_ref`` [Ohm] + * ``R_s`` [Ohm] + + References + ---------- + .. [1] E. I. Batzelis, "Simple PV Performance Equations Theoretically Well + Founded on the Single-Diode Model," Journal of Photovoltaics vol. 7, + no. 5, pp. 1400-1409, Sep 2017, :doi:`10.1109/JPHOTOV.2017.2711431` + """ + t0 = 298.15 # K + del0 = (1 - beta_voc * t0) / (50.1 - alpha_sc * t0) # Eq 9 + w0 = lambertw(np.exp(1/del0 + 1)).real + + # Eqs 11-15 + a0 = del0 * voc0 + Rs0 = (a0 * (w0 - 1) - vmp0) / imp0 + Rsh0 = a0 * (w0 - 1) / (isc0 * (1 - 1/w0) - imp0) + Iph0 = (1 + Rs0 / Rsh0) * isc0 + Isat0 = Iph0 * np.exp(-1/del0) + + return { + 'alpha_sc': alpha_sc * isc0, # convert 1/K to A/K + 'a_ref': a0, + 'I_L_ref': Iph0, + 'I_o_ref': Isat0, + 'R_sh_ref': Rsh0, + 'R_s': Rs0, + } diff --git a/pvlib/pvarray.py b/pvlib/pvarray.py index dd8f6d54ed..7f64840052 100644 --- a/pvlib/pvarray.py +++ b/pvlib/pvarray.py @@ -429,13 +429,13 @@ def batzelis(effective_irradiance, temp_cell, Returns ------- dict - The returned dict-like object always contains the keys/columns: + The returned dict-like object contains the keys/columns: - * p_mp - power at maximum power point. [W] - * i_mp - current at maximum power point. [A] - * v_mp - voltage at maximum power point. [V] - * i_sc - short circuit current. [A] - * v_oc - open circuit voltage. [V] + * ``p_mp`` - power at maximum power point. [W] + * ``i_mp`` - current at maximum power point. [A] + * ``v_mp`` - voltage at maximum power point. [V] + * ``i_sc`` - short circuit current. [A] + * ``v_oc`` - open circuit voltage. [V] Notes ----- @@ -505,6 +505,7 @@ def batzelis(effective_irradiance, temp_cell, vmp = np.clip(vmp, a_min=0, a_max=None) voc = np.clip(voc, a_min=0, a_max=None) + # TODO return dataframe for is_pandas return { 'p_mp': vmp * imp, 'i_mp': imp, diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index ff3b9497a6..a8dfd8cb7e 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -861,3 +861,73 @@ def _pwr_optfcn(df, loc): df['resistance_shunt'], df['nNsVth']) return current * df[loc] + + +def batzelis_keypoints(photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth): + """ + Estimate maximum power, open-circuit, and short-circuit values + using Batzelis's method. + + This method is described in Section II.B of [1]_. + + Parameters + ---------- + photocurrent : numeric + Light-generated current. [A] + + saturation_current : numeric + Diode saturation current. [A] + + resistance_series : numeric + Series resistance. [Ohm] + + resistance_shunt : numeric + Shunt resistance. [Ohm] + + nNsVth : numeric + The product of the usual diode ideality factor (n, unitless), + number of cells in series (Ns), and cell thermal voltage at + specified effective irradiance and cell temperature. [V] + + Returns + ------- + dict + The returned dict-like object contains the keys/columns: + + * ``p_mp`` - power at maximum power point. [W] + * ``i_mp`` - current at maximum power point. [A] + * ``v_mp`` - voltage at maximum power point. [V] + * ``i_sc`` - short circuit current. [A] + * ``v_oc`` - open circuit voltage. [V] + + References + ---------- + .. [1] E. I. Batzelis, "Simple PV Performance Equations Theoretically Well + Founded on the Single-Diode Model," Journal of Photovoltaics vol. 7, + no. 5, pp. 1400-1409, Sep 2017, :doi:`10.1109/JPHOTOV.2017.2711431` + """ + # convenience variables + Iph = photocurrent + Is = saturation_current + Rsh = resistance_shunt + Rs = resistance_series + a = nNsVth + + # Eqs 3-4 + isc = Iph * Rsh / (Rs + Rsh) + voc = a * np.log(Iph / Is) + + # Eqs 5-8 + w = lambertw(np.e * Iph / Is).real + #vmp = (1 + Rs/Rsh) * a * (w - 1) - Rs * Iph * (1 - 1/w) # not needed + imp = Iph * (1 - 1/w) - a * (w - 1) / Rsh + vmp = a * (w - 1) - Rs * imp + + return { + 'p_mp': imp * vmp, + 'i_mp': imp, + 'v_mp': vmp, + 'i_sc': isc, + 'v_oc': voc, + } From 994a85301327b59718babf3abc13f2a7100da211 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 2 Oct 2025 12:40:10 -0400 Subject: [PATCH 05/15] lint --- pvlib/ivtools/sdm/batzelis.py | 2 +- pvlib/pvarray.py | 6 +++--- pvlib/singlediode.py | 2 +- tests/test_pvarray.py | 3 ++- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pvlib/ivtools/sdm/batzelis.py b/pvlib/ivtools/sdm/batzelis.py index 1c6cd44ee4..fe37c96264 100644 --- a/pvlib/ivtools/sdm/batzelis.py +++ b/pvlib/ivtools/sdm/batzelis.py @@ -44,7 +44,7 @@ def fit_desoto_batzelis(isc0, voc0, imp0, vmp0, alpha_sc, beta_voc): References ---------- .. [1] E. I. Batzelis, "Simple PV Performance Equations Theoretically Well - Founded on the Single-Diode Model," Journal of Photovoltaics vol. 7, + Founded on the Single-Diode Model," Journal of Photovoltaics vol. 7, no. 5, pp. 1400-1409, Sep 2017, :doi:`10.1109/JPHOTOV.2017.2711431` """ t0 = 298.15 # K diff --git a/pvlib/pvarray.py b/pvlib/pvarray.py index 7f64840052..d47a7111a6 100644 --- a/pvlib/pvarray.py +++ b/pvlib/pvarray.py @@ -424,7 +424,7 @@ def batzelis(effective_irradiance, temp_cell, alpha_sc : float Short-circuit current temperature coefficient at STC. [1/K] beta_voc : float - Open-circuit voltage temperature coefficient at STC. [1/K] + Open-circuit voltage temperature coefficient at STC. [1/K] Returns ------- @@ -455,7 +455,7 @@ def batzelis(effective_irradiance, temp_cell, References ---------- .. [1] E. I. Batzelis, "Simple PV Performance Equations Theoretically Well - Founded on the Single-Diode Model," Journal of Photovoltaics vol. 7, + Founded on the Single-Diode Model," Journal of Photovoltaics vol. 7, no. 5, pp. 1400-1409, Sep 2017, :doi:`10.1109/JPHOTOV.2017.2711431` Examples @@ -477,7 +477,7 @@ def batzelis(effective_irradiance, temp_cell, # for zero/negative irradiance, use lnG=large negative number so that # computed voltages are negative and then clipped to zero with np.errstate(divide='ignore'): # needed for pandas for some reason - lnG = np.log(g, out=np.full_like(g, -9e9), where=g>0) + lnG = np.log(g, out=np.full_like(g, -9e9), where=(g > 0)) lnG = np.where(np.isfinite(g), lnG, np.nan) # also preserve nans # Eq 9-10 diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index a8dfd8cb7e..1c80b66842 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -904,7 +904,7 @@ def batzelis_keypoints(photocurrent, saturation_current, resistance_series, References ---------- .. [1] E. I. Batzelis, "Simple PV Performance Equations Theoretically Well - Founded on the Single-Diode Model," Journal of Photovoltaics vol. 7, + Founded on the Single-Diode Model," Journal of Photovoltaics vol. 7, no. 5, pp. 1400-1409, Sep 2017, :doi:`10.1109/JPHOTOV.2017.2711431` """ # convenience variables diff --git a/tests/test_pvarray.py b/tests/test_pvarray.py index df73e8fe24..747090fe5e 100644 --- a/tests/test_pvarray.py +++ b/tests/test_pvarray.py @@ -129,7 +129,7 @@ def test_batzelis(): 'i_sc': [ 15.980, 7.972, 19.132, 8.082, 19.397, 0, nan, nan], 'v_oc': [ 50.260, 49.687, 51.172, 45.948, 47.585, 0, nan, nan], } - + # numpy array actual = pvarray.batzelis(g, t, **params) for key, exp in expected.items(): @@ -152,3 +152,4 @@ def test_batzelis_negative_voltage(): actual = pvarray.batzelis(1e-10, 25, **params) assert actual['v_mp'] == 0 assert actual['v_oc'] == 0 + From 6f94408fb12a3919de4b429dbe24578529d79a06 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 2 Oct 2025 13:54:10 -0400 Subject: [PATCH 06/15] tests for the two additional functions --- pvlib/ivtools/sdm/__init__.py | 7 +--- pvlib/ivtools/sdm/batzelis.py | 68 -------------------------------- pvlib/ivtools/sdm/desoto.py | 62 +++++++++++++++++++++++++++++ pvlib/pvarray.py | 16 ++++++-- pvlib/singlediode.py | 29 +++++++++++--- tests/ivtools/sdm/test_desoto.py | 25 ++++++++++++ tests/test_pvarray.py | 3 +- tests/test_singlediode.py | 48 +++++++++++++++++++++- 8 files changed, 173 insertions(+), 85 deletions(-) delete mode 100644 pvlib/ivtools/sdm/batzelis.py diff --git a/pvlib/ivtools/sdm/__init__.py b/pvlib/ivtools/sdm/__init__.py index 536eed80be..2bb8b9876b 100644 --- a/pvlib/ivtools/sdm/__init__.py +++ b/pvlib/ivtools/sdm/__init__.py @@ -4,17 +4,14 @@ fitting method. """ -from pvlib.ivtools.sdm.batzelis import ( # noqa: F401 - fit_desoto_batzelis, -) - from pvlib.ivtools.sdm.cec import ( # noqa: F401 fit_cec_sam, ) from pvlib.ivtools.sdm.desoto import ( # noqa: F401 fit_desoto, - fit_desoto_sandia + fit_desoto_batzelis, + fit_desoto_sandia, ) from pvlib.ivtools.sdm.pvsyst import ( # noqa: F401 diff --git a/pvlib/ivtools/sdm/batzelis.py b/pvlib/ivtools/sdm/batzelis.py deleted file mode 100644 index fe37c96264..0000000000 --- a/pvlib/ivtools/sdm/batzelis.py +++ /dev/null @@ -1,68 +0,0 @@ -""" -Batzelis's method for estimating single-diode model parameters from -datasheet values. -""" - -import numpy as np -from scipy.special import lambertw - - -def fit_desoto_batzelis(isc0, voc0, imp0, vmp0, alpha_sc, beta_voc): - """ - Determine De Soto single-diode model parameters from datasheet values - using Batzelis's method. - - This method is described in Section II.C of [1]_. - - Parameters - ---------- - isc0 : float - Short-circuit current at STC. [A] - voc0 : float - Open-circuit voltage at STC. [V] - imp0 : float - Maximum power point current at STC. [A] - vmp0 : float - Maximum power point voltage at STC. [V] - alpha_sc : float - Short-circuit current temperature coefficient at STC. [1/K] - beta_voc : float - Open-circuit voltage temperature coefficient at STC. [1/K] - - Returns - ------- - dict - The returned dict contains the keys: - - * ``alpha_sc`` [A/K] - * ``a_ref`` [V] - * ``I_L_ref`` [A] - * ``I_o_ref`` [A] - * ``R_sh_ref`` [Ohm] - * ``R_s`` [Ohm] - - References - ---------- - .. [1] E. I. Batzelis, "Simple PV Performance Equations Theoretically Well - Founded on the Single-Diode Model," Journal of Photovoltaics vol. 7, - no. 5, pp. 1400-1409, Sep 2017, :doi:`10.1109/JPHOTOV.2017.2711431` - """ - t0 = 298.15 # K - del0 = (1 - beta_voc * t0) / (50.1 - alpha_sc * t0) # Eq 9 - w0 = lambertw(np.exp(1/del0 + 1)).real - - # Eqs 11-15 - a0 = del0 * voc0 - Rs0 = (a0 * (w0 - 1) - vmp0) / imp0 - Rsh0 = a0 * (w0 - 1) / (isc0 * (1 - 1/w0) - imp0) - Iph0 = (1 + Rs0 / Rsh0) * isc0 - Isat0 = Iph0 * np.exp(-1/del0) - - return { - 'alpha_sc': alpha_sc * isc0, # convert 1/K to A/K - 'a_ref': a0, - 'I_L_ref': Iph0, - 'I_o_ref': Isat0, - 'R_sh_ref': Rsh0, - 'R_s': Rs0, - } diff --git a/pvlib/ivtools/sdm/desoto.py b/pvlib/ivtools/sdm/desoto.py index 6b7fa619ca..3c7ad6ee04 100644 --- a/pvlib/ivtools/sdm/desoto.py +++ b/pvlib/ivtools/sdm/desoto.py @@ -2,6 +2,7 @@ from scipy import constants from scipy import optimize +from scipy.special import lambertw from pvlib.ivtools.utils import rectify_iv_curve from pvlib.ivtools.sde import _fit_sandia_cocontent @@ -399,3 +400,64 @@ def _fit_desoto_sandia_diode(ee, voc, vth, tc, specs, const): new_x = sm.add_constant(x) res = sm.RLM(y, new_x).fit() return np.array(res.params)[1] + + +def fit_desoto_batzelis(isc0, voc0, imp0, vmp0, alpha_sc, beta_voc): + """ + Determine De Soto single-diode model parameters from datasheet values + using Batzelis's method. + + This method is described in Section II.C of [1]_. + + Parameters + ---------- + isc0 : float + Short-circuit current at STC. [A] + voc0 : float + Open-circuit voltage at STC. [V] + imp0 : float + Maximum power point current at STC. [A] + vmp0 : float + Maximum power point voltage at STC. [V] + alpha_sc : float + Short-circuit current temperature coefficient at STC. [1/K] + beta_voc : float + Open-circuit voltage temperature coefficient at STC. [1/K] + + Returns + ------- + dict + The returned dict contains the keys: + + * ``alpha_sc`` [A/K] + * ``a_ref`` [V] + * ``I_L_ref`` [A] + * ``I_o_ref`` [A] + * ``R_sh_ref`` [Ohm] + * ``R_s`` [Ohm] + + References + ---------- + .. [1] E. I. Batzelis, "Simple PV Performance Equations Theoretically Well + Founded on the Single-Diode Model," Journal of Photovoltaics vol. 7, + no. 5, pp. 1400-1409, Sep 2017, :doi:`10.1109/JPHOTOV.2017.2711431` + """ + t0 = 298.15 # K + del0 = (1 - beta_voc * t0) / (50.1 - alpha_sc * t0) # Eq 9 + w0 = np.real(lambertw(np.exp(1/del0 + 1))) + + # Eqs 11-15 + a0 = del0 * voc0 + Rs0 = (a0 * (w0 - 1) - vmp0) / imp0 + Rsh0 = a0 * (w0 - 1) / (isc0 * (1 - 1/w0) - imp0) + Iph0 = (1 + Rs0 / Rsh0) * isc0 + Isat0 = Iph0 * np.exp(-1/del0) + + return { + 'alpha_sc': alpha_sc * isc0, # convert 1/K to A/K + 'a_ref': a0, + 'I_L_ref': Iph0, + 'I_o_ref': Isat0, + 'R_sh_ref': Rsh0, + 'R_s': Rs0, + } diff --git a/pvlib/pvarray.py b/pvlib/pvarray.py index d47a7111a6..36ee973aac 100644 --- a/pvlib/pvarray.py +++ b/pvlib/pvarray.py @@ -9,6 +9,7 @@ """ import numpy as np +import pandas as pd from scipy.optimize import curve_fit from scipy.special import exp10, lambertw @@ -482,7 +483,7 @@ def batzelis(effective_irradiance, temp_cell, # Eq 9-10 del0 = (1 - beta_voc * t0) / (50.1 - alpha_sc * t0) - w0 = lambertw(np.exp(1/del0 + 1)).real + w0 = np.real(lambertw(np.exp(1/del0 + 1))) # Eqs 27-28 alpha_imp = alpha_sc + (beta_voc - 1/t0) / (w0 - 1) @@ -505,11 +506,20 @@ def batzelis(effective_irradiance, temp_cell, vmp = np.clip(vmp, a_min=0, a_max=None) voc = np.clip(voc, a_min=0, a_max=None) - # TODO return dataframe for is_pandas - return { + out = { 'p_mp': vmp * imp, 'i_mp': imp, 'v_mp': vmp, 'i_sc': isc, 'v_oc': voc, } + + # if pandas in, ensure pandas out + pandas_inputs = [ + x for x in [effective_irradiance, temp_cell] + if isinstance(x, pd.Series) + ] + if pandas_inputs: + out = pd.DataFrame(out, index=pandas_inputs[0].index) + + return out diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index 1c80b66842..b2049648ce 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -3,6 +3,7 @@ """ import numpy as np +import pandas as pd from pvlib.tools import _golden_sect_DataFrame from scipy.optimize import brentq, newton @@ -866,8 +867,8 @@ def _pwr_optfcn(df, loc): def batzelis_keypoints(photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth): """ - Estimate maximum power, open-circuit, and short-circuit values - using Batzelis's method. + Estimate maximum power, open-circuit, and short-circuit points from + single-diode equation parameters using Batzelis's method. This method is described in Section II.B of [1]_. @@ -916,18 +917,34 @@ def batzelis_keypoints(photocurrent, saturation_current, resistance_series, # Eqs 3-4 isc = Iph * Rsh / (Rs + Rsh) - voc = a * np.log(Iph / Is) + with np.errstate(divide='ignore'): # zero Iph + voc = a * np.log(Iph / Is) # Eqs 5-8 - w = lambertw(np.e * Iph / Is).real + w = np.real(lambertw(np.e * Iph / Is)) #vmp = (1 + Rs/Rsh) * a * (w - 1) - Rs * Iph * (1 - 1/w) # not needed - imp = Iph * (1 - 1/w) - a * (w - 1) / Rsh + with np.errstate(divide='ignore', invalid='ignore'): # zero Iph -> zero w + imp = Iph * (1 - 1/w) - a * (w - 1) / Rsh + vmp = a * (w - 1) - Rs * imp - return { + vmp = np.where(Iph > 0, vmp, 0) + voc = np.where(Iph > 0, voc, 0) + imp = np.where(Iph > 0, imp, 0) + isc = np.where(Iph > 0, isc, 0) + + out = { 'p_mp': imp * vmp, 'i_mp': imp, 'v_mp': vmp, 'i_sc': isc, 'v_oc': voc, } + + # if pandas in, ensure pandas out + pandas_inputs = [ + x for x in [Iph, Is, Rsh, Rs, a] if isinstance(x, pd.Series)] + if pandas_inputs: + out = pd.DataFrame(out, index=pandas_inputs[0].index) + + return out diff --git a/tests/ivtools/sdm/test_desoto.py b/tests/ivtools/sdm/test_desoto.py index b861b26819..811685e76b 100644 --- a/tests/ivtools/sdm/test_desoto.py +++ b/tests/ivtools/sdm/test_desoto.py @@ -92,3 +92,28 @@ def test_fit_desoto_sandia(cec_params_cansol_cs5p_220p): assert_allclose(result['dEgdT'], -0.0002677) assert_allclose(result['EgRef'], 1.3112547292120638) assert_allclose(result['cells_in_series'], specs['cells_in_series']) + + +def test_fit_desoto_batzelis(): + params = {'isc0': 15.98, 'voc0': 50.26, 'imp0': 15.27, 'vmp0': 42.57, + 'alpha_sc': 0.00046, 'beta_voc': -0.0024} + expected = { # calculated with the function itself + 'alpha_sc': 0.0073508, + 'a_ref': 1.7257631194825132, + 'I_L_ref': 15.985408869737098, + 'I_o_ref': 3.594300567343102e-12, + 'R_sh_ref': 389.4378389153357, + 'R_s': 0.1318159287478395 + } + out = sdm.fit_desoto_batzelis(**params) + for k in expected: + assert out[k] == pytest.approx(expected[k]) + + # ensure the STC values are reproduced + iv = pvsystem.singlediode(out['I_L_ref'], out['I_o_ref'], out['R_s'], + out['R_sh_ref'], out['a_ref']) + assert iv['i_sc'] == pytest.approx(params['isc0']) + assert iv['i_mp'] == pytest.approx(params['imp0'], rel=3e-3) + assert iv['v_oc'] == pytest.approx(params['voc0'], rel=3e-4) + assert iv['v_mp'] == pytest.approx(params['vmp0'], rel=4e-3) + \ No newline at end of file diff --git a/tests/test_pvarray.py b/tests/test_pvarray.py index 747090fe5e..80ecc8dd19 100644 --- a/tests/test_pvarray.py +++ b/tests/test_pvarray.py @@ -137,8 +137,9 @@ def test_batzelis(): # pandas series actual = pvarray.batzelis(pd.Series(g), pd.Series(t), **params) + assert isinstance(actual, pd.DataFrame) for key, exp in expected.items(): - pd.testing.assert_series_equal(actual[key], pd.Series(exp), atol=1e-3) + np.testing.assert_allclose(actual[key], pd.Series(exp), atol=1e-3) # scalar actual = pvarray.batzelis(g[1], t[1], **params) diff --git a/tests/test_singlediode.py b/tests/test_singlediode.py index efded9ff3c..26e66ab496 100644 --- a/tests/test_singlediode.py +++ b/tests/test_singlediode.py @@ -7,7 +7,8 @@ import scipy from pvlib import pvsystem from pvlib.singlediode import (bishop88_mpp, estimate_voc, VOLTAGE_BUILTIN, - bishop88, bishop88_i_from_v, bishop88_v_from_i) + bishop88, bishop88_i_from_v, bishop88_v_from_i, + batzelis_keypoints) import pytest from numpy.testing import assert_array_equal from .conftest import TESTS_DATA_DIR @@ -600,9 +601,52 @@ def test_bishop88_init_cond(method): NsVbi=NsVbi)) bad_results = np.isnan(vmp2) | (vmp2 < 0) | (err > 0.00001) assert not bad_results.any() - # test v_from_i + # test i_from_v imp2 = bishop88_i_from_v(vmp, *sde_params, d2mutau=d2mutau, NsVbi=NsVbi) err = np.abs(_sde_check_solution(imp2, vmp, *sde_params, d2mutau=d2mutau, NsVbi=NsVbi)) bad_results = np.isnan(imp2) | (imp2 < 0) | (err > 0.00001) assert not bad_results.any() + + +def test_batzelis_keypoints(): + params = {'photocurrent': 10, 'saturation_current': 1e-10, + 'resistance_series': 0.2, 'resistance_shunt': 3000, + 'nNsVth': 1.7} + + exact_values = { # calculated using pvlib.pvsystem.singlediode + 'i_sc': 9.999333377550565, + 'v_oc': 43.05589965219406, + 'i_mp': 9.513255314772051, + 'v_mp': 35.97259289596944, + 'p_mp': 342.21646055371264, + } + rtol = 5e-3 # accurate to within half a percent in this case + + output = batzelis_keypoints(**params) + for key in exact_values: + assert output[key] == pytest.approx(exact_values[key], rel=rtol) + + # numpy arrays + params2 = {k: np.array([v] * 2) for k, v in params.items()} + output2 = batzelis_keypoints(**params2) + for key in exact_values: + exp = np.array([exact_values[key]] * 2) + np.testing.assert_allclose(output2[key], exp, rtol=rtol) + + # pandas + params3 = {k: pd.Series(v) for k, v in params2.items()} + output3 = batzelis_keypoints(**params3) + assert isinstance(output3, pd.DataFrame) + for key in exact_values: + exp = pd.Series([exact_values[key]] * 2) + np.testing.assert_allclose(output3[key], exp, rtol=rtol) + + +def test_batzelis_keypoints_night(): + # SDMs produce photocurrent=0 and resistance_shunt=inf at night + out = batzelis_keypoints(photocurrent=0, saturation_current=1e-10, + resistance_series=0.2, resistance_shunt=np.inf, + nNsVth=1.7) + for k, v in out.items(): + assert v == 0, k # ensure all outputs are zero (not nan, etc) From 1e61d098b88a080edf73c2133056b1f213828da5 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 2 Oct 2025 13:54:19 -0400 Subject: [PATCH 07/15] whatsnew --- docs/sphinx/source/whatsnew/v0.13.2.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.13.2.rst b/docs/sphinx/source/whatsnew/v0.13.2.rst index f649a93fd0..06f68a0246 100644 --- a/docs/sphinx/source/whatsnew/v0.13.2.rst +++ b/docs/sphinx/source/whatsnew/v0.13.2.rst @@ -18,6 +18,14 @@ Bug fixes Enhancements ~~~~~~~~~~~~ +* Add :py:func:`~pvlib.ivtools.sdm.fit_desoto_batzelis`, a function to estimate + parameters for the De Soto single-diode model from datasheet values. (:pull:`2563`) +* Add :py:func:`~pvlib.ivtools.singlediode.batzelis_keypoints`, a function to estimate + maximum power, open circuit, and short circuit points using parameters for + the single-diode equation. (:pull:`2563`) +* Add :py:func:`~pvlib.pvarray.batzelis`, a function to estimate maximum power + open circuit, and short circuit points from datasheet values. (:pull:`2563`) + Documentation @@ -42,4 +50,3 @@ Maintenance Contributors ~~~~~~~~~~~~ - From 7d431ab765e23dc4f43d0af2f4aa69ac03b729c2 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 2 Oct 2025 14:07:23 -0400 Subject: [PATCH 08/15] lint --- pvlib/ivtools/sdm/desoto.py | 2 +- pvlib/singlediode.py | 6 +----- tests/ivtools/sdm/test_desoto.py | 1 - tests/test_pvarray.py | 9 ++++----- 4 files changed, 6 insertions(+), 12 deletions(-) diff --git a/pvlib/ivtools/sdm/desoto.py b/pvlib/ivtools/sdm/desoto.py index 3c7ad6ee04..d35bf32cde 100644 --- a/pvlib/ivtools/sdm/desoto.py +++ b/pvlib/ivtools/sdm/desoto.py @@ -422,7 +422,7 @@ def fit_desoto_batzelis(isc0, voc0, imp0, vmp0, alpha_sc, beta_voc): alpha_sc : float Short-circuit current temperature coefficient at STC. [1/K] beta_voc : float - Open-circuit voltage temperature coefficient at STC. [1/K] + Open-circuit voltage temperature coefficient at STC. [1/K] Returns ------- diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index b2049648ce..641d40f2ef 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -876,16 +876,12 @@ def batzelis_keypoints(photocurrent, saturation_current, resistance_series, ---------- photocurrent : numeric Light-generated current. [A] - saturation_current : numeric Diode saturation current. [A] - resistance_series : numeric Series resistance. [Ohm] - resistance_shunt : numeric Shunt resistance. [Ohm] - nNsVth : numeric The product of the usual diode ideality factor (n, unitless), number of cells in series (Ns), and cell thermal voltage at @@ -922,7 +918,7 @@ def batzelis_keypoints(photocurrent, saturation_current, resistance_series, # Eqs 5-8 w = np.real(lambertw(np.e * Iph / Is)) - #vmp = (1 + Rs/Rsh) * a * (w - 1) - Rs * Iph * (1 - 1/w) # not needed + # vmp = (1 + Rs/Rsh) * a * (w - 1) - Rs * Iph * (1 - 1/w) # not needed with np.errstate(divide='ignore', invalid='ignore'): # zero Iph -> zero w imp = Iph * (1 - 1/w) - a * (w - 1) / Rsh diff --git a/tests/ivtools/sdm/test_desoto.py b/tests/ivtools/sdm/test_desoto.py index 811685e76b..0b9048c649 100644 --- a/tests/ivtools/sdm/test_desoto.py +++ b/tests/ivtools/sdm/test_desoto.py @@ -116,4 +116,3 @@ def test_fit_desoto_batzelis(): assert iv['i_mp'] == pytest.approx(params['imp0'], rel=3e-3) assert iv['v_oc'] == pytest.approx(params['voc0'], rel=3e-4) assert iv['v_mp'] == pytest.approx(params['vmp0'], rel=4e-3) - \ No newline at end of file diff --git a/tests/test_pvarray.py b/tests/test_pvarray.py index 80ecc8dd19..4c76e21d80 100644 --- a/tests/test_pvarray.py +++ b/tests/test_pvarray.py @@ -124,10 +124,10 @@ def test_batzelis(): t = np.array([25, 20, 20, 50, 50, 25, 0, nan]) expected = { # these values were computed using the function itself 'p_mp': [650.044, 328.599, 789.136, 300.079, 723.401, 0, nan, nan], - 'i_mp': [ 15.270, 7.626, 18.302, 7.680, 18.433, 0, nan, nan], - 'v_mp': [ 42.570, 43.090, 43.117, 39.071, 39.246, 0, nan, nan], - 'i_sc': [ 15.980, 7.972, 19.132, 8.082, 19.397, 0, nan, nan], - 'v_oc': [ 50.260, 49.687, 51.172, 45.948, 47.585, 0, nan, nan], + 'i_mp': [ 15.270, 7.626, 18.302, 7.680, 18.433, 0, nan, nan], + 'v_mp': [ 42.570, 43.090, 43.117, 39.071, 39.246, 0, nan, nan], + 'i_sc': [ 15.980, 7.972, 19.132, 8.082, 19.397, 0, nan, nan], + 'v_oc': [ 50.260, 49.687, 51.172, 45.948, 47.585, 0, nan, nan], } # numpy array @@ -153,4 +153,3 @@ def test_batzelis_negative_voltage(): actual = pvarray.batzelis(1e-10, 25, **params) assert actual['v_mp'] == 0 assert actual['v_oc'] == 0 - From 839d0b526a9450901a251b1e6559f9b91016f175 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 3 Oct 2025 09:07:35 -0400 Subject: [PATCH 09/15] Apply suggestions from code review Co-authored-by: Cliff Hansen --- pvlib/ivtools/sdm/desoto.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pvlib/ivtools/sdm/desoto.py b/pvlib/ivtools/sdm/desoto.py index d35bf32cde..a886cb38dd 100644 --- a/pvlib/ivtools/sdm/desoto.py +++ b/pvlib/ivtools/sdm/desoto.py @@ -407,7 +407,7 @@ def fit_desoto_batzelis(isc0, voc0, imp0, vmp0, alpha_sc, beta_voc): Determine De Soto single-diode model parameters from datasheet values using Batzelis's method. - This method is described in Section II.C of [1]_. + This method is described in Section II.C of [1]_ and fully documented in [2]_. Parameters ---------- @@ -441,6 +441,10 @@ def fit_desoto_batzelis(isc0, voc0, imp0, vmp0, alpha_sc, beta_voc): .. [1] E. I. Batzelis, "Simple PV Performance Equations Theoretically Well Founded on the Single-Diode Model," Journal of Photovoltaics vol. 7, no. 5, pp. 1400-1409, Sep 2017, :doi:`10.1109/JPHOTOV.2017.2711431` + .. [2] E. I. Batzelis and S. A. Papathanassiou, "A method for the analytical + extraction of the single-diode PV model parameters, IEEE Trans. + Sustain. Energy, vol. 7, no. 2, pp. 504-512, Apr 2016. + :doi:`10.1109/TSTE.2015.2503435` """ t0 = 298.15 # K del0 = (1 - beta_voc * t0) / (50.1 - alpha_sc * t0) # Eq 9 From 55cb1eb04cf1793e9f1ca33db22492f8e7770e6a Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 3 Oct 2025 09:25:03 -0400 Subject: [PATCH 10/15] lint --- pvlib/ivtools/sdm/desoto.py | 10 ++++++---- tests/test_pvarray.py | 4 ++-- tests/test_singlediode.py | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/pvlib/ivtools/sdm/desoto.py b/pvlib/ivtools/sdm/desoto.py index a886cb38dd..428520af16 100644 --- a/pvlib/ivtools/sdm/desoto.py +++ b/pvlib/ivtools/sdm/desoto.py @@ -407,7 +407,8 @@ def fit_desoto_batzelis(isc0, voc0, imp0, vmp0, alpha_sc, beta_voc): Determine De Soto single-diode model parameters from datasheet values using Batzelis's method. - This method is described in Section II.C of [1]_ and fully documented in [2]_. + This method is described in Section II.C of [1]_ and fully documented + in [2]_. Parameters ---------- @@ -441,11 +442,12 @@ def fit_desoto_batzelis(isc0, voc0, imp0, vmp0, alpha_sc, beta_voc): .. [1] E. I. Batzelis, "Simple PV Performance Equations Theoretically Well Founded on the Single-Diode Model," Journal of Photovoltaics vol. 7, no. 5, pp. 1400-1409, Sep 2017, :doi:`10.1109/JPHOTOV.2017.2711431` - .. [2] E. I. Batzelis and S. A. Papathanassiou, "A method for the analytical - extraction of the single-diode PV model parameters, IEEE Trans. - Sustain. Energy, vol. 7, no. 2, pp. 504-512, Apr 2016. + .. [2] E. I. Batzelis and S. A. Papathanassiou, "A method for the + analytical extraction of the single-diode PV model parameters," + IEEE Trans. Sustain. Energy, vol. 7, no. 2, pp. 504-512, Apr 2016. :doi:`10.1109/TSTE.2015.2503435` """ + # Equation numbers refer to [1] t0 = 298.15 # K del0 = (1 - beta_voc * t0) / (50.1 - alpha_sc * t0) # Eq 9 w0 = np.real(lambertw(np.exp(1/del0 + 1))) diff --git a/tests/test_pvarray.py b/tests/test_pvarray.py index 4c76e21d80..6258841f02 100644 --- a/tests/test_pvarray.py +++ b/tests/test_pvarray.py @@ -122,13 +122,13 @@ def test_batzelis(): 'alpha_sc': 0.00046, 'beta_voc': -0.0024} g = np.array([1000, 500, 1200, 500, 1200, 0, nan, 1000]) t = np.array([25, 20, 20, 50, 50, 25, 0, nan]) - expected = { # these values were computed using the function itself + expected = { # these values were computed using pvarray.batzelis itself 'p_mp': [650.044, 328.599, 789.136, 300.079, 723.401, 0, nan, nan], 'i_mp': [ 15.270, 7.626, 18.302, 7.680, 18.433, 0, nan, nan], 'v_mp': [ 42.570, 43.090, 43.117, 39.071, 39.246, 0, nan, nan], 'i_sc': [ 15.980, 7.972, 19.132, 8.082, 19.397, 0, nan, nan], 'v_oc': [ 50.260, 49.687, 51.172, 45.948, 47.585, 0, nan, nan], - } + } # numpy array actual = pvarray.batzelis(g, t, **params) diff --git a/tests/test_singlediode.py b/tests/test_singlediode.py index a7b4969266..e2ff8e540b 100644 --- a/tests/test_singlediode.py +++ b/tests/test_singlediode.py @@ -630,7 +630,7 @@ def test_batzelis_keypoints(): params = {'photocurrent': 10, 'saturation_current': 1e-10, 'resistance_series': 0.2, 'resistance_shunt': 3000, 'nNsVth': 1.7} - + exact_values = { # calculated using pvlib.pvsystem.singlediode 'i_sc': 9.999333377550565, 'v_oc': 43.05589965219406, From e6419561d8998beb389962056afc235d88a7d13b Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 3 Oct 2025 09:29:03 -0400 Subject: [PATCH 11/15] better handling of Rsh=np.inf --- pvlib/singlediode.py | 2 +- tests/test_singlediode.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index 8b10d66987..ecc6c2d6cd 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -1000,7 +1000,7 @@ def batzelis_keypoints(photocurrent, saturation_current, resistance_series, a = nNsVth # Eqs 3-4 - isc = Iph * Rsh / (Rs + Rsh) + isc = Iph / (Rs / Rsh + 1) # manipulated to handle Rsh=np.inf correctly with np.errstate(divide='ignore'): # zero Iph voc = a * np.log(Iph / Is) diff --git a/tests/test_singlediode.py b/tests/test_singlediode.py index e2ff8e540b..435aa91d84 100644 --- a/tests/test_singlediode.py +++ b/tests/test_singlediode.py @@ -667,3 +667,10 @@ def test_batzelis_keypoints_night(): nNsVth=1.7) for k, v in out.items(): assert v == 0, k # ensure all outputs are zero (not nan, etc) + + # test also when Rsh=inf but Iph > 0 + out = batzelis_keypoints(photocurrent=0.1, saturation_current=1e-10, + resistance_series=0.2, resistance_shunt=np.inf, + nNsVth=1.7) + for k, v in out.items(): + assert v > 0, k # ensure all outputs >0 (not nan, etc) From e7ec33c596a885c582e7b8c46929f726cfc8ec3b Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 3 Oct 2025 10:04:32 -0400 Subject: [PATCH 12/15] rename parameters --- pvlib/ivtools/sdm/desoto.py | 26 +++++++++++++------------- pvlib/pvarray.py | 32 ++++++++++++++++---------------- tests/ivtools/sdm/test_desoto.py | 10 +++++----- tests/test_pvarray.py | 4 ++-- 4 files changed, 36 insertions(+), 36 deletions(-) diff --git a/pvlib/ivtools/sdm/desoto.py b/pvlib/ivtools/sdm/desoto.py index 428520af16..86ae50a8a3 100644 --- a/pvlib/ivtools/sdm/desoto.py +++ b/pvlib/ivtools/sdm/desoto.py @@ -402,7 +402,7 @@ def _fit_desoto_sandia_diode(ee, voc, vth, tc, specs, const): return np.array(res.params)[1] -def fit_desoto_batzelis(isc0, voc0, imp0, vmp0, alpha_sc, beta_voc): +def fit_desoto_batzelis(v_mp, i_mp, v_oc, i_sc, alpha_sc, beta_voc): """ Determine De Soto single-diode model parameters from datasheet values using Batzelis's method. @@ -412,14 +412,14 @@ def fit_desoto_batzelis(isc0, voc0, imp0, vmp0, alpha_sc, beta_voc): Parameters ---------- - isc0 : float - Short-circuit current at STC. [A] - voc0 : float - Open-circuit voltage at STC. [V] - imp0 : float - Maximum power point current at STC. [A] - vmp0 : float + v_mp : float Maximum power point voltage at STC. [V] + i_mp : float + Maximum power point current at STC. [A] + v_oc : float + Open-circuit voltage at STC. [V] + i_sc : float + Short-circuit current at STC. [A] alpha_sc : float Short-circuit current temperature coefficient at STC. [1/K] beta_voc : float @@ -453,14 +453,14 @@ def fit_desoto_batzelis(isc0, voc0, imp0, vmp0, alpha_sc, beta_voc): w0 = np.real(lambertw(np.exp(1/del0 + 1))) # Eqs 11-15 - a0 = del0 * voc0 - Rs0 = (a0 * (w0 - 1) - vmp0) / imp0 - Rsh0 = a0 * (w0 - 1) / (isc0 * (1 - 1/w0) - imp0) - Iph0 = (1 + Rs0 / Rsh0) * isc0 + a0 = del0 * v_oc + Rs0 = (a0 * (w0 - 1) - v_mp) / i_mp + Rsh0 = a0 * (w0 - 1) / (i_sc * (1 - 1/w0) - i_mp) + Iph0 = (1 + Rs0 / Rsh0) * i_sc Isat0 = Iph0 * np.exp(-1/del0) return { - 'alpha_sc': alpha_sc * isc0, # convert 1/K to A/K + 'alpha_sc': alpha_sc * i_sc, # convert 1/K to A/K 'a_ref': a0, 'I_L_ref': Iph0, 'I_o_ref': Isat0, diff --git a/pvlib/pvarray.py b/pvlib/pvarray.py index 36ee973aac..d663ee6090 100644 --- a/pvlib/pvarray.py +++ b/pvlib/pvarray.py @@ -398,7 +398,7 @@ def huld(effective_irradiance, temp_mod, pdc0, k=None, cell_type=None, def batzelis(effective_irradiance, temp_cell, - isc0, voc0, imp0, vmp0, alpha_sc, beta_voc): + v_mp, i_mp, v_oc, i_sc, alpha_sc, beta_voc): """ Compute maximum power point, open circuit, and short circuit values using Batzelis's method. @@ -414,14 +414,14 @@ def batzelis(effective_irradiance, temp_cell, Effective irradiance incident on the PV module. [Wm⁻²] temp_cell : numeric PV module operating temperature. [°C] - isc0 : float - Short-circuit current at STC. [A] - voc0 : float - Open-circuit voltage at STC. [V] - imp0 : float - Maximum power point current at STC. [A] - vmp0 : float + v_mp : float Maximum power point voltage at STC. [V] + i_mp : float + Maximum power point current at STC. [A] + v_oc : float + Open-circuit voltage at STC. [V] + i_sc : float + Short-circuit current at STC. [A] alpha_sc : float Short-circuit current temperature coefficient at STC. [1/K] beta_voc : float @@ -461,8 +461,8 @@ def batzelis(effective_irradiance, temp_cell, Examples -------- - >>> params = {'isc0': 15.98, 'voc0': 50.26, 'imp0': 15.27, 'vmp0': 42.57, ... 'alpha_sc': 0.00046, 'beta_voc': -0.0024} + >>> params = {'i_sc': 15.98, 'v_oc': 50.26, 'i_mp': 15.27, 'v_mp': 42.57, >>> batzelis(np.array([1000, 800]), np.array([25, 30]), **params) {'p_mp': array([650.0439 , 512.99195952]), 'i_mp': array([15.27 , 12.23049227]), @@ -487,20 +487,20 @@ def batzelis(effective_irradiance, temp_cell, # Eqs 27-28 alpha_imp = alpha_sc + (beta_voc - 1/t0) / (w0 - 1) - beta_vmp = (voc0 / vmp0) * ( + beta_vmp = (v_oc / v_mp) * ( beta_voc / (1 + del0) + (del0 * (w0 - 1) - 1/(1 + del0)) / t0 ) # Eq 26 - eps0 = (del0 / (1 + del0)) * (voc0 / vmp0) - eps1 = del0 * (w0 - 1) * (voc0 / vmp0) - 1 + eps0 = (del0 / (1 + del0)) * (v_oc / v_mp) + eps1 = del0 * (w0 - 1) * (v_oc / v_mp) - 1 # Eqs 22-25 - isc = g * isc0 * (1 + alpha_sc * delT) - voc = voc0 * (1 + del0 * lamT * lnG + beta_voc * delT) - imp = g * imp0 * (1 + alpha_imp * delT) - vmp = vmp0 * (1 + eps0 * lamT * lnG + eps1 * (1 - g) + beta_vmp * delT) + isc = g * i_sc * (1 + alpha_sc * delT) + voc = v_oc * (1 + del0 * lamT * lnG + beta_voc * delT) + imp = g * i_mp * (1 + alpha_imp * delT) + vmp = v_mp * (1 + eps0 * lamT * lnG + eps1 * (1 - g) + beta_vmp * delT) # handle negative voltages from zero and extremely small irradiance vmp = np.clip(vmp, a_min=0, a_max=None) diff --git a/tests/ivtools/sdm/test_desoto.py b/tests/ivtools/sdm/test_desoto.py index 0b9048c649..f96aa626f1 100644 --- a/tests/ivtools/sdm/test_desoto.py +++ b/tests/ivtools/sdm/test_desoto.py @@ -95,8 +95,8 @@ def test_fit_desoto_sandia(cec_params_cansol_cs5p_220p): def test_fit_desoto_batzelis(): - params = {'isc0': 15.98, 'voc0': 50.26, 'imp0': 15.27, 'vmp0': 42.57, 'alpha_sc': 0.00046, 'beta_voc': -0.0024} + params = {'i_sc': 15.98, 'v_oc': 50.26, 'i_mp': 15.27, 'v_mp': 42.57, expected = { # calculated with the function itself 'alpha_sc': 0.0073508, 'a_ref': 1.7257631194825132, @@ -112,7 +112,7 @@ def test_fit_desoto_batzelis(): # ensure the STC values are reproduced iv = pvsystem.singlediode(out['I_L_ref'], out['I_o_ref'], out['R_s'], out['R_sh_ref'], out['a_ref']) - assert iv['i_sc'] == pytest.approx(params['isc0']) - assert iv['i_mp'] == pytest.approx(params['imp0'], rel=3e-3) - assert iv['v_oc'] == pytest.approx(params['voc0'], rel=3e-4) - assert iv['v_mp'] == pytest.approx(params['vmp0'], rel=4e-3) + assert iv['i_sc'] == pytest.approx(params['i_sc']) + assert iv['i_mp'] == pytest.approx(params['i_mp'], rel=3e-3) + assert iv['v_oc'] == pytest.approx(params['v_oc'], rel=3e-4) + assert iv['v_mp'] == pytest.approx(params['v_mp'], rel=4e-3) diff --git a/tests/test_pvarray.py b/tests/test_pvarray.py index 6258841f02..591e4b20c9 100644 --- a/tests/test_pvarray.py +++ b/tests/test_pvarray.py @@ -118,8 +118,8 @@ def test_huld_errors(): def test_batzelis(): - params = {'isc0': 15.98, 'voc0': 50.26, 'imp0': 15.27, 'vmp0': 42.57, 'alpha_sc': 0.00046, 'beta_voc': -0.0024} + params = {'i_sc': 15.98, 'v_oc': 50.26, 'i_mp': 15.27, 'v_mp': 42.57, g = np.array([1000, 500, 1200, 500, 1200, 0, nan, 1000]) t = np.array([25, 20, 20, 50, 50, 25, 0, nan]) expected = { # these values were computed using pvarray.batzelis itself @@ -148,8 +148,8 @@ def test_batzelis(): def test_batzelis_negative_voltage(): - params = {'isc0': 15.98, 'voc0': 50.26, 'imp0': 15.27, 'vmp0': 42.57, 'alpha_sc': 0.00046, 'beta_voc': -0.0024} + params = {'i_sc': 15.98, 'v_oc': 50.26, 'i_mp': 15.27, 'v_mp': 42.57, actual = pvarray.batzelis(1e-10, 25, **params) assert actual['v_mp'] == 0 assert actual['v_oc'] == 0 From 8becc1a19412ee4611e28c65b4d4b8a52b2f44f9 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 3 Oct 2025 10:05:48 -0400 Subject: [PATCH 13/15] switch to non-normalized temp coeffs --- pvlib/ivtools/sdm/desoto.py | 8 ++++++-- pvlib/pvarray.py | 22 +++++++++++----------- tests/ivtools/sdm/test_desoto.py | 14 +++++++------- tests/test_pvarray.py | 4 ++-- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/pvlib/ivtools/sdm/desoto.py b/pvlib/ivtools/sdm/desoto.py index 86ae50a8a3..389e0f0ef0 100644 --- a/pvlib/ivtools/sdm/desoto.py +++ b/pvlib/ivtools/sdm/desoto.py @@ -421,9 +421,9 @@ def fit_desoto_batzelis(v_mp, i_mp, v_oc, i_sc, alpha_sc, beta_voc): i_sc : float Short-circuit current at STC. [A] alpha_sc : float - Short-circuit current temperature coefficient at STC. [1/K] + Short-circuit current temperature coefficient at STC. [A/K] beta_voc : float - Open-circuit voltage temperature coefficient at STC. [1/K] + Open-circuit voltage temperature coefficient at STC. [V/K] Returns ------- @@ -447,6 +447,10 @@ def fit_desoto_batzelis(v_mp, i_mp, v_oc, i_sc, alpha_sc, beta_voc): IEEE Trans. Sustain. Energy, vol. 7, no. 2, pp. 504-512, Apr 2016. :doi:`10.1109/TSTE.2015.2503435` """ + # convert temp coeffs from A/K and V/K to 1/K + alpha_sc = alpha_sc / i_sc + beta_voc = beta_voc / v_oc + # Equation numbers refer to [1] t0 = 298.15 # K del0 = (1 - beta_voc * t0) / (50.1 - alpha_sc * t0) # Eq 9 diff --git a/pvlib/pvarray.py b/pvlib/pvarray.py index d663ee6090..b43d8d349a 100644 --- a/pvlib/pvarray.py +++ b/pvlib/pvarray.py @@ -423,9 +423,9 @@ def batzelis(effective_irradiance, temp_cell, i_sc : float Short-circuit current at STC. [A] alpha_sc : float - Short-circuit current temperature coefficient at STC. [1/K] + Short-circuit current temperature coefficient at STC. [A/K] beta_voc : float - Open-circuit voltage temperature coefficient at STC. [1/K] + Open-circuit voltage temperature coefficient at STC. [V/K] Returns ------- @@ -447,9 +447,6 @@ def batzelis(effective_irradiance, temp_cell, (taken from the De Soto model) 3. estimating the MPP, OC, and SC points on the resulting I-V curve. - The ``alpha_sc`` and ``beta_voc`` temperature coefficient parameters - must be given as normalized values. - At extremely low irradiance (e.g. 1e-10 Wm⁻²), this model can produce negative voltages. This function clips any negative voltages to zero. @@ -461,15 +458,18 @@ def batzelis(effective_irradiance, temp_cell, Examples -------- - ... 'alpha_sc': 0.00046, 'beta_voc': -0.0024} >>> params = {'i_sc': 15.98, 'v_oc': 50.26, 'i_mp': 15.27, 'v_mp': 42.57, + ... 'alpha_sc': 0.007351, 'beta_voc': -0.120624} >>> batzelis(np.array([1000, 800]), np.array([25, 30]), **params) - {'p_mp': array([650.0439 , 512.99195952]), - 'i_mp': array([15.27 , 12.23049227]), - 'v_mp': array([42.57 , 41.94368864]), - 'i_sc': array([15.98 , 12.8134032]), - 'v_oc': array([50.26 , 49.26532905])} + {'p_mp': array([650.0439 , 512.99199048]), + 'i_mp': array([15.27 , 12.23049303]), + 'v_mp': array([42.57 , 41.94368856]), + 'i_sc': array([15.98 , 12.813404]), + 'v_oc': array([50.26 , 49.26532902])} """ + # convert temp coeffs from A/K and V/K to 1/K + alpha_sc = alpha_sc / i_sc + beta_voc = beta_voc / v_oc t0 = 298.15 delT = temp_cell - (t0 - 273.15) diff --git a/tests/ivtools/sdm/test_desoto.py b/tests/ivtools/sdm/test_desoto.py index f96aa626f1..710bf15d51 100644 --- a/tests/ivtools/sdm/test_desoto.py +++ b/tests/ivtools/sdm/test_desoto.py @@ -95,15 +95,15 @@ def test_fit_desoto_sandia(cec_params_cansol_cs5p_220p): def test_fit_desoto_batzelis(): - 'alpha_sc': 0.00046, 'beta_voc': -0.0024} params = {'i_sc': 15.98, 'v_oc': 50.26, 'i_mp': 15.27, 'v_mp': 42.57, + 'alpha_sc': 0.007351, 'beta_voc': -0.120624} expected = { # calculated with the function itself - 'alpha_sc': 0.0073508, - 'a_ref': 1.7257631194825132, - 'I_L_ref': 15.985408869737098, - 'I_o_ref': 3.594300567343102e-12, - 'R_sh_ref': 389.4378389153357, - 'R_s': 0.1318159287478395 + 'alpha_sc': 0.007351, + 'a_ref': 1.7257632483733483, + 'I_L_ref': 15.985408866796396, + 'I_o_ref': 3.594308384705643e-12, + 'R_sh_ref': 389.4379947026243, + 'R_s': 0.13181590981241956, } out = sdm.fit_desoto_batzelis(**params) for k in expected: diff --git a/tests/test_pvarray.py b/tests/test_pvarray.py index 591e4b20c9..e614f885db 100644 --- a/tests/test_pvarray.py +++ b/tests/test_pvarray.py @@ -118,8 +118,8 @@ def test_huld_errors(): def test_batzelis(): - 'alpha_sc': 0.00046, 'beta_voc': -0.0024} params = {'i_sc': 15.98, 'v_oc': 50.26, 'i_mp': 15.27, 'v_mp': 42.57, + 'alpha_sc': 0.007351, 'beta_voc': -0.120624} g = np.array([1000, 500, 1200, 500, 1200, 0, nan, 1000]) t = np.array([25, 20, 20, 50, 50, 25, 0, nan]) expected = { # these values were computed using pvarray.batzelis itself @@ -148,8 +148,8 @@ def test_batzelis(): def test_batzelis_negative_voltage(): - 'alpha_sc': 0.00046, 'beta_voc': -0.0024} params = {'i_sc': 15.98, 'v_oc': 50.26, 'i_mp': 15.27, 'v_mp': 42.57, + 'alpha_sc': 0.007351, 'beta_voc': -0.120624} actual = pvarray.batzelis(1e-10, 25, **params) assert actual['v_mp'] == 0 assert actual['v_oc'] == 0 From 006ebb035673c0e37e949d5c4520e2ce34b772ed Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 3 Oct 2025 10:05:56 -0400 Subject: [PATCH 14/15] fix whatsnew --- docs/sphinx/source/whatsnew/v0.13.2.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.13.2.rst b/docs/sphinx/source/whatsnew/v0.13.2.rst index dec06f62e3..5c132831f4 100644 --- a/docs/sphinx/source/whatsnew/v0.13.2.rst +++ b/docs/sphinx/source/whatsnew/v0.13.2.rst @@ -20,7 +20,7 @@ Enhancements ~~~~~~~~~~~~ * Add :py:func:`~pvlib.ivtools.sdm.fit_desoto_batzelis`, a function to estimate parameters for the De Soto single-diode model from datasheet values. (:pull:`2563`) -* Add :py:func:`~pvlib.ivtools.singlediode.batzelis_keypoints`, a function to estimate +* Add :py:func:`~pvlib.singlediode.batzelis_keypoints`, a function to estimate maximum power, open circuit, and short circuit points using parameters for the single-diode equation. (:pull:`2563`) * Add :py:func:`~pvlib.pvarray.batzelis`, a function to estimate maximum power From 4d726b53de273eebc54970b46ae205f395ba84d7 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 30 Oct 2025 08:52:29 -0400 Subject: [PATCH 15/15] changes from review --- .../source/reference/pv_modeling/sdm.rst | 2 +- docs/sphinx/source/whatsnew/v0.13.2.rst | 2 +- pvlib/singlediode.py | 4 +-- tests/test_singlediode.py | 27 ++++++++++--------- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/docs/sphinx/source/reference/pv_modeling/sdm.rst b/docs/sphinx/source/reference/pv_modeling/sdm.rst index 2aff477883..077cdb16ee 100644 --- a/docs/sphinx/source/reference/pv_modeling/sdm.rst +++ b/docs/sphinx/source/reference/pv_modeling/sdm.rst @@ -17,7 +17,7 @@ Functions relevant for single diode models. pvsystem.v_from_i pvsystem.max_power_point ivtools.sdm.pvsyst_temperature_coeff - singlediode.batzelis_keypoints + singlediode.batzelis Low-level functions for solving the single diode equation. diff --git a/docs/sphinx/source/whatsnew/v0.13.2.rst b/docs/sphinx/source/whatsnew/v0.13.2.rst index 5c132831f4..84b8b70833 100644 --- a/docs/sphinx/source/whatsnew/v0.13.2.rst +++ b/docs/sphinx/source/whatsnew/v0.13.2.rst @@ -20,7 +20,7 @@ Enhancements ~~~~~~~~~~~~ * Add :py:func:`~pvlib.ivtools.sdm.fit_desoto_batzelis`, a function to estimate parameters for the De Soto single-diode model from datasheet values. (:pull:`2563`) -* Add :py:func:`~pvlib.singlediode.batzelis_keypoints`, a function to estimate +* Add :py:func:`~pvlib.singlediode.batzelis`, a function to estimate maximum power, open circuit, and short circuit points using parameters for the single-diode equation. (:pull:`2563`) * Add :py:func:`~pvlib.pvarray.batzelis`, a function to estimate maximum power diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index ecc6c2d6cd..1745e44cd5 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -952,8 +952,8 @@ def _pwr_optfcn(df, loc): return current * df[loc] -def batzelis_keypoints(photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth): +def batzelis(photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth): """ Estimate maximum power, open-circuit, and short-circuit points from single-diode equation parameters using Batzelis's method. diff --git a/tests/test_singlediode.py b/tests/test_singlediode.py index 435aa91d84..5d2a6f2c17 100644 --- a/tests/test_singlediode.py +++ b/tests/test_singlediode.py @@ -8,7 +8,7 @@ from pvlib import pvsystem from pvlib.singlediode import (bishop88_mpp, estimate_voc, VOLTAGE_BUILTIN, bishop88, bishop88_i_from_v, bishop88_v_from_i, - batzelis_keypoints) + batzelis) import pytest from numpy.testing import assert_array_equal from .conftest import TESTS_DATA_DIR @@ -626,7 +626,7 @@ def test_bishop88_init_cond(method): assert not bad_results.any() -def test_batzelis_keypoints(): +def test_batzelis(): params = {'photocurrent': 10, 'saturation_current': 1e-10, 'resistance_series': 0.2, 'resistance_shunt': 3000, 'nNsVth': 1.7} @@ -640,37 +640,38 @@ def test_batzelis_keypoints(): } rtol = 5e-3 # accurate to within half a percent in this case - output = batzelis_keypoints(**params) + output = batzelis(**params) for key in exact_values: assert output[key] == pytest.approx(exact_values[key], rel=rtol) # numpy arrays params2 = {k: np.array([v] * 2) for k, v in params.items()} - output2 = batzelis_keypoints(**params2) + output2 = batzelis(**params2) for key in exact_values: exp = np.array([exact_values[key]] * 2) np.testing.assert_allclose(output2[key], exp, rtol=rtol) # pandas params3 = {k: pd.Series(v) for k, v in params2.items()} - output3 = batzelis_keypoints(**params3) + output3 = batzelis(**params3) assert isinstance(output3, pd.DataFrame) for key in exact_values: exp = pd.Series([exact_values[key]] * 2) np.testing.assert_allclose(output3[key], exp, rtol=rtol) -def test_batzelis_keypoints_night(): - # SDMs produce photocurrent=0 and resistance_shunt=inf at night - out = batzelis_keypoints(photocurrent=0, saturation_current=1e-10, - resistance_series=0.2, resistance_shunt=np.inf, - nNsVth=1.7) +def test_batzelis_night(): + # The De Soto SDM produces photocurrent=0 and resistance_shunt=inf + # at 0 W/m2 irradiance + out = batzelis(photocurrent=0, saturation_current=1e-10, + resistance_series=0.2, resistance_shunt=np.inf, + nNsVth=1.7) for k, v in out.items(): assert v == 0, k # ensure all outputs are zero (not nan, etc) # test also when Rsh=inf but Iph > 0 - out = batzelis_keypoints(photocurrent=0.1, saturation_current=1e-10, - resistance_series=0.2, resistance_shunt=np.inf, - nNsVth=1.7) + out = batzelis(photocurrent=0.1, saturation_current=1e-10, + resistance_series=0.2, resistance_shunt=np.inf, + nNsVth=1.7) for k, v in out.items(): assert v > 0, k # ensure all outputs >0 (not nan, etc)