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.
Files changed (53) hide show
  1. tobac/__init__.py +112 -0
  2. tobac/analysis/__init__.py +31 -0
  3. tobac/analysis/cell_analysis.py +628 -0
  4. tobac/analysis/feature_analysis.py +212 -0
  5. tobac/analysis/spatial.py +619 -0
  6. tobac/centerofgravity.py +226 -0
  7. tobac/feature_detection.py +1758 -0
  8. tobac/merge_split.py +324 -0
  9. tobac/plotting.py +2321 -0
  10. tobac/segmentation/__init__.py +10 -0
  11. tobac/segmentation/watershed_segmentation.py +1316 -0
  12. tobac/testing.py +1179 -0
  13. tobac/tests/segmentation_tests/test_iris_xarray_segmentation.py +0 -0
  14. tobac/tests/segmentation_tests/test_segmentation.py +1183 -0
  15. tobac/tests/segmentation_tests/test_segmentation_time_pad.py +104 -0
  16. tobac/tests/test_analysis_spatial.py +1109 -0
  17. tobac/tests/test_convert.py +265 -0
  18. tobac/tests/test_datetime.py +216 -0
  19. tobac/tests/test_decorators.py +148 -0
  20. tobac/tests/test_feature_detection.py +1321 -0
  21. tobac/tests/test_generators.py +273 -0
  22. tobac/tests/test_import.py +24 -0
  23. tobac/tests/test_iris_xarray_match_utils.py +244 -0
  24. tobac/tests/test_merge_split.py +351 -0
  25. tobac/tests/test_pbc_utils.py +497 -0
  26. tobac/tests/test_sample_data.py +197 -0
  27. tobac/tests/test_testing.py +747 -0
  28. tobac/tests/test_tracking.py +714 -0
  29. tobac/tests/test_utils.py +650 -0
  30. tobac/tests/test_utils_bulk_statistics.py +789 -0
  31. tobac/tests/test_utils_coordinates.py +328 -0
  32. tobac/tests/test_utils_internal.py +97 -0
  33. tobac/tests/test_xarray_utils.py +232 -0
  34. tobac/tracking.py +613 -0
  35. tobac/utils/__init__.py +27 -0
  36. tobac/utils/bulk_statistics.py +360 -0
  37. tobac/utils/datetime.py +184 -0
  38. tobac/utils/decorators.py +540 -0
  39. tobac/utils/general.py +753 -0
  40. tobac/utils/generators.py +87 -0
  41. tobac/utils/internal/__init__.py +2 -0
  42. tobac/utils/internal/coordinates.py +430 -0
  43. tobac/utils/internal/iris_utils.py +462 -0
  44. tobac/utils/internal/label_props.py +82 -0
  45. tobac/utils/internal/xarray_utils.py +439 -0
  46. tobac/utils/mask.py +364 -0
  47. tobac/utils/periodic_boundaries.py +419 -0
  48. tobac/wrapper.py +244 -0
  49. tobac-1.6.2.dist-info/METADATA +154 -0
  50. tobac-1.6.2.dist-info/RECORD +53 -0
  51. tobac-1.6.2.dist-info/WHEEL +5 -0
  52. tobac-1.6.2.dist-info/licenses/LICENSE +29 -0
  53. 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