foxes 1.2__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 (57) hide show
  1. examples/abl_states/run.py +5 -5
  2. examples/induction/run.py +5 -5
  3. examples/random_timeseries/run.py +13 -13
  4. examples/scan_row/run.py +12 -7
  5. examples/sector_management/run.py +11 -7
  6. examples/single_state/run.py +5 -5
  7. examples/tab_file/run.py +1 -1
  8. examples/timeseries/run.py +5 -5
  9. examples/timeseries_slurm/run.py +5 -5
  10. examples/wind_rose/run.py +1 -1
  11. examples/yawed_wake/run.py +5 -5
  12. foxes/algorithms/downwind/downwind.py +15 -5
  13. foxes/algorithms/sequential/sequential.py +1 -1
  14. foxes/core/algorithm.py +24 -20
  15. foxes/core/axial_induction_model.py +18 -0
  16. foxes/core/engine.py +2 -14
  17. foxes/core/farm_controller.py +18 -0
  18. foxes/core/ground_model.py +19 -0
  19. foxes/core/partial_wakes_model.py +9 -21
  20. foxes/core/point_data_model.py +18 -0
  21. foxes/core/rotor_model.py +2 -18
  22. foxes/core/states.py +2 -17
  23. foxes/core/turbine_model.py +2 -18
  24. foxes/core/turbine_type.py +2 -18
  25. foxes/core/vertical_profile.py +8 -20
  26. foxes/core/wake_frame.py +2 -20
  27. foxes/core/wake_model.py +19 -20
  28. foxes/core/wake_superposition.py +19 -0
  29. foxes/input/states/__init__.py +1 -1
  30. foxes/input/states/field_data_nc.py +14 -1
  31. foxes/input/states/{scan_ws.py → scan.py} +39 -52
  32. foxes/input/yaml/__init__.py +1 -1
  33. foxes/input/yaml/dict.py +221 -50
  34. foxes/input/yaml/yaml.py +5 -5
  35. foxes/output/__init__.py +2 -1
  36. foxes/output/farm_results_eval.py +57 -35
  37. foxes/output/output.py +2 -18
  38. foxes/output/plt.py +19 -0
  39. foxes/output/rose_plot.py +413 -207
  40. foxes/utils/__init__.py +1 -2
  41. foxes/utils/subclasses.py +69 -0
  42. {foxes-1.2.dist-info → foxes-1.2.1.dist-info}/METADATA +1 -2
  43. {foxes-1.2.dist-info → foxes-1.2.1.dist-info}/RECORD +56 -56
  44. tests/0_consistency/iterative/test_iterative.py +1 -1
  45. tests/0_consistency/partial_wakes/test_partial_wakes.py +1 -1
  46. tests/1_verification/flappy_0_6/row_Jensen_linear_centre/test_row_Jensen_linear_centre.py +7 -2
  47. tests/1_verification/flappy_0_6/row_Jensen_linear_tophat/test_row_Jensen_linear_tophat.py +7 -2
  48. tests/1_verification/flappy_0_6/row_Jensen_linear_tophat_IECTI2005/test_row_Jensen_linear_tophat_IECTI_2005.py +7 -2
  49. tests/1_verification/flappy_0_6/row_Jensen_linear_tophat_IECTI2019/test_row_Jensen_linear_tophat_IECTI_2019.py +7 -2
  50. tests/1_verification/flappy_0_6/row_Jensen_quadratic_centre/test_row_Jensen_quadratic_centre.py +7 -2
  51. tests/1_verification/flappy_0_6_2/row_Bastankhah_Crespo/test_row_Bastankhah_Crespo.py +7 -3
  52. tests/1_verification/flappy_0_6_2/row_Bastankhah_linear_centre/test_row_Bastankhah_linear_centre.py +7 -2
  53. foxes/utils/windrose_plot.py +0 -152
  54. {foxes-1.2.dist-info → foxes-1.2.1.dist-info}/LICENSE +0 -0
  55. {foxes-1.2.dist-info → foxes-1.2.1.dist-info}/WHEEL +0 -0
  56. {foxes-1.2.dist-info → foxes-1.2.1.dist-info}/entry_points.txt +0 -0
  57. {foxes-1.2.dist-info → foxes-1.2.1.dist-info}/top_level.txt +0 -0
foxes/output/rose_plot.py CHANGED
@@ -1,7 +1,10 @@
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
- from foxes.utils import wd2uv, uv2wd, TabWindroseAxes
5
8
  from foxes.algorithms import Downwind
6
9
  from foxes.core import WindFarm, Turbine
7
10
  from foxes.models import ModelBook
@@ -24,30 +27,38 @@ class RosePlotOutput(Output):
24
27
 
25
28
  """
26
29
 
27
- def __init__(self, results, **kwargs):
30
+ def __init__(
31
+ self,
32
+ farm_results=None,
33
+ point_results=None,
34
+ use_points=False,
35
+ **kwargs,
36
+ ):
28
37
  """
29
38
  Constructor.
30
39
 
31
40
  Parameters
32
41
  ----------
33
- results: xarray.Dataset
34
- 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
35
49
  kwargs: dict, optional
36
50
  Additional parameters for the base class
37
51
 
38
52
  """
39
53
  super().__init__(**kwargs)
40
- dims = list(results.sizes.keys())
41
- if dims[1] == FC.TURBINE:
42
- self._rtype = FC.TURBINE
43
- elif dims[1] == FC.POINT:
54
+ if use_points or (farm_results is None and point_results is not None):
55
+ self.results = point_results
44
56
  self._rtype = FC.POINT
57
+ elif farm_results is not None:
58
+ self.results = farm_results
59
+ self._rtype = FC.TURBINE
45
60
  else:
46
- raise KeyError(
47
- f"Results dimension 1 is neither '{FC.TURBINE}' nor '{FC.POINT}': dims = {results.dims}"
48
- )
49
-
50
- self.results = results.to_dataframe()
61
+ raise KeyError(f"Require either farm_results or point_results")
51
62
 
52
63
  @classmethod
53
64
  def get_data_info(cls, dname):
@@ -104,224 +115,198 @@ class RosePlotOutput(Output):
104
115
 
105
116
  def get_data(
106
117
  self,
107
- sectors,
108
- var,
109
- var_bins,
118
+ wd_sectors,
119
+ ws_var,
120
+ ws_bins,
110
121
  wd_var=FV.AMB_WD,
111
- turbine=None,
112
- point=None,
113
- start0=False,
122
+ turbine=0,
123
+ point=0,
124
+ add_inf=False,
114
125
  ):
115
126
  """
116
- Get pandas DataFrame with wind rose data.
127
+ Generates the plot data
117
128
 
118
129
  Parameters
119
130
  ----------
120
- sectors: int
121
- The number of wind direction sectors
122
- var: str
123
- The data variable name
124
- var_bins: list of float
125
- The variable bin separation values
126
- wd_var: str, optional
127
- The wind direction variable name
128
- turbine: int, optional
129
- Only relevant in case of farm results.
130
- If None, mean over all turbines.
131
- Else, data from a single turbine
132
- point: int, optional
133
- Only relevant in case of point results.
134
- If None, mean over all points.
135
- Else, data from a single point
136
- start0: bool
137
- Flag for starting the first sector at
138
- 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
139
147
 
140
148
  Returns
141
149
  -------
142
- pd.DataFrame:
143
- The wind rose data
150
+ data: xarray.Dataset
151
+ The plot data
144
152
 
145
153
  """
146
-
147
- dwd = 360.0 / sectors
148
- wds = np.arange(0.0, 360.0, dwd)
149
- wdb = np.append(wds, 360) if start0 else np.arange(-dwd / 2, 360.0, dwd)
150
- lgd = f"interval_{var}"
151
-
152
- data = self.results[[wd_var, FV.WEIGHT]].copy()
153
- data[lgd] = self.results[var]
154
- uv = wd2uv(data[wd_var].to_numpy())
155
- data["u"] = uv[:, 0]
156
- data["v"] = uv[:, 1]
157
-
158
- data[FV.WEIGHT] *= 100
159
- data = data.rename(columns={FV.WEIGHT: "frequency"})
160
-
161
- el = turbine if self._rtype == FC.TURBINE else point
162
- if el is None:
163
- data = data.groupby(level=0).mean()
164
- else:
165
- sname = data.index.names[0]
166
- grp = data.reset_index().groupby(self._rtype)
167
- data = grp.get_group(el).set_index(sname)
168
-
169
- data["wd"] = uv2wd(data[["u", "v"]].to_numpy())
170
- data.drop(["u", "v"], axis=1, inplace=True)
171
- if not start0:
172
- data.loc[data["wd"] > 360.0 - dwd / 2, "wd"] -= 360.0
173
-
174
- data[wd_var] = pd.cut(data["wd"], wdb, labels=wds)
175
- data[lgd] = pd.cut(data[lgd], var_bins, right=False, include_lowest=True)
176
-
177
- grp = data[[wd_var, lgd, "frequency"]].groupby([wd_var, lgd], observed=False)
178
- data = grp.sum().reset_index()
179
-
180
- data[wd_var] = data[wd_var].astype(np.float64)
181
- data[lgd] = list(data[lgd])
182
- if start0:
183
- data[wd_var] += dwd / 2
184
-
185
- ii = pd.IntervalIndex(data[lgd])
186
- data[var] = ii.mid
187
- data[f"bin_min_{var}"] = ii.left
188
- data[f"bin_max_{var}"] = ii.right
189
- data[f"bin_min_{wd_var}"] = data[wd_var] - dwd / 2
190
- data[f"bin_max_{wd_var}"] = data[wd_var] + dwd / 2
191
- data["sector"] = (data[wd_var] / dwd).astype(int)
192
-
193
- data = data[
194
- [
195
- wd_var,
196
- var,
197
- "sector",
198
- f"bin_min_{wd_var}",
199
- f"bin_max_{wd_var}",
200
- f"bin_min_{var}",
201
- f"bin_max_{var}",
202
- lgd,
203
- "frequency",
204
- ]
205
- ]
206
- 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
+ )
207
184
 
208
185
  return data
209
186
 
210
187
  def get_figure(
211
188
  self,
212
- sectors,
213
- var,
214
- var_bins,
189
+ wd_sectors,
190
+ ws_var,
191
+ ws_bins,
215
192
  wd_var=FV.AMB_WD,
216
- turbine=None,
217
- point=None,
218
- title=None,
219
- legend=None,
220
- design="bar",
221
- start0=False,
222
193
  fig=None,
194
+ ax=None,
223
195
  figsize=None,
224
- rect=None,
196
+ freq_delta=3,
197
+ cmap="summer",
198
+ title=None,
199
+ legend_pars=None,
225
200
  ret_data=False,
226
201
  **kwargs,
227
202
  ):
228
203
  """
229
- Creates figure object
204
+ Creates the figure
230
205
 
231
206
  Parameters
232
207
  ----------
233
- sectors: int
234
- The number of wind direction sectors
235
- var: str
236
- The data variable name
237
- var_bins: list of float
238
- The variable bin separation values
239
- wd_var: str, optional
240
- The wind direction variable name
241
- turbine: int, optional
242
- Only relevant in case of farm results.
243
- If None, mean over all turbines.
244
- Else, data from a single turbine
245
- point: int, optional
246
- Only relevant in case of point results.
247
- If None, mean over all points.
248
- Else, data from a single point
249
- title. str, optional
250
- The title
251
- legend: str, optional
252
- The data legend string
253
- design: str
254
- The wind rose design: bar, contour, ...
255
- start0: bool
256
- Flag for starting the first sector at
257
- zero degrees instead of minus half width
258
- fig: matplotlib.Figure
259
- 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
260
220
  figsize: tuple, optional
261
- The figsize of the newly created figure
262
- rect: list, optional
263
- The rectangle of the figure which to fill,
264
- 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
265
231
  ret_data: bool
266
232
  Flag for returning wind rose data
267
233
  kwargs: dict, optional
268
- Additional arguments for TabWindroseAxes
269
- plot function
234
+ Additional parameters for get_data
270
235
 
271
236
  Returns
272
237
  -------
273
- fig: matplotlib.pyplot.Figure
274
- The rose plot figure
275
- data: pd.DataFrame, optional
276
- The wind rose data
238
+ ax: pyplot.Axes
239
+ The axes object
240
+ data: xarray.Dataset, optional
241
+ The plot data
277
242
 
278
243
  """
279
- lg = legend
280
- if title is None or legend is None:
281
- ttl, lg = self.get_data_info(var)
282
- if title is not None:
283
- ttl = title
284
-
285
- wrdata = self.get_data(
286
- sectors=sectors,
287
- var=var,
288
- var_bins=var_bins,
289
- wd_var=wd_var,
290
- turbine=turbine,
291
- point=point,
292
- start0=start0,
293
- )
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)
294
286
 
295
- ax = TabWindroseAxes.from_ax(fig=fig, rect=rect, figsize=figsize)
296
- fig = ax.get_figure()
297
-
298
- plfun = getattr(ax, design)
299
- plfun(
300
- direction=wrdata[wd_var].to_numpy(),
301
- var=wrdata[var].to_numpy(),
302
- weights=wrdata["frequency"].to_numpy(),
303
- bin_min_dir=np.sort(wrdata[f"bin_min_{wd_var}"].unique()),
304
- bin_min_var=np.sort(wrdata[f"bin_min_{var}"].unique()),
305
- bin_max_var=np.sort(wrdata[f"bin_max_{var}"].unique()),
306
- **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}",
307
295
  )
308
- ax.set_legend(title=lg)
309
- 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)
310
303
 
311
304
  if ret_data:
312
- return fig, wrdata
305
+ return ax, data
313
306
  else:
314
- return fig
307
+ return ax
315
308
 
316
- def write_figure(
317
- self,
318
- file_name,
319
- sectors,
320
- var,
321
- var_bins,
322
- ret_data=False,
323
- **kwargs,
324
- ):
309
+ def write_figure(self, file_name, *args, ret_data=False, **kwargs):
325
310
  """
326
311
  Write rose plot to file
327
312
 
@@ -329,16 +314,12 @@ class RosePlotOutput(Output):
329
314
  ----------
330
315
  file_name: str
331
316
  Name of the output file
332
- sectors: int
333
- The number of wind direction sectors
334
- var: str
335
- The data variable name
336
- var_bins: list of float
337
- The variable bin separation values
317
+ args: tuple, optional
318
+ Additional parameters for get_figure
338
319
  ret_data: bool
339
320
  Flag for returning wind rose data
340
321
  kwargs: dict, optional
341
- Additional parameters for get_figure()
322
+ Additional parameters for get_figure
342
323
 
343
324
  Returns
344
325
  -------
@@ -347,19 +328,13 @@ class RosePlotOutput(Output):
347
328
 
348
329
  """
349
330
 
350
- r = self.get_figure(
351
- sectors=sectors,
352
- var=var,
353
- var_bins=var_bins,
354
- ret_data=ret_data,
355
- **kwargs,
356
- )
331
+ r = self.get_figure(*args, ret_data=ret_data, **kwargs)
357
332
  fpath = self.get_fpath(file_name)
358
333
  if ret_data:
359
- r[0].write_image(fpath)
334
+ r[0].get_figure().savefig(fpath, bbox_inches="tight")
360
335
  return r[1]
361
336
  else:
362
- r.write_image(fpath)
337
+ r.get_figure().savefig(fpath, bbox_inches="tight")
363
338
 
364
339
 
365
340
  class StatesRosePlotOutput(RosePlotOutput):
@@ -409,3 +384,234 @@ class StatesRosePlotOutput(RosePlotOutput):
409
384
  results = algo.calc_farm(ambient=True).rename_vars({ws_var: FV.AMB_WS})
410
385
 
411
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")