Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(wind): Bias correct wind speeds based on scaling to a known average #403

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 43 additions & 6 deletions atlite/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,19 +478,28 @@


# wind
def convert_wind(ds, turbine):
def convert_wind(
ds: xr.Dataset, turbine, windspeed_bias_correction=None, from_height=None
):
"""
Convert wind speeds for turbine to wind energy generation.
"""
V, POW, hub_height, P = itemgetter("V", "POW", "hub_height", "P")(turbine)

wnd_hub = windm.extrapolate_wind_speed(ds, to_height=hub_height)
if windspeed_bias_correction is not None:
ds = ds.assign(

Check warning on line 490 in atlite/convert.py

View check run for this annotation

Codecov / codecov/patch

atlite/convert.py#L490

Added line #L490 was not covered by tests
{f"wnd{from_height}m": ds[f"wnd{from_height}m"] * windspeed_bias_correction}
)

wnd_hub = windm.extrapolate_wind_speed(

Check warning on line 494 in atlite/convert.py

View check run for this annotation

Codecov / codecov/patch

atlite/convert.py#L494

Added line #L494 was not covered by tests
ds, to_height=hub_height, from_height=from_height
)

def _interpolate(da):
def apply_power_curve(da):

Check warning on line 498 in atlite/convert.py

View check run for this annotation

Codecov / codecov/patch

atlite/convert.py#L498

Added line #L498 was not covered by tests
return np.interp(da, V, POW / P)

da = xr.apply_ufunc(
_interpolate,
apply_power_curve,
wnd_hub,
input_core_dims=[[]],
output_core_dims=[[]],
Expand All @@ -503,7 +512,15 @@
return da


def wind(cutout, turbine, smooth=False, add_cutout_windspeed=False, **params):
def wind(
cutout,
turbine,
smooth=False,
add_cutout_windspeed=False,
real_long_run_average_windspeed=None,
real_long_run_average_height=100,
**params,
):
"""
Generate wind generation time-series.

Expand All @@ -529,6 +546,10 @@
output at the highest wind speed in the power curve. If False, a warning will be
raised if the power curve does not have a cut-out wind speed. The default is
False.
real_long_run_average_speed : Path or rasterio dataset or None
Raster dataset with wind speeds to bias correct long run average wind speeds
real_long_run_average_height : int = 100
Height in meters of provided real_long_average_speed dataset

Note
----
Expand All @@ -549,8 +570,24 @@
if smooth:
turbine = windturbine_smooth(turbine, params=smooth)

windspeed_bias_correction = None
from_height = None

Check warning on line 574 in atlite/convert.py

View check run for this annotation

Codecov / codecov/patch

atlite/convert.py#L573-L574

Added lines #L573 - L574 were not covered by tests

if real_long_run_average_windspeed is not None:
windspeed_bias_correction = windm.calculate_windspeed_bias_correction(

Check warning on line 577 in atlite/convert.py

View check run for this annotation

Codecov / codecov/patch

atlite/convert.py#L577

Added line #L577 was not covered by tests
cutout.data,
real_long_run_average_windspeed,
real_long_run_average_height,
crs=cutout.crs,
)
from_height = real_long_run_average_height

Check warning on line 583 in atlite/convert.py

View check run for this annotation

Codecov / codecov/patch

atlite/convert.py#L583

Added line #L583 was not covered by tests

return cutout.convert_and_aggregate(
convert_func=convert_wind, turbine=turbine, **params
convert_func=convert_wind,
turbine=turbine,
windspeed_bias_correction=windspeed_bias_correction,
from_height=from_height,
**params,
)


Expand Down
50 changes: 45 additions & 5 deletions atlite/datasets/era5.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
https://confluence.ecmwf.int/display/CKB/ERA5%3A+data+documentation
"""

import datetime
import logging
import os
import warnings
Expand Down Expand Up @@ -45,6 +46,7 @@
features = {
"height": ["height"],
"wind": ["wnd100m", "wnd_azimuth", "roughness"],
"wind_lra": ["wnd100m_lra"],
"influx": [
"influx_toa",
"influx_direct",
Expand All @@ -58,6 +60,7 @@
}

static_features = {"height"}
longrunaverage_features = {"wind_lra"}


def _add_height(ds):
Expand Down Expand Up @@ -101,6 +104,32 @@
return ds


def get_data_wind_lra(retrieval_params):
"""
Get long run average wind data for given retrieval parameters.
"""
ds = retrieve_data(
variable=[
"100m_u_component_of_wind",
"100m_v_component_of_wind",
],
**retrieval_params,
)
ds = _rename_and_clean_coords(ds)

Check warning on line 118 in atlite/datasets/era5.py

View check run for this annotation

Codecov / codecov/patch

atlite/datasets/era5.py#L118

Added line #L118 was not covered by tests

ds["wnd100m_lra"] = (

Check warning on line 120 in atlite/datasets/era5.py

View check run for this annotation

Codecov / codecov/patch

atlite/datasets/era5.py#L120

Added line #L120 was not covered by tests
sqrt(ds["u100"] ** 2 + ds["v100"] ** 2)
.mean("date")
.assign_attrs(
units=ds["u100"].attrs["units"],
long_name="100 metre wind speed as long run average",
)
)
ds = ds.drop_vars(["u100", "v100"])

Check warning on line 128 in atlite/datasets/era5.py

View check run for this annotation

Codecov / codecov/patch

atlite/datasets/era5.py#L128

Added line #L128 was not covered by tests

return ds

Check warning on line 130 in atlite/datasets/era5.py

View check run for this annotation

Codecov / codecov/patch

atlite/datasets/era5.py#L130

Added line #L130 was not covered by tests


def get_data_wind(retrieval_params):
"""
Get wind data for given retrieval parameters.
Expand Down Expand Up @@ -255,7 +284,7 @@
return [y1, x0, y0, x1]


def retrieval_times(coords, static=False, monthly_requests=False):
def retrieval_times(coords, static=False, monthly_requests=False, longrunaverage=False):
"""
Get list of retrieval cdsapi arguments for time dimension in coordinates.

Expand Down Expand Up @@ -287,6 +316,12 @@
"day": str(time[0].day),
"time": time[0].strftime("%H:00"),
}
elif longrunaverage:
return {
"year": [str(y) for y in range(1980, datetime.date.today().year)],
"month": [f"{m:02}" for m in range(1, 12 + 1)],
"time": ["00:00"],
}

# Prepare request for all months and years
times = []
Expand Down Expand Up @@ -323,7 +358,7 @@
logger.error(f"Unable to delete file {path}, as it is still in use.")


def retrieve_data(product, chunks=None, tmpdir=None, lock=None, **updates):
def retrieve_data(dataset, chunks=None, tmpdir=None, lock=None, **updates):
"""
Download data like ERA5 from the Climate Data Store (CDS).

Expand All @@ -340,7 +375,7 @@
client = cdsapi.Client(
info_callback=logger.debug, debug=logging.DEBUG >= logging.root.level
)
result = client.retrieve(product, request)
result = client.retrieve(dataset, request)

if lock is None:
lock = nullcontext()
Expand Down Expand Up @@ -418,7 +453,8 @@
sanitize = creation_parameters.get("sanitize", True)

retrieval_params = {
"product": "reanalysis-era5-single-levels",
"dataset": "reanalysis-era5-single-levels",
"product_type": "reanalysis",
"area": _area(coords),
"chunks": cutout.chunks,
"grid": [cutout.dx, cutout.dy],
Expand All @@ -431,14 +467,18 @@

logger.info(f"Requesting data for feature {feature}...")

def retrieve_once(time):
def retrieve_once(time, longrunaverage=False):
ds = func({**retrieval_params, **time})
if sanitize and sanitize_func is not None:
ds = sanitize_func(ds)
return ds

if feature in static_features:
return retrieve_once(retrieval_times(coords, static=True)).squeeze()
elif feature in longrunaverage_features:
retrieval_params["dataset"] += "-monthly-means"
retrieval_params["product_type"] = "monthly_averaged_reanalysis"
return retrieve_once(retrieval_times(coords, longrunaverage=True))

time_chunks = retrieval_times(coords, monthly_requests=monthly_requests)
if concurrent_requests:
Expand Down
49 changes: 40 additions & 9 deletions atlite/wind.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@

import logging
import re
from pathlib import Path

import numpy as np
import rasterio as rio
import xarray as xr

from .gis import _as_transform

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -51,14 +56,11 @@
Retrieved 2019-02-15.

"""
# Fast lane
to_name = f"wnd{int(to_height):0d}m"
if to_name in ds:
return ds[to_name]

if from_height is None:
# Determine closest height to to_name
heights = np.asarray([int(s[3:-1]) for s in ds if re.match(r"wnd\d+m", s)])
heights = np.asarray(

Check warning on line 61 in atlite/wind.py

View check run for this annotation

Codecov / codecov/patch

atlite/wind.py#L61

Added line #L61 was not covered by tests
[int(m.group(1)) for s in ds if (m := re.match(r"wnd(\d+)m", s))]
)

if len(heights) == 0:
raise AssertionError("Wind speed is not in dataset")
Expand All @@ -67,17 +69,46 @@

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

# Fast lane
if from_height == to_height:
return ds[from_name]

Check warning on line 74 in atlite/wind.py

View check run for this annotation

Codecov / codecov/patch

atlite/wind.py#L74

Added line #L74 was not covered by tests

# Wind speed extrapolation
wnd_spd = ds[from_name] * (
np.log(to_height / ds["roughness"]) / np.log(from_height / ds["roughness"])
)

wnd_spd.attrs.update(
return wnd_spd.assign_attrs(

Check warning on line 81 in atlite/wind.py

View check run for this annotation

Codecov / codecov/patch

atlite/wind.py#L81

Added line #L81 was not covered by tests
{
"long name": f"extrapolated {to_height} m wind speed using logarithmic "
f"method with roughness and {from_height} m wind speed",
"units": "m s**-1",
}
)
).rename(f"wnd{to_height}m")


def calculate_windspeed_bias_correction(ds, real_lra, lra_height, crs):
data_lra = ds[f"wnd{lra_height}m_lra"]

Check warning on line 91 in atlite/wind.py

View check run for this annotation

Codecov / codecov/patch

atlite/wind.py#L91

Added line #L91 was not covered by tests

if isinstance(real_lra, (str, Path)):
real_lra = rio.open(real_lra)

Check warning on line 94 in atlite/wind.py

View check run for this annotation

Codecov / codecov/patch

atlite/wind.py#L94

Added line #L94 was not covered by tests

if isinstance(real_lra, rio.DatasetReader):
real_lra = rio.band(real_lra, 1)

Check warning on line 97 in atlite/wind.py

View check run for this annotation

Codecov / codecov/patch

atlite/wind.py#L97

Added line #L97 was not covered by tests

if isinstance(real_lra, rio.Band):
real_lra, transform = rio.warp.reproject(

Check warning on line 100 in atlite/wind.py

View check run for this annotation

Codecov / codecov/patch

atlite/wind.py#L100

Added line #L100 was not covered by tests
real_lra,
np.empty(data_lra.shape),
dst_crs=crs,
dst_transform=_as_transform(
x=data_lra.indexes["x"], y=data_lra.indexes["y"]
),
resampling=rio.enums.Resampling.average,
)

real_lra = xr.DataArray(

Check warning on line 110 in atlite/wind.py

View check run for this annotation

Codecov / codecov/patch

atlite/wind.py#L110

Added line #L110 was not covered by tests
real_lra, [data_lra.indexes["y"], data_lra.indexes["x"]]
)

return wnd_spd.rename(to_name)
return real_lra / data_lra

Check warning on line 114 in atlite/wind.py

View check run for this annotation

Codecov / codecov/patch

atlite/wind.py#L114

Added line #L114 was not covered by tests
Loading