foxes 1.1.1__py3-none-any.whl → 1.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of foxes might be problematic. Click here for more details.

Files changed (155) hide show
  1. docs/source/conf.py +3 -1
  2. examples/abl_states/run.py +5 -5
  3. examples/dyn_wakes/run.py +2 -2
  4. examples/induction/run.py +5 -5
  5. examples/random_timeseries/run.py +13 -13
  6. examples/scan_row/run.py +12 -7
  7. examples/sector_management/run.py +11 -7
  8. examples/single_state/run.py +5 -5
  9. examples/tab_file/run.py +1 -1
  10. examples/timelines/run.py +1 -1
  11. examples/timeseries/run.py +5 -5
  12. examples/timeseries_slurm/run.py +5 -5
  13. examples/wind_rose/run.py +1 -1
  14. examples/yawed_wake/run.py +5 -5
  15. foxes/__init__.py +13 -2
  16. foxes/algorithms/downwind/downwind.py +21 -6
  17. foxes/algorithms/downwind/models/init_farm_data.py +5 -2
  18. foxes/algorithms/downwind/models/point_wakes_calc.py +0 -1
  19. foxes/algorithms/iterative/iterative.py +1 -1
  20. foxes/algorithms/sequential/sequential.py +5 -4
  21. foxes/config/__init__.py +1 -0
  22. foxes/config/config.py +134 -0
  23. foxes/constants.py +15 -6
  24. foxes/core/algorithm.py +46 -30
  25. foxes/core/axial_induction_model.py +18 -0
  26. foxes/core/data.py +2 -1
  27. foxes/core/engine.py +43 -49
  28. foxes/core/farm_controller.py +22 -3
  29. foxes/core/farm_data_model.py +6 -2
  30. foxes/core/ground_model.py +19 -0
  31. foxes/core/model.py +2 -1
  32. foxes/core/partial_wakes_model.py +9 -21
  33. foxes/core/point_data_model.py +22 -2
  34. foxes/core/rotor_model.py +9 -21
  35. foxes/core/states.py +2 -17
  36. foxes/core/turbine_model.py +2 -18
  37. foxes/core/turbine_type.py +2 -18
  38. foxes/core/vertical_profile.py +8 -20
  39. foxes/core/wake_frame.py +9 -25
  40. foxes/core/wake_model.py +24 -20
  41. foxes/core/wake_superposition.py +19 -0
  42. foxes/data/__init__.py +1 -1
  43. foxes/data/static_data.py +0 -7
  44. foxes/engines/dask.py +4 -3
  45. foxes/engines/single.py +1 -1
  46. foxes/input/__init__.py +1 -1
  47. foxes/input/farm_layout/from_csv.py +3 -1
  48. foxes/input/farm_layout/from_file.py +10 -10
  49. foxes/input/farm_layout/from_json.py +4 -3
  50. foxes/input/farm_layout/grid.py +3 -3
  51. foxes/input/states/__init__.py +1 -1
  52. foxes/input/states/create/random_abl_states.py +5 -3
  53. foxes/input/states/field_data_nc.py +36 -15
  54. foxes/input/states/multi_height.py +26 -15
  55. foxes/input/states/one_point_flow.py +6 -5
  56. foxes/input/states/{scan_ws.py → scan.py} +42 -52
  57. foxes/input/states/single.py +15 -6
  58. foxes/input/states/slice_data_nc.py +18 -12
  59. foxes/input/states/states_table.py +17 -10
  60. foxes/input/yaml/__init__.py +3 -0
  61. foxes/input/yaml/dict.py +381 -0
  62. foxes/input/yaml/windio/__init__.py +4 -0
  63. foxes/input/{windio → yaml/windio}/get_states.py +7 -7
  64. foxes/input/{windio → yaml/windio}/read_attributes.py +61 -40
  65. foxes/input/{windio → yaml/windio}/read_farm.py +34 -43
  66. foxes/input/{windio → yaml/windio}/read_fields.py +11 -10
  67. foxes/input/yaml/windio/read_outputs.py +147 -0
  68. foxes/input/yaml/windio/windio.py +269 -0
  69. foxes/input/yaml/yaml.py +103 -0
  70. foxes/models/partial_wakes/axiwake.py +7 -6
  71. foxes/models/partial_wakes/centre.py +3 -2
  72. foxes/models/partial_wakes/segregated.py +5 -2
  73. foxes/models/point_models/set_uniform_data.py +5 -3
  74. foxes/models/rotor_models/centre.py +2 -2
  75. foxes/models/rotor_models/grid.py +5 -5
  76. foxes/models/rotor_models/levels.py +6 -6
  77. foxes/models/turbine_models/kTI_model.py +3 -1
  78. foxes/models/turbine_models/lookup_table.py +7 -4
  79. foxes/models/turbine_models/power_mask.py +14 -8
  80. foxes/models/turbine_models/sector_management.py +4 -2
  81. foxes/models/turbine_models/set_farm_vars.py +53 -23
  82. foxes/models/turbine_models/table_factors.py +8 -7
  83. foxes/models/turbine_models/yaw2yawm.py +0 -1
  84. foxes/models/turbine_models/yawm2yaw.py +0 -1
  85. foxes/models/turbine_types/CpCt_file.py +6 -3
  86. foxes/models/turbine_types/CpCt_from_two.py +6 -3
  87. foxes/models/turbine_types/PCt_file.py +7 -6
  88. foxes/models/turbine_types/PCt_from_two.py +11 -2
  89. foxes/models/turbine_types/TBL_file.py +3 -4
  90. foxes/models/turbine_types/wsrho2PCt_from_two.py +19 -11
  91. foxes/models/turbine_types/wsti2PCt_from_two.py +19 -11
  92. foxes/models/vertical_profiles/abl_log_neutral_ws.py +1 -1
  93. foxes/models/vertical_profiles/abl_log_stable_ws.py +1 -1
  94. foxes/models/vertical_profiles/abl_log_unstable_ws.py +1 -1
  95. foxes/models/vertical_profiles/abl_log_ws.py +1 -1
  96. foxes/models/wake_frames/dynamic_wakes.py +17 -9
  97. foxes/models/wake_frames/farm_order.py +4 -3
  98. foxes/models/wake_frames/rotor_wd.py +3 -1
  99. foxes/models/wake_frames/seq_dynamic_wakes.py +14 -7
  100. foxes/models/wake_frames/streamlines.py +9 -6
  101. foxes/models/wake_frames/timelines.py +21 -14
  102. foxes/models/wake_frames/yawed_wakes.py +3 -1
  103. foxes/models/wake_models/induction/vortex_sheet.py +0 -1
  104. foxes/models/wake_models/ti/crespo_hernandez.py +2 -1
  105. foxes/models/wake_models/wind/bastankhah14.py +3 -2
  106. foxes/models/wake_models/wind/bastankhah16.py +2 -1
  107. foxes/models/wake_models/wind/turbopark.py +9 -7
  108. foxes/models/wake_superpositions/ws_product.py +0 -1
  109. foxes/output/__init__.py +2 -1
  110. foxes/output/calc_points.py +7 -4
  111. foxes/output/farm_layout.py +30 -18
  112. foxes/output/farm_results_eval.py +61 -38
  113. foxes/output/grids.py +8 -7
  114. foxes/output/output.py +9 -20
  115. foxes/output/plt.py +19 -0
  116. foxes/output/results_writer.py +10 -11
  117. foxes/output/rose_plot.py +448 -224
  118. foxes/output/rotor_point_plots.py +7 -3
  119. foxes/output/slice_data.py +1 -1
  120. foxes/output/state_turbine_map.py +5 -1
  121. foxes/output/state_turbine_table.py +7 -3
  122. foxes/output/turbine_type_curves.py +7 -2
  123. foxes/utils/__init__.py +1 -2
  124. foxes/utils/dict.py +107 -3
  125. foxes/utils/geopandas_utils.py +3 -2
  126. foxes/utils/subclasses.py +69 -0
  127. {foxes-1.1.1.dist-info → foxes-1.2.1.dist-info}/METADATA +18 -18
  128. {foxes-1.1.1.dist-info → foxes-1.2.1.dist-info}/RECORD +145 -145
  129. {foxes-1.1.1.dist-info → foxes-1.2.1.dist-info}/WHEEL +1 -1
  130. foxes-1.2.1.dist-info/entry_points.txt +3 -0
  131. tests/0_consistency/iterative/test_iterative.py +65 -67
  132. tests/0_consistency/partial_wakes/test_partial_wakes.py +58 -61
  133. tests/1_verification/flappy_0_6/PCt_files/test_PCt_files.py +56 -53
  134. tests/1_verification/flappy_0_6/abl_states/test_abl_states.py +41 -41
  135. tests/1_verification/flappy_0_6/partial_top_hat/test_partial_top_hat.py +34 -34
  136. tests/1_verification/flappy_0_6/row_Jensen_linear_centre/test_row_Jensen_linear_centre.py +57 -52
  137. tests/1_verification/flappy_0_6/row_Jensen_linear_tophat/test_row_Jensen_linear_tophat.py +58 -54
  138. tests/1_verification/flappy_0_6/row_Jensen_linear_tophat_IECTI2005/test_row_Jensen_linear_tophat_IECTI_2005.py +80 -76
  139. tests/1_verification/flappy_0_6/row_Jensen_linear_tophat_IECTI2019/test_row_Jensen_linear_tophat_IECTI_2019.py +80 -76
  140. tests/1_verification/flappy_0_6/row_Jensen_quadratic_centre/test_row_Jensen_quadratic_centre.py +58 -51
  141. tests/1_verification/flappy_0_6_2/grid_rotors/test_grid_rotors.py +101 -103
  142. tests/1_verification/flappy_0_6_2/row_Bastankhah_Crespo/test_row_Bastankhah_Crespo.py +67 -64
  143. tests/1_verification/flappy_0_6_2/row_Bastankhah_linear_centre/test_row_Bastankhah_linear_centre.py +58 -54
  144. examples/windio/run.py +0 -29
  145. foxes/data/states/windio_timeseries_5000.nc +0 -0
  146. foxes/data/windio/DTU_10MW_turbine.yaml +0 -10
  147. foxes/data/windio/__init__.py +0 -0
  148. foxes/data/windio/windio_5turbines_timeseries.yaml +0 -79
  149. foxes/input/windio/__init__.py +0 -11
  150. foxes/input/windio/read_outputs.py +0 -172
  151. foxes/input/windio/runner.py +0 -183
  152. foxes/input/windio/windio.py +0 -193
  153. foxes/utils/windrose_plot.py +0 -152
  154. {foxes-1.1.1.dist-info → foxes-1.2.1.dist-info}/LICENSE +0 -0
  155. {foxes-1.1.1.dist-info → foxes-1.2.1.dist-info}/top_level.txt +0 -0
foxes/output/rose_plot.py CHANGED
@@ -1,12 +1,16 @@
1
1
  import numpy as np
2
- import pandas as pd
2
+ import matplotlib.pyplot as plt
3
+ from xarray import Dataset
4
+ from matplotlib.cm import ScalarMappable
5
+ from matplotlib.projections.polar import PolarAxes
6
+ from matplotlib.lines import Line2D
3
7
 
4
- import foxes.variables as FV
5
- import foxes.constants as FC
6
- from foxes.utils import wd2uv, uv2wd, TabWindroseAxes
7
8
  from foxes.algorithms import Downwind
8
9
  from foxes.core import WindFarm, Turbine
9
10
  from foxes.models import ModelBook
11
+ import foxes.variables as FV
12
+ import foxes.constants as FC
13
+
10
14
  from .output import Output
11
15
 
12
16
 
@@ -23,27 +27,38 @@ class RosePlotOutput(Output):
23
27
 
24
28
  """
25
29
 
26
- def __init__(self, results):
30
+ def __init__(
31
+ self,
32
+ farm_results=None,
33
+ point_results=None,
34
+ use_points=False,
35
+ **kwargs,
36
+ ):
27
37
  """
28
38
  Constructor.
29
39
 
30
40
  Parameters
31
41
  ----------
32
- results: xarray.Dataset
33
- The calculation results (farm or points)
42
+ farm_results: xarray.Dataset, optional
43
+ The farm results
44
+ point_results: xarray.Dataset, optional
45
+ The point results
46
+ use_points: bool
47
+ Flag for using points in cases where both
48
+ farm and point results are given
49
+ kwargs: dict, optional
50
+ Additional parameters for the base class
34
51
 
35
52
  """
36
- dims = list(results.sizes.keys())
37
- if dims[1] == FC.TURBINE:
38
- self._rtype = FC.TURBINE
39
- elif dims[1] == FC.POINT:
53
+ super().__init__(**kwargs)
54
+ if use_points or (farm_results is None and point_results is not None):
55
+ self.results = point_results
40
56
  self._rtype = FC.POINT
57
+ elif farm_results is not None:
58
+ self.results = farm_results
59
+ self._rtype = FC.TURBINE
41
60
  else:
42
- raise KeyError(
43
- f"Results dimension 1 is neither '{FC.TURBINE}' nor '{FC.POINT}': dims = {results.dims}"
44
- )
45
-
46
- self.results = results.to_dataframe()
61
+ raise KeyError(f"Require either farm_results or point_results")
47
62
 
48
63
  @classmethod
49
64
  def get_data_info(cls, dname):
@@ -100,241 +115,211 @@ class RosePlotOutput(Output):
100
115
 
101
116
  def get_data(
102
117
  self,
103
- sectors,
104
- var,
105
- var_bins,
118
+ wd_sectors,
119
+ ws_var,
120
+ ws_bins,
106
121
  wd_var=FV.AMB_WD,
107
- turbine=None,
108
- point=None,
109
- start0=False,
122
+ turbine=0,
123
+ point=0,
124
+ add_inf=False,
110
125
  ):
111
126
  """
112
- Get pandas DataFrame with wind rose data.
127
+ Generates the plot data
113
128
 
114
129
  Parameters
115
130
  ----------
116
- sectors: int
117
- The number of wind direction sectors
118
- var: str
119
- The data variable name
120
- var_bins: list of float
121
- The variable bin separation values
122
- wd_var: str, optional
123
- The wind direction variable name
124
- turbine: int, optional
125
- Only relevant in case of farm results.
126
- If None, mean over all turbines.
127
- Else, data from a single turbine
128
- point: int, optional
129
- Only relevant in case of point results.
130
- If None, mean over all points.
131
- Else, data from a single point
132
- start0: bool
133
- Flag for starting the first sector at
134
- zero degrees instead of minus half width
131
+ wd_sectors: int
132
+ The number of wind rose sectors
133
+ ws_var: str
134
+ The wind speed variable
135
+ ws_bins: list of float
136
+ The wind speed bins
137
+ wd_var: str
138
+ The wind direction variable
139
+ turbine: int
140
+ The turbine index, for weights and for
141
+ data if farm_results are given
142
+ point: int
143
+ The point index, for data if point_results
144
+ are given
145
+ add_inf: bool
146
+ Add an upper bin up to infinity
135
147
 
136
148
  Returns
137
149
  -------
138
- pd.DataFrame:
139
- The wind rose data
150
+ data: xarray.Dataset
151
+ The plot data
140
152
 
141
153
  """
142
-
143
- dwd = 360.0 / sectors
144
- wds = np.arange(0.0, 360.0, dwd)
145
- wdb = np.append(wds, 360) if start0 else np.arange(-dwd / 2, 360.0, dwd)
146
- lgd = f"interval_{var}"
147
-
148
- data = self.results[[wd_var, FV.WEIGHT]].copy()
149
- data[lgd] = self.results[var]
150
- uv = wd2uv(data[wd_var].to_numpy())
151
- data["u"] = uv[:, 0]
152
- data["v"] = uv[:, 1]
153
-
154
- data[FV.WEIGHT] *= 100
155
- data = data.rename(columns={FV.WEIGHT: "frequency"})
156
-
157
- el = turbine if self._rtype == FC.TURBINE else point
158
- if el is None:
159
- data = data.groupby(level=0).mean()
160
- else:
161
- sname = data.index.names[0]
162
- grp = data.reset_index().groupby(self._rtype)
163
- data = grp.get_group(el).set_index(sname)
164
-
165
- data["wd"] = uv2wd(data[["u", "v"]].to_numpy())
166
- data.drop(["u", "v"], axis=1, inplace=True)
167
- if not start0:
168
- data.loc[data["wd"] > 360.0 - dwd / 2, "wd"] -= 360.0
169
-
170
- data[wd_var] = pd.cut(data["wd"], wdb, labels=wds)
171
- data[lgd] = pd.cut(data[lgd], var_bins, right=False, include_lowest=True)
172
-
173
- grp = data[[wd_var, lgd, "frequency"]].groupby([wd_var, lgd], observed=False)
174
- data = grp.sum().reset_index()
175
-
176
- data[wd_var] = data[wd_var].astype(np.float64)
177
- data[lgd] = list(data[lgd])
178
- if start0:
179
- data[wd_var] += dwd / 2
180
-
181
- ii = pd.IntervalIndex(data[lgd])
182
- data[var] = ii.mid
183
- data[f"bin_min_{var}"] = ii.left
184
- data[f"bin_max_{var}"] = ii.right
185
- data[f"bin_min_{wd_var}"] = data[wd_var] - dwd / 2
186
- data[f"bin_max_{wd_var}"] = data[wd_var] + dwd / 2
187
- data["sector"] = (data[wd_var] / dwd).astype(int)
188
-
189
- data = data[
190
- [
191
- wd_var,
192
- var,
193
- "sector",
194
- f"bin_min_{wd_var}",
195
- f"bin_max_{wd_var}",
196
- f"bin_min_{var}",
197
- f"bin_max_{var}",
198
- lgd,
199
- "frequency",
200
- ]
201
- ]
202
- data.index.name = "bin"
154
+ if add_inf:
155
+ ws_bins = list(ws_bins) + [np.inf]
156
+ w = self.results[FV.WEIGHT].to_numpy()[:, turbine]
157
+ t = turbine if self._rtype == FC.TURBINE else point
158
+ ws = self.results[ws_var].to_numpy()[:, t]
159
+ wd = self.results[wd_var].to_numpy()[:, t].copy()
160
+ wd_delta = 360 / wd_sectors
161
+ wd[wd >= 360 - wd_delta / 2] -= 360
162
+ wd_bins = np.arange(-wd_delta / 2, 360, wd_delta)
163
+ ws_bins = np.asarray(ws_bins, dtype=ws.dtype)
164
+
165
+ freq = 100 * np.histogram2d(wd, ws, (wd_bins, ws_bins), weights=w)[0]
166
+
167
+ data = Dataset(
168
+ coords={
169
+ wd_var: np.arange(0, 360, wd_delta),
170
+ ws_var: 0.5 * (ws_bins[:-1] + ws_bins[1:]),
171
+ },
172
+ data_vars={
173
+ f"bin_min_{wd_var}": (wd_var, wd_bins[:-1]),
174
+ f"bin_max_{wd_var}": (wd_var, wd_bins[1:]),
175
+ f"bin_min_{ws_var}": (ws_var, ws_bins[:-1]),
176
+ f"bin_max_{ws_var}": (ws_var, ws_bins[1:]),
177
+ "frequency": ((wd_var, ws_var), freq),
178
+ },
179
+ attrs={
180
+ f"{wd_var}_bounds": wd_bins,
181
+ f"{ws_var}_bounds": ws_bins,
182
+ },
183
+ )
203
184
 
204
185
  return data
205
186
 
206
187
  def get_figure(
207
188
  self,
208
- sectors,
209
- var,
210
- var_bins,
189
+ wd_sectors,
190
+ ws_var,
191
+ ws_bins,
211
192
  wd_var=FV.AMB_WD,
212
- turbine=None,
213
- point=None,
214
- title=None,
215
- legend=None,
216
- design="bar",
217
- start0=False,
218
193
  fig=None,
194
+ ax=None,
219
195
  figsize=None,
220
- rect=None,
196
+ freq_delta=3,
197
+ cmap="summer",
198
+ title=None,
199
+ legend_pars=None,
221
200
  ret_data=False,
222
201
  **kwargs,
223
202
  ):
224
203
  """
225
- Creates figure object
204
+ Creates the figure
226
205
 
227
206
  Parameters
228
207
  ----------
229
- sectors: int
230
- The number of wind direction sectors
231
- var: str
232
- The data variable name
233
- var_bins: list of float
234
- The variable bin separation values
235
- wd_var: str, optional
236
- The wind direction variable name
237
- turbine: int, optional
238
- Only relevant in case of farm results.
239
- If None, mean over all turbines.
240
- Else, data from a single turbine
241
- point: int, optional
242
- Only relevant in case of point results.
243
- If None, mean over all points.
244
- Else, data from a single point
245
- title. str, optional
246
- The title
247
- legend: str, optional
248
- The data legend string
249
- design: str
250
- The wind rose design: bar, contour, ...
251
- start0: bool
252
- Flag for starting the first sector at
253
- zero degrees instead of minus half width
254
- fig: matplotlib.Figure
255
- The figure to which to add an axis
208
+ wd_sectors: int
209
+ The number of wind rose sectors
210
+ ws_var: str
211
+ The wind speed variable
212
+ ws_bins: list of float
213
+ The wind speed bins
214
+ wd_var: str
215
+ The wind direction variable
216
+ fig: pyplot.Figure, optional
217
+ The figure object
218
+ ax: pyplot.Axes, optional
219
+ The axes object
256
220
  figsize: tuple, optional
257
- The figsize of the newly created figure
258
- rect: list, optional
259
- The rectangle of the figure which to fill,
260
- e.g. [0.1, 0.1, 0.8, 0.8]
221
+ The figsize argument for plt.subplots
222
+ freq_delta: int
223
+ The frequency delta for the label
224
+ in percent
225
+ cmap: str
226
+ The color map
227
+ title: str, optional
228
+ The title
229
+ legend_pars: dict, optional
230
+ Parameters for the legend
261
231
  ret_data: bool
262
232
  Flag for returning wind rose data
263
233
  kwargs: dict, optional
264
- Additional arguments for TabWindroseAxes
265
- plot function
234
+ Additional parameters for get_data
266
235
 
267
236
  Returns
268
237
  -------
269
- fig: matplotlib.pyplot.Figure
270
- The rose plot figure
271
- data: pd.DataFrame, optional
272
- The wind rose data
238
+ ax: pyplot.Axes
239
+ The axes object
240
+ data: xarray.Dataset, optional
241
+ The plot data
273
242
 
274
243
  """
275
- lg = legend
276
- if title is None or legend is None:
277
- ttl, lg = self.get_data_info(var)
278
- if title is not None:
279
- ttl = title
280
-
281
- wrdata = self.get_data(
282
- sectors=sectors,
283
- var=var,
284
- var_bins=var_bins,
285
- wd_var=wd_var,
286
- turbine=turbine,
287
- point=point,
288
- start0=start0,
289
- )
244
+ data = self.get_data(wd_sectors, ws_var, ws_bins, wd_var, **kwargs)
245
+
246
+ n_wsb = data.sizes[ws_var]
247
+ n_wdb = data.sizes[wd_var]
248
+ ws_bins = np.asarray(data.attrs[f"{ws_var}_bounds"])
249
+ wd_cent = np.mod(90 - data[wd_var].to_numpy(), 360)
250
+ wd_cent = np.radians(wd_cent)
251
+ wd_delta = 360 / n_wdb
252
+ wd_width = np.radians(0.9 * wd_delta)
253
+ freq = data["frequency"].to_numpy()
254
+
255
+ if ax is not None:
256
+ if not isinstance(ax, PolarAxes):
257
+ raise TypeError(
258
+ f"Require axes of type '{PolarAxes.__name__}' for '{type(self).__name__}', got '{type(ax).__name__}'"
259
+ )
260
+ else:
261
+ fig, ax = plt.subplots(figsize=figsize, subplot_kw={"projection": "polar"})
262
+
263
+ bcmap = plt.get_cmap(cmap, n_wsb)
264
+ color_list = bcmap(np.linspace(0, 1, n_wsb))
265
+
266
+ bottom = np.zeros(n_wdb)
267
+ for wsi in range(n_wsb):
268
+ ax.bar(
269
+ wd_cent,
270
+ freq[:, wsi],
271
+ bottom=bottom,
272
+ width=wd_width,
273
+ color=color_list[wsi],
274
+ )
275
+ bottom += freq[:, wsi]
276
+
277
+ fmax = np.max(np.sum(freq, axis=1))
278
+ freq_delta = int(freq_delta)
279
+ freq_ticks = np.arange(0, fmax + freq_delta / 2, freq_delta, dtype=np.int32)[1:]
280
+
281
+ tksl = np.arange(0, 360, max(wd_delta, 30))
282
+ tks = np.radians(np.mod(90 - tksl, 360))
283
+ ax.set_xticks(tks, [f"{int(d)}°" for d in tksl])
284
+ ax.set_yticks(freq_ticks, [f"{f}%" for f in freq_ticks])
285
+ ax.set_title(title)
290
286
 
291
- ax = TabWindroseAxes.from_ax(fig=fig, rect=rect, figsize=figsize)
292
- fig = ax.get_figure()
293
-
294
- plfun = getattr(ax, design)
295
- plfun(
296
- direction=wrdata[wd_var].to_numpy(),
297
- var=wrdata[var].to_numpy(),
298
- weights=wrdata["frequency"].to_numpy(),
299
- bin_min_dir=np.sort(wrdata[f"bin_min_{wd_var}"].unique()),
300
- bin_min_var=np.sort(wrdata[f"bin_min_{var}"].unique()),
301
- bin_max_var=np.sort(wrdata[f"bin_max_{var}"].unique()),
302
- **kwargs,
287
+ llines = [Line2D([0], [0], color=c, lw=10) for c in np.flip(color_list, axis=0)]
288
+ lleg = [
289
+ f"[{ws_bins[i]:.1f}, {ws_bins[i+1]:.1f})" for i in range(n_wsb - 1, -1, -1)
290
+ ]
291
+ lpars = dict(
292
+ loc="upper left",
293
+ bbox_to_anchor=(0.8, 0.5),
294
+ title=f"{ws_var}",
303
295
  )
304
- ax.set_legend(title=lg)
305
- ax.set_title(ttl)
296
+ wsl = [FV.WS, FV.REWS, FV.REWS2, FV.REWS3]
297
+ wsl += [FV.var2amb[v] for v in wsl]
298
+ if ws_var in wsl:
299
+ lpars["title"] += " [m/s]"
300
+ if legend_pars is not None:
301
+ lpars.update(legend_pars)
302
+ ax.legend(llines, lleg, **lpars)
306
303
 
307
304
  if ret_data:
308
- return fig, wrdata
305
+ return ax, data
309
306
  else:
310
- return fig
307
+ return ax
311
308
 
312
- def write_figure(
313
- self,
314
- file_name,
315
- sectors,
316
- var,
317
- var_bins,
318
- ret_data=False,
319
- **kwargs,
320
- ):
309
+ def write_figure(self, file_name, *args, ret_data=False, **kwargs):
321
310
  """
322
311
  Write rose plot to file
323
312
 
324
313
  Parameters
325
314
  ----------
326
315
  file_name: str
327
- Path to the output file
328
- sectors: int
329
- The number of wind direction sectors
330
- var: str
331
- The data variable name
332
- var_bins: list of float
333
- The variable bin separation values
316
+ Name of the output file
317
+ args: tuple, optional
318
+ Additional parameters for get_figure
334
319
  ret_data: bool
335
320
  Flag for returning wind rose data
336
321
  kwargs: dict, optional
337
- Additional parameters for get_figure()
322
+ Additional parameters for get_figure
338
323
 
339
324
  Returns
340
325
  -------
@@ -343,38 +328,46 @@ class RosePlotOutput(Output):
343
328
 
344
329
  """
345
330
 
346
- r = self.get_figure(
347
- sectors=sectors,
348
- var=var,
349
- var_bins=var_bins,
350
- ret_data=ret_data,
351
- **kwargs,
352
- )
331
+ r = self.get_figure(*args, ret_data=ret_data, **kwargs)
332
+ fpath = self.get_fpath(file_name)
353
333
  if ret_data:
354
- r[0].write_image(file_name)
334
+ r[0].get_figure().savefig(fpath, bbox_inches="tight")
355
335
  return r[1]
356
336
  else:
357
- r.write_image(file_name)
337
+ r.get_figure().savefig(fpath, bbox_inches="tight")
358
338
 
359
339
 
360
340
  class StatesRosePlotOutput(RosePlotOutput):
361
341
  """
362
- Class for rose plot creation directly from states
363
-
364
- Parameters
365
- ----------
366
- states: foxes.core.States
367
- The states from which to compute the wind rose
368
- point: numpy.ndarray
369
- The evaluation point, shape: (3,)
370
- mbook: foxes.models.ModelBook, optional
371
- The model book
372
-
342
+ Class for rose plot creation directly from states
373
343
  :group: output
374
-
375
344
  """
376
345
 
377
- def __init__(self, states, point, mbook=None, ws_var=FV.AMB_REWS):
346
+ def __init__(
347
+ self,
348
+ states,
349
+ point,
350
+ mbook=None,
351
+ ws_var=FV.AMB_REWS,
352
+ **kwargs,
353
+ ):
354
+ """
355
+ Constructor.
356
+
357
+ Parameters
358
+ ----------
359
+ states: foxes.core.States
360
+ The states from which to compute the wind rose
361
+ point: numpy.ndarray
362
+ The evaluation point, shape: (3,)
363
+ mbook: foxes.models.ModelBook, optional
364
+ The model book
365
+ ws_var: str
366
+ The wind speed variable name
367
+ kwargs: dict, optional
368
+ Additional parameters for the base class
369
+
370
+ """
378
371
  farm = WindFarm()
379
372
  farm.add_turbine(
380
373
  Turbine(
@@ -390,4 +383,235 @@ class StatesRosePlotOutput(RosePlotOutput):
390
383
 
391
384
  results = algo.calc_farm(ambient=True).rename_vars({ws_var: FV.AMB_WS})
392
385
 
393
- super().__init__(results)
386
+ super().__init__(results, **kwargs)
387
+
388
+
389
+ class WindRoseBinPlot(Output):
390
+ """
391
+ Plots mean data in wind rose bins
392
+
393
+ Attributes
394
+ ----------
395
+ farm_results: xarray.Dataset
396
+ The wind farm results
397
+
398
+ :group: output
399
+
400
+ """
401
+
402
+ def __init__(self, farm_results, **kwargs):
403
+ """
404
+ Constructor
405
+
406
+ Parameters
407
+ ----------
408
+ farm_results: xarray.Dataset
409
+ The wind farm results
410
+ kwargs: dict, optional
411
+ Parameters for the base class
412
+
413
+ """
414
+ super().__init__(**kwargs)
415
+ self.farm_results = farm_results
416
+
417
+ def get_data(
418
+ self,
419
+ variable,
420
+ ws_bins,
421
+ wd_sectors=12,
422
+ wd_var=FV.AMB_WD,
423
+ ws_var=FV.AMB_REWS,
424
+ turbine=0,
425
+ contraction="weights",
426
+ ):
427
+ """
428
+ Generates the plot data
429
+
430
+ Parameters
431
+ ----------
432
+ variable: str
433
+ The variable name
434
+ ws_bins: list of float
435
+ The wind speed bins
436
+ wd_var: str
437
+ The wind direction variable
438
+ ws_var: str
439
+ The wind speed variable
440
+ turbine: int
441
+ The turbine index
442
+ contraction: str
443
+ The contraction method for states:
444
+ weights, mean_no_weights, sum_no_weights
445
+
446
+ Returns
447
+ -------
448
+ data: xarray.Dataset
449
+ The plot data
450
+
451
+ """
452
+ var = self.farm_results[variable].to_numpy()[:, turbine]
453
+ w = self.farm_results[FV.WEIGHT].to_numpy()[:, turbine]
454
+ ws = self.farm_results[ws_var].to_numpy()[:, turbine]
455
+ wd = self.farm_results[wd_var].to_numpy()[:, turbine].copy()
456
+ wd_delta = 360 / wd_sectors
457
+ wd[wd >= 360 - wd_delta / 2] -= 360
458
+ wd_bins = np.arange(-wd_delta / 2, 360, wd_delta)
459
+ ws_bins = np.asarray(ws_bins)
460
+
461
+ if contraction == "weights":
462
+ z = np.histogram2d(wd, ws, (wd_bins, ws_bins), weights=w)[0]
463
+ z[z < 1e-13] = np.nan
464
+ z = np.histogram2d(wd, ws, (wd_bins, ws_bins), weights=w * var)[0] / z
465
+ elif contraction == "mean_no_weights":
466
+ z = np.histogram2d(wd, ws, (wd_bins, ws_bins))[0].astype(w.dtype)
467
+ z[z < 1] = np.nan
468
+ z = np.histogram2d(wd, ws, (wd_bins, ws_bins), weights=var)[0] / z
469
+ elif contraction == "sum_no_weights":
470
+ z = np.histogram2d(wd, ws, (wd_bins, ws_bins), weights=var)[0]
471
+ else:
472
+ raise KeyError(
473
+ f"Contraction '{contraction}' not supported. Choices: weights, mean_no_weights, sum_no_weights"
474
+ )
475
+
476
+ data = Dataset(
477
+ coords={
478
+ wd_var: 0.5 * (wd_bins[:-1] + wd_bins[1:]),
479
+ ws_var: 0.5 * (ws_bins[:-1] + ws_bins[1:]),
480
+ },
481
+ data_vars={
482
+ variable: ((wd_var, ws_var), z),
483
+ },
484
+ attrs={
485
+ f"{wd_var}_bounds": wd_bins,
486
+ f"{ws_var}_bounds": ws_bins,
487
+ },
488
+ )
489
+
490
+ return data
491
+
492
+ def get_figure(
493
+ self,
494
+ variable,
495
+ ws_bins,
496
+ wd_sectors=12,
497
+ wd_var=FV.AMB_WD,
498
+ ws_var=FV.AMB_REWS,
499
+ turbine=0,
500
+ contraction="weights",
501
+ fig=None,
502
+ ax=None,
503
+ title=None,
504
+ figsize=None,
505
+ ret_data=False,
506
+ **kwargs,
507
+ ):
508
+ """
509
+ Creates the figure
510
+
511
+ Parameters
512
+ ----------
513
+ variable: str
514
+ The variable name
515
+ ws_bins: list of float
516
+ The wind speed bins
517
+ wd_var: str
518
+ The wind direction variable
519
+ ws_var: str
520
+ The wind speed variable
521
+ turbine: int
522
+ The turbine index
523
+ contraction: str
524
+ The contraction method for states:
525
+ weights, mean_no_weights, sum_no_weights
526
+ fig: pyplot.Figure, optional
527
+ The figure object
528
+ ax: pyplot.Axes, optional
529
+ The axes object
530
+ title: str, optional
531
+ The title
532
+ figsize: tuple, optional
533
+ The figsize argument for plt.subplots
534
+ ret_data: bool
535
+ Flag for returning wind rose data
536
+ kwargs: dict, optional
537
+ Additional parameters for plt.pcolormesh
538
+
539
+ Returns
540
+ -------
541
+ ax: pyplot.Axes
542
+ The axes object
543
+
544
+ """
545
+ data = self.get_data(
546
+ variable=variable,
547
+ ws_bins=ws_bins,
548
+ wd_sectors=wd_sectors,
549
+ wd_var=wd_var,
550
+ ws_var=ws_var,
551
+ turbine=turbine,
552
+ contraction=contraction,
553
+ )
554
+
555
+ wd_delta = 360 / data.sizes[wd_var]
556
+ wd_bins = np.mod(90 - data.attrs[f"{wd_var}_bounds"], 360)
557
+ wd_bins = np.radians(wd_bins)
558
+ ws_bins = data.attrs[f"{ws_var}_bounds"]
559
+
560
+ if ax is not None:
561
+ if not isinstance(ax, PolarAxes):
562
+ raise TypeError(
563
+ f"Require axes of type '{PolarAxes.__name__}' for '{type(self).__name__}', got '{type(ax).__name__}'"
564
+ )
565
+ else:
566
+ fig, ax = plt.subplots(figsize=figsize, subplot_kw={"projection": "polar"})
567
+
568
+ y, x = np.meshgrid(ws_bins, wd_bins)
569
+ z = data[variable].to_numpy()
570
+
571
+ prgs = {"shading": "flat"}
572
+ prgs.update(kwargs)
573
+
574
+ img = ax.pcolormesh(x, y, z, **prgs)
575
+
576
+ tksl = np.arange(0, 360, max(wd_delta, 30))
577
+ tks = np.radians(np.mod(90 - tksl, 360))
578
+ ax.set_xticks(tks, [f"{d}°" for d in tksl])
579
+ ax.set_yticks(ws_bins)
580
+ ax.set_title(title)
581
+ cbar = fig.colorbar(img, ax=ax, pad=0.12)
582
+ cbar.ax.set_title(variable)
583
+
584
+ if ret_data:
585
+ return ax, data
586
+ else:
587
+ return ax
588
+
589
+ def write_figure(self, file_name, *args, ret_data=False, **kwargs):
590
+ """
591
+ Write rose plot to file
592
+
593
+ Parameters
594
+ ----------
595
+ file_name: str
596
+ Name of the output file
597
+ args: tuple, optional
598
+ Additional parameters for get_figure
599
+ ret_data: bool
600
+ Flag for returning wind rose data
601
+ kwargs: dict, optional
602
+ Additional parameters for get_figure
603
+
604
+ Returns
605
+ -------
606
+ data: pd.DataFrame, optional
607
+ The wind rose data
608
+
609
+ """
610
+
611
+ r = self.get_figure(*args, ret_data=ret_data, **kwargs)
612
+ fpath = self.get_fpath(file_name)
613
+ if ret_data:
614
+ r[0].get_figure().savefig(fpath, bbox_inches="tight")
615
+ return r[1]
616
+ else:
617
+ r.get_figure().savefig(fpath, bbox_inches="tight")