prompt: 
  We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  golden-section search fails when upper and lower bounds are equal
  **Describe the bug**
  I was using pvlib for sometime now and until now I was always passing a big dataframe containing readings of a long period. Because of some changes in our software architecture, I need to pass the weather readings as a single reading (a dataframe with only one row) and I noticed that for readings that GHI-DHI are zero pvlib fails to calculate the output and returns below error while the same code executes correctly with weather information that has non-zero GHI-DHI:
  ```python
  import os
  import pathlib
  import time
  import json
  from datetime import datetime
  from time import mktime, gmtime
  
  import pandas as pd
  
  from pvlib import pvsystem
  from pvlib import location as pvlocation
  from pvlib import modelchain
  from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS as PARAMS # not used -- to remove
  from pvlib.bifacial.pvfactors import pvfactors_timeseries
  from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS
  
  class PV:
      def pv_transform_time(self, val):
          # tt = gmtime(val / 1000)
          tt = gmtime(val)
          dd = datetime.fromtimestamp(mktime(tt))
          timestamp = pd.Timestamp(dd)
          return timestamp
  
      def __init__(self, model: str, inverter: str, latitude: float, longitude: float, **kwargs):
          # super().__init__(**kwargs)
  
          temperature_model_parameters = TEMPERATURE_MODEL_PARAMETERS["sapm"][
              "open_rack_glass_glass"
          ]
          # Load the database of CEC module model parameters
          modules = pvsystem.retrieve_sam("cecmod")
          # Load the database of CEC inverter model parameters
          inverters = pvsystem.retrieve_sam("cecinverter")
  
  
          # A bare bone PV simulator
  
          # Load the database of CEC module model parameters
          modules = pvsystem.retrieve_sam('cecmod')
          inverters = pvsystem.retrieve_sam('cecinverter')
          module_parameters = modules[model]
          inverter_parameters = inverters[inverter]
  
          location = pvlocation.Location(latitude=latitude, longitude=longitude)
          system = pvsystem.PVSystem(module_parameters=module_parameters, inverter_parameters=inverter_parameters, temperature_model_parameters=temperature_model_parameters)
          self.modelchain = modelchain.ModelChain(system, location, aoi_model='no_loss', spectral_model="no_loss")
  
      def process(self, data):
          weather = pd.read_json(data)
          # print(f"raw_weather: {weather}")
          weather.drop('time.1', axis=1, inplace=True)
          weather['time'] = pd.to_datetime(weather['time']).map(datetime.timestamp) # --> this works for the new process_weather code and also the old weather file
          weather["time"] = weather["time"].apply(self.pv_transform_time)
          weather.index = weather["time"]
          # print(f"weather: {weather}")
          # print(weather.dtypes)
          # print(weather['ghi'][0])
          # print(type(weather['ghi'][0]))
  
          # simulate
          self.modelchain.run_model(weather)
          # print(self.modelchain.results.ac.to_frame().to_json())
          print(self.modelchain.results.ac)
  
  
  # good data
  good_data = "{\"time\":{\"12\":\"2010-01-01 13:30:00+00:00\"},\"ghi\":{\"12\":36},\"dhi\":{\"12\":36},\"dni\":{\"12\":0},\"Tamb\":{\"12\":8.0},\"WindVel\":{\"12\":5.0},\"WindDir\":{\"12\":270},\"time.1\":{\"12\":\"2010-01-01 13:30:00+00:00\"}}"
  
  # data that causes error
  data = "{\"time\":{\"4\":\"2010-01-01 05:30:00+00:00\"},\"ghi\":{\"4\":0},\"dhi\":{\"4\":0},\"dni\":{\"4\":0},\"Tamb\":{\"4\":8.0},\"WindVel\":{\"4\":4.0},\"WindDir\":{\"4\":240},\"time.1\":{\"4\":\"2010-01-01 05:30:00+00:00\"}}"
  p1 = PV(model="Trina_Solar_TSM_300DEG5C_07_II_", inverter="ABB__MICRO_0_25_I_OUTD_US_208__208V_", latitude=51.204483, longitude=5.265472)
  p1.process(good_data)
  print("=====")
  p1.process(data)
  ```
  Error:
  ```log
  $ python3 ./tmp-pv.py 
  time
  2010-01-01 13:30:00    7.825527
  dtype: float64
  =====
  /home/user/.local/lib/python3.10/site-packages/pvlib/tools.py:340: RuntimeWarning: divide by zero encountered in divide
    np.trunc(np.log(atol / (df['VH'] - df['VL'])) / np.log(phim1)))
  Traceback (most recent call last):
    File "/home/user/workspace/enorch/simulator/simulator_processor/src/pv/./tmp-pv.py", line 88, in <module>
      p1.process(data)
    File "/home/user/workspace/enorch/simulator/simulator_processor/src/pv/./tmp-pv.py", line 75, in process
      self.modelchain.run_model(weather)
    File "/home/user/.local/lib/python3.10/site-packages/pvlib/modelchain.py", line 1770, in run_model
      self._run_from_effective_irrad(weather)
    File "/home/user/.local/lib/python3.10/site-packages/pvlib/modelchain.py", line 1858, in _run_from_effective_irrad
      self.dc_model()
    File "/home/user/.local/lib/python3.10/site-packages/pvlib/modelchain.py", line 790, in cec
      return self._singlediode(self.system.calcparams_cec)
    File "/home/user/.local/lib/python3.10/site-packages/pvlib/modelchain.py", line 772, in _singlediode
      self.results.dc = tuple(itertools.starmap(
    File "/home/user/.local/lib/python3.10/site-packages/pvlib/pvsystem.py", line 931, in singlediode
      return singlediode(photocurrent, saturation_current,
    File "/home/user/.local/lib/python3.10/site-packages/pvlib/pvsystem.py", line 2826, in singlediode
      out = _singlediode._lambertw(
    File "/home/user/.local/lib/python3.10/site-packages/pvlib/singlediode.py", line 651, in _lambertw
      p_mp, v_mp = _golden_sect_DataFrame(params, 0., v_oc * 1.14,
    File "/home/user/.local/lib/python3.10/site-packages/pvlib/tools.py", line 364, in _golden_sect_DataFrame
      raise Exception("Iterations exceeded maximum. Check that func",
  Exception: ('Iterations exceeded maximum. Check that func', ' is not NaN in (lower, upper)')
  ```
  
  I have to mention that for now the workaround that I am using is to pass the weather data as a dataframe with two rows, the first row is a good weather data that pvlib can process and the second row is the incoming weather reading (I can also post that code if you want).
  
  **Expected behavior**
  PVlib should have consistent behavior and regardless of GHI-DHI readings.
  
  **Versions:**
  ```python
  >>> import pvlib
  >>> import pandas
  >>> pvlib.__version__
  '0.9.1'
  >>> pandas.__version__
  '1.4.3'
  ``` 
   - python: 3.10.6
  - OS: Ubuntu 22.04.1 LTS
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  
  --- BEGIN FILE ---
  ```
  ### pvlib/tools.py
  """
  Collection of functions used in pvlib_python
  """
  
  import datetime as dt
  import numpy as np
  import pandas as pd
  import pytz
  import warnings
  
  
  def cosd(angle):
      """
      Cosine with angle input in degrees
  
      Parameters
      ----------
      angle : float or array-like
          Angle in degrees
  
      Returns
      -------
      result : float or array-like
          Cosine of the angle
      """
  
      res = np.cos(np.radians(angle))
      return res
  
  
  def sind(angle):
      """
      Sine with angle input in degrees
  
      Parameters
      ----------
      angle : float
          Angle in degrees
  
      Returns
      -------
      result : float
          Sin of the angle
      """
  
      res = np.sin(np.radians(angle))
      return res
  
  
  def tand(angle):
      """
      Tan with angle input in degrees
  
      Parameters
      ----------
      angle : float
          Angle in degrees
  
      Returns
      -------
      result : float
          Tan of the angle
      """
  
      res = np.tan(np.radians(angle))
      return res
  
  
  def asind(number):
      """
      Inverse Sine returning an angle in degrees
  
      Parameters
      ----------
      number : float
          Input number
  
      Returns
      -------
      result : float
          arcsin result
      """
  
      res = np.degrees(np.arcsin(number))
      return res
  
  
  def acosd(number):
      """
      Inverse Cosine returning an angle in degrees
  
      Parameters
      ----------
      number : float
          Input number
  
      Returns
      -------
      result : float
          arccos result
      """
  
      res = np.degrees(np.arccos(number))
      return res
  
  
  def localize_to_utc(time, location):
      """
      Converts or localizes a time series to UTC.
  
      Parameters
      ----------
      time : datetime.datetime, pandas.DatetimeIndex,
             or pandas.Series/DataFrame with a DatetimeIndex.
      location : pvlib.Location object
  
      Returns
      -------
      pandas object localized to UTC.
      """
      if isinstance(time, dt.datetime):
          if time.tzinfo is None:
              time = pytz.timezone(location.tz).localize(time)
          time_utc = time.astimezone(pytz.utc)
      else:
          try:
              time_utc = time.tz_convert('UTC')
          except TypeError:
              time_utc = time.tz_localize(location.tz).tz_convert('UTC')
  
      return time_utc
  
  
  def datetime_to_djd(time):
      """
      Converts a datetime to the Dublin Julian Day
  
      Parameters
      ----------
      time : datetime.datetime
          time to convert
  
      Returns
      -------
      float
          fractional days since 12/31/1899+0000
      """
  
      if time.tzinfo is None:
          time_utc = pytz.utc.localize(time)
      else:
          time_utc = time.astimezone(pytz.utc)
  
      djd_start = pytz.utc.localize(dt.datetime(1899, 12, 31, 12))
      djd = (time_utc - djd_start).total_seconds() * 1.0/(60 * 60 * 24)
  
      return djd
  
  
  def djd_to_datetime(djd, tz='UTC'):
      """
      Converts a Dublin Julian Day float to a datetime.datetime object
  
      Parameters
      ----------
      djd : float
          fractional days since 12/31/1899+0000
      tz : str, default 'UTC'
          timezone to localize the result to
  
      Returns
      -------
      datetime.datetime
         The resultant datetime localized to tz
      """
  
      djd_start = pytz.utc.localize(dt.datetime(1899, 12, 31, 12))
  
      utc_time = djd_start + dt.timedelta(days=djd)
      return utc_time.astimezone(pytz.timezone(tz))
  
  
  def _pandas_to_doy(pd_object):
      """
      Finds the day of year for a pandas datetime-like object.
  
      Useful for delayed evaluation of the dayofyear attribute.
  
      Parameters
      ----------
      pd_object : DatetimeIndex or Timestamp
  
      Returns
      -------
      dayofyear
      """
      return pd_object.dayofyear
  
  
  def _doy_to_datetimeindex(doy, epoch_year=2014):
      """
      Convert a day of year scalar or array to a pd.DatetimeIndex.
  
      Parameters
      ----------
      doy : numeric
          Contains days of the year
  
      Returns
      -------
      pd.DatetimeIndex
      """
      doy = np.atleast_1d(doy).astype('float')
      epoch = pd.Timestamp('{}-12-31'.format(epoch_year - 1))
      timestamps = [epoch + dt.timedelta(days=adoy) for adoy in doy]
      return pd.DatetimeIndex(timestamps)
  
  
  def _datetimelike_scalar_to_doy(time):
      return pd.DatetimeIndex([pd.Timestamp(time)]).dayofyear
  
  
  def _datetimelike_scalar_to_datetimeindex(time):
      return pd.DatetimeIndex([pd.Timestamp(time)])
  
  
  def _scalar_out(arg):
      if np.isscalar(arg):
          output = arg
      else:  #
          # works if it's a 1 length array and
          # will throw a ValueError otherwise
          output = np.asarray(arg).item()
  
      return output
  
  
  def _array_out(arg):
      if isinstance(arg, pd.Series):
          output = arg.values
      else:
          output = arg
  
      return output
  
  
  def _build_kwargs(keys, input_dict):
      """
      Parameters
      ----------
      keys : iterable
          Typically a list of strings.
      input_dict : dict-like
          A dictionary from which to attempt to pull each key.
  
      Returns
      -------
      kwargs : dict
          A dictionary with only the keys that were in input_dict
      """
  
      kwargs = {}
      for key in keys:
          try:
              kwargs[key] = input_dict[key]
          except KeyError:
              pass
  
      return kwargs
  
  
  def _build_args(keys, input_dict, dict_name):
      """
      Parameters
      ----------
      keys : iterable
          Typically a list of strings.
      input_dict : dict-like
          A dictionary from which to pull each key.
      dict_name : str
          A variable name to include in an error message for missing keys
  
      Returns
      -------
      kwargs : list
          A list with values corresponding to keys
      """
      try:
          args = [input_dict[key] for key in keys]
      except KeyError as e:
          missing_key = e.args[0]
          msg = (f"Missing required parameter '{missing_key}'. Found "
                 f"{input_dict} in {dict_name}.")
          raise KeyError(msg)
      return args
  
  
  # Created April,2014
  # Author: Rob Andrews, Calama Consulting
  # Modified: November, 2020 by C. W. Hansen, to add atol and change exit
  # criteria
  def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8):
      """
      Vectorized golden section search for finding maximum of a function of a
      single variable.
  
      Parameters
      ----------
      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. Must be the same length as each
          entry of params.
  
      upper: numeric
          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
          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
      -------
      numeric
          function evaluated at the optimal points
  
      numeric
          optimal points
  
      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
      --------
      pvlib.singlediode._pwr_optfcn
      """
  
      phim1 = (np.sqrt(5) - 1) / 2
  
      df = params
      df['VH'] = upper
      df['VL'] = lower
  
      converged = False
      iterations = 0
  
      # 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
          df['V2'] = df['VH'] - phi
  
          df['f1'] = func(df, 'V1')
          df['f2'] = func(df, 'V2')
          df['SW_Flag'] = df['f1'] > df['f2']
  
          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 = abs(df['V2'] - df['V1'])
  
          # works with single value because err is np.float64
          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. 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_result, x
  
  
  def _get_sample_intervals(times, win_length):
      """ Calculates time interval and samples per window for Reno-style clear
      sky detection functions
      """
      deltas = np.diff(times.values) / np.timedelta64(1, '60s')
  
      # determine if we can proceed
      if times.inferred_freq and len(np.unique(deltas)) == 1:
          sample_interval = times[1] - times[0]
          sample_interval = sample_interval.seconds / 60  # in minutes
          samples_per_window = int(win_length / sample_interval)
          return sample_interval, samples_per_window
      else:
          message = (
              'algorithm does not yet support unequal time intervals. consider '
              'resampling your data and checking for gaps from missing '
              'periods, leap days, etc.'
          )
          raise NotImplementedError(message)
  
  
  def _degrees_to_index(degrees, coordinate):
      """Transform input degrees to an output index integer.
      Specify a degree value and either 'latitude' or 'longitude' to get
      the appropriate index number for these two index numbers.
      Parameters
      ----------
      degrees : float or int
          Degrees of either latitude or longitude.
      coordinate : string
          Specify whether degrees arg is latitude or longitude. Must be set to
          either 'latitude' or 'longitude' or an error will be raised.
      Returns
      -------
      index : np.int16
          The latitude or longitude index number to use when looking up values
          in the Linke turbidity lookup table.
      """
      # Assign inputmin, inputmax, and outputmax based on degree type.
      if coordinate == 'latitude':
          inputmin = 90
          inputmax = -90
          outputmax = 2160
      elif coordinate == 'longitude':
          inputmin = -180
          inputmax = 180
          outputmax = 4320
      else:
          raise IndexError("coordinate must be 'latitude' or 'longitude'.")
  
      inputrange = inputmax - inputmin
      scale = outputmax/inputrange  # number of indices per degree
      center = inputmin + 1 / scale / 2  # shift to center of index
      outputmax -= 1  # shift index to zero indexing
      index = (degrees - center) * scale
      err = IndexError('Input, %g, is out of range (%g, %g).' %
                       (degrees, inputmin, inputmax))
  
      # If the index is still out of bounds after rounding, raise an error.
      # 0.500001 is used in comparisons instead of 0.5 to allow for a small
      # margin of error which can occur when dealing with floating point numbers.
      if index > outputmax:
          if index - outputmax <= 0.500001:
              index = outputmax
          else:
              raise err
      elif index < 0:
          if -index <= 0.500001:
              index = 0
          else:
              raise err
      # If the index wasn't set to outputmax or 0, round it and cast it as an
      # integer so it can be used in integer-based indexing.
      else:
          index = int(np.around(index))
  
      return index
  
  ```
  --- END FILE ---
  
  Please first localize the bug based on the issue statement, and then generate *SEARCH/REPLACE* edits to fix the issue.
  
  Every *SEARCH/REPLACE* edit must use this format:
  1. The file path
  2. The start of search block: <<<<<<< SEARCH
  3. A contiguous chunk of lines to search for in the existing source code
  4. The dividing line: =======
  5. The lines to replace into the source code
  6. The end of the replace block: >>>>>>> REPLACE
  
  Here is an example:
  
  ```python
  ### mathweb/flask/app.py
  <<<<<<< SEARCH
  from flask import Flask
  =======
  import math
  from flask import Flask
  >>>>>>> REPLACE
  ```
  
  Please note that the *SEARCH/REPLACE* edit REQUIRES PROPER INDENTATION. If you would like to add the line '        print(x)', you must fully write that out, with all those spaces before the code!
  Wrap the *SEARCH/REPLACE* edit in blocks ```python...```.
  
instance: {"FAIL_TO_PASS": "[\"pvlib/tests/test_tools.py::test__golden_sect_DataFrame_vector\"]", "PASS_TO_PASS": "[\"pvlib/tests/test_tools.py::test_build_kwargs[keys0-input_dict0-expected0]\", \"pvlib/tests/test_tools.py::test_build_kwargs[keys1-input_dict1-expected1]\", \"pvlib/tests/test_tools.py::test_build_kwargs[keys2-input_dict2-expected2]\", \"pvlib/tests/test_tools.py::test_build_kwargs[keys3-input_dict3-expected3]\", \"pvlib/tests/test_tools.py::test__golden_sect_DataFrame[params0-0.0-1.0-0.5-_obj_test_golden_sect]\", \"pvlib/tests/test_tools.py::test__golden_sect_DataFrame[params1-0.0-1.0-0.07230200263994839-_obj_test_golden_sect]\", \"pvlib/tests/test_tools.py::test__golden_sect_DataFrame[params2-0.0-100.0-89.14332727531685-_obj_test_golden_sect]\", \"pvlib/tests/test_tools.py::test__golden_sect_DataFrame_atol\", \"pvlib/tests/test_tools.py::test__golden_sect_DataFrame_nans\", \"pvlib/tests/test_tools.py::test_degrees_to_index_1\"]", "base_commit": "c78b50f4337ecbe536a961336ca91a1176efc0e8", "created_at": "2022-12-07T21:12:08Z", "environment_setup_commit": "ef8ad2fee9840a77d14b0dfd17fc489dd85c9b91", "hints_text": "Confirmed. This appears to be an oversight in `pvlib.tools._golden_section_DataFrame` involving error messaging, likely introduced with #1089 .\r\n\r\nIn this code when processing the content of `data`, photocurrent is 0., hence the shunt resistance is infinite and v_oc is 0. That sets the range for the golden section search to be [0., 0.]. [iterlimit](https://github.com/pvlib/pvlib-python/blob/582b956c63c463e5178fbb7a88fa545fa5b1c257/pvlib/tools.py#L358) is then -infinity, which skips the loop (`iterations <= iterlimit`) but since `iterations > iterlimit` raises the \"Iterations exceeded...\" exception.\r\n", "instance_id": "pvlib__pvlib-python-1606", "patch": "diff --git a/pvlib/tools.py b/pvlib/tools.py\n--- a/pvlib/tools.py\n+++ b/pvlib/tools.py\n@@ -341,6 +341,8 @@ def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8):\n     --------\n     pvlib.singlediode._pwr_optfcn\n     \"\"\"\n+    if np.any(upper - lower < 0.):\n+        raise ValueError('upper >= lower is required')\n \n     phim1 = (np.sqrt(5) - 1) / 2\n \n@@ -349,16 +351,8 @@ def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8):\n     df['VL'] = lower\n \n     converged = False\n-    iterations = 0\n \n-    # handle all NaN case gracefully\n-    with warnings.catch_warnings():\n-        warnings.filterwarnings(action='ignore',\n-                                message='All-NaN slice encountered')\n-        iterlimit = 1 + np.nanmax(\n-            np.trunc(np.log(atol / (df['VH'] - df['VL'])) / np.log(phim1)))\n-\n-    while not converged and (iterations <= iterlimit):\n+    while not converged:\n \n         phi = phim1 * (df['VH'] - df['VL'])\n         df['V1'] = df['VL'] + phi\n@@ -373,22 +367,16 @@ def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8):\n \n         err = abs(df['V2'] - df['V1'])\n \n-        # works with single value because err is np.float64\n-        converged = (err[~np.isnan(err)] < atol).all()\n-        # err will be less than atol before iterations hit the limit\n-        # but just to be safe\n-        iterations += 1\n-\n-    if iterations > iterlimit:\n-        raise Exception(\"Iterations exceeded maximum. Check that func\",\n-                        \" is not NaN in (lower, upper)\")  # pragma: no cover\n+        # handle all NaN case gracefully\n+        with warnings.catch_warnings():\n+            warnings.filterwarnings(action='ignore',\n+                                    message='All-NaN slice encountered')\n+            converged = np.all(err[~np.isnan(err)] < atol)\n \n-    try:\n-        func_result = func(df, 'V1')\n-        x = np.where(np.isnan(func_result), np.nan, df['V1'])\n-    except KeyError:\n-        func_result = np.full_like(upper, np.nan)\n-        x = func_result.copy()\n+    # best estimate of location of maximum\n+    df['max'] = 0.5 * (df['V1'] + df['V2'])\n+    func_result = func(df, 'max')\n+    x = np.where(np.isnan(func_result), np.nan, df['max'])\n \n     return func_result, x\n \n", "problem_statement": "golden-section search fails when upper and lower bounds are equal\n**Describe the bug**\r\nI was using pvlib for sometime now and until now I was always passing a big dataframe containing readings of a long period. Because of some changes in our software architecture, I need to pass the weather readings as a single reading (a dataframe with only one row) and I noticed that for readings that GHI-DHI are zero pvlib fails to calculate the output and returns below error while the same code executes correctly with weather information that has non-zero GHI-DHI:\r\n```python\r\nimport os\r\nimport pathlib\r\nimport time\r\nimport json\r\nfrom datetime import datetime\r\nfrom time import mktime, gmtime\r\n\r\nimport pandas as pd\r\n\r\nfrom pvlib import pvsystem\r\nfrom pvlib import location as pvlocation\r\nfrom pvlib import modelchain\r\nfrom pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS as PARAMS # not used -- to remove\r\nfrom pvlib.bifacial.pvfactors import pvfactors_timeseries\r\nfrom pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS\r\n\r\nclass PV:\r\n    def pv_transform_time(self, val):\r\n        # tt = gmtime(val / 1000)\r\n        tt = gmtime(val)\r\n        dd = datetime.fromtimestamp(mktime(tt))\r\n        timestamp = pd.Timestamp(dd)\r\n        return timestamp\r\n\r\n    def __init__(self, model: str, inverter: str, latitude: float, longitude: float, **kwargs):\r\n        # super().__init__(**kwargs)\r\n\r\n        temperature_model_parameters = TEMPERATURE_MODEL_PARAMETERS[\"sapm\"][\r\n            \"open_rack_glass_glass\"\r\n        ]\r\n        # Load the database of CEC module model parameters\r\n        modules = pvsystem.retrieve_sam(\"cecmod\")\r\n        # Load the database of CEC inverter model parameters\r\n        inverters = pvsystem.retrieve_sam(\"cecinverter\")\r\n\r\n\r\n        # A bare bone PV simulator\r\n\r\n        # Load the database of CEC module model parameters\r\n        modules = pvsystem.retrieve_sam('cecmod')\r\n        inverters = pvsystem.retrieve_sam('cecinverter')\r\n        module_parameters = modules[model]\r\n        inverter_parameters = inverters[inverter]\r\n\r\n        location = pvlocation.Location(latitude=latitude, longitude=longitude)\r\n        system = pvsystem.PVSystem(module_parameters=module_parameters, inverter_parameters=inverter_parameters, temperature_model_parameters=temperature_model_parameters)\r\n        self.modelchain = modelchain.ModelChain(system, location, aoi_model='no_loss', spectral_model=\"no_loss\")\r\n\r\n    def process(self, data):\r\n        weather = pd.read_json(data)\r\n        # print(f\"raw_weather: {weather}\")\r\n        weather.drop('time.1', axis=1, inplace=True)\r\n        weather['time'] = pd.to_datetime(weather['time']).map(datetime.timestamp) # --> this works for the new process_weather code and also the old weather file\r\n        weather[\"time\"] = weather[\"time\"].apply(self.pv_transform_time)\r\n        weather.index = weather[\"time\"]\r\n        # print(f\"weather: {weather}\")\r\n        # print(weather.dtypes)\r\n        # print(weather['ghi'][0])\r\n        # print(type(weather['ghi'][0]))\r\n\r\n        # simulate\r\n        self.modelchain.run_model(weather)\r\n        # print(self.modelchain.results.ac.to_frame().to_json())\r\n        print(self.modelchain.results.ac)\r\n\r\n\r\n# good data\r\ngood_data = \"{\\\"time\\\":{\\\"12\\\":\\\"2010-01-01 13:30:00+00:00\\\"},\\\"ghi\\\":{\\\"12\\\":36},\\\"dhi\\\":{\\\"12\\\":36},\\\"dni\\\":{\\\"12\\\":0},\\\"Tamb\\\":{\\\"12\\\":8.0},\\\"WindVel\\\":{\\\"12\\\":5.0},\\\"WindDir\\\":{\\\"12\\\":270},\\\"time.1\\\":{\\\"12\\\":\\\"2010-01-01 13:30:00+00:00\\\"}}\"\r\n\r\n# data that causes error\r\ndata = \"{\\\"time\\\":{\\\"4\\\":\\\"2010-01-01 05:30:00+00:00\\\"},\\\"ghi\\\":{\\\"4\\\":0},\\\"dhi\\\":{\\\"4\\\":0},\\\"dni\\\":{\\\"4\\\":0},\\\"Tamb\\\":{\\\"4\\\":8.0},\\\"WindVel\\\":{\\\"4\\\":4.0},\\\"WindDir\\\":{\\\"4\\\":240},\\\"time.1\\\":{\\\"4\\\":\\\"2010-01-01 05:30:00+00:00\\\"}}\"\r\np1 = PV(model=\"Trina_Solar_TSM_300DEG5C_07_II_\", inverter=\"ABB__MICRO_0_25_I_OUTD_US_208__208V_\", latitude=51.204483, longitude=5.265472)\r\np1.process(good_data)\r\nprint(\"=====\")\r\np1.process(data)\r\n```\r\nError:\r\n```log\r\n$ python3 ./tmp-pv.py \r\ntime\r\n2010-01-01 13:30:00    7.825527\r\ndtype: float64\r\n=====\r\n/home/user/.local/lib/python3.10/site-packages/pvlib/tools.py:340: RuntimeWarning: divide by zero encountered in divide\r\n  np.trunc(np.log(atol / (df['VH'] - df['VL'])) / np.log(phim1)))\r\nTraceback (most recent call last):\r\n  File \"/home/user/workspace/enorch/simulator/simulator_processor/src/pv/./tmp-pv.py\", line 88, in <module>\r\n    p1.process(data)\r\n  File \"/home/user/workspace/enorch/simulator/simulator_processor/src/pv/./tmp-pv.py\", line 75, in process\r\n    self.modelchain.run_model(weather)\r\n  File \"/home/user/.local/lib/python3.10/site-packages/pvlib/modelchain.py\", line 1770, in run_model\r\n    self._run_from_effective_irrad(weather)\r\n  File \"/home/user/.local/lib/python3.10/site-packages/pvlib/modelchain.py\", line 1858, in _run_from_effective_irrad\r\n    self.dc_model()\r\n  File \"/home/user/.local/lib/python3.10/site-packages/pvlib/modelchain.py\", line 790, in cec\r\n    return self._singlediode(self.system.calcparams_cec)\r\n  File \"/home/user/.local/lib/python3.10/site-packages/pvlib/modelchain.py\", line 772, in _singlediode\r\n    self.results.dc = tuple(itertools.starmap(\r\n  File \"/home/user/.local/lib/python3.10/site-packages/pvlib/pvsystem.py\", line 931, in singlediode\r\n    return singlediode(photocurrent, saturation_current,\r\n  File \"/home/user/.local/lib/python3.10/site-packages/pvlib/pvsystem.py\", line 2826, in singlediode\r\n    out = _singlediode._lambertw(\r\n  File \"/home/user/.local/lib/python3.10/site-packages/pvlib/singlediode.py\", line 651, in _lambertw\r\n    p_mp, v_mp = _golden_sect_DataFrame(params, 0., v_oc * 1.14,\r\n  File \"/home/user/.local/lib/python3.10/site-packages/pvlib/tools.py\", line 364, in _golden_sect_DataFrame\r\n    raise Exception(\"Iterations exceeded maximum. Check that func\",\r\nException: ('Iterations exceeded maximum. Check that func', ' is not NaN in (lower, upper)')\r\n```\r\n\r\nI have to mention that for now the workaround that I am using is to pass the weather data as a dataframe with two rows, the first row is a good weather data that pvlib can process and the second row is the incoming weather reading (I can also post that code if you want).\r\n\r\n**Expected behavior**\r\nPVlib should have consistent behavior and regardless of GHI-DHI readings.\r\n\r\n**Versions:**\r\n```python\r\n>>> import pvlib\r\n>>> import pandas\r\n>>> pvlib.__version__\r\n'0.9.1'\r\n>>> pandas.__version__\r\n'1.4.3'\r\n``` \r\n - python: 3.10.6\r\n- OS: Ubuntu 22.04.1 LTS\n", "repo": "pvlib/pvlib-python", "test_patch": "diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py\n--- a/pvlib/tests/test_tools.py\n+++ b/pvlib/tests/test_tools.py\n@@ -45,6 +45,22 @@ def test__golden_sect_DataFrame_vector():\n     v, x = tools._golden_sect_DataFrame(params, lower, upper,\n                                         _obj_test_golden_sect)\n     assert np.allclose(x, expected, atol=1e-8)\n+    # some upper and lower bounds equal\n+    params = {'c': np.array([1., 2., 1.]), 'n': np.array([1., 1., 1.])}\n+    lower = np.array([0., 0.001, 1.])\n+    upper = np.array([1., 1.2, 1.])\n+    expected = np.array([0.5, 0.25, 1.0])  # x values for maxima\n+    v, x = tools._golden_sect_DataFrame(params, lower, upper,\n+                                        _obj_test_golden_sect)\n+    assert np.allclose(x, expected, atol=1e-8)\n+    # all upper and lower bounds equal, arrays of length 1\n+    params = {'c': np.array([1.]), 'n': np.array([1.])}\n+    lower = np.array([1.])\n+    upper = np.array([1.])\n+    expected = np.array([1.])  # x values for maxima\n+    v, x = tools._golden_sect_DataFrame(params, lower, upper,\n+                                        _obj_test_golden_sect)\n+    assert np.allclose(x, expected, atol=1e-8)\n \n \n def test__golden_sect_DataFrame_nans():\n", "version": "0.8"}
parsed_commit_content: {"file_diffs":[{"old_file_content":"\"\"\"\nCollection of functions used in pvlib_python\n\"\"\"\n\nimport datetime as dt\nimport numpy as np\nimport pandas as pd\nimport pytz\nimport warnings\n\n\ndef cosd(angle):\n    \"\"\"\n    Cosine with angle input in degrees\n\n    Parameters\n    ----------\n    angle : float or array-like\n        Angle in degrees\n\n    Returns\n    -------\n    result : float or array-like\n        Cosine of the angle\n    \"\"\"\n\n    res = np.cos(np.radians(angle))\n    return res\n\n\ndef sind(angle):\n    \"\"\"\n    Sine with angle input in degrees\n\n    Parameters\n    ----------\n    angle : float\n        Angle in degrees\n\n    Returns\n    -------\n    result : float\n        Sin of the angle\n    \"\"\"\n\n    res = np.sin(np.radians(angle))\n    return res\n\n\ndef tand(angle):\n    \"\"\"\n    Tan with angle input in degrees\n\n    Parameters\n    ----------\n    angle : float\n        Angle in degrees\n\n    Returns\n    -------\n    result : float\n        Tan of the angle\n    \"\"\"\n\n    res = np.tan(np.radians(angle))\n    return res\n\n\ndef asind(number):\n    \"\"\"\n    Inverse Sine returning an angle in degrees\n\n    Parameters\n    ----------\n    number : float\n        Input number\n\n    Returns\n    -------\n    result : float\n        arcsin result\n    \"\"\"\n\n    res = np.degrees(np.arcsin(number))\n    return res\n\n\ndef acosd(number):\n    \"\"\"\n    Inverse Cosine returning an angle in degrees\n\n    Parameters\n    ----------\n    number : float\n        Input number\n\n    Returns\n    -------\n    result : float\n        arccos result\n    \"\"\"\n\n    res = np.degrees(np.arccos(number))\n    return res\n\n\ndef localize_to_utc(time, location):\n    \"\"\"\n    Converts or localizes a time series to UTC.\n\n    Parameters\n    ----------\n    time : datetime.datetime, pandas.DatetimeIndex,\n           or pandas.Series/DataFrame with a DatetimeIndex.\n    location : pvlib.Location object\n\n    Returns\n    -------\n    pandas object localized to UTC.\n    \"\"\"\n    if isinstance(time, dt.datetime):\n        if time.tzinfo is None:\n            time = pytz.timezone(location.tz).localize(time)\n        time_utc = time.astimezone(pytz.utc)\n    else:\n        try:\n            time_utc = time.tz_convert('UTC')\n        except TypeError:\n            time_utc = time.tz_localize(location.tz).tz_convert('UTC')\n\n    return time_utc\n\n\ndef datetime_to_djd(time):\n    \"\"\"\n    Converts a datetime to the Dublin Julian Day\n\n    Parameters\n    ----------\n    time : datetime.datetime\n        time to convert\n\n    Returns\n    -------\n    float\n        fractional days since 12/31/1899+0000\n    \"\"\"\n\n    if time.tzinfo is None:\n        time_utc = pytz.utc.localize(time)\n    else:\n        time_utc = time.astimezone(pytz.utc)\n\n    djd_start = pytz.utc.localize(dt.datetime(1899, 12, 31, 12))\n    djd = (time_utc - djd_start).total_seconds() * 1.0/(60 * 60 * 24)\n\n    return djd\n\n\ndef djd_to_datetime(djd, tz='UTC'):\n    \"\"\"\n    Converts a Dublin Julian Day float to a datetime.datetime object\n\n    Parameters\n    ----------\n    djd : float\n        fractional days since 12/31/1899+0000\n    tz : str, default 'UTC'\n        timezone to localize the result to\n\n    Returns\n    -------\n    datetime.datetime\n       The resultant datetime localized to tz\n    \"\"\"\n\n    djd_start = pytz.utc.localize(dt.datetime(1899, 12, 31, 12))\n\n    utc_time = djd_start + dt.timedelta(days=djd)\n    return utc_time.astimezone(pytz.timezone(tz))\n\n\ndef _pandas_to_doy(pd_object):\n    \"\"\"\n    Finds the day of year for a pandas datetime-like object.\n\n    Useful for delayed evaluation of the dayofyear attribute.\n\n    Parameters\n    ----------\n    pd_object : DatetimeIndex or Timestamp\n\n    Returns\n    -------\n    dayofyear\n    \"\"\"\n    return pd_object.dayofyear\n\n\ndef _doy_to_datetimeindex(doy, epoch_year=2014):\n    \"\"\"\n    Convert a day of year scalar or array to a pd.DatetimeIndex.\n\n    Parameters\n    ----------\n    doy : numeric\n        Contains days of the year\n\n    Returns\n    -------\n    pd.DatetimeIndex\n    \"\"\"\n    doy = np.atleast_1d(doy).astype('float')\n    epoch = pd.Timestamp('{}-12-31'.format(epoch_year - 1))\n    timestamps = [epoch + dt.timedelta(days=adoy) for adoy in doy]\n    return pd.DatetimeIndex(timestamps)\n\n\ndef _datetimelike_scalar_to_doy(time):\n    return pd.DatetimeIndex([pd.Timestamp(time)]).dayofyear\n\n\ndef _datetimelike_scalar_to_datetimeindex(time):\n    return pd.DatetimeIndex([pd.Timestamp(time)])\n\n\ndef _scalar_out(arg):\n    if np.isscalar(arg):\n        output = arg\n    else:  #\n        # works if it's a 1 length array and\n        # will throw a ValueError otherwise\n        output = np.asarray(arg).item()\n\n    return output\n\n\ndef _array_out(arg):\n    if isinstance(arg, pd.Series):\n        output = arg.values\n    else:\n        output = arg\n\n    return output\n\n\ndef _build_kwargs(keys, input_dict):\n    \"\"\"\n    Parameters\n    ----------\n    keys : iterable\n        Typically a list of strings.\n    input_dict : dict-like\n        A dictionary from which to attempt to pull each key.\n\n    Returns\n    -------\n    kwargs : dict\n        A dictionary with only the keys that were in input_dict\n    \"\"\"\n\n    kwargs = {}\n    for key in keys:\n        try:\n            kwargs[key] = input_dict[key]\n        except KeyError:\n            pass\n\n    return kwargs\n\n\ndef _build_args(keys, input_dict, dict_name):\n    \"\"\"\n    Parameters\n    ----------\n    keys : iterable\n        Typically a list of strings.\n    input_dict : dict-like\n        A dictionary from which to pull each key.\n    dict_name : str\n        A variable name to include in an error message for missing keys\n\n    Returns\n    -------\n    kwargs : list\n        A list with values corresponding to keys\n    \"\"\"\n    try:\n        args = [input_dict[key] for key in keys]\n    except KeyError as e:\n        missing_key = e.args[0]\n        msg = (f\"Missing required parameter '{missing_key}'. Found \"\n               f\"{input_dict} in {dict_name}.\")\n        raise KeyError(msg)\n    return args\n\n\n# Created April,2014\n# Author: Rob Andrews, Calama Consulting\n# Modified: November, 2020 by C. W. Hansen, to add atol and change exit\n# criteria\ndef _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8):\n    \"\"\"\n    Vectorized golden section search for finding maximum of a function of a\n    single variable.\n\n    Parameters\n    ----------\n    params : dict of numeric\n        Parameters to be passed to `func`. Each entry must be of the same\n        length.\n\n    lower: numeric\n        Lower bound for the optimization. Must be the same length as each\n        entry of params.\n\n    upper: numeric\n        Upper bound for the optimization. Must be the same length as each\n        entry of params.\n\n    func: function\n        Function to be optimized. Must be in the form\n        result = f(dict or DataFrame, str), where result is a dict or DataFrame\n        that also contains the function output, and str is the key\n        corresponding to the function's input variable.\n\n    Returns\n    -------\n    numeric\n        function evaluated at the optimal points\n\n    numeric\n        optimal points\n\n    Notes\n    -----\n    This function will find the points where the function is maximized.\n    Returns nan where lower or upper is nan, or where func evaluates to nan.\n\n    See also\n    --------\n    pvlib.singlediode._pwr_optfcn\n    \"\"\"\n\n    phim1 = (np.sqrt(5) - 1) / 2\n\n    df = params\n    df['VH'] = upper\n    df['VL'] = lower\n\n    converged = False\n    iterations = 0\n\n    # handle all NaN case gracefully\n    with warnings.catch_warnings():\n        warnings.filterwarnings(action='ignore',\n                                message='All-NaN slice encountered')\n        iterlimit = 1 + np.nanmax(\n            np.trunc(np.log(atol / (df['VH'] - df['VL'])) / np.log(phim1)))\n\n    while not converged and (iterations <= iterlimit):\n\n        phi = phim1 * (df['VH'] - df['VL'])\n        df['V1'] = df['VL'] + phi\n        df['V2'] = df['VH'] - phi\n\n        df['f1'] = func(df, 'V1')\n        df['f2'] = func(df, 'V2')\n        df['SW_Flag'] = df['f1'] > df['f2']\n\n        df['VL'] = df['V2']*df['SW_Flag'] + df['VL']*(~df['SW_Flag'])\n        df['VH'] = df['V1']*~df['SW_Flag'] + df['VH']*(df['SW_Flag'])\n\n        err = abs(df['V2'] - df['V1'])\n\n        # works with single value because err is np.float64\n        converged = (err[~np.isnan(err)] < atol).all()\n        # err will be less than atol before iterations hit the limit\n        # but just to be safe\n        iterations += 1\n\n    if iterations > iterlimit:\n        raise Exception(\"Iterations exceeded maximum. Check that func\",\n                        \" is not NaN in (lower, upper)\")  # pragma: no cover\n\n    try:\n        func_result = func(df, 'V1')\n        x = np.where(np.isnan(func_result), np.nan, df['V1'])\n    except KeyError:\n        func_result = np.full_like(upper, np.nan)\n        x = func_result.copy()\n\n    return func_result, x\n\n\ndef _get_sample_intervals(times, win_length):\n    \"\"\" Calculates time interval and samples per window for Reno-style clear\n    sky detection functions\n    \"\"\"\n    deltas = np.diff(times.values) / np.timedelta64(1, '60s')\n\n    # determine if we can proceed\n    if times.inferred_freq and len(np.unique(deltas)) == 1:\n        sample_interval = times[1] - times[0]\n        sample_interval = sample_interval.seconds / 60  # in minutes\n        samples_per_window = int(win_length / sample_interval)\n        return sample_interval, samples_per_window\n    else:\n        message = (\n            'algorithm does not yet support unequal time intervals. consider '\n            'resampling your data and checking for gaps from missing '\n            'periods, leap days, etc.'\n        )\n        raise NotImplementedError(message)\n\n\ndef _degrees_to_index(degrees, coordinate):\n    \"\"\"Transform input degrees to an output index integer.\n    Specify a degree value and either 'latitude' or 'longitude' to get\n    the appropriate index number for these two index numbers.\n    Parameters\n    ----------\n    degrees : float or int\n        Degrees of either latitude or longitude.\n    coordinate : string\n        Specify whether degrees arg is latitude or longitude. Must be set to\n        either 'latitude' or 'longitude' or an error will be raised.\n    Returns\n    -------\n    index : np.int16\n        The latitude or longitude index number to use when looking up values\n        in the Linke turbidity lookup table.\n    \"\"\"\n    # Assign inputmin, inputmax, and outputmax based on degree type.\n    if coordinate == 'latitude':\n        inputmin = 90\n        inputmax = -90\n        outputmax = 2160\n    elif coordinate == 'longitude':\n        inputmin = -180\n        inputmax = 180\n        outputmax = 4320\n    else:\n        raise IndexError(\"coordinate must be 'latitude' or 'longitude'.\")\n\n    inputrange = inputmax - inputmin\n    scale = outputmax/inputrange  # number of indices per degree\n    center = inputmin + 1 / scale / 2  # shift to center of index\n    outputmax -= 1  # shift index to zero indexing\n    index = (degrees - center) * scale\n    err = IndexError('Input, %g, is out of range (%g, %g).' %\n                     (degrees, inputmin, inputmax))\n\n    # If the index is still out of bounds after rounding, raise an error.\n    # 0.500001 is used in comparisons instead of 0.5 to allow for a small\n    # margin of error which can occur when dealing with floating point numbers.\n    if index > outputmax:\n        if index - outputmax <= 0.500001:\n            index = outputmax\n        else:\n            raise err\n    elif index < 0:\n        if -index <= 0.500001:\n            index = 0\n        else:\n            raise err\n    # If the index wasn't set to outputmax or 0, round it and cast it as an\n    # integer so it can be used in integer-based indexing.\n    else:\n        index = int(np.around(index))\n\n    return index\n","new_file_content":"\"\"\"\nCollection of functions used in pvlib_python\n\"\"\"\n\nimport datetime as dt\nimport numpy as np\nimport pandas as pd\nimport pytz\nimport warnings\n\n\ndef cosd(angle):\n    \"\"\"\n    Cosine with angle input in degrees\n\n    Parameters\n    ----------\n    angle : float or array-like\n        Angle in degrees\n\n    Returns\n    -------\n    result : float or array-like\n        Cosine of the angle\n    \"\"\"\n\n    res = np.cos(np.radians(angle))\n    return res\n\n\ndef sind(angle):\n    \"\"\"\n    Sine with angle input in degrees\n\n    Parameters\n    ----------\n    angle : float\n        Angle in degrees\n\n    Returns\n    -------\n    result : float\n        Sin of the angle\n    \"\"\"\n\n    res = np.sin(np.radians(angle))\n    return res\n\n\ndef tand(angle):\n    \"\"\"\n    Tan with angle input in degrees\n\n    Parameters\n    ----------\n    angle : float\n        Angle in degrees\n\n    Returns\n    -------\n    result : float\n        Tan of the angle\n    \"\"\"\n\n    res = np.tan(np.radians(angle))\n    return res\n\n\ndef asind(number):\n    \"\"\"\n    Inverse Sine returning an angle in degrees\n\n    Parameters\n    ----------\n    number : float\n        Input number\n\n    Returns\n    -------\n    result : float\n        arcsin result\n    \"\"\"\n\n    res = np.degrees(np.arcsin(number))\n    return res\n\n\ndef acosd(number):\n    \"\"\"\n    Inverse Cosine returning an angle in degrees\n\n    Parameters\n    ----------\n    number : float\n        Input number\n\n    Returns\n    -------\n    result : float\n        arccos result\n    \"\"\"\n\n    res = np.degrees(np.arccos(number))\n    return res\n\n\ndef localize_to_utc(time, location):\n    \"\"\"\n    Converts or localizes a time series to UTC.\n\n    Parameters\n    ----------\n    time : datetime.datetime, pandas.DatetimeIndex,\n           or pandas.Series/DataFrame with a DatetimeIndex.\n    location : pvlib.Location object\n\n    Returns\n    -------\n    pandas object localized to UTC.\n    \"\"\"\n    if isinstance(time, dt.datetime):\n        if time.tzinfo is None:\n            time = pytz.timezone(location.tz).localize(time)\n        time_utc = time.astimezone(pytz.utc)\n    else:\n        try:\n            time_utc = time.tz_convert('UTC')\n        except TypeError:\n            time_utc = time.tz_localize(location.tz).tz_convert('UTC')\n\n    return time_utc\n\n\ndef datetime_to_djd(time):\n    \"\"\"\n    Converts a datetime to the Dublin Julian Day\n\n    Parameters\n    ----------\n    time : datetime.datetime\n        time to convert\n\n    Returns\n    -------\n    float\n        fractional days since 12/31/1899+0000\n    \"\"\"\n\n    if time.tzinfo is None:\n        time_utc = pytz.utc.localize(time)\n    else:\n        time_utc = time.astimezone(pytz.utc)\n\n    djd_start = pytz.utc.localize(dt.datetime(1899, 12, 31, 12))\n    djd = (time_utc - djd_start).total_seconds() * 1.0/(60 * 60 * 24)\n\n    return djd\n\n\ndef djd_to_datetime(djd, tz='UTC'):\n    \"\"\"\n    Converts a Dublin Julian Day float to a datetime.datetime object\n\n    Parameters\n    ----------\n    djd : float\n        fractional days since 12/31/1899+0000\n    tz : str, default 'UTC'\n        timezone to localize the result to\n\n    Returns\n    -------\n    datetime.datetime\n       The resultant datetime localized to tz\n    \"\"\"\n\n    djd_start = pytz.utc.localize(dt.datetime(1899, 12, 31, 12))\n\n    utc_time = djd_start + dt.timedelta(days=djd)\n    return utc_time.astimezone(pytz.timezone(tz))\n\n\ndef _pandas_to_doy(pd_object):\n    \"\"\"\n    Finds the day of year for a pandas datetime-like object.\n\n    Useful for delayed evaluation of the dayofyear attribute.\n\n    Parameters\n    ----------\n    pd_object : DatetimeIndex or Timestamp\n\n    Returns\n    -------\n    dayofyear\n    \"\"\"\n    return pd_object.dayofyear\n\n\ndef _doy_to_datetimeindex(doy, epoch_year=2014):\n    \"\"\"\n    Convert a day of year scalar or array to a pd.DatetimeIndex.\n\n    Parameters\n    ----------\n    doy : numeric\n        Contains days of the year\n\n    Returns\n    -------\n    pd.DatetimeIndex\n    \"\"\"\n    doy = np.atleast_1d(doy).astype('float')\n    epoch = pd.Timestamp('{}-12-31'.format(epoch_year - 1))\n    timestamps = [epoch + dt.timedelta(days=adoy) for adoy in doy]\n    return pd.DatetimeIndex(timestamps)\n\n\ndef _datetimelike_scalar_to_doy(time):\n    return pd.DatetimeIndex([pd.Timestamp(time)]).dayofyear\n\n\ndef _datetimelike_scalar_to_datetimeindex(time):\n    return pd.DatetimeIndex([pd.Timestamp(time)])\n\n\ndef _scalar_out(arg):\n    if np.isscalar(arg):\n        output = arg\n    else:  #\n        # works if it's a 1 length array and\n        # will throw a ValueError otherwise\n        output = np.asarray(arg).item()\n\n    return output\n\n\ndef _array_out(arg):\n    if isinstance(arg, pd.Series):\n        output = arg.values\n    else:\n        output = arg\n\n    return output\n\n\ndef _build_kwargs(keys, input_dict):\n    \"\"\"\n    Parameters\n    ----------\n    keys : iterable\n        Typically a list of strings.\n    input_dict : dict-like\n        A dictionary from which to attempt to pull each key.\n\n    Returns\n    -------\n    kwargs : dict\n        A dictionary with only the keys that were in input_dict\n    \"\"\"\n\n    kwargs = {}\n    for key in keys:\n        try:\n            kwargs[key] = input_dict[key]\n        except KeyError:\n            pass\n\n    return kwargs\n\n\ndef _build_args(keys, input_dict, dict_name):\n    \"\"\"\n    Parameters\n    ----------\n    keys : iterable\n        Typically a list of strings.\n    input_dict : dict-like\n        A dictionary from which to pull each key.\n    dict_name : str\n        A variable name to include in an error message for missing keys\n\n    Returns\n    -------\n    kwargs : list\n        A list with values corresponding to keys\n    \"\"\"\n    try:\n        args = [input_dict[key] for key in keys]\n    except KeyError as e:\n        missing_key = e.args[0]\n        msg = (f\"Missing required parameter '{missing_key}'. Found \"\n               f\"{input_dict} in {dict_name}.\")\n        raise KeyError(msg)\n    return args\n\n\n# Created April,2014\n# Author: Rob Andrews, Calama Consulting\n# Modified: November, 2020 by C. W. Hansen, to add atol and change exit\n# criteria\ndef _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8):\n    \"\"\"\n    Vectorized golden section search for finding maximum of a function of a\n    single variable.\n\n    Parameters\n    ----------\n    params : dict of numeric\n        Parameters to be passed to `func`. Each entry must be of the same\n        length.\n\n    lower: numeric\n        Lower bound for the optimization. Must be the same length as each\n        entry of params.\n\n    upper: numeric\n        Upper bound for the optimization. Must be the same length as each\n        entry of params.\n\n    func: function\n        Function to be optimized. Must be in the form\n        result = f(dict or DataFrame, str), where result is a dict or DataFrame\n        that also contains the function output, and str is the key\n        corresponding to the function's input variable.\n\n    Returns\n    -------\n    numeric\n        function evaluated at the optimal points\n\n    numeric\n        optimal points\n\n    Notes\n    -----\n    This function will find the points where the function is maximized.\n    Returns nan where lower or upper is nan, or where func evaluates to nan.\n\n    See also\n    --------\n    pvlib.singlediode._pwr_optfcn\n    \"\"\"\n    if np.any(upper - lower < 0.):\n        raise ValueError('upper >= lower is required')\n\n    phim1 = (np.sqrt(5) - 1) / 2\n\n    df = params\n    df['VH'] = upper\n    df['VL'] = lower\n\n    converged = False\n\n    while not converged:\n\n        phi = phim1 * (df['VH'] - df['VL'])\n        df['V1'] = df['VL'] + phi\n        df['V2'] = df['VH'] - phi\n\n        df['f1'] = func(df, 'V1')\n        df['f2'] = func(df, 'V2')\n        df['SW_Flag'] = df['f1'] > df['f2']\n\n        df['VL'] = df['V2']*df['SW_Flag'] + df['VL']*(~df['SW_Flag'])\n        df['VH'] = df['V1']*~df['SW_Flag'] + df['VH']*(df['SW_Flag'])\n\n        err = abs(df['V2'] - df['V1'])\n\n        # handle all NaN case gracefully\n        with warnings.catch_warnings():\n            warnings.filterwarnings(action='ignore',\n                                    message='All-NaN slice encountered')\n            converged = np.all(err[~np.isnan(err)] < atol)\n\n    # best estimate of location of maximum\n    df['max'] = 0.5 * (df['V1'] + df['V2'])\n    func_result = func(df, 'max')\n    x = np.where(np.isnan(func_result), np.nan, df['max'])\n\n    return func_result, x\n\n\ndef _get_sample_intervals(times, win_length):\n    \"\"\" Calculates time interval and samples per window for Reno-style clear\n    sky detection functions\n    \"\"\"\n    deltas = np.diff(times.values) / np.timedelta64(1, '60s')\n\n    # determine if we can proceed\n    if times.inferred_freq and len(np.unique(deltas)) == 1:\n        sample_interval = times[1] - times[0]\n        sample_interval = sample_interval.seconds / 60  # in minutes\n        samples_per_window = int(win_length / sample_interval)\n        return sample_interval, samples_per_window\n    else:\n        message = (\n            'algorithm does not yet support unequal time intervals. consider '\n            'resampling your data and checking for gaps from missing '\n            'periods, leap days, etc.'\n        )\n        raise NotImplementedError(message)\n\n\ndef _degrees_to_index(degrees, coordinate):\n    \"\"\"Transform input degrees to an output index integer.\n    Specify a degree value and either 'latitude' or 'longitude' to get\n    the appropriate index number for these two index numbers.\n    Parameters\n    ----------\n    degrees : float or int\n        Degrees of either latitude or longitude.\n    coordinate : string\n        Specify whether degrees arg is latitude or longitude. Must be set to\n        either 'latitude' or 'longitude' or an error will be raised.\n    Returns\n    -------\n    index : np.int16\n        The latitude or longitude index number to use when looking up values\n        in the Linke turbidity lookup table.\n    \"\"\"\n    # Assign inputmin, inputmax, and outputmax based on degree type.\n    if coordinate == 'latitude':\n        inputmin = 90\n        inputmax = -90\n        outputmax = 2160\n    elif coordinate == 'longitude':\n        inputmin = -180\n        inputmax = 180\n        outputmax = 4320\n    else:\n        raise IndexError(\"coordinate must be 'latitude' or 'longitude'.\")\n\n    inputrange = inputmax - inputmin\n    scale = outputmax/inputrange  # number of indices per degree\n    center = inputmin + 1 / scale / 2  # shift to center of index\n    outputmax -= 1  # shift index to zero indexing\n    index = (degrees - center) * scale\n    err = IndexError('Input, %g, is out of range (%g, %g).' %\n                     (degrees, inputmin, inputmax))\n\n    # If the index is still out of bounds after rounding, raise an error.\n    # 0.500001 is used in comparisons instead of 0.5 to allow for a small\n    # margin of error which can occur when dealing with floating point numbers.\n    if index > outputmax:\n        if index - outputmax <= 0.500001:\n            index = outputmax\n        else:\n            raise err\n    elif index < 0:\n        if -index <= 0.500001:\n            index = 0\n        else:\n            raise err\n    # If the index wasn't set to outputmax or 0, round it and cast it as an\n    # integer so it can be used in integer-based indexing.\n    else:\n        index = int(np.around(index))\n\n    return index\n","header":{"file":{"path":"pvlib/tools.py"},"misc_line":null},"index_line":null,"is_binary_file":false,"binary_line":null,"minus_file":{"path":"a/pvlib/tools.py"},"plus_file":{"path":"b/pvlib/tools.py"},"hunks":[{"descriptor":{"old_range":{"start":341,"length":6},"new_range":{"start":341,"length":8},"section":"def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8):"},"line_group":{"all_lines":[{"content":"    --------","type":"context"},{"content":"    pvlib.singlediode._pwr_optfcn","type":"context"},{"content":"    \"\"\"","type":"context"},{"content":"    if np.any(upper - lower < 0.):","type":"added"},{"content":"        raise ValueError('upper >= lower is required')","type":"added"},{"content":"","type":"context"},{"content":"    phim1 = (np.sqrt(5) - 1) / 2","type":"context"},{"content":"","type":"context"}]},"modified_entities":[],"added_entities":[],"deleted_entities":[]},{"descriptor":{"old_range":{"start":349,"length":16},"new_range":{"start":351,"length":8},"section":"def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8):"},"line_group":{"all_lines":[{"content":"    df['VL'] = lower","type":"context"},{"content":"","type":"context"},{"content":"    converged = False","type":"context"},{"content":"    iterations = 0","type":"deleted"},{"content":"","type":"context"},{"content":"    # handle all NaN case gracefully","type":"deleted"},{"content":"    with warnings.catch_warnings():","type":"deleted"},{"content":"        warnings.filterwarnings(action='ignore',","type":"deleted"},{"content":"                                message='All-NaN slice encountered')","type":"deleted"},{"content":"        iterlimit = 1 + np.nanmax(","type":"deleted"},{"content":"            np.trunc(np.log(atol / (df['VH'] - df['VL'])) / np.log(phim1)))","type":"deleted"},{"content":"","type":"deleted"},{"content":"    while not converged and (iterations <= iterlimit):","type":"deleted"},{"content":"    while not converged:","type":"added"},{"content":"","type":"context"},{"content":"        phi = phim1 * (df['VH'] - df['VL'])","type":"context"},{"content":"        df['V1'] = df['VL'] + phi","type":"context"}]},"modified_entities":[],"added_entities":[],"deleted_entities":[]},{"descriptor":{"old_range":{"start":373,"length":22},"new_range":{"start":367,"length":16},"section":"def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8):"},"line_group":{"all_lines":[{"content":"","type":"context"},{"content":"        err = abs(df['V2'] - df['V1'])","type":"context"},{"content":"","type":"context"},{"content":"        # works with single value because err is np.float64","type":"deleted"},{"content":"        converged = (err[~np.isnan(err)] < atol).all()","type":"deleted"},{"content":"        # err will be less than atol before iterations hit the limit","type":"deleted"},{"content":"        # but just to be safe","type":"deleted"},{"content":"        iterations += 1","type":"deleted"},{"content":"","type":"deleted"},{"content":"    if iterations > iterlimit:","type":"deleted"},{"content":"        raise Exception(\"Iterations exceeded maximum. Check that func\",","type":"deleted"},{"content":"                        \" is not NaN in (lower, upper)\")  # pragma: no cover","type":"deleted"},{"content":"        # handle all NaN case gracefully","type":"added"},{"content":"        with warnings.catch_warnings():","type":"added"},{"content":"            warnings.filterwarnings(action='ignore',","type":"added"},{"content":"                                    message='All-NaN slice encountered')","type":"added"},{"content":"            converged = np.all(err[~np.isnan(err)] < atol)","type":"added"},{"content":"","type":"context"},{"content":"    try:","type":"deleted"},{"content":"        func_result = func(df, 'V1')","type":"deleted"},{"content":"        x = np.where(np.isnan(func_result), np.nan, df['V1'])","type":"deleted"},{"content":"    except KeyError:","type":"deleted"},{"content":"        func_result = np.full_like(upper, np.nan)","type":"deleted"},{"content":"        x = func_result.copy()","type":"deleted"},{"content":"    # best estimate of location of maximum","type":"added"},{"content":"    df['max'] = 0.5 * (df['V1'] + df['V2'])","type":"added"},{"content":"    func_result = func(df, 'max')","type":"added"},{"content":"    x = np.where(np.isnan(func_result), np.nan, df['max'])","type":"added"},{"content":"","type":"context"},{"content":"    return func_result, x","type":"context"},{"content":"","type":"context"},{"content":"","type":"context"}]},"modified_entities":[],"added_entities":[],"deleted_entities":[]}]}],"old_commit_hash":"c78b50f4337ecbe536a961336ca91a1176efc0e8","new_commit_hash":"42f97867262a14677319085fcedde0fdf82ab0d2","commit_message":"remove try/except not needed","commit_date":"2022-12-09T10:30:12-07:00","metadata":{}}
problem_statement: golden-section search fails when upper and lower bounds are equal
  **Describe the bug**
  I was using pvlib for sometime now and until now I was always passing a big dataframe containing readings of a long period. Because of some changes in our software architecture, I need to pass the weather readings as a single reading (a dataframe with only one row) and I noticed that for readings that GHI-DHI are zero pvlib fails to calculate the output and returns below error while the same code executes correctly with weather information that has non-zero GHI-DHI:
  ```python
  import os
  import pathlib
  import time
  import json
  from datetime import datetime
  from time import mktime, gmtime
  
  import pandas as pd
  
  from pvlib import pvsystem
  from pvlib import location as pvlocation
  from pvlib import modelchain
  from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS as PARAMS # not used -- to remove
  from pvlib.bifacial.pvfactors import pvfactors_timeseries
  from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS
  
  class PV:
      def pv_transform_time(self, val):
          # tt = gmtime(val / 1000)
          tt = gmtime(val)
          dd = datetime.fromtimestamp(mktime(tt))
          timestamp = pd.Timestamp(dd)
          return timestamp
  
      def __init__(self, model: str, inverter: str, latitude: float, longitude: float, **kwargs):
          # super().__init__(**kwargs)
  
          temperature_model_parameters = TEMPERATURE_MODEL_PARAMETERS["sapm"][
              "open_rack_glass_glass"
          ]
          # Load the database of CEC module model parameters
          modules = pvsystem.retrieve_sam("cecmod")
          # Load the database of CEC inverter model parameters
          inverters = pvsystem.retrieve_sam("cecinverter")
  
  
          # A bare bone PV simulator
  
          # Load the database of CEC module model parameters
          modules = pvsystem.retrieve_sam('cecmod')
          inverters = pvsystem.retrieve_sam('cecinverter')
          module_parameters = modules[model]
          inverter_parameters = inverters[inverter]
  
          location = pvlocation.Location(latitude=latitude, longitude=longitude)
          system = pvsystem.PVSystem(module_parameters=module_parameters, inverter_parameters=inverter_parameters, temperature_model_parameters=temperature_model_parameters)
          self.modelchain = modelchain.ModelChain(system, location, aoi_model='no_loss', spectral_model="no_loss")
  
      def process(self, data):
          weather = pd.read_json(data)
          # print(f"raw_weather: {weather}")
          weather.drop('time.1', axis=1, inplace=True)
          weather['time'] = pd.to_datetime(weather['time']).map(datetime.timestamp) # --> this works for the new process_weather code and also the old weather file
          weather["time"] = weather["time"].apply(self.pv_transform_time)
          weather.index = weather["time"]
          # print(f"weather: {weather}")
          # print(weather.dtypes)
          # print(weather['ghi'][0])
          # print(type(weather['ghi'][0]))
  
          # simulate
          self.modelchain.run_model(weather)
          # print(self.modelchain.results.ac.to_frame().to_json())
          print(self.modelchain.results.ac)
  
  
  # good data
  good_data = "{\"time\":{\"12\":\"2010-01-01 13:30:00+00:00\"},\"ghi\":{\"12\":36},\"dhi\":{\"12\":36},\"dni\":{\"12\":0},\"Tamb\":{\"12\":8.0},\"WindVel\":{\"12\":5.0},\"WindDir\":{\"12\":270},\"time.1\":{\"12\":\"2010-01-01 13:30:00+00:00\"}}"
  
  # data that causes error
  data = "{\"time\":{\"4\":\"2010-01-01 05:30:00+00:00\"},\"ghi\":{\"4\":0},\"dhi\":{\"4\":0},\"dni\":{\"4\":0},\"Tamb\":{\"4\":8.0},\"WindVel\":{\"4\":4.0},\"WindDir\":{\"4\":240},\"time.1\":{\"4\":\"2010-01-01 05:30:00+00:00\"}}"
  p1 = PV(model="Trina_Solar_TSM_300DEG5C_07_II_", inverter="ABB__MICRO_0_25_I_OUTD_US_208__208V_", latitude=51.204483, longitude=5.265472)
  p1.process(good_data)
  print("=====")
  p1.process(data)
  ```
  Error:
  ```log
  $ python3 ./tmp-pv.py 
  time
  2010-01-01 13:30:00    7.825527
  dtype: float64
  =====
  /home/user/.local/lib/python3.10/site-packages/pvlib/tools.py:340: RuntimeWarning: divide by zero encountered in divide
    np.trunc(np.log(atol / (df['VH'] - df['VL'])) / np.log(phim1)))
  Traceback (most recent call last):
    File "/home/user/workspace/enorch/simulator/simulator_processor/src/pv/./tmp-pv.py", line 88, in <module>
      p1.process(data)
    File "/home/user/workspace/enorch/simulator/simulator_processor/src/pv/./tmp-pv.py", line 75, in process
      self.modelchain.run_model(weather)
    File "/home/user/.local/lib/python3.10/site-packages/pvlib/modelchain.py", line 1770, in run_model
      self._run_from_effective_irrad(weather)
    File "/home/user/.local/lib/python3.10/site-packages/pvlib/modelchain.py", line 1858, in _run_from_effective_irrad
      self.dc_model()
    File "/home/user/.local/lib/python3.10/site-packages/pvlib/modelchain.py", line 790, in cec
      return self._singlediode(self.system.calcparams_cec)
    File "/home/user/.local/lib/python3.10/site-packages/pvlib/modelchain.py", line 772, in _singlediode
      self.results.dc = tuple(itertools.starmap(
    File "/home/user/.local/lib/python3.10/site-packages/pvlib/pvsystem.py", line 931, in singlediode
      return singlediode(photocurrent, saturation_current,
    File "/home/user/.local/lib/python3.10/site-packages/pvlib/pvsystem.py", line 2826, in singlediode
      out = _singlediode._lambertw(
    File "/home/user/.local/lib/python3.10/site-packages/pvlib/singlediode.py", line 651, in _lambertw
      p_mp, v_mp = _golden_sect_DataFrame(params, 0., v_oc * 1.14,
    File "/home/user/.local/lib/python3.10/site-packages/pvlib/tools.py", line 364, in _golden_sect_DataFrame
      raise Exception("Iterations exceeded maximum. Check that func",
  Exception: ('Iterations exceeded maximum. Check that func', ' is not NaN in (lower, upper)')
  ```
  
  I have to mention that for now the workaround that I am using is to pass the weather data as a dataframe with two rows, the first row is a good weather data that pvlib can process and the second row is the incoming weather reading (I can also post that code if you want).
  
  **Expected behavior**
  PVlib should have consistent behavior and regardless of GHI-DHI readings.
  
  **Versions:**
  ```python
  >>> import pvlib
  >>> import pandas
  >>> pvlib.__version__
  '0.9.1'
  >>> pandas.__version__
  '1.4.3'
  ``` 
   - python: 3.10.6
  - OS: Ubuntu 22.04.1 LTS
  
patch: diff --git a/pvlib/tools.py b/pvlib/tools.py
  --- a/pvlib/tools.py
  +++ b/pvlib/tools.py
  @@ -341,6 +341,8 @@ def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8):
       --------
       pvlib.singlediode._pwr_optfcn
       """
  +    if np.any(upper - lower < 0.):
  +        raise ValueError('upper >= lower is required')
   
       phim1 = (np.sqrt(5) - 1) / 2
   
  @@ -349,16 +351,8 @@ def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8):
       df['VL'] = lower
   
       converged = False
  -    iterations = 0
   
  -    # 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):
  +    while not converged:
   
           phi = phim1 * (df['VH'] - df['VL'])
           df['V1'] = df['VL'] + phi
  @@ -373,22 +367,16 @@ 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[~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. Check that func",
  -                        " is not NaN in (lower, upper)")  # pragma: no cover
  +        # handle all NaN case gracefully
  +        with warnings.catch_warnings():
  +            warnings.filterwarnings(action='ignore',
  +                                    message='All-NaN slice encountered')
  +            converged = np.all(err[~np.isnan(err)] < atol)
   
  -    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()
  +    # best estimate of location of maximum
  +    df['max'] = 0.5 * (df['V1'] + df['V2'])
  +    func_result = func(df, 'max')
  +    x = np.where(np.isnan(func_result), np.nan, df['max'])
   
       return func_result, x
   
   
  
file_context: {"pvlib/tools.py": "\"\"\"\nCollection of functions used in pvlib_python\n\"\"\"\n\nimport datetime as dt\nimport numpy as np\nimport pandas as pd\nimport pytz\nimport warnings\n\n\ndef cosd(angle):\n    \"\"\"\n    Cosine with angle input in degrees\n\n    Parameters\n    ----------\n    angle : float or array-like\n        Angle in degrees\n\n    Returns\n    -------\n    result : float or array-like\n        Cosine of the angle\n    \"\"\"\n\n    res = np.cos(np.radians(angle))\n    return res\n\n\ndef sind(angle):\n    \"\"\"\n    Sine with angle input in degrees\n\n    Parameters\n    ----------\n    angle : float\n        Angle in degrees\n\n    Returns\n    -------\n    result : float\n        Sin of the angle\n    \"\"\"\n\n    res = np.sin(np.radians(angle))\n    return res\n\n\ndef tand(angle):\n    \"\"\"\n    Tan with angle input in degrees\n\n    Parameters\n    ----------\n    angle : float\n        Angle in degrees\n\n    Returns\n    -------\n    result : float\n        Tan of the angle\n    \"\"\"\n\n    res = np.tan(np.radians(angle))\n    return res\n\n\ndef asind(number):\n    \"\"\"\n    Inverse Sine returning an angle in degrees\n\n    Parameters\n    ----------\n    number : float\n        Input number\n\n    Returns\n    -------\n    result : float\n        arcsin result\n    \"\"\"\n\n    res = np.degrees(np.arcsin(number))\n    return res\n\n\ndef acosd(number):\n    \"\"\"\n    Inverse Cosine returning an angle in degrees\n\n    Parameters\n    ----------\n    number : float\n        Input number\n\n    Returns\n    -------\n    result : float\n        arccos result\n    \"\"\"\n\n    res = np.degrees(np.arccos(number))\n    return res\n\n\ndef localize_to_utc(time, location):\n    \"\"\"\n    Converts or localizes a time series to UTC.\n\n    Parameters\n    ----------\n    time : datetime.datetime, pandas.DatetimeIndex,\n           or pandas.Series/DataFrame with a DatetimeIndex.\n    location : pvlib.Location object\n\n    Returns\n    -------\n    pandas object localized to UTC.\n    \"\"\"\n    if isinstance(time, dt.datetime):\n        if time.tzinfo is None:\n            time = pytz.timezone(location.tz).localize(time)\n        time_utc = time.astimezone(pytz.utc)\n    else:\n        try:\n            time_utc = time.tz_convert('UTC')\n        except TypeError:\n            time_utc = time.tz_localize(location.tz).tz_convert('UTC')\n\n    return time_utc\n\n\ndef datetime_to_djd(time):\n    \"\"\"\n    Converts a datetime to the Dublin Julian Day\n\n    Parameters\n    ----------\n    time : datetime.datetime\n        time to convert\n\n    Returns\n    -------\n    float\n        fractional days since 12/31/1899+0000\n    \"\"\"\n\n    if time.tzinfo is None:\n        time_utc = pytz.utc.localize(time)\n    else:\n        time_utc = time.astimezone(pytz.utc)\n\n    djd_start = pytz.utc.localize(dt.datetime(1899, 12, 31, 12))\n    djd = (time_utc - djd_start).total_seconds() * 1.0/(60 * 60 * 24)\n\n    return djd\n\n\ndef djd_to_datetime(djd, tz='UTC'):\n    \"\"\"\n    Converts a Dublin Julian Day float to a datetime.datetime object\n\n    Parameters\n    ----------\n    djd : float\n        fractional days since 12/31/1899+0000\n    tz : str, default 'UTC'\n        timezone to localize the result to\n\n    Returns\n    -------\n    datetime.datetime\n       The resultant datetime localized to tz\n    \"\"\"\n\n    djd_start = pytz.utc.localize(dt.datetime(1899, 12, 31, 12))\n\n    utc_time = djd_start + dt.timedelta(days=djd)\n    return utc_time.astimezone(pytz.timezone(tz))\n\n\ndef _pandas_to_doy(pd_object):\n    \"\"\"\n    Finds the day of year for a pandas datetime-like object.\n\n    Useful for delayed evaluation of the dayofyear attribute.\n\n    Parameters\n    ----------\n    pd_object : DatetimeIndex or Timestamp\n\n    Returns\n    -------\n    dayofyear\n    \"\"\"\n    return pd_object.dayofyear\n\n\ndef _doy_to_datetimeindex(doy, epoch_year=2014):\n    \"\"\"\n    Convert a day of year scalar or array to a pd.DatetimeIndex.\n\n    Parameters\n    ----------\n    doy : numeric\n        Contains days of the year\n\n    Returns\n    -------\n    pd.DatetimeIndex\n    \"\"\"\n    doy = np.atleast_1d(doy).astype('float')\n    epoch = pd.Timestamp('{}-12-31'.format(epoch_year - 1))\n    timestamps = [epoch + dt.timedelta(days=adoy) for adoy in doy]\n    return pd.DatetimeIndex(timestamps)\n\n\ndef _datetimelike_scalar_to_doy(time):\n    return pd.DatetimeIndex([pd.Timestamp(time)]).dayofyear\n\n\ndef _datetimelike_scalar_to_datetimeindex(time):\n    return pd.DatetimeIndex([pd.Timestamp(time)])\n\n\ndef _scalar_out(arg):\n    if np.isscalar(arg):\n        output = arg\n    else:  #\n        # works if it's a 1 length array and\n        # will throw a ValueError otherwise\n        output = np.asarray(arg).item()\n\n    return output\n\n\ndef _array_out(arg):\n    if isinstance(arg, pd.Series):\n        output = arg.values\n    else:\n        output = arg\n\n    return output\n\n\ndef _build_kwargs(keys, input_dict):\n    \"\"\"\n    Parameters\n    ----------\n    keys : iterable\n        Typically a list of strings.\n    input_dict : dict-like\n        A dictionary from which to attempt to pull each key.\n\n    Returns\n    -------\n    kwargs : dict\n        A dictionary with only the keys that were in input_dict\n    \"\"\"\n\n    kwargs = {}\n    for key in keys:\n        try:\n            kwargs[key] = input_dict[key]\n        except KeyError:\n            pass\n\n    return kwargs\n\n\ndef _build_args(keys, input_dict, dict_name):\n    \"\"\"\n    Parameters\n    ----------\n    keys : iterable\n        Typically a list of strings.\n    input_dict : dict-like\n        A dictionary from which to pull each key.\n    dict_name : str\n        A variable name to include in an error message for missing keys\n\n    Returns\n    -------\n    kwargs : list\n        A list with values corresponding to keys\n    \"\"\"\n    try:\n        args = [input_dict[key] for key in keys]\n    except KeyError as e:\n        missing_key = e.args[0]\n        msg = (f\"Missing required parameter '{missing_key}'. Found \"\n               f\"{input_dict} in {dict_name}.\")\n        raise KeyError(msg)\n    return args\n\n\n# Created April,2014\n# Author: Rob Andrews, Calama Consulting\n# Modified: November, 2020 by C. W. Hansen, to add atol and change exit\n# criteria\ndef _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8):\n    \"\"\"\n    Vectorized golden section search for finding maximum of a function of a\n    single variable.\n\n    Parameters\n    ----------\n    params : dict of numeric\n        Parameters to be passed to `func`. Each entry must be of the same\n        length.\n\n    lower: numeric\n        Lower bound for the optimization. Must be the same length as each\n        entry of params.\n\n    upper: numeric\n        Upper bound for the optimization. Must be the same length as each\n        entry of params.\n\n    func: function\n        Function to be optimized. Must be in the form\n        result = f(dict or DataFrame, str), where result is a dict or DataFrame\n        that also contains the function output, and str is the key\n        corresponding to the function's input variable.\n\n    Returns\n    -------\n    numeric\n        function evaluated at the optimal points\n\n    numeric\n        optimal points\n\n    Notes\n    -----\n    This function will find the points where the function is maximized.\n    Returns nan where lower or upper is nan, or where func evaluates to nan.\n\n    See also\n    --------\n    pvlib.singlediode._pwr_optfcn\n    \"\"\"\n\n    phim1 = (np.sqrt(5) - 1) / 2\n\n    df = params\n    df['VH'] = upper\n    df['VL'] = lower\n\n    converged = False\n    iterations = 0\n\n    # handle all NaN case gracefully\n    with warnings.catch_warnings():\n        warnings.filterwarnings(action='ignore',\n                                message='All-NaN slice encountered')\n        iterlimit = 1 + np.nanmax(\n            np.trunc(np.log(atol / (df['VH'] - df['VL'])) / np.log(phim1)))\n\n    while not converged and (iterations <= iterlimit):\n\n        phi = phim1 * (df['VH'] - df['VL'])\n        df['V1'] = df['VL'] + phi\n        df['V2'] = df['VH'] - phi\n\n        df['f1'] = func(df, 'V1')\n        df['f2'] = func(df, 'V2')\n        df['SW_Flag'] = df['f1'] > df['f2']\n\n        df['VL'] = df['V2']*df['SW_Flag'] + df['VL']*(~df['SW_Flag'])\n        df['VH'] = df['V1']*~df['SW_Flag'] + df['VH']*(df['SW_Flag'])\n\n        err = abs(df['V2'] - df['V1'])\n\n        # works with single value because err is np.float64\n        converged = (err[~np.isnan(err)] < atol).all()\n        # err will be less than atol before iterations hit the limit\n        # but just to be safe\n        iterations += 1\n\n    if iterations > iterlimit:\n        raise Exception(\"Iterations exceeded maximum. Check that func\",\n                        \" is not NaN in (lower, upper)\")  # pragma: no cover\n\n    try:\n        func_result = func(df, 'V1')\n        x = np.where(np.isnan(func_result), np.nan, df['V1'])\n    except KeyError:\n        func_result = np.full_like(upper, np.nan)\n        x = func_result.copy()\n\n    return func_result, x\n\n\ndef _get_sample_intervals(times, win_length):\n    \"\"\" Calculates time interval and samples per window for Reno-style clear\n    sky detection functions\n    \"\"\"\n    deltas = np.diff(times.values) / np.timedelta64(1, '60s')\n\n    # determine if we can proceed\n    if times.inferred_freq and len(np.unique(deltas)) == 1:\n        sample_interval = times[1] - times[0]\n        sample_interval = sample_interval.seconds / 60  # in minutes\n        samples_per_window = int(win_length / sample_interval)\n        return sample_interval, samples_per_window\n    else:\n        message = (\n            'algorithm does not yet support unequal time intervals. consider '\n            'resampling your data and checking for gaps from missing '\n            'periods, leap days, etc.'\n        )\n        raise NotImplementedError(message)\n\n\ndef _degrees_to_index(degrees, coordinate):\n    \"\"\"Transform input degrees to an output index integer.\n    Specify a degree value and either 'latitude' or 'longitude' to get\n    the appropriate index number for these two index numbers.\n    Parameters\n    ----------\n    degrees : float or int\n        Degrees of either latitude or longitude.\n    coordinate : string\n        Specify whether degrees arg is latitude or longitude. Must be set to\n        either 'latitude' or 'longitude' or an error will be raised.\n    Returns\n    -------\n    index : np.int16\n        The latitude or longitude index number to use when looking up values\n        in the Linke turbidity lookup table.\n    \"\"\"\n    # Assign inputmin, inputmax, and outputmax based on degree type.\n    if coordinate == 'latitude':\n        inputmin = 90\n        inputmax = -90\n        outputmax = 2160\n    elif coordinate == 'longitude':\n        inputmin = -180\n        inputmax = 180\n        outputmax = 4320\n    else:\n        raise IndexError(\"coordinate must be 'latitude' or 'longitude'.\")\n\n    inputrange = inputmax - inputmin\n    scale = outputmax/inputrange  # number of indices per degree\n    center = inputmin + 1 / scale / 2  # shift to center of index\n    outputmax -= 1  # shift index to zero indexing\n    index = (degrees - center) * scale\n    err = IndexError('Input, %g, is out of range (%g, %g).' %\n                     (degrees, inputmin, inputmax))\n\n    # If the index is still out of bounds after rounding, raise an error.\n    # 0.500001 is used in comparisons instead of 0.5 to allow for a small\n    # margin of error which can occur when dealing with floating point numbers.\n    if index > outputmax:\n        if index - outputmax <= 0.500001:\n            index = outputmax\n        else:\n            raise err\n    elif index < 0:\n        if -index <= 0.500001:\n            index = 0\n        else:\n            raise err\n    # If the index wasn't set to outputmax or 0, round it and cast it as an\n    # integer so it can be used in integer-based indexing.\n    else:\n        index = int(np.around(index))\n\n    return index\n"}
filelevel_patches: {"pvlib/tools.py": "@@ -341,6 +341,8 @@ def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8):\n     --------\n     pvlib.singlediode._pwr_optfcn\n     \"\"\"\n+    if np.any(upper - lower < 0.):\n+        raise ValueError('upper >= lower is required')\n \n     phim1 = (np.sqrt(5) - 1) / 2\n@@ -349,16 +351,8 @@ def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8):\n     df['VL'] = lower\n \n     converged = False\n-    iterations = 0\n \n-    # handle all NaN case gracefully\n-    with warnings.catch_warnings():\n-        warnings.filterwarnings(action='ignore',\n-                                message='All-NaN slice encountered')\n-        iterlimit = 1 + np.nanmax(\n-            np.trunc(np.log(atol / (df['VH'] - df['VL'])) / np.log(phim1)))\n-\n-    while not converged and (iterations <= iterlimit):\n+    while not converged:\n \n         phi = phim1 * (df['VH'] - df['VL'])\n         df['V1'] = df['VL'] + phi\n@@ -373,22 +367,16 @@ def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8):\n \n         err = abs(df['V2'] - df['V1'])\n \n-        # works with single value because err is np.float64\n-        converged = (err[~np.isnan(err)] < atol).all()\n-        # err will be less than atol before iterations hit the limit\n-        # but just to be safe\n-        iterations += 1\n-\n-    if iterations > iterlimit:\n-        raise Exception(\"Iterations exceeded maximum. Check that func\",\n-                        \" is not NaN in (lower, upper)\")  # pragma: no cover\n+        # handle all NaN case gracefully\n+        with warnings.catch_warnings():\n+            warnings.filterwarnings(action='ignore',\n+                                    message='All-NaN slice encountered')\n+            converged = np.all(err[~np.isnan(err)] < atol)\n \n-    try:\n-        func_result = func(df, 'V1')\n-        x = np.where(np.isnan(func_result), np.nan, df['V1'])\n-    except KeyError:\n-        func_result = np.full_like(upper, np.nan)\n-        x = func_result.copy()\n+    # best estimate of location of maximum\n+    df['max'] = 0.5 * (df['V1'] + df['V2'])\n+    func_result = func(df, 'max')\n+    x = np.where(np.isnan(func_result), np.nan, df['max'])\n \n     return func_result, x"}
messages.content: 
  We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  golden-section search fails when upper and lower bounds are equal
  **Describe the bug**
  I was using pvlib for sometime now and until now I was always passing a big dataframe containing readings of a long period. Because of some changes in our software architecture, I need to pass the weather readings as a single reading (a dataframe with only one row) and I noticed that for readings that GHI-DHI are zero pvlib fails to calculate the output and returns below error while the same code executes correctly with weather information that has non-zero GHI-DHI:
  ```python
  import os
  import pathlib
  import time
  import json
  from datetime import datetime
  from time import mktime, gmtime
  
  import pandas as pd
  
  from pvlib import pvsystem
  from pvlib import location as pvlocation
  from pvlib import modelchain
  from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS as PARAMS # not used -- to remove
  from pvlib.bifacial.pvfactors import pvfactors_timeseries
  from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS
  
  class PV:
      def pv_transform_time(self, val):
          # tt = gmtime(val / 1000)
          tt = gmtime(val)
          dd = datetime.fromtimestamp(mktime(tt))
          timestamp = pd.Timestamp(dd)
          return timestamp
  
      def __init__(self, model: str, inverter: str, latitude: float, longitude: float, **kwargs):
          # super().__init__(**kwargs)
  
          temperature_model_parameters = TEMPERATURE_MODEL_PARAMETERS["sapm"][
              "open_rack_glass_glass"
          ]
          # Load the database of CEC module model parameters
          modules = pvsystem.retrieve_sam("cecmod")
          # Load the database of CEC inverter model parameters
          inverters = pvsystem.retrieve_sam("cecinverter")
  
  
          # A bare bone PV simulator
  
          # Load the database of CEC module model parameters
          modules = pvsystem.retrieve_sam('cecmod')
          inverters = pvsystem.retrieve_sam('cecinverter')
          module_parameters = modules[model]
          inverter_parameters = inverters[inverter]
  
          location = pvlocation.Location(latitude=latitude, longitude=longitude)
          system = pvsystem.PVSystem(module_parameters=module_parameters, inverter_parameters=inverter_parameters, temperature_model_parameters=temperature_model_parameters)
          self.modelchain = modelchain.ModelChain(system, location, aoi_model='no_loss', spectral_model="no_loss")
  
      def process(self, data):
          weather = pd.read_json(data)
          # print(f"raw_weather: {weather}")
          weather.drop('time.1', axis=1, inplace=True)
          weather['time'] = pd.to_datetime(weather['time']).map(datetime.timestamp) # --> this works for the new process_weather code and also the old weather file
          weather["time"] = weather["time"].apply(self.pv_transform_time)
          weather.index = weather["time"]
          # print(f"weather: {weather}")
          # print(weather.dtypes)
          # print(weather['ghi'][0])
          # print(type(weather['ghi'][0]))
  
          # simulate
          self.modelchain.run_model(weather)
          # print(self.modelchain.results.ac.to_frame().to_json())
          print(self.modelchain.results.ac)
  
  
  # good data
  good_data = "{\"time\":{\"12\":\"2010-01-01 13:30:00+00:00\"},\"ghi\":{\"12\":36},\"dhi\":{\"12\":36},\"dni\":{\"12\":0},\"Tamb\":{\"12\":8.0},\"WindVel\":{\"12\":5.0},\"WindDir\":{\"12\":270},\"time.1\":{\"12\":\"2010-01-01 13:30:00+00:00\"}}"
  
  # data that causes error
  data = "{\"time\":{\"4\":\"2010-01-01 05:30:00+00:00\"},\"ghi\":{\"4\":0},\"dhi\":{\"4\":0},\"dni\":{\"4\":0},\"Tamb\":{\"4\":8.0},\"WindVel\":{\"4\":4.0},\"WindDir\":{\"4\":240},\"time.1\":{\"4\":\"2010-01-01 05:30:00+00:00\"}}"
  p1 = PV(model="Trina_Solar_TSM_300DEG5C_07_II_", inverter="ABB__MICRO_0_25_I_OUTD_US_208__208V_", latitude=51.204483, longitude=5.265472)
  p1.process(good_data)
  print("=====")
  p1.process(data)
  ```
  Error:
  ```log
  $ python3 ./tmp-pv.py 
  time
  2010-01-01 13:30:00    7.825527
  dtype: float64
  =====
  /home/user/.local/lib/python3.10/site-packages/pvlib/tools.py:340: RuntimeWarning: divide by zero encountered in divide
    np.trunc(np.log(atol / (df['VH'] - df['VL'])) / np.log(phim1)))
  Traceback (most recent call last):
    File "/home/user/workspace/enorch/simulator/simulator_processor/src/pv/./tmp-pv.py", line 88, in <module>
      p1.process(data)
    File "/home/user/workspace/enorch/simulator/simulator_processor/src/pv/./tmp-pv.py", line 75, in process
      self.modelchain.run_model(weather)
    File "/home/user/.local/lib/python3.10/site-packages/pvlib/modelchain.py", line 1770, in run_model
      self._run_from_effective_irrad(weather)
    File "/home/user/.local/lib/python3.10/site-packages/pvlib/modelchain.py", line 1858, in _run_from_effective_irrad
      self.dc_model()
    File "/home/user/.local/lib/python3.10/site-packages/pvlib/modelchain.py", line 790, in cec
      return self._singlediode(self.system.calcparams_cec)
    File "/home/user/.local/lib/python3.10/site-packages/pvlib/modelchain.py", line 772, in _singlediode
      self.results.dc = tuple(itertools.starmap(
    File "/home/user/.local/lib/python3.10/site-packages/pvlib/pvsystem.py", line 931, in singlediode
      return singlediode(photocurrent, saturation_current,
    File "/home/user/.local/lib/python3.10/site-packages/pvlib/pvsystem.py", line 2826, in singlediode
      out = _singlediode._lambertw(
    File "/home/user/.local/lib/python3.10/site-packages/pvlib/singlediode.py", line 651, in _lambertw
      p_mp, v_mp = _golden_sect_DataFrame(params, 0., v_oc * 1.14,
    File "/home/user/.local/lib/python3.10/site-packages/pvlib/tools.py", line 364, in _golden_sect_DataFrame
      raise Exception("Iterations exceeded maximum. Check that func",
  Exception: ('Iterations exceeded maximum. Check that func', ' is not NaN in (lower, upper)')
  ```
  
  I have to mention that for now the workaround that I am using is to pass the weather data as a dataframe with two rows, the first row is a good weather data that pvlib can process and the second row is the incoming weather reading (I can also post that code if you want).
  
  **Expected behavior**
  PVlib should have consistent behavior and regardless of GHI-DHI readings.
  
  **Versions:**
  ```python
  >>> import pvlib
  >>> import pandas
  >>> pvlib.__version__
  '0.9.1'
  >>> pandas.__version__
  '1.4.3'
  ``` 
   - python: 3.10.6
  - OS: Ubuntu 22.04.1 LTS
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  
  --- BEGIN FILE ---
  ```
  ### pvlib/tools.py
  """
  Collection of functions used in pvlib_python
  """
  
  import datetime as dt
  import numpy as np
  import pandas as pd
  import pytz
  import warnings
  
  
  def cosd(angle):
      """
      Cosine with angle input in degrees
  
      Parameters
      ----------
      angle : float or array-like
          Angle in degrees
  
      Returns
      -------
      result : float or array-like
          Cosine of the angle
      """
  
      res = np.cos(np.radians(angle))
      return res
  
  
  def sind(angle):
      """
      Sine with angle input in degrees
  
      Parameters
      ----------
      angle : float
          Angle in degrees
  
      Returns
      -------
      result : float
          Sin of the angle
      """
  
      res = np.sin(np.radians(angle))
      return res
  
  
  def tand(angle):
      """
      Tan with angle input in degrees
  
      Parameters
      ----------
      angle : float
          Angle in degrees
  
      Returns
      -------
      result : float
          Tan of the angle
      """
  
      res = np.tan(np.radians(angle))
      return res
  
  
  def asind(number):
      """
      Inverse Sine returning an angle in degrees
  
      Parameters
      ----------
      number : float
          Input number
  
      Returns
      -------
      result : float
          arcsin result
      """
  
      res = np.degrees(np.arcsin(number))
      return res
  
  
  def acosd(number):
      """
      Inverse Cosine returning an angle in degrees
  
      Parameters
      ----------
      number : float
          Input number
  
      Returns
      -------
      result : float
          arccos result
      """
  
      res = np.degrees(np.arccos(number))
      return res
  
  
  def localize_to_utc(time, location):
      """
      Converts or localizes a time series to UTC.
  
      Parameters
      ----------
      time : datetime.datetime, pandas.DatetimeIndex,
             or pandas.Series/DataFrame with a DatetimeIndex.
      location : pvlib.Location object
  
      Returns
      -------
      pandas object localized to UTC.
      """
      if isinstance(time, dt.datetime):
          if time.tzinfo is None:
              time = pytz.timezone(location.tz).localize(time)
          time_utc = time.astimezone(pytz.utc)
      else:
          try:
              time_utc = time.tz_convert('UTC')
          except TypeError:
              time_utc = time.tz_localize(location.tz).tz_convert('UTC')
  
      return time_utc
  
  
  def datetime_to_djd(time):
      """
      Converts a datetime to the Dublin Julian Day
  
      Parameters
      ----------
      time : datetime.datetime
          time to convert
  
      Returns
      -------
      float
          fractional days since 12/31/1899+0000
      """
  
      if time.tzinfo is None:
          time_utc = pytz.utc.localize(time)
      else:
          time_utc = time.astimezone(pytz.utc)
  
      djd_start = pytz.utc.localize(dt.datetime(1899, 12, 31, 12))
      djd = (time_utc - djd_start).total_seconds() * 1.0/(60 * 60 * 24)
  
      return djd
  
  
  def djd_to_datetime(djd, tz='UTC'):
      """
      Converts a Dublin Julian Day float to a datetime.datetime object
  
      Parameters
      ----------
      djd : float
          fractional days since 12/31/1899+0000
      tz : str, default 'UTC'
          timezone to localize the result to
  
      Returns
      -------
      datetime.datetime
         The resultant datetime localized to tz
      """
  
      djd_start = pytz.utc.localize(dt.datetime(1899, 12, 31, 12))
  
      utc_time = djd_start + dt.timedelta(days=djd)
      return utc_time.astimezone(pytz.timezone(tz))
  
  
  def _pandas_to_doy(pd_object):
      """
      Finds the day of year for a pandas datetime-like object.
  
      Useful for delayed evaluation of the dayofyear attribute.
  
      Parameters
      ----------
      pd_object : DatetimeIndex or Timestamp
  
      Returns
      -------
      dayofyear
      """
      return pd_object.dayofyear
  
  
  def _doy_to_datetimeindex(doy, epoch_year=2014):
      """
      Convert a day of year scalar or array to a pd.DatetimeIndex.
  
      Parameters
      ----------
      doy : numeric
          Contains days of the year
  
      Returns
      -------
      pd.DatetimeIndex
      """
      doy = np.atleast_1d(doy).astype('float')
      epoch = pd.Timestamp('{}-12-31'.format(epoch_year - 1))
      timestamps = [epoch + dt.timedelta(days=adoy) for adoy in doy]
      return pd.DatetimeIndex(timestamps)
  
  
  def _datetimelike_scalar_to_doy(time):
      return pd.DatetimeIndex([pd.Timestamp(time)]).dayofyear
  
  
  def _datetimelike_scalar_to_datetimeindex(time):
      return pd.DatetimeIndex([pd.Timestamp(time)])
  
  
  def _scalar_out(arg):
      if np.isscalar(arg):
          output = arg
      else:  #
          # works if it's a 1 length array and
          # will throw a ValueError otherwise
          output = np.asarray(arg).item()
  
      return output
  
  
  def _array_out(arg):
      if isinstance(arg, pd.Series):
          output = arg.values
      else:
          output = arg
  
      return output
  
  
  def _build_kwargs(keys, input_dict):
      """
      Parameters
      ----------
      keys : iterable
          Typically a list of strings.
      input_dict : dict-like
          A dictionary from which to attempt to pull each key.
  
      Returns
      -------
      kwargs : dict
          A dictionary with only the keys that were in input_dict
      """
  
      kwargs = {}
      for key in keys:
          try:
              kwargs[key] = input_dict[key]
          except KeyError:
              pass
  
      return kwargs
  
  
  def _build_args(keys, input_dict, dict_name):
      """
      Parameters
      ----------
      keys : iterable
          Typically a list of strings.
      input_dict : dict-like
          A dictionary from which to pull each key.
      dict_name : str
          A variable name to include in an error message for missing keys
  
      Returns
      -------
      kwargs : list
          A list with values corresponding to keys
      """
      try:
          args = [input_dict[key] for key in keys]
      except KeyError as e:
          missing_key = e.args[0]
          msg = (f"Missing required parameter '{missing_key}'. Found "
                 f"{input_dict} in {dict_name}.")
          raise KeyError(msg)
      return args
  
  
  # Created April,2014
  # Author: Rob Andrews, Calama Consulting
  # Modified: November, 2020 by C. W. Hansen, to add atol and change exit
  # criteria
  def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8):
      """
      Vectorized golden section search for finding maximum of a function of a
      single variable.
  
      Parameters
      ----------
      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. Must be the same length as each
          entry of params.
  
      upper: numeric
          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
          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
      -------
      numeric
          function evaluated at the optimal points
  
      numeric
          optimal points
  
      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
      --------
      pvlib.singlediode._pwr_optfcn
      """
  
      phim1 = (np.sqrt(5) - 1) / 2
  
      df = params
      df['VH'] = upper
      df['VL'] = lower
  
      converged = False
      iterations = 0
  
      # 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
          df['V2'] = df['VH'] - phi
  
          df['f1'] = func(df, 'V1')
          df['f2'] = func(df, 'V2')
          df['SW_Flag'] = df['f1'] > df['f2']
  
          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 = abs(df['V2'] - df['V1'])
  
          # works with single value because err is np.float64
          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. 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_result, x
  
  
  def _get_sample_intervals(times, win_length):
      """ Calculates time interval and samples per window for Reno-style clear
      sky detection functions
      """
      deltas = np.diff(times.values) / np.timedelta64(1, '60s')
  
      # determine if we can proceed
      if times.inferred_freq and len(np.unique(deltas)) == 1:
          sample_interval = times[1] - times[0]
          sample_interval = sample_interval.seconds / 60  # in minutes
          samples_per_window = int(win_length / sample_interval)
          return sample_interval, samples_per_window
      else:
          message = (
              'algorithm does not yet support unequal time intervals. consider '
              'resampling your data and checking for gaps from missing '
              'periods, leap days, etc.'
          )
          raise NotImplementedError(message)
  
  
  def _degrees_to_index(degrees, coordinate):
      """Transform input degrees to an output index integer.
      Specify a degree value and either 'latitude' or 'longitude' to get
      the appropriate index number for these two index numbers.
      Parameters
      ----------
      degrees : float or int
          Degrees of either latitude or longitude.
      coordinate : string
          Specify whether degrees arg is latitude or longitude. Must be set to
          either 'latitude' or 'longitude' or an error will be raised.
      Returns
      -------
      index : np.int16
          The latitude or longitude index number to use when looking up values
          in the Linke turbidity lookup table.
      """
      # Assign inputmin, inputmax, and outputmax based on degree type.
      if coordinate == 'latitude':
          inputmin = 90
          inputmax = -90
          outputmax = 2160
      elif coordinate == 'longitude':
          inputmin = -180
          inputmax = 180
          outputmax = 4320
      else:
          raise IndexError("coordinate must be 'latitude' or 'longitude'.")
  
      inputrange = inputmax - inputmin
      scale = outputmax/inputrange  # number of indices per degree
      center = inputmin + 1 / scale / 2  # shift to center of index
      outputmax -= 1  # shift index to zero indexing
      index = (degrees - center) * scale
      err = IndexError('Input, %g, is out of range (%g, %g).' %
                       (degrees, inputmin, inputmax))
  
      # If the index is still out of bounds after rounding, raise an error.
      # 0.500001 is used in comparisons instead of 0.5 to allow for a small
      # margin of error which can occur when dealing with floating point numbers.
      if index > outputmax:
          if index - outputmax <= 0.500001:
              index = outputmax
          else:
              raise err
      elif index < 0:
          if -index <= 0.500001:
              index = 0
          else:
              raise err
      # If the index wasn't set to outputmax or 0, round it and cast it as an
      # integer so it can be used in integer-based indexing.
      else:
          index = int(np.around(index))
  
      return index
  
  ```
  --- END FILE ---
  
  Please first localize the bug based on the issue statement, and then generate *SEARCH/REPLACE* edits to fix the issue.
  
  Every *SEARCH/REPLACE* edit must use this format:
  1. The file path
  2. The start of search block: <<<<<<< SEARCH
  3. A contiguous chunk of lines to search for in the existing source code
  4. The dividing line: =======
  5. The lines to replace into the source code
  6. The end of the replace block: >>>>>>> REPLACE
  
  Here is an example:
  
  ```python
  ### mathweb/flask/app.py
  <<<<<<< SEARCH
  from flask import Flask
  =======
  import math
  from flask import Flask
  >>>>>>> REPLACE
  ```
  
  Please note that the *SEARCH/REPLACE* edit REQUIRES PROPER INDENTATION. If you would like to add the line '        print(x)', you must fully write that out, with all those spaces before the code!
  Wrap the *SEARCH/REPLACE* edit in blocks ```python...```.
  
messages.role: user
verification_info: {"file_context": {"pvlib/tools.py": "\"\"\"\nCollection of functions used in pvlib_python\n\"\"\"\n\nimport datetime as dt\nimport numpy as np\nimport pandas as pd\nimport pytz\nimport warnings\n\n\ndef cosd(angle):\n    \"\"\"\n    Cosine with angle input in degrees\n\n    Parameters\n    ----------\n    angle : float or array-like\n        Angle in degrees\n\n    Returns\n    -------\n    result : float or array-like\n        Cosine of the angle\n    \"\"\"\n\n    res = np.cos(np.radians(angle))\n    return res\n\n\ndef sind(angle):\n    \"\"\"\n    Sine with angle input in degrees\n\n    Parameters\n    ----------\n    angle : float\n        Angle in degrees\n\n    Returns\n    -------\n    result : float\n        Sin of the angle\n    \"\"\"\n\n    res = np.sin(np.radians(angle))\n    return res\n\n\ndef tand(angle):\n    \"\"\"\n    Tan with angle input in degrees\n\n    Parameters\n    ----------\n    angle : float\n        Angle in degrees\n\n    Returns\n    -------\n    result : float\n        Tan of the angle\n    \"\"\"\n\n    res = np.tan(np.radians(angle))\n    return res\n\n\ndef asind(number):\n    \"\"\"\n    Inverse Sine returning an angle in degrees\n\n    Parameters\n    ----------\n    number : float\n        Input number\n\n    Returns\n    -------\n    result : float\n        arcsin result\n    \"\"\"\n\n    res = np.degrees(np.arcsin(number))\n    return res\n\n\ndef acosd(number):\n    \"\"\"\n    Inverse Cosine returning an angle in degrees\n\n    Parameters\n    ----------\n    number : float\n        Input number\n\n    Returns\n    -------\n    result : float\n        arccos result\n    \"\"\"\n\n    res = np.degrees(np.arccos(number))\n    return res\n\n\ndef localize_to_utc(time, location):\n    \"\"\"\n    Converts or localizes a time series to UTC.\n\n    Parameters\n    ----------\n    time : datetime.datetime, pandas.DatetimeIndex,\n           or pandas.Series/DataFrame with a DatetimeIndex.\n    location : pvlib.Location object\n\n    Returns\n    -------\n    pandas object localized to UTC.\n    \"\"\"\n    if isinstance(time, dt.datetime):\n        if time.tzinfo is None:\n            time = pytz.timezone(location.tz).localize(time)\n        time_utc = time.astimezone(pytz.utc)\n    else:\n        try:\n            time_utc = time.tz_convert('UTC')\n        except TypeError:\n            time_utc = time.tz_localize(location.tz).tz_convert('UTC')\n\n    return time_utc\n\n\ndef datetime_to_djd(time):\n    \"\"\"\n    Converts a datetime to the Dublin Julian Day\n\n    Parameters\n    ----------\n    time : datetime.datetime\n        time to convert\n\n    Returns\n    -------\n    float\n        fractional days since 12/31/1899+0000\n    \"\"\"\n\n    if time.tzinfo is None:\n        time_utc = pytz.utc.localize(time)\n    else:\n        time_utc = time.astimezone(pytz.utc)\n\n    djd_start = pytz.utc.localize(dt.datetime(1899, 12, 31, 12))\n    djd = (time_utc - djd_start).total_seconds() * 1.0/(60 * 60 * 24)\n\n    return djd\n\n\ndef djd_to_datetime(djd, tz='UTC'):\n    \"\"\"\n    Converts a Dublin Julian Day float to a datetime.datetime object\n\n    Parameters\n    ----------\n    djd : float\n        fractional days since 12/31/1899+0000\n    tz : str, default 'UTC'\n        timezone to localize the result to\n\n    Returns\n    -------\n    datetime.datetime\n       The resultant datetime localized to tz\n    \"\"\"\n\n    djd_start = pytz.utc.localize(dt.datetime(1899, 12, 31, 12))\n\n    utc_time = djd_start + dt.timedelta(days=djd)\n    return utc_time.astimezone(pytz.timezone(tz))\n\n\ndef _pandas_to_doy(pd_object):\n    \"\"\"\n    Finds the day of year for a pandas datetime-like object.\n\n    Useful for delayed evaluation of the dayofyear attribute.\n\n    Parameters\n    ----------\n    pd_object : DatetimeIndex or Timestamp\n\n    Returns\n    -------\n    dayofyear\n    \"\"\"\n    return pd_object.dayofyear\n\n\ndef _doy_to_datetimeindex(doy, epoch_year=2014):\n    \"\"\"\n    Convert a day of year scalar or array to a pd.DatetimeIndex.\n\n    Parameters\n    ----------\n    doy : numeric\n        Contains days of the year\n\n    Returns\n    -------\n    pd.DatetimeIndex\n    \"\"\"\n    doy = np.atleast_1d(doy).astype('float')\n    epoch = pd.Timestamp('{}-12-31'.format(epoch_year - 1))\n    timestamps = [epoch + dt.timedelta(days=adoy) for adoy in doy]\n    return pd.DatetimeIndex(timestamps)\n\n\ndef _datetimelike_scalar_to_doy(time):\n    return pd.DatetimeIndex([pd.Timestamp(time)]).dayofyear\n\n\ndef _datetimelike_scalar_to_datetimeindex(time):\n    return pd.DatetimeIndex([pd.Timestamp(time)])\n\n\ndef _scalar_out(arg):\n    if np.isscalar(arg):\n        output = arg\n    else:  #\n        # works if it's a 1 length array and\n        # will throw a ValueError otherwise\n        output = np.asarray(arg).item()\n\n    return output\n\n\ndef _array_out(arg):\n    if isinstance(arg, pd.Series):\n        output = arg.values\n    else:\n        output = arg\n\n    return output\n\n\ndef _build_kwargs(keys, input_dict):\n    \"\"\"\n    Parameters\n    ----------\n    keys : iterable\n        Typically a list of strings.\n    input_dict : dict-like\n        A dictionary from which to attempt to pull each key.\n\n    Returns\n    -------\n    kwargs : dict\n        A dictionary with only the keys that were in input_dict\n    \"\"\"\n\n    kwargs = {}\n    for key in keys:\n        try:\n            kwargs[key] = input_dict[key]\n        except KeyError:\n            pass\n\n    return kwargs\n\n\ndef _build_args(keys, input_dict, dict_name):\n    \"\"\"\n    Parameters\n    ----------\n    keys : iterable\n        Typically a list of strings.\n    input_dict : dict-like\n        A dictionary from which to pull each key.\n    dict_name : str\n        A variable name to include in an error message for missing keys\n\n    Returns\n    -------\n    kwargs : list\n        A list with values corresponding to keys\n    \"\"\"\n    try:\n        args = [input_dict[key] for key in keys]\n    except KeyError as e:\n        missing_key = e.args[0]\n        msg = (f\"Missing required parameter '{missing_key}'. Found \"\n               f\"{input_dict} in {dict_name}.\")\n        raise KeyError(msg)\n    return args\n\n\n# Created April,2014\n# Author: Rob Andrews, Calama Consulting\n# Modified: November, 2020 by C. W. Hansen, to add atol and change exit\n# criteria\ndef _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8):\n    \"\"\"\n    Vectorized golden section search for finding maximum of a function of a\n    single variable.\n\n    Parameters\n    ----------\n    params : dict of numeric\n        Parameters to be passed to `func`. Each entry must be of the same\n        length.\n\n    lower: numeric\n        Lower bound for the optimization. Must be the same length as each\n        entry of params.\n\n    upper: numeric\n        Upper bound for the optimization. Must be the same length as each\n        entry of params.\n\n    func: function\n        Function to be optimized. Must be in the form\n        result = f(dict or DataFrame, str), where result is a dict or DataFrame\n        that also contains the function output, and str is the key\n        corresponding to the function's input variable.\n\n    Returns\n    -------\n    numeric\n        function evaluated at the optimal points\n\n    numeric\n        optimal points\n\n    Notes\n    -----\n    This function will find the points where the function is maximized.\n    Returns nan where lower or upper is nan, or where func evaluates to nan.\n\n    See also\n    --------\n    pvlib.singlediode._pwr_optfcn\n    \"\"\"\n\n    phim1 = (np.sqrt(5) - 1) / 2\n\n    df = params\n    df['VH'] = upper\n    df['VL'] = lower\n\n    converged = False\n    iterations = 0\n\n    # handle all NaN case gracefully\n    with warnings.catch_warnings():\n        warnings.filterwarnings(action='ignore',\n                                message='All-NaN slice encountered')\n        iterlimit = 1 + np.nanmax(\n            np.trunc(np.log(atol / (df['VH'] - df['VL'])) / np.log(phim1)))\n\n    while not converged and (iterations <= iterlimit):\n\n        phi = phim1 * (df['VH'] - df['VL'])\n        df['V1'] = df['VL'] + phi\n        df['V2'] = df['VH'] - phi\n\n        df['f1'] = func(df, 'V1')\n        df['f2'] = func(df, 'V2')\n        df['SW_Flag'] = df['f1'] > df['f2']\n\n        df['VL'] = df['V2']*df['SW_Flag'] + df['VL']*(~df['SW_Flag'])\n        df['VH'] = df['V1']*~df['SW_Flag'] + df['VH']*(df['SW_Flag'])\n\n        err = abs(df['V2'] - df['V1'])\n\n        # works with single value because err is np.float64\n        converged = (err[~np.isnan(err)] < atol).all()\n        # err will be less than atol before iterations hit the limit\n        # but just to be safe\n        iterations += 1\n\n    if iterations > iterlimit:\n        raise Exception(\"Iterations exceeded maximum. Check that func\",\n                        \" is not NaN in (lower, upper)\")  # pragma: no cover\n\n    try:\n        func_result = func(df, 'V1')\n        x = np.where(np.isnan(func_result), np.nan, df['V1'])\n    except KeyError:\n        func_result = np.full_like(upper, np.nan)\n        x = func_result.copy()\n\n    return func_result, x\n\n\ndef _get_sample_intervals(times, win_length):\n    \"\"\" Calculates time interval and samples per window for Reno-style clear\n    sky detection functions\n    \"\"\"\n    deltas = np.diff(times.values) / np.timedelta64(1, '60s')\n\n    # determine if we can proceed\n    if times.inferred_freq and len(np.unique(deltas)) == 1:\n        sample_interval = times[1] - times[0]\n        sample_interval = sample_interval.seconds / 60  # in minutes\n        samples_per_window = int(win_length / sample_interval)\n        return sample_interval, samples_per_window\n    else:\n        message = (\n            'algorithm does not yet support unequal time intervals. consider '\n            'resampling your data and checking for gaps from missing '\n            'periods, leap days, etc.'\n        )\n        raise NotImplementedError(message)\n\n\ndef _degrees_to_index(degrees, coordinate):\n    \"\"\"Transform input degrees to an output index integer.\n    Specify a degree value and either 'latitude' or 'longitude' to get\n    the appropriate index number for these two index numbers.\n    Parameters\n    ----------\n    degrees : float or int\n        Degrees of either latitude or longitude.\n    coordinate : string\n        Specify whether degrees arg is latitude or longitude. Must be set to\n        either 'latitude' or 'longitude' or an error will be raised.\n    Returns\n    -------\n    index : np.int16\n        The latitude or longitude index number to use when looking up values\n        in the Linke turbidity lookup table.\n    \"\"\"\n    # Assign inputmin, inputmax, and outputmax based on degree type.\n    if coordinate == 'latitude':\n        inputmin = 90\n        inputmax = -90\n        outputmax = 2160\n    elif coordinate == 'longitude':\n        inputmin = -180\n        inputmax = 180\n        outputmax = 4320\n    else:\n        raise IndexError(\"coordinate must be 'latitude' or 'longitude'.\")\n\n    inputrange = inputmax - inputmin\n    scale = outputmax/inputrange  # number of indices per degree\n    center = inputmin + 1 / scale / 2  # shift to center of index\n    outputmax -= 1  # shift index to zero indexing\n    index = (degrees - center) * scale\n    err = IndexError('Input, %g, is out of range (%g, %g).' %\n                     (degrees, inputmin, inputmax))\n\n    # If the index is still out of bounds after rounding, raise an error.\n    # 0.500001 is used in comparisons instead of 0.5 to allow for a small\n    # margin of error which can occur when dealing with floating point numbers.\n    if index > outputmax:\n        if index - outputmax <= 0.500001:\n            index = outputmax\n        else:\n            raise err\n    elif index < 0:\n        if -index <= 0.500001:\n            index = 0\n        else:\n            raise err\n    # If the index wasn't set to outputmax or 0, round it and cast it as an\n    # integer so it can be used in integer-based indexing.\n    else:\n        index = int(np.around(index))\n\n    return index\n"}, "filelevel_patches": {"pvlib/tools.py": "@@ -341,6 +341,8 @@ def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8):\n     --------\n     pvlib.singlediode._pwr_optfcn\n     \"\"\"\n+    if np.any(upper - lower < 0.):\n+        raise ValueError('upper >= lower is required')\n \n     phim1 = (np.sqrt(5) - 1) / 2\n@@ -349,16 +351,8 @@ def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8):\n     df['VL'] = lower\n \n     converged = False\n-    iterations = 0\n \n-    # handle all NaN case gracefully\n-    with warnings.catch_warnings():\n-        warnings.filterwarnings(action='ignore',\n-                                message='All-NaN slice encountered')\n-        iterlimit = 1 + np.nanmax(\n-            np.trunc(np.log(atol / (df['VH'] - df['VL'])) / np.log(phim1)))\n-\n-    while not converged and (iterations <= iterlimit):\n+    while not converged:\n \n         phi = phim1 * (df['VH'] - df['VL'])\n         df['V1'] = df['VL'] + phi\n@@ -373,22 +367,16 @@ def _golden_sect_DataFrame(params, lower, upper, func, atol=1e-8):\n \n         err = abs(df['V2'] - df['V1'])\n \n-        # works with single value because err is np.float64\n-        converged = (err[~np.isnan(err)] < atol).all()\n-        # err will be less than atol before iterations hit the limit\n-        # but just to be safe\n-        iterations += 1\n-\n-    if iterations > iterlimit:\n-        raise Exception(\"Iterations exceeded maximum. Check that func\",\n-                        \" is not NaN in (lower, upper)\")  # pragma: no cover\n+        # handle all NaN case gracefully\n+        with warnings.catch_warnings():\n+            warnings.filterwarnings(action='ignore',\n+                                    message='All-NaN slice encountered')\n+            converged = np.all(err[~np.isnan(err)] < atol)\n \n-    try:\n-        func_result = func(df, 'V1')\n-        x = np.where(np.isnan(func_result), np.nan, df['V1'])\n-    except KeyError:\n-        func_result = np.full_like(upper, np.nan)\n-        x = func_result.copy()\n+    # best estimate of location of maximum\n+    df['max'] = 0.5 * (df['V1'] + df['V2'])\n+    func_result = func(df, 'max')\n+    x = np.where(np.isnan(func_result), np.nan, df['max'])\n \n     return func_result, x"}}