Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add method to fit Huld model #1979

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/sphinx/source/reference/pv_modeling/system_models.rst
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,4 @@ PVGIS model
:toctree: ../generated/

pvarray.huld
pvarray.fit_huld
1 change: 1 addition & 0 deletions docs/sphinx/source/whatsnew/v0.10.4.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ v0.10.4 (March 19, 2024)
Enhancements
~~~~~~~~~~~~
* Added the Huld PV model used by PVGIS (:pull:`1940`)
* Added a method to fit the Huld PV model (:pull:`1979`)
cwhanse marked this conversation as resolved.
Show resolved Hide resolved
* Add :py:func:`~pvlib.iotools.get_solargis` for retrieving Solargis
irradiance data. (:pull:`1969`)
* Added function :py:func:`pvlib.shading.projected_solar_zenith_angle`,
Expand Down
108 changes: 104 additions & 4 deletions pvlib/pvarray.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@
return k


def huld(effective_irradiance, temp_mod, pdc0, k=None, cell_type=None):
def huld(effective_irradiance, temp_module, pdc0, k=None, cell_type=None):
r"""
Power (DC) using the Huld model.

Expand All @@ -263,13 +263,13 @@
----------
effective_irradiance : numeric
The irradiance that is converted to photocurrent. [:math:`W/m^2`]
temp_mod: numeric
temp_module: numeric
Module back-surface temperature. [C]
pdc0: numeric
Power of the modules at reference conditions 1000 :math:`W/m^2`
and :math:`25^{\circ}C`. [W]
k : tuple, optional
Empirical coefficients used in the power model. Length 6. If ``k`` is
Empirical coefficients used in the Huld model. Length 6. If ``k`` is
not provided, ``cell_type`` must be specified.
cell_type : str, optional
If provided, must be one of ``'cSi'``, ``'CIS'``, or ``'CdTe'``.
Expand Down Expand Up @@ -330,6 +330,10 @@
E. Dunlop. A power-rating model for crystalline silicon PV modules.
Solar Energy Materials and Solar Cells 95, (2011), pp. 3359-3369.
:doi:`10.1016/j.solmat.2011.07.026`.

See Also
--------
pvlib.pvarray.fit_huld
"""
if k is None:
if cell_type is not None:
Expand All @@ -338,7 +342,7 @@
raise ValueError('Either k or cell_type must be specified')

gprime = effective_irradiance / 1000
tprime = temp_mod - 25
tprime = temp_module - 25
# accomodate gprime<=0
with np.errstate(divide='ignore'):
logGprime = np.log(gprime, out=np.zeros_like(gprime),
Expand All @@ -348,3 +352,99 @@
k[2] * tprime + k[3] * tprime * logGprime +
k[4] * tprime * logGprime**2 + k[5] * tprime**2)
return pdc


def _build_iec61853():
ee = np.array([100, 100, 200, 200, 400, 400, 400, 600, 600, 600, 600,
800, 800, 800, 800, 1000, 1000, 1000, 1000, 1100, 1100,
1100], dtype=np.float64).T
tc = np.array([15, 25, 15, 25, 15, 25, 50, 15, 25, 50, 75,
15, 25, 50, 75, 15, 25, 50, 75, 25, 50, 75],
dtype=np.float64).T
return ee, tc


def fit_huld(effective_irradiance, temp_module, pdc, method='ols'):
r'''
Fit the Huld model to the input data.

Parameters
----------
effective_irradiance : array-like
The irradiance that is converted to photocurrent. [:math:`W/m^2`]
temp_module: array-like
Module back-surface temperature. [C]
pdc: array-like
DC power at ``effective_irradiance`` and ``temp_module``. [W]
method : string, default 'ols'
The regression method. Must be either ``'ols'`` or ``'robust'``.

Returns
-------
pdc0: numeric
Power of the modules at reference conditions 1000 :math:`W/m^2`
and :math:`25^{\circ}C`. [W]
k : tuple
Empirical coefficients used in the Huld model. Length 6.

Notes
-----
Requires ``statsmodels``.

Input data ``effective_irradiance``, ``temp_module`` and ``pdc`` must be
equal length. Data are aligned as columns and any row with a NaN in any
column is dropped. Remaining data must be at least length 7.

Huld [1]_ describes fitting by least-squares but doesn't detail the
procedure, although it is reasonable that ordinary least-squares regression
was used. Ordinary least-squares regression can be unduly influenced by
data points with large deviations from the mean of the dependent variable
``pdc``. For example, these data may occur when short-duration shading is
present. If filtering is applied to exclude such data, a suitable model
can be obtained with ordinary least-squares regression. As an alternative,
this function also provides a 'robust' regression option (an iteratively
reweighted least-squares method) that is more tolerant of outliers in the
dependent variable.

References
----------
.. [1] T. Huld, G. Friesen, A. Skoczek, R. Kenny, T. Sample, M. Field,
E. Dunlop. A power-rating model for crystalline silicon PV modules.
Solar Energy Materials and Solar Cells 95, (2011), pp. 3359-3369.
:doi:`10.1016/j.solmat.2011.07.026`.

See Also
--------
pvlib.pvarray.huld
cwhanse marked this conversation as resolved.
Show resolved Hide resolved
'''

try:
import statsmodels.api as sm
except ImportError:
raise ImportError(

Check warning on line 424 in pvlib/pvarray.py

View check run for this annotation

Codecov / codecov/patch

pvlib/pvarray.py#L423-L424

Added lines #L423 - L424 were not covered by tests
'fit_huld requires statsmodels')

gprime = effective_irradiance / 1000
tprime = temp_module - 25
# accomodate gprime<=0
with np.errstate(divide='ignore'):
logGprime = np.log(gprime, out=np.zeros_like(gprime),
where=np.array(gprime > 0))
Y = np.divide(pdc, gprime, out=np.zeros_like(gprime),
where=np.array(gprime > 0))
kandersolar marked this conversation as resolved.
Show resolved Hide resolved

X = np.stack((logGprime, logGprime**2, tprime, tprime*logGprime,
tprime*logGprime**2, tprime**2), axis=0).T
X = sm.add_constant(X)

if method == 'ols':
model = sm.OLS(Y, X, missing='drop')
elif method == 'robust':
model = sm.RLM(Y, X, missing='drop')
else:
raise ValueError("method must be ols or robust")

result = model.fit()
pdc0 = result.params[0]
k = result.params[1:]
return pdc0, k
44 changes: 43 additions & 1 deletion pvlib/tests/test_pvarray.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from numpy.testing import assert_allclose
from .conftest import assert_series_equal
import pytest

from pvlib.tests.conftest import requires_statsmodels
from pvlib import pvarray


Expand Down Expand Up @@ -69,3 +69,45 @@ def test_huld():
with pytest.raises(ValueError,
match='Either k or cell_type must be specified'):
res = pvarray.huld(1000, 25, 100)


@pytest.mark.parametrize('method', ['ols', 'robust'])
@requires_statsmodels
def test_fit_huld(method):
# test is to recover the parameters in _infer_huld_k for each cell type
# IEC61853 conditions to make data for fitting
ee, tc = pvarray._build_iec61853()
techs = ['csi', 'cis', 'cdte']
pdc0 = 250
for tech in techs:
k0 = pvarray._infer_k_huld(tech, pdc0)
pdc = pvarray.huld(ee, tc, pdc0, cell_type=tech)
m_pdc0, k = pvarray.fit_huld(ee, tc, pdc, method=method)
expected = np.array([pdc0, ] + [v for v in k0], dtype=float)
modeled = np.hstack((m_pdc0, k))
assert_allclose(expected, modeled, rtol=1e-8)
# once more to check that NaNs are handled
ee[7] = np.nan
tc[9] = np.nan
k0 = pvarray._infer_k_huld('csi', pdc0)
pdc = pvarray.huld(ee, tc, pdc0, cell_type='csi')
pdc[11] = np.nan
m_pdc0, k = pvarray.fit_huld(ee, tc, pdc, method='ols')
# known solution for OLS obtained with statsmodels v0.14.0
expected = np.array([2.07118648e+02, -8.35033390e+01, -3.83844606e+01,
2.75629738e+00, 1.87571414e+00, -2.86429730e-01,
-6.00418853e-02])
kandersolar marked this conversation as resolved.
Show resolved Hide resolved
modeled = np.hstack((m_pdc0, k))
assert_allclose(expected, modeled, rtol=1e-5)


@requires_statsmodels
def test_fit_huld_method_error():
# test is to recover the parameters in _infer_huld_k for each cell type
# IEC61853 conditions to make data for fitting
cwhanse marked this conversation as resolved.
Show resolved Hide resolved
ee, tc = pvarray._build_iec61853()
pdc0 = 250
pdc = pvarray.huld(ee, tc, pdc0, cell_type='csi')
method = 'brute_force'
with pytest.raises(ValueError, match="method must be ols or robust"):
m_pdc0, k = pvarray.fit_huld(ee, tc, pdc, method=method)
Loading