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,273 @@
1
+ """Unit tests for tobac.utils.generators module"""
2
+
3
+ from datetime import datetime, timedelta
4
+
5
+ import cftime
6
+ import numpy as np
7
+ import pandas as pd
8
+ import pytest
9
+ import xarray as xr
10
+ from pandas.testing import assert_frame_equal
11
+
12
+ from tobac.utils import generators
13
+
14
+
15
+ def test_field_and_features_over_time():
16
+ """Test iterating over field_and_features_over_time generator"""
17
+ test_data = xr.DataArray(
18
+ np.zeros([2, 10, 10]),
19
+ dims=("time", "y", "x"),
20
+ coords={"time": [datetime(2000, 1, 1), datetime(2000, 1, 1, 1)]},
21
+ )
22
+
23
+ test_features = pd.DataFrame(
24
+ {
25
+ "feature": [1, 2, 3],
26
+ "frame": [0, 0, 1],
27
+ "time": [
28
+ datetime(2000, 1, 1),
29
+ datetime(2000, 1, 1),
30
+ datetime(2000, 1, 1, 1),
31
+ ],
32
+ }
33
+ )
34
+
35
+ iterator = generators.field_and_features_over_time(test_data, test_features)
36
+
37
+ iter_0 = next(iterator)
38
+
39
+ assert iter_0[0] == 0
40
+ assert iter_0[1] == np.datetime64("2000-01-01")
41
+ assert np.all(iter_0[2] == test_data.isel(time=0))
42
+ assert_frame_equal(
43
+ iter_0[3], test_features[test_features.time == datetime(2000, 1, 1)]
44
+ )
45
+
46
+ iter_1 = next(iterator)
47
+
48
+ assert iter_1[0] == 1
49
+ assert iter_1[1] == np.datetime64("2000-01-01 01:00:00")
50
+ assert np.all(iter_1[2] == test_data.isel(time=1))
51
+ assert_frame_equal(
52
+ iter_1[3], test_features[test_features.time == datetime(2000, 1, 1, 1)]
53
+ )
54
+
55
+ with pytest.raises(StopIteration):
56
+ next(iterator)
57
+
58
+
59
+ def test_field_and_features_over_time_no_0_dataframe():
60
+ """Test that field and features over time works when the dataframe has no 0
61
+ index value
62
+ """
63
+
64
+ test_data = xr.DataArray(
65
+ np.zeros([2, 10, 10]),
66
+ dims=("time", "y", "x"),
67
+ coords={"time": [datetime(2000, 1, 1), datetime(2000, 1, 1, 1)]},
68
+ )
69
+
70
+ test_features = pd.DataFrame(
71
+ {
72
+ "feature": [1, 2, 3],
73
+ "frame": [0, 0, 1],
74
+ "time": [
75
+ datetime(2000, 1, 1),
76
+ datetime(2000, 1, 1),
77
+ datetime(2000, 1, 1, 1),
78
+ ],
79
+ },
80
+ index=[1, 2, 3],
81
+ )
82
+
83
+ iterator = generators.field_and_features_over_time(test_data, test_features)
84
+
85
+ iter_0 = next(iterator)
86
+
87
+ assert iter_0[0] == 0
88
+ assert iter_0[1] == np.datetime64("2000-01-01")
89
+ assert np.all(iter_0[2] == test_data.isel(time=0))
90
+ assert_frame_equal(
91
+ iter_0[3], test_features[test_features.time == datetime(2000, 1, 1)]
92
+ )
93
+
94
+
95
+ def test_field_and_features_over_time_time_padding():
96
+ """Test the time_padding functionality of field_and_features_over_time
97
+ generator
98
+ """
99
+ test_data = xr.DataArray(
100
+ np.zeros([1, 10, 10]),
101
+ dims=("time", "y", "x"),
102
+ coords={"time": [datetime(2000, 1, 1)]},
103
+ )
104
+
105
+ test_features = pd.DataFrame(
106
+ {
107
+ "feature": [1, 2, 3],
108
+ "frame": [0, 0, 0],
109
+ "time": [
110
+ datetime(2000, 1, 1),
111
+ datetime(2000, 1, 1, 0, 0, 1),
112
+ datetime(2000, 1, 1, 0, 0, 2),
113
+ ],
114
+ }
115
+ )
116
+
117
+ # Test no time padding
118
+ _, _, _, df_slice = next(
119
+ generators.field_and_features_over_time(test_data, test_features)
120
+ )
121
+
122
+ assert len(df_slice) == 1
123
+ assert_frame_equal(df_slice, test_features.loc[0:0])
124
+
125
+ # Test time padding of 1 second
126
+ _, _, _, df_slice = next(
127
+ generators.field_and_features_over_time(
128
+ test_data, test_features, time_padding=timedelta(seconds=1)
129
+ )
130
+ )
131
+
132
+ assert len(df_slice) == 2
133
+ assert_frame_equal(df_slice, test_features.loc[0:1])
134
+
135
+ # Test time padding of 2 seconds
136
+ _, _, _, df_slice = next(
137
+ generators.field_and_features_over_time(
138
+ test_data, test_features, time_padding=timedelta(seconds=2)
139
+ )
140
+ )
141
+
142
+ assert len(df_slice) == 3
143
+ assert_frame_equal(df_slice, test_features.loc[0:2])
144
+
145
+
146
+ def test_field_and_features_over_time_cftime():
147
+ """Test field_and_features_over_time when given cftime datetime formats"""
148
+ test_data = xr.DataArray(
149
+ np.zeros([2, 10, 10]),
150
+ dims=("time", "y", "x"),
151
+ coords={
152
+ "time": [
153
+ cftime.Datetime360Day(2000, 1, 1),
154
+ cftime.Datetime360Day(2000, 1, 1, 1),
155
+ ]
156
+ },
157
+ )
158
+
159
+ test_features = pd.DataFrame(
160
+ {
161
+ "feature": [1, 2, 3],
162
+ "frame": [0, 0, 1],
163
+ "time": [
164
+ cftime.Datetime360Day(2000, 1, 1),
165
+ cftime.Datetime360Day(2000, 1, 1, 0, 0, 1),
166
+ cftime.Datetime360Day(2000, 1, 1, 1),
167
+ ],
168
+ }
169
+ )
170
+
171
+ iterator = generators.field_and_features_over_time(
172
+ test_data, test_features, time_padding=timedelta(seconds=1)
173
+ )
174
+
175
+ iter_0 = next(iterator)
176
+
177
+ assert iter_0[0] == 0
178
+ assert iter_0[1] == cftime.Datetime360Day(2000, 1, 1)
179
+ assert np.all(iter_0[2] == test_data.isel(time=0))
180
+ assert_frame_equal(iter_0[3], test_features.loc[0:1])
181
+
182
+ iter_1 = next(iterator)
183
+
184
+ assert iter_1[0] == 1
185
+ assert iter_1[1] == cftime.Datetime360Day(2000, 1, 1, 1)
186
+ assert np.all(iter_1[2] == test_data.isel(time=1))
187
+ assert_frame_equal(
188
+ iter_1[3],
189
+ test_features[test_features.time == cftime.Datetime360Day(2000, 1, 1, 1)],
190
+ )
191
+
192
+ with pytest.raises(StopIteration):
193
+ next(iterator)
194
+
195
+
196
+ def test_field_and_features_over_time_time_var_name():
197
+ """Test field_and_features_over_time generator works correctly with a time
198
+ coordinate name other than "time"
199
+ """
200
+ # Test non-standard time coord name:
201
+ test_data = xr.DataArray(
202
+ np.zeros([2, 10, 10]),
203
+ dims=("time_testing", "y", "x"),
204
+ coords={"time_testing": [datetime(2000, 1, 1), datetime(2000, 1, 1, 1)]},
205
+ )
206
+
207
+ test_features = pd.DataFrame(
208
+ {
209
+ "feature": [1, 2, 3],
210
+ "frame": [0, 0, 1],
211
+ "time_testing": [
212
+ datetime(2000, 1, 1),
213
+ datetime(2000, 1, 1),
214
+ datetime(2000, 1, 1, 1),
215
+ ],
216
+ }
217
+ )
218
+
219
+ _ = next(
220
+ generators.field_and_features_over_time(
221
+ test_data, test_features, time_var_name="time_testing"
222
+ )
223
+ )
224
+
225
+
226
+ def test_field_and_features_over_time_time_var_name_error():
227
+ """Test that field_and_features_over_time generator raises the correct
228
+ error when the name of the time coordinates do not match between the given
229
+ data and dataframe
230
+ """
231
+ # Test if time_var_name not in dataarray:
232
+ test_data = xr.DataArray(
233
+ np.zeros([2, 10, 10]),
234
+ dims=("time_testing", "y", "x"),
235
+ coords={"time_testing": [datetime(2000, 1, 1), datetime(2000, 1, 1, 1)]},
236
+ )
237
+
238
+ test_features = pd.DataFrame(
239
+ {
240
+ "feature": [1, 2, 3],
241
+ "frame": [0, 0, 1],
242
+ "time": [
243
+ datetime(2000, 1, 1),
244
+ datetime(2000, 1, 1),
245
+ datetime(2000, 1, 1, 1),
246
+ ],
247
+ }
248
+ )
249
+
250
+ with pytest.raises(ValueError, match="time not present in input field*"):
251
+ next(generators.field_and_features_over_time(test_data, test_features))
252
+
253
+ # Test if time var name not in dataframe:
254
+ test_data = xr.DataArray(
255
+ np.zeros([2, 10, 10]),
256
+ dims=("time", "y", "x"),
257
+ coords={"time": [datetime(2000, 1, 1), datetime(2000, 1, 1, 1)]},
258
+ )
259
+
260
+ test_features = pd.DataFrame(
261
+ {
262
+ "feature": [1, 2, 3],
263
+ "frame": [0, 0, 1],
264
+ "time_testing": [
265
+ datetime(2000, 1, 1),
266
+ datetime(2000, 1, 1),
267
+ datetime(2000, 1, 1, 1),
268
+ ],
269
+ }
270
+ )
271
+
272
+ with pytest.raises(ValueError, match="time not present in input feature*"):
273
+ next(generators.field_and_features_over_time(test_data, test_features))
@@ -0,0 +1,24 @@
1
+ import pytest
2
+ import tobac
3
+
4
+
5
+ def test_dummy_function():
6
+ assert 1 == 1
7
+
8
+
9
+ def test_version():
10
+ """Test to make sure that we have a version number included.
11
+ Also test to make sure that the version number complies with
12
+ semantic versioning guidelines.
13
+ If it's not, this should result in an error.
14
+ """
15
+ import re
16
+
17
+ assert type(tobac.__version__) is str
18
+ # Make sure that we are following semantic versioning
19
+ # i.e., our version is of form x.x.x, where x are all
20
+ # integer numbers.
21
+ assert (
22
+ re.match(r"[0-9]+\.[0-9]+\.[0-9]+", tobac.__version__) is not None
23
+ or tobac.__version__ == "unknown_dev_version"
24
+ )
@@ -0,0 +1,244 @@
1
+ """Tests to confirm that xarray and iris pathways work the same and produce the same data
2
+ for the same input datasets.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import copy
8
+ import datetime
9
+
10
+ import iris.cube
11
+ import numpy as np
12
+ import pandas as pd
13
+ import xarray as xr
14
+ import pytest
15
+
16
+
17
+ import tobac.testing as tbtest
18
+ import tobac.utils.internal.iris_utils as iris_utils
19
+ import tobac.utils.internal.xarray_utils as xr_utils
20
+ import tobac.utils.datetime as datetime_utils
21
+ from tobac.utils.decorators import convert_cube_to_dataarray
22
+
23
+
24
+ @pytest.mark.parametrize(
25
+ "feature_positions, coordinates, expected_val",
26
+ [
27
+ (
28
+ ((0, 0, 0), (9, 9, 9)),
29
+ {"x": ("x", np.linspace(0, 10, 10)), "z": ("z", np.linspace(0, 10, 10))},
30
+ {"x": (0, 10)},
31
+ ),
32
+ (
33
+ ((0, 0), (9, 9)),
34
+ {"x": ("x", np.linspace(0, 10, 10))},
35
+ {"x": (0, 10)},
36
+ ),
37
+ (
38
+ ((0, 0), (9, 9), (5, 7)),
39
+ {
40
+ "longitude": ("x", np.linspace(-30, 60, 10)),
41
+ "latitude": ("y", np.linspace(-70, 20, 10)),
42
+ },
43
+ {"latitude": (-70, 20, 0), "longitude": (-30, 60, 20)},
44
+ ),
45
+ (
46
+ ((0, 0), (9, 9), (5, 7), (3.6, 7.9)),
47
+ {
48
+ "longitude": (
49
+ ("x", "y"),
50
+ np.arange(-180, -80).reshape(10, -1),
51
+ ),
52
+ "latitude": (("x", "y"), np.arange(-50, 50).reshape(10, -1)),
53
+ },
54
+ {
55
+ "latitude": (-50, 49, 7, -6.1),
56
+ "longitude": (-180, -81, -123, -136.1),
57
+ },
58
+ ),
59
+ ],
60
+ )
61
+ def test_add_coordinates_xarray_base(
62
+ feature_positions: tuple[tuple[float]],
63
+ coordinates: dict[str : tuple[str, np.ndarray]],
64
+ expected_val: dict[str : tuple[float]],
65
+ ):
66
+ """
67
+ Test that adding coordinates for xarray and iris are equal, using an
68
+ xarray generated dataset as the base.
69
+
70
+ Parameters
71
+ ----------
72
+ feature_positions: tuple of tuple of floats
73
+ Locations of the features to test in (hdim_1, hdim_2, zdim [optional]) coordinates
74
+ coordinates: dict, key: str; value: tuple of str, numpy array
75
+ Coordinates to use, in xarray coordinate style. Dims will be ('x', 'y', 'z') for 3D
76
+ data (determined by feature_positions) and ('x', 'y') for 2D data. All axes will have
77
+ size 10.
78
+ expected_val: dict, key: str; value: tuple of floats
79
+ Expected interpolated coordinates
80
+
81
+ """
82
+
83
+ all_indiv_feats = []
84
+ if len(feature_positions[0]) == 2:
85
+ is_3D = False
86
+ elif len(feature_positions[0]) == 3:
87
+ is_3D = True
88
+ else:
89
+ raise ValueError("Feature positions should be 2 or 3D")
90
+ for i, single_feat_position in enumerate(feature_positions):
91
+ if not is_3D and len(single_feat_position) == 2:
92
+ all_indiv_feats.append(
93
+ tbtest.generate_single_feature(
94
+ single_feat_position[0],
95
+ single_feat_position[1],
96
+ feature_num=i,
97
+ max_h1=10,
98
+ max_h2=10,
99
+ )
100
+ )
101
+ elif is_3D and len(single_feat_position) == 3:
102
+ all_indiv_feats.append(
103
+ tbtest.generate_single_feature(
104
+ single_feat_position[0],
105
+ single_feat_position[1],
106
+ start_v=single_feat_position[2],
107
+ feature_num=i,
108
+ max_h1=10,
109
+ max_h2=10,
110
+ )
111
+ )
112
+
113
+ else:
114
+ raise ValueError("Feature positions should be 2 or 3D")
115
+
116
+ all_feats = pd.concat(all_indiv_feats)
117
+
118
+ da_size = (1, 10, 10, 10) if is_3D else (1, 10, 10)
119
+ dims = ("time", "x", "y", "z") if is_3D else ("time", "x", "y")
120
+ coordinates["time"] = np.array((datetime.datetime(2000, 1, 1, 0),))
121
+ da_with_coords = xr.DataArray(data=np.empty(da_size), dims=dims, coords=coordinates)
122
+ if is_3D:
123
+ iris_coord_interp = iris_utils.add_coordinates_3D(
124
+ all_feats, da_with_coords.to_iris()
125
+ )
126
+ xr_coord_interp = xr_utils.add_coordinates_to_features(
127
+ all_feats, da_with_coords
128
+ )
129
+
130
+ else:
131
+ iris_coord_interp = iris_utils.add_coordinates(
132
+ all_feats, da_with_coords.to_iris()
133
+ )
134
+ xr_coord_interp = xr_utils.add_coordinates_to_features(
135
+ all_feats, da_with_coords
136
+ )
137
+ for val_name in expected_val:
138
+ np.testing.assert_almost_equal(
139
+ iris_coord_interp[val_name], expected_val[val_name]
140
+ )
141
+ np.testing.assert_almost_equal(
142
+ xr_coord_interp[val_name], expected_val[val_name]
143
+ )
144
+
145
+ # assert (iris_coord_interp[val_name] == expected_val[val_name]).all()
146
+ # assert (xr_coord_interp[val_name] == expected_val[val_name]).all()
147
+
148
+ # Convert datetimes to ensure that they are the same type:
149
+ xr_coord_interp["time"] = datetime_utils.match_datetime_format(
150
+ xr_coord_interp.time, iris_coord_interp.time
151
+ )
152
+
153
+ pd.testing.assert_frame_equal(iris_coord_interp, xr_coord_interp)
154
+
155
+
156
+ @pytest.mark.parametrize(
157
+ "coordinate_names, coordinate_standard_names",
158
+ [(("lat",), ("latitude",))],
159
+ )
160
+ def test_add_coordinates_xarray_std_names(
161
+ coordinate_names: tuple[str],
162
+ coordinate_standard_names: tuple[str],
163
+ ):
164
+ """
165
+ Test that adding coordinates for xarray and iris result in the same coordinate names
166
+ when standard_names are added to the xarray coordinates
167
+
168
+ Parameters
169
+ ----------
170
+ coordinate_names: tuple of str
171
+ names of coordinates to give
172
+ coordinate_standard_name: tuple of str
173
+ standard_names of coordinates to give
174
+
175
+ """
176
+
177
+ all_feats = tbtest.generate_single_feature(
178
+ 0,
179
+ 0,
180
+ feature_num=1,
181
+ max_h1=10,
182
+ max_h2=10,
183
+ )
184
+
185
+ da_size = (1, 10, 10)
186
+ dims = ("time", "x", "y")
187
+ coordinates = dict()
188
+ coordinates["time"] = np.array((datetime.datetime(2000, 1, 1, 0),))
189
+
190
+ for coord_name, coord_standard_name in zip(
191
+ coordinate_names, coordinate_standard_names
192
+ ):
193
+ coordinates[coord_name] = xr.DataArray(data=np.arange(10), dims="x")
194
+ coordinates[coord_name].attrs["standard_name"] = coord_standard_name
195
+
196
+ da_with_coords = xr.DataArray(data=np.empty(da_size), dims=dims, coords=coordinates)
197
+
198
+ iris_coord_interp = iris_utils.add_coordinates(
199
+ copy.deepcopy(all_feats), da_with_coords.to_iris()
200
+ )
201
+ xr_coord_interp = xr_utils.add_coordinates_to_features(
202
+ copy.deepcopy(all_feats), da_with_coords
203
+ )
204
+ xr_coord_interp["time"] = datetime_utils.match_datetime_format(
205
+ xr_coord_interp.time, iris_coord_interp.time
206
+ )
207
+ pd.testing.assert_frame_equal(iris_coord_interp, xr_coord_interp)
208
+
209
+
210
+ def test_preserve_iris_datetime_types():
211
+ """
212
+ Test that xarray.add_coordinates_to_features correctly returns the same time types as
213
+ iris when preserve_iris_datetime_types = True.
214
+ """
215
+
216
+ all_feats = tbtest.generate_single_feature(
217
+ 0,
218
+ 0,
219
+ feature_num=1,
220
+ max_h1=10,
221
+ max_h2=10,
222
+ )
223
+ var_array: iris.cube.Cube = tbtest.make_simple_sample_data_2D(data_type="iris")
224
+
225
+ xarray_output = xr_utils.add_coordinates_to_features(
226
+ all_feats,
227
+ convert_cube_to_dataarray(var_array, preserve_iris_datetime_types=True),
228
+ )
229
+ iris_output = iris_utils.add_coordinates(all_feats, var_array)
230
+
231
+ pd.testing.assert_frame_equal(xarray_output, iris_output)
232
+ assert xarray_output["time"].values[0] == iris_output["time"].values[0]
233
+ assert isinstance(
234
+ xarray_output["time"].values[0], type(iris_output["time"].values[0])
235
+ )
236
+
237
+ xarray_output_datetime_preserve_off = xr_utils.add_coordinates_to_features(
238
+ all_feats,
239
+ convert_cube_to_dataarray(var_array, preserve_iris_datetime_types=False),
240
+ )
241
+
242
+ assert isinstance(
243
+ xarray_output_datetime_preserve_off["time"].values[0], np.datetime64
244
+ )