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.
- examples/quickstart/run.py +17 -0
- foxes/__init__.py +1 -1
- foxes/algorithms/downwind/downwind.py +9 -15
- foxes/algorithms/downwind/models/farm_wakes_calc.py +13 -7
- foxes/algorithms/downwind/models/init_farm_data.py +4 -4
- foxes/algorithms/downwind/models/reorder_farm_output.py +5 -1
- foxes/algorithms/downwind/models/set_amb_point_results.py +1 -1
- foxes/algorithms/iterative/models/farm_wakes_calc.py +6 -3
- foxes/algorithms/sequential/models/seq_state.py +0 -18
- foxes/algorithms/sequential/sequential.py +5 -18
- foxes/constants.py +6 -0
- foxes/core/data.py +44 -18
- foxes/core/engine.py +19 -1
- foxes/core/farm_data_model.py +1 -0
- foxes/core/rotor_model.py +42 -38
- foxes/core/states.py +2 -47
- foxes/input/states/__init__.py +1 -0
- foxes/input/states/field_data_nc.py +39 -61
- foxes/input/states/multi_height.py +35 -58
- foxes/input/states/one_point_flow.py +22 -21
- foxes/input/states/scan.py +6 -19
- foxes/input/states/single.py +5 -17
- foxes/input/states/states_table.py +19 -41
- foxes/input/states/wrg_states.py +301 -0
- foxes/models/partial_wakes/rotor_points.py +8 -2
- foxes/models/partial_wakes/segregated.py +9 -4
- foxes/models/rotor_models/centre.py +6 -4
- foxes/models/wake_frames/seq_dynamic_wakes.py +5 -2
- foxes/models/wake_frames/timelines.py +10 -0
- foxes/models/wake_models/induction/vortex_sheet.py +6 -9
- foxes/output/farm_layout.py +12 -4
- foxes/output/farm_results_eval.py +36 -12
- foxes/output/rose_plot.py +20 -2
- foxes/output/slice_data.py +16 -19
- foxes/utils/wrg_utils.py +84 -1
- {foxes-1.2.4.dist-info → foxes-1.3.dist-info}/METADATA +12 -8
- {foxes-1.2.4.dist-info → foxes-1.3.dist-info}/RECORD +54 -52
- {foxes-1.2.4.dist-info → foxes-1.3.dist-info}/WHEEL +1 -1
- tests/0_consistency/iterative/test_iterative.py +2 -3
- tests/0_consistency/partial_wakes/test_partial_wakes.py +2 -2
- tests/1_verification/flappy_0_6/PCt_files/test_PCt_files.py +48 -56
- tests/1_verification/flappy_0_6/abl_states/test_abl_states.py +33 -36
- tests/1_verification/flappy_0_6/row_Jensen_linear_centre/test_row_Jensen_linear_centre.py +3 -2
- tests/1_verification/flappy_0_6/row_Jensen_linear_tophat/test_row_Jensen_linear_tophat.py +3 -3
- tests/1_verification/flappy_0_6/row_Jensen_linear_tophat_IECTI2005/test_row_Jensen_linear_tophat_IECTI_2005.py +3 -3
- tests/1_verification/flappy_0_6/row_Jensen_linear_tophat_IECTI2019/test_row_Jensen_linear_tophat_IECTI_2019.py +3 -3
- tests/1_verification/flappy_0_6/row_Jensen_quadratic_centre/test_row_Jensen_quadratic_centre.py +3 -3
- tests/1_verification/flappy_0_6_2/grid_rotors/test_grid_rotors.py +3 -3
- tests/1_verification/flappy_0_6_2/row_Bastankhah_Crespo/test_row_Bastankhah_Crespo.py +3 -2
- tests/1_verification/flappy_0_6_2/row_Bastankhah_linear_centre/test_row_Bastankhah_linear_centre.py +3 -3
- tests/3_examples/test_examples.py +3 -2
- {foxes-1.2.4.dist-info → foxes-1.3.dist-info}/LICENSE +0 -0
- {foxes-1.2.4.dist-info → foxes-1.3.dist-info}/entry_points.txt +0 -0
- {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 = {
|
|
95
|
-
|
|
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 = {
|
|
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(
|
|
151
|
-
|
|
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
|
-
|
|
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(
|
|
127
|
+
if len(rpoint_weights) > 1:
|
|
128
128
|
return super().eval_rpoint_results(
|
|
129
|
-
algo, mdata, fdata, rpoint_results,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
foxes/output/farm_layout.py
CHANGED
|
@@ -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
|
-
|
|
233
|
-
|
|
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
|
|
80
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
100
|
+
if np.any(nas):
|
|
101
|
+
sel = ~np.any(nas, axis=1)
|
|
102
|
+
fields = [f[sel] for f in fields]
|
|
85
103
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|