Repository: pvlib/pvlib-python (python)

Issue:
critical error in singlediode p_mp calculation
Bad news. I discovered a major error in the singlediode p_mp calculation: given a time series input, the numerical solver exits if the solution for any single time is within the error tolerance, rather than requiring that the solution for all times be close enough. The error is in [this line](https://github.com/pvlib/pvlib-python/blob/5218c9d3dd2d5a4e410140de3180b7ef00e8b1ee/pvlib/pvsystem.py#L1728). 

Here's some sample code:

```python
photocurrent = np.linspace(0, 10, 11)
resistance_shunt = 16
resistance_series = 0.094
nNsVth = 0.473
saturation_current = 1.943e-09

sd = pvsystem.singlediode(photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth)

sd['i_mp']
array([ 0.        ,  0.59453544,  1.56145498,  2.54105091,  3.52353436,
        4.50658749,  5.4890739 ,  6.47016579,  7.44909413,  8.42504244,
        9.39708533])

# change .all() to .any()

pvsystem = reload(pvlib.pvsystem)

sd_any = pvsystem.singlediode(photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth)

sd_any['i_mp']
array([ 0.        ,  0.54538398,  1.43273966,  2.36328163,  3.29255606,
        4.23101358,  5.16177031,  6.09368251,  7.02197553,  7.96846051,
        8.88220557])

sd['i_mp']/sd_any['i_mp']
array([        nan,  1.09012266,  1.0898386 ,  1.07522137,  1.07015167,
        1.06513189,  1.06340917,  1.06178256,  1.06082599,  1.05729864,
        1.05796756])

sd['p_mp']/sd_any['p_mp']
array([        nan,  0.98250248,  0.92482996,  0.91242588,  0.91052337,
        0.91256532,  0.9165425 ,  0.92159883,  0.92731275,  0.93339723,
        0.93974898])
```

So the error here is on the order of 5 to 10%, but the results will depend on your exact input.

The blue dots on the IV curve below show the p_mp points in the old code. The black dots show the p_mp points in fixed code. These IV curves are generated using a fix for #83 that I'll make a PR for soon. They're also what led me to discovering the problem.

![ivcurves](https://cloud.githubusercontent.com/assets/4383303/17006598/cedaa96c-4e96-11e6-9e9a-64ed1377536c.png) 

On a related note, someone should put more effort into verifying that the matlab and python libraries give the same results. Not me.

Pull request:
Fix major error in singlediode p_mp calculation for array/Series input
Fixes #221. 

I'll merge tomorrow.

Patch:
diff --git a/docs/sphinx/source/package_overview.rst b/docs/sphinx/source/package_overview.rst
index c9e733d..35eb00a 100644
--- a/docs/sphinx/source/package_overview.rst
+++ b/docs/sphinx/source/package_overview.rst
@@ -105,10 +105,10 @@ to accomplish our system modeling goal:
         temps = pvlib.pvsystem.sapm_celltemp(total_irrad['poa_global'],
                                              wind_speed, temp_air)
         effective_irradiance = pvlib.pvsystem.sapm_effective_irradiance(
-            module, total_irrad['poa_direct'], total_irrad['poa_diffuse'],
-            am_abs, aoi)
-        dc = pvlib.pvsystem.sapm(module, effective_irradiance, temps['temp_cell'])
-        ac = pvlib.pvsystem.snlinverter(inverter, dc['v_mp'], dc['p_mp'])
+            total_irrad['poa_direct'], total_irrad['poa_diffuse'],
+            am_abs, aoi, module)
+        dc = pvlib.pvsystem.sapm(effective_irradiance, temps['temp_cell'], module)
+        ac = pvlib.pvsystem.snlinverter(dc['v_mp'], dc['p_mp'], inverter)
         annual_energy = ac.sum()
         energies[name] = annual_energy
 
diff --git a/docs/sphinx/source/whatsnew/v0.4.0.txt b/docs/sphinx/source/whatsnew/v0.4.0.txt
index 03eaa8a..c140fe7 100644
--- a/docs/sphinx/source/whatsnew/v0.4.0.txt
+++ b/docs/sphinx/source/whatsnew/v0.4.0.txt
@@ -19,6 +19,9 @@ API Changes
   in addition to arrays and Series. Furthermore, these functions no
   longer promote scalar or array input to Series output.
   Also applies to atmosphere.relativeairmass. (:issue:`201`, :issue:`214`)
+* Reorder the ashraeiam, physicaliam, and snlinverter arguments to put
+  the most variable arguments first. Adds default arguments for the IAM
+  functions. (:issue:`197`)
 * The irradiance.extraradiation function input/output type consistency
   across different methods has been dramatically improved.
   (:issue:`217`, :issue:`219`)
@@ -48,6 +51,12 @@ Enhancements
 Bug fixes
 ~~~~~~~~~
 
+* Fixed an error in pvsystem.singlediode's i_mp, v_mp, and p_mp
+  calculations when using array or Series input. The function wrongly
+  returned solutions when any single point is within the error tolerance,
+  rather than requiring that the solution for all points be within the
+  error tolerance. Results in test scenarios changed by 1-10%.
+  (:issue:`221`)
 * dirint function yielded the wrong results for non-sea-level pressures.
   Fixed. (:issue:`212`)
 * Fixed a bug in the day angle calculation used by the 'spencer' and 'asce'
diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py
index 161da93..a797301 100644
--- a/pvlib/modelchain.py
+++ b/pvlib/modelchain.py
@@ -170,13 +170,13 @@ def basic_chain(times, latitude, longitude,
                                    weather['temp_air'])
 
     effective_irradiance = pvsystem.sapm_effective_irradiance(
-        module_parameters, total_irrad['poa_direct'],
-        total_irrad['poa_diffuse'], airmass, aoi)
+        total_irrad['poa_direct'], total_irrad['poa_diffuse'], airmass, aoi,
+        module_parameters)
 
-    dc = pvsystem.sapm(module_parameters, effective_irradiance,
-                       temps['temp_cell'])
+    dc = pvsystem.sapm(effective_irradiance, temps['temp_cell'],
+                       module_parameters)
 
-    ac = pvsystem.snlinverter(inverter_parameters, dc['v_mp'], dc['p_mp'])
+    ac = pvsystem.snlinverter(dc['v_mp'], dc['p_mp'], inverter_parameters)
 
     return dc, ac
 
diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py
index 812c745..d9f8c8f 100644
--- a/pvlib/pvsystem.py
+++ b/pvlib/pvsystem.py
@@ -16,6 +16,7 @@ import numpy as np
 import pandas as pd
 
 from pvlib import tools
+from pvlib.tools import _build_kwargs
 from pvlib.location import Location
 from pvlib import irradiance, atmosphere
 
@@ -227,6 +228,8 @@ class PVSystem(object):
         ``self.module_parameters['b']``, ``aoi``,
         and the :py:func:`ashraeiam` function.
 
+        Uses default arguments if keys not in module_parameters.
+
         Parameters
         ----------
         aoi : numeric
@@ -237,30 +240,34 @@ class PVSystem(object):
         modifier : numeric
             The AOI modifier.
         """
-        b = self.module_parameters['b']
-        return ashraeiam(b, aoi)
+        kwargs = _build_kwargs(['b'], self.module_parameters)
+
+        return ashraeiam(aoi, **kwargs)
 
     def physicaliam(self, aoi):
         """
-        Determine the incidence angle modifier using
+        Determine the incidence angle modifier using ``aoi``,
         ``self.module_parameters['K']``,
         ``self.module_parameters['L']``,
         ``self.module_parameters['n']``,
-        ``aoi``, and the
+        and the
         :py:func:`physicaliam` function.
 
+        Uses default arguments if keys not in module_parameters.
+
         Parameters
         ----------
-        See pvsystem.physicaliam for details
+        aoi : numeric
+            The angle of incidence in degrees.
 
         Returns
         -------
-        See pvsystem.physicaliam for details
+        modifier : numeric
+            The AOI modifier.
         """
-        K = self.module_parameters['K']
-        L = self.module_parameters['L']
-        n = self.module_parameters['n']
-        return physicaliam(K, L, n, aoi)
+        kwargs = _build_kwargs(['K', 'L', 'n'], self.module_parameters)
+
+        return physicaliam(aoi, **kwargs)
 
     def calcparams_desoto(self, poa_global, temp_cell, **kwargs):
         """
@@ -319,7 +326,7 @@ class PVSystem(object):
         -------
         See pvsystem.sapm for details
         """
-        return sapm(self.module_parameters, effective_irradiance, temp_cell)
+        return sapm(effective_irradiance, temp_cell, self.module_parameters)
 
     def sapm_celltemp(self, irrad, wind, temp):
         """Uses :py:func:`sapm_celltemp` to calculate module and cell
@@ -351,7 +358,7 @@ class PVSystem(object):
         F1 : numeric
             The SAPM spectral loss coefficient.
         """
-        return sapm_spectral_loss(self.module_parameters, airmass_absolute)
+        return sapm_spectral_loss(airmass_absolute, self.module_parameters)
 
     def sapm_aoi_loss(self, aoi):
         """
@@ -368,7 +375,7 @@ class PVSystem(object):
         F2 : numeric
             The SAPM angle of incidence loss coefficient.
         """
-        return sapm_aoi_loss(self.module_parameters, aoi)
+        return sapm_aoi_loss(aoi, self.module_parameters)
 
     def sapm_effective_irradiance(self, poa_direct, poa_diffuse,
                                   airmass_absolute, aoi,
@@ -400,10 +407,9 @@ class PVSystem(object):
         effective_irradiance : numeric
             The SAPM effective irradiance.
         """
-        return sapm_effective_irradiance(self.module_parameters,
-                                         poa_direct, poa_diffuse,
-                                         airmass_absolute, aoi,
-                                         reference_irradiance)
+        return sapm_effective_irradiance(
+            poa_direct, poa_diffuse, airmass_absolute, aoi,
+            self.module_parameters, reference_irradiance=reference_irradiance)
 
     def singlediode(self, photocurrent, saturation_current,
                     resistance_series, resistance_shunt, nNsVth):
@@ -448,7 +454,7 @@ class PVSystem(object):
         -------
         See pvsystem.snlinverter for details
         """
-        return snlinverter(self.inverter_parameters, v_dc, p_dc)
+        return snlinverter(v_dc, p_dc, self.inverter_parameters)
 
     def scale_voltage_current_power(self, data):
         """
@@ -656,7 +662,7 @@ def systemdef(meta, surface_tilt, surface_azimuth, albedo, modules_per_string,
     return system
 
 
-def ashraeiam(b, aoi):
+def ashraeiam(aoi, b=0.05):
     '''
     Determine the incidence angle modifier using the ASHRAE transmission
     model.
@@ -671,27 +677,26 @@ def ashraeiam(b, aoi):
 
     Parameters
     ----------
+    aoi : numeric
+        The angle of incidence between the module normal vector and the
+        sun-beam vector in degrees.
+
     b : float
         A parameter to adjust the modifier as a function of angle of
         incidence. Typical values are on the order of 0.05 [3].
-    aoi : Series
-        The angle of incidence between the module normal vector and the
-        sun-beam vector in degrees.
 
     Returns
     -------
-    IAM : Series
-
+    IAM : numeric
         The incident angle modifier calculated as 1-b*(sec(aoi)-1) as
         described in [2,3].
 
-        Returns nan for all abs(aoi) >= 90 and for all IAM values
-        that would be less than 0.
+        Returns nan for all abs(aoi) >= 90 and for all IAM values that
+        would be less than 0.
 
     References
     ----------
-
-    [1] Souka A.F., Safwat H.H., "Determindation of the optimum
+    [1] Souka A.F., Safwat H.H., "Determination of the optimum
     orientations for the double exposure flat-plate collector and its
     reflections". Solar Energy vol .10, pp 170-174. 1966.
 
@@ -707,15 +712,18 @@ def ashraeiam(b, aoi):
     physicaliam
     '''
 
-    IAM = 1 - b*((1/np.cos(np.radians(aoi)) - 1))
+    iam = 1 - b*((1/np.cos(np.radians(aoi)) - 1))
+
+    iam = np.where(np.abs(aoi) >= 90, np.nan, iam)
+    iam = np.maximum(0, iam)
 
-    IAM[abs(aoi) >= 90] = np.nan
-    IAM[IAM < 0] = np.nan
+    if isinstance(iam, pd.Series):
+        iam = pd.Series(iam, index=aoi.index)
 
-    return IAM
+    return iam
 
 
-def physicaliam(K, L, n, aoi):
+def physicaliam(aoi, n=1.526, K=4., L=0.002):
     '''
     Determine the incidence angle modifier using refractive index,
     glazing thickness, and extinction coefficient
@@ -733,33 +741,33 @@ def physicaliam(K, L, n, aoi):
 
     Parameters
     ----------
-    K : float
+    aoi : numeric
+        The angle of incidence between the module normal vector and the
+        sun-beam vector in degrees.
+
+    n : numeric
+        The effective index of refraction (unitless). Reference [1]
+        indicates that a value of 1.526 is acceptable for glass. n must
+        be a numeric scalar or vector with all values >=0. If n is a
+        vector, it must be the same size as all other input vectors.
+
+    K : numeric
         The glazing extinction coefficient in units of 1/meters.
         Reference [1] indicates that a value of  4 is reasonable for
         "water white" glass. K must be a numeric scalar or vector with
         all values >=0. If K is a vector, it must be the same size as
         all other input vectors.
 
-    L : float
+    L : numeric
         The glazing thickness in units of meters. Reference [1]
         indicates that 0.002 meters (2 mm) is reasonable for most
         glass-covered PV panels. L must be a numeric scalar or vector
         with all values >=0. If L is a vector, it must be the same size
         as all other input vectors.
 
-    n : float
-        The effective index of refraction (unitless). Reference [1]
-        indicates that a value of 1.526 is acceptable for glass. n must
-        be a numeric scalar or vector with all values >=0. If n is a
-        vector, it must be the same size as all other input vectors.
-
-    aoi : Series
-        The angle of incidence between the module normal vector and the
-        sun-beam vector in degrees.
-
     Returns
     -------
-    IAM : float or Series
+    IAM : numeric
         The incident angle modifier as specified in eqns. 14-16 of [1].
         IAM is a column vector with the same number of elements as the
         largest input vector.
@@ -803,12 +811,14 @@ def physicaliam(K, L, n, aoi):
              ((tools.tand(thetar_deg0 - zeroang)) ** 2) /
              ((tools.tand(thetar_deg0 + zeroang)) ** 2))))))
 
-    IAM = tau / tau0
+    iam = tau / tau0
+
+    iam = np.where((np.abs(aoi) >= 90) | (iam < 0), np.nan, iam)
 
-    IAM[abs(aoi) >= 90] = np.nan
-    IAM[IAM < 0] = np.nan
+    if isinstance(aoi, pd.Series):
+        iam = pd.Series(iam, index=aoi.index)
 
-    return IAM
+    return iam
 
 
 def calcparams_desoto(poa_global, temp_cell, alpha_isc, module_parameters,
@@ -826,10 +836,10 @@ def calcparams_desoto(poa_global, temp_cell, alpha_isc, module_parameters,
 
     Parameters
     ----------
-    poa_global : float or Series
+    poa_global : numeric
         The irradiance (in W/m^2) absorbed by the module.
 
-    temp_cell : float or Series
+    temp_cell : numeric
         The average cell temperature of cells within a module in C.
 
     alpha_isc : float
@@ -869,7 +879,7 @@ def calcparams_desoto(poa_global, temp_cell, alpha_isc, module_parameters,
         condition (this may be useful if dEgdT is a function of
         temperature).
 
-    M : float or Series (optional, default=1)
+    M : numeric (optional, default=1)
         An optional airmass modifier, if omitted, M is given a value of
         1, which assumes absolute (pressure corrected) airmass = 1.5. In
         this code, M is equal to M/Mref as described in [1] (i.e. Mref
@@ -891,11 +901,11 @@ def calcparams_desoto(poa_global, temp_cell, alpha_isc, module_parameters,
     -------
     Tuple of the following results:
 
-    photocurrent : float or Series
+    photocurrent : numeric
         Light-generated current in amperes at irradiance=S and
         cell temperature=Tcell.
 
-    saturation_current : float or Series
+    saturation_current : numeric
         Diode saturation curent in amperes at irradiance
         S and cell temperature Tcell.
 
@@ -903,11 +913,11 @@ def calcparams_desoto(poa_global, temp_cell, alpha_isc, module_parameters,
         Series resistance in ohms at irradiance S and cell temperature
         Tcell.
 
-    resistance_shunt : float or Series
+    resistance_shunt : numeric
         Shunt resistance in ohms at irradiance S and cell temperature
         Tcell.
 
-    nNsVth : float or Series
+    nNsVth : numeric
         Modified diode ideality factor at irradiance S and cell
         temperature Tcell. Note that in source [1] nNsVth = a (equation
         2). nNsVth is the product of the usual diode ideality factor
@@ -1001,7 +1011,7 @@ def calcparams_desoto(poa_global, temp_cell, alpha_isc, module_parameters,
          Source: [4]
     '''
 
-    M = np.max(M, 0)
+    M = np.maximum(M, 0)
     a_ref = module_parameters['a_ref']
     IL_ref = module_parameters['I_L_ref']
     I0_ref = module_parameters['I_o_ref']
@@ -1151,7 +1161,7 @@ def _parse_raw_sam_df(csvdata):
     return df
 
 
-def sapm(module, effective_irradiance, temp_cell):
+def sapm(effective_irradiance, temp_cell, module):
     '''
     The Sandia PV Array Performance Model (SAPM) generates 5 points on a
     PV module's I-V curve (Voc, Isc, Ix, Ixx, Vmp/Imp) according to
@@ -1159,16 +1169,16 @@ def sapm(module, effective_irradiance, temp_cell):
 
     Parameters
     ----------
-    module : dict-like
-        A dict, Series, or DataFrame defining the SAPM performance
-        parameters. See the notes section for more details.
-
     effective_irradiance : numeric
         Effective irradiance (suns).
 
     temp_cell : numeric
         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.
+
     Returns
     -------
     A DataFrame with the columns:
@@ -1376,20 +1386,20 @@ def sapm_celltemp(poa_global, wind_speed, temp_air,
     return pd.DataFrame({'temp_cell': temp_cell, 'temp_module': temp_module})
 
 
-def sapm_spectral_loss(module, airmass_absolute):
+def sapm_spectral_loss(airmass_absolute, module):
     """
     Calculates the SAPM spectral loss coefficient, F1.
 
     Parameters
     ----------
+    airmass_absolute : numeric
+        Absolute airmass
+
     module : dict-like
         A dict, Series, or DataFrame defining the SAPM performance
         parameters. See the :py:func:`sapm` notes section for more
         details.
 
-    airmass_absolute : numeric
-        Absolute airmass
-
     Returns
     -------
     F1 : numeric
@@ -1413,21 +1423,21 @@ def sapm_spectral_loss(module, airmass_absolute):
     return spectral_loss
 
 
-def sapm_aoi_loss(module, aoi, upper=None):
+def sapm_aoi_loss(aoi, module, upper=None):
     """
     Calculates the SAPM angle of incidence loss coefficient, F2.
 
     Parameters
     ----------
+    aoi : numeric
+        Angle of incidence in degrees. Negative input angles will return
+        nan values.
+
     module : dict-like
         A dict, Series, or DataFrame defining the SAPM performance
         parameters. See the :py:func:`sapm` notes section for more
         details.
 
-    aoi : numeric
-        Angle of incidence in degrees. Negative input angles will return
-        nan values.
-
     upper : None or float
         Upper limit on the results.
 
@@ -1471,20 +1481,14 @@ def sapm_aoi_loss(module, aoi, upper=None):
     return aoi_loss
 
 
-def sapm_effective_irradiance(module, poa_direct, poa_diffuse,
-                              airmass_absolute, aoi,
-                              reference_irradiance=1000):
+def sapm_effective_irradiance(poa_direct, poa_diffuse, airmass_absolute, aoi,
+                              module, reference_irradiance=1000):
     """
     Calculates the SAPM effective irradiance using the SAPM spectral
     loss and SAPM angle of incidence loss functions.
 
     Parameters
     ----------
-    module : dict-like
-        A dict, Series, or DataFrame defining the SAPM performance
-        parameters. See the :py:func:`sapm` notes section for more
-        details.
-
     poa_direct : numeric
         The direct irradiance incident upon the module.
 
@@ -1497,6 +1501,11 @@ def sapm_effective_irradiance(module, poa_direct, poa_diffuse,
     aoi : numeric
         Angle of incidence in degrees.
 
+    module : dict-like
+        A dict, Series, or DataFrame defining the SAPM performance
+        parameters. See the :py:func:`sapm` notes section for more
+        details.
+
     reference_irradiance : numeric
         Reference irradiance by which to divide the input irradiance.
 
@@ -1506,8 +1515,8 @@ def sapm_effective_irradiance(module, poa_direct, poa_diffuse,
         The SAPM effective irradiance.
     """
 
-    F1 = sapm_spectral_loss(module, airmass_absolute)
-    F2 = sapm_aoi_loss(module, aoi)
+    F1 = sapm_spectral_loss(airmass_absolute, module)
+    F2 = sapm_aoi_loss(aoi, module)
 
     E0 = reference_irradiance
 
@@ -1725,7 +1734,7 @@ def _golden_sect_DataFrame(params, VL, VH, func):
 
         err = df['V1'] - df['V2']
         try:
-            errflag = (abs(err) > .01).all()
+            errflag = (abs(err) > .01).any()
         except ValueError:
             errflag = (abs(err) > .01)
 
@@ -1754,15 +1763,15 @@ def v_from_i(resistance_shunt, resistance_series, nNsVth, current,
 
     Parameters
     ----------
-    resistance_shunt : float or Series
+    resistance_shunt : numeric
         Shunt resistance in ohms under desired IV curve conditions.
         Often abbreviated ``Rsh``.
 
-    resistance_series : float or Series
+    resistance_series : numeric
         Series resistance in ohms under desired IV curve conditions.
         Often abbreviated ``Rs``.
 
-    nNsVth : float or Series
+    nNsVth : numeric
         The product of three components. 1) The usual diode ideal factor
         (n), 2) the number of cells in series (Ns), and 3) the cell
         thermal voltage under the desired IV curve conditions (Vth). The
@@ -1771,14 +1780,14 @@ def v_from_i(resistance_shunt, resistance_series, nNsVth, current,
         temp_cell is the temperature of the p-n junction in Kelvin, and
         q is the charge of an electron (coulombs).
 
-    current : float or Series
+    current : numeric
         The current in amperes under desired IV curve conditions.
 
-    saturation_current : float or Series
+    saturation_current : numeric
         Diode saturation current in amperes under desired IV curve
         conditions. Often abbreviated ``I_0``.
 
-    photocurrent : float or Series
+    photocurrent : numeric
         Light-generated current (photocurrent) in amperes under desired
         IV curve conditions. Often abbreviated ``I_L``.
 
@@ -1820,15 +1829,15 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage,
 
     Parameters
     ----------
-    resistance_shunt : float or Series
+    resistance_shunt : numeric
         Shunt resistance in ohms under desired IV curve conditions.
         Often abbreviated ``Rsh``.
 
-    resistance_series : float or Series
+    resistance_series : numeric
         Series resistance in ohms under desired IV curve conditions.
         Often abbreviated ``Rs``.
 
-    nNsVth : float or Series
+    nNsVth : numeric
         The product of three components. 1) The usual diode ideal factor
         (n), 2) the number of cells in series (Ns), and 3) the cell
         thermal voltage under the desired IV curve conditions (Vth). The
@@ -1837,14 +1846,14 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage,
         temp_cell is the temperature of the p-n junction in Kelvin, and
         q is the charge of an electron (coulombs).
 
-    voltage : float or Series
+    voltage : numeric
         The voltage in Volts under desired IV curve conditions.
 
-    saturation_current : float or Series
+    saturation_current : numeric
         Diode saturation current in amperes under desired IV curve
         conditions. Often abbreviated ``I_0``.
 
-    photocurrent : float or Series
+    photocurrent : numeric
         Light-generated current (photocurrent) in amperes under desired
         IV curve conditions. Often abbreviated ``I_L``.
 
@@ -1880,7 +1889,7 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage,
     return I.real
 
 
-def snlinverter(inverter, v_dc, p_dc):
+def snlinverter(v_dc, p_dc, inverter):
     '''
     Converts DC power and voltage to AC power using Sandia's
     Grid-Connected PV Inverter model.
@@ -1895,7 +1904,15 @@ def snlinverter(inverter, v_dc, p_dc):
 
     Parameters
     ----------
-    inverter : dict or Series
+    v_dc : numeric
+        DC voltages, in volts, which are provided as input to the
+        inverter. Vdc must be >= 0.
+
+    p_dc : numeric
+        A scalar or DataFrame of DC powers, in watts, which are provided
+        as input to the inverter. Pdc must be >= 0.
+
+    inverter : dict-like
         A dict-like object defining the inverter to be used, giving the
         inverter performance parameters according to the Sandia
         Grid-Connected Photovoltaic Inverter Model (SAND 2007-5036) [1].
@@ -1931,16 +1948,9 @@ def snlinverter(inverter, v_dc, p_dc):
                  maintain circuitry required to sense PV array voltage (W)
         ======   ============================================================
 
-    v_dc : float or Series
-        DC voltages, in volts, which are provided as input to the
-        inverter. Vdc must be >= 0.
-    p_dc : float or Series
-        A scalar or DataFrame of DC powers, in watts, which are provided
-        as input to the inverter. Pdc must be >= 0.
-
     Returns
     -------
-    ac_power : float or Series
+    ac_power : numeric
         Modeled AC power output given the input DC voltage, Vdc, and
         input DC power, Pdc. When ac_power would be greater than Pac0,
         it is set to Pac0 to represent inverter "clipping". When
@@ -1978,15 +1988,12 @@ def snlinverter(inverter, v_dc, p_dc):
     B = Pso * (1 + C2*(v_dc - Vdco))
     C = C0 * (1 + C3*(v_dc - Vdco))
 
-    # ensures that function works with scalar or Series input
-    p_dc = pd.Series(p_dc)
-
     ac_power = (Paco/(A-B) - C*(A-B)) * (p_dc-B) + C*((p_dc-B)**2)
-    ac_power[ac_power > Paco] = Paco
-    ac_power[p_dc < Pso] = - 1.0 * abs(Pnt)
+    ac_power = np.minimum(Paco, ac_power)
+    ac_power = np.where(p_dc < Pso, -1.0 * abs(Pnt), ac_power)
 
-    if len(ac_power) == 1:
-        ac_power = ac_power.ix[0]
+    if isinstance(p_dc, pd.Series):
+        ac_power = pd.Series(ac_power, index=p_dc.index)
 
     return ac_power
 
diff --git a/pvlib/tools.py b/pvlib/tools.py
index bab3ab6..c779703 100644
--- a/pvlib/tools.py
+++ b/pvlib/tools.py
@@ -117,7 +117,6 @@ def localize_to_utc(time, location):
             time_utc = time.tz_localize(location.tz).tz_convert('UTC')
             pvl_logger.debug('tz_localize to %s and then tz_convert to UTC',
                              location.tz)
-
     return time_utc
 
 
@@ -232,3 +231,28 @@ def _array_out(input):
         output = input
 
     return output
+
+
+def _build_kwargs(keys, input_dict):
+    """
+    Parameters
+    ----------
+    keys : iterable
+        Typically a list of strings.
+    adict : 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


Test patch:
diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py
index 3c8aa08..5807de5 100644
--- a/pvlib/test/test_pvsystem.py
+++ b/pvlib/test/test_pvsystem.py
@@ -92,42 +92,44 @@ def test_systemdef_dict():
     assert expected == pvsystem.systemdef(meta, 5, 0, .1, 5, 5)
 
 
+@needs_numpy_1_10
 def test_ashraeiam():
     thetas = np.linspace(-90, 90, 9)
-    iam = pvsystem.ashraeiam(.05, thetas)
+    iam = pvsystem.ashraeiam(thetas, .05)
     expected = np.array([        nan,  0.9193437 ,  0.97928932,  0.99588039,  1.        ,
         0.99588039,  0.97928932,  0.9193437 ,         nan])
-    assert np.isclose(iam, expected, equal_nan=True).all()
+    assert_allclose(iam, expected, equal_nan=True)
 
 
+@needs_numpy_1_10
 def test_PVSystem_ashraeiam():
     module_parameters = pd.Series({'b': 0.05})
-    system = pvsystem.PVSystem(module='blah', inverter='blarg',
-                               module_parameters=module_parameters)
+    system = pvsystem.PVSystem(module_parameters=module_parameters)
     thetas = np.linspace(-90, 90, 9)
     iam = system.ashraeiam(thetas)
     expected = np.array([        nan,  0.9193437 ,  0.97928932,  0.99588039,  1.        ,
         0.99588039,  0.97928932,  0.9193437 ,         nan])
-    assert np.isclose(iam, expected, equal_nan=True).all()
+    assert_allclose(iam, expected, equal_nan=True)
 
 
+@needs_numpy_1_10
 def test_physicaliam():
     thetas = np.linspace(-90, 90, 9)
-    iam = pvsystem.physicaliam(4, 0.002, 1.526, thetas)
+    iam = pvsystem.physicaliam(thetas, 1.526, 0.002, 4)
     expected = np.array([        nan,  0.8893998 ,  0.98797788,  0.99926198,         nan,
         0.99926198,  0.98797788,  0.8893998 ,         nan])
-    assert np.isclose(iam, expected, equal_nan=True).all()
+    assert_allclose(iam, expected, equal_nan=True)
 
 
+@needs_numpy_1_10
 def test_PVSystem_physicaliam():
     module_parameters = pd.Series({'K': 4, 'L': 0.002, 'n': 1.526})
-    system = pvsystem.PVSystem(module='blah', inverter='blarg',
-                               module_parameters=module_parameters)
+    system = pvsystem.PVSystem(module_parameters=module_parameters)
     thetas = np.linspace(-90, 90, 9)
     iam = system.physicaliam(thetas)
     expected = np.array([        nan,  0.8893998 ,  0.98797788,  0.99926198,         nan,
         0.99926198,  0.98797788,  0.8893998 ,         nan])
-    assert np.isclose(iam, expected, equal_nan=True).all()
+    assert_allclose(iam, expected, equal_nan=True)
 
 
 # if this completes successfully we'll be able to do more tests below.
@@ -154,7 +156,7 @@ def test_sapm(sapm_module_params):
     effective_irradiance = pd.Series([-1, 0.5, 1.1, np.nan, 1], index=times)
     temp_cell = pd.Series([10, 25, 50, 25, np.nan], index=times)
 
-    out = pvsystem.sapm(sapm_module_params, effective_irradiance, temp_cell)
+    out = pvsystem.sapm(effective_irradiance, temp_cell, sapm_module_params)
 
     expected = pd.DataFrame(np.array(
       [[  -5.0608322 ,   -4.65037767,           nan,           nan,
@@ -172,7 +174,7 @@ def test_sapm(sapm_module_params):
 
     assert_frame_equal(out, expected, check_less_precise=4)
 
-    out = pvsystem.sapm(sapm_module_params, 1, 25)
+    out = pvsystem.sapm(1, 25, sapm_module_params)
 
     expected = OrderedDict()
     expected['i_sc'] = 5.09115
@@ -187,8 +189,8 @@ def test_sapm(sapm_module_params):
         assert_allclose(out[k], v, atol=1e-4)
 
     # just make sure it works with a dict input
-    pvsystem.sapm(sapm_module_params.to_dict(), effective_irradiance,
-                  temp_cell)
+    pvsystem.sapm(effective_irradiance, temp_cell,
+                  sapm_module_params.to_dict())
 
 
 def test_PVSystem_sapm(sapm_module_params):
@@ -208,7 +210,7 @@ def test_PVSystem_sapm(sapm_module_params):
 ])
 def test_sapm_spectral_loss(sapm_module_params, airmass, expected):
 
-    out = pvsystem.sapm_spectral_loss(sapm_module_params, airmass)
+    out = pvsystem.sapm_spectral_loss(airmass, sapm_module_params)
 
     if isinstance(airmass, pd.Series):
         assert_series_equal(out, expected, check_less_precise=4)
@@ -233,7 +235,7 @@ def test_PVSystem_sapm_spectral_loss(sapm_module_params):
 ])
 def test_sapm_aoi_loss(sapm_module_params, aoi, expected):
 
-    out = pvsystem.sapm_aoi_loss(sapm_module_params, aoi)
+    out = pvsystem.sapm_aoi_loss(aoi, sapm_module_params)
 
     if isinstance(aoi, pd.Series):
         assert_series_equal(out, expected, check_less_precise=4)
@@ -243,13 +245,13 @@ def test_sapm_aoi_loss(sapm_module_params, aoi, expected):
 
 def test_sapm_aoi_loss_limits():
     module_parameters = {'B0': 5, 'B1': 0, 'B2': 0, 'B3': 0, 'B4': 0, 'B5': 0}
-    assert pvsystem.sapm_aoi_loss(module_parameters, 1) == 5
+    assert pvsystem.sapm_aoi_loss(1, module_parameters) == 5
 
     module_parameters = {'B0': 5, 'B1': 0, 'B2': 0, 'B3': 0, 'B4': 0, 'B5': 0}
-    assert pvsystem.sapm_aoi_loss(module_parameters, 1, upper=1) == 1
+    assert pvsystem.sapm_aoi_loss(1, module_parameters, upper=1) == 1
 
     module_parameters = {'B0': -5, 'B1': 0, 'B2': 0, 'B3': 0, 'B4': 0, 'B5': 0}
-    assert pvsystem.sapm_aoi_loss(module_parameters, 1) == 0
+    assert pvsystem.sapm_aoi_loss(1, module_parameters) == 0
 
 
 def test_PVSystem_sapm_aoi_loss(sapm_module_params):
@@ -275,7 +277,15 @@ def test_PVSystem_sapm_aoi_loss(sapm_module_params):
 ])
 def test_sapm_effective_irradiance(sapm_module_params, test_input, expected):
 
-    out = pvsystem.sapm_effective_irradiance(sapm_module_params, *test_input)
+    try:
+        kwargs = {'reference_irradiance': test_input[4]}
+        test_input = test_input[:-1]
+    except IndexError:
+        kwargs = {}
+
+    test_input.append(sapm_module_params)
+
+    out = pvsystem.sapm_effective_irradiance(*test_input, **kwargs)
 
     if isinstance(test_input, pd.Series):
         assert_series_equal(out, expected, check_less_precise=4)
@@ -383,6 +393,26 @@ def test_singlediode_series(sam_data):
     assert isinstance(out, pd.DataFrame)
 
 
+@requires_scipy
+def test_singlediode_array():
+    # github issue 221
+    photocurrent = np.linspace(0, 10, 11)
+    resistance_shunt = 16
+    resistance_series = 0.094
+    nNsVth = 0.473
+    saturation_current = 1.943e-09
+
+    sd = pvsystem.singlediode(photocurrent, saturation_current,
+                              resistance_series, resistance_shunt, nNsVth)
+
+    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)
+
+
 @requires_scipy
 def test_singlediode_floats(sam_data):
     module = 'Example_Module'
@@ -500,7 +530,7 @@ def test_snlinverter(sam_data):
     idcs = pd.Series(np.linspace(0,11,3))
     pdcs = idcs * vdcs
 
-    pacs = pvsystem.snlinverter(inverters[testinv], vdcs, pdcs)
+    pacs = pvsystem.snlinverter(vdcs, pdcs, inverters[testinv])
     assert_series_equal(pacs, pd.Series([-0.020000, 132.004308, 250.000000]))
 
 
@@ -524,7 +554,7 @@ def test_snlinverter_float(sam_data):
     idcs = 5.5
     pdcs = idcs * vdcs
 
-    pacs = pvsystem.snlinverter(inverters[testinv], vdcs, pdcs)
+    pacs = pvsystem.snlinverter(vdcs, pdcs, inverters[testinv])
     assert_allclose(pacs, 132.004278, 5)
 
 
@@ -535,7 +565,7 @@ def test_snlinverter_Pnt_micro(sam_data):
     idcs = pd.Series(np.linspace(0,11,3))
     pdcs = idcs * vdcs
 
-    pacs = pvsystem.snlinverter(inverters[testinv], vdcs, pdcs)
+    pacs = pvsystem.snlinverter(vdcs, pdcs, inverters[testinv])
     assert_series_equal(pacs, pd.Series([-0.043000, 132.545914746, 240.000000]))
 
 
diff --git a/pvlib/test/test_tools.py b/pvlib/test/test_tools.py
new file mode 100644
index 0000000..dfecea2
--- /dev/null
+++ b/pvlib/test/test_tools.py
@@ -0,0 +1,13 @@
+import pytest
+
+from pvlib import tools
+
+@pytest.mark.parametrize('keys, input_dict, expected', [
+    (['a', 'b'], {'a': 1, 'b': 2, 'c': 3}, {'a': 1, 'b': 2}),
+    (['a', 'b', 'd'], {'a': 1, 'b': 2, 'c': 3}, {'a': 1, 'b': 2}),
+    (['a'], {}, {}),
+    (['a'], {'b': 2}, {})
+])
+def test_build_kwargs(keys, input_dict, expected):
+    kwargs = tools._build_kwargs(keys, input_dict)
+    assert kwargs == expected