foxes 1.4__py3-none-any.whl → 1.5.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 (95) hide show
  1. docs/source/conf.py +1 -1
  2. examples/abl_states/run.py +58 -56
  3. examples/dyn_wakes/run.py +110 -118
  4. examples/field_data_nc/run.py +23 -21
  5. examples/multi_height/run.py +8 -6
  6. examples/scan_row/run.py +89 -87
  7. examples/sector_management/run.py +40 -38
  8. examples/states_lookup_table/run.py +6 -4
  9. examples/streamline_wakes/run.py +10 -8
  10. examples/timelines/run.py +100 -98
  11. examples/timeseries/run.py +71 -76
  12. examples/wind_rose/run.py +27 -25
  13. examples/yawed_wake/run.py +85 -81
  14. foxes/algorithms/downwind/downwind.py +5 -5
  15. foxes/algorithms/downwind/models/init_farm_data.py +58 -28
  16. foxes/algorithms/downwind/models/set_amb_farm_results.py +1 -1
  17. foxes/core/algorithm.py +6 -5
  18. foxes/core/data.py +75 -4
  19. foxes/core/data_calc_model.py +4 -2
  20. foxes/core/engine.py +33 -40
  21. foxes/core/farm_data_model.py +16 -13
  22. foxes/core/model.py +19 -1
  23. foxes/core/point_data_model.py +19 -14
  24. foxes/core/rotor_model.py +1 -0
  25. foxes/core/wake_deflection.py +3 -3
  26. foxes/data/states/point_cloud_100.nc +0 -0
  27. foxes/data/states/weibull_cloud_4.nc +0 -0
  28. foxes/data/states/weibull_grid.nc +0 -0
  29. foxes/engines/dask.py +3 -6
  30. foxes/engines/default.py +2 -2
  31. foxes/engines/numpy.py +11 -10
  32. foxes/engines/pool.py +21 -11
  33. foxes/engines/single.py +8 -6
  34. foxes/input/farm_layout/__init__.py +1 -0
  35. foxes/input/farm_layout/from_arrays.py +68 -0
  36. foxes/input/states/__init__.py +7 -1
  37. foxes/input/states/dataset_states.py +710 -0
  38. foxes/input/states/field_data.py +531 -0
  39. foxes/input/states/multi_height.py +2 -0
  40. foxes/input/states/one_point_flow.py +1 -0
  41. foxes/input/states/point_cloud_data.py +618 -0
  42. foxes/input/states/scan.py +2 -0
  43. foxes/input/states/single.py +2 -0
  44. foxes/input/states/states_table.py +13 -23
  45. foxes/input/states/weibull_sectors.py +182 -77
  46. foxes/input/states/wrg_states.py +1 -1
  47. foxes/input/yaml/dict.py +25 -24
  48. foxes/input/yaml/windio/read_attributes.py +40 -27
  49. foxes/input/yaml/windio/read_farm.py +12 -10
  50. foxes/input/yaml/windio/read_outputs.py +25 -15
  51. foxes/input/yaml/windio/read_site.py +121 -12
  52. foxes/input/yaml/windio/windio.py +22 -10
  53. foxes/input/yaml/yaml.py +1 -0
  54. foxes/models/model_book.py +16 -15
  55. foxes/models/rotor_models/__init__.py +1 -0
  56. foxes/models/rotor_models/centre.py +1 -1
  57. foxes/models/rotor_models/direct_infusion.py +241 -0
  58. foxes/models/turbine_models/calculator.py +16 -3
  59. foxes/models/turbine_models/kTI_model.py +1 -0
  60. foxes/models/turbine_models/lookup_table.py +2 -0
  61. foxes/models/turbine_models/power_mask.py +1 -0
  62. foxes/models/turbine_models/rotor_centre_calc.py +2 -0
  63. foxes/models/turbine_models/sector_management.py +1 -0
  64. foxes/models/turbine_models/set_farm_vars.py +3 -8
  65. foxes/models/turbine_models/table_factors.py +2 -0
  66. foxes/models/turbine_models/thrust2ct.py +1 -0
  67. foxes/models/turbine_models/yaw2yawm.py +2 -0
  68. foxes/models/turbine_models/yawm2yaw.py +2 -0
  69. foxes/models/turbine_types/PCt_file.py +2 -4
  70. foxes/models/turbine_types/PCt_from_two.py +1 -0
  71. foxes/models/turbine_types/__init__.py +1 -0
  72. foxes/models/turbine_types/calculator_type.py +123 -0
  73. foxes/models/turbine_types/null_type.py +1 -0
  74. foxes/models/turbine_types/wsrho2PCt_from_two.py +2 -0
  75. foxes/models/turbine_types/wsti2PCt_from_two.py +3 -1
  76. foxes/output/farm_layout.py +2 -0
  77. foxes/output/farm_results_eval.py +4 -1
  78. foxes/output/flow_plots_2d/flow_plots.py +18 -0
  79. foxes/output/flow_plots_2d/get_fig.py +1 -0
  80. foxes/output/output.py +6 -1
  81. foxes/output/results_writer.py +1 -1
  82. foxes/output/rose_plot.py +10 -0
  83. foxes/output/rotor_point_plots.py +3 -0
  84. foxes/output/state_turbine_map.py +3 -0
  85. foxes/output/turbine_type_curves.py +3 -0
  86. foxes/utils/dict.py +46 -34
  87. foxes/utils/factory.py +2 -2
  88. foxes/utils/xarray_utils.py +20 -12
  89. {foxes-1.4.dist-info → foxes-1.5.1.dist-info}/METADATA +32 -52
  90. {foxes-1.4.dist-info → foxes-1.5.1.dist-info}/RECORD +94 -86
  91. foxes/input/states/field_data_nc.py +0 -833
  92. {foxes-1.4.dist-info → foxes-1.5.1.dist-info}/WHEEL +0 -0
  93. {foxes-1.4.dist-info → foxes-1.5.1.dist-info}/entry_points.txt +0 -0
  94. {foxes-1.4.dist-info → foxes-1.5.1.dist-info}/licenses/LICENSE +0 -0
  95. {foxes-1.4.dist-info → foxes-1.5.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,618 @@
1
+ import numpy as np
2
+ from scipy.interpolate import (
3
+ LinearNDInterpolator,
4
+ NearestNDInterpolator,
5
+ RBFInterpolator,
6
+ )
7
+
8
+ from foxes.config import config
9
+ from foxes.utils import wd2uv, uv2wd, weibull_weights
10
+ import foxes.variables as FV
11
+ import foxes.constants as FC
12
+
13
+ from .dataset_states import DatasetStates
14
+
15
+
16
+ class PointCloudData(DatasetStates):
17
+ """
18
+ Inflow data with point cloud support.
19
+
20
+ Attributes
21
+ ----------
22
+ states_coord: str
23
+ The states coordinate name in the data
24
+ point_coord: str
25
+ The point coordinate name in the data
26
+ x_ncvar: str
27
+ The x variable name in the data
28
+ y_ncvar: str
29
+ The y variable name in the data
30
+ h_ncvar: str, optional
31
+ The height variable name in the data
32
+ weight_ncvar: str, optional
33
+ The name of the weights variable in the data
34
+ interp_method: str
35
+ The interpolation method, "linear", "nearest" or "radialBasisFunction"
36
+ interp_fallback_nearest: bool
37
+ If True, use nearest neighbor interpolation if the
38
+ interpolation method fails.
39
+ interp_pars: dict
40
+ Additional arguments for the interpolation
41
+
42
+ :group: input.states
43
+
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ *args,
49
+ states_coord="Time",
50
+ point_coord="point",
51
+ x_ncvar="x",
52
+ y_ncvar="y",
53
+ h_ncvar=None,
54
+ weight_ncvar=None,
55
+ interp_method="linear",
56
+ interp_fallback_nearest=False,
57
+ interp_pars={},
58
+ **kwargs,
59
+ ):
60
+ """
61
+ Constructor.
62
+
63
+ Parameters
64
+ ----------
65
+ args: tuple, optional
66
+ Arguments for the base class
67
+ states_coord: str
68
+ The states coordinate name in the data
69
+ point_coord: str
70
+ The point coordinate name in the data
71
+ x_ncvar: str
72
+ The x variable name in the data
73
+ y_ncvar: str
74
+ The y variable name in the data
75
+ h_ncvar: str, optional
76
+ The height variable name in the data
77
+ weight_ncvar: str, optional
78
+ The name of the weights variable in the data
79
+ interp_method: str
80
+ The interpolation method, "linear", "nearest" or "radialBasisFunction"
81
+ interp_fallback_nearest: bool
82
+ If True, use nearest neighbor interpolation if the
83
+ interpolation method fails.
84
+ interp_pars: dict
85
+ Additional arguments for the interpolation
86
+ kwargs: dict, optional
87
+ Additional parameters for the base class
88
+
89
+ """
90
+ super().__init__(*args, **kwargs)
91
+
92
+ self.states_coord = states_coord
93
+ self.point_coord = point_coord
94
+ self.x_ncvar = x_ncvar
95
+ self.y_ncvar = y_ncvar
96
+ self.h_ncvar = h_ncvar
97
+ self.weight_ncvar = weight_ncvar
98
+ self.interp_method = interp_method
99
+ self.interp_pars = interp_pars
100
+ self.interp_fallback_nearest = interp_fallback_nearest
101
+
102
+ self.variables = [FV.X, FV.Y]
103
+ self.variables += [v for v in self.ovars if v not in self.fixed_vars]
104
+ self.var2ncvar[FV.X] = x_ncvar
105
+ self.var2ncvar[FV.Y] = y_ncvar
106
+ if weight_ncvar is not None:
107
+ self.var2ncvar[FV.WEIGHT] = weight_ncvar
108
+ self.variables.append(FV.WEIGHT)
109
+ elif FV.WEIGHT in self.var2ncvar:
110
+ raise KeyError(
111
+ f"States '{self.name}': Cannot have '{FV.WEIGHT}' in var2ncvar, use weight_ncvar instead"
112
+ )
113
+
114
+ self._n_pt = None
115
+ self._n_wd = None
116
+ self._n_ws = None
117
+
118
+ if FV.WS not in self.ovars:
119
+ raise ValueError(
120
+ f"States '{self.name}': Expecting output variable '{FV.WS}', got {self.ovars}"
121
+ )
122
+ if FV.WD not in self.ovars:
123
+ raise ValueError(
124
+ f"States '{self.name}': Expecting output variable '{FV.WD}', got {self.ovars}"
125
+ )
126
+ for v in [FV.WEIBULL_A, FV.WEIBULL_k, FV.WEIGHT]:
127
+ if v in self.ovars:
128
+ raise ValueError(
129
+ f"States '{self.name}': Cannot have '{v}' as output variable"
130
+ )
131
+
132
+ self._cmap = {
133
+ FC.STATE: self.states_coord,
134
+ FC.POINT: self.point_coord,
135
+ }
136
+
137
+ def __repr__(self):
138
+ return f"{type(self).__name__}(n_pt={self._n_pt}, n_wd={self._n_wd}, n_ws={self._n_ws})"
139
+
140
+ def _read_ds(self, ds, cmap, variables, verbosity=0):
141
+ """
142
+ Helper function for _get_data, extracts data from the original Dataset.
143
+
144
+ Parameters
145
+ ----------
146
+ ds: xarray.Dataset
147
+ The Dataset to read data from
148
+ cmap: dict
149
+ A mapping from foxes variable names to Dataset dimension names
150
+ variables: list of str
151
+ The variables to extract from the Dataset
152
+ verbosity: int
153
+ The verbosity level, 0 = silent
154
+
155
+ Returns
156
+ -------
157
+ coords: dict
158
+ keys: Foxes variable names, values: 1D coordinate value arrays
159
+ data: dict
160
+ The extracted data, keys are variable names,
161
+ values are tuples (dims, data_array)
162
+ where dims is a tuple of dimension names and
163
+ data_array is a numpy.ndarray with the data values
164
+
165
+ """
166
+ coords, data = super()._read_ds(ds, cmap, variables, verbosity)
167
+
168
+ assert FV.X in data and FV.Y in data, (
169
+ f"States '{self.name}': Expecting variables '{FV.X}' and '{FV.Y}' in data, found {list(data.keys())}"
170
+ )
171
+ assert data[FV.X][0] == (FC.POINT,), (
172
+ f"States '{self.name}': Expecting variable '{FV.X}' to have dimensions '({FC.POINT},)', got {data[FV.X][0]}"
173
+ )
174
+ assert data[FV.Y][0] == (FC.POINT,), (
175
+ f"States '{self.name}': Expecting variable '{FV.Y}' to have dimensions '({FC.POINT},)', got {data[FV.Y][0]}"
176
+ )
177
+ if FV.H in data:
178
+ assert data[FV.H][0] == (FC.POINT,), (
179
+ f"States '{self.name}': Expecting variable '{FV.H}' to have dimensions '({FC.POINT},)', got {data[FV.H][0]}"
180
+ )
181
+
182
+ points = [data.pop(FV.X)[1], data.pop(FV.Y)[1]]
183
+ if FV.H in data:
184
+ points.append(data.pop(FV.H)[1])
185
+ coords[FC.POINT] = np.stack(points, axis=-1)
186
+
187
+ return coords, data
188
+
189
+ def load_data(self, algo, verbosity=0):
190
+ """
191
+ Load and/or create all model data that is subject to chunking.
192
+
193
+ Such data should not be stored under self, for memory reasons. The
194
+ data returned here will automatically be chunked and then provided
195
+ as part of the mdata object during calculations.
196
+
197
+ Parameters
198
+ ----------
199
+ algo: foxes.core.Algorithm
200
+ The calculation algorithm
201
+ verbosity: int
202
+ The verbosity level, 0 = silent
203
+
204
+ Returns
205
+ -------
206
+ idata: dict
207
+ The dict has exactly two entries: `data_vars`,
208
+ a dict with entries `name_str -> (dim_tuple, data_ndarray)`;
209
+ and `coords`, a dict with entries `dim_name_str -> dim_array`
210
+
211
+ """
212
+ return super().load_data(
213
+ algo,
214
+ cmap=self._cmap,
215
+ variables=self.variables,
216
+ bounds_extra_space=None,
217
+ verbosity=verbosity,
218
+ )
219
+
220
+ def calculate(self, algo, mdata, fdata, tdata):
221
+ """
222
+ The main model calculation.
223
+
224
+ This function is executed on a single chunk of data,
225
+ all computations should be based on numpy arrays.
226
+
227
+ Parameters
228
+ ----------
229
+ algo: foxes.core.Algorithm
230
+ The calculation algorithm
231
+ mdata: foxes.core.MData
232
+ The model data
233
+ fdata: foxes.core.FData
234
+ The farm data
235
+ tdata: foxes.core.TData
236
+ The target point data
237
+
238
+ Returns
239
+ -------
240
+ results: dict
241
+ The resulting data, keys: output variable str.
242
+ Values: numpy.ndarray with shape
243
+ (n_states, n_targets, n_tpoints)
244
+
245
+ """
246
+ # prepare
247
+ self.ensure_output_vars(algo, tdata)
248
+ n_states = tdata.n_states
249
+ n_targets = tdata.n_targets
250
+ n_tpoints = tdata.n_tpoints
251
+ n_states = fdata.n_states
252
+ n_pts = n_states * n_targets * n_tpoints
253
+ coords = [self.states_coord, self.point_coord]
254
+
255
+ # get data for calculation
256
+ coords, data, weights = self.get_calc_data(mdata, self._cmap, self.variables)
257
+ coords[FC.STATE] = np.arange(n_states, dtype=config.dtype_int)
258
+
259
+ # interpolate data to points:
260
+ out = {}
261
+ for dims, (vrs, d) in data.items():
262
+ # prepare
263
+ n_vrs = len(vrs)
264
+ qts = coords[FC.POINT]
265
+ n_qts, n_dms = qts.shape
266
+ idims = dims[:-1]
267
+
268
+ if idims == (FC.STATE,):
269
+ for i, v in enumerate(vrs):
270
+ if v in self.ovars:
271
+ out[v] = np.zeros(
272
+ (n_states, n_targets, n_tpoints), dtype=config.dtype_double
273
+ )
274
+ out[v][:] = d[:, None, None, i]
275
+ continue
276
+
277
+ elif idims == (FC.POINT,):
278
+ # prepare grid data
279
+ gts = qts
280
+ n_gts = n_qts
281
+
282
+ # prepare evaluation points
283
+ pts = tdata[FC.TARGETS][..., :n_dms].reshape(n_pts, n_dms)
284
+
285
+ elif idims == (FC.STATE, FC.POINT):
286
+ # prepare grid data, add state index to last axis
287
+ gts = np.zeros((n_qts, n_states, n_dms + 1), dtype=config.dtype_double)
288
+ gts[..., :n_dms] = qts[:, None, :]
289
+ gts[..., n_dms] = np.arange(n_states)[None, :]
290
+ n_gts = n_qts * n_states
291
+ gts = gts.reshape(n_gts, n_dms + 1)
292
+
293
+ # reorder data, first to shape (n_qts, n_states, n_vars),
294
+ # then to (n_gts, n_vrs)
295
+ d = np.swapaxes(d, 0, 1)
296
+ d = d.reshape(n_gts, n_vrs)
297
+
298
+ # prepare evaluation points, add state index to last axis
299
+ pts = np.zeros(
300
+ (n_states, tdata.n_targets, tdata.n_tpoints, n_dms + 1),
301
+ dtype=config.dtype_double,
302
+ )
303
+ pts[..., :n_dms] = tdata[FC.TARGETS][..., :n_dms]
304
+ pts[..., n_dms] = np.arange(n_states)[:, None, None]
305
+ pts = pts.reshape(n_pts, n_dms + 1)
306
+
307
+ else:
308
+ raise ValueError(
309
+ f"States '{self.name}': Unsupported dimensions {dims} for variables {vrs}"
310
+ )
311
+
312
+ # translate (WD, WS) to (U, V):
313
+ if FV.WD in vrs or FV.WS in vrs:
314
+ assert FV.WD in vrs and (FV.WS in vrs or FV.WS in self.fixed_vars), (
315
+ f"States '{self.name}': Missing '{FV.WD}' or '{FV.WS}' in data variables {vrs} for dimensions {dims}"
316
+ )
317
+ iwd = vrs.index(FV.WD)
318
+ iws = vrs.index(FV.WS)
319
+ ws = d[..., iws] if FV.WS in vrs else self.fixed_vars[FV.WS]
320
+ d[..., [iwd, iws]] = wd2uv(d[..., iwd], ws, axis=-1)
321
+ del ws
322
+
323
+ # create interpolator
324
+ if self.interp_method == "linear":
325
+ interp = LinearNDInterpolator(gts, d, **self.interp_pars)
326
+ elif self.interp_method == "nearest":
327
+ interp = NearestNDInterpolator(gts, d, **self.interp_pars)
328
+ elif self.interp_method == "radialBasisFunction":
329
+ pars = {"neighbors": 10}
330
+ pars.update(self.interp_pars)
331
+ interp = RBFInterpolator(gts, d, **pars)
332
+ else:
333
+ raise NotImplementedError(
334
+ f"States '{self.name}': Interpolation method '{self.interp_method}' not implemented, choices are: 'linear', 'nearest', 'radialBasisFunction'"
335
+ )
336
+
337
+ # run interpolation
338
+ ires = interp(pts)
339
+ del interp
340
+
341
+ # check for error:
342
+ sel = np.any(np.isnan(ires), axis=-1)
343
+ if np.any(sel):
344
+ i = np.where(sel)[0]
345
+ if self.interp_fallback_nearest:
346
+ interp = NearestNDInterpolator(gts, d)
347
+ pts = pts[i]
348
+ ires[i] = interp(pts)
349
+ del interp
350
+ else:
351
+ p = pts[i[0], :n_dms]
352
+ qmin = np.min(qts[:, :n_dms], axis=0)
353
+ qmax = np.max(qts[:, :n_dms], axis=0)
354
+ raise ValueError(
355
+ f"States '{self.name}': Interpolation method '{self.interp_method}' failed for {np.sum(sel)} points, e.g. for point {p}, outside of bounds {qmin} - {qmax}"
356
+ )
357
+ del pts, gts, d
358
+
359
+ # translate (U, V) into (WD, WS):
360
+ if FV.WD in vrs:
361
+ uv = ires[..., [iwd, iws]]
362
+ ires[..., iwd] = uv2wd(uv)
363
+ ires[..., iws] = np.linalg.norm(uv, axis=-1)
364
+ del uv
365
+
366
+ # set output:
367
+ for i, v in enumerate(vrs):
368
+ out[v] = ires[..., i].reshape(n_states, n_targets, n_tpoints)
369
+ del ires
370
+
371
+ # set fixed variables:
372
+ for v, d in self.fixed_vars.items():
373
+ out[v] = np.full(
374
+ (n_states, n_targets, n_tpoints), d, dtype=config.dtype_double
375
+ )
376
+
377
+ # add weights:
378
+ if weights is not None:
379
+ tdata[FV.WEIGHT] = weights[:, None, None]
380
+ elif FV.WEIGHT in out:
381
+ tdata[FV.WEIGHT] = out.pop(FV.WEIGHT)
382
+ else:
383
+ tdata[FV.WEIGHT] = np.full(
384
+ (n_states, 1, 1), 1 / self._N, dtype=config.dtype_double
385
+ )
386
+ tdata.dims[FV.WEIGHT] = (FC.STATE, FC.TARGET, FC.TPOINT)
387
+
388
+ return {v: out[v] for v in self.ovars}
389
+
390
+
391
+ class WeibullPointCloud(PointCloudData):
392
+ """
393
+ Weibull sectors at point cloud support, e.g., at turbine locations.
394
+
395
+ Attributes
396
+ ----------
397
+ wd_coord: str
398
+ The wind direction coordinate name
399
+ ws_coord: str
400
+ The wind speed coordinate name, if wind speed bin
401
+ centres are in data, else None
402
+ ws_bins: numpy.ndarray
403
+ The wind speed bins, including
404
+ lower and upper bounds, shape: (n_ws_bins+1,)
405
+
406
+ :group: input.states
407
+
408
+ """
409
+
410
+ def __init__(
411
+ self,
412
+ *args,
413
+ wd_coord,
414
+ ws_coord=None,
415
+ ws_bins=None,
416
+ **kwargs,
417
+ ):
418
+ """
419
+ Constructor.
420
+
421
+ Parameters
422
+ ----------
423
+ args: tuple, optional
424
+ Positional arguments for the base class
425
+ wd_coord: str
426
+ The wind direction coordinate name
427
+ ws_coord: str, optional
428
+ The wind speed coordinate name, if wind speed bin
429
+ centres are in data
430
+ ws_bins: list of float, optional
431
+ The wind speed bins, including
432
+ lower and upper bounds
433
+ kwargs: dict, optional
434
+ Keyword arguments for the base class
435
+
436
+ """
437
+ super().__init__(
438
+ *args,
439
+ states_coord=wd_coord,
440
+ time_format=None,
441
+ load_mode="preload",
442
+ **kwargs,
443
+ )
444
+ self.wd_coord = wd_coord
445
+ self.ws_coord = ws_coord
446
+ self.ws_bins = None if ws_bins is None else np.sort(np.asarray(ws_bins))
447
+
448
+ assert ws_coord is not None or ws_bins is not None, (
449
+ f"States '{self.name}': Expecting either ws_coord or ws_bins"
450
+ )
451
+ assert ws_coord is None or ws_bins is None, (
452
+ f"States '{self.name}': Expecting either ws_coord or ws_bins, not both"
453
+ )
454
+
455
+ if FV.WD not in self.ovars:
456
+ raise ValueError(
457
+ f"States '{self.name}': Expecting output variable '{FV.WD}', got {self.ovars}"
458
+ )
459
+ for v in [FV.WEIBULL_A, FV.WEIBULL_k, FV.WEIGHT]:
460
+ if v in self.ovars:
461
+ raise ValueError(
462
+ f"States '{self.name}': Cannot have '{v}' as output variable"
463
+ )
464
+ if v not in self.variables:
465
+ self.variables.append(v)
466
+
467
+ for v in [FV.WS, FV.WD]:
468
+ if v in self.variables:
469
+ self.variables.remove(v)
470
+
471
+ self._n_wd = None
472
+ self._n_ws = None
473
+
474
+ def __repr__(self):
475
+ return f"{type(self).__name__}(n_wd={self._n_wd}, n_ws={self._n_ws})"
476
+
477
+ def _read_ds(self, ds, cmap, variables, verbosity=0):
478
+ """
479
+ Helper function for _get_data, extracts data from the original Dataset.
480
+
481
+ Parameters
482
+ ----------
483
+ ds: xarray.Dataset
484
+ The Dataset to read data from
485
+ cmap: dict
486
+ A mapping from foxes variable names to Dataset dimension names
487
+ variables: list of str
488
+ The variables to extract from the Dataset
489
+ verbosity: int
490
+ The verbosity level, 0 = silent
491
+
492
+ Returns
493
+ -------
494
+ coords: dict
495
+ keys: Foxes variable names, values: 1D coordinate value arrays
496
+ data: dict
497
+ The extracted data, keys are variable names,
498
+ values are tuples (dims, data_array)
499
+ where dims is a tuple of dimension names and
500
+ data_array is a numpy.ndarray with the data values
501
+
502
+ """
503
+ # read data, using wd_coord as state coordinate
504
+ hcmap = cmap.copy()
505
+ if self.ws_coord is not None:
506
+ hcmap = {FV.WS: self.ws_coord, **cmap}
507
+ coords, data0 = super()._read_ds(ds, hcmap, variables, verbosity)
508
+ wd = coords.pop(FC.STATE)
509
+ wss = coords.pop(FV.WS, None)
510
+
511
+ # replace state by wd coordinate
512
+ data0 = {
513
+ v: (tuple({FC.STATE: FV.WD}.get(c, c) for c in dims), d)
514
+ for v, (dims, d) in data0.items()
515
+ }
516
+
517
+ # check weights
518
+ if FV.WEIGHT not in data0:
519
+ raise KeyError(
520
+ f"States '{self.name}': Missing weights variable '{FV.WEIGHT}' in data, found {sorted(list(data0.keys()))}"
521
+ )
522
+ else:
523
+ dims = data0[FV.WEIGHT][0]
524
+ if FV.WD not in dims:
525
+ raise KeyError(
526
+ f"States '{self.name}': Expecting weights variable '{FV.WEIGHT}' to contain dimension '{FV.WD}', got {dims}"
527
+ )
528
+ if FV.WS in dims:
529
+ raise KeyError(
530
+ f"States '{self.name}': Expecting weights variable '{FV.WEIGHT}' to not contain dimension '{FV.WS}', got {dims}"
531
+ )
532
+
533
+ # construct wind speed bins and bin deltas
534
+ assert FV.WS not in data0, (
535
+ f"States '{self.name}': Cannot have '{FV.WS}' in data, found variables {list(data0.keys())}"
536
+ )
537
+ if self.ws_bins is not None:
538
+ wsb = self.ws_bins
539
+ wss = 0.5 * (wsb[:-1] + wsb[1:])
540
+ elif wss is not None:
541
+ wsb = np.zeros((len(wss) + 1,), dtype=config.dtype_double)
542
+ wsb[1:-1] = 0.5 * (wss[1:] + wss[:-1])
543
+ wsb[0] = wss[0] - 0.5 * wsb[1]
544
+ wsb[-1] = wss[-1] + 0.5 * wsb[-2]
545
+ self.ws_bins = wsb
546
+ else:
547
+ raise ValueError(
548
+ f"States '{self.name}': Expecting ws_bins argument, or '{self.ws_coord}' among data coordinates, got {list(ds.coords.keys())}"
549
+ )
550
+ wsd = wsb[1:] - wsb[:-1]
551
+ n_ws = len(wss)
552
+ n_wd = len(wd)
553
+ del wsb
554
+
555
+ # calculate Weibull weights
556
+ dms = [FV.WS, FV.WD]
557
+ shp = [n_ws, n_wd]
558
+ for v in [FV.WEIBULL_A, FV.WEIBULL_k]:
559
+ if FC.POINT in data0[v][0]:
560
+ dms.append(FC.POINT)
561
+ shp.append(data0[v][1].shape[data0[v][0].index(FC.POINT)])
562
+ break
563
+ dms = tuple(dms)
564
+ shp = tuple(shp)
565
+ if data0[FV.WEIGHT][0] == dms:
566
+ w = data0.pop(FV.WEIGHT)[1]
567
+ else:
568
+ s_w = tuple([np.s_[:] if c in data0[FV.WEIGHT][0] else None for c in dms])
569
+ w = np.zeros(shp, dtype=config.dtype_double)
570
+ w[:] = data0.pop(FV.WEIGHT)[1][s_w]
571
+ s_ws = tuple([np.s_[:], None] + [None] * (len(dms) - 2))
572
+ s_A = tuple([np.s_[:] if c in data0[FV.WEIBULL_A][0] else None for c in dms])
573
+ s_k = tuple([np.s_[:] if c in data0[FV.WEIBULL_A][0] else None for c in dms])
574
+ data0[FV.WEIGHT] = (
575
+ dms,
576
+ w
577
+ * weibull_weights(
578
+ ws=wss[s_ws],
579
+ ws_deltas=wsd[s_ws],
580
+ A=data0.pop(FV.WEIBULL_A)[1][s_A],
581
+ k=data0.pop(FV.WEIBULL_k)[1][s_k],
582
+ ),
583
+ )
584
+ del w, s_ws, s_A, s_k
585
+
586
+ # translate binned data to states
587
+ self._N = n_ws * n_wd
588
+ self._inds = None
589
+ data = {
590
+ FV.WS: np.zeros((n_ws, n_wd), dtype=config.dtype_double),
591
+ FV.WD: np.zeros((n_ws, n_wd), dtype=config.dtype_double),
592
+ }
593
+ data[FV.WS][:] = wss[:, None]
594
+ data[FV.WD][:] = wd[None, :]
595
+ data[FV.WS] = ((FC.STATE,), data[FV.WS].reshape(self._N))
596
+ data[FV.WD] = ((FC.STATE,), data[FV.WD].reshape(self._N))
597
+ for v in list(data0.keys()):
598
+ dims, d = data0.pop(v)
599
+ if len(dims) >= 2 and dims[:2] == (FV.WS, FV.WD):
600
+ dms = tuple([FC.STATE] + list(dims[2:]))
601
+ shp = [self._N] + list(d.shape[2:])
602
+ data[v] = (dms, d.reshape(shp))
603
+ elif dims[0] == FV.WD:
604
+ dms = tuple([FC.STATE] + list(dims[1:]))
605
+ shp = [n_ws] + list(d.shape)
606
+ data[v] = np.zeros(shp, dtype=config.dtype_double)
607
+ data[v][:] = d[None, ...]
608
+ data[v] = (dms, data[v].reshape([self._N] + shp[2:]))
609
+ elif dims[0] == FV.WS:
610
+ dms = tuple([FC.STATE] + list(dims[1:]))
611
+ shp = [n_ws, n_wd] + list(d.shape[2:])
612
+ data[v] = np.zeros(shp, dtype=config.dtype_double)
613
+ data[v][:] = d[:, None, ...]
614
+ data[v] = (dms, data[v].reshape([self._N] + shp[2:]))
615
+ else:
616
+ data[v] = (dims, d)
617
+
618
+ return coords, data
@@ -202,6 +202,8 @@ class ScanStates(States):
202
202
  (n_states, n_targets, n_tpoints)
203
203
 
204
204
  """
205
+ self.ensure_output_vars(algo, tdata)
206
+
205
207
  for i, v in enumerate(self._vars):
206
208
  if v not in tdata:
207
209
  tdata[v] = np.zeros_like(tdata[FC.TARGETS][..., 0])
@@ -183,6 +183,8 @@ class SingleStateStates(States):
183
183
  (n_states, n_targets, n_tpoints)
184
184
 
185
185
  """
186
+ self.ensure_output_vars(algo, tdata)
187
+
186
188
  if self.ws is not None:
187
189
  tdata[FV.WS] = np.full(
188
190
  (tdata.n_states, tdata.n_targets, tdata.n_tpoints),