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,1109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test spatial analysis functions
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
import pytest
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import xarray as xr
|
|
10
|
+
from iris.analysis.cartography import area_weights
|
|
11
|
+
|
|
12
|
+
from tobac.analysis.spatial import (
|
|
13
|
+
calculate_distance,
|
|
14
|
+
calculate_velocity_individual,
|
|
15
|
+
calculate_velocity,
|
|
16
|
+
calculate_nearestneighbordistance,
|
|
17
|
+
calculate_area,
|
|
18
|
+
calculate_areas_2Dlatlon,
|
|
19
|
+
)
|
|
20
|
+
from tobac.utils.datetime import to_cftime, to_datetime64
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_calculate_distance_xy():
|
|
24
|
+
"""
|
|
25
|
+
Test for tobac.analysis.spatial.calculate_distance with cartesian coordinates
|
|
26
|
+
"""
|
|
27
|
+
test_features = pd.DataFrame(
|
|
28
|
+
{
|
|
29
|
+
"feature": [1, 2],
|
|
30
|
+
"frame": [0, 0],
|
|
31
|
+
"time": [
|
|
32
|
+
datetime(2000, 1, 1),
|
|
33
|
+
datetime(2000, 1, 1),
|
|
34
|
+
],
|
|
35
|
+
"projection_x_coordinate": [0, 1000],
|
|
36
|
+
"projection_y_coordinate": [0, 0],
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
assert calculate_distance(test_features.iloc[0], test_features.iloc[1]) == 1000
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_calculate_distance_latlon():
|
|
44
|
+
"""
|
|
45
|
+
Test for tobac.analysis.spatial.calculate_distance with latitude/longitude
|
|
46
|
+
coordinates
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
test_features = pd.DataFrame(
|
|
50
|
+
{
|
|
51
|
+
"feature": [1, 2],
|
|
52
|
+
"frame": [0, 0],
|
|
53
|
+
"time": [
|
|
54
|
+
datetime(2000, 1, 1),
|
|
55
|
+
datetime(2000, 1, 1),
|
|
56
|
+
],
|
|
57
|
+
"longitude": [0, 1],
|
|
58
|
+
"latitude": [0, 0],
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
assert calculate_distance(
|
|
63
|
+
test_features.iloc[0], test_features.iloc[1]
|
|
64
|
+
) == pytest.approx(1.11e5, rel=1e4)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_calculate_distance_latlon_wrong_order():
|
|
68
|
+
"""
|
|
69
|
+
Test for tobac.analysis.spatial.calculate_distance with latitude/longitude
|
|
70
|
+
coordinates provided in the wrong order. When lat/lon are provided with
|
|
71
|
+
standard naming the function should detect this and switch their order to
|
|
72
|
+
ensure that haversine distances are calculated correctly.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
test_features = pd.DataFrame(
|
|
76
|
+
{
|
|
77
|
+
"feature": [1, 2],
|
|
78
|
+
"frame": [0, 0],
|
|
79
|
+
"time": [
|
|
80
|
+
datetime(2000, 1, 1),
|
|
81
|
+
datetime(2000, 1, 1),
|
|
82
|
+
],
|
|
83
|
+
"longitude": [0, 1],
|
|
84
|
+
"latitude": [0, 0],
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
# Test that if latitude and longitude coord names are given in the wrong order, then they are swapped:
|
|
88
|
+
# (expectation is hdim1=y=latitude, hdim2=x=longitude, doesn't matter for x/y but does matter for lat/lon)
|
|
89
|
+
assert calculate_distance(
|
|
90
|
+
test_features.iloc[0],
|
|
91
|
+
test_features.iloc[1],
|
|
92
|
+
hdim1_coord="longitude",
|
|
93
|
+
hdim2_coord="latitude",
|
|
94
|
+
method_distance="latlon",
|
|
95
|
+
) == pytest.approx(1.11e5, rel=1e4)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_calculate_distance_error_invalid_method():
|
|
99
|
+
"""Test invalid method_distance"""
|
|
100
|
+
with pytest.raises(ValueError, match="method_distance invalid*"):
|
|
101
|
+
calculate_distance(
|
|
102
|
+
pd.DataFrame(), pd.DataFrame(), method_distance="invalid_method_distance"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_calculate_distance_error_no_coords():
|
|
107
|
+
"""Test no horizontal coordinates in input dataframe"""
|
|
108
|
+
test_features = pd.DataFrame(
|
|
109
|
+
{
|
|
110
|
+
"feature": [1, 2],
|
|
111
|
+
"frame": [0, 0],
|
|
112
|
+
"time": [
|
|
113
|
+
datetime(2000, 1, 1),
|
|
114
|
+
datetime(2000, 1, 1),
|
|
115
|
+
],
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
with pytest.raises(ValueError):
|
|
120
|
+
calculate_distance(test_features.iloc[0], test_features.iloc[1])
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_calculate_distance_error_mismatched_coords():
|
|
124
|
+
"""Test dataframes with mismatching coordinates"""
|
|
125
|
+
with pytest.raises(ValueError, match="Discovered coordinates*"):
|
|
126
|
+
calculate_distance(
|
|
127
|
+
pd.DataFrame(
|
|
128
|
+
{
|
|
129
|
+
"feature": [1],
|
|
130
|
+
"frame": [0],
|
|
131
|
+
"time": [datetime(2000, 1, 1)],
|
|
132
|
+
"projection_x_coordinate": [0],
|
|
133
|
+
"projection_y_coordinate": [0],
|
|
134
|
+
}
|
|
135
|
+
),
|
|
136
|
+
pd.DataFrame(
|
|
137
|
+
{
|
|
138
|
+
"feature": [1],
|
|
139
|
+
"frame": [0],
|
|
140
|
+
"time": [datetime(2000, 1, 1)],
|
|
141
|
+
"longitude": [0],
|
|
142
|
+
"latitude": [0],
|
|
143
|
+
}
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_calculate_distance_error_no_method():
|
|
149
|
+
"""Test hdim1_coord/hdim2_coord specified but no method_distance"""
|
|
150
|
+
test_features = pd.DataFrame(
|
|
151
|
+
{
|
|
152
|
+
"feature": [1, 2],
|
|
153
|
+
"frame": [0, 0],
|
|
154
|
+
"time": [
|
|
155
|
+
datetime(2000, 1, 1),
|
|
156
|
+
datetime(2000, 1, 1),
|
|
157
|
+
],
|
|
158
|
+
"projection_x_coordinate": [0, 1000],
|
|
159
|
+
"projection_y_coordinate": [0, 0],
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
with pytest.raises(ValueError, match="method_distance parameter must*"):
|
|
164
|
+
calculate_distance(
|
|
165
|
+
test_features.iloc[0],
|
|
166
|
+
test_features.iloc[1],
|
|
167
|
+
hdim1_coord="projection_y_coordinate",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
with pytest.raises(ValueError, match="method_distance parameter must*"):
|
|
171
|
+
calculate_distance(
|
|
172
|
+
test_features.iloc[0],
|
|
173
|
+
test_features.iloc[1],
|
|
174
|
+
hdim2_coord="projection_x_coordinate",
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@pytest.mark.parametrize(
|
|
179
|
+
"x_coord, y_coord",
|
|
180
|
+
[("x", "y"), ("projection_x_coordinate", "projection_y_coordinate")],
|
|
181
|
+
)
|
|
182
|
+
def test_calculate_velocity_individual_xy(x_coord, y_coord):
|
|
183
|
+
"""
|
|
184
|
+
Test calculate_velocity_individual gives the correct result for a single
|
|
185
|
+
track woth different x/y coordinate names
|
|
186
|
+
"""
|
|
187
|
+
test_features = pd.DataFrame(
|
|
188
|
+
{
|
|
189
|
+
"feature": [1, 2],
|
|
190
|
+
"frame": [0, 1],
|
|
191
|
+
"time": [
|
|
192
|
+
datetime(2000, 1, 1, 0, 0),
|
|
193
|
+
datetime(2000, 1, 1, 0, 10),
|
|
194
|
+
],
|
|
195
|
+
x_coord: [0, 6000],
|
|
196
|
+
y_coord: [0, 0],
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
assert (
|
|
201
|
+
calculate_velocity_individual(test_features.iloc[0], test_features.iloc[1])
|
|
202
|
+
== 10
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@pytest.mark.parametrize(
|
|
207
|
+
"lat_coord, lon_coord", [("lat", "lon"), ("latitude", "longitude")]
|
|
208
|
+
)
|
|
209
|
+
def test_calculate_velocity_individual_latlon(lat_coord, lon_coord):
|
|
210
|
+
"""
|
|
211
|
+
Test calculate_velocity_individual gives the correct result for a single
|
|
212
|
+
track woth different lat/lon coordinate names
|
|
213
|
+
"""
|
|
214
|
+
test_features = pd.DataFrame(
|
|
215
|
+
{
|
|
216
|
+
"feature": [1, 2],
|
|
217
|
+
"frame": [0, 0],
|
|
218
|
+
"time": [
|
|
219
|
+
datetime(2000, 1, 1, 0, 0),
|
|
220
|
+
datetime(2000, 1, 1, 0, 10),
|
|
221
|
+
],
|
|
222
|
+
lon_coord: [0, 1],
|
|
223
|
+
lat_coord: [0, 0],
|
|
224
|
+
}
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
assert calculate_velocity_individual(
|
|
228
|
+
test_features.iloc[0], test_features.iloc[1]
|
|
229
|
+
) == pytest.approx(1.11e5 / 600, rel=1e2)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@pytest.mark.parametrize(
|
|
233
|
+
"time_format", ("datetime", "datetime64", "proleptic_gregorian", "360_day")
|
|
234
|
+
)
|
|
235
|
+
def test_calculate_velocity(time_format):
|
|
236
|
+
"""
|
|
237
|
+
Test velocity calculation using different time formats
|
|
238
|
+
"""
|
|
239
|
+
test_features = pd.DataFrame(
|
|
240
|
+
{
|
|
241
|
+
"feature": [1, 2, 3, 4],
|
|
242
|
+
"frame": [0, 0, 1, 1],
|
|
243
|
+
"time": [
|
|
244
|
+
datetime(2000, 1, 1, 0, 0),
|
|
245
|
+
datetime(2000, 1, 1, 0, 0),
|
|
246
|
+
datetime(2000, 1, 1, 0, 10),
|
|
247
|
+
datetime(2000, 1, 1, 0, 10),
|
|
248
|
+
],
|
|
249
|
+
"x": [0, 0, 6000, 0],
|
|
250
|
+
"y": [0, 0, 0, 9000],
|
|
251
|
+
"cell": [1, 2, 1, 2],
|
|
252
|
+
}
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
if time_format == "datetime":
|
|
256
|
+
pass
|
|
257
|
+
elif time_format == "datetime64":
|
|
258
|
+
test_features["time"] = to_datetime64(test_features.time)
|
|
259
|
+
else:
|
|
260
|
+
test_features["time"] = to_cftime(test_features.time, calendar=time_format)
|
|
261
|
+
|
|
262
|
+
assert calculate_velocity(test_features).at[0, "v"] == 10
|
|
263
|
+
assert calculate_velocity(test_features).at[1, "v"] == 15
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def test_calculate_nearestneighbordistance():
|
|
267
|
+
test_features = pd.DataFrame(
|
|
268
|
+
{
|
|
269
|
+
"feature": [1, 2, 3, 4],
|
|
270
|
+
"frame": [0, 0, 1, 1],
|
|
271
|
+
"time": [
|
|
272
|
+
datetime(2000, 1, 1, 0, 0),
|
|
273
|
+
datetime(2000, 1, 1, 0, 0),
|
|
274
|
+
datetime(2000, 1, 1, 0, 10),
|
|
275
|
+
datetime(2000, 1, 1, 0, 10),
|
|
276
|
+
],
|
|
277
|
+
"projection_x_coordinate": [0, 1000, 0, 2000],
|
|
278
|
+
"projection_y_coordinate": [0, 0, 0, 0],
|
|
279
|
+
"cell": [1, 2, 1, 2],
|
|
280
|
+
}
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
assert calculate_nearestneighbordistance(test_features)[
|
|
284
|
+
"min_distance"
|
|
285
|
+
].to_list() == [1000, 1000, 2000, 2000]
|
|
286
|
+
|
|
287
|
+
test_features = pd.DataFrame(
|
|
288
|
+
{
|
|
289
|
+
"feature": [1, 2],
|
|
290
|
+
"frame": [0, 1],
|
|
291
|
+
"time": [
|
|
292
|
+
datetime(2000, 1, 1, 0, 0),
|
|
293
|
+
datetime(2000, 1, 1, 0, 10),
|
|
294
|
+
],
|
|
295
|
+
"projection_x_coordinate": [0, 6000],
|
|
296
|
+
"projection_y_coordinate": [0, 0],
|
|
297
|
+
"cell": [1, 1],
|
|
298
|
+
}
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
assert np.all(
|
|
302
|
+
np.isnan(calculate_nearestneighbordistance(test_features)["min_distance"])
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def test_calculate_area():
|
|
307
|
+
"""
|
|
308
|
+
Test the calculate_area function for 2D and 3D masks
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
test_labels = np.array(
|
|
312
|
+
[
|
|
313
|
+
[
|
|
314
|
+
[0, 0, 0, 0, 0],
|
|
315
|
+
[0, 1, 0, 2, 0],
|
|
316
|
+
[0, 1, 0, 2, 0],
|
|
317
|
+
[0, 1, 0, 0, 0],
|
|
318
|
+
[0, 0, 0, 0, 0],
|
|
319
|
+
],
|
|
320
|
+
],
|
|
321
|
+
dtype=int,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
test_labels = xr.DataArray(
|
|
325
|
+
test_labels,
|
|
326
|
+
dims=("time", "projection_y_coordinate", "projection_x_coordinate"),
|
|
327
|
+
coords={
|
|
328
|
+
"time": [datetime(2000, 1, 1)],
|
|
329
|
+
"projection_y_coordinate": np.arange(5),
|
|
330
|
+
"projection_x_coordinate": np.arange(5),
|
|
331
|
+
},
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# We need to do this to avoid round trip bug with xarray to iris conversion
|
|
335
|
+
test_cube = test_labels.to_iris()
|
|
336
|
+
test_cube = test_cube.copy(test_cube.core_data().filled())
|
|
337
|
+
|
|
338
|
+
test_features = pd.DataFrame(
|
|
339
|
+
{
|
|
340
|
+
"feature": [1, 2],
|
|
341
|
+
"frame": [0, 0],
|
|
342
|
+
"time": [
|
|
343
|
+
datetime(2000, 1, 1),
|
|
344
|
+
datetime(2000, 1, 1),
|
|
345
|
+
],
|
|
346
|
+
}
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
expected_areas = np.array([3, 2])
|
|
350
|
+
|
|
351
|
+
area = calculate_area(test_features, test_cube)
|
|
352
|
+
|
|
353
|
+
assert np.all(area["area"] == expected_areas)
|
|
354
|
+
|
|
355
|
+
test_labels = np.array(
|
|
356
|
+
[
|
|
357
|
+
[
|
|
358
|
+
[
|
|
359
|
+
[0, 0, 0, 0, 0],
|
|
360
|
+
[0, 1, 0, 2, 0],
|
|
361
|
+
[0, 1, 0, 2, 0],
|
|
362
|
+
[0, 1, 0, 0, 0],
|
|
363
|
+
[0, 0, 0, 0, 0],
|
|
364
|
+
],
|
|
365
|
+
[
|
|
366
|
+
[0, 0, 0, 0, 0],
|
|
367
|
+
[0, 1, 0, 0, 0],
|
|
368
|
+
[0, 1, 0, 3, 0],
|
|
369
|
+
[0, 1, 0, 3, 0],
|
|
370
|
+
[0, 0, 0, 0, 0],
|
|
371
|
+
],
|
|
372
|
+
],
|
|
373
|
+
],
|
|
374
|
+
dtype=int,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
test_labels = xr.DataArray(
|
|
378
|
+
test_labels,
|
|
379
|
+
dims=(
|
|
380
|
+
"time",
|
|
381
|
+
"model_level_number",
|
|
382
|
+
"projection_y_coordinate",
|
|
383
|
+
"projection_x_coordinate",
|
|
384
|
+
),
|
|
385
|
+
coords={
|
|
386
|
+
"time": [datetime(2000, 1, 1)],
|
|
387
|
+
"model_level_number": np.arange(2),
|
|
388
|
+
"projection_y_coordinate": np.arange(5),
|
|
389
|
+
"projection_x_coordinate": np.arange(5),
|
|
390
|
+
},
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
# We need to do this to avoid round trip bug with xarray to iris conversion
|
|
394
|
+
test_cube = test_labels.to_iris()
|
|
395
|
+
test_cube = test_cube.copy(test_cube.core_data().filled())
|
|
396
|
+
|
|
397
|
+
test_features = pd.DataFrame(
|
|
398
|
+
{
|
|
399
|
+
"feature": [1, 2, 3],
|
|
400
|
+
"frame": [0, 0, 0],
|
|
401
|
+
"time": [
|
|
402
|
+
datetime(2000, 1, 1),
|
|
403
|
+
datetime(2000, 1, 1),
|
|
404
|
+
datetime(2000, 1, 1),
|
|
405
|
+
],
|
|
406
|
+
}
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
expected_areas = np.array([3, 2, 2])
|
|
410
|
+
|
|
411
|
+
area = calculate_area(test_features, test_cube)
|
|
412
|
+
|
|
413
|
+
assert np.all(area["area"] == expected_areas)
|
|
414
|
+
|
|
415
|
+
test_labels = xr.DataArray(
|
|
416
|
+
test_labels,
|
|
417
|
+
dims=(
|
|
418
|
+
"time",
|
|
419
|
+
"model_level_number",
|
|
420
|
+
"hdim_0",
|
|
421
|
+
"hdim_1",
|
|
422
|
+
),
|
|
423
|
+
coords={
|
|
424
|
+
"time": [datetime(2000, 1, 1)],
|
|
425
|
+
"model_level_number": np.arange(2),
|
|
426
|
+
},
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
# Test failure to find valid coordinates
|
|
430
|
+
with pytest.raises(ValueError):
|
|
431
|
+
calculate_area(test_features, test_labels)
|
|
432
|
+
|
|
433
|
+
# Test failure for invalid method
|
|
434
|
+
with pytest.raises(ValueError):
|
|
435
|
+
calculate_area(test_features, test_labels, method_area="invalid_method")
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def test_calculate_area_latlon():
|
|
439
|
+
# Test with latitude/longitude
|
|
440
|
+
test_labels = np.array(
|
|
441
|
+
[
|
|
442
|
+
[
|
|
443
|
+
[0, 0, 0, 0, 0],
|
|
444
|
+
[0, 1, 0, 2, 0],
|
|
445
|
+
[0, 1, 0, 2, 0],
|
|
446
|
+
[0, 1, 0, 0, 0],
|
|
447
|
+
[0, 0, 0, 0, 0],
|
|
448
|
+
],
|
|
449
|
+
[
|
|
450
|
+
[0, 0, 0, 0, 0],
|
|
451
|
+
[0, 4, 0, 0, 0],
|
|
452
|
+
[0, 4, 0, 3, 0],
|
|
453
|
+
[0, 4, 0, 3, 0],
|
|
454
|
+
[0, 0, 0, 0, 0],
|
|
455
|
+
],
|
|
456
|
+
],
|
|
457
|
+
dtype=int,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
test_labels = xr.DataArray(
|
|
461
|
+
test_labels,
|
|
462
|
+
dims=(
|
|
463
|
+
"time",
|
|
464
|
+
"latitude",
|
|
465
|
+
"longitude",
|
|
466
|
+
),
|
|
467
|
+
coords={
|
|
468
|
+
"time": [datetime(2000, 1, 1), datetime(2000, 1, 1, 1)],
|
|
469
|
+
"latitude": xr.DataArray(
|
|
470
|
+
np.arange(5), dims="latitude", attrs={"units": "degrees"}
|
|
471
|
+
),
|
|
472
|
+
"longitude": xr.DataArray(
|
|
473
|
+
np.arange(5), dims="longitude", attrs={"units": "degrees"}
|
|
474
|
+
),
|
|
475
|
+
},
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
test_features = pd.DataFrame(
|
|
479
|
+
{
|
|
480
|
+
"feature": [1, 2, 3, 4],
|
|
481
|
+
"frame": [0, 0, 1, 1],
|
|
482
|
+
"time": [
|
|
483
|
+
datetime(2000, 1, 1, 0),
|
|
484
|
+
datetime(2000, 1, 1, 0),
|
|
485
|
+
datetime(2000, 1, 1, 1),
|
|
486
|
+
datetime(2000, 1, 1, 1),
|
|
487
|
+
],
|
|
488
|
+
}
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
area = calculate_area(test_features, test_labels)
|
|
492
|
+
|
|
493
|
+
expected_areas = np.array([3, 2, 2, 3]) * 1.11e5**2
|
|
494
|
+
|
|
495
|
+
assert np.all(np.isclose(area["area"], expected_areas, atol=1e8))
|
|
496
|
+
|
|
497
|
+
# Test invalid lat/lon dimensions
|
|
498
|
+
|
|
499
|
+
# Test 1D lat but 2D lon
|
|
500
|
+
test_labels = xr.DataArray(
|
|
501
|
+
test_labels.values,
|
|
502
|
+
dims=(
|
|
503
|
+
"time",
|
|
504
|
+
"y_dim",
|
|
505
|
+
"x_dim",
|
|
506
|
+
),
|
|
507
|
+
coords={
|
|
508
|
+
"time": [datetime(2000, 1, 1), datetime(2000, 1, 1, 1)],
|
|
509
|
+
"latitude": xr.DataArray(
|
|
510
|
+
np.arange(5), dims="y_dim", attrs={"units": "degrees"} # 1D lat
|
|
511
|
+
),
|
|
512
|
+
"longitude": xr.DataArray(
|
|
513
|
+
np.tile(np.arange(5), (5, 1)),
|
|
514
|
+
dims=("y_dim", "x_dim"), # 2D lon
|
|
515
|
+
attrs={"units": "degrees"},
|
|
516
|
+
),
|
|
517
|
+
},
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
with pytest.raises(ValueError):
|
|
521
|
+
calculate_area(test_features, test_labels, method_area="latlon")
|
|
522
|
+
|
|
523
|
+
# Test 3D lat/lon
|
|
524
|
+
test_labels = xr.DataArray(
|
|
525
|
+
np.tile(test_labels.values[:, np.newaxis, ...], (1, 2, 1, 1)),
|
|
526
|
+
dims=(
|
|
527
|
+
"time",
|
|
528
|
+
"z_dim",
|
|
529
|
+
"y_dim",
|
|
530
|
+
"x_dim",
|
|
531
|
+
),
|
|
532
|
+
coords={
|
|
533
|
+
"time": [datetime(2000, 1, 1), datetime(2000, 1, 1, 1)],
|
|
534
|
+
"latitude": xr.DataArray(
|
|
535
|
+
np.tile(np.arange(5)[:, np.newaxis], (2, 1, 5)),
|
|
536
|
+
dims=("z_dim", "y_dim", "x_dim"),
|
|
537
|
+
attrs={"units": "degrees"},
|
|
538
|
+
),
|
|
539
|
+
"longitude": xr.DataArray(
|
|
540
|
+
np.tile(np.arange(5), (2, 5, 1)),
|
|
541
|
+
dims=("z_dim", "y_dim", "x_dim"),
|
|
542
|
+
attrs={"units": "degrees"},
|
|
543
|
+
),
|
|
544
|
+
},
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
with pytest.raises(ValueError):
|
|
548
|
+
calculate_area(test_features, test_labels, method_area="latlon")
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def test_calculate_area_1D_latlon():
|
|
552
|
+
"""
|
|
553
|
+
Test area calculation using 1D lat/lon coords
|
|
554
|
+
"""
|
|
555
|
+
test_labels = np.array(
|
|
556
|
+
[
|
|
557
|
+
[
|
|
558
|
+
[0, 0, 0, 0, 0],
|
|
559
|
+
[0, 1, 0, 2, 0],
|
|
560
|
+
[0, 1, 0, 2, 0],
|
|
561
|
+
[0, 1, 0, 0, 0],
|
|
562
|
+
[0, 0, 0, 0, 0],
|
|
563
|
+
],
|
|
564
|
+
],
|
|
565
|
+
dtype=int,
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
test_labels = xr.DataArray(
|
|
569
|
+
test_labels,
|
|
570
|
+
dims=("time", "latitude", "longitude"),
|
|
571
|
+
coords={
|
|
572
|
+
"time": [datetime(2000, 1, 1)],
|
|
573
|
+
"latitude": xr.DataArray(
|
|
574
|
+
np.arange(5), dims=("latitude",), attrs={"units": "degrees"}
|
|
575
|
+
),
|
|
576
|
+
"longitude": xr.DataArray(
|
|
577
|
+
np.arange(5), dims=("longitude",), attrs={"units": "degrees"}
|
|
578
|
+
),
|
|
579
|
+
},
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
# We need to do this to avoid round trip bug with xarray to iris conversion
|
|
583
|
+
test_cube = test_labels.to_iris()
|
|
584
|
+
test_cube = test_cube.copy(test_cube.core_data().filled())
|
|
585
|
+
|
|
586
|
+
test_features = pd.DataFrame(
|
|
587
|
+
{
|
|
588
|
+
"feature": [1, 2],
|
|
589
|
+
"frame": [0, 0],
|
|
590
|
+
"time": [
|
|
591
|
+
datetime(2000, 1, 1),
|
|
592
|
+
datetime(2000, 1, 1),
|
|
593
|
+
],
|
|
594
|
+
}
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
# Calculate expected areas
|
|
598
|
+
copy_of_test_cube = test_cube.copy()
|
|
599
|
+
copy_of_test_cube.coord("latitude").guess_bounds()
|
|
600
|
+
copy_of_test_cube.coord("longitude").guess_bounds()
|
|
601
|
+
area_array = area_weights(copy_of_test_cube, normalize=False)
|
|
602
|
+
|
|
603
|
+
expected_areas = np.array(
|
|
604
|
+
[np.sum(area_array[test_labels.data == i]) for i in [1, 2]]
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
area = calculate_area(test_features, test_cube)
|
|
608
|
+
|
|
609
|
+
assert np.all(area["area"] == expected_areas)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def test_calculate_areas_2Dlatlon():
|
|
613
|
+
"""
|
|
614
|
+
Test calculation of area array from 2D lat/lon coords
|
|
615
|
+
Note, in future this needs to be updated to account for non-orthogonal lat/lon arrays
|
|
616
|
+
"""
|
|
617
|
+
|
|
618
|
+
test_labels = np.ones([1, 5, 5], dtype=int)
|
|
619
|
+
|
|
620
|
+
test_labels = xr.DataArray(
|
|
621
|
+
test_labels,
|
|
622
|
+
dims=("time", "latitude", "longitude"),
|
|
623
|
+
coords={
|
|
624
|
+
"time": [datetime(2000, 1, 1)],
|
|
625
|
+
"latitude": xr.DataArray(
|
|
626
|
+
np.arange(5), dims=("latitude",), attrs={"units": "degrees"}
|
|
627
|
+
),
|
|
628
|
+
"longitude": xr.DataArray(
|
|
629
|
+
np.arange(5), dims=("longitude",), attrs={"units": "degrees"}
|
|
630
|
+
),
|
|
631
|
+
},
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
test_cube = test_labels.to_iris()
|
|
635
|
+
test_cube = test_cube.copy(test_cube.core_data().filled())
|
|
636
|
+
copy_of_test_cube = test_cube.copy()
|
|
637
|
+
copy_of_test_cube.coord("latitude").guess_bounds()
|
|
638
|
+
copy_of_test_cube.coord("longitude").guess_bounds()
|
|
639
|
+
area_array = area_weights(copy_of_test_cube, normalize=False)
|
|
640
|
+
|
|
641
|
+
lat_2d = xr.DataArray(
|
|
642
|
+
np.stack([np.arange(5)] * 5, axis=1),
|
|
643
|
+
dims=("y", "x"),
|
|
644
|
+
attrs={"units": "degrees"},
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
lon_2d = xr.DataArray(
|
|
648
|
+
np.stack([np.arange(5)] * 5, axis=0),
|
|
649
|
+
dims=("y", "x"),
|
|
650
|
+
attrs={"units": "degrees"},
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
test_labels = xr.DataArray(
|
|
654
|
+
test_labels,
|
|
655
|
+
dims=("time", "y", "x"),
|
|
656
|
+
coords={
|
|
657
|
+
"time": [datetime(2000, 1, 1)],
|
|
658
|
+
"latitude": lat_2d,
|
|
659
|
+
"longitude": lon_2d,
|
|
660
|
+
},
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
test_cube = test_labels.to_iris()
|
|
664
|
+
test_cube = test_cube.copy(test_cube.core_data().filled())
|
|
665
|
+
|
|
666
|
+
assert np.allclose(
|
|
667
|
+
calculate_areas_2Dlatlon(
|
|
668
|
+
test_cube.coord("latitude"), test_cube.coord("longitude")
|
|
669
|
+
),
|
|
670
|
+
area_array,
|
|
671
|
+
rtol=0.01,
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def test_calculate_area_2D_latlon():
|
|
676
|
+
"""
|
|
677
|
+
Test area calculation using 2D lat/lon coords
|
|
678
|
+
"""
|
|
679
|
+
|
|
680
|
+
test_labels = np.array(
|
|
681
|
+
[
|
|
682
|
+
[
|
|
683
|
+
[0, 0, 0, 0, 0],
|
|
684
|
+
[0, 1, 0, 2, 0],
|
|
685
|
+
[0, 1, 0, 2, 0],
|
|
686
|
+
[0, 1, 0, 0, 0],
|
|
687
|
+
[0, 0, 0, 0, 0],
|
|
688
|
+
],
|
|
689
|
+
],
|
|
690
|
+
dtype=int,
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
lat_2d = xr.DataArray(
|
|
694
|
+
np.stack([np.arange(5)] * 5, axis=1),
|
|
695
|
+
dims=("y", "x"),
|
|
696
|
+
attrs={"units": "degrees"},
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
lon_2d = xr.DataArray(
|
|
700
|
+
np.stack([np.arange(5)] * 5, axis=0),
|
|
701
|
+
dims=("y", "x"),
|
|
702
|
+
attrs={"units": "degrees"},
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
test_labels = xr.DataArray(
|
|
706
|
+
test_labels,
|
|
707
|
+
dims=("time", "y", "x"),
|
|
708
|
+
coords={
|
|
709
|
+
"time": [datetime(2000, 1, 1)],
|
|
710
|
+
"latitude": lat_2d,
|
|
711
|
+
"longitude": lon_2d,
|
|
712
|
+
},
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
test_cube = test_labels.to_iris()
|
|
716
|
+
test_cube = test_cube.copy(test_cube.core_data().filled())
|
|
717
|
+
|
|
718
|
+
area_array = calculate_areas_2Dlatlon(
|
|
719
|
+
test_cube.coord("latitude"), test_cube.coord("longitude")
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
expected_areas = np.array(
|
|
723
|
+
[np.sum(area_array[test_labels[0].data == i]) for i in [1, 2]]
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
test_features = pd.DataFrame(
|
|
727
|
+
{
|
|
728
|
+
"feature": [1, 2],
|
|
729
|
+
"frame": [0, 0],
|
|
730
|
+
"time": [
|
|
731
|
+
datetime(2000, 1, 1),
|
|
732
|
+
datetime(2000, 1, 1),
|
|
733
|
+
],
|
|
734
|
+
}
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
area = calculate_area(test_features, test_cube)
|
|
738
|
+
|
|
739
|
+
assert np.all(area["area"] == expected_areas)
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def test_calculate_distance_xy_3d():
|
|
743
|
+
"""
|
|
744
|
+
3D distance for xy with use_3d flag and vertical coord
|
|
745
|
+
"""
|
|
746
|
+
test_features = pd.DataFrame(
|
|
747
|
+
{
|
|
748
|
+
"feature": [1, 2],
|
|
749
|
+
"frame": [0, 0],
|
|
750
|
+
"time": [datetime(2000, 1, 1), datetime(2000, 1, 1)],
|
|
751
|
+
"projection_x_coordinate": [0, 1000],
|
|
752
|
+
"projection_y_coordinate": [0, 0],
|
|
753
|
+
"height": [0, 600],
|
|
754
|
+
}
|
|
755
|
+
)
|
|
756
|
+
d3d = calculate_distance(
|
|
757
|
+
test_features.iloc[0],
|
|
758
|
+
test_features.iloc[1],
|
|
759
|
+
method_distance="xy",
|
|
760
|
+
vertical_coord="height",
|
|
761
|
+
use_3d=True,
|
|
762
|
+
)
|
|
763
|
+
assert d3d == pytest.approx(np.sqrt(1000**2 + 600**2), rel=1e-9)
|
|
764
|
+
|
|
765
|
+
res = calculate_distance(
|
|
766
|
+
test_features.iloc[0],
|
|
767
|
+
test_features.iloc[1],
|
|
768
|
+
method_distance="xy",
|
|
769
|
+
vertical_coord="height",
|
|
770
|
+
use_3d=True,
|
|
771
|
+
return_components=True,
|
|
772
|
+
)
|
|
773
|
+
assert set(res.keys()) == {"distance_3d", "dx", "dy", "dz"}
|
|
774
|
+
assert res["distance_3d"] == pytest.approx(np.sqrt(1000**2 + 600**2), rel=1e-9)
|
|
775
|
+
|
|
776
|
+
assert res["dx"] == pytest.approx(1000, rel=1e-12)
|
|
777
|
+
assert res["dy"] == pytest.approx(0, rel=1e-12)
|
|
778
|
+
assert res["dz"] == pytest.approx(600, rel=1e-12)
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def test_calculate_distance_latlon_3d():
|
|
782
|
+
"""
|
|
783
|
+
3D distance for lat/lon with use_3d flag and vertical coord
|
|
784
|
+
"""
|
|
785
|
+
test_features = pd.DataFrame(
|
|
786
|
+
{
|
|
787
|
+
"feature": [1, 2],
|
|
788
|
+
"frame": [0, 0],
|
|
789
|
+
"time": [datetime(2000, 1, 1), datetime(2000, 1, 1)],
|
|
790
|
+
"longitude": [0, 1],
|
|
791
|
+
"latitude": [0, 0],
|
|
792
|
+
"height": [0, 1000],
|
|
793
|
+
}
|
|
794
|
+
)
|
|
795
|
+
d2d = calculate_distance(
|
|
796
|
+
test_features.iloc[0], test_features.iloc[1], method_distance="latlon"
|
|
797
|
+
)
|
|
798
|
+
d3d = calculate_distance(
|
|
799
|
+
test_features.iloc[0],
|
|
800
|
+
test_features.iloc[1],
|
|
801
|
+
method_distance="latlon",
|
|
802
|
+
vertical_coord="height",
|
|
803
|
+
use_3d=True,
|
|
804
|
+
)
|
|
805
|
+
assert d3d == pytest.approx(np.sqrt(d2d**2 + 1000**2), rel=1e-9)
|
|
806
|
+
|
|
807
|
+
res = calculate_distance(
|
|
808
|
+
test_features.iloc[0],
|
|
809
|
+
test_features.iloc[1],
|
|
810
|
+
method_distance="latlon",
|
|
811
|
+
vertical_coord="height",
|
|
812
|
+
use_3d=True,
|
|
813
|
+
return_components=True,
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
assert set(res.keys()) == {"distance_3d", "dx", "dy", "dz"}
|
|
817
|
+
|
|
818
|
+
assert res["distance_3d"] == pytest.approx(d3d, rel=1e-9)
|
|
819
|
+
horizontal = np.hypot(res["dx"], res["dy"])
|
|
820
|
+
assert horizontal == pytest.approx(d2d, rel=1e-9)
|
|
821
|
+
|
|
822
|
+
assert res["dx"] == pytest.approx(d2d, rel=1e-9)
|
|
823
|
+
assert res["dy"] == pytest.approx(0.0, abs=1e-6)
|
|
824
|
+
assert res["dz"] == pytest.approx(1000, rel=1e-12)
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def test_calculate_velocity_individual_xy_3d():
|
|
828
|
+
"""
|
|
829
|
+
3D velocity for xy with vertical coord and use_3d=True/False
|
|
830
|
+
"""
|
|
831
|
+
test_features = pd.DataFrame(
|
|
832
|
+
{
|
|
833
|
+
"feature": [1, 2],
|
|
834
|
+
"frame": [0, 1],
|
|
835
|
+
"time": [datetime(2000, 1, 1, 0, 0), datetime(2000, 1, 1, 0, 10)],
|
|
836
|
+
"projection_x_coordinate": [0, 6000],
|
|
837
|
+
"projection_y_coordinate": [0, 300],
|
|
838
|
+
"height": [0, 800],
|
|
839
|
+
}
|
|
840
|
+
)
|
|
841
|
+
v3d = calculate_velocity_individual(
|
|
842
|
+
test_features.iloc[0],
|
|
843
|
+
test_features.iloc[1],
|
|
844
|
+
method_distance="xy",
|
|
845
|
+
vertical_coord="height",
|
|
846
|
+
use_3d=True,
|
|
847
|
+
)
|
|
848
|
+
assert v3d == pytest.approx(np.sqrt(6000**2 + 300**2 + 800**2) / 600, rel=1e-9)
|
|
849
|
+
|
|
850
|
+
res = calculate_velocity_individual(
|
|
851
|
+
test_features.iloc[0],
|
|
852
|
+
test_features.iloc[1],
|
|
853
|
+
method_distance="xy",
|
|
854
|
+
vertical_coord="height",
|
|
855
|
+
use_3d=True,
|
|
856
|
+
return_components=True,
|
|
857
|
+
)
|
|
858
|
+
assert set(res.keys()) >= {"v_3d", "vx", "vy", "vz"}
|
|
859
|
+
assert res["v_3d"] == pytest.approx(
|
|
860
|
+
np.sqrt(6000**2 + 300**2 + 800**2) / 600, rel=1e-9
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
assert res["vx"] == pytest.approx(10.0, rel=1e-12)
|
|
864
|
+
assert res["vy"] == pytest.approx(0.5, rel=1e-12)
|
|
865
|
+
assert res["vz"] == pytest.approx(800 / 600, rel=1e-12)
|
|
866
|
+
|
|
867
|
+
v3d_from_components = np.sqrt(res["vx"] ** 2 + res["vy"] ** 2 + res["vz"] ** 2)
|
|
868
|
+
assert res["v_3d"] == pytest.approx(v3d_from_components, rel=1e-12)
|
|
869
|
+
|
|
870
|
+
res2d = calculate_velocity_individual(
|
|
871
|
+
test_features.iloc[0],
|
|
872
|
+
test_features.iloc[1],
|
|
873
|
+
method_distance="xy",
|
|
874
|
+
vertical_coord="height",
|
|
875
|
+
use_3d=False,
|
|
876
|
+
return_components=True,
|
|
877
|
+
)
|
|
878
|
+
assert set(res2d.keys()) >= {"v", "vx", "vy"}
|
|
879
|
+
assert res2d["v"] == pytest.approx(np.sqrt(6000**2 + 300**2) / 600, rel=1e-9)
|
|
880
|
+
|
|
881
|
+
assert res2d["vx"] == pytest.approx(10.0, rel=1e-12)
|
|
882
|
+
assert res2d["vy"] == pytest.approx(0.5, rel=1e-12)
|
|
883
|
+
|
|
884
|
+
v2d_from_components = np.sqrt(res2d["vx"] ** 2 + res2d["vy"] ** 2)
|
|
885
|
+
assert res2d["v"] == pytest.approx(v2d_from_components, rel=1e-12)
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
def test_calculate_velocity_individual_latlon_3d():
|
|
889
|
+
"""
|
|
890
|
+
3D velocity for lat/lon with vertical coord and use_3d=True
|
|
891
|
+
"""
|
|
892
|
+
test_features = pd.DataFrame(
|
|
893
|
+
{
|
|
894
|
+
"feature": [1, 2],
|
|
895
|
+
"frame": [0, 0],
|
|
896
|
+
"time": [datetime(2000, 1, 1, 0, 0), datetime(2000, 1, 1, 0, 10)],
|
|
897
|
+
"longitude": [0, 1],
|
|
898
|
+
"latitude": [0, 0],
|
|
899
|
+
"height": [0, 1000],
|
|
900
|
+
}
|
|
901
|
+
)
|
|
902
|
+
d2d = calculate_distance(
|
|
903
|
+
test_features.iloc[0],
|
|
904
|
+
test_features.iloc[1],
|
|
905
|
+
method_distance="latlon",
|
|
906
|
+
return_components=True,
|
|
907
|
+
)
|
|
908
|
+
v3d = calculate_velocity_individual(
|
|
909
|
+
test_features.iloc[0],
|
|
910
|
+
test_features.iloc[1],
|
|
911
|
+
method_distance="latlon",
|
|
912
|
+
vertical_coord="height",
|
|
913
|
+
use_3d=True,
|
|
914
|
+
)
|
|
915
|
+
assert v3d == pytest.approx(np.sqrt(d2d["distance"] ** 2 + 1000**2) / 600, rel=1e-9)
|
|
916
|
+
|
|
917
|
+
res = calculate_velocity_individual(
|
|
918
|
+
test_features.iloc[0],
|
|
919
|
+
test_features.iloc[1],
|
|
920
|
+
method_distance="latlon",
|
|
921
|
+
vertical_coord="height",
|
|
922
|
+
use_3d=True,
|
|
923
|
+
return_components=True,
|
|
924
|
+
)
|
|
925
|
+
assert set(res.keys()) >= {"v_3d", "vx", "vy", "vz"}
|
|
926
|
+
|
|
927
|
+
dt = (test_features.iloc[1]["time"] - test_features.iloc[0]["time"]).total_seconds()
|
|
928
|
+
dx = d2d["dx"]
|
|
929
|
+
assert res["vx"] == pytest.approx(dx / dt, rel=1e-9)
|
|
930
|
+
|
|
931
|
+
assert res["vy"] == pytest.approx(0.0, abs=1e-6)
|
|
932
|
+
assert res["vz"] == pytest.approx(1000 / 600, rel=1e-12)
|
|
933
|
+
|
|
934
|
+
assert res["v_3d"] == pytest.approx(v3d, rel=1e-9)
|
|
935
|
+
|
|
936
|
+
v_horizontal = np.hypot(res["vx"], res["vy"])
|
|
937
|
+
assert v_horizontal == pytest.approx(d2d["distance"] / 600, rel=1e-9)
|
|
938
|
+
|
|
939
|
+
v3d_from_components = np.sqrt(res["vx"] ** 2 + res["vy"] ** 2 + res["vz"] ** 2)
|
|
940
|
+
assert res["v_3d"] == pytest.approx(v3d_from_components, rel=1e-12)
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def test_calculate_velocity_3d_track():
|
|
944
|
+
"""
|
|
945
|
+
Track with Z: use_3d=True -> 'v_3d' gets set
|
|
946
|
+
"""
|
|
947
|
+
test_features = pd.DataFrame(
|
|
948
|
+
{
|
|
949
|
+
"feature": [1, 2, 3, 4],
|
|
950
|
+
"frame": [0, 0, 1, 1],
|
|
951
|
+
"time": [
|
|
952
|
+
datetime(2000, 1, 1, 0, 0),
|
|
953
|
+
datetime(2000, 1, 1, 0, 0),
|
|
954
|
+
datetime(2000, 1, 1, 0, 10),
|
|
955
|
+
datetime(2000, 1, 1, 0, 10),
|
|
956
|
+
],
|
|
957
|
+
"projection_x_coordinate": [0, 0, 6000, 0],
|
|
958
|
+
"projection_y_coordinate": [0, 0, 0, 9000],
|
|
959
|
+
"height": [0, 0, 800, 1200],
|
|
960
|
+
"cell": [1, 2, 1, 2],
|
|
961
|
+
}
|
|
962
|
+
)
|
|
963
|
+
out = calculate_velocity(test_features, method_distance="xy", use_3d=True)
|
|
964
|
+
assert out.at[0, "v_3d"] == pytest.approx(np.sqrt(6000**2 + 800**2) / 600, rel=1e-9)
|
|
965
|
+
assert out.at[1, "v_3d"] == pytest.approx(
|
|
966
|
+
np.sqrt(9000**2 + 1200**2) / 600, rel=1e-9
|
|
967
|
+
)
|
|
968
|
+
|
|
969
|
+
out_w_components = calculate_velocity(
|
|
970
|
+
test_features,
|
|
971
|
+
method_distance="xy",
|
|
972
|
+
use_3d=True,
|
|
973
|
+
return_components=True,
|
|
974
|
+
)
|
|
975
|
+
|
|
976
|
+
dt = 600.0
|
|
977
|
+
# Expected values for cell 1
|
|
978
|
+
dx1 = 6000.0
|
|
979
|
+
dy1 = 0.0
|
|
980
|
+
dz1 = 800.0
|
|
981
|
+
v1_3d = np.sqrt(dx1**2 + dy1**2 + dz1**2) / dt
|
|
982
|
+
vx1 = dx1 / dt
|
|
983
|
+
vy1 = dy1 / dt
|
|
984
|
+
vz1 = dz1 / dt
|
|
985
|
+
|
|
986
|
+
# Expected values for cell 2
|
|
987
|
+
dx2 = 0.0
|
|
988
|
+
dy2 = 9000.0
|
|
989
|
+
dz2 = 1200.0
|
|
990
|
+
v2_3d = np.sqrt(dx2**2 + dy2**2 + dz2**2) / dt
|
|
991
|
+
vx2 = dx2 / dt
|
|
992
|
+
vy2 = dy2 / dt
|
|
993
|
+
vz2 = dz2 / dt
|
|
994
|
+
|
|
995
|
+
for col in ("v_3d", "vx", "vy", "vz"):
|
|
996
|
+
assert col in out_w_components.columns
|
|
997
|
+
|
|
998
|
+
# Cell 1:
|
|
999
|
+
assert out_w_components.at[0, "v_3d"] == pytest.approx(v1_3d, rel=1e-12)
|
|
1000
|
+
assert out_w_components.at[0, "vx"] == pytest.approx(vx1, rel=1e-12)
|
|
1001
|
+
assert out_w_components.at[0, "vy"] == pytest.approx(vy1, rel=1e-12)
|
|
1002
|
+
assert out_w_components.at[0, "vz"] == pytest.approx(vz1, rel=1e-12)
|
|
1003
|
+
|
|
1004
|
+
# Cell 2:
|
|
1005
|
+
assert out_w_components.at[1, "v_3d"] == pytest.approx(v2_3d, rel=1e-12)
|
|
1006
|
+
assert out_w_components.at[1, "vx"] == pytest.approx(vx2, rel=1e-12)
|
|
1007
|
+
assert out_w_components.at[1, "vy"] == pytest.approx(vy2, rel=1e-12)
|
|
1008
|
+
assert out_w_components.at[1, "vz"] == pytest.approx(vz2, rel=1e-12)
|
|
1009
|
+
|
|
1010
|
+
for idx in (2, 3):
|
|
1011
|
+
assert np.isnan(out_w_components.at[idx, "v_3d"])
|
|
1012
|
+
assert np.isnan(out_w_components.at[idx, "vx"])
|
|
1013
|
+
assert np.isnan(out_w_components.at[idx, "vy"])
|
|
1014
|
+
assert np.isnan(out_w_components.at[idx, "vz"])
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
def test_latlon_3d_no_degree_components():
|
|
1018
|
+
"""
|
|
1019
|
+
For lat/lon + use_3d + return_components: components dict must contain all components
|
|
1020
|
+
v_3d plus optional vx, vy, vz are expected.
|
|
1021
|
+
"""
|
|
1022
|
+
test_features = pd.DataFrame(
|
|
1023
|
+
{
|
|
1024
|
+
"feature": [1, 2],
|
|
1025
|
+
"frame": [0, 0],
|
|
1026
|
+
"time": [datetime(2000, 1, 1), datetime(2000, 1, 1)],
|
|
1027
|
+
"longitude": [0.0, 1.0],
|
|
1028
|
+
"latitude": [0.0, 0.0],
|
|
1029
|
+
"height": [100.0, 500.0],
|
|
1030
|
+
}
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
res = calculate_velocity_individual(
|
|
1034
|
+
test_features.iloc[0],
|
|
1035
|
+
test_features.iloc[1],
|
|
1036
|
+
method_distance="latlon",
|
|
1037
|
+
vertical_coord="height",
|
|
1038
|
+
use_3d=True,
|
|
1039
|
+
return_components=True,
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
assert "v_3d" in res and "vx" in res and "vy" in res and "vz" in res
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
def test_latlon_3d_dt_zero_returns_nan_scalar():
|
|
1046
|
+
"""
|
|
1047
|
+
Δt = 0 should return NaN (not crash) for scalar output.
|
|
1048
|
+
"""
|
|
1049
|
+
t = datetime(2000, 1, 1, 0, 0, 0)
|
|
1050
|
+
test_features = pd.DataFrame(
|
|
1051
|
+
{
|
|
1052
|
+
"feature": [1, 2],
|
|
1053
|
+
"frame": [0, 0],
|
|
1054
|
+
"time": [t, t],
|
|
1055
|
+
"longitude": [0.0, 1.0],
|
|
1056
|
+
"latitude": [0.0, 0.0],
|
|
1057
|
+
"height": [0.0, 1000.0],
|
|
1058
|
+
}
|
|
1059
|
+
)
|
|
1060
|
+
|
|
1061
|
+
v = calculate_velocity_individual(
|
|
1062
|
+
test_features.iloc[0],
|
|
1063
|
+
test_features.iloc[1],
|
|
1064
|
+
method_distance="latlon",
|
|
1065
|
+
vertical_coord="height",
|
|
1066
|
+
use_3d=True,
|
|
1067
|
+
return_components=False,
|
|
1068
|
+
)
|
|
1069
|
+
assert np.isnan(v)
|
|
1070
|
+
|
|
1071
|
+
|
|
1072
|
+
def test_latlon_3d_dt_zero_returns_nan_components():
|
|
1073
|
+
"""
|
|
1074
|
+
Δt = 0 should return NaN values in the components dict (not raise).
|
|
1075
|
+
"""
|
|
1076
|
+
t = datetime(2000, 1, 1, 0, 0, 0)
|
|
1077
|
+
test_features = pd.DataFrame(
|
|
1078
|
+
{
|
|
1079
|
+
"feature": [1, 2],
|
|
1080
|
+
"frame": [0, 0],
|
|
1081
|
+
"time": [t, t],
|
|
1082
|
+
"longitude": [0.0, 1.0],
|
|
1083
|
+
"latitude": [0.0, 0.0],
|
|
1084
|
+
"height": [0.0, 1000.0],
|
|
1085
|
+
}
|
|
1086
|
+
)
|
|
1087
|
+
|
|
1088
|
+
res = calculate_velocity_individual(
|
|
1089
|
+
test_features.iloc[0],
|
|
1090
|
+
test_features.iloc[1],
|
|
1091
|
+
method_distance="latlon",
|
|
1092
|
+
vertical_coord="height",
|
|
1093
|
+
use_3d=True,
|
|
1094
|
+
return_components=True,
|
|
1095
|
+
)
|
|
1096
|
+
# Keys present, values NaN
|
|
1097
|
+
assert "v_3d" in res and "vx" in res and "vy" in res and "vz" in res
|
|
1098
|
+
assert np.isnan(res["v_3d"])
|
|
1099
|
+
assert "v" not in res
|
|
1100
|
+
|
|
1101
|
+
res_2d = calculate_velocity_individual(
|
|
1102
|
+
test_features.iloc[0],
|
|
1103
|
+
test_features.iloc[1],
|
|
1104
|
+
method_distance="latlon",
|
|
1105
|
+
vertical_coord="height",
|
|
1106
|
+
use_3d=False,
|
|
1107
|
+
return_components=True,
|
|
1108
|
+
)
|
|
1109
|
+
assert np.isnan(res_2d["v"])
|