Repository: pvlib/pvlib-python
Pull request #1401 (2022-02-09 22:06:12)

Deprecate modelchain.basic_chain
 - ~[ ] Closes #xxxx~ 
 - [x] I am familiar with the [contributing guidelines](https://pvlib-python.readthedocs.io/en/latest/contributing.html)
 - [x] Tests added
 - [ ] Updates entries in [`docs/sphinx/source/reference`](https://github.com/pvlib/pvlib-python/blob/master/docs/sphinx/source/reference) for API changes.
 - [x] Adds description and name entries in the appropriate "what's new" file in [`docs/sphinx/source/whatsnew`](https://github.com/pvlib/pvlib-python/tree/master/docs/sphinx/source/whatsnew) for all changes. Includes link to the GitHub Issue with `` :issue:`num` `` or this Pull Request with `` :pull:`num` ``. Includes contributor name and/or GitHub username (link with `` :ghuser:`user` ``).
 - ~New code is fully documented. Includes [numpydoc](https://numpydoc.readthedocs.io/en/latest/format.html) compliant docstrings, examples, and comments where necessary.~
 - [x] Pull request is nearly complete and ready for detailed review.
 - [x] Maintainer: Appropriate GitHub Labels and Milestone are assigned to the Pull Request and linked Issue.

See https://github.com/pvlib/pvlib-python/discussions/1385#discussioncomment-2035493

Do we remove functions from the API listing at deprecation or at removal?  SingleAxisTracker and some other deprecated things are currently listed, so I've not removed `basic_chain` here. 

mikofski: I think there is some Sphinx “deprecated” directive, and also like “new since vX.Y” — I think we should use that
cwhanse: > Do we remove functions from the API listing at deprecation or at removal? 

I favor removal at removal. If a user gets a deprecation message, they may want to look at the old documentation as part of their migration work. Removing it from the API listing makes it a bit harder to find.

Patch:
diff --git a/benchmarks/asv.conf.json b/benchmarks/asv.conf.json
index d82365b..c7da42a 100644
--- a/benchmarks/asv.conf.json
+++ b/benchmarks/asv.conf.json
@@ -115,7 +115,7 @@
         // minimum supported versions
         {
             "python": "3.6",
-            "numpy": "1.12.0",
+            "numpy": "1.16.0",
             "pandas": "0.22.0",
             "scipy": "1.2.0",
             // Note: these don't have a minimum in setup.py
diff --git a/ci/requirements-py36-min.yml b/ci/requirements-py36-min.yml
index e605e53..495099c 100644
--- a/ci/requirements-py36-min.yml
+++ b/ci/requirements-py36-min.yml
@@ -15,7 +15,7 @@ dependencies:
     - pip:
         - dataclasses
         - h5py==3.1.0
-        - numpy==1.12.0
+        - numpy==1.16.0
         - pandas==0.22.0
         - scipy==1.2.0
         - pytest-rerunfailures # conda version is >3.6
diff --git a/ci/requirements-py36.yml b/ci/requirements-py36.yml
index 222ea92..295bedd 100644
--- a/ci/requirements-py36.yml
+++ b/ci/requirements-py36.yml
@@ -10,7 +10,7 @@ dependencies:
     - netcdf4
     - nose
     - numba
-    - numpy >= 1.12.0
+    - numpy >= 1.16.0
     - pandas >= 0.22.0
     - pip
     - pytest
diff --git a/ci/requirements-py37.yml b/ci/requirements-py37.yml
index 83db4b7..241cafe 100644
--- a/ci/requirements-py37.yml
+++ b/ci/requirements-py37.yml
@@ -10,7 +10,7 @@ dependencies:
     - netcdf4
     - nose
     - numba
-    - numpy >= 1.12.0
+    - numpy >= 1.16.0
     - pandas >= 0.22.0
     - pip
     - pytest
diff --git a/ci/requirements-py38.yml b/ci/requirements-py38.yml
index 8fecb52..6ff5d2d 100644
--- a/ci/requirements-py38.yml
+++ b/ci/requirements-py38.yml
@@ -10,7 +10,7 @@ dependencies:
     - netcdf4
     - nose
     - numba
-    - numpy >= 1.12.0
+    - numpy >= 1.16.0
     - pandas >= 0.22.0
     - pip
     - pytest
diff --git a/ci/requirements-py39.yml b/ci/requirements-py39.yml
index d1283b4..32abde0 100644
--- a/ci/requirements-py39.yml
+++ b/ci/requirements-py39.yml
@@ -10,7 +10,7 @@ dependencies:
     # - netcdf4  # pulls in a different version of numpy with ImportError
     - nose
     # - numba  # python 3.9 compat in early 2021
-    - numpy >= 1.12.0
+    - numpy >= 1.16.0
     - pandas >= 0.22.0
     - pip
     - pytest
diff --git a/docs/sphinx/source/conf.py b/docs/sphinx/source/conf.py
index 32314eb..9826781 100644
--- a/docs/sphinx/source/conf.py
+++ b/docs/sphinx/source/conf.py
@@ -159,6 +159,7 @@ html_theme_options = {
     "use_edit_page_button": True,
     "show_toc_level": 1,
     "footer_items": ["copyright", "sphinx-version", "sidebar-ethical-ads"],
+    "left_sidebar_end": [],
 }
 
 # Add any paths that contain custom themes here, relative to this directory.
@@ -199,9 +200,7 @@ html_static_path = ['_static']
 #html_use_smartypants = True
 
 # Custom sidebar templates, maps document names to template names.
-html_sidebars = {
-    "**": ["search-field", "sidebar-nav-bs"]  # "sidebar-ethical-ads"
-}
+# html_sidebars = {}
 
 # Additional templates that should be rendered to pages, maps page names to
 # template names.
diff --git a/docs/sphinx/source/contributing.rst b/docs/sphinx/source/contributing.rst
index f5182a2..3e224f6 100644
--- a/docs/sphinx/source/contributing.rst
+++ b/docs/sphinx/source/contributing.rst
@@ -150,13 +150,14 @@ a timely manner is to:
 
 #. Start by creating an issue. The issue should be well-defined and
    actionable.
-#. Ask the maintainers to tag the issue with the appropriate milestone.
+#. Ask the `maintainers <https://github.com/orgs/pvlib/people>`_ to tag
+   the issue with the appropriate milestone.
 #. Make a limited-scope pull request. It can be a lot of work to check all of
    the boxes in `pull request guidelines
    <https://github.com/pvlib/pvlib-python/blob/master/.github/PULL_REQUEST_TEMPLATE.md>`_,
    especially for pull requests with a lot of new primary code.
    See :ref:`pull-request-scope`.
-#. Tag pvlib community members or ``@pvlib/maintainer`` when the pull
+#. Tag pvlib community members or ``@pvlib`` when the pull
    request is ready for review. (see :ref:`pull-request-reviews`)
 
 
@@ -166,7 +167,7 @@ Pull request reviews
 --------------------
 
 The pvlib community and maintainers will review your pull request in a
-timely fashion. Please "ping" ``@pvlib/maintainer`` if it seems that
+timely fashion. Please "ping" ``@pvlib`` if it seems that
 your pull request has been forgotten at any point in the pull request
 process.
 
diff --git a/docs/sphinx/source/whatsnew/v0.9.1.rst b/docs/sphinx/source/whatsnew/v0.9.1.rst
index 5ff19a4..77b0956 100644
--- a/docs/sphinx/source/whatsnew/v0.9.1.rst
+++ b/docs/sphinx/source/whatsnew/v0.9.1.rst
@@ -8,6 +8,11 @@ Breaking changes
 
 Deprecations
 ~~~~~~~~~~~~
+* :py:func:`pvlib.modelchain.basic_chain` is deprecated.
+  See :py:meth:`pvlib.modelchain.ModelChain.with_pvwatts` and
+  :py:meth:`pvlib.modelchain.ModelChain.with_sapm` for alternative simplified
+  :py:class:`~pvlib.modelchain.ModelChain` interfaces, although note that the
+  inputs do not directly translate. (:pull:`1401`)
 
 Enhancements
 ~~~~~~~~~~~~
@@ -26,6 +31,10 @@ Bug fixes
   and ``surface_azimuth`` inputs caused an error (:issue:`1127`, :issue:`1332`, :pull:`1361`) 
 * Changed the metadata entry for the wind speed unit to "Wind Speed Units" in
   the PSM3 iotools function (:pull:`1375`)
+* Improved convergence when determining the maximum power point using
+  for :py:func:`pvlib.pvsystem.singlediode` with ``method='lambertw'``. Tolerance
+  is determined for the voltage at the maximum power point, and is improved
+  from 0.01 V to 1e-8 V. (:issue:`1087`, :pull:`1089`)
 
 Testing
 ~~~~~~~
@@ -40,6 +49,8 @@ Documentation
 
 Requirements
 ~~~~~~~~~~~~
+* numpy >= 1.16.0 is now required for all python versions. (:pull:`1400`)
+
 
 Contributors
 ~~~~~~~~~~~~
diff --git a/docs/tutorials/irradiance.ipynb b/docs/tutorials/irradiance.ipynb
index 5b1b9a8..39409c1 100644
--- a/docs/tutorials/irradiance.ipynb
+++ b/docs/tutorials/irradiance.ipynb
@@ -367,7 +367,7 @@
    "source": [
     "See the online documentation for clear sky modeling examples.\n",
     "\n",
-    "http://pvlib-python.readthedocs.io/en/latest/clearsky.html\n",
+    "http://pvlib-python.readthedocs.io/en/stable/user_guide/clearsky.html\n",
     "\n",
     "Here we only generate data for the functions below."
    ]
diff --git a/pvlib/iotools/midc.py b/pvlib/iotools/midc.py
index c113d87..063326b 100644
--- a/pvlib/iotools/midc.py
+++ b/pvlib/iotools/midc.py
@@ -187,8 +187,8 @@ def read_midc(filename, variable_map={}, raw_data=False, **kwargs):
              {'Global Horizontal [W/m^2]': 'ghi'}
 
     See the MIDC_VARIABLE_MAP for collection of mappings by site.
-    For a full list of pvlib variable names see the `Variable Style Rules
-    <https://pvlib-python.readthedocs.io/en/latest/variables_style_rules.html>`_.
+    For a full list of pvlib variable names see the
+    :ref:`variables_style_rules`.
 
     Be sure to check the units for the variables you will use on the
     `MIDC site <https://midcdmz.nrel.gov/>`_.
diff --git a/pvlib/iotools/psm3.py b/pvlib/iotools/psm3.py
index e98f45b..10ff919 100644
--- a/pvlib/iotools/psm3.py
+++ b/pvlib/iotools/psm3.py
@@ -262,7 +262,7 @@ def parse_psm3(fbuf):
     .. [1] `NREL National Solar Radiation Database (NSRDB)
        <https://nsrdb.nrel.gov/>`_
     .. [2] `Standard Time Series Data File Format
-       <https://rredc.nrel.gov/solar/old_data/nsrdb/2005-2012/wfcsv.pdf>`_
+       <https://web.archive.org/web/20170207203107/https://sam.nrel.gov/sites/default/files/content/documents/pdf/wfcsv.pdf>`_
     """
     # The first 2 lines of the response are headers with metadata
     metadata_fields = fbuf.readline().split(',')
@@ -331,7 +331,7 @@ def read_psm3(filename):
     .. [1] `NREL National Solar Radiation Database (NSRDB)
        <https://nsrdb.nrel.gov/>`_
     .. [2] `Standard Time Series Data File Format
-       <https://rredc.nrel.gov/solar/old_data/nsrdb/2005-2012/wfcsv.pdf>`_
+       <https://web.archive.org/web/20170207203107/https://sam.nrel.gov/sites/default/files/content/documents/pdf/wfcsv.pdf>`_
     """
     with open(str(filename), 'r') as fbuf:
         content = parse_psm3(fbuf)
diff --git a/pvlib/iotools/surfrad.py b/pvlib/iotools/surfrad.py
index c82b7fb..b819923 100644
--- a/pvlib/iotools/surfrad.py
+++ b/pvlib/iotools/surfrad.py
@@ -47,7 +47,7 @@ def read_surfrad(filename, map_variables=True):
         Filepath or url.
     map_variables: bool
         When true, renames columns of the Dataframe to pvlib variable names
-        where applicable. See variable SURFRAD_COLUMNS.
+        where applicable. See variable :const:`VARIABLE_MAP`.
 
     Returns
     -------
@@ -113,7 +113,7 @@ def read_surfrad(filename, map_variables=True):
     =======================  ======  ==========================================
 
     See README files located in the station directories in the SURFRAD
-    data archives[2]_ for details on SURFRAD daily data files.
+    data archives [2]_ for details on SURFRAD daily data files.
 
     References
     ----------
diff --git a/pvlib/ivtools/sdm.py b/pvlib/ivtools/sdm.py
index 04263c3..013e63f 100644
--- a/pvlib/ivtools/sdm.py
+++ b/pvlib/ivtools/sdm.py
@@ -979,7 +979,8 @@ def _filter_params(ee, isc, io, rs, rsh):
     negrs = rs < 0.
     badrs = np.logical_or(rs > rsh, np.isnan(rs))
     imagrs = ~(np.isreal(rs))
-    badio = np.logical_or(~(np.isreal(rs)), io <= 0)
+    badio = np.logical_or(np.logical_or(~(np.isreal(rs)), io <= 0),
+                          np.isnan(io))
     goodr = np.logical_and(~badrsh, ~imagrs)
     goodr = np.logical_and(goodr, ~negrs)
     goodr = np.logical_and(goodr, ~badrs)
diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py
index eb0db81..2798c39 100644
--- a/pvlib/modelchain.py
+++ b/pvlib/modelchain.py
@@ -21,6 +21,8 @@ from pvlib.pvsystem import _DC_MODEL_PARAMS
 from pvlib._deprecation import pvlibDeprecationWarning
 from pvlib.tools import _build_kwargs
 
+from pvlib._deprecation import deprecated
+
 # keys that are used to detect input data and assign data to appropriate
 # ModelChain attribute
 # for ModelChain.weather
@@ -62,6 +64,13 @@ SAPM_CONFIG = dict(
 )
 
 
+@deprecated(
+    since='0.9.1',
+    name='pvlib.modelchain.basic_chain',
+    alternative=('pvlib.modelchain.ModelChain.with_pvwatts'
+                 ' or pvlib.modelchain.ModelChain.with_sapm'),
+    addendum='Note that the with_xyz methods take different model parameters.'
+)
 def basic_chain(times, latitude, longitude,
                 surface_tilt, surface_azimuth,
                 module_parameters, temperature_model_parameters,
@@ -382,8 +391,7 @@ class ModelChain:
     appropriate model parameters. For example, if ``dc_model='pvwatts'``,
     then each ``Array.module_parameters`` must contain ``'pdc0'``.
 
-    See https://pvlib-python.readthedocs.io/en/stable/modelchain.html
-    for examples.
+    See :ref:`modelchaindoc` for examples.
 
     Parameters
     ----------
diff --git a/pvlib/tools.py b/pvlib/tools.py
index 94bd042..a36849f 100644
--- a/pvlib/tools.py
+++ b/pvlib/tools.py
@@ -277,51 +277,61 @@ def _build_args(keys, input_dict, dict_name):
 
 # Created April,2014
 # Author: Rob Andrews, Calama Consulting
-
-def _golden_sect_DataFrame(params, VL, VH, func):
+# 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 MPP from a dataframe
-    timeseries.
+    Vectorized golden section search for finding maximum of a function of a
+    single variable.
 
     Parameters
     ----------
-    params : dict
-        Dictionary containing scalars or arrays
-        of inputs to the function to be optimized.
-        Each row should represent an independent optimization.
+    params : dict or Dataframe
+        Parameters to be passed to `func`.
 
-    VL: float
-        Lower bound of the optimization
+    lower: numeric
+        Lower bound for the optimization
 
-    VH: float
-        Upper bound of the optimization
+    upper: numeric
+        Upper bound for the optimization
 
     func: function
-        Function to be optimized must be in the form f(array-like, x)
+        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
     -------
-    func(df,'V1') : DataFrame
-        function evaluated at the optimal point
+    numeric
+        function evaluated at the optimal points
 
-    df['V1']: Dataframe
-        Dataframe of optimal points
+    numeric
+        optimal points
 
     Notes
     -----
-    This function will find the MAXIMUM of a function
+    This function will find the points where the function is maximized.
+
+    See also
+    --------
+    pvlib.singlediode._pwr_optfcn
     """
 
+    phim1 = (np.sqrt(5) - 1) / 2
+
     df = params
-    df['VH'] = VH
-    df['VL'] = VL
+    df['VH'] = upper
+    df['VL'] = lower
 
-    errflag = True
+    converged = False
     iterations = 0
+    iterlimit = 1 + np.max(
+        np.trunc(np.log(atol / (df['VH'] - df['VL'])) / np.log(phim1)))
 
-    while errflag:
+    while not converged and (iterations < iterlimit):
 
-        phi = (np.sqrt(5)-1)/2*(df['VH']-df['VL'])
+        phi = phim1 * (df['VH'] - df['VL'])
         df['V1'] = df['VL'] + phi
         df['V2'] = df['VH'] - phi
 
@@ -332,16 +342,16 @@ def _golden_sect_DataFrame(params, VL, VH, func):
         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 = df['V1'] - df['V2']
-        try:
-            errflag = (abs(err) > .01).any()
-        except ValueError:
-            errflag = (abs(err) > .01)
+        err = abs(df['V2'] - df['V1'])
 
+        # works with single value because err is np.float64
+        converged = (err < atol).all()
+        # err will be less than atol before iterations hit the limit
+        # but just to be safe
         iterations += 1
 
-        if iterations > 50:
-            raise Exception("EXCEPTION:iterations exceeded maximum (50)")
+    if iterations > iterlimit:
+        raise Exception("iterations exceeded maximum")  # pragma: no cover
 
     return func(df, 'V1'), df['V1']
 
diff --git a/setup.py b/setup.py
index b2cbf48..5b92e0f 100755
--- a/setup.py
+++ b/setup.py
@@ -38,7 +38,7 @@ AUTHOR = 'pvlib python Developers'
 MAINTAINER_EMAIL = 'holmgren@email.arizona.edu'
 URL = 'https://github.com/pvlib/pvlib-python'
 
-INSTALL_REQUIRES = ['numpy >= 1.12.0',
+INSTALL_REQUIRES = ['numpy >= 1.16.0',
                     'pandas >= 0.22.0',
                     'pytz',
                     'requests',
@@ -57,8 +57,8 @@ EXTRAS_REQUIRE = {
                  'pvfactors', 'siphon', 'statsmodels',
                  'cftime >= 1.1.1'],
     'doc': ['ipython', 'matplotlib', 'sphinx == 3.1.2',
-            'pydata-sphinx-theme', 'sphinx-gallery', 'docutils == 0.15.2',
-            'pillow', 'netcdf4', 'siphon',
+            'pydata-sphinx-theme == 0.8.0', 'sphinx-gallery',
+            'docutils == 0.15.2', 'pillow', 'netcdf4', 'siphon',
             'sphinx-toggleprompt >= 0.0.5'],
     'test': TESTS_REQUIRE
 }


Test patch:
diff --git a/pvlib/tests/test_inverter.py b/pvlib/tests/test_inverter.py
index 9717a7b..4962d3e 100644
--- a/pvlib/tests/test_inverter.py
+++ b/pvlib/tests/test_inverter.py
@@ -106,16 +106,16 @@ def test_sandia_multi(cec_inverter_parameters):
     pdcs = idcs * vdcs
     pacs = inverter.sandia_multi((vdcs, vdcs), (pdcs, pdcs),
                                  cec_inverter_parameters)
-    assert_series_equal(pacs, pd.Series([-0.020000, 132.004308, 250.000000]))
+    assert_series_equal(pacs, pd.Series([-0.020000, 132.004278, 250.000000]))
     # with lists instead of tuples
     pacs = inverter.sandia_multi([vdcs, vdcs], [pdcs, pdcs],
                                  cec_inverter_parameters)
-    assert_series_equal(pacs, pd.Series([-0.020000, 132.004308, 250.000000]))
+    assert_series_equal(pacs, pd.Series([-0.020000, 132.004278, 250.000000]))
     # with arrays instead of tuples
     pacs = inverter.sandia_multi(np.array([vdcs, vdcs]),
                                  np.array([pdcs, pdcs]),
                                  cec_inverter_parameters)
-    assert_series_equal(pacs, pd.Series([-0.020000, 132.004308, 250.000000]))
+    assert_allclose(pacs, np.array([-0.020000, 132.004278, 250.000000]))
 
 
 def test_sandia_multi_length_error(cec_inverter_parameters):
@@ -184,10 +184,10 @@ def test_pvwatts_multi():
     # with Series
     pdc = pd.Series(pdc)
     out = inverter.pvwatts_multi((pdc, pdc), pdc0, 0.95)
-    assert_series_equal(expected, out)
+    assert_series_equal(pd.Series(expected), out)
     # with list instead of tuple
     out = inverter.pvwatts_multi([pdc, pdc], pdc0, 0.95)
-    assert_series_equal(expected, out)
+    assert_series_equal(pd.Series(expected), out)
 
 
 INVERTER_TEST_MEAS = DATA_DIR / 'inverter_fit_snl_meas.csv'
diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py
index 17887dc..f4a92ea 100644
--- a/pvlib/tests/test_modelchain.py
+++ b/pvlib/tests/test_modelchain.py
@@ -1799,10 +1799,11 @@ def test_basic_chain_alt_az(sam_data, cec_inverter_parameters,
     modules = sam_data['sandiamod']
     module_parameters = modules['Canadian_Solar_CS5P_220M___2009_']
     temp_model_params = sapm_temperature_cs5p_220m.copy()
-    dc, ac = modelchain.basic_chain(times, latitude, longitude,
-                                    surface_tilt, surface_azimuth,
-                                    module_parameters, temp_model_params,
-                                    cec_inverter_parameters)
+    with pytest.warns(pvlibDeprecationWarning, match='with_pvwatts'):
+        dc, ac = modelchain.basic_chain(times, latitude, longitude,
+                                        surface_tilt, surface_azimuth,
+                                        module_parameters, temp_model_params,
+                                        cec_inverter_parameters)
 
     expected = pd.Series(np.array([111.621405, -2.00000000e-02]),
                          index=times)
@@ -1821,21 +1822,23 @@ def test_basic_chain_altitude_pressure(sam_data, cec_inverter_parameters,
     modules = sam_data['sandiamod']
     module_parameters = modules['Canadian_Solar_CS5P_220M___2009_']
     temp_model_params = sapm_temperature_cs5p_220m.copy()
-    dc, ac = modelchain.basic_chain(times, latitude, longitude,
-                                    surface_tilt, surface_azimuth,
-                                    module_parameters, temp_model_params,
-                                    cec_inverter_parameters,
-                                    pressure=93194)
+    with pytest.warns(pvlibDeprecationWarning, match='with_pvwatts'):
+        dc, ac = modelchain.basic_chain(times, latitude, longitude,
+                                        surface_tilt, surface_azimuth,
+                                        module_parameters, temp_model_params,
+                                        cec_inverter_parameters,
+                                        pressure=93194)
 
     expected = pd.Series(np.array([113.190045, -2.00000000e-02]),
                          index=times)
     assert_series_equal(ac, expected)
 
-    dc, ac = modelchain.basic_chain(times, latitude, longitude,
-                                    surface_tilt, surface_azimuth,
-                                    module_parameters, temp_model_params,
-                                    cec_inverter_parameters,
-                                    altitude=altitude)
+    with pytest.warns(pvlibDeprecationWarning, match='with_pvwatts'):
+        dc, ac = modelchain.basic_chain(times, latitude, longitude,
+                                        surface_tilt, surface_azimuth,
+                                        module_parameters, temp_model_params,
+                                        cec_inverter_parameters,
+                                        altitude=altitude)
 
     expected = pd.Series(np.array([113.189814, -2.00000000e-02]),
                          index=times)
diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py
index 505016a..1141e49 100644
--- a/pvlib/tests/test_pvsystem.py
+++ b/pvlib/tests/test_pvsystem.py
@@ -1313,32 +1313,35 @@ def test_singlediode_array():
                               resistance_series, resistance_shunt, nNsVth,
                               method='lambertw')
 
-    expected = np.array([
-        0.        ,  0.54538398,  1.43273966,  2.36328163,  3.29255606,
-        4.23101358,  5.16177031,  6.09368251,  7.02197553,  7.96846051,
-        8.88220557])
-
-    assert_allclose(sd['i_mp'], expected, atol=0.01)
+    expected_i = np.array([
+        0., 0.54614798740338, 1.435026463529, 2.3621366610078, 3.2953968319952,
+        4.2303869378787, 5.1655276691892, 6.1000269648604, 7.0333996177802,
+        7.9653036915959, 8.8954716265647])
+    expected_v = np.array([
+        0., 7.0966259059555, 7.9961986643428, 8.2222496810656, 8.3255927555753,
+        8.3766915453915, 8.3988872440242, 8.4027948807891, 8.3941399580559,
+        8.3763655188855, 8.3517057522791])
+
+    assert_allclose(sd['i_mp'], expected_i, atol=1e-8)
+    assert_allclose(sd['v_mp'], expected_v, atol=1e-8)
 
     sd = pvsystem.singlediode(photocurrent, saturation_current,
                               resistance_series, resistance_shunt, nNsVth)
-
     expected = pvsystem.i_from_v(resistance_shunt, resistance_series, nNsVth,
                                  sd['v_mp'], saturation_current, photocurrent,
                                  method='lambertw')
-
-    assert_allclose(sd['i_mp'], expected, atol=0.01)
+    assert_allclose(sd['i_mp'], expected, atol=1e-8)
 
 
 def test_singlediode_floats():
-    out = pvsystem.singlediode(7, 6e-7, .1, 20, .5, method='lambertw')
-    expected = {'i_xx': 4.2498,
-                'i_mp': 6.1275,
-                'v_oc': 8.1063,
-                'p_mp': 38.1937,
-                'i_x': 6.7558,
-                'i_sc': 6.9651,
-                'v_mp': 6.2331,
+    out = pvsystem.singlediode(7., 6.e-7, .1, 20., .5, method='lambertw')
+    expected = {'i_xx': 4.264060478,
+                'i_mp': 6.136267360,
+                'v_oc': 8.106300147,
+                'p_mp': 38.19421055,
+                'i_x': 6.7558815684,
+                'i_sc': 6.965172322,
+                'v_mp': 6.224339375,
                 'i': None,
                 'v': None}
     assert isinstance(out, dict)
@@ -1346,23 +1349,26 @@ def test_singlediode_floats():
         if k in ['i', 'v']:
             assert v is None
         else:
-            assert_allclose(v, expected[k], atol=1e-3)
+            assert_allclose(v, expected[k], atol=1e-6)
 
 
 def test_singlediode_floats_ivcurve():
-    out = pvsystem.singlediode(7, 6e-7, .1, 20, .5, ivcurve_pnts=3, method='lambertw')
-    expected = {'i_xx': 4.2498,
-                'i_mp': 6.1275,
-                'v_oc': 8.1063,
-                'p_mp': 38.1937,
-                'i_x': 6.7558,
-                'i_sc': 6.9651,
-                'v_mp': 6.2331,
-                'i': np.array([6.965172e+00, 6.755882e+00, 2.575717e-14]),
-                'v': np.array([0., 4.05315, 8.1063])}
+    out = pvsystem.singlediode(7., 6e-7, .1, 20., .5, ivcurve_pnts=3,
+                               method='lambertw')
+    expected = {'i_xx': 4.264060478,
+                'i_mp': 6.136267360,
+                'v_oc': 8.106300147,
+                'p_mp': 38.19421055,
+                'i_x': 6.7558815684,
+                'i_sc': 6.965172322,
+                'v_mp': 6.224339375,
+                'i': np.array([
+                    6.965172322, 6.755881568, 2.664535259e-14]),
+                'v': np.array([
+                    0., 4.053150073, 8.106300147])}
     assert isinstance(out, dict)
     for k, v in out.items():
-        assert_allclose(v, expected[k], atol=1e-3)
+        assert_allclose(v, expected[k], atol=1e-6)
 
 
 def test_singlediode_series_ivcurve(cec_module_params):
@@ -1383,21 +1389,20 @@ def test_singlediode_series_ivcurve(cec_module_params):
     out = pvsystem.singlediode(IL, I0, Rs, Rsh, nNsVth, ivcurve_pnts=3,
                                method='lambertw')
 
-    expected = OrderedDict([('i_sc', array([0., 3.01054475, 6.00675648])),
-                            ('v_oc', array([0., 9.96886962, 10.29530483])),
-                            ('i_mp', array([0., 2.65191983, 5.28594672])),
-                            ('v_mp', array([0., 8.33392491, 8.4159707])),
-                            ('p_mp', array([0., 22.10090078, 44.48637274])),
-                            ('i_x', array([0., 2.88414114, 5.74622046])),
-                            ('i_xx', array([0., 2.04340914, 3.90007956])),
+    expected = OrderedDict([('i_sc', array([0., 3.01079860, 6.00726296])),
+                            ('v_oc', array([0., 9.96959733, 10.29603253])),
+                            ('i_mp', array([0., 2.656285960, 5.290525645])),
+                            ('v_mp', array([0., 8.321092255, 8.409413795])),
+                            ('p_mp', array([0., 22.10320053, 44.49021934])),
+                            ('i_x', array([0., 2.884132006, 5.746202281])),
+                            ('i_xx', array([0., 2.052691562, 3.909673879])),
                             ('v', array([[0., 0., 0.],
-                                         [0., 4.98443481, 9.96886962],
-                                         [0., 5.14765242, 10.29530483]])),
+                                         [0., 4.984798663, 9.969597327],
+                                         [0., 5.148016266, 10.29603253]])),
                             ('i', array([[0., 0., 0.],
-                                         [3.01079860e+00, 2.88414114e+00,
-                                          3.10862447e-14],
-                                         [6.00726296e+00, 5.74622046e+00,
-                                          0.00000000e+00]]))])
+                                         [3.0107985972, 2.8841320056, 0.],
+                                         [6.0072629615, 5.7462022810, 0.]]))])
+
 
     for k, v in out.items():
         assert_allclose(v, expected[k], atol=1e-2)
@@ -1414,7 +1419,7 @@ def test_singlediode_series_ivcurve(cec_module_params):
                                          method='lambertw').T
 
     for k, v in out.items():
-        assert_allclose(v, expected[k], atol=1e-2)
+        assert_allclose(v, expected[k], atol=1e-6)
 
 
 def test_scale_voltage_current_power():
diff --git a/pvlib/tests/test_tools.py b/pvlib/tests/test_tools.py
index 42eaedc..fa42fbf 100644
--- a/pvlib/tests/test_tools.py
+++ b/pvlib/tests/test_tools.py
@@ -1,6 +1,7 @@
 import pytest
 
 from pvlib import tools
+import numpy as np
 
 
 @pytest.mark.parametrize('keys, input_dict, expected', [
@@ -12,3 +13,35 @@ from pvlib import tools
 def test_build_kwargs(keys, input_dict, expected):
     kwargs = tools._build_kwargs(keys, input_dict)
     assert kwargs == expected
+
+
+def _obj_test_golden_sect(params, loc):
+    return params[loc] * (1. - params['c'] * params[loc]**params['n'])
+
+
+@pytest.mark.parametrize('params, lb, ub, expected, func', [
+    ({'c': 1., 'n': 1.}, 0., 1., 0.5, _obj_test_golden_sect),
+    ({'c': 1e6, 'n': 6.}, 0., 1., 0.07230200263994839, _obj_test_golden_sect),
+    ({'c': 0.2, 'n': 0.3}, 0., 100., 89.14332727531685, _obj_test_golden_sect)
+])
+def test__golden_sect_DataFrame(params, lb, ub, expected, func):
+    v, x = tools._golden_sect_DataFrame(params, lb, ub, func)
+    assert np.isclose(x, expected, atol=1e-8)
+
+
+def test__golden_sect_DataFrame_atol():
+    params = {'c': 0.2, 'n': 0.3}
+    expected = 89.14332727531685
+    v, x = tools._golden_sect_DataFrame(
+        params, 0., 100., _obj_test_golden_sect, atol=1e-12)
+    assert np.isclose(x, expected, atol=1e-12)
+
+
+def test__golden_sect_DataFrame_vector():
+    params = {'c': np.array([1., 2.]), 'n': np.array([1., 1.])}
+    lower = np.array([0., 0.001])
+    upper = np.array([1.1, 1.2])
+    expected = np.array([0.5, 0.25])
+    v, x = tools._golden_sect_DataFrame(params, lower, upper,
+                                        _obj_test_golden_sect)
+    assert np.allclose(x, expected, atol=1e-8)