pyelq 1.1.3__py3-none-any.whl → 1.2.0__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.
- pyelq/__init__.py +1 -0
- pyelq/component/__init__.py +1 -0
- pyelq/component/background.py +19 -13
- pyelq/component/component.py +2 -1
- pyelq/component/error_model.py +2 -1
- pyelq/component/offset.py +2 -1
- pyelq/component/source_model.py +78 -29
- pyelq/coordinate_system.py +1 -0
- pyelq/data_access/__init__.py +1 -0
- pyelq/data_access/data_access.py +1 -1
- pyelq/dispersion_model/__init__.py +4 -3
- pyelq/dispersion_model/dispersion_model.py +202 -0
- pyelq/dispersion_model/finite_volume.py +1084 -0
- pyelq/dispersion_model/gaussian_plume.py +8 -189
- pyelq/dispersion_model/site_layout.py +97 -0
- pyelq/dlm.py +11 -15
- pyelq/gas_species.py +1 -0
- pyelq/meteorology/__init__.py +6 -0
- pyelq/{meteorology.py → meteorology/meteorology.py} +388 -387
- pyelq/meteorology/meteorology_windfield.py +180 -0
- pyelq/model.py +2 -1
- pyelq/plotting/__init__.py +1 -0
- pyelq/plotting/plot.py +16 -25
- pyelq/preprocessing.py +98 -38
- pyelq/sensor/__init__.py +1 -0
- pyelq/sensor/sensor.py +70 -5
- pyelq/source_map.py +1 -0
- pyelq/support_functions/__init__.py +1 -0
- pyelq/support_functions/post_processing.py +1 -0
- pyelq/support_functions/spatio_temporal_interpolation.py +1 -0
- {pyelq-1.1.3.dist-info → pyelq-1.2.0.dist-info}/METADATA +45 -44
- pyelq-1.2.0.dist-info/RECORD +37 -0
- {pyelq-1.1.3.dist-info → pyelq-1.2.0.dist-info}/WHEEL +1 -1
- pyelq-1.1.3.dist-info/RECORD +0 -32
- {pyelq-1.1.3.dist-info → pyelq-1.2.0.dist-info/licenses}/LICENSE.md +0 -0
- {pyelq-1.1.3.dist-info → pyelq-1.2.0.dist-info/licenses}/LICENSES/Apache-2.0.txt +0 -0
|
@@ -1,387 +1,388 @@
|
|
|
1
|
-
# SPDX-FileCopyrightText:
|
|
2
|
-
#
|
|
3
|
-
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
|
|
5
|
-
# -*- coding: utf-8 -*-
|
|
6
|
-
"""Meteorology module.
|
|
7
|
-
|
|
8
|
-
The superclass for the meteorology classes
|
|
9
|
-
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import
|
|
16
|
-
import
|
|
17
|
-
import plotly.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
from pyelq.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
self.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
the
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
dataframe
|
|
143
|
-
dataframe["
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
dataframe = dataframe.
|
|
153
|
-
dataframe
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
dataframe
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
"
|
|
172
|
-
"
|
|
173
|
-
|
|
174
|
-
"
|
|
175
|
-
"
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
fig.update_layout(
|
|
198
|
-
fig.update_layout(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
+ f"{
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
for
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
for
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
for
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
f"
|
|
315
|
-
f"
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
"
|
|
357
|
-
"
|
|
358
|
-
|
|
359
|
-
"
|
|
360
|
-
"
|
|
361
|
-
"
|
|
362
|
-
"
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
fig.update_layout(
|
|
386
|
-
fig.update_layout(
|
|
387
|
-
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Shell Global Solutions International B.V. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
# -*- coding: utf-8 -*-
|
|
6
|
+
"""Meteorology module.
|
|
7
|
+
|
|
8
|
+
The superclass for the meteorology classes
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import warnings
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pandas as pd
|
|
17
|
+
import plotly.express as px
|
|
18
|
+
import plotly.graph_objects as go
|
|
19
|
+
from pandas.arrays import DatetimeArray
|
|
20
|
+
|
|
21
|
+
from pyelq.coordinate_system import Coordinate
|
|
22
|
+
from pyelq.sensor.sensor import SensorGroup
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Meteorology:
|
|
27
|
+
"""Defines the properties and methods of the meteorology class.
|
|
28
|
+
|
|
29
|
+
Sizes of all attributes should match.
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
wind_speed (np.ndarray, optional): Wind speed [m/s]
|
|
33
|
+
wind_direction (np.ndarray, optional): Meteorological wind direction (from) [deg], see
|
|
34
|
+
https://confluence.ecmwf.int/pages/viewpage.action?pageId=133262398
|
|
35
|
+
u_component (np.ndarray, optional): u component of wind [m/s] in the easterly direction
|
|
36
|
+
v_component (np.ndarray, optional): v component of wind [m/s] in the northerly direction
|
|
37
|
+
w_component (np.ndarray, optional): w component of wind [m/s] in the vertical direction
|
|
38
|
+
wind_turbulence_horizontal (np.ndarray, optional): Parameter of the wind stability in
|
|
39
|
+
horizontal direction [deg]
|
|
40
|
+
wind_turbulence_vertical (np.ndarray, optional): Parameter of the wind stability in
|
|
41
|
+
vertical direction [deg]
|
|
42
|
+
pressure (np.ndarray, optional): Pressure [kPa]
|
|
43
|
+
temperature (np.ndarray, optional): Temperature [K]
|
|
44
|
+
atmospheric_boundary_layer (np.ndarray, optional): Atmospheric boundary layer [m]
|
|
45
|
+
surface_albedo (np.ndarray, optional): Surface reflectance parameter [unitless]
|
|
46
|
+
time (pandas.arrays.DatetimeArray, optional): Array containing time values associated with the
|
|
47
|
+
meteorological observation
|
|
48
|
+
location: (Coordinate, optional): Coordinate object specifying the meteorological observation locations
|
|
49
|
+
label (str, optional): String label for object
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
wind_speed: np.ndarray = field(init=False, default=None)
|
|
54
|
+
wind_direction: np.ndarray = field(init=False, default=None)
|
|
55
|
+
u_component: np.ndarray = field(init=False, default=None)
|
|
56
|
+
v_component: np.ndarray = field(init=False, default=None)
|
|
57
|
+
w_component: np.ndarray = field(init=False, default=None)
|
|
58
|
+
wind_turbulence_horizontal: np.ndarray = field(init=False, default=None)
|
|
59
|
+
wind_turbulence_vertical: np.ndarray = field(init=False, default=None)
|
|
60
|
+
pressure: np.ndarray = field(init=False, default=None)
|
|
61
|
+
temperature: np.ndarray = field(init=False, default=None)
|
|
62
|
+
atmospheric_boundary_layer: np.ndarray = field(init=False, default=None)
|
|
63
|
+
surface_albedo: np.ndarray = field(init=False, default=None)
|
|
64
|
+
time: DatetimeArray = field(init=False, default=None)
|
|
65
|
+
location: Coordinate = field(init=False, default=None)
|
|
66
|
+
label: str = field(init=False)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def nof_observations(self) -> int:
|
|
70
|
+
"""Number of observations."""
|
|
71
|
+
if self.time is None:
|
|
72
|
+
return 0
|
|
73
|
+
return self.time.size
|
|
74
|
+
|
|
75
|
+
def calculate_wind_speed_from_uv(self) -> None:
|
|
76
|
+
"""Calculate wind speed.
|
|
77
|
+
|
|
78
|
+
Calculate the wind speed from u and v components. Result gets stored in the wind_speed attribute
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
self.wind_speed = np.sqrt(self.u_component**2 + self.v_component**2)
|
|
82
|
+
|
|
83
|
+
def calculate_wind_direction_from_uv(self) -> None:
|
|
84
|
+
"""Calculate wind direction: meteorological convention 0 is wind from the North.
|
|
85
|
+
|
|
86
|
+
Calculate the wind direction from u and v components. Result gets stored in the wind_direction attribute
|
|
87
|
+
See: https://confluence.ecmwf.int/pages/viewpage.action?pageId=133262398
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
self.wind_direction = (270 - 180 / np.pi * np.arctan2(self.v_component, self.u_component)) % 360
|
|
91
|
+
|
|
92
|
+
def calculate_uv_from_wind_speed_direction(self) -> None:
|
|
93
|
+
"""Calculate u and v components from wind speed and direction.
|
|
94
|
+
|
|
95
|
+
Results get stored in the u_component and v_component attributes.
|
|
96
|
+
See: https://confluence.ecmwf.int/pages/viewpage.action?pageId=133262398
|
|
97
|
+
|
|
98
|
+
"""
|
|
99
|
+
self.u_component = -1 * self.wind_speed * np.sin(self.wind_direction * (np.pi / 180))
|
|
100
|
+
self.v_component = -1 * self.wind_speed * np.cos(self.wind_direction * (np.pi / 180))
|
|
101
|
+
|
|
102
|
+
def calculate_wind_turbulence_horizontal(self, window: str) -> None:
|
|
103
|
+
"""Calculate the horizontal wind turbulence values from the wind direction attribute.
|
|
104
|
+
|
|
105
|
+
Wind turbulence values are calculated as the circular standard deviation of wind direction
|
|
106
|
+
(https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.circstd.html).
|
|
107
|
+
The implementation here is equivalent to using the circstd function from scipy.stats as an apply
|
|
108
|
+
function on a rolling window. However, using the rolling mean on sin and cos speeds up
|
|
109
|
+
the calculation by a factor of 100.
|
|
110
|
+
|
|
111
|
+
Outputted values are calculated at the center of the window and at least 3 observations are required in a
|
|
112
|
+
window for the calculation. If the window contains less values the result will be np.nan.
|
|
113
|
+
The result of the calculation will be stored as the wind_turbulence_horizontal attribute.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
window (str): The size of the window in which values are aggregated specified as an offset alias:
|
|
117
|
+
https://pandas.pydata.org/docs/user_guide/timeseries.html#timeseries-offset-aliases
|
|
118
|
+
|
|
119
|
+
"""
|
|
120
|
+
data_series = pd.Series(data=self.wind_direction, index=self.time)
|
|
121
|
+
sin_rolling = (np.sin(data_series * np.pi / 180)).rolling(window=window, center=True, min_periods=3).mean()
|
|
122
|
+
cos_rolling = (np.cos(data_series * np.pi / 180)).rolling(window=window, center=True, min_periods=3).mean()
|
|
123
|
+
aggregated_data = np.sqrt(-2 * np.log((sin_rolling**2 + cos_rolling**2) ** 0.5)) * 180 / np.pi
|
|
124
|
+
self.wind_turbulence_horizontal = aggregated_data.values
|
|
125
|
+
|
|
126
|
+
def plot_polar_hist(self, nof_sectors: int = 16, nof_divisions: int = 5, template: object = None) -> go.Figure():
|
|
127
|
+
"""Plots a histogram of wind speed and wind direction in polar Coordinates.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
nof_sectors (int, optional): The number of wind direction sectors into which the data is binned.
|
|
131
|
+
nof_divisions (int, optional): The number of wind speed divisions into which the data is binned.
|
|
132
|
+
template (object): A layout template which can be applied to the plot. Defaults to None.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
fig (go.Figure): A plotly go figure containing the trace of the rose plot.
|
|
136
|
+
|
|
137
|
+
"""
|
|
138
|
+
sector_half_width = 0.5 * (360 / nof_sectors)
|
|
139
|
+
wind_direction_bin_edges = np.linspace(-sector_half_width, 360 - sector_half_width, nof_sectors + 1)
|
|
140
|
+
wind_speed_bin_edges = np.linspace(np.min(self.wind_speed), np.max(self.wind_speed), nof_divisions)
|
|
141
|
+
|
|
142
|
+
dataframe = pd.DataFrame()
|
|
143
|
+
dataframe["wind_direction"] = [x - 360 if x > (360 - sector_half_width) else x for x in self.wind_direction]
|
|
144
|
+
dataframe["wind_speed"] = self.wind_speed
|
|
145
|
+
|
|
146
|
+
dataframe["sector"] = pd.cut(dataframe["wind_direction"], wind_direction_bin_edges, include_lowest=True)
|
|
147
|
+
if np.allclose(wind_speed_bin_edges[0], wind_speed_bin_edges):
|
|
148
|
+
dataframe["speed"] = wind_speed_bin_edges[0]
|
|
149
|
+
else:
|
|
150
|
+
dataframe["speed"] = pd.cut(dataframe["wind_speed"], wind_speed_bin_edges, include_lowest=True)
|
|
151
|
+
|
|
152
|
+
dataframe = dataframe.groupby(["sector", "speed"], observed=False).count()
|
|
153
|
+
dataframe = dataframe.rename(columns={"wind_speed": "count"}).drop(columns=["wind_direction"])
|
|
154
|
+
dataframe["%"] = dataframe["count"] / dataframe["count"].sum()
|
|
155
|
+
|
|
156
|
+
dataframe = dataframe.reset_index()
|
|
157
|
+
dataframe["theta"] = dataframe.apply(lambda x: x["sector"].mid, axis=1)
|
|
158
|
+
|
|
159
|
+
fig = px.bar_polar(
|
|
160
|
+
dataframe,
|
|
161
|
+
r="%",
|
|
162
|
+
theta="theta",
|
|
163
|
+
color="speed",
|
|
164
|
+
direction="clockwise",
|
|
165
|
+
start_angle=90,
|
|
166
|
+
color_discrete_sequence=px.colors.sequential.Sunset_r,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
ticktext = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
|
|
170
|
+
polar_dict = {
|
|
171
|
+
"radialaxis": {"tickangle": 90},
|
|
172
|
+
"radialaxis_angle": 90,
|
|
173
|
+
"angularaxis": {
|
|
174
|
+
"tickmode": "array",
|
|
175
|
+
"ticktext": ticktext,
|
|
176
|
+
"tickvals": list(np.linspace(0, 360 - (360 / 8), 8)),
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
fig.add_annotation(
|
|
180
|
+
x=1,
|
|
181
|
+
y=1,
|
|
182
|
+
yref="paper",
|
|
183
|
+
xref="paper",
|
|
184
|
+
xanchor="right",
|
|
185
|
+
yanchor="top",
|
|
186
|
+
align="left",
|
|
187
|
+
font={"size": 18, "color": "#000000"},
|
|
188
|
+
showarrow=False,
|
|
189
|
+
borderwidth=2,
|
|
190
|
+
borderpad=10,
|
|
191
|
+
bgcolor="#ffffff",
|
|
192
|
+
bordercolor="#000000",
|
|
193
|
+
opacity=0.8,
|
|
194
|
+
text="<b>Radial Axis:</b> Proportion<br>of wind measurements<br>in a given direction.",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
fig.update_layout(polar=polar_dict)
|
|
198
|
+
fig.update_layout(template=template)
|
|
199
|
+
fig.update_layout(title="Distribution of Wind Speeds and Directions")
|
|
200
|
+
|
|
201
|
+
return fig
|
|
202
|
+
|
|
203
|
+
def plot_polar_scatter(self, fig: go.Figure, sensor_object: SensorGroup, template: object = None) -> go.Figure():
|
|
204
|
+
"""Plots a scatter plot of concentration with respect to wind direction in polar Coordinates.
|
|
205
|
+
|
|
206
|
+
This function implements the polar scatter functionality for a (single) Meteorology object. Assuming the all
|
|
207
|
+
Sensors in the SensorGroup are consistent with the Meteorology object.
|
|
208
|
+
|
|
209
|
+
Note we do plot the sensors which do not contain any values when present in the SensorGroup to keep consistency
|
|
210
|
+
in plot colors.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
fig (go.Figure): A plotly figure onto which traces can be drawn.
|
|
214
|
+
sensor_object (SensorGroup): SensorGroup object which contains the concentration information
|
|
215
|
+
template (object): A layout template which can be applied to the plot. Defaults to None.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
fig (go.Figure): A plotly go figure containing the trace of the rose plot.
|
|
219
|
+
|
|
220
|
+
"""
|
|
221
|
+
max_concentration = 0
|
|
222
|
+
|
|
223
|
+
for i, (sensor_key, sensor) in enumerate(sensor_object.items()):
|
|
224
|
+
if sensor.concentration.shape != self.wind_direction.shape:
|
|
225
|
+
warnings.warn(
|
|
226
|
+
f"Concentration values for sensor {sensor_key} are of shape "
|
|
227
|
+
+ f"{sensor.concentration.shape}, but self.wind_direction has shape "
|
|
228
|
+
+ f"{self.wind_direction.shape}. It will not be plotted on the polar scatter plot."
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
theta = self.wind_direction
|
|
232
|
+
color_idx = i % len(sensor_object.color_map)
|
|
233
|
+
|
|
234
|
+
fig.add_trace(
|
|
235
|
+
go.Scatterpolar(
|
|
236
|
+
r=sensor.concentration,
|
|
237
|
+
theta=theta,
|
|
238
|
+
mode="markers",
|
|
239
|
+
name=sensor_key,
|
|
240
|
+
marker={"color": sensor_object.color_map[color_idx]},
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
if sensor.concentration.size > 0:
|
|
244
|
+
max_concentration = np.maximum(np.nanmax(sensor.concentration), max_concentration)
|
|
245
|
+
|
|
246
|
+
fig = set_plot_polar_scatter_layout(max_concentration=max_concentration, fig=fig, template=template)
|
|
247
|
+
|
|
248
|
+
return fig
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@dataclass
|
|
252
|
+
class MeteorologyGroup(dict):
|
|
253
|
+
"""A dictionary containing multiple Meteorology objects.
|
|
254
|
+
|
|
255
|
+
This class is used when we want to define/store a collection of meteorology objects consistent with an associated
|
|
256
|
+
SensorGroup which can then be used in further processing, e.g. Gaussian plume coupling computation.
|
|
257
|
+
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def nof_objects(self) -> int:
|
|
262
|
+
"""Int: Number of meteorology objects contained in the MeteorologyGroup."""
|
|
263
|
+
return len(self)
|
|
264
|
+
|
|
265
|
+
def add_object(self, met_object: Meteorology):
|
|
266
|
+
"""Add an object to the MeteorologyGroup."""
|
|
267
|
+
self[met_object.label] = met_object
|
|
268
|
+
|
|
269
|
+
def calculate_uv_from_wind_speed_direction(self):
|
|
270
|
+
"""Calculate the u and v components for each member of the group."""
|
|
271
|
+
for met in self.values():
|
|
272
|
+
met.calculate_uv_from_wind_speed_direction()
|
|
273
|
+
|
|
274
|
+
def calculate_wind_direction_from_uv(self):
|
|
275
|
+
"""Calculate wind direction from the u and v components for each member of the group."""
|
|
276
|
+
for met in self.values():
|
|
277
|
+
met.calculate_wind_direction_from_uv()
|
|
278
|
+
|
|
279
|
+
def calculate_wind_speed_from_uv(self):
|
|
280
|
+
"""Calculate wind speed from the u and v components for each member of the group."""
|
|
281
|
+
for met in self.values():
|
|
282
|
+
met.calculate_wind_speed_from_uv()
|
|
283
|
+
|
|
284
|
+
def plot_polar_scatter(self, fig: go.Figure, sensor_object: SensorGroup, template: object = None) -> go.Figure():
|
|
285
|
+
"""Plots a scatter plot of concentration with respect to wind direction in polar coordinates.
|
|
286
|
+
|
|
287
|
+
This function implements the polar scatter functionality for a MeteorologyGroup object. It assumes each object
|
|
288
|
+
in the SensorGroup has an associated Meteorology object in the MeteorologyGroup.
|
|
289
|
+
|
|
290
|
+
Note we do plot the sensors which do not contain any values when present in the SensorGroup to keep consistency
|
|
291
|
+
in plot colors.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
fig (go.Figure): A plotly figure onto which traces can be drawn.
|
|
295
|
+
sensor_object (SensorGroup): SensorGroup object which contains the concentration information
|
|
296
|
+
template (object): A layout template which can be applied to the plot. Defaults to None.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
fig (go.Figure): A plotly go figure containing the trace of the rose plot.
|
|
300
|
+
|
|
301
|
+
Raises
|
|
302
|
+
ValueError: When there is a sensor key which is not present in the MeteorologyGroup.
|
|
303
|
+
|
|
304
|
+
"""
|
|
305
|
+
max_concentration = 0
|
|
306
|
+
|
|
307
|
+
for i, (sensor_key, sensor) in enumerate(sensor_object.items()):
|
|
308
|
+
if sensor_key not in self.keys():
|
|
309
|
+
raise ValueError(f"Key {sensor_key} not found in MeteorologyGroup.")
|
|
310
|
+
temp_met_object = self[sensor_key]
|
|
311
|
+
if sensor.concentration.shape != temp_met_object.wind_direction.shape:
|
|
312
|
+
warnings.warn(
|
|
313
|
+
f"Concentration values for sensor {sensor_key} are of shape "
|
|
314
|
+
+ f"{sensor.concentration.shape}, but wind_direction values for meteorology object {sensor_key} "
|
|
315
|
+
f"has shape {temp_met_object.wind_direction.shape}. It will not be plotted on the polar scatter "
|
|
316
|
+
f"plot."
|
|
317
|
+
)
|
|
318
|
+
else:
|
|
319
|
+
theta = temp_met_object.wind_direction
|
|
320
|
+
color_idx = i % len(sensor_object.color_map)
|
|
321
|
+
|
|
322
|
+
fig.add_trace(
|
|
323
|
+
go.Scatterpolar(
|
|
324
|
+
r=sensor.concentration,
|
|
325
|
+
theta=theta,
|
|
326
|
+
mode="markers",
|
|
327
|
+
name=sensor_key,
|
|
328
|
+
marker={"color": sensor_object.color_map[color_idx]},
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
if sensor.concentration.size > 0:
|
|
333
|
+
max_concentration = np.maximum(np.nanmax(sensor.concentration), max_concentration)
|
|
334
|
+
|
|
335
|
+
fig = set_plot_polar_scatter_layout(max_concentration=max_concentration, fig=fig, template=template)
|
|
336
|
+
|
|
337
|
+
return fig
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def set_plot_polar_scatter_layout(max_concentration: float, fig: go.Figure(), template: object) -> go.Figure:
|
|
341
|
+
"""Helper function to set the layout of the polar scatter plot.
|
|
342
|
+
|
|
343
|
+
Helps avoid code duplication.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
max_concentration (float): The maximum concentration value used to update radial axis range.
|
|
347
|
+
fig (go.Figure): A plotly figure onto which traces can be drawn.
|
|
348
|
+
template (object): A layout template which can be applied to the plot.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
fig (go.Figure): A plotly go figure containing the trace of the rose plot.
|
|
352
|
+
|
|
353
|
+
"""
|
|
354
|
+
ticktext = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
|
|
355
|
+
polar_dict = {
|
|
356
|
+
"radialaxis": {"tickangle": 0, "range": [0.0, 1.01 * max_concentration]},
|
|
357
|
+
"radialaxis_angle": 0,
|
|
358
|
+
"angularaxis": {
|
|
359
|
+
"tickmode": "array",
|
|
360
|
+
"ticktext": ticktext,
|
|
361
|
+
"direction": "clockwise",
|
|
362
|
+
"rotation": 90,
|
|
363
|
+
"tickvals": list(np.linspace(0, 360 - (360 / 8), 8)),
|
|
364
|
+
},
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
fig.add_annotation(
|
|
368
|
+
x=1,
|
|
369
|
+
y=1,
|
|
370
|
+
yref="paper",
|
|
371
|
+
xref="paper",
|
|
372
|
+
xanchor="right",
|
|
373
|
+
yanchor="top",
|
|
374
|
+
align="left",
|
|
375
|
+
font={"size": 18, "color": "#000000"},
|
|
376
|
+
showarrow=False,
|
|
377
|
+
borderwidth=2,
|
|
378
|
+
borderpad=10,
|
|
379
|
+
bgcolor="#ffffff",
|
|
380
|
+
bordercolor="#000000",
|
|
381
|
+
opacity=0.8,
|
|
382
|
+
text="<b>Radial Axis:</b> Wind<br>speed in m/s.",
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
fig.update_layout(polar=polar_dict)
|
|
386
|
+
fig.update_layout(template=template)
|
|
387
|
+
fig.update_layout(title="Measured Concentration against Wind Direction.")
|
|
388
|
+
return fig
|