From f66365ccc514e4aa0c2292bc5dcdeaf6d213fba6 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Mon, 16 Nov 2020 16:17:15 -0700 Subject: [PATCH 01/13] add atol, add tests --- pvlib/tests/test_tools.py | 25 +++++++++++++++++++++ pvlib/tools.py | 46 ++++++++++++++++++++++++--------------- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py index 42eaedca58..1735b1bc9d 100644 --- a/pvlib/tests/test_tools.py +++ b/pvlib/tests/test_tools.py @@ -1,6 +1,7 @@ import pytest from pvlib import tools +import numpy as np @pytest.mark.parametrize('keys, input_dict, expected', [ @@ -12,3 +13,27 @@ def test_build_kwargs(keys, input_dict, expected): kwargs = tools._build_kwargs(keys, input_dict) assert kwargs == expected + + +def _obj_test_golden_sect(params, loc): + return params[loc] * (1. - params['c'] * params[loc]**params['n']) + + +def test__golden_sect_DataFrame(params, lb, ub, expected, func): + v, x = tools._golden_sect_DataFrame(params, lb, ub, func) + assert np.isclose(x, expected, atol=1e-8) + + +def test__golden_sect_DataFrame_atol(): + params = {'c': 0.2, 'n': 0.3} + expected = 89.14332727531685 + v, x = tools._golden_sect_DataFrame(params, 0., 100., _obj_test_golden_sect, + atol=1e-12) + assert np.isclose(x, expected, atol=1e-12) + + +def test__golden_sect_DataFrame_vector(): + params = {'c': np.array([1., 2.]), 'n': np.array([1., 1.])} + expected = np.array([0.5, 0.25]) + v, x = tools._golden_sect_DataFrame(params, 0., 1., _obj_test_golden_sect) + assert np.allclose(x, expected, atol=1e-8) diff --git a/pvlib/tools.py b/pvlib/tools.py index b6ee3e7c3a..02b347884d 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -249,36 +249,38 @@ def _build_kwargs(keys, input_dict): return kwargs -# Created April,2014 -# Author: Rob Andrews, Calama Consulting - -def _golden_sect_DataFrame(params, VL, VH, func): +# Created April,2014 by Rob Andrews, Calama Consulting +# Modified: November, 2020 by C. W. Hansen, to add atol and change exit +# criteria +def _golden_sect_DataFrame(params, VL, VH, func, atol=1e-8): """ Vectorized golden section search for finding MPP from a dataframe timeseries. Parameters ---------- - params : dict - Dictionary containing scalars or arrays + params : dict or Dataframe + Parameters for the IV curve(s) where MPP will be found. Must contain + keys: 'r_sh', 'r_s', 'nNsVth', 'i_0', 'i_l' of inputs to the function to be optimized. Each row should represent an independent optimization. VL: float - Lower bound of the optimization + Lower bound on voltage for the optimization - VH: float - Upper bound of the optimization + VH: array-like + Upper bound on voltage for the optimization func: function - Function to be optimized must be in the form f(array-like, x) + Function to be optimized must be in the form f(dict or Dataframe, str) + where str is the key corresponding to voltage Returns ------- - func(df,'V1') : DataFrame + func(params, 'V1') : dict or DataFrame function evaluated at the optimal point - df['V1']: Dataframe + df['V1']: array-like or Series Dataframe of optimal points Notes @@ -286,16 +288,19 @@ def _golden_sect_DataFrame(params, VL, VH, func): This function will find the MAXIMUM of a function """ + phim1 = (np.sqrt(5) - 1) / 2 + df = params df['VH'] = VH df['VL'] = VL errflag = True iterations = 0 + iterlimit = np.max(np.trunc(np.log(atol / (VH - VL)) / np.log(phim1))) + 1 while errflag: - phi = (np.sqrt(5)-1)/2*(df['VH']-df['VL']) + phi = phim1 * (df['VH'] - df['VL']) df['V1'] = df['VL'] + phi df['V2'] = df['VH'] - phi @@ -306,15 +311,20 @@ def _golden_sect_DataFrame(params, VL, VH, func): df['VL'] = df['V2']*df['SW_Flag'] + df['VL']*(~df['SW_Flag']) df['VH'] = df['V1']*~df['SW_Flag'] + df['VH']*(df['SW_Flag']) - err = df['V1'] - df['V2'] + err = abs(df['V2'] - df['V1']) try: - errflag = (abs(err) > .01).any() + errflag = (err > atol).any() except ValueError: - errflag = (abs(err) > .01) + errflag = err > atol iterations += 1 - if iterations > 50: - raise Exception("EXCEPTION:iterations exceeded maximum (50)") + try: + iterflag = (iterations > iterlimit).any() + except ValueError: + iterflag = iterations > iterlimit + + if iterflag and errflag: + raise Exception("EXCEPTION: iteration limit exceeded") return func(df, 'V1'), df['V1'] From eb88eb80ab7eba870f308afb25ba7397ec80cd53 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Tue, 17 Nov 2020 08:25:11 -0700 Subject: [PATCH 02/13] add parametrize --- pvlib/tests/test_tools.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py index 1735b1bc9d..1013c89f12 100644 --- a/pvlib/tests/test_tools.py +++ b/pvlib/tests/test_tools.py @@ -18,17 +18,22 @@ def test_build_kwargs(keys, input_dict, expected): def _obj_test_golden_sect(params, loc): return params[loc] * (1. - params['c'] * params[loc]**params['n']) - + +@pytest.mark.parametrize('params, lb, ub, expected, func', [ + ({'c': 1., 'n': 1.}, 0., 1., 0.5, _obj_test_golden_sect), + ({'c': 1e6, 'n': 6.}, 0., 1., 0.07230200263994839, _obj_test_golden_sect), + ({'c': 0.2, 'n': 0.3}, 0., 100., 89.14332727531685, _obj_test_golden_sect) +]) def test__golden_sect_DataFrame(params, lb, ub, expected, func): - v, x = tools._golden_sect_DataFrame(params, lb, ub, func) + v, x = tools._golden_sect_DataFrame(params, lb, ub, func) assert np.isclose(x, expected, atol=1e-8) def test__golden_sect_DataFrame_atol(): params = {'c': 0.2, 'n': 0.3} expected = 89.14332727531685 - v, x = tools._golden_sect_DataFrame(params, 0., 100., _obj_test_golden_sect, - atol=1e-12) + v, x = tools._golden_sect_DataFrame( + params, 0., 100., _obj_test_golden_sect, atol=1e-12) assert np.isclose(x, expected, atol=1e-12) From 6b123abc8dad10ecf2da818aaa051db259e7814b Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Tue, 17 Nov 2020 09:02:13 -0700 Subject: [PATCH 03/13] stickler --- pvlib/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/tools.py b/pvlib/tools.py index 02b347884d..2b7342d5fc 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -273,7 +273,7 @@ def _golden_sect_DataFrame(params, VL, VH, func, atol=1e-8): func: function Function to be optimized must be in the form f(dict or Dataframe, str) - where str is the key corresponding to voltage + where str is the key corresponding to voltage Returns ------- From 6667d1726c68cc47091a3928d916c6ac9a902632 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Tue, 18 Jan 2022 18:59:39 -0700 Subject: [PATCH 04/13] re-add atol --- pvlib/tools.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pvlib/tools.py b/pvlib/tools.py index 66ec8e9369..9b5f07a31b 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -277,8 +277,9 @@ def _build_args(keys, input_dict, dict_name): # Created April,2014 # Author: Rob Andrews, Calama Consulting - -def _golden_sect_DataFrame(params, VL, VH, func): +# Modified: November, 2020 by C. W. Hansen, to add atol and change exit +# criteria +def _golden_sect_DataFrame(params, VL, VH, func, atol=1e-8): """ Vectorized golden section search for finding MPP from a dataframe timeseries. From 79502f1373946c2021d761fc5daba82daa2b45aa Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 19 Jan 2022 13:47:48 -0700 Subject: [PATCH 05/13] remake pvsystem.singlediode tests with more precision --- pvlib/tests/test_pvsystem.py | 91 +++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 43 deletions(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 505016a7ce..1141e490e9 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1313,32 +1313,35 @@ def test_singlediode_array(): resistance_series, resistance_shunt, nNsVth, method='lambertw') - expected = np.array([ - 0. , 0.54538398, 1.43273966, 2.36328163, 3.29255606, - 4.23101358, 5.16177031, 6.09368251, 7.02197553, 7.96846051, - 8.88220557]) - - assert_allclose(sd['i_mp'], expected, atol=0.01) + expected_i = np.array([ + 0., 0.54614798740338, 1.435026463529, 2.3621366610078, 3.2953968319952, + 4.2303869378787, 5.1655276691892, 6.1000269648604, 7.0333996177802, + 7.9653036915959, 8.8954716265647]) + expected_v = np.array([ + 0., 7.0966259059555, 7.9961986643428, 8.2222496810656, 8.3255927555753, + 8.3766915453915, 8.3988872440242, 8.4027948807891, 8.3941399580559, + 8.3763655188855, 8.3517057522791]) + + assert_allclose(sd['i_mp'], expected_i, atol=1e-8) + assert_allclose(sd['v_mp'], expected_v, atol=1e-8) sd = pvsystem.singlediode(photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth) - expected = pvsystem.i_from_v(resistance_shunt, resistance_series, nNsVth, sd['v_mp'], saturation_current, photocurrent, method='lambertw') - - assert_allclose(sd['i_mp'], expected, atol=0.01) + assert_allclose(sd['i_mp'], expected, atol=1e-8) def test_singlediode_floats(): - out = pvsystem.singlediode(7, 6e-7, .1, 20, .5, method='lambertw') - expected = {'i_xx': 4.2498, - 'i_mp': 6.1275, - 'v_oc': 8.1063, - 'p_mp': 38.1937, - 'i_x': 6.7558, - 'i_sc': 6.9651, - 'v_mp': 6.2331, + out = pvsystem.singlediode(7., 6.e-7, .1, 20., .5, method='lambertw') + expected = {'i_xx': 4.264060478, + 'i_mp': 6.136267360, + 'v_oc': 8.106300147, + 'p_mp': 38.19421055, + 'i_x': 6.7558815684, + 'i_sc': 6.965172322, + 'v_mp': 6.224339375, 'i': None, 'v': None} assert isinstance(out, dict) @@ -1346,23 +1349,26 @@ def test_singlediode_floats(): if k in ['i', 'v']: assert v is None else: - assert_allclose(v, expected[k], atol=1e-3) + assert_allclose(v, expected[k], atol=1e-6) def test_singlediode_floats_ivcurve(): - out = pvsystem.singlediode(7, 6e-7, .1, 20, .5, ivcurve_pnts=3, method='lambertw') - expected = {'i_xx': 4.2498, - 'i_mp': 6.1275, - 'v_oc': 8.1063, - 'p_mp': 38.1937, - 'i_x': 6.7558, - 'i_sc': 6.9651, - 'v_mp': 6.2331, - 'i': np.array([6.965172e+00, 6.755882e+00, 2.575717e-14]), - 'v': np.array([0., 4.05315, 8.1063])} + out = pvsystem.singlediode(7., 6e-7, .1, 20., .5, ivcurve_pnts=3, + method='lambertw') + expected = {'i_xx': 4.264060478, + 'i_mp': 6.136267360, + 'v_oc': 8.106300147, + 'p_mp': 38.19421055, + 'i_x': 6.7558815684, + 'i_sc': 6.965172322, + 'v_mp': 6.224339375, + 'i': np.array([ + 6.965172322, 6.755881568, 2.664535259e-14]), + 'v': np.array([ + 0., 4.053150073, 8.106300147])} assert isinstance(out, dict) for k, v in out.items(): - assert_allclose(v, expected[k], atol=1e-3) + assert_allclose(v, expected[k], atol=1e-6) def test_singlediode_series_ivcurve(cec_module_params): @@ -1383,21 +1389,20 @@ def test_singlediode_series_ivcurve(cec_module_params): out = pvsystem.singlediode(IL, I0, Rs, Rsh, nNsVth, ivcurve_pnts=3, method='lambertw') - expected = OrderedDict([('i_sc', array([0., 3.01054475, 6.00675648])), - ('v_oc', array([0., 9.96886962, 10.29530483])), - ('i_mp', array([0., 2.65191983, 5.28594672])), - ('v_mp', array([0., 8.33392491, 8.4159707])), - ('p_mp', array([0., 22.10090078, 44.48637274])), - ('i_x', array([0., 2.88414114, 5.74622046])), - ('i_xx', array([0., 2.04340914, 3.90007956])), + expected = OrderedDict([('i_sc', array([0., 3.01079860, 6.00726296])), + ('v_oc', array([0., 9.96959733, 10.29603253])), + ('i_mp', array([0., 2.656285960, 5.290525645])), + ('v_mp', array([0., 8.321092255, 8.409413795])), + ('p_mp', array([0., 22.10320053, 44.49021934])), + ('i_x', array([0., 2.884132006, 5.746202281])), + ('i_xx', array([0., 2.052691562, 3.909673879])), ('v', array([[0., 0., 0.], - [0., 4.98443481, 9.96886962], - [0., 5.14765242, 10.29530483]])), + [0., 4.984798663, 9.969597327], + [0., 5.148016266, 10.29603253]])), ('i', array([[0., 0., 0.], - [3.01079860e+00, 2.88414114e+00, - 3.10862447e-14], - [6.00726296e+00, 5.74622046e+00, - 0.00000000e+00]]))]) + [3.0107985972, 2.8841320056, 0.], + [6.0072629615, 5.7462022810, 0.]]))]) + for k, v in out.items(): assert_allclose(v, expected[k], atol=1e-2) @@ -1414,7 +1419,7 @@ def test_singlediode_series_ivcurve(cec_module_params): method='lambertw').T for k, v in out.items(): - assert_allclose(v, expected[k], atol=1e-2) + assert_allclose(v, expected[k], atol=1e-6) def test_scale_voltage_current_power(): From f78b3e92cc3bfe389679bbe6025596352cb786f6 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 19 Jan 2022 14:31:26 -0700 Subject: [PATCH 06/13] remove unneeded try, improve coverage --- pvlib/tests/test_tools.py | 4 ++++ pvlib/tools.py | 16 ++++------------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py index 1013c89f12..0a899e4c78 100644 --- a/pvlib/tests/test_tools.py +++ b/pvlib/tests/test_tools.py @@ -42,3 +42,7 @@ def test__golden_sect_DataFrame_vector(): expected = np.array([0.5, 0.25]) v, x = tools._golden_sect_DataFrame(params, 0., 1., _obj_test_golden_sect) assert np.allclose(x, expected, atol=1e-8) + + +params, lb, ub, expected, func = ({'c': 1., 'n': 1.}, 0., 1., 0.5, _obj_test_golden_sect) +test__golden_sect_DataFrame(params, lb, ub, expected, func) diff --git a/pvlib/tools.py b/pvlib/tools.py index 9b5f07a31b..2200f57b82 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -325,7 +325,7 @@ def _golden_sect_DataFrame(params, VL, VH, func, atol=1e-8): iterations = 0 iterlimit = np.max(np.trunc(np.log(atol / (VH - VL)) / np.log(phim1))) + 1 - while errflag: + while errflag and (iterations < iterlimit): phi = phim1 * (df['VH'] - df['VL']) df['V1'] = df['VL'] + phi @@ -339,19 +339,11 @@ def _golden_sect_DataFrame(params, VL, VH, func, atol=1e-8): df['VH'] = df['V1']*~df['SW_Flag'] + df['VH']*(df['SW_Flag']) err = abs(df['V2'] - df['V1']) - try: - errflag = (err > atol).any() - except ValueError: - errflag = err > atol + errflag = (err > atol).any() # works because err is np.float64 iterations += 1 - try: - iterflag = (iterations > iterlimit).any() - except ValueError: - iterflag = iterations > iterlimit - - if iterflag and errflag: - raise Exception("EXCEPTION: iteration limit exceeded") + if iterations > iterlimit: + raise Exception("EXCEPTION: iteration limit exceeded") return func(df, 'V1'), df['V1'] From aa5c19c5541aa0872f78239c45d8d86246d21b92 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 19 Jan 2022 14:32:45 -0700 Subject: [PATCH 07/13] delete debugging stuff --- pvlib/tests/test_tools.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py index 0a899e4c78..1013c89f12 100644 --- a/pvlib/tests/test_tools.py +++ b/pvlib/tests/test_tools.py @@ -42,7 +42,3 @@ def test__golden_sect_DataFrame_vector(): expected = np.array([0.5, 0.25]) v, x = tools._golden_sect_DataFrame(params, 0., 1., _obj_test_golden_sect) assert np.allclose(x, expected, atol=1e-8) - - -params, lb, ub, expected, func = ({'c': 1., 'n': 1.}, 0., 1., 0.5, _obj_test_golden_sect) -test__golden_sect_DataFrame(params, lb, ub, expected, func) From 446b640d189f7f5623fc3818dc7f719e9311c517 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 19 Jan 2022 20:52:48 -0700 Subject: [PATCH 08/13] screen out Io=nan in parameter estimation --- pvlib/ivtools/sdm.py | 3 ++- pvlib/tests/ivtools/test_sdm.py | 18 +++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/pvlib/ivtools/sdm.py b/pvlib/ivtools/sdm.py index 04263c34c5..013e63f9a6 100644 --- a/pvlib/ivtools/sdm.py +++ b/pvlib/ivtools/sdm.py @@ -979,7 +979,8 @@ def _filter_params(ee, isc, io, rs, rsh): negrs = rs < 0. badrs = np.logical_or(rs > rsh, np.isnan(rs)) imagrs = ~(np.isreal(rs)) - badio = np.logical_or(~(np.isreal(rs)), io <= 0) + badio = np.logical_or(np.logical_or(~(np.isreal(rs)), io <= 0), + np.isnan(io)) goodr = np.logical_and(~badrsh, ~imagrs) goodr = np.logical_and(goodr, ~negrs) goodr = np.logical_and(goodr, ~badrs) diff --git a/pvlib/tests/ivtools/test_sdm.py b/pvlib/tests/ivtools/test_sdm.py index 13bab1ecde..e64928f80e 100644 --- a/pvlib/tests/ivtools/test_sdm.py +++ b/pvlib/tests/ivtools/test_sdm.py @@ -9,7 +9,7 @@ from pvlib.tests.conftest import requires_pysam, requires_statsmodels -from ..conftest import DATA_DIR +#from ..conftest import DATA_DIR @pytest.fixture @@ -125,8 +125,10 @@ def _read_iv_curves_for_test(datafile, npts): ivcurves = dict.fromkeys(['i_sc', 'i_mp', 'v_mp', 'v_oc', 'poa', 'tc', 'ee']) - infilen = DATA_DIR / datafile - with infilen.open(mode='r') as f: +# infilen = DATA_DIR / datafile + infilen = DATA_DIR + '/PVsyst_demo.csv' +# with infilen.open(mode='r') as f: + with open(infilen, mode='r') as f: Ns, aIsc, bVoc, descr = f.readline().split(',') @@ -189,8 +191,10 @@ def _read_pvsyst_expected(datafile): varlist = ['iph', 'io', 'rs', 'rsh', 'u'] pvsyst = dict.fromkeys(paramlist + varlist) - infilen = DATA_DIR / datafile - with infilen.open(mode='r') as f: +# infilen = DATA_DIR / datafile +# with infilen.open(mode='r') as f: + infilen = DATA_DIR + '\PVsyst_demo_model.csv' + with open(infilen, mode='r') as f: Ns, aIsc, bVoc, descr = f.readline().split(',') @@ -400,3 +404,7 @@ def test_pvsyst_temperature_coeff(): params['I_L_ref'], params['I_o_ref'], params['R_sh_ref'], params['R_sh_0'], params['R_s'], params['cells_in_series']) assert_allclose(gamma_pdc, expected, rtol=0.0005) + + +DATA_DIR = 'C:\python\pvlib-remote\pvlib-python\pvlib\data' +test_fit_pvsyst_sandia(npts=3000) From 89a7d0836f4133c031980e672b6bd39235e13266 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 19 Jan 2022 20:54:09 -0700 Subject: [PATCH 09/13] remove debugging code --- pvlib/tests/ivtools/test_sdm.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/pvlib/tests/ivtools/test_sdm.py b/pvlib/tests/ivtools/test_sdm.py index e64928f80e..13bab1ecde 100644 --- a/pvlib/tests/ivtools/test_sdm.py +++ b/pvlib/tests/ivtools/test_sdm.py @@ -9,7 +9,7 @@ from pvlib.tests.conftest import requires_pysam, requires_statsmodels -#from ..conftest import DATA_DIR +from ..conftest import DATA_DIR @pytest.fixture @@ -125,10 +125,8 @@ def _read_iv_curves_for_test(datafile, npts): ivcurves = dict.fromkeys(['i_sc', 'i_mp', 'v_mp', 'v_oc', 'poa', 'tc', 'ee']) -# infilen = DATA_DIR / datafile - infilen = DATA_DIR + '/PVsyst_demo.csv' -# with infilen.open(mode='r') as f: - with open(infilen, mode='r') as f: + infilen = DATA_DIR / datafile + with infilen.open(mode='r') as f: Ns, aIsc, bVoc, descr = f.readline().split(',') @@ -191,10 +189,8 @@ def _read_pvsyst_expected(datafile): varlist = ['iph', 'io', 'rs', 'rsh', 'u'] pvsyst = dict.fromkeys(paramlist + varlist) -# infilen = DATA_DIR / datafile -# with infilen.open(mode='r') as f: - infilen = DATA_DIR + '\PVsyst_demo_model.csv' - with open(infilen, mode='r') as f: + infilen = DATA_DIR / datafile + with infilen.open(mode='r') as f: Ns, aIsc, bVoc, descr = f.readline().split(',') @@ -404,7 +400,3 @@ def test_pvsyst_temperature_coeff(): params['I_L_ref'], params['I_o_ref'], params['R_sh_ref'], params['R_sh_0'], params['R_s'], params['cells_in_series']) assert_allclose(gamma_pdc, expected, rtol=0.0005) - - -DATA_DIR = 'C:\python\pvlib-remote\pvlib-python\pvlib\data' -test_fit_pvsyst_sandia(npts=3000) From 7234a10c85f373fd855ca3b3628d76962dae2957 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 20 Jan 2022 09:54:25 -0700 Subject: [PATCH 10/13] remove iteration exception, not needed --- pvlib/tools.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pvlib/tools.py b/pvlib/tools.py index 2200f57b82..2324c408fb 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -321,11 +321,11 @@ def _golden_sect_DataFrame(params, VL, VH, func, atol=1e-8): df['VH'] = VH df['VL'] = VL - errflag = True + converged = False iterations = 0 iterlimit = np.max(np.trunc(np.log(atol / (VH - VL)) / np.log(phim1))) + 1 - while errflag and (iterations < iterlimit): + while not converged and (iterations < iterlimit): phi = phim1 * (df['VH'] - df['VL']) df['V1'] = df['VL'] + phi @@ -339,11 +339,11 @@ def _golden_sect_DataFrame(params, VL, VH, func, atol=1e-8): df['VH'] = df['V1']*~df['SW_Flag'] + df['VH']*(df['SW_Flag']) err = abs(df['V2'] - df['V1']) - errflag = (err > atol).any() # works because err is np.float64 + # works with single value because err is np.float64 + converged = (err < atol).all() + # err will be less than atol before iterations hit the limit + # but just to be safe iterations += 1 - if iterations > iterlimit: - raise Exception("EXCEPTION: iteration limit exceeded") - return func(df, 'V1'), df['V1'] From 7755635c37755b1beaac3371059650cd8731a9e1 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 20 Jan 2022 09:56:57 -0700 Subject: [PATCH 11/13] whatsnew --- docs/sphinx/source/whatsnew/v0.9.1.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sphinx/source/whatsnew/v0.9.1.rst b/docs/sphinx/source/whatsnew/v0.9.1.rst index 4ddcb9bd51..92672d9bf8 100644 --- a/docs/sphinx/source/whatsnew/v0.9.1.rst +++ b/docs/sphinx/source/whatsnew/v0.9.1.rst @@ -24,6 +24,8 @@ Bug fixes and ``surface_azimuth`` inputs caused an error (:issue:`1127`, :issue:`1332`, :pull:`1361`) * Changed the metadata entry for the wind speed unit to "Wind Speed Units" in the PSM3 iotools function (:pull:`1375`) +* Improved convergence to maximum power point for :py:func:`pvlib.pvsystem.singlediode` + (:issue:`1087`, :pull:`1089`) Testing ~~~~~~~ From efb310788c380636b3a264cc9841f27a734c3b84 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Mon, 24 Jan 2022 10:27:55 -0700 Subject: [PATCH 12/13] edits from review --- docs/sphinx/source/whatsnew/v0.9.1.rst | 6 ++-- pvlib/tests/test_tools.py | 5 ++- pvlib/tools.py | 49 +++++++++++++++----------- 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.9.1.rst b/docs/sphinx/source/whatsnew/v0.9.1.rst index 92672d9bf8..52e909d1ef 100644 --- a/docs/sphinx/source/whatsnew/v0.9.1.rst +++ b/docs/sphinx/source/whatsnew/v0.9.1.rst @@ -24,8 +24,10 @@ Bug fixes and ``surface_azimuth`` inputs caused an error (:issue:`1127`, :issue:`1332`, :pull:`1361`) * Changed the metadata entry for the wind speed unit to "Wind Speed Units" in the PSM3 iotools function (:pull:`1375`) -* Improved convergence to maximum power point for :py:func:`pvlib.pvsystem.singlediode` - (:issue:`1087`, :pull:`1089`) +* Improved convergence when determining the maximum power point using + for :py:func:`pvlib.pvsystem.singlediode` with `method='lambertw'`. Tolerance + is determined for the voltage at the maximum power point, and is improved + from 0.01 V to 1e-8 V. (:issue:`1087`, :pull:`1089`) Testing ~~~~~~~ diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py index 1013c89f12..fa42fbf82d 100644 --- a/pvlib/tests/test_tools.py +++ b/pvlib/tests/test_tools.py @@ -39,6 +39,9 @@ def test__golden_sect_DataFrame_atol(): def test__golden_sect_DataFrame_vector(): params = {'c': np.array([1., 2.]), 'n': np.array([1., 1.])} + lower = np.array([0., 0.001]) + upper = np.array([1.1, 1.2]) expected = np.array([0.5, 0.25]) - v, x = tools._golden_sect_DataFrame(params, 0., 1., _obj_test_golden_sect) + v, x = tools._golden_sect_DataFrame(params, lower, upper, + _obj_test_golden_sect) assert np.allclose(x, expected, atol=1e-8) diff --git a/pvlib/tools.py b/pvlib/tools.py index 2324c408fb..2177c75ba5 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -279,51 +279,55 @@ def _build_args(keys, input_dict, dict_name): # Author: Rob Andrews, Calama Consulting # Modified: November, 2020 by C. W. Hansen, to add atol and change exit # criteria -def _golden_sect_DataFrame(params, VL, VH, func, atol=1e-8): +def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8): """ - Vectorized golden section search for finding MPP from a dataframe - timeseries. + Vectorized golden section search for finding maximum of a function of a + single variable. Parameters ---------- params : dict or Dataframe - Parameters for the IV curve(s) where MPP will be found. Must contain - keys: 'r_sh', 'r_s', 'nNsVth', 'i_0', 'i_l' - of inputs to the function to be optimized. - Each row should represent an independent optimization. + Parameters to be passed to `func`. - VL: float - Lower bound on voltage for the optimization + lower: numeric + Lower bound for the optimization - VH: array-like - Upper bound on voltage for the optimization + upper: numeric + Upper bound for the optimization func: function - Function to be optimized must be in the form f(dict or Dataframe, str) - where str is the key corresponding to voltage + Function to be optimized. Must be in the form + result = f(dict or DataFrame, str), where result is a dict or DataFrame + that also contains the function output, and str is the key + corresponding to the function's input variable. Returns ------- - func(params, 'V1') : dict or DataFrame - function evaluated at the optimal point + numeric + function evaluated at the optimal points - df['V1']: array-like or Series - Dataframe of optimal points + numeric + optimal points Notes ----- - This function will find the MAXIMUM of a function + This function will find the points where the function is maximized. + + See also + -------- + pvlib.singlediode._pwr_optfcn """ phim1 = (np.sqrt(5) - 1) / 2 df = params - df['VH'] = VH - df['VL'] = VL + df['VH'] = upper + df['VL'] = lower converged = False iterations = 0 - iterlimit = np.max(np.trunc(np.log(atol / (VH - VL)) / np.log(phim1))) + 1 + iterlimit = 1 + np.max( + np.trunc(np.log(atol / (df['VH'] - df['VL'])) / np.log(phim1))) while not converged and (iterations < iterlimit): @@ -346,4 +350,7 @@ def _golden_sect_DataFrame(params, VL, VH, func, atol=1e-8): # but just to be safe iterations += 1 + if iterations > iterlimit: + raise Exception("iterations exceeded maximum") # pragma: no cover + return func(df, 'V1'), df['V1'] From 8b55eed569e41c258219d1b565e8d296516d542f Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 16 Feb 2022 11:39:02 -0700 Subject: [PATCH 13/13] Update docs/sphinx/source/whatsnew/v0.9.1.rst Co-authored-by: Will Holmgren --- docs/sphinx/source/whatsnew/v0.9.1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.9.1.rst b/docs/sphinx/source/whatsnew/v0.9.1.rst index 52e909d1ef..80a47ff0b4 100644 --- a/docs/sphinx/source/whatsnew/v0.9.1.rst +++ b/docs/sphinx/source/whatsnew/v0.9.1.rst @@ -25,7 +25,7 @@ Bug fixes * Changed the metadata entry for the wind speed unit to "Wind Speed Units" in the PSM3 iotools function (:pull:`1375`) * Improved convergence when determining the maximum power point using - for :py:func:`pvlib.pvsystem.singlediode` with `method='lambertw'`. Tolerance + for :py:func:`pvlib.pvsystem.singlediode` with ``method='lambertw'``. Tolerance is determined for the voltage at the maximum power point, and is improved from 0.01 V to 1e-8 V. (:issue:`1087`, :pull:`1089`)