Simple-Track 2.1.0__tar.gz → 2.2.1__tar.gz
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.
- {simple_track-2.1.0 → simple_track-2.2.1}/PKG-INFO +1 -3
- {simple_track-2.1.0 → simple_track-2.2.1}/README.md +0 -2
- {simple_track-2.1.0 → simple_track-2.2.1}/pyproject.toml +2 -2
- {simple_track-2.1.0 → simple_track-2.2.1}/src/Simple_Track.egg-info/PKG-INFO +1 -3
- {simple_track-2.1.0 → simple_track-2.2.1}/src/simpletrack/feature.py +5 -4
- {simple_track-2.1.0 → simple_track-2.2.1}/src/simpletrack/flow_solver.py +70 -23
- {simple_track-2.1.0 → simple_track-2.2.1}/src/simpletrack/frame.py +12 -5
- {simple_track-2.1.0 → simple_track-2.2.1}/src/simpletrack/frame_tracker.py +7 -7
- {simple_track-2.1.0 → simple_track-2.2.1}/src/simpletrack/load.py +1 -10
- {simple_track-2.1.0 → simple_track-2.2.1}/src/simpletrack/track.py +9 -2
- {simple_track-2.1.0 → simple_track-2.2.1}/tests/test_flow_solver.py +45 -2
- {simple_track-2.1.0 → simple_track-2.2.1}/tests/test_frame.py +4 -9
- {simple_track-2.1.0 → simple_track-2.2.1}/tests/test_frame_tracker.py +2 -1
- {simple_track-2.1.0 → simple_track-2.2.1}/tests/test_mwe_output.py +22 -1
- {simple_track-2.1.0 → simple_track-2.2.1}/LICENSE +0 -0
- {simple_track-2.1.0 → simple_track-2.2.1}/setup.cfg +0 -0
- {simple_track-2.1.0 → simple_track-2.2.1}/src/Simple_Track.egg-info/SOURCES.txt +0 -0
- {simple_track-2.1.0 → simple_track-2.2.1}/src/Simple_Track.egg-info/dependency_links.txt +0 -0
- {simple_track-2.1.0 → simple_track-2.2.1}/src/Simple_Track.egg-info/entry_points.txt +0 -0
- {simple_track-2.1.0 → simple_track-2.2.1}/src/Simple_Track.egg-info/requires.txt +0 -0
- {simple_track-2.1.0 → simple_track-2.2.1}/src/Simple_Track.egg-info/top_level.txt +0 -0
- {simple_track-2.1.0 → simple_track-2.2.1}/src/run_simple_track.py +0 -0
- {simple_track-2.1.0 → simple_track-2.2.1}/src/simpletrack/__init__.py +0 -0
- {simple_track-2.1.0 → simple_track-2.2.1}/src/simpletrack/exceptions.py +0 -0
- {simple_track-2.1.0 → simple_track-2.2.1}/src/simpletrack/frame_output.py +0 -0
- {simple_track-2.1.0 → simple_track-2.2.1}/src/simpletrack/utils.py +0 -0
- {simple_track-2.1.0 → simple_track-2.2.1}/tests/test_cli.py +0 -0
- {simple_track-2.1.0 → simple_track-2.2.1}/tests/test_feature.py +0 -0
- {simple_track-2.1.0 → simple_track-2.2.1}/tests/test_load.py +0 -0
- {simple_track-2.1.0 → simple_track-2.2.1}/tests/test_simple_track_and_load.py +0 -0
- {simple_track-2.1.0 → simple_track-2.2.1}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Simple-Track
|
|
3
|
-
Version: 2.1
|
|
3
|
+
Version: 2.2.1
|
|
4
4
|
Summary: Threshold-based object tracking algorithm for 2D data
|
|
5
5
|
Author-email: Adam Gainford <adam.gainford@reading.ac.uk>, Thorwald Stein <t.h.m.stein@reading.ac.uk>
|
|
6
6
|
License-Expression: MPL-2.0
|
|
@@ -52,8 +52,6 @@ While Simple-Track is designed to accept a wide range of input data, certain req
|
|
|
52
52
|
|
|
53
53
|
* The input data must be gridded and contain a consistent spatial domain and resolution between frames.
|
|
54
54
|
|
|
55
|
-
* The input grid must be evenly shaped (this will be relaxed in the future)
|
|
56
|
-
|
|
57
55
|
* The features of interest must be defined by a threshold value, and these features must translate as a result of a spatially consistent background flow.
|
|
58
56
|
|
|
59
57
|
* The time between frames should be sufficiently short such that features can be reasonably expected to persist between frames. This is not a strict requirement since the tool includes an artificial advection step that projects data onto a common time, but it is likely that longer time steps will lead to more errors in feature matching and therefore less accurate tracking statistics.
|
|
@@ -30,8 +30,6 @@ While Simple-Track is designed to accept a wide range of input data, certain req
|
|
|
30
30
|
|
|
31
31
|
* The input data must be gridded and contain a consistent spatial domain and resolution between frames.
|
|
32
32
|
|
|
33
|
-
* The input grid must be evenly shaped (this will be relaxed in the future)
|
|
34
|
-
|
|
35
33
|
* The features of interest must be defined by a threshold value, and these features must translate as a result of a spatially consistent background flow.
|
|
36
34
|
|
|
37
35
|
* The time between frames should be sufficiently short such that features can be reasonably expected to persist between frames. This is not a strict requirement since the tool includes an artificial advection step that projects data onto a common time, but it is likely that longer time steps will lead to more errors in feature matching and therefore less accurate tracking statistics.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "Simple-Track"
|
|
7
|
-
version = "2.1
|
|
7
|
+
version = "2.2.1"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Adam Gainford", email="adam.gainford@reading.ac.uk" },
|
|
10
10
|
{ name="Thorwald Stein", email="t.h.m.stein@reading.ac.uk"}
|
|
@@ -43,4 +43,4 @@ testpaths = ["tests"]
|
|
|
43
43
|
|
|
44
44
|
[tool.ruff.lint]
|
|
45
45
|
select = ["E", "F", "B", "I", "UP", "SIM", "D101"]
|
|
46
|
-
ignore = ["SIM102"]
|
|
46
|
+
ignore = ["SIM102"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Simple-Track
|
|
3
|
-
Version: 2.1
|
|
3
|
+
Version: 2.2.1
|
|
4
4
|
Summary: Threshold-based object tracking algorithm for 2D data
|
|
5
5
|
Author-email: Adam Gainford <adam.gainford@reading.ac.uk>, Thorwald Stein <t.h.m.stein@reading.ac.uk>
|
|
6
6
|
License-Expression: MPL-2.0
|
|
@@ -52,8 +52,6 @@ While Simple-Track is designed to accept a wide range of input data, certain req
|
|
|
52
52
|
|
|
53
53
|
* The input data must be gridded and contain a consistent spatial domain and resolution between frames.
|
|
54
54
|
|
|
55
|
-
* The input grid must be evenly shaped (this will be relaxed in the future)
|
|
56
|
-
|
|
57
55
|
* The features of interest must be defined by a threshold value, and these features must translate as a result of a spatially consistent background flow.
|
|
58
56
|
|
|
59
57
|
* The time between frames should be sufficiently short such that features can be reasonably expected to persist between frames. This is not a strict requirement since the tool includes an artificial advection step that projects data onto a common time, but it is likely that longer time steps will lead to more errors in feature matching and therefore less accurate tracking statistics.
|
|
@@ -311,11 +311,12 @@ class Feature:
|
|
|
311
311
|
|
|
312
312
|
# Reverse eigenvectors to be in (y,x) format, consistent with rest of package
|
|
313
313
|
# Native converts from numpy type to python type
|
|
314
|
+
# Real ensures types are not complex, which can happen with PCA decomposition
|
|
314
315
|
return (
|
|
315
|
-
native(major_unit_vector[::-1]),
|
|
316
|
-
native(minor_unit_vector[::-1]),
|
|
317
|
-
native(major_diameter / 2),
|
|
318
|
-
native(minor_diameter / 2),
|
|
316
|
+
native(np.real(major_unit_vector[::-1])),
|
|
317
|
+
native(np.real(minor_unit_vector[::-1])),
|
|
318
|
+
native(np.real(major_diameter / 2)),
|
|
319
|
+
native(np.real(minor_diameter / 2)),
|
|
319
320
|
)
|
|
320
321
|
|
|
321
322
|
def calculate_centroid(self) -> tuple:
|
|
@@ -55,9 +55,15 @@ class FlowSolver:
|
|
|
55
55
|
Defaults to True.
|
|
56
56
|
"""
|
|
57
57
|
if isinstance(subdomain_size, int):
|
|
58
|
+
if subdomain_size % 2 != 0:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"Expected subdomain_size to be even, got {subdomain_size}"
|
|
61
|
+
)
|
|
58
62
|
self.subdomain_shape = np.array([subdomain_size, subdomain_size], dtype=int)
|
|
59
|
-
elif isinstance(subdomain_size, float):
|
|
60
|
-
raise TypeError(
|
|
63
|
+
elif isinstance(subdomain_size, (float, str)):
|
|
64
|
+
raise TypeError(
|
|
65
|
+
f"Expected int or array-like, got type{type(subdomain_size)}"
|
|
66
|
+
)
|
|
61
67
|
elif subdomain_size is None:
|
|
62
68
|
self.subdomain_shape = None
|
|
63
69
|
else:
|
|
@@ -108,9 +114,16 @@ class FlowSolver:
|
|
|
108
114
|
prev_features, current_features, equal_shape=True, ndim=2, dtype=int
|
|
109
115
|
)
|
|
110
116
|
|
|
117
|
+
# If input arrays are not an even/"nice" shape, pad them to the next order of
|
|
118
|
+
# magnitude. This is required for an integer number of overlapping subdomains
|
|
119
|
+
# fit into the domain. Flow field will be cropped to original shape
|
|
120
|
+
input_shape = prev_features.shape
|
|
121
|
+
prev_features = self.pad_to_max_order_of_magnitude(prev_features)
|
|
122
|
+
current_features = self.pad_to_max_order_of_magnitude(current_features)
|
|
123
|
+
|
|
111
124
|
# Determine a subdomain size if not provided
|
|
112
125
|
if self.subdomain_shape is None:
|
|
113
|
-
self.subdomain_shape = self.
|
|
126
|
+
self.subdomain_shape = self.setup_subdomain_shape(prev_features.shape)
|
|
114
127
|
|
|
115
128
|
# Check inputs, don't proceed if not validated
|
|
116
129
|
prev_features, current_features = self._check_inputs(
|
|
@@ -163,16 +176,67 @@ class FlowSolver:
|
|
|
163
176
|
interior_y_subdom_bounds,
|
|
164
177
|
interior_x_subdom_bounds,
|
|
165
178
|
subdomain_dy,
|
|
166
|
-
|
|
179
|
+
input_shape,
|
|
167
180
|
)
|
|
168
181
|
x_flow = self.interpolate_subdomain_flows(
|
|
169
182
|
interior_y_subdom_bounds,
|
|
170
183
|
interior_x_subdom_bounds,
|
|
171
184
|
subdomain_dx,
|
|
172
|
-
|
|
185
|
+
input_shape,
|
|
173
186
|
)
|
|
174
187
|
return y_flow, x_flow
|
|
175
188
|
|
|
189
|
+
def pad_to_max_order_of_magnitude(self, arr: np.ndarray) -> np.ndarray:
|
|
190
|
+
"""
|
|
191
|
+
Pad an array with zeros so each dimension is rounded up to the maximum order of
|
|
192
|
+
magnitude across all dimensions.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
def _get_magnitude(n: int) -> int:
|
|
196
|
+
if n <= 1:
|
|
197
|
+
return 1
|
|
198
|
+
return 10 ** np.floor(np.log10(n))
|
|
199
|
+
|
|
200
|
+
def _round_to_magnitude(n: int, magnitude: int) -> int:
|
|
201
|
+
return int(np.ceil(n / magnitude) * magnitude)
|
|
202
|
+
|
|
203
|
+
# Get the maximum order of magnitude across all dimensions
|
|
204
|
+
magnitudes = [_get_magnitude(s) for s in arr.shape]
|
|
205
|
+
max_magnitude = max(magnitudes)
|
|
206
|
+
|
|
207
|
+
# Round each dimension to the next multiple of max_magnitude
|
|
208
|
+
target_shape = tuple(_round_to_magnitude(s, max_magnitude) for s in arr.shape)
|
|
209
|
+
pad_width = tuple(
|
|
210
|
+
(0, t - s) for s, t in zip(arr.shape, target_shape, strict=True)
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
return np.pad(arr, pad_width, mode="constant", constant_values=0)
|
|
214
|
+
|
|
215
|
+
def setup_subdomain_shape(self, feature_field_shape: np.ndarray) -> np.ndarray:
|
|
216
|
+
"""
|
|
217
|
+
Set up the subdomain shape based on the feature field shape.
|
|
218
|
+
If the subdomain shape has already been set in init, this will
|
|
219
|
+
check that the subdomain shape can fit into the feature field shape.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
feature_field_shape (np.ndarray): Shape of the feature field
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
np.ndarray: The setup subdomain shape.
|
|
226
|
+
"""
|
|
227
|
+
if self.subdomain_shape is None:
|
|
228
|
+
# Check input field shape
|
|
229
|
+
sd_shape = np.array(feature_field_shape) // 5
|
|
230
|
+
if not self.check_subdomain_size_fits_in_full_domain(
|
|
231
|
+
feature_field_shape, sd_shape
|
|
232
|
+
):
|
|
233
|
+
# TODO: do something more intelligent here rather than just raise an error
|
|
234
|
+
# Try to find another subdomain shape that could fit
|
|
235
|
+
raise Exception(
|
|
236
|
+
f"Subdomain shape ({sd_shape}) cannot fit ({feature_field_shape})"
|
|
237
|
+
)
|
|
238
|
+
return sd_shape
|
|
239
|
+
|
|
176
240
|
def get_subdomain_containment_arrays(
|
|
177
241
|
self, full_domain_shape: NDArray, subdomain_shape: NDArray
|
|
178
242
|
) -> list[NDArray, NDArray]:
|
|
@@ -276,23 +340,6 @@ class FlowSolver:
|
|
|
276
340
|
subdomain_vals[invalid_tolerance] = np.nan
|
|
277
341
|
return subdomain_vals
|
|
278
342
|
|
|
279
|
-
def get_subdomain_shape(self, feature_field_shape):
|
|
280
|
-
# TODO: figure out some logic here for getting a good sd size
|
|
281
|
-
# if none is provided.
|
|
282
|
-
# Use this for now, but it won't work in all cases!
|
|
283
|
-
# TODO: this is entirely arbitrary. Check if this is sensible. It probably isnt
|
|
284
|
-
# TODO: what if domain is an odd shape?? What then??
|
|
285
|
-
sd_shape = np.array(feature_field_shape) // 5
|
|
286
|
-
if not self.check_subdomain_size_fits_in_full_domain(
|
|
287
|
-
feature_field_shape, sd_shape
|
|
288
|
-
):
|
|
289
|
-
# TODO: do something more intelligent here rather than just raise an error
|
|
290
|
-
# Try to find another subdomain shape that could fit
|
|
291
|
-
raise Exception(
|
|
292
|
-
f"Subdomain shape ({sd_shape}) cannot fit ({feature_field_shape})"
|
|
293
|
-
)
|
|
294
|
-
return sd_shape
|
|
295
|
-
|
|
296
343
|
def check_subdomain_size_fits_in_full_domain(
|
|
297
344
|
self, feature_field_shape: NDArray, subdomain_shape: NDArray
|
|
298
345
|
) -> bool:
|
|
@@ -538,7 +585,7 @@ class FlowSolver:
|
|
|
538
585
|
subdomain_count = np.prod(self.subdomain_shape)
|
|
539
586
|
min_feature_coverage = subdomain_count * self.min_fractional_coverage
|
|
540
587
|
if np.sum(arr1) < min_feature_coverage or np.sum(arr2) < min_feature_coverage:
|
|
541
|
-
print(f"Threshold for running optical flow: {
|
|
588
|
+
print(f"Threshold for running optical flow: {min_feature_coverage}")
|
|
542
589
|
print(f"Number of pixels above threshold in arr1: {np.sum(arr1)}")
|
|
543
590
|
print(f"Number of pixels above threshold in arr2: {np.sum(arr2)}")
|
|
544
591
|
print("Number of features in arr1 and/or arr2 less than threshold. ")
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import datetime as dt
|
|
2
|
+
import warnings
|
|
2
3
|
|
|
3
4
|
import numpy as np
|
|
4
5
|
import scipy.ndimage as ndimage
|
|
@@ -280,15 +281,21 @@ class Frame:
|
|
|
280
281
|
Update the feature_field to reflect provisional ids.
|
|
281
282
|
"""
|
|
282
283
|
if self._feature_field is None:
|
|
283
|
-
|
|
284
|
-
|
|
284
|
+
print(
|
|
285
|
+
UserWarning(
|
|
286
|
+
"Feature field is not set. Cannot update using provisional ids."
|
|
287
|
+
)
|
|
285
288
|
)
|
|
289
|
+
return
|
|
286
290
|
|
|
287
291
|
if not self._features:
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
292
|
+
print(
|
|
293
|
+
UserWarning(
|
|
294
|
+
"Features have not been loaded into this Frame. "
|
|
295
|
+
"Cannot update using provisional ids."
|
|
296
|
+
)
|
|
291
297
|
)
|
|
298
|
+
return
|
|
292
299
|
|
|
293
300
|
updated_feature_field = np.zeros_like(self._feature_field)
|
|
294
301
|
updated_lifetime_field = np.zeros_like(self._feature_field)
|
|
@@ -768,13 +768,13 @@ class FrameTracker:
|
|
|
768
768
|
# Set the first value of the hist to 0 since this represents the background
|
|
769
769
|
overlap_hist[0] = 0
|
|
770
770
|
|
|
771
|
-
# Normalise overlap histogram by size of each feature in advected field only
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
)
|
|
771
|
+
# Normalise overlap histogram by size of each feature in advected field only.
|
|
772
|
+
# bincount computes all per-id pixel counts in a single pass; the previous
|
|
773
|
+
# per-id np.count_nonzero loop was O(max_id * domain) and dominated runtime
|
|
774
|
+
# for long sequences (feature_field holds persistent ids, which grow large).
|
|
775
|
+
norm_sizes = np.bincount(
|
|
776
|
+
advected_feature_field.ravel(), minlength=len(overlap_hist)
|
|
777
|
+
)[: len(overlap_hist)]
|
|
778
778
|
# Replace any zero sizes with 1 to avoid division by zero
|
|
779
779
|
norm_sizes = np.where(norm_sizes == 0, 1, norm_sizes)
|
|
780
780
|
overlap_normed = overlap_hist / norm_sizes
|
|
@@ -43,16 +43,7 @@ class BaseLoader:
|
|
|
43
43
|
# Check consistency of data shape
|
|
44
44
|
if self.domain_shape is None:
|
|
45
45
|
self.domain_shape = output_arr.shape
|
|
46
|
-
|
|
47
|
-
# This requirement will ideally be relaxed in future
|
|
48
|
-
# but currently required for flow_solver to work correctly.
|
|
49
|
-
if not all([size % 2 == 0 for size in self.domain_shape]):
|
|
50
|
-
msg = (
|
|
51
|
-
"Simple-Track requires even domain sizes, "
|
|
52
|
-
+ f"input is shape {self.domain_shape}. Consider"
|
|
53
|
-
+ " padding or cropping your data to meet this requirement."
|
|
54
|
-
)
|
|
55
|
-
raise ValueError(msg)
|
|
46
|
+
|
|
56
47
|
output_arr = check_arrays(output_arr, shape=self.domain_shape, ndim=2)
|
|
57
48
|
|
|
58
49
|
# Check output time is a sensible type
|
|
@@ -152,8 +152,15 @@ class Tracker:
|
|
|
152
152
|
|
|
153
153
|
# Now run flow solver between previous and current frame
|
|
154
154
|
prev_frame = self.timeline.get_previous_frame(frame.time)
|
|
155
|
-
# Set max id for assigning to new features
|
|
156
|
-
frame
|
|
155
|
+
# Set max id for assigning to new features.
|
|
156
|
+
# An all-quiet first frame leaves prev_frame.max_id None
|
|
157
|
+
# (identify_features returns early without setting it when no
|
|
158
|
+
# features are above threshold). Skip the carry-forward then;
|
|
159
|
+
# get_next_available_feature_id lazily initialises max_id from the
|
|
160
|
+
# frame's own feature_field, and there are no earlier ids to collide
|
|
161
|
+
# with.
|
|
162
|
+
if prev_frame.max_id is not None:
|
|
163
|
+
frame.max_id = prev_frame.max_id
|
|
157
164
|
# Get the flow field that translates features between the two frames
|
|
158
165
|
y_flow, x_flow = self.flow_solver.analyse_flow(prev_frame, frame)
|
|
159
166
|
|
|
@@ -573,7 +573,9 @@ def test_multiple_unequal_feature_unequal_flow_advection(
|
|
|
573
573
|
@pytest.mark.parametrize(
|
|
574
574
|
"test_dy_f0, test_dx_f0, test_dy_f1, test_dx_f1, f1_y_growth, expected_dy, expected_dx",
|
|
575
575
|
[
|
|
576
|
-
|
|
576
|
+
# First test shows inconsistent behaviour between different architectures (Linux vs MacOS), needs further refinement
|
|
577
|
+
# [10, 0, 0, 0, 7, 7, 0], # F0 advects, F1 grows by 7 -> expected dy = 7 (?)
|
|
578
|
+
# All other tests do not show the expected outcome on any architecture.
|
|
577
579
|
# [10, 0, 10, 0, 7, 17, 0], # F1 grows by 7 advects by 10, should get dy = 17, get 0
|
|
578
580
|
# [0, 0, 0, 5, 7, 7, 5], # F1 grows by 7 and dx=5, expect dy=7 and dx=5. get dy=0
|
|
579
581
|
# [0, 0, 5, 5, 7, 12, 5], # Expect dy=12, get 0
|
|
@@ -621,7 +623,8 @@ def test_growing_feature(
|
|
|
621
623
|
[10, 0, 15, 0, 10, 10, 0], # F1 advects 15 but shrinks 10. Expect dy=10 from f0
|
|
622
624
|
[0, 10, 0, 15, 5, 0, 10], # Similar behaviour if just dx rather than just dy
|
|
623
625
|
[10, 10, 15, 15, 5, 10, 10], # Also works if features move in diagonal
|
|
624
|
-
|
|
626
|
+
# Inconsistent behaviour between different architectures (Linux vs MacOS), needs further refinement
|
|
627
|
+
# [0, 0, 10, 10, 5, 0, 0], # If f0 is stationary but f1 moves, solver picks f0
|
|
625
628
|
[10, 10, 0, 0, 5, 10, 10], # If f0 moves and f1 is stationary, solver picks f0
|
|
626
629
|
],
|
|
627
630
|
)
|
|
@@ -720,3 +723,43 @@ def test_subdomain_iter(
|
|
|
720
723
|
assert result == expected_subdomain_iter
|
|
721
724
|
except expected_subdomain_iter:
|
|
722
725
|
pass
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
@pytest.mark.parametrize(
|
|
729
|
+
"subdomain_size, expected_error",
|
|
730
|
+
[
|
|
731
|
+
[9, ValueError], # Odd subdomain size
|
|
732
|
+
[10.5, TypeError], # Non-integer subdomain size
|
|
733
|
+
[-10, ValueError], # Negative subdomain size
|
|
734
|
+
["10", TypeError], # Non-numeric subdomain size
|
|
735
|
+
],
|
|
736
|
+
)
|
|
737
|
+
def test_init_catches_invalid_subdomain_size(subdomain_size, expected_error):
|
|
738
|
+
try:
|
|
739
|
+
FlowSolver(subdomain_size=subdomain_size)
|
|
740
|
+
except expected_error:
|
|
741
|
+
pass
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def test_init_accepts_valid_subdomain_size():
|
|
745
|
+
try:
|
|
746
|
+
solver = FlowSolver(subdomain_size=10)
|
|
747
|
+
np.testing.assert_array_equal(solver.subdomain_shape, np.array([10, 10]))
|
|
748
|
+
except Exception as e:
|
|
749
|
+
pytest.fail(f"Unexpected exception raised: {e}")
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
@pytest.mark.parametrize(
|
|
753
|
+
"input_array, expected_output",
|
|
754
|
+
[
|
|
755
|
+
[np.zeros((10, 10)), np.zeros((10, 10))],
|
|
756
|
+
[np.zeros((5, 5)), np.zeros((5, 5))],
|
|
757
|
+
[np.zeros((21, 140)), np.zeros((100, 200))],
|
|
758
|
+
[np.zeros((230, 410)), np.zeros((300, 500))],
|
|
759
|
+
[np.zeros((3, 78)), np.zeros((10, 80))],
|
|
760
|
+
[np.zeros((4, 140)), np.zeros((100, 200))],
|
|
761
|
+
],
|
|
762
|
+
)
|
|
763
|
+
def test_pad_to_max_order_of_magnitude(input_array, expected_output):
|
|
764
|
+
result = of_solver.pad_to_max_order_of_magnitude(input_array)
|
|
765
|
+
np.testing.assert_array_equal(result, expected_output)
|
|
@@ -394,10 +394,8 @@ def test_update_fields_using_provisional_ids_with_no_feature_field():
|
|
|
394
394
|
test_frame.populate_features() # No feature field set, so no features populated
|
|
395
395
|
|
|
396
396
|
# Update the feature field without setting any provisional ids
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
except FeaturesNotFoundError:
|
|
400
|
-
pass
|
|
397
|
+
# Just prints a warning and returns without error
|
|
398
|
+
test_frame.update_fields_using_provisional_ids()
|
|
401
399
|
|
|
402
400
|
|
|
403
401
|
def test_update_fields_using_provisional_ids_with_no_features():
|
|
@@ -406,11 +404,8 @@ def test_update_fields_using_provisional_ids_with_no_features():
|
|
|
406
404
|
test_frame.feature_field = test_feature_field
|
|
407
405
|
test_frame.populate_features() # No features populated as feature field is all zeros
|
|
408
406
|
|
|
409
|
-
# Update the feature field without any features
|
|
410
|
-
|
|
411
|
-
test_frame.update_fields_using_provisional_ids()
|
|
412
|
-
except FeaturesNotFoundError:
|
|
413
|
-
pass
|
|
407
|
+
# Update the feature field without any features. Just prints a warning
|
|
408
|
+
test_frame.update_fields_using_provisional_ids()
|
|
414
409
|
|
|
415
410
|
|
|
416
411
|
def test_get_new_features():
|
|
@@ -446,7 +446,8 @@ def test_overlap_histogram_with_multiple_overlaps_and_different_labels(
|
|
|
446
446
|
],
|
|
447
447
|
[np.zeros((10), dtype=int), np.zeros((10), dtype=int), 1, 0, ArrayShapeError],
|
|
448
448
|
[np.zeros((5, 10), dtype=int), zero_arr, 1, 0, ArrayShapeError],
|
|
449
|
-
|
|
449
|
+
# This test should pass now that check_arrays converts to int type if values don't change
|
|
450
|
+
# [np.zeros((10, 10), dtype=float), np.zeros((10, 10)), 1, 0, ArrayTypeError],
|
|
450
451
|
[zero_arr, zero_arr, -1, 0, NegativeIDError],
|
|
451
452
|
[zero_arr, zero_arr, 1.5, 0, FloatIDError],
|
|
452
453
|
[zero_arr, zero_arr, 0, 0, ZeroIDError],
|
|
@@ -50,6 +50,12 @@ def generate_mwe_files(save_path=None):
|
|
|
50
50
|
# Cells merge
|
|
51
51
|
mwe_dt8[35:60, 50:70] = 1
|
|
52
52
|
|
|
53
|
+
# Ninth timestep: no features
|
|
54
|
+
mwe_dt9 = mwe_domain.copy()
|
|
55
|
+
|
|
56
|
+
# Tenth timestep: tracking when both frames contain no features
|
|
57
|
+
mwe_dt10 = mwe_domain.copy()
|
|
58
|
+
|
|
53
59
|
mwe_fields = [
|
|
54
60
|
mwe_dt1,
|
|
55
61
|
mwe_dt2,
|
|
@@ -59,13 +65,15 @@ def generate_mwe_files(save_path=None):
|
|
|
59
65
|
mwe_dt6,
|
|
60
66
|
mwe_dt7,
|
|
61
67
|
mwe_dt8,
|
|
68
|
+
mwe_dt9,
|
|
69
|
+
mwe_dt10,
|
|
62
70
|
]
|
|
63
71
|
if save_path is not None:
|
|
64
72
|
# Make containing directory if it doesn't exist
|
|
65
73
|
Path(save_path).mkdir(parents=True, exist_ok=True)
|
|
66
74
|
|
|
67
75
|
for mwe_idx, mwe in enumerate(mwe_fields):
|
|
68
|
-
np.savetxt(f"{save_path}/mwe_dt{mwe_idx + 1}.field", mwe)
|
|
76
|
+
np.savetxt(f"{save_path}/mwe_dt{str(mwe_idx + 1).zfill(2)}.field", mwe)
|
|
69
77
|
return mwe_fields
|
|
70
78
|
|
|
71
79
|
|
|
@@ -349,6 +357,19 @@ def test_seventh_mwe_outputs(mwe_timeline):
|
|
|
349
357
|
assert np.all(frame.get_flow()) is not None
|
|
350
358
|
|
|
351
359
|
|
|
360
|
+
def test_ninth_mwe_outputs(mwe_timeline):
|
|
361
|
+
"""
|
|
362
|
+
Test that there are no features in the ninth timestep
|
|
363
|
+
"""
|
|
364
|
+
base_time = dt.datetime(2024, 1, 1, 0, 0, 0)
|
|
365
|
+
mwe_idx = 8
|
|
366
|
+
frame_time = base_time + dt.timedelta(minutes=5 * int(mwe_idx))
|
|
367
|
+
frame = mwe_timeline.get_frame(frame_time)
|
|
368
|
+
|
|
369
|
+
# test there we are back to one feature
|
|
370
|
+
assert len(frame.features) == 0
|
|
371
|
+
|
|
372
|
+
|
|
352
373
|
if __name__ == "__main__":
|
|
353
374
|
mwe_file_path = "./mwe_test_files"
|
|
354
375
|
Path(mwe_file_path).mkdir(parents=True, exist_ok=True)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|