tobac 1.6.2__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.
- tobac/__init__.py +112 -0
- tobac/analysis/__init__.py +31 -0
- tobac/analysis/cell_analysis.py +628 -0
- tobac/analysis/feature_analysis.py +212 -0
- tobac/analysis/spatial.py +619 -0
- tobac/centerofgravity.py +226 -0
- tobac/feature_detection.py +1758 -0
- tobac/merge_split.py +324 -0
- tobac/plotting.py +2321 -0
- tobac/segmentation/__init__.py +10 -0
- tobac/segmentation/watershed_segmentation.py +1316 -0
- tobac/testing.py +1179 -0
- tobac/tests/segmentation_tests/test_iris_xarray_segmentation.py +0 -0
- tobac/tests/segmentation_tests/test_segmentation.py +1183 -0
- tobac/tests/segmentation_tests/test_segmentation_time_pad.py +104 -0
- tobac/tests/test_analysis_spatial.py +1109 -0
- tobac/tests/test_convert.py +265 -0
- tobac/tests/test_datetime.py +216 -0
- tobac/tests/test_decorators.py +148 -0
- tobac/tests/test_feature_detection.py +1321 -0
- tobac/tests/test_generators.py +273 -0
- tobac/tests/test_import.py +24 -0
- tobac/tests/test_iris_xarray_match_utils.py +244 -0
- tobac/tests/test_merge_split.py +351 -0
- tobac/tests/test_pbc_utils.py +497 -0
- tobac/tests/test_sample_data.py +197 -0
- tobac/tests/test_testing.py +747 -0
- tobac/tests/test_tracking.py +714 -0
- tobac/tests/test_utils.py +650 -0
- tobac/tests/test_utils_bulk_statistics.py +789 -0
- tobac/tests/test_utils_coordinates.py +328 -0
- tobac/tests/test_utils_internal.py +97 -0
- tobac/tests/test_xarray_utils.py +232 -0
- tobac/tracking.py +613 -0
- tobac/utils/__init__.py +27 -0
- tobac/utils/bulk_statistics.py +360 -0
- tobac/utils/datetime.py +184 -0
- tobac/utils/decorators.py +540 -0
- tobac/utils/general.py +753 -0
- tobac/utils/generators.py +87 -0
- tobac/utils/internal/__init__.py +2 -0
- tobac/utils/internal/coordinates.py +430 -0
- tobac/utils/internal/iris_utils.py +462 -0
- tobac/utils/internal/label_props.py +82 -0
- tobac/utils/internal/xarray_utils.py +439 -0
- tobac/utils/mask.py +364 -0
- tobac/utils/periodic_boundaries.py +419 -0
- tobac/wrapper.py +244 -0
- tobac-1.6.2.dist-info/METADATA +154 -0
- tobac-1.6.2.dist-info/RECORD +53 -0
- tobac-1.6.2.dist-info/WHEEL +5 -0
- tobac-1.6.2.dist-info/licenses/LICENSE +29 -0
- tobac-1.6.2.dist-info/top_level.txt +1 -0
tobac/testing.py
ADDED
|
@@ -0,0 +1,1179 @@
|
|
|
1
|
+
"""Containing methods to make simple sample data for testing."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import numpy as np
|
|
5
|
+
from xarray import DataArray
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from collections import Counter
|
|
8
|
+
from .utils import periodic_boundaries as pbc_utils
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def make_simple_sample_data_2D(data_type="iris"):
|
|
12
|
+
"""Create a simple dataset to use in tests.
|
|
13
|
+
|
|
14
|
+
The grid has a grid spacing of 1km in both horizontal directions
|
|
15
|
+
and 100 grid cells in x direction and 500 in y direction.
|
|
16
|
+
Time resolution is 1 minute and the total length of the dataset is
|
|
17
|
+
100 minutes around a abritraty date (2000-01-01 12:00).
|
|
18
|
+
The longitude and latitude coordinates are added as 2D aux
|
|
19
|
+
coordinates and arbitrary, but in realisitic range.
|
|
20
|
+
The data contains a single blob travelling on a linear trajectory
|
|
21
|
+
through the dataset for part of the time.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
data_type : {'iris', 'xarray'}, optional
|
|
26
|
+
Choose type of the dataset that will be produced.
|
|
27
|
+
Default is 'iris'
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
sample_data : iris.cube.Cube or xarray.DataArray
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from iris.cube import Cube
|
|
35
|
+
from iris.coords import DimCoord, AuxCoord
|
|
36
|
+
|
|
37
|
+
t_0 = datetime.datetime(2000, 1, 1, 12, 0, 0)
|
|
38
|
+
|
|
39
|
+
x = np.arange(0, 100e3, 1000)
|
|
40
|
+
y = np.arange(0, 50e3, 1000)
|
|
41
|
+
t = t_0 + np.arange(0, 100, 1) * datetime.timedelta(minutes=1)
|
|
42
|
+
xx, yy = np.meshgrid(x, y)
|
|
43
|
+
|
|
44
|
+
t_temp = np.arange(0, 60, 1)
|
|
45
|
+
track1_t = t_0 + t_temp * datetime.timedelta(minutes=1)
|
|
46
|
+
x_0_1 = 10e3
|
|
47
|
+
y_0_1 = 10e3
|
|
48
|
+
track1_x = x_0_1 + 30 * t_temp * 60
|
|
49
|
+
track1_y = y_0_1 + 14 * t_temp * 60
|
|
50
|
+
track1_magnitude = 10 * np.ones(track1_x.shape)
|
|
51
|
+
|
|
52
|
+
data = np.zeros((t.shape[0], y.shape[0], x.shape[0]))
|
|
53
|
+
for i_t, t_i in enumerate(t):
|
|
54
|
+
if np.any(t_i in track1_t):
|
|
55
|
+
x_i = track1_x[track1_t == t_i]
|
|
56
|
+
y_i = track1_y[track1_t == t_i]
|
|
57
|
+
mag_i = track1_magnitude[track1_t == t_i]
|
|
58
|
+
data[i_t] = data[i_t] + mag_i * np.exp(
|
|
59
|
+
-np.power(xx - x_i, 2.0) / (2 * np.power(10e3, 2.0))
|
|
60
|
+
) * np.exp(-np.power(yy - y_i, 2.0) / (2 * np.power(10e3, 2.0)))
|
|
61
|
+
|
|
62
|
+
t_start = datetime.datetime(1970, 1, 1, 0, 0)
|
|
63
|
+
t_points = (t - t_start).astype("timedelta64[ms]").astype(int) // 1000
|
|
64
|
+
t_coord = DimCoord(
|
|
65
|
+
t_points,
|
|
66
|
+
standard_name="time",
|
|
67
|
+
var_name="time",
|
|
68
|
+
units="seconds since 1970-01-01 00:00",
|
|
69
|
+
)
|
|
70
|
+
x_coord = DimCoord(
|
|
71
|
+
x, standard_name="projection_x_coordinate", var_name="x", units="m"
|
|
72
|
+
)
|
|
73
|
+
y_coord = DimCoord(
|
|
74
|
+
y, standard_name="projection_y_coordinate", var_name="y", units="m"
|
|
75
|
+
)
|
|
76
|
+
lat_coord = AuxCoord(
|
|
77
|
+
24 + 1e-5 * xx, standard_name="latitude", var_name="latitude", units="degree"
|
|
78
|
+
)
|
|
79
|
+
lon_coord = AuxCoord(
|
|
80
|
+
150 + 1e-5 * yy, standard_name="longitude", var_name="longitude", units="degree"
|
|
81
|
+
)
|
|
82
|
+
sample_data = Cube(
|
|
83
|
+
data,
|
|
84
|
+
dim_coords_and_dims=[(t_coord, 0), (y_coord, 1), (x_coord, 2)],
|
|
85
|
+
aux_coords_and_dims=[(lat_coord, (1, 2)), (lon_coord, (1, 2))],
|
|
86
|
+
var_name="w",
|
|
87
|
+
units="m s-1",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if data_type == "xarray":
|
|
91
|
+
sample_data = DataArray.from_iris(sample_data)
|
|
92
|
+
|
|
93
|
+
return sample_data
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def make_sample_data_2D_3blobs(data_type="iris"):
|
|
97
|
+
"""Create a simple dataset to use in tests.
|
|
98
|
+
|
|
99
|
+
The grid has a grid spacing of 1km in both horizontal directions
|
|
100
|
+
and 100 grid cells in x direction and 200 in y direction.
|
|
101
|
+
Time resolution is 1 minute and the total length of the dataset is
|
|
102
|
+
100 minutes around a arbitrary date (2000-01-01 12:00).
|
|
103
|
+
The longitude and latitude coordinates are added as 2D aux
|
|
104
|
+
coordinates and arbitrary, but in realisitic range.
|
|
105
|
+
The data contains three individual blobs travelling on a linear
|
|
106
|
+
trajectory through the dataset for part of the time.
|
|
107
|
+
|
|
108
|
+
Parameters
|
|
109
|
+
----------
|
|
110
|
+
data_type : {'iris', 'xarray'}, optional
|
|
111
|
+
Choose type of the dataset that will be produced.
|
|
112
|
+
Default is 'iris'
|
|
113
|
+
|
|
114
|
+
Returns
|
|
115
|
+
-------
|
|
116
|
+
sample_data : iris.cube.Cube or xarray.DataArray
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
from iris.cube import Cube
|
|
120
|
+
from iris.coords import DimCoord, AuxCoord
|
|
121
|
+
|
|
122
|
+
t_0 = datetime.datetime(2000, 1, 1, 12, 0, 0)
|
|
123
|
+
|
|
124
|
+
x = np.arange(0, 100e3, 1000)
|
|
125
|
+
y = np.arange(0, 200e3, 1000)
|
|
126
|
+
t = t_0 + np.arange(0, 100, 1) * datetime.timedelta(minutes=1)
|
|
127
|
+
xx, yy = np.meshgrid(x, y)
|
|
128
|
+
|
|
129
|
+
t_temp = np.arange(0, 60, 1)
|
|
130
|
+
track1_t = t_0 + t_temp * datetime.timedelta(minutes=1)
|
|
131
|
+
x_0_1 = 10e3
|
|
132
|
+
y_0_1 = 10e3
|
|
133
|
+
track1_x = x_0_1 + 30 * t_temp * 60
|
|
134
|
+
track1_y = y_0_1 + 14 * t_temp * 60
|
|
135
|
+
track1_magnitude = 10 * np.ones(track1_x.shape)
|
|
136
|
+
|
|
137
|
+
t_temp = np.arange(0, 30, 1)
|
|
138
|
+
track2_t = t_0 + (t_temp + 40) * datetime.timedelta(minutes=1)
|
|
139
|
+
x_0_2 = 20e3
|
|
140
|
+
y_0_2 = 10e3
|
|
141
|
+
track2_x = x_0_2 + 24 * (t_temp * 60) ** 2 / 1000
|
|
142
|
+
track2_y = y_0_2 + 12 * t_temp * 60
|
|
143
|
+
track2_magnitude = 20 * np.ones(track2_x.shape)
|
|
144
|
+
|
|
145
|
+
t_temp = np.arange(0, 20, 1)
|
|
146
|
+
track3_t = t_0 + (t_temp + 50) * datetime.timedelta(minutes=1)
|
|
147
|
+
x_0_3 = 70e3
|
|
148
|
+
y_0_3 = 110e3
|
|
149
|
+
track3_x = x_0_3 + 20 * (t_temp * 60) ** 2 / 1000
|
|
150
|
+
track3_y = y_0_3 + 20 * t_temp * 60
|
|
151
|
+
track3_magnitude = 15 * np.ones(track3_x.shape)
|
|
152
|
+
|
|
153
|
+
data = np.zeros((t.shape[0], y.shape[0], x.shape[0]))
|
|
154
|
+
for i_t, t_i in enumerate(t):
|
|
155
|
+
if np.any(t_i in track1_t):
|
|
156
|
+
x_i = track1_x[track1_t == t_i]
|
|
157
|
+
y_i = track1_y[track1_t == t_i]
|
|
158
|
+
mag_i = track1_magnitude[track1_t == t_i]
|
|
159
|
+
data[i_t] = data[i_t] + mag_i * np.exp(
|
|
160
|
+
-np.power(xx - x_i, 2.0) / (2 * np.power(10e3, 2.0))
|
|
161
|
+
) * np.exp(-np.power(yy - y_i, 2.0) / (2 * np.power(10e3, 2.0)))
|
|
162
|
+
if np.any(t_i in track2_t):
|
|
163
|
+
x_i = track2_x[track2_t == t_i]
|
|
164
|
+
y_i = track2_y[track2_t == t_i]
|
|
165
|
+
mag_i = track2_magnitude[track2_t == t_i]
|
|
166
|
+
data[i_t] = data[i_t] + mag_i * np.exp(
|
|
167
|
+
-np.power(xx - x_i, 2.0) / (2 * np.power(10e3, 2.0))
|
|
168
|
+
) * np.exp(-np.power(yy - y_i, 2.0) / (2 * np.power(10e3, 2.0)))
|
|
169
|
+
if np.any(t_i in track3_t):
|
|
170
|
+
x_i = track3_x[track3_t == t_i]
|
|
171
|
+
y_i = track3_y[track3_t == t_i]
|
|
172
|
+
mag_i = track3_magnitude[track3_t == t_i]
|
|
173
|
+
data[i_t] = data[i_t] + mag_i * np.exp(
|
|
174
|
+
-np.power(xx - x_i, 2.0) / (2 * np.power(10e3, 2.0))
|
|
175
|
+
) * np.exp(-np.power(yy - y_i, 2.0) / (2 * np.power(10e3, 2.0)))
|
|
176
|
+
t_start = datetime.datetime(1970, 1, 1, 0, 0)
|
|
177
|
+
t_points = (t - t_start).astype("timedelta64[ms]").astype(int) // 1000
|
|
178
|
+
t_coord = DimCoord(
|
|
179
|
+
t_points,
|
|
180
|
+
standard_name="time",
|
|
181
|
+
var_name="time",
|
|
182
|
+
units="seconds since 1970-01-01 00:00",
|
|
183
|
+
)
|
|
184
|
+
x_coord = DimCoord(
|
|
185
|
+
x, standard_name="projection_x_coordinate", var_name="x", units="m"
|
|
186
|
+
)
|
|
187
|
+
y_coord = DimCoord(
|
|
188
|
+
y, standard_name="projection_y_coordinate", var_name="y", units="m"
|
|
189
|
+
)
|
|
190
|
+
lat_coord = AuxCoord(
|
|
191
|
+
24 + 1e-5 * xx, standard_name="latitude", var_name="latitude", units="degree"
|
|
192
|
+
)
|
|
193
|
+
lon_coord = AuxCoord(
|
|
194
|
+
150 + 1e-5 * yy, standard_name="longitude", var_name="longitude", units="degree"
|
|
195
|
+
)
|
|
196
|
+
sample_data = Cube(
|
|
197
|
+
data,
|
|
198
|
+
dim_coords_and_dims=[(t_coord, 0), (y_coord, 1), (x_coord, 2)],
|
|
199
|
+
aux_coords_and_dims=[(lat_coord, (1, 2)), (lon_coord, (1, 2))],
|
|
200
|
+
var_name="w",
|
|
201
|
+
units="m s-1",
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if data_type == "xarray":
|
|
205
|
+
sample_data = DataArray.from_iris(sample_data)
|
|
206
|
+
|
|
207
|
+
return sample_data
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def make_sample_data_2D_3blobs_inv(data_type="iris"):
|
|
211
|
+
"""Create a version of the dataset with switched coordinates.
|
|
212
|
+
|
|
213
|
+
Create a version of the dataset created in the function
|
|
214
|
+
make_sample_cube_2D, but with switched coordinate order for the
|
|
215
|
+
horizontal coordinates for tests to ensure that this does not
|
|
216
|
+
affect the results.
|
|
217
|
+
|
|
218
|
+
Parameters
|
|
219
|
+
----------
|
|
220
|
+
data_type : {'iris', 'xarray'}, optional
|
|
221
|
+
Choose type of the dataset that will be produced.
|
|
222
|
+
Default is 'iris'
|
|
223
|
+
|
|
224
|
+
Returns
|
|
225
|
+
-------
|
|
226
|
+
sample_data : iris.cube.Cube or xarray.DataArray
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
from iris.cube import Cube
|
|
230
|
+
from iris.coords import DimCoord, AuxCoord
|
|
231
|
+
|
|
232
|
+
t_0 = datetime.datetime(2000, 1, 1, 12, 0, 0)
|
|
233
|
+
x = np.arange(0, 100e3, 1000)
|
|
234
|
+
y = np.arange(0, 200e3, 1000)
|
|
235
|
+
t = t_0 + np.arange(0, 100, 1) * datetime.timedelta(minutes=1)
|
|
236
|
+
yy, xx = np.meshgrid(y, x)
|
|
237
|
+
|
|
238
|
+
t_temp = np.arange(0, 60, 1)
|
|
239
|
+
track1_t = t_0 + t_temp * datetime.timedelta(minutes=1)
|
|
240
|
+
x_0_1 = 10e3
|
|
241
|
+
y_0_1 = 10e3
|
|
242
|
+
track1_x = x_0_1 + 30 * t_temp * 60
|
|
243
|
+
track1_y = y_0_1 + 14 * t_temp * 60
|
|
244
|
+
track1_magnitude = 10 * np.ones(track1_x.shape)
|
|
245
|
+
|
|
246
|
+
t_temp = np.arange(0, 30, 1)
|
|
247
|
+
track2_t = t_0 + (t_temp + 40) * datetime.timedelta(minutes=1)
|
|
248
|
+
x_0_2 = 20e3
|
|
249
|
+
y_0_2 = 10e3
|
|
250
|
+
track2_x = x_0_2 + 24 * (t_temp * 60) ** 2 / 1000
|
|
251
|
+
track2_y = y_0_2 + 12 * t_temp * 60
|
|
252
|
+
track2_magnitude = 20 * np.ones(track2_x.shape)
|
|
253
|
+
|
|
254
|
+
t_temp = np.arange(0, 20, 1)
|
|
255
|
+
track3_t = t_0 + (t_temp + 50) * datetime.timedelta(minutes=1)
|
|
256
|
+
x_0_3 = 70e3
|
|
257
|
+
y_0_3 = 110e3
|
|
258
|
+
track3_x = x_0_3 + 20 * (t_temp * 60) ** 2 / 1000
|
|
259
|
+
track3_y = y_0_3 + 20 * t_temp * 60
|
|
260
|
+
track3_magnitude = 15 * np.ones(track3_x.shape)
|
|
261
|
+
|
|
262
|
+
data = np.zeros((t.shape[0], x.shape[0], y.shape[0]))
|
|
263
|
+
for i_t, t_i in enumerate(t):
|
|
264
|
+
if np.any(t_i in track1_t):
|
|
265
|
+
x_i = track1_x[track1_t == t_i]
|
|
266
|
+
y_i = track1_y[track1_t == t_i]
|
|
267
|
+
mag_i = track1_magnitude[track1_t == t_i]
|
|
268
|
+
data[i_t] = data[i_t] + mag_i * np.exp(
|
|
269
|
+
-np.power(xx - x_i, 2.0) / (2 * np.power(10e3, 2.0))
|
|
270
|
+
) * np.exp(-np.power(yy - y_i, 2.0) / (2 * np.power(10e3, 2.0)))
|
|
271
|
+
if np.any(t_i in track2_t):
|
|
272
|
+
x_i = track2_x[track2_t == t_i]
|
|
273
|
+
y_i = track2_y[track2_t == t_i]
|
|
274
|
+
mag_i = track2_magnitude[track2_t == t_i]
|
|
275
|
+
data[i_t] = data[i_t] + mag_i * np.exp(
|
|
276
|
+
-np.power(xx - x_i, 2.0) / (2 * np.power(10e3, 2.0))
|
|
277
|
+
) * np.exp(-np.power(yy - y_i, 2.0) / (2 * np.power(10e3, 2.0)))
|
|
278
|
+
if np.any(t_i in track3_t):
|
|
279
|
+
x_i = track3_x[track3_t == t_i]
|
|
280
|
+
y_i = track3_y[track3_t == t_i]
|
|
281
|
+
mag_i = track3_magnitude[track3_t == t_i]
|
|
282
|
+
data[i_t] = data[i_t] + mag_i * np.exp(
|
|
283
|
+
-np.power(xx - x_i, 2.0) / (2 * np.power(10e3, 2.0))
|
|
284
|
+
) * np.exp(-np.power(yy - y_i, 2.0) / (2 * np.power(10e3, 2.0)))
|
|
285
|
+
|
|
286
|
+
t_start = datetime.datetime(1970, 1, 1, 0, 0)
|
|
287
|
+
t_points = (t - t_start).astype("timedelta64[ms]").astype(int) // 1000
|
|
288
|
+
|
|
289
|
+
t_coord = DimCoord(
|
|
290
|
+
t_points,
|
|
291
|
+
standard_name="time",
|
|
292
|
+
var_name="time",
|
|
293
|
+
units="seconds since 1970-01-01 00:00",
|
|
294
|
+
)
|
|
295
|
+
x_coord = DimCoord(
|
|
296
|
+
x, standard_name="projection_x_coordinate", var_name="x", units="m"
|
|
297
|
+
)
|
|
298
|
+
y_coord = DimCoord(
|
|
299
|
+
y, standard_name="projection_y_coordinate", var_name="y", units="m"
|
|
300
|
+
)
|
|
301
|
+
lat_coord = AuxCoord(
|
|
302
|
+
24 + 1e-5 * xx, standard_name="latitude", var_name="latitude", units="degree"
|
|
303
|
+
)
|
|
304
|
+
lon_coord = AuxCoord(
|
|
305
|
+
150 + 1e-5 * yy, standard_name="longitude", var_name="longitude", units="degree"
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
sample_data = Cube(
|
|
309
|
+
data,
|
|
310
|
+
dim_coords_and_dims=[(t_coord, 0), (y_coord, 2), (x_coord, 1)],
|
|
311
|
+
aux_coords_and_dims=[(lat_coord, (1, 2)), (lon_coord, (1, 2))],
|
|
312
|
+
var_name="w",
|
|
313
|
+
units="m s-1",
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
if data_type == "xarray":
|
|
317
|
+
sample_data = DataArray.from_iris(sample_data)
|
|
318
|
+
|
|
319
|
+
return sample_data
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def make_sample_data_3D_1blob(data_type="iris", invert_xy=False):
|
|
323
|
+
"""Create a simple dataset to use in tests with one blob moving diagonally.
|
|
324
|
+
The grid has a grid spacing of 1km in both horizontal directions
|
|
325
|
+
and 100 grid cells in the x direction and 50 in the y direction.
|
|
326
|
+
Time resolution is 1 minute, and the total length of the dataset is
|
|
327
|
+
50 minutes starting from an arbitrary date (2000-01-01 12:00).
|
|
328
|
+
The longitude and latitude coordinates are added as 2D aux
|
|
329
|
+
coordinates and are arbitrary, but within a realistic range.
|
|
330
|
+
The data contains a single blob traveling on a linear trajectory
|
|
331
|
+
through the dataset for part of the time. The blob follows a
|
|
332
|
+
trajectory that starts at (x=10km, y=10km, z=4km) and moves along a
|
|
333
|
+
linear path at each time step.
|
|
334
|
+
Parameters
|
|
335
|
+
----------
|
|
336
|
+
data_type : {'iris', 'xarray'}, optional
|
|
337
|
+
Choose type of the dataset that will be produced.
|
|
338
|
+
Default is 'iris'.
|
|
339
|
+
invert_xy : bool, optional
|
|
340
|
+
Flag to determine whether to switch x and y coordinates.
|
|
341
|
+
Default is False.
|
|
342
|
+
Returns
|
|
343
|
+
-------
|
|
344
|
+
sample_data : iris.cube.Cube or xarray.DataArray
|
|
345
|
+
"""
|
|
346
|
+
|
|
347
|
+
from iris.cube import Cube
|
|
348
|
+
from iris.coords import DimCoord, AuxCoord
|
|
349
|
+
|
|
350
|
+
t_0 = datetime.datetime(2000, 1, 1, 12, 0, 0)
|
|
351
|
+
|
|
352
|
+
x = np.arange(0, 100e3, 1000)
|
|
353
|
+
y = np.arange(0, 50e3, 1000)
|
|
354
|
+
z = np.arange(0, 50e3, 1000)
|
|
355
|
+
|
|
356
|
+
t = t_0 + np.arange(0, 50, 2) * datetime.timedelta(minutes=1)
|
|
357
|
+
|
|
358
|
+
t_temp = np.arange(0, 60, 1)
|
|
359
|
+
track1_t = t_0 + t_temp * datetime.timedelta(minutes=1)
|
|
360
|
+
x_0_1 = 10e3
|
|
361
|
+
y_0_1 = 10e3
|
|
362
|
+
z_0_1 = 4e3
|
|
363
|
+
|
|
364
|
+
track1_x = x_0_1 + 30 * t_temp * 60
|
|
365
|
+
track1_y = y_0_1 + 14 * t_temp * 60
|
|
366
|
+
track1_z = z_0_1 + 16 * t_temp * 60
|
|
367
|
+
track1_magnitude = 10 * np.ones(track1_x.shape)
|
|
368
|
+
|
|
369
|
+
size_factor = 5e3
|
|
370
|
+
|
|
371
|
+
if invert_xy == False:
|
|
372
|
+
zz, yy, xx = np.meshgrid(z, y, x, indexing="ij")
|
|
373
|
+
y_dim = 2
|
|
374
|
+
x_dim = 3
|
|
375
|
+
data = np.zeros((t.shape[0], z.shape[0], y.shape[0], x.shape[0]))
|
|
376
|
+
|
|
377
|
+
else:
|
|
378
|
+
zz, xx, yy = np.meshgrid(z, x, y, indexing="ij")
|
|
379
|
+
x_dim = 2
|
|
380
|
+
y_dim = 3
|
|
381
|
+
data = np.zeros((t.shape[0], z.shape[0], x.shape[0], y.shape[0]))
|
|
382
|
+
|
|
383
|
+
for i_t, t_i in enumerate(t):
|
|
384
|
+
if np.any(t_i in track1_t):
|
|
385
|
+
x_i = track1_x[track1_t == t_i]
|
|
386
|
+
y_i = track1_y[track1_t == t_i]
|
|
387
|
+
z_i = track1_z[track1_t == t_i]
|
|
388
|
+
mag_i = track1_magnitude[track1_t == t_i]
|
|
389
|
+
data[i_t] = data[i_t] + mag_i * np.exp(
|
|
390
|
+
-np.power(xx - x_i, 2.0) / (2 * np.power(size_factor, 2.0))
|
|
391
|
+
) * np.exp(
|
|
392
|
+
-np.power(yy - y_i, 2.0) / (2 * np.power(size_factor, 2.0))
|
|
393
|
+
) * np.exp(
|
|
394
|
+
-np.power(zz - z_i, 2.0) / (2 * np.power(5e3, 2.0))
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
t_start = datetime.datetime(1970, 1, 1, 0, 0)
|
|
398
|
+
t_points = (t - t_start).astype("timedelta64[ms]").astype(int) // 1000
|
|
399
|
+
t_coord = DimCoord(
|
|
400
|
+
t_points,
|
|
401
|
+
standard_name="time",
|
|
402
|
+
var_name="time",
|
|
403
|
+
units="seconds since 1970-01-01 00:00",
|
|
404
|
+
)
|
|
405
|
+
z_coord = DimCoord(z, standard_name="geopotential_height", var_name="z", units="m")
|
|
406
|
+
y_coord = DimCoord(
|
|
407
|
+
y, standard_name="projection_y_coordinate", var_name="y", units="m"
|
|
408
|
+
)
|
|
409
|
+
x_coord = DimCoord(
|
|
410
|
+
x, standard_name="projection_x_coordinate", var_name="x", units="m"
|
|
411
|
+
)
|
|
412
|
+
lat_coord = AuxCoord(
|
|
413
|
+
24 + 1e-5 * xx[0], standard_name="latitude", var_name="latitude", units="degree"
|
|
414
|
+
)
|
|
415
|
+
lon_coord = AuxCoord(
|
|
416
|
+
150 + 1e-5 * yy[0],
|
|
417
|
+
standard_name="longitude",
|
|
418
|
+
var_name="longitude",
|
|
419
|
+
units="degree",
|
|
420
|
+
)
|
|
421
|
+
sample_data = Cube(
|
|
422
|
+
data,
|
|
423
|
+
dim_coords_and_dims=[
|
|
424
|
+
(t_coord, 0),
|
|
425
|
+
(z_coord, 1),
|
|
426
|
+
(y_coord, y_dim),
|
|
427
|
+
(x_coord, x_dim),
|
|
428
|
+
],
|
|
429
|
+
aux_coords_and_dims=[(lat_coord, (2, 3)), (lon_coord, (2, 3))],
|
|
430
|
+
var_name="w",
|
|
431
|
+
units="m s-1",
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
if data_type == "xarray":
|
|
435
|
+
sample_data = DataArray.from_iris(sample_data)
|
|
436
|
+
|
|
437
|
+
return sample_data
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def make_sample_data_3D_3blobs(data_type="iris", invert_xy=False):
|
|
441
|
+
"""Create a simple dataset to use in tests.
|
|
442
|
+
|
|
443
|
+
The grid has a grid spacing of 1km in both horizontal directions
|
|
444
|
+
and 100 grid cells in x direction and 200 in y direction.
|
|
445
|
+
Time resolution is 1 minute and the total length of the dataset is
|
|
446
|
+
100 minutes around a abritraty date (2000-01-01 12:00).
|
|
447
|
+
The longitude and latitude coordinates are added as 2D aux
|
|
448
|
+
coordinates and arbitrary, but in realisitic range.
|
|
449
|
+
The data contains three individual blobs travelling on a linear
|
|
450
|
+
trajectory through the dataset for part of the time.
|
|
451
|
+
|
|
452
|
+
Parameters
|
|
453
|
+
----------
|
|
454
|
+
data_type : {'iris', 'xarray'}, optional
|
|
455
|
+
Choose type of the dataset that will be produced.
|
|
456
|
+
Default is 'iris'
|
|
457
|
+
|
|
458
|
+
invert_xy : bool, optional
|
|
459
|
+
Flag to determine wether to switch x and y coordinates
|
|
460
|
+
Default is False
|
|
461
|
+
|
|
462
|
+
Returns
|
|
463
|
+
-------
|
|
464
|
+
sample_data : iris.cube.Cube or xarray.DataArray
|
|
465
|
+
"""
|
|
466
|
+
|
|
467
|
+
from iris.cube import Cube
|
|
468
|
+
from iris.coords import DimCoord, AuxCoord
|
|
469
|
+
|
|
470
|
+
t_0 = datetime.datetime(2000, 1, 1, 12, 0, 0)
|
|
471
|
+
|
|
472
|
+
x = np.arange(0, 100e3, 1000)
|
|
473
|
+
y = np.arange(0, 200e3, 1000)
|
|
474
|
+
z = np.arange(0, 20e3, 1000)
|
|
475
|
+
|
|
476
|
+
t = t_0 + np.arange(0, 50, 2) * datetime.timedelta(minutes=1)
|
|
477
|
+
|
|
478
|
+
t_temp = np.arange(0, 60, 1)
|
|
479
|
+
track1_t = t_0 + t_temp * datetime.timedelta(minutes=1)
|
|
480
|
+
x_0_1 = 10e3
|
|
481
|
+
y_0_1 = 10e3
|
|
482
|
+
z_0_1 = 4e3
|
|
483
|
+
track1_x = x_0_1 + 30 * t_temp * 60
|
|
484
|
+
track1_y = y_0_1 + 14 * t_temp * 60
|
|
485
|
+
track1_magnitude = 10 * np.ones(track1_x.shape)
|
|
486
|
+
|
|
487
|
+
t_temp = np.arange(0, 30, 1)
|
|
488
|
+
track2_t = t_0 + (t_temp + 40) * datetime.timedelta(minutes=1)
|
|
489
|
+
x_0_2 = 20e3
|
|
490
|
+
y_0_2 = 10e3
|
|
491
|
+
z_0_2 = 6e3
|
|
492
|
+
track2_x = x_0_2 + 24 * (t_temp * 60) ** 2 / 1000
|
|
493
|
+
track2_y = y_0_2 + 12 * t_temp * 60
|
|
494
|
+
track2_magnitude = 20 * np.ones(track2_x.shape)
|
|
495
|
+
|
|
496
|
+
t_temp = np.arange(0, 20, 1)
|
|
497
|
+
track3_t = t_0 + (t_temp + 50) * datetime.timedelta(minutes=1)
|
|
498
|
+
x_0_3 = 70e3
|
|
499
|
+
y_0_3 = 110e3
|
|
500
|
+
z_0_3 = 8e3
|
|
501
|
+
track3_x = x_0_3 + 20 * (t_temp * 60) ** 2 / 1000
|
|
502
|
+
track3_y = y_0_3 + 20 * t_temp * 60
|
|
503
|
+
track3_magnitude = 15 * np.ones(track3_x.shape)
|
|
504
|
+
|
|
505
|
+
if invert_xy == False:
|
|
506
|
+
zz, yy, xx = np.meshgrid(z, y, x, indexing="ij")
|
|
507
|
+
y_dim = 2
|
|
508
|
+
x_dim = 3
|
|
509
|
+
data = np.zeros((t.shape[0], z.shape[0], y.shape[0], x.shape[0]))
|
|
510
|
+
|
|
511
|
+
else:
|
|
512
|
+
zz, xx, yy = np.meshgrid(z, x, y, indexing="ij")
|
|
513
|
+
x_dim = 2
|
|
514
|
+
y_dim = 3
|
|
515
|
+
data = np.zeros((t.shape[0], z.shape[0], x.shape[0], y.shape[0]))
|
|
516
|
+
|
|
517
|
+
for i_t, t_i in enumerate(t):
|
|
518
|
+
if np.any(t_i in track1_t):
|
|
519
|
+
x_i = track1_x[track1_t == t_i]
|
|
520
|
+
y_i = track1_y[track1_t == t_i]
|
|
521
|
+
z_i = z_0_1
|
|
522
|
+
mag_i = track1_magnitude[track1_t == t_i]
|
|
523
|
+
data[i_t] = data[i_t] + mag_i * np.exp(
|
|
524
|
+
-np.power(xx - x_i, 2.0) / (2 * np.power(10e3, 2.0))
|
|
525
|
+
) * np.exp(-np.power(yy - y_i, 2.0) / (2 * np.power(10e3, 2.0))) * np.exp(
|
|
526
|
+
-np.power(zz - z_i, 2.0) / (2 * np.power(5e3, 2.0))
|
|
527
|
+
)
|
|
528
|
+
if np.any(t_i in track2_t):
|
|
529
|
+
x_i = track2_x[track2_t == t_i]
|
|
530
|
+
y_i = track2_y[track2_t == t_i]
|
|
531
|
+
z_i = z_0_2
|
|
532
|
+
mag_i = track2_magnitude[track2_t == t_i]
|
|
533
|
+
data[i_t] = data[i_t] + mag_i * np.exp(
|
|
534
|
+
-np.power(xx - x_i, 2.0) / (2 * np.power(10e3, 2.0))
|
|
535
|
+
) * np.exp(-np.power(yy - y_i, 2.0) / (2 * np.power(10e3, 2.0))) * np.exp(
|
|
536
|
+
-np.power(zz - z_i, 2.0) / (2 * np.power(5e3, 2.0))
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
if np.any(t_i in track3_t):
|
|
540
|
+
x_i = track3_x[track3_t == t_i]
|
|
541
|
+
y_i = track3_y[track3_t == t_i]
|
|
542
|
+
z_i = z_0_3
|
|
543
|
+
mag_i = track3_magnitude[track3_t == t_i]
|
|
544
|
+
data[i_t] = data[i_t] + mag_i * np.exp(
|
|
545
|
+
-np.power(xx - x_i, 2.0) / (2 * np.power(10e3, 2.0))
|
|
546
|
+
) * np.exp(-np.power(yy - y_i, 2.0) / (2 * np.power(10e3, 2.0))) * np.exp(
|
|
547
|
+
-np.power(zz - z_i, 2.0) / (2 * np.power(5e3, 2.0))
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
t_start = datetime.datetime(1970, 1, 1, 0, 0)
|
|
551
|
+
t_points = (t - t_start).astype("timedelta64[ms]").astype(int) // 1000
|
|
552
|
+
t_coord = DimCoord(
|
|
553
|
+
t_points,
|
|
554
|
+
standard_name="time",
|
|
555
|
+
var_name="time",
|
|
556
|
+
units="seconds since 1970-01-01 00:00",
|
|
557
|
+
)
|
|
558
|
+
z_coord = DimCoord(z, standard_name="geopotential_height", var_name="z", units="m")
|
|
559
|
+
y_coord = DimCoord(
|
|
560
|
+
y, standard_name="projection_y_coordinate", var_name="y", units="m"
|
|
561
|
+
)
|
|
562
|
+
x_coord = DimCoord(
|
|
563
|
+
x, standard_name="projection_x_coordinate", var_name="x", units="m"
|
|
564
|
+
)
|
|
565
|
+
lat_coord = AuxCoord(
|
|
566
|
+
24 + 1e-5 * xx[0], standard_name="latitude", var_name="latitude", units="degree"
|
|
567
|
+
)
|
|
568
|
+
lon_coord = AuxCoord(
|
|
569
|
+
150 + 1e-5 * yy[0],
|
|
570
|
+
standard_name="longitude",
|
|
571
|
+
var_name="longitude",
|
|
572
|
+
units="degree",
|
|
573
|
+
)
|
|
574
|
+
sample_data = Cube(
|
|
575
|
+
data,
|
|
576
|
+
dim_coords_and_dims=[
|
|
577
|
+
(t_coord, 0),
|
|
578
|
+
(z_coord, 1),
|
|
579
|
+
(y_coord, y_dim),
|
|
580
|
+
(x_coord, x_dim),
|
|
581
|
+
],
|
|
582
|
+
aux_coords_and_dims=[(lat_coord, (2, 3)), (lon_coord, (2, 3))],
|
|
583
|
+
var_name="w",
|
|
584
|
+
units="m s-1",
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
if data_type == "xarray":
|
|
588
|
+
sample_data = DataArray.from_iris(sample_data)
|
|
589
|
+
|
|
590
|
+
return sample_data
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def make_dataset_from_arr(
|
|
594
|
+
in_arr,
|
|
595
|
+
data_type="xarray",
|
|
596
|
+
time_dim_num=None,
|
|
597
|
+
z_dim_num=None,
|
|
598
|
+
z_dim_name="altitude",
|
|
599
|
+
y_dim_num=0,
|
|
600
|
+
x_dim_num=1,
|
|
601
|
+
):
|
|
602
|
+
"""Makes a dataset (xarray or iris) for feature detection/segmentation from
|
|
603
|
+
a raw numpy/dask/etc. array.
|
|
604
|
+
|
|
605
|
+
Parameters
|
|
606
|
+
----------
|
|
607
|
+
in_arr: array-like
|
|
608
|
+
The input array to convert to iris/xarray
|
|
609
|
+
|
|
610
|
+
data_type: str('xarray' or 'iris'), optional
|
|
611
|
+
Type of the dataset to return
|
|
612
|
+
Default is 'xarray'
|
|
613
|
+
|
|
614
|
+
time_dim_num: int or None, optional
|
|
615
|
+
What axis is the time dimension on, None for a single timestep
|
|
616
|
+
Default is None
|
|
617
|
+
|
|
618
|
+
z_dim_num: int or None, optional
|
|
619
|
+
What axis is the z dimension on, None for a 2D array
|
|
620
|
+
z_dim_name: str
|
|
621
|
+
What the z dimension name is named
|
|
622
|
+
y_dim_num: int
|
|
623
|
+
What axis is the y dimension on, typically 0 for a 2D array
|
|
624
|
+
Default is 0
|
|
625
|
+
|
|
626
|
+
x_dim_num: int, optional
|
|
627
|
+
What axis is the x dimension on, typically 1 for a 2D array
|
|
628
|
+
Default is 1
|
|
629
|
+
|
|
630
|
+
Returns
|
|
631
|
+
-------
|
|
632
|
+
Iris or xarray dataset with everything we need for feature detection/tracking.
|
|
633
|
+
|
|
634
|
+
"""
|
|
635
|
+
|
|
636
|
+
import xarray as xr
|
|
637
|
+
import iris
|
|
638
|
+
|
|
639
|
+
time_dim_name = "time"
|
|
640
|
+
has_time = time_dim_num is not None
|
|
641
|
+
|
|
642
|
+
is_3D = z_dim_num is not None
|
|
643
|
+
output_arr = xr.DataArray(in_arr)
|
|
644
|
+
if is_3D:
|
|
645
|
+
z_max = in_arr.shape[z_dim_num]
|
|
646
|
+
|
|
647
|
+
if has_time:
|
|
648
|
+
time_min = datetime.datetime(2022, 1, 1)
|
|
649
|
+
time_num = in_arr.shape[time_dim_num]
|
|
650
|
+
time_vals = pd.date_range(start=time_min, periods=time_num).values.astype(
|
|
651
|
+
"datetime64[s]"
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
if data_type == "xarray":
|
|
655
|
+
# add dimension and coordinates
|
|
656
|
+
if is_3D:
|
|
657
|
+
output_arr = output_arr.rename(
|
|
658
|
+
new_name_or_name_dict={"dim_" + str(z_dim_num): z_dim_name}
|
|
659
|
+
)
|
|
660
|
+
output_arr = output_arr.assign_coords(
|
|
661
|
+
{z_dim_name: (z_dim_name, np.arange(0, z_max))}
|
|
662
|
+
)
|
|
663
|
+
# add dimension and coordinates
|
|
664
|
+
if has_time:
|
|
665
|
+
output_arr = output_arr.rename(
|
|
666
|
+
new_name_or_name_dict={"dim_" + str(time_dim_num): time_dim_name}
|
|
667
|
+
)
|
|
668
|
+
output_arr = output_arr.assign_coords(
|
|
669
|
+
{time_dim_name: (time_dim_name, time_vals)}
|
|
670
|
+
)
|
|
671
|
+
return output_arr
|
|
672
|
+
if data_type == "iris":
|
|
673
|
+
out_arr_iris = output_arr.to_iris()
|
|
674
|
+
|
|
675
|
+
if is_3D:
|
|
676
|
+
out_arr_iris.add_dim_coord(
|
|
677
|
+
iris.coords.DimCoord(np.arange(0, z_max), standard_name=z_dim_name),
|
|
678
|
+
z_dim_num,
|
|
679
|
+
)
|
|
680
|
+
if has_time:
|
|
681
|
+
out_arr_iris.add_dim_coord(
|
|
682
|
+
iris.coords.DimCoord(
|
|
683
|
+
time_vals.astype(int),
|
|
684
|
+
standard_name=time_dim_name,
|
|
685
|
+
units="seconds since epoch",
|
|
686
|
+
),
|
|
687
|
+
time_dim_num,
|
|
688
|
+
)
|
|
689
|
+
return out_arr_iris
|
|
690
|
+
else:
|
|
691
|
+
raise ValueError("data_type must be 'xarray' or 'iris'")
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def make_feature_blob(
|
|
695
|
+
in_arr,
|
|
696
|
+
h1_loc,
|
|
697
|
+
h2_loc,
|
|
698
|
+
v_loc=None,
|
|
699
|
+
h1_size=1,
|
|
700
|
+
h2_size=1,
|
|
701
|
+
v_size=1,
|
|
702
|
+
shape="rectangle",
|
|
703
|
+
amplitude=1,
|
|
704
|
+
PBC_flag="none",
|
|
705
|
+
):
|
|
706
|
+
"""Function to make a defined "blob" in location (zloc, yloc, xloc) with
|
|
707
|
+
user-specified shape and amplitude. Note that this function will
|
|
708
|
+
round the size and locations to the nearest point within the array.
|
|
709
|
+
|
|
710
|
+
Parameters
|
|
711
|
+
----------
|
|
712
|
+
in_arr: array-like
|
|
713
|
+
input array to add the "blob" to
|
|
714
|
+
|
|
715
|
+
h1_loc: float
|
|
716
|
+
Center hdim_1 location of the blob, required
|
|
717
|
+
|
|
718
|
+
h2_loc: float
|
|
719
|
+
Center hdim_2 location of the blob, required
|
|
720
|
+
|
|
721
|
+
v_loc: float, optional
|
|
722
|
+
Center vdim location of the blob, optional. If this is None, we assume that the
|
|
723
|
+
dataset is 2D.
|
|
724
|
+
Default is None
|
|
725
|
+
|
|
726
|
+
h1_size: float, optional
|
|
727
|
+
Size of the bubble in array coordinates in hdim_1
|
|
728
|
+
Default is 1
|
|
729
|
+
|
|
730
|
+
h2_size: float, optional
|
|
731
|
+
Size of the bubble in array coordinates in hdim_2
|
|
732
|
+
Default is 1
|
|
733
|
+
|
|
734
|
+
v_size: float, optional
|
|
735
|
+
Size of the bubble in array coordinates in vdim
|
|
736
|
+
Default is 1
|
|
737
|
+
|
|
738
|
+
shape: str('rectangle'), optional
|
|
739
|
+
The shape of the blob that is added. For now, this is just rectangle
|
|
740
|
+
'rectangle' adds a rectangular/rectangular prism bubble with constant amplitude `amplitude`.
|
|
741
|
+
Default is "rectangle"
|
|
742
|
+
|
|
743
|
+
amplitude: float, optional
|
|
744
|
+
Maximum amplitude of the blob
|
|
745
|
+
Default is 1
|
|
746
|
+
|
|
747
|
+
PBC_flag : str('none', 'hdim_1', 'hdim_2', 'both')
|
|
748
|
+
Sets whether to use periodic boundaries, and if so in which directions.
|
|
749
|
+
'none' means that we do not have periodic boundaries
|
|
750
|
+
'hdim_1' means that we are periodic along hdim1
|
|
751
|
+
'hdim_2' means that we are periodic along hdim2
|
|
752
|
+
'both' means that we are periodic along both horizontal dimensions
|
|
753
|
+
|
|
754
|
+
Returns
|
|
755
|
+
-------
|
|
756
|
+
array-like
|
|
757
|
+
An array with the same type as `in_arr` that has the blob added.
|
|
758
|
+
"""
|
|
759
|
+
|
|
760
|
+
import xarray as xr
|
|
761
|
+
|
|
762
|
+
# Check if z location is there and set our 3D-ness based on this.
|
|
763
|
+
if v_loc is None:
|
|
764
|
+
is_3D = False
|
|
765
|
+
start_loc = 0
|
|
766
|
+
start_v = None
|
|
767
|
+
end_v = None
|
|
768
|
+
|
|
769
|
+
else:
|
|
770
|
+
is_3D = True
|
|
771
|
+
start_loc = 1
|
|
772
|
+
v_min = 0
|
|
773
|
+
v_max = in_arr.shape[start_loc]
|
|
774
|
+
start_v = int(np.ceil(max(v_min, v_loc - v_size / 2)))
|
|
775
|
+
end_v = int(np.ceil(min(v_max - 1, v_loc + v_size / 2)))
|
|
776
|
+
if v_size > v_max - v_min:
|
|
777
|
+
raise ValueError("v_size larger than domain size")
|
|
778
|
+
|
|
779
|
+
# Get min/max coordinates for hdim_1 and hdim_2
|
|
780
|
+
# Min is inclusive, end is exclusive
|
|
781
|
+
h1_min = 0
|
|
782
|
+
h1_max = in_arr.shape[start_loc]
|
|
783
|
+
|
|
784
|
+
h2_min = 0
|
|
785
|
+
h2_max = in_arr.shape[start_loc + 1]
|
|
786
|
+
|
|
787
|
+
if (h1_size > h1_max - h1_min) or (h2_size > h2_max - h2_min):
|
|
788
|
+
raise ValueError("Horizontal size larger than domain size")
|
|
789
|
+
|
|
790
|
+
# let's get start/end x/y/z
|
|
791
|
+
start_h1 = int(np.ceil(h1_loc - h1_size / 2))
|
|
792
|
+
end_h1 = int(np.ceil(h1_loc + h1_size / 2))
|
|
793
|
+
|
|
794
|
+
start_h2 = int(np.ceil(h2_loc - h2_size / 2))
|
|
795
|
+
end_h2 = int(np.ceil(h2_loc + h2_size / 2))
|
|
796
|
+
|
|
797
|
+
# get the coordinate sets
|
|
798
|
+
coords_to_fill = pbc_utils.get_pbc_coordinates(
|
|
799
|
+
h1_min,
|
|
800
|
+
h1_max,
|
|
801
|
+
h2_min,
|
|
802
|
+
h2_max,
|
|
803
|
+
start_h1,
|
|
804
|
+
end_h1,
|
|
805
|
+
start_h2,
|
|
806
|
+
end_h2,
|
|
807
|
+
PBC_flag=PBC_flag,
|
|
808
|
+
)
|
|
809
|
+
if shape == "rectangle":
|
|
810
|
+
for coord_box in coords_to_fill:
|
|
811
|
+
in_arr = set_arr_2D_3D(
|
|
812
|
+
in_arr,
|
|
813
|
+
amplitude,
|
|
814
|
+
coord_box[0],
|
|
815
|
+
coord_box[1],
|
|
816
|
+
coord_box[2],
|
|
817
|
+
coord_box[3],
|
|
818
|
+
start_v,
|
|
819
|
+
end_v,
|
|
820
|
+
)
|
|
821
|
+
return in_arr
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def set_arr_2D_3D(
|
|
825
|
+
in_arr, value, start_h1, end_h1, start_h2, end_h2, start_v=None, end_v=None
|
|
826
|
+
):
|
|
827
|
+
"""Function to set part of `in_arr` for either 2D or 3D points to `value`.
|
|
828
|
+
If `start_v` and `end_v` are not none, we assume that the array is 3D. If they
|
|
829
|
+
are none, we will set the array as if it is a 2D array.
|
|
830
|
+
|
|
831
|
+
Parameters
|
|
832
|
+
----------
|
|
833
|
+
in_arr: array-like
|
|
834
|
+
Array of values to set
|
|
835
|
+
|
|
836
|
+
value: int, float, or array-like of size (end_v-start_v, end_h1-start_h1, end_h2-start_h2)
|
|
837
|
+
The value to assign to in_arr. This will work to assign an array, but the array
|
|
838
|
+
must have the same dimensions as the size specified in the function.
|
|
839
|
+
|
|
840
|
+
start_h1: int
|
|
841
|
+
Start index to set for hdim_1
|
|
842
|
+
|
|
843
|
+
end_h1: int
|
|
844
|
+
End index to set for hdim_1 (exclusive, so it acts like [start_h1:end_h1])
|
|
845
|
+
|
|
846
|
+
start_h2: int
|
|
847
|
+
Start index to set for hdim_2
|
|
848
|
+
|
|
849
|
+
end_h2: int
|
|
850
|
+
End index to set for hdim_2
|
|
851
|
+
|
|
852
|
+
start_v: int, optional
|
|
853
|
+
Start index to set for vdim
|
|
854
|
+
Default is None
|
|
855
|
+
|
|
856
|
+
end_v: int, optional
|
|
857
|
+
End index to set for vdim
|
|
858
|
+
Default is None
|
|
859
|
+
|
|
860
|
+
Returns
|
|
861
|
+
-------
|
|
862
|
+
array-like
|
|
863
|
+
in_arr with the new values set.
|
|
864
|
+
"""
|
|
865
|
+
|
|
866
|
+
if start_v is not None and end_v is not None:
|
|
867
|
+
in_arr[start_v:end_v, start_h1:end_h1, start_h2:end_h2] = value
|
|
868
|
+
else:
|
|
869
|
+
in_arr[start_h1:end_h1, start_h2:end_h2] = value
|
|
870
|
+
|
|
871
|
+
return in_arr
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
def get_single_pbc_coordinate(
|
|
875
|
+
h1_min, h1_max, h2_min, h2_max, h1_coord, h2_coord, PBC_flag="none"
|
|
876
|
+
):
|
|
877
|
+
"""Function to get the PBC-adjusted coordinate for an original non-PBC adjusted
|
|
878
|
+
coordinate.
|
|
879
|
+
|
|
880
|
+
Parameters
|
|
881
|
+
----------
|
|
882
|
+
h1_min: int
|
|
883
|
+
Minimum point in hdim_1
|
|
884
|
+
h1_max: int
|
|
885
|
+
Maximum point in hdim_1
|
|
886
|
+
h2_min: int
|
|
887
|
+
Minimum point in hdim_2
|
|
888
|
+
h2_max: int
|
|
889
|
+
Maximum point in hdim_2
|
|
890
|
+
h1_coord: int
|
|
891
|
+
hdim_1 query coordinate
|
|
892
|
+
h2_coord: int
|
|
893
|
+
hdim_2 query coordinate
|
|
894
|
+
PBC_flag : str('none', 'hdim_1', 'hdim_2', 'both')
|
|
895
|
+
Sets whether to use periodic boundaries, and if so in which directions.
|
|
896
|
+
'none' means that we do not have periodic boundaries
|
|
897
|
+
'hdim_1' means that we are periodic along hdim1
|
|
898
|
+
'hdim_2' means that we are periodic along hdim2
|
|
899
|
+
'both' means that we are periodic along both horizontal dimensions
|
|
900
|
+
|
|
901
|
+
Returns
|
|
902
|
+
-------
|
|
903
|
+
tuple
|
|
904
|
+
Returns a tuple of (hdim_1, hdim_2).
|
|
905
|
+
|
|
906
|
+
Raises
|
|
907
|
+
------
|
|
908
|
+
ValueError
|
|
909
|
+
Raises a ValueError if the point is invalid (e.g., h1_coord < h1_min
|
|
910
|
+
when PBC_flag = 'none')
|
|
911
|
+
"""
|
|
912
|
+
# Avoiding duplicating code here, so throwing this into a loop.
|
|
913
|
+
is_pbc = [False, False]
|
|
914
|
+
if PBC_flag in ["hdim_1", "both"]:
|
|
915
|
+
is_pbc[0] = True
|
|
916
|
+
if PBC_flag in ["hdim_2", "both"]:
|
|
917
|
+
is_pbc[1] = True
|
|
918
|
+
|
|
919
|
+
out_coords = list()
|
|
920
|
+
|
|
921
|
+
for point_query, dim_min, dim_max, dim_pbc in zip(
|
|
922
|
+
[h1_coord, h2_coord], [h1_min, h2_min], [h1_max, h2_max], is_pbc
|
|
923
|
+
):
|
|
924
|
+
dim_size = dim_max - dim_min
|
|
925
|
+
if point_query >= dim_min and point_query < dim_max:
|
|
926
|
+
out_coords.append(point_query)
|
|
927
|
+
# off at least one domain
|
|
928
|
+
elif point_query < dim_min:
|
|
929
|
+
if not dim_pbc:
|
|
930
|
+
raise ValueError("Point invalid!")
|
|
931
|
+
if abs(point_query // dim_size) > 1:
|
|
932
|
+
# we are more than one interval length away from the periodic boundary
|
|
933
|
+
point_query = point_query + (
|
|
934
|
+
dim_size * (abs(point_query // dim_size) - 1)
|
|
935
|
+
)
|
|
936
|
+
|
|
937
|
+
out_coords.append(point_query + dim_size)
|
|
938
|
+
elif point_query >= dim_max:
|
|
939
|
+
if not dim_pbc:
|
|
940
|
+
raise ValueError("Point invalid!")
|
|
941
|
+
if abs(point_query // dim_size) > 1:
|
|
942
|
+
# we are more than one interval length away from the periodic boundary
|
|
943
|
+
point_query = point_query - (
|
|
944
|
+
dim_size * (abs(point_query // dim_size) - 1)
|
|
945
|
+
)
|
|
946
|
+
out_coords.append(point_query - dim_size)
|
|
947
|
+
|
|
948
|
+
return tuple(out_coords)
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
def generate_single_feature(
|
|
952
|
+
start_h1,
|
|
953
|
+
start_h2,
|
|
954
|
+
start_v=None,
|
|
955
|
+
spd_h1=1,
|
|
956
|
+
spd_h2=1,
|
|
957
|
+
spd_v=1,
|
|
958
|
+
min_h1=0,
|
|
959
|
+
max_h1=None,
|
|
960
|
+
min_h2=0,
|
|
961
|
+
max_h2=None,
|
|
962
|
+
num_frames=1,
|
|
963
|
+
dt=datetime.timedelta(minutes=5),
|
|
964
|
+
start_date=datetime.datetime(2022, 1, 1, 0),
|
|
965
|
+
PBC_flag="none",
|
|
966
|
+
frame_start=0,
|
|
967
|
+
feature_num=1,
|
|
968
|
+
feature_size=None,
|
|
969
|
+
threshold_val=None,
|
|
970
|
+
):
|
|
971
|
+
"""Function to generate a dummy feature dataframe to test the tracking functionality
|
|
972
|
+
|
|
973
|
+
Parameters
|
|
974
|
+
----------
|
|
975
|
+
start_h1: float
|
|
976
|
+
Starting point of the feature in hdim_1 space
|
|
977
|
+
|
|
978
|
+
start_h2: float
|
|
979
|
+
Starting point of the feature in hdim_2 space
|
|
980
|
+
|
|
981
|
+
start_v: float, optional
|
|
982
|
+
Starting point of the feature in vdim space (if 3D). For 2D, set to None.
|
|
983
|
+
Default is None
|
|
984
|
+
|
|
985
|
+
spd_h1: float, optional
|
|
986
|
+
Speed (per frame) of the feature in hdim_1
|
|
987
|
+
Default is 1
|
|
988
|
+
|
|
989
|
+
spd_h2: float, optional
|
|
990
|
+
Speed (per frame) of the feature in hdim_2
|
|
991
|
+
Default is 1
|
|
992
|
+
|
|
993
|
+
spd_v: float, optional
|
|
994
|
+
Speed (per frame) of the feature in vdim
|
|
995
|
+
Default is 1
|
|
996
|
+
|
|
997
|
+
min_h1: int, optional
|
|
998
|
+
Minimum value of hdim_1 allowed. If PBC_flag is not 'none', then
|
|
999
|
+
this will be used to know when to wrap around periodic boundaries.
|
|
1000
|
+
If PBC_flag is 'none', features will disappear if they are above/below
|
|
1001
|
+
these bounds.
|
|
1002
|
+
Default is 0
|
|
1003
|
+
|
|
1004
|
+
max_h1: int, optional
|
|
1005
|
+
Similar to min_h1, but the max value of hdim_1 allowed.
|
|
1006
|
+
Default is 1000
|
|
1007
|
+
|
|
1008
|
+
min_h2: int, optional
|
|
1009
|
+
Similar to min_h1, but the minimum value of hdim_2 allowed.
|
|
1010
|
+
Default is 0
|
|
1011
|
+
|
|
1012
|
+
max_h2: int, optional
|
|
1013
|
+
Similar to min_h1, but the maximum value of hdim_2 allowed.
|
|
1014
|
+
Default is 1000
|
|
1015
|
+
|
|
1016
|
+
num_frames: int, optional
|
|
1017
|
+
Number of frames to generate
|
|
1018
|
+
Default is 1
|
|
1019
|
+
|
|
1020
|
+
dt: datetime.timedelta, optional
|
|
1021
|
+
Difference in time between each frame
|
|
1022
|
+
Default is datetime.timedelta(minutes=5)
|
|
1023
|
+
|
|
1024
|
+
start_date: datetime.datetime, optional
|
|
1025
|
+
Start datetime
|
|
1026
|
+
Default is datetime.datetime(2022, 1, 1, 0)
|
|
1027
|
+
|
|
1028
|
+
PBC_flag : str('none', 'hdim_1', 'hdim_2', 'both')
|
|
1029
|
+
Sets whether to use periodic boundaries, and if so in which directions.
|
|
1030
|
+
'none' means that we do not have periodic boundaries
|
|
1031
|
+
'hdim_1' means that we are periodic along hdim1
|
|
1032
|
+
'hdim_2' means that we are periodic along hdim2
|
|
1033
|
+
'both' means that we are periodic along both horizontal dimensions
|
|
1034
|
+
frame_start: int
|
|
1035
|
+
Number to start the frame at
|
|
1036
|
+
Default is 1
|
|
1037
|
+
|
|
1038
|
+
feature_num: int, optional
|
|
1039
|
+
What number to start the feature at
|
|
1040
|
+
Default is 1
|
|
1041
|
+
feature_size: int or None
|
|
1042
|
+
'num' column in output; feature size
|
|
1043
|
+
If None, doesn't set this column
|
|
1044
|
+
threshold_val: float or None
|
|
1045
|
+
Threshold value of this feature
|
|
1046
|
+
"""
|
|
1047
|
+
|
|
1048
|
+
if max_h1 is None or max_h2 is None:
|
|
1049
|
+
raise ValueError("Max coords must be specified.")
|
|
1050
|
+
|
|
1051
|
+
out_list_of_dicts = list()
|
|
1052
|
+
curr_h1 = start_h1
|
|
1053
|
+
curr_h2 = start_h2
|
|
1054
|
+
curr_v = start_v
|
|
1055
|
+
curr_dt = start_date
|
|
1056
|
+
is_3D = not (start_v is None)
|
|
1057
|
+
for i in range(num_frames):
|
|
1058
|
+
curr_dict = dict()
|
|
1059
|
+
curr_h1, curr_h2 = get_single_pbc_coordinate(
|
|
1060
|
+
min_h1, max_h1, min_h2, max_h2, curr_h1, curr_h2, PBC_flag
|
|
1061
|
+
)
|
|
1062
|
+
curr_dict["hdim_1"] = curr_h1
|
|
1063
|
+
curr_dict["hdim_2"] = curr_h2
|
|
1064
|
+
curr_dict["frame"] = frame_start + i
|
|
1065
|
+
curr_dict["idx"] = 0
|
|
1066
|
+
if curr_v is not None:
|
|
1067
|
+
curr_dict["vdim"] = curr_v
|
|
1068
|
+
curr_v += spd_v
|
|
1069
|
+
curr_dict["time"] = curr_dt
|
|
1070
|
+
curr_dict["feature"] = feature_num + i
|
|
1071
|
+
if feature_size is not None:
|
|
1072
|
+
curr_dict["num"] = feature_size
|
|
1073
|
+
if threshold_val is not None:
|
|
1074
|
+
curr_dict["threshold_value"] = threshold_val
|
|
1075
|
+
curr_h1 += spd_h1
|
|
1076
|
+
curr_h2 += spd_h2
|
|
1077
|
+
curr_dt += dt
|
|
1078
|
+
out_list_of_dicts.append(curr_dict)
|
|
1079
|
+
|
|
1080
|
+
return pd.DataFrame.from_dict(out_list_of_dicts)
|
|
1081
|
+
|
|
1082
|
+
|
|
1083
|
+
def get_start_end_of_feat(center_point, size, axis_min, axis_max, is_pbc=False):
|
|
1084
|
+
"""Gets the start and ending points for a feature given a size and PBC
|
|
1085
|
+
conditions
|
|
1086
|
+
|
|
1087
|
+
Parameters
|
|
1088
|
+
----------
|
|
1089
|
+
center_point: float
|
|
1090
|
+
The center point of the feature
|
|
1091
|
+
size: float
|
|
1092
|
+
The size of the feature in this dimension
|
|
1093
|
+
axis_min: int
|
|
1094
|
+
Minimum point on the axis (usually 0)
|
|
1095
|
+
axis_max: int
|
|
1096
|
+
Maximum point on the axis (exclusive). This is 1 after
|
|
1097
|
+
the last real point on the axis, such that axis_max - axis_min
|
|
1098
|
+
is the size of the axis
|
|
1099
|
+
is_pbc: bool
|
|
1100
|
+
True if we should give wrap around points, false if we shouldn't.
|
|
1101
|
+
|
|
1102
|
+
Returns
|
|
1103
|
+
-------
|
|
1104
|
+
tuple (start_point, end_point)
|
|
1105
|
+
Note that if is_pbc is True, start_point can be less than axis_min and
|
|
1106
|
+
end_point can be greater than or equal to axis_max. This is designed to be used with
|
|
1107
|
+
```get_pbc_coordinates```
|
|
1108
|
+
"""
|
|
1109
|
+
import numpy as np
|
|
1110
|
+
|
|
1111
|
+
min_pt = int(np.ceil(center_point - size / 2))
|
|
1112
|
+
max_pt = int(np.ceil(center_point + size / 2))
|
|
1113
|
+
# adjust points for boundaries, if needed.
|
|
1114
|
+
if min_pt < axis_min and not is_pbc:
|
|
1115
|
+
min_pt = axis_min
|
|
1116
|
+
if max_pt > axis_max and not is_pbc:
|
|
1117
|
+
max_pt = axis_max
|
|
1118
|
+
|
|
1119
|
+
return (min_pt, max_pt)
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def generate_grid_coords(min_max_coords, lengths):
|
|
1123
|
+
"""Generates a grid of coordinates, such as fake lat/lons for testing.
|
|
1124
|
+
|
|
1125
|
+
Parameters
|
|
1126
|
+
----------
|
|
1127
|
+
min_max_coords: array-like, either length 2, length 4, or length 6.
|
|
1128
|
+
The minimum and maximum values in each dimension as:
|
|
1129
|
+
(min_dim1, max_dim1, min_dim2, max_dim2, min_dim3, max_dim3) to use
|
|
1130
|
+
all 3 dimensions. You can omit any dimensions that you aren't using.
|
|
1131
|
+
lengths: array-like, either length 1, 2, or 3.
|
|
1132
|
+
The lengths of values in each dimension. Length must equal 1/2 the length
|
|
1133
|
+
of min_max_coords.
|
|
1134
|
+
|
|
1135
|
+
Returns
|
|
1136
|
+
-------
|
|
1137
|
+
1, 2, or 3 array-likes
|
|
1138
|
+
array-like of grid coordinates in the number of dimensions requested
|
|
1139
|
+
and with the number of arrays specified (meshed coordinates)
|
|
1140
|
+
|
|
1141
|
+
"""
|
|
1142
|
+
import numpy as np
|
|
1143
|
+
|
|
1144
|
+
if len(min_max_coords) != len(lengths) * 2:
|
|
1145
|
+
raise ValueError(
|
|
1146
|
+
"The length of min_max_coords must be exactly 2 times"
|
|
1147
|
+
" the length of lengths."
|
|
1148
|
+
)
|
|
1149
|
+
|
|
1150
|
+
if len(lengths) == 1:
|
|
1151
|
+
return np.mgrid[
|
|
1152
|
+
min_max_coords[0] : min_max_coords[1] : complex(imag=lengths[0])
|
|
1153
|
+
]
|
|
1154
|
+
|
|
1155
|
+
if len(lengths) == 2:
|
|
1156
|
+
return np.mgrid[
|
|
1157
|
+
min_max_coords[0] : min_max_coords[1] : complex(imag=lengths[0]),
|
|
1158
|
+
min_max_coords[2] : min_max_coords[3] : complex(imag=lengths[1]),
|
|
1159
|
+
]
|
|
1160
|
+
|
|
1161
|
+
if len(lengths) == 3:
|
|
1162
|
+
return np.mgrid[
|
|
1163
|
+
min_max_coords[0] : min_max_coords[1] : complex(imag=lengths[0]),
|
|
1164
|
+
min_max_coords[2] : min_max_coords[3] : complex(imag=lengths[1]),
|
|
1165
|
+
min_max_coords[4] : min_max_coords[5] : complex(imag=lengths[2]),
|
|
1166
|
+
]
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
def lists_equal_without_order(a, b):
|
|
1170
|
+
"""
|
|
1171
|
+
This will make sure the inner list contain the same,
|
|
1172
|
+
but doesn't account for duplicate groups.
|
|
1173
|
+
from: https://stackoverflow.com/questions/31501909/assert-list-of-list-equality-without-order-in-python/31502000
|
|
1174
|
+
"""
|
|
1175
|
+
for l1 in a:
|
|
1176
|
+
check_counter = Counter(l1)
|
|
1177
|
+
if not any(Counter(l2) == check_counter for l2 in b):
|
|
1178
|
+
return False
|
|
1179
|
+
return True
|