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

Add NREL turbine data to atlite #233

Merged
merged 8 commits into from
Apr 13, 2022
Merged

Add NREL turbine data to atlite #233

merged 8 commits into from
Apr 13, 2022

Conversation

thesethtruth
Copy link
Contributor

@thesethtruth thesethtruth commented Apr 11, 2022

Description

Adds more turbine .yaml to the database.

Motivation and Context

Some turbines in the current database are a bit outdated. Moreover, if used in ESOM, users might want to use power curves for future / expected turbine types (with a higher rated power and different power curve). This is done based on the NREL turbine data set, which I've parsed to the atlite format using some basic python and Jinja2 templating (see: turbine parsing repo).

In our current project we use PyPSA-eur and felt like this feature was limiting. This was discussed with @fneum.

How Has This Been Tested?

Tested in an PyPSA run and through atlite on ERA5 dataset

Type of change

Extension of existing data

Feel free to propose updates / changes. Let me know what you need to use this data.

Copy link
Collaborator

@euronion euronion left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @thesethtruth ,

thanks for the PR! You are right: We have been putting off introducing new turbines for a long time. There are a two things which need to be adressed. See the comments in one of the files (the comments apply to all files).

Let me know if you have any questions.

@thesethtruth
Copy link
Contributor Author

Comments from @euronion processed.

If in the future you'd want to parse the data yourself; I've automated most in https://github.com/thesethtruth/NREL-to-atlite-turbines.

Cheers!

@euronion
Copy link
Collaborator

Nice, LGTM, thanks @thesethtruth !

Pinging @fneum since you've discussed this earlier elsewhere: Is it GTM?

@fneum
Copy link
Member

fneum commented Apr 13, 2022

GTM! Thanks!

@euronion
Copy link
Collaborator

Ah; nearly missed it: Can add a oneliner to the RELEASE_NOTES mentioning new turbines ? @thesethtruth

@thesethtruth
Copy link
Contributor Author

Done! See below:

  • Atlite now includes the reference turbines from the NREL turbine archive (see: https://nrel.github.io/turbine-models/). Available turbines can be consulted using atlite.windturbines and can be passed as string argument, e.g. coutout.wind(turbine).

@euronion euronion merged commit ccc064e into PyPSA:master Apr 13, 2022
@euronion
Copy link
Collaborator

Great, thanks!

@coroa
Copy link
Member

coroa commented May 24, 2022

Thanks for that merge, quite helpful!

As a further idea you could consider adding your existing parsing code directly to atlite as an extension to get_windturbineconfig in the same manner as we do for the oedb; so that one could use:

cutout.wind(turbine="nrel:2016CACost_NREL_Reference_8MW_180")

to transparently fetch the turbine defined at https://nrel.github.io/turbine-models/2016CACost_NREL_Reference_8MW_180.html

That would work by adding a new hook in get_windturbineconfig.

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

And putting your code into a new function like get_oedb_windturbineconfig.

atlite/atlite/resource.py

Lines 238 to 371 in 2ab6c9e

def get_oedb_windturbineconfig(search=None, **search_params):
"""
Download a windturbine configuration from the OEDB database.
Download the configuration of a windturbine model from the OEDB database
into the local 'windturbine_dir'.
The OEDB database can be viewed here:
https://openenergy-platform.org/dataedit/view/supply/wind_turbine_library
(2019-07-22)
Only one turbine configuration is downloaded at a time, if the
search parameters yield an ambigious result, no data is downloaded.
Parameters
----------
search : int|str
Smart search parameter, if int use as model id, if str look in name or turbine_type
**search_params : dict
Recognized arguments are 'id', 'name', 'turbine_type' and 'manufacturer'
Returns
-------
turbineconfig : dict
The turbine configuration in the format from 'atlite.ressource.get_turbineconf(name)'.
Example
-------
>>> get_oedb_windturbineconfig(10)
{'V': ..., 'POW': ..., ...}
>>> get_oedb_windturbineconfig(name="E-53/800", manufacturer="Enercon")
{'V': ..., 'POW': ..., ...}
"""
# Parse information of different allowed 'turbine' values
if isinstance(search, int):
search_params.setdefault("id", search)
search = None
# Retrieve and cache OEDB turbine data
OEDB_URL = "https://openenergy-platform.org/api/v0/schema/supply/tables/wind_turbine_library/rows"
# Cache turbine request locally
global _oedb_turbines
if _oedb_turbines is None:
# Get the turbine list
result = requests.get(OEDB_URL)
# Convert JSON to dataframe for easier filtering
# Only consider turbines with power curves available
df = pd.DataFrame.from_dict(result.json())
_oedb_turbines = df[df.has_power_curve]
logger.info(
"Searching turbine power curve in OEDB database using "
+ ", ".join(f"{k}='{v}'" for (k, v) in search_params.items())
+ "."
)
# Working copy
df = _oedb_turbines
selector = True
if search is not None:
selector &= df.name.str.contains(
search, case=False
) | df.turbine_type.str.contains(search, case=False)
if "id" in search_params:
selector &= df.id == int(search_params["id"])
if "name" in search_params:
selector &= df.name.str.contains(search_params["name"], case=False)
if "turbine_type" in search_params:
selector &= df.turbine_type.str.contains(search_params["name"], case=False)
if "manufacturer" in search_params:
selector &= df.manufacturer.str.contains(
search_params["manufacturer"], case=False
)
df = df.loc[selector]
if len(df) < 1:
raise RuntimeError("No turbine found.")
elif len(df) > 1:
raise RuntimeError(
f"Provided information corresponds to {len(df)} turbines,"
" use `id` for an unambiguous search.\n"
+ str(df[["id", "manufacturer", "turbine_type"]])
)
# Convert to series for simpliticty
ds = df.iloc[0]
# convert power from kW to MW
power = np.array(json.loads(ds.power_curve_values)) / 1e3
hub_height = ds.hub_height
if not hub_height:
hub_height = 100
logger.warning(
"No hub_height defined in dataset. Manual clean-up required."
"Assuming a hub_height of 100m for now."
)
elif isinstance(hub_height, str):
hub_heights = [float(t) for t in re.split(r"\s*;\s*", hub_height.strip()) if t]
if len(hub_heights) > 1:
hub_height = np.mean(hub_heights, dtype=int)
logger.warning(
"Multiple values for hub_height in dataset (%s). "
"Manual clean-up required. Using the averge %dm for now.",
hub_heights,
hub_height,
)
else:
hub_height = hub_heights[0]
turbineconf = {
"name": ds.turbine_type.strip(),
"manufacturer": ds.manufacturer.strip(),
"source": f"Original: {ds.source}. Via OEDB {OEDB_URL}",
"hub_height": hub_height,
"V": np.array(json.loads(ds.power_curve_wind_speeds)),
"POW": power,
"P": power.max(),
}
# Cache in windturbines
global windturbines
charmap = str.maketrans("/- ", "___")
name = "{manufacturer}_{name}".format(**turbineconf).translate(charmap)
windturbines[name] = turbineconf
return turbineconf

Maybe that is good first issue. But, many thanks either way!

@thesethtruth
Copy link
Contributor Author

@coroa Thanks for your comment. I had the email notification parked for a long time now (hence the late response) hoping I would find the time to implement your suggestion. Unfortunately, I won't be able to since I'm very tight for time at the moment. If I have time at a later stage and this is still relevant I will implement it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants