Source code for foxes.output.rose_plot

import numpy as np
import matplotlib.pyplot as plt
from xarray import Dataset
from matplotlib.cm import ScalarMappable
from matplotlib.projections.polar import PolarAxes
from matplotlib.lines import Line2D

from foxes.algorithms import Downwind
from foxes.core import WindFarm, Turbine
from foxes.models import ModelBook
import foxes.variables as FV
import foxes.constants as FC

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, farm_results=None, point_results=None, use_points=False, **kwargs, ): """ Constructor. Parameters ---------- farm_results: xarray.Dataset, optional The farm results point_results: xarray.Dataset, optional The point results use_points: bool Flag for using points in cases where both farm and point results are given kwargs: dict, optional Additional parameters for the base class """ super().__init__(**kwargs) if use_points or (farm_results is None and point_results is not None): self.results = point_results self._rtype = FC.POINT elif farm_results is not None: self.results = farm_results self._rtype = FC.TURBINE else: raise KeyError(f"Require either farm_results or point_results")
[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, wd_sectors, ws_var, ws_bins, wd_var=FV.AMB_WD, turbine=0, point=0, add_inf=False, ): """ Generates the plot data Parameters ---------- wd_sectors: int The number of wind rose sectors ws_var: str The wind speed variable ws_bins: list of float The wind speed bins wd_var: str The wind direction variable turbine: int The turbine index, for weights and for data if farm_results are given point: int The point index, for data if point_results are given add_inf: bool Add an upper bin up to infinity Returns ------- data: xarray.Dataset The plot data """ if add_inf: ws_bins = list(ws_bins) + [np.inf] w = self.results[FV.WEIGHT].to_numpy()[:, turbine] t = turbine if self._rtype == FC.TURBINE else point ws = self.results[ws_var].to_numpy()[:, t] wd = self.results[wd_var].to_numpy()[:, t].copy() wd_delta = 360 / wd_sectors wd[wd >= 360 - wd_delta / 2] -= 360 wd_bins = np.arange(-wd_delta / 2, 360, wd_delta) ws_bins = np.asarray(ws_bins, dtype=ws.dtype) freq = 100 * np.histogram2d(wd, ws, (wd_bins, ws_bins), weights=w)[0] data = Dataset( coords={ wd_var: np.arange(0, 360, wd_delta), ws_var: 0.5 * (ws_bins[:-1] + ws_bins[1:]), }, data_vars={ f"bin_min_{wd_var}": (wd_var, wd_bins[:-1]), f"bin_max_{wd_var}": (wd_var, wd_bins[1:]), f"bin_min_{ws_var}": (ws_var, ws_bins[:-1]), f"bin_max_{ws_var}": (ws_var, ws_bins[1:]), "frequency": ((wd_var, ws_var), freq), }, attrs={ f"{wd_var}_bounds": wd_bins, f"{ws_var}_bounds": ws_bins, }, ) return data
[docs] def get_figure( self, wd_sectors, ws_var, ws_bins, wd_var=FV.AMB_WD, fig=None, ax=None, figsize=None, freq_delta=3, cmap="summer", title=None, legend_pars=None, ret_data=False, **kwargs, ): """ Creates the figure Parameters ---------- wd_sectors: int The number of wind rose sectors ws_var: str The wind speed variable ws_bins: list of float The wind speed bins wd_var: str The wind direction variable fig: pyplot.Figure, optional The figure object ax: pyplot.Axes, optional The axes object figsize: tuple, optional The figsize argument for plt.subplots freq_delta: int The frequency delta for the label in percent cmap: str The color map title: str, optional The title legend_pars: dict, optional Parameters for the legend ret_data: bool Flag for returning wind rose data kwargs: dict, optional Additional parameters for get_data Returns ------- ax: pyplot.Axes The axes object data: xarray.Dataset, optional The plot data """ data = self.get_data(wd_sectors, ws_var, ws_bins, wd_var, **kwargs) n_wsb = data.sizes[ws_var] n_wdb = data.sizes[wd_var] ws_bins = np.asarray(data.attrs[f"{ws_var}_bounds"]) wd_cent = np.mod(90 - data[wd_var].to_numpy(), 360) wd_cent = np.radians(wd_cent) wd_delta = 360 / n_wdb wd_width = np.radians(0.9 * wd_delta) freq = data["frequency"].to_numpy() if ax is not None: if not isinstance(ax, PolarAxes): raise TypeError( f"Require axes of type '{PolarAxes.__name__}' for '{type(self).__name__}', got '{type(ax).__name__}'" ) else: fig, ax = plt.subplots(figsize=figsize, subplot_kw={"projection": "polar"}) bcmap = plt.get_cmap(cmap, n_wsb) color_list = bcmap(np.linspace(0, 1, n_wsb)) bottom = np.zeros(n_wdb) for wsi in range(n_wsb): ax.bar( wd_cent, freq[:, wsi], bottom=bottom, width=wd_width, color=color_list[wsi], ) bottom += freq[:, wsi] fmax = np.max(np.sum(freq, axis=1)) freq_delta = int(freq_delta) freq_ticks = np.arange(0, fmax + freq_delta / 2, freq_delta, dtype=np.int32)[1:] tksl = np.arange(0, 360, max(wd_delta, 30)) tks = np.radians(np.mod(90 - tksl, 360)) ax.set_xticks(tks, [f"{int(d)}°" for d in tksl]) ax.set_yticks(freq_ticks, [f"{f}%" for f in freq_ticks]) ax.set_title(title) llines = [Line2D([0], [0], color=c, lw=10) for c in np.flip(color_list, axis=0)] lleg = [ f"[{ws_bins[i]:.1f}, {ws_bins[i+1]:.1f})" for i in range(n_wsb - 1, -1, -1) ] lpars = dict( loc="upper left", bbox_to_anchor=(0.8, 0.5), title=f"{ws_var}", ) wsl = [FV.WS, FV.REWS, FV.REWS2, FV.REWS3] wsl += [FV.var2amb[v] for v in wsl] if ws_var in wsl: lpars["title"] += " [m/s]" if legend_pars is not None: lpars.update(legend_pars) ax.legend(llines, lleg, **lpars) if ret_data: return ax, data else: return ax
[docs] def write_figure(self, file_name, *args, ret_data=False, **kwargs): """ Write rose plot to file Parameters ---------- file_name: str Name of the output file args: tuple, optional Additional parameters for get_figure 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(*args, ret_data=ret_data, **kwargs) fpath = self.get_fpath(file_name) if ret_data: r[0].get_figure().savefig(fpath, bbox_inches="tight") return r[1] else: r.get_figure().savefig(fpath, bbox_inches="tight")
[docs] class StatesRosePlotOutput(RosePlotOutput): """ Class for rose plot creation directly from states :group: output """
[docs] def __init__( self, states, point, mbook=None, ws_var=FV.AMB_REWS, **kwargs, ): """ Constructor. 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 ws_var: str The wind speed variable name kwargs: dict, optional Additional parameters for the base class """ 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(farm, states, wake_models=[], mbook=mbook, verbosity=0) results = algo.calc_farm(ambient=True).rename_vars({ws_var: FV.AMB_WS}) super().__init__(results, **kwargs)
[docs] class WindRoseBinPlot(Output): """ Plots mean data in wind rose bins Attributes ---------- farm_results: xarray.Dataset The wind farm results :group: output """
[docs] def __init__(self, farm_results, **kwargs): """ Constructor Parameters ---------- farm_results: xarray.Dataset The wind farm results kwargs: dict, optional Parameters for the base class """ super().__init__(**kwargs) self.farm_results = farm_results
[docs] def get_data( self, variable, ws_bins, wd_sectors=12, wd_var=FV.AMB_WD, ws_var=FV.AMB_REWS, turbine=0, contraction="weights", ): """ Generates the plot data Parameters ---------- variable: str The variable name ws_bins: list of float The wind speed bins wd_var: str The wind direction variable ws_var: str The wind speed variable turbine: int The turbine index contraction: str The contraction method for states: weights, mean_no_weights, sum_no_weights Returns ------- data: xarray.Dataset The plot data """ var = self.farm_results[variable].to_numpy()[:, turbine] w = self.farm_results[FV.WEIGHT].to_numpy()[:, turbine] ws = self.farm_results[ws_var].to_numpy()[:, turbine] wd = self.farm_results[wd_var].to_numpy()[:, turbine].copy() wd_delta = 360 / wd_sectors wd[wd >= 360 - wd_delta / 2] -= 360 wd_bins = np.arange(-wd_delta / 2, 360, wd_delta) ws_bins = np.asarray(ws_bins) if contraction == "weights": z = np.histogram2d(wd, ws, (wd_bins, ws_bins), weights=w)[0] z[z < 1e-13] = np.nan z = np.histogram2d(wd, ws, (wd_bins, ws_bins), weights=w * var)[0] / z elif contraction == "mean_no_weights": z = np.histogram2d(wd, ws, (wd_bins, ws_bins))[0].astype(w.dtype) z[z < 1] = np.nan z = np.histogram2d(wd, ws, (wd_bins, ws_bins), weights=var)[0] / z elif contraction == "sum_no_weights": z = np.histogram2d(wd, ws, (wd_bins, ws_bins), weights=var)[0] else: raise KeyError( f"Contraction '{contraction}' not supported. Choices: weights, mean_no_weights, sum_no_weights" ) data = Dataset( coords={ wd_var: 0.5 * (wd_bins[:-1] + wd_bins[1:]), ws_var: 0.5 * (ws_bins[:-1] + ws_bins[1:]), }, data_vars={ variable: ((wd_var, ws_var), z), }, attrs={ f"{wd_var}_bounds": wd_bins, f"{ws_var}_bounds": ws_bins, }, ) return data
[docs] def get_figure( self, variable, ws_bins, wd_sectors=12, wd_var=FV.AMB_WD, ws_var=FV.AMB_REWS, turbine=0, contraction="weights", fig=None, ax=None, title=None, figsize=None, ret_data=False, **kwargs, ): """ Creates the figure Parameters ---------- variable: str The variable name ws_bins: list of float The wind speed bins wd_var: str The wind direction variable ws_var: str The wind speed variable turbine: int The turbine index contraction: str The contraction method for states: weights, mean_no_weights, sum_no_weights fig: pyplot.Figure, optional The figure object ax: pyplot.Axes, optional The axes object title: str, optional The title figsize: tuple, optional The figsize argument for plt.subplots ret_data: bool Flag for returning wind rose data kwargs: dict, optional Additional parameters for plt.pcolormesh Returns ------- ax: pyplot.Axes The axes object """ data = self.get_data( variable=variable, ws_bins=ws_bins, wd_sectors=wd_sectors, wd_var=wd_var, ws_var=ws_var, turbine=turbine, contraction=contraction, ) wd_delta = 360 / data.sizes[wd_var] wd_bins = np.mod(90 - data.attrs[f"{wd_var}_bounds"], 360) wd_bins = np.radians(wd_bins) ws_bins = data.attrs[f"{ws_var}_bounds"] if ax is not None: if not isinstance(ax, PolarAxes): raise TypeError( f"Require axes of type '{PolarAxes.__name__}' for '{type(self).__name__}', got '{type(ax).__name__}'" ) else: fig, ax = plt.subplots(figsize=figsize, subplot_kw={"projection": "polar"}) y, x = np.meshgrid(ws_bins, wd_bins) z = data[variable].to_numpy() prgs = {"shading": "flat"} prgs.update(kwargs) img = ax.pcolormesh(x, y, z, **prgs) tksl = np.arange(0, 360, max(wd_delta, 30)) tks = np.radians(np.mod(90 - tksl, 360)) ax.set_xticks(tks, [f"{d}°" for d in tksl]) ax.set_yticks(ws_bins) ax.set_title(title) cbar = fig.colorbar(img, ax=ax, pad=0.12) cbar.ax.set_title(variable) if ret_data: return ax, data else: return ax
[docs] def write_figure(self, file_name, *args, ret_data=False, **kwargs): """ Write rose plot to file Parameters ---------- file_name: str Name of the output file args: tuple, optional Additional parameters for get_figure 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(*args, ret_data=ret_data, **kwargs) fpath = self.get_fpath(file_name) if ret_data: r[0].get_figure().savefig(fpath, bbox_inches="tight") return r[1] else: r.get_figure().savefig(fpath, bbox_inches="tight")