diff --git a/docs/sphinx/source/whatsnew/v0.13.2.rst b/docs/sphinx/source/whatsnew/v0.13.2.rst index 3177f7a022..1a680f24c5 100644 --- a/docs/sphinx/source/whatsnew/v0.13.2.rst +++ b/docs/sphinx/source/whatsnew/v0.13.2.rst @@ -27,8 +27,8 @@ Enhancements :py:func:`~pvlib.singlediode.bishop88_mpp`, :py:func:`~pvlib.singlediode.bishop88_v_from_i`, and :py:func:`~pvlib.singlediode.bishop88_i_from_v`. (:issue:`2497`, :pull:`2498`) - - +* Add Marion 2008 non-linear irradiance adjustment factor to + :py:func:`pvlib.pvsystem.pvwatts_dc`. (:issue:`2566`, :pull:`2569`) Documentation ~~~~~~~~~~~~~ @@ -52,4 +52,4 @@ Maintenance Contributors ~~~~~~~~~~~~ - +* Will Hobbs (:ghuser:`williamhobbs`) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 2b703f3a52..1d993eed31 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -2878,53 +2878,118 @@ def scale_voltage_current_power(data, voltage=1, current=1): @renamed_kwarg_warning( "0.13.0", "g_poa_effective", "effective_irradiance") -def pvwatts_dc(effective_irradiance, temp_cell, pdc0, gamma_pdc, temp_ref=25.): +def pvwatts_dc(effective_irradiance, temp_cell, pdc0, gamma_pdc, temp_ref=25., + k=None, cap_adjustment=False): r""" - Implements NREL's PVWatts DC power model. The PVWatts DC model [1]_ is: - - .. math:: - - P_{dc} = \frac{G_{poa eff}}{1000} P_{dc0} ( 1 + \gamma_{pdc} (T_{cell} - T_{ref})) - - Note that ``pdc0`` is also used as a symbol in - :py:func:`pvlib.inverter.pvwatts`. ``pdc0`` in this function refers to the DC - power of the modules at reference conditions. ``pdc0`` in - :py:func:`pvlib.inverter.pvwatts` refers to the DC power input limit of - the inverter. + Implement NREL's PVWatts (Version 5) DC power model. Parameters ---------- effective_irradiance: numeric - Irradiance transmitted to the PV cells. To be - fully consistent with PVWatts, the user must have already - applied angle of incidence losses, but not soiling, spectral, - etc. [W/m^2] + Irradiance transmitted to the PV cells. To be fully consistent with + PVWatts, the user must have already applied angle of incidence losses, + but not soiling, spectral, etc. [Wm⁻²] temp_cell: numeric Cell temperature [C]. pdc0: numeric - Power of the modules at 1000 W/m^2 and cell reference temperature. [W] + Power of the modules at 1000 Wm⁻² and cell reference temperature. [W] gamma_pdc: numeric - The temperature coefficient of power. Typically -0.002 to - -0.005 per degree C. [1/C] + The temperature coefficient of power. Typically -0.002 to -0.005 per + degree C. [1/°C] temp_ref: numeric, default 25.0 - Cell reference temperature. PVWatts defines it to be 25 C and - is included here for flexibility. [C] + Cell reference temperature. PVWatts defines it to be 25 °C and is + included here for flexibility. [°C] + k: numeric, optional + Irradiance correction factor, defined in [2]_. Typically positive. + [unitless] + cap_adjustment: Boolean, default False + If True, only apply the optional adjustment at and below 1000 Wm⁻² Returns ------- pdc: numeric DC power. [W] + Notes + ----- + The PVWatts Version 5 DC model [1]_ is: + + .. math:: + + P_{dc} = \frac{G_{poa eff}}{1000} P_{dc0} ( 1 + \gamma_{pdc} (T_{cell} - T_{ref})) + + This model has also been referred to as the power temperature coefficient + model. + + An optional adjustment can be applied to :math:`P_{dc}` as described in + [2]_. The adjustment accounts for the variation in module efficiency with + irradiance. The piece-wise adjustment to power is parameterized by `k`, + where `k` is the reduction in actual power at 200 Wm⁻² relative to power + calculated at 200 Wm⁻² as 0.2*`pdc0`. For example, a module that is rated + at 500 W at STC but produces 95 W at 200 Wm⁻² (a 5% relative reduction in + efficiency) would have a value of `k` = 0.01. + + .. math:: + + k=\frac{0.2P_{dc0}-P_{200}}{P_{dc0}} + + For positive `k` values, and `k` is typically positive, this adjustment + would also increase relative efficiency when irradiance is above 1000 Wm⁻². + This may not be desired, as modules with nonlinear irradiance response + often have peak efficiency near 1000 Wm⁻², and it is either flat or + declining at higher irradiance. An optional parameter, `cap_adjustment`, + can address this by modifying the adjustment from [2]_ to only apply below + 1000 Wm⁻². + + Note that ``pdc0`` is also used as a symbol in + :py:func:`pvlib.inverter.pvwatts`. ``pdc0`` in this function refers to the + DC power of the modules at reference conditions. ``pdc0`` in + :py:func:`pvlib.inverter.pvwatts` refers to the DC power input limit of + the inverter. + References ---------- .. [1] A. P. Dobos, "PVWatts Version 5 Manual" http://pvwatts.nrel.gov/downloads/pvwattsv5.pdf (2014). + .. [2] B. Marion, "Comparison of Predictive Models for + Photovoltaic Module Performance," + :doi:`10.1109/PVSC.2008.4922586`, + https://docs.nrel.gov/docs/fy08osti/42511.pdf + (2008). """ # noqa: E501 pdc = (effective_irradiance * 0.001 * pdc0 * (1 + gamma_pdc * (temp_cell - temp_ref))) + # apply Marion's correction if k is provided + if k is not None: + + # preserve input types + index = pdc.index if isinstance(pdc, pd.Series) else None + is_scalar = np.isscalar(pdc) + + # calculate error adjustments + err_1 = k * (1 - (1 - effective_irradiance / 200)**4) + err_2 = k * (1000 - effective_irradiance) / (1000 - 200) + err = np.where(effective_irradiance <= 200, err_1, err_2) + + # cap adjustment, if needed + if cap_adjustment: + err = np.where(effective_irradiance >= 1000, 0, err) + + # make error adjustment + pdc = pdc - pdc0 * err + + # set negative power to zero + pdc = np.where(pdc < 0, 0, pdc) + + # preserve input types + if index is not None: + pdc = pd.Series(pdc, index=index) + elif is_scalar: + pdc = float(pdc) + return pdc diff --git a/tests/test_pvsystem.py b/tests/test_pvsystem.py index b7d8ba6173..4fbd782e65 100644 --- a/tests/test_pvsystem.py +++ b/tests/test_pvsystem.py @@ -2181,6 +2181,54 @@ def test_pvwatts_dc_series(): assert_series_equal(expected, out) +def test_pvwatts_dc_scalars_with_k(): + expected = 8.9125 + out = pvsystem.pvwatts_dc(100, 30, 100, -0.003, k=0.01) + assert_allclose(out, expected) + + +def test_pvwatts_dc_arrays_with_k(): + irrad_trans = np.array([np.nan, 100, 1200]) + temp_cell = np.array([30, np.nan, 30]) + irrad_trans, temp_cell = np.meshgrid(irrad_trans, temp_cell) + expected = np.array([[nan, 8.9125, 118.45], + [nan, nan, nan], + [nan, 8.9125, 118.45]]) + out = pvsystem.pvwatts_dc(irrad_trans, temp_cell, 100, -0.003, k=0.01) + assert_allclose(out, expected, equal_nan=True) + + +def test_pvwatts_dc_series_with_k(): + irrad_trans = pd.Series([np.nan, 100, 100, 1200]) + temp_cell = pd.Series([30, np.nan, 30, 30]) + expected = pd.Series(np.array([ nan, nan, 8.9125, 118.45])) + out = pvsystem.pvwatts_dc(irrad_trans, temp_cell, 100, -0.003, k=0.01) + assert_series_equal(expected, out) + + +def test_pvwatts_dc_with_k_and_cap_adjustment(): + irrad_trans = [100, 1200] + temp_cell = 25 + out = [] + expected = [0, 120.0] + for irrad in irrad_trans: + out.append(pvsystem.pvwatts_dc(irrad, temp_cell, 100, -0.003, k=0.15, + cap_adjustment=True)) + assert_allclose(out, expected) + + +def test_pvwatts_dc_arrays_with_k_and_cap_adjustment(): + irrad_trans = np.array([np.nan, 100, 1200]) + temp_cell = np.array([30, np.nan, 30]) + irrad_trans, temp_cell = np.meshgrid(irrad_trans, temp_cell) + expected = np.array([[nan, 8.9125, 118.2], + [nan, nan, nan], + [nan, 8.9125, 118.2]]) + out = pvsystem.pvwatts_dc(irrad_trans, temp_cell, 100, -0.003, k=0.01, + cap_adjustment=True) + assert_allclose(out, expected, equal_nan=True) + + def test_pvwatts_losses_default(): expected = 14.075660688264469 out = pvsystem.pvwatts_losses()