Skip to content

Commit f6b1d2a

Browse files
IoannisSifnaiosRDaxiniechedey-lscwhanseAdamRJensen
authored
Function to estimate wind speed at different heights (#2124)
* 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 <143435106+RDaxini@users.noreply.github.com> Co-authored-by: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> * Update pvlib/tests/test_windspeed.py Co-authored-by: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> * changed function name * Update pvlib/windspeed.py Co-authored-by: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> * 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 <cwhanse@sandia.gov> Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com> * changed name (again) * condition fix * corrected the Sandia wording --------- Co-authored-by: RDaxini <143435106+RDaxini@users.noreply.github.com> Co-authored-by: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> Co-authored-by: Cliff Hansen <cwhanse@sandia.gov> Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com>
1 parent 83d90eb commit f6b1d2a

File tree

4 files changed

+232
-2
lines changed

4 files changed

+232
-2
lines changed

docs/sphinx/source/reference/airmass_atmospheric.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ Airmass and atmospheric models
1717
atmosphere.kasten96_lt
1818
atmosphere.angstrom_aod_at_lambda
1919
atmosphere.angstrom_alpha
20+
atmosphere.windspeed_powerlaw

docs/sphinx/source/whatsnew/v0.11.1.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ Enhancements
1717
:py:func:`pvlib.spectrum.spectral_factor_firstsolar`.
1818
(:issue:`2086`, :pull:`2100`)
1919

20+
* Added function for calculating wind speed at different heights,
21+
:py:func:`pvlib.atmosphere.windspeed_powerlaw`.
22+
(:issue:`2118`, :pull:`2124`)
2023

2124
Bug fixes
2225
~~~~~~~~~
@@ -45,4 +48,3 @@ Contributors
4548
* Leonardo Micheli (:ghuser:`lmicheli`)
4649
* Echedey Luis (:ghuser:`echedey-ls`)
4750
* Rajiv Daxini (:ghuser:`RDaxini`)
48-

pvlib/atmosphere.py

Lines changed: 157 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
The ``atmosphere`` module contains methods to calculate relative and
3-
absolute airmass and to determine pressure from altitude or vice versa.
3+
absolute airmass, determine pressure from altitude or vice versa, and wind
4+
speed at different heights.
45
"""
56

67
import numpy as np
@@ -533,3 +534,158 @@ def angstrom_alpha(aod1, lambda1, aod2, lambda2):
533534
pvlib.atmosphere.angstrom_aod_at_lambda
534535
"""
535536
return - np.log(aod1 / aod2) / np.log(lambda1 / lambda2)
537+
538+
539+
# Values of the Hellmann exponent
540+
HELLMANN_SURFACE_EXPONENTS = {
541+
'unstable_air_above_open_water_surface': 0.06,
542+
'neutral_air_above_open_water_surface': 0.10,
543+
'stable_air_above_open_water_surface': 0.27,
544+
'unstable_air_above_flat_open_coast': 0.11,
545+
'neutral_air_above_flat_open_coast': 0.16,
546+
'stable_air_above_flat_open_coast': 0.40,
547+
'unstable_air_above_human_inhabited_areas': 0.27,
548+
'neutral_air_above_human_inhabited_areas': 0.34,
549+
'stable_air_above_human_inhabited_areas': 0.60,
550+
}
551+
552+
553+
def windspeed_powerlaw(wind_speed_reference, height_reference,
554+
height_desired, exponent=None,
555+
surface_type=None):
556+
r"""
557+
Estimate wind speed for different heights.
558+
559+
The model is based on the power law equation by Hellmann [1]_ [2]_.
560+
561+
Parameters
562+
----------
563+
wind_speed_reference : numeric
564+
Measured wind speed. [m/s]
565+
566+
height_reference : float
567+
The height above ground at which the wind speed is measured. [m]
568+
569+
height_desired : float
570+
The height above ground at which the wind speed will be estimated. [m]
571+
572+
exponent : float, optional
573+
Exponent based on the surface type. [unitless]
574+
575+
surface_type : string, optional
576+
If supplied, overrides ``exponent``. Can be one of the following
577+
(see [1]_):
578+
579+
* ``'unstable_air_above_open_water_surface'``
580+
* ``'neutral_air_above_open_water_surface'``
581+
* ``'stable_air_above_open_water_surface'``
582+
* ``'unstable_air_above_flat_open_coast'``
583+
* ``'neutral_air_above_flat_open_coast'``
584+
* ``'stable_air_above_flat_open_coast'``
585+
* ``'unstable_air_above_human_inhabited_areas'``
586+
* ``'neutral_air_above_human_inhabited_areas'``
587+
* ``'stable_air_above_human_inhabited_areas'``
588+
589+
Returns
590+
-------
591+
wind_speed : numeric
592+
Adjusted wind speed for the desired height. [m/s]
593+
594+
Raises
595+
------
596+
ValueError
597+
If neither of ``exponent`` nor a ``surface_type`` is given.
598+
If both ``exponent`` and a ``surface_type`` is given. These parameters
599+
are mutually exclusive.
600+
601+
KeyError
602+
If the specified ``surface_type`` is invalid.
603+
604+
Notes
605+
-----
606+
Module temperature functions often require wind speeds at a height of 10 m
607+
and not the wind speed at the module height.
608+
609+
For example, the following temperature functions require the input wind
610+
speed to be 10 m: :py:func:`~pvlib.temperature.sapm_cell`, and
611+
:py:func:`~pvlib.temperature.sapm_module` whereas the
612+
:py:func:`~pvlib.temperature.fuentes` model requires wind speed at 9.144 m.
613+
614+
Additionally, the heat loss coefficients of some models have been developed
615+
for wind speed measurements at 10 m (e.g.,
616+
:py:func:`~pvlib.temperature.pvsyst_cell`,
617+
:py:func:`~pvlib.temperature.faiman`, and
618+
:py:func:`~pvlib.temperature.faiman_rad`).
619+
620+
The equation for calculating the wind speed at a height of :math:`h` is
621+
given by the following power law equation [1]_ [2]_:
622+
623+
.. math::
624+
:label: wind speed
625+
626+
WS_{h} = WS_{ref} \cdot \left( \frac{h}{h_{ref}} \right)^a
627+
628+
where :math:`h` [m] is the height at which we would like to calculate the
629+
wind speed, :math:`h_{ref}` [m] is the reference height at which the wind
630+
speed is known, and :math:`WS_{h}` [m/s] and :math:`WS_{ref}`
631+
[m/s] are the corresponding wind speeds at these heights. The exponent
632+
:math:`a` [unitless] depends on the surface type. Some values found in the
633+
literature [1]_ for :math:`a` are:
634+
635+
.. table:: Values for the Hellmann-exponent
636+
637+
+-----------+--------------------+------------------+------------------+
638+
| Stability | Open water surface | Flat, open coast | Cities, villages |
639+
+===========+====================+==================+==================+
640+
| Unstable | 0.06 | 0.10 | 0.27 |
641+
+-----------+--------------------+------------------+------------------+
642+
| Neutral | 0.11 | 0.16 | 0.40 |
643+
+-----------+--------------------+------------------+------------------+
644+
| Stable | 0.27 | 0.34 | 0.60 |
645+
+-----------+--------------------+------------------+------------------+
646+
647+
In a report by Sandia [3]_, the equation was experimentally tested for a
648+
height of 30 ft (:math:`h_{ref} = 9.144` [m]) at their test site in
649+
Albuquerque for a period of six weeks where a coefficient of
650+
:math:`a = 0.219` was calculated.
651+
652+
It should be noted that the equation returns a value of NaN if the
653+
reference heights or wind speed are negative.
654+
655+
References
656+
----------
657+
.. [1] Kaltschmitt M., Streicher W., Wiese A. (2007). "Renewable Energy:
658+
Technology, Economics and Environment." Springer,
659+
:doi:`10.1007/3-540-70949-5`.
660+
661+
.. [2] Hellmann G. (1915). "Über die Bewegung der Luft in den untersten
662+
Schichten der Atmosphäre." Meteorologische Zeitschrift, 32
663+
664+
.. [3] Menicucci D.F., Hall I.J. (1985). "Estimating wind speed as a
665+
function of height above ground: An analysis of data obtained at the
666+
southwest residential experiment station, Las Cruses, New Mexico."
667+
SAND84-2530, Sandia National Laboratories.
668+
Accessed at:
669+
https://web.archive.org/web/20230418202422/https://www2.jpl.nasa.gov/adv_tech/photovol/2016CTR/SNL%20-%20Est%20Wind%20Speed%20vs%20Height_1985.pdf
670+
""" # noqa:E501
671+
if surface_type is not None and exponent is None:
672+
# use the Hellmann exponent from dictionary
673+
exponent = HELLMANN_SURFACE_EXPONENTS[surface_type]
674+
elif surface_type is None and exponent is not None:
675+
# use the provided exponent
676+
pass
677+
else:
678+
raise ValueError(
679+
"Either a 'surface_type' or an 'exponent' parameter must be given")
680+
681+
wind_speed = wind_speed_reference * (
682+
(height_desired / height_reference) ** exponent)
683+
684+
# if wind speed is negative or complex return NaN
685+
wind_speed = np.where(np.iscomplex(wind_speed) | (wind_speed < 0),
686+
np.nan, wind_speed)
687+
688+
if isinstance(wind_speed_reference, pd.Series):
689+
wind_speed = pd.Series(wind_speed, index=wind_speed_reference.index)
690+
691+
return wind_speed

pvlib/tests/test_atmosphere.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,74 @@ def test_bird_hulstrom80_aod_bb():
131131
aod380, aod500 = 0.22072480948195175, 0.1614279181106312
132132
bird_hulstrom = atmosphere.bird_hulstrom80_aod_bb(aod380, aod500)
133133
assert np.isclose(0.11738229553812768, bird_hulstrom)
134+
135+
136+
@pytest.fixture
137+
def windspeeds_data_powerlaw():
138+
data = pd.DataFrame(
139+
index=pd.date_range(start="2015-01-01 00:00", end="2015-01-01 05:00",
140+
freq="1h"),
141+
columns=["wind_ref", "height_ref", "height_desired", "wind_calc"],
142+
data=[
143+
(10, -2, 5, np.nan),
144+
(-10, 2, 5, np.nan),
145+
(5, 4, 5, 5.067393209486324),
146+
(7, 6, 10, 7.2178684911195905),
147+
(10, 8, 20, 10.565167835216586),
148+
(12, 10, 30, 12.817653329393977)
149+
]
150+
)
151+
return data
152+
153+
154+
def test_windspeed_powerlaw_ndarray(windspeeds_data_powerlaw):
155+
# test wind speed estimation by passing in surface_type
156+
result_surface = atmosphere.windspeed_powerlaw(
157+
windspeeds_data_powerlaw["wind_ref"].to_numpy(),
158+
windspeeds_data_powerlaw["height_ref"],
159+
windspeeds_data_powerlaw["height_desired"],
160+
surface_type='unstable_air_above_open_water_surface')
161+
assert_allclose(windspeeds_data_powerlaw["wind_calc"].to_numpy(),
162+
result_surface)
163+
# test wind speed estimation by passing in the exponent corresponding
164+
# to the surface_type above
165+
result_exponent = atmosphere.windspeed_powerlaw(
166+
windspeeds_data_powerlaw["wind_ref"].to_numpy(),
167+
windspeeds_data_powerlaw["height_ref"],
168+
windspeeds_data_powerlaw["height_desired"],
169+
exponent=0.06)
170+
assert_allclose(windspeeds_data_powerlaw["wind_calc"].to_numpy(),
171+
result_exponent)
172+
173+
174+
def test_windspeed_powerlaw_series(windspeeds_data_powerlaw):
175+
result = atmosphere.windspeed_powerlaw(
176+
windspeeds_data_powerlaw["wind_ref"],
177+
windspeeds_data_powerlaw["height_ref"],
178+
windspeeds_data_powerlaw["height_desired"],
179+
surface_type='unstable_air_above_open_water_surface')
180+
assert_series_equal(windspeeds_data_powerlaw["wind_calc"],
181+
result, check_names=False)
182+
183+
184+
def test_windspeed_powerlaw_invalid():
185+
with pytest.raises(ValueError, match="Either a 'surface_type' or an "
186+
"'exponent' parameter must be given"):
187+
# no exponent or surface_type given
188+
atmosphere.windspeed_powerlaw(wind_speed_reference=10,
189+
height_reference=5,
190+
height_desired=10)
191+
with pytest.raises(ValueError, match="Either a 'surface_type' or an "
192+
"'exponent' parameter must be given"):
193+
# no exponent or surface_type given
194+
atmosphere.windspeed_powerlaw(wind_speed_reference=10,
195+
height_reference=5,
196+
height_desired=10,
197+
exponent=1.2,
198+
surface_type="surf")
199+
with pytest.raises(KeyError, match='not_an_exponent'):
200+
# invalid surface_type
201+
atmosphere.windspeed_powerlaw(wind_speed_reference=10,
202+
height_reference=5,
203+
height_desired=10,
204+
surface_type='not_an_exponent')

0 commit comments

Comments
 (0)