Repository: pvlib/pvlib-python (python)

Issue:
Implement IEC 61853 module temperature model
The Faiman module temperature model is used in IEC 61853.  It conceptually not much different from others, but uses slightly different parameters.

Pull request description:
Implement IEC 61853 module temperature model
 - [x] Closes #750
 - [x] I am familiar with the [contributing guidelines](https://pvlib-python.readthedocs.io/en/latest/contributing.html)
 - [x] Tests added
 - [x] Updates entries to [`docs/sphinx/source/api.rst`](https://github.com/pvlib/pvlib-python/blob/master/docs/sphinx/source/api.rst) for API changes.
 - [x] Adds description and name entries in the appropriate "what's new" file in [`docs/sphinx/source/whatsnew`](https://github.com/pvlib/pvlib-python/tree/master/docs/sphinx/source/whatsnew) for all changes. Includes link to the GitHub Issue with `` :issue:`num` `` or this Pull Request with `` :pull:`num` ``. Includes contributor name and/or GitHub username (link with `` :ghuser:`user` ``).
 - [x] New code is fully documented. Includes [numpydoc](https://numpydoc.readthedocs.io/en/latest/format.html) compliant docstrings, examples, and comments where necessary.
 - [x] Pull request is nearly complete and ready for detailed review.
 - [x] Maintainer: Appropriate GitHub Labels and Milestone are assigned to the Pull Request and linked Issue.


Patch:
diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst
index f55faa5..002e694 100644
--- a/docs/sphinx/source/api.rst
+++ b/docs/sphinx/source/api.rst
@@ -233,6 +233,7 @@ PV temperature models
    temperature.sapm_cell
    temperature.sapm_module
    temperature.pvsyst_cell
+   temperature.faiman
 
 Single diode models
 -------------------
diff --git a/docs/sphinx/source/whatsnew/v0.7.0.rst b/docs/sphinx/source/whatsnew/v0.7.0.rst
index c7d4fa5..e2f247b 100644
--- a/docs/sphinx/source/whatsnew/v0.7.0.rst
+++ b/docs/sphinx/source/whatsnew/v0.7.0.rst
@@ -119,8 +119,12 @@ Other API Changes
 
 Enhancements
 ~~~~~~~~~~~~
+* Created one new temperature model function:
+  :py:func:`pvlib.temperature.faiman`. (:issue:`750`)
 * Created two new incidence angle modifier (IAM) functions:
   :py:func:`pvlib.iam.martin_ruiz` and :py:func:`pvlib.iam.interp`. (:issue:`751`)
+* Created one new incidence angle modifier (IAM) function for diffuse irradiance:
+  :py:func:`pvlib.iam.martin_ruiz_diffuse`. (: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`)
@@ -138,8 +142,6 @@ Enhancements
 * Add `timeout` to :py:func:`pvlib.iotools.get_psm3`.
 * Add :py:func:`~pvlib.scaling.wvm`, a port of the wavelet variability model for
   computing reductions in variability due to a spatially distributed plant.
-* Created one new incidence angle modifier (IAM) function for diffuse irradiance:
-  :py:func:`pvlib.iam.martin_ruiz_diffuse`. (:issue:`751`)
 * Add :py:meth:`~pvlib.location.Location.from_epw`, a method to create a Location
   object from epw metadata, typically coming from `pvlib.iotools.epw.read_epw`.
 
diff --git a/pvlib/temperature.py b/pvlib/temperature.py
index f43d372..326c677 100644
--- a/pvlib/temperature.py
+++ b/pvlib/temperature.py
@@ -269,3 +269,69 @@ def pvsyst_cell(poa_global, temp_air, wind_speed=1.0, u_c=29.0, u_v=0.0,
     heat_input = poa_global * alpha_absorption * (1 - eta_m)
     temp_difference = heat_input / total_loss_factor
     return temp_air + temp_difference
+
+
+def faiman(poa_global, temp_air, wind_speed=1.0, u0=25.0, u1=6.84):
+    '''
+    Calculate cell or module temperature using an empirical heat loss factor
+    model as proposed by Faiman [1] and adopted in the IEC 61853
+    standards [2] and [3].
+
+    Usage of this model in the IEC 61853 standard does not distinguish
+    between cell and module temperature.
+
+    Parameters
+    ----------
+    poa_global : numeric
+        Total incident irradiance [W/m^2].
+
+    temp_air : numeric
+        Ambient dry bulb temperature [C].
+
+    wind_speed : numeric, default 1.0
+        Wind speed in m/s measured at the same height for which the wind loss
+        factor was determined.  The default value 1.0 m/s is the wind
+        speed at module height used to determine NOCT. [m/s]
+
+    u0 : numeric, default 25.0
+        Combined heat loss factor coefficient. The default value is one
+        determined by Faiman for 7 silicon modules. [W/(m^2 C)].
+
+    u1 : numeric, default 6.84
+        Combined heat loss factor influenced by wind. The default value is one
+        determined by Faiman for 7 silicon modules. [(W/m^2 C)(m/s)].
+
+    Returns
+    -------
+    numeric, values in degrees Celsius
+
+    Notes
+    -----
+    All arguments may be scalars or vectors. If multiple arguments
+    are vectors they must be the same length.
+
+    References
+    ----------
+    [1] Faiman, D. (2008). "Assessing the outdoor operating temperature of
+    photovoltaic modules." Progress in Photovoltaics 16(4): 307-315.
+
+    [2] "IEC 61853-2 Photovoltaic (PV) module performance testing and energy
+    rating - Part 2: Spectral responsivity, incidence angle and module
+    operating temperature measurements". IEC, Geneva, 2018.
+
+    [3] "IEC 61853-3 Photovoltaic (PV) module performance testing and energy
+    rating - Part 3: Energy rating of PV modules". IEC, Geneva, 2018.
+
+    '''
+    # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Dec., 2019
+
+    # The following lines may seem odd since u0 & u1 are probably scalar,
+    # but it serves an indirect and easy way of allowing lists and
+    # tuples for the other function arguments.
+    u0 = np.asanyarray(u0)
+    u1 = np.asanyarray(u1)
+
+    total_loss_factor = u0 + u1 * wind_speed
+    heat_input = poa_global
+    temp_difference = heat_input / total_loss_factor
+    return temp_air + temp_difference


Test patch:
diff --git a/pvlib/test/test_temperature.py b/pvlib/test/test_temperature.py
index c3112d8..7027c97 100644
--- a/pvlib/test/test_temperature.py
+++ b/pvlib/test/test_temperature.py
@@ -90,6 +90,45 @@ def test_pvsyst_cell_series():
     assert_series_equal(expected, result)
 
 
+def test_faiman_default():
+    result = temperature.faiman(900, 20, 5)
+    assert_allclose(result, 35.203, 0.001)
+
+
+def test_faiman_kwargs():
+    result = temperature.faiman(900, 20, wind_speed=5.0, u0=22.0, u1=6.)
+    assert_allclose(result, 37.308, 0.001)
+
+
+def test_faiman_list():
+    temps = [0, 10, 5]
+    irrads = [0, 500, 0]
+    winds = [10, 5, 0]
+    result = temperature.faiman(irrads, temps, wind_speed=winds)
+    expected = np.array([0.0, 18.446, 5.0])
+    assert_allclose(expected, result, 3)
+
+
+def test_faiman_ndarray():
+    temps = np.array([0, 10, 5])
+    irrads = np.array([0, 500, 0])
+    winds = np.array([10, 5, 0])
+    result = temperature.faiman(irrads, temps, wind_speed=winds)
+    expected = np.array([0.0, 18.446, 5.0])
+    assert_allclose(expected, result, 3)
+
+
+def test_faiman_series():
+    times = pd.date_range(start="2015-01-01", end="2015-01-02", freq="12H")
+    temps = pd.Series([0, 10, 5], index=times)
+    irrads = pd.Series([0, 500, 0], index=times)
+    winds = pd.Series([10, 5, 0], index=times)
+
+    result = temperature.faiman(irrads, temps, wind_speed=winds)
+    expected = pd.Series([0.0, 18.446, 5.0], index=times)
+    assert_series_equal(expected, result)
+
+
 def test__temperature_model_params():
     params = temperature._temperature_model_params('sapm',
                                                    'open_rack_glass_glass')