"""
Wind Toolkit Data Utility Functions
===================================
This module contains a collection of utility functions designed to facilitate
the extraction, caching, and visualization of wind data from the WIND Toolkit
hindcast dataset hosted on AWS. This dataset includes offshore wind hindcast data
with various parameters like wind speed, direction, temperature, and pressure.
Key Functions:
--------------
- `region_selection`: Determines which predefined wind region a given latitude
and longitude fall within.
- `get_region_data`: Retrieves latitude and longitude data points for a specified
wind region. Uses caching to speed up repeated requests.
- `plot_region`: Plots the geographical extent of a specified wind region and
can overlay a given latitude-longitude point.
- `elevation_to_string`: Converts a parameter (e.g., 'windspeed') and elevation
values (e.g., [20, 40, 120]) to the formatted strings used in the WIND Toolkit.
- `request_wtk_point_data`: Fetches specified wind data parameters for given
latitude-longitude points and years from the WIND Toolkit hindcast dataset.
Supports caching for faster repeated data retrieval.
Dependencies:
-------------
- rex: Library to handle renewable energy datasets.
- pandas: Data manipulation and analysis.
- os, hashlib, pickle: Used for caching functionality.
- matplotlib: Used for plotting.
Notes:
------
- To access the WIND Toolkit hindcast data, users need to configure `h5pyd`
for data access on HSDS (see the metocean_example or WPTO_hindcast_example
notebook for more details).
- While some functions perform basic checks (e.g., verifying that latitude
and longitude are within a predefined region), it's essential to understand
the boundaries of each region and the available parameters and elevations in the dataset.
Author:
-------
akeeste
ssolson
Date:
-----
2023-09-26
"""
import os
import hashlib
import pickle
import pandas as pd
from rex import MultiYearWindX
import matplotlib.pyplot as plt
from mhkit.utils.cache import handle_caching
from mhkit.utils.type_handling import convert_to_dataset
[docs]
def region_selection(lat_lon, preferred_region=""):
"""
Returns the name of the predefined region in which the given coordinates reside.
Can be used to check if the passed lat/lon pair is within the WIND Toolkit hindcast dataset.
Parameters
----------
lat_lon : tuple
Latitude and longitude coordinates as floats or integers
preferred_region : string (optional)
Latitude and longitude coordinates as floats or integers
Returns
-------
region : string
Name of predefined region for given coordinates
"""
if not isinstance(lat_lon, tuple):
raise TypeError(f"lat_lon must be of type tuple, got {type(lat_lon).__name__}")
if len(lat_lon) != 2:
raise ValueError(f"lat_lon must be of length 2, got length {len(lat_lon)}")
if not isinstance(lat_lon[0], (float, int)):
raise TypeError(
f"lat_lon values must be floats or ints, got {type(lat_lon[0]).__name__}"
)
if not isinstance(lat_lon[1], (float, int)):
raise TypeError(
f"lat_lon values must be floats or ints, got {type(lat_lon[1]).__name__}"
)
if not isinstance(preferred_region, str):
raise TypeError(
f"preferred_region must be a string, got {type(preferred_region).__name__}"
)
# Note that this check is fast, but not robust because region are not
# rectangular on a lat-lon grid
rDict = {
"CA_NWP_overlap": {"lat": [41.213, 42.642], "lon": [-129.090, -121.672]},
"Offshore_CA": {"lat": [31.932, 42.642], "lon": [-129.090, -115.806]},
"Hawaii": {"lat": [15.565, 26.221], "lon": [-164.451, -151.278]},
"NW_Pacific": {"lat": [41.213, 49.579], "lon": [-130.831, -121.672]},
"Mid_Atlantic": {"lat": [37.273, 42.211], "lon": [-76.427, -64.800]},
}
def region_search(x):
return all(
(
True if rDict[x][dk][0] <= d <= rDict[x][dk][1] else False
for dk, d in {"lat": lat_lon[0], "lon": lat_lon[1]}.items()
)
)
region = [key for key in rDict if region_search(key)]
if region[0] == "CA_NWP_overlap":
if preferred_region == "Offshore_CA":
region[0] = "Offshore_CA"
elif preferred_region == "NW_Pacific":
region[0] = "NW_Pacific"
else:
raise TypeError(
f"Preferred_region ({preferred_region}) must be 'Offshore_CA' or 'NW_Pacific' when lat_lon {lat_lon} falls in the overlap region"
)
if len(region) == 0:
raise TypeError(f"Coordinates {lat_lon} out of bounds. Must be within {rDict}")
else:
return region[0]
[docs]
def get_region_data(region):
"""
Retrieves the latitude and longitude data points for the specified region
from the cache if available; otherwise, fetches the data and caches it for
subsequent calls.
The function forms a unique identifier from the `region` parameter and checks
whether the corresponding data is available in the cache. If the data is found,
it's loaded and returned. If not, the data is fetched, cached, and then returned.
Parameters
----------
region : str
Name of the predefined region in the WIND Toolkit for which to
retrieve latitude and longitude data points. It is case-sensitive.
Examples: 'Offshore_CA','Hawaii','Mid_Atlantic','NW_Pacific'
Returns
-------
lats : numpy.ndarray
A 1D array containing the latitude coordinates of data points
in the specified region.
lons : numpy.ndarray
A 1D array containing the longitude coordinates of data points
in the specified region.
Example
-------
>>> lats, lons = get_region_data('Offshore_CA')
"""
if not isinstance(region, str):
raise TypeError("region must be of type string")
# Define the path to the cache directory
cache_dir = os.path.join(os.path.expanduser("~"), ".cache", "mhkit", "hindcast")
# Create a unique identifier for this function call
hash_id = hashlib.md5(region.encode()).hexdigest()
# Create cache directory if it doesn't exist
os.makedirs(cache_dir, exist_ok=True)
# Create a path to the cache file for this function call
cache_file = os.path.join(cache_dir, f"{hash_id}.pkl")
if os.path.isfile(cache_file):
# If the cache file exists, load the data from the cache
with open(cache_file, "rb") as f:
lats, lons = pickle.load(f)
return lats, lons
else:
wind_path = "/nrel/wtk/" + region.lower() + "/" + region + "_*.h5"
windKwargs = {
"tree": None,
"unscale": True,
"str_decode": True,
"hsds": True,
"years": [2019],
}
# Get the latitude and longitude list from the region in rex
rex_wind = MultiYearWindX(wind_path, **windKwargs)
lats = rex_wind.lat_lon[:, 0]
lons = rex_wind.lat_lon[:, 1]
# Save data to cache
with open(cache_file, "wb") as f:
pickle.dump((lats, lons), f)
return lats, lons
[docs]
def plot_region(region, lat_lon=None, ax=None):
"""
Visualizes the area that a given region covers. Can help users understand
the extent of a region since they are not all rectangular.
Parameters
----------
region : string
Name of predefined region in the WIND Toolkit
Options: 'Offshore_CA','Hawaii','Mid_Atlantic','NW_Pacific'
lat_lon : couple (optional)
Latitude and longitude pair to plot on top of the chosen region. Useful
to inform accurate latitude-longitude selection for data analysis.
ax : matplotlib axes object (optional)
Axes for plotting. If None, then a new figure is created.
Returns
---------
ax : matplotlib pyplot axes
"""
if not isinstance(region, str):
raise TypeError("region must be of type string")
supported_regions = ["Offshore_CA", "Hawaii", "Mid_Atlantic", "NW_Pacific"]
if region not in supported_regions:
raise ValueError(
f'{region} not in list of supported regions: {", ".join(supported_regions)}'
)
lats, lons = get_region_data(region)
# Plot the latitude longitude pairs
if ax is None:
fig, ax = plt.subplots()
ax.plot(lons, lats, "o", label=f"{region} region")
if lat_lon is not None:
ax.plot(lat_lon[1], lat_lon[0], "o", label="Specified lat-lon point")
ax.set_xlabel("Longitude (deg)")
ax.set_ylabel("Latitude (deg)")
ax.grid()
ax.set_title(f"Extent of the WIND Toolkit {region} region")
ax.legend()
return ax
[docs]
def elevation_to_string(parameter, elevations):
"""
Takes in a parameter (e.g. 'windspeed') and elevations (e.g. [20, 40, 120])
and returns the formatted strings that are input to WIND Toolkit (e.g. windspeed_10m).
Does not check parameter against the elevation levels. This is done in request_wtk_point_data.
Parameters
----------
parameter: string
Name of the WIND toolkit parameter.
Options: 'windspeed', 'winddirection', 'temperature', 'pressure'
elevations : list
List of elevations (float).
Values can range from approxiamtely 20 to 200 in increments of 20, depending
on the parameter in question. See Documentation for request_wtk_point_data
for the full list of available parameters.
Returns
---------
parameter_list: list
Formatted List of WIND Toolkit parameter strings
"""
if not isinstance(parameter, str):
raise TypeError(f"parameter must be a string, got {type(parameter)}")
if not isinstance(elevations, (float, list)):
raise TypeError(f"elevations must be a float or list, got {type(elevations)}")
if parameter not in ["windspeed", "winddirection", "temperature", "pressure"]:
raise ValueError(f"Invalid parameter: {parameter}")
parameter_list = []
for e in elevations:
parameter_list.append(parameter + "_" + str(e) + "m")
return parameter_list
[docs]
def request_wtk_point_data(
time_interval,
parameter,
lat_lon,
years,
preferred_region="",
tree=None,
unscale=True,
str_decode=True,
hsds=True,
clear_cache=False,
to_pandas=True,
):
"""
Returns data from the WIND Toolkit offshore wind hindcast hosted on
AWS at the specified latitude and longitude point(s), or the closest
available point(s).Visit https://registry.opendata.aws/nrel-pds-wtk/
for more information about the dataset and available locations and years.
Calls with multiple parameters must have the same time interval. Calls
with multiple locations must use the same region (use the plot_region function).
Note: To access the WIND Toolkit hindcast data, you will need to
configure h5pyd for data access on HSDS. Please see the
metocean_example or WPTO_hindcast_example notebook for more information.
Parameters
----------
time_interval : string
Data set type of interest
Options: '1-hour' '5-minute'
parameter : string or list of strings
Dataset parameter to be downloaded. Other parameters may be available.
This list is limited to those available at both 5-minute and 1-hour
time intervals for all regions.
Options:
'precipitationrate_0m', 'inversemoninobukhovlength_2m',
'relativehumidity_2m', 'surface_sea_temperature',
'pressure_0m', 'pressure_100m', 'pressure_200m',
'temperature_10m', 'temperature_20m', 'temperature_40m',
'temperature_60m', 'temperature_80m', 'temperature_100m',
'temperature_120m', 'temperature_140m', 'temperature_160m',
'temperature_180m', 'temperature_200m',
'winddirection_10m', 'winddirection_20m', 'winddirection_40m',
'winddirection_60m', 'winddirection_80m', 'winddirection_100m',
'winddirection_120m', 'winddirection_140m', 'winddirection_160m',
'winddirection_180m', 'winddirection_200m',
'windspeed_10m', 'windspeed_20m', 'windspeed_40m',
'windspeed_60m', 'windspeed_80m', 'windspeed_100m',
'windspeed_120m', 'windspeed_140m', 'windspeed_160m',
'windspeed_180m', 'windspeed_200m'
lat_lon : tuple or list of tuples
Latitude longitude pairs at which to extract data. Use plot_region() or
region_selection() to see the corresponding region for a given location.
years : list
Year(s) to be accessed. The years 2000-2019 available (up to 2020
for Mid-Atlantic). Examples: [2015] or [2004,2006,2007]
preferred_region : string (optional)
Region that the lat_lon belongs to ('Offshore_CA' or 'NW_Pacific').
Required when a lat_lon point falls in both the Offshore California
and NW Pacific regions. Overlap region defined by
latitude = (41.213, 42.642) and longitude = (-129.090, -121.672).
Default = ''
tree : str | cKDTree (optional)
cKDTree or path to .pkl file containing pre-computed tree
of lat, lon coordinates, default = None
unscale : bool (optional)
Boolean flag to automatically unscale variables on extraction
Default = True
str_decode : bool (optional)
Boolean flag to decode the bytestring meta data into normal
strings. Setting this to False will speed up the meta data read.
Default = True
hsds : bool (optional)
Boolean flag to use h5pyd to handle .h5 'files' hosted on AWS
behind HSDS. Setting to False will indicate to look for files on
local machine, not AWS. Default = True
clear_cache : bool (optional)
Boolean flag to clear the cache related to this specific request.
Default is False.
to_pandas: bool (optional)
Flag to output pandas instead of xarray. Default = True.
Returns
---------
data: DataFrame
Data indexed by datetime with columns named for parameter and
cooresponding metadata index
meta: DataFrame
Location metadata for the requested data location
"""
if not isinstance(parameter, (str, list)):
raise TypeError("parameter must be of type string or list")
if not isinstance(lat_lon, (list, tuple)):
raise TypeError("lat_lon must be of type list or tuple")
if not isinstance(time_interval, str):
raise TypeError("time_interval must be a string")
if not isinstance(years, list):
raise TypeError("years must be a list")
if not isinstance(preferred_region, str):
raise TypeError("preferred_region must be a string")
if not isinstance(tree, (str, type(None))):
raise TypeError("tree must be a string or None")
if not isinstance(unscale, bool):
raise TypeError("unscale must be bool type")
if not isinstance(str_decode, bool):
raise TypeError("str_decode must be bool type")
if not isinstance(hsds, bool):
raise TypeError("hsds must be bool type")
if not isinstance(clear_cache, bool):
raise TypeError("clear_cache must be of type bool")
# Define the path to the cache directory
cache_dir = os.path.join(os.path.expanduser("~"), ".cache", "mhkit", "hindcast")
# Construct a string representation of the function parameters
hash_params = f"{time_interval}_{parameter}_{lat_lon}_{years}_{preferred_region}_{tree}_{unscale}_{str_decode}_{hsds}"
# Use handle_caching to manage caching.
data, meta, _ = handle_caching(
hash_params,
cache_dir,
cache_content={"data": None, "metadata": None, "write_json": None},
clear_cache_file=clear_cache,
)
if data is not None and meta is not None:
if not to_pandas:
data = convert_to_dataset(data)
data.attrs = meta
return data, meta # Return cached data and meta if available
else:
# check for multiple region selection
if isinstance(lat_lon[0], float):
region = region_selection(lat_lon, preferred_region)
else:
reglist = []
for loc in lat_lon:
reglist.append(region_selection(loc, preferred_region))
if reglist.count(reglist[0]) == len(lat_lon):
region = reglist[0]
else:
raise TypeError("Coordinates must be within the same region!")
if time_interval == "1-hour":
wind_path = f"/nrel/wtk/{region.lower()}/{region}_*.h5"
elif time_interval == "5-minute":
wind_path = f"/nrel/wtk/{region.lower()}-5min/{region}_*.h5"
else:
raise TypeError(
f"Invalid time_interval '{time_interval}', must be '1-hour' or '5-minute'"
)
windKwargs = {
"tree": tree,
"unscale": unscale,
"str_decode": str_decode,
"hsds": hsds,
"years": years,
}
data_list = []
with MultiYearWindX(wind_path, **windKwargs) as rex_wind:
if isinstance(parameter, list):
for p in parameter:
temp_data = rex_wind.get_lat_lon_df(p, lat_lon)
col = temp_data.columns[:]
for i, c in zip(range(len(col)), col):
temp = f"{p}_{i}"
temp_data = temp_data.rename(columns={c: temp})
data_list.append(temp_data)
data = pd.concat(data_list, axis=1)
else:
data = rex_wind.get_lat_lon_df(parameter, lat_lon)
col = data.columns[:]
for i, c in zip(range(len(col)), col):
temp = f"{parameter}_{i}"
data = data.rename(columns={c: temp})
meta = rex_wind.meta.loc[col, :]
meta = meta.reset_index(drop=True)
# Save the retrieved data and metadata to cache.
handle_caching(
hash_params,
cache_dir,
cache_content={"data": data, "metadata": meta, "write_json": None},
)
if not to_pandas:
data = convert_to_dataset(data)
data.attrs = meta
return data, meta