diff --git a/trackpy/linking/utils.py b/trackpy/linking/utils.py index ab9c5841..a4a2589b 100644 --- a/trackpy/linking/utils.py +++ b/trackpy/linking/utils.py @@ -166,6 +166,10 @@ def in_track(self): def track(self): """Returns the track that this :class:`Point` is in. May be `None` """ return self._track + + def __hash__(self): + """Returns a deterministic value so that set() order is more stable.""" + return self.uuid class TrackUnstored: diff --git a/trackpy/tests/test_find_link.py b/trackpy/tests/test_find_link.py index f9a369e9..45b9905c 100644 --- a/trackpy/tests/test_find_link.py +++ b/trackpy/tests/test_find_link.py @@ -16,6 +16,7 @@ class FindLinkTests(SubnetNeededTests): def setUp(self): super().setUp() + self.coordinates_exact = False # b/c we roundtrip to images self.linker_opts['separation'] = 10 self.linker_opts['diameter'] = 15 self.linker_opts['preprocess'] = False diff --git a/trackpy/tests/test_linking.py b/trackpy/tests/test_linking.py index 4ff5fe03..f113d26c 100644 --- a/trackpy/tests/test_linking.py +++ b/trackpy/tests/test_linking.py @@ -499,6 +499,43 @@ def test_penalty(self): pandas_sort(actual, ['x'], inplace=True) assert_equal(actual['particle'].values.astype(int), case2['particle'].values.astype(int)) + + def test_degeneracy_forward(self): + """Check that a trivial degenerate subnet is resolved stably.""" + if not getattr(self, 'coordinates_exact', True): + return # Test will not be meaningful + result_counts = {0: 0, 1: 0} + for i in range(100): + # degen_df = pd.DataFrame({ + # 'y': [-1, 1, 0], + # 'x': [-1, 1, 0], + # 'frame': [ 0, 0, 1] + # }) + degen_df = pd.DataFrame({ + 'y': [0, -1, 1], + 'x': [0, -1, 1], + 'frame': [0, 1, 1] + }) + out = self.link(degen_df, search_range=1.8, t_column="frame") + result_counts[out.particle.iloc[-1]] += 1 + # There are two degenerate forward candidates for the last frame. + assert any((c == 100 for c in result_counts.values())) # Stable + + def test_degeneracy_source(self): + """Check that a trivial degenerate subnet is resolved stably.""" + if not getattr(self, 'coordinates_exact', True): + return # Test will not be meaningful + result_counts = {0: 0, 1: 0} + for i in range(100): + degen_df = pd.DataFrame({ + 'y': [-1, 1, 0], + 'x': [-1, 1, 0], + 'frame': [ 0, 0, 1] + }) + out = self.link(degen_df, search_range=1.8, t_column="frame") + result_counts[out.particle.iloc[-1]] += 1 + # There are two degenerate source candidates for the last frame. + assert any((c == 100 for c in result_counts.values())) # Stable def test_memory(self): """A unit-stepping trajectory and a random walk are observed @@ -565,6 +602,22 @@ def test_memory_on_one_gap(self): actual = self.link(f1, 5, memory=1) assert_traj_equal(actual, expected) + def test_memory_degeneracy(self): + """Check that with memory, degeneracies are resolved stably.""" + if not getattr(self, 'coordinates_exact', True): + return # Test will not be meaningful + result_counts = {0: 0, 1: 0} + for i in range(100): + mem_df = pd.DataFrame({ + 'y': [-1, 1, 100, 0], + 'x': [-1, 1, 100, 0], + 'frame': [ 0, 0, 1, 2] + }) + out = self.link(mem_df, search_range=1.8, memory=1, t_column="frame") + result_counts[out.particle.iloc[-1]] += 1 + # With memory > 0, there are two degenerate candidates for the last frame. + assert any((c == 100 for c in result_counts.values())) # Stable + def test_pathological_tracking(self): level_count = 5 p_count = 16