diff --git a/motile/constraints/constraint.py b/motile/constraints/constraint.py index 8dc3b2c..04cccbf 100644 --- a/motile/constraints/constraint.py +++ b/motile/constraints/constraint.py @@ -11,7 +11,9 @@ class Constraint(ABC): @abstractmethod - def instantiate(self, solver: Solver) -> Iterable[ilpy.Constraint]: + def instantiate( + self, solver: Solver + ) -> Iterable[ilpy.Constraint | ilpy.Expression]: """Create and return specific linear constraints for the given solver. Args: diff --git a/motile/constraints/max_children.py b/motile/constraints/max_children.py index 2782702..e22d4be 100644 --- a/motile/constraints/max_children.py +++ b/motile/constraints/max_children.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Iterable -import ilpy +from ilpy.expressions import Constant, Expression from ..variables import EdgeSelected from .constraint import Constraint @@ -30,18 +30,12 @@ class MaxChildren(Constraint): def __init__(self, max_children: int) -> None: self.max_children = max_children - def instantiate(self, solver: Solver) -> Iterable[ilpy.Constraint]: + def instantiate(self, solver: Solver) -> Iterable[Expression]: edge_indicators = solver.get_variables(EdgeSelected) for node in solver.graph.nodes: - constraint = ilpy.Constraint() + n_edges = sum( + (edge_indicators[e] for e in solver.graph.next_edges[node]), Constant(0) + ) - # all outgoing edges - for edge in solver.graph.next_edges[node]: - constraint.set_coefficient(edge_indicators[edge], 1) - - # relation, value - constraint.set_relation(ilpy.Relation.LessEqual) - - constraint.set_value(self.max_children) - yield constraint + yield n_edges <= self.max_children diff --git a/motile/constraints/max_parents.py b/motile/constraints/max_parents.py index b261017..d26b13e 100644 --- a/motile/constraints/max_parents.py +++ b/motile/constraints/max_parents.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Iterable -import ilpy +from ilpy.expressions import Constant, Expression from ..variables import EdgeSelected from .constraint import Constraint @@ -30,18 +30,13 @@ class MaxParents(Constraint): def __init__(self, max_parents: int) -> None: self.max_parents = max_parents - def instantiate(self, solver: Solver) -> Iterable[ilpy.Constraint]: + def instantiate(self, solver: Solver) -> Iterable[Expression]: edge_indicators = solver.get_variables(EdgeSelected) for node in solver.graph.nodes: - constraint = ilpy.Constraint() - # all incoming edges - for edge in solver.graph.prev_edges[node]: - constraint.set_coefficient(edge_indicators[edge], 1) - - # relation, value - constraint.set_relation(ilpy.Relation.LessEqual) - - constraint.set_value(self.max_parents) - yield constraint + s = sum( + (edge_indicators[e] for e in solver.graph.prev_edges[node]), + start=Constant(0), + ) + yield s <= self.max_parents diff --git a/motile/constraints/select_edge_nodes.py b/motile/constraints/select_edge_nodes.py index d05c016..a9c6488 100644 --- a/motile/constraints/select_edge_nodes.py +++ b/motile/constraints/select_edge_nodes.py @@ -2,12 +2,12 @@ from typing import TYPE_CHECKING, Iterable -import ilpy - from ..variables import EdgeSelected, NodeSelected from .constraint import Constraint if TYPE_CHECKING: + from ilpy.expressions import Expression + from motile.solver import Solver @@ -24,20 +24,11 @@ class SelectEdgeNodes(Constraint): This constraint will be added by default to any :class:`Solver` instance. """ - def instantiate(self, solver: Solver) -> Iterable[ilpy.Constraint]: + def instantiate(self, solver: Solver) -> Iterable[Expression]: node_indicators = solver.get_variables(NodeSelected) edge_indicators = solver.get_variables(EdgeSelected) for edge in solver.graph.edges: - nodes = solver.graph.nodes_of(edge) - - ind_e = edge_indicators[edge] - nodes_ind = [node_indicators[node] for node in nodes] - - constraint = ilpy.Constraint() - constraint.set_coefficient(ind_e, len(nodes_ind)) - for node_ind in nodes_ind: - constraint.set_coefficient(node_ind, -1) - constraint.set_relation(ilpy.Relation.LessEqual) - constraint.set_value(0) - yield constraint + nodes = list(solver.graph.nodes_of(edge)) + x_e = edge_indicators[edge] + yield len(nodes) * x_e - sum(node_indicators[n] for n in nodes) <= 0 diff --git a/motile/costs/features.py b/motile/costs/features.py index f61f5e4..dcefaea 100644 --- a/motile/costs/features.py +++ b/motile/costs/features.py @@ -1,7 +1,12 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import numpy as np +if TYPE_CHECKING: + import ilpy + class Features: def __init__(self) -> None: @@ -33,10 +38,11 @@ def _increase_features(self, num_features: int) -> None: self._values = np.hstack((self._values, new_features)) def add_feature( - self, variable_index: int, feature_index: int, value: float + self, variable_index: int | ilpy.Variable, feature_index: int, value: float ) -> None: num_variables, num_features = self._values.shape + variable_index = int(variable_index) if variable_index >= num_variables or feature_index >= num_features: self.resize( max(variable_index + 1, num_variables), diff --git a/motile/solver.py b/motile/solver.py index fccc6d7..43c76cf 100644 --- a/motile/solver.py +++ b/motile/solver.py @@ -180,7 +180,9 @@ def get_variables(self, cls: type[V]) -> V: self._add_variables(cls) return cast("V", self.variables[cls]) - def add_variable_cost(self, index: int, value: float, weight: Weight) -> None: + def add_variable_cost( + self, index: int | ilpy.Variable, value: float, weight: Weight + ) -> None: """Add costs for an individual variable. To be used within implementations of :class:`motile.costs.Costs`. diff --git a/motile/variables/node_appear.py b/motile/variables/node_appear.py index 63e01af..f513114 100644 --- a/motile/variables/node_appear.py +++ b/motile/variables/node_appear.py @@ -2,13 +2,13 @@ from typing import TYPE_CHECKING, Collection, Iterable -import ilpy - from .edge_selected import EdgeSelected from .node_selected import NodeSelected from .variable import Variable if TYPE_CHECKING: + import ilpy + from motile._types import NodeId from motile.solver import Solver @@ -39,26 +39,20 @@ def instantiate(solver: Solver) -> Collection[NodeId]: return solver.graph.nodes @staticmethod - def instantiate_constraints(solver: Solver) -> Iterable[ilpy.Constraint]: + def instantiate_constraints(solver: Solver) -> Iterable[ilpy.Expression]: appear_indicators = solver.get_variables(NodeAppear) node_indicators = solver.get_variables(NodeSelected) edge_indicators = solver.get_variables(EdgeSelected) for node in solver.graph.nodes: prev_edges = solver.graph.prev_edges[node] - num_prev_edges = len(prev_edges) + selected = node_indicators[node] + appear = appear_indicators[node] - if num_prev_edges == 0: + if not prev_edges: # special case: no incoming edges, appear indicator is equal to # selection indicator - constraint = ilpy.Constraint() - constraint.set_coefficient(node_indicators[node], 1.0) - constraint.set_coefficient(appear_indicators[node], -1.0) - constraint.set_relation(ilpy.Relation.Equal) - constraint.set_value(0.0) - - yield constraint - + yield selected == appear continue # Ensure that the following holds: @@ -72,33 +66,8 @@ def instantiate_constraints(solver: Solver) -> Iterable[ilpy.Constraint]: # (1) s - appear <= num_prev - 1 # (2) s - appear * num_prev >= 0 - constraint1 = ilpy.Constraint() - constraint2 = ilpy.Constraint() - - # set s for both constraints: - - # num_prev * selected - constraint1.set_coefficient(node_indicators[node], num_prev_edges) - constraint2.set_coefficient(node_indicators[node], num_prev_edges) - - # - sum(prev_selected) - for prev_edge in prev_edges: - constraint1.set_coefficient(edge_indicators[prev_edge], -1.0) - constraint2.set_coefficient(edge_indicators[prev_edge], -1.0) - - # constraint specific parts: - - # - appear - constraint1.set_coefficient(appear_indicators[node], -1.0) - - # - appear * num_prev - constraint2.set_coefficient(appear_indicators[node], -num_prev_edges) - - constraint1.set_relation(ilpy.Relation.LessEqual) - constraint2.set_relation(ilpy.Relation.GreaterEqual) - - constraint1.set_value(num_prev_edges - 1) - constraint2.set_value(0) + num_prev = len(prev_edges) + s = num_prev * selected - sum(edge_indicators[e] for e in prev_edges) - yield constraint1 - yield constraint2 + yield s - appear <= num_prev - 1 + yield s - appear >= 0 diff --git a/motile/variables/variable.py b/motile/variables/variable.py index e10bbd6..c6c2c98 100644 --- a/motile/variables/variable.py +++ b/motile/variables/variable.py @@ -20,7 +20,7 @@ _KT = TypeVar("_KT", bound=Hashable) -class Variable(ABC, Mapping[_KT, int]): +class Variable(ABC, Mapping[_KT, ilpy.Variable]): """Base class for solver variables. New variables can be introduced by inheriting from this base class and @@ -38,6 +38,9 @@ class Variable(ABC, Mapping[_KT, int]): solution = solver.solve() + # here `node_selected` is an instance of a Variable subclass + # specifically, it will be an instance of NodeSelected, which + # maps node Ids to variables in the solver. node_selected = solver.get_variables(NodeSelected) for node in graph.nodes: @@ -89,7 +92,9 @@ def instantiate(solver): pass @staticmethod - def instantiate_constraints(solver: Solver) -> Iterable[ilpy.Constraint]: + 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. @@ -100,9 +105,9 @@ def instantiate_constraints(solver: Solver) -> Iterable[ilpy.Constraint]: Returns: - A iterable of :class:`ilpy.Constraint`. See - :class:`motile.constraints.Constraint` for how to create linear - constraints. + A iterable of :class:`ilpy.Constraint` or + :class:`ilpy.expressions.Expression.` See + :class:`motile.constraints.Constraint` for how to create linear constraints. """ return [] @@ -125,8 +130,9 @@ def __repr__(self) -> str: rs.append(r) return "\n".join(rs) - def __getitem__(self, key: _KT) -> int: - return self._index_map[key] + def __getitem__(self, key: _KT) -> ilpy.Variable: + name = f"{type(self).__name__}({key})" + return ilpy.Variable(name, index=self._index_map[key]) def __iter__(self) -> Iterator[_KT]: return iter(self._index_map) diff --git a/pyproject.toml b/pyproject.toml index 7381183..1793742 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ authors = [ { name = 'Florian Jug', email = 'florian.jug@fht.org' }, ] dynamic = ["version"] -dependencies = ['networkx', 'ilpy>=0.3.0', 'numpy', 'structsvm'] +dependencies = ['networkx', 'ilpy>=0.3.1', 'numpy', 'structsvm'] [project.optional-dependencies] dev = ["pre-commit", "pytest", "pytest-cov", "ruff", "twine", "build"]