Skip to content

Commit 2e445cf

Browse files
coroalkstrppre-commit-ci[bot]
authored
feat(wind): Add power law interpolation method for wind conversion (#402)
* feat(wind): Add power law interpolation method for wind conversion Refer to #231 for context. * fix wind docstring * Update atlite/convert.py Co-authored-by: Lukas Trippe <[email protected]> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add typing for wind conversion * fix(wind): Raise RuntimeError from extrapolate_wind_speed with an explanation If auxiliary data is missing from the cutout. --------- Co-authored-by: Jonas Hoersch <[email protected]> Co-authored-by: Lukas Trippe <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 481f3af commit 2e445cf

File tree

6 files changed

+176
-67
lines changed

6 files changed

+176
-67
lines changed

atlite/convert.py

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
All functions for converting weather data into energy system model data.
66
"""
77

8+
from __future__ import annotations
9+
810
import datetime as dt
911
import logging
1012
from collections import namedtuple
1113
from operator import itemgetter
1214
from pathlib import Path
15+
from typing import TYPE_CHECKING
1316

1417
import geopandas as gpd
1518
import numpy as np
@@ -39,6 +42,11 @@
3942

4043
logger = logging.getLogger(__name__)
4144

45+
if TYPE_CHECKING:
46+
from typing import Literal
47+
48+
from atlite.resource import TurbineConfig
49+
4250

4351
def convert_and_aggregate(
4452
cutout,
@@ -478,19 +486,25 @@ def solar_thermal(
478486

479487

480488
# wind
481-
def convert_wind(ds, turbine):
489+
def convert_wind(
490+
ds: xr.Dataset,
491+
turbine: TurbineConfig,
492+
interpolation_method: Literal["logarithmic", "power"],
493+
) -> xr.DataArray:
482494
"""
483495
Convert wind speeds for turbine to wind energy generation.
484496
"""
485497
V, POW, hub_height, P = itemgetter("V", "POW", "hub_height", "P")(turbine)
486498

487-
wnd_hub = windm.extrapolate_wind_speed(ds, to_height=hub_height)
499+
wnd_hub = windm.extrapolate_wind_speed(
500+
ds, to_height=hub_height, method=interpolation_method
501+
)
488502

489-
def _interpolate(da):
503+
def apply_power_curve(da):
490504
return np.interp(da, V, POW / P)
491505

492506
da = xr.apply_ufunc(
493-
_interpolate,
507+
apply_power_curve,
494508
wnd_hub,
495509
input_core_dims=[[]],
496510
output_core_dims=[[]],
@@ -503,12 +517,19 @@ def _interpolate(da):
503517
return da
504518

505519

506-
def wind(cutout, turbine, smooth=False, add_cutout_windspeed=False, **params):
520+
def wind(
521+
cutout,
522+
turbine: str | Path | dict,
523+
smooth: bool | dict = False,
524+
add_cutout_windspeed: bool = False,
525+
interpolation_method: Literal["logarithmic", "power"] = "logarithmic",
526+
**params,
527+
) -> xr.DataArray:
507528
"""
508529
Generate wind generation time-series.
509530
510-
Extrapolates 10m wind speed with monthly surface roughness to hub
511-
height and evaluates the power curve.
531+
Extrapolates wind speed to hub height (using logarithmic or power law) and
532+
evaluates the power curve.
512533
513534
Parameters
514535
----------
@@ -529,6 +550,9 @@ def wind(cutout, turbine, smooth=False, add_cutout_windspeed=False, **params):
529550
output at the highest wind speed in the power curve. If False, a warning will be
530551
raised if the power curve does not have a cut-out wind speed. The default is
531552
False.
553+
interpolation_method : {"logarithmic", "power"}
554+
Law to interpolate wind speed to turbine hub height. Refer to
555+
:py:func:`atlite.wind.extrapolate_wind_speed`.
532556
533557
Note
534558
----
@@ -537,20 +561,20 @@ def wind(cutout, turbine, smooth=False, add_cutout_windspeed=False, **params):
537561
538562
References
539563
----------
540-
[1] Andresen G B, Søndergaard A A and Greiner M 2015 Energy 93, Part 1
541-
1074 – 1088. doi:10.1016/j.energy.2015.09.071
564+
.. [1] Andresen G B, Søndergaard A A and Greiner M 2015 Energy 93, Part 1
565+
1074 – 1088. doi:10.1016/j.energy.2015.09.071
542566
543567
"""
544-
if isinstance(turbine, (str, Path, dict)):
545-
turbine = get_windturbineconfig(
546-
turbine, add_cutout_windspeed=add_cutout_windspeed
547-
)
568+
turbine = get_windturbineconfig(turbine, add_cutout_windspeed=add_cutout_windspeed)
548569

549570
if smooth:
550571
turbine = windturbine_smooth(turbine, params=smooth)
551572

552573
return cutout.convert_and_aggregate(
553-
convert_func=convert_wind, turbine=turbine, **params
574+
convert_func=convert_wind,
575+
turbine=turbine,
576+
interpolation_method=interpolation_method,
577+
**params,
554578
)
555579

556580

atlite/datasets/era5.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def nullcontext():
4444

4545
features = {
4646
"height": ["height"],
47-
"wind": ["wnd100m", "wnd_azimuth", "roughness"],
47+
"wind": ["wnd100m", "wnd_shear_exp", "wnd_azimuth", "roughness"],
4848
"influx": [
4949
"influx_toa",
5050
"influx_direct",
@@ -107,6 +107,8 @@ def get_data_wind(retrieval_params):
107107
"""
108108
ds = retrieve_data(
109109
variable=[
110+
"10m_u_component_of_wind",
111+
"10m_v_component_of_wind",
110112
"100m_u_component_of_wind",
111113
"100m_v_component_of_wind",
112114
"forecast_surface_roughness",
@@ -115,13 +117,19 @@ def get_data_wind(retrieval_params):
115117
)
116118
ds = _rename_and_clean_coords(ds)
117119

118-
ds["wnd100m"] = sqrt(ds["u100"] ** 2 + ds["v100"] ** 2).assign_attrs(
119-
units=ds["u100"].attrs["units"], long_name="100 metre wind speed"
120-
)
120+
for h in [10, 100]:
121+
ds[f"wnd{h}m"] = sqrt(ds[f"u{h}"] ** 2 + ds[f"v{h}"] ** 2).assign_attrs(
122+
units=ds[f"u{h}"].attrs["units"], long_name=f"{h} metre wind speed"
123+
)
124+
ds["wnd_shear_exp"] = (
125+
np.log(ds["wnd10m"] / ds["wnd100m"]) / np.log(10 / 100)
126+
).assign_attrs(units="", long_name="wind shear exponent")
127+
121128
# span the whole circle: 0 is north, π/2 is east, -π is south, 3π/2 is west
122129
azimuth = arctan2(ds["u100"], ds["v100"])
123130
ds["wnd_azimuth"] = azimuth.where(azimuth >= 0, azimuth + 2 * np.pi)
124-
ds = ds.drop_vars(["u100", "v100"])
131+
132+
ds = ds.drop_vars(["u100", "v100", "u10", "v10", "wnd10m"])
125133
ds = ds.rename({"fsr": "roughness"})
126134

127135
return ds

atlite/resource.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@
66
panel configurations.
77
"""
88

9+
from __future__ import annotations
10+
911
import json
1012
import logging
1113
import re
1214
import warnings
1315
from operator import itemgetter
1416
from pathlib import Path
17+
from typing import TYPE_CHECKING
1518

1619
import numpy as np
1720
import pandas as pd
@@ -30,8 +33,24 @@
3033
SOLARPANEL_DIRECTORY = RESOURCE_DIRECTORY / "solarpanel"
3134
CSPINSTALLATION_DIRECTORY = RESOURCE_DIRECTORY / "cspinstallation"
3235

36+
if TYPE_CHECKING:
37+
from typing import TypedDict
38+
39+
from typing_extensions import NotRequired
40+
41+
class TurbineConfig(TypedDict):
42+
V: np.ndarray
43+
POW: np.ndarray
44+
P: float
45+
hub_height: float | int
46+
name: NotRequired[str]
47+
manufacturer: NotRequired[str]
48+
source: NotRequired[str]
49+
3350

34-
def get_windturbineconfig(turbine, add_cutout_windspeed=False):
51+
def get_windturbineconfig(
52+
turbine: str | Path | dict, add_cutout_windspeed: bool = False
53+
) -> TurbineConfig:
3554
"""
3655
Load the wind 'turbine' configuration.
3756
@@ -61,7 +80,7 @@ def get_windturbineconfig(turbine, add_cutout_windspeed=False):
6180
Config with details on the turbine
6281
6382
"""
64-
assert isinstance(turbine, (str, Path, dict))
83+
assert isinstance(turbine, str | Path | dict)
6584

6685
if add_cutout_windspeed is False:
6786
msg = (
@@ -73,7 +92,7 @@ def get_windturbineconfig(turbine, add_cutout_windspeed=False):
7392
if isinstance(turbine, str) and turbine.startswith("oedb:"):
7493
conf = get_oedb_windturbineconfig(turbine[len("oedb:") :])
7594

76-
elif isinstance(turbine, (str, Path)):
95+
elif isinstance(turbine, str | Path):
7796
if isinstance(turbine, str):
7897
turbine_path = windturbines[turbine.replace(".yaml", "")]
7998

@@ -287,7 +306,9 @@ def _max_v_is_zero_pow(turbine):
287306
return np.any(turbine["POW"][turbine["V"] == turbine["V"].max()] == 0)
288307

289308

290-
def _validate_turbine_config_dict(turbine: dict, add_cutout_windspeed: bool):
309+
def _validate_turbine_config_dict(
310+
turbine: dict, add_cutout_windspeed: bool
311+
) -> TurbineConfig:
291312
"""
292313
Checks the turbine config dict format and power curve.
293314
@@ -356,7 +377,9 @@ def _validate_turbine_config_dict(turbine: dict, add_cutout_windspeed: bool):
356377
return turbine
357378

358379

359-
def get_oedb_windturbineconfig(search=None, **search_params):
380+
def get_oedb_windturbineconfig(
381+
search: int | str | None = None, **search_params
382+
) -> TurbineConfig:
360383
"""
361384
Download a windturbine configuration from the OEDB database.
362385

atlite/wind.py

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,51 +5,72 @@
55
Functions for use in conjunction with wind data generation.
66
"""
77

8+
from __future__ import annotations
9+
810
import logging
911
import re
12+
from typing import TYPE_CHECKING
1013

1114
import numpy as np
15+
import xarray as xr
1216

1317
logger = logging.getLogger(__name__)
1418

1519

16-
def extrapolate_wind_speed(ds, to_height, from_height=None):
20+
if TYPE_CHECKING:
21+
from typing import Literal
22+
23+
24+
def extrapolate_wind_speed(
25+
ds: xr.Dataset,
26+
to_height: int | float,
27+
from_height: int | None = None,
28+
method: Literal["logarithmic", "power"] = "logarithmic",
29+
) -> xr.DataArray:
1730
"""
1831
Extrapolate the wind speed from a given height above ground to another.
1932
2033
If ds already contains a key refering to wind speeds at the desired to_height,
2134
no conversion is done and the wind speeds are directly returned.
2235
23-
Extrapolation of the wind speed follows the logarithmic law as desribed in [1].
36+
Extrapolation of the wind speed can either use the "logarithmic" law as
37+
described in [1]_ or the "power" law as described in [2]_. See also discussion
38+
in GH issue: https://github.com/PyPSA/atlite/issues/231 .
2439
2540
Parameters
2641
----------
2742
ds : xarray.Dataset
2843
Dataset containing the wind speed time-series at 'from_height' with key
2944
'wnd{height:d}m' and the surface orography with key 'roughness' at the
3045
geographic locations of the wind speeds.
31-
from_height : int
32-
(Optional)
33-
Height (m) from which the wind speeds are interpolated to 'to_height'.
34-
If not provided, the closest height to 'to_height' is selected.
3546
to_height : int|float
3647
Height (m) to which the wind speeds are extrapolated to.
48+
from_height : int, optional
49+
Height (m) from which the wind speeds are interpolated to 'to_height'.
50+
If not provided, the closest height to 'to_height' is selected.
51+
method : {"logarithmic", "power"}
52+
Method to use for extra/interpolating wind speeds
3753
3854
Returns
3955
-------
4056
da : xarray.DataArray
4157
DataArray containing the extrapolated wind speeds. Name of the DataArray
4258
is 'wnd{to_height:d}'.
4359
60+
Raises
61+
------
62+
RuntimeError
63+
If the cutout is missing the data for the chosen `method`
64+
4465
References
4566
----------
46-
[1] Equation (2) in Andresen, G. et al (2015): 'Validation of Danish wind
47-
time series from a new global renewable energy atlas for energy system
48-
analysis'.
49-
50-
[2] https://en.wikipedia.org/w/index.php?title=Roughness_length&oldid=862127433,
51-
Retrieved 2019-02-15.
67+
.. [1] Equation (2) in Andresen, G. et al (2015): 'Validation of Danish
68+
wind time series from a new global renewable energy atlas for energy
69+
system analysis'.
5270
71+
.. [2] Gualtieri, G. (2021): 'Reliability of ERA5 Reanalysis Data for
72+
Wind Resource Assessment: A Comparison against Tall Towers'
73+
https://doi.org/10.3390/en14144169 .
5374
"""
5475
# Fast lane
5576
to_name = f"wnd{int(to_height):0d}m"
@@ -67,15 +88,40 @@ def extrapolate_wind_speed(ds, to_height, from_height=None):
6788

6889
from_name = f"wnd{int(from_height):0d}m"
6990

70-
# Wind speed extrapolation
71-
wnd_spd = ds[from_name] * (
72-
np.log(to_height / ds["roughness"]) / np.log(from_height / ds["roughness"])
73-
)
91+
if method == "logarithmic":
92+
try:
93+
roughness = ds["roughness"]
94+
except KeyError:
95+
raise RuntimeError(
96+
"The logarithmic interpolation method requires surface roughness (roughness);\n"
97+
"make sure you choose a compatible dataset like ERA5"
98+
)
99+
wnd_spd = ds[from_name] * (
100+
np.log(to_height / roughness) / np.log(from_height / roughness)
101+
)
102+
method_desc = "logarithmic method with roughness"
103+
elif method == "power":
104+
try:
105+
wnd_shear_exp = ds["wnd_shear_exp"]
106+
except KeyError:
107+
raise RuntimeError(
108+
"The power law interpolation method requires a wind shear exponent (wnd_shear_exp);\n"
109+
"make sure you choose a compatible dataset like ERA5 and update your cutout"
110+
)
111+
wnd_spd = ds[from_name] * (to_height / from_height) ** wnd_shear_exp
112+
method_desc = "power method with wind shear exponent"
113+
else:
114+
raise ValueError(
115+
f"Interpolation method must be 'logarithmic' or 'power', "
116+
f" but is: {method}"
117+
)
74118

75119
wnd_spd.attrs.update(
76120
{
77-
"long name": f"extrapolated {to_height} m wind speed using logarithmic "
78-
f"method with roughness and {from_height} m wind speed",
121+
"long name": (
122+
f"extrapolated {to_height} m wind speed using {method_desc} "
123+
f" and {from_height} m wind speed"
124+
),
79125
"units": "m s**-1",
80126
}
81127
)

0 commit comments

Comments
 (0)