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:
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')