From e836b5788401a32e738aa8425b48eb44a7a19a21 Mon Sep 17 00:00:00 2001 From: Benjamin Gallusser Date: Mon, 12 Aug 2024 18:10:41 +0200 Subject: [PATCH 1/4] Remove dis/appear cost in last/first frames --- motile/costs/appear.py | 11 +++++++---- motile/costs/disappear.py | 10 +++++++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/motile/costs/appear.py b/motile/costs/appear.py index fa4e151..80985e7 100644 --- a/motile/costs/appear.py +++ b/motile/costs/appear.py @@ -13,6 +13,8 @@ class Appear(Cost): """Cost for :class:`~motile.variables.NodeAppear` variables. + This is cost is not applied to nodes in the first frame of the graph. + Args: weight: The weight to apply to the cost of each starting track. @@ -27,8 +29,6 @@ class Appear(Cost): ignore_attribute: The name of an optional node attribute that, if it is set and evaluates to ``True``, will not set the appear cost for that node. - This is useful to allow nodes in the first frame to appear at no - cost. """ def __init__( @@ -45,13 +45,16 @@ def __init__( def apply(self, solver: Solver) -> None: appear_indicators = solver.get_variables(NodeAppear) + G = solver.graph for node, index in appear_indicators.items(): if self.ignore_attribute is not None: - if solver.graph.nodes[node].get(self.ignore_attribute, False): + if G.nodes[node].get(self.ignore_attribute, False): continue + if G.nodes[node][G.frame_attribute] == G.get_frames()[0]: + continue if self.attribute is not None: solver.add_variable_cost( - index, solver.graph.nodes[node][self.attribute], self.weight + index, G.nodes[node][self.attribute], self.weight ) solver.add_variable_cost(index, 1.0, self.constant) diff --git a/motile/costs/disappear.py b/motile/costs/disappear.py index 0cf7e99..5f0c118 100644 --- a/motile/costs/disappear.py +++ b/motile/costs/disappear.py @@ -13,6 +13,8 @@ class Disappear(Cost): """Cost for :class:`motile.variables.NodeDisappear` variables. + This is cost is not applied to nodes in the last frame of the graph. + Args: constant (float): A constant cost for each node that ends a track. @@ -20,8 +22,7 @@ class Disappear(Cost): ignore_attribute: The name of an optional node attribute that, if it is set and evaluates to ``True``, will not set the disappear cost for that - node. This is useful to allow nodes in the last frame to disappear - at no cost. + node. """ def __init__(self, constant: float, ignore_attribute: str | None = None) -> None: @@ -30,9 +31,12 @@ def __init__(self, constant: float, ignore_attribute: str | None = None) -> None def apply(self, solver: Solver) -> None: disappear_indicators = solver.get_variables(NodeDisappear) + G = solver.graph for node, index in disappear_indicators.items(): if self.ignore_attribute is not None: - if solver.graph.nodes[node].get(self.ignore_attribute, False): + if G.nodes[node].get(self.ignore_attribute, False): continue + if G.nodes[node][G.frame_attribute] == G.get_frames()[1]: + continue solver.add_variable_cost(index, 1.0, self.constant) From b2cce3d2cfd9cb7f202cc605c8ee162b9688f724 Mon Sep 17 00:00:00 2001 From: Benjamin Gallusser Date: Tue, 13 Aug 2024 10:52:45 +0200 Subject: [PATCH 2/4] Update tests - update solution cost for API test - update found weights for SSVM tests - skip ignore_attribute test --- tests/test_api.py | 2 +- tests/test_costs.py | 2 ++ tests/test_structsvm.py | 28 ++++++++++------------------ 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 46786ca..5b8f2b2 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -75,4 +75,4 @@ def test_solver(arlo_graph): ) assert list(subgraph.nodes) == _selected_nodes(solver) == [0, 1, 2, 3, 4, 5] cost = solution.get_value() - assert cost == -206.0, f"{cost=}" + assert cost == -604.0, f"{cost=}" diff --git a/tests/test_costs.py b/tests/test_costs.py index 9eafbb4..c61391c 100644 --- a/tests/test_costs.py +++ b/tests/test_costs.py @@ -1,4 +1,5 @@ import motile +import pytest from motile.constraints import MaxChildren, MaxParents from motile.costs import ( Appear, @@ -9,6 +10,7 @@ ) +@pytest.mark.skip("Should test ignoring appear and disappear costs based on location.") def test_ignore_attributes(arlo_graph): graph = arlo_graph diff --git a/tests/test_structsvm.py b/tests/test_structsvm.py index a8845a9..e8a4ab5 100644 --- a/tests/test_structsvm.py +++ b/tests/test_structsvm.py @@ -75,20 +75,18 @@ def test_structsvm_common_toy_example(toy_graph): optimal_weights = solver.weights np.testing.assert_allclose( - optimal_weights[("NodeSelection", "weight")], -4.9771062468440785, rtol=0.01 + optimal_weights[("NodeSelection", "weight")], -3.27, atol=0.01 ) np.testing.assert_allclose( - optimal_weights[("NodeSelection", "constant")], -3.60083857250377, rtol=0.01 + optimal_weights[("NodeSelection", "constant")], 1.78, atol=0.01 ) np.testing.assert_allclose( - optimal_weights[("EdgeSelection", "weight")], -6.209937259450144, rtol=0.01 + optimal_weights[("EdgeSelection", "weight")], -3.23, atol=0.01 ) np.testing.assert_allclose( - optimal_weights[("EdgeSelection", "constant")], -2.4005590483600203, rtol=0.01 - ) - np.testing.assert_allclose( - optimal_weights[("Appear", "constant")], 32.13305455424766, rtol=0.01 + optimal_weights[("EdgeSelection", "constant")], 1.06, atol=0.01 ) + np.testing.assert_allclose(optimal_weights[("Appear", "constant")], 0.20, atol=0.01) solver = create_toy_solver(graph) solver.weights.from_ndarray(optimal_weights.to_ndarray()) @@ -171,20 +169,18 @@ def test_structsvm_noise(): logger.debug(solver.features.to_ndarray()) np.testing.assert_allclose( - optimal_weights[("NodeSelection", "weight")], -2.7777798708004564, rtol=0.01 - ) - np.testing.assert_allclose( - optimal_weights[("NodeSelection", "constant")], -1.3883786845544988, rtol=0.01 + optimal_weights[("NodeSelection", "weight")], -2.77, atol=0.01 ) np.testing.assert_allclose( - optimal_weights[("EdgeSelection", "weight")], -3.3333338262308043, rtol=0.01 + optimal_weights[("NodeSelection", "constant")], 0.39, atol=0.01 ) np.testing.assert_allclose( - optimal_weights[("EdgeSelection", "constant")], -0.9255857897041805, rtol=0.01 + optimal_weights[("EdgeSelection", "weight")], -3.33, atol=0.01 ) np.testing.assert_allclose( - optimal_weights[("Appear", "constant")], 19.53720680712646, rtol=0.01 + optimal_weights[("EdgeSelection", "constant")], 0, atol=0.01 ) + np.testing.assert_allclose(optimal_weights[("Appear", "constant")], 0.39, atol=0.01) def _assert_edges(solver, solution): edge_indicators = solver.get_variables(EdgeSelected) @@ -213,7 +209,3 @@ def _assert_edges(solver, solution): logger.debug(solver.get_variables(EdgeSelected)) _assert_edges(solver, solution) - - -if __name__ == "__main__": - test_structsvm_noise() From 9bd862d54a39b78c6a40476e5a5b0e7e35607a4a Mon Sep 17 00:00:00 2001 From: Benjamin Gallusser Date: Wed, 14 Aug 2024 10:04:52 +0200 Subject: [PATCH 3/4] Set TrackGraph get_frame defaults to ints --- motile/plot.py | 3 +-- motile/track_graph.py | 7 ++++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/motile/plot.py b/motile/plot.py index 7182803..e24f9df 100644 --- a/motile/plot.py +++ b/motile/plot.py @@ -155,8 +155,7 @@ def label_edge_func(edge): label_edge_func = label_func frame_attribute = graph.frame_attribute - # (get_frames() will return a tuple including None if the graph has no nodes) - frames = list(range(*graph.get_frames())) # type: ignore + frames = list(range(*graph.get_frames())) node_positions = np.asarray( [ diff --git a/motile/track_graph.py b/motile/track_graph.py index 3057ced..b2ef5fa 100644 --- a/motile/track_graph.py +++ b/motile/track_graph.py @@ -228,10 +228,11 @@ def _hyperedge_nx_node_to_edge_tuple_and_neighbors( return edge_tuple, in_nodes, out_nodes - def get_frames(self) -> tuple[int | None, int | None]: + def get_frames(self) -> tuple[int, int]: """Return tuple with first and last (exclusive) frame this graph has nodes for. Returns ``(t_begin, t_end)`` where ``t_end`` is exclusive. + Returns ``(0, 0)`` for empty graph. """ self._update_metadata() @@ -254,8 +255,8 @@ def _update_metadata(self) -> None: if not self.nodes: self._nodes_by_frame = {} - self.t_begin = None - self.t_end = None + self.t_begin = 0 + self.t_end = 0 return self._nodes_by_frame = {} From fc81fe6f02fed8c5b552bb616d7d7a92351bfa8a Mon Sep 17 00:00:00 2001 From: Benjamin Gallusser Date: Wed, 14 Aug 2024 10:05:02 +0200 Subject: [PATCH 4/4] Fix and test Disappear --- motile/costs/disappear.py | 2 +- tests/test_api.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/motile/costs/disappear.py b/motile/costs/disappear.py index 5f0c118..89f2bdb 100644 --- a/motile/costs/disappear.py +++ b/motile/costs/disappear.py @@ -37,6 +37,6 @@ def apply(self, solver: Solver) -> None: if self.ignore_attribute is not None: if G.nodes[node].get(self.ignore_attribute, False): continue - if G.nodes[node][G.frame_attribute] == G.get_frames()[1]: + if G.nodes[node][G.frame_attribute] == G.get_frames()[1] - 1: continue solver.add_variable_cost(index, 1.0, self.constant) diff --git a/tests/test_api.py b/tests/test_api.py index 5b8f2b2..5fac301 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -4,6 +4,7 @@ from motile.constraints import MaxChildren, MaxParents from motile.costs import ( Appear, + Disappear, EdgeDistance, EdgeSelection, NodeSelection, @@ -58,6 +59,7 @@ def test_solver(arlo_graph): ) solver.add_cost(EdgeDistance(position_attribute=("x",), weight=0.5)) solver.add_cost(Appear(constant=200.0, attribute="score", weight=-1.0)) + solver.add_cost(Disappear(constant=55.0)) solver.add_cost(Split(constant=100.0, attribute="score", weight=1.0)) solver.add_constraint(MaxParents(1))