From b7025c097ba5c4a4cb5ac54cad2e692c63c54aee Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 1 May 2024 15:05:42 -0600 Subject: [PATCH 1/5] modify bishop88_mpp --- pvlib/singlediode.py | 22 ++++++++++++++------ pvlib/tests/test_singlediode.py | 36 +++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index fcaa61c240..ecf5bd8993 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -304,7 +304,10 @@ def fv(x, v, *a): if method == 'brentq': # first bound the search using voc voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) - + # start iteration slightly less than NsVbi when voc_est > NsVbi, to + # avoid the asymptote at NsVbi + xp = np.where(voc_est < NsVbi, voc_est, 0.9999*NsVbi) + # 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 @@ -317,7 +320,7 @@ def vd_from_brent(voc, v, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, **method_kwargs) vd_from_brent_vectorized = np.vectorize(vd_from_brent) - vd = vd_from_brent_vectorized(voc_est, voltage, *args) + vd = vd_from_brent_vectorized(xp, voltage, *args) elif method == 'newton': x0, (voltage, *args), method_kwargs = \ _prepare_newton_inputs(voltage, (voltage, *args), method_kwargs) @@ -431,6 +434,9 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, # first bound the search using voc voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) + # start iteration slightly less than NsVbi when voc_est > NsVbi, to avoid + # the asymptote at NsVbi + xp = np.where(voc_est < NsVbi, voc_est, 0.9999*NsVbi) def fi(x, i, *a): # calculate current residual given diode voltage "x" @@ -449,10 +455,10 @@ def vd_from_brent(voc, i, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, **method_kwargs) vd_from_brent_vectorized = np.vectorize(vd_from_brent) - vd = vd_from_brent_vectorized(voc_est, current, *args) + vd = vd_from_brent_vectorized(xp, current, *args) elif method == 'newton': x0, (current, *args), method_kwargs = \ - _prepare_newton_inputs(voc_est, (current, *args), method_kwargs) + _prepare_newton_inputs(xp, (current, *args), method_kwargs) 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) @@ -561,6 +567,9 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series, # first bound the search using voc voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) + # start iteration slightly less than NsVbi when voc_est > NsVbi, to avoid + # the asymptote at NsVbi + xp = np.where(voc_est < NsVbi, voc_est, 0.9999*NsVbi) def fmpp(x, *a): return bishop88(x, *a, gradients=True)[6] @@ -574,12 +583,13 @@ def fmpp(x, *a): vbr_a, vbr, vbr_exp), **method_kwargs) ) - vd = vec_fun(voc_est, *args) + vd = vec_fun(xp, *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 + x0, args, method_kwargs = \ - _prepare_newton_inputs(voc_est, args, method_kwargs) + _prepare_newton_inputs(xp, args, method_kwargs) vd = newton(func=fmpp, x0=x0, fprime=lambda x, *a: bishop88(x, *a, gradients=True)[7], args=args, **method_kwargs) diff --git a/pvlib/tests/test_singlediode.py b/pvlib/tests/test_singlediode.py index 9089820db0..5ea678f23c 100644 --- a/pvlib/tests/test_singlediode.py +++ b/pvlib/tests/test_singlediode.py @@ -575,3 +575,39 @@ def test_bishop88_pdSeries_len_one(method, bishop88_arguments): bishop88_i_from_v(pd.Series([0]), **bishop88_arguments, method=method) bishop88_v_from_i(pd.Series([0]), **bishop88_arguments, method=method) bishop88_mpp(**bishop88_arguments, method=method) + + +def _sde_check_solution(i, v, il, io, rs, rsh, a, d2mutau=0., NsVbi=np.inf): + vd = v + rs * i + return il - io*np.expm1(vd/a) - vd/rsh - il*d2mutau/(NsVbi - vd) - i + + +@pytest.mark.parametrize('method', ['newton', 'brentq']) +def test_bishop88_init_cond(method): + p = {'alpha_sc': 0.0012256, + 'gamma_ref': 1.2916241612804187, + 'mu_gamma': 0.00047308959960937403, + 'I_L_ref': 3.068717040806731, + 'I_o_ref': 2.2691248021217617e-11, + 'R_sh_ref': 7000, + 'R_sh_0': 7000, + 'R_s': 4.602, + 'cells_in_series': 268, + 'R_sh_exp': 5.5, + 'EgRef': 1.5} + NsVbi = 268 * 0.9 + d2mutau = 1.4 + irrad = np.arange(20, 1100, 20) + tc = np.arange(-25, 74, 1) + weather = np.array(np.meshgrid(irrad, tc)).T.reshape(-1, 2) + # with the above parameters and weather conditions, a few combinations + # result in voc_est > NsVbi, which causes failure of brentq and newton + # when the recombination parameters NsVbi and d2mutau are used. + sde_params = pvsystem.calcparams_pvsyst(weather[:, 0], weather[:, 1], **p) + result = bishop88_mpp(*sde_params, d2mutau=d2mutau, NsVbi=NsVbi) + imp, vmp, pmp = result + err = np.abs(_sde_check_solution( + imp, vmp, sde_params[0], sde_params[1], sde_params[2], sde_params[3], + sde_params[4], d2mutau=d2mutau, NsVbi=NsVbi)) + bad_results = np.isnan(pmp) | (pmp < 0) | (err > 0.00001) # 0.01mA error + assert not bad_results.any() From eee8a392b06d1a498f12fa0bb3ad38eca5699bc1 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 1 May 2024 15:22:29 -0600 Subject: [PATCH 2/5] lint --- pvlib/singlediode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/singlediode.py b/pvlib/singlediode.py index ecf5bd8993..f20c4206e8 100644 --- a/pvlib/singlediode.py +++ b/pvlib/singlediode.py @@ -307,7 +307,7 @@ def fv(x, v, *a): # start iteration slightly less than NsVbi when voc_est > NsVbi, to # avoid the asymptote at NsVbi xp = np.where(voc_est < NsVbi, voc_est, 0.9999*NsVbi) - + # 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 From 7d2b93340c9fdd7bd7ad165ae20b35aab91d32e2 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 2 May 2024 07:53:59 -0600 Subject: [PATCH 3/5] extend coverage to v_from_i, i_from_v --- 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 5ea678f23c..2b4345e465 100644 --- a/pvlib/tests/test_singlediode.py +++ b/pvlib/tests/test_singlediode.py @@ -584,6 +584,7 @@ def _sde_check_solution(i, v, il, io, rs, rsh, a, d2mutau=0., NsVbi=np.inf): @pytest.mark.parametrize('method', ['newton', 'brentq']) def test_bishop88_init_cond(method): + # GH 2013 p = {'alpha_sc': 0.0012256, 'gamma_ref': 1.2916241612804187, 'mu_gamma': 0.00047308959960937403, @@ -604,6 +605,7 @@ def test_bishop88_init_cond(method): # result in voc_est > NsVbi, which causes failure of brentq and newton # when the recombination parameters NsVbi and d2mutau are used. sde_params = pvsystem.calcparams_pvsyst(weather[:, 0], weather[:, 1], **p) + # test _mpp result = bishop88_mpp(*sde_params, d2mutau=d2mutau, NsVbi=NsVbi) imp, vmp, pmp = result err = np.abs(_sde_check_solution( @@ -611,3 +613,15 @@ def test_bishop88_init_cond(method): sde_params[4], d2mutau=d2mutau, NsVbi=NsVbi)) bad_results = np.isnan(pmp) | (pmp < 0) | (err > 0.00001) # 0.01mA error assert not bad_results.any() + # test v_from_i + vmp2 = bishop88_v_from_i(imp, *sde_params, d2mutau=d2mutau, NsVbi=NsVbi) + err = np.abs(_sde_check_solution(imp, vmp2, *sde_params, d2mutau=d2mutau, + NsVbi=NsVbi)) + bad_results = np.isnan(vmp2) | (vmp2 < 0) | (err > 0.00001) + assert not bad_results.any() + # test v_from_i + imp2 = bishop88_i_from_v(vmp, *sde_params, d2mutau=d2mutau, NsVbi=NsVbi) + err = np.abs(_sde_check_solution(imp2, vmp, *sde_params, d2mutau=d2mutau, + NsVbi=NsVbi)) + bad_results = np.isnan(imp2) | (imp2 < 0) | (err > 0.00001) + assert not bad_results.any() From 682635706d96461d1e64d02c8fa692ed46fef00d Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 2 May 2024 08:00:06 -0600 Subject: [PATCH 4/5] lint --- 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 2b4345e465..3cf98dd2d5 100644 --- a/pvlib/tests/test_singlediode.py +++ b/pvlib/tests/test_singlediode.py @@ -619,7 +619,7 @@ def test_bishop88_init_cond(method): NsVbi=NsVbi)) bad_results = np.isnan(vmp2) | (vmp2 < 0) | (err > 0.00001) assert not bad_results.any() - # test v_from_i + # test v_from_i imp2 = bishop88_i_from_v(vmp, *sde_params, d2mutau=d2mutau, NsVbi=NsVbi) err = np.abs(_sde_check_solution(imp2, vmp, *sde_params, d2mutau=d2mutau, NsVbi=NsVbi)) From 51027581d5201b14a7c6a93aa073f3f7b1cbb081 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 2 May 2024 16:02:57 -0600 Subject: [PATCH 5/5] whatsnew --- docs/sphinx/source/whatsnew/v0.10.5.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.10.5.rst b/docs/sphinx/source/whatsnew/v0.10.5.rst index 9bbb8c9745..9cdfc8bbd4 100644 --- a/docs/sphinx/source/whatsnew/v0.10.5.rst +++ b/docs/sphinx/source/whatsnew/v0.10.5.rst @@ -15,7 +15,10 @@ Enhancements Bug fixes ~~~~~~~~~ - +* Improved reliability of :py:func:`pvlib.singlediode.bishop88_mpp`, + :py:func:`pvlib.singlediode.bishop88_i_from_v` and + :py:func:`pvlib.singlediode.bishop88_v_from_i` by improving the initial + guess for the newton and brentq algorithms. (:issue:`2013`, :pull:`2032`) Testing ~~~~~~~