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

feat: use ilpy.expressions in motile Constraints #30

Merged
merged 20 commits into from
May 12, 2023
Merged
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
4 changes: 3 additions & 1 deletion motile/constraints/constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
18 changes: 6 additions & 12 deletions 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

import ilpy
from ilpy.expressions import Constant, Expression

from ..variables import EdgeSelected
from .constraint import Constraint
Expand Down Expand Up @@ -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
19 changes: 7 additions & 12 deletions 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

import ilpy
from ilpy.expressions import Constant, Expression

from ..variables import EdgeSelected
from .constraint import Constraint
Expand Down Expand Up @@ -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
21 changes: 6 additions & 15 deletions motile/constraints/select_edge_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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
8 changes: 7 additions & 1 deletion motile/costs/features.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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),
Expand Down
4 changes: 3 additions & 1 deletion motile/solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
53 changes: 11 additions & 42 deletions motile/variables/node_appear.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand All @@ -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
20 changes: 13 additions & 7 deletions motile/variables/variable.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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.

Expand All @@ -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 []

Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
Loading