diff --git a/docs/sphinx/source/reference/airmass_atmospheric.rst b/docs/sphinx/source/reference/airmass_atmospheric.rst index fbd33a5f28..384c345aec 100644 --- a/docs/sphinx/source/reference/airmass_atmospheric.rst +++ b/docs/sphinx/source/reference/airmass_atmospheric.rst @@ -17,3 +17,4 @@ Airmass and atmospheric models atmosphere.kasten96_lt atmosphere.angstrom_aod_at_lambda atmosphere.angstrom_alpha + atmosphere.windspeed_powerlaw diff --git a/docs/sphinx/source/whatsnew/v0.11.1.rst b/docs/sphinx/source/whatsnew/v0.11.1.rst index 4ccc9687c3..3197882d93 100644 --- a/docs/sphinx/source/whatsnew/v0.11.1.rst +++ b/docs/sphinx/source/whatsnew/v0.11.1.rst @@ -17,6 +17,9 @@ Enhancements :py:func:`pvlib.spectrum.spectral_factor_firstsolar`. (:issue:`2086`, :pull:`2100`) +* Added function for calculating wind speed at different heights, + :py:func:`pvlib.atmosphere.windspeed_powerlaw`. + (:issue:`2118`, :pull:`2124`) Bug fixes ~~~~~~~~~ @@ -45,4 +48,3 @@ Contributors * Leonardo Micheli (:ghuser:`lmicheli`) * Echedey Luis (:ghuser:`echedey-ls`) * Rajiv Daxini (:ghuser:`RDaxini`) - diff --git a/pvlib/atmosphere.py b/pvlib/atmosphere.py index 08e40b9fc2..5b7ffff4ee 100644 --- a/pvlib/atmosphere.py +++ b/pvlib/atmosphere.py @@ -1,6 +1,7 @@ """ The ``atmosphere`` module contains methods to calculate relative and -absolute airmass and to determine pressure from altitude or vice versa. +absolute airmass, determine pressure from altitude or vice versa, and wind +speed at different heights. """ import numpy as np @@ -533,3 +534,158 @@ def angstrom_alpha(aod1, lambda1, aod2, lambda2): pvlib.atmosphere.angstrom_aod_at_lambda """ return - np.log(aod1 / aod2) / np.log(lambda1 / lambda2) + + +# Values of the Hellmann exponent +HELLMANN_SURFACE_EXPONENTS = { + 'unstable_air_above_open_water_surface': 0.06, + 'neutral_air_above_open_water_surface': 0.10, + 'stable_air_above_open_water_surface': 0.27, + 'unstable_air_above_flat_open_coast': 0.11, + 'neutral_air_above_flat_open_coast': 0.16, + 'stable_air_above_flat_open_coast': 0.40, + 'unstable_air_above_human_inhabited_areas': 0.27, + 'neutral_air_above_human_inhabited_areas': 0.34, + 'stable_air_above_human_inhabited_areas': 0.60, +} + + +def windspeed_powerlaw(wind_speed_reference, height_reference, + height_desired, exponent=None, + surface_type=None): + r""" + Estimate wind speed for different heights. + + The model is based on the power law equation by Hellmann [1]_ [2]_. + + Parameters + ---------- + wind_speed_reference : numeric + Measured wind speed. [m/s] + + height_reference : float + The height above ground at which the wind speed is measured. [m] + + height_desired : float + The height above ground at which the wind speed will be estimated. [m] + + exponent : float, optional + Exponent based on the surface type. [unitless] + + surface_type : string, optional + If supplied, overrides ``exponent``. Can be one of the following + (see [1]_): + + * ``'unstable_air_above_open_water_surface'`` + * ``'neutral_air_above_open_water_surface'`` + * ``'stable_air_above_open_water_surface'`` + * ``'unstable_air_above_flat_open_coast'`` + * ``'neutral_air_above_flat_open_coast'`` + * ``'stable_air_above_flat_open_coast'`` + * ``'unstable_air_above_human_inhabited_areas'`` + * ``'neutral_air_above_human_inhabited_areas'`` + * ``'stable_air_above_human_inhabited_areas'`` + + Returns + ------- + wind_speed : numeric + Adjusted wind speed for the desired height. [m/s] + + Raises + ------ + ValueError + If neither of ``exponent`` nor a ``surface_type`` is given. + If both ``exponent`` and a ``surface_type`` is given. These parameters + are mutually exclusive. + + KeyError + If the specified ``surface_type`` is invalid. + + Notes + ----- + Module temperature functions often require wind speeds at a height of 10 m + and not the wind speed at the module height. + + For example, the following temperature functions require the input wind + speed to be 10 m: :py:func:`~pvlib.temperature.sapm_cell`, and + :py:func:`~pvlib.temperature.sapm_module` whereas the + :py:func:`~pvlib.temperature.fuentes` model requires wind speed at 9.144 m. + + Additionally, the heat loss coefficients of some models have been developed + for wind speed measurements at 10 m (e.g., + :py:func:`~pvlib.temperature.pvsyst_cell`, + :py:func:`~pvlib.temperature.faiman`, and + :py:func:`~pvlib.temperature.faiman_rad`). + + The equation for calculating the wind speed at a height of :math:`h` is + given by the following power law equation [1]_ [2]_: + + .. math:: + :label: wind speed + + WS_{h} = WS_{ref} \cdot \left( \frac{h}{h_{ref}} \right)^a + + where :math:`h` [m] is the height at which we would like to calculate the + wind speed, :math:`h_{ref}` [m] is the reference height at which the wind + speed is known, and :math:`WS_{h}` [m/s] and :math:`WS_{ref}` + [m/s] are the corresponding wind speeds at these heights. The exponent + :math:`a` [unitless] depends on the surface type. Some values found in the + literature [1]_ for :math:`a` are: + + .. table:: Values for the Hellmann-exponent + + +-----------+--------------------+------------------+------------------+ + | Stability | Open water surface | Flat, open coast | Cities, villages | + +===========+====================+==================+==================+ + | Unstable | 0.06 | 0.10 | 0.27 | + +-----------+--------------------+------------------+------------------+ + | Neutral | 0.11 | 0.16 | 0.40 | + +-----------+--------------------+------------------+------------------+ + | Stable | 0.27 | 0.34 | 0.60 | + +-----------+--------------------+------------------+------------------+ + + In a report by Sandia [3]_, the equation was experimentally tested for a + height of 30 ft (:math:`h_{ref} = 9.144` [m]) at their test site in + Albuquerque for a period of six weeks where a coefficient of + :math:`a = 0.219` was calculated. + + It should be noted that the equation returns a value of NaN if the + reference heights or wind speed are negative. + + References + ---------- + .. [1] Kaltschmitt M., Streicher W., Wiese A. (2007). "Renewable Energy: + Technology, Economics and Environment." Springer, + :doi:`10.1007/3-540-70949-5`. + + .. [2] Hellmann G. (1915). "Über die Bewegung der Luft in den untersten + Schichten der Atmosphäre." Meteorologische Zeitschrift, 32 + + .. [3] Menicucci D.F., Hall I.J. (1985). "Estimating wind speed as a + function of height above ground: An analysis of data obtained at the + southwest residential experiment station, Las Cruses, New Mexico." + SAND84-2530, Sandia National Laboratories. + Accessed at: + https://web.archive.org/web/20230418202422/https://www2.jpl.nasa.gov/adv_tech/photovol/2016CTR/SNL%20-%20Est%20Wind%20Speed%20vs%20Height_1985.pdf + """ # noqa:E501 + if surface_type is not None and exponent is None: + # use the Hellmann exponent from dictionary + exponent = HELLMANN_SURFACE_EXPONENTS[surface_type] + elif surface_type is None and exponent is not None: + # use the provided exponent + pass + else: + raise ValueError( + "Either a 'surface_type' or an 'exponent' parameter must be given") + + wind_speed = wind_speed_reference * ( + (height_desired / height_reference) ** exponent) + + # if wind speed is negative or complex return NaN + wind_speed = np.where(np.iscomplex(wind_speed) | (wind_speed < 0), + np.nan, wind_speed) + + if isinstance(wind_speed_reference, pd.Series): + wind_speed = pd.Series(wind_speed, index=wind_speed_reference.index) + + return wind_speed diff --git a/pvlib/tests/test_atmosphere.py b/pvlib/tests/test_atmosphere.py index 46db622ee5..e12a41dc6d 100644 --- a/pvlib/tests/test_atmosphere.py +++ b/pvlib/tests/test_atmosphere.py @@ -131,3 +131,74 @@ def test_bird_hulstrom80_aod_bb(): aod380, aod500 = 0.22072480948195175, 0.1614279181106312 bird_hulstrom = atmosphere.bird_hulstrom80_aod_bb(aod380, aod500) assert np.isclose(0.11738229553812768, bird_hulstrom) + + +@pytest.fixture +def windspeeds_data_powerlaw(): + data = pd.DataFrame( + index=pd.date_range(start="2015-01-01 00:00", end="2015-01-01 05:00", + freq="1h"), + columns=["wind_ref", "height_ref", "height_desired", "wind_calc"], + data=[ + (10, -2, 5, np.nan), + (-10, 2, 5, np.nan), + (5, 4, 5, 5.067393209486324), + (7, 6, 10, 7.2178684911195905), + (10, 8, 20, 10.565167835216586), + (12, 10, 30, 12.817653329393977) + ] + ) + return data + + +def test_windspeed_powerlaw_ndarray(windspeeds_data_powerlaw): + # test wind speed estimation by passing in surface_type + result_surface = atmosphere.windspeed_powerlaw( + windspeeds_data_powerlaw["wind_ref"].to_numpy(), + windspeeds_data_powerlaw["height_ref"], + windspeeds_data_powerlaw["height_desired"], + surface_type='unstable_air_above_open_water_surface') + assert_allclose(windspeeds_data_powerlaw["wind_calc"].to_numpy(), + result_surface) + # test wind speed estimation by passing in the exponent corresponding + # to the surface_type above + result_exponent = atmosphere.windspeed_powerlaw( + windspeeds_data_powerlaw["wind_ref"].to_numpy(), + windspeeds_data_powerlaw["height_ref"], + windspeeds_data_powerlaw["height_desired"], + exponent=0.06) + assert_allclose(windspeeds_data_powerlaw["wind_calc"].to_numpy(), + result_exponent) + + +def test_windspeed_powerlaw_series(windspeeds_data_powerlaw): + result = atmosphere.windspeed_powerlaw( + windspeeds_data_powerlaw["wind_ref"], + windspeeds_data_powerlaw["height_ref"], + windspeeds_data_powerlaw["height_desired"], + surface_type='unstable_air_above_open_water_surface') + assert_series_equal(windspeeds_data_powerlaw["wind_calc"], + result, check_names=False) + + +def test_windspeed_powerlaw_invalid(): + with pytest.raises(ValueError, match="Either a 'surface_type' or an " + "'exponent' parameter must be given"): + # no exponent or surface_type given + atmosphere.windspeed_powerlaw(wind_speed_reference=10, + height_reference=5, + height_desired=10) + with pytest.raises(ValueError, match="Either a 'surface_type' or an " + "'exponent' parameter must be given"): + # no exponent or surface_type given + atmosphere.windspeed_powerlaw(wind_speed_reference=10, + height_reference=5, + height_desired=10, + exponent=1.2, + surface_type="surf") + with pytest.raises(KeyError, match='not_an_exponent'): + # invalid surface_type + atmosphere.windspeed_powerlaw(wind_speed_reference=10, + height_reference=5, + height_desired=10, + surface_type='not_an_exponent')