From f65583b850a2503e8948d11e96abca9a57b69198 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 8 Jul 2025 16:28:18 -0400 Subject: [PATCH 01/11] add method='chandrupatla' for bishop88 functions --- pvlib/pvsystem.py | 12 +++-- pvlib/singlediode.py | 117 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 102 insertions(+), 27 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 23ca1a934a..6c1a5ef7c3 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -2498,7 +2498,8 @@ def singlediode(photocurrent, saturation_current, resistance_series, method : str, default 'lambertw' Determines the method used to calculate points on the IV curve. The - options are ``'lambertw'``, ``'newton'``, or ``'brentq'``. + options are ``'lambertw'``, ``'newton'``, ``'brentq'``, or + ``'chandrupatla'`` (requires scipy 1.15 or greater). Returns ------- @@ -2630,7 +2631,8 @@ def max_power_point(photocurrent, saturation_current, resistance_series, cells ``Ns`` and the builtin voltage ``Vbi`` of the intrinsic layer. [V]. method : str - either ``'newton'`` or ``'brentq'`` + either ``'newton'``, ``'brentq'``, or ``'chandrupatla'`` (requires + scipy 1.15 or greater) Returns ------- @@ -2713,7 +2715,8 @@ def v_from_i(current, photocurrent, saturation_current, resistance_series, 0 < nNsVth method : str - Method to use: ``'lambertw'``, ``'newton'``, or ``'brentq'``. *Note*: + Method to use: ``'lambertw'``, ``'newton'``, ``'brentq'``, or + ``'chandrupatla'`` (requires scipy 1.15 or greater). *Note*: ``'brentq'`` is limited to 1st quadrant only. Returns @@ -2795,7 +2798,8 @@ def i_from_v(voltage, photocurrent, saturation_current, resistance_series, 0 < nNsVth method : str - Method to use: ``'lambertw'``, ``'newton'``, or ``'brentq'``. *Note*: + Method to use: ``'lambertw'``, ``'newton'``, ``'brentq'``, or + ``'chandrupatla'`` (requires scipy 1.15 or greater). *Note*: ``'brentq'`` is limited to 1st quadrant only. Returns diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index e76ea8f263..8285bbc38a 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -162,12 +162,11 @@ def bishop88(diode_voltage, photocurrent, saturation_current, # calculate temporary values to simplify calculations v_star = diode_voltage / nNsVth # non-dimensional diode voltage g_sh = 1.0 / resistance_shunt # conductance - if breakdown_factor > 0: # reverse bias is considered - brk_term = 1 - diode_voltage / breakdown_voltage - brk_pwr = np.power(brk_term, -breakdown_exp) - i_breakdown = breakdown_factor * diode_voltage * g_sh * brk_pwr - else: - i_breakdown = 0. + + brk_term = 1 - diode_voltage / breakdown_voltage + brk_pwr = np.power(brk_term, -breakdown_exp) + i_breakdown = breakdown_factor * diode_voltage * g_sh * brk_pwr + i = (photocurrent - saturation_current * np.expm1(v_star) # noqa: W503 - diode_voltage * g_sh - i_recomb - i_breakdown) # noqa: W503 v = diode_voltage - i * resistance_series @@ -177,18 +176,14 @@ def bishop88(diode_voltage, photocurrent, saturation_current, grad_i_recomb = np.where(is_recomb, i_recomb / v_recomb, 0) grad_2i_recomb = np.where(is_recomb, 2 * grad_i_recomb / v_recomb, 0) g_diode = saturation_current * np.exp(v_star) / nNsVth # conductance - if breakdown_factor > 0: # reverse bias is considered - brk_pwr_1 = np.power(brk_term, -breakdown_exp - 1) - brk_pwr_2 = np.power(brk_term, -breakdown_exp - 2) - brk_fctr = breakdown_factor * g_sh - grad_i_brk = brk_fctr * (brk_pwr + diode_voltage * - -breakdown_exp * brk_pwr_1) - grad2i_brk = (brk_fctr * -breakdown_exp # noqa: W503 - * (2 * brk_pwr_1 + diode_voltage # noqa: W503 - * (-breakdown_exp - 1) * brk_pwr_2)) # noqa: W503 - else: - grad_i_brk = 0. - grad2i_brk = 0. + brk_pwr_1 = np.power(brk_term, -breakdown_exp - 1) + brk_pwr_2 = np.power(brk_term, -breakdown_exp - 2) + brk_fctr = breakdown_factor * g_sh + grad_i_brk = brk_fctr * (brk_pwr + diode_voltage * + -breakdown_exp * brk_pwr_1) + grad2i_brk = (brk_fctr * -breakdown_exp # noqa: W503 + * (2 * brk_pwr_1 + diode_voltage # noqa: W503 + * (-breakdown_exp - 1) * brk_pwr_2)) # noqa: W503 grad_i = -g_diode - g_sh - grad_i_recomb - grad_i_brk # di/dvd grad_v = 1.0 - grad_i * resistance_series # dv/dvd # dp/dv = d(iv)/dv = v * di/dv + i @@ -247,12 +242,14 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, breakdown_exp : float, default 3.28 avalanche breakdown exponent :math:`m` [unitless] method : str, default 'newton' - Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'`` + Either ``'newton'``, ``'brentq'``, or ``'chandrupatla'`` (requires + scipy 1.15 or greater). ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. method_kwargs : dict, optional Keyword arguments passed to root finder method. See - :py:func:`scipy:scipy.optimize.brentq` and - :py:func:`scipy:scipy.optimize.newton` parameters. + :py:func:`scipy:scipy.optimize.brentq`, + :py:func:`scipy:scipy.optimize.newton`, and + :py:func:`scipy:scipy.optimize.elementwise.find_root` for parameters. ``'full_output': True`` is allowed, and ``optimizer_output`` would be returned. See examples section. @@ -333,6 +330,30 @@ def vd_from_brent(voc, v, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, vd = newton(func=lambda x, *a: fv(x, voltage, *a), x0=x0, fprime=lambda x, *a: bishop88(x, *a, gradients=True)[4], args=args, **method_kwargs) + elif method == 'chandrupatla': + try: + from scipy.optimize.elementwise import find_root + except ModuleNotFoundError as e: + # TODO remove this when our minimum scipy version is >=1.15 + msg = ( + "method='chandrupatla' requires scipy v1.15 or greater. " + "Select another method, or update your version of scipy. " + f"({str(e)})" + ) + raise ImportError(msg) + + voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) + shape = _shape_of_max_size(voltage, voc_est) + vlo = np.zeros(shape) + vhi = np.full(shape, voc_est) + bounds = (vlo, vhi) + kwargs_trimmed = method_kwargs.copy() + kwargs_trimmed.pop("full_output", None) # not valid for find_root + + result = find_root(fv, bounds, args=(voltage, *args), **kwargs_trimmed) + vd = result.x + if method_kwargs.get('full_output'): + vd = (vd, result) # mimic the other methods else: raise NotImplementedError("Method '%s' isn't implemented" % method) @@ -388,7 +409,8 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, breakdown_exp : float, default 3.28 avalanche breakdown exponent :math:`m` [unitless] method : str, default 'newton' - Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'`` + Either ``'newton'``, ``'brentq'``, or ``'chandrupatla'`` (requires + scipy 1.15 or greater). ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. method_kwargs : dict, optional Keyword arguments passed to root finder method. See @@ -474,6 +496,29 @@ def vd_from_brent(voc, i, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, vd = newton(func=lambda x, *a: fi(x, current, *a), x0=x0, fprime=lambda x, *a: bishop88(x, *a, gradients=True)[3], args=args, **method_kwargs) + elif method == 'chandrupatla': + try: + from scipy.optimize.elementwise import find_root + except ModuleNotFoundError as e: + # TODO remove this when our minimum scipy version is >=1.15 + msg = ( + "method='chandrupatla' requires scipy v1.15 or greater. " + "Select another method, or update your version of scipy. " + f"({str(e)})" + ) + raise ImportError(msg) + + shape = _shape_of_max_size(current, voc_est) + vlo = np.zeros(shape) + vhi = np.full(shape, voc_est) + bounds = (vlo, vhi) + kwargs_trimmed = method_kwargs.copy() + kwargs_trimmed.pop("full_output", None) # not valid for find_root + + result = find_root(fi, bounds, args=(current, *args), **kwargs_trimmed) + vd = result.x + if method_kwargs.get('full_output'): + vd = (vd, result) # mimic the other methods else: raise NotImplementedError("Method '%s' isn't implemented" % method) @@ -526,7 +571,8 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series, breakdown_exp : numeric, default 3.28 avalanche breakdown exponent :math:`m` [unitless] method : str, default 'newton' - Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'`` + Either ``'newton'``, ``'brentq'``, or ``'chandrupatla'`` (requires + scipy 1.15 or greater). ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. method_kwargs : dict, optional Keyword arguments passed to root finder method. See @@ -611,6 +657,31 @@ def fmpp(x, *a): vd = newton(func=fmpp, x0=x0, fprime=lambda x, *a: bishop88(x, *a, gradients=True)[7], args=args, **method_kwargs) + elif method == 'chandrupatla': + try: + from scipy.optimize.elementwise import find_root + except ModuleNotFoundError as e: + # TODO remove this when our minimum scipy version is >=1.15 + msg = ( + "method='chandrupatla' requires scipy v1.15 or greater. " + "Select another method, or update your version of scipy. " + f"({str(e)})" + ) + raise ImportError(msg) + + vlo = np.zeros_like(photocurrent) + vhi = np.full_like(photocurrent, voc_est) + kwargs_trimmed = method_kwargs.copy() + kwargs_trimmed.pop("full_output", None) # not valid for find_root + + result = find_root(fmpp, + (vlo, vhi), + args=args, + **kwargs_trimmed) + vd = result.x + if method_kwargs.get('full_output'): + vd = (vd, result) # mimic the other methods + else: raise NotImplementedError("Method '%s' isn't implemented" % method) From c7ba903ab98123eafe7dcb778de323eecc2e3db5 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 8 Jul 2025 16:28:25 -0400 Subject: [PATCH 02/11] update tests --- tests/test_pvsystem.py | 36 ++++++++++++++++-------------------- tests/test_singlediode.py | 35 ++++++++++++++++++++++++----------- 2 files changed, 40 insertions(+), 31 deletions(-) diff --git a/tests/test_pvsystem.py b/tests/test_pvsystem.py index b58f9fd9e4..38afb35fa2 100644 --- a/tests/test_pvsystem.py +++ b/tests/test_pvsystem.py @@ -1371,7 +1371,8 @@ def fixture_i_from_v(request): @pytest.mark.parametrize( - 'method, atol', [('lambertw', 1e-11), ('brentq', 1e-11), ('newton', 1e-11)] + 'method, atol', [('lambertw', 1e-11), ('brentq', 1e-11), ('newton', 1e-11), + ('chandrupatla', 1e-11)] ) def test_i_from_v(fixture_i_from_v, method, atol): # Solution set loaded from fixture @@ -1406,6 +1407,9 @@ def test_i_from_v_size(): with pytest.raises(ValueError): pvsystem.i_from_v([7.5] * 3, 7., 6e-7, [0.1] * 2, 20, 0.5, method='brentq') + with pytest.raises(ValueError): + pvsystem.i_from_v([7.5] * 3, 7., 6e-7, [0.1] * 2, 20, 0.5, + method='chandrupatla') with pytest.raises(ValueError): pvsystem.i_from_v([7.5] * 3, np.array([7., 7.]), 6e-7, 0.1, 20, 0.5, method='newton') @@ -1417,6 +1421,8 @@ def test_v_from_i_size(): with pytest.raises(ValueError): pvsystem.v_from_i([3.] * 3, 7., 6e-7, [0.1] * 2, 20, 0.5, method='brentq') + pvsystem.v_from_i([3.] * 3, 7., 6e-7, [0.1] * 2, 20, 0.5, + method='chandrupatla') with pytest.raises(ValueError): pvsystem.v_from_i([3.] * 3, np.array([7., 7.]), 6e-7, [0.1], 20, 0.5, method='newton') @@ -1437,7 +1443,8 @@ def test_mpp_floats(): assert np.isclose(v, expected[k]) -def test_mpp_recombination(): +@pytest.mark.parametrize('method', ['brentq', 'newton', 'chandrupatla']) +def test_mpp_recombination(method): """test max_power_point""" pvsyst_fs_495 = get_pvsyst_fs_495() IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_pvsyst( @@ -1455,7 +1462,7 @@ def test_mpp_recombination(): IL, I0, Rs, Rsh, nNsVth, d2mutau=pvsyst_fs_495['d2mutau'], NsVbi=VOLTAGE_BUILTIN*pvsyst_fs_495['cells_in_series'], - method='brentq') + method=method) expected_imp = pvsyst_fs_495['I_mp_ref'] expected_vmp = pvsyst_fs_495['V_mp_ref'] expected_pmp = expected_imp*expected_vmp @@ -1465,36 +1472,28 @@ def test_mpp_recombination(): assert isinstance(out, dict) for k, v in out.items(): assert np.isclose(v, expected[k], 0.01) - out = pvsystem.max_power_point( - IL, I0, Rs, Rsh, nNsVth, - d2mutau=pvsyst_fs_495['d2mutau'], - NsVbi=VOLTAGE_BUILTIN*pvsyst_fs_495['cells_in_series'], - method='newton') - for k, v in out.items(): - assert np.isclose(v, expected[k], 0.01) -def test_mpp_array(): +@pytest.mark.parametrize('method', ['brentq', 'newton', 'chandrupatla']) +def test_mpp_array(method): """test max_power_point""" IL, I0, Rs, Rsh, nNsVth = (np.array([7, 7]), 6e-7, .1, 20, .5) - out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method='brentq') + out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method=method) expected = {'i_mp': [6.1362673597376753] * 2, 'v_mp': [6.2243393757884284] * 2, 'p_mp': [38.194210547580511] * 2} assert isinstance(out, dict) for k, v in out.items(): assert np.allclose(v, expected[k]) - out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method='newton') - for k, v in out.items(): - assert np.allclose(v, expected[k]) -def test_mpp_series(): +@pytest.mark.parametrize('method', ['brentq', 'newton', 'chandrupatla']) +def test_mpp_series(method): """test max_power_point""" idx = ['2008-02-17T11:30:00-0800', '2008-02-17T12:30:00-0800'] IL, I0, Rs, Rsh, nNsVth = (np.array([7, 7]), 6e-7, .1, 20, .5) IL = pd.Series(IL, index=idx) - out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method='brentq') + out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method=method) expected = pd.DataFrame({'i_mp': [6.1362673597376753] * 2, 'v_mp': [6.2243393757884284] * 2, 'p_mp': [38.194210547580511] * 2}, @@ -1502,9 +1501,6 @@ def test_mpp_series(): assert isinstance(out, pd.DataFrame) for k, v in out.items(): assert np.allclose(v, expected[k]) - out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method='newton') - for k, v in out.items(): - assert np.allclose(v, expected[k]) def test_singlediode_series(cec_module_params): diff --git a/tests/test_singlediode.py b/tests/test_singlediode.py index efded9ff3c..c30b4a247f 100644 --- a/tests/test_singlediode.py +++ b/tests/test_singlediode.py @@ -16,7 +16,7 @@ TCELL = 55 -@pytest.mark.parametrize('method', ['brentq', 'newton']) +@pytest.mark.parametrize('method', ['brentq', 'newton', 'chandrupatla']) def test_method_spr_e20_327(method, cec_module_spr_e20_327): """test pvsystem.singlediode with different methods on SPR-E20-327""" spr_e20_327 = cec_module_spr_e20_327 @@ -38,7 +38,7 @@ def test_method_spr_e20_327(method, cec_module_spr_e20_327): assert np.isclose(pvs['i_xx'], out['i_xx']) -@pytest.mark.parametrize('method', ['brentq', 'newton']) +@pytest.mark.parametrize('method', ['brentq', 'newton', 'chandrupatla']) def test_newton_fs_495(method, cec_module_fs_495): """test pvsystem.singlediode with different methods on FS495""" fs_495 = cec_module_fs_495 @@ -146,7 +146,8 @@ def precise_iv_curves(request): return singlediode_params, pc -@pytest.mark.parametrize('method', ['lambertw', 'brentq', 'newton']) +@pytest.mark.parametrize('method', ['lambertw', 'brentq', 'newton', + 'chandrupatla']) def test_singlediode_precision(method, precise_iv_curves): """ Tests the accuracy of singlediode. ivcurve_pnts is not tested. @@ -187,7 +188,8 @@ def test_singlediode_lambert_negative_voc(mocker): assert_array_equal(outs["v_oc"], [0, 0]) -@pytest.mark.parametrize('method', ['lambertw', 'brentq', 'newton']) +@pytest.mark.parametrize('method', ['lambertw', 'brentq', 'newton', + 'chandrupatla']) def test_v_from_i_i_from_v_precision(method, precise_iv_curves): """ Tests the accuracy of pvsystem.v_from_i and pvsystem.i_from_v. @@ -256,7 +258,7 @@ def get_pvsyst_fs_495(): ) ] ) -@pytest.mark.parametrize('method', ['newton', 'brentq']) +@pytest.mark.parametrize('method', ['newton', 'brentq', 'chandrupatla']) def test_pvsyst_recombination_loss(method, poa, temp_cell, expected, tol): """test PVSst recombination loss""" pvsyst_fs_495 = get_pvsyst_fs_495() @@ -348,7 +350,7 @@ def test_pvsyst_recombination_loss(method, poa, temp_cell, expected, tol): ) ] ) -@pytest.mark.parametrize('method', ['newton', 'brentq']) +@pytest.mark.parametrize('method', ['newton', 'brentq', 'chandrupatla']) def test_pvsyst_breakdown(method, brk_params, recomb_params, poa, temp_cell, expected, tol): """test PVSyst recombination loss""" @@ -456,7 +458,12 @@ def bishop88_arguments(): 'xtol': 1e-8, 'rtol': 1e-8, 'maxiter': 30, - }) + }), + # can't include chandrupatla since the function is not available to patch + #('chandrupatla', { + # 'tolerances ': {'xtol': 1e-8, 'rtol': 1e-8}, + # 'maxiter': 30, + #}), ]) def test_bishop88_kwargs_transfer(method, method_kwargs, mocker, bishop88_arguments): @@ -495,7 +502,13 @@ def test_bishop88_kwargs_transfer(method, method_kwargs, mocker, 'rtol': 1e-4, 'maxiter': 20, '_inexistent_param': "0.01" - }) + }), + ('chandrupatla', { + 'xtol': 1e-4, + 'rtol': 1e-4, + 'maxiter': 20, + '_inexistent_param': "0.01" + }), ]) def test_bishop88_kwargs_fails(method, method_kwargs, bishop88_arguments): """test invalid method_kwargs passed onto the optimizer fail""" @@ -513,7 +526,7 @@ def test_bishop88_kwargs_fails(method, method_kwargs, bishop88_arguments): method_kwargs=method_kwargs) -@pytest.mark.parametrize('method', ['newton', 'brentq']) +@pytest.mark.parametrize('method', ['newton', 'brentq', 'chandrupatla']) def test_bishop88_full_output_kwarg(method, bishop88_arguments): """test call to bishop88_.* with full_output=True return values are ok""" method_kwargs = {'full_output': True} @@ -547,7 +560,7 @@ def test_bishop88_full_output_kwarg(method, bishop88_arguments): assert len(ret_val[1]) >= 2 -@pytest.mark.parametrize('method', ['newton', 'brentq']) +@pytest.mark.parametrize('method', ['newton', 'brentq', 'chandrupatla']) def test_bishop88_pdSeries_len_one(method, bishop88_arguments): for k, v in bishop88_arguments.items(): bishop88_arguments[k] = pd.Series([v]) @@ -563,7 +576,7 @@ def _sde_check_solution(i, v, il, io, rs, rsh, a, d2mutau=0., NsVbi=np.inf): return il - io*np.expm1(vd/a) - vd/rsh - il*d2mutau/(NsVbi - vd) - i -@pytest.mark.parametrize('method', ['newton', 'brentq']) +@pytest.mark.parametrize('method', ['newton', 'brentq', 'chandrupatla']) def test_bishop88_init_cond(method): # GH 2013 p = {'alpha_sc': 0.0012256, From 0b5cc71d1b2ec334bf1e15582d407db537b47e61 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 8 Jul 2025 16:28:32 -0400 Subject: [PATCH 03/11] whatsnew --- docs/sphinx/source/whatsnew/v0.13.1.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.13.1.rst b/docs/sphinx/source/whatsnew/v0.13.1.rst index cf591f7d0b..e9399c1961 100644 --- a/docs/sphinx/source/whatsnew/v0.13.1.rst +++ b/docs/sphinx/source/whatsnew/v0.13.1.rst @@ -18,7 +18,14 @@ Bug fixes Enhancements ~~~~~~~~~~~~ - +* Added ``method='chandrupatla'`` to :py:func:`pvlib.pvsystem.singlediode`, + :py:func:`~pvlib.pvsystem.i_from_v`, + :py:func:`~pvlib.pvsystem.v_from_i`, + :py:func:`~pvlib.pvsystem.max_power_point`, + :py:func:`~pvlib.singlediode.bishop88`, + :py:func:`~pvlib.singlediode.bishop88_v_from_i`, and + :py:func:`~pvlib.singlediode.bishop88_i_from_v`. (:issue:`2497`, :pull:`2498`) + Documentation ~~~~~~~~~~~~~ @@ -44,3 +51,4 @@ Maintenance Contributors ~~~~~~~~~~~~ * Elijah Passmore (:ghuser:`eljpsm`) +* Kevin Anderson (:ghuser:`kandersolar`) From 01f7d567a9fd83497b4d9ddf86cb2edfadce848c Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 8 Jul 2025 16:59:10 -0400 Subject: [PATCH 04/11] doc tweaks --- pvlib/singlediode.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index 8285bbc38a..bf66516d2e 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -109,13 +109,13 @@ def bishop88(diode_voltage, photocurrent, saturation_current, (a-Si) modules that is the product of the PV module number of series cells :math:`N_{s}` and the builtin voltage :math:`V_{bi}` of the intrinsic layer. [V]. - breakdown_factor : float, default 0 + breakdown_factor : numeric, default 0 fraction of ohmic current involved in avalanche breakdown :math:`a`. Default of 0 excludes the reverse bias term from the model. [unitless] - breakdown_voltage : float, default -5.5 + breakdown_voltage : numeric, default -5.5 reverse breakdown voltage of the photovoltaic junction :math:`V_{br}` [V] - breakdown_exp : float, default 3.28 + breakdown_exp : numeric, default 3.28 avalanche breakdown exponent :math:`m` [unitless] gradients : bool False returns only I, V, and P. True also returns gradients From 3aa8acdfa8a880e32fd4ce65915ebf22fabec972 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 8 Jul 2025 17:01:14 -0400 Subject: [PATCH 05/11] test tweak --- tests/test_pvsystem.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_pvsystem.py b/tests/test_pvsystem.py index 38afb35fa2..c9d5ba1f93 100644 --- a/tests/test_pvsystem.py +++ b/tests/test_pvsystem.py @@ -1421,6 +1421,7 @@ def test_v_from_i_size(): with pytest.raises(ValueError): pvsystem.v_from_i([3.] * 3, 7., 6e-7, [0.1] * 2, 20, 0.5, method='brentq') + with pytest.raises(ValueError): pvsystem.v_from_i([3.] * 3, 7., 6e-7, [0.1] * 2, 20, 0.5, method='chandrupatla') with pytest.raises(ValueError): From 394cc58ae8c22a3cb6e6df22b62525ca5807d509 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 9 Jul 2025 09:17:54 -0400 Subject: [PATCH 06/11] try out skipping chandrupatla on py3.9 --- tests/conftest.py | 12 ++++++++ tests/test_pvsystem.py | 61 +++++++++++++++++++-------------------- tests/test_singlediode.py | 25 +++++++++------- 3 files changed, 55 insertions(+), 43 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 28ae973390..6e653d9d83 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ import pandas as pd import os +import sys from packaging.version import Version import pytest from functools import wraps @@ -194,6 +195,17 @@ def has_spa_c(): reason="requires pandas>=2.0.0") +# single-diode equation functions have method=='chandrupatla', which relies +# on scipy.optimize.elementwise.find_root, which is only available in +# scipy>=1.15. That is only available for python 3.10 and above, so +# we need to skip those tests on python 3.9. +# TODO remove this when we drop support for python 3.9. +chandrupatla_available = sys.version_info >= (3, 10) +chandrupatla = pytest.param( + "chandrupatla", marks=pytest.mark.skipif(not chandrupatla_available) +) + + @pytest.fixture() def golden(): return Location(39.742476, -105.1786, 'America/Denver', 1830.14) diff --git a/tests/test_pvsystem.py b/tests/test_pvsystem.py index c9d5ba1f93..49928d0900 100644 --- a/tests/test_pvsystem.py +++ b/tests/test_pvsystem.py @@ -22,6 +22,8 @@ from tests.test_singlediode import get_pvsyst_fs_495 +from .conftest import chandrupatla, chandrupatla_available + @pytest.mark.parametrize('iam_model,model_params', [ ('ashrae', {'b': 0.05}), @@ -1371,8 +1373,11 @@ def fixture_i_from_v(request): @pytest.mark.parametrize( - 'method, atol', [('lambertw', 1e-11), ('brentq', 1e-11), ('newton', 1e-11), - ('chandrupatla', 1e-11)] + 'method, atol', [ + ('lambertw', 1e-11), ('brentq', 1e-11), ('newton', 1e-11), + pytest.param("chandrupatla", 1e-11, + marks=pytest.mark.skipif(not chandrupatla_available)), + ] ) def test_i_from_v(fixture_i_from_v, method, atol): # Solution set loaded from fixture @@ -1401,50 +1406,42 @@ def test_PVSystem_i_from_v(mocker): m.assert_called_once_with(*args) -def test_i_from_v_size(): - with pytest.raises(ValueError): - pvsystem.i_from_v([7.5] * 3, 7., 6e-7, [0.1] * 2, 20, 0.5) - with pytest.raises(ValueError): - pvsystem.i_from_v([7.5] * 3, 7., 6e-7, [0.1] * 2, 20, 0.5, - method='brentq') - with pytest.raises(ValueError): - pvsystem.i_from_v([7.5] * 3, 7., 6e-7, [0.1] * 2, 20, 0.5, - method='chandrupatla') +@pytest.mark.parametrize('method', ['lambertw', 'brentq', 'newton', + chandrupatla]) +def test_i_from_v_size(method): + if method == 'newton': + args = ([7.5] * 3, np.array([7., 7.]), 6e-7, 0.1, 20, 0.5) + else: + args = ([7.5] * 3, 7., 6e-7, [0.1] * 2, 20, 0.5) with pytest.raises(ValueError): - pvsystem.i_from_v([7.5] * 3, np.array([7., 7.]), 6e-7, 0.1, 20, 0.5, - method='newton') + pvsystem.i_from_v(*args, method=method) -def test_v_from_i_size(): - with pytest.raises(ValueError): - pvsystem.v_from_i([3.] * 3, 7., 6e-7, [0.1] * 2, 20, 0.5) - with pytest.raises(ValueError): - pvsystem.v_from_i([3.] * 3, 7., 6e-7, [0.1] * 2, 20, 0.5, - method='brentq') - with pytest.raises(ValueError): - pvsystem.v_from_i([3.] * 3, 7., 6e-7, [0.1] * 2, 20, 0.5, - method='chandrupatla') +@pytest.mark.parametrize('method', ['lambertw', 'brentq', 'newton', + chandrupatla]) +def test_v_from_i_size(method): + if method == 'newton': + args = ([3.] * 3, np.array([7., 7.]), 6e-7, [0.1], 20, 0.5) + else: + args = ([3.] * 3, 7., 6e-7, [0.1] * 2, 20, 0.5) with pytest.raises(ValueError): - pvsystem.v_from_i([3.] * 3, np.array([7., 7.]), 6e-7, [0.1], 20, 0.5, - method='newton') + pvsystem.v_from_i(*args, method=method) -def test_mpp_floats(): +@pytest.mark.parametrize('method', ['brentq', 'newton', chandrupatla]) +def test_mpp_floats(method): """test max_power_point""" IL, I0, Rs, Rsh, nNsVth = (7, 6e-7, .1, 20, .5) - out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method='brentq') + out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method=method) expected = {'i_mp': 6.1362673597376753, # 6.1390251797935704, lambertw 'v_mp': 6.2243393757884284, # 6.221535886625464, lambertw 'p_mp': 38.194210547580511} # 38.194165464983037} lambertw assert isinstance(out, dict) for k, v in out.items(): assert np.isclose(v, expected[k]) - out = pvsystem.max_power_point(IL, I0, Rs, Rsh, nNsVth, method='newton') - for k, v in out.items(): - assert np.isclose(v, expected[k]) -@pytest.mark.parametrize('method', ['brentq', 'newton', 'chandrupatla']) +@pytest.mark.parametrize('method', ['brentq', 'newton', chandrupatla]) def test_mpp_recombination(method): """test max_power_point""" pvsyst_fs_495 = get_pvsyst_fs_495() @@ -1475,7 +1472,7 @@ def test_mpp_recombination(method): assert np.isclose(v, expected[k], 0.01) -@pytest.mark.parametrize('method', ['brentq', 'newton', 'chandrupatla']) +@pytest.mark.parametrize('method', ['brentq', 'newton', chandrupatla]) def test_mpp_array(method): """test max_power_point""" IL, I0, Rs, Rsh, nNsVth = (np.array([7, 7]), 6e-7, .1, 20, .5) @@ -1488,7 +1485,7 @@ def test_mpp_array(method): assert np.allclose(v, expected[k]) -@pytest.mark.parametrize('method', ['brentq', 'newton', 'chandrupatla']) +@pytest.mark.parametrize('method', ['brentq', 'newton', chandrupatla]) def test_mpp_series(method): """test max_power_point""" idx = ['2008-02-17T11:30:00-0800', '2008-02-17T12:30:00-0800'] diff --git a/tests/test_singlediode.py b/tests/test_singlediode.py index c30b4a247f..dc373f48ff 100644 --- a/tests/test_singlediode.py +++ b/tests/test_singlediode.py @@ -12,11 +12,13 @@ from numpy.testing import assert_array_equal from .conftest import TESTS_DATA_DIR +from .conftest import chandrupatla, chandrupatla_available + POA = 888 TCELL = 55 -@pytest.mark.parametrize('method', ['brentq', 'newton', 'chandrupatla']) +@pytest.mark.parametrize('method', ['brentq', 'newton', chandrupatla]) def test_method_spr_e20_327(method, cec_module_spr_e20_327): """test pvsystem.singlediode with different methods on SPR-E20-327""" spr_e20_327 = cec_module_spr_e20_327 @@ -38,7 +40,7 @@ def test_method_spr_e20_327(method, cec_module_spr_e20_327): assert np.isclose(pvs['i_xx'], out['i_xx']) -@pytest.mark.parametrize('method', ['brentq', 'newton', 'chandrupatla']) +@pytest.mark.parametrize('method', ['brentq', 'newton', chandrupatla]) def test_newton_fs_495(method, cec_module_fs_495): """test pvsystem.singlediode with different methods on FS495""" fs_495 = cec_module_fs_495 @@ -147,7 +149,7 @@ def precise_iv_curves(request): @pytest.mark.parametrize('method', ['lambertw', 'brentq', 'newton', - 'chandrupatla']) + chandrupatla]) def test_singlediode_precision(method, precise_iv_curves): """ Tests the accuracy of singlediode. ivcurve_pnts is not tested. @@ -189,7 +191,7 @@ def test_singlediode_lambert_negative_voc(mocker): @pytest.mark.parametrize('method', ['lambertw', 'brentq', 'newton', - 'chandrupatla']) + chandrupatla]) def test_v_from_i_i_from_v_precision(method, precise_iv_curves): """ Tests the accuracy of pvsystem.v_from_i and pvsystem.i_from_v. @@ -258,7 +260,7 @@ def get_pvsyst_fs_495(): ) ] ) -@pytest.mark.parametrize('method', ['newton', 'brentq', 'chandrupatla']) +@pytest.mark.parametrize('method', ['newton', 'brentq', chandrupatla]) def test_pvsyst_recombination_loss(method, poa, temp_cell, expected, tol): """test PVSst recombination loss""" pvsyst_fs_495 = get_pvsyst_fs_495() @@ -350,7 +352,7 @@ def test_pvsyst_recombination_loss(method, poa, temp_cell, expected, tol): ) ] ) -@pytest.mark.parametrize('method', ['newton', 'brentq', 'chandrupatla']) +@pytest.mark.parametrize('method', ['newton', 'brentq', chandrupatla]) def test_pvsyst_breakdown(method, brk_params, recomb_params, poa, temp_cell, expected, tol): """test PVSyst recombination loss""" @@ -460,6 +462,7 @@ def bishop88_arguments(): 'maxiter': 30, }), # can't include chandrupatla since the function is not available to patch + # TODO: add this once chandrupatla becomes non-optional functionality #('chandrupatla', { # 'tolerances ': {'xtol': 1e-8, 'rtol': 1e-8}, # 'maxiter': 30, @@ -503,12 +506,12 @@ def test_bishop88_kwargs_transfer(method, method_kwargs, mocker, 'maxiter': 20, '_inexistent_param': "0.01" }), - ('chandrupatla', { + pytest.param('chandrupatla', { 'xtol': 1e-4, 'rtol': 1e-4, 'maxiter': 20, '_inexistent_param': "0.01" - }), + }, marks=pytest.mark.skipif(not chandrupatla_available)), ]) def test_bishop88_kwargs_fails(method, method_kwargs, bishop88_arguments): """test invalid method_kwargs passed onto the optimizer fail""" @@ -526,7 +529,7 @@ def test_bishop88_kwargs_fails(method, method_kwargs, bishop88_arguments): method_kwargs=method_kwargs) -@pytest.mark.parametrize('method', ['newton', 'brentq', 'chandrupatla']) +@pytest.mark.parametrize('method', ['newton', 'brentq', chandrupatla]) def test_bishop88_full_output_kwarg(method, bishop88_arguments): """test call to bishop88_.* with full_output=True return values are ok""" method_kwargs = {'full_output': True} @@ -560,7 +563,7 @@ def test_bishop88_full_output_kwarg(method, bishop88_arguments): assert len(ret_val[1]) >= 2 -@pytest.mark.parametrize('method', ['newton', 'brentq', 'chandrupatla']) +@pytest.mark.parametrize('method', ['newton', 'brentq', chandrupatla]) def test_bishop88_pdSeries_len_one(method, bishop88_arguments): for k, v in bishop88_arguments.items(): bishop88_arguments[k] = pd.Series([v]) @@ -576,7 +579,7 @@ def _sde_check_solution(i, v, il, io, rs, rsh, a, d2mutau=0., NsVbi=np.inf): return il - io*np.expm1(vd/a) - vd/rsh - il*d2mutau/(NsVbi - vd) - i -@pytest.mark.parametrize('method', ['newton', 'brentq', 'chandrupatla']) +@pytest.mark.parametrize('method', ['newton', 'brentq', chandrupatla]) def test_bishop88_init_cond(method): # GH 2013 p = {'alpha_sc': 0.0012256, From 99e3ae9c746889a81f54a4fd1b3ae3ec68640441 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 9 Jul 2025 09:30:40 -0400 Subject: [PATCH 07/11] fix py3.9 skips --- tests/conftest.py | 3 ++- tests/test_pvsystem.py | 3 ++- tests/test_singlediode.py | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 6e653d9d83..4b4733d4a2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -202,7 +202,8 @@ def has_spa_c(): # TODO remove this when we drop support for python 3.9. chandrupatla_available = sys.version_info >= (3, 10) chandrupatla = pytest.param( - "chandrupatla", marks=pytest.mark.skipif(not chandrupatla_available) + "chandrupatla", marks=pytest.mark.skipif(not chandrupatla_available, + reason="needs scipy 1.15") ) diff --git a/tests/test_pvsystem.py b/tests/test_pvsystem.py index 49928d0900..b7d8ba6173 100644 --- a/tests/test_pvsystem.py +++ b/tests/test_pvsystem.py @@ -1376,7 +1376,8 @@ def fixture_i_from_v(request): 'method, atol', [ ('lambertw', 1e-11), ('brentq', 1e-11), ('newton', 1e-11), pytest.param("chandrupatla", 1e-11, - marks=pytest.mark.skipif(not chandrupatla_available)), + marks=pytest.mark.skipif(not chandrupatla_available, + reason="needs scipy 1.15")), ] ) def test_i_from_v(fixture_i_from_v, method, atol): diff --git a/tests/test_singlediode.py b/tests/test_singlediode.py index dc373f48ff..6323bc9061 100644 --- a/tests/test_singlediode.py +++ b/tests/test_singlediode.py @@ -511,7 +511,8 @@ def test_bishop88_kwargs_transfer(method, method_kwargs, mocker, 'rtol': 1e-4, 'maxiter': 20, '_inexistent_param': "0.01" - }, marks=pytest.mark.skipif(not chandrupatla_available)), + }, marks=pytest.mark.skipif(not chandrupatla_available, + reason="needs scipy 1.15")), ]) def test_bishop88_kwargs_fails(method, method_kwargs, bishop88_arguments): """test invalid method_kwargs passed onto the optimizer fail""" From a72041171b81ac02b78f2aefb4fe3fb8a436e114 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 9 Jul 2025 10:36:59 -0400 Subject: [PATCH 08/11] Apply suggestions from code review Co-authored-by: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> --- pvlib/pvsystem.py | 5 ++++- pvlib/singlediode.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 6c1a5ef7c3..6709d258c0 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -2499,7 +2499,10 @@ def singlediode(photocurrent, saturation_current, resistance_series, method : str, default 'lambertw' Determines the method used to calculate points on the IV curve. The options are ``'lambertw'``, ``'newton'``, ``'brentq'``, or - ``'chandrupatla'`` (requires scipy 1.15 or greater). + ``'chandrupatla'``. + + .. note:: + ``'chandrupatla'`` requires scipy 1.15 or greater. Returns ------- diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index bf66516d2e..daf009e6f8 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -249,7 +249,7 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, Keyword arguments passed to root finder method. See :py:func:`scipy:scipy.optimize.brentq`, :py:func:`scipy:scipy.optimize.newton`, and - :py:func:`scipy:scipy.optimize.elementwise.find_root` for parameters. + :py:func:`scipy:scipy.optimize.elementwise.find_root` (when ``method='chandrupatla'``) for parameters. ``'full_output': True`` is allowed, and ``optimizer_output`` would be returned. See examples section. @@ -502,7 +502,7 @@ def vd_from_brent(voc, i, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, except ModuleNotFoundError as e: # TODO remove this when our minimum scipy version is >=1.15 msg = ( - "method='chandrupatla' requires scipy v1.15 or greater. " + "method='chandrupatla' requires scipy v1.15 or greater (available for Python3.10+). " "Select another method, or update your version of scipy. " f"({str(e)})" ) From 106712b31050a0b9494ae1ec849fe7f23c8b04c0 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 9 Jul 2025 10:59:47 -0400 Subject: [PATCH 09/11] more edits from review --- docs/sphinx/source/whatsnew/v0.13.1.rst | 4 +- pvlib/pvsystem.py | 23 +++++-- pvlib/singlediode.py | 87 +++++++++++++++---------- tests/test_singlediode.py | 8 +-- 4 files changed, 76 insertions(+), 46 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.13.1.rst b/docs/sphinx/source/whatsnew/v0.13.1.rst index e9399c1961..98314ab70e 100644 --- a/docs/sphinx/source/whatsnew/v0.13.1.rst +++ b/docs/sphinx/source/whatsnew/v0.13.1.rst @@ -18,7 +18,9 @@ Bug fixes Enhancements ~~~~~~~~~~~~ -* Added ``method='chandrupatla'`` to :py:func:`pvlib.pvsystem.singlediode`, +* Added ``method='chandrupatla'`` (faster than ``brentq`` and slower than ``newton``, + but convergence is guaranteed) to + :py:func:`pvlib.pvsystem.singlediode`, :py:func:`~pvlib.pvsystem.i_from_v`, :py:func:`~pvlib.pvsystem.v_from_i`, :py:func:`~pvlib.pvsystem.max_power_point`, diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 6709d258c0..f519acbfa1 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -2634,8 +2634,11 @@ def max_power_point(photocurrent, saturation_current, resistance_series, cells ``Ns`` and the builtin voltage ``Vbi`` of the intrinsic layer. [V]. method : str - either ``'newton'``, ``'brentq'``, or ``'chandrupatla'`` (requires - scipy 1.15 or greater) + either ``'newton'``, ``'brentq'``, or ``'chandrupatla'``. + + .. note:: + ``'chandrupatla'`` requires scipy 1.15 or greater. + Returns ------- @@ -2719,8 +2722,12 @@ def v_from_i(current, photocurrent, saturation_current, resistance_series, method : str Method to use: ``'lambertw'``, ``'newton'``, ``'brentq'``, or - ``'chandrupatla'`` (requires scipy 1.15 or greater). *Note*: - ``'brentq'`` is limited to 1st quadrant only. + ``'chandrupatla'``. *Note*: ``'brentq'`` is limited to 1st quadrant + only. + + .. note:: + ``'chandrupatla'`` requires scipy 1.15 or greater. + Returns ------- @@ -2802,8 +2809,12 @@ def i_from_v(voltage, photocurrent, saturation_current, resistance_series, method : str Method to use: ``'lambertw'``, ``'newton'``, ``'brentq'``, or - ``'chandrupatla'`` (requires scipy 1.15 or greater). *Note*: - ``'brentq'`` is limited to 1st quadrant only. + ``'chandrupatla'``. *Note*: ``'brentq'`` is limited to 1st quadrant + only. + + .. note:: + ``'chandrupatla'`` requires scipy 1.15 or greater. + Returns ------- diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index daf009e6f8..ae7ed1c602 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -242,14 +242,19 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, breakdown_exp : float, default 3.28 avalanche breakdown exponent :math:`m` [unitless] method : str, default 'newton' - Either ``'newton'``, ``'brentq'``, or ``'chandrupatla'`` (requires - scipy 1.15 or greater). ''method'' must be ``'newton'`` - if ``breakdown_factor`` is not 0. + Either ``'newton'``, ``'brentq'``, or ``'chandrupatla'``. + ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. + + .. note:: + ``'chandrupatla'`` requires scipy 1.15 or greater. + method_kwargs : dict, optional - Keyword arguments passed to root finder method. See - :py:func:`scipy:scipy.optimize.brentq`, - :py:func:`scipy:scipy.optimize.newton`, and - :py:func:`scipy:scipy.optimize.elementwise.find_root` (when ``method='chandrupatla'``) for parameters. + Keyword arguments passed to the root finder. For options, see: + + * ``method='brentq'``: :py:func:`scipy:scipy.optimize.brentq` + * ``method='newton'``: :py:func:`scipy:scipy.optimize.newton` + * ``method='chandrupatla'``: :py:func:`scipy:scipy.optimize.elementwise.find_root` + ``'full_output': True`` is allowed, and ``optimizer_output`` would be returned. See examples section. @@ -288,7 +293,7 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, .. [1] "Computer simulation of the effects of electrical mismatches in photovoltaic cell interconnection circuits" JW Bishop, Solar Cell (1988) :doi:`10.1016/0379-6787(88)90059-2` - """ + """ # noqa: E501 # collect args args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, d2mutau, NsVbi, @@ -336,11 +341,11 @@ def vd_from_brent(voc, v, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, except ModuleNotFoundError as e: # TODO remove this when our minimum scipy version is >=1.15 msg = ( - "method='chandrupatla' requires scipy v1.15 or greater. " - "Select another method, or update your version of scipy. " - f"({str(e)})" + "method='chandrupatla' requires scipy v1.15 or greater " + "(available for Python 3.10+). " + "Select another method, or update your version of scipy." ) - raise ImportError(msg) + raise ImportError(msg) from e voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) shape = _shape_of_max_size(voltage, voc_est) @@ -409,13 +414,19 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, breakdown_exp : float, default 3.28 avalanche breakdown exponent :math:`m` [unitless] method : str, default 'newton' - Either ``'newton'``, ``'brentq'``, or ``'chandrupatla'`` (requires - scipy 1.15 or greater). ''method'' must be ``'newton'`` - if ``breakdown_factor`` is not 0. + Either ``'newton'``, ``'brentq'``, or ``'chandrupatla'``. + ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. + + .. note:: + ``'chandrupatla'`` requires scipy 1.15 or greater. + method_kwargs : dict, optional - Keyword arguments passed to root finder method. See - :py:func:`scipy:scipy.optimize.brentq` and - :py:func:`scipy:scipy.optimize.newton` parameters. + Keyword arguments passed to the root finder. For options, see: + + * ``method='brentq'``: :py:func:`scipy:scipy.optimize.brentq` + * ``method='newton'``: :py:func:`scipy:scipy.optimize.newton` + * ``method='chandrupatla'``: :py:func:`scipy:scipy.optimize.elementwise.find_root` + ``'full_output': True`` is allowed, and ``optimizer_output`` would be returned. See examples section. @@ -454,7 +465,7 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, .. [1] "Computer simulation of the effects of electrical mismatches in photovoltaic cell interconnection circuits" JW Bishop, Solar Cell (1988) :doi:`10.1016/0379-6787(88)90059-2` - """ + """ # noqa: E501 # collect args args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, d2mutau, NsVbi, @@ -502,11 +513,11 @@ def vd_from_brent(voc, i, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, except ModuleNotFoundError as e: # TODO remove this when our minimum scipy version is >=1.15 msg = ( - "method='chandrupatla' requires scipy v1.15 or greater (available for Python3.10+). " - "Select another method, or update your version of scipy. " - f"({str(e)})" + "method='chandrupatla' requires scipy v1.15 or greater " + "(available for Python 3.10+). " + "Select another method, or update your version of scipy." ) - raise ImportError(msg) + raise ImportError(msg) from e shape = _shape_of_max_size(current, voc_est) vlo = np.zeros(shape) @@ -571,13 +582,19 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series, breakdown_exp : numeric, default 3.28 avalanche breakdown exponent :math:`m` [unitless] method : str, default 'newton' - Either ``'newton'``, ``'brentq'``, or ``'chandrupatla'`` (requires - scipy 1.15 or greater). ''method'' must be ``'newton'`` - if ``breakdown_factor`` is not 0. + Either ``'newton'``, ``'brentq'``, or ``'chandrupatla'``. + ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. + + .. note:: + ``'chandrupatla'`` requires scipy 1.15 or greater. + method_kwargs : dict, optional - Keyword arguments passed to root finder method. See - :py:func:`scipy:scipy.optimize.brentq` and - :py:func:`scipy:scipy.optimize.newton` parameters. + Keyword arguments passed to the root finder. For options, see: + + * ``method='brentq'``: :py:func:`scipy:scipy.optimize.brentq` + * ``method='newton'``: :py:func:`scipy:scipy.optimize.newton` + * ``method='chandrupatla'``: :py:func:`scipy:scipy.optimize.elementwise.find_root` + ``'full_output': True`` is allowed, and ``optimizer_output`` would be returned. See examples section. @@ -617,7 +634,7 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series, .. [1] "Computer simulation of the effects of electrical mismatches in photovoltaic cell interconnection circuits" JW Bishop, Solar Cell (1988) :doi:`10.1016/0379-6787(88)90059-2` - """ + """ # noqa: E501 # collect args args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, d2mutau, NsVbi, @@ -663,17 +680,17 @@ def fmpp(x, *a): except ModuleNotFoundError as e: # TODO remove this when our minimum scipy version is >=1.15 msg = ( - "method='chandrupatla' requires scipy v1.15 or greater. " - "Select another method, or update your version of scipy. " - f"({str(e)})" + "method='chandrupatla' requires scipy v1.15 or greater " + "(available for Python 3.10+). " + "Select another method, or update your version of scipy." ) - raise ImportError(msg) + raise ImportError(msg) from e vlo = np.zeros_like(photocurrent) vhi = np.full_like(photocurrent, voc_est) kwargs_trimmed = method_kwargs.copy() kwargs_trimmed.pop("full_output", None) # not valid for find_root - + result = find_root(fmpp, (vlo, vhi), args=args, diff --git a/tests/test_singlediode.py b/tests/test_singlediode.py index 6323bc9061..8f3b4012c3 100644 --- a/tests/test_singlediode.py +++ b/tests/test_singlediode.py @@ -463,10 +463,10 @@ def bishop88_arguments(): }), # can't include chandrupatla since the function is not available to patch # TODO: add this once chandrupatla becomes non-optional functionality - #('chandrupatla', { - # 'tolerances ': {'xtol': 1e-8, 'rtol': 1e-8}, - # 'maxiter': 30, - #}), + # ('chandrupatla', { + # 'tolerances ': {'xtol': 1e-8, 'rtol': 1e-8}, + # 'maxiter': 30, + # }), ]) def test_bishop88_kwargs_transfer(method, method_kwargs, mocker, bishop88_arguments): From d4cd02845adfe1c0cda9cd2064083d2141afe128 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 9 Jul 2025 11:10:26 -0400 Subject: [PATCH 10/11] docs fixes --- docs/sphinx/source/whatsnew/v0.13.1.rst | 2 +- pvlib/pvsystem.py | 2 +- pvlib/singlediode.py | 30 ++++++++++++------------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.13.1.rst b/docs/sphinx/source/whatsnew/v0.13.1.rst index 98314ab70e..7aaac497c5 100644 --- a/docs/sphinx/source/whatsnew/v0.13.1.rst +++ b/docs/sphinx/source/whatsnew/v0.13.1.rst @@ -24,7 +24,7 @@ Enhancements :py:func:`~pvlib.pvsystem.i_from_v`, :py:func:`~pvlib.pvsystem.v_from_i`, :py:func:`~pvlib.pvsystem.max_power_point`, - :py:func:`~pvlib.singlediode.bishop88`, + :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`) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index f519acbfa1..3dd9c9e9a8 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -2500,7 +2500,7 @@ def singlediode(photocurrent, saturation_current, resistance_series, Determines the method used to calculate points on the IV curve. The options are ``'lambertw'``, ``'newton'``, ``'brentq'``, or ``'chandrupatla'``. - + .. note:: ``'chandrupatla'`` requires scipy 1.15 or greater. diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index ae7ed1c602..337f270db1 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -242,8 +242,8 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, breakdown_exp : float, default 3.28 avalanche breakdown exponent :math:`m` [unitless] method : str, default 'newton' - Either ``'newton'``, ``'brentq'``, or ``'chandrupatla'``. - ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. + Either ``'newton'``, ``'brentq'``, or ``'chandrupatla'``. + ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. .. note:: ``'chandrupatla'`` requires scipy 1.15 or greater. @@ -251,9 +251,9 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, method_kwargs : dict, optional Keyword arguments passed to the root finder. For options, see: - * ``method='brentq'``: :py:func:`scipy:scipy.optimize.brentq` - * ``method='newton'``: :py:func:`scipy:scipy.optimize.newton` - * ``method='chandrupatla'``: :py:func:`scipy:scipy.optimize.elementwise.find_root` + * ``method='brentq'``: :py:func:`scipy:scipy.optimize.brentq` + * ``method='newton'``: :py:func:`scipy:scipy.optimize.newton` + * ``method='chandrupatla'``: :py:func:`scipy:scipy.optimize.elementwise.find_root` ``'full_output': True`` is allowed, and ``optimizer_output`` would be returned. See examples section. @@ -414,8 +414,8 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, breakdown_exp : float, default 3.28 avalanche breakdown exponent :math:`m` [unitless] method : str, default 'newton' - Either ``'newton'``, ``'brentq'``, or ``'chandrupatla'``. - ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. + Either ``'newton'``, ``'brentq'``, or ``'chandrupatla'``. + ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. .. note:: ``'chandrupatla'`` requires scipy 1.15 or greater. @@ -423,9 +423,9 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, method_kwargs : dict, optional Keyword arguments passed to the root finder. For options, see: - * ``method='brentq'``: :py:func:`scipy:scipy.optimize.brentq` - * ``method='newton'``: :py:func:`scipy:scipy.optimize.newton` - * ``method='chandrupatla'``: :py:func:`scipy:scipy.optimize.elementwise.find_root` + * ``method='brentq'``: :py:func:`scipy:scipy.optimize.brentq` + * ``method='newton'``: :py:func:`scipy:scipy.optimize.newton` + * ``method='chandrupatla'``: :py:func:`scipy:scipy.optimize.elementwise.find_root` ``'full_output': True`` is allowed, and ``optimizer_output`` would be returned. See examples section. @@ -582,8 +582,8 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series, breakdown_exp : numeric, default 3.28 avalanche breakdown exponent :math:`m` [unitless] method : str, default 'newton' - Either ``'newton'``, ``'brentq'``, or ``'chandrupatla'``. - ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. + Either ``'newton'``, ``'brentq'``, or ``'chandrupatla'``. + ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. .. note:: ``'chandrupatla'`` requires scipy 1.15 or greater. @@ -591,9 +591,9 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series, method_kwargs : dict, optional Keyword arguments passed to the root finder. For options, see: - * ``method='brentq'``: :py:func:`scipy:scipy.optimize.brentq` - * ``method='newton'``: :py:func:`scipy:scipy.optimize.newton` - * ``method='chandrupatla'``: :py:func:`scipy:scipy.optimize.elementwise.find_root` + * ``method='brentq'``: :py:func:`scipy:scipy.optimize.brentq` + * ``method='newton'``: :py:func:`scipy:scipy.optimize.newton` + * ``method='chandrupatla'``: :py:func:`scipy:scipy.optimize.elementwise.find_root` ``'full_output': True`` is allowed, and ``optimizer_output`` would be returned. See examples section. From ca6e4b896e131523e071582552882b2122627c25 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 9 Jul 2025 12:04:59 -0400 Subject: [PATCH 11/11] Apply suggestions from code review Co-authored-by: Cliff Hansen --- docs/sphinx/source/whatsnew/v0.13.1.rst | 4 ++-- pvlib/pvsystem.py | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.13.1.rst b/docs/sphinx/source/whatsnew/v0.13.1.rst index 7aaac497c5..0a63291378 100644 --- a/docs/sphinx/source/whatsnew/v0.13.1.rst +++ b/docs/sphinx/source/whatsnew/v0.13.1.rst @@ -18,8 +18,8 @@ Bug fixes Enhancements ~~~~~~~~~~~~ -* Added ``method='chandrupatla'`` (faster than ``brentq`` and slower than ``newton``, - but convergence is guaranteed) to +* Add ``method='chandrupatla'`` (faster than ``brentq`` and slower than ``newton``, + but convergence is guaranteed) as an option for :py:func:`pvlib.pvsystem.singlediode`, :py:func:`~pvlib.pvsystem.i_from_v`, :py:func:`~pvlib.pvsystem.v_from_i`, diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 3dd9c9e9a8..68e9111e28 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -2722,8 +2722,7 @@ def v_from_i(current, photocurrent, saturation_current, resistance_series, method : str Method to use: ``'lambertw'``, ``'newton'``, ``'brentq'``, or - ``'chandrupatla'``. *Note*: ``'brentq'`` is limited to 1st quadrant - only. + ``'chandrupatla'``. *Note*: ``'brentq'`` is limited to non-negative current. .. note:: ``'chandrupatla'`` requires scipy 1.15 or greater. @@ -2809,8 +2808,7 @@ def i_from_v(voltage, photocurrent, saturation_current, resistance_series, method : str Method to use: ``'lambertw'``, ``'newton'``, ``'brentq'``, or - ``'chandrupatla'``. *Note*: ``'brentq'`` is limited to 1st quadrant - only. + ``'chandrupatla'``. *Note*: ``'brentq'`` is limited to non-negative current. .. note:: ``'chandrupatla'`` requires scipy 1.15 or greater.