Repository: pvlib/pvlib-python
Pull request #783 (2019-10-02 20:22:49)

Create iam.py, consistent naming for IAM functions
 - [x] Closes  #680. Related to #436 
 - [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):

- Functions that calculate incident angle modifier IAM are moved from `pvsystem.py` to the new `iam.py`.
- Function names are shorted to only the model, e.g., `iam.ashrae` instead of `pvsystem.ashraeiam`.
- Method names for `PVSystem are prepended by `iam_`, e.g., `PVSystem.iam_ashrae` instead of `PVSystem.ashrae_iam`.
- The SAPM IAM function is renamed from `pvsystem.sapm_aoi_loss` to `iam.sapm`.
- Edits to function docstrings.

wholmgren: > Method names for PVSystem are prepended by iam_, e.g., PVSystem.iam_ashraeinstead ofPVSystem.ashrae_iam`.

What about a single `PVSystem.iam` wrapper method? Might present a cleaner API.
cwhanse: > Method names for PVSystem are prepended by iam_, e.g., PVSystem.iam_ashraeinstead ofPVSystem.ashrae_iam`.
> 
> What about a single PVSystem.iam wrapper method? Might present a cleaner API.

Do you think we should start using .setter properties the same way we do in ModelChain?  
cwhanse: Ready for review. Notes on the scope of this PR and the test failure:

- `iam.interp` is not connected to `PVSystem` or `ModelChain`. I didn't think of a convenient way to pass the `interp` unique arguments through `PVSystem.get_iam`.
- `get_psm3` is failing due to a change in data on the NSRDB. `get_psm3` is fetching pressure in units of decibars, the recorded test file (and NSRDB documentation) says mbar. I've sent a note to NREL asking about the change, which I think may be in error, since TMY data fetched through the web interface is in mbar. Otherwise it appears that `get_psm3` works as intended.

Patch:
diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst
index 6e93dc5..c37bedf 100644
--- a/docs/sphinx/source/api.rst
+++ b/docs/sphinx/source/api.rst
@@ -211,15 +211,17 @@ wrap the functions listed below. See its documentation for details.
    pvsystem.PVSystem
    pvsystem.LocalizedPVSystem
 
-AOI modifiers
--------------
+Incident angle modifiers
+------------------------
 
 .. autosummary::
    :toctree: generated/
 
-   pvsystem.physicaliam
-   pvsystem.ashraeiam
-   pvsystem.sapm_aoi_loss
+   iam.physical
+   iam.ashrae
+   iam.martin_ruiz
+   iam.sapm
+   iam.interp
 
 PV temperature models
 ---------------------
diff --git a/docs/sphinx/source/forecasts.rst b/docs/sphinx/source/forecasts.rst
index 56e88ac..2637f8d 100644
--- a/docs/sphinx/source/forecasts.rst
+++ b/docs/sphinx/source/forecasts.rst
@@ -447,7 +447,7 @@ for details.
     sandia_modules = retrieve_sam('sandiamod')
     cec_inverters = retrieve_sam('cecinverter')
     module = sandia_modules['Canadian_Solar_CS5P_220M___2009_']
-    inverter = cec_inverters['SMA_America__SC630CP_US_315V__CEC_2012_']
+    inverter = cec_inverters['SMA_America__SC630CP_US__with_ABB_EcoDry_Ultra_transformer_']
     temperature_model_parameters = TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass']
 
     # model a big tracker for more fun
@@ -461,7 +461,7 @@ for details.
     mc = ModelChain(system, fx_model.location)
 
     # extract relevant data for model chain
-    mc.run_model(fx_data.index, weather=fx_data);
+    mc.run_model(fx_data);
 
 Now we plot a couple of modeling intermediates and the forecast power.
 Here's the forecast plane of array irradiance...
diff --git a/docs/sphinx/source/introexamples.rst b/docs/sphinx/source/introexamples.rst
index e1b01d9..c574cc3 100644
--- a/docs/sphinx/source/introexamples.rst
+++ b/docs/sphinx/source/introexamples.rst
@@ -45,7 +45,7 @@ configuration at a handful of sites listed below.
     sandia_modules = pvlib.pvsystem.retrieve_sam('SandiaMod')
     sapm_inverters = pvlib.pvsystem.retrieve_sam('cecinverter')
     module = sandia_modules['Canadian_Solar_CS5P_220M___2009_']
-    inverter = sapm_inverters['ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_']
+    inverter = sapm_inverters['ABB__MICRO_0_25_I_OUTD_US_208__208V_']
     temperature_model_parameters = pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass']
 
     # specify constant ambient air temp and wind for simplicity
diff --git a/docs/sphinx/source/modelchain.rst b/docs/sphinx/source/modelchain.rst
index 4025451..b0ef39b 100644
--- a/docs/sphinx/source/modelchain.rst
+++ b/docs/sphinx/source/modelchain.rst
@@ -53,7 +53,7 @@ objects, module data, and inverter data.
     cec_inverters = pvlib.pvsystem.retrieve_sam('cecinverter')
 
     sandia_module = sandia_modules['Canadian_Solar_CS5P_220M___2009_']
-    cec_inverter = cec_inverters['ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_']
+    cec_inverter = cec_inverters['ABB__MICRO_0_25_I_OUTD_US_208__208V_']
 
 Now we create a Location object, a PVSystem object, and a ModelChain
 object.
diff --git a/docs/sphinx/source/pvsystem.rst b/docs/sphinx/source/pvsystem.rst
index aa10d03..52b5974 100644
--- a/docs/sphinx/source/pvsystem.rst
+++ b/docs/sphinx/source/pvsystem.rst
@@ -130,9 +130,9 @@ the :py:func:`~pvlib.pvsystem.retrieve_sam` function:
     # retrieve_sam returns a dict. the dict keys are module names,
     # and the values are model parameters for that module
     modules = pvsystem.retrieve_sam('cecmod')
-    module_parameters = modules['Example_Module']
+    module_parameters = modules['Canadian_Solar_Inc__CS5P_220M']
     inverters = pvsystem.retrieve_sam('cecinverter')
-    inverter_parameters = inverters['ABB__MICRO_0_25_I_OUTD_US_208_208V__CEC_2014_']
+    inverter_parameters = inverters['ABB__MICRO_0_25_I_OUTD_US_208__208V_']
     system = pvsystem.PVSystem(module_parameters=module_parameters, inverter_parameters=inverter_parameters)
 
 
diff --git a/docs/sphinx/source/whatsnew/v0.7.0.rst b/docs/sphinx/source/whatsnew/v0.7.0.rst
index d358c88..5a90f11 100644
--- a/docs/sphinx/source/whatsnew/v0.7.0.rst
+++ b/docs/sphinx/source/whatsnew/v0.7.0.rst
@@ -69,6 +69,20 @@ API Changes
       - `modelchain.basic_chain` has a new required argument
         `temperature_model_parameters`.
 
+* Changes related to IAM (AOI loss) functions (:issue:`680`):
+   * Changes to functions
+      - Moved functions from `pvsystem.py` to `iam.py`. `pvsystem` IAM
+       functions are deprecated and will be removed in v0.8.
+      - Functions are renamed to a consistent pattern:
+         - `pvsystem.physicaliam` is `iam.physical`
+         - `pvsystem.ashraeiam` is `iam.ashrae`
+         - `pvsystem.sapm_aoi_loss` is `iam.sapm`
+      - Created dict `iam.IAM_MODEL_PARAMS` to aid in identifying IAM models
+   * Changes to `PVSystem` class
+      - IAM models are provided by `PVSystem.get_iam` with kwarg `iam_model`.
+      - Methods `PVSystem.ashraeiam`, `PVSystem.physicaliam` and
+       `PVSystem.sapm_aoi_loss` are deprecated and will be removed in v0.8.
+
 * Calling :py:func:`pvlib.pvsystem.retrieve_sam` with no parameters will raise
   an exception instead of displaying a dialog.
 * The `times` keyword argument has been deprecated in the
@@ -83,8 +97,9 @@ API Changes
 
 Enhancements
 ~~~~~~~~~~~~
-* Created two new incidence angle modifier functions: :py:func:`pvlib.pvsystem.iam_martin_ruiz`
-  and :py:func:`pvlib.pvsystem.iam_interp`. (:issue:`751`)
+* Created two new incidence angle modifier (IAM) functions:
+  :py:func:`pvlib.iam.martin_ruiz` and :py:func:`pvlib.iam.interp`. (:issue:`751`)
+* Add the `martin_ruiz` IAM function as an option for `ModelChain.aoi_model`.
 * Updated the file for module parameters for the CEC model, from the SAM file
   dated 2017-6-5 to the SAM file dated 2019-03-05. (:issue:`761`)
 * Updated the file for inverter parameters for the CEC model, from the SAM file
@@ -96,6 +111,7 @@ 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 `timeout` to :py:func:`pvlib.iotools.get_psm3`.
 
 Bug fixes
 ~~~~~~~~~
@@ -113,10 +129,13 @@ Testing
   in NSRDB (:issue:`733`)
 * Added tests for methods in bifacial.py.
 * Added tests for changes to cell temperature models.
+* Added tests for changes to IAM models.
+* Added test for `ModelChain.infer_aoi_model`.
 
 Documentation
 ~~~~~~~~~~~~~
 * Corrected docstring for `pvsystem.PVSystem.sapm`
+* Fixed broken ipython examples from CEC data updates
 
 Removal of prior version deprecations
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -132,11 +151,6 @@ Removal of prior version deprecations
 * Removed `ModelChain.prepare_inputs` clearsky assumption when no irradiance
   data was provided.
 
-Enhancements
-~~~~~~~~~~~~
-* Add `timeout` to :py:func:`pvlib.iotools.get_psm3`.
-
-
 Contributors
 ~~~~~~~~~~~~
 * Mark Campanellli (:ghuser:`markcampanelli`)
@@ -146,3 +160,4 @@ Contributors
 * Anton Driesse (:ghuser:`adriesse`)
 * Alexander Morgan (:ghuser:`alexandermorgan`)
 * Miguel Sánchez de León Peque (:ghuser:`Peque`)
+* Veronica Guo (:ghuser:`veronicaguo`)
diff --git a/pvlib/iam.py b/pvlib/iam.py
new file mode 100644
index 0000000..3d869f2
--- /dev/null
+++ b/pvlib/iam.py
@@ -0,0 +1,422 @@
+r"""
+The ``iam`` module contains functions that implement models for the incidence
+angle modifier (IAM). The IAM quantifies the fraction of direct irradiance on
+a module's front surface that is transmitted through the module materials to
+the cells. Stated differently, the quantity 1 - IAM is the fraction of direct
+irradiance that is reflected away or absorbed by the module's front materials.
+IAM is typically a function of the angle of incidence (AOI) of the direct
+irradiance to the module's surface.
+"""
+
+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 = {
+    'ashrae': set(['b']),
+    'physical': set(['n', 'K', 'L']),
+    'martin_ruiz': set(['a_r']),
+    'sapm': set(['B0', 'B1', 'B2', 'B3', 'B4', 'B5']),
+    'interp': set([])
+}
+
+
+def ashrae(aoi, b=0.05):
+    r"""
+    Determine the incidence angle modifier using the ASHRAE transmission
+    model.
+
+    The ASHRAE (American Society of Heating, Refrigeration, and Air
+    Conditioning Engineers) transmission model is developed in
+    [1], and in [2]. The model has been used in software such as PVSyst [3].
+
+    Parameters
+    ----------
+    aoi : numeric
+        The angle of incidence (AOI) between the module normal vector and the
+        sun-beam vector in degrees. Angles of nan will result in nan.
+
+    b : float, default 0.05
+        A parameter to adjust the incidence angle modifier as a function of
+        angle of incidence. Typical values are on the order of 0.05 [3].
+
+    Returns
+    -------
+    iam : numeric
+        The incident angle modifier (IAM). Returns zero for all abs(aoi) >= 90
+        and for all ``iam`` values that would be less than 0.
+
+    Notes
+    -----
+    The incidence angle modifier is calculated as
+
+    .. math::
+
+        IAM = 1 - b (\sec(aoi) - 1)
+
+    As AOI approaches 90 degrees, the model yields negative values for IAM;
+    negative IAM values are set to zero in this implementation.
+
+    References
+    ----------
+    [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.
+
+    [2] ASHRAE standard 93-77
+
+    [3] PVsyst Contextual Help.
+    https://files.pvsyst.com/help/index.html?iam_loss.htm retrieved on
+    October 14, 2019
+
+    See Also
+    --------
+    iam.physical
+    iam.martin_ruiz
+    iam.interp
+    """
+
+    iam = 1 - b * ((1 / np.cos(np.radians(aoi)) - 1))
+    aoi_gte_90 = np.full_like(aoi, False, dtype='bool')
+    np.greater_equal(np.abs(aoi), 90, where=~np.isnan(aoi), out=aoi_gte_90)
+    iam = np.where(aoi_gte_90, 0, iam)
+    iam = np.maximum(0, iam)
+
+    if isinstance(aoi, pd.Series):
+        iam = pd.Series(iam, index=aoi.index)
+
+    return iam
+
+
+def physical(aoi, n=1.526, K=4., L=0.002):
+    r"""
+    Determine the incidence angle modifier using refractive index ``n``,
+    extinction coefficient ``K``, and glazing thickness ``L``.
+
+    ``iam.physical`` calculates the incidence angle modifier as described in
+    [1], Section 3. The calculation is based on a physical model of absorbtion
+    and transmission through a transparent cover.
+
+    Parameters
+    ----------
+    aoi : numeric
+        The angle of incidence between the module normal vector and the
+        sun-beam vector in degrees. Angles of 0 are replaced with 1e-06
+        to ensure non-nan results. Angles of nan will result in nan.
+
+    n : numeric, default 1.526
+        The effective index of refraction (unitless). Reference [1]
+        indicates that a value of 1.526 is acceptable for glass.
+
+    K : numeric, default 4.0
+        The glazing extinction coefficient in units of 1/meters.
+        Reference [1] indicates that a value of 4 is reasonable for
+        "water white" glass.
+
+    L : numeric, default 0.002
+        The glazing thickness in units of meters. Reference [1]
+        indicates that 0.002 meters (2 mm) is reasonable for most
+        glass-covered PV panels.
+
+    Returns
+    -------
+    iam : numeric
+        The incident angle modifier
+
+    Notes
+    -----
+    The pvlib python authors believe that Eqn. 14 in [1] is
+    incorrect, which presents :math:`\theta_{r} = \arcsin(n \sin(AOI))`.
+    Here, :math:`\theta_{r} = \arcsin(1/n \times \sin(AOI))`
+
+    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] Duffie, John A. & Beckman, William A.. (2006). Solar Engineering
+    of Thermal Processes, third edition. [Books24x7 version] Available
+    from http://common.books24x7.com/toc.aspx?bookid=17160.
+
+    See Also
+    --------
+    iam.martin_ruiz
+    iam.ashrae
+    iam.interp
+    iam.sapm
+    """
+    zeroang = 1e-06
+
+    # hold a new reference to the input aoi object since we're going to
+    # overwrite the aoi reference below, but we'll need it for the
+    # series check at the end of the function
+    aoi_input = aoi
+
+    aoi = np.where(aoi == 0, zeroang, aoi)
+
+    # angle of reflection
+    thetar_deg = asind(1.0 / n * (sind(aoi)))
+
+    # reflectance and transmittance for normal incidence light
+    rho_zero = ((1-n) / (1+n)) ** 2
+    tau_zero = np.exp(-K*L)
+
+    # reflectance for parallel and perpendicular polarized light
+    rho_para = (tand(thetar_deg - aoi) / tand(thetar_deg + aoi)) ** 2
+    rho_perp = (sind(thetar_deg - aoi) / sind(thetar_deg + aoi)) ** 2
+
+    # transmittance for non-normal light
+    tau = np.exp(-K * L / cosd(thetar_deg))
+
+    # iam is ratio of non-normal to normal incidence transmitted light
+    # after deducting the reflected portion of each
+    iam = ((1 - (rho_para + rho_perp) / 2) / (1 - rho_zero) * tau / tau_zero)
+
+    with np.errstate(invalid='ignore'):
+        # angles near zero produce nan, but iam is defined as one
+        small_angle = 1e-06
+        iam = np.where(np.abs(aoi) < small_angle, 1.0, iam)
+
+        # angles at 90 degrees can produce tiny negative values,
+        # which should be zero. this is a result of calculation precision
+        # rather than the physical model
+        iam = np.where(iam < 0, 0, iam)
+
+        # for light coming from behind the plane, none can enter the module
+        iam = np.where(aoi > 90, 0, iam)
+
+    if isinstance(aoi_input, pd.Series):
+        iam = pd.Series(iam, index=aoi_input.index)
+
+    return iam
+
+
+def martin_ruiz(aoi, a_r=0.16):
+    r'''
+    Determine the incidence angle modifier (IAM) using the Martin
+    and Ruiz incident angle model.
+
+    Parameters
+    ----------
+    aoi : numeric, degrees
+        The angle of incidence between the module normal vector and the
+        sun-beam vector in degrees.
+
+    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.
+
+    Returns
+    -------
+    iam : numeric
+        The incident angle modifier(s)
+
+    Notes
+    -----
+    `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.
+
+    The incident angle modifier is defined as
+
+    .. math::
+
+       IAM = \frac{1 - \exp(-\cos(\frac{aoi}{a_r}))}
+       {1 - \exp(\frac{-1}{a_r}}
+
+    which is presented as :math:`AL(\alpha) = 1 - IAM` in equation 4 of [1],
+    with :math:`\alpha` representing the angle of incidence AOI. Thus IAM = 1
+    at AOI = 0, and IAM = 0 at AOI = 90.  This equation is only valid for
+    -90 <= aoi <= 90, therefore `iam` is constrained to 0.0 outside this
+    interval.
+
+    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.
+
+    See Also
+    --------
+    iam.physical
+    iam.ashrae
+    iam.interp
+    iam.sapm
+    '''
+    # Contributed by Anton Driesse (@adriesse), PV Performance Labs. July, 2019
+
+    aoi_input = aoi
+
+    aoi = np.asanyarray(aoi)
+    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.")
+
+    with np.errstate(invalid='ignore'):
+        iam = (1 - np.exp(-cosd(aoi) / a_r)) / (1 - np.exp(-1 / a_r))
+        iam = np.where(np.abs(aoi) >= 90.0, 0.0, iam)
+
+    if isinstance(aoi_input, pd.Series):
+        iam = pd.Series(iam, index=aoi_input.index)
+
+    return iam
+
+
+def interp(aoi, theta_ref, iam_ref, method='linear', normalize=True):
+    r'''
+    Determine the incidence angle modifier (IAM) by interpolating a set of
+    reference values, which are usually measured values.
+
+    Parameters
+    ----------
+    aoi : numeric
+        The angle of incidence between the module normal vector and the
+        sun-beam vector [degrees].
+
+    theta_ref : numeric
+        Vector of angles at which the IAM is known [degrees].
+
+    iam_ref : numeric
+        IAM values for each angle in ``theta_ref`` [unitless].
+
+    method : str, default 'linear'
+        Specifies the interpolation method.
+        Useful options are: 'linear', 'quadratic','cubic'.
+        See scipy.interpolate.interp1d for more options.
+
+    normalize : boolean, default True
+        When true, the interpolated values are divided by the interpolated
+        value at zero degrees.  This ensures that ``iam``=1.0 at normal
+        incidence.
+
+    Returns
+    -------
+    iam : numeric
+        The incident angle modifier(s) [unitless]
+
+    Notes
+    -----
+    ``theta_ref`` must have two or more points and may span any range of
+    angles. Typically there will be a dozen or more points in the range 0-90
+    degrees. Beyond the range of ``theta_ref``, IAM values are extrapolated,
+    but constrained to be non-negative.
+
+    The sign of ``aoi`` is ignored; only the magnitude is used.
+
+    See Also
+    --------
+    iam.physical
+    iam.ashrae
+    iam.martin_ruiz
+    iam.sapm
+    '''
+    # Contributed by Anton Driesse (@adriesse), PV Performance Labs. July, 2019
+
+    from scipy.interpolate import interp1d
+
+    # Scipy doesn't give the clearest feedback, so check number of points here.
+    MIN_REF_VALS = {'linear': 2, 'quadratic': 3, 'cubic': 4, 1: 2, 2: 3, 3: 4}
+
+    if len(theta_ref) < MIN_REF_VALS.get(method, 2):
+        raise ValueError("Too few reference points defined "
+                         "for interpolation method '%s'." % method)
+
+    if np.any(np.less(iam_ref, 0)):
+        raise ValueError("Negative value(s) found in 'iam_ref'. "
+                         "This is not physically possible.")
+
+    interpolator = interp1d(theta_ref, iam_ref, kind=method,
+                            fill_value='extrapolate')
+    aoi_input = aoi
+
+    aoi = np.asanyarray(aoi)
+    aoi = np.abs(aoi)
+    iam = interpolator(aoi)
+    iam = np.clip(iam, 0, None)
+
+    if normalize:
+        iam /= interpolator(0)
+
+    if isinstance(aoi_input, pd.Series):
+        iam = pd.Series(iam, index=aoi_input.index)
+
+    return iam
+
+
+def sapm(aoi, module, upper=None):
+    r"""
+    Determine the incidence angle modifier (IAM) using the SAPM model.
+
+    Parameters
+    ----------
+    aoi : numeric
+        Angle of incidence in degrees. Negative input angles will return
+        zeros.
+
+    module : dict-like
+        A dict or Series with the SAPM IAM model parameters.
+        See the :py:func:`sapm` notes section for more details.
+
+    upper : None or float, default None
+        Upper limit on the results.
+
+    Returns
+    -------
+    iam : numeric
+        The SAPM angle of incidence loss coefficient, termed F2 in [1].
+
+    Notes
+    -----
+    The SAPM [1] traditionally does not define an upper limit on the AOI
+    loss function and values slightly exceeding 1 may exist for moderate
+    angles of incidence (15-40 degrees). However, users may consider
+    imposing an upper limit of 1.
+
+    References
+    ----------
+    [1] King, D. et al, 2004, "Sandia Photovoltaic Array Performance
+    Model", SAND Report 3535, Sandia National Laboratories, Albuquerque,
+    NM.
+
+    [2] B.H. King et al, "Procedure to Determine Coefficients for the
+    Sandia Array Performance Model (SAPM)," SAND2016-5284, Sandia
+    National Laboratories (2016).
+
+    [3] B.H. King et al, "Recent Advancements in Outdoor Measurement
+    Techniques for Angle of Incidence Effects," 42nd IEEE PVSC (2015).
+    DOI: 10.1109/PVSC.2015.7355849
+
+    See Also
+    --------
+    iam.physical
+    iam.ashrae
+    iam.martin_ruiz
+    iam.interp
+    """
+
+    aoi_coeff = [module['B5'], module['B4'], module['B3'], module['B2'],
+                 module['B1'], module['B0']]
+
+    iam = np.polyval(aoi_coeff, aoi)
+    iam = np.clip(iam, 0, upper)
+    # nan tolerant masking
+    aoi_lt_0 = np.full_like(aoi, False, dtype='bool')
+    np.less(aoi, 0, where=~np.isnan(aoi), out=aoi_lt_0)
+    iam = np.where(aoi_lt_0, 0, iam)
+
+    if isinstance(aoi, pd.Series):
+        iam = pd.Series(iam, aoi.index)
+
+    return iam
diff --git a/pvlib/iotools/tmy.py b/pvlib/iotools/tmy.py
index a3860c3..f17908f 100644
--- a/pvlib/iotools/tmy.py
+++ b/pvlib/iotools/tmy.py
@@ -468,7 +468,7 @@ def _read_tmy2(string, columns, hdr_columns, fname):
                     continue
 
                 # Read the next increment from the marker list
-                increment = int(re.findall('\d+', marker)[0])
+                increment = int(re.findall(r'\d+', marker)[0])
                 next_cursor = cursor + increment
 
                 # Extract the value from the line in the file
diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py
index 6aad00c..16bd7ba 100644
--- a/pvlib/modelchain.py
+++ b/pvlib/modelchain.py
@@ -271,8 +271,8 @@ class ModelChain(object):
     aoi_model: None, str, or function, default None
         If None, the model will be inferred from the contents of
         system.module_parameters. Valid strings are 'physical',
-        'ashrae', 'sapm', 'no_loss'. The ModelChain instance will be
-        passed as the first argument to a user-defined function.
+        'ashrae', 'sapm', 'martin_ruiz', 'no_loss'. The ModelChain instance
+        will be passed as the first argument to a user-defined function.
 
     spectral_model: None, str, or function, default None
         If None, the model will be inferred from the contents of
@@ -540,6 +540,8 @@ class ModelChain(object):
                 self._aoi_model = self.physical_aoi_loss
             elif model == 'sapm':
                 self._aoi_model = self.sapm_aoi_loss
+            elif model == 'martin_ruiz':
+                self._aoi_model = self.martin_ruiz_aoi_loss
             elif model == 'no_loss':
                 self._aoi_model = self.no_aoi_loss
             else:
@@ -555,24 +557,31 @@ class ModelChain(object):
             return self.sapm_aoi_loss
         elif set(['b']) <= params:
             return self.ashrae_aoi_loss
+        elif set(['a_r']) <= params:
+            return self.martin_ruiz_aoi_loss
         else:
             raise ValueError('could not infer AOI model from '
                              'system.module_parameters. Check that the '
                              'system.module_parameters contain parameters for '
-                             'the physical, aoi, or ashrae model; explicitly '
-                             'set model with aoi_model kwarg; or set '
-                             'aoi_model="no_loss".')
+                             'the physical, aoi, ashrae or martin_ruiz model; '
+                             'explicitly set the model with the aoi_model '
+                             'kwarg; or set aoi_model="no_loss".')
 
     def ashrae_aoi_loss(self):
-        self.aoi_modifier = self.system.ashraeiam(self.aoi)
+        self.aoi_modifier = self.system.get_iam(self.aoi, iam_model='ashrae')
         return self
 
     def physical_aoi_loss(self):
-        self.aoi_modifier = self.system.physicaliam(self.aoi)
+        self.aoi_modifier = self.system.get_iam(self.aoi, iam_model='physical')
         return self
 
     def sapm_aoi_loss(self):
-        self.aoi_modifier = self.system.sapm_aoi_loss(self.aoi)
+        self.aoi_modifier = self.system.get_iam(self.aoi, iam_model='sapm')
+        return self
+
+    def martin_ruiz_aoi_loss(self):
+        self.aoi_modifier = self.system.get_iam(self.aoi,
+                                                iam_model='martin_ruiz')
         return self
 
     def no_aoi_loss(self):
diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py
index bebe2f1..feaf9c4 100644
--- a/pvlib/pvsystem.py
+++ b/pvlib/pvsystem.py
@@ -13,9 +13,9 @@ import pandas as pd
 
 from pvlib._deprecation import deprecated
 
-from pvlib import (atmosphere, irradiance, singlediode as _singlediode,
+from pvlib import (atmosphere, iam, irradiance, singlediode as _singlediode,
                    temperature)
-from pvlib.tools import _build_kwargs, cosd, asind, sind, tand
+from pvlib.tools import _build_kwargs
 from pvlib.location import Location
 from pvlib._deprecation import pvlibDeprecationWarning
 
@@ -25,7 +25,7 @@ DC_MODEL_PARAMS = {
     'sapm': set([
         'A0', 'A1', 'A2', 'A3', 'A4', 'B0', 'B1', 'B2', 'B3',
         'B4', 'B5', 'C0', 'C1', 'C2', 'C3', 'C4', 'C5', 'C6',
-        'C7', 'Isco', 'Impo', 'Aisc', 'Aimp', 'Bvoco',
+        'C7', 'Isco', 'Impo', 'Voco', 'Vmpo', 'Aisc', 'Aimp', 'Bvoco',
         'Mbvoc', 'Bvmpo', 'Mbvmp', 'N', 'Cells_in_Series',
         'IXO', 'IXXO', 'FD']),
     'desoto': set([
@@ -316,52 +316,66 @@ class PVSystem(object):
                                                albedo=self.albedo,
                                                **kwargs)
 
-    def ashraeiam(self, aoi):
+    def get_iam(self, aoi, iam_model='physical'):
         """
-        Determine the incidence angle modifier using
-        ``self.module_parameters['b']``, ``aoi``,
-        and the :py:func:`ashraeiam` function.
+        Determine the incidence angle modifier using the method specified by
+        ``iam_model``.
 
-        Uses default arguments if keys not in module_parameters.
+        Parameters for the selected IAM model are expected to be in
+        ``PVSystem.module_parameters``. Default parameters are available for
+        the 'physical', 'ashrae' and 'martin_ruiz' models.
 
         Parameters
         ----------
         aoi : numeric
             The angle of incidence in degrees.
 
+        aoi_model : string, default 'physical'
+            The IAM model to be used. Valid strings are 'physical', 'ashrae',
+            'martin_ruiz' and 'sapm'.
+
         Returns
         -------
-        modifier : numeric
+        iam : numeric
             The AOI modifier.
+
+        Raises
+        ------
+        ValueError if `iam_model` is not a valid model name.
         """
-        kwargs = _build_kwargs(['b'], self.module_parameters)
+        model = iam_model.lower()
+        if model in ['ashrae', 'physical', 'martin_ruiz']:
+            param_names = iam.IAM_MODEL_PARAMS[model]
+            kwargs = _build_kwargs(param_names, self.module_parameters)
+            func = getattr(iam, model)
+            return func(aoi, **kwargs)
+        elif model == 'sapm':
+            return iam.sapm(aoi, self.module_parameters)
+        elif model == 'interp':
+            raise ValueError(model + ' is not implemented as an IAM model'
+                             'option for PVSystem')
+        else:
+            raise ValueError(model + ' is not a valid IAM model')
 
-        return ashraeiam(aoi, **kwargs)
+    def ashraeiam(self, aoi):
+        """
+        Deprecated. Use ``PVSystem.get_iam`` instead.
+        """
+        import warnings
+        warnings.warn('PVSystem.ashraeiam is deprecated and will be removed in'
+                      'v0.8, use PVSystem.get_iam instead',
+                      pvlibDeprecationWarning)
+        return PVSystem.get_iam(self, aoi, iam_model='ashrae')
 
     def physicaliam(self, aoi):
         """
-        Determine the incidence angle modifier using ``aoi``,
-        ``self.module_parameters['K']``,
-        ``self.module_parameters['L']``,
-        ``self.module_parameters['n']``,
-        and the
-        :py:func:`physicaliam` function.
-
-        Uses default arguments if keys not in module_parameters.
-
-        Parameters
-        ----------
-        aoi : numeric
-            The angle of incidence in degrees.
-
-        Returns
-        -------
-        modifier : numeric
-            The AOI modifier.
+        Deprecated. Use ``PVSystem.get_iam`` instead.
         """
-        kwargs = _build_kwargs(['K', 'L', 'n'], self.module_parameters)
-
-        return physicaliam(aoi, **kwargs)
+        import warnings
+        warnings.warn('PVSystem.physicaliam is deprecated and will be removed'
+                      ' in v0.8, use PVSystem.get_iam instead',
+                      pvlibDeprecationWarning)
+        return PVSystem.get_iam(self, aoi, iam_model='physical')
 
     def calcparams_desoto(self, effective_irradiance, temp_cell, **kwargs):
         """
@@ -530,20 +544,13 @@ class PVSystem(object):
 
     def sapm_aoi_loss(self, aoi):
         """
-        Use the :py:func:`sapm_aoi_loss` function, the input parameters,
-        and ``self.module_parameters`` to calculate F2.
-
-        Parameters
-        ----------
-        aoi : numeric
-            Angle of incidence in degrees.
-
-        Returns
-        -------
-        F2 : numeric
-            The SAPM angle of incidence loss coefficient.
+        Deprecated. Use ``PVSystem.get_iam`` instead.
         """
-        return sapm_aoi_loss(aoi, self.module_parameters)
+        import warnings
+        warnings.warn('PVSystem.sapm_aoi_loss is deprecated and will be'
+                      ' removed in v0.8, use PVSystem.get_iam instead',
+                      pvlibDeprecationWarning)
+        return PVSystem.get_iam(self, aoi, iam_model='sapm')
 
     def sapm_effective_irradiance(self, poa_direct, poa_diffuse,
                                   airmass_absolute, aoi,
@@ -955,333 +962,6 @@ def systemdef(meta, surface_tilt, surface_azimuth, albedo, modules_per_string,
     return system
 
 
-def ashraeiam(aoi, b=0.05):
-    '''
-    Determine the incidence angle modifier using the ASHRAE transmission
-    model.
-
-    ashraeiam calculates the incidence angle modifier as developed in
-    [1], and adopted by ASHRAE (American Society of Heating,
-    Refrigeration, and Air Conditioning Engineers) [2]. The model has
-    been used by model programs such as PVSyst [3].
-
-    Note: For incident angles near 90 degrees, this model has a
-    discontinuity which has been addressed in this function.
-
-    Parameters
-    ----------
-    aoi : numeric
-        The angle of incidence between the module normal vector and the
-        sun-beam vector in degrees. Angles of nan will result in nan.
-
-    b : float, default 0.05
-        A parameter to adjust the modifier as a function of angle of
-        incidence. Typical values are on the order of 0.05 [3].
-
-    Returns
-    -------
-    IAM : numeric
-        The incident angle modifier calculated as 1-b*(sec(aoi)-1) as
-        described in [2,3].
-
-        Returns zeros for all abs(aoi) >= 90 and for all IAM values that
-        would be less than 0.
-
-    References
-    ----------
-    [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.
-
-    [2] ASHRAE standard 93-77
-
-    [3] PVsyst Contextual Help.
-    http://files.pvsyst.com/help/index.html?iam_loss.htm retrieved on
-    September 10, 2012
-
-    See Also
-    --------
-    irradiance.aoi
-    physicaliam
-    '''
-
-    iam = 1 - b * ((1 / np.cos(np.radians(aoi)) - 1))
-    aoi_gte_90 = np.full_like(aoi, False, dtype='bool')
-    np.greater_equal(np.abs(aoi), 90, where=~np.isnan(aoi), out=aoi_gte_90)
-    iam = np.where(aoi_gte_90, 0, iam)
-    iam = np.maximum(0, iam)
-
-    if isinstance(aoi, pd.Series):
-        iam = pd.Series(iam, index=aoi.index)
-
-    return iam
-
-
-def physicaliam(aoi, n=1.526, K=4., L=0.002):
-    '''
-    Determine the incidence angle modifier using refractive index,
-    extinction coefficient, and glazing thickness.
-
-    physicaliam calculates the incidence angle modifier as described in
-    De Soto et al. "Improvement and validation of a model for
-    photovoltaic array performance", section 3. The calculation is based
-    on a physical model of absorbtion and transmission through a
-    cover.
-
-    Note: The authors of this function believe that eqn. 14 in [1] is
-    incorrect. This function uses the following equation in its place:
-    theta_r = arcsin(1/n * sin(aoi))
-
-    Parameters
-    ----------
-    aoi : numeric
-        The angle of incidence between the module normal vector and the
-        sun-beam vector in degrees. Angles of 0 are replaced with 1e-06
-        to ensure non-nan results. Angles of nan will result in nan.
-
-    n : numeric, default 1.526
-        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, default 4.0
-        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 : numeric, default 0.002
-        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.
-
-    Returns
-    -------
-    iam : numeric
-        The incident angle modifier
-
-    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] Duffie, John A. & Beckman, William A.. (2006). Solar Engineering
-    of Thermal Processes, third edition. [Books24x7 version] Available
-    from http://common.books24x7.com/toc.aspx?bookid=17160.
-
-    See Also
-    --------
-    getaoi
-    ephemeris
-    spa
-    ashraeiam
-    '''
-    zeroang = 1e-06
-
-    # hold a new reference to the input aoi object since we're going to
-    # overwrite the aoi reference below, but we'll need it for the
-    # series check at the end of the function
-    aoi_input = aoi
-
-    aoi = np.where(aoi == 0, zeroang, aoi)
-
-    # angle of reflection
-    thetar_deg = asind(1.0 / n * (sind(aoi)))
-
-    # reflectance and transmittance for normal incidence light
-    rho_zero = ((1-n) / (1+n)) ** 2
-    tau_zero = np.exp(-K*L)
-
-    # reflectance for parallel and perpendicular polarized light
-    rho_para = (tand(thetar_deg - aoi) / tand(thetar_deg + aoi)) ** 2
-    rho_perp = (sind(thetar_deg - aoi) / sind(thetar_deg + aoi)) ** 2
-
-    # transmittance for non-normal light
-    tau = np.exp(-K * L / cosd(thetar_deg))
-
-    # iam is ratio of non-normal to normal incidence transmitted light
-    # after deducting the reflected portion of each
-    iam = ((1 - (rho_para + rho_perp) / 2) / (1 - rho_zero) * tau / tau_zero)
-
-    with np.errstate(invalid='ignore'):
-        # angles near zero produce nan, but iam is defined as one
-        small_angle = 1e-06
-        iam = np.where(np.abs(aoi) < small_angle, 1.0, iam)
-
-        # angles at 90 degrees can produce tiny negative values,
-        # which should be zero. this is a result of calculation precision
-        # rather than the physical model
-        iam = np.where(iam < 0, 0, iam)
-
-        # for light coming from behind the plane, none can enter the module
-        iam = np.where(aoi > 90, 0, iam)
-
-    if isinstance(aoi_input, pd.Series):
-        iam = pd.Series(iam, index=aoi_input.index)
-
-    return iam
-
-
-def iam_martin_ruiz(aoi, a_r=0.16):
-    '''
-    Determine the incidence angle modifier (iam) using the Martin
-    and Ruiz incident angle model.
-
-    Parameters
-    ----------
-    aoi : numeric, degrees
-        The angle of incidence between the module normal vector and the
-        sun-beam vector in degrees. Theta must be a numeric scalar or vector.
-        iam is 0 where |aoi| > 90.
-
-    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 a positive numeric scalar or vector (same length as aoi).
-
-    Returns
-    -------
-    iam : numeric
-        The incident angle modifier(s)
-
-    Notes
-    -----
-    iam_martin_ruiz calculates the incidence angle modifier (iam)
-    as described by Martin and Ruiz in [1]. The information
-    required is the incident angle (aoi) and the angular losses
-    coefficient (a_r). Please note that [1] has a corrigendum which makes
-    the document much simpler to understand.
-
-    The incident angle modifier is defined as
-    [1-exp(-cos(aoi/ar))] / [1-exp(-1/ar)], which is
-    presented as AL(alpha) = 1 - IAM in equation 4 of [1]. Thus IAM is
-    equal to 1 at aoi = 0, and equal to 0 at aoi = 90.  This equation is only
-    valid for -90 <= aoi <= 90, therefore iam must be constrained to 0.0
-    beyond this range.
-
-    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.
-
-    See Also
-    --------
-    physicaliam
-    ashraeiam
-    iam_interp
-    '''
-    # Contributed by Anton Driesse (@adriesse), PV Performance Labs. July, 2019
-
-    aoi_input = aoi
-
-    aoi = np.asanyarray(aoi)
-    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.")
-
-    with np.errstate(invalid='ignore'):
-        iam = (1 - np.exp(-cosd(aoi) / a_r)) / (1 - np.exp(-1 / a_r))
-        iam = np.where(np.abs(aoi) >= 90.0, 0.0, iam)
-
-    if isinstance(aoi_input, pd.Series):
-        iam = pd.Series(iam, index=aoi_input.index)
-
-    return iam
-
-
-def iam_interp(aoi, theta_ref, iam_ref, method='linear', normalize=True):
-    '''
-    Determine the incidence angle modifier (iam) by interpolating a set of
-    reference values, which are usually measured values.
-
-    Parameters
-    ----------
-    aoi : numeric, degrees
-        The angle of incidence between the module normal vector and the
-        sun-beam vector in degrees.
-
-    theta_ref : numeric, degrees
-        Vector of angles at which the iam is known.
-
-    iam_ref : numeric, unitless
-        iam values for each angle in theta_ref.
-
-    method : str, default 'linear'
-        Specifies the interpolation method.
-        Useful options are: 'linear', 'quadratic','cubic'.
-        See scipy.interpolate.interp1d for more options.
-
-    normalize : boolean
-        When true, the interpolated values are divided by the interpolated
-        value at zero degrees.  This ensures that the iam at normal
-        incidence is equal to 1.0.
-
-    Returns
-    -------
-    iam : numeric
-        The incident angle modifier(s)
-
-    Notes:
-    ------
-    theta_ref must have two or more points and may span any range of angles.
-    Typically there will be a dozen or more points in the range 0-90 degrees.
-    iam beyond the range of theta_ref are extrapolated, but constrained to be
-    non-negative.
-
-    The sign of aoi is ignored; only the magnitude is used.
-
-    See Also
-    --------
-    physicaliam
-    ashraeiam
-    iam_martin_ruiz
-    '''
-    # Contributed by Anton Driesse (@adriesse), PV Performance Labs. July, 2019
-
-    from scipy.interpolate import interp1d
-
-    # Scipy doesn't give the clearest feedback, so check number of points here.
-    MIN_REF_VALS = {'linear': 2, 'quadratic': 3, 'cubic': 4, 1: 2, 2: 3, 3: 4}
-
-    if len(theta_ref) < MIN_REF_VALS.get(method, 2):
-        raise ValueError("Too few reference points defined "
-                         "for interpolation method '%s'." % method)
-
-    if np.any(np.less(iam_ref, 0)):
-        raise ValueError("Negative value(s) found in 'iam_ref'. "
-                         "This is not physically possible.")
-
-    interpolator = interp1d(theta_ref, iam_ref, kind=method,
-                            fill_value='extrapolate')
-    aoi_input = aoi
-
-    aoi = np.asanyarray(aoi)
-    aoi = np.abs(aoi)
-    iam = interpolator(aoi)
-    iam = np.clip(iam, 0, None)
-
-    if normalize:
-        iam /= interpolator(0)
-
-    if isinstance(aoi_input, pd.Series):
-        iam = pd.Series(iam, index=aoi_input.index)
-
-    return iam
-
-
 def calcparams_desoto(effective_irradiance, temp_cell,
                       alpha_sc, a_ref, I_L_ref, I_o_ref, R_sh_ref, R_s,
                       EgRef=1.121, dEgdT=-0.0002677,
@@ -1800,7 +1480,10 @@ def retrieve_sam(name=None, path=None):
 
     Notes
     -----
-    Files available at https://sam.nrel.gov/sites/default/files/
+    Files available at
+        https://github.com/NREL/SAM/tree/develop/deploy/libraries
+    Documentation for module and inverter data sets:
+        https://sam.nrel.gov/photovoltaic/pv-sub-page-2.html
 
     Examples
     --------
@@ -1940,6 +1623,8 @@ def sapm(effective_irradiance, temp_cell, module):
                        Imp, Vmp, Ix, and Ixx to effective irradiance
     Isco               Short circuit current at reference condition (amps)
     Impo               Maximum power current at reference condition (amps)
+    Voco               Open circuit voltage at reference condition (amps)
+    Vmpo               Maximum power voltage at reference condition (amps)
     Aisc               Short circuit current temperature coefficient at
                        reference condition (1/C)
     Aimp               Maximum power current temperature coefficient at
@@ -2153,67 +1838,6 @@ def sapm_spectral_loss(airmass_absolute, module):
     return spectral_loss
 
 
-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
-        zeros.
-
-    module : dict-like
-        A dict, Series, or DataFrame defining the SAPM performance
-        parameters. See the :py:func:`sapm` notes section for more
-        details.
-
-    upper : None or float, default None
-        Upper limit on the results.
-
-    Returns
-    -------
-    F2 : numeric
-        The SAPM angle of incidence loss coefficient.
-
-    Notes
-    -----
-    The SAPM traditionally does not define an upper limit on the AOI
-    loss function and values slightly exceeding 1 may exist for moderate
-    angles of incidence (15-40 degrees). However, users may consider
-    imposing an upper limit of 1.
-
-    References
-    ----------
-    [1] King, D. et al, 2004, "Sandia Photovoltaic Array Performance
-    Model", SAND Report 3535, Sandia National Laboratories, Albuquerque,
-    NM.
-
-    [2] B.H. King et al, "Procedure to Determine Coefficients for the
-    Sandia Array Performance Model (SAPM)," SAND2016-5284, Sandia
-    National Laboratories (2016).
-
-    [3] B.H. King et al, "Recent Advancements in Outdoor Measurement
-    Techniques for Angle of Incidence Effects," 42nd IEEE PVSC (2015).
-    DOI: 10.1109/PVSC.2015.7355849
-    """
-
-    aoi_coeff = [module['B5'], module['B4'], module['B3'], module['B2'],
-                 module['B1'], module['B0']]
-
-    aoi_loss = np.polyval(aoi_coeff, aoi)
-    aoi_loss = np.clip(aoi_loss, 0, upper)
-    # nan tolerant masking
-    aoi_lt_0 = np.full_like(aoi, False, dtype='bool')
-    np.less(aoi, 0, where=~np.isnan(aoi), out=aoi_lt_0)
-    aoi_loss = np.where(aoi_lt_0, 0, aoi_loss)
-
-    if isinstance(aoi, pd.Series):
-        aoi_loss = pd.Series(aoi_loss, aoi.index)
-
-    return aoi_loss
-
-
 def sapm_effective_irradiance(poa_direct, poa_diffuse, airmass_absolute, aoi,
                               module, reference_irradiance=1000):
     """
@@ -2249,7 +1873,7 @@ def sapm_effective_irradiance(poa_direct, poa_diffuse, airmass_absolute, aoi,
     """
 
     F1 = sapm_spectral_loss(airmass_absolute, module)
-    F2 = sapm_aoi_loss(aoi, module)
+    F2 = iam.sapm(aoi, module)
 
     E0 = reference_irradiance
 
@@ -3127,3 +2751,15 @@ def pvwatts_ac(pdc, pdc0, eta_inv_nom=0.96, eta_inv_ref=0.9637):
     pac = np.maximum(0, pac)     # GH 541
 
     return pac
+
+
+ashraeiam = deprecated('0.7', alternative='iam.ashrae', name='ashraeiam',
+                       removal='0.8')(iam.ashrae)
+
+
+physicaliam = deprecated('0.7', alternative='iam.physical', name='physicaliam',
+                         removal='0.8')(iam.physical)
+
+
+sapm_aoi_loss = deprecated('0.7', alternative='iam.sapm', name='sapm_aoi_loss',
+                           removal='0.8')(iam.sapm)


Test patch:
diff --git a/pvlib/test/conftest.py b/pvlib/test/conftest.py
index 8789bf9..9c83824 100644
--- a/pvlib/test/conftest.py
+++ b/pvlib/test/conftest.py
@@ -365,3 +365,50 @@ def sapm_temperature_cs5p_220m():
     # SAPM temperature model parameters for Canadian_Solar_CS5P_220M
     # (glass/polymer) in open rack
     return {'a': -3.40641, 'b': -0.0842075, 'deltaT': 3}
+
+
+@pytest.fixture(scope='function')
+def sapm_module_params():
+    """
+    Define SAPM model parameters for Canadian Solar CS5P 220M module.
+
+    The scope of the fixture is set to ``'function'`` to allow tests to modify
+    parameters if required without affecting other tests.
+    """
+    parameters = {'Material': 'c-Si',
+                  'Cells_in_Series': 96,
+                  'Parallel_Strings': 1,
+                  'A0': 0.928385,
+                  'A1': 0.068093,
+                  'A2': -0.0157738,
+                  'A3': 0.0016606,
+                  'A4': -6.93E-05,
+                  'B0': 1,
+                  'B1': -0.002438,
+                  'B2': 0.0003103,
+                  'B3': -0.00001246,
+                  'B4': 2.11E-07,
+                  'B5': -1.36E-09,
+                  'C0': 1.01284,
+                  'C1': -0.0128398,
+                  'C2': 0.279317,
+                  'C3': -7.24463,
+                  'C4': 0.996446,
+                  'C5': 0.003554,
+                  'C6': 1.15535,
+                  'C7': -0.155353,
+                  'Isco': 5.09115,
+                  'Impo': 4.54629,
+                  'Voco': 59.2608,
+                  'Vmpo': 48.3156,
+                  'Aisc': 0.000397,
+                  'Aimp': 0.000181,
+                  'Bvoco': -0.21696,
+                  'Mbvoc': 0.0,
+                  'Bvmpo': -0.235488,
+                  'Mbvmp': 0.0,
+                  'N': 1.4032,
+                  'IXO': 4.97599,
+                  'IXXO': 3.18803,
+                  'FD': 1}
+    return parameters
diff --git a/pvlib/test/test_iam.py b/pvlib/test/test_iam.py
new file mode 100644
index 0000000..0c51b00
--- /dev/null
+++ b/pvlib/test/test_iam.py
@@ -0,0 +1,177 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Wed Oct  2 10:14:16 2019
+
+@author: cwhanse
+"""
+
+import numpy as np
+import pandas as pd
+
+import pytest
+from pandas.util.testing import assert_series_equal
+from numpy.testing import assert_allclose
+
+from pvlib import iam as _iam
+from conftest import needs_numpy_1_10, requires_scipy
+
+
+@needs_numpy_1_10
+def test_ashrae():
+    thetas = np.array([-90., -67.5, -45., -22.5, 0., 22.5, 45., 67.5, 89., 90.,
+                       np.nan])
+    expected = np.array([0, 0.9193437, 0.97928932, 0.99588039, 1., 0.99588039,
+                         0.97928932, 0.9193437, 0, 0, np.nan])
+    iam = _iam.ashrae(thetas, .05)
+    assert_allclose(iam, expected, equal_nan=True)
+    iam_series = _iam.ashrae(pd.Series(thetas))
+    assert_series_equal(iam_series, pd.Series(expected))
+
+
+@needs_numpy_1_10
+def test_ashrae_scalar():
+    thetas = -45.
+    iam = _iam.ashrae(thetas, .05)
+    expected = 0.97928932
+    assert_allclose(iam, expected, equal_nan=True)
+    thetas = np.nan
+    iam = _iam.ashrae(thetas, .05)
+    expected = np.nan
+    assert_allclose(iam, expected, equal_nan=True)
+
+
+@needs_numpy_1_10
+def test_physical():
+    aoi = np.array([-90., -67.5, -45., -22.5, 0., 22.5, 45., 67.5, 90.,
+                    np.nan])
+    expected = np.array([0, 0.8893998, 0.98797788, 0.99926198, 1, 0.99926198,
+                         0.98797788, 0.8893998, 0, np.nan])
+    iam = _iam.physical(aoi, 1.526, 0.002, 4)
+    assert_allclose(iam, expected, equal_nan=True)
+
+    # GitHub issue 397
+    aoi = pd.Series(aoi)
+    iam = _iam.physical(aoi, 1.526, 0.002, 4)
+    expected = pd.Series(expected)
+    assert_series_equal(iam, expected)
+
+
+@needs_numpy_1_10
+def test_physical_scalar():
+    aoi = -45.
+    iam = _iam.physical(aoi, 1.526, 0.002, 4)
+    expected = 0.98797788
+    assert_allclose(iam, expected, equal_nan=True)
+    aoi = np.nan
+    iam = _iam.physical(aoi, 1.526, 0.002, 4)
+    expected = np.nan
+    assert_allclose(iam, expected, equal_nan=True)
+
+
+def test_martin_ruiz():
+
+    aoi = 45.
+    a_r = 0.16
+    expected = 0.98986965
+
+    # 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)
+
+    a_r = 0.18
+    aoi = [-100, -60, 0, 60, 100, np.nan, np.inf]
+    expected = [0.0, 0.9414631, 1.0, 0.9414631, 0.0, np.nan, 0.0]
+
+    # check out of range of inputs as list
+    iam = _iam.martin_ruiz(aoi, a_r)
+    assert_allclose(iam, expected, equal_nan=True)
+
+    # check out of range of inputs as array
+    iam = _iam.martin_ruiz(np.array(aoi), a_r)
+    assert_allclose(iam, expected, equal_nan=True)
+
+    # check out of range of inputs as Series
+    aoi = pd.Series(aoi)
+    expected = pd.Series(expected)
+    iam = _iam.martin_ruiz(aoi, a_r)
+    assert_series_equal(iam, expected)
+
+    # check exception clause
+    with pytest.raises(RuntimeError):
+        _iam.martin_ruiz(0.0, a_r=0.0)
+
+
+@requires_scipy
+def test_iam_interp():
+
+    aoi_meas = [0.0, 45.0, 65.0, 75.0]
+    iam_meas = [1.0,  0.9,  0.8,  0.6]
+
+    # simple default linear method
+    aoi = 55.0
+    expected = 0.85
+    iam = _iam.interp(aoi, aoi_meas, iam_meas)
+    assert_allclose(iam, expected)
+
+    # simple non-default method
+    aoi = 55.0
+    expected = 0.8878062
+    iam = _iam.interp(aoi, aoi_meas, iam_meas, method='cubic')
+    assert_allclose(iam, expected)
+
+    # check with all reference values
+    aoi = aoi_meas
+    expected = iam_meas
+    iam = _iam.interp(aoi, aoi_meas, iam_meas)
+    assert_allclose(iam, expected)
+
+    # check normalization and Series
+    aoi = pd.Series(aoi)
+    expected = pd.Series(expected)
+    iam_mult = np.multiply(0.9, iam_meas)
+    iam = _iam.interp(aoi, aoi_meas, iam_mult, normalize=True)
+    assert_series_equal(iam, expected)
+
+    # check beyond reference values
+    aoi = [-45, 0, 45, 85, 90, 95, 100, 105, 110]
+    expected = [0.9, 1.0, 0.9, 0.4, 0.3, 0.2, 0.1, 0.0, 0.0]
+    iam = _iam.interp(aoi, aoi_meas, iam_meas)
+    assert_allclose(iam, expected)
+
+    # check exception clause
+    with pytest.raises(ValueError):
+        _iam.interp(0.0, [0], [1])
+
+    # check exception clause
+    with pytest.raises(ValueError):
+        _iam.interp(0.0, [0, 90], [1, -1])
+
+
+@pytest.mark.parametrize('aoi,expected', [
+    (45, 0.9975036250000002),
+    (np.array([[-30, 30, 100, np.nan]]),
+     np.array([[0, 1.007572, 0, np.nan]])),
+    (pd.Series([80]), pd.Series([0.597472]))
+])
+def test_sapm(sapm_module_params, aoi, expected):
+
+    out = _iam.sapm(aoi, sapm_module_params)
+
+    if isinstance(aoi, pd.Series):
+        assert_series_equal(out, expected, check_less_precise=4)
+    else:
+        assert_allclose(out, expected, atol=1e-4)
+
+
+def test_sapm_limits():
+    module_parameters = {'B0': 5, 'B1': 0, 'B2': 0, 'B3': 0, 'B4': 0, 'B5': 0}
+    assert _iam.sapm(1, module_parameters) == 5
+
+    module_parameters = {'B0': 5, 'B1': 0, 'B2': 0, 'B3': 0, 'B4': 0, 'B5': 0}
+    assert _iam.sapm(1, module_parameters, upper=1) == 1
+
+    module_parameters = {'B0': -5, 'B1': 0, 'B2': 0, 'B3': 0, 'B4': 0, 'B5': 0}
+    assert _iam.sapm(1, module_parameters) == 0
diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py
index 4623d9f..c4d7c7a 100644
--- a/pvlib/test/test_modelchain.py
+++ b/pvlib/test/test_modelchain.py
@@ -3,7 +3,7 @@ import sys
 import numpy as np
 import pandas as pd
 
-from pvlib import modelchain, pvsystem, temperature
+from pvlib import iam, modelchain, pvsystem, temperature
 from pvlib.modelchain import ModelChain
 from pvlib.pvsystem import PVSystem
 from pvlib.tracking import SingleAxisTracker
@@ -16,11 +16,11 @@ import pytest
 from conftest import fail_on_pvlib_version, requires_scipy, requires_tables
 
 
-@pytest.fixture
-def system(sam_data, cec_inverter_parameters, sapm_temperature_cs5p_220m):
-    modules = sam_data['sandiamod']
+@pytest.fixture(scope='function')
+def system(sapm_module_params, cec_inverter_parameters,
+           sapm_temperature_cs5p_220m):
     module = 'Canadian_Solar_CS5P_220M___2009_'
-    module_parameters = modules[module].copy()
+    module_parameters = sapm_module_params.copy()
     temp_model_params = sapm_temperature_cs5p_220m.copy()
     system = PVSystem(surface_tilt=32.2, surface_azimuth=180,
                       module=module,
@@ -101,7 +101,7 @@ def pvwatts_dc_snl_ac_system(cec_inverter_parameters):
     return system
 
 
-@pytest.fixture
+@pytest.fixture(scope="function")
 def pvwatts_dc_pvwatts_ac_system(sapm_temperature_cs5p_220m):
     module_parameters = {'pdc0': 220, 'gamma_pdc': -0.003}
     temp_model_params = sapm_temperature_cs5p_220m.copy()
@@ -113,6 +113,21 @@ def pvwatts_dc_pvwatts_ac_system(sapm_temperature_cs5p_220m):
     return system
 
 
+@pytest.fixture(scope="function")
+def system_no_aoi(cec_module_cs5p_220m, sapm_temperature_cs5p_220m,
+                  cec_inverter_parameters):
+    module_parameters = cec_module_cs5p_220m.copy()
+    module_parameters['EgRef'] = 1.121
+    module_parameters['dEgdT'] = -0.0002677
+    temp_model_params = sapm_temperature_cs5p_220m.copy()
+    inverter_parameters = cec_inverter_parameters.copy()
+    system = PVSystem(surface_tilt=32.2, surface_azimuth=180,
+                      module_parameters=module_parameters,
+                      temperature_model_parameters=temp_model_params,
+                      inverter_parameters=inverter_parameters)
+    return system
+
+
 @pytest.fixture
 def location():
     return Location(32.2, -111, altitude=700)
@@ -405,14 +420,14 @@ def constant_aoi_loss(mc):
     mc.aoi_modifier = 0.9
 
 
-@pytest.mark.parametrize('aoi_model, method', [
-    ('sapm', 'sapm_aoi_loss'), ('ashrae', 'ashraeiam'),
-    ('physical', 'physicaliam')])
-def test_aoi_models(system, location, aoi_model, method, weather, mocker):
+@pytest.mark.parametrize('aoi_model', [
+    'sapm', 'ashrae', 'physical', 'martin_ruiz'
+])
+def test_aoi_models(system, location, aoi_model, weather, mocker):
     mc = ModelChain(system, location, dc_model='sapm',
                     aoi_model=aoi_model, spectral_model='no_loss')
-    m = mocker.spy(system, method)
-    mc.run_model(weather)
+    m = mocker.spy(system, 'get_iam')
+    mc.run_model(weather=weather)
     assert m.call_count == 1
     assert isinstance(mc.ac, pd.Series)
     assert not mc.ac.empty
@@ -442,6 +457,25 @@ def test_aoi_model_user_func(system, location, weather, mocker):
     assert mc.ac[1] < 1
 
 
+@pytest.mark.parametrize('aoi_model', [
+    'sapm', 'ashrae', 'physical', 'martin_ruiz'
+])
+def test_infer_aoi_model(location, system_no_aoi, aoi_model):
+    for k in iam.IAM_MODEL_PARAMS[aoi_model]:
+        system_no_aoi.module_parameters.update({k: 1.0})
+    mc = ModelChain(system_no_aoi, location,
+                    orientation_strategy='None',
+                    spectral_model='no_loss')
+    assert isinstance(mc, ModelChain)
+
+
+def test_infer_aoi_model_invalid(location, system_no_aoi):
+    with pytest.raises(ValueError) as excinfo:
+        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):
     mc.spectral_modifier = 0.9
 
diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py
index 117b416..8157c57 100644
--- a/pvlib/test/test_pvsystem.py
+++ b/pvlib/test/test_pvsystem.py
@@ -1,6 +1,5 @@
 import inspect
 import os
-import datetime
 from collections import OrderedDict
 
 import numpy as np
@@ -12,10 +11,8 @@ from pandas.util.testing import assert_series_equal, assert_frame_equal
 from numpy.testing import assert_allclose
 
 from pvlib import pvsystem
-from pvlib import clearsky
-from pvlib import irradiance
 from pvlib import atmosphere
-from pvlib import solarposition
+from pvlib import iam as _iam
 from pvlib.location import Location
 from pvlib import temperature
 from pvlib._deprecation import pvlibDeprecationWarning
@@ -82,162 +79,39 @@ def test_systemdef_dict():
     assert expected == pvsystem.systemdef(meta, 5, 0, .1, 5, 5)
 
 
-@needs_numpy_1_10
-def test_ashraeiam():
-    thetas = np.array([-90. , -67.5, -45. , -22.5,   0. ,  22.5,  45. ,  67.5, 89.,  90. , np.nan])
-    iam = pvsystem.ashraeiam(thetas, .05)
-    expected = np.array([        0,  0.9193437 ,  0.97928932,  0.99588039,  1.        ,
-        0.99588039,  0.97928932,  0.9193437 ,         0, 0,  np.nan])
-    assert_allclose(iam, expected, equal_nan=True)
-
-
-@needs_numpy_1_10
-def test_ashraeiam_scalar():
-    thetas = -45.
-    iam = pvsystem.ashraeiam(thetas, .05)
-    expected = 0.97928932
-    assert_allclose(iam, expected, equal_nan=True)
-    thetas = np.nan
-    iam = pvsystem.ashraeiam(thetas, .05)
-    expected = np.nan
-    assert_allclose(iam, expected, equal_nan=True)
-
-
-def test_PVSystem_ashraeiam(mocker):
-    mocker.spy(pvsystem, 'ashraeiam')
-    module_parameters = pd.Series({'b': 0.05})
-    system = pvsystem.PVSystem(module_parameters=module_parameters)
-    thetas = 1
-    iam = system.ashraeiam(thetas)
-    pvsystem.ashraeiam.assert_called_once_with(thetas, b=0.05)
-    assert iam < 1.
-
-
-@needs_numpy_1_10
-def test_physicaliam():
-    aoi = np.array([-90. , -67.5, -45. , -22.5,   0. ,  22.5,  45. ,  67.5,  90. , np.nan])
-    iam = pvsystem.physicaliam(aoi, 1.526, 0.002, 4)
-    expected = np.array([        0,  0.8893998,  0.98797788,  0.99926198,         1,
-        0.99926198,  0.98797788,  0.8893998,         0, np.nan])
-    assert_allclose(iam, expected, equal_nan=True)
-
-    # GitHub issue 397
-    aoi = pd.Series(aoi)
-    iam = pvsystem.physicaliam(aoi, 1.526, 0.002, 4)
-    expected = pd.Series(expected)
-    assert_series_equal(iam, expected)
-
-
-@needs_numpy_1_10
-def test_physicaliam_scalar():
-    aoi = -45.
-    iam = pvsystem.physicaliam(aoi, 1.526, 0.002, 4)
-    expected = 0.98797788
-    assert_allclose(iam, expected, equal_nan=True)
-    aoi = np.nan
-    iam = pvsystem.physicaliam(aoi, 1.526, 0.002, 4)
-    expected = np.nan
-    assert_allclose(iam, expected, equal_nan=True)
-
-
-def test_PVSystem_physicaliam(mocker):
-    module_parameters = pd.Series({'K': 4, 'L': 0.002, 'n': 1.526})
-    system = pvsystem.PVSystem(module_parameters=module_parameters)
-    mocker.spy(pvsystem, 'physicaliam')
+@pytest.mark.parametrize('iam_model,model_params', [
+    ('ashrae', {'b': 0.05}),
+    ('physical', {'K': 4, 'L': 0.002, 'n': 1.526}),
+    ('martin_ruiz', {'a_r': 0.16}),
+])
+def test_PVSystem_get_iam(mocker, iam_model, model_params):
+    m = mocker.spy(_iam, iam_model)
+    system = pvsystem.PVSystem(module_parameters=model_params)
     thetas = 1
-    iam = system.physicaliam(thetas)
-    pvsystem.physicaliam.assert_called_once_with(thetas, **module_parameters)
+    iam = system.get_iam(thetas, iam_model=iam_model)
+    m.assert_called_with(thetas, **model_params)
     assert iam < 1.
 
 
-def test_iam_martin_ruiz():
-
-    aoi = 45.
-    a_r = 0.16
-    expected = 0.98986965
-
-    # will fail of default values change
-    iam = pvsystem.iam_martin_ruiz(aoi)
-    assert_allclose(iam, expected)
-    # will fail of parameter names change
-    iam = pvsystem.iam_martin_ruiz(aoi=aoi, a_r=a_r)
-    assert_allclose(iam, expected)
-
-    a_r = 0.18
-    aoi = [-100, -60, 0, 60, 100, np.nan, np.inf]
-    expected = [0.0, 0.9414631, 1.0, 0.9414631, 0.0, np.nan, 0.0]
-
-    # check out of range of inputs as list
-    iam = pvsystem.iam_martin_ruiz(aoi, a_r)
-    assert_allclose(iam, expected, equal_nan=True)
-
-    # check out of range of inputs as array
-    iam = pvsystem.iam_martin_ruiz(np.array(aoi), a_r)
-    assert_allclose(iam, expected, equal_nan=True)
-
-    # check out of range of inputs as Series
-    aoi = pd.Series(aoi)
-    expected = pd.Series(expected)
-    iam = pvsystem.iam_martin_ruiz(aoi, a_r)
-    assert_series_equal(iam, expected)
-
-    # check exception clause
-    with pytest.raises(RuntimeError):
-        pvsystem.iam_martin_ruiz(0.0, a_r=0.0)
-
+def test_PVSystem_get_iam_sapm(sapm_module_params, mocker):
+    system = pvsystem.PVSystem(module_parameters=sapm_module_params)
+    mocker.spy(_iam, 'sapm')
+    aoi = 0
+    out = system.get_iam(aoi, 'sapm')
+    _iam.sapm.assert_called_once_with(aoi, sapm_module_params)
+    assert_allclose(out, 1.0, atol=0.01)
 
-@requires_scipy
-def test_iam_interp():
-
-    aoi_meas = [0.0, 45.0, 65.0, 75.0]
-    iam_meas = [1.0,  0.9,  0.8,  0.6]
-
-    # simple default linear method
-    aoi = 55.0
-    expected = 0.85
-    iam = pvsystem.iam_interp(aoi, aoi_meas, iam_meas)
-    assert_allclose(iam, expected)
-
-    # simple non-default method
-    aoi = 55.0
-    expected = 0.8878062
-    iam = pvsystem.iam_interp(aoi, aoi_meas, iam_meas, method='cubic')
-    assert_allclose(iam, expected)
-
-    # check with all reference values
-    aoi = aoi_meas
-    expected = iam_meas
-    iam = pvsystem.iam_interp(aoi, aoi_meas, iam_meas)
-    assert_allclose(iam, expected)
-
-    # check normalization and Series
-    aoi = pd.Series(aoi)
-    expected = pd.Series(expected)
-    iam_mult = np.multiply(0.9, iam_meas)
-    iam = pvsystem.iam_interp(aoi, aoi_meas, iam_mult, normalize=True)
-    assert_series_equal(iam, expected)
-
-    # check beyond reference values
-    aoi = [-45, 0, 45, 85, 90, 95, 100, 105, 110]
-    expected = [0.9, 1.0, 0.9, 0.4, 0.3, 0.2, 0.1, 0.0, 0.0]
-    iam = pvsystem.iam_interp(aoi, aoi_meas, iam_meas)
-    assert_allclose(iam, expected)
-
-    # check exception clause
-    with pytest.raises(ValueError):
-        pvsystem.iam_interp(0.0, [0], [1])
 
-    # check exception clause
+def test_PVSystem_get_iam_interp(sapm_module_params, mocker):
+    system = pvsystem.PVSystem(module_parameters=sapm_module_params)
     with pytest.raises(ValueError):
-        pvsystem.iam_interp(0.0, [0, 90], [1, -1])
+        system.get_iam(45, iam_model='interp')
 
 
-@pytest.fixture(scope="session")
-def sapm_module_params(sam_data):
-    modules = sam_data['sandiamod']
-    module = 'Canadian_Solar_CS5P_220M___2009_'
-    module_parameters = modules[module]
-    return module_parameters
+def test_PVSystem_get_iam_invalid(sapm_module_params, mocker):
+    system = pvsystem.PVSystem(module_parameters=sapm_module_params)
+    with pytest.raises(ValueError):
+        system.get_iam(45, iam_model='not_a_model')
 
 
 def test_retrieve_sam_raise_no_parameters():
@@ -356,9 +230,9 @@ def test_sapm(sapm_module_params):
     for k, v in expected.items():
         assert_allclose(out[k], v, atol=1e-4)
 
-    # just make sure it works with a dict input
+    # just make sure it works with Series input
     pvsystem.sapm(effective_irradiance, temp_cell,
-                  sapm_module_params.to_dict())
+                  pd.Series(sapm_module_params))
 
 
 def test_PVSystem_sapm(sapm_module_params, mocker):
@@ -420,42 +294,6 @@ def test_PVSystem_first_solar_spectral_loss(module_parameters, module_type,
     assert_allclose(out, 1, atol=0.5)
 
 
-@pytest.mark.parametrize('aoi,expected', [
-    (45, 0.9975036250000002),
-    (np.array([[-30, 30, 100, np.nan]]),
-     np.array([[0, 1.007572, 0, np.nan]])),
-    (pd.Series([80]), pd.Series([0.597472]))
-])
-def test_sapm_aoi_loss(sapm_module_params, aoi, expected):
-
-    out = pvsystem.sapm_aoi_loss(aoi, sapm_module_params)
-
-    if isinstance(aoi, pd.Series):
-        assert_series_equal(out, expected, check_less_precise=4)
-    else:
-        assert_allclose(out, expected, atol=1e-4)
-
-
-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(1, module_parameters) == 5
-
-    module_parameters = {'B0': 5, 'B1': 0, 'B2': 0, 'B3': 0, 'B4': 0, 'B5': 0}
-    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(1, module_parameters) == 0
-
-
-def test_PVSystem_sapm_aoi_loss(sapm_module_params, mocker):
-    system = pvsystem.PVSystem(module_parameters=sapm_module_params)
-    mocker.spy(pvsystem, 'sapm_aoi_loss')
-    aoi = 0
-    out = system.sapm_aoi_loss(aoi)
-    pvsystem.sapm_aoi_loss.assert_called_once_with(aoi, sapm_module_params)
-    assert_allclose(out, 1.0, atol=0.01)
-
-
 @pytest.mark.parametrize('test_input,expected', [
     ([1000, 100, 5, 45, 1000], 1.1400510967821877),
     ([np.array([np.nan, 1000, 1000]),
@@ -1627,10 +1465,39 @@ def test_deprecated_08():
     with pytest.warns(pvlibDeprecationWarning):
         pvsystem.pvsyst_celltemp(1000, 25)
     module_parameters = {'R_sh_ref': 1, 'a_ref': 1, 'I_o_ref': 1,
-                         'alpha_sc': 1, 'I_L_ref': 1, 'R_s': 1}
+                         'alpha_sc': 1, 'I_L_ref': 1, 'R_s': 1,
+                         'B5': 0.0, 'B4': 0.0, 'B3': 0.0, 'B2': 0.0,
+                         'B1': 0.0, 'B0': 1.0,
+                         'b': 0.05, 'K': 4, 'L': 0.002, 'n': 1.526,
+                         'a_r': 0.16}
+    temp_model_params = temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][
+        'open_rack_glass_glass']
+    # for missing temperature_model_parameters
     with pytest.warns(pvlibDeprecationWarning):
         pvsystem.PVSystem(module_parameters=module_parameters,
                           racking_model='open', module_type='glass_glass')
+    pv = pvsystem.PVSystem(module_parameters=module_parameters,
+                           temperature_model_parameters=temp_model_params,
+                           racking_model='open', module_type='glass_glass')
+    # deprecated method PVSystem.ashraeiam
+    with pytest.warns(pvlibDeprecationWarning):
+        pv.ashraeiam(45)
+    # deprecated function ashraeiam
+    with pytest.warns(pvlibDeprecationWarning):
+        pvsystem.ashraeiam(45)
+    # deprecated method PVSystem.physicaliam
+    with pytest.warns(pvlibDeprecationWarning):
+        pv.physicaliam(45)
+    # deprecated function physicaliam
+    with pytest.warns(pvlibDeprecationWarning):
+        pvsystem.physicaliam(45)
+    # deprecated method PVSystem.sapm_aoi_loss
+    with pytest.warns(pvlibDeprecationWarning):
+        pv.sapm_aoi_loss(45)
+    # deprecated function sapm_aoi_loss
+    with pytest.warns(pvlibDeprecationWarning):
+        pvsystem.sapm_aoi_loss(45, {'B5': 0.0, 'B4': 0.0, 'B3': 0.0, 'B2': 0.0,
+                                    'B1': 0.0, 'B0': 1.0})
 
 
 @fail_on_pvlib_version('0.8')