From 08bcf800921dbe7e3d5e4081ee41962b2187fdec Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Wed, 7 Jun 2023 23:30:14 +0200 Subject: [PATCH 01/31] Change of use in bishop88_i_from_v --- pvlib/singlediode.py | 42 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index 81d6ce3761..1f5a69a999 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -2,15 +2,19 @@ Low-level functions for solving the single diode equation. """ -from functools import partial +import warnings import numpy as np from pvlib.tools import _golden_sect_DataFrame from scipy.optimize import brentq, newton from scipy.special import lambertw -# set keyword arguments for all uses of newton in this module -newton = partial(newton, tol=1e-6, maxiter=100, fprime2=None) +# newton method default parameters for this module +NEWTON_DEFAULT_PARAMS = { + 'tol': 1e-6, + 'maxiter': 100, + 'fprime2': None +} # intrinsic voltage per cell junction for a:Si, CdTe, Mertens et al. VOLTAGE_BUILTIN = 0.9 # [V] @@ -206,7 +210,7 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, d2mutau=0, NsVbi=np.Inf, breakdown_factor=0., breakdown_voltage=-5.5, breakdown_exp=3.28, - method='newton'): + method='newton', **kwargs): """ Find current given any voltage. @@ -257,15 +261,22 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, d2mutau, NsVbi, breakdown_factor, breakdown_voltage, breakdown_exp) + method = method.lower() def fv(x, v, *a): # calculate voltage residual given diode voltage "x" return bishop88(x, *a)[1] - v - if method.lower() == 'brentq': + if method == 'brentq': # first bound the search using voc voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) + # get tolerances from kwargs + # default values are from scipy v1.10.1 + xtol = kwargs.pop('xtol', 2e-12) + rtol = kwargs.pop('rtol', 4 * np.finfo(float).eps) + maxiter = kwargs.pop('maxiter', 100) + # brentq only works with scalar inputs, so we need a set up function # and np.vectorize to repeatedly call the optimizer with the right # arguments for possible array input @@ -274,19 +285,34 @@ def vd_from_brent(voc, v, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, return brentq(fv, 0.0, voc, args=(v, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, breakdown_factor, breakdown_voltage, - breakdown_exp)) + breakdown_exp), + xtol=xtol, rtol=rtol, maxiter=maxiter) vd_from_brent_vectorized = np.vectorize(vd_from_brent) vd = vd_from_brent_vectorized(voc_est, voltage, *args) - elif method.lower() == 'newton': + elif method == 'newton': + # get tolerances from kwargs + tol = kwargs.pop('tol', NEWTON_DEFAULT_PARAMS['tol']) + rtol = kwargs.pop('rtol', 0) # scipy v1.10.1 default + maxiter = kwargs.pop('maxiter', NEWTON_DEFAULT_PARAMS['maxiter']) + # make sure all args are numpy arrays if max size > 1 # if voltage is an array, then make a copy to use for initial guess, v0 args, v0 = _prepare_newton_inputs((voltage,), args, voltage) vd = newton(func=lambda x, *a: fv(x, voltage, *a), x0=v0, fprime=lambda x, *a: bishop88(x, *a, gradients=True)[4], - args=args) + args=args, + tol=tol, rtol=rtol, maxiter=maxiter) else: raise NotImplementedError("Method '%s' isn't implemented" % method) + + # Warn user if any of their arguments was not used + # Intended to warn on mistype (e.g., xtol vs tol) + if len(kwargs) is not 0: + warnings.warn(f'Unused arguments {kwargs} in function call. ' \ + 'They were not passed to scipy solver {method}. ' \ + 'Please check scipy documentation.') + return bishop88(vd, *args)[0] From 0fcc51e7955d993c9701bf3fe0f4a3b1d79f4470 Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Thu, 8 Jun 2023 00:12:17 +0200 Subject: [PATCH 02/31] What the warnings test should look like --- pvlib/tests/test_singlediode.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pvlib/tests/test_singlediode.py b/pvlib/tests/test_singlediode.py index 4b2a9e9e66..c989c9d3c1 100644 --- a/pvlib/tests/test_singlediode.py +++ b/pvlib/tests/test_singlediode.py @@ -408,3 +408,17 @@ def test_pvsyst_breakdown(method, brk_params, recomb_params, poa, temp_cell, vsc_88 = bishop88_v_from_i(isc_88, *x, **y, method=method) assert np.isclose(vsc_88, 0.0, *tol) + + +# @pytest.mark.parametrize('method', ['newton', 'brentq']) +# def test_bishop88_warnings(method): +# """test warnings raised for incorrect kwargs""" +# kwargs = { +# 'method': method, +# 'tol': 1e-4, +# 'xtol': 1e-4, +# 'rtol': 1e-3, +# 'maxiter': 35 +# } +# pytest.warns(UserWarning, bishop88_i_from_v, +# args=(<¿¿??>), kwargs=kwargs) From c7917a3b7972512faf593562147c86163633c826 Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Fri, 9 Jun 2023 00:09:38 +0200 Subject: [PATCH 03/31] Apply Adriesse's suggestion Co-Authored-By: Anton Driesse <9001027+adriesse@users.noreply.github.com> --- pvlib/singlediode.py | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index 1f5a69a999..166410fc87 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -271,12 +271,6 @@ def fv(x, v, *a): # first bound the search using voc voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) - # get tolerances from kwargs - # default values are from scipy v1.10.1 - xtol = kwargs.pop('xtol', 2e-12) - rtol = kwargs.pop('rtol', 4 * np.finfo(float).eps) - maxiter = kwargs.pop('maxiter', 100) - # brentq only works with scalar inputs, so we need a set up function # and np.vectorize to repeatedly call the optimizer with the right # arguments for possible array input @@ -286,15 +280,15 @@ def vd_from_brent(voc, v, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, args=(v, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, breakdown_factor, breakdown_voltage, breakdown_exp), - xtol=xtol, rtol=rtol, maxiter=maxiter) + **kwargs) vd_from_brent_vectorized = np.vectorize(vd_from_brent) vd = vd_from_brent_vectorized(voc_est, voltage, *args) elif method == 'newton': # get tolerances from kwargs - tol = kwargs.pop('tol', NEWTON_DEFAULT_PARAMS['tol']) - rtol = kwargs.pop('rtol', 0) # scipy v1.10.1 default - maxiter = kwargs.pop('maxiter', NEWTON_DEFAULT_PARAMS['maxiter']) + kwargs['tol'] = kwargs.pop('tol', NEWTON_DEFAULT_PARAMS['tol']) + kwargs['maxiter'] = kwargs.pop('maxiter', + NEWTON_DEFAULT_PARAMS['maxiter']) # make sure all args are numpy arrays if max size > 1 # if voltage is an array, then make a copy to use for initial guess, v0 @@ -302,16 +296,9 @@ def vd_from_brent(voc, v, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, vd = newton(func=lambda x, *a: fv(x, voltage, *a), x0=v0, fprime=lambda x, *a: bishop88(x, *a, gradients=True)[4], args=args, - tol=tol, rtol=rtol, maxiter=maxiter) + **kwargs) else: raise NotImplementedError("Method '%s' isn't implemented" % method) - - # Warn user if any of their arguments was not used - # Intended to warn on mistype (e.g., xtol vs tol) - if len(kwargs) is not 0: - warnings.warn(f'Unused arguments {kwargs} in function call. ' \ - 'They were not passed to scipy solver {method}. ' \ - 'Please check scipy documentation.') return bishop88(vd, *args)[0] From fe430747dfec9c055cce1f477c1d4960783ff0a0 Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Fri, 9 Jun 2023 00:21:37 +0200 Subject: [PATCH 04/31] Update test_singlediode.py --- pvlib/tests/test_singlediode.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/pvlib/tests/test_singlediode.py b/pvlib/tests/test_singlediode.py index c989c9d3c1..0c26207a50 100644 --- a/pvlib/tests/test_singlediode.py +++ b/pvlib/tests/test_singlediode.py @@ -410,15 +410,19 @@ def test_pvsyst_breakdown(method, brk_params, recomb_params, poa, temp_cell, assert np.isclose(vsc_88, 0.0, *tol) -# @pytest.mark.parametrize('method', ['newton', 'brentq']) -# def test_bishop88_warnings(method): -# """test warnings raised for incorrect kwargs""" -# kwargs = { -# 'method': method, -# 'tol': 1e-4, -# 'xtol': 1e-4, -# 'rtol': 1e-3, -# 'maxiter': 35 -# } -# pytest.warns(UserWarning, bishop88_i_from_v, -# args=(<¿¿??>), kwargs=kwargs) +@pytest.mark.parametrize('method, kwargs', [ + ('newton', { + 'tol': 1e-3, + 'rtol': 1e-3, + 'maxiter': 20, + }), + ('brentq', { + 'xtol': 1e-3, + 'rtol': 1e-3, + 'maxiter': 20, + }) +]) +def test_bishop88_kwargs(method, kwargs): + """test kwargs modifying optimizer does not break anything""" + # bishop88_i_from_v(method=method, **kwargs) + assert False From 2276e91f911f81a8788b15b64bb3e61b4bf1afde Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Fri, 9 Jun 2023 13:44:33 +0200 Subject: [PATCH 05/31] Minimun runnable test for pass conditions --- pvlib/tests/test_singlediode.py | 53 +++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/pvlib/tests/test_singlediode.py b/pvlib/tests/test_singlediode.py index 0c26207a50..3d8191be9e 100644 --- a/pvlib/tests/test_singlediode.py +++ b/pvlib/tests/test_singlediode.py @@ -422,7 +422,54 @@ def test_pvsyst_breakdown(method, brk_params, recomb_params, poa, temp_cell, 'maxiter': 20, }) ]) -def test_bishop88_kwargs(method, kwargs): +def test_bishop88_kwargs_pass(method, kwargs): """test kwargs modifying optimizer does not break anything""" - # bishop88_i_from_v(method=method, **kwargs) - assert False + # build test tolerances from the kwargs passed to the method + tol = { + 'atol': np.nanmax([kwargs.get('tol', np.nan), + kwargs.get('xtol', np.nan)]), + 'rtol': kwargs.get('rtol') + } + poa, temp_cell, expected = ( # reference conditions + get_pvsyst_fs_495()['irrad_ref'], + get_pvsyst_fs_495()['temp_ref'], + { + 'pmp': (get_pvsyst_fs_495()['I_mp_ref'] * + get_pvsyst_fs_495()['V_mp_ref']), + 'isc': get_pvsyst_fs_495()['I_sc_ref'], + 'voc': get_pvsyst_fs_495()['V_oc_ref'] + } + ) + + pvsyst_fs_495 = get_pvsyst_fs_495() + # evaluate PVSyst model with thin-film recombination loss current + # at reference conditions + x = pvsystem.calcparams_pvsyst( + effective_irradiance=poa, temp_cell=temp_cell, + alpha_sc=pvsyst_fs_495['alpha_sc'], + gamma_ref=pvsyst_fs_495['gamma_ref'], + mu_gamma=pvsyst_fs_495['mu_gamma'], I_L_ref=pvsyst_fs_495['I_L_ref'], + I_o_ref=pvsyst_fs_495['I_o_ref'], R_sh_ref=pvsyst_fs_495['R_sh_ref'], + R_sh_0=pvsyst_fs_495['R_sh_0'], R_sh_exp=pvsyst_fs_495['R_sh_exp'], + R_s=pvsyst_fs_495['R_s'], + cells_in_series=pvsyst_fs_495['cells_in_series'], + EgRef=pvsyst_fs_495['EgRef'] + ) + y = dict(d2mutau=pvsyst_fs_495['d2mutau'], + NsVbi=VOLTAGE_BUILTIN*pvsyst_fs_495['cells_in_series']) + # Now, (*x, **y) comprise cell parameters + + # mpp_88 = bishop88_mpp(*x, **y, method=method, **kwargs) + # assert np.isclose(mpp_88[2], expected['pmp'], **tol) + + isc_88 = bishop88_i_from_v(0, *x, **y, method=method, **kwargs) + assert np.isclose(isc_88, expected['isc'], **tol) + + # voc_88 = bishop88_v_from_i(0, *x, **y, method=method, **kwargs) + # assert np.isclose(voc_88, expected['voc'], **tol) + + # ioc_88 = bishop88_i_from_v(voc_88, *x, **y, method=method, **kwargs) + # assert np.isclose(ioc_88, 0.0, **tol) + + # vsc_88 = bishop88_v_from_i(isc_88, *x, **y, method=method, **kwargs) + # assert np.isclose(vsc_88, 0.0, **tol) From 09c8ed0f8568c8e4188720e32636572879bae07f Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Fri, 9 Jun 2023 14:39:55 +0200 Subject: [PATCH 06/31] Cleanup import --- pvlib/singlediode.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index 166410fc87..9c0b326d84 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -2,7 +2,6 @@ Low-level functions for solving the single diode equation. """ -import warnings import numpy as np from pvlib.tools import _golden_sect_DataFrame From 856513114c4c83ed895868385d645739f23bee0d Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Fri, 9 Jun 2023 14:40:26 +0200 Subject: [PATCH 07/31] Add functional test for success and failing calls with kwargs --- pvlib/tests/test_singlediode.py | 108 ++++++++++++++++++++++---------- 1 file changed, 76 insertions(+), 32 deletions(-) diff --git a/pvlib/tests/test_singlediode.py b/pvlib/tests/test_singlediode.py index 3d8191be9e..5a635e2125 100644 --- a/pvlib/tests/test_singlediode.py +++ b/pvlib/tests/test_singlediode.py @@ -410,6 +410,36 @@ def test_pvsyst_breakdown(method, brk_params, recomb_params, poa, temp_cell, assert np.isclose(vsc_88, 0.0, *tol) +def bishop88_arguments(): + pvsyst_fs_495 = get_pvsyst_fs_495() + # evaluate PVSyst model with thin-film recombination loss current + # at reference conditions + x = pvsystem.calcparams_pvsyst( + effective_irradiance=pvsyst_fs_495['irrad_ref'], + temp_cell=pvsyst_fs_495['temp_ref'], + alpha_sc=pvsyst_fs_495['alpha_sc'], + gamma_ref=pvsyst_fs_495['gamma_ref'], + mu_gamma=pvsyst_fs_495['mu_gamma'], I_L_ref=pvsyst_fs_495['I_L_ref'], + I_o_ref=pvsyst_fs_495['I_o_ref'], R_sh_ref=pvsyst_fs_495['R_sh_ref'], + R_sh_0=pvsyst_fs_495['R_sh_0'], R_sh_exp=pvsyst_fs_495['R_sh_exp'], + R_s=pvsyst_fs_495['R_s'], + cells_in_series=pvsyst_fs_495['cells_in_series'], + EgRef=pvsyst_fs_495['EgRef'] + ) + y = dict(d2mutau=pvsyst_fs_495['d2mutau'], + NsVbi=VOLTAGE_BUILTIN*pvsyst_fs_495['cells_in_series']) + # Convert (*x, **y) in a bishop88_.* call to dict of arguments + args_dict = { + 'photocurrent': x[0], + 'saturation_current': x[1], + 'resistance_series': x[2], + 'resistance_shunt': x[3], + 'nNsVth': x[4], + } + args_dict.update(y) + return args_dict + + @pytest.mark.parametrize('method, kwargs', [ ('newton', { 'tol': 1e-3, @@ -430,46 +460,60 @@ def test_bishop88_kwargs_pass(method, kwargs): kwargs.get('xtol', np.nan)]), 'rtol': kwargs.get('rtol') } - poa, temp_cell, expected = ( # reference conditions - get_pvsyst_fs_495()['irrad_ref'], - get_pvsyst_fs_495()['temp_ref'], - { - 'pmp': (get_pvsyst_fs_495()['I_mp_ref'] * - get_pvsyst_fs_495()['V_mp_ref']), - 'isc': get_pvsyst_fs_495()['I_sc_ref'], - 'voc': get_pvsyst_fs_495()['V_oc_ref'] - } - ) + expected = { # from reference conditions + 'pmp': (get_pvsyst_fs_495()['I_mp_ref'] * + get_pvsyst_fs_495()['V_mp_ref']), + 'isc': get_pvsyst_fs_495()['I_sc_ref'], + 'voc': get_pvsyst_fs_495()['V_oc_ref'] + } - pvsyst_fs_495 = get_pvsyst_fs_495() - # evaluate PVSyst model with thin-film recombination loss current - # at reference conditions - x = pvsystem.calcparams_pvsyst( - effective_irradiance=poa, temp_cell=temp_cell, - alpha_sc=pvsyst_fs_495['alpha_sc'], - gamma_ref=pvsyst_fs_495['gamma_ref'], - mu_gamma=pvsyst_fs_495['mu_gamma'], I_L_ref=pvsyst_fs_495['I_L_ref'], - I_o_ref=pvsyst_fs_495['I_o_ref'], R_sh_ref=pvsyst_fs_495['R_sh_ref'], - R_sh_0=pvsyst_fs_495['R_sh_0'], R_sh_exp=pvsyst_fs_495['R_sh_exp'], - R_s=pvsyst_fs_495['R_s'], - cells_in_series=pvsyst_fs_495['cells_in_series'], - EgRef=pvsyst_fs_495['EgRef'] - ) - y = dict(d2mutau=pvsyst_fs_495['d2mutau'], - NsVbi=VOLTAGE_BUILTIN*pvsyst_fs_495['cells_in_series']) - # Now, (*x, **y) comprise cell parameters + # get arguments common to all bishop88_.* functions + bishop88_args = bishop88_arguments() - # mpp_88 = bishop88_mpp(*x, **y, method=method, **kwargs) + # mpp_88 = bishop88_mpp(**bishop88_args, method=method, **kwargs) # assert np.isclose(mpp_88[2], expected['pmp'], **tol) - isc_88 = bishop88_i_from_v(0, *x, **y, method=method, **kwargs) + isc_88 = bishop88_i_from_v(0, **bishop88_args, method=method, + **kwargs) assert np.isclose(isc_88, expected['isc'], **tol) - # voc_88 = bishop88_v_from_i(0, *x, **y, method=method, **kwargs) + # voc_88 = bishop88_v_from_i(0, **bishop88_args, method=method, + # **kwargs) # assert np.isclose(voc_88, expected['voc'], **tol) - # ioc_88 = bishop88_i_from_v(voc_88, *x, **y, method=method, **kwargs) + # ioc_88 = bishop88_i_from_v(voc_88, **bishop88_args, method=method, + # **kwargs) # assert np.isclose(ioc_88, 0.0, **tol) - # vsc_88 = bishop88_v_from_i(isc_88, *x, **y, method=method, **kwargs) + # vsc_88 = bishop88_v_from_i(isc_88, **bishop88_args, method=method, + # **kwargs) # assert np.isclose(vsc_88, 0.0, **tol) + + +@pytest.mark.parametrize('method, kwargs', [ + ('newton', { + 'tol': 1e-3, + 'rtol': 1e-3, + 'maxiter': 20, + '_inexistent_param': "0.01" + }), + ('brentq', { + 'xtol': 1e-3, + 'rtol': 1e-3, + 'maxiter': 20, + '_inexistent_param': "0.01" + }) +]) +def test_bishop88_kwargs_fails(method, kwargs): + """test invalid kwargs passed onto the optimizer fail""" + # get arguments common to all bishop88_.* functions + bishop88_args = bishop88_arguments() + + # pytest.raises(TypeError, bishop88_mpp, + # **bishop88_args, method=method, **kwargs) + + pytest.raises(TypeError, bishop88_i_from_v, + 0, **bishop88_args, method=method, **kwargs) + + # pytest.raises(TypeError, bishop88_v_from_i, + # 0, **bishop88_args, method=method, **kwargs) From 6a94af4fa9753e253040f4a45ed87fbdfa0e91c7 Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Fri, 9 Jun 2023 14:54:53 +0200 Subject: [PATCH 08/31] Add documentation (minimal, only bishop88_i_from_v) --- pvlib/singlediode.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index 9c0b326d84..2973b84fb5 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -250,6 +250,9 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, method : str, default 'newton' Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. + kwargs : keyword arguments + Passed to root finder method. See :ref:`scipy:scipy.optimize.brentq` + and :ref:`scipy:scipy.optimize.newton` parameters. Returns ------- @@ -287,7 +290,7 @@ def vd_from_brent(voc, v, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, # get tolerances from kwargs kwargs['tol'] = kwargs.pop('tol', NEWTON_DEFAULT_PARAMS['tol']) kwargs['maxiter'] = kwargs.pop('maxiter', - NEWTON_DEFAULT_PARAMS['maxiter']) + NEWTON_DEFAULT_PARAMS['maxiter']) # make sure all args are numpy arrays if max size > 1 # if voltage is an array, then make a copy to use for initial guess, v0 From 0f5c17da1d802bff85524ce8a374257745b284f9 Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Fri, 9 Jun 2023 15:24:59 +0200 Subject: [PATCH 09/31] Try to fix scipy links (I) --- pvlib/singlediode.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index 2973b84fb5..77add99434 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -251,8 +251,9 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. kwargs : keyword arguments - Passed to root finder method. See :ref:`scipy:scipy.optimize.brentq` - and :ref:`scipy:scipy.optimize.newton` parameters. + Passed to root finder method. See + :py:func:`scipy:optimize.brentq` and + :py:func:`scipy:scipy.optimize.newton` parameters. Returns ------- From 3423314259c80ee8f0fc0d8f40b17b1cc03bb784 Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Fri, 9 Jun 2023 15:38:22 +0200 Subject: [PATCH 10/31] =?UTF-8?q?It=20worked=20=F0=9F=8E=89=F0=9F=8E=89?= =?UTF-8?q?=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pvlib/singlediode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index 77add99434..323cbfdd4f 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -252,7 +252,7 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, if ``breakdown_factor`` is not 0. kwargs : keyword arguments Passed to root finder method. See - :py:func:`scipy:optimize.brentq` and + :py:func:`scipy:scipy.optimize.brentq` and :py:func:`scipy:scipy.optimize.newton` parameters. Returns From 3578397de714ab0faab67e8f3c480e8d4dfd200f Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Fri, 9 Jun 2023 15:50:04 +0200 Subject: [PATCH 11/31] Move default newton values to _prepare_newton_inputs --- pvlib/singlediode.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index 323cbfdd4f..33d9f7ec94 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -288,14 +288,12 @@ def vd_from_brent(voc, v, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, vd_from_brent_vectorized = np.vectorize(vd_from_brent) vd = vd_from_brent_vectorized(voc_est, voltage, *args) elif method == 'newton': - # get tolerances from kwargs - kwargs['tol'] = kwargs.pop('tol', NEWTON_DEFAULT_PARAMS['tol']) - kwargs['maxiter'] = kwargs.pop('maxiter', - NEWTON_DEFAULT_PARAMS['maxiter']) + # make sure all args are numpy arrays if max size > 1 # if voltage is an array, then make a copy to use for initial guess, v0 - args, v0 = _prepare_newton_inputs((voltage,), args, voltage) + args, v0, kwargs = _prepare_newton_inputs((voltage,), args, voltage, + kwargs) vd = newton(func=lambda x, *a: fv(x, voltage, *a), x0=v0, fprime=lambda x, *a: bishop88(x, *a, gradients=True)[4], args=args, @@ -498,7 +496,7 @@ def _get_size_and_shape(args): return size, shape -def _prepare_newton_inputs(i_or_v_tup, args, v0): +def _prepare_newton_inputs(i_or_v_tup, args, v0, kwargs): # broadcast arguments for newton method # the first argument should be a tuple, eg: (i,), (v,) or () size, shape = _get_size_and_shape(i_or_v_tup + args) @@ -508,7 +506,12 @@ def _prepare_newton_inputs(i_or_v_tup, args, v0): # copy v0 to a new array and broadcast it to the shape of max size if shape is not None: v0 = np.broadcast_to(v0, shape).copy() - return args, v0 + + # set abs tolerance and maxiter from kwargs if not provided + kwargs['tol'] = kwargs.pop('tol', NEWTON_DEFAULT_PARAMS['tol']) + kwargs['maxiter'] = kwargs.pop('maxiter', NEWTON_DEFAULT_PARAMS['maxiter']) + + return args, v0, kwargs def _lambertw_v_from_i(current, photocurrent, saturation_current, From e36fecf4bb83165723cee82cc70b6ad533da31cb Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Fri, 9 Jun 2023 16:13:25 +0200 Subject: [PATCH 12/31] Modify bishop88_v_from_i, bishop88_mpp; update tests and docs `maxiter` had to be changed so solutions converge near target value correctly. --- pvlib/singlediode.py | 42 +++++++++++++++++++++------------ pvlib/tests/test_singlediode.py | 42 ++++++++++++++++----------------- 2 files changed, 48 insertions(+), 36 deletions(-) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index 33d9f7ec94..956bcc8089 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -288,8 +288,6 @@ def vd_from_brent(voc, v, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, vd_from_brent_vectorized = np.vectorize(vd_from_brent) vd = vd_from_brent_vectorized(voc_est, voltage, *args) elif method == 'newton': - - # make sure all args are numpy arrays if max size > 1 # if voltage is an array, then make a copy to use for initial guess, v0 args, v0, kwargs = _prepare_newton_inputs((voltage,), args, voltage, @@ -308,7 +306,7 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, d2mutau=0, NsVbi=np.Inf, breakdown_factor=0., breakdown_voltage=-5.5, breakdown_exp=3.28, - method='newton'): + method='newton', **kwargs): """ Find voltage given any current. @@ -349,6 +347,10 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, method : str, default 'newton' Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. + kwargs : keyword arguments + Passed to root finder method. See + :py:func:`scipy:scipy.optimize.brentq` and + :py:func:`scipy:scipy.optimize.newton` parameters. Returns ------- @@ -359,6 +361,7 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, d2mutau, NsVbi, breakdown_factor, breakdown_voltage, breakdown_exp) + method = method.lower() # first bound the search using voc voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) @@ -366,7 +369,7 @@ def fi(x, i, *a): # calculate current residual given diode voltage "x" return bishop88(x, *a)[0] - i - if method.lower() == 'brentq': + if method == 'brentq': # brentq only works with scalar inputs, so we need a set up function # and np.vectorize to repeatedly call the optimizer with the right # arguments for possible array input @@ -375,17 +378,20 @@ def vd_from_brent(voc, i, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, return brentq(fi, 0.0, voc, args=(i, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, breakdown_factor, breakdown_voltage, - breakdown_exp)) + breakdown_exp), + **kwargs) vd_from_brent_vectorized = np.vectorize(vd_from_brent) vd = vd_from_brent_vectorized(voc_est, current, *args) - elif method.lower() == 'newton': + elif method == 'newton': # make sure all args are numpy arrays if max size > 1 # if voc_est is an array, then make a copy to use for initial guess, v0 - args, v0 = _prepare_newton_inputs((current,), args, voc_est) + args, v0, kwargs = _prepare_newton_inputs((current,), args, voc_est, + kwargs) vd = newton(func=lambda x, *a: fi(x, current, *a), x0=v0, fprime=lambda x, *a: bishop88(x, *a, gradients=True)[3], - args=args) + args=args, + **kwargs) else: raise NotImplementedError("Method '%s' isn't implemented" % method) return bishop88(vd, *args)[1] @@ -394,7 +400,7 @@ def vd_from_brent(voc, i, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, def bishop88_mpp(photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, d2mutau=0, NsVbi=np.Inf, breakdown_factor=0., breakdown_voltage=-5.5, - breakdown_exp=3.28, method='newton'): + breakdown_exp=3.28, method='newton', **kwargs): """ Find max power point. @@ -433,6 +439,10 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series, method : str, default 'newton' Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. + kwargs : keyword arguments + Passed to root finder method. See + :py:func:`scipy:scipy.optimize.brentq` and + :py:func:`scipy:scipy.optimize.newton` parameters. Returns ------- @@ -444,29 +454,31 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series, args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, d2mutau, NsVbi, breakdown_factor, breakdown_voltage, breakdown_exp) + method = method.lower() # first bound the search using voc voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) def fmpp(x, *a): return bishop88(x, *a, gradients=True)[6] - if method.lower() == 'brentq': + if method == 'brentq': # break out arguments for numpy.vectorize to handle broadcasting vec_fun = np.vectorize( lambda voc, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, vbr_a, vbr, vbr_exp: brentq(fmpp, 0.0, voc, args=(iph, isat, rs, rsh, gamma, d2mutau, NsVbi, - vbr_a, vbr, vbr_exp)) + vbr_a, vbr, vbr_exp), + **kwargs) ) vd = vec_fun(voc_est, *args) - elif method.lower() == 'newton': + elif method == 'newton': # make sure all args are numpy arrays if max size > 1 # if voc_est is an array, then make a copy to use for initial guess, v0 - args, v0 = _prepare_newton_inputs((), args, voc_est) + args, v0, kwargs = _prepare_newton_inputs((), args, voc_est, kwargs) vd = newton( func=fmpp, x0=v0, - fprime=lambda x, *a: bishop88(x, *a, gradients=True)[7], args=args - ) + fprime=lambda x, *a: bishop88(x, *a, gradients=True)[7], args=args, + **kwargs) else: raise NotImplementedError("Method '%s' isn't implemented" % method) return bishop88(vd, *args) diff --git a/pvlib/tests/test_singlediode.py b/pvlib/tests/test_singlediode.py index 5a635e2125..30904ff504 100644 --- a/pvlib/tests/test_singlediode.py +++ b/pvlib/tests/test_singlediode.py @@ -444,12 +444,12 @@ def bishop88_arguments(): ('newton', { 'tol': 1e-3, 'rtol': 1e-3, - 'maxiter': 20, + 'maxiter': 30, }), ('brentq', { 'xtol': 1e-3, 'rtol': 1e-3, - 'maxiter': 20, + 'maxiter': 30, }) ]) def test_bishop88_kwargs_pass(method, kwargs): @@ -470,36 +470,36 @@ def test_bishop88_kwargs_pass(method, kwargs): # get arguments common to all bishop88_.* functions bishop88_args = bishop88_arguments() - # mpp_88 = bishop88_mpp(**bishop88_args, method=method, **kwargs) - # assert np.isclose(mpp_88[2], expected['pmp'], **tol) + mpp_88 = bishop88_mpp(**bishop88_args, method=method, **kwargs) + assert np.isclose(mpp_88[2], expected['pmp'], **tol) isc_88 = bishop88_i_from_v(0, **bishop88_args, method=method, **kwargs) assert np.isclose(isc_88, expected['isc'], **tol) - # voc_88 = bishop88_v_from_i(0, **bishop88_args, method=method, - # **kwargs) - # assert np.isclose(voc_88, expected['voc'], **tol) + voc_88 = bishop88_v_from_i(0, **bishop88_args, method=method, + **kwargs) + assert np.isclose(voc_88, expected['voc'], **tol) - # ioc_88 = bishop88_i_from_v(voc_88, **bishop88_args, method=method, - # **kwargs) - # assert np.isclose(ioc_88, 0.0, **tol) + ioc_88 = bishop88_i_from_v(voc_88, **bishop88_args, method=method, + **kwargs) + assert np.isclose(ioc_88, 0.0, **tol) - # vsc_88 = bishop88_v_from_i(isc_88, **bishop88_args, method=method, - # **kwargs) - # assert np.isclose(vsc_88, 0.0, **tol) + vsc_88 = bishop88_v_from_i(isc_88, **bishop88_args, method=method, + **kwargs) + assert np.isclose(vsc_88, 0.0, **tol) @pytest.mark.parametrize('method, kwargs', [ ('newton', { - 'tol': 1e-3, - 'rtol': 1e-3, + 'tol': 1e-4, + 'rtol': 1e-4, 'maxiter': 20, '_inexistent_param': "0.01" }), ('brentq', { - 'xtol': 1e-3, - 'rtol': 1e-3, + 'xtol': 1e-4, + 'rtol': 1e-4, 'maxiter': 20, '_inexistent_param': "0.01" }) @@ -509,11 +509,11 @@ def test_bishop88_kwargs_fails(method, kwargs): # get arguments common to all bishop88_.* functions bishop88_args = bishop88_arguments() - # pytest.raises(TypeError, bishop88_mpp, - # **bishop88_args, method=method, **kwargs) + pytest.raises(TypeError, bishop88_mpp, + **bishop88_args, method=method, **kwargs) pytest.raises(TypeError, bishop88_i_from_v, 0, **bishop88_args, method=method, **kwargs) - # pytest.raises(TypeError, bishop88_v_from_i, - # 0, **bishop88_args, method=method, **kwargs) + pytest.raises(TypeError, bishop88_v_from_i, + 0, **bishop88_args, method=method, **kwargs) From f1165bc508f51af9b8dc4e6457bed7af221ecc13 Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Fri, 9 Jun 2023 16:56:48 +0200 Subject: [PATCH 13/31] Typo in kwargs type error Co-Authored-By: Cliff Hansen <5393711+cwhanse@users.noreply.github.com> --- pvlib/singlediode.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index 956bcc8089..4da3d8d91d 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -250,7 +250,7 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, method : str, default 'newton' Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. - kwargs : keyword arguments + kwargs : dict Passed to root finder method. See :py:func:`scipy:scipy.optimize.brentq` and :py:func:`scipy:scipy.optimize.newton` parameters. @@ -347,7 +347,7 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, method : str, default 'newton' Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. - kwargs : keyword arguments + kwargs : dict Passed to root finder method. See :py:func:`scipy:scipy.optimize.brentq` and :py:func:`scipy:scipy.optimize.newton` parameters. @@ -439,7 +439,7 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series, method : str, default 'newton' Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. - kwargs : keyword arguments + kwargs : dict Passed to root finder method. See :py:func:`scipy:scipy.optimize.brentq` and :py:func:`scipy:scipy.optimize.newton` parameters. From 8b070be61452862eb952949095c34cce2ab9ad21 Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Fri, 9 Jun 2023 17:06:21 +0200 Subject: [PATCH 14/31] Add what's new entry --- docs/sphinx/source/whatsnew/v0.10.0.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/sphinx/source/whatsnew/v0.10.0.rst b/docs/sphinx/source/whatsnew/v0.10.0.rst index a4406072f8..d76b93617b 100644 --- a/docs/sphinx/source/whatsnew/v0.10.0.rst +++ b/docs/sphinx/source/whatsnew/v0.10.0.rst @@ -21,6 +21,11 @@ Deprecations Enhancements ~~~~~~~~~~~~ +* Allow passing keyword arguments to :py:func:`scipy:scipy.optimize.brentq` and + :py:func:`scipy:scipy.optimize.newton` solvers in :py:func:`bishop88_mpp`, + :py:func:`bishop88_i_from_v` and :py:func:`bishop88_v_from_i`. Among others, + tolerance and number of iterations can be set. + (:issue:`1249`, :pull:`1764`) Bug fixes @@ -45,3 +50,4 @@ Requirements Contributors ~~~~~~~~~~~~ * Taos Transue (:ghuser:`reepoi`) +* Echedey Luis (:ghuser:`echedey-ls`) From 9e5b6348e51b8bc9daaedef253e5151ecc21a155 Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Fri, 9 Jun 2023 22:02:37 +0200 Subject: [PATCH 15/31] Delete fprime2 default newton param Co-Authored-By: Kevin Anderson <57452607+kandersolar@users.noreply.github.com> --- pvlib/singlediode.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index 4da3d8d91d..e55e901963 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -11,8 +11,7 @@ # newton method default parameters for this module NEWTON_DEFAULT_PARAMS = { 'tol': 1e-6, - 'maxiter': 100, - 'fprime2': None + 'maxiter': 100 } # intrinsic voltage per cell junction for a:Si, CdTe, Mertens et al. From 4ec2c3ce001571eadb2b6076f107c18be17d5395 Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Sat, 10 Jun 2023 10:22:28 +0200 Subject: [PATCH 16/31] Rename kwargs to method_kwargs Co-Authored-By: Anton Driesse <9001027+adriesse@users.noreply.github.com> --- pvlib/singlediode.py | 47 ++++++++++++++++++--------------- pvlib/tests/test_singlediode.py | 36 ++++++++++++------------- 2 files changed, 43 insertions(+), 40 deletions(-) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index e55e901963..e640a5ef16 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -208,7 +208,7 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, d2mutau=0, NsVbi=np.Inf, breakdown_factor=0., breakdown_voltage=-5.5, breakdown_exp=3.28, - method='newton', **kwargs): + method='newton', **method_kwargs): """ Find current given any voltage. @@ -249,7 +249,7 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, method : str, default 'newton' Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. - kwargs : dict + method_kwargs : dict Passed to root finder method. See :py:func:`scipy:scipy.optimize.brentq` and :py:func:`scipy:scipy.optimize.newton` parameters. @@ -282,19 +282,19 @@ def vd_from_brent(voc, v, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, args=(v, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, breakdown_factor, breakdown_voltage, breakdown_exp), - **kwargs) + **method_kwargs) vd_from_brent_vectorized = np.vectorize(vd_from_brent) vd = vd_from_brent_vectorized(voc_est, voltage, *args) elif method == 'newton': # make sure all args are numpy arrays if max size > 1 # if voltage is an array, then make a copy to use for initial guess, v0 - args, v0, kwargs = _prepare_newton_inputs((voltage,), args, voltage, - kwargs) + args, v0, method_kwargs = \ + _prepare_newton_inputs((voltage,), args, voltage, method_kwargs) vd = newton(func=lambda x, *a: fv(x, voltage, *a), x0=v0, fprime=lambda x, *a: bishop88(x, *a, gradients=True)[4], args=args, - **kwargs) + **method_kwargs) else: raise NotImplementedError("Method '%s' isn't implemented" % method) @@ -305,7 +305,7 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, d2mutau=0, NsVbi=np.Inf, breakdown_factor=0., breakdown_voltage=-5.5, breakdown_exp=3.28, - method='newton', **kwargs): + method='newton', **method_kwargs): """ Find voltage given any current. @@ -346,7 +346,7 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, method : str, default 'newton' Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. - kwargs : dict + method_kwargs : dict Passed to root finder method. See :py:func:`scipy:scipy.optimize.brentq` and :py:func:`scipy:scipy.optimize.newton` parameters. @@ -378,19 +378,19 @@ def vd_from_brent(voc, i, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, args=(i, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, breakdown_factor, breakdown_voltage, breakdown_exp), - **kwargs) + **method_kwargs) vd_from_brent_vectorized = np.vectorize(vd_from_brent) vd = vd_from_brent_vectorized(voc_est, current, *args) elif method == 'newton': # make sure all args are numpy arrays if max size > 1 # if voc_est is an array, then make a copy to use for initial guess, v0 - args, v0, kwargs = _prepare_newton_inputs((current,), args, voc_est, - kwargs) + args, v0, method_kwargs = \ + _prepare_newton_inputs((current,), args, voc_est, method_kwargs) vd = newton(func=lambda x, *a: fi(x, current, *a), x0=v0, fprime=lambda x, *a: bishop88(x, *a, gradients=True)[3], args=args, - **kwargs) + **method_kwargs) else: raise NotImplementedError("Method '%s' isn't implemented" % method) return bishop88(vd, *args)[1] @@ -399,7 +399,7 @@ def vd_from_brent(voc, i, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, def bishop88_mpp(photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, d2mutau=0, NsVbi=np.Inf, breakdown_factor=0., breakdown_voltage=-5.5, - breakdown_exp=3.28, method='newton', **kwargs): + breakdown_exp=3.28, method='newton', **method_kwargs): """ Find max power point. @@ -438,7 +438,7 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series, method : str, default 'newton' Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. - kwargs : dict + method_kwargs : dict Passed to root finder method. See :py:func:`scipy:scipy.optimize.brentq` and :py:func:`scipy:scipy.optimize.newton` parameters. @@ -467,17 +467,18 @@ def fmpp(x, *a): vbr_exp: brentq(fmpp, 0.0, voc, args=(iph, isat, rs, rsh, gamma, d2mutau, NsVbi, vbr_a, vbr, vbr_exp), - **kwargs) + **method_kwargs) ) vd = vec_fun(voc_est, *args) elif method == 'newton': # make sure all args are numpy arrays if max size > 1 # if voc_est is an array, then make a copy to use for initial guess, v0 - args, v0, kwargs = _prepare_newton_inputs((), args, voc_est, kwargs) + args, v0, method_kwargs = \ + _prepare_newton_inputs((), args, voc_est, method_kwargs) vd = newton( func=fmpp, x0=v0, fprime=lambda x, *a: bishop88(x, *a, gradients=True)[7], args=args, - **kwargs) + **method_kwargs) else: raise NotImplementedError("Method '%s' isn't implemented" % method) return bishop88(vd, *args) @@ -507,7 +508,7 @@ def _get_size_and_shape(args): return size, shape -def _prepare_newton_inputs(i_or_v_tup, args, v0, kwargs): +def _prepare_newton_inputs(i_or_v_tup, args, v0, method_kwargs): # broadcast arguments for newton method # the first argument should be a tuple, eg: (i,), (v,) or () size, shape = _get_size_and_shape(i_or_v_tup + args) @@ -518,11 +519,13 @@ def _prepare_newton_inputs(i_or_v_tup, args, v0, kwargs): if shape is not None: v0 = np.broadcast_to(v0, shape).copy() - # set abs tolerance and maxiter from kwargs if not provided - kwargs['tol'] = kwargs.pop('tol', NEWTON_DEFAULT_PARAMS['tol']) - kwargs['maxiter'] = kwargs.pop('maxiter', NEWTON_DEFAULT_PARAMS['maxiter']) + # set abs tolerance and maxiter from method_kwargs if not provided + method_kwargs['tol'] = \ + method_kwargs.pop('tol', NEWTON_DEFAULT_PARAMS['tol']) + method_kwargs['maxiter'] = \ + method_kwargs.pop('maxiter', NEWTON_DEFAULT_PARAMS['maxiter']) - return args, v0, kwargs + return args, v0, method_kwargs def _lambertw_v_from_i(current, photocurrent, saturation_current, diff --git a/pvlib/tests/test_singlediode.py b/pvlib/tests/test_singlediode.py index 30904ff504..d0dec45394 100644 --- a/pvlib/tests/test_singlediode.py +++ b/pvlib/tests/test_singlediode.py @@ -440,7 +440,7 @@ def bishop88_arguments(): return args_dict -@pytest.mark.parametrize('method, kwargs', [ +@pytest.mark.parametrize('method, method_kwargs', [ ('newton', { 'tol': 1e-3, 'rtol': 1e-3, @@ -452,13 +452,13 @@ def bishop88_arguments(): 'maxiter': 30, }) ]) -def test_bishop88_kwargs_pass(method, kwargs): - """test kwargs modifying optimizer does not break anything""" - # build test tolerances from the kwargs passed to the method +def test_bishop88_kwargs_pass(method, method_kwargs): + """test method_kwargs modifying optimizer does not break anything""" + # build test tolerances from method_kwargs tol = { - 'atol': np.nanmax([kwargs.get('tol', np.nan), - kwargs.get('xtol', np.nan)]), - 'rtol': kwargs.get('rtol') + 'atol': np.nanmax([method_kwargs.get('tol', np.nan), + method_kwargs.get('xtol', np.nan)]), + 'rtol': method_kwargs.get('rtol') } expected = { # from reference conditions 'pmp': (get_pvsyst_fs_495()['I_mp_ref'] * @@ -470,27 +470,27 @@ def test_bishop88_kwargs_pass(method, kwargs): # get arguments common to all bishop88_.* functions bishop88_args = bishop88_arguments() - mpp_88 = bishop88_mpp(**bishop88_args, method=method, **kwargs) + mpp_88 = bishop88_mpp(**bishop88_args, method=method, **method_kwargs) assert np.isclose(mpp_88[2], expected['pmp'], **tol) isc_88 = bishop88_i_from_v(0, **bishop88_args, method=method, - **kwargs) + **method_kwargs) assert np.isclose(isc_88, expected['isc'], **tol) voc_88 = bishop88_v_from_i(0, **bishop88_args, method=method, - **kwargs) + **method_kwargs) assert np.isclose(voc_88, expected['voc'], **tol) ioc_88 = bishop88_i_from_v(voc_88, **bishop88_args, method=method, - **kwargs) + **method_kwargs) assert np.isclose(ioc_88, 0.0, **tol) vsc_88 = bishop88_v_from_i(isc_88, **bishop88_args, method=method, - **kwargs) + **method_kwargs) assert np.isclose(vsc_88, 0.0, **tol) -@pytest.mark.parametrize('method, kwargs', [ +@pytest.mark.parametrize('method, method_kwargs', [ ('newton', { 'tol': 1e-4, 'rtol': 1e-4, @@ -504,16 +504,16 @@ def test_bishop88_kwargs_pass(method, kwargs): '_inexistent_param': "0.01" }) ]) -def test_bishop88_kwargs_fails(method, kwargs): - """test invalid kwargs passed onto the optimizer fail""" +def test_bishop88_kwargs_fails(method, method_kwargs): + """test invalid method_kwargs passed onto the optimizer fail""" # get arguments common to all bishop88_.* functions bishop88_args = bishop88_arguments() pytest.raises(TypeError, bishop88_mpp, - **bishop88_args, method=method, **kwargs) + **bishop88_args, method=method, **method_kwargs) pytest.raises(TypeError, bishop88_i_from_v, - 0, **bishop88_args, method=method, **kwargs) + 0, **bishop88_args, method=method, **method_kwargs) pytest.raises(TypeError, bishop88_v_from_i, - 0, **bishop88_args, method=method, **kwargs) + 0, **bishop88_args, method=method, **method_kwargs) From 3445d279ad75ffcdb691431f883bfcf0f18287c7 Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Tue, 13 Jun 2023 01:15:34 +0200 Subject: [PATCH 17/31] Adriesse's suggestion to make full output available Missing tests Co-Authored-By: Anton Driesse <9001027+adriesse@users.noreply.github.com> --- pvlib/singlediode.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index e640a5ef16..e4e5d93389 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -298,7 +298,13 @@ def vd_from_brent(voc, v, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, else: raise NotImplementedError("Method '%s' isn't implemented" % method) - return bishop88(vd, *args)[0] + # When 'full_output' parameter is specified, returned 'vd' is a tuple with + # many elements, where the root is the first one. So we use it to output + # the bishop88 result and return tuple(scalar, tuple with method results) + if method_kwargs.get('full_output') is True: + return (bishop88(vd[0], *args)[0], vd) + else: + return bishop88(vd, *args)[0] def bishop88_v_from_i(current, photocurrent, saturation_current, @@ -393,7 +399,14 @@ def vd_from_brent(voc, i, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, **method_kwargs) else: raise NotImplementedError("Method '%s' isn't implemented" % method) - return bishop88(vd, *args)[1] + + # When 'full_output' parameter is specified, returned 'vd' is a tuple with + # many elements, where the root is the first one. So we use it to output + # the bishop88 result and return tuple(scalar, tuple with method results) + if method_kwargs.get('full_output') is True: + return (bishop88(vd[0], *args)[1], vd) + else: + return bishop88(vd, *args)[1] def bishop88_mpp(photocurrent, saturation_current, resistance_series, @@ -481,7 +494,15 @@ def fmpp(x, *a): **method_kwargs) else: raise NotImplementedError("Method '%s' isn't implemented" % method) - return bishop88(vd, *args) + + # When 'full_output' parameter is specified, returned 'vd' is a tuple with + # many elements, where the root is the first one. So we use it to output + # the bishop88 result and return + # tuple(tuple with bishop88 solution, tuple with method results) + if method_kwargs.get('full_output') is True: + return (bishop88(vd[0], *args), vd) + else: + return bishop88(vd, *args) def _get_size_and_shape(args): From 6208a89dd745a8ac478d3a6696c051b73c1606e8 Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Tue, 13 Jun 2023 01:16:55 +0200 Subject: [PATCH 18/31] Update tests from Kevin suggestion Co-Authored-By: Kevin Anderson <57452607+kandersolar@users.noreply.github.com> --- pvlib/tests/test_singlediode.py | 56 +++++++++++++-------------------- 1 file changed, 21 insertions(+), 35 deletions(-) diff --git a/pvlib/tests/test_singlediode.py b/pvlib/tests/test_singlediode.py index d0dec45394..f0b485b266 100644 --- a/pvlib/tests/test_singlediode.py +++ b/pvlib/tests/test_singlediode.py @@ -6,9 +6,11 @@ import pandas as pd import scipy from pvlib import pvsystem +from pvlib import singlediode from pvlib.singlediode import (bishop88_mpp, estimate_voc, VOLTAGE_BUILTIN, bishop88, bishop88_i_from_v, bishop88_v_from_i) import pytest +import pytest_mock from .conftest import DATA_DIR POA = 888 @@ -442,52 +444,36 @@ def bishop88_arguments(): @pytest.mark.parametrize('method, method_kwargs', [ ('newton', { - 'tol': 1e-3, - 'rtol': 1e-3, + 'tol': 1e-8, + 'rtol': 1e-8, 'maxiter': 30, }), ('brentq', { - 'xtol': 1e-3, - 'rtol': 1e-3, + 'xtol': 1e-8, + 'rtol': 1e-8, 'maxiter': 30, }) ]) -def test_bishop88_kwargs_pass(method, method_kwargs): +def test_bishop88_kwargs_transfer(method, method_kwargs, mocker): """test method_kwargs modifying optimizer does not break anything""" - # build test tolerances from method_kwargs - tol = { - 'atol': np.nanmax([method_kwargs.get('tol', np.nan), - method_kwargs.get('xtol', np.nan)]), - 'rtol': method_kwargs.get('rtol') - } - expected = { # from reference conditions - 'pmp': (get_pvsyst_fs_495()['I_mp_ref'] * - get_pvsyst_fs_495()['V_mp_ref']), - 'isc': get_pvsyst_fs_495()['I_sc_ref'], - 'voc': get_pvsyst_fs_495()['V_oc_ref'] - } + # patch method namespace at singlediode module namespace + optimizer_mock = mocker.patch('pvlib.singlediode.' + method) # get arguments common to all bishop88_.* functions bishop88_args = bishop88_arguments() - mpp_88 = bishop88_mpp(**bishop88_args, method=method, **method_kwargs) - assert np.isclose(mpp_88[2], expected['pmp'], **tol) + # kwargs assertions are done with <= operator since calls inside + # bishop88_* are done with more keyword arguments than 'method_kwargs' + # and this comparison is only available with sets, so dict.items() is used - isc_88 = bishop88_i_from_v(0, **bishop88_args, method=method, - **method_kwargs) - assert np.isclose(isc_88, expected['isc'], **tol) + bishop88_i_from_v(0, **bishop88_args, method=method, **method_kwargs) + assert method_kwargs.items() <= optimizer_mock.call_args.kwargs.items() - voc_88 = bishop88_v_from_i(0, **bishop88_args, method=method, - **method_kwargs) - assert np.isclose(voc_88, expected['voc'], **tol) + bishop88_v_from_i(0, **bishop88_args, method=method, **method_kwargs) + assert method_kwargs.items() <= optimizer_mock.call_args.kwargs.items() - ioc_88 = bishop88_i_from_v(voc_88, **bishop88_args, method=method, - **method_kwargs) - assert np.isclose(ioc_88, 0.0, **tol) - - vsc_88 = bishop88_v_from_i(isc_88, **bishop88_args, method=method, - **method_kwargs) - assert np.isclose(vsc_88, 0.0, **tol) + bishop88_mpp(**bishop88_args, method=method, **method_kwargs) + assert method_kwargs.items() <= optimizer_mock.call_args.kwargs.items() @pytest.mark.parametrize('method, method_kwargs', [ @@ -509,11 +495,11 @@ def test_bishop88_kwargs_fails(method, method_kwargs): # get arguments common to all bishop88_.* functions bishop88_args = bishop88_arguments() - pytest.raises(TypeError, bishop88_mpp, - **bishop88_args, method=method, **method_kwargs) - pytest.raises(TypeError, bishop88_i_from_v, 0, **bishop88_args, method=method, **method_kwargs) pytest.raises(TypeError, bishop88_v_from_i, 0, **bishop88_args, method=method, **method_kwargs) + + pytest.raises(TypeError, bishop88_mpp, + **bishop88_args, method=method, **method_kwargs) From c04eadec4740afe7f5fa55bb5a13af7154b41675 Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Tue, 13 Jun 2023 01:28:16 +0200 Subject: [PATCH 19/31] I won't miss Py3.7 --- pvlib/tests/test_singlediode.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pvlib/tests/test_singlediode.py b/pvlib/tests/test_singlediode.py index f0b485b266..ce714615b5 100644 --- a/pvlib/tests/test_singlediode.py +++ b/pvlib/tests/test_singlediode.py @@ -467,13 +467,16 @@ def test_bishop88_kwargs_transfer(method, method_kwargs, mocker): # and this comparison is only available with sets, so dict.items() is used bishop88_i_from_v(0, **bishop88_args, method=method, **method_kwargs) - assert method_kwargs.items() <= optimizer_mock.call_args.kwargs.items() + assert set(method_kwargs.items()) \ + .issubset(optimizer_mock.call_args.kwargs.items()) bishop88_v_from_i(0, **bishop88_args, method=method, **method_kwargs) - assert method_kwargs.items() <= optimizer_mock.call_args.kwargs.items() + assert set(method_kwargs.items()) \ + .issubset(optimizer_mock.call_args.kwargs.items()) bishop88_mpp(**bishop88_args, method=method, **method_kwargs) - assert method_kwargs.items() <= optimizer_mock.call_args.kwargs.items() + assert set(method_kwargs.items()) \ + .issubset(optimizer_mock.call_args.kwargs.items()) @pytest.mark.parametrize('method, method_kwargs', [ From 9dc1d340e47e65d1d48a53812e3292ab61a9ad4e Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Tue, 13 Jun 2023 01:42:25 +0200 Subject: [PATCH 20/31] Uhm, yeah, don't like this solution but anyways... --- pvlib/tests/test_singlediode.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/pvlib/tests/test_singlediode.py b/pvlib/tests/test_singlediode.py index ce714615b5..9609014dfb 100644 --- a/pvlib/tests/test_singlediode.py +++ b/pvlib/tests/test_singlediode.py @@ -462,21 +462,22 @@ def test_bishop88_kwargs_transfer(method, method_kwargs, mocker): # get arguments common to all bishop88_.* functions bishop88_args = bishop88_arguments() - # kwargs assertions are done with <= operator since calls inside - # bishop88_* are done with more keyword arguments than 'method_kwargs' - # and this comparison is only available with sets, so dict.items() is used + # only test if present kwargs keys in call are more than those passed to + # bishop_* functions. Value comparison can't be made due to 'numpy.ndarray' + # not being a hashable type. This can be changed with Py3.7 is deprecated: + # assert method_kwargs.items() <= optimizer_mock.call_args.kwargs.items() bishop88_i_from_v(0, **bishop88_args, method=method, **method_kwargs) - assert set(method_kwargs.items()) \ - .issubset(optimizer_mock.call_args.kwargs.items()) + assert set(optimizer_mock.call_args.kwargs.keys()) \ + .issuperset(method_kwargs.keys()) bishop88_v_from_i(0, **bishop88_args, method=method, **method_kwargs) - assert set(method_kwargs.items()) \ - .issubset(optimizer_mock.call_args.kwargs.items()) + assert set(optimizer_mock.call_args.kwargs.keys()) \ + .issuperset(method_kwargs.keys()) bishop88_mpp(**bishop88_args, method=method, **method_kwargs) - assert set(method_kwargs.items()) \ - .issubset(optimizer_mock.call_args.kwargs.items()) + assert set(optimizer_mock.call_args.kwargs.keys()) \ + .issuperset(method_kwargs.keys()) @pytest.mark.parametrize('method, method_kwargs', [ From b043f672965906bf0212b692484de16ceb9508e9 Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Tue, 13 Jun 2023 11:51:49 +0200 Subject: [PATCH 21/31] Tests should be fixed now --- pvlib/tests/test_singlediode.py | 45 ++++++++++++++------------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/pvlib/tests/test_singlediode.py b/pvlib/tests/test_singlediode.py index 9609014dfb..8d6f9f09a6 100644 --- a/pvlib/tests/test_singlediode.py +++ b/pvlib/tests/test_singlediode.py @@ -6,11 +6,9 @@ import pandas as pd import scipy from pvlib import pvsystem -from pvlib import singlediode from pvlib.singlediode import (bishop88_mpp, estimate_voc, VOLTAGE_BUILTIN, bishop88, bishop88_i_from_v, bishop88_v_from_i) import pytest -import pytest_mock from .conftest import DATA_DIR POA = 888 @@ -412,6 +410,7 @@ def test_pvsyst_breakdown(method, brk_params, recomb_params, poa, temp_cell, assert np.isclose(vsc_88, 0.0, *tol) +@pytest.fixture def bishop88_arguments(): pvsyst_fs_495 = get_pvsyst_fs_495() # evaluate PVSyst model with thin-film recombination loss current @@ -454,30 +453,26 @@ def bishop88_arguments(): 'maxiter': 30, }) ]) -def test_bishop88_kwargs_transfer(method, method_kwargs, mocker): +def test_bishop88_kwargs_transfer(method, method_kwargs, mocker, + bishop88_arguments): """test method_kwargs modifying optimizer does not break anything""" # patch method namespace at singlediode module namespace optimizer_mock = mocker.patch('pvlib.singlediode.' + method) - # get arguments common to all bishop88_.* functions - bishop88_args = bishop88_arguments() + # check kwargs passed to bishop_.* are a subset of the call args + # since they are called with more keyword arguments - # only test if present kwargs keys in call are more than those passed to - # bishop_* functions. Value comparison can't be made due to 'numpy.ndarray' - # not being a hashable type. This can be changed with Py3.7 is deprecated: - # assert method_kwargs.items() <= optimizer_mock.call_args.kwargs.items() + bishop88_i_from_v(0, **bishop88_arguments, method=method, **method_kwargs) + _, kwargs = optimizer_mock.call_args + assert method_kwargs.items() <= kwargs.items() - bishop88_i_from_v(0, **bishop88_args, method=method, **method_kwargs) - assert set(optimizer_mock.call_args.kwargs.keys()) \ - .issuperset(method_kwargs.keys()) + bishop88_v_from_i(0, **bishop88_arguments, method=method, **method_kwargs) + _, kwargs = optimizer_mock.call_args + assert method_kwargs.items() <= kwargs.items() - bishop88_v_from_i(0, **bishop88_args, method=method, **method_kwargs) - assert set(optimizer_mock.call_args.kwargs.keys()) \ - .issuperset(method_kwargs.keys()) - - bishop88_mpp(**bishop88_args, method=method, **method_kwargs) - assert set(optimizer_mock.call_args.kwargs.keys()) \ - .issuperset(method_kwargs.keys()) + bishop88_mpp(**bishop88_arguments, method=method, **method_kwargs) + _, kwargs = optimizer_mock.call_args + assert method_kwargs.items() <= kwargs.items() @pytest.mark.parametrize('method, method_kwargs', [ @@ -494,16 +489,14 @@ def test_bishop88_kwargs_transfer(method, method_kwargs, mocker): '_inexistent_param': "0.01" }) ]) -def test_bishop88_kwargs_fails(method, method_kwargs): +def test_bishop88_kwargs_fails(method, method_kwargs, bishop88_arguments): """test invalid method_kwargs passed onto the optimizer fail""" - # get arguments common to all bishop88_.* functions - bishop88_args = bishop88_arguments() pytest.raises(TypeError, bishop88_i_from_v, - 0, **bishop88_args, method=method, **method_kwargs) + 0, **bishop88_arguments, method=method, **method_kwargs) pytest.raises(TypeError, bishop88_v_from_i, - 0, **bishop88_args, method=method, **method_kwargs) - + 0, **bishop88_arguments, method=method, **method_kwargs) + pytest.raises(TypeError, bishop88_mpp, - **bishop88_args, method=method, **method_kwargs) + **bishop88_arguments, method=method, **method_kwargs) From 0b89f56660bb8c22566ba52e68417599a785715a Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Tue, 13 Jun 2023 13:17:50 +0200 Subject: [PATCH 22/31] full_output=True tests --- pvlib/tests/test_singlediode.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pvlib/tests/test_singlediode.py b/pvlib/tests/test_singlediode.py index 8d6f9f09a6..a7cb0d5a57 100644 --- a/pvlib/tests/test_singlediode.py +++ b/pvlib/tests/test_singlediode.py @@ -500,3 +500,18 @@ def test_bishop88_kwargs_fails(method, method_kwargs, bishop88_arguments): pytest.raises(TypeError, bishop88_mpp, **bishop88_arguments, method=method, **method_kwargs) + + +@pytest.mark.parametrize('method', ['newton', 'brentq']) +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} + + ret_val = bishop88_i_from_v(0, **bishop88_arguments, method=method, + **method_kwargs) + assert isinstance(ret_val, tuple) # ret_val must be a tuple + assert len(ret_val) == 2 # of two elements + assert isinstance(ret_val[0], float) # first one has bishop88 result + assert isinstance(ret_val[1], tuple) # second is output from optimizer + # any root finder returns at least 2 elements with full_output=True + assert len(ret_val[1]) >= 2 From c626a492c73c53ddd60346da5d494c16a1dbb059 Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Tue, 13 Jun 2023 13:42:36 +0200 Subject: [PATCH 23/31] Update docs --- pvlib/singlediode.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index e4e5d93389..a72c2cb56f 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -258,6 +258,8 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, ------- current : numeric current (I) at the specified voltage (V). [A] + optimizer_return : optional, present if ``full_output = True`` + see root finder documentation for selected method """ # collect args args = (photocurrent, saturation_current, resistance_series, @@ -361,6 +363,8 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, ------- voltage : numeric voltage (V) at the specified current (I) in volts [V] + optimizer_return : optional, present if ``full_output = True`` + see root finder documentation for selected method """ # collect args args = (photocurrent, saturation_current, resistance_series, @@ -461,6 +465,8 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series, tuple max power current ``i_mp`` [A], max power voltage ``v_mp`` [V], and max power ``p_mp`` [W] + optimizer_return : optional, present if ``full_output = True`` + see root finder documentation for selected method """ # collect args args = (photocurrent, saturation_current, resistance_series, From 542dd30f74414c3dcb09278e3b5fbec3900e66d9 Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Tue, 13 Jun 2023 13:44:14 +0200 Subject: [PATCH 24/31] Now these are tests --- pvlib/tests/test_singlediode.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pvlib/tests/test_singlediode.py b/pvlib/tests/test_singlediode.py index a7cb0d5a57..cbd58da865 100644 --- a/pvlib/tests/test_singlediode.py +++ b/pvlib/tests/test_singlediode.py @@ -515,3 +515,21 @@ def test_bishop88_full_output_kwarg(method, bishop88_arguments): assert isinstance(ret_val[1], tuple) # second is output from optimizer # any root finder returns at least 2 elements with full_output=True assert len(ret_val[1]) >= 2 + + ret_val = bishop88_v_from_i(0, **bishop88_arguments, method=method, + **method_kwargs) + assert isinstance(ret_val, tuple) # ret_val must be a tuple + assert len(ret_val) == 2 # of two elements + assert isinstance(ret_val[0], float) # first one has bishop88 result + assert isinstance(ret_val[1], tuple) # second is output from optimizer + # any root finder returns at least 2 elements with full_output=True + assert len(ret_val[1]) >= 2 + + ret_val = bishop88_mpp(**bishop88_arguments, method=method, + **method_kwargs) + assert isinstance(ret_val, tuple) # ret_val must be a tuple + assert len(ret_val) == 2 # of two elements + assert isinstance(ret_val[0], float) # first one has bishop88 result + assert isinstance(ret_val[1], tuple) # second is output from optimizer + # any root finder returns at least 2 elements with full_output=True + assert len(ret_val[1]) >= 2 From 440c9febd781055a9c8a72c4567623684c7ccd53 Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Tue, 13 Jun 2023 13:51:10 +0200 Subject: [PATCH 25/31] Bruh should have run locally --- pvlib/tests/test_singlediode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/tests/test_singlediode.py b/pvlib/tests/test_singlediode.py index cbd58da865..2c8a40f9a8 100644 --- a/pvlib/tests/test_singlediode.py +++ b/pvlib/tests/test_singlediode.py @@ -529,7 +529,7 @@ def test_bishop88_full_output_kwarg(method, bishop88_arguments): **method_kwargs) assert isinstance(ret_val, tuple) # ret_val must be a tuple assert len(ret_val) == 2 # of two elements - assert isinstance(ret_val[0], float) # first one has bishop88 result + assert isinstance(ret_val[0], tuple) # first one has bishop88 result assert isinstance(ret_val[1], tuple) # second is output from optimizer # any root finder returns at least 2 elements with full_output=True assert len(ret_val[1]) >= 2 From 42c547bfcd174b1ec33becf083b262480eb53f5d Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Mon, 19 Jun 2023 16:01:44 +0200 Subject: [PATCH 26/31] Docs: add use cases and document as `**method_kwargs :` --- pvlib/singlediode.py | 76 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 12 deletions(-) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index a72c2cb56f..5813bb1051 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -249,17 +249,35 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, method : str, default 'newton' Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. - method_kwargs : dict - Passed to root finder method. See + **method_kwargs : + Keyword arguments passed to root finder method. See :py:func:`scipy:scipy.optimize.brentq` and :py:func:`scipy:scipy.optimize.newton` parameters. + ``full_output=True`` is allowed. See examples section. Returns ------- current : numeric current (I) at the specified voltage (V). [A] - optimizer_return : optional, present if ``full_output = True`` - see root finder documentation for selected method + optimizer_output : optional, present if ``full_output=True`` + see root finder documentation for selected method. + Found root is diode voltage in [1]_. + + Examples + -------- + Use default values: + + >>> i = bishop88_i_from_v() + + Specify tolerances and maximum number of iterations: + + >>> i = bishop88_i_from_v(, + tol=1e-3, rtol=1e-3, maxiter=20) + + Retrieve full output from the root finder: + + >>> i, method_output = bishop88_i_from_v(, + full_output=True) """ # collect args args = (photocurrent, saturation_current, resistance_series, @@ -354,8 +372,8 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, method : str, default 'newton' Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. - method_kwargs : dict - Passed to root finder method. See + **method_kwargs : + Keyword arguments passed to root finder method. See :py:func:`scipy:scipy.optimize.brentq` and :py:func:`scipy:scipy.optimize.newton` parameters. @@ -363,8 +381,25 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, ------- voltage : numeric voltage (V) at the specified current (I) in volts [V] - optimizer_return : optional, present if ``full_output = True`` - see root finder documentation for selected method + optimizer_output : optional, present if ``full_output=True`` + see root finder documentation for selected method. + Found root is diode voltage in [1]_. + + Examples + -------- + Use default values: + + >>> v = bishop88_v_from_i() + + Specify tolerances and maximum number of iterations: + + >>> v = bishop88_v_from_i(, + tol=1e-3, rtol=1e-3, maxiter=20) + + Retrieve full output from the root finder: + + >>> v, method_output = bishop88_v_from_i(, + full_output=True) """ # collect args args = (photocurrent, saturation_current, resistance_series, @@ -455,8 +490,8 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series, method : str, default 'newton' Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. - method_kwargs : dict - Passed to root finder method. See + **method_kwargs : + Keyword arguments passed to root finder method. See :py:func:`scipy:scipy.optimize.brentq` and :py:func:`scipy:scipy.optimize.newton` parameters. @@ -465,8 +500,25 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series, tuple max power current ``i_mp`` [A], max power voltage ``v_mp`` [V], and max power ``p_mp`` [W] - optimizer_return : optional, present if ``full_output = True`` - see root finder documentation for selected method + optimizer_output : optional, present if ``full_output=True`` + see root finder documentation for selected method. + Found root is diode voltage in [1]_. + + Examples + -------- + Use default values: + + >>> i_mp, v_mp, p_mp = bishop88_mpp() + + Specify tolerances and maximum number of iterations: + + >>> i_mp, v_mp, p_mp = bishop88_mpp(, + tol=1e-3, rtol=1e-3, maxiter=20) + + Retrieve full output from the root finder: + + >>> (i_mp, v_mp, p_mp), method_output = bishop88_mpp(, + full_output=True) """ # collect args args = (photocurrent, saturation_current, resistance_series, From 13dbb570d9467e1908ca4d4542861a011cd8687c Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Mon, 19 Jun 2023 16:16:42 +0200 Subject: [PATCH 27/31] Tests: also test expected length of the first value In case some unpacking went wrong --- pvlib/tests/test_singlediode.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pvlib/tests/test_singlediode.py b/pvlib/tests/test_singlediode.py index 2c8a40f9a8..c067c72755 100644 --- a/pvlib/tests/test_singlediode.py +++ b/pvlib/tests/test_singlediode.py @@ -530,6 +530,7 @@ def test_bishop88_full_output_kwarg(method, bishop88_arguments): assert isinstance(ret_val, tuple) # ret_val must be a tuple assert len(ret_val) == 2 # of two elements assert isinstance(ret_val[0], tuple) # first one has bishop88 result + assert len(ret_val[0]) == 3 # of three elements (I,V,P) assert isinstance(ret_val[1], tuple) # second is output from optimizer # any root finder returns at least 2 elements with full_output=True assert len(ret_val[1]) >= 2 From 6d83c8156d400157f86cdd091c059ab702077fe4 Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Mon, 19 Jun 2023 16:55:25 +0200 Subject: [PATCH 28/31] Docs: doctest examples - make functional --- pvlib/singlediode.py | 51 +++++++++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index 5813bb1051..78a4bbee5f 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -265,19 +265,26 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, Examples -------- + Using the following arguments: + + >>> args = {'photocurrent': 1.5743233463848496, + ... 'saturation_current': 9.62e-10, 'resistance_series': 4.6, + ... 'resistance_shunt': 5000.0, 'nNsVth': 4.106701846714363, + ... 'd2mutau': 1.31, 'NsVbi': 97.2} + Use default values: - >>> i = bishop88_i_from_v() + >>> i = bishop88_i_from_v(0.0, **args) Specify tolerances and maximum number of iterations: - >>> i = bishop88_i_from_v(, - tol=1e-3, rtol=1e-3, maxiter=20) + >>> i = bishop88_i_from_v(0.0, **args, + ... tol=1e-3, rtol=1e-3, maxiter=20) Retrieve full output from the root finder: - >>> i, method_output = bishop88_i_from_v(, - full_output=True) + >>> i, method_output = bishop88_i_from_v(0.0, **args, + ... full_output=True) """ # collect args args = (photocurrent, saturation_current, resistance_series, @@ -387,19 +394,26 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, Examples -------- + Using the following arguments: + + >>> args = {'photocurrent': 1.5743233463848496, + ... 'saturation_current': 9.62e-10, 'resistance_series': 4.6, + ... 'resistance_shunt': 5000.0, 'nNsVth': 4.106701846714363, + ... 'd2mutau': 1.31, 'NsVbi': 97.2} + Use default values: - >>> v = bishop88_v_from_i() + >>> v = bishop88_v_from_i(0.0, **args) Specify tolerances and maximum number of iterations: - >>> v = bishop88_v_from_i(, - tol=1e-3, rtol=1e-3, maxiter=20) + >>> v = bishop88_v_from_i(0.0, **args, + ... tol=1e-3, rtol=1e-3, maxiter=20) Retrieve full output from the root finder: - >>> v, method_output = bishop88_v_from_i(, - full_output=True) + >>> v, method_output = bishop88_v_from_i(0.0, **args, + ... full_output=True) """ # collect args args = (photocurrent, saturation_current, resistance_series, @@ -506,19 +520,26 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series, Examples -------- + Using the following arguments: + + >>> args = {'photocurrent': 1.5743233463848496, + ... 'saturation_current': 9.62e-10, 'resistance_series': 4.6, + ... 'resistance_shunt': 5000.0, 'nNsVth': 4.106701846714363, + ... 'd2mutau': 1.31, 'NsVbi': 97.2} + Use default values: - >>> i_mp, v_mp, p_mp = bishop88_mpp() + >>> i_mp, v_mp, p_mp = bishop88_mpp(**args) Specify tolerances and maximum number of iterations: - >>> i_mp, v_mp, p_mp = bishop88_mpp(, - tol=1e-3, rtol=1e-3, maxiter=20) + >>> i_mp, v_mp, p_mp = bishop88_mpp(**args, + ... tol=1e-3, rtol=1e-3, maxiter=20) Retrieve full output from the root finder: - >>> (i_mp, v_mp, p_mp), method_output = bishop88_mpp(, - full_output=True) + >>> (i_mp, v_mp, p_mp), method_output = bishop88_mpp(**args, + ... full_output=True) """ # collect args args = (photocurrent, saturation_current, resistance_series, From 114b0037f90bf8ddb66cc32457440068bfa65c93 Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Wed, 21 Jun 2023 22:14:19 +0200 Subject: [PATCH 29/31] Behaviour & docs: Change how method_kwargs are passed, enhance docs --- pvlib/singlediode.py | 64 +++++++++++++++++---------------- pvlib/tests/test_singlediode.py | 24 ++++++++----- 2 files changed, 48 insertions(+), 40 deletions(-) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index 78a4bbee5f..46ba282913 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -208,7 +208,7 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, d2mutau=0, NsVbi=np.Inf, breakdown_factor=0., breakdown_voltage=-5.5, breakdown_exp=3.28, - method='newton', **method_kwargs): + method='newton', method_kwargs={}): """ Find current given any voltage. @@ -249,28 +249,28 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, method : str, default 'newton' Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. - **method_kwargs : + 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. - ``full_output=True`` is allowed. See examples section. + ``'full_output': True`` is allowed, and ``optimizer_output`` would be + returned. See examples section. Returns ------- current : numeric current (I) at the specified voltage (V). [A] - optimizer_output : optional, present if ``full_output=True`` + optimizer_output : tuple, optional, if specified in ``method_kwargs`` see root finder documentation for selected method. Found root is diode voltage in [1]_. Examples -------- - Using the following arguments: + Using the following arguments that may come from any + `calcparams_.*` function in :py:mod:`pvlib.pvsystem`: - >>> args = {'photocurrent': 1.5743233463848496, - ... 'saturation_current': 9.62e-10, 'resistance_series': 4.6, - ... 'resistance_shunt': 5000.0, 'nNsVth': 4.106701846714363, - ... 'd2mutau': 1.31, 'NsVbi': 97.2} + >>> args = {'photocurrent': 1., 'saturation_current': 9e-10, 'nNsVth': 4., + ... 'resistance_series': 4., 'resistance_shunt': 5000.0} Use default values: @@ -279,12 +279,12 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, Specify tolerances and maximum number of iterations: >>> i = bishop88_i_from_v(0.0, **args, - ... tol=1e-3, rtol=1e-3, maxiter=20) + ... method_kwargs={'tol': 1e-3, 'rtol': 1e-3, 'maxiter': 20}) Retrieve full output from the root finder: >>> i, method_output = bishop88_i_from_v(0.0, **args, - ... full_output=True) + ... method_kwargs={'full_output': True}) """ # collect args args = (photocurrent, saturation_current, resistance_series, @@ -338,7 +338,7 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, d2mutau=0, NsVbi=np.Inf, breakdown_factor=0., breakdown_voltage=-5.5, breakdown_exp=3.28, - method='newton', **method_kwargs): + method='newton', method_kwargs={}): """ Find voltage given any current. @@ -379,27 +379,28 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, method : str, default 'newton' Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. - **method_kwargs : + 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. + ``'full_output': True`` is allowed, and ``optimizer_output`` would be + returned. See examples section. Returns ------- voltage : numeric voltage (V) at the specified current (I) in volts [V] - optimizer_output : optional, present if ``full_output=True`` + optimizer_output : tuple, optional, if specified in ``method_kwargs`` see root finder documentation for selected method. Found root is diode voltage in [1]_. Examples -------- - Using the following arguments: + Using the following arguments that may come from any + `calcparams_.*` function in :py:mod:`pvlib.pvsystem`: - >>> args = {'photocurrent': 1.5743233463848496, - ... 'saturation_current': 9.62e-10, 'resistance_series': 4.6, - ... 'resistance_shunt': 5000.0, 'nNsVth': 4.106701846714363, - ... 'd2mutau': 1.31, 'NsVbi': 97.2} + >>> args = {'photocurrent': 1., 'saturation_current': 9e-10, 'nNsVth': 4., + ... 'resistance_series': 4., 'resistance_shunt': 5000.0} Use default values: @@ -408,12 +409,12 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, Specify tolerances and maximum number of iterations: >>> v = bishop88_v_from_i(0.0, **args, - ... tol=1e-3, rtol=1e-3, maxiter=20) + ... method_kwargs={'tol': 1e-3, 'rtol': 1e-3, 'maxiter': 20}) Retrieve full output from the root finder: >>> v, method_output = bishop88_v_from_i(0.0, **args, - ... full_output=True) + ... method_kwargs={'full_output': True}) """ # collect args args = (photocurrent, saturation_current, resistance_series, @@ -465,7 +466,7 @@ def vd_from_brent(voc, i, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, def bishop88_mpp(photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, d2mutau=0, NsVbi=np.Inf, breakdown_factor=0., breakdown_voltage=-5.5, - breakdown_exp=3.28, method='newton', **method_kwargs): + breakdown_exp=3.28, method='newton', method_kwargs={}): """ Find max power point. @@ -504,28 +505,29 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series, method : str, default 'newton' Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0. - **method_kwargs : + 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. + ``'full_output': True`` is allowed, and ``optimizer_output`` would be + returned. See examples section. Returns ------- tuple max power current ``i_mp`` [A], max power voltage ``v_mp`` [V], and max power ``p_mp`` [W] - optimizer_output : optional, present if ``full_output=True`` + optimizer_output : tuple, optional, if specified in ``method_kwargs`` see root finder documentation for selected method. Found root is diode voltage in [1]_. Examples -------- - Using the following arguments: + Using the following arguments that may come from any + `calcparams_.*` function in :py:mod:`pvlib.pvsystem`: - >>> args = {'photocurrent': 1.5743233463848496, - ... 'saturation_current': 9.62e-10, 'resistance_series': 4.6, - ... 'resistance_shunt': 5000.0, 'nNsVth': 4.106701846714363, - ... 'd2mutau': 1.31, 'NsVbi': 97.2} + >>> args = {'photocurrent': 1., 'saturation_current': 9e-10, 'nNsVth': 4., + ... 'resistance_series': 4., 'resistance_shunt': 5000.0} Use default values: @@ -534,12 +536,12 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series, Specify tolerances and maximum number of iterations: >>> i_mp, v_mp, p_mp = bishop88_mpp(**args, - ... tol=1e-3, rtol=1e-3, maxiter=20) + ... method_kwargs={'tol': 1e-3, 'rtol': 1e-3, 'maxiter': 20}) Retrieve full output from the root finder: >>> (i_mp, v_mp, p_mp), method_output = bishop88_mpp(**args, - ... full_output=True) + ... method_kwargs={'full_output': True}) """ # collect args args = (photocurrent, saturation_current, resistance_series, diff --git a/pvlib/tests/test_singlediode.py b/pvlib/tests/test_singlediode.py index c067c72755..3a82f4d790 100644 --- a/pvlib/tests/test_singlediode.py +++ b/pvlib/tests/test_singlediode.py @@ -462,15 +462,18 @@ def test_bishop88_kwargs_transfer(method, method_kwargs, mocker, # check kwargs passed to bishop_.* are a subset of the call args # since they are called with more keyword arguments - bishop88_i_from_v(0, **bishop88_arguments, method=method, **method_kwargs) + bishop88_i_from_v(0, **bishop88_arguments, method=method, + method_kwargs=method_kwargs) _, kwargs = optimizer_mock.call_args assert method_kwargs.items() <= kwargs.items() - bishop88_v_from_i(0, **bishop88_arguments, method=method, **method_kwargs) + bishop88_v_from_i(0, **bishop88_arguments, method=method, + method_kwargs=method_kwargs) _, kwargs = optimizer_mock.call_args assert method_kwargs.items() <= kwargs.items() - bishop88_mpp(**bishop88_arguments, method=method, **method_kwargs) + bishop88_mpp(**bishop88_arguments, method=method, + method_kwargs=method_kwargs) _, kwargs = optimizer_mock.call_args assert method_kwargs.items() <= kwargs.items() @@ -493,13 +496,16 @@ def test_bishop88_kwargs_fails(method, method_kwargs, bishop88_arguments): """test invalid method_kwargs passed onto the optimizer fail""" pytest.raises(TypeError, bishop88_i_from_v, - 0, **bishop88_arguments, method=method, **method_kwargs) + 0, **bishop88_arguments, method=method, + method_kwargs=method_kwargs) pytest.raises(TypeError, bishop88_v_from_i, - 0, **bishop88_arguments, method=method, **method_kwargs) + 0, **bishop88_arguments, method=method, + method_kwargs=method_kwargs) pytest.raises(TypeError, bishop88_mpp, - **bishop88_arguments, method=method, **method_kwargs) + **bishop88_arguments, method=method, + method_kwargs=method_kwargs) @pytest.mark.parametrize('method', ['newton', 'brentq']) @@ -508,7 +514,7 @@ def test_bishop88_full_output_kwarg(method, bishop88_arguments): method_kwargs = {'full_output': True} ret_val = bishop88_i_from_v(0, **bishop88_arguments, method=method, - **method_kwargs) + method_kwargs=method_kwargs) assert isinstance(ret_val, tuple) # ret_val must be a tuple assert len(ret_val) == 2 # of two elements assert isinstance(ret_val[0], float) # first one has bishop88 result @@ -517,7 +523,7 @@ def test_bishop88_full_output_kwarg(method, bishop88_arguments): assert len(ret_val[1]) >= 2 ret_val = bishop88_v_from_i(0, **bishop88_arguments, method=method, - **method_kwargs) + method_kwargs=method_kwargs) assert isinstance(ret_val, tuple) # ret_val must be a tuple assert len(ret_val) == 2 # of two elements assert isinstance(ret_val[0], float) # first one has bishop88 result @@ -526,7 +532,7 @@ def test_bishop88_full_output_kwarg(method, bishop88_arguments): assert len(ret_val[1]) >= 2 ret_val = bishop88_mpp(**bishop88_arguments, method=method, - **method_kwargs) + method_kwargs=method_kwargs) assert isinstance(ret_val, tuple) # ret_val must be a tuple assert len(ret_val) == 2 # of two elements assert isinstance(ret_val[0], tuple) # first one has bishop88 result From 06749ee7f084fdf2d0e937f19cc5dc48a698c4f4 Mon Sep 17 00:00:00 2001 From: echedey-ls <80125792+echedey-ls@users.noreply.github.com> Date: Thu, 22 Jun 2023 00:49:42 +0200 Subject: [PATCH 30/31] omg who would have expected the mutable default parameter bug --- pvlib/singlediode.py | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index 46ba282913..283c7f639e 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -208,7 +208,7 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, d2mutau=0, NsVbi=np.Inf, breakdown_factor=0., breakdown_voltage=-5.5, breakdown_exp=3.28, - method='newton', method_kwargs={}): + method='newton', method_kwargs=None): """ Find current given any voltage. @@ -278,12 +278,12 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, Specify tolerances and maximum number of iterations: - >>> i = bishop88_i_from_v(0.0, **args, + >>> i = bishop88_i_from_v(0.0, **args, method='newton', ... method_kwargs={'tol': 1e-3, 'rtol': 1e-3, 'maxiter': 20}) Retrieve full output from the root finder: - >>> i, method_output = bishop88_i_from_v(0.0, **args, + >>> i, method_output = bishop88_i_from_v(0.0, **args, method='newton', ... method_kwargs={'full_output': True}) """ # collect args @@ -292,6 +292,11 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, breakdown_factor, breakdown_voltage, breakdown_exp) method = method.lower() + # method_kwargs create dict if not provided + # this pattern avoids bugs with Mutable Default Parameters + if not method_kwargs: + method_kwargs = {} + def fv(x, v, *a): # calculate voltage residual given diode voltage "x" return bishop88(x, *a)[1] - v @@ -338,7 +343,7 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, d2mutau=0, NsVbi=np.Inf, breakdown_factor=0., breakdown_voltage=-5.5, breakdown_exp=3.28, - method='newton', method_kwargs={}): + method='newton', method_kwargs=None): """ Find voltage given any current. @@ -408,12 +413,12 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, Specify tolerances and maximum number of iterations: - >>> v = bishop88_v_from_i(0.0, **args, + >>> v = bishop88_v_from_i(0.0, **args, method='newton', ... method_kwargs={'tol': 1e-3, 'rtol': 1e-3, 'maxiter': 20}) Retrieve full output from the root finder: - >>> v, method_output = bishop88_v_from_i(0.0, **args, + >>> v, method_output = bishop88_v_from_i(0.0, **args, method='newton', ... method_kwargs={'full_output': True}) """ # collect args @@ -421,6 +426,12 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, resistance_shunt, nNsVth, d2mutau, NsVbi, breakdown_factor, breakdown_voltage, breakdown_exp) method = method.lower() + + # method_kwargs create dict if not provided + # this pattern avoids bugs with Mutable Default Parameters + if not method_kwargs: + method_kwargs = {} + # first bound the search using voc voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) @@ -466,7 +477,7 @@ def vd_from_brent(voc, i, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, def bishop88_mpp(photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, d2mutau=0, NsVbi=np.Inf, breakdown_factor=0., breakdown_voltage=-5.5, - breakdown_exp=3.28, method='newton', method_kwargs={}): + breakdown_exp=3.28, method='newton', method_kwargs=None): """ Find max power point. @@ -535,19 +546,25 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series, Specify tolerances and maximum number of iterations: - >>> i_mp, v_mp, p_mp = bishop88_mpp(**args, + >>> i_mp, v_mp, p_mp = bishop88_mpp(**args, method='newton', ... method_kwargs={'tol': 1e-3, 'rtol': 1e-3, 'maxiter': 20}) Retrieve full output from the root finder: >>> (i_mp, v_mp, p_mp), method_output = bishop88_mpp(**args, - ... method_kwargs={'full_output': True}) + ... method='newton', method_kwargs={'full_output': True}) """ # collect args args = (photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, d2mutau, NsVbi, breakdown_factor, breakdown_voltage, breakdown_exp) method = method.lower() + + # method_kwargs create dict if not provided + # this pattern avoids bugs with Mutable Default Parameters + if not method_kwargs: + method_kwargs = {} + # first bound the search using voc voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) From 9406e2a517db1e00be3a042864cbb17230df7b29 Mon Sep 17 00:00:00 2001 From: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> Date: Thu, 22 Jun 2023 16:14:53 +0200 Subject: [PATCH 31/31] Apply suggestions from code review Co-authored by @kandersolar Co-authored-by: Kevin Anderson --- docs/sphinx/source/whatsnew/v0.10.0.rst | 6 ++++-- pvlib/singlediode.py | 6 ++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.10.0.rst b/docs/sphinx/source/whatsnew/v0.10.0.rst index fd61476c8a..90f666422c 100644 --- a/docs/sphinx/source/whatsnew/v0.10.0.rst +++ b/docs/sphinx/source/whatsnew/v0.10.0.rst @@ -29,8 +29,10 @@ Enhancements * Added `map_variables` parameter to :py:func:`pvlib.iotools.read_srml` and :py:func:`pvlib.iotools.read_srml_month_from_solardat` (:pull:`1773`) * Allow passing keyword arguments to :py:func:`scipy:scipy.optimize.brentq` and - :py:func:`scipy:scipy.optimize.newton` solvers in :py:func:`bishop88_mpp`, - :py:func:`bishop88_i_from_v` and :py:func:`bishop88_v_from_i`. Among others, + :py:func:`scipy:scipy.optimize.newton` solvers in + :py:func:`~pvlib.singlediode.bishop88_mpp`, + :py:func:`~pvlib.singlediode.bishop88_i_from_v` and + :py:func:`~pvlib.singlediode.bishop88_v_from_i`. Among others, tolerance and number of iterations can be set. (:issue:`1249`, :pull:`1764`) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index 283c7f639e..a4a760f1b9 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -639,10 +639,8 @@ def _prepare_newton_inputs(i_or_v_tup, args, v0, method_kwargs): v0 = np.broadcast_to(v0, shape).copy() # set abs tolerance and maxiter from method_kwargs if not provided - method_kwargs['tol'] = \ - method_kwargs.pop('tol', NEWTON_DEFAULT_PARAMS['tol']) - method_kwargs['maxiter'] = \ - method_kwargs.pop('maxiter', NEWTON_DEFAULT_PARAMS['maxiter']) + # apply defaults, but giving priority to user-specified values + method_kwargs = {**NEWTON_DEFAULT_PARAMS, **method_kwargs} return args, v0, method_kwargs