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