foxes 1.2.4__py3-none-any.whl → 1.3__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 (54) hide show
  1. examples/quickstart/run.py +17 -0
  2. foxes/__init__.py +1 -1
  3. foxes/algorithms/downwind/downwind.py +9 -15
  4. foxes/algorithms/downwind/models/farm_wakes_calc.py +13 -7
  5. foxes/algorithms/downwind/models/init_farm_data.py +4 -4
  6. foxes/algorithms/downwind/models/reorder_farm_output.py +5 -1
  7. foxes/algorithms/downwind/models/set_amb_point_results.py +1 -1
  8. foxes/algorithms/iterative/models/farm_wakes_calc.py +6 -3
  9. foxes/algorithms/sequential/models/seq_state.py +0 -18
  10. foxes/algorithms/sequential/sequential.py +5 -18
  11. foxes/constants.py +6 -0
  12. foxes/core/data.py +44 -18
  13. foxes/core/engine.py +19 -1
  14. foxes/core/farm_data_model.py +1 -0
  15. foxes/core/rotor_model.py +42 -38
  16. foxes/core/states.py +2 -47
  17. foxes/input/states/__init__.py +1 -0
  18. foxes/input/states/field_data_nc.py +39 -61
  19. foxes/input/states/multi_height.py +35 -58
  20. foxes/input/states/one_point_flow.py +22 -21
  21. foxes/input/states/scan.py +6 -19
  22. foxes/input/states/single.py +5 -17
  23. foxes/input/states/states_table.py +19 -41
  24. foxes/input/states/wrg_states.py +301 -0
  25. foxes/models/partial_wakes/rotor_points.py +8 -2
  26. foxes/models/partial_wakes/segregated.py +9 -4
  27. foxes/models/rotor_models/centre.py +6 -4
  28. foxes/models/wake_frames/seq_dynamic_wakes.py +5 -2
  29. foxes/models/wake_frames/timelines.py +10 -0
  30. foxes/models/wake_models/induction/vortex_sheet.py +6 -9
  31. foxes/output/farm_layout.py +12 -4
  32. foxes/output/farm_results_eval.py +36 -12
  33. foxes/output/rose_plot.py +20 -2
  34. foxes/output/slice_data.py +16 -19
  35. foxes/utils/wrg_utils.py +84 -1
  36. {foxes-1.2.4.dist-info → foxes-1.3.dist-info}/METADATA +12 -8
  37. {foxes-1.2.4.dist-info → foxes-1.3.dist-info}/RECORD +54 -52
  38. {foxes-1.2.4.dist-info → foxes-1.3.dist-info}/WHEEL +1 -1
  39. tests/0_consistency/iterative/test_iterative.py +2 -3
  40. tests/0_consistency/partial_wakes/test_partial_wakes.py +2 -2
  41. tests/1_verification/flappy_0_6/PCt_files/test_PCt_files.py +48 -56
  42. tests/1_verification/flappy_0_6/abl_states/test_abl_states.py +33 -36
  43. tests/1_verification/flappy_0_6/row_Jensen_linear_centre/test_row_Jensen_linear_centre.py +3 -2
  44. tests/1_verification/flappy_0_6/row_Jensen_linear_tophat/test_row_Jensen_linear_tophat.py +3 -3
  45. tests/1_verification/flappy_0_6/row_Jensen_linear_tophat_IECTI2005/test_row_Jensen_linear_tophat_IECTI_2005.py +3 -3
  46. tests/1_verification/flappy_0_6/row_Jensen_linear_tophat_IECTI2019/test_row_Jensen_linear_tophat_IECTI_2019.py +3 -3
  47. tests/1_verification/flappy_0_6/row_Jensen_quadratic_centre/test_row_Jensen_quadratic_centre.py +3 -3
  48. tests/1_verification/flappy_0_6_2/grid_rotors/test_grid_rotors.py +3 -3
  49. tests/1_verification/flappy_0_6_2/row_Bastankhah_Crespo/test_row_Bastankhah_Crespo.py +3 -2
  50. tests/1_verification/flappy_0_6_2/row_Bastankhah_linear_centre/test_row_Bastankhah_linear_centre.py +3 -3
  51. tests/3_examples/test_examples.py +3 -2
  52. {foxes-1.2.4.dist-info → foxes-1.3.dist-info}/LICENSE +0 -0
  53. {foxes-1.2.4.dist-info → foxes-1.3.dist-info}/entry_points.txt +0 -0
  54. {foxes-1.2.4.dist-info → foxes-1.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,301 @@
1
+ import numpy as np
2
+ from scipy.interpolate import interpn
3
+
4
+ from foxes.core.states import States
5
+ from foxes.config import config, get_input_path
6
+ from foxes.utils.wrg_utils import ReaderWRG
7
+ from foxes.data import STATES
8
+ import foxes.variables as FV
9
+ import foxes.constants as FC
10
+
11
+
12
+ class WRGStates(States):
13
+ """
14
+ Ambient states based on WRG data
15
+
16
+ Attributes
17
+ ----------
18
+ wrg_fname: str
19
+ Name of the WRG file
20
+ ws_bins: numpy.ndarray
21
+ The wind speed bins, including
22
+ lower and upper bounds, shape: (n_ws_bins+1,)
23
+ fixed_vars: dict
24
+ Fixed uniform variable values, instead of
25
+ reading from data
26
+ bounds_extra_space: float or str
27
+ The extra space, either float in m,
28
+ or str for units of D, e.g. '2.5D'
29
+ interpn_pars: dict
30
+ Additional parameters for scipy.interpolate.interpn
31
+
32
+ :group: input.states
33
+
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ wrg_fname,
39
+ ws_bins,
40
+ fixed_vars={},
41
+ bounds_extra_space="1D",
42
+ **interpn_pars,
43
+ ):
44
+ """
45
+ Constructor
46
+
47
+ Parameters
48
+ ----------
49
+ wrg_fname: str
50
+ Name of the WRG file
51
+ ws_bins: list of float
52
+ The wind speed bins, including
53
+ lower and upper bounds
54
+ fixed_vars: dict
55
+ Fixed uniform variable values, instead of
56
+ reading from data
57
+ bounds_extra_space: float or str, optional
58
+ The extra space, either float in m,
59
+ or str for units of D, e.g. '2.5D'
60
+ interpn_pars: dict, optional
61
+ Additional parameters for scipy.interpolate.interpn
62
+
63
+ """
64
+ super().__init__()
65
+ self.wrg_fname = wrg_fname
66
+ self.ws_bins = np.asarray(ws_bins)
67
+ self.fixed_vars = fixed_vars
68
+ self.bounds_extra_space = bounds_extra_space
69
+ self.interpn_pars = interpn_pars
70
+
71
+ def load_data(self, algo, verbosity=0):
72
+ """
73
+ Load and/or create all model data that is subject to chunking.
74
+
75
+ Such data should not be stored under self, for memory reasons. The
76
+ data returned here will automatically be chunked and then provided
77
+ as part of the mdata object during calculations.
78
+
79
+ Parameters
80
+ ----------
81
+ algo: foxes.core.Algorithm
82
+ The calculation algorithm
83
+ verbosity: int
84
+ The verbosity level, 0 = silent
85
+
86
+ Returns
87
+ -------
88
+ idata: dict
89
+ The dict has exactly two entries: `data_vars`,
90
+ a dict with entries `name_str -> (dim_tuple, data_ndarray)`;
91
+ and `coords`, a dict with entries `dim_name_str -> dim_array`
92
+
93
+ """
94
+ # read wrg file:
95
+ fpath = get_input_path(self.wrg_fname)
96
+ if not fpath.is_file():
97
+ if verbosity > 0:
98
+ print(
99
+ f"States '{self.name}': Reading static data '{self.wrg_fname}' from context '{STATES}'"
100
+ )
101
+ fpath = algo.dbook.get_file_path(STATES, self.wrg_fname, check_raw=False)
102
+ if verbosity > 0:
103
+ print(f"Path: {fpath}")
104
+ elif verbosity:
105
+ print(f"States '{self.name}': Reading file {fpath}")
106
+ wrg = ReaderWRG(fpath)
107
+ p0 = np.array([wrg.x0, wrg.y0], dtype=config.dtype_double)
108
+ nx = wrg.nx
109
+ ny = wrg.ny
110
+ ns = wrg.n_sectors
111
+ res = wrg.resolution
112
+ p1 = p0 + np.array([nx * res, ny * res])
113
+ if verbosity > 0:
114
+ print(f"States '{self.name}': Data bounds {p0} - {p1}")
115
+
116
+ # find bounds:
117
+ if self.bounds_extra_space is not None:
118
+ xy_min, xy_max = algo.farm.get_xy_bounds(
119
+ extra_space=self.bounds_extra_space, algo=algo
120
+ )
121
+ if verbosity > 0:
122
+ print(f"States '{self.name}': Farm bounds {xy_min} - {xy_max}")
123
+ ij_min = np.asarray((xy_min - p0) / res, dtype=config.dtype_int)
124
+ ij_max = np.asarray((xy_max - p0) / res, dtype=config.dtype_int) + 1
125
+ sx = slice(ij_min[0], ij_max[0])
126
+ sy = slice(ij_min[1], ij_max[1])
127
+ else:
128
+ sx = np.s_[:]
129
+ sy = np.s_[:]
130
+ self._x = p0[0] + np.arange(nx) * res
131
+ self._x = self._x[sx]
132
+ self._y = p0[1] + np.arange(ny) * res
133
+ self._y = self._y[sy]
134
+ if len(self._x) < 2 or len(self._y) < 2:
135
+ raise ValueError(f"No overlap between data bounds and farm bounds")
136
+ p0[0] = np.min(self._x)
137
+ p0[1] = np.min(self._y)
138
+ p1[0] = np.max(self._x)
139
+ p1[1] = np.max(self._y)
140
+ if verbosity > 0:
141
+ print(f"States '{self.name}': New bounds {p0} - {p1}")
142
+
143
+ # store data:
144
+ A = []
145
+ k = []
146
+ f = []
147
+ for s in range(ns):
148
+ A.append(wrg.data[f"As_{s}"].to_numpy().reshape(ny, nx)[sy, sx])
149
+ k.append(wrg.data[f"Ks_{s}"].to_numpy().reshape(ny, nx)[sy, sx])
150
+ f.append(wrg.data[f"fs_{s}"].to_numpy().reshape(ny, nx)[sy, sx])
151
+ del wrg
152
+ A = np.stack(A, axis=0).T
153
+ k = np.stack(k, axis=0).T
154
+ f = np.stack(f, axis=0).T
155
+ self._data = np.stack([A, k, f], axis=-1) # (x, y, wd, AKfs)
156
+
157
+ # store ws and wd:
158
+ self.VARS = self.var("VARS")
159
+ self.DATA = self.var("DATA")
160
+ self._wds = np.arange(0.0, 360.0, 360 / ns)
161
+ self._wsd = self.ws_bins[1:] - self.ws_bins[:-1]
162
+ self._wss = 0.5 * (self.ws_bins[:-1] + self.ws_bins[1:])
163
+ self._N = len(self._wss) * ns
164
+ data = np.zeros((len(self._wss), ns, 3), dtype=config.dtype_double)
165
+ data[..., 0] = self._wss[:, None]
166
+ data[..., 1] = self._wds[None, :]
167
+ data[..., 2] = self._wsd[:, None]
168
+ data = data.reshape(self._N, 3)
169
+ idata = super().load_data(algo, verbosity)
170
+ idata["coords"][self.VARS] = ["ws", "wd", "dws"]
171
+ idata["data_vars"][self.DATA] = ((FC.STATE, self.VARS), data)
172
+
173
+ return idata
174
+
175
+ def size(self):
176
+ """
177
+ The total number of states.
178
+
179
+ Returns
180
+ -------
181
+ int:
182
+ The total number of states
183
+
184
+ """
185
+ return self._N
186
+
187
+ def output_point_vars(self, algo):
188
+ """
189
+ The variables which are being modified by the model.
190
+
191
+ Parameters
192
+ ----------
193
+ algo: foxes.core.Algorithm
194
+ The calculation algorithm
195
+
196
+ Returns
197
+ -------
198
+ output_vars: list of str
199
+ The output variable names
200
+
201
+ """
202
+ ovars = set([FV.WS, FV.WD])
203
+ ovars.update(self.fixed_vars.keys())
204
+ return list(ovars)
205
+
206
+ def calculate(self, algo, mdata, fdata, tdata):
207
+ """
208
+ The main model calculation.
209
+
210
+ This function is executed on a single chunk of data,
211
+ all computations should be based on numpy arrays.
212
+
213
+ Parameters
214
+ ----------
215
+ algo: foxes.core.Algorithm
216
+ The calculation algorithm
217
+ mdata: foxes.core.MData
218
+ The model data
219
+ fdata: foxes.core.FData
220
+ The farm data
221
+ tdata: foxes.core.TData
222
+ The target point data
223
+
224
+ Returns
225
+ -------
226
+ results: dict
227
+ The resulting data, keys: output variable str.
228
+ Values: numpy.ndarray with shape
229
+ (n_states, n_targets, n_tpoints)
230
+
231
+ """
232
+
233
+ # prepare:
234
+ n_states = tdata.n_states
235
+ n_targets = tdata.n_targets
236
+ n_tpoints = tdata.n_tpoints
237
+ n_pts = n_states * n_targets * n_tpoints
238
+ points = tdata[FC.TARGETS]
239
+ ws = mdata[self.DATA][:, 0]
240
+ wd = mdata[self.DATA][:, 1]
241
+ wsd = mdata[self.DATA][:, 2]
242
+
243
+ out = {}
244
+
245
+ out[FV.WS] = tdata[FV.WS]
246
+ out[FV.WS][:] = ws[:, None, None]
247
+
248
+ out[FV.WD] = tdata[FV.WD]
249
+ out[FV.WD][:] = wd[:, None, None]
250
+
251
+ for v, d in self.fixed_vars.items():
252
+ out[v] = tdata[v]
253
+ out[v][:] = d
254
+
255
+ # interpolate A, k, f from x, y, wd
256
+ z = points[..., 2].copy()
257
+ points[..., 2] = wd[:, None, None]
258
+ pts = points.reshape(n_pts, 3)
259
+ gvars = (self._x, self._y, self._wds)
260
+ try:
261
+ ipars = dict(bounds_error=True, fill_value=None)
262
+ ipars.update(self.interpn_pars)
263
+ data = interpn(gvars, self._data, pts, **ipars).reshape(
264
+ n_states, n_targets, n_tpoints, 3
265
+ )
266
+ except ValueError as e:
267
+ print(f"\nStates '{self.name}': Interpolation error")
268
+ print("INPUT VARS: (x, y, wd)")
269
+ print(
270
+ "DATA BOUNDS:",
271
+ [float(np.min(d)) for d in gvars],
272
+ [float(np.max(d)) for d in gvars],
273
+ )
274
+ print(
275
+ "EVAL BOUNDS:",
276
+ [float(np.min(p)) for p in pts.T],
277
+ [float(np.max(p)) for p in pts.T],
278
+ )
279
+ print(
280
+ "\nMaybe you want to try the option 'bounds_error=False'? This will extrapolate the data.\n"
281
+ )
282
+ raise e
283
+
284
+ A = data[..., 0]
285
+ k = data[..., 1]
286
+ f = data[..., 2]
287
+ points[..., 2] = z
288
+ del data, gvars, pts, z, wd
289
+
290
+ tdata.add(
291
+ FV.WEIGHT,
292
+ f,
293
+ dims=(FC.STATE, FC.TARGET, FC.TPOINT),
294
+ )
295
+
296
+ wsA = out[FV.WS] / A
297
+ tdata[FV.WEIGHT] *= wsd[:, None, None] * (
298
+ k / A * wsA ** (k - 1) * np.exp(-(wsA**k))
299
+ )
300
+
301
+ return out
@@ -91,8 +91,14 @@ class RotorPoints(PartialWakesModel):
91
91
  of shape (n_states, n_rotor_points)
92
92
 
93
93
  """
94
- ares = {v: d[:, downwind_index, None] for v, d in amb_res.items()}
95
- wdel = {v: d[:, downwind_index, None].copy() for v, d in wake_deltas.items()}
94
+ ares = {
95
+ v: d[:, downwind_index, None] if d.shape[1] > 1 else d[:, 0, None]
96
+ for v, d in amb_res.items()
97
+ }
98
+ wdel = {
99
+ v: d[:, downwind_index, None].copy() if d.shape[1] > 1 else d[:, 0, None]
100
+ for v, d in wake_deltas.items()
101
+ }
96
102
  wmodel.finalize_wake_deltas(algo, mdata, fdata, ares, wdel)
97
103
 
98
104
  return {v: d[:, 0] for v, d in wdel.items()}
@@ -140,16 +140,21 @@ class PartialSegregated(PartialWakesModel):
140
140
  wdel = {v: d[:, downwind_index, None].copy() for v, d in wake_deltas.items()}
141
141
 
142
142
  if n_rotor_points == tdata.n_tpoints:
143
- ares = {v: d[:, downwind_index, None] for v, d in amb_res.items()}
143
+ ares = {
144
+ v: d[:, downwind_index, None] if d.shape[1] > 1 else d[:, 0, None]
145
+ for v, d in amb_res.items()
146
+ }
144
147
  else:
145
148
  ares = {}
146
149
  for v, d in amb_res.items():
147
150
  ares[v] = np.zeros(
148
151
  (n_states, 1, tdata.n_tpoints), dtype=config.dtype_double
149
152
  )
150
- ares[v][:] = np.einsum("sp,p->s", d[:, downwind_index], rpoint_weights)[
151
- :, None, None
152
- ]
153
+ ares[v][:] = np.einsum(
154
+ "sp,p->s",
155
+ d[:, downwind_index] if d.shape[1] > 1 else d[:, 0],
156
+ rpoint_weights,
157
+ )[:, None, None]
153
158
 
154
159
  wmodel.finalize_wake_deltas(algo, mdata, fdata, ares, wdel)
155
160
 
@@ -89,7 +89,7 @@ class CentreRotor(RotorModel):
89
89
  mdata,
90
90
  fdata,
91
91
  rpoint_results,
92
- weights,
92
+ rpoint_weights,
93
93
  downwind_index=None,
94
94
  copy_to_ambient=False,
95
95
  ):
@@ -124,9 +124,9 @@ class CentreRotor(RotorModel):
124
124
  variables after calculation
125
125
 
126
126
  """
127
- if len(weights) > 1:
127
+ if len(rpoint_weights) > 1:
128
128
  return super().eval_rpoint_results(
129
- algo, mdata, fdata, rpoint_results, weights, downwind_index
129
+ algo, mdata, fdata, rpoint_results, rpoint_weights, downwind_index
130
130
  )
131
131
 
132
132
  n_states = mdata.n_states
@@ -192,7 +192,9 @@ class CentreRotor(RotorModel):
192
192
  del uvp
193
193
 
194
194
  for v in self.calc_vars:
195
- if v not in vdone:
195
+ if v not in vdone and (
196
+ fdata[v].shape[1] > 1 or downwind_index is None or downwind_index == 0
197
+ ):
196
198
  res = rpoint_results[v][:, :, 0]
197
199
  self._set_res(fdata, v, res, downwind_index)
198
200
  del res
@@ -186,12 +186,15 @@ class SeqDynamicWakes(FarmOrder):
186
186
 
187
187
  # compute wind vectors at wake traces:
188
188
  # TODO: dz from U_z is missing here
189
- hpdata = TData.from_points(points=self._traces_p[None, :N, downwind_index])
189
+ svrs = algo.states.output_point_vars(algo)
190
+ hpdata = TData.from_points(
191
+ points=self._traces_p[None, :N, downwind_index], variables=svrs
192
+ )
190
193
  res = algo.states.calculate(algo, mdata, fdata, hpdata)
191
194
  self._traces_v[:N, downwind_index, :2] = wd2uv(
192
195
  res[FV.WD][0, :, 0], res[FV.WS][0, :, 0]
193
196
  )
194
- del hpdata, res
197
+ del hpdata, res, svrs
195
198
 
196
199
  # find nearest wake point:
197
200
  dists = cdist(points[0], self._traces_p[:N, downwind_index])
@@ -105,6 +105,7 @@ class Timelines(WakeFrame):
105
105
 
106
106
  # calculate all heights:
107
107
  self.timelines_data = {"dxy": (("height", FC.STATE, "dir"), [])}
108
+ weight_data = None
108
109
  for h in heights:
109
110
 
110
111
  if verbosity > 0:
@@ -118,6 +119,13 @@ class Timelines(WakeFrame):
118
119
  )
119
120
 
120
121
  res = states.calculate(algo, mdata, fdata, tdata)
122
+
123
+ if weight_data is None:
124
+ weight_data = ((FC.STATE,), tdata[FV.WEIGHT][:, 0, 0])
125
+ elif not np.all(tdata[FV.WEIGHT] == weight_data[1]):
126
+ raise AssertionError(
127
+ f"States '{self.name}': weight data mismatch between heights"
128
+ )
121
129
  del tdata
122
130
 
123
131
  uv = wd2uv(res[FV.WD], res[FV.WS])[:, 0, 0, :2]
@@ -163,6 +171,8 @@ class Timelines(WakeFrame):
163
171
  },
164
172
  )
165
173
 
174
+ return weight_data
175
+
166
176
  def initialize(self, algo, verbosity=0):
167
177
  """
168
178
  Initializes the model.
@@ -183,12 +183,11 @@ class VortexSheet(TurbineInductionModel):
183
183
  sp_sel = (ct > 1e-8) & (x <= 0)
184
184
  ct_sel = ct[sp_sel]
185
185
  r_sph_sel = r_sph[sp_sel]
186
- D_sel = D[sp_sel]
186
+ R_sel = D[sp_sel] / 2
187
+ xi = r_sph_sel / R_sel
187
188
 
188
189
  if np.any(sp_sel):
189
- blockage = self.induction.ct2a(ct_sel) * (
190
- (1 + 2 * r_sph_sel / D_sel) * (1 + (2 * r_sph_sel / D_sel) ** 2)
191
- ) ** (-0.5)
190
+ blockage = self.induction.ct2a(ct_sel) * (1 + -xi / np.sqrt(1 + xi**2))
192
191
 
193
192
  self._superp.add_wake(
194
193
  algo,
@@ -208,12 +207,10 @@ class VortexSheet(TurbineInductionModel):
208
207
  ) # mirror in rotor plane and inverse blockage, but not directly behind rotor
209
208
  ct_sel = ct[sp_sel]
210
209
  r_sph_sel = r_sph[sp_sel]
211
- D_sel = D[sp_sel]
210
+ R_sel = D[sp_sel] / 2
211
+ xi = r_sph_sel / R_sel
212
212
  if np.any(sp_sel):
213
- blockage = self.induction.ct2a(ct_sel) * (
214
- (1 + 2 * r_sph_sel / D_sel) * (1 + (2 * r_sph_sel / D_sel) ** 2)
215
- ) ** (-0.5)
216
-
213
+ blockage = self.induction.ct2a(ct_sel) * (1 + -xi / np.sqrt(1 + xi**2))
217
214
  self._superp.add_wake(
218
215
  algo,
219
216
  mdata,
@@ -5,8 +5,9 @@ import matplotlib.pyplot as plt
5
5
  from mpl_toolkits.axes_grid1 import make_axes_locatable
6
6
 
7
7
  from foxes.config import config
8
- import foxes.variables as FV
9
8
  from foxes.output.output import Output
9
+ import foxes.variables as FV
10
+ import foxes.constants as FC
10
11
 
11
12
 
12
13
  class FarmLayoutOutput(Output):
@@ -229,9 +230,16 @@ class FarmLayoutOutput(Output):
229
230
  if self.fres is None:
230
231
  raise ValueError(f"Missing farm_results for color_by '{color_by}'")
231
232
  if color_by[:5] == "mean_":
232
- kw["c"] = np.einsum(
233
- "st,st->t", self.fres[color_by[5:]], self.fres[FV.WEIGHT]
234
- )
233
+ weights = self.fres[FV.WEIGHT]
234
+ if weights.dims == (FC.STATE,):
235
+ wx = "s"
236
+ elif weights.dims == (FC.STATE, FC.TURBINE):
237
+ wx = "st"
238
+ else:
239
+ raise ValueError(
240
+ f"Unsupported dimensions for '{FV.WEIGHT}': Expecting '{(FC.STATE,)}' or '{(FC.STATE, FC.TURBINE)}', got '{weights.dims}'"
241
+ )
242
+ kw["c"] = np.einsum(f"st,{wx}->t", self.fres[color_by[5:]], weights)
235
243
  elif color_by[:4] == "sum_":
236
244
  kw["c"] = np.sum(self.fres[color_by[4:]], axis=0)
237
245
  elif color_by[:4] == "min_":
@@ -76,22 +76,46 @@ class FarmResultsEval(Output):
76
76
  nas = np.zeros_like(fields[-1], dtype=bool)
77
77
  nas = nas | np.isnan(fields[-1])
78
78
 
79
- inds = ["st" for v in fields] + ["st"]
80
- expr = ",".join(inds) + "->" + rhs
79
+ inds = ["st" for __ in fields]
80
+ if self.results[FV.WEIGHT].dims == (FC.STATE,):
81
+ inds += ["s"]
82
+
83
+ if np.any(nas):
84
+ sel = ~np.any(nas, axis=1)
85
+ fields = [f[sel] for f in fields]
86
+
87
+ weights0 = self.results[FV.WEIGHT].to_numpy()
88
+ w0 = np.sum(weights0)
89
+ weights = weights0[sel]
90
+ w1 = np.sum(weights)
91
+ weights *= w0 / w1
92
+ fields.append(weights)
93
+
94
+ else:
95
+ fields.append(self.results[FV.WEIGHT].to_numpy())
96
+
97
+ elif self.results[FV.WEIGHT].dims == (FC.STATE, FC.TURBINE):
98
+ inds += ["st"]
81
99
 
82
- if np.any(nas):
83
- sel = ~np.any(nas, axis=1)
84
- fields = [f[sel] for f in fields]
100
+ if np.any(nas):
101
+ sel = ~np.any(nas, axis=1)
102
+ fields = [f[sel] for f in fields]
85
103
 
86
- weights0 = self.results[FV.WEIGHT].to_numpy()
87
- w0 = np.sum(weights0, axis=0)[None, :]
88
- weights = weights0[sel]
89
- w1 = np.sum(weights, axis=0)[None, :]
90
- weights *= w0 / w1
91
- fields.append(weights)
104
+ weights0 = self.results[FV.WEIGHT].to_numpy()
105
+ w0 = np.sum(weights0, axis=0)[None, :]
106
+ weights = weights0[sel]
107
+ w1 = np.sum(weights, axis=0)[None, :]
108
+ weights *= w0 / w1
109
+ fields.append(weights)
110
+
111
+ else:
112
+ fields.append(self.results[FV.WEIGHT].to_numpy())
92
113
 
93
114
  else:
94
- fields.append(self.results[FV.WEIGHT].to_numpy())
115
+ raise ValueError(
116
+ f"Expecting '{FV.WEIGHT}' variable with dimensions {(FC.STATE,)} or {(FC.STATE, FC.TURBINE)}, got {self.results[FV.WEIGHT].dims}"
117
+ )
118
+ expr = ",".join(inds) + "->" + rhs
95
119
 
96
120
  return np.einsum(expr, *fields)
97
121
 
foxes/output/rose_plot.py CHANGED
@@ -151,9 +151,19 @@ class RosePlotOutput(Output):
151
151
  The plot data
152
152
 
153
153
  """
154
+ if self.results[FV.WEIGHT].dims == (FC.STATE,):
155
+ w = self.results[FV.WEIGHT].to_numpy()
156
+ elif self.results[FV.WEIGHT].dims == (FC.STATE, FC.TURBINE):
157
+ w = self.results[FV.WEIGHT].to_numpy()[:, turbine]
158
+ elif self.results[FV.WEIGHT].dims == (FC.STATE, FC.POINT):
159
+ w = self.results[FV.WEIGHT].to_numpy()[:, point]
160
+ else:
161
+ raise ValueError(
162
+ f"Wrong dimensions for '{FV.WEIGHT}'. Expecting {(FC.STATE,)}, {(FC.STATE, FC.TURBINE)} or {(FC.STATE, FC.POINT)}, got {self.results[FV.WEIGHT].dims}"
163
+ )
164
+
154
165
  if add_inf:
155
166
  ws_bins = list(ws_bins) + [np.inf]
156
- w = self.results[FV.WEIGHT].to_numpy()[:, turbine]
157
167
  t = turbine if self._rtype == FC.TURBINE else point
158
168
  ws = self.results[ws_var].to_numpy()[:, t]
159
169
  wd = self.results[wd_var].to_numpy()[:, t].copy()
@@ -449,8 +459,16 @@ class WindRoseBinPlot(Output):
449
459
  The plot data
450
460
 
451
461
  """
462
+ if self.farm_results[FV.WEIGHT].dims == (FC.STATE,):
463
+ w = self.farm_results[FV.WEIGHT].to_numpy()
464
+ elif self.farm_results[FV.WEIGHT].dims == (FC.STATE, FC.TURBINE):
465
+ w = self.farm_results[FV.WEIGHT].to_numpy()[:, turbine]
466
+ else:
467
+ raise ValueError(
468
+ f"Wrong dimensions for '{FV.WEIGHT}'. Expecting {(FC.STATE,)} or {(FC.STATE, FC.TURBINE)}, got {self.farm_results[FV.WEIGHT].dims}"
469
+ )
470
+
452
471
  var = self.farm_results[variable].to_numpy()[:, turbine]
453
- w = self.farm_results[FV.WEIGHT].to_numpy()[:, turbine]
454
472
  ws = self.farm_results[ws_var].to_numpy()[:, turbine]
455
473
  wd = self.farm_results[wd_var].to_numpy()[:, turbine].copy()
456
474
  wd_delta = 360 / wd_sectors