Repository: pvlib/pvlib-python
Pull request #815 (2019-11-07 15:53:44)

Change units on SAPM effective irradiance from suns to W/m2
 - [x] Closes  #472
 - [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):

Changes units on `pvsystem.sapm` input `effective_irradiance` from suns to W/m2, for consistency with other DC models.  Deprecation is handled by raising a `pvlibDeprecationWarning` if all `effective_irradiance` values are less than 2.
Adds kwargs `irrad_ref` and `temp_ref` to `pvsystem.sapm`; these constants were hardwired.
Changes units on output of `pvsystem.sapm_effective_irradiance` from suns to W/m2.
Docstring enhancement for `pvsystem.sapm_effective_irradiance. 


Patch:
diff --git a/docs/sphinx/source/whatsnew/v0.7.0.rst b/docs/sphinx/source/whatsnew/v0.7.0.rst
index 4bfe9fa..81e0213 100644
--- a/docs/sphinx/source/whatsnew/v0.7.0.rst
+++ b/docs/sphinx/source/whatsnew/v0.7.0.rst
@@ -10,8 +10,16 @@ compatibility notes.
 **Python 2.7 support ended on June 1, 2019.** (:issue:`501`)
 **Minimum numpy version is now 1.10.4. Minimum pandas version is now 0.18.1.**
 
-API Changes
-~~~~~~~~~~~
+API Breaking Changes
+~~~~~~~~~~~~~~~~~~~~
+* The `effective_irradiance` argument for :py:func:`pvsystem.sapm` now requires
+  units of W/m^2. Previously, units for this input were suns. A RuntimeWarning
+  warning is raised if all `effective_irradiance < 2.0`.
+* The output of :py:func:`pvsystem.sapm_effective_irradiance` is now in units
+  of W/m2 rather than suns.
+
+API Changes with Deprecations
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 * Changes related to cell temperature models (:issue:`678`):
    * Changes to functions
       - Moved functions for cell temperature from `pvsystem.py` to `temperature.py`.
diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py
index 46bf080..1ab916c 100644
--- a/pvlib/modelchain.py
+++ b/pvlib/modelchain.py
@@ -440,7 +440,7 @@ class ModelChain(object):
                              'set the model with the dc_model kwarg.')
 
     def sapm(self):
-        self.dc = self.system.sapm(self.effective_irradiance/1000.,
+        self.dc = self.system.sapm(self.effective_irradiance,
                                    self.cell_temperature)
 
         self.dc = self.system.scale_voltage_current_power(self.dc)
diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py
index 550bea0..93491a5 100644
--- a/pvlib/pvsystem.py
+++ b/pvlib/pvsystem.py
@@ -563,28 +563,25 @@ class PVSystem(object):
         Parameters
         ----------
         poa_direct : numeric
-            The direct irradiance incident upon the module.
+            The direct irradiance incident upon the module.  [W/m2]
 
         poa_diffuse : numeric
-            The diffuse irradiance incident on module.
+            The diffuse irradiance incident on module.  [W/m2]
 
         airmass_absolute : numeric
-            Absolute airmass.
+            Absolute airmass. [unitless]
 
         aoi : numeric
-            Angle of incidence in degrees.
-
-        reference_irradiance : numeric, default 1000
-            Reference irradiance by which to divide the input irradiance.
+            Angle of incidence. [degrees]
 
         Returns
         -------
         effective_irradiance : numeric
-            The SAPM effective irradiance.
+            The SAPM effective irradiance. [W/m2]
         """
         return sapm_effective_irradiance(
             poa_direct, poa_diffuse, airmass_absolute, aoi,
-            self.module_parameters, reference_irradiance=reference_irradiance)
+            self.module_parameters)
 
     def pvsyst_celltemp(self, poa_global, temp_air, wind_speed=1.0):
         """Uses :py:func:`temperature.pvsyst_cell` to calculate cell
@@ -1580,10 +1577,11 @@ def sapm(effective_irradiance, temp_cell, module):
     Parameters
     ----------
     effective_irradiance : numeric
-        Effective irradiance (suns).
+        Irradiance reaching the module's cells, after reflections and
+        adjustment for spectrum. [W/m2]
 
     temp_cell : numeric
-        The cell temperature (degrees C).
+        Cell temperature [C].
 
     module : dict-like
         A dict or Series defining the SAPM parameters. See the notes section
@@ -1659,12 +1657,23 @@ def sapm(effective_irradiance, temp_cell, module):
     temperature.sapm_module
     '''
 
-    T0 = 25
+    # TODO: someday, change temp_ref and irrad_ref to reference_temperature and
+    # reference_irradiance and expose
+    temp_ref = 25
+    irrad_ref = 1000
+    # TODO: remove this warning in v0.8 after deprecation period for change in
+    # effective irradiance units, made in v0.7
+    if np.all(effective_irradiance) < 2.0:
+        import warnings
+        warnings.warn('effective_irradiance inputs appear to be in suns.'
+                      ' Units changed in v0.7 from suns to W/m2',
+                      RuntimeWarning)
+
     q = 1.60218e-19  # Elementary charge in units of coulombs
     kb = 1.38066e-23  # Boltzmann's constant in units of J/K
 
     # avoid problem with integer input
-    Ee = np.array(effective_irradiance, dtype='float64')
+    Ee = np.array(effective_irradiance, dtype='float64') / irrad_ref
 
     # set up masking for 0, positive, and nan inputs
     Ee_gt_0 = np.full_like(Ee, False, dtype='bool')
@@ -1687,32 +1696,32 @@ def sapm(effective_irradiance, temp_cell, module):
     out = OrderedDict()
 
     out['i_sc'] = (
-        module['Isco'] * Ee * (1 + module['Aisc']*(temp_cell - T0)))
+        module['Isco'] * Ee * (1 + module['Aisc']*(temp_cell - temp_ref)))
 
     out['i_mp'] = (
         module['Impo'] * (module['C0']*Ee + module['C1']*(Ee**2)) *
-        (1 + module['Aimp']*(temp_cell - T0)))
+        (1 + module['Aimp']*(temp_cell - temp_ref)))
 
     out['v_oc'] = np.maximum(0, (
         module['Voco'] + cells_in_series * delta * logEe +
-        Bvoco*(temp_cell - T0)))
+        Bvoco*(temp_cell - temp_ref)))
 
     out['v_mp'] = np.maximum(0, (
         module['Vmpo'] +
         module['C2'] * cells_in_series * delta * logEe +
         module['C3'] * cells_in_series * ((delta * logEe) ** 2) +
-        Bvmpo*(temp_cell - T0)))
+        Bvmpo*(temp_cell - temp_ref)))
 
     out['p_mp'] = out['i_mp'] * out['v_mp']
 
     out['i_x'] = (
         module['IXO'] * (module['C4']*Ee + module['C5']*(Ee**2)) *
-        (1 + module['Aisc']*(temp_cell - T0)))
+        (1 + module['Aisc']*(temp_cell - temp_ref)))
 
     # the Ixx calculation in King 2004 has a typo (mixes up Aisc and Aimp)
     out['i_xx'] = (
         module['IXXO'] * (module['C6']*Ee + module['C7']*(Ee**2)) *
-        (1 + module['Aisc']*(temp_cell - T0)))
+        (1 + module['Aisc']*(temp_cell - temp_ref)))
 
     if isinstance(out['i_sc'], pd.Series):
         out = pd.DataFrame(out)
@@ -1839,45 +1848,70 @@ def sapm_spectral_loss(airmass_absolute, module):
 
 
 def sapm_effective_irradiance(poa_direct, poa_diffuse, airmass_absolute, aoi,
-                              module, reference_irradiance=1000):
-    """
+                              module):
+    r"""
     Calculates the SAPM effective irradiance using the SAPM spectral
     loss and SAPM angle of incidence loss functions.
 
     Parameters
     ----------
     poa_direct : numeric
-        The direct irradiance incident upon the module.
+        The direct irradiance incident upon the module. [W/m2]
 
     poa_diffuse : numeric
-        The diffuse irradiance incident on module.
+        The diffuse irradiance incident on module.  [W/m2]
 
     airmass_absolute : numeric
-        Absolute airmass.
+        Absolute airmass. [unitless]
 
     aoi : numeric
-        Angle of incidence in degrees.
+        Angle of incidence. [degrees]
 
     module : dict-like
         A dict, Series, or DataFrame defining the SAPM performance
         parameters. See the :py:func:`sapm` notes section for more
         details.
 
-    reference_irradiance : numeric, default 1000
-        Reference irradiance by which to divide the input irradiance.
-
     Returns
     -------
     effective_irradiance : numeric
-        The SAPM effective irradiance.
+        Effective irradiance accounting for reflections and spectral content.
+        [W/m2]
+
+    Notes
+    -----
+    The SAPM model for effective irradiance [1] translates broadband direct and
+    diffuse irradiance on the plane of array to the irradiance absorbed by a
+    module's cells.
+
+    The model is
+    .. math::
+
+        `Ee = f_1(AM_a) (E_b f_2(AOI) + f_d E_d)`
+
+    where :math:`Ee` is effective irradiance (W/m2), :math:`f_1` is a fourth
+    degree polynomial in air mass :math:`AM_a`, :math:`E_b` is beam (direct)
+    irradiance on the plane of array, :math:`E_d` is diffuse irradiance on the
+    plane of array, :math:`f_2` is a fifth degree polynomial in the angle of
+    incidence :math:`AOI`, and :math:`f_d` is the fraction of diffuse
+    irradiance on the plane of array that is not reflected away.
+
+    References
+    ----------
+    [1] D. King et al, "Sandia Photovoltaic Array Performance Model",
+    SAND2004-3535, Sandia National Laboratories, Albuquerque, NM
+
+    See also
+    --------
+    pvlib.iam.sapm
+    pvlib.pvsystem.sapm_spectral_loss
+    pvlib.pvsystem.sapm
     """
 
     F1 = sapm_spectral_loss(airmass_absolute, module)
     F2 = iam.sapm(aoi, module)
 
-    E0 = reference_irradiance
-
-    Ee = F1 * (poa_direct*F2 + module['FD']*poa_diffuse) / E0
+    Ee = F1 * (poa_direct * F2 + module['FD'] * poa_diffuse)
 
     return Ee
 
@@ -1992,7 +2026,7 @@ def singlediode(photocurrent, saturation_current, resistance_series,
     the IV curve are linearly spaced.
 
     References
-    -----------
+    ----------
     [1] S.R. Wenham, M.A. Green, M.E. Watt, "Applied Photovoltaics" ISBN
     0 86758 909 4
 


Test patch:
diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py
index bc9f142..c9bec95 100644
--- a/pvlib/test/test_pvsystem.py
+++ b/pvlib/test/test_pvsystem.py
@@ -195,7 +195,8 @@ def test_retrieve_sam_cecinverter():
 def test_sapm(sapm_module_params):
 
     times = pd.date_range(start='2015-01-01', periods=5, freq='12H')
-    effective_irradiance = pd.Series([-1, 0.5, 1.1, np.nan, 1], index=times)
+    effective_irradiance = pd.Series([-1000, 500, 1100, np.nan, 1000],
+                                     index=times)
     temp_cell = pd.Series([10, 25, 50, 25, np.nan], index=times)
 
     out = pvsystem.sapm(effective_irradiance, temp_cell, sapm_module_params)
@@ -216,7 +217,7 @@ def test_sapm(sapm_module_params):
 
     assert_frame_equal(out, expected, check_less_precise=4)
 
-    out = pvsystem.sapm(1, 25, sapm_module_params)
+    out = pvsystem.sapm(1000, 25, sapm_module_params)
 
     expected = OrderedDict()
     expected['i_sc'] = 5.09115
@@ -235,10 +236,21 @@ def test_sapm(sapm_module_params):
                   pd.Series(sapm_module_params))
 
 
+def test_pvsystem_sapm_warning(sapm_module_params):
+    # deprecation warning for change in effective_irradiance units in
+    # pvsystem.sapm
+    # TODO: remove after deprecation period (v0.8)
+    effective_irradiance = np.array([0.1, 0.2, 1.3])
+    temp_cell = np.array([25, 25, 50])
+    warn_txt = 'effective_irradiance inputs appear to be in suns'
+    with pytest.warns(RuntimeWarning, match=warn_txt):
+        pvsystem.sapm(effective_irradiance, temp_cell, sapm_module_params)
+
+
 def test_PVSystem_sapm(sapm_module_params, mocker):
     mocker.spy(pvsystem, 'sapm')
     system = pvsystem.PVSystem(module_parameters=sapm_module_params)
-    effective_irradiance = 0.5
+    effective_irradiance = 500
     temp_cell = 25
     out = system.sapm(effective_irradiance, temp_cell)
     pvsystem.sapm.assert_called_once_with(effective_irradiance, temp_cell,
@@ -295,33 +307,23 @@ def test_PVSystem_first_solar_spectral_loss(module_parameters, module_type,
 
 
 @pytest.mark.parametrize('test_input,expected', [
-    ([1000, 100, 5, 45, 1000], 1.1400510967821877),
+    ([1000, 100, 5, 45], 1140.0510967821877),
     ([np.array([np.nan, 1000, 1000]),
       np.array([100, np.nan, 100]),
       np.array([1.1, 1.1, 1.1]),
-      np.array([10, 10, 10]),
-      1000],
-     np.array([np.nan, np.nan, 1.081157])),
+      np.array([10, 10, 10])],
+     np.array([np.nan, np.nan, 1081.1574])),
     ([pd.Series([1000]), pd.Series([100]), pd.Series([1.1]),
-      pd.Series([10]), 1370],
-     pd.Series([0.789166]))
+      pd.Series([10])],
+     pd.Series([1081.1574]))
 ])
 def test_sapm_effective_irradiance(sapm_module_params, test_input, expected):
-
-    try:
-        kwargs = {'reference_irradiance': test_input[4]}
-        test_input = test_input[:-1]
-    except IndexError:
-        kwargs = {}
-
     test_input.append(sapm_module_params)
-
-    out = pvsystem.sapm_effective_irradiance(*test_input, **kwargs)
-
+    out = pvsystem.sapm_effective_irradiance(*test_input)
     if isinstance(test_input, pd.Series):
         assert_series_equal(out, expected, check_less_precise=4)
     else:
-        assert_allclose(out, expected, atol=1e-4)
+        assert_allclose(out, expected, atol=1e-1)
 
 
 def test_PVSystem_sapm_effective_irradiance(sapm_module_params, mocker):
@@ -332,15 +334,16 @@ def test_PVSystem_sapm_effective_irradiance(sapm_module_params, mocker):
     poa_diffuse = 100
     airmass_absolute = 1.5
     aoi = 0
-    reference_irradiance = 1000
-
+    p = (sapm_module_params['A4'], sapm_module_params['A3'],
+         sapm_module_params['A2'], sapm_module_params['A1'],
+         sapm_module_params['A0'])
+    f1 = np.polyval(p, airmass_absolute)
+    expected = f1 * (poa_direct + sapm_module_params['FD'] * poa_diffuse)
     out = system.sapm_effective_irradiance(
-        poa_direct, poa_diffuse, airmass_absolute,
-        aoi, reference_irradiance=reference_irradiance)
+        poa_direct, poa_diffuse, airmass_absolute, aoi)
     pvsystem.sapm_effective_irradiance.assert_called_once_with(
-        poa_direct, poa_diffuse, airmass_absolute, aoi, sapm_module_params,
-        reference_irradiance=reference_irradiance)
-    assert_allclose(out, 1, atol=0.1)
+        poa_direct, poa_diffuse, airmass_absolute, aoi, sapm_module_params)
+    assert_allclose(out, expected, atol=0.1)
 
 
 def test_PVSystem_sapm_celltemp(mocker):
@@ -1464,8 +1467,10 @@ def test_PVSystem_pvwatts_ac_kwargs(mocker):
 
 @fail_on_pvlib_version('0.8')
 def test_deprecated_08():
+    # deprecated function pvsystem.sapm_celltemp
     with pytest.warns(pvlibDeprecationWarning):
         pvsystem.sapm_celltemp(1000, 25, 1)
+    # deprecated function pvsystem.pvsyst_celltemp
     with pytest.warns(pvlibDeprecationWarning):
         pvsystem.pvsyst_celltemp(1000, 25)
     module_parameters = {'R_sh_ref': 1, 'a_ref': 1, 'I_o_ref': 1,