Repository: pvlib/pvlib-python
Pull request #1408 (2022-02-17 18:43:19)

handle nan in tools.golden_sect_DataFrame
 - ~~[ ] Closes #xxxx~~
 - [x] I am familiar with the [contributing guidelines](https://pvlib-python.readthedocs.io/en/latest/contributing.html)
 - [x] Tests added
 - ~~[ ] Updates entries in [`docs/sphinx/source/reference`](https://github.com/pvlib/pvlib-python/blob/master/docs/sphinx/source/reference) for API changes.~~
 - ~~[ ] Adds description and name entries in the appropriate "what's new" file in [`docs/sphinx/source/whatsnew`](https://github.com/pvlib/pvlib-python/tree/master/docs/sphinx/source/whatsnew) for all changes. Includes link to the GitHub Issue with `` :issue:`num` `` or this Pull Request with `` :pull:`num` ``. Includes contributor name and/or GitHub username (link with `` :ghuser:`user` ``).~~
 - [x] New code is fully documented. Includes [numpydoc](https://numpydoc.readthedocs.io/en/latest/format.html) compliant docstrings, examples, and comments where necessary.
 - [x] Pull request is nearly complete and ready for detailed review.
 - [x] Maintainer: Appropriate GitHub Labels and Milestone are assigned to the Pull Request and linked Issue.

Supplements #1089. See [discussion](https://github.com/pvlib/pvlib-python/pull/1089/files/8b55eed569e41c258219d1b565e8d296516d542f#r809145242).


Patch:
diff --git a/pvlib/tools.py b/pvlib/tools.py
index a36849f..5c6e1dd 100644
--- a/pvlib/tools.py
+++ b/pvlib/tools.py
@@ -6,6 +6,7 @@ import datetime as dt
 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):


Test patch:
diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py
index fa42fbf..e6b8e90 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)