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

fix: power curves with missing cutoff #316

Merged
merged 6 commits into from
Sep 27, 2023
Merged
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
5 changes: 5 additions & 0 deletions RELEASE_NOTES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ Release Notes
.. Upcoming Release
.. ================

.. * Fix: the wind turbine power curve is checked for a missing cut-out wind speed and an option to add a
.. cut-out wind speed at the end of the power curve is introduced. From the next release v0.2.13, adding
.. a cut-out wind speed will be the default behavior (`GH #316 <https://github.com/PyPSA/atlite/pull/316>`_)


Version 0.2.11
==============

Expand Down
13 changes: 10 additions & 3 deletions atlite/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ def _interpolate(da):
return da


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

Expand All @@ -501,6 +501,11 @@ def wind(cutout, turbine, smooth=False, **params):
If True smooth power curve with a gaussian kernel as
determined for the Danish wind fleet to Delta_v = 1.27 and
sigma = 2.29. A dict allows to tune these values.
add_cutout_windspeed : bool
If True and in case the power curve does not end with a zero, will add zero power
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.

Note
----
Expand All @@ -512,8 +517,10 @@ def wind(cutout, turbine, smooth=False, **params):
[1] Andresen G B, Søndergaard A A and Greiner M 2015 Energy 93, Part 1
1074 – 1088. doi:10.1016/j.energy.2015.09.071
"""
if isinstance(turbine, (str, Path)):
turbine = get_windturbineconfig(turbine)
if isinstance(turbine, (str, Path, dict)):
turbine = get_windturbineconfig(
turbine, add_cutout_windspeed=add_cutout_windspeed
)

if smooth:
turbine = windturbine_smooth(turbine, params=smooth)
Expand Down
133 changes: 115 additions & 18 deletions atlite/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import json
import logging
import re
import warnings
from operator import itemgetter
from pathlib import Path

Expand All @@ -32,13 +33,13 @@
CSPINSTALLATION_DIRECTORY = RESOURCE_DIRECTORY / "cspinstallation"


def get_windturbineconfig(turbine):
def get_windturbineconfig(turbine, add_cutout_windspeed=False):
"""
Load the wind 'turbine' configuration.

Parameters
----------
turbine : str or pathlib.Path
turbine : str or pathlib.Path or dict
if str:
The name of a preshipped turbine from alite.resources.windturbine .
Alternatively, if a str starting with 'oedb:<name>' is passed the Open
Expand All @@ -47,32 +48,52 @@ def get_windturbineconfig(turbine):
`atlite.resource.get_oedb_windturbineconfig(...)`
if `pathlib.Path` is provided the configuration is read from this local
path instead
if dict:
a user provided config dict. Needs to have the keys "POW", "V", "P", and
"hub_height". Values for "POW" and "V" need to be list or np.ndarray with
equal length.
add_cutout_windspeed : bool
If True and in case the power curve does not end with a zero, will add zero power
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.

Returns
----------
config : dict
Config with details on the turbine
"""
assert isinstance(turbine, (str, Path))
assert isinstance(turbine, (str, Path, dict))

if isinstance(turbine, str) and turbine.startswith("oedb:"):
return get_oedb_windturbineconfig(turbine[len("oedb:") :])

elif isinstance(turbine, str):
turbine_path = windturbines[turbine.replace(".yaml", "")]
if add_cutout_windspeed is False:
msg = (
"'add_cutout_windspeed' for wind turbine\npower curves will default to "
"True in atlite relase v0.2.13."
)
warnings.warn(msg, FutureWarning)

elif isinstance(turbine, Path):
turbine_path = turbine
if isinstance(turbine, str) and turbine.startswith("oedb:"):
conf = get_oedb_windturbineconfig(turbine[len("oedb:") :])

elif isinstance(turbine, (str, Path)):
if isinstance(turbine, str):
turbine_path = windturbines[turbine.replace(".yaml", "")]

elif isinstance(turbine, Path):
turbine_path = turbine

with open(turbine_path, "r") as f:
conf = yaml.safe_load(f)
conf = dict(
V=np.array(conf["V"]),
POW=np.array(conf["POW"]),
hub_height=conf["HUB_HEIGHT"],
P=np.max(conf["POW"]),
)

with open(turbine_path, "r") as f:
conf = yaml.safe_load(f)
elif isinstance(turbine, dict):
conf = turbine

return dict(
V=np.array(conf["V"]),
POW=np.array(conf["POW"]),
hub_height=conf["HUB_HEIGHT"],
P=np.max(conf["POW"]),
)
return _validate_turbine_config_dict(conf, add_cutout_windspeed)


def get_solarpanelconfig(panel):
Expand Down Expand Up @@ -260,6 +281,82 @@ def smooth(velocities, power):
return turbine


def _max_v_is_zero_pow(turbine):
return np.any((turbine["POW"][turbine["V"] == turbine["V"].max()] == 0))


def _validate_turbine_config_dict(turbine: dict, add_cutout_windspeed: bool):
"""
Checks the turbine config dict format and power curve.

Parameters
----------
turbine : dict
turbine configuration dict. Needs the keys "POW", "V", "P", and "hub_height".
Values for "V" and "POW" need to be list or np.ndarray.
add_cutout_windspeed : bool
If True and in case the power curve does not end with a zero, will add zero power
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.

Returns
-------
dict
validated and potentially modified turbine config dict
"""
if not all(key in turbine for key in ("POW", "V", "P", "hub_height")):
err_msg = (
"turbine config dict needs at least the following keys: ['POW', 'V', 'P', "
f"'hub_height']\nbut are currently: {list(turbine.keys())}"
)
raise ValueError(err_msg)

if not all(isinstance(turbine[p], (np.ndarray, list)) for p in ("POW", "V")):
err_msg = "turbine entries 'POW' and 'V' must be np.ndarray or list"
raise ValueError(err_msg)

# convert lists from user provided turbine dicts to numpy arrays
if any(isinstance(turbine[p], list) for p in ("POW", "V")):
turbine["V"] = np.array(turbine["V"])
turbine["POW"] = np.array(turbine["POW"])

if len(turbine["POW"]) != len(turbine["V"]):
err_msg = "turbine wind speed and power arrays do not have equal length."
raise ValueError(err_msg)

if not np.all(np.diff(turbine["V"]) >= 0):
# This check is not strict as it uses `>=` instead of `>` and thus allows equal
# wind speeds in the array. However, many power curves have two entries for the
# same wind speed at the cut-in and cut-out speeds which would make them fail if
# using `>` only.
err_msg = (
"wind speed 'V' in the turbine config dict is expected to be increasing, "
f"but is currently not in ascending order:\n{turbine['V']}"
)
raise ValueError(err_msg)

if add_cutout_windspeed is True and not _max_v_is_zero_pow(turbine):
turbine["V"] = np.pad(turbine["V"], (0, 1), "maximum")
turbine["POW"] = np.pad(turbine["POW"], (0, 1), "constant", constant_values=0)
logger.info(
(
"adding a cut-out wind speed to the turbine power curve at "
f"V={turbine['V'][-1]} m/s."
)
)

if not _max_v_is_zero_pow(turbine):
logger.warning(
(
"The power curve does not have a cut-out wind speed, i.e. the power"
" output corresponding to the\nhighest wind speed is not zero. You can"
" either change the power curve manually or set\n"
"'add_cutout_windspeed=True' in the Cutout.wind conversion method."
)
)
return turbine


def get_oedb_windturbineconfig(search=None, **search_params):
"""
Download a windturbine configuration from the OEDB database.
Expand Down
11 changes: 10 additions & 1 deletion test/test_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"""
import pytest

from atlite.resource import get_oedb_windturbineconfig
from atlite.resource import get_oedb_windturbineconfig, get_windturbineconfig


def test_oedb_windturbineconfig():
Expand All @@ -23,3 +23,12 @@ def test_oedb_windturbineconfig():

# test string search with param
assert get_oedb_windturbineconfig("E-101/3500 E2", hub_height=99)


@pytest.mark.parametrize("add_cutout, last_pow", [(True, 0.0), (False, 1.0)])
def test_windturbineconfig_add_cutout(add_cutout, last_pow):
t = get_windturbineconfig(
{"V": [0, 25], "POW": [0.0, 1.0], "hub_height": 1.0, "P": 1.0},
add_cutout_windspeed=add_cutout,
)
assert t["POW"][-1] == last_pow