From dbda890aa7dfd5d52cb213551bd79fcafb0b9bdb Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Fri, 12 May 2023 15:04:24 -0400 Subject: [PATCH 1/7] add docstyle linting, wip --- motile/constraints/constraint.py | 4 +-- motile/constraints/expression.py | 1 - motile/constraints/max_children.py | 1 - motile/constraints/max_parents.py | 1 - motile/constraints/pin.py | 1 - motile/costs/appear.py | 1 - motile/costs/costs.py | 3 +- motile/costs/edge_distance.py | 1 - motile/costs/edge_selection.py | 1 - motile/costs/features.py | 5 ++++ motile/costs/node_selection.py | 1 - motile/costs/split.py | 1 - motile/costs/weight.py | 9 ++++++ motile/costs/weights.py | 20 +++++++++++++ motile/data.py | 31 +++++++++++--------- motile/plot.py | 47 +++++++++++++++++------------- motile/solver.py | 15 +--------- motile/ssvm.py | 23 +++++++++++++++ motile/track_graph.py | 6 ++-- motile/variables/variable.py | 4 --- pyproject.toml | 15 ++++++++++ 21 files changed, 123 insertions(+), 68 deletions(-) diff --git a/motile/constraints/constraint.py b/motile/constraints/constraint.py index 04cccbf..ea5104c 100644 --- a/motile/constraints/constraint.py +++ b/motile/constraints/constraint.py @@ -10,6 +10,8 @@ class Constraint(ABC): + """A base class for a constraint that can be added to a solver.""" + @abstractmethod def instantiate( self, solver: Solver @@ -17,11 +19,9 @@ def instantiate( """Create and return specific linear constraints for the given solver. Args: - solver (:class:`Solver`): The solver instance to create linear constraints for. Returns: - An iterable of :class:`ilpy.Constraint`. """ diff --git a/motile/constraints/expression.py b/motile/constraints/expression.py index 9b7aeb3..a127d41 100644 --- a/motile/constraints/expression.py +++ b/motile/constraints/expression.py @@ -47,7 +47,6 @@ class ExpressionConstraint(Constraint): Whether to evaluate the expression for edges. By default, True. Example: - If the nodes of a graph are: cells = [ {"id": 0, "t": 0, "color": "red", "score": 1.0}, diff --git a/motile/constraints/max_children.py b/motile/constraints/max_children.py index e22d4be..f4bd828 100644 --- a/motile/constraints/max_children.py +++ b/motile/constraints/max_children.py @@ -22,7 +22,6 @@ class MaxChildren(Constraint): \sum_{e \in \\text{out_edges}(v)} x_e \leq \\text{max_children} Args: - max_children (int): The maximum number of children allowed. """ diff --git a/motile/constraints/max_parents.py b/motile/constraints/max_parents.py index d26b13e..02e1d3d 100644 --- a/motile/constraints/max_parents.py +++ b/motile/constraints/max_parents.py @@ -22,7 +22,6 @@ class MaxParents(Constraint): \sum_{e \in \\text{in_edges}(v)} x_e \leq \\text{max_parents} Args: - max_parents (int): The maximum number of parents allowed. """ diff --git a/motile/constraints/pin.py b/motile/constraints/pin.py index bc86074..f3e1491 100644 --- a/motile/constraints/pin.py +++ b/motile/constraints/pin.py @@ -17,7 +17,6 @@ class Pin(ExpressionConstraint): edges. Args: - attribute (string): The name of the node/edge attribute to use. """ diff --git a/motile/costs/appear.py b/motile/costs/appear.py index acac553..e7dea31 100644 --- a/motile/costs/appear.py +++ b/motile/costs/appear.py @@ -14,7 +14,6 @@ class Appear(Costs): """Costs for :class:`motile.variables.NodeAppear` variables. Args: - constant (float): A constant cost for each node that starts a track. """ diff --git a/motile/costs/costs.py b/motile/costs/costs.py index e4543a0..3a69d0e 100644 --- a/motile/costs/costs.py +++ b/motile/costs/costs.py @@ -8,6 +8,8 @@ class Costs(ABC): + """A base class for costs that can be added to a solver.""" + @abstractmethod def apply(self, solver: Solver) -> None: """Apply costs to the given solver. Use @@ -15,7 +17,6 @@ def apply(self, solver: Solver) -> None: :func:`motile.Solver.add_variable_cost`. Args: - solver (:class:`Solver`): The solver to create costs for. """ diff --git a/motile/costs/edge_distance.py b/motile/costs/edge_distance.py index 733286e..1283323 100644 --- a/motile/costs/edge_distance.py +++ b/motile/costs/edge_distance.py @@ -17,7 +17,6 @@ class EdgeDistance(Costs): spatial distance of the incident nodes. Args: - position_attributes (tuple of string): The names of the node attributes that correspond to their spatial position, e.g., ``('z', 'y', 'x')``. diff --git a/motile/costs/edge_selection.py b/motile/costs/edge_selection.py index 00b17dd..54f72c2 100644 --- a/motile/costs/edge_selection.py +++ b/motile/costs/edge_selection.py @@ -14,7 +14,6 @@ class EdgeSelection(Costs): """Costs for :class:`motile.variables.EdgeSelected` variables. Args: - weight (float): The weight to apply to the cost given by the ``costs`` attribute of each edge. diff --git a/motile/costs/features.py b/motile/costs/features.py index dcefaea..c39c1f4 100644 --- a/motile/costs/features.py +++ b/motile/costs/features.py @@ -9,6 +9,11 @@ class Features: + """Simple container for features with resizeable dimensions. + + A :class:`motile.Solver` has a :class:`Features` instance. + """ + def __init__(self) -> None: self._values = np.zeros((0, 0), dtype=np.float32) diff --git a/motile/costs/node_selection.py b/motile/costs/node_selection.py index aad8be0..13afeb6 100644 --- a/motile/costs/node_selection.py +++ b/motile/costs/node_selection.py @@ -14,7 +14,6 @@ class NodeSelection(Costs): """Costs for :class:`motile.variables.NodeSelected` variables. Args: - weight (float): The weight to apply to the cost given by the ``costs`` attribute of each node. diff --git a/motile/costs/split.py b/motile/costs/split.py index f0332bc..0944586 100644 --- a/motile/costs/split.py +++ b/motile/costs/split.py @@ -14,7 +14,6 @@ class Split(Costs): """Costs for :class:`motile.variables.NodeSplit` variables. Args: - constant (float): A constant cost for each node that has more than one selected child. diff --git a/motile/costs/weight.py b/motile/costs/weight.py index 7366981..13563f5 100644 --- a/motile/costs/weight.py +++ b/motile/costs/weight.py @@ -4,6 +4,15 @@ class Weight: + """A single Weight with observer/callback pattern on update. + + See also :class:`motile.costs.weights.Weights`. + + Args: + initial_value (float): + The initial value of the weight. + """ + def __init__(self, initial_value: float) -> None: self._value = initial_value self._modify_callbacks: List[Callback] = [] diff --git a/motile/costs/weights.py b/motile/costs/weights.py index d5c8b51..927f51d 100644 --- a/motile/costs/weights.py +++ b/motile/costs/weights.py @@ -11,6 +11,13 @@ class Weights: + """A simple container for weights with observer/callback pattern on update. + + A :class:`motile.Solver` has a :class:`Weights` instance that is used to + store the weights of the model. Changes to the weights can be observed with + ``Solver.weights.register_modify_callback`` + """ + def __init__(self) -> None: self._weights: list[Weight] = [] self._weights_by_name: dict[Hashable, Weight] = {} @@ -18,6 +25,7 @@ def __init__(self) -> None: self._modify_callbacks: list[Callback] = [] def add_weight(self, weight: Weight, name: Hashable) -> None: + """Add a weight to the container.""" self._weight_indices[weight] = len(self._weights) self._weights.append(weight) self._weights_by_name[name] = weight @@ -28,24 +36,36 @@ def add_weight(self, weight: Weight, name: Hashable) -> None: self._notify_modified(None, weight.value) def register_modify_callback(self, callback: Callback) -> None: + """Register ``callback`` to be called when a weight is modified.""" self._modify_callbacks.append(callback) for weight in self._weights: weight.register_modify_callback(callback) def to_ndarray(self) -> np.ndarray: + """Export the weights as a numpy array. + + Note: you can also use np.asarray(weights) to convert a Weights instance. + """ return np.array([w.value for w in self._weights], dtype=np.float32) + def __array__(self) -> np.ndarray: + return self.to_ndarray() + def from_ndarray(self, values: Iterable[float]) -> None: + """Update weights from an iterable of floats.""" for weight, value in zip(self._weights, values): weight.value = value def index_of(self, weight: Weight) -> int: + """Return the index of ``weight`` in this container.""" return self._weight_indices[weight] def __getitem__(self, name: str) -> float: + """Return the value of the weight with the given name.""" return self._weights_by_name[name].value def __setitem__(self, name: str, value: float) -> None: + """Set the value of the weight with the given name.""" self._weights_by_name[name].value = value def _notify_modified(self, old_value: float | None, new_value: float) -> None: diff --git a/motile/data.py b/motile/data.py index 575d3b8..81f6f85 100644 --- a/motile/data.py +++ b/motile/data.py @@ -4,7 +4,7 @@ def arlo_graph_nx() -> nx.DiGraph: - """Create the "Arlo graph", a simple toy graph for testing: + """Create the "Arlo graph", a simple toy graph for testing. x | @@ -16,7 +16,6 @@ def arlo_graph_nx() -> nx.DiGraph: ------------------------------------ t 0 1 2 """ - cells = [ {"id": 0, "t": 0, "x": 101, "score": 1.0}, {"id": 1, "t": 0, "x": 150, "score": 1.0}, @@ -46,11 +45,14 @@ def arlo_graph_nx() -> nx.DiGraph: def arlo_graph() -> TrackGraph: + """Return the "Arlo graph" as a :class:`motile.TrackGraph` instance.""" return TrackGraph(arlo_graph_nx()) def toy_graph_nx() -> nx.DiGraph: - """Create variation of the "Arlo graph", with + """Return variation of the "Arlo graph". + + Relative to arlo_graph, this graph has: - one simple edge modified. - normalized node and edge scores. - sparse ground truth annotations. @@ -92,22 +94,24 @@ def toy_graph_nx() -> nx.DiGraph: def toy_graph() -> TrackGraph: + """Return the `toy_graph_nx` as a :class:`motile.TrackGraph` instance.""" return TrackGraph(toy_graph_nx()) def toy_hypergraph_nx() -> nx.DiGraph: - """Create variation of the "Arlo graph", with one simple - edge modified and one hyperedge added. + """Return variation of `toy_graph` with an edge modified and one hyperedge added. - x - | - | --- 6 - | / / - | 1---3---5 - | / x - | 0---2---4 Hyperedge: (0,(2,3)) + Visually: + + x + | + | --- 6 + | / / + | 1---3---5 + | / x + | 0---2---4 Hyperedge: (0,(2,3)) ------------------------------------ t - 0 1 2 + 0 1 2 """ cells = [ {"id": 0, "t": 0, "x": 1, "score": 0.8, "gt": 1}, @@ -148,4 +152,5 @@ def toy_hypergraph_nx() -> nx.DiGraph: def toy_hypergraph() -> TrackGraph: + """Return the `toy_hypergraph_nx` as a :class:`motile.TrackGraph` instance.""" return TrackGraph(toy_hypergraph_nx()) diff --git a/motile/plot.py b/motile/plot.py index 6b905d3..a22c2e0 100644 --- a/motile/plot.py +++ b/motile/plot.py @@ -42,11 +42,11 @@ def draw_track_graph( width: int = 660, height: int = 400, ) -> go.Figure: - """Create a plotly figure showing the given graph, with time on the x-axis - and node positions on the y-axis. + """Create a plotly figure showing the given graph. - Args: + Time is shown on the x-axis and node positions on the y-axis. + Args: graph (:class:`TrackGraph`): The graph to plot. @@ -73,17 +73,21 @@ def draw_track_graph( node_size (``float``): The size of nodes. - node_color, edge_color (``tuple`` of ``int``): - The RGB color to use for nodes and edges. + node_color (``tuple`` of ``int``): + The RGB color to use for nodes. - width, height (``int``): - The width and height of the plot, in pixels. Default: 700 x 400. + edge_color (``tuple`` of ``int``): + The RGB color to use for edges. - Returns: + width (``int``): + The width of the plot, in pixels. Default: 660. + height (``int``): + The height of the plot, in pixels. Default: 400. + + Returns: ``plotly`` figure showing the graph. """ - if position_attribute is not None and position_func is not None: raise RuntimeError( "Only one of position_attribute and position_func can be given" @@ -163,8 +167,8 @@ def label_edge_func(edge): node_alphas: list[float] = [alpha_node_func(node) for node in graph.nodes] edge_alphas: list[float] = [alpha_edge_func(edge) for edge in graph.edges] # can be a list for different colors per node/edge - node_colors = to_rgba(node_color, node_alphas) - edge_colors = to_rgba(edge_color, edge_alphas) + node_colors = _to_rgba(node_color, node_alphas) + edge_colors = _to_rgba(edge_color, edge_alphas) node_labels = [str(label_node_func(node)) for node in graph.nodes] edge_labels = [str(label_edge_func(edge)) for edge in graph.edges] @@ -278,21 +282,21 @@ def draw_solution( by the given solver. Args: - graph (:class:`TrackGraph`): The graph to plot. solver :class:`Solver`): The solver that was used to find the solution. - args, kwargs: + *args: Pass-through arguments to :func:`draw_track_graph`. - Returns: + **kwargs: + Pass-through keyword arguments to :func:`draw_track_graph`. + Returns: ``plotly`` figure showing the graph. """ - solution = solver.solution if solution is None: raise RuntimeError("Solver has no solution. Call solve() first.") @@ -311,25 +315,26 @@ def edge_alpha_func(edge: EdgeId) -> float: @overload -def to_rgba(color: list[Color], alpha: float | list[float] = 1.0) -> list[str]: +def _to_rgba(color: list[Color], alpha: float | list[float] = 1.0) -> list[str]: ... @overload -def to_rgba(color: Color, alpha: float | list[float] = 1.0) -> str: +def _to_rgba(color: Color, alpha: float | list[float] = 1.0) -> str: ... -def to_rgba( +def _to_rgba( color: Color | list[Color], alpha: float | list[float] = 1.0 ) -> str | list[str]: + """Convert a color to a rgba string.""" if isinstance(color, list): if isinstance(alpha, list): - return [to_rgba(c, a) for c, a in zip(color, alpha)] + return [_to_rgba(c, a) for c, a in zip(color, alpha)] else: # only color is list - return [to_rgba(c, alpha) for c in color] + return [_to_rgba(c, alpha) for c in color] elif isinstance(alpha, list): # only alpha is list - return [to_rgba(color, a) for a in alpha] + return [_to_rgba(color, a) for a in alpha] # we fake alpha by mixing with white(ish) # transparancy is tricky... diff --git a/motile/solver.py b/motile/solver.py index 43c76cf..85ee0bc 100644 --- a/motile/solver.py +++ b/motile/solver.py @@ -25,7 +25,6 @@ class Solver: """Create a solver for a given track graph. Args: - track_graph (:class:`TrackGraph`): The graph of objects to track over time. @@ -63,7 +62,6 @@ def add_costs(self, costs: Costs, name: str | None = None) -> None: """Add linear costs to the value of variables in this solver. Args: - costs (:class:`motile.costs.Costs`): The costs to add. @@ -72,7 +70,6 @@ def add_costs(self, costs: Costs, name: str | None = None) -> None: costs in an unambiguous manner. Defaults to the name of the costs class, if not given. """ - # default name of costs is the class name if name is None: name = type(costs).__name__ @@ -100,11 +97,9 @@ def add_constraints(self, constraints: Constraint) -> None: """Add linear constraints to the solver. Args: - - constraints (:class:`motile.constraints.Constraint`) + constraints (:class:`motile.constraints.Constraint`): The constraints to add. """ - logger.info("Adding %s constraints...", type(constraints).__name__) for constraint in constraints.instantiate(self): @@ -114,7 +109,6 @@ def solve(self, timeout: float = 0.0, num_threads: int = 1) -> ilpy.Solution: """Solve the global optimization problem. Args: - timeout (float): The timeout for the ILP solver, in seconds. Default (0.0) is no timeout. If the solver times out, the best solution encountered @@ -124,12 +118,10 @@ def solve(self, timeout: float = 0.0, num_threads: int = 1) -> ilpy.Solution: The number of threads the ILP solver uses. Returns: - :class:`ilpy.Solution`, a vector of variable values. Use :func:`get_variables` to find the indices of variables in this vector. """ - self.objective = ilpy.Objective(self.num_variables) for i, c in enumerate(self.costs): logger.debug("Setting cost of var %d to %.3f", i, c) @@ -165,17 +157,14 @@ def get_variables(self, cls: type[V]) -> V: created. Args: - cls (type of :class:`motile.variables.Variable`): A subclass of :class:`motile.variables.Variable`. Returns: - A singleton instance of :class:`motile.variables.Variable`, mimicking a dictionary that can be used to look up variable indices by their keys. See :class:`motile.variables.Variable` for details. """ - if cls not in self.variables: self._add_variables(cls) return cast("V", self.variables[cls]) @@ -187,7 +176,6 @@ def add_variable_cost( To be used within implementations of :class:`motile.costs.Costs`. """ - variable_index = index feature_index = self.weights.index_of(weight) self.features.add_feature(variable_index, feature_index, value) @@ -204,7 +192,6 @@ def fit_weights( Updates the weights in the solver object to the found solution. Args: - gt_attribute: Node/edge attribute that marks the ground truth for fitting. diff --git a/motile/ssvm.py b/motile/ssvm.py index 26915cc..43c2c64 100644 --- a/motile/ssvm.py +++ b/motile/ssvm.py @@ -18,6 +18,29 @@ def fit_weights( max_iterations: int | None, eps: float, ) -> np.ndarray: + """Return the optimal weights for the given solver. + + This uses `structsvm.BundleMethod` to fit the weights. + + Args: + solver (Solver): + The solver to fit the weights for. + gt_attribute (str): + Node/edge attribute that marks the ground truth for fitting. + `gt_attribute` is expected to be set to `1` for objects labeled as + ground truth, `0` for objects explicitly labeled as not part of the + ground truth, and `None` or not set for unlabeled objects. + regularizer_weight (float): + The weight of the quadratic regularizer. + max_iterations (int): + Maximum number of gradient steps in the structured SVM. + eps (float): + Convergence threshold. + + Returns: + np.ndarray: + The optimal weights for the given solver. + """ features = solver.features.to_ndarray() mask = np.zeros((solver.num_variables,), dtype=np.float32) diff --git a/motile/track_graph.py b/motile/track_graph.py index 2bc7139..1e2f494 100644 --- a/motile/track_graph.py +++ b/motile/track_graph.py @@ -19,7 +19,6 @@ class TrackGraph: all the methods inherited from :class:`networkx.DiGraph`. Args: - nx_graph (``DiGraph``, optional): A directed networkx graph representing the TrackGraph to be created. @@ -194,15 +193,14 @@ def _hyperedge_nx_node_to_edge_tuple_and_neighbors( def get_frames(self) -> tuple[int | None, int | None]: """Get a tuple ``(t_begin, t_end)`` of the first and last frame - (exclusive) this track graph has nodes for.""" - + (exclusive) this track graph has nodes for. + """ self._update_metadata() return (self.t_begin, self.t_end) def nodes_by_frame(self, t: int) -> list[Hashable]: """Get all nodes in frame ``t``.""" - self._update_metadata() if t not in self._nodes_by_frame: diff --git a/motile/variables/variable.py b/motile/variables/variable.py index c6c2c98..9691574 100644 --- a/motile/variables/variable.py +++ b/motile/variables/variable.py @@ -80,12 +80,10 @@ def instantiate(solver): print(f"Selection indicator of node {node} has index {index}") Args: - solver (:class:`Solver`): The solver instance to create variables for. Returns: - A list of keys (anything that is hashable, e.g., nodes of a graph), one for each variable to create. """ @@ -99,12 +97,10 @@ def instantiate_constraints( are coupled to other variables of the solver. Args: - solver (:class:`Solver`): The solver instance to create variable constraints for. Returns: - A iterable of :class:`ilpy.Constraint` or :class:`ilpy.expressions.Expression.` See :class:`motile.constraints.Constraint` for how to create linear constraints. diff --git a/pyproject.toml b/pyproject.toml index 1793742..0de1706 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,22 @@ select = [ "I", # isort "UP", # pyupgrade "RUF", # ruff specific rules + "D", ] +ignore = [ + "D100", # Missing docstring in public mod + "D104", # Missing docstring in public package + "D105", # Missing docstring in magic method + "D107", # Missing docstring in `__init__` + + "D102", # Missing docstring in public method + "D205", # 1 blank line required between summary line and description + +] +[tool.ruff.pydocstyle] +convention = "google" +[tool.ruff.per-file-ignores] +"tests/*" = ["D"] # https://docs.pytest.org/en/6.2.x/customize.html [tool.pytest.ini_options] From 0b22501e899cad838a49a155fdadd594b9465a85 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 17 May 2023 15:03:12 -0400 Subject: [PATCH 2/7] fix first lines --- .pre-commit-config.yaml | 6 +++--- motile/constraints/expression.py | 5 +++-- motile/constraints/max_children.py | 5 +++-- motile/constraints/max_parents.py | 5 +++-- motile/constraints/pin.py | 3 +-- motile/constraints/select_edge_nodes.py | 5 +++-- motile/costs/costs.py | 5 +++-- motile/costs/edge_distance.py | 5 +++-- motile/plot.py | 4 +++- motile/track_graph.py | 17 ++++++++++------- motile/variables/edge_selected.py | 4 +--- motile/variables/node_appear.py | 6 +++--- motile/variables/node_selected.py | 4 +--- motile/variables/node_split.py | 6 +++--- motile/variables/variable.py | 5 +++-- pyproject.toml | 2 +- 16 files changed, 47 insertions(+), 40 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 321f9da..5fce103 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,18 +5,18 @@ ci: repos: - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.254 + rev: v0.0.267 hooks: - id: ruff args: [--fix] - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 23.3.0 hooks: - id: black - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.1.1 + rev: v1.3.0 hooks: - id: mypy files: "^motile/" diff --git a/motile/constraints/expression.py b/motile/constraints/expression.py index a127d41..2ce3da1 100644 --- a/motile/constraints/expression.py +++ b/motile/constraints/expression.py @@ -17,8 +17,9 @@ class ExpressionConstraint(Constraint): - """Enforces the selection of nodes/edges based on an expression evaluated - with the node/edge dict as a namespace. + """Enforcew selection of nodes/edges based on an expression. + + The expression string is evaluated with the node/edge dict as a namespace. This is a powerful general constraint that allows you to select nodes/edges based on any combination of node/edge attributes. The `expression` string is evaluated for diff --git a/motile/constraints/max_children.py b/motile/constraints/max_children.py index f4bd828..b6b7bb6 100644 --- a/motile/constraints/max_children.py +++ b/motile/constraints/max_children.py @@ -12,8 +12,9 @@ class MaxChildren(Constraint): - r"""Ensures that every selected node has no more than ``max_children`` - selected edges to the next frame. + r"""Ensures that every selected node has no more than ``max_children``. + + Where a "child" is a selected edges to the next frame. Adds the following linear constraint for each node :math:`v`: diff --git a/motile/constraints/max_parents.py b/motile/constraints/max_parents.py index 02e1d3d..5aa6674 100644 --- a/motile/constraints/max_parents.py +++ b/motile/constraints/max_parents.py @@ -12,8 +12,9 @@ class MaxParents(Constraint): - r"""Ensures that every selected node has no more than ``max_parents`` - selected edges to the previous frame. + r"""Ensures that every selected node has no more than ``max_parents``. + + Where a "parent" is defined as an incoming selected edge from the previous frame. Adds the following linear constraint for each node :math:`v`: diff --git a/motile/constraints/pin.py b/motile/constraints/pin.py index f3e1491..b105763 100644 --- a/motile/constraints/pin.py +++ b/motile/constraints/pin.py @@ -4,8 +4,7 @@ class Pin(ExpressionConstraint): - """Enforces the selection of certain nodes and edges based on the value of - a given attribute. + """Enforces the selection of nodes/edges based on truthiness of a given attribute. Every node or edge that has the given attribute will be selected if the attribute value is ``True`` (and not selected if the attribute value is diff --git a/motile/constraints/select_edge_nodes.py b/motile/constraints/select_edge_nodes.py index a9c6488..00d6b3c 100644 --- a/motile/constraints/select_edge_nodes.py +++ b/motile/constraints/select_edge_nodes.py @@ -12,8 +12,9 @@ class SelectEdgeNodes(Constraint): - r"""Ensures that if an edge :math:`(u, v)` is selected, :math:`u` and - :math:`v` have to be selected as well. + r"""Ensures that if an edge is selected, its nodes are selected as well. + + If :math:`(u, v)` is selected, :math:`u` and :math:`v` have to be selected as well. Adds the following linear constraint for each edge :math:`e = (u,v)`: diff --git a/motile/costs/costs.py b/motile/costs/costs.py index 3a69d0e..b6339b9 100644 --- a/motile/costs/costs.py +++ b/motile/costs/costs.py @@ -12,8 +12,9 @@ class Costs(ABC): @abstractmethod def apply(self, solver: Solver) -> None: - """Apply costs to the given solver. Use - :func:`motile.Solver.get_variables` and + """Apply costs to the given solver. + + Use :func:`motile.Solver.get_variables` and :func:`motile.Solver.add_variable_cost`. Args: diff --git a/motile/costs/edge_distance.py b/motile/costs/edge_distance.py index 1283323..2712627 100644 --- a/motile/costs/edge_distance.py +++ b/motile/costs/edge_distance.py @@ -13,8 +13,9 @@ class EdgeDistance(Costs): - """Costs for :class:`motile.variables.EdgeSelected` variables, based on the - spatial distance of the incident nodes. + """Costs for :class:`motile.variables.EdgeSelected` variables. + + Costs are based on the spatial distance of the incident nodes. Args: position_attributes (tuple of string): diff --git a/motile/plot.py b/motile/plot.py index a22c2e0..f229a45 100644 --- a/motile/plot.py +++ b/motile/plot.py @@ -278,7 +278,9 @@ def label_edge_func(edge): def draw_solution( graph: TrackGraph, solver: Solver, *args: Any, **kwargs: Any ) -> go.Figure: - """Wrapper around :func:`draw_track_graph` highlighting the solution found + """Draw ``graph`` with the current ``solver.solution`` highlighted. + + This is a wrapper around :func:`draw_track_graph` highlighting the solution found by the given solver. Args: diff --git a/motile/track_graph.py b/motile/track_graph.py index 1e2f494..06308cd 100644 --- a/motile/track_graph.py +++ b/motile/track_graph.py @@ -12,8 +12,9 @@ class TrackGraph: - """A :class:`networkx.DiGraph` of objects with positions in time and space, - and inter-frame edges between them. + """A graph of nodes placed in time & space, and edges connecting them across time. + + This wraps a :class:`networkx.DiGraph` object. Provides a few convenience methods for time series graphs in addition to all the methods inherited from :class:`networkx.DiGraph`. @@ -70,8 +71,7 @@ def add_edge(self, edge_id: EdgeId, data: GraphObject) -> None: self.edges[edge_id] = data def add_from_nx_graph(self, nx_graph: DiGraph) -> None: - """Adds the TrackGraph represented by the given ``nx_graph`` to the - existing TrackGraph. + """Add nodes/edges from ``nx_graph`` to this TrackGraph. Hyperedges are represented by nodes in the ``nx_graph`` that do not have the ``frame_attribute`` property. All 'regular' nodes connected to such a hyperedge @@ -140,7 +140,9 @@ def nodes_of(self, edge: EdgeId | int) -> Iterator[int]: yield edge def _is_hyperedge_nx_node(self, nx_graph: DiGraph, nx_node: Any) -> bool: - """Checks if the given networkx node in the given directed networkx graph + """Return ``True`` if ``nx_node`` is a hyperedge node in ``nx_graph``. + + Checks if the given networkx node in the given directed networkx graph represents an hyperedge. This boils down to checking if the node does not have the ``frame_attribute`` set. @@ -192,8 +194,9 @@ 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]: - """Get a tuple ``(t_begin, t_end)`` of the first and last frame - (exclusive) this track graph has nodes for. + """Return tuple with first and last (exclusive) frame this graph has nodes for. + + Returns ``(t_begin, t_end)`` where t_end is exclusive. """ self._update_metadata() diff --git a/motile/variables/edge_selected.py b/motile/variables/edge_selected.py index 3f8fbc7..e3e0e06 100644 --- a/motile/variables/edge_selected.py +++ b/motile/variables/edge_selected.py @@ -10,9 +10,7 @@ class EdgeSelected(Variable["EdgeId"]): - """A binary variable for each edge that indicates whether the edge is part - of the solution or not. - """ + """Binary variable indicates whether an edge is part of the solution or not.""" @staticmethod def instantiate(solver: Solver) -> Collection[EdgeId]: diff --git a/motile/variables/node_appear.py b/motile/variables/node_appear.py index f513114..cfbca50 100644 --- a/motile/variables/node_appear.py +++ b/motile/variables/node_appear.py @@ -14,9 +14,9 @@ class NodeAppear(Variable["NodeId"]): - r"""A binary variable for each node that indicates whether the node is the - start of a track (i.e., the node is selected and has no selected incoming - edges). + r"""Binary variable indicating whether a node is the start of a track. + + (i.e., the node is selected and has no selected incoming edges). This variable is coupled to the node and edge selection variables through the following linear constraints: diff --git a/motile/variables/node_selected.py b/motile/variables/node_selected.py index 01822ab..eb4dd1a 100644 --- a/motile/variables/node_selected.py +++ b/motile/variables/node_selected.py @@ -10,9 +10,7 @@ class NodeSelected(Variable["NodeId"]): - """A binary variable for each node that indicates whether the node is part - of the solution or not. - """ + """Binary variable indicating whether a node is part of the solution or not.""" @staticmethod def instantiate(solver: Solver) -> Collection[NodeId]: diff --git a/motile/variables/node_split.py b/motile/variables/node_split.py index 8a68d13..82b8823 100644 --- a/motile/variables/node_split.py +++ b/motile/variables/node_split.py @@ -13,9 +13,9 @@ class NodeSplit(Variable): - r"""A binary variable for each node that indicates whether the node has - more than one children (i.e., the node is selected and has more than one - selected outgoing edge). + r"""Binary variable indicating whether a node has more than one child. + + (i.e., the node is selected and has more than one selected outgoing edge). This variable is coupled to the edge selection variables through the following linear constraints: diff --git a/motile/variables/variable.py b/motile/variables/variable.py index 9691574..046f0af 100644 --- a/motile/variables/variable.py +++ b/motile/variables/variable.py @@ -93,8 +93,9 @@ def instantiate(solver): def instantiate_constraints( solver: Solver, ) -> Iterable[ilpy.Constraint | ilpy.Expression]: - """Add linear constraints to the solver to ensure that these variables - are coupled to other variables of the solver. + """Add constraints for this variable to the solver. + + This ensures that these variables are coupled to other variables of the solver. Args: solver (:class:`Solver`): diff --git a/pyproject.toml b/pyproject.toml index 0de1706..c0dc3c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ ignore = [ "D107", # Missing docstring in `__init__` "D102", # Missing docstring in public method - "D205", # 1 blank line required between summary line and description + # "D205", # 1 blank line required between summary line and description ] [tool.ruff.pydocstyle] From 48fdf882e05deb382fe4a7917b29561a8ca18aff Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 17 May 2023 17:00:35 -0400 Subject: [PATCH 3/7] docs: many manual updates --- Makefile | 2 +- docs/source/api.rst | 61 ++++++++++++++++++++++++- docs/source/conf.py | 16 +++++++ motile/_types.py | 8 ++-- motile/constraints/constraint.py | 4 +- motile/constraints/expression.py | 38 +++++++-------- motile/constraints/max_children.py | 4 +- motile/constraints/max_parents.py | 4 +- motile/constraints/pin.py | 4 +- motile/constraints/select_edge_nodes.py | 6 ++- motile/costs/appear.py | 4 +- motile/costs/costs.py | 4 +- motile/costs/edge_distance.py | 6 +-- motile/costs/edge_selection.py | 12 ++--- motile/costs/features.py | 29 +++++++++++- motile/costs/node_selection.py | 12 ++--- motile/costs/split.py | 4 +- motile/costs/weight.py | 10 +++- motile/costs/weights.py | 19 ++++++-- motile/plot.py | 22 ++++----- motile/solver.py | 43 +++++++++-------- motile/track_graph.py | 49 ++++++++++---------- motile/variables/node_appear.py | 8 ++-- motile/variables/node_split.py | 4 +- motile/variables/variable.py | 14 +++--- 25 files changed, 259 insertions(+), 128 deletions(-) diff --git a/Makefile b/Makefile index 9b8ab7f..6281203 100644 --- a/Makefile +++ b/Makefile @@ -22,4 +22,4 @@ docs: .PHONY: docs-watch docs-watch: pip install sphinx-autobuild - sphinx-autobuild docs/source docs/_build/html + sphinx-autobuild docs/source docs/_build/html --watch motile --watch docs/source --open-browser diff --git a/docs/source/api.rst b/docs/source/api.rst index 804e5fa..19797a4 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -6,11 +6,36 @@ API Reference .. automodule:: motile :noindex: +.. admonition:: A note on ``NodeId`` and ``EdgeId`` types + :class: note, dropdown + + The following types are used throughout the docs + + - All objects in a graph (both ``Nodes`` and ``Edges``) are represented as + dictionaries mapping string attribute names to value. For example, a node + might be ``{ "id": 1, "x": 0.5, "y": 0.5, "t": 0 }`` + + ``GraphObject: TypeAlias = Mapping[str, Any]`` + + - Node IDs may be integers, or a "meta-node" as a tuple of integers. + + ``NodeId: TypeAlias = Union[int, tuple[int, ...]]`` + + - Edges IDs are tuples of ``NodeId``. + + ``EdgeId: TypeAlias = tuple[NodeId, ...]`` + + - ``(0, 1)`` is an edge from node 0 to node 1. + - ``((0, 1), 2)`` is a hyperedge from nodes 0 and 1 to node 2 (i.e. a merge). + - ``((0,), (1, 2))`` is a hyperedge from node 0 to nodes 1 and 2 (i.e. a split). + + + Track Graph ----------- .. autoclass:: TrackGraph - :members: get_frames, nodes_by_frame + :members: Solver ------ @@ -78,6 +103,28 @@ EdgeDistance ^^^^^^^^^^^^ .. autoclass:: EdgeDistance +Features +-------- + + .. autoclass:: Features + :members: + + +Weights +------- + +Weight +^^^^^^ + + .. autoclass:: Weight + :members: + +Weights +^^^^^^^ + + .. autoclass:: Weights + :members: + Constraints ----------- @@ -93,15 +140,27 @@ The following lists all constraints that are already implemented in ``motile``. MaxChildren ^^^^^^^^^^^ .. autoclass:: MaxChildren + :show-inheritance: MaxParents ^^^^^^^^^^ .. autoclass:: MaxParents + :show-inheritance: + +ExpressionConstraint +^^^^^^^^^^^^^^^^^^^^ + .. autoclass:: ExpressionConstraint + :show-inheritance: Pin ^^^ .. autoclass:: Pin + :show-inheritance: SelectEdgeNodes (internal use) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. autoclass:: SelectEdgeNodes + :show-inheritance: + + + diff --git a/docs/source/conf.py b/docs/source/conf.py index 244c46e..105db38 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -28,11 +28,14 @@ extensions = [ "jupyter_sphinx", "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx_autodoc_typehints", "sphinx.ext.githubpages", "sphinx.ext.mathjax", "sphinx_rtd_theme", "sphinx_togglebutton", "sphinxcontrib.jquery", + "sphinx.ext.intersphinx", ] templates_path = ["_templates"] @@ -52,3 +55,16 @@ togglebutton_hint_hide = "" pygments_style = "lovelace" + +# Napoleon settings +napoleon_google_docstring = True + +autodoc_type_aliases = { + "EdgeId": "motile._types.EdgeId", +} + +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "networkx": ("https://networkx.org/documentation/stable/", None), + "numpy": ("https://numpy.org/doc/stable/", None), +} diff --git a/motile/_types.py b/motile/_types.py index c75db1d..934be3e 100644 --- a/motile/_types.py +++ b/motile/_types.py @@ -5,12 +5,12 @@ # Nodes are represented as integers, or a "meta-node" tuple of integers. NodeId: TypeAlias = Union[int, tuple[int, ...]] -# objects in the graph are represented as dicts -# eg. { "id": 1, "x": 0.5, "y": 0.5, "t": 0 } -GraphObject: TypeAlias = Mapping[str, Any] - # Edges are represented as tuples of NodeId. # (0, 1) is an edge from node 0 to node 1. # ((0, 1), 2) is a hyperedge from nodes 0 and 1 to node 2 (i.e. a merge). # ((0,), (1, 2)) is a hyperedge from node 0 to nodes 1 and 2 (i.e. a split). EdgeId: TypeAlias = tuple[NodeId, ...] + +# objects in the graph are represented as dicts +# eg. { "id": 1, "x": 0.5, "y": 0.5, "t": 0 } +GraphObject: TypeAlias = Mapping[str, Any] diff --git a/motile/constraints/constraint.py b/motile/constraints/constraint.py index ea5104c..3b72095 100644 --- a/motile/constraints/constraint.py +++ b/motile/constraints/constraint.py @@ -19,8 +19,8 @@ def instantiate( """Create and return specific linear constraints for the given solver. Args: - solver (:class:`Solver`): - The solver instance to create linear constraints for. + solver: + The :class:`~motile.Solver` instance to create linear constraints for. Returns: An iterable of :class:`ilpy.Constraint`. diff --git a/motile/constraints/expression.py b/motile/constraints/expression.py index 2ce3da1..78fffe7 100644 --- a/motile/constraints/expression.py +++ b/motile/constraints/expression.py @@ -17,7 +17,7 @@ class ExpressionConstraint(Constraint): - """Enforcew selection of nodes/edges based on an expression. + """Enforce selection of nodes/edges based on an expression. The expression string is evaluated with the node/edge dict as a namespace. @@ -30,32 +30,34 @@ class ExpressionConstraint(Constraint): This takes advantaged of python's `eval` function, like this: - ```python - my_expression = "some_attribute == True" - eval(my_expression, None, {"some_attribute": True}) # returns True (select) - eval(my_expression, None, {"some_attribute": False}) # returns False (exclude) - eval(my_expression, None, {}) # raises NameError (do nothing) - ``` + .. code-block:: python + + my_expression = "some_attribute == True" + eval(my_expression, None, {"some_attribute": True}) # returns True (select) + eval(my_expression, None, {"some_attribute": False}) # returns False (exclude) + eval(my_expression, None, {}) # raises NameError (do nothing) Args: - expression (string): + expression: An expression to evaluate for each node/edge. The expression must evaluate to a boolean value. The expression can use any names of node/edge attributes as variables. - eval_nodes (bool): + eval_nodes: Whether to evaluate the expression for nodes. By default, True. - eval_edges (bool): + eval_edges: Whether to evaluate the expression for edges. By default, True. Example: - If the nodes of a graph are: - cells = [ - {"id": 0, "t": 0, "color": "red", "score": 1.0}, - {"id": 1, "t": 0, "color": "green", "score": 1.0}, - {"id": 2, "t": 1, "color": "blue", "score": 1.0}, - ] - - Then the following constraint will select node 0: + If the nodes of a graph are: + + >>> cells = [ + ... {"id": 0, "t": 0, "color": "red", "score": 1.0}, + ... {"id": 1, "t": 0, "color": "green", "score": 1.0}, + ... {"id": 2, "t": 1, "color": "blue", "score": 1.0}, + ... ] + + Then the following constraint will select node 0: + >>> expr = "t == 0 and color != 'green'" >>> solver.add_constraints(ExpressionConstraint(expr)) """ diff --git a/motile/constraints/max_children.py b/motile/constraints/max_children.py index b6b7bb6..ced891d 100644 --- a/motile/constraints/max_children.py +++ b/motile/constraints/max_children.py @@ -20,10 +20,10 @@ class MaxChildren(Constraint): .. math:: - \sum_{e \in \\text{out_edges}(v)} x_e \leq \\text{max_children} + \sum_{e \in \text{out_edges}(v)} x_e \leq \text{max_children} Args: - max_children (int): + max_children: The maximum number of children allowed. """ diff --git a/motile/constraints/max_parents.py b/motile/constraints/max_parents.py index 5aa6674..3c26c9b 100644 --- a/motile/constraints/max_parents.py +++ b/motile/constraints/max_parents.py @@ -20,10 +20,10 @@ class MaxParents(Constraint): .. math:: - \sum_{e \in \\text{in_edges}(v)} x_e \leq \\text{max_parents} + \sum_{e \in \text{in_edges}(v)} x_e \leq \text{max_parents} Args: - max_parents (int): + max_parents: The maximum number of parents allowed. """ diff --git a/motile/constraints/pin.py b/motile/constraints/pin.py index b105763..517505d 100644 --- a/motile/constraints/pin.py +++ b/motile/constraints/pin.py @@ -4,7 +4,7 @@ class Pin(ExpressionConstraint): - """Enforces the selection of nodes/edges based on truthiness of a given attribute. + """Enforces the selection of nodes/edges based on truthiness of a given attribute. Every node or edge that has the given attribute will be selected if the attribute value is ``True`` (and not selected if the attribute value is @@ -16,7 +16,7 @@ class Pin(ExpressionConstraint): edges. Args: - attribute (string): + attribute: The name of the node/edge attribute to use. """ diff --git a/motile/constraints/select_edge_nodes.py b/motile/constraints/select_edge_nodes.py index 00d6b3c..90e2269 100644 --- a/motile/constraints/select_edge_nodes.py +++ b/motile/constraints/select_edge_nodes.py @@ -14,6 +14,10 @@ class SelectEdgeNodes(Constraint): r"""Ensures that if an edge is selected, its nodes are selected as well. + .. NOTE:: + + This class is for internal use. + If :math:`(u, v)` is selected, :math:`u` and :math:`v` have to be selected as well. Adds the following linear constraint for each edge :math:`e = (u,v)`: @@ -22,7 +26,7 @@ class SelectEdgeNodes(Constraint): 2 x_e - x_u - x_v \leq 0 - This constraint will be added by default to any :class:`Solver` instance. + This constraint will be added by default to any :class:`~motile.Solver` instance. """ def instantiate(self, solver: Solver) -> Iterable[Expression]: diff --git a/motile/costs/appear.py b/motile/costs/appear.py index e7dea31..b40ad00 100644 --- a/motile/costs/appear.py +++ b/motile/costs/appear.py @@ -11,10 +11,10 @@ class Appear(Costs): - """Costs for :class:`motile.variables.NodeAppear` variables. + """Costs for :class:`~motile.variables.NodeAppear` variables. Args: - constant (float): + constant: A constant cost for each node that starts a track. """ diff --git a/motile/costs/costs.py b/motile/costs/costs.py index b6339b9..0f31067 100644 --- a/motile/costs/costs.py +++ b/motile/costs/costs.py @@ -18,7 +18,7 @@ def apply(self, solver: Solver) -> None: :func:`motile.Solver.add_variable_cost`. Args: - solver (:class:`Solver`): - The solver to create costs for. + solver: + The :class:`~motile.Solver` to create costs for. """ pass diff --git a/motile/costs/edge_distance.py b/motile/costs/edge_distance.py index 2712627..3e5dce2 100644 --- a/motile/costs/edge_distance.py +++ b/motile/costs/edge_distance.py @@ -13,16 +13,16 @@ class EdgeDistance(Costs): - """Costs for :class:`motile.variables.EdgeSelected` variables. + """Costs for :class:`~motile.variables.EdgeSelected` variables. Costs are based on the spatial distance of the incident nodes. Args: - position_attributes (tuple of string): + position_attributes: The names of the node attributes that correspond to their spatial position, e.g., ``('z', 'y', 'x')``. - weight (float): + weight: The weight to apply to the distance to convert it into a cost. """ diff --git a/motile/costs/edge_selection.py b/motile/costs/edge_selection.py index 54f72c2..9845d96 100644 --- a/motile/costs/edge_selection.py +++ b/motile/costs/edge_selection.py @@ -11,19 +11,19 @@ class EdgeSelection(Costs): - """Costs for :class:`motile.variables.EdgeSelected` variables. + """Costs for :class:`~motile.variables.EdgeSelected` variables. Args: - weight (float): + weight: The weight to apply to the cost given by the ``costs`` attribute of each edge. - attribute (string): + attribute: The name of the edge attribute to use to look up costs. Default is - ``costs``. + ``'costs'``. - constant (float): - A constant cost for each selected edge. + constant: + A constant cost for each selected edge. Default is ``0.0``. """ def __init__( diff --git a/motile/costs/features.py b/motile/costs/features.py index c39c1f4..e429c6f 100644 --- a/motile/costs/features.py +++ b/motile/costs/features.py @@ -11,7 +11,7 @@ class Features: """Simple container for features with resizeable dimensions. - A :class:`motile.Solver` has a :class:`Features` instance. + A :class:`~motile.Solver` has a :class:`Features` instance. """ def __init__(self) -> None: @@ -20,6 +20,16 @@ def __init__(self) -> None: def resize( self, num_variables: int | None = None, num_features: int | None = None ) -> None: + """Resize the feature matrix. + + Args: + num_variables: + The number of variables to resize to. If None, the number of + variables is not changed. + num_features: + The number of features to resize to. If None, the number of + features is not changed. + """ if num_variables is None: num_variables = self._values.shape[0] if num_features is None: @@ -45,6 +55,16 @@ def _increase_features(self, num_features: int) -> None: def add_feature( self, variable_index: int | ilpy.Variable, feature_index: int, value: float ) -> None: + """Add a value to a feature. + + Args: + variable_index: + The index of the variable to add the value to. + feature_index: + The index of the feature to add the value to. + value: + The value to add. + """ num_variables, num_features = self._values.shape variable_index = int(variable_index) @@ -57,10 +77,17 @@ def add_feature( self._values[variable_index, feature_index] += value def to_ndarray(self) -> np.ndarray: + """Export the feature matrix as a numpy array. + + Note: you can also use ``np.asarray(features)``. + """ # _values is already an ndarray, but this might change in the future # Note: consider implementing return self._values + def __array__(self) -> np.ndarray: + return self.to_ndarray() + def __repr__(self) -> str: r = f"array of shape={self._values.shape}" if self._values.size > 0: diff --git a/motile/costs/node_selection.py b/motile/costs/node_selection.py index 13afeb6..2d16b04 100644 --- a/motile/costs/node_selection.py +++ b/motile/costs/node_selection.py @@ -11,19 +11,19 @@ class NodeSelection(Costs): - """Costs for :class:`motile.variables.NodeSelected` variables. + """Costs for :class:`~motile.variables.NodeSelected` variables. Args: - weight (float): + weight: The weight to apply to the cost given by the ``costs`` attribute of each node. - attribute (string): + attribute: The name of the node attribute to use to look up costs. Default is - ``costs``. + ``'costs'``. - constant (float): - A constant cost for each selected node. + constant: + A constant cost for each selected node. Default is ``0.0``. """ def __init__( diff --git a/motile/costs/split.py b/motile/costs/split.py index 0944586..1fba84e 100644 --- a/motile/costs/split.py +++ b/motile/costs/split.py @@ -11,10 +11,10 @@ class Split(Costs): - """Costs for :class:`motile.variables.NodeSplit` variables. + """Costs for :class:`~motile.variables.NodeSplit` variables. Args: - constant (float): + constant: A constant cost for each node that has more than one selected child. """ diff --git a/motile/costs/weight.py b/motile/costs/weight.py index 13563f5..7e65fed 100644 --- a/motile/costs/weight.py +++ b/motile/costs/weight.py @@ -9,7 +9,7 @@ class Weight: See also :class:`motile.costs.weights.Weights`. Args: - initial_value (float): + initial_value: The initial value of the weight. """ @@ -19,15 +19,23 @@ def __init__(self, initial_value: float) -> None: @property def value(self) -> float: + """Return the value of this weight.""" return self._value @value.setter def value(self, new_value: float) -> None: + """Set the value of this weight.""" old_value = self._value self._value = new_value self._notify_modified(old_value, new_value) def register_modify_callback(self, callback: Callback) -> None: + """Register a ``callback`` to be called when the weight is modified. + + Args: + callback: + A function that takes two arguments: the old value and the new value. + """ self._modify_callbacks.append(callback) def _notify_modified(self, old_value: float, new_value: float) -> None: diff --git a/motile/costs/weights.py b/motile/costs/weights.py index 927f51d..836b310 100644 --- a/motile/costs/weights.py +++ b/motile/costs/weights.py @@ -25,7 +25,14 @@ def __init__(self) -> None: self._modify_callbacks: list[Callback] = [] def add_weight(self, weight: Weight, name: Hashable) -> None: - """Add a weight to the container.""" + """Add a weight to the container. + + Args: + weight: + The :class:`~motile.costs.Weight` to add. + name: + The name of the weight. + """ self._weight_indices[weight] = len(self._weights) self._weights.append(weight) self._weights_by_name[name] = weight @@ -36,7 +43,13 @@ def add_weight(self, weight: Weight, name: Hashable) -> None: self._notify_modified(None, weight.value) def register_modify_callback(self, callback: Callback) -> None: - """Register ``callback`` to be called when a weight is modified.""" + """Register ``callback`` to be called when a weight is modified. + + Args: + callback: + A function that takes two arguments: the old value (which may be + ``None``) and the new value. + """ self._modify_callbacks.append(callback) for weight in self._weights: weight.register_modify_callback(callback) @@ -44,7 +57,7 @@ def register_modify_callback(self, callback: Callback) -> None: def to_ndarray(self) -> np.ndarray: """Export the weights as a numpy array. - Note: you can also use np.asarray(weights) to convert a Weights instance. + Note: you can also use ``np.asarray(weights)``. """ return np.array([w.value for w in self._weights], dtype=np.float32) diff --git a/motile/plot.py b/motile/plot.py index f229a45..09af318 100644 --- a/motile/plot.py +++ b/motile/plot.py @@ -47,46 +47,46 @@ def draw_track_graph( Time is shown on the x-axis and node positions on the y-axis. Args: - graph (:class:`TrackGraph`): - The graph to plot. + graph: + The :class:`~motile.TrackGraph` to plot. - position_attribute (``string``): + position_attribute (str): The name of the node attribute to use to place nodes on the y-axis. position_func (callable): A function returning the position of a given node on the y-axis. - alpha_attribute (``string``): + alpha_attribute (str): The name of a node or edge attribute to use for the transparency. alpha_func (callable): A function returning the alpha value to use for each node or edge. Can be a tuple for node and edge functions, respectively. - label_attribute (``string``): + label_attribute (str): The name of a node or edge attribute to use for a text label. label_func (callable): A function returning the label to use for each node or edge. Can be a tuple for node and edge functions, respectively. - node_size (``float``): + node_size (float): The size of nodes. - node_color (``tuple`` of ``int``): + node_color (tuple[int, ...]): The RGB color to use for nodes. - edge_color (``tuple`` of ``int``): + edge_color (tuple[int, ...]): The RGB color to use for edges. - width (``int``): + width (int): The width of the plot, in pixels. Default: 660. - height (``int``): + height (int): The height of the plot, in pixels. Default: 400. Returns: - ``plotly`` figure showing the graph. + :class:`plotly.graph_objects.Figure` showing the graph. """ if position_attribute is not None and position_func is not None: raise RuntimeError( diff --git a/motile/solver.py b/motile/solver.py index 85ee0bc..6c9328a 100644 --- a/motile/solver.py +++ b/motile/solver.py @@ -25,10 +25,10 @@ class Solver: """Create a solver for a given track graph. Args: - track_graph (:class:`TrackGraph`): - The graph of objects to track over time. + track_graph: + The :class:`~motile.TrackGraph` of objects to track over time. - skip_core_constraints (bool, default=False): + skip_core_constraints (:obj:`bool`, default=False): If true, add no constraints to the solver at all. Otherwise, core constraints that ensure consistencies between selected nodes and edges are added. @@ -62,10 +62,10 @@ def add_costs(self, costs: Costs, name: str | None = None) -> None: """Add linear costs to the value of variables in this solver. Args: - costs (:class:`motile.costs.Costs`): - The costs to add. + costs: + The costs to add. An instance of :class:`~motile.costs.Costs`. - name (``string``): + name: An optional name of the costs, used to refer to weights of costs in an unambiguous manner. Defaults to the name of the costs class, if not given. @@ -97,8 +97,8 @@ def add_constraints(self, constraints: Constraint) -> None: """Add linear constraints to the solver. Args: - constraints (:class:`motile.constraints.Constraint`): - The constraints to add. + constraints: + The :class:`~motile.constraints.Constraint` to add. """ logger.info("Adding %s constraints...", type(constraints).__name__) @@ -109,12 +109,12 @@ def solve(self, timeout: float = 0.0, num_threads: int = 1) -> ilpy.Solution: """Solve the global optimization problem. Args: - timeout (float): + timeout: The timeout for the ILP solver, in seconds. Default (0.0) is no timeout. If the solver times out, the best solution encountered so far is returned (if any has been found at all). - num_threads (int): + num_threads: The number of threads the ILP solver uses. Returns: @@ -161,9 +161,10 @@ def get_variables(self, cls: type[V]) -> V: A subclass of :class:`motile.variables.Variable`. Returns: - A singleton instance of :class:`motile.variables.Variable`, - mimicking a dictionary that can be used to look up variable indices - by their keys. See :class:`motile.variables.Variable` for details. + A singleton instance of :class:`~motile.variables.Variable` (of whatever + type was passed in as ``cls``), mimicking a dictionary that can be used to + look up variable indices by their keys. See + :class:`~motile.variables.Variable` for details. """ if cls not in self.variables: self._add_variables(cls) @@ -191,27 +192,24 @@ def fit_weights( Updates the weights in the solver object to the found solution. + See https://github.com/funkelab/structsvm for details. + Args: gt_attribute: - Node/edge attribute that marks the ground truth for fitting. - `gt_attribute` is expected to be set to + `gt_attribute` is expected to be set to: - - `1` for objects labaled as ground truth. - - `0` for objects explicitly labeled as not part of the - ground truth. - - `None` or not set for unlabeled objects. + - ``1`` for objects labaled as ground truth. + - ``0`` for objects explicitly labeled as not part of the ground truth. + - ``None`` or not set for unlabeled objects. regularizer_weight: - The weight of the quadratic regularizer. max_iterations: - Maximum number of gradient steps in the structured SVM. eps: - Convergence threshold. """ optimal_weights = fit_weights( @@ -221,6 +219,7 @@ def fit_weights( @property def costs(self) -> np.ndarray: + """Returns the costs as a :class:`numpy.ndarray`.""" if self._weights_changed: self._compute_costs() self._weights_changed = False diff --git a/motile/track_graph.py b/motile/track_graph.py index 06308cd..058828e 100644 --- a/motile/track_graph.py +++ b/motile/track_graph.py @@ -6,28 +6,30 @@ logger = logging.getLogger(__name__) if TYPE_CHECKING: - from networkx.classes import DiGraph + import networkx from motile._types import EdgeId, GraphObject, NodeId class TrackGraph: - """A graph of nodes placed in time & space, and edges connecting them across time. + """A graph of nodes in time & space, with edges connecting them across time. This wraps a :class:`networkx.DiGraph` object. + Both ``nodes`` and ``edges`` are represented by a dictionary of properties. + Provides a few convenience methods for time series graphs in addition to all the methods inherited from :class:`networkx.DiGraph`. Args: - nx_graph (``DiGraph``, optional): + nx_graph: A directed networkx graph representing the TrackGraph to be created. Hyperedges are represented by networkx nodes that do not have the ``frame_attribute`` and are connected to nodes that do have this attribute. - frame_attribute (``string``, optional): + frame_attribute: The name of the node attribute that corresponds to the frame (i.e., the time dimension) of the object. Defaults to ``'t'``. @@ -35,7 +37,7 @@ class TrackGraph: def __init__( self, - nx_graph: DiGraph = None, + nx_graph: networkx.DiGraph | None = None, frame_attribute: str = "t", ): self.frame_attribute = frame_attribute @@ -55,8 +57,8 @@ def add_node(self, node_id: NodeId, data: GraphObject) -> None: """Adds a new node to this TrackGraph. Args: - node_id (int | tuple[int, ...]): the node to be added. - data (dict[Hashable, Any]): all properties associated to the added node. + node_id: the node to be added. + data: all properties associated to the added node. """ self.nodes[node_id] = data @@ -64,13 +66,13 @@ def add_edge(self, edge_id: EdgeId, data: GraphObject) -> None: """Adds an edge to this TrackGraph. Args: - edge_id (EdgeId): an ``EdgeId`` (tuple of NodeIds) defining the edge + edge_id: an ``EdgeId`` (tuple of NodeIds) defining the edge (or hyperedge) to be added. - data (dict[Hashable, Any]): all properties associated to the added edge. + data: all properties associated to the added edge. """ self.edges[edge_id] = data - def add_from_nx_graph(self, nx_graph: DiGraph) -> None: + def add_from_nx_graph(self, nx_graph: networkx.DiGraph) -> None: """Add nodes/edges from ``nx_graph`` to this TrackGraph. Hyperedges are represented by nodes in the ``nx_graph`` that do not have the @@ -78,12 +80,13 @@ def add_from_nx_graph(self, nx_graph: DiGraph) -> None: node will be added as a hyperedge. Args: - nx_graph (networkx.DiGraph): + nx_graph: A directed networkx graph representing a TrackGraph to be added. Hyperedges are represented by networkx nodes that do not have the ``frame_attribute`` and are connected to nodes that do have this attribute. + Duplicate nodes and edges will not be added again but new attributes associated to nodes and edges added. If attributes of existing nodes or edges do already exist, the values set in the given ``nx_graph`` @@ -128,10 +131,10 @@ def nodes_of(self, edge: EdgeId | int) -> Iterator[int]: """Returns an ``Iterator`` of node id's that are incident to the given edge. Args: - edge (EdgeId | int): an edge of this TrackGraph. + edge: an edge of this TrackGraph. Yields: - Iterator[int]: all nodes incident to the given edge. + all nodes incident to the given edge. """ if isinstance(edge, tuple): for x in edge: @@ -139,7 +142,7 @@ def nodes_of(self, edge: EdgeId | int) -> Iterator[int]: else: yield edge - def _is_hyperedge_nx_node(self, nx_graph: DiGraph, nx_node: Any) -> bool: + def _is_hyperedge_nx_node(self, nx_graph: networkx.DiGraph, nx_node: Any) -> bool: """Return ``True`` if ``nx_node`` is a hyperedge node in ``nx_graph``. Checks if the given networkx node in the given directed networkx graph @@ -147,8 +150,8 @@ def _is_hyperedge_nx_node(self, nx_graph: DiGraph, nx_node: Any) -> bool: have the ``frame_attribute`` set. Args: - nx_graph (DiGraph): a networkx ``DiGraph``. - nx_node (Any): a node in the given ``nx_graph``. + nx_graph: a networkx ``DiGraph``. + nx_node: a node in the given ``nx_graph``. Returns: bool: true iff the given ``nx_node`` does not posses the @@ -157,19 +160,19 @@ def _is_hyperedge_nx_node(self, nx_graph: DiGraph, nx_node: Any) -> bool: return self.frame_attribute not in nx_graph.nodes[nx_node] def _hyperedge_nx_node_to_edge_tuple_and_neighbors( - self, nx_graph: DiGraph, hyperedge_node: Any + self, nx_graph: networkx.DiGraph, hyperedge_node: Any ) -> tuple[tuple[NodeId, ...], list[NodeId], list[NodeId]]: """Creates a hyperedge tuple for hyperedge node in a given networkx ``DiGraph``. Args: - nx_graph (DiGraph): a networkx ``DiGraph``. - hyperedge_node (Any): a node in the given ``nx_graph`` that represents + nx_graph: a networkx ``DiGraph``. + hyperedge_node: a node in the given ``nx_graph`` that represents a hyperedge. Returns: - tuple[Hashable, ...]: a tuple representing the hyperedge the given - ``nx_node`` represented. (It will be a tuple with one entry per - involved time point, listing all nodes at that time point.) + a tuple representing the hyperedge the given ``nx_node`` represented. (It + will be a tuple with one entry per involved time point, listing all nodes at + that time point.) """ assert self._is_hyperedge_nx_node(nx_graph, hyperedge_node) @@ -196,7 +199,7 @@ def _hyperedge_nx_node_to_edge_tuple_and_neighbors( def get_frames(self) -> tuple[int | None, int | None]: """Return tuple with first and last (exclusive) frame this graph has nodes for. - Returns ``(t_begin, t_end)`` where t_end is exclusive. + Returns ``(t_begin, t_end)`` where ``t_end`` is exclusive. """ self._update_metadata() diff --git a/motile/variables/node_appear.py b/motile/variables/node_appear.py index cfbca50..37caecb 100644 --- a/motile/variables/node_appear.py +++ b/motile/variables/node_appear.py @@ -23,11 +23,11 @@ class NodeAppear(Variable["NodeId"]): .. math:: - |\\text{in_edges}(v)|\cdot x_v - &\sum_{e \in \\text{in_edges}(v)} x_e - - a_v &\leq&\;\; |\\text{in_edges}(v)| - 1 + |\text{in_edges}(v)|\cdot x_v - &\sum_{e \in \text{in_edges}(v)} x_e + - a_v &\leq&\;\; |\text{in_edges}(v)| - 1 - |\\text{in_edges}(v)|\cdot x_v - &\sum_{e \in \\text{in_edges}(v)} x_e - - a_v\cdot |\\text{in_edges}(v)| &\geq&\;\; 0 + |\text{in_edges}(v)|\cdot x_v - &\sum_{e \in \text{in_edges}(v)} x_e + - a_v\cdot |\text{in_edges}(v)| &\geq&\;\; 0 where :math:`x_v` and :math:`x_e` are selection indicators for node :math:`v` and edge :math:`e`, and :math:`a_v` is the appear indicator for diff --git a/motile/variables/node_split.py b/motile/variables/node_split.py index 82b8823..4e8c1bb 100644 --- a/motile/variables/node_split.py +++ b/motile/variables/node_split.py @@ -22,9 +22,9 @@ class NodeSplit(Variable): .. math:: - 2 s_v\; - &\sum_{e\in\\text{out_edges}(v)} x_e &\leq&\;\; 0 + 2 s_v\; - &\sum_{e\in\text{out_edges}(v)} x_e &\leq&\;\; 0 - (|\\text{out_edges}(v)| - 1) s_v\; - &\sum_{e\in\\text{out_edges}(v)} + (|\text{out_edges}(v)| - 1) s_v\; - &\sum_{e\in\text{out_edges}(v)} x_e &\geq&\;\; -1 where :math:`x_e` are selection indicators for edge :math:`e`, and diff --git a/motile/variables/variable.py b/motile/variables/variable.py index 046f0af..24ecb84 100644 --- a/motile/variables/variable.py +++ b/motile/variables/variable.py @@ -28,7 +28,7 @@ class Variable(ABC, Mapping[_KT, ilpy.Variable]): :func:`instantiate_constraints`. Variable classes should not be instantiated by a user. Instead, the - :class:`Solver` provides access to concrete variables through the class + :class:`~motile.Solver` provides access to concrete variables through the class name. The following example shows how to obtain the variable values after optimization:: @@ -69,7 +69,7 @@ def instantiate(solver): The solver will create one variable for each key. The index of that variable can be accessed through a dictionary returned by - :func:`Solver.get_variables`:: + :meth:`motile.Solver.get_variables`:: solver = Solver(graph) @@ -80,11 +80,11 @@ def instantiate(solver): print(f"Selection indicator of node {node} has index {index}") Args: - solver (:class:`Solver`): - The solver instance to create variables for. + solver: + The :class:`~motile.Solver` instance to create variables for. Returns: - A list of keys (anything that is hashable, e.g., nodes of a graph), + A collection of keys (anything that is hashable, e.g., nodes of a graph), one for each variable to create. """ pass @@ -98,8 +98,8 @@ def instantiate_constraints( This ensures that these variables are coupled to other variables of the solver. Args: - solver (:class:`Solver`): - The solver instance to create variable constraints for. + solver: + The :class:`~motile.Solver` instance to create variable constraints for. Returns: A iterable of :class:`ilpy.Constraint` or From d9f6f0aa8841c8525ff5838309d543a8184a54f0 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 17 May 2023 17:02:37 -0400 Subject: [PATCH 4/7] add sphinx_autodoc_typehints dep --- pyproject.toml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c0dc3c7..c040267 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,12 +23,13 @@ dependencies = ['networkx', 'ilpy>=0.3.1', 'numpy', 'structsvm'] dev = ["pre-commit", "pytest", "pytest-cov", "ruff", "twine", "build"] test = ["pytest", "pytest-cov", "plotly"] docs = [ - "sphinx", + "jupyter_sphinx", + "plotly", + "sphinx_autodoc_typehints", "sphinx_rtd_theme", "sphinx_togglebutton", + "sphinx", "tomli", - "jupyter_sphinx", - "plotly", ] [project.urls] From 9ffda71ef3fa798aa8bbae415a13873e3c9d579d Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 17 May 2023 17:17:09 -0400 Subject: [PATCH 5/7] remove type aliases --- docs/source/conf.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 105db38..6d42cdb 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -59,10 +59,6 @@ # Napoleon settings napoleon_google_docstring = True -autodoc_type_aliases = { - "EdgeId": "motile._types.EdgeId", -} - intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "networkx": ("https://networkx.org/documentation/stable/", None), From 3d684918f2582ce099624f956e0dc62f2482d935 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 17 May 2023 17:17:32 -0400 Subject: [PATCH 6/7] undo types change --- motile/_types.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/motile/_types.py b/motile/_types.py index 934be3e..c75db1d 100644 --- a/motile/_types.py +++ b/motile/_types.py @@ -5,12 +5,12 @@ # Nodes are represented as integers, or a "meta-node" tuple of integers. NodeId: TypeAlias = Union[int, tuple[int, ...]] +# objects in the graph are represented as dicts +# eg. { "id": 1, "x": 0.5, "y": 0.5, "t": 0 } +GraphObject: TypeAlias = Mapping[str, Any] + # Edges are represented as tuples of NodeId. # (0, 1) is an edge from node 0 to node 1. # ((0, 1), 2) is a hyperedge from nodes 0 and 1 to node 2 (i.e. a merge). # ((0,), (1, 2)) is a hyperedge from node 0 to nodes 1 and 2 (i.e. a split). EdgeId: TypeAlias = tuple[NodeId, ...] - -# objects in the graph are represented as dicts -# eg. { "id": 1, "x": 0.5, "y": 0.5, "t": 0 } -GraphObject: TypeAlias = Mapping[str, Any] From 108c79ecf4ec0046a14883a7379a6c3020e21225 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Thu, 2 Nov 2023 12:10:03 -0400 Subject: [PATCH 7/7] update lint --- .pre-commit-config.yaml | 10 +++++----- docs/source/install.rst | 2 +- docs/source/learning.rst | 2 +- docs/source/quickstart.rst | 2 +- motile/costs/disappear.py | 1 - motile/plot.py | 12 ++++++------ motile/track_graph.py | 5 +++-- motile/variables/node_disappear.py | 7 ++++--- 8 files changed, 21 insertions(+), 20 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5fce103..a59d989 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,19 +4,19 @@ ci: autoupdate_commit_msg: "ci(pre-commit.ci): autoupdate" repos: - - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.267 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.3 hooks: - id: ruff - args: [--fix] + args: [--fix, --unsafe-fixes] - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 23.10.1 hooks: - id: black - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.3.0 + rev: v1.6.1 hooks: - id: mypy files: "^motile/" diff --git a/docs/source/install.rst b/docs/source/install.rst index f6460ed..1a7566b 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -29,7 +29,7 @@ Do I have to use ``conda``? --------------------------- Kinda. ``motile`` uses `ilpy `_ to solve the -optimzation problem. Conda packages for ``ilpy`` are available for all major +optimization problem. Conda packages for ``ilpy`` are available for all major platforms, linking against the conda packages for SCIP and Gurobi. It is possible to not use ``conda``: If you have SCIP or Gurobi installed diff --git a/docs/source/learning.rst b/docs/source/learning.rst index 8dcbcb5..81f4be1 100644 --- a/docs/source/learning.rst +++ b/docs/source/learning.rst @@ -117,7 +117,7 @@ to the solver. In the example above, the :class:`motile.variables.EdgeSelected` variable (which is the target of the cost :class:`motile.costs.EdgeSelection`), has the -follwing weights and features: +following weights and features: .. math:: \vct{w} diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index d4537fc..d67c359 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -142,7 +142,7 @@ Variables are instantiated and managed by the solver. All we have to do is to ask the solver for the kind of variables we are interested in via :func:`Solver.get_variables`. The returned value (e.g., ``node_selected``) is a dictionary that maps *what* to *where*: in the case of node variables, the -dictionary keys are the nodes themselves (an integer) and the dictionay values +dictionary keys are the nodes themselves (an integer) and the dictionary values are the indices in the ``solution`` vector where we can find the value of the variable. Both node and edge indicators are binary variables (one if selected, zero otherwise). diff --git a/motile/costs/disappear.py b/motile/costs/disappear.py index 99d0ddc..937dfc4 100644 --- a/motile/costs/disappear.py +++ b/motile/costs/disappear.py @@ -14,7 +14,6 @@ class Disappear(Costs): """Costs for :class:`motile.variables.NodeDisappear` variables. Args: - constant (float): A constant cost for each node that ends a track. """ diff --git a/motile/plot.py b/motile/plot.py index d9ca934..41ad005 100644 --- a/motile/plot.py +++ b/motile/plot.py @@ -103,7 +103,7 @@ def draw_track_graph( if position_func is None: def position_func(node: NodeId) -> float: - return float(graph.nodes[node][position_attribute]) # type: ignore + return float(graph.nodes[node][position_attribute]) alpha_node_func: ReturnsFloat alpha_edge_func: ReturnsFloat @@ -113,10 +113,10 @@ def position_func(node: NodeId) -> float: if alpha_attribute is not None: def alpha_node_func(node): - return graph.nodes[node].get(alpha_attribute, 1.0) # type: ignore + return graph.nodes[node].get(alpha_attribute, 1.0) def alpha_edge_func(edge): - return graph.edges[edge].get(alpha_attribute, 1.0) # type: ignore + return graph.edges[edge].get(alpha_attribute, 1.0) elif alpha_func is None: @@ -135,10 +135,10 @@ def alpha_edge_func(_): if label_attribute is not None: def label_node_func(node): - return graph.nodes[node].get(label_attribute, "") # type: ignore + return graph.nodes[node].get(label_attribute, "") def label_edge_func(edge): - return graph.edges[edge].get(label_attribute, "") # type: ignore + return graph.edges[edge].get(label_attribute, "") elif label_func is None: @@ -339,6 +339,6 @@ def _to_rgba( return [_to_rgba(color, a) for a in alpha] # we fake alpha by mixing with white(ish) - # transparancy is tricky... + # transparency is tricky... r, g, b = tuple(int(c * alpha + 220 * (1.0 - alpha)) for c in color) return f"rgb({r},{g},{b})" diff --git a/motile/track_graph.py b/motile/track_graph.py index 058828e..947316e 100644 --- a/motile/track_graph.py +++ b/motile/track_graph.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +from collections import defaultdict from typing import TYPE_CHECKING, Any, DefaultDict, Hashable, Iterator logger = logging.getLogger(__name__) @@ -45,8 +46,8 @@ def __init__( self.nodes: dict[NodeId, GraphObject] = {} self.edges: dict[EdgeId, GraphObject] = {} - self.prev_edges: DefaultDict[NodeId, list[EdgeId]] = DefaultDict(list) - self.next_edges: DefaultDict[NodeId, list[EdgeId]] = DefaultDict(list) + self.prev_edges: defaultdict[NodeId, list[EdgeId]] = DefaultDict(list) + self.next_edges: defaultdict[NodeId, list[EdgeId]] = DefaultDict(list) if nx_graph: self.add_from_nx_graph(nx_graph) diff --git a/motile/variables/node_disappear.py b/motile/variables/node_disappear.py index f499377..93e9683 100644 --- a/motile/variables/node_disappear.py +++ b/motile/variables/node_disappear.py @@ -14,9 +14,10 @@ class NodeDisappear(Variable["NodeId"]): - r"""A binary variable for each node that indicates whether the node is the - end of a track (i.e., the node is selected and has no selected outgoing - edges). + r"""Binary variable to indicate whether a node disappears. + + This variable indicates whether the node is the end of a track (i.e., the node is + selected and has no selected outgoing edges). This variable is coupled to the node and edge selection variables through the following linear constraints: