Skip to content

Commit e8da6a5

Browse files
authored
Revise golden mean search convergence (#1606)
* revise convergence, add test with equal upper and lower * whatsnew * clarify error message * improve from review * length 1 array test * remove try/except not needed
1 parent df8055e commit e8da6a5

File tree

3 files changed

+34
-28
lines changed

3 files changed

+34
-28
lines changed

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,21 @@ Enhancements
1212
* Multiple code style issues fixed that were reported by LGTM analysis. (:issue:`1275`, :pull:`1559`)
1313
* Added a direct IAM model :py:func:`pvlib.iam.schlick` which can be used with
1414
:py:func:`~pvlib.iam.marion_diffuse`, and a diffuse IAM model
15-
:py:func:`pvlib.iam.schlick_diffuse` (:pull:`1562`, :issue:`1564`)
15+
:py:func:`pvlib.iam.schlick_diffuse`. (:pull:`1562`, :issue:`1564`)
1616
* Added a function to calculate one of GHI, DHI, and DNI from values of the other two.
17-
:py:func:`~pvlib.irradiance.complete_irradiance`
17+
:py:func:`~pvlib.irradiance.complete_irradiance`.
1818
(:issue:`1565`, :pull:`1567`)
1919
* Add optional ``return_components`` parameter to :py:func:`pvlib.irradiance.haydavies` to return
20-
individual diffuse irradiance components (:issue:`1553`, :pull:`1568`)
20+
individual diffuse irradiance components. (:issue:`1553`, :pull:`1568`)
2121

2222

2323
Bug fixes
2424
~~~~~~~~~
2525

2626
* Fixed bug in :py:func:`pvlib.shading.masking_angle` and :py:func:`pvlib.bifacial.infinite_sheds._ground_angle`
27-
where zero ``gcr`` input caused a ZeroDivisionError (:issue:`1576`, :pull:`1589`)
27+
where zero ``gcr`` input caused a ZeroDivisionError. (:issue:`1576`, :pull:`1589`)
28+
* Fixed bug in :py:func:`pvlib.tools._golden_sect_DataFrame` so that a result is returned when the search
29+
interval is length 0 (which occurs in :py:func:`pvlib.pvsystem.singlediode` if v_oc is 0.) (:issue:`1603`, :pull:`1606`)
2830

2931
Testing
3032
~~~~~~~

pvlib/tests/test_tools.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,22 @@ def test__golden_sect_DataFrame_vector():
4545
v, x = tools._golden_sect_DataFrame(params, lower, upper,
4646
_obj_test_golden_sect)
4747
assert np.allclose(x, expected, atol=1e-8)
48+
# some upper and lower bounds equal
49+
params = {'c': np.array([1., 2., 1.]), 'n': np.array([1., 1., 1.])}
50+
lower = np.array([0., 0.001, 1.])
51+
upper = np.array([1., 1.2, 1.])
52+
expected = np.array([0.5, 0.25, 1.0]) # x values for maxima
53+
v, x = tools._golden_sect_DataFrame(params, lower, upper,
54+
_obj_test_golden_sect)
55+
assert np.allclose(x, expected, atol=1e-8)
56+
# all upper and lower bounds equal, arrays of length 1
57+
params = {'c': np.array([1.]), 'n': np.array([1.])}
58+
lower = np.array([1.])
59+
upper = np.array([1.])
60+
expected = np.array([1.]) # x values for maxima
61+
v, x = tools._golden_sect_DataFrame(params, lower, upper,
62+
_obj_test_golden_sect)
63+
assert np.allclose(x, expected, atol=1e-8)
4864

4965

5066
def test__golden_sect_DataFrame_nans():

pvlib/tools.py

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,8 @@ def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8):
341341
--------
342342
pvlib.singlediode._pwr_optfcn
343343
"""
344+
if np.any(upper - lower < 0.):
345+
raise ValueError('upper >= lower is required')
344346

345347
phim1 = (np.sqrt(5) - 1) / 2
346348

@@ -349,16 +351,8 @@ def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8):
349351
df['VL'] = lower
350352

351353
converged = False
352-
iterations = 0
353354

354-
# handle all NaN case gracefully
355-
with warnings.catch_warnings():
356-
warnings.filterwarnings(action='ignore',
357-
message='All-NaN slice encountered')
358-
iterlimit = 1 + np.nanmax(
359-
np.trunc(np.log(atol / (df['VH'] - df['VL'])) / np.log(phim1)))
360-
361-
while not converged and (iterations <= iterlimit):
355+
while not converged:
362356

363357
phi = phim1 * (df['VH'] - df['VL'])
364358
df['V1'] = df['VL'] + phi
@@ -373,22 +367,16 @@ def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8):
373367

374368
err = abs(df['V2'] - df['V1'])
375369

376-
# works with single value because err is np.float64
377-
converged = (err[~np.isnan(err)] < atol).all()
378-
# err will be less than atol before iterations hit the limit
379-
# but just to be safe
380-
iterations += 1
381-
382-
if iterations > iterlimit:
383-
raise Exception("Iterations exceeded maximum. Check that func",
384-
" is not NaN in (lower, upper)") # pragma: no cover
370+
# handle all NaN case gracefully
371+
with warnings.catch_warnings():
372+
warnings.filterwarnings(action='ignore',
373+
message='All-NaN slice encountered')
374+
converged = np.all(err[~np.isnan(err)] < atol)
385375

386-
try:
387-
func_result = func(df, 'V1')
388-
x = np.where(np.isnan(func_result), np.nan, df['V1'])
389-
except KeyError:
390-
func_result = np.full_like(upper, np.nan)
391-
x = func_result.copy()
376+
# best estimate of location of maximum
377+
df['max'] = 0.5 * (df['V1'] + df['V2'])
378+
func_result = func(df, 'max')
379+
x = np.where(np.isnan(func_result), np.nan, df['max'])
392380

393381
return func_result, x
394382

0 commit comments

Comments
 (0)