Source code for foxes.output.rose_plot
import numpy as np
import pandas as pd
import foxes.variables as FV
import foxes.constants as FC
from foxes.utils import wd2uv, uv2wd, TabWindroseAxes
from foxes.algorithms import Downwind
from foxes.core import WindFarm, Turbine
from foxes.models import ModelBook
from .output import Output
[docs]class RosePlotOutput(Output):
"""
Class for rose plot creation
Attributes
----------
results: pandas.DataFrame
The calculation results (farm or points)
:group: output
"""
[docs] def __init__(self, results):
"""
Constructor.
Parameters
----------
results: xarray.Dataset
The calculation results (farm or points)
"""
dims = list(results.dims.keys())
if dims[1] == FC.TURBINE:
self._rtype = FC.TURBINE
elif dims[1] == FC.POINT:
self._rtype = FC.POINT
else:
raise KeyError(
f"Results dimension 1 is neither '{FC.TURBINE}' nor '{FC.POINT}': dims = {results.dims}"
)
self.results = results.to_dataframe()
[docs] @classmethod
def get_data_info(cls, dname):
"""
Returns default description for a variable.
Parameters
----------
dname: str
The variable name
Returns
-------
title: str
The long name of the variable
legend: str
The legend/axis text
"""
if dname == FV.D:
return "Rotor diameter", f"{FV.D} [m]"
if dname == FV.H:
return "Hub height", f"{FV.H} [m]"
if dname == FV.WS:
return "Wind speed", f"{FV.WS} [m/s]"
if dname == FV.REWS:
return "Rotor equivalent wind speed", f"{FV.REWS} [m/s]"
if dname == FV.REWS2:
return "Rotor equivalent wind speed (2nd moment)", f"{FV.REWS2} [m/s]"
if dname == FV.REWS3:
return "Rotor equivalent wind speed (3rd moment)", f"{FV.REWS3} [m/s]"
if dname == FV.WD:
return "Wind direction", f"{FV.WD} [deg]"
if dname == FV.TI:
return "Turbulence intensity", f"{FV.TI} [1]"
if dname == FV.RHO:
return "Air density", f"{FV.RHO} [kg/m3]"
if dname == FV.CT:
return "Thrust coefficient", f"{FV.CT} [1]"
if dname == FV.P:
return "Power", f"{FV.P} [kW]"
if dname == FV.YAW:
return "Yaw angle", f"{FV.YAW} [deg]"
if dname == FV.YAWM:
return "Yaw misalignment", f"{FV.YAWM} [deg]"
if dname in FV.amb2var:
title, legend = cls.get_data_info(FV.amb2var[dname])
return f"Ambient {title.lower()}", f"AMB_{legend}"
return dname, dname
[docs] def get_data(
self,
sectors,
var,
var_bins,
wd_var=FV.AMB_WD,
turbine=None,
point=None,
start0=False,
):
"""
Get pandas DataFrame with wind rose data.
Parameters
----------
sectors: int
The number of wind direction sectors
var: str
The data variable name
var_bins: list of float
The variable bin seperation values
wd_var: str, optional
The wind direction variable name
turbine: int, optional
Only relevant in case of farm results.
If None, mean over all turbines.
Else, data from a single turbine
point: int, optional
Only relevant in case of point results.
If None, mean over all points.
Else, data from a single point
start0: bool
Flag for starting the first sector at
zero degrees instead of minus half width
Returns
-------
pd.DataFrame:
The wind rose data
"""
dwd = 360.0 / sectors
wds = np.arange(0.0, 360.0, dwd)
wdb = np.append(wds, 360) if start0 else np.arange(-dwd / 2, 360.0, dwd)
lgd = f"interval_{var}"
data = self.results[[wd_var, FV.WEIGHT]].copy()
data[lgd] = self.results[var]
uv = wd2uv(data[wd_var].to_numpy())
data["u"] = uv[:, 0]
data["v"] = uv[:, 1]
data[FV.WEIGHT] *= 100
data = data.rename(columns={FV.WEIGHT: "frequency"})
el = turbine if self._rtype == FC.TURBINE else point
if el is None:
data = data.groupby(level=0).mean()
else:
sname = data.index.names[0]
grp = data.reset_index().groupby(self._rtype)
data = grp.get_group(el).set_index(sname)
data["wd"] = uv2wd(data[["u", "v"]].to_numpy())
data.drop(["u", "v"], axis=1, inplace=True)
if not start0:
data.loc[data["wd"] > 360.0 - dwd / 2, "wd"] -= 360.0
data[wd_var] = pd.cut(data["wd"], wdb, labels=wds)
data[lgd] = pd.cut(data[lgd], var_bins, right=False, include_lowest=True)
grp = data[[wd_var, lgd, "frequency"]].groupby([wd_var, lgd])
data = grp.sum().reset_index()
data[wd_var] = data[wd_var].astype(np.float64)
data[lgd] = list(data[lgd])
if start0:
data[wd_var] += dwd / 2
ii = pd.IntervalIndex(data[lgd])
data[var] = ii.mid
data[f"bin_min_{var}"] = ii.left
data[f"bin_max_{var}"] = ii.right
data[f"bin_min_{wd_var}"] = data[wd_var] - dwd / 2
data[f"bin_max_{wd_var}"] = data[wd_var] + dwd / 2
data["sector"] = (data[wd_var] / dwd).astype(int)
data = data[
[
wd_var,
var,
"sector",
f"bin_min_{wd_var}",
f"bin_max_{wd_var}",
f"bin_min_{var}",
f"bin_max_{var}",
lgd,
"frequency",
]
]
data.index.name = "bin"
return data
[docs] def get_figure(
self,
sectors,
var,
var_bins,
wd_var=FV.AMB_WD,
turbine=None,
point=None,
title=None,
legend=None,
design="bar",
start0=False,
fig=None,
figsize=None,
rect=None,
ret_data=False,
**kwargs,
):
"""
Creates figure object
Parameters
----------
sectors: int
The number of wind direction sectors
var: str
The data variable name
var_bins: list of float
The variable bin seperation values
wd_var: str, optional
The wind direction variable name
turbine: int, optional
Only relevant in case of farm results.
If None, mean over all turbines.
Else, data from a single turbine
point: int, optional
Only relevant in case of point results.
If None, mean over all points.
Else, data from a single point
title. str, optional
The title
legend: str, optional
The data legend string
design: str
The wind rose design: bar, contour, ...
start0: bool
Flag for starting the first sector at
zero degrees instead of minus half width
fig: matplotlib.Figure
The figure to which to add an axis
figsize: tuple, optional
The figsize of the newly created figure
rect: list, optional
The rectangle of the figure which to fill,
e.g. [0.1, 0.1, 0.8, 0.8]
ret_data: bool
Flag for returning wind rose data
kwargs: dict, optional
Additional arguments for TabWindroseAxes
plot function
Returns
-------
fig: matplotlib.pyplot.Figure
The rose plot figure
data: pd.DataFrame, optional
The wind rose data
"""
lg = legend
if title is None or legend is None:
ttl, lg = self.get_data_info(var)
if title is not None:
ttl = title
wrdata = self.get_data(
sectors=sectors,
var=var,
var_bins=var_bins,
wd_var=wd_var,
turbine=turbine,
point=point,
start0=start0,
)
ax = TabWindroseAxes.from_ax(fig=fig, rect=rect, figsize=figsize)
fig = ax.get_figure()
plfun = getattr(ax, design)
plfun(
direction=wrdata[wd_var].to_numpy(),
var=wrdata[var].to_numpy(),
weights=wrdata["frequency"].to_numpy(),
bin_min_dir=np.sort(wrdata[f"bin_min_{wd_var}"].unique()),
bin_min_var=np.sort(wrdata[f"bin_min_{var}"].unique()),
bin_max_var=np.sort(wrdata[f"bin_max_{var}"].unique()),
**kwargs,
)
ax.set_legend(title=lg)
ax.set_title(ttl)
if ret_data:
return fig, wrdata
else:
return fig
[docs] def write_figure(
self,
file_name,
sectors,
var,
var_bins,
ret_data=False,
**kwargs,
):
"""
Write rose plot to file
Parameters
----------
file_name: str
Path to the output file
sectors: int
The number of wind direction sectors
var: str
The data variable name
var_bins: list of float
The variable bin seperation values
ret_data: bool
Flag for returning wind rose data
kwargs: dict, optional
Additional parameters for get_figure()
Returns
-------
data: pd.DataFrame, optional
The wind rose data
"""
r = self.get_figure(
sectors=sectors,
var=var,
var_bins=var_bins,
ret_data=ret_data,
**kwargs,
)
if ret_data:
r[0].write_image(file_name)
return r[1]
else:
r.write_image(file_name)
[docs]class StatesRosePlotOutput(RosePlotOutput):
"""
Class for rose plot creation directly from states
Parameters
----------
states: foxes.core.States
The states from which to compute the wind rose
point: numpy.ndarray
The evaluation point, shape: (3,)
mbook: foxes.models.ModelBook, optional
The model book
:group: output
"""
[docs] def __init__(self, states, point, mbook=None, ws_var=FV.AMB_REWS):
farm = WindFarm()
farm.add_turbine(
Turbine(
xy=point[:2],
H=point[2],
turbine_models=["null_type"],
),
verbosity=0,
)
mbook = mbook if mbook is not None else ModelBook()
algo = Downwind(mbook, farm, states, wake_models=[], verbosity=0)
results = algo.calc_farm(ambient=True).rename_vars({ws_var: FV.AMB_WS})
super().__init__(results)