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
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"""Utilities for handling indexing and distance calculation with periodic boundaries"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import functools
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from tobac.utils.decorators import njit_if_available
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def adjust_pbc_point(in_dim: int, dim_min: int, dim_max: int) -> int:
|
|
12
|
+
"""Function to adjust a point to the other boundary for PBCs
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
in_dim : int
|
|
17
|
+
Input coordinate to adjust
|
|
18
|
+
dim_min : int
|
|
19
|
+
Minimum point for the dimension
|
|
20
|
+
dim_max : int
|
|
21
|
+
Maximum point for the dimension (inclusive)
|
|
22
|
+
|
|
23
|
+
Returns
|
|
24
|
+
-------
|
|
25
|
+
int
|
|
26
|
+
The adjusted point on the opposite boundary
|
|
27
|
+
|
|
28
|
+
Raises
|
|
29
|
+
------
|
|
30
|
+
ValueError
|
|
31
|
+
If in_dim isn't on one of the boundary points
|
|
32
|
+
"""
|
|
33
|
+
if in_dim == dim_min:
|
|
34
|
+
return dim_max
|
|
35
|
+
elif in_dim == dim_max:
|
|
36
|
+
return dim_min
|
|
37
|
+
else:
|
|
38
|
+
raise ValueError("In adjust_pbc_point, in_dim isn't on a boundary.")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_pbc_coordinates(
|
|
42
|
+
h1_min: int,
|
|
43
|
+
h1_max: int,
|
|
44
|
+
h2_min: int,
|
|
45
|
+
h2_max: int,
|
|
46
|
+
h1_start_coord: int,
|
|
47
|
+
h1_end_coord: int,
|
|
48
|
+
h2_start_coord: int,
|
|
49
|
+
h2_end_coord: int,
|
|
50
|
+
PBC_flag: str = "none",
|
|
51
|
+
) -> list[tuple[int, int, int, int]]:
|
|
52
|
+
"""Function to get the real (i.e., shifted away from periodic boundaries) coordinate
|
|
53
|
+
boxes of interest given a set of coordinates that may cross periodic boundaries. This computes,
|
|
54
|
+
for example, multiple bounding boxes to encompass the real coordinates when given periodic
|
|
55
|
+
coordinates that loop around to the other boundary.
|
|
56
|
+
|
|
57
|
+
For example, if you pass in [as h1_start_coord, h1_end_coord, h2_start_coord, h2_end_coord]
|
|
58
|
+
(-3, 5, 2,6) with PBC_flag of 'both' or 'hdim_1', h1_max of 10, and h1_min of 0
|
|
59
|
+
this function will return: [(0,5,2,6), (7,10,2,6)].
|
|
60
|
+
|
|
61
|
+
If you pass in something outside the bounds of the array, this will truncate your
|
|
62
|
+
requested box. For example, if you pass in [as h1_start_coord, h1_end_coord, h2_start_coord, h2_end_coord]
|
|
63
|
+
(-3, 5, 2,6) with PBC_flag of 'none' or 'hdim_2', this function will return:
|
|
64
|
+
[(0,5,2,6)], assuming h1_min is 0.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
h1_min: int
|
|
69
|
+
Minimum array value in hdim_1, typically 0.
|
|
70
|
+
h1_max: int
|
|
71
|
+
Maximum array value in hdim_1 (exclusive). h1_max - h1_min should be the size in h1.
|
|
72
|
+
h2_min: int
|
|
73
|
+
Minimum array value in hdim_2, typically 0.
|
|
74
|
+
h2_max: int
|
|
75
|
+
Maximum array value in hdim_2 (exclusive). h2_max - h2_min should be the size in h2.
|
|
76
|
+
h1_start_coord: int
|
|
77
|
+
Start coordinate in hdim_1. Can be < h1_min if dealing with PBCs.
|
|
78
|
+
h1_end_coord: int
|
|
79
|
+
End coordinate in hdim_1. Can be >= h1_max if dealing with PBCs.
|
|
80
|
+
h2_start_coord: int
|
|
81
|
+
Start coordinate in hdim_2. Can be < h2_min if dealing with PBCs.
|
|
82
|
+
h2_end_coord: int
|
|
83
|
+
End coordinate in hdim_2. Can be >= h2_max if dealing with PBCs.
|
|
84
|
+
PBC_flag : str('none', 'hdim_1', 'hdim_2', 'both')
|
|
85
|
+
Sets whether to use periodic boundaries, and if so in which directions.
|
|
86
|
+
'none' means that we do not have periodic boundaries
|
|
87
|
+
'hdim_1' means that we are periodic along hdim1
|
|
88
|
+
'hdim_2' means that we are periodic along hdim2
|
|
89
|
+
'both' means that we are periodic along both horizontal dimensions
|
|
90
|
+
|
|
91
|
+
Returns
|
|
92
|
+
-------
|
|
93
|
+
list of tuples
|
|
94
|
+
A list of tuples containing (h1_start, h1_end, h2_start, h2_end) of each of the
|
|
95
|
+
boxes needed to encompass the coordinates.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
if PBC_flag not in ["none", "hdim_1", "hdim_2", "both"]:
|
|
99
|
+
raise ValueError("PBC_flag must be 'none', 'hdim_1', 'hdim_2', or 'both'")
|
|
100
|
+
|
|
101
|
+
h1_start_coords = list()
|
|
102
|
+
h1_end_coords = list()
|
|
103
|
+
h2_start_coords = list()
|
|
104
|
+
h2_end_coords = list()
|
|
105
|
+
|
|
106
|
+
# In both of these cases, we just need to truncate the hdim_1 points.
|
|
107
|
+
if PBC_flag in ["none", "hdim_2"]:
|
|
108
|
+
h1_start_coords.append(max(h1_min, h1_start_coord))
|
|
109
|
+
h1_end_coords.append(min(h1_max, h1_end_coord))
|
|
110
|
+
|
|
111
|
+
# In both of these cases, we only need to truncate the hdim_2 points.
|
|
112
|
+
if PBC_flag in ["none", "hdim_1"]:
|
|
113
|
+
h2_start_coords.append(max(h2_min, h2_start_coord))
|
|
114
|
+
h2_end_coords.append(min(h2_max, h2_end_coord))
|
|
115
|
+
|
|
116
|
+
# If the PBC flag is none, we can just return.
|
|
117
|
+
if PBC_flag == "none":
|
|
118
|
+
return [
|
|
119
|
+
(h1_start_coords[0], h1_end_coords[0], h2_start_coords[0], h2_end_coords[0])
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
# We have at least one periodic boundary.
|
|
123
|
+
|
|
124
|
+
# hdim_1 boundary is periodic.
|
|
125
|
+
if PBC_flag in ["hdim_1", "both"]:
|
|
126
|
+
if (h1_end_coord - h1_start_coord) >= (h1_max - h1_min):
|
|
127
|
+
# In this case, we have selected the full h1 length of the domain,
|
|
128
|
+
# so we set the start and end coords to just that.
|
|
129
|
+
h1_start_coords.append(h1_min)
|
|
130
|
+
h1_end_coords.append(h1_max)
|
|
131
|
+
|
|
132
|
+
# We know we only have either h1_end_coord > h1_max or h1_start_coord < h1_min
|
|
133
|
+
# and not both. If both are true, the previous if statement should trigger.
|
|
134
|
+
elif h1_start_coord < h1_min:
|
|
135
|
+
# First set of h1 start coordinates
|
|
136
|
+
h1_start_coords.append(h1_min)
|
|
137
|
+
h1_end_coords.append(h1_end_coord)
|
|
138
|
+
# Second set of h1 start coordinates
|
|
139
|
+
pts_from_begin = h1_min - h1_start_coord
|
|
140
|
+
h1_start_coords.append(h1_max - pts_from_begin)
|
|
141
|
+
h1_end_coords.append(h1_max)
|
|
142
|
+
|
|
143
|
+
elif h1_end_coord > h1_max:
|
|
144
|
+
h1_start_coords.append(h1_start_coord)
|
|
145
|
+
h1_end_coords.append(h1_max)
|
|
146
|
+
pts_from_end = h1_end_coord - h1_max
|
|
147
|
+
h1_start_coords.append(h1_min)
|
|
148
|
+
h1_end_coords.append(h1_min + pts_from_end)
|
|
149
|
+
|
|
150
|
+
# We have no PBC-related issues, actually
|
|
151
|
+
else:
|
|
152
|
+
h1_start_coords.append(h1_start_coord)
|
|
153
|
+
h1_end_coords.append(h1_end_coord)
|
|
154
|
+
|
|
155
|
+
if PBC_flag in ["hdim_2", "both"]:
|
|
156
|
+
if (h2_end_coord - h2_start_coord) >= (h2_max - h2_min):
|
|
157
|
+
# In this case, we have selected the full h2 length of the domain,
|
|
158
|
+
# so we set the start and end coords to just that.
|
|
159
|
+
h2_start_coords.append(h2_min)
|
|
160
|
+
h2_end_coords.append(h2_max)
|
|
161
|
+
|
|
162
|
+
# We know we only have either h1_end_coord > h1_max or h1_start_coord < h1_min
|
|
163
|
+
# and not both. If both are true, the previous if statement should trigger.
|
|
164
|
+
elif h2_start_coord < h2_min:
|
|
165
|
+
# First set of h1 start coordinates
|
|
166
|
+
h2_start_coords.append(h2_min)
|
|
167
|
+
h2_end_coords.append(h2_end_coord)
|
|
168
|
+
# Second set of h1 start coordinates
|
|
169
|
+
pts_from_begin = h2_min - h2_start_coord
|
|
170
|
+
h2_start_coords.append(h2_max - pts_from_begin)
|
|
171
|
+
h2_end_coords.append(h2_max)
|
|
172
|
+
|
|
173
|
+
elif h2_end_coord > h2_max:
|
|
174
|
+
h2_start_coords.append(h2_start_coord)
|
|
175
|
+
h2_end_coords.append(h2_max)
|
|
176
|
+
pts_from_end = h2_end_coord - h2_max
|
|
177
|
+
h2_start_coords.append(h2_min)
|
|
178
|
+
h2_end_coords.append(h2_min + pts_from_end)
|
|
179
|
+
|
|
180
|
+
# We have no PBC-related issues, actually
|
|
181
|
+
else:
|
|
182
|
+
h2_start_coords.append(h2_start_coord)
|
|
183
|
+
h2_end_coords.append(h2_end_coord)
|
|
184
|
+
|
|
185
|
+
out_coords = list()
|
|
186
|
+
for h1_start_coord_single, h1_end_coord_single in zip(
|
|
187
|
+
h1_start_coords, h1_end_coords
|
|
188
|
+
):
|
|
189
|
+
for h2_start_coord_single, h2_end_coord_single in zip(
|
|
190
|
+
h2_start_coords, h2_end_coords
|
|
191
|
+
):
|
|
192
|
+
out_coords.append(
|
|
193
|
+
(
|
|
194
|
+
h1_start_coord_single,
|
|
195
|
+
h1_end_coord_single,
|
|
196
|
+
h2_start_coord_single,
|
|
197
|
+
h2_end_coord_single,
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
return out_coords
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@njit_if_available
|
|
204
|
+
def calc_distance_coords_pbc(
|
|
205
|
+
coords_1: np.ndarray[float], coords_2: np.ndarray[float], max_dims: np.ndarray[int]
|
|
206
|
+
) -> float:
|
|
207
|
+
"""Function to calculate the distance between 2D cartesian
|
|
208
|
+
coordinate set 1 and coordinate set 2. Note that we assume both
|
|
209
|
+
coordinates are within their min/max already.
|
|
210
|
+
|
|
211
|
+
Parameters
|
|
212
|
+
----------
|
|
213
|
+
coords_1: 2D or 3D array-like
|
|
214
|
+
Set of coordinates passed in from trackpy of either (vdim, hdim_1, hdim_2)
|
|
215
|
+
coordinates or (hdim_1, hdim_2) coordinates.
|
|
216
|
+
coords_2: 2D or 3D array-like
|
|
217
|
+
Similar to coords_1, but for the second pair of coordinates
|
|
218
|
+
max_dims: array-like
|
|
219
|
+
Array of same length as dimensionality of coords. Each item in max_dims
|
|
220
|
+
corresponds to a dimension of coords ([(vdim), hdim_1, hdim_2]) with
|
|
221
|
+
value equal to the size of that dimension if periodic, or 0 if not
|
|
222
|
+
|
|
223
|
+
Returns
|
|
224
|
+
-------
|
|
225
|
+
float
|
|
226
|
+
Distance between coords_1 and coords_2 in cartesian space.
|
|
227
|
+
|
|
228
|
+
"""
|
|
229
|
+
deltas = np.abs(coords_1 - coords_2)
|
|
230
|
+
deltas = np.where(deltas > 0.5 * max_dims, deltas - max_dims, deltas)
|
|
231
|
+
return np.sqrt(np.sum(deltas**2))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def build_distance_function(min_h1, max_h1, min_h2, max_h2, PBC_flag, is_3D):
|
|
235
|
+
"""Function to build a partial ```calc_distance_coords_pbc``` function
|
|
236
|
+
suitable for use with trackpy
|
|
237
|
+
|
|
238
|
+
Parameters
|
|
239
|
+
----------
|
|
240
|
+
min_h1: int
|
|
241
|
+
Minimum point in hdim_1
|
|
242
|
+
max_h1: int
|
|
243
|
+
Maximum point in hdim_1
|
|
244
|
+
min_h2: int
|
|
245
|
+
Minimum point in hdim_2
|
|
246
|
+
max_h2: int
|
|
247
|
+
Maximum point in hdim_2
|
|
248
|
+
PBC_flag : str('none', 'hdim_1', 'hdim_2', 'both')
|
|
249
|
+
Sets whether to use periodic boundaries, and if so in which directions.
|
|
250
|
+
'none' means that we do not have periodic boundaries
|
|
251
|
+
'hdim_1' means that we are periodic along hdim1
|
|
252
|
+
'hdim_2' means that we are periodic along hdim2
|
|
253
|
+
'both' means that we are periodic along both horizontal dimensions
|
|
254
|
+
is_3D : bool
|
|
255
|
+
True if coordinates are to be provided in 3D, False if 2D
|
|
256
|
+
|
|
257
|
+
Returns
|
|
258
|
+
-------
|
|
259
|
+
function object
|
|
260
|
+
A version of calc_distance_coords_pbc suitable to be called by
|
|
261
|
+
just f(coords_1, coords_2)
|
|
262
|
+
|
|
263
|
+
"""
|
|
264
|
+
h1_size, h2_size = validate_pbc_dims(min_h1, max_h1, min_h2, max_h2, PBC_flag)
|
|
265
|
+
|
|
266
|
+
if is_3D:
|
|
267
|
+
max_dims = np.array([0, h1_size, h2_size])
|
|
268
|
+
else:
|
|
269
|
+
max_dims = np.array([h1_size, h2_size])
|
|
270
|
+
return functools.partial(
|
|
271
|
+
calc_distance_coords_pbc,
|
|
272
|
+
max_dims=max_dims,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def validate_pbc_dims(
|
|
277
|
+
min_h1: int, max_h1: int, min_h2: int, max_h2: int, PBC_flag: str
|
|
278
|
+
) -> tuple[int, int]:
|
|
279
|
+
"""Validate the input parameters for build_distance_function and return size of each axis
|
|
280
|
+
|
|
281
|
+
Parameters
|
|
282
|
+
----------
|
|
283
|
+
min_h1: int
|
|
284
|
+
Minimum point in hdim_1
|
|
285
|
+
max_h1: int
|
|
286
|
+
Maximum point in hdim_1
|
|
287
|
+
min_h2: int
|
|
288
|
+
Minimum point in hdim_2
|
|
289
|
+
max_h2: int
|
|
290
|
+
Maximum point in hdim_2
|
|
291
|
+
PBC_flag : str('none', 'hdim_1', 'hdim_2', 'both')
|
|
292
|
+
Sets whether to use periodic boundaries, and if so in which directions.
|
|
293
|
+
'none' means that we do not have periodic boundaries
|
|
294
|
+
'hdim_1' means that we are periodic along hdim1
|
|
295
|
+
'hdim_2' means that we are periodic along hdim2
|
|
296
|
+
'both' means that we are periodic along both horizontal dimensions
|
|
297
|
+
|
|
298
|
+
Returns
|
|
299
|
+
-------
|
|
300
|
+
tuple[int, int]
|
|
301
|
+
size of domain in hdim1 and hdim2
|
|
302
|
+
"""
|
|
303
|
+
if PBC_flag == "none":
|
|
304
|
+
return (0, 0)
|
|
305
|
+
if PBC_flag == "both":
|
|
306
|
+
invalid_dim_limits = invalid_limit_names(
|
|
307
|
+
min_h1=min_h1, max_h1=max_h1, min_h2=min_h2, max_h2=max_h2
|
|
308
|
+
)
|
|
309
|
+
if invalid_dim_limits:
|
|
310
|
+
raise PBCLimitError(invalid_dim_limits, PBC_flag)
|
|
311
|
+
return (max_h1 - min_h1, max_h2 - min_h2)
|
|
312
|
+
if PBC_flag == "hdim_1":
|
|
313
|
+
invalid_dim_limits = invalid_limit_names(min_h1=min_h1, max_h1=max_h1)
|
|
314
|
+
if invalid_dim_limits:
|
|
315
|
+
raise PBCLimitError(invalid_dim_limits, PBC_flag)
|
|
316
|
+
return (max_h1 - min_h1, 0)
|
|
317
|
+
if PBC_flag == "hdim_2":
|
|
318
|
+
invalid_dim_limits = invalid_limit_names(min_h2=min_h2, max_h2=max_h2)
|
|
319
|
+
if invalid_dim_limits:
|
|
320
|
+
raise PBCLimitError(invalid_dim_limits, PBC_flag)
|
|
321
|
+
return (0, max_h2 - min_h2)
|
|
322
|
+
# if PBC_flag not in ('none', 'hdim_1', 'hdim_2', 'both'):
|
|
323
|
+
raise PBCflagError()
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def invalid_limit_names(**limits) -> list[str]:
|
|
327
|
+
"""Return the names of keywords if their value is None
|
|
328
|
+
|
|
329
|
+
Returns
|
|
330
|
+
-------
|
|
331
|
+
list[str]
|
|
332
|
+
List of provided keywords with value None
|
|
333
|
+
"""
|
|
334
|
+
return [k for k, v in limits.items() if v is None]
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class PBCflagError(ValueError):
|
|
338
|
+
def __init__(self):
|
|
339
|
+
super().__init__(
|
|
340
|
+
"PBC_flag keyword is not valid, must be one of ['none', 'hdim_1', 'hdim_2', 'both']"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class PBCLimitError(ValueError):
|
|
345
|
+
def __init__(self, invalid_limits, PBC_flag):
|
|
346
|
+
self.message = f"Keyword parameters {invalid_limits} must be provided for PBC_flag {PBC_flag}"
|
|
347
|
+
super().__init__(self.message)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def weighted_circmean(
|
|
351
|
+
values: np.ndarray,
|
|
352
|
+
weights: np.ndarray,
|
|
353
|
+
high: float = 2 * np.pi,
|
|
354
|
+
low: float = 0,
|
|
355
|
+
axis: int | None = None,
|
|
356
|
+
) -> np.ndarray:
|
|
357
|
+
"""
|
|
358
|
+
Calculate the weighted circular mean over a set of values. If all the
|
|
359
|
+
weights are equal, this function is equivalent to scipy.stats.circmean
|
|
360
|
+
|
|
361
|
+
Parameters
|
|
362
|
+
----------
|
|
363
|
+
values: array-like
|
|
364
|
+
Array of values to calculate the mean over
|
|
365
|
+
weights: array-like
|
|
366
|
+
Array of weights corresponding to each value
|
|
367
|
+
high: float, optional
|
|
368
|
+
Upper bound of the range of values. Defaults to 2*pi
|
|
369
|
+
low: float, optional
|
|
370
|
+
Lower bound of the range of values. Defaults to 0
|
|
371
|
+
axis: int | None, optional
|
|
372
|
+
Axis over which to take the average. If None, the average will be taken
|
|
373
|
+
over the entire array. Defaults to None
|
|
374
|
+
|
|
375
|
+
Returns
|
|
376
|
+
-------
|
|
377
|
+
rescaled_average: numpy.ndarray
|
|
378
|
+
The weighted, circular mean over the given values
|
|
379
|
+
|
|
380
|
+
"""
|
|
381
|
+
scaling_factor = (high - low) / (2 * np.pi)
|
|
382
|
+
scaled_values = (np.asarray(values) - low) / scaling_factor
|
|
383
|
+
sin_average = np.average(np.sin(scaled_values), axis=axis, weights=weights)
|
|
384
|
+
cos_average = np.average(np.cos(scaled_values), axis=axis, weights=weights)
|
|
385
|
+
# If the values are evenly spaced throughout the range rounding errors have a big impact. Default to np.pi (half way between low and high) if this is the case
|
|
386
|
+
if np.isclose(sin_average, 0) and np.isclose(cos_average, 0):
|
|
387
|
+
angle_average = np.pi
|
|
388
|
+
else:
|
|
389
|
+
angle_average = np.arctan2(sin_average, cos_average) % (2 * np.pi)
|
|
390
|
+
rescaled_average = (angle_average * scaling_factor) + low
|
|
391
|
+
# Round return value to try and supress rounding errors
|
|
392
|
+
rescaled_average = np.round(rescaled_average, 12)
|
|
393
|
+
if rescaled_average == high:
|
|
394
|
+
rescaled_average = low
|
|
395
|
+
return rescaled_average
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def transfm_pbc_point(in_dim, dim_min, dim_max):
|
|
399
|
+
"""Function to transform a PBC-feature point for contiguity
|
|
400
|
+
|
|
401
|
+
Parameters
|
|
402
|
+
----------
|
|
403
|
+
in_dim : int
|
|
404
|
+
Input coordinate to adjust
|
|
405
|
+
dim_min : int
|
|
406
|
+
Minimum point for the dimension
|
|
407
|
+
dim_max : int
|
|
408
|
+
Maximum point for the dimension (inclusive)
|
|
409
|
+
|
|
410
|
+
Returns
|
|
411
|
+
-------
|
|
412
|
+
int
|
|
413
|
+
The transformed point
|
|
414
|
+
|
|
415
|
+
"""
|
|
416
|
+
if in_dim < ((dim_min + dim_max) / 2):
|
|
417
|
+
return in_dim + dim_max + 1
|
|
418
|
+
else:
|
|
419
|
+
return in_dim
|
tobac/wrapper.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import logging
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def tracking_wrapper(
|
|
7
|
+
field_in_features,
|
|
8
|
+
field_in_segmentation,
|
|
9
|
+
time_spacing=None,
|
|
10
|
+
grid_spacing=None,
|
|
11
|
+
parameters_features=None,
|
|
12
|
+
parameters_tracking=None,
|
|
13
|
+
parameters_segmentation=None,
|
|
14
|
+
):
|
|
15
|
+
from .feature_detection import feature_detection_multithreshold
|
|
16
|
+
from .tracking import linking_trackpy
|
|
17
|
+
from tobac.segmentation.watershed_segmentation import (
|
|
18
|
+
segmentation_3D,
|
|
19
|
+
segmentation_2D,
|
|
20
|
+
)
|
|
21
|
+
from .utils import get_spacings
|
|
22
|
+
|
|
23
|
+
warnings.warn(
|
|
24
|
+
"tracking_wrapper is depreciated and will be removed in v2.0.",
|
|
25
|
+
DeprecationWarning,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger("trackpy")
|
|
29
|
+
logger.propagate = False
|
|
30
|
+
logger.setLevel(logging.WARNING)
|
|
31
|
+
|
|
32
|
+
### Prepare Tracking
|
|
33
|
+
|
|
34
|
+
dxy, dt = get_spacings(
|
|
35
|
+
field_in_features, grid_spacing=grid_spacing, time_spacing=time_spacing
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
### Start Tracking
|
|
39
|
+
# Feature detection:
|
|
40
|
+
|
|
41
|
+
method_detection = parameters_features.pop("method_detection", None)
|
|
42
|
+
if method_detection in ["threshold", "threshold_multi"]:
|
|
43
|
+
features = feature_detection_multithreshold(
|
|
44
|
+
field_in_features, **parameters_features
|
|
45
|
+
)
|
|
46
|
+
else:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
"method_detection unknown, has to be either threshold_multi or threshold"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
method_segmentation = parameters_features.pop("method_segmentation", None)
|
|
52
|
+
|
|
53
|
+
if method_segmentation == "watershedding":
|
|
54
|
+
if field_in_segmentation.ndim == 4:
|
|
55
|
+
segmentation_mask, features_segmentation = segmentation_3D(
|
|
56
|
+
features, field_in_segmentation, **parameters_segmentation
|
|
57
|
+
)
|
|
58
|
+
if field_in_segmentation.ndim == 3:
|
|
59
|
+
segmentation_mask, features_segmentation = segmentation_2D(
|
|
60
|
+
features, field_in_segmentation, **parameters_segmentation
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Link the features in the individual frames to trajectories:
|
|
64
|
+
method_linking = parameters_features.pop("method_linking", None)
|
|
65
|
+
|
|
66
|
+
if method_linking == "trackpy":
|
|
67
|
+
trajectories = linking_trackpy(features, **parameters_tracking)
|
|
68
|
+
logging.debug("Finished tracking")
|
|
69
|
+
else:
|
|
70
|
+
raise ValueError("method_linking unknown, has to be trackpy")
|
|
71
|
+
|
|
72
|
+
return features, segmentation_mask, trajectories
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def maketrack(
|
|
76
|
+
field_in,
|
|
77
|
+
grid_spacing=None,
|
|
78
|
+
time_spacing=None,
|
|
79
|
+
target="maximum",
|
|
80
|
+
v_max=None,
|
|
81
|
+
d_max=None,
|
|
82
|
+
memory=0,
|
|
83
|
+
stubs=5,
|
|
84
|
+
order=1,
|
|
85
|
+
extrapolate=0,
|
|
86
|
+
method_detection="threshold",
|
|
87
|
+
position_threshold="center",
|
|
88
|
+
sigma_threshold=0.5,
|
|
89
|
+
n_erosion_threshold=0,
|
|
90
|
+
threshold=1,
|
|
91
|
+
min_num=0,
|
|
92
|
+
min_distance=0,
|
|
93
|
+
method_linking="random",
|
|
94
|
+
cell_number_start=1,
|
|
95
|
+
subnetwork_size=None,
|
|
96
|
+
adaptive_stop=None,
|
|
97
|
+
adaptive_step=None,
|
|
98
|
+
return_intermediate=False,
|
|
99
|
+
):
|
|
100
|
+
from .feature_detection import feature_detection_multithreshold
|
|
101
|
+
from .tracking import linking_trackpy
|
|
102
|
+
|
|
103
|
+
"""
|
|
104
|
+
Function identifiying features andlinking them into trajectories
|
|
105
|
+
|
|
106
|
+
Parameters:
|
|
107
|
+
field_in: iris.cube.Cube
|
|
108
|
+
2D input field tracking is performed on
|
|
109
|
+
grid_spacing: float
|
|
110
|
+
grid spacing in input data (m)
|
|
111
|
+
time_spacing: float
|
|
112
|
+
time resolution of input data (s)
|
|
113
|
+
target string
|
|
114
|
+
Switch to determine if algorithm looks for maxima or minima in input field (maximum: look for maxima (default), minimum: look for minima)
|
|
115
|
+
v_max: float
|
|
116
|
+
Assumed maximum speed of tracked objects (m/s)
|
|
117
|
+
memory: int
|
|
118
|
+
Number of timesteps for which objects can be missed by the algorithm to still give a constistent track
|
|
119
|
+
stubs: float
|
|
120
|
+
Minumum number of timesteps for which objects have to be detected to not be filtered out as spurious
|
|
121
|
+
min_num: int
|
|
122
|
+
Minumum number of cells above threshold in the feature to be tracked
|
|
123
|
+
order: int
|
|
124
|
+
order if interpolation spline to fill gaps in tracking(from allowing memory to be larger than 0)
|
|
125
|
+
extrapolate int
|
|
126
|
+
number of points to extrapolate individual tracks by
|
|
127
|
+
method_detection: str('threshold' or 'threshold_multi')
|
|
128
|
+
flag choosing method used for feature detection
|
|
129
|
+
position_threshold: str('extreme', 'weighted_diff', 'weighted_abs' or 'center')
|
|
130
|
+
flag choosing method used for the position of the tracked feature
|
|
131
|
+
sigma_threshold: float
|
|
132
|
+
standard deviation for intial filtering step
|
|
133
|
+
|
|
134
|
+
n_erosion_threshold: int
|
|
135
|
+
number of pixel by which to erode the identified features
|
|
136
|
+
|
|
137
|
+
method_linking: str('predict' or 'random')
|
|
138
|
+
flag choosing method used for trajectory linking
|
|
139
|
+
|
|
140
|
+
return_intermediate: boolean
|
|
141
|
+
flag to tetermine if only final tracjectories are output (False, default) or if detected features, filtered features and unfilled tracks are returned additionally (True)
|
|
142
|
+
|
|
143
|
+
Output:
|
|
144
|
+
trajectories_final: pandas.DataFrame
|
|
145
|
+
Tracked updrafts, one row per timestep and updraft, includes dimensions 'time','latitude','longitude','projection_x_variable', 'projection_y_variable' based on w cube.
|
|
146
|
+
'hdim_1' and 'hdim_2' are used for segementation step.
|
|
147
|
+
|
|
148
|
+
Optional output:
|
|
149
|
+
features_filtered: pandas.DataFrame
|
|
150
|
+
|
|
151
|
+
features_unfiltered: pandas.DataFrame
|
|
152
|
+
|
|
153
|
+
trajectories_filtered_unfilled: pandas.DataFrame
|
|
154
|
+
|
|
155
|
+
"""
|
|
156
|
+
from copy import deepcopy
|
|
157
|
+
|
|
158
|
+
warnings.warn(
|
|
159
|
+
"maketrack is depreciated and will be removed in v2.0.",
|
|
160
|
+
DeprecationWarning,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
logger = logging.getLogger("trackpy")
|
|
164
|
+
logger.propagate = False
|
|
165
|
+
logger.setLevel(logging.WARNING)
|
|
166
|
+
|
|
167
|
+
### Prepare Tracking
|
|
168
|
+
|
|
169
|
+
# set horizontal grid spacing of input data
|
|
170
|
+
# If cartesian x and y corrdinates are present, use these to determine dxy (vertical grid spacing used to transfer pixel distances to real distances):
|
|
171
|
+
coord_names = [coord.name() for coord in field_in.coords()]
|
|
172
|
+
|
|
173
|
+
if (
|
|
174
|
+
"projection_x_coordinate" in coord_names
|
|
175
|
+
and "projection_y_coordinate" in coord_names
|
|
176
|
+
) and (grid_spacing is None):
|
|
177
|
+
x_coord = deepcopy(field_in.coord("projection_x_coordinate"))
|
|
178
|
+
x_coord.convert_units("metre")
|
|
179
|
+
dx = np.diff(field_in.coord("projection_y_coordinate")[0:2].points)[0]
|
|
180
|
+
y_coord = deepcopy(field_in.coord("projection_y_coordinate"))
|
|
181
|
+
y_coord.convert_units("metre")
|
|
182
|
+
dy = np.diff(field_in.coord("projection_y_coordinate")[0:2].points)[0]
|
|
183
|
+
dxy = 0.5 * (dx + dy)
|
|
184
|
+
elif grid_spacing is not None:
|
|
185
|
+
dxy = grid_spacing
|
|
186
|
+
else:
|
|
187
|
+
ValueError(
|
|
188
|
+
"no information about grid spacing, need either input cube with projection_x_coord and projection_y_coord or keyword argument grid_spacing"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# set horizontal grid spacing of input data
|
|
192
|
+
if time_spacing is None:
|
|
193
|
+
# get time resolution of input data from first to steps of input cube:
|
|
194
|
+
time_coord = field_in.coord("time")
|
|
195
|
+
dt = (
|
|
196
|
+
time_coord.units.num2date(time_coord.points[1])
|
|
197
|
+
- time_coord.units.num2date(time_coord.points[0])
|
|
198
|
+
).seconds
|
|
199
|
+
elif time_spacing is not None:
|
|
200
|
+
# use value of time_spacing for dt:
|
|
201
|
+
dt = time_spacing
|
|
202
|
+
|
|
203
|
+
### Start Tracking
|
|
204
|
+
# Feature detection:
|
|
205
|
+
if method_detection in ["threshold", "threshold_multi"]:
|
|
206
|
+
features = feature_detection_multithreshold(
|
|
207
|
+
field_in=field_in,
|
|
208
|
+
threshold=threshold,
|
|
209
|
+
dxy=dxy,
|
|
210
|
+
target=target,
|
|
211
|
+
position_threshold=position_threshold,
|
|
212
|
+
sigma_threshold=sigma_threshold,
|
|
213
|
+
n_erosion_threshold=n_erosion_threshold,
|
|
214
|
+
)
|
|
215
|
+
features_filtered = features.drop(features[features["num"] < min_num].index)
|
|
216
|
+
|
|
217
|
+
else:
|
|
218
|
+
raise ValueError(
|
|
219
|
+
"method_detection unknown, has to be either threshold_multi or threshold"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Link the features in the individual frames to trajectories:
|
|
223
|
+
|
|
224
|
+
trajectories = linking_trackpy(
|
|
225
|
+
features=features_filtered,
|
|
226
|
+
field_in=field_in,
|
|
227
|
+
dxy=dxy,
|
|
228
|
+
dt=dt,
|
|
229
|
+
memory=memory,
|
|
230
|
+
subnetwork_size=subnetwork_size,
|
|
231
|
+
adaptive_stop=adaptive_stop,
|
|
232
|
+
adaptive_step=adaptive_step,
|
|
233
|
+
v_max=v_max,
|
|
234
|
+
d_max=d_max,
|
|
235
|
+
stubs=stubs,
|
|
236
|
+
order=order,
|
|
237
|
+
extrapolate=extrapolate,
|
|
238
|
+
method_linking=method_linking,
|
|
239
|
+
cell_number_start=1,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
logging.debug("Finished tracking")
|
|
243
|
+
|
|
244
|
+
return trajectories, features
|