diff --git a/docs/sphinx/source/whatsnew/v0.13.1.rst b/docs/sphinx/source/whatsnew/v0.13.1.rst index cf591f7d0b..0a63291378 100644 --- a/docs/sphinx/source/whatsnew/v0.13.1.rst +++ b/docs/sphinx/source/whatsnew/v0.13.1.rst @@ -18,7 +18,16 @@ Bug fixes Enhancements ~~~~~~~~~~~~ - +* 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`, + :py:func:`~pvlib.pvsystem.max_power_point`, + :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`) + Documentation ~~~~~~~~~~~~~ @@ -44,3 +53,4 @@ Maintenance Contributors ~~~~~~~~~~~~ * Elijah Passmore (:ghuser:`eljpsm`) +* Kevin Anderson (:ghuser:`kandersolar`) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 23ca1a934a..68e9111e28 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -2498,7 +2498,11 @@ 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'``. + + .. note:: + ``'chandrupatla'`` requires scipy 1.15 or greater. Returns ------- @@ -2630,7 +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'`` or ``'brentq'`` + either ``'newton'``, ``'brentq'``, or ``'chandrupatla'``. + + .. note:: + ``'chandrupatla'`` requires scipy 1.15 or greater. + Returns ------- @@ -2713,8 +2721,12 @@ def v_from_i(current, photocurrent, saturation_current, resistance_series, 0 < nNsVth method : str - Method to use: ``'lambertw'``, ``'newton'``, or ``'brentq'``. *Note*: - ``'brentq'`` is limited to 1st quadrant only. + Method to use: ``'lambertw'``, ``'newton'``, ``'brentq'``, or + ``'chandrupatla'``. *Note*: ``'brentq'`` is limited to non-negative current. + + .. note:: + ``'chandrupatla'`` requires scipy 1.15 or greater. + Returns ------- @@ -2795,8 +2807,12 @@ def i_from_v(voltage, photocurrent, saturation_current, resistance_series, 0 < nNsVth method : str - Method to use: ``'lambertw'``, ``'newton'``, or ``'brentq'``. *Note*: - ``'brentq'`` is limited to 1st quadrant only. + Method to use: ``'lambertw'``, ``'newton'``, ``'brentq'``, or + ``'chandrupatla'``. *Note*: ``'brentq'`` is limited to non-negative current. + + .. note:: + ``'chandrupatla'`` requires scipy 1.15 or greater. + Returns ------- diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index e76ea8f263..337f270db1 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 @@ -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,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'`` or ``'brentq'``. ''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. @@ -291,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, @@ -333,6 +335,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 " + "(available for Python 3.10+). " + "Select another method, or update your version of scipy." + ) + raise ImportError(msg) from e + + 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,12 +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'`` or ``'brentq'``. ''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. @@ -432,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, @@ -474,6 +507,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 " + "(available for Python 3.10+). " + "Select another method, or update your version of scipy." + ) + raise ImportError(msg) from e + + 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,12 +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'`` or ``'brentq'``. ''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. @@ -571,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, @@ -611,6 +674,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 " + "(available for Python 3.10+). " + "Select another method, or update your version of scipy." + ) + 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, + **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) diff --git a/tests/conftest.py b/tests/conftest.py index 28ae973390..4b4733d4a2 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,18 @@ 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, + reason="needs scipy 1.15") +) + + @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 b58f9fd9e4..b7d8ba6173 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,7 +1373,12 @@ 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), + pytest.param("chandrupatla", 1e-11, + marks=pytest.mark.skipif(not chandrupatla_available, + reason="needs scipy 1.15")), + ] ) def test_i_from_v(fixture_i_from_v, method, atol): # Solution set loaded from fixture @@ -1400,44 +1407,43 @@ 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') +@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') +@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]) -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 +1461,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 +1471,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 +1500,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..8f3b4012c3 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']) +@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']) +@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 +148,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 +190,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 +260,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 +352,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 +460,13 @@ def bishop88_arguments(): 'xtol': 1e-8, 'rtol': 1e-8, '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, + # }), ]) def test_bishop88_kwargs_transfer(method, method_kwargs, mocker, bishop88_arguments): @@ -495,7 +505,14 @@ def test_bishop88_kwargs_transfer(method, method_kwargs, mocker, 'rtol': 1e-4, 'maxiter': 20, '_inexistent_param': "0.01" - }) + }), + pytest.param('chandrupatla', { + 'xtol': 1e-4, + 'rtol': 1e-4, + 'maxiter': 20, + '_inexistent_param': "0.01" + }, 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""" @@ -513,7 +530,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 +564,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 +580,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,