diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py index fa42fbf82d..e6b8e9091a 100644 --- a/pvlib/tests/test_tools.py +++ b/pvlib/tests/test_tools.py @@ -45,3 +45,30 @@ def test__golden_sect_DataFrame_vector(): v, x = tools._golden_sect_DataFrame(params, lower, upper, _obj_test_golden_sect) assert np.allclose(x, expected, atol=1e-8) + + +def test__golden_sect_DataFrame_nans(): + # nan in bounds + params = {'c': np.array([1., 2., 1.]), 'n': np.array([1., 1., 1.])} + lower = np.array([0., 0.001, np.nan]) + upper = np.array([1.1, 1.2, 1.]) + expected = np.array([0.5, 0.25, np.nan]) + v, x = tools._golden_sect_DataFrame(params, lower, upper, + _obj_test_golden_sect) + assert np.allclose(x, expected, atol=1e-8, equal_nan=True) + # nan in function values + params = {'c': np.array([1., 2., np.nan]), 'n': np.array([1., 1., 1.])} + lower = np.array([0., 0.001, 0.]) + upper = np.array([1.1, 1.2, 1.]) + expected = np.array([0.5, 0.25, np.nan]) + v, x = tools._golden_sect_DataFrame(params, lower, upper, + _obj_test_golden_sect) + assert np.allclose(x, expected, atol=1e-8, equal_nan=True) + # all nan in bounds + params = {'c': np.array([1., 2., 1.]), 'n': np.array([1., 1., 1.])} + lower = np.array([np.nan, np.nan, np.nan]) + upper = np.array([1.1, 1.2, 1.]) + expected = np.array([np.nan, np.nan, np.nan]) + v, x = tools._golden_sect_DataFrame(params, lower, upper, + _obj_test_golden_sect) + assert np.allclose(x, expected, atol=1e-8, equal_nan=True) diff --git a/pvlib/tools.py b/pvlib/tools.py index a36849f903..5c6e1dd293 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -6,6 +6,7 @@ import numpy as np import pandas as pd import pytz +import warnings def cosd(angle): @@ -286,14 +287,17 @@ def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8): Parameters ---------- - params : dict or Dataframe - Parameters to be passed to `func`. + params : dict of numeric + Parameters to be passed to `func`. Each entry must be of the same + length. lower: numeric - Lower bound for the optimization + Lower bound for the optimization. Must be the same length as each + entry of params. upper: numeric - Upper bound for the optimization + Upper bound for the optimization. Must be the same length as each + entry of params. func: function Function to be optimized. Must be in the form @@ -312,6 +316,7 @@ def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8): Notes ----- This function will find the points where the function is maximized. + Returns nan where lower or upper is nan, or where func evaluates to nan. See also -------- @@ -326,10 +331,15 @@ def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8): converged = False iterations = 0 - iterlimit = 1 + np.max( - np.trunc(np.log(atol / (df['VH'] - df['VL'])) / np.log(phim1))) - while not converged and (iterations < iterlimit): + # handle all NaN case gracefully + with warnings.catch_warnings(): + warnings.filterwarnings(action='ignore', + message='All-NaN slice encountered') + iterlimit = 1 + np.nanmax( + np.trunc(np.log(atol / (df['VH'] - df['VL'])) / np.log(phim1))) + + while not converged and (iterations <= iterlimit): phi = phim1 * (df['VH'] - df['VL']) df['V1'] = df['VL'] + phi @@ -345,15 +355,23 @@ def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8): err = abs(df['V2'] - df['V1']) # works with single value because err is np.float64 - converged = (err < atol).all() + converged = (err[~np.isnan(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("iterations exceeded maximum") # pragma: no cover + raise Exception("Iterations exceeded maximum. Check that func", + " is not NaN in (lower, upper)") # pragma: no cover + + try: + func_result = func(df, 'V1') + x = np.where(np.isnan(func_result), np.nan, df['V1']) + except KeyError: + func_result = np.full_like(upper, np.nan) + x = func_result.copy() - return func(df, 'V1'), df['V1'] + return func_result, x def _get_sample_intervals(times, win_length):