Skip to content

Commit

Permalink
Function to estimate wind speed at different heights (#2124)
Browse files Browse the repository at this point in the history
* wind_speed_at_different_heights

* minor fixed

* fixed toc and added test

* edit math mode

* changed link for reference

* Apply suggestions from code review

Co-authored-by: RDaxini <[email protected]>
Co-authored-by: Echedey Luis <[email protected]>

* Update pvlib/tests/test_windspeed.py

Co-authored-by: Echedey Luis <[email protected]>

* changed function name

* Update pvlib/windspeed.py

Co-authored-by: Echedey Luis <[email protected]>

* change measured height to reference

* moved wind speed to atmosphere

* minor formatting fixes

* update test parameter names

* updated error message

* fixed error message vol.2

* updated tests

* Update v0.11.1.rst

* changed nan condition

* changed function name

* Apply suggestions from code review

Co-authored-by: Cliff Hansen <[email protected]>
Co-authored-by: Adam R. Jensen <[email protected]>

* changed name (again)

* condition fix

* corrected the Sandia wording

---------

Co-authored-by: RDaxini <[email protected]>
Co-authored-by: Echedey Luis <[email protected]>
Co-authored-by: Cliff Hansen <[email protected]>
Co-authored-by: Adam R. Jensen <[email protected]>
  • Loading branch information
5 people authored Aug 5, 2024
1 parent 83d90eb commit f6b1d2a
Show file tree
Hide file tree
Showing 4 changed files with 232 additions and 2 deletions.
1 change: 1 addition & 0 deletions docs/sphinx/source/reference/airmass_atmospheric.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ Airmass and atmospheric models
atmosphere.kasten96_lt
atmosphere.angstrom_aod_at_lambda
atmosphere.angstrom_alpha
atmosphere.windspeed_powerlaw
4 changes: 3 additions & 1 deletion docs/sphinx/source/whatsnew/v0.11.1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~
Expand Down Expand Up @@ -45,4 +48,3 @@ Contributors
* Leonardo Micheli (:ghuser:`lmicheli`)
* Echedey Luis (:ghuser:`echedey-ls`)
* Rajiv Daxini (:ghuser:`RDaxini`)

158 changes: 157 additions & 1 deletion pvlib/atmosphere.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
71 changes: 71 additions & 0 deletions pvlib/tests/test_atmosphere.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

0 comments on commit f6b1d2a

Please sign in to comment.