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

refactor: keep ilpy usage inside of Solver.solve [demo] #61

Closed
wants to merge 9 commits into from
Closed
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
2 changes: 1 addition & 1 deletion docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Solver variables are introduced by inheriting from the following abstract base c

.. automodule:: motile.variables

.. autoclass:: Variable
.. autoclass:: Variables
:members:

The following lists all variables that are already implemented in ``motile``.
Expand Down
46 changes: 10 additions & 36 deletions docs/source/extending.rst
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ Adding Constraints
New constraints are introduced by subclassing :class:`Constraint
<motile.constraints.Constraint>` and implementing the :func:`instantiate
<motile.constraints.Constraint.instantiate>` method. This method should return
a list of ``ilpy.Constraint``.
a list of ``motile.expression.Expression``.

Imagine we know precisely that we want to track at most :math:`k` objects, but
we don't know beforehand which of the many objects in the track graph those
Expand All @@ -115,7 +115,6 @@ This can be done with a constraint as follows:

.. jupyter-execute::

import ilpy
from motile.variables import NodeAppear


Expand All @@ -125,16 +124,8 @@ This can be done with a constraint as follows:
self.num_tracks = num_tracks

def instantiate(self, solver):

appear_indicators = solver.get_variables(NodeAppear)

constraint = ilpy.Constraint()
for appear_indicator, index in appear_indicators.items():
constraint.set_coefficient(index, 1.0)
constraint.set_relation(ilpy.Relation.LessEqual)
constraint.set_value(self.num_tracks)

return [constraint]
yield sum(appear_indicators.values()) <= self.num_tracks

The ``instantiate`` method gets access to the solver the constraint is added
to. Through the solver, we can then access variables to formulate constraints
Expand Down Expand Up @@ -234,9 +225,9 @@ graph:
Adding Variables
----------------

Variables in ``motile`` are added by subclassing the :class:`Variable
<motile.variables.Variable>` class. Subclasses need to implement at least a
static method :func:`instantiate <motile.variables.Variable.instantiate>`. This
Variables in ``motile`` are added by subclassing the :class:`Variables
<motile.variables.Variables>` class. Subclasses need to implement at least a
static method :func:`instantiate <motile.variables.Variables.instantiate>`. This
method should return keys for *what* kind of things we would like to create a
variable for. This should be a list of anything that is hashable (the keys will
be used in a dictionary).
Expand All @@ -255,24 +246,23 @@ measure the curvature of a track and put a cost on that.

To create our new variables, we simply return a list of all pairs of edges we
wish to have a variable for in :func:`instantiate
<motile.variables.Variable.instantiate>`. However, declaring those variables
<motile.variables.Variables.instantiate>`. However, declaring those variables
alone is not sufficient. To give them semantic meaning, we also have to make
sure that our edge-pair variables are set to 1 if the two edges they represent
have been selected, and 0 otherwise. To that end, we also add constraints that
are specific to our variables by overriding the :func:`instantiate_constraints
<motile.variables.Variable.instantiate_constraints>` method, such that our
<motile.variables.Variables.instantiate_constraints>` method, such that our
variables are linked to the already existing :class:`EdgeSelected
<motile.variables.EdgeSelected>` variables.

The complete variable declaration looks like this:

.. jupyter-execute::

import ilpy
from motile.variables import EdgeSelected


class EdgePairs(motile.variables.Variable):
class EdgePairs(motile.variables.Variables):

@staticmethod
def instantiate(solver):
Expand All @@ -294,30 +284,14 @@ The complete variable declaration looks like this:
edge_indicators = solver.get_variables(EdgeSelected)
edge_pair_indicators = solver.get_variables(EdgePairs)

constraints = []
for (in_edge, out_edge), pair_index in edge_pair_indicators.items():

in_edge_index = edge_indicators[in_edge]
out_edge_index = edge_indicators[out_edge]

# edge pair indicator = 1 <=> in edge = 1 and out edge = 1
constraint = ilpy.Constraint()
constraint.set_coefficient(pair_index, 2)
constraint.set_coefficient(in_edge_index, -1)
constraint.set_coefficient(out_edge_index, -1)
constraint.set_relation(ilpy.Relation.LessEqual)
constraint.set_value(0)
constraints.append(constraint)

constraint = ilpy.Constraint()
constraint.set_coefficient(pair_index, -1)
constraint.set_coefficient(in_edge_index, 1)
constraint.set_coefficient(out_edge_index, 1)
constraint.set_relation(ilpy.Relation.LessEqual)
constraint.set_value(1)
constraints.append(constraint)

return constraints
yield 2 * pair_index - in_edge_index - out_edge_index <= 0
yield -pair_index + in_edge_index + out_edge_index <= 1

Variables on their own, however, don't do anything yet. They only start to
affect the solution if they are involved in constraints or have a cost.
Expand Down
8 changes: 3 additions & 5 deletions motile/constraints/constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,17 @@
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Iterable

if TYPE_CHECKING:
import ilpy
from motile.expressions import Expression

if TYPE_CHECKING:
from motile.solver import Solver


class Constraint(ABC):
"""A base class for a constraint that can be added to a solver."""

@abstractmethod
def instantiate(
self, solver: Solver
) -> Iterable[ilpy.Constraint | ilpy.Expression]:
def instantiate(self, solver: Solver) -> Iterable[Expression]:
"""Create and return specific linear constraints for the given solver.

Args:
Expand Down
23 changes: 10 additions & 13 deletions motile/constraints/expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
import contextlib
from typing import TYPE_CHECKING, Union

import ilpy
from motile.expressions import Constant, Expression

from ..variables import EdgeSelected, NodeSelected, Variable
from ..variables import EdgeSelected, NodeSelected, Variables
from .constraint import Constraint

if TYPE_CHECKING:
Expand Down Expand Up @@ -76,13 +76,13 @@ def __init__(
self.eval_nodes = eval_nodes
self.eval_edges = eval_edges

def instantiate(self, solver: Solver) -> list[ilpy.Constraint]:
def instantiate(self, solver: Solver) -> list[Expression]:
# create two constraints: one to select nodes/edges, and one to exclude
select = ilpy.Constraint()
exclude = ilpy.Constraint()
select: Expression = Constant(0)
exclude: Expression = Constant(0)
n_selected = 0 # number of nodes/edges selected

to_evaluate: list[tuple[NodesOrEdges, type[Variable]]] = []
to_evaluate: list[tuple[NodesOrEdges, type[Variables]]] = []
if self.eval_nodes:
to_evaluate.append((solver.graph.nodes, NodeSelected))
if self.eval_edges:
Expand All @@ -99,17 +99,14 @@ def instantiate(self, solver: Solver) -> list[ilpy.Constraint]:
# contextlib.suppress (above) will just skip it and move on...
if eval(self._expression, None, node_or_edge):
# if the expression evaluates to True, we select the node/edge
select.set_coefficient(indicator_variables[id_], 1)
select += indicator_variables[id_]
n_selected += 1
else:
# Otherwise, we exclude it.
exclude.set_coefficient(indicator_variables[id_], 1)
exclude += indicator_variables[id_]

# finally, apply the relation and value to the constraints
select.set_relation(ilpy.Relation.Equal)
select.set_value(n_selected)

exclude.set_relation(ilpy.Relation.Equal)
exclude.set_value(0)
select = select == n_selected
exclude = exclude == 0

return [select, exclude]
2 changes: 1 addition & 1 deletion motile/constraints/max_children.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from typing import TYPE_CHECKING, Iterable

from ilpy.expressions import Constant, Expression
from motile.expressions import Constant, Expression

from ..variables import EdgeSelected
from .constraint import Constraint
Expand Down
2 changes: 1 addition & 1 deletion motile/constraints/max_parents.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from typing import TYPE_CHECKING, Iterable

from ilpy.expressions import Constant, Expression
from motile.expressions import Constant, Expression

from ..variables import EdgeSelected
from .constraint import Constraint
Expand Down
7 changes: 2 additions & 5 deletions motile/costs/features.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np

if TYPE_CHECKING:
import ilpy
from motile.expressions import Variable


class Features:
Expand Down Expand Up @@ -53,7 +50,7 @@ def _increase_features(self, num_features: int) -> None:
self._values = np.hstack((self._values, new_features))

def add_feature(
self, variable_index: int | ilpy.Variable, feature_index: int, value: float
self, variable_index: int | Variable, feature_index: int, value: float
) -> None:
"""Add a value to a feature.

Expand Down
Loading
Loading