Skip to content

Commit 9a6f76e

Browse files
cwhansewholmgren
andauthored
add tolerance to tools._golden_sect_Dataframe (#1089)
* add atol, add tests * add parametrize * stickler * re-add atol * remake pvsystem.singlediode tests with more precision * remove unneeded try, improve coverage * delete debugging stuff * screen out Io=nan in parameter estimation * remove debugging code * remove iteration exception, not needed * whatsnew * edits from review * Update docs/sphinx/source/whatsnew/v0.9.1.rst Co-authored-by: Will Holmgren <[email protected]> Co-authored-by: Will Holmgren <[email protected]>
1 parent d6eece8 commit 9a6f76e

File tree

5 files changed

+127
-74
lines changed

5 files changed

+127
-74
lines changed

docs/sphinx/source/whatsnew/v0.9.1.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ Bug fixes
2626
and ``surface_azimuth`` inputs caused an error (:issue:`1127`, :issue:`1332`, :pull:`1361`)
2727
* Changed the metadata entry for the wind speed unit to "Wind Speed Units" in
2828
the PSM3 iotools function (:pull:`1375`)
29+
* Improved convergence when determining the maximum power point using
30+
for :py:func:`pvlib.pvsystem.singlediode` with ``method='lambertw'``. Tolerance
31+
is determined for the voltage at the maximum power point, and is improved
32+
from 0.01 V to 1e-8 V. (:issue:`1087`, :pull:`1089`)
2933

3034
Testing
3135
~~~~~~~

pvlib/ivtools/sdm.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -979,7 +979,8 @@ def _filter_params(ee, isc, io, rs, rsh):
979979
negrs = rs < 0.
980980
badrs = np.logical_or(rs > rsh, np.isnan(rs))
981981
imagrs = ~(np.isreal(rs))
982-
badio = np.logical_or(~(np.isreal(rs)), io <= 0)
982+
badio = np.logical_or(np.logical_or(~(np.isreal(rs)), io <= 0),
983+
np.isnan(io))
983984
goodr = np.logical_and(~badrsh, ~imagrs)
984985
goodr = np.logical_and(goodr, ~negrs)
985986
goodr = np.logical_and(goodr, ~badrs)

pvlib/tests/test_pvsystem.py

Lines changed: 48 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,56 +1313,62 @@ def test_singlediode_array():
13131313
resistance_series, resistance_shunt, nNsVth,
13141314
method='lambertw')
13151315

1316-
expected = np.array([
1317-
0. , 0.54538398, 1.43273966, 2.36328163, 3.29255606,
1318-
4.23101358, 5.16177031, 6.09368251, 7.02197553, 7.96846051,
1319-
8.88220557])
1320-
1321-
assert_allclose(sd['i_mp'], expected, atol=0.01)
1316+
expected_i = np.array([
1317+
0., 0.54614798740338, 1.435026463529, 2.3621366610078, 3.2953968319952,
1318+
4.2303869378787, 5.1655276691892, 6.1000269648604, 7.0333996177802,
1319+
7.9653036915959, 8.8954716265647])
1320+
expected_v = np.array([
1321+
0., 7.0966259059555, 7.9961986643428, 8.2222496810656, 8.3255927555753,
1322+
8.3766915453915, 8.3988872440242, 8.4027948807891, 8.3941399580559,
1323+
8.3763655188855, 8.3517057522791])
1324+
1325+
assert_allclose(sd['i_mp'], expected_i, atol=1e-8)
1326+
assert_allclose(sd['v_mp'], expected_v, atol=1e-8)
13221327

13231328
sd = pvsystem.singlediode(photocurrent, saturation_current,
13241329
resistance_series, resistance_shunt, nNsVth)
1325-
13261330
expected = pvsystem.i_from_v(resistance_shunt, resistance_series, nNsVth,
13271331
sd['v_mp'], saturation_current, photocurrent,
13281332
method='lambertw')
1329-
1330-
assert_allclose(sd['i_mp'], expected, atol=0.01)
1333+
assert_allclose(sd['i_mp'], expected, atol=1e-8)
13311334

13321335

13331336
def test_singlediode_floats():
1334-
out = pvsystem.singlediode(7, 6e-7, .1, 20, .5, method='lambertw')
1335-
expected = {'i_xx': 4.2498,
1336-
'i_mp': 6.1275,
1337-
'v_oc': 8.1063,
1338-
'p_mp': 38.1937,
1339-
'i_x': 6.7558,
1340-
'i_sc': 6.9651,
1341-
'v_mp': 6.2331,
1337+
out = pvsystem.singlediode(7., 6.e-7, .1, 20., .5, method='lambertw')
1338+
expected = {'i_xx': 4.264060478,
1339+
'i_mp': 6.136267360,
1340+
'v_oc': 8.106300147,
1341+
'p_mp': 38.19421055,
1342+
'i_x': 6.7558815684,
1343+
'i_sc': 6.965172322,
1344+
'v_mp': 6.224339375,
13421345
'i': None,
13431346
'v': None}
13441347
assert isinstance(out, dict)
13451348
for k, v in out.items():
13461349
if k in ['i', 'v']:
13471350
assert v is None
13481351
else:
1349-
assert_allclose(v, expected[k], atol=1e-3)
1352+
assert_allclose(v, expected[k], atol=1e-6)
13501353

13511354

13521355
def test_singlediode_floats_ivcurve():
1353-
out = pvsystem.singlediode(7, 6e-7, .1, 20, .5, ivcurve_pnts=3, method='lambertw')
1354-
expected = {'i_xx': 4.2498,
1355-
'i_mp': 6.1275,
1356-
'v_oc': 8.1063,
1357-
'p_mp': 38.1937,
1358-
'i_x': 6.7558,
1359-
'i_sc': 6.9651,
1360-
'v_mp': 6.2331,
1361-
'i': np.array([6.965172e+00, 6.755882e+00, 2.575717e-14]),
1362-
'v': np.array([0., 4.05315, 8.1063])}
1356+
out = pvsystem.singlediode(7., 6e-7, .1, 20., .5, ivcurve_pnts=3,
1357+
method='lambertw')
1358+
expected = {'i_xx': 4.264060478,
1359+
'i_mp': 6.136267360,
1360+
'v_oc': 8.106300147,
1361+
'p_mp': 38.19421055,
1362+
'i_x': 6.7558815684,
1363+
'i_sc': 6.965172322,
1364+
'v_mp': 6.224339375,
1365+
'i': np.array([
1366+
6.965172322, 6.755881568, 2.664535259e-14]),
1367+
'v': np.array([
1368+
0., 4.053150073, 8.106300147])}
13631369
assert isinstance(out, dict)
13641370
for k, v in out.items():
1365-
assert_allclose(v, expected[k], atol=1e-3)
1371+
assert_allclose(v, expected[k], atol=1e-6)
13661372

13671373

13681374
def test_singlediode_series_ivcurve(cec_module_params):
@@ -1383,21 +1389,20 @@ def test_singlediode_series_ivcurve(cec_module_params):
13831389
out = pvsystem.singlediode(IL, I0, Rs, Rsh, nNsVth, ivcurve_pnts=3,
13841390
method='lambertw')
13851391

1386-
expected = OrderedDict([('i_sc', array([0., 3.01054475, 6.00675648])),
1387-
('v_oc', array([0., 9.96886962, 10.29530483])),
1388-
('i_mp', array([0., 2.65191983, 5.28594672])),
1389-
('v_mp', array([0., 8.33392491, 8.4159707])),
1390-
('p_mp', array([0., 22.10090078, 44.48637274])),
1391-
('i_x', array([0., 2.88414114, 5.74622046])),
1392-
('i_xx', array([0., 2.04340914, 3.90007956])),
1392+
expected = OrderedDict([('i_sc', array([0., 3.01079860, 6.00726296])),
1393+
('v_oc', array([0., 9.96959733, 10.29603253])),
1394+
('i_mp', array([0., 2.656285960, 5.290525645])),
1395+
('v_mp', array([0., 8.321092255, 8.409413795])),
1396+
('p_mp', array([0., 22.10320053, 44.49021934])),
1397+
('i_x', array([0., 2.884132006, 5.746202281])),
1398+
('i_xx', array([0., 2.052691562, 3.909673879])),
13931399
('v', array([[0., 0., 0.],
1394-
[0., 4.98443481, 9.96886962],
1395-
[0., 5.14765242, 10.29530483]])),
1400+
[0., 4.984798663, 9.969597327],
1401+
[0., 5.148016266, 10.29603253]])),
13961402
('i', array([[0., 0., 0.],
1397-
[3.01079860e+00, 2.88414114e+00,
1398-
3.10862447e-14],
1399-
[6.00726296e+00, 5.74622046e+00,
1400-
0.00000000e+00]]))])
1403+
[3.0107985972, 2.8841320056, 0.],
1404+
[6.0072629615, 5.7462022810, 0.]]))])
1405+
14011406

14021407
for k, v in out.items():
14031408
assert_allclose(v, expected[k], atol=1e-2)
@@ -1414,7 +1419,7 @@ def test_singlediode_series_ivcurve(cec_module_params):
14141419
method='lambertw').T
14151420

14161421
for k, v in out.items():
1417-
assert_allclose(v, expected[k], atol=1e-2)
1422+
assert_allclose(v, expected[k], atol=1e-6)
14181423

14191424

14201425
def test_scale_voltage_current_power():

pvlib/tests/test_tools.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22

33
from pvlib import tools
4+
import numpy as np
45

56

67
@pytest.mark.parametrize('keys, input_dict, expected', [
@@ -12,3 +13,35 @@
1213
def test_build_kwargs(keys, input_dict, expected):
1314
kwargs = tools._build_kwargs(keys, input_dict)
1415
assert kwargs == expected
16+
17+
18+
def _obj_test_golden_sect(params, loc):
19+
return params[loc] * (1. - params['c'] * params[loc]**params['n'])
20+
21+
22+
@pytest.mark.parametrize('params, lb, ub, expected, func', [
23+
({'c': 1., 'n': 1.}, 0., 1., 0.5, _obj_test_golden_sect),
24+
({'c': 1e6, 'n': 6.}, 0., 1., 0.07230200263994839, _obj_test_golden_sect),
25+
({'c': 0.2, 'n': 0.3}, 0., 100., 89.14332727531685, _obj_test_golden_sect)
26+
])
27+
def test__golden_sect_DataFrame(params, lb, ub, expected, func):
28+
v, x = tools._golden_sect_DataFrame(params, lb, ub, func)
29+
assert np.isclose(x, expected, atol=1e-8)
30+
31+
32+
def test__golden_sect_DataFrame_atol():
33+
params = {'c': 0.2, 'n': 0.3}
34+
expected = 89.14332727531685
35+
v, x = tools._golden_sect_DataFrame(
36+
params, 0., 100., _obj_test_golden_sect, atol=1e-12)
37+
assert np.isclose(x, expected, atol=1e-12)
38+
39+
40+
def test__golden_sect_DataFrame_vector():
41+
params = {'c': np.array([1., 2.]), 'n': np.array([1., 1.])}
42+
lower = np.array([0., 0.001])
43+
upper = np.array([1.1, 1.2])
44+
expected = np.array([0.5, 0.25])
45+
v, x = tools._golden_sect_DataFrame(params, lower, upper,
46+
_obj_test_golden_sect)
47+
assert np.allclose(x, expected, atol=1e-8)

pvlib/tools.py

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -277,51 +277,61 @@ def _build_args(keys, input_dict, dict_name):
277277

278278
# Created April,2014
279279
# Author: Rob Andrews, Calama Consulting
280-
281-
def _golden_sect_DataFrame(params, VL, VH, func):
280+
# Modified: November, 2020 by C. W. Hansen, to add atol and change exit
281+
# criteria
282+
def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8):
282283
"""
283-
Vectorized golden section search for finding MPP from a dataframe
284-
timeseries.
284+
Vectorized golden section search for finding maximum of a function of a
285+
single variable.
285286
286287
Parameters
287288
----------
288-
params : dict
289-
Dictionary containing scalars or arrays
290-
of inputs to the function to be optimized.
291-
Each row should represent an independent optimization.
289+
params : dict or Dataframe
290+
Parameters to be passed to `func`.
292291
293-
VL: float
294-
Lower bound of the optimization
292+
lower: numeric
293+
Lower bound for the optimization
295294
296-
VH: float
297-
Upper bound of the optimization
295+
upper: numeric
296+
Upper bound for the optimization
298297
299298
func: function
300-
Function to be optimized must be in the form f(array-like, x)
299+
Function to be optimized. Must be in the form
300+
result = f(dict or DataFrame, str), where result is a dict or DataFrame
301+
that also contains the function output, and str is the key
302+
corresponding to the function's input variable.
301303
302304
Returns
303305
-------
304-
func(df,'V1') : DataFrame
305-
function evaluated at the optimal point
306+
numeric
307+
function evaluated at the optimal points
306308
307-
df['V1']: Dataframe
308-
Dataframe of optimal points
309+
numeric
310+
optimal points
309311
310312
Notes
311313
-----
312-
This function will find the MAXIMUM of a function
314+
This function will find the points where the function is maximized.
315+
316+
See also
317+
--------
318+
pvlib.singlediode._pwr_optfcn
313319
"""
314320

321+
phim1 = (np.sqrt(5) - 1) / 2
322+
315323
df = params
316-
df['VH'] = VH
317-
df['VL'] = VL
324+
df['VH'] = upper
325+
df['VL'] = lower
318326

319-
errflag = True
327+
converged = False
320328
iterations = 0
329+
iterlimit = 1 + np.max(
330+
np.trunc(np.log(atol / (df['VH'] - df['VL'])) / np.log(phim1)))
321331

322-
while errflag:
332+
while not converged and (iterations < iterlimit):
323333

324-
phi = (np.sqrt(5)-1)/2*(df['VH']-df['VL'])
334+
phi = phim1 * (df['VH'] - df['VL'])
325335
df['V1'] = df['VL'] + phi
326336
df['V2'] = df['VH'] - phi
327337

@@ -332,16 +342,16 @@ def _golden_sect_DataFrame(params, VL, VH, func):
332342
df['VL'] = df['V2']*df['SW_Flag'] + df['VL']*(~df['SW_Flag'])
333343
df['VH'] = df['V1']*~df['SW_Flag'] + df['VH']*(df['SW_Flag'])
334344

335-
err = df['V1'] - df['V2']
336-
try:
337-
errflag = (abs(err) > .01).any()
338-
except ValueError:
339-
errflag = (abs(err) > .01)
345+
err = abs(df['V2'] - df['V1'])
340346

347+
# works with single value because err is np.float64
348+
converged = (err < atol).all()
349+
# err will be less than atol before iterations hit the limit
350+
# but just to be safe
341351
iterations += 1
342352

343-
if iterations > 50:
344-
raise Exception("EXCEPTION:iterations exceeded maximum (50)")
353+
if iterations > iterlimit:
354+
raise Exception("iterations exceeded maximum") # pragma: no cover
345355

346356
return func(df, 'V1'), df['V1']
347357

0 commit comments

Comments
 (0)