Repository: pvlib/pvlib-python (python)

Issue:
Remove times kwarg from ModelChain.prepare_inputs and ModelChain.run_model
The `ModelChain.run_model` and `ModelChain.prepare_inputs` methods `times` kwarg is nearly pointless. I'm guessing/hoping that the vast majority of `ModelChain` users supply a `weather` DataFrame with a meaningful `DatetimeIndex`. So, it would greatly simplify the API if we removed the `times` kwarg and inspected  `weather.index` whenever we need timestamp info (e.g. solar position). `ModelChain` is committed to time series modeling using pandas, so I think it's reasonable to require users to supply a meaningful index. 

Current signatures:

```python
    def prepare_inputs(self, times=None, weather=None):
    def run_model(self, times=None, weather=None):
```

proposed signatures:

```python
    def prepare_inputs(self, weather):
    def run_model(self, weather):
```

Pull request:
ModelChain 0.7 deprecations, remove times kwarg
 - [x] Closes #706 
 - [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.

Also removes deprecated 'singlediode' option.

Also fixes incorrect documentation about what happens if `weather=None` or if `weather` does not have air temperature or wind speed.

I'll wait to update the whatsnew until after #772 


Patch:
diff --git a/ci/requirements-py35.yml b/ci/requirements-py35.yml
index 65b5c76..5fc0077 100644
--- a/ci/requirements-py35.yml
+++ b/ci/requirements-py35.yml
@@ -24,4 +24,5 @@ dependencies:
     - shapely  # pvfactors dependency
     - siphon  # conda-forge
     - pip:
+        - nrel-pysam
         - pvfactors==1.0.1
diff --git a/ci/requirements-py36.yml b/ci/requirements-py36.yml
index a21bce2..e415f67 100644
--- a/ci/requirements-py36.yml
+++ b/ci/requirements-py36.yml
@@ -24,4 +24,5 @@ dependencies:
     - shapely  # pvfactors dependency
     - siphon  # conda-forge
     - pip:
+        - nrel-pysam
         - pvfactors==1.0.1
diff --git a/ci/requirements-py37.yml b/ci/requirements-py37.yml
index 3783e8f..1a6809f 100644
--- a/ci/requirements-py37.yml
+++ b/ci/requirements-py37.yml
@@ -24,4 +24,5 @@ dependencies:
     - shapely  # pvfactors dependency
     - siphon  # conda-forge
     - pip:
+        - nrel-pysam
         - pvfactors==1.0.1
diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst
index 8af2bf0..6e93dc5 100644
--- a/docs/sphinx/source/api.rst
+++ b/docs/sphinx/source/api.rst
@@ -294,6 +294,14 @@ PVWatts model
    pvsystem.pvwatts_losses
    pvsystem.pvwatts_losses
 
+Functions for fitting PV models
+-------------------------------
+.. autosummary::
+   :toctree: generated/
+
+    ivtools.fit_sde_sandia
+    ivtools.fit_sdm_cec_sam
+
 Other
 -----
 
diff --git a/docs/sphinx/source/whatsnew/v0.7.0.rst b/docs/sphinx/source/whatsnew/v0.7.0.rst
index d9045a5..378e8cb 100644
--- a/docs/sphinx/source/whatsnew/v0.7.0.rst
+++ b/docs/sphinx/source/whatsnew/v0.7.0.rst
@@ -21,7 +21,7 @@ API Changes
          old `pvsystem.sapm_celltemp` returned a `DataFrame` with both cell and module temperatures.
       - Created `temperature.sapm_module` to return module temperature using the SAPM temperature model.
       - Changed the order of arguments for`pvsystem.sapm_celltemp`,
-        `pvsystem.pvsyst_celltemp` and `PVSystem.sapm_celltemp` to be consistent 
+        `pvsystem.pvsyst_celltemp` and `PVSystem.sapm_celltemp` to be consistent
         among cell temperature model functions.
       - Removed `model` as a kwarg from `temperature.sapm_cell` and
         `temperature.pvsyst_cell`. These functions now require model-specific parameters.
@@ -35,7 +35,7 @@ API Changes
         'open_rack_polymer_thinfilm_steel' and '22x_concentrator_tracker'
         are considered obsolete and have been removed.
    * Changes to `PVSystem` class
-      - Changed the `model` kwarg in `PVSystem.sapm_celltemp` and 
+      - Changed the `model` kwarg in `PVSystem.sapm_celltemp` and
         `PVSystem.pvsyst_celltemp` to `parameter_set`. `parameter_set` expects
         a str which is a valid key for `temperature.TEMPERATURE_MODEL_PARAMETERS`
         for the corresponding temperature model.
@@ -71,6 +71,15 @@ API Changes
 
 * 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
+  :py:meth:`pvlib.modelchain.ModelChain.run_model`,
+  :py:meth:`pvlib.modelchain.ModelChain.prepare_inputs`, and
+  :py:meth:`pvlib.modelchain.ModelChain.complete_irradiance` methods.
+  Model times are now determined by the input `weather`. `DataFrame`.
+  Therefore, the `weather` DataFrame must have a `DatetimeIndex`.
+  The `weather` argument of the above methods is now the first, required
+  positional argument and the `times` argument is kept as the second keyword
+  argument for capability during the deprecation period.
 
 Enhancements
 ~~~~~~~~~~~~
@@ -82,6 +91,11 @@ Enhancements
   dated 2018-3-18 to the SAM file dated 2019-03-05. (:issue:`761`)
 * Added recombination current parameters to bishop88 single-diode functions and also
   to :py:func:`pvlib.pvsystem.max_power_point`. (:issue:`762`)
+* Add `ivtools` module to contain functions for IV model fitting.
+* Add :py:func:`~pvlib.ivtools.fit_sde_sandia`, a simple method to fit
+  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.
 
 Bug fixes
 ~~~~~~~~~
@@ -114,13 +128,16 @@ Removal of prior version deprecations
 * Removed `atmosphere.relativeairmass`.
 * Removed `solarposition.get_sun_rise_set_transit`.
 * Removed `tmy` module.
+* Removed `ModelChain.singlediode` method.
+* Removed `ModelChain.prepare_inputs` clearsky assumption when no irradiance
+  data was provided.
 
 Contributors
 ~~~~~~~~~~~~
 * Mark Campanellli (:ghuser:`markcampanelli`)
 * Will Holmgren (:ghuser:`wholmgren`)
+* Cliff Hansen (:ghuser:`cwhanse`)
 * Oscar Dowson (:ghuser:`odow`)
 * Anton Driesse (:ghuser:`adriesse`)
 * Alexander Morgan (:ghuser:`alexandermorgan`)
-* Cliff Hansen (:ghuser:`cwhanse`)
 * Miguel Sánchez de León Peque (:ghuser:`Peque`)
diff --git a/pvlib/__init__.py b/pvlib/__init__.py
index a1fb92e..f4e3de4 100644
--- a/pvlib/__init__.py
+++ b/pvlib/__init__.py
@@ -7,6 +7,7 @@ from pvlib import irradiance
 from pvlib import location
 from pvlib import solarposition
 from pvlib import iotools
+from pvlib import ivtools
 from pvlib import tracking
 from pvlib import pvsystem
 from pvlib import spa
diff --git a/pvlib/ivtools.py b/pvlib/ivtools.py
new file mode 100644
index 0000000..353a0b8
--- /dev/null
+++ b/pvlib/ivtools.py
@@ -0,0 +1,350 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Fri Mar 29 10:34:10 2019
+
+@author: cwhanse
+"""
+
+import numpy as np
+
+
+def fit_sdm_cec_sam(celltype, v_mp, i_mp, v_oc, i_sc, alpha_sc, beta_voc,
+                    gamma_pmp, cells_in_series, temp_ref=25):
+    """
+    Estimates parameters for the CEC single diode model (SDM) using the SAM
+    SDK.
+
+    Parameters
+    ----------
+    celltype : str
+        Value is one of 'monoSi', 'multiSi', 'polySi', 'cis', 'cigs', 'cdte',
+        'amorphous'
+    v_mp : float
+        Voltage at maximum power point [V]
+    i_mp : float
+        Current at maximum power point [A]
+    v_oc : float
+        Open circuit voltage [V]
+    i_sc : float
+        Short circuit current [A]
+    alpha_sc : float
+        Temperature coefficient of short circuit current [A/C]
+    beta_voc : float
+        Temperature coefficient of open circuit voltage [V/C]
+    gamma_pmp : float
+        Temperature coefficient of power at maximum point point [%/C]
+    cells_in_series : int
+        Number of cells in series
+    temp_ref : float, default 25
+        Reference temperature condition [C]
+
+    Returns
+    -------
+    tuple of the following elements:
+
+        * I_L_ref : float
+            The light-generated current (or photocurrent) at reference
+            conditions [A]
+
+        * I_o_ref : float
+            The dark or diode reverse saturation current at reference
+            conditions [A]
+
+        * R_sh_ref : float
+            The shunt resistance at reference conditions, in ohms.
+
+        * R_s : float
+            The series resistance at reference conditions, in ohms.
+
+        * a_ref : float
+            The product of the usual diode ideality factor ``n`` (unitless),
+            number of cells in series ``Ns``, and cell thermal voltage at
+            reference conditions [V]
+
+        * Adjust : float
+            The adjustment to the temperature coefficient for short circuit
+            current, in percent.
+
+    Raises
+    ------
+        ImportError if NREL-PySAM is not installed.
+
+        RuntimeError if parameter extraction is not successful.
+
+    Notes
+    -----
+    Inputs ``v_mp``, ``v_oc``, ``i_mp`` and ``i_sc`` are assumed to be from a
+    single IV curve at constant irradiance and cell temperature. Irradiance is
+    not explicitly used by the fitting procedure. The irradiance level at which
+    the input IV curve is determined and the specified cell temperature
+    ``temp_ref`` are the reference conditions for the output parameters
+    ``I_L_ref``, ``I_o_ref``, ``R_sh_ref``, ``R_s``, ``a_ref`` and ``Adjust``.
+
+    References
+    ----------
+    [1] A. Dobos, "An Improved Coefficient Calculator for the California
+    Energy Commission 6 Parameter Photovoltaic Module Model", Journal of
+    Solar Energy Engineering, vol 134, 2012.
+    """
+
+    try:
+        from PySAM import PySSC
+    except ImportError:
+        raise ImportError("Requires NREL's PySAM package at "
+                          "https://pypi.org/project/NREL-PySAM/.")
+
+    datadict = {'tech_model': '6parsolve', 'financial_model': 'none',
+                'celltype': celltype, 'Vmp': v_mp,
+                'Imp': i_mp, 'Voc': v_oc, 'Isc': i_sc, 'alpha_isc': alpha_sc,
+                'beta_voc': beta_voc, 'gamma_pmp': gamma_pmp,
+                'Nser': cells_in_series, 'Tref': temp_ref}
+
+    result = PySSC.ssc_sim_from_dict(datadict)
+    if result['cmod_success'] == 1:
+        return tuple([result[k] for k in ['Il', 'Io', 'Rsh', 'Rs', 'a',
+                      'Adj']])
+    else:
+        raise RuntimeError('Parameter estimation failed')
+
+
+def fit_sde_sandia(voltage, current, v_oc=None, i_sc=None, v_mp_i_mp=None,
+                   vlim=0.2, ilim=0.1):
+    r"""
+    Fits the single diode equation (SDE) to an IV curve.
+
+    Parameters
+    ----------
+    voltage : ndarray
+        1D array of `float` type containing voltage at each point on the IV
+        curve, increasing from 0 to ``v_oc`` inclusive [V]
+
+    current : ndarray
+        1D array of `float` type containing current at each point on the IV
+        curve, from ``i_sc`` to 0 inclusive [A]
+
+    v_oc : float, default None
+        Open circuit voltage [V]. If not provided, ``v_oc`` is taken as the
+        last point in the ``voltage`` array.
+
+    i_sc : float, default None
+        Short circuit current [A]. If not provided, ``i_sc`` is taken as the
+        first point in the ``current`` array.
+
+    v_mp_i_mp : tuple of float, default None
+        Voltage, current at maximum power point in units of [V], [A].
+        If not provided, the maximum power point is found at the maximum of
+        ``voltage`` \times ``current``.
+
+    vlim : float, default 0.2
+        Defines portion of IV curve where the exponential term in the single
+        diode equation can be neglected, i.e.
+        ``voltage`` <= ``vlim`` x ``v_oc`` [V]
+
+    ilim : float, default 0.1
+        Defines portion of the IV curve where the exponential term in the
+        single diode equation is signficant, approximately defined by
+        ``current`` < (1 - ``ilim``) x ``i_sc`` [A]
+
+    Returns
+    -------
+    tuple of the following elements:
+
+        * photocurrent : float
+            photocurrent [A]
+        * saturation_current : float
+            dark (saturation) current [A]
+        * resistance_shunt : float
+            shunt (parallel) resistance, in ohms
+        * resistance_series : float
+            series resistance, in ohms
+        * nNsVth : float
+            product of thermal voltage ``Vth`` [V], diode ideality factor
+            ``n``, and number of series cells ``Ns``
+
+    Raises
+    ------
+    RuntimeError if parameter extraction is not successful.
+
+    Notes
+    -----
+    Inputs ``voltage``, ``current``, ``v_oc``, ``i_sc`` and ``v_mp_i_mp`` are
+    assumed to be from a single IV curve at constant irradiance and cell
+    temperature.
+
+    :py:func:`fit_single_diode_sandia` obtains values for the five parameters
+    for the single diode equation [1]:
+
+    .. math::
+
+        I = I_{L} - I_{0} (\exp \frac{V + I R_{s}}{nNsVth} - 1)
+        - \frac{V + I R_{s}}{R_{sh}}
+
+    See :py:func:`pvsystem.singlediode` for definition of the parameters.
+
+    The extraction method [2] proceeds in six steps.
+
+    1. In the single diode equation, replace :math:`R_{sh} = 1/G_{p}` and
+       re-arrange
+
+    .. math::
+
+        I = \frac{I_{L}}{1 + G_{p} R_{s}} - \frac{G_{p} V}{1 + G_{p} R_{s}}
+        - \frac{I_{0}}{1 + G_{p} R_{s}} (\exp(\frac{V + I R_{s}}{nNsVth}) - 1)
+
+    2. The linear portion of the IV curve is defined as
+       :math:`V \le vlim \times v_oc`. Over this portion of the IV curve,
+
+    .. math::
+
+        \frac{I_{0}}{1 + G_{p} R_{s}} (\exp(\frac{V + I R_{s}}{nNsVth}) - 1)
+        \approx 0
+
+    3. Fit the linear portion of the IV curve with a line.
+
+    .. math::
+
+        I &\approx \frac{I_{L}}{1 + G_{p} R_{s}} - \frac{G_{p} V}{1 + G_{p}
+        R_{s}} \\
+        &= \beta_{0} + \beta_{1} V
+
+    4. The exponential portion of the IV curve is defined by
+       :math:`\beta_{0} + \beta_{1} \times V - I > ilim \times i_sc`.
+       Over this portion of the curve, :math:`exp((V + IRs)/nNsVth) >> 1`
+       so that
+
+    .. math::
+
+        \exp(\frac{V + I R_{s}}{nNsVth}) - 1 \approx
+        \exp(\frac{V + I R_{s}}{nNsVth})
+
+    5. Fit the exponential portion of the IV curve.
+
+    .. math::
+
+        \log(\beta_{0} - \beta_{1} V - I)
+        &\approx \log(\frac{I_{0}}{1 + G_{p} R_{s}} + \frac{V}{nNsVth}
+        + \frac{I R_{s}}{nNsVth} \\
+        &= \beta_{2} + beta_{3} V + \beta_{4} I
+
+    6. Calculate values for ``IL, I0, Rs, Rsh,`` and ``nNsVth`` from the
+       regression coefficents :math:`\beta_{0}, \beta_{1}, \beta_{3}` and
+       :math:`\beta_{4}`.
+
+
+    References
+    ----------
+    [1] S.R. Wenham, M.A. Green, M.E. Watt, "Applied Photovoltaics" ISBN
+    0 86758 909 4
+    [2] C. B. Jones, C. W. Hansen, Single Diode Parameter Extraction from
+    In-Field Photovoltaic I-V Curves on a Single Board Computer, 46th IEEE
+    Photovoltaic Specialist Conference, Chicago, IL, 2019
+    """
+
+    # If not provided, extract v_oc, i_sc, v_mp and i_mp from the IV curve data
+    if v_oc is None:
+        v_oc = voltage[-1]
+    if i_sc is None:
+        i_sc = current[0]
+    if v_mp_i_mp is not None:
+        v_mp, i_mp = v_mp_i_mp
+    else:
+        v_mp, i_mp = _find_mp(voltage, current)
+
+    # Find beta0 and beta1 from linear portion of the IV curve
+    beta0, beta1 = _find_beta0_beta1(voltage, current, vlim, v_oc)
+
+    # Find beta3 and beta4 from the exponential portion of the IV curve
+    beta3, beta4 = _find_beta3_beta4(voltage, current, beta0, beta1, ilim,
+                                     i_sc)
+
+    # calculate single diode parameters from regression coefficients
+    return _calculate_sde_parameters(beta0, beta1, beta3, beta4, v_mp, i_mp,
+                                     v_oc)
+
+
+def _find_mp(voltage, current):
+    """
+    Finds voltage and current at maximum power point.
+
+    Parameters
+    ----------
+    voltage : ndarray
+        1D array containing voltage at each point on the IV curve, increasing
+        from 0 to v_oc inclusive, of `float` type [V]
+
+    current : ndarray
+        1D array containing current at each point on the IV curve, decreasing
+        from i_sc to 0 inclusive, of `float` type [A]
+
+    Returns
+    -------
+    v_mp, i_mp : tuple
+        voltage ``v_mp`` and current ``i_mp`` at the maximum power point [V],
+        [A]
+    """
+    p = voltage * current
+    idx = np.argmax(p)
+    return voltage[idx], current[idx]
+
+
+def _calc_I0(IL, I, V, Gp, Rs, nNsVth):
+    return (IL - I - Gp * V - Gp * Rs * I) / np.exp((V + Rs * I) / nNsVth)
+
+
+def _find_beta0_beta1(v, i, vlim, v_oc):
+    # Get intercept and slope of linear portion of IV curve.
+    # Start with V =< vlim * v_oc, extend by adding points until slope is
+    # negative (downward).
+    beta0 = np.nan
+    beta1 = np.nan
+    first_idx = np.searchsorted(v, vlim * v_oc)
+    for idx in range(first_idx, len(v)):
+        coef = np.polyfit(v[:idx], i[:idx], deg=1)
+        if coef[0] < 0:
+            # intercept term
+            beta0 = coef[1].item()
+            # sign change of slope to get positive parameter value
+            beta1 = -coef[0].item()
+            break
+    if any(np.isnan([beta0, beta1])):
+        raise RuntimeError("Parameter extraction failed: beta0={}, beta1={}"
+                           .format(beta0, beta1))
+    else:
+        return beta0, beta1
+
+
+def _find_beta3_beta4(voltage, current, beta0, beta1, ilim, i_sc):
+    # Subtract the IV curve from the linear fit.
+    y = beta0 - beta1 * voltage - current
+    x = np.array([np.ones_like(voltage), voltage, current]).T
+    # Select points where y > ilim * i_sc to regress log(y) onto x
+    idx = (y > ilim * i_sc)
+    result = np.linalg.lstsq(x[idx], np.log(y[idx]), rcond=None)
+    coef = result[0]
+    beta3 = coef[1].item()
+    beta4 = coef[2].item()
+    if any(np.isnan([beta3, beta4])):
+        raise RuntimeError("Parameter extraction failed: beta3={}, beta4={}"
+                           .format(beta3, beta4))
+    else:
+        return beta3, beta4
+
+
+def _calculate_sde_parameters(beta0, beta1, beta3, beta4, v_mp, i_mp, v_oc):
+    nNsVth = 1.0 / beta3
+    Rs = beta4 / beta3
+    Gp = beta1 / (1.0 - Rs * beta1)
+    Rsh = 1.0 / Gp
+    IL = (1 + Gp * Rs) * beta0
+    # calculate I0
+    I0_vmp = _calc_I0(IL, i_mp, v_mp, Gp, Rs, nNsVth)
+    I0_voc = _calc_I0(IL, 0, v_oc, Gp, Rs, nNsVth)
+    if any(np.isnan([I0_vmp, I0_voc])) or ((I0_vmp <= 0) and (I0_voc <= 0)):
+        raise RuntimeError("Parameter extraction failed: I0 is undetermined.")
+    elif (I0_vmp > 0) and (I0_voc > 0):
+        I0 = 0.5 * (I0_vmp + I0_voc)
+    elif (I0_vmp > 0):
+        I0 = I0_vmp
+    else:  # I0_voc > 0
+        I0 = I0_voc
+    return (IL, I0, Rsh, Rs, nNsVth)
diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py
index 92a92e2..6aad00c 100644
--- a/pvlib/modelchain.py
+++ b/pvlib/modelchain.py
@@ -8,7 +8,6 @@ the time to read the source code for the module.
 
 from functools import partial
 import warnings
-import pandas as pd
 
 from pvlib import (atmosphere, clearsky, pvsystem, solarposition, temperature,
                    tools)
@@ -412,13 +411,6 @@ class ModelChain(object):
                     self._dc_model = self.pvsyst
                 elif model == 'pvwatts':
                     self._dc_model = self.pvwatts_dc
-                elif model == 'singlediode':
-                    warnings.warn('DC model keyword singlediode used for '
-                                  'ModelChain object. singlediode is '
-                                  'ambiguous, use desoto instead. singlediode '
-                                  'keyword will be removed in v0.7.0 and '
-                                  'later', pvlibDeprecationWarning)
-                    self._dc_model = self.desoto
             else:
                 raise ValueError(model + ' is not a valid DC power model')
         else:
@@ -480,24 +472,6 @@ class ModelChain(object):
     def pvsyst(self):
         return self._singlediode(self.system.calcparams_pvsyst)
 
-    def singlediode(self):
-        """Deprecated"""
-        (photocurrent, saturation_current, resistance_series,
-         resistance_shunt, nNsVth) = (
-            self.system.calcparams_desoto(self.effective_irradiance,
-                                          self.cell_temperature))
-
-        self.desoto = (photocurrent, saturation_current, resistance_series,
-                       resistance_shunt, nNsVth)
-
-        self.dc = self.system.singlediode(
-            photocurrent, saturation_current, resistance_series,
-            resistance_shunt, nNsVth)
-
-        self.dc = self.system.scale_voltage_current_power(self.dc).fillna(0)
-
-        return self
-
     def pvwatts_dc(self):
         self.dc = self.system.pvwatts_dc(self.effective_irradiance,
                                          self.cell_temperature)
@@ -746,7 +720,7 @@ class ModelChain(object):
             fd*self.total_irrad['poa_diffuse'])
         return self
 
-    def complete_irradiance(self, times=None, weather=None):
+    def complete_irradiance(self, weather, times=None):
         """
         Determine the missing irradiation columns. Only two of the
         following data columns (dni, ghi, dhi) are needed to calculate
@@ -758,19 +732,21 @@ class ModelChain(object):
 
         Parameters
         ----------
-        times : None or DatetimeIndex, default None
-            Times at which to evaluate the model. Can be None if
-            attribute `times` is already set.
-        weather : None or pandas.DataFrame, default None
-            Table with at least two columns containing one of the
-            following data sets: dni, dhi, ghi. Can be None if attribute
-            `weather` is already set.
+        weather : DataFrame
+            Column names must be ``'dni'``, ``'ghi'``, ``'dhi'``,
+            ``'wind_speed'``, ``'temp_air'``. All irradiance components
+            are required. Air temperature of 20 C and wind speed
+            of 0 m/s will be added to the DataFrame if not provided.
+        times : None, deprecated
+            Deprecated argument included for API compatibility, but not
+            used internally. The index of the weather DataFrame is used
+            for times.
 
         Returns
         -------
         self
 
-        Assigns attributes: times, weather
+        Assigns attributes: weather
 
         Examples
         --------
@@ -782,19 +758,23 @@ class ModelChain(object):
 
         >>> # my_weather containing 'dhi' and 'ghi'.
         >>> mc = ModelChain(my_system, my_location)  # doctest: +SKIP
-        >>> mc.complete_irradiance(my_datetime, my_weather)  # doctest: +SKIP
-        >>> mc.run_model()  # doctest: +SKIP
+        >>> mc.complete_irradiance(my_weather)  # doctest: +SKIP
+        >>> mc.run_model(mc.weather)  # doctest: +SKIP
 
         >>> # my_weather containing 'dhi', 'ghi' and 'dni'.
         >>> mc = ModelChain(my_system, my_location)  # doctest: +SKIP
-        >>> mc.run_model(my_datetime, my_weather)  # doctest: +SKIP
+        >>> mc.run_model(my_weather)  # doctest: +SKIP
         """
-        if weather is not None:
-            self.weather = weather
+        self.weather = weather
+
         if times is not None:
-            self.times = times
+            warnings.warn('times keyword argument is deprecated and will be '
+                          'removed in 0.8. The index of the weather DataFrame '
+                          'is used for times.', pvlibDeprecationWarning)
+
         self.solar_position = self.location.get_solarposition(
-            self.times, method=self.solar_position_method)
+            self.weather.index, method=self.solar_position_method)
+
         icolumns = set(self.weather.columns)
         wrn_txt = ("This function is not safe at the moment.\n" +
                    "Results can be too high or negative.\n" +
@@ -803,7 +783,7 @@ class ModelChain(object):
 
         if {'ghi', 'dhi'} <= icolumns and 'dni' not in icolumns:
             clearsky = self.location.get_clearsky(
-                times, solar_position=self.solar_position)
+                self.weather.index, solar_position=self.solar_position)
             self.weather.loc[:, 'dni'] = pvlib.irradiance.dni(
                 self.weather.loc[:, 'ghi'], self.weather.loc[:, 'dhi'],
                 self.solar_position.zenith,
@@ -822,62 +802,54 @@ class ModelChain(object):
 
         return self
 
-    def prepare_inputs(self, times=None, weather=None):
+    def prepare_inputs(self, weather, times=None):
         """
         Prepare the solar position, irradiance, and weather inputs to
         the model.
 
         Parameters
         ----------
-        times : None or DatetimeIndex, default None
-            Times at which to evaluate the model. Can be None if
-            attribute `times` is already set.
-        weather : None or DataFrame, default None
-            If ``None``, the weather attribute is used. Column names
-            must be ``'dni'``, ``'ghi'``, ``'dhi'``, ``'wind_speed'``,
-            ``'temp_air'``. All irradiance components are required.
-            Assumes air temperature is 20 C and wind speed is 0 m/s if
-            not provided.
+        weather : DataFrame
+            Column names must be ``'dni'``, ``'ghi'``, ``'dhi'``,
+            ``'wind_speed'``, ``'temp_air'``. All irradiance components
+            are required. Air temperature of 20 C and wind speed
+            of 0 m/s will be added to the DataFrame if not provided.
+        times : None, deprecated
+            Deprecated argument included for API compatibility, but not
+            used internally. The index of the weather DataFrame is used
+            for times.
 
         Notes
         -----
-        Assigns attributes: ``times``, ``solar_position``, ``airmass``,
+        Assigns attributes: ``solar_position``, ``airmass``,
         ``total_irrad``, `aoi`
 
         See also
         --------
         ModelChain.complete_irradiance
         """
-        if weather is not None:
-            self.weather = weather
-        if self.weather is None:
-            self.weather = pd.DataFrame(index=times)
+
+        if not {'ghi', 'dni', 'dhi'} <= set(weather.columns):
+            raise ValueError(
+                "Uncompleted irradiance data set. Please check your input "
+                "data.\nData set needs to have 'dni', 'dhi' and 'ghi'.\n"
+                "Detected data: {0}".format(list(weather.columns)))
+
+        self.weather = weather
 
         if times is not None:
-            self.times = times
+            warnings.warn('times keyword argument is deprecated and will be '
+                          'removed in 0.8. The index of the weather DataFrame '
+                          'is used for times.', pvlibDeprecationWarning)
+
+        self.times = self.weather.index
 
         self.solar_position = self.location.get_solarposition(
-            self.times, method=self.solar_position_method)
+            self.weather.index, method=self.solar_position_method)
 
         self.airmass = self.location.get_airmass(
             solar_position=self.solar_position, model=self.airmass_model)
 
-        if not any([x in ['ghi', 'dni', 'dhi'] for x in self.weather.columns]):
-            warnings.warn('Clear sky assumption for no input irradiance is '
-                          'deprecated and will be removed in v0.7.0. Use '
-                          'location.get_clearsky instead',
-                          pvlibDeprecationWarning)
-            self.weather[['ghi', 'dni', 'dhi']] = self.location.get_clearsky(
-                self.solar_position.index, self.clearsky_model,
-                solar_position=self.solar_position,
-                airmass_absolute=self.airmass['airmass_absolute'])
-
-        if not {'ghi', 'dni', 'dhi'} <= set(self.weather.columns):
-            raise ValueError(
-                "Uncompleted irradiance data set. Please check your input "
-                "data.\nData set needs to have 'dni', 'dhi' and 'ghi'.\n"
-                "Detected data: {0}".format(list(self.weather.columns)))
-
         # PVSystem.get_irradiance and SingleAxisTracker.get_irradiance
         # and PVSystem.get_aoi and SingleAxisTracker.get_aoi
         # have different method signatures. Use partial to handle
@@ -921,32 +893,36 @@ class ModelChain(object):
             self.weather['temp_air'] = 20
         return self
 
-    def run_model(self, times=None, weather=None):
+    def run_model(self, weather, times=None):
         """
         Run the model.
 
         Parameters
         ----------
-        times : None or DatetimeIndex, default None
-            Times at which to evaluate the model. Can be None if
-            attribute `times` is already set.
-        weather : None or DataFrame, default None
-            If ``None``, the weather attribute is used. Column names
-            must be ``'dni'``, ``'ghi'``, ``'dhi'``, ``'wind_speed'``,
-            ``'temp_air'``. All irradiance components are required.
-            Assumes air temperature is 20 C and wind speed is 0 m/s if
-            not provided.
+        weather : DataFrame
+            Column names must be ``'dni'``, ``'ghi'``, ``'dhi'``,
+            ``'wind_speed'``, ``'temp_air'``. All irradiance components
+            are required. Air temperature of 20 C and wind speed
+            of 0 m/s will be added to the DataFrame if not provided.
+        times : None, deprecated
+            Deprecated argument included for API compatibility, but not
+            used internally. The index of the weather DataFrame is used
+            for times.
 
         Returns
         -------
         self
 
-        Assigns attributes: times, solar_position, airmass, irradiance,
+        Assigns attributes: solar_position, airmass, irradiance,
         total_irrad, effective_irradiance, weather, cell_temperature, aoi,
         aoi_modifier, spectral_modifier, dc, ac, losses.
         """
+        if times is not None:
+            warnings.warn('times keyword argument is deprecated and will be '
+                          'removed in 0.8. The index of the weather DataFrame '
+                          'is used for times.', pvlibDeprecationWarning)
 
-        self.prepare_inputs(times, weather)
+        self.prepare_inputs(weather)
         self.aoi_model()
         self.spectral_model()
         self.effective_irradiance_model()
diff --git a/setup.py b/setup.py
index f2e320c..a492b69 100755
--- a/setup.py
+++ b/setup.py
@@ -44,8 +44,8 @@ INSTALL_REQUIRES = ['numpy >= 1.10.4',
 TESTS_REQUIRE = ['nose', 'pytest', 'pytest-cov', 'pytest-mock',
                  'pytest-timeout']
 EXTRAS_REQUIRE = {
-    'optional': ['ephem', 'cython', 'netcdf4', 'numba', 'pvfactors', 'scipy',
-                 'siphon', 'tables'],
+    'optional': ['ephem', 'cython', 'netcdf4', 'nrel-pysam', 'numba',
+                 'pvfactors', 'scipy', 'siphon', 'tables'],
     'doc': ['ipython', 'matplotlib', 'sphinx', 'sphinx_rtd_theme'],
     'test': TESTS_REQUIRE
 }


Test patch:
diff --git a/pvlib/test/conftest.py b/pvlib/test/conftest.py
index 1c690c2..8789bf9 100644
--- a/pvlib/test/conftest.py
+++ b/pvlib/test/conftest.py
@@ -151,6 +151,15 @@ requires_pvfactors = pytest.mark.skipif(not has_pvfactors,
                                         reason='requires pvfactors')
 
 
+try:
+    import PySAM  # noqa: F401
+    has_pysam = True
+except ImportError:
+    has_pysam = False
+
+requires_pysam = pytest.mark.skipif(not has_pysam, reason="requires PySAM")
+
+
 @pytest.fixture(scope="session")
 def sam_data():
     data = {}
diff --git a/pvlib/test/test_ivtools.py b/pvlib/test/test_ivtools.py
new file mode 100644
index 0000000..d4aca05
--- /dev/null
+++ b/pvlib/test/test_ivtools.py
@@ -0,0 +1,242 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Thu May  9 10:51:15 2019
+
+@author: cwhanse
+"""
+
+import numpy as np
+import pandas as pd
+import pytest
+from pvlib import pvsystem
+from pvlib import ivtools
+from pvlib.test.conftest import requires_scipy, requires_pysam
+
+
+@pytest.fixture
+def get_test_iv_params():
+    return {'IL': 8.0, 'I0': 5e-10, 'Rsh': 1000, 'Rs': 0.2, 'nNsVth': 1.61864}
+
+
+@pytest.fixture
+def get_cec_params_cansol_cs5p_220p():
+    return {'input': {'V_mp_ref': 46.6, 'I_mp_ref': 4.73, 'V_oc_ref': 58.3,
+                      'I_sc_ref': 5.05, 'alpha_sc': 0.0025,
+                      'beta_voc': -0.19659, 'gamma_pmp': -0.43,
+                      'cells_in_series': 96},
+            'output': {'a_ref': 2.3674, 'I_L_ref': 5.056, 'I_o_ref': 1.01e-10,
+                       'R_sh_ref': 837.51, 'R_s': 1.004, 'Adjust': 2.3}}
+
+
+@requires_scipy
+def test_fit_sde_sandia(get_test_iv_params, get_bad_iv_curves):
+    test_params = get_test_iv_params
+    testcurve = pvsystem.singlediode(photocurrent=test_params['IL'],
+                                     saturation_current=test_params['I0'],
+                                     resistance_shunt=test_params['Rsh'],
+                                     resistance_series=test_params['Rs'],
+                                     nNsVth=test_params['nNsVth'],
+                                     ivcurve_pnts=300)
+    expected = tuple(test_params[k] for k in ['IL', 'I0', 'Rsh', 'Rs',
+                     'nNsVth'])
+    result = ivtools.fit_sde_sandia(voltage=testcurve['v'],
+                                    current=testcurve['i'])
+    assert np.allclose(result, expected, rtol=5e-5)
+    result = ivtools.fit_sde_sandia(voltage=testcurve['v'],
+                                    current=testcurve['i'],
+                                    v_oc=testcurve['v_oc'],
+                                    i_sc=testcurve['i_sc'])
+    assert np.allclose(result, expected, rtol=5e-5)
+    result = ivtools.fit_sde_sandia(voltage=testcurve['v'],
+                                    current=testcurve['i'],
+                                    v_oc=testcurve['v_oc'],
+                                    i_sc=testcurve['i_sc'],
+                                    v_mp_i_mp=(testcurve['v_mp'],
+                                    testcurve['i_mp']))
+    assert np.allclose(result, expected, rtol=5e-5)
+    result = ivtools.fit_sde_sandia(voltage=testcurve['v'],
+                                    current=testcurve['i'], vlim=0.1)
+    assert np.allclose(result, expected, rtol=5e-5)
+
+
+@requires_scipy
+def test_fit_sde_sandia_bad_iv(get_bad_iv_curves):
+    # bad IV curves for coverage of if/then in _calculate_sde_parameters
+    v1, i1, v2, i2 = get_bad_iv_curves
+    result = ivtools.fit_sde_sandia(voltage=v1, current=i1)
+    assert np.allclose(result, (-2.4322856072799985, 8.854688976836396,
+                                -63.56227601452038, 111.18558915546389,
+                                -137.9965046659527))
+    result = ivtools.fit_sde_sandia(voltage=v2, current=i2)
+    assert np.allclose(result, (2.62405311949227, 1.8657963912925288,
+                                110.35202827739991, -65.652554411442,
+                                174.49362093001415))
+
+
+@requires_pysam
+def test_fit_sdm_cec_sam(get_cec_params_cansol_cs5p_220p):
+    input_data = get_cec_params_cansol_cs5p_220p['input']
+    I_L_ref, I_o_ref, R_sh_ref, R_s, a_ref, Adjust = \
+        ivtools.fit_sdm_cec_sam(
+            celltype='polySi', v_mp=input_data['V_mp_ref'],
+            i_mp=input_data['I_mp_ref'], v_oc=input_data['V_oc_ref'],
+            i_sc=input_data['I_sc_ref'], alpha_sc=input_data['alpha_sc'],
+            beta_voc=input_data['beta_voc'],
+            gamma_pmp=input_data['gamma_pmp'],
+            cells_in_series=input_data['cells_in_series'])
+    expected = pd.Series(get_cec_params_cansol_cs5p_220p['output'])
+    modeled = pd.Series(index=expected.index, data=np.nan)
+    modeled['a_ref'] = a_ref
+    modeled['I_L_ref'] = I_L_ref
+    modeled['I_o_ref'] = I_o_ref
+    modeled['R_sh_ref'] = R_sh_ref
+    modeled['R_s'] = R_s
+    modeled['Adjust'] = Adjust
+    assert np.allclose(modeled.values, expected.values, rtol=5e-2)
+    # test for fitting failure
+    with pytest.raises(RuntimeError):
+        I_L_ref, I_o_ref, R_sh_ref, R_s, a_ref, Adjust = \
+            ivtools.fit_sdm_cec_sam(
+                celltype='polySi', v_mp=0.45, i_mp=5.25, v_oc=0.55, i_sc=5.5,
+                alpha_sc=0.00275, beta_voc=0.00275, gamma_pmp=0.0055,
+                cells_in_series=1, temp_ref=25)
+
+
+@pytest.fixture
+def get_bad_iv_curves():
+    # v1, i1 produces a bad value for I0_voc
+    v1 = np.array([0, 0.338798867469060, 0.677597734938121, 1.01639660240718,
+                   1.35519546987624, 1.69399433734530, 2.03279320481436,
+                   2.37159207228342, 2.71039093975248, 3.04918980722154,
+                   3.38798867469060, 3.72678754215966, 4.06558640962873,
+                   4.40438527709779, 4.74318414456685, 5.08198301203591,
+                   5.42078187950497, 5.75958074697403, 6.09837961444309,
+                   6.43717848191215, 6.77597734938121, 7.11477621685027,
+                   7.45357508431933, 7.79237395178839, 8.13117281925745,
+                   8.46997168672651, 8.80877055419557, 9.14756942166463,
+                   9.48636828913369, 9.82516715660275, 10.1639660240718,
+                   10.5027648915409, 10.8415637590099, 11.1803626264790,
+                   11.5191614939481, 11.8579603614171, 12.1967592288862,
+                   12.5355580963552, 12.8743569638243, 13.2131558312934,
+                   13.5519546987624, 13.8907535662315, 14.2295524337005,
+                   14.5683513011696, 14.9071501686387, 15.2459490361077,
+                   15.5847479035768, 15.9235467710458, 16.2623456385149,
+                   16.6011445059840, 16.9399433734530, 17.2787422409221,
+                   17.6175411083911, 17.9563399758602, 18.2951388433293,
+                   18.6339377107983, 18.9727365782674, 19.3115354457364,
+                   19.6503343132055, 19.9891331806746, 20.3279320481436,
+                   20.6667309156127, 21.0055297830817, 21.3443286505508,
+                   21.6831275180199, 22.0219263854889, 22.3607252529580,
+                   22.6995241204270, 23.0383229878961, 23.3771218553652,
+                   23.7159207228342, 24.0547195903033, 24.3935184577724,
+                   24.7323173252414, 25.0711161927105, 25.4099150601795,
+                   25.7487139276486, 26.0875127951177, 26.4263116625867,
+                   26.7651105300558, 27.1039093975248, 27.4427082649939,
+                   27.7815071324630, 28.1203059999320, 28.4591048674011,
+                   28.7979037348701, 29.1367026023392, 29.4755014698083,
+                   29.8143003372773, 30.1530992047464, 30.4918980722154,
+                   30.8306969396845, 31.1694958071536, 31.5082946746226,
+                   31.8470935420917, 32.1858924095607, 32.5246912770298,
+                   32.8634901444989, 33.2022890119679, 33.5410878794370])
+    i1 = np.array([3.39430882774470, 2.80864492110761, 3.28358165429196,
+                   3.41191190551673, 3.11975662808148, 3.35436585834612,
+                   3.23953272899809, 3.60307083325333, 2.80478101508277,
+                   2.80505102853845, 3.16918996870373, 3.21088388439857,
+                   3.46332865310431, 3.09224155015883, 3.17541550741062,
+                   3.32470179290389, 3.33224664316240, 3.07709000050741,
+                   2.89141245343405, 3.01365768561537, 3.23265176770231,
+                   3.32253647634228, 2.97900657569736, 3.31959549243966,
+                   3.03375461550111, 2.97579298978937, 3.25432831375159,
+                   2.89178382564454, 3.00341909207567, 3.72637492250097,
+                   3.28379856976360, 2.96516169245835, 3.25658381110230,
+                   3.41655911533139, 3.02718097944604, 3.11458376760376,
+                   3.24617304369762, 3.45935502367636, 3.21557333256913,
+                   3.27611176482650, 2.86954135732485, 3.32416319254657,
+                   3.15277467598732, 3.08272557013770, 3.15602202666259,
+                   3.49432799877150, 3.53863997177632, 3.10602611478455,
+                   3.05373911151821, 3.09876772570781, 2.97417228624287,
+                   2.84573593699237, 3.16288578405195, 3.06533173612783,
+                   3.02118336639575, 3.34374977225502, 2.97255164138821,
+                   3.19286135682863, 3.10999753817133, 3.26925354620079,
+                   3.11957809501529, 3.20155017481720, 3.31724984405837,
+                   3.42879043512927, 3.17933067619240, 3.47777362613969,
+                   3.20708912539777, 3.48205761174907, 3.16804363684327,
+                   3.14055472378230, 3.13445657434470, 2.91152696252998,
+                   3.10984113847427, 2.80443349399489, 3.23146278164875,
+                   2.94521083406108, 3.17388903141715, 3.05930294897030,
+                   3.18985234673287, 3.27946609274898, 3.33717523113602,
+                   2.76394303462702, 3.19375132937510, 2.82628616689450,
+                   2.85238527394143, 2.82975892599489, 2.79196912313914,
+                   2.72860792049395, 2.75585977414140, 2.44280222448805,
+                   2.36052347370628, 2.26785071765738, 2.10868255743462,
+                   2.06165739407987, 1.90047259509385, 1.39925575828709,
+                   1.24749015957606, 0.867823806536762, 0.432752457749993, 0])
+    # v2, i2 produces a bad value for I0_vmp
+    v2 = np.array([0, 0.365686097622586, 0.731372195245173, 1.09705829286776,
+                   1.46274439049035, 1.82843048811293, 2.19411658573552,
+                   2.55980268335810, 2.92548878098069, 3.29117487860328,
+                   3.65686097622586, 4.02254707384845, 4.38823317147104,
+                   4.75391926909362, 5.11960536671621, 5.48529146433880,
+                   5.85097756196138, 6.21666365958397, 6.58234975720655,
+                   6.94803585482914, 7.31372195245173, 7.67940805007431,
+                   8.04509414769690, 8.41078024531949, 8.77646634294207,
+                   9.14215244056466, 9.50783853818725, 9.87352463580983,
+                   10.2392107334324, 10.6048968310550, 10.9705829286776,
+                   11.3362690263002, 11.7019551239228, 12.0676412215454,
+                   12.4333273191679, 12.7990134167905, 13.1646995144131,
+                   13.5303856120357, 13.8960717096583, 14.2617578072809,
+                   14.6274439049035, 14.9931300025260, 15.3588161001486,
+                   15.7245021977712, 16.0901882953938, 16.4558743930164,
+                   16.8215604906390, 17.1872465882616, 17.5529326858841,
+                   17.9186187835067, 18.2843048811293, 18.6499909787519,
+                   19.0156770763745, 19.3813631739971, 19.7470492716197,
+                   20.1127353692422, 20.4784214668648, 20.8441075644874,
+                   21.2097936621100, 21.5754797597326, 21.9411658573552,
+                   22.3068519549778, 22.6725380526004, 23.0382241502229,
+                   23.4039102478455, 23.7695963454681, 24.1352824430907,
+                   24.5009685407133, 24.8666546383359, 25.2323407359585,
+                   25.5980268335810, 25.9637129312036, 26.3293990288262,
+                   26.6950851264488, 27.0607712240714, 27.4264573216940,
+                   27.7921434193166, 28.1578295169392, 28.5235156145617,
+                   28.8892017121843, 29.2548878098069, 29.6205739074295,
+                   29.9862600050521, 30.3519461026747, 30.7176322002973,
+                   31.0833182979198, 31.4490043955424, 31.8146904931650,
+                   32.1803765907876, 32.5460626884102, 32.9117487860328,
+                   33.2774348836554, 33.6431209812779, 34.0088070789005,
+                   34.3744931765231, 34.7401792741457, 35.1058653717683,
+                   35.4715514693909, 35.8372375670135, 36.2029236646360])
+    i2 = np.array([6.49218806928330, 6.49139336899548, 6.17810697175204,
+                   6.75197816263663, 6.59529074137515, 6.18164578868300,
+                   6.38709397931910, 6.30685422248427, 6.44640615548925,
+                   6.88727230397772, 6.42074852785591, 6.46348580823746,
+                   6.38642309763941, 5.66356277572311, 6.61010381702082,
+                   6.33288284311125, 6.22475343933610, 6.30651399433833,
+                   6.44435022944051, 6.43741711131908, 6.03536180208946,
+                   6.23814639328170, 5.97229140403242, 6.20790000748341,
+                   6.22933550182341, 6.22992127804882, 6.13400871899299,
+                   6.83491312449950, 6.07952797245846, 6.35837746415450,
+                   6.41972128662324, 6.85256717258275, 6.25807797296759,
+                   6.25124948151766, 6.22229212812413, 6.72249444167406,
+                   6.41085549981649, 6.75792874870056, 6.22096181559171,
+                   6.47839564388996, 6.56010208597432, 6.63300966556949,
+                   6.34617546039339, 6.79812221146153, 6.14486056194136,
+                   6.14979256889311, 6.16883037644880, 6.57309183229605,
+                   6.40064681038509, 6.18861448239873, 6.91340138179698,
+                   5.94164388433788, 6.23638991745862, 6.31898940411710,
+                   6.45247884556830, 6.58081455524297, 6.64915284801713,
+                   6.07122119270245, 6.41398258148256, 6.62144271089614,
+                   6.36377197712687, 6.51487678829345, 6.53418950147730,
+                   6.18886469125371, 6.26341063475750, 6.83488211680259,
+                   6.62699397226695, 6.41286837534735, 6.44060085001851,
+                   6.48114130629288, 6.18607038456406, 6.16923370572396,
+                   6.64223126283631, 6.07231852289266, 5.79043710204375,
+                   6.48463886529882, 6.36263392044401, 6.11212476454494,
+                   6.14573900812925, 6.12568047243240, 6.43836230231577,
+                   6.02505694060219, 6.13819468942244, 6.22100593815064,
+                   6.02394682666345, 5.89016573063789, 5.74448527739202,
+                   5.50415294280017, 5.31883018164157, 4.87476769510305,
+                   4.74386713755523, 4.60638346931628, 4.06177345572680,
+                   3.73334482123538, 3.13848311672243, 2.71638862600768,
+                   2.02963773590165, 1.49291145092070, 0.818343889647352, 0])
+
+    return v1, i1, v2, i2
diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py
index 20a8a95..4623d9f 100644
--- a/pvlib/test/test_modelchain.py
+++ b/pvlib/test/test_modelchain.py
@@ -127,7 +127,7 @@ def weather():
 
 
 def test_ModelChain_creation(system, location):
-    mc = ModelChain(system, location)
+    ModelChain(system, location)
 
 
 @pytest.mark.parametrize('strategy, expected', [
@@ -144,29 +144,50 @@ def test_orientation_strategy(strategy, expected, system, location):
     assert system.surface_azimuth == expected[1]
 
 
-@requires_scipy
-def test_run_model(system, location):
+def test_run_model_with_irradiance(system, location):
     mc = ModelChain(system, location)
     times = pd.date_range('20160101 1200-0700', periods=2, freq='6H')
+    irradiance = pd.DataFrame({'dni': 900, 'ghi': 600, 'dhi': 150},
+                              index=times)
+    ac = mc.run_model(irradiance).ac
 
-    with pytest.warns(pvlibDeprecationWarning):
-        ac = mc.run_model(times).ac
-
-    expected = pd.Series(np.array([183.522449305, -2.00000000e-02]),
+    expected = pd.Series(np.array([187.80746494643176, -0.02]),
                          index=times)
-    assert_series_equal(ac, expected, check_less_precise=1)
+    assert_series_equal(ac, expected)
 
 
-def test_run_model_with_irradiance(system, location):
+def test_run_model_times(system, location):
     mc = ModelChain(system, location)
     times = pd.date_range('20160101 1200-0700', periods=2, freq='6H')
     irradiance = pd.DataFrame({'dni': 900, 'ghi': 600, 'dhi': 150},
                               index=times)
-    ac = mc.run_model(times, weather=irradiance).ac
+    with pytest.warns(pvlibDeprecationWarning):
+        mc.run_model(irradiance, times=times)
 
-    expected = pd.Series(np.array([187.80746495, -2.00000000e-02]),
-                         index=times)
-    assert_series_equal(ac, expected)
+
+def test_prepare_inputs_times(system, location):
+    mc = ModelChain(system, location)
+    times = pd.date_range('20160101 1200-0700', periods=2, freq='6H')
+    irradiance = pd.DataFrame({'dni': 900, 'ghi': 600, 'dhi': 150},
+                              index=times)
+    with pytest.warns(pvlibDeprecationWarning):
+        mc.prepare_inputs(irradiance, times=times)
+
+
+def test_prepare_inputs_no_irradiance(system, location):
+    mc = ModelChain(system, location)
+    weather = pd.DataFrame()
+    with pytest.raises(ValueError):
+        mc.prepare_inputs(weather)
+
+
+@requires_tables
+def test_complete_irradiance_times(system, location):
+    mc = ModelChain(system, location)
+    times = pd.date_range('20160101 1200-0700', periods=2, freq='6H')
+    irradiance = pd.DataFrame({'ghi': 600., 'dhi': 150.}, index=times)
+    with pytest.warns(pvlibDeprecationWarning):
+        mc.complete_irradiance(irradiance, times=times)
 
 
 def test_run_model_perez(system, location):
@@ -174,7 +195,7 @@ def test_run_model_perez(system, location):
     times = pd.date_range('20160101 1200-0700', periods=2, freq='6H')
     irradiance = pd.DataFrame({'dni': 900, 'ghi': 600, 'dhi': 150},
                               index=times)
-    ac = mc.run_model(times, weather=irradiance).ac
+    ac = mc.run_model(irradiance).ac
 
     expected = pd.Series(np.array([187.94295642, -2.00000000e-02]),
                          index=times)
@@ -187,7 +208,7 @@ def test_run_model_gueymard_perez(system, location):
     times = pd.date_range('20160101 1200-0700', periods=2, freq='6H')
     irradiance = pd.DataFrame({'dni': 900, 'ghi': 600, 'dhi': 150},
                               index=times)
-    ac = mc.run_model(times, weather=irradiance).ac
+    ac = mc.run_model(irradiance).ac
 
     expected = pd.Series(np.array([187.94317405, -2.00000000e-02]),
                          index=times)
@@ -203,7 +224,7 @@ def test_run_model_with_weather(system, location, weather, mocker):
     mc = ModelChain(system, location)
     mc.temperature_model = 'sapm'
     m_sapm = mocker.spy(system, 'sapm_celltemp')
-    mc.run_model(weather.index, weather=weather)
+    mc.run_model(weather)
     assert m_sapm.call_count == 1
     # assert_called_once_with cannot be used with series, so need to use
     # assert_series_equal on call_args
@@ -217,7 +238,7 @@ def test_run_model_with_weather(system, location, weather, mocker):
     mc = ModelChain(system, location)
     mc.temperature_model = 'pvsyst'
     m_pvsyst = mocker.spy(system, 'pvsyst_celltemp')
-    mc.run_model(weather.index, weather=weather)
+    mc.run_model(weather)
     assert m_pvsyst.call_count == 1
     assert_series_equal(m_pvsyst.call_args[0][1], weather['temp_air'])
     assert_series_equal(m_pvsyst.call_args[0][2], weather['wind_speed'])
@@ -231,7 +252,7 @@ def test_run_model_tracker(system, location, weather, mocker):
         inverter_parameters=system.inverter_parameters)
     mocker.spy(system, 'singleaxis')
     mc = ModelChain(system, location)
-    mc.run_model(weather.index, weather=weather)
+    mc.run_model(weather)
     assert system.singleaxis.call_count == 1
     assert (mc.tracking.columns == ['tracker_theta', 'aoi', 'surface_azimuth',
                                     'surface_tilt']).all()
@@ -284,7 +305,7 @@ def test_infer_dc_model(system, cec_dc_snl_ac_system, pvsyst_dc_snl_ac_system,
     mc = ModelChain(system, location,
                     aoi_model='no_loss', spectral_model='no_loss',
                     temperature_model=temp_model_function[dc_model])
-    mc.run_model(weather.index, weather=weather)
+    mc.run_model(weather)
     assert m.call_count == 1
     assert isinstance(mc.dc, (pd.Series, pd.DataFrame))
 
@@ -338,7 +359,7 @@ def test_dc_model_user_func(pvwatts_dc_pvwatts_ac_system, location, weather,
     m = mocker.spy(sys.modules[__name__], 'poadc')
     mc = ModelChain(pvwatts_dc_pvwatts_ac_system, location, dc_model=poadc,
                     aoi_model='no_loss', spectral_model='no_loss')
-    mc.run_model(weather.index, weather=weather)
+    mc.run_model(weather)
     assert m.call_count == 1
     assert isinstance(mc.ac, (pd.Series, pd.DataFrame))
     assert not mc.ac.empty
@@ -362,7 +383,7 @@ def test_ac_models(system, cec_dc_adr_ac_system, pvwatts_dc_pvwatts_ac_system,
     if ac_model == 'pvwatts':
         ac_model += '_ac'
     m = mocker.spy(system, ac_model)
-    mc.run_model(weather.index, weather=weather)
+    mc.run_model(weather)
     assert m.call_count == 1
     assert isinstance(mc.ac, pd.Series)
     assert not mc.ac.empty
@@ -374,7 +395,7 @@ def test_ac_model_user_func(pvwatts_dc_pvwatts_ac_system, location, weather,
     m = mocker.spy(sys.modules[__name__], 'acdc')
     mc = ModelChain(pvwatts_dc_pvwatts_ac_system, location, ac_model=acdc,
                     aoi_model='no_loss', spectral_model='no_loss')
-    mc.run_model(weather.index, weather=weather)
+    mc.run_model(weather)
     assert m.call_count == 1
     assert_series_equal(mc.ac, mc.dc)
     assert not mc.ac.empty
@@ -391,7 +412,7 @@ def test_aoi_models(system, location, aoi_model, method, 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.index, weather=weather)
+    mc.run_model(weather)
     assert m.call_count == 1
     assert isinstance(mc.ac, pd.Series)
     assert not mc.ac.empty
@@ -402,7 +423,7 @@ def test_aoi_models(system, location, aoi_model, method, weather, mocker):
 def test_aoi_model_no_loss(system, location, weather):
     mc = ModelChain(system, location, dc_model='sapm',
                     aoi_model='no_loss', spectral_model='no_loss')
-    mc.run_model(weather.index, weather=weather)
+    mc.run_model(weather)
     assert mc.aoi_modifier == 1.0
     assert not mc.ac.empty
     assert mc.ac[0] > 150 and mc.ac[0] < 200
@@ -413,7 +434,7 @@ def test_aoi_model_user_func(system, location, weather, mocker):
     m = mocker.spy(sys.modules[__name__], 'constant_aoi_loss')
     mc = ModelChain(system, location, dc_model='sapm',
                     aoi_model=constant_aoi_loss, spectral_model='no_loss')
-    mc.run_model(weather.index, weather=weather)
+    mc.run_model(weather)
     assert m.call_count == 1
     assert mc.aoi_modifier == 0.9
     assert not mc.ac.empty
@@ -434,8 +455,7 @@ def test_spectral_models(system, location, spectral_model, weather):
     weather['precipitable_water'] = [0.3, 0.5]
     mc = ModelChain(system, location, dc_model='sapm',
                     aoi_model='no_loss', spectral_model=spectral_model)
-    spectral_modifier = mc.run_model(times=weather.index,
-                                     weather=weather).spectral_modifier
+    spectral_modifier = mc.run_model(weather).spectral_modifier
     assert isinstance(spectral_modifier, (pd.Series, float, int))
 
 
@@ -452,7 +472,7 @@ def test_losses_models_pvwatts(pvwatts_dc_pvwatts_ac_system, location, weather,
     mc = ModelChain(pvwatts_dc_pvwatts_ac_system, location, dc_model='pvwatts',
                     aoi_model='no_loss', spectral_model='no_loss',
                     losses_model='pvwatts')
-    mc.run_model(weather.index, weather=weather)
+    mc.run_model(weather)
     assert m.call_count == 1
     m.assert_called_with(age=age)
     assert isinstance(mc.ac, (pd.Series, pd.DataFrame))
@@ -463,7 +483,7 @@ def test_losses_models_pvwatts(pvwatts_dc_pvwatts_ac_system, location, weather,
     mc = ModelChain(pvwatts_dc_pvwatts_ac_system, location, dc_model='pvwatts',
                     aoi_model='no_loss', spectral_model='no_loss',
                     losses_model='no_loss')
-    mc.run_model(weather.index, weather=weather)
+    mc.run_model(weather)
     assert not np.allclose(mc.dc, dc_with_loss, equal_nan=True)
 
 
@@ -473,7 +493,7 @@ def test_losses_models_ext_def(pvwatts_dc_pvwatts_ac_system, location, weather,
     mc = ModelChain(pvwatts_dc_pvwatts_ac_system, location, dc_model='pvwatts',
                     aoi_model='no_loss', spectral_model='no_loss',
                     losses_model=constant_losses)
-    mc.run_model(weather.index, weather=weather)
+    mc.run_model(weather)
     assert m.call_count == 1
     assert isinstance(mc.ac, (pd.Series, pd.DataFrame))
     assert mc.losses == 0.9
@@ -487,7 +507,7 @@ def test_losses_models_no_loss(pvwatts_dc_pvwatts_ac_system, location, weather,
                     aoi_model='no_loss', spectral_model='no_loss',
                     losses_model='no_loss')
     assert mc.losses_model == mc.no_extra_losses
-    mc.run_model(weather.index, weather=weather)
+    mc.run_model(weather)
     assert m.call_count == 0
     assert mc.losses == 1
 
@@ -531,24 +551,6 @@ def test_bad_get_orientation():
         modelchain.get_orientation('bad value')
 
 
-@fail_on_pvlib_version('0.7')
-def test_deprecated_07():
-    # explicit system creation call because fail_on_pvlib_version
-    # does not support decorators.
-    # does not matter what the parameters are, just fake it until we make it
-    module_parameters = {'R_sh_ref': 1, 'a_ref': 1, 'I_o_ref': 1,
-                         'alpha_sc': 1, 'I_L_ref': 1, 'R_s': 1}
-    temp_model_params = {'a': -3.5, 'b': -0.05, 'deltaT': 3}
-    system = PVSystem(module_parameters=module_parameters,
-                      temperature_model_parameters=temp_model_params)
-    with pytest.warns(pvlibDeprecationWarning):
-        ModelChain(system, location,
-                   dc_model='singlediode',  # this should fail after 0.7
-                   aoi_model='no_loss', spectral_model='no_loss',
-                   temperature_model='sapm',
-                   ac_model='snlinverter')
-
-
 @fail_on_pvlib_version('0.8')
 def test_deprecated_08():
     # explicit system creation call because fail_on_pvlib_version
@@ -582,23 +584,6 @@ def test_deprecated_08():
                    ac_model='snlinverter')
 
 
-@requires_tables
-@fail_on_pvlib_version('0.7')
-def test_deprecated_clearsky_07():
-    # explicit system creation call because fail_on_pvlib_version
-    # does not support decorators.
-    system = PVSystem(module_parameters={'pdc0': 1, 'gamma_pdc': -0.003},
-                      temperature_model_parameters={'a': -3.5, 'b': -0.05,
-                                                    'deltaT': 3})
-    location = Location(32.2, -110.9)
-    mc = ModelChain(system, location, dc_model='pvwatts', ac_model='pvwatts',
-                    aoi_model='no_loss', spectral_model='no_loss')
-    times = pd.date_range(start='20160101 1200-0700',
-                          end='20160101 1800-0700', freq='6H')
-    with pytest.warns(pvlibDeprecationWarning):
-        mc.prepare_inputs(times=times)
-
-
 @requires_scipy
 def test_basic_chain_required(sam_data, cec_inverter_parameters,
                               sapm_temperature_cs5p_220m):
@@ -635,7 +620,7 @@ def test_basic_chain_alt_az(sam_data, cec_inverter_parameters,
                                     surface_tilt=surface_tilt,
                                     surface_azimuth=surface_azimuth)
 
-    expected = pd.Series(np.array([  115.40352679,  -2.00000000e-02]),
+    expected = pd.Series(np.array([115.40352679, -2.00000000e-02]),
                          index=times)
     assert_series_equal(ac, expected, check_less_precise=1)
 
@@ -656,7 +641,7 @@ def test_basic_chain_strategy(sam_data, cec_inverter_parameters,
         cec_inverter_parameters, orientation_strategy='south_at_latitude_tilt',
         altitude=altitude)
 
-    expected = pd.Series(np.array([  183.522449305,  -2.00000000e-02]),
+    expected = pd.Series(np.array([183.522449305, -2.00000000e-02]),
                          index=times)
     assert_series_equal(ac, expected, check_less_precise=1)
 
@@ -681,7 +666,7 @@ def test_basic_chain_altitude_pressure(sam_data, cec_inverter_parameters,
                                     surface_azimuth=surface_azimuth,
                                     pressure=93194)
 
-    expected = pd.Series(np.array([  116.595664887,  -2.00000000e-02]),
+    expected = pd.Series(np.array([116.595664887, -2.00000000e-02]),
                          index=times)
     assert_series_equal(ac, expected, check_less_precise=1)
 
@@ -692,7 +677,7 @@ def test_basic_chain_altitude_pressure(sam_data, cec_inverter_parameters,
                                     surface_azimuth=surface_azimuth,
                                     altitude=altitude)
 
-    expected = pd.Series(np.array([  116.595664887,  -2.00000000e-02]),
+    expected = pd.Series(np.array([116.595664887, -2.00000000e-02]),
                          index=times)
     assert_series_equal(ac, expected, check_less_precise=1)
 
@@ -732,7 +717,7 @@ def test_complete_irradiance_clean_run(system, location):
     i = pd.DataFrame(
         {'dni': [2, 3], 'dhi': [4, 6], 'ghi': [9, 5]}, index=times)
 
-    mc.complete_irradiance(times, weather=i)
+    mc.complete_irradiance(i)
 
     assert_series_equal(mc.weather['dni'],
                         pd.Series([2, 3], index=times, name='dni'))
@@ -752,18 +737,18 @@ def test_complete_irradiance(system, location):
                       'dhi': [356.543700, 465.44400]}, index=times)
 
     with pytest.warns(UserWarning):
-        mc.complete_irradiance(times, weather=i[['ghi', 'dni']])
+        mc.complete_irradiance(i[['ghi', 'dni']])
     assert_series_equal(mc.weather['dhi'],
                         pd.Series([356.543700, 465.44400],
                                   index=times, name='dhi'))
 
     with pytest.warns(UserWarning):
-        mc.complete_irradiance(times, weather=i[['dhi', 'dni']])
+        mc.complete_irradiance(i[['dhi', 'dni']])
     assert_series_equal(mc.weather['ghi'],
                         pd.Series([372.103976116, 497.087579068],
                                   index=times, name='ghi'))
 
-    mc.complete_irradiance(times, weather=i[['dhi', 'ghi']])
+    mc.complete_irradiance(i[['dhi', 'ghi']])
     assert_series_equal(mc.weather['dni'],
                         pd.Series([49.756966, 62.153947],
                                   index=times, name='dni'))