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
@@ -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