Repository: pvlib/pvlib-python
Pull request #1190 (2021-03-09 22:52:52)

Function to calculate temperature coefficient of power for pvsyst
 - ~~[ ] Closes #xxxx~~
 - [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.

The temperature coefficient of power is not an input to the pvsyst single diode model. Rather, it is a result of the model, the derivative of power with respect to cell temperature, at the maximum power point, at reference conditions.

cwhanse: Ready for review
wholmgren: thanks @cwhanse and @adriesse 

Patch:
diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst
index 373c879..633a9d8 100644
--- a/docs/sphinx/source/api.rst
+++ b/docs/sphinx/source/api.rst
@@ -268,6 +268,7 @@ Functions relevant for single diode models.
    pvsystem.singlediode
    pvsystem.v_from_i
    pvsystem.max_power_point
+   ivtools.sdm.pvsyst_temperature_coeff
 
 Low-level functions for solving the single diode equation.
 
@@ -334,6 +335,7 @@ Pvsyst model
    temperature.pvsyst_cell
    pvsystem.calcparams_pvsyst
    pvsystem.singlediode
+   ivtools.sdm.pvsyst_temperature_coeff
    pvsystem.dc_ohms_from_percent
    pvsystem.dc_ohmic_losses
 
diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst
index 2c8e178..43dc7b0 100644
--- a/docs/sphinx/source/whatsnew/v0.9.0.rst
+++ b/docs/sphinx/source/whatsnew/v0.9.0.rst
@@ -102,7 +102,10 @@ Enhancements
   from DC power. Use parameter ``model`` to specify which inverter model to use.
   (:pull:`1147`, :issue:`998`, :pull:`1150`)
 * Added :py:func:`~pvlib.temperature.noct_sam`, a cell temperature model
-  implemented in SAM (:pull:`1177`, :pull:`1195`)
+  implemented in SAM. (:pull:`1177`, :pull:`1195`)
+* Added :py:func:`~pvlib.ivtools.sdm.pvsyst_temperature_coeff` to calculate
+  the temperature coefficient of power for the pvsyst module model.
+   (:pull:`1190`)
 
 Bug fixes
 ~~~~~~~~~
diff --git a/pvlib/ivtools/sdm.py b/pvlib/ivtools/sdm.py
index 29f5860..04263c3 100644
--- a/pvlib/ivtools/sdm.py
+++ b/pvlib/ivtools/sdm.py
@@ -11,8 +11,10 @@ import numpy as np
 import scipy.constants
 from scipy import optimize
 from scipy.special import lambertw
+from scipy.misc import derivative
 
-from pvlib.pvsystem import singlediode, v_from_i
+from pvlib.pvsystem import calcparams_pvsyst, singlediode, v_from_i
+from pvlib.singlediode import bishop88_mpp
 
 from pvlib.ivtools.utils import rectify_iv_curve, _numdiff
 from pvlib.ivtools.sde import _fit_sandia_cocontent
@@ -1252,3 +1254,91 @@ def _calc_theta_phi_exact(vmp, imp, iph, io, rs, rsh, nnsvth):
     theta = np.transpose(theta)
 
     return theta, phi
+
+
+def pvsyst_temperature_coeff(alpha_sc, gamma_ref, mu_gamma, I_L_ref, I_o_ref,
+                             R_sh_ref, R_sh_0, R_s, cells_in_series,
+                             R_sh_exp=5.5, EgRef=1.121, irrad_ref=1000,
+                             temp_ref=25):
+    r"""
+    Calculates the temperature coefficient of power for a pvsyst single
+    diode model.
+
+    The temperature coefficient is determined as the numerical derivative
+    :math:`\frac{dP}{dT}` at the maximum power point at reference conditions
+    [1]_.
+
+    Parameters
+    ----------
+    alpha_sc : float
+        The short-circuit current temperature coefficient of the module. [A/C]
+
+    gamma_ref : float
+        The diode ideality factor. [unitless]
+
+    mu_gamma : float
+        The temperature coefficient for the diode ideality factor. [1/K]
+
+    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. [ohm]
+
+    R_sh_0 : float
+        The shunt resistance at zero irradiance conditions. [ohm]
+
+    R_s : float
+        The series resistance at reference conditions. [ohm]
+
+    cells_in_series : int
+        The number of cells connected in series.
+
+    R_sh_exp : float, default 5.5
+        The exponent in the equation for shunt resistance. [unitless]
+
+    EgRef : float, default 1.121
+        The energy bandgap of the module's cells at reference temperature.
+        Default of 1.121 eV is for crystalline silicon. Must be positive. [eV]
+
+    irrad_ref : float, default 1000
+        Reference irradiance. [W/m^2].
+
+    temp_ref : float, default 25
+        Reference cell temperature. [C]
+
+
+    Returns
+    -------
+    gamma_pdc : float
+        Temperature coefficient of power at maximum power point at reference
+        conditions. [1/C]
+
+    References
+    ----------
+    .. [1] K. Sauer, T. Roessler, C. W. Hansen, Modeling the Irradiance and
+       Temperature Dependence of Photovoltaic Modules in PVsyst, IEEE Journal
+       of Photovoltaics v5(1), January 2015.
+    """
+
+    def maxp(temp_cell, irrad_ref, alpha_sc, gamma_ref, mu_gamma, I_L_ref,
+             I_o_ref, R_sh_ref, R_sh_0, R_s, cells_in_series, R_sh_exp, EgRef,
+             temp_ref):
+        params = calcparams_pvsyst(
+            irrad_ref, temp_cell, alpha_sc, gamma_ref, mu_gamma, I_L_ref,
+            I_o_ref, R_sh_ref, R_sh_0, R_s, cells_in_series, R_sh_exp, EgRef,
+            irrad_ref, temp_ref)
+        res = bishop88_mpp(*params)
+        return res[2]
+
+    args = (irrad_ref, alpha_sc, gamma_ref, mu_gamma, I_L_ref,
+            I_o_ref, R_sh_ref, R_sh_0, R_s, cells_in_series, R_sh_exp, EgRef,
+            temp_ref)
+    pmp = maxp(temp_ref, *args)
+    gamma_pdc = derivative(maxp, temp_ref, args=args)
+    return gamma_pdc / pmp


Test patch:
diff --git a/pvlib/tests/ivtools/test_sdm.py b/pvlib/tests/ivtools/test_sdm.py
index 67bae22..c1df6b1 100644
--- a/pvlib/tests/ivtools/test_sdm.py
+++ b/pvlib/tests/ivtools/test_sdm.py
@@ -2,6 +2,7 @@ import numpy as np
 import pandas as pd
 
 import pytest
+from numpy.testing import assert_allclose
 
 from pvlib.ivtools import sdm
 from pvlib import pvsystem
@@ -306,7 +307,7 @@ def test__update_rsh_fixed_pt_nans(vmp, imp, iph, io, rs, rsh, nnsvth,
 def test__update_rsh_fixed_pt_vmp0():
     outrsh = sdm._update_rsh_fixed_pt(vmp=0., imp=2., iph=2., io=2., rs=2.,
                                       rsh=2., nnsvth=2.)
-    np.testing.assert_allclose(outrsh, np.array([502.]), atol=.0001)
+    assert_allclose(outrsh, np.array([502.]), atol=.0001)
 
 
 def test__update_rsh_fixed_pt_vector():
@@ -318,7 +319,7 @@ def test__update_rsh_fixed_pt_vector():
                                       imp=np.array([.2, .2, -1., 2.]),
                                       vmp=np.array([0., -1, 0., 0.]))
     assert np.all(np.isnan(outrsh[0:3]))
-    np.testing.assert_allclose(outrsh[3], np.array([502.]), atol=.0001)
+    assert_allclose(outrsh[3], np.array([502.]), atol=.0001)
 
 
 @pytest.mark.parametrize('voc, iph, io, rs, rsh, nnsvth, expected', [
@@ -329,7 +330,7 @@ def test__update_rsh_fixed_pt_vector():
     (0., 2., 2., 2., 2., 2., 17.9436)])
 def test__update_io(voc, iph, io, rs, rsh, nnsvth, expected):
     outio = sdm._update_io(voc, iph, io, rs, rsh, nnsvth)
-    np.testing.assert_allclose(outio, expected, atol=.0001)
+    assert_allclose(outio, expected, atol=.0001)
 
 
 @pytest.mark.parametrize('voc, iph, io, rs, rsh, nnsvth', [
@@ -347,8 +348,8 @@ def test__update_io_nan(voc, iph, io, rs, rsh, nnsvth):
     (0., 2., 2., 2., 2., 2., 2., (1.5571, 2.))])
 def test__calc_theta_phi_exact(vmp, imp, iph, io, rs, rsh, nnsvth, expected):
     theta, phi = sdm._calc_theta_phi_exact(vmp, imp, iph, io, rs, rsh, nnsvth)
-    np.testing.assert_allclose(theta, expected[0], atol=.0001)
-    np.testing.assert_allclose(phi, expected[1], atol=.0001)
+    assert_allclose(theta, expected[0], atol=.0001)
+    assert_allclose(phi, expected[1], atol=.0001)
 
 
 @pytest.mark.parametrize('vmp, imp, iph, io, rs, rsh, nnsvth', [
@@ -365,7 +366,7 @@ def test__calc_theta_phi_exact_one_nan():
     theta, phi = sdm._calc_theta_phi_exact(imp=2., iph=2., vmp=2., io=2.,
                                            nnsvth=2., rs=0., rsh=2.)
     assert np.isnan(theta)
-    np.testing.assert_allclose(phi, 2., atol=.0001)
+    assert_allclose(phi, 2., atol=.0001)
 
 
 def test__calc_theta_phi_exact_vector():
@@ -379,4 +380,23 @@ def test__calc_theta_phi_exact_vector():
     assert np.isnan(theta[0])
     assert np.isnan(theta[1])
     assert np.isnan(phi[0])
-    np.testing.assert_allclose(phi[1], 2.2079, atol=.0001)
+    assert_allclose(phi[1], 2.2079, atol=.0001)
+
+
+def test_pvsyst_temperature_coeff():
+    # test for consistency with dP/dT estimated with secant rule
+    params = {'alpha_sc': 0., 'gamma_ref': 1.1, 'mu_gamma': 0.,
+              'I_L_ref': 6., 'I_o_ref': 5.e-9, 'R_sh_ref': 200.,
+              'R_sh_0': 2000., 'R_s': 0.5, 'cells_in_series': 60}
+    expected = -0.004886706494879083
+    # params defines a Pvsyst model for a notional module.
+    # expected value is created by calculating power at 1000 W/m2, and cell
+    # temperature of 24 and 26C, using pvsystem.calcparams_pvsyst and
+    # pvsystem.singlediode. The derivative (value for expected) is estimated
+    # as the slope (p_mp at 26C - p_mp at 24C) / 2
+    # using the secant rule for derivatives.
+    gamma_pdc = sdm.pvsyst_temperature_coeff(
+        params['alpha_sc'], params['gamma_ref'], params['mu_gamma'],
+        params['I_L_ref'], params['I_o_ref'], params['R_sh_ref'],
+        params['R_sh_0'], params['R_s'], params['cells_in_series'])
+    assert_allclose(gamma_pdc, expected, rtol=0.0005)