Repository: pvlib/pvlib-python
Pull request #811 (2019-11-05 17:56:39)

Drop DataFrame as option for `module` input to pvsystem.sapm
 - [x] Closes issue #785
 - [x] I am familiar with the [contributing guidelines](http://pvlib-python.readthedocs.io/en/latest/contributing.html).
 - [x] Fully tested. Added and/or modified tests to ensure correct behavior for all reasonable inputs. Tests (usually) must pass on the TravisCI and Appveyor testing services.
 - [x] Updates entries to `docs/sphinx/source/api.rst` for API changes.
 - [x] Adds description and name entries in the appropriate `docs/sphinx/source/whatsnew` file for all changes.
 - [x] Code quality and style is sufficient. Passes LGTM and SticklerCI checks.
 - [x] New code is fully documented. Includes sphinx/numpydoc compliant docstrings and comments in the code where necessary.
 - [x] Pull request is nearly complete and ready for detailed review.

Brief description of the problem and proposed solution (if not already fully described in the issue linked to above):
Removes options of DataFrame from docstring, no changes to function.


Patch:
diff --git a/README.md b/README.md
index aa30cdb..6ce2d64 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
 [![TravisCI](https://travis-ci.org/pvlib/pvlib-python.svg?branch=master)](https://travis-ci.org/pvlib/pvlib-python)
 [![Coverage Status](https://img.shields.io/coveralls/pvlib/pvlib-python.svg)](https://coveralls.io/r/pvlib/pvlib-python)
 [![codecov](https://codecov.io/gh/pvlib/pvlib-python/branch/master/graph/badge.svg)](https://codecov.io/gh/pvlib/pvlib-python)
-[![Documentation Status](https://readthedocs.org/projects/pvlib-python/badge/?version=latest)](http://pvlib-python.readthedocs.org/en/latest/)
+[![Documentation Status](https://readthedocs.org/projects/pvlib-python/badge/?version=stable)](http://pvlib-python.readthedocs.org/en/stable/)
 [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.2554311.svg)](https://doi.org/10.5281/zenodo.2554311)
 [![status](http://joss.theoj.org/papers/41187535cad22dd4b076c89b72f874b1/status.svg)](http://joss.theoj.org/papers/41187535cad22dd4b076c89b72f874b1)
 [![Code Quality: Python](https://img.shields.io/lgtm/grade/python/g/pvlib/pvlib-python.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/pvlib/pvlib-python/context:python)
@@ -23,14 +23,14 @@ but operate independently of it.
 Documentation
 =============
 
-Full documentation can be found at [readthedocs](http://pvlib-python.readthedocs.io/en/latest/).
+Full documentation can be found at [readthedocs](http://pvlib-python.readthedocs.io/en/stable/).
 
 
 Installation
 ============
 
 pvlib-python releases may be installed using the ``pip`` and ``conda`` tools.
-Please see the [Installation page](http://pvlib-python.readthedocs.io/en/latest/installation.html) of the documentation for complete instructions.
+Please see the [Installation page](http://pvlib-python.readthedocs.io/en/stable/installation.html) of the documentation for complete instructions.
 
 pvlib-python is compatible with Python 3.5 and above.
 
@@ -41,7 +41,7 @@ Contributing
 ============
 
 We need your help to make pvlib-python a great tool!
-Please see the [Contributing page](http://pvlib-python.readthedocs.io/en/latest/contributing.html) for more on how you can contribute.
+Please see the [Contributing page](http://pvlib-python.readthedocs.io/en/stable/contributing.html) for more on how you can contribute.
 The long-term success of pvlib-python requires substantial community support.
 
 
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 6a23126..a626e1c 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -134,6 +134,43 @@ jobs:
       testRunTitle: 'Windows $(python.version)'
 
 
+- job: 'Test_bare_macOS_Mojave'
+
+  pool:
+    vmImage: 'macOS-10.14'
+  strategy:
+    matrix:
+      Python35:
+        python.version: '3.5'
+      Python36:
+        python.version: '3.6'
+      Python37:
+        python.version: '3.7'
+
+  steps:
+  - task: UsePythonVersion@0
+    inputs:
+      versionSpec: '$(python.version)'
+
+  - script: |
+      pip install pytest pytest-cov pytest-mock pytest-timeout pytest-azurepipelines
+      pip install -e .
+      pytest pvlib/test --junitxml=junit/test-results.xml --cov=pvlib --cov-report=xml --cov-report=html
+    displayName: 'Test with pytest'
+
+  - task: PublishTestResults@2
+    condition: succeededOrFailed()
+    inputs:
+      testResultsFiles: '**/test-*.xml'
+      testRunTitle: 'Publish test results for Python $(python.version)'
+
+  - task: PublishCodeCoverageResults@1
+    inputs:
+      codeCoverageTool: Cobertura
+      summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml'
+      reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov'
+
+
 - job: 'Publish'
   dependsOn: 'Test_conda_linux'
   pool:
@@ -146,4 +183,4 @@ jobs:
       architecture: 'x64'
 
   - script: python setup.py sdist
-    displayName: 'Build sdist'
\ No newline at end of file
+    displayName: 'Build sdist'
diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst
index c37bedf..7241f78 100644
--- a/docs/sphinx/source/api.rst
+++ b/docs/sphinx/source/api.rst
@@ -220,6 +220,7 @@ Incident angle modifiers
    iam.physical
    iam.ashrae
    iam.martin_ruiz
+   iam.martin_ruiz_diffuse
    iam.sapm
    iam.interp
 
@@ -303,6 +304,7 @@ Functions for fitting PV models
 
     ivtools.fit_sde_sandia
     ivtools.fit_sdm_cec_sam
+    ivtools.fit_sdm_desoto
 
 Other
 -----
diff --git a/docs/sphinx/source/whatsnew/v0.7.0.rst b/docs/sphinx/source/whatsnew/v0.7.0.rst
index 2b65b17..79eba40 100644
--- a/docs/sphinx/source/whatsnew/v0.7.0.rst
+++ b/docs/sphinx/source/whatsnew/v0.7.0.rst
@@ -1,4 +1,4 @@
-.. _whatsnew_0700:
+﻿.. _whatsnew_0700:
 
 v0.7.0 (MONTH DAY, YEAR)
 ------------------------
@@ -111,7 +111,11 @@ Enhancements
   the single diode equation to an IV curve.
 * Add :py:func:`~pvlib.ivtools.fit_sdm_cec_sam`, a wrapper for the CEC single
   diode model fitting function '6parsolve' from NREL's System Advisor Model.
+* Add :py:func:`~pvlib.ivtools.fit_sdm_desoto`, a method to fit the De Soto single
+  diode model to the typical specifications given in manufacturers datasheets.
 * Add `timeout` to :py:func:`pvlib.iotools.get_psm3`.
+* Created one new incidence angle modifier (IAM) function for diffuse irradiance:
+  :py:func:`pvlib.iam.martin_ruiz_diffuse`. (:issue:`751`)
 
 Bug fixes
 ~~~~~~~~~
@@ -138,6 +142,9 @@ Documentation
 ~~~~~~~~~~~~~
 * Corrected docstring for `pvsystem.PVSystem.sapm`
 * Fixed broken ipython examples from CEC data updates
+* Edited docstring for `pvsystem.sapm` to remove DataFrame option for input
+  `module`. The DataFrame option was never tested and would cause an error if
+  used. (:issue:`785`)
 
 Removal of prior version deprecations
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -162,4 +169,5 @@ Contributors
 * Anton Driesse (:ghuser:`adriesse`)
 * Alexander Morgan (:ghuser:`alexandermorgan`)
 * Miguel Sánchez de León Peque (:ghuser:`Peque`)
+* Tanguy Lunel (:ghuser:`tylunel`)
 * Veronica Guo (:ghuser:`veronicaguo`)
diff --git a/pvlib/clearsky.py b/pvlib/clearsky.py
index 073ee78..24e8d80 100644
--- a/pvlib/clearsky.py
+++ b/pvlib/clearsky.py
@@ -114,8 +114,10 @@ def ineichen(apparent_zenith, airmass_absolute, linke_turbidity,
     # reinsert tl nans
     ghi = cg1 * dni_extra * cos_zenith * tl / tl * np.fmax(ghi, 0)
 
-    # BncI = "normal beam clear sky radiation"
+    # From [1] (Following [2] leads to 0.664 + 0.16268 / fh1)
+    # See https://github.com/pvlib/pvlib-python/pull/808
     b = 0.664 + 0.163/fh1
+    # BncI = "normal beam clear sky radiation"
     bnci = b * np.exp(-0.09 * airmass_absolute * (tl - 1))
     bnci = dni_extra * np.fmax(bnci, 0)
 
diff --git a/pvlib/iam.py b/pvlib/iam.py
index 3d869f2..f2f968f 100644
--- a/pvlib/iam.py
+++ b/pvlib/iam.py
@@ -12,7 +12,6 @@ import numpy as np
 import pandas as pd
 from pvlib.tools import cosd, sind, tand, asind
 
-
 # a dict of required parameter names for each IAM model
 # keys are the function names for the IAM models
 IAM_MODEL_PARAMS = {
@@ -220,8 +219,8 @@ def martin_ruiz(aoi, a_r=0.16):
     -----
     `martin_ruiz` calculates the incidence angle modifier (IAM) as described in
     [1]. The information required is the incident angle (AOI) and the angular
-    losses coefficient (a_r). Note that [1] has a corrigendum [2] which makes
-    the document much simpler to understand.
+    losses coefficient (a_r). Note that [1] has a corrigendum [2] which
+    clarifies a mix-up of 'alpha's and 'a's in the former.
 
     The incident angle modifier is defined as
 
@@ -249,6 +248,7 @@ def martin_ruiz(aoi, a_r=0.16):
 
     See Also
     --------
+    iam.martin_ruiz_diffuse
     iam.physical
     iam.ashrae
     iam.interp
@@ -262,7 +262,7 @@ def martin_ruiz(aoi, a_r=0.16):
     a_r = np.asanyarray(a_r)
 
     if np.any(np.less_equal(a_r, 0)):
-        raise RuntimeError("The parameter 'a_r' cannot be zero or negative.")
+        raise ValueError("The parameter 'a_r' cannot be zero or negative.")
 
     with np.errstate(invalid='ignore'):
         iam = (1 - np.exp(-cosd(aoi) / a_r)) / (1 - np.exp(-1 / a_r))
@@ -274,6 +274,111 @@ def martin_ruiz(aoi, a_r=0.16):
     return iam
 
 
+def martin_ruiz_diffuse(surface_tilt, a_r=0.16, c1=0.4244, c2=None):
+    '''
+    Determine the incidence angle modifiers (iam) for diffuse sky and
+    ground-reflected irradiance using the Martin and Ruiz incident angle model.
+
+    Parameters
+    ----------
+    surface_tilt: float or array-like, default 0
+        Surface tilt angles in decimal degrees.
+        The tilt angle is defined as degrees from horizontal
+        (e.g. surface facing up = 0, surface facing horizon = 90)
+        surface_tilt must be in the range [0, 180]
+
+    a_r : numeric
+        The angular losses coefficient described in equation 3 of [1].
+        This is an empirical dimensionless parameter. Values of a_r are
+        generally on the order of 0.08 to 0.25 for flat-plate PV modules.
+        a_r must be greater than zero.
+
+    c1 : float
+        First fitting parameter for the expressions that approximate the
+        integral of diffuse irradiance coming from different directions.
+        c1 is given as the constant 4 / 3 / pi (0.4244) in [1].
+
+    c2 : float
+        Second fitting parameter for the expressions that approximate the
+        integral of diffuse irradiance coming from different directions.
+        If c2 is None, it will be calculated according to the linear
+        relationship given in [3].
+
+    Returns
+    -------
+    iam_sky : numeric
+        The incident angle modifier for sky diffuse
+
+    iam_ground : numeric
+        The incident angle modifier for ground-reflected diffuse
+
+    Notes
+    -----
+    Sky and ground modifiers are complementary: iam_sky for tilt = 30 is
+    equal to iam_ground for tilt = 180 - 30.  For vertical surfaces,
+    tilt = 90, the two factors are equal.
+
+    References
+    ----------
+    [1] N. Martin and J. M. Ruiz, "Calculation of the PV modules angular
+    losses under field conditions by means of an analytical model", Solar
+    Energy Materials & Solar Cells, vol. 70, pp. 25-38, 2001.
+
+    [2] N. Martin and J. M. Ruiz, "Corrigendum to 'Calculation of the PV
+    modules angular losses under field conditions by means of an
+    analytical model'", Solar Energy Materials & Solar Cells, vol. 110,
+    pp. 154, 2013.
+
+    [3] "IEC 61853-3 Photovoltaic (PV) module performance testing and energy
+    rating - Part 3: Energy rating of PV modules". IEC, Geneva, 2018.
+
+    See Also
+    --------
+    iam.martin_ruiz
+    iam.physical
+    iam.ashrae
+    iam.interp
+    iam.sapm
+    '''
+    # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Oct. 2019
+
+    if isinstance(surface_tilt, pd.Series):
+        out_index = surface_tilt.index
+    else:
+        out_index = None
+
+    surface_tilt = np.asanyarray(surface_tilt)
+
+    # avoid undefined results for horizontal or upside-down surfaces
+    zeroang = 1e-06
+
+    surface_tilt = np.where(surface_tilt == 0, zeroang, surface_tilt)
+    surface_tilt = np.where(surface_tilt == 180, 180 - zeroang, surface_tilt)
+
+    if c2 is None:
+        # This equation is from [3] Sect. 7.2
+        c2 = 0.5 * a_r - 0.154
+
+    beta = np.radians(surface_tilt)
+
+    from numpy import pi, sin, cos, exp
+
+    # because sin(pi) isn't exactly zero
+    sin_beta = np.where(surface_tilt < 90, sin(beta), sin(pi - beta))
+
+    trig_term_sky = sin_beta + (pi - beta - sin_beta) / (1 + cos(beta))
+    trig_term_gnd = sin_beta +      (beta - sin_beta) / (1 - cos(beta)) # noqa: E222 E261 E501
+
+    iam_sky = 1 - exp(-(c1 + c2 * trig_term_sky) * trig_term_sky / a_r)
+    iam_gnd = 1 - exp(-(c1 + c2 * trig_term_gnd) * trig_term_gnd / a_r)
+
+    if out_index is not None:
+        iam_sky = pd.Series(iam_sky, index=out_index, name='iam_sky')
+        iam_gnd = pd.Series(iam_gnd, index=out_index, name='iam_ground')
+
+    return iam_sky, iam_gnd
+
+
 def interp(aoi, theta_ref, iam_ref, method='linear', normalize=True):
     r'''
     Determine the incidence angle modifier (IAM) by interpolating a set of
diff --git a/pvlib/ivtools.py b/pvlib/ivtools.py
index 353a0b8..4806b2e 100644
--- a/pvlib/ivtools.py
+++ b/pvlib/ivtools.py
@@ -262,6 +262,147 @@ def fit_sde_sandia(voltage, current, v_oc=None, i_sc=None, v_mp_i_mp=None,
                                      v_oc)
 
 
+def fit_sdm_desoto(v_mp, i_mp, v_oc, i_sc, alpha_sc, beta_voc,
+                   cells_in_series, EgRef=1.121, dEgdT=-0.0002677,
+                   temp_ref=25, irrad_ref=1000, root_kwargs={}):
+    """
+    Calculates the parameters for the De Soto single diode model using the
+    procedure described in [1]. This procedure has the advantage of
+    using common specifications given by manufacturers in the
+    datasheets of PV modules.
+
+    The solution is found using the scipy.optimize.root() function,
+    with the corresponding default solver method 'hybr'.
+    No restriction is put on the fit variables, i.e. series
+    or shunt resistance could go negative. Nevertheless, if it happens,
+    check carefully the inputs and their units; alpha_sc and beta_voc are
+    often given in %/K in manufacturers datasheets and should be given
+    in A/K and V/K here.
+
+    The parameters returned by this function can be used by
+    pvsystem.calcparams_desoto to calculate the values at different
+    irradiance and cell temperature.
+
+    Parameters
+    ----------
+    v_mp: float
+        Module voltage at the maximum-power point at reference conditions [V].
+    i_mp: float
+        Module current at the maximum-power point at reference conditions [A].
+    v_oc: float
+        Open-circuit voltage at reference conditions [V].
+    i_sc: float
+        Short-circuit current at reference conditions [A].
+    alpha_sc: float
+        The short-circuit current (i_sc) temperature coefficient of the
+        module [A/K].
+    beta_voc: float
+        The open-circuit voltage (v_oc) temperature coefficient of the
+        module [V/K].
+    cells_in_series: integer
+        Number of cell in the module.
+    EgRef: float, default 1.121 eV - value for silicon
+        Energy of bandgap of semi-conductor used [eV]
+    dEgdT: float, default -0.0002677 - value for silicon
+        Variation of bandgap according to temperature [eV/K]
+    temp_ref: float, default 25
+        Reference temperature condition [C]
+    irrad_ref: float, default 1000
+        Reference irradiance condition [W/m2]
+    root_kwargs: dictionary, default None
+        Dictionary of arguments to pass onto scipy.optimize.root()
+
+    Returns
+    -------
+    Tuple of the following elements:
+
+        * Dictionary with the following elements:
+            I_L_ref: float
+                Light-generated current at reference conditions [A]
+            I_o_ref: float
+                Diode saturation current at reference conditions [A]
+            R_s: float
+                Series resistance [ohms]
+            R_sh_ref: float
+                Shunt resistance at reference conditions [ohms].
+            a_ref: float
+                Modified ideality factor at reference conditions.
+                The product of the usual diode ideality factor (n, unitless),
+                number of cells in series (Ns), and cell thermal voltage at
+                specified effective irradiance and cell temperature.
+            alpha_sc: float
+                The short-circuit current (i_sc) temperature coefficient of the
+                module [A/K].
+            EgRef: float
+                Energy of bandgap of semi-conductor used [eV]
+            dEgdT: float
+                Variation of bandgap according to temperature [eV/K]
+            irrad_ref: float
+                Reference irradiance condition [W/m2]
+            temp_ref: float
+                Reference temperature condition [C]
+        * scipy.optimize.OptimizeResult
+            Optimization result of scipy.optimize.root().
+            See scipy.optimize.OptimizeResult for more details.
+
+    References
+    ----------
+    [1] W. De Soto et al., "Improvement and validation of a model for
+    photovoltaic array performance", Solar Energy, vol 80, pp. 78-88,
+    2006.
+
+    [2] John A Dufﬁe, William A Beckman, "Solar Engineering of Thermal
+    Processes", Wiley, 2013
+    """
+
+    try:
+        from scipy.optimize import root
+        from scipy import constants
+    except ImportError:
+        raise ImportError("The fit_sdm_desoto function requires scipy.")
+
+    # Constants
+    k = constants.value('Boltzmann constant in eV/K')
+    Tref = temp_ref + 273.15  # [K]
+
+    # initial guesses of variables for computing convergence:
+    # Values are taken from [2], p753
+    Rsh_0 = 100.0
+    a_0 = 1.5*k*Tref*cells_in_series
+    IL_0 = i_sc
+    Io_0 = i_sc * np.exp(-v_oc/a_0)
+    Rs_0 = (a_0*np.log1p((IL_0-i_mp)/Io_0) - v_mp)/i_mp
+    # params_i : initial values vector
+    params_i = np.array([IL_0, Io_0, a_0, Rsh_0, Rs_0])
+
+    # specs of module
+    specs = (i_sc, v_oc, i_mp, v_mp, beta_voc, alpha_sc, EgRef, dEgdT,
+             Tref, k)
+
+    # computing with system of equations described in [1]
+    optimize_result = root(_system_of_equations_desoto, x0=params_i,
+                           args=(specs,), **root_kwargs)
+
+    if optimize_result.success:
+        sdm_params = optimize_result.x
+    else:
+        raise RuntimeError(
+            'Parameter estimation failed:\n' + optimize_result.message)
+
+    # results
+    return ({'I_L_ref': sdm_params[0],
+             'I_o_ref': sdm_params[1],
+             'a_ref': sdm_params[2],
+             'R_sh_ref': sdm_params[3],
+             'R_s': sdm_params[4],
+             'alpha_sc': alpha_sc,
+             'EgRef': EgRef,
+             'dEgdT': dEgdT,
+             'irrad_ref': irrad_ref,
+             'temp_ref': temp_ref},
+            optimize_result)
+
+
 def _find_mp(voltage, current):
     """
     Finds voltage and current at maximum power point.
@@ -348,3 +489,69 @@ def _calculate_sde_parameters(beta0, beta1, beta3, beta4, v_mp, i_mp, v_oc):
     else:  # I0_voc > 0
         I0 = I0_voc
     return (IL, I0, Rsh, Rs, nNsVth)
+
+
+def _system_of_equations_desoto(params, specs):
+    """Evaluates the systems of equations used to solve for the single
+    diode equation parameters. Function designed to be used by
+    scipy.optimize.root() in fit_sdm_desoto().
+
+    Parameters
+    ----------
+    params: ndarray
+        Array with parameters of the De Soto single diode model. Must be
+        given in the following order: IL, Io, a, Rsh, Rs
+    specs: tuple
+        Specifications of pv module given by manufacturer. Must be given
+        in the following order: Isc, Voc, Imp, Vmp, beta_oc, alpha_sc
+
+    Returns
+    -------
+    system of equations to solve with scipy.optimize.root().
+
+
+    References
+    ----------
+    [1] W. De Soto et al., "Improvement and validation of a model for
+    photovoltaic array performance", Solar Energy, vol 80, pp. 78-88,
+    2006.
+
+    [2] John A Dufﬁe, William A Beckman, "Solar Engineering of Thermal
+    Processes", Wiley, 2013
+    """
+
+    # six input known variables
+    Isc, Voc, Imp, Vmp, beta_oc, alpha_sc, EgRef, dEgdT, Tref, k = specs
+
+    # five parameters vector to find
+    IL, Io, a, Rsh, Rs = params
+
+    # five equation vector
+    y = [0, 0, 0, 0, 0]
+
+    # 1st equation - short-circuit - eq(3) in [1]
+    y[0] = Isc - IL + Io * np.expm1(Isc * Rs / a) + Isc * Rs / Rsh
+
+    # 2nd equation - open-circuit Tref - eq(4) in [1]
+    y[1] = -IL + Io * np.expm1(Voc / a) + Voc / Rsh
+
+    # 3rd equation - Imp & Vmp - eq(5) in [1]
+    y[2] = Imp - IL + Io * np.expm1((Vmp + Imp * Rs) / a) \
+        + (Vmp + Imp * Rs) / Rsh
+
+    # 4th equation - Pmp derivated=0 - eq23.2.6 in [2]
+    # caution: eq(6) in [1] has a sign error
+    y[3] = Imp \
+        - Vmp * ((Io / a) * np.exp((Vmp + Imp * Rs) / a) + 1.0 / Rsh) \
+        / (1.0 + (Io * Rs / a) * np.exp((Vmp + Imp * Rs) / a) + Rs / Rsh)
+
+    # 5th equation - open-circuit T2 - eq (4) at temperature T2 in [1]
+    T2 = Tref + 2
+    Voc2 = (T2 - Tref) * beta_oc + Voc  # eq (7) in [1]
+    a2 = a * T2 / Tref  # eq (8) in [1]
+    IL2 = IL + alpha_sc * (T2 - Tref)  # eq (11) in [1]
+    Eg2 = EgRef * (1 + dEgdT * (T2 - Tref))  # eq (10) in [1]
+    Io2 = Io * (T2 / Tref)**3 * np.exp(1 / k * (EgRef/Tref - Eg2/T2))  # eq (9)
+    y[4] = -IL2 + Io2 * np.expm1(Voc2 / a2) + Voc2 / Rsh  # eq (4) at T2
+
+    return y
diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py
index 16bd7ba..4786667 100644
--- a/pvlib/modelchain.py
+++ b/pvlib/modelchain.py
@@ -324,13 +324,15 @@ class ModelChain(object):
         # TODO: deprecated kwarg temp_model. Remove use of temp_model in v0.8
         temp_model = kwargs.pop('temp_model', None)
         if temp_model is not None:
-            warnings.warn('The temp_model keyword argument is deprecated. Use '
-                          'temperature_model instead', pvlibDeprecationWarning)
             if temperature_model is None:
+                warnings.warn('The temp_model keyword argument is deprecated.'
+                              ' Use temperature_model instead',
+                              pvlibDeprecationWarning)
                 temperature_model = temp_model
             elif temp_model == temperature_model:
                 warnings.warn('Provide only one of temperature_model or '
-                              'temp_model (deprecated).')
+                              'temp_model (deprecated).',
+                              pvlibDeprecationWarning)
             else:
                 raise ValueError(
                     'Conflicting temperature_model {} and temp_model {}. '
diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py
index feaf9c4..28d99f8 100644
--- a/pvlib/pvsystem.py
+++ b/pvlib/pvsystem.py
@@ -1586,8 +1586,8 @@ def sapm(effective_irradiance, temp_cell, module):
         The cell temperature (degrees C).
 
     module : dict-like
-        A dict, Series, or DataFrame defining the SAPM performance
-        parameters. See the notes section for more details.
+        A dict or Series defining the SAPM parameters. See the notes section
+        for more details.
 
     Returns
     -------
@@ -1605,11 +1605,11 @@ def sapm(effective_irradiance, temp_cell, module):
 
     Notes
     -----
-    The coefficients from SAPM which are required in ``module`` are
+    The SAPM parameters which are required in ``module`` are
     listed in the following table.
 
-    The modules in the Sandia module database contain these
-    coefficients, but the modules in the CEC module database do not.
+    The Sandia module database contains parameter values for a limited set
+    of modules. The CEC module database does not contain these parameters.
     Both databases can be accessed using :py:func:`retrieve_sam`.
 
     ================   ========================================================
diff --git a/pvlib/tools.py b/pvlib/tools.py
index cbd59a8..1f8c1b7 100644
--- a/pvlib/tools.py
+++ b/pvlib/tools.py
@@ -206,22 +206,22 @@ def _datetimelike_scalar_to_datetimeindex(time):
     return pd.DatetimeIndex([pd.Timestamp(time)])
 
 
-def _scalar_out(input):
-    if np.isscalar(input):
-        output = input
+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 = input.item()
+        output = np.asarray(arg).item()
 
     return output
 
 
-def _array_out(input):
-    if isinstance(input, pd.Series):
-        output = input.values
+def _array_out(arg):
+    if isinstance(arg, pd.Series):
+        output = arg.values
     else:
-        output = input
+        output = arg
 
     return output
 


Test patch:
diff --git a/pvlib/test/test_iam.py b/pvlib/test/test_iam.py
index 0c51b00..8e04ee2 100644
--- a/pvlib/test/test_iam.py
+++ b/pvlib/test/test_iam.py
@@ -77,6 +77,7 @@ def test_martin_ruiz():
     # will fail if default values change
     iam = _iam.martin_ruiz(aoi)
     assert_allclose(iam, expected)
+
     # will fail if parameter names change
     iam = _iam.martin_ruiz(aoi=aoi, a_r=a_r)
     assert_allclose(iam, expected)
@@ -99,11 +100,53 @@ def test_martin_ruiz():
     iam = _iam.martin_ruiz(aoi, a_r)
     assert_series_equal(iam, expected)
 
-    # check exception clause
-    with pytest.raises(RuntimeError):
+
+def test_martin_ruiz_exception():
+
+    with pytest.raises(ValueError):
         _iam.martin_ruiz(0.0, a_r=0.0)
 
 
+def test_martin_ruiz_diffuse():
+
+    surface_tilt = 30.
+    a_r = 0.16
+    expected = (0.9549735, 0.7944426)
+
+    # will fail if default values change
+    iam = _iam.martin_ruiz_diffuse(surface_tilt)
+    assert_allclose(iam, expected)
+
+    # will fail if parameter names change
+    iam = _iam.martin_ruiz_diffuse(surface_tilt=surface_tilt, a_r=a_r)
+    assert_allclose(iam, expected)
+
+    a_r = 0.18
+    surface_tilt = [0, 30, 90, 120, 180, np.nan, np.inf]
+    expected_sky = [0.9407678, 0.9452250, 0.9407678, 0.9055541, 0.0000000,
+                    np.nan, np.nan]
+    expected_gnd = [0.0000000, 0.7610849, 0.9407678, 0.9483508, 0.9407678,
+                    np.nan, np.nan]
+
+    # check various inputs as list
+    iam = _iam.martin_ruiz_diffuse(surface_tilt, a_r)
+    assert_allclose(iam[0], expected_sky, atol=1e-7, equal_nan=True)
+    assert_allclose(iam[1], expected_gnd, atol=1e-7, equal_nan=True)
+
+    # check various inputs as array
+    iam = _iam.martin_ruiz_diffuse(np.array(surface_tilt), a_r)
+    assert_allclose(iam[0], expected_sky, atol=1e-7, equal_nan=True)
+    assert_allclose(iam[1], expected_gnd, atol=1e-7, equal_nan=True)
+
+    # check various inputs as Series
+    surface_tilt = pd.Series(surface_tilt)
+    expected_sky = pd.Series(expected_sky, name='iam_sky')
+    expected_gnd = pd.Series(expected_gnd, name='iam_ground')
+    iam = _iam.martin_ruiz_diffuse(surface_tilt, a_r)
+    assert_series_equal(iam[0], expected_sky)
+    assert_series_equal(iam[1], expected_gnd)
+
+
 @requires_scipy
 def test_iam_interp():
 
diff --git a/pvlib/test/test_ivtools.py b/pvlib/test/test_ivtools.py
index d4aca05..6f9fdd6 100644
--- a/pvlib/test/test_ivtools.py
+++ b/pvlib/test/test_ivtools.py
@@ -102,6 +102,35 @@ def test_fit_sdm_cec_sam(get_cec_params_cansol_cs5p_220p):
                 cells_in_series=1, temp_ref=25)
 
 
+@requires_scipy
+def test_fit_sdm_desoto():
+    result, _ = ivtools.fit_sdm_desoto(v_mp=31.0, i_mp=8.71, v_oc=38.3,
+                                       i_sc=9.43, alpha_sc=0.005658,
+                                       beta_voc=-0.13788,
+                                       cells_in_series=60)
+    result_expected = {'I_L_ref': 9.45232,
+                       'I_o_ref': 3.22460e-10,
+                       'a_ref': 1.59128,
+                       'R_sh_ref': 125.798,
+                       'R_s': 0.297814,
+                       'alpha_sc': 0.005658,
+                       'EgRef': 1.121,
+                       'dEgdT': -0.0002677,
+                       'irrad_ref': 1000,
+                       'temp_ref': 25}
+    assert np.allclose(pd.Series(result), pd.Series(result_expected),
+                       rtol=1e-4)
+
+
+@requires_scipy
+def test_fit_sdm_desoto_failure():
+    with pytest.raises(RuntimeError) as exc:
+        ivtools.fit_sdm_desoto(v_mp=31.0, i_mp=8.71, v_oc=38.3, i_sc=9.43,
+                               alpha_sc=0.005658, beta_voc=-0.13788,
+                               cells_in_series=10)
+    assert ('Parameter estimation failed') in str(exc.value)
+
+
 @pytest.fixture
 def get_bad_iv_curves():
     # v1, i1 produces a bad value for I0_voc
diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py
index c4d7c7a..cafaa81 100644
--- a/pvlib/test/test_modelchain.py
+++ b/pvlib/test/test_modelchain.py
@@ -470,10 +470,10 @@ def test_infer_aoi_model(location, system_no_aoi, aoi_model):
 
 
 def test_infer_aoi_model_invalid(location, system_no_aoi):
-    with pytest.raises(ValueError) as excinfo:
+    exc_text = 'could not infer AOI model'
+    with pytest.raises(ValueError, match=exc_text):
         ModelChain(system_no_aoi, location, orientation_strategy='None',
                    spectral_model='no_loss')
-    assert 'could not infer AOI model' in str(excinfo.value)
 
 
 def constant_spectral_loss(mc):
@@ -593,29 +593,26 @@ def test_deprecated_08():
     module_parameters = {'R_sh_ref': 1, 'a_ref': 1, 'I_o_ref': 1,
                          'alpha_sc': 1, 'I_L_ref': 1, 'R_s': 1}
     # do not assign PVSystem.temperature_model_parameters
+    # leave out PVSystem.racking_model and PVSystem.module_type
     system = PVSystem(module_parameters=module_parameters)
-    with pytest.warns(pvlibDeprecationWarning):
-        ModelChain(system, location,
-                   dc_model='desoto',
-                   aoi_model='no_loss', spectral_model='no_loss',
-                   temp_model='sapm',
-                   ac_model='snlinverter')
-    system = PVSystem(module_parameters=module_parameters)
-    with pytest.warns(pvlibDeprecationWarning):
-        ModelChain(system, location,
-                   dc_model='desoto',
-                   aoi_model='no_loss', spectral_model='no_loss',
-                   temperature_model='sapm',
-                   temp_model='sapm',
-                   ac_model='snlinverter')
-    system = PVSystem(module_parameters=module_parameters)
-    with pytest.raises(ValueError):
-        ModelChain(system, location,
-                   dc_model='desoto',
-                   aoi_model='no_loss', spectral_model='no_loss',
-                   temperature_model='pvsyst',
-                   temp_model='sapm',
-                   ac_model='snlinverter')
+    # deprecated temp_model kwarg
+    warn_txt = 'temp_model keyword argument is deprecated'
+    with pytest.warns(pvlibDeprecationWarning, match=warn_txt):
+        ModelChain(system, location, dc_model='desoto', aoi_model='no_loss',
+                   spectral_model='no_loss', ac_model='snlinverter',
+                   temp_model='sapm')
+    # provide both temp_model and temperature_model kwargs
+    warn_txt = 'Provide only one of temperature_model'
+    with pytest.warns(pvlibDeprecationWarning, match=warn_txt):
+        ModelChain(system, location, dc_model='desoto', aoi_model='no_loss',
+                   spectral_model='no_loss', ac_model='snlinverter',
+                   temperature_model='sapm', temp_model='sapm')
+    # conflicting temp_model and temperature_model kwargs
+    exc_text = 'Conflicting temperature_model'
+    with pytest.raises(ValueError, match=exc_text):
+        ModelChain(system, location, dc_model='desoto', aoi_model='no_loss',
+                   spectral_model='no_loss', ac_model='snlinverter',
+                   temperature_model='pvsyst', temp_model='sapm')
 
 
 @requires_scipy
diff --git a/pvlib/test/test_psm3.py b/pvlib/test/test_psm3.py
index 43c4ca4..a594979 100644
--- a/pvlib/test/test_psm3.py
+++ b/pvlib/test/test_psm3.py
@@ -25,6 +25,7 @@ PVLIB_EMAIL = 'pvlib-admin@googlegroups.com'
 DEMO_KEY = 'DEMO_KEY'
 
 
+@pytest.mark.xfail(strict=True)
 @needs_pandas_0_22
 def test_get_psm3():
     """test get_psm3"""
diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py
index 8157c57..bc9f142 100644
--- a/pvlib/test/test_pvsystem.py
+++ b/pvlib/test/test_pvsystem.py
@@ -419,16 +419,20 @@ def test__infer_temperature_model_params():
     expected = temperature.TEMPERATURE_MODEL_PARAMETERS[
         'sapm']['open_rack_glass_polymer']
     assert expected == system._infer_temperature_model_params()
-    expected = temperature.TEMPERATURE_MODEL_PARAMETERS[
-        'pvsyst']['freestanding']
     system = pvsystem.PVSystem(module_parameters={},
                                racking_model='freestanding',
                                module_type='glass_polymer')
+    expected = temperature.TEMPERATURE_MODEL_PARAMETERS[
+        'pvsyst']['freestanding']
     assert expected == system._infer_temperature_model_params()
-    system = pvsystem.PVSystem(module_parameters={},
-                               racking_model='not_a_rack_model',
-                               module_type='glass_polymer')
-    assert {} == system._infer_temperature_model_params()
+
+
+def test__infer_temperature_model_params_deprec_warning():
+    warn_txt = "Reverting to deprecated default"
+    with pytest.warns(pvlibDeprecationWarning, match=warn_txt):
+        pvsystem.PVSystem(module_parameters={},
+                          racking_model='not_a_rack_model',
+                          module_type='glass_polymer')
 
 
 def test_calcparams_desoto(cec_module_params):