Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Free appear/disappear in first/last frames #118

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions motile/costs/appear.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -27,8 +29,6 @@
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__(
Expand All @@ -45,13 +45,16 @@

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):

Check warning on line 52 in motile/costs/appear.py

View check run for this annotation

Codecov / codecov/patch

motile/costs/appear.py#L52

Added line #L52 was not covered by tests
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)
10 changes: 7 additions & 3 deletions motile/costs/disappear.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@
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.

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:
Expand All @@ -30,9 +31,12 @@

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):

Check warning on line 38 in motile/costs/disappear.py

View check run for this annotation

Codecov / codecov/patch

motile/costs/disappear.py#L38

Added line #L38 was not covered by tests
continue
if G.nodes[node][G.frame_attribute] == G.get_frames()[1] - 1:
continue
solver.add_variable_cost(index, 1.0, self.constant)
7 changes: 4 additions & 3 deletions motile/track_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,10 +220,11 @@ def _convert_nx_hypernode(

return (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()

Expand All @@ -246,8 +247,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 = {}
Expand Down
4 changes: 3 additions & 1 deletion tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from motile.constraints import MaxChildren, MaxParents
from motile.costs import (
Appear,
Disappear,
EdgeDistance,
EdgeSelection,
NodeSelection,
Expand Down Expand Up @@ -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))
Expand All @@ -75,4 +77,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=}"
2 changes: 2 additions & 0 deletions tests/test_costs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import motile
import pytest
from motile.constraints import MaxChildren, MaxParents
from motile.costs import (
Appear,
Expand All @@ -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

Expand Down
28 changes: 10 additions & 18 deletions tests/test_structsvm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()