diff --git a/docs/sphinx/source/whatsnew/v0.13.1.rst b/docs/sphinx/source/whatsnew/v0.13.1.rst index bda8285180..91e6f8c25c 100644 --- a/docs/sphinx/source/whatsnew/v0.13.1.rst +++ b/docs/sphinx/source/whatsnew/v0.13.1.rst @@ -20,6 +20,7 @@ Bug fixes Enhancements ~~~~~~~~~~~~ +* Add new parameters for the Huld PV array mode :py:func:`~pvlib.pvarray.huld` (:issue:`2461`, :pull:`2486`) * Add :py:func:`pvlib.iotools.get_nasa_power` to retrieve data from NASA POWER free API. (:pull:`2500`) * :py:func:`pvlib.spectrum.spectral_factor_firstsolar` no longer emits warnings @@ -51,6 +52,9 @@ Maintenance Contributors ~~~~~~~~~~~~ * Elijah Passmore (:ghuser:`eljpsm`) +* Omar Bahamida (:ghuser:`OmarBahamida`) +* Cliff Hansen (:ghuser:`cwhanse`) + * Ioannis Sifnaios (:ghuser:`IoannisSifnaios`) * Rajiv Daxini (:ghuser:`RDaxini`) * Omar Bahamida (:ghuser:`OmarBahamida`) diff --git a/pvlib/pvarray.py b/pvlib/pvarray.py index ab7530f3e5..3c6ffbf57f 100644 --- a/pvlib/pvarray.py +++ b/pvlib/pvarray.py @@ -37,7 +37,7 @@ def pvefficiency_adr(effective_irradiance, temp_cell, the reference conditions. [unitless] k_d : numeric, negative - “Dark irradiance” or diode coefficient which influences the voltage + "Dark irradiance" or diode coefficient which influences the voltage increase with irradiance. [unitless] tc_d : numeric @@ -225,24 +225,55 @@ def adr_wrapper(xdata, *params): return popt -def _infer_k_huld(cell_type, pdc0): +def _infer_k_huld(cell_type, pdc0, k_version): + r""" + Get the EU JRC updated coefficients for the Huld model. + + Parameters + ---------- + cell_type : str + Must be one of 'csi', 'cis', or 'cdte' + pdc0 : numeric + Power of the modules at reference conditions [W] + k_version : str + Either '2011' or '2025'. + + Returns + ------- + tuple + The six coefficients (k1-k6) for the Huld model, scaled by pdc0 + """ # from PVGIS documentation, "PVGIS data sources & calculation methods", # Section 5.2.3, accessed 12/22/2023 # The parameters in PVGIS' documentation are for a version of Huld's # equation that has factored Pdc0 out of the polynomial: # P = G/1000 * Pdc0 * (1 + k1 log(Geff) + ...) so these parameters are # multiplied by pdc0 - huld_params = {'csi': (-0.017237, -0.040465, -0.004702, 0.000149, - 0.000170, 0.000005), - 'cis': (-0.005554, -0.038724, -0.003723, -0.000905, - -0.001256, 0.000001), - 'cdte': (-0.046689, -0.072844, -0.002262, 0.000276, - 0.000159, -0.000006)} + if k_version.lower() == 'pvgis5': + # coefficients from PVGIS webpage + huld_params = {'csi': (-0.017237, -0.040465, -0.004702, 0.000149, + 0.000170, 0.000005), + 'cis': (-0.005554, -0.038724, -0.003723, -0.000905, + -0.001256, 0.000001), + 'cdte': (-0.046689, -0.072844, -0.002262, 0.000276, + 0.000159, -0.000006)} + elif k_version.lower() == 'pvgis6': + # Coefficients from EU JRC paper + huld_params = {'csi': (-0.0067560, -0.016444, -0.003015, -0.000045, + -0.000043, 0.0), + 'cis': (-0.011001, -0.029734, -0.002887, 0.000217, + -0.000163, 0.0), + 'cdte': (-0.020644, -0.035316, -0.003406, 0.000073, + -0.000141, 0.000002)} + else: + raise ValueError(f'Invalid k_version={k_version}: must be either ' + '"pvgis5" or "pvgis6"') k = tuple([x*pdc0 for x in huld_params[cell_type.lower()]]) return k -def huld(effective_irradiance, temp_mod, pdc0, k=None, cell_type=None): +def huld(effective_irradiance, temp_mod, pdc0, k=None, cell_type=None, + k_version='pvgis5'): r""" Power (DC) using the Huld model. @@ -274,6 +305,11 @@ def huld(effective_irradiance, temp_mod, pdc0, k=None, cell_type=None): cell_type : str, optional If provided, must be one of ``'cSi'``, ``'CIS'``, or ``'CdTe'``. Used to look up default values for ``k`` if ``k`` is not specified. + k_version : str, optional + Either ``'pvgis5'`` (default) or ``'pvgis6'``. Selects values + for ``k`` if ``k`` is not specified. If ``'pvgis5'``, values are + from PVGIS documentation and are labeled in [2]_ as "current". + If ``'pvgis6'`` values are from [2]_ labeled as "updated". Returns ------- @@ -328,14 +364,19 @@ def huld(effective_irradiance, temp_mod, pdc0, k=None, cell_type=None): 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`. + .. [1] T. Huld, G. Friesen, A. Skoczek, R. Kenny, T. Sample, M. Field, and + 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`. + .. [2] A. Chatzipanagi, N. Taylor, I. Suarez, A. Martinez, T. Lyubenova, + and E. Dunlop, "An Updated Simplified Energy Yield Model for Recent + Photovoltaic Module Technologies," + Progress in Photovoltaics: Research and Applications 33, + no. 8 (2025): 905–917, :doi:`10.1002/pip.3926`. """ if k is None: if cell_type is not None: - k = _infer_k_huld(cell_type, pdc0) + k = _infer_k_huld(cell_type, pdc0, k_version) else: raise ValueError('Either k or cell_type must be specified') @@ -346,7 +387,10 @@ def huld(effective_irradiance, temp_mod, pdc0, k=None, cell_type=None): logGprime = np.log(gprime, out=np.zeros_like(gprime), where=np.array(gprime > 0)) # Eq. 1 in [1] - pdc = gprime * (pdc0 + k[0] * logGprime + k[1] * logGprime**2 + - k[2] * tprime + k[3] * tprime * logGprime + - k[4] * tprime * logGprime**2 + k[5] * tprime**2) + pdc = gprime * ( + pdc0 + k[0] * logGprime + k[1] * logGprime**2 + + k[2] * tprime + k[3] * tprime * logGprime + + k[4] * tprime * logGprime**2 + + k[5] * tprime**2 + ) return pdc diff --git a/tests/test_pvarray.py b/tests/test_pvarray.py index 693ef78b2a..a1fc1c4ac3 100644 --- a/tests/test_pvarray.py +++ b/tests/test_pvarray.py @@ -50,10 +50,12 @@ def test_pvefficiency_adr_round_trip(): def test_huld(): + # tests with default k_version='pvgis5' pdc0 = 100 res = pvarray.huld(1000, 25, pdc0, cell_type='cSi') assert np.isclose(res, pdc0) - exp_sum = np.exp(1) * (np.sum(pvarray._infer_k_huld('cSi', pdc0)) + pdc0) + k = pvarray._infer_k_huld('cSi', pdc0, 'pvgis5') + exp_sum = np.exp(1) * (np.sum(k) + pdc0) res = pvarray.huld(1000*np.exp(1), 26, pdc0, cell_type='cSi') assert np.isclose(res, exp_sum) res = pvarray.huld(100, 30, pdc0, k=(1, 1, 1, 1, 1, 1)) @@ -67,5 +69,48 @@ def test_huld(): res = pvarray.huld(eff_irr, tm, pdc0, k=(1, 1, 1, 1, 1, 1)) assert_series_equal(res, expected) with pytest.raises(ValueError, - match='Either k or cell_type must be specified'): - res = pvarray.huld(1000, 25, 100) + match='Either k or cell_type must be specified' + ): + pvarray.huld(1000, 25, 100) + + +def test_huld_params(): + """Test Huld with built-in coefficients.""" + pdc0 = 100 + # Use non-reference values so coefficients affect the result + eff_irr = 800 # W/m^2 (not 1000) + temp_mod = 35 # deg C (not 25) + # calculated by C. Hansen using Excel, 2025 + expected = {'pvgis5': {'csi': 76.405089, + 'cis': 77.086016, + 'cdte': 78.642762 + }, + 'pvgis6': {'csi': 77.649421, + 'cis': 77.723110, + 'cdte': 77.500399 + } + } + # Test with PVGIS5 coefficients for all cell types + for yr in expected: + for cell_type in expected[yr]: + result = pvarray.huld(eff_irr, temp_mod, pdc0, cell_type=cell_type, + k_version=yr) + assert np.isclose(result, expected[yr][cell_type]) + + +def test_huld_errors(): + # Check errors + pdc0 = 100 + # Use non-reference values so coefficients affect the result + eff_irr = 800 # W/m^2 (not 1000) + temp_mod = 35 # deg C (not 25) + # provide both cell_type and k_version + with pytest.raises(KeyError): + pvarray.huld( + eff_irr, temp_mod, pdc0, cell_type='invalid', k_version='pvgis5' + ) + # provide invalid k_version + with pytest.raises(ValueError, match='Invalid k_version=2021'): + pvarray.huld( + eff_irr, temp_mod, pdc0, cell_type='csi', k_version='2021' + )