Skip to content

Commit

Permalink
Merge branch 'lp-delete-constraints' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
jonls committed Jul 22, 2015
2 parents 93ef6b5 + 5b39a60 commit be2d662
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 33 deletions.
64 changes: 48 additions & 16 deletions psamm/lpsolver/cplex.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import cplex as cp

from .lp import Solver as BaseSolver
from .lp import Constraint as BaseConstraint
from .lp import Problem as BaseProblem
from .lp import Result as BaseResult
from .lp import (VariableSet, Expression, Relation,
Expand Down Expand Up @@ -83,6 +84,7 @@ def __init__(self, **kwargs):

self._variables = {}
self._var_names = ('x'+str(i) for i in count(1))
self._constr_names = ('c'+str(i) for i in count(1))

self._result = None

Expand Down Expand Up @@ -147,12 +149,41 @@ def set(self, names):
set(names) - set(self._variables)))
return Expression({VariableSet(names): 1})

def _add_constraints(self, relation):
"""Add the given relation as one or more constraints
Return a list of the names of the constraints added.
"""
if relation.sense in (
Relation.StrictlyGreater, Relation.StrictlyLess):
raise ValueError(
'Strict relations are invalid in LP-problems:'
' {}'.format(relation))

expression = relation.expression
pairs = []
for value_set in expression.value_sets():
ind, val = zip(*((self._variables[variable], float(value))
for variable, value in value_set))
pairs.append(cp.SparsePair(ind=ind, val=val))

names = [next(self._constr_names) for _ in pairs]

self._cp.linear_constraints.add(
names=names, lin_expr=pairs,
senses=tuple(repeat(relation.sense, len(pairs))),
rhs=tuple(repeat(float(-expression.offset), len(pairs))))

return names

def add_linear_constraints(self, *relations):
"""Add constraints to the problem
Each constraint is represented by a Relation, and the
expression in that relation can be a set expression.
"""
constraints = []

for relation in relations:
if isinstance(relation, bool):
# A bool in place of a relation is accepted to mean
Expand All @@ -161,23 +192,12 @@ def add_linear_constraints(self, *relations):
# '0 == 0' or '2 >= 3').
if not relation:
raise ValueError('Unsatisfiable relation added')
constraints.append(Constraint(self, None))
else:
if relation.sense in (
Relation.StrictlyGreater, Relation.StrictlyLess):
raise ValueError(
'Strict relations are invalid in LP-problems:'
' {}'.format(relation))

expression = relation.expression
pairs = []
for value_set in expression.value_sets():
ind, val = zip(*((self._variables[variable], float(value))
for variable, value in value_set))
pairs.append(cp.SparsePair(ind=ind, val=val))
self._cp.linear_constraints.add(
lin_expr=pairs,
senses=tuple(repeat(relation.sense, len(pairs))),
rhs=tuple(repeat(float(-expression.offset), len(pairs))))
for name in self._add_constraints(relation):
constraints.append(Constraint(self, name))

return constraints

def set_linear_objective(self, expression):
"""Set linear objective of problem"""
Expand Down Expand Up @@ -214,6 +234,18 @@ def result(self):
return self._result


class Constraint(BaseConstraint):
"""Represents a constraint in a cplex.Problem"""

def __init__(self, prob, name):
self._prob = prob
self._name = name

def delete(self):
if self._name is not None:
self._prob._cp.linear_constraints.delete(self._name)


class Result(BaseResult):
"""Represents the solution to a cplex.Problem
Expand Down
14 changes: 12 additions & 2 deletions psamm/lpsolver/lp.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,15 @@ def create_problem(self):
"""Create a new :class:`.Problem` instance"""


@add_metaclass(abc.ABCMeta)
class Constraint(object):
"""Represents a constraint within an LP Problem"""

@abc.abstractmethod
def delete(self):
"""Remove constraint from Problem instance"""


@add_metaclass(abc.ABCMeta)
class Problem(object):
"""Representation of LP Problem instance
Expand Down Expand Up @@ -330,8 +339,9 @@ def set(self, names):
def add_linear_constraints(self, *relations):
"""Add constraints to the problem
Each constraint is represented by a :class:`.Relation`, and the
expression in that relation can be a set expression.
Each constraint is given as a :class:`.Relation`, and the expression
in that relation can be a set expression. Returns a sequence of
:class:`.Constraint`s.
"""

@abc.abstractmethod
Expand Down
57 changes: 45 additions & 12 deletions psamm/lpsolver/qsoptex.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import qsoptex

from .lp import Solver as BaseSolver
from .lp import Constraint as BaseConstraint
from .lp import Problem as BaseProblem
from .lp import Result as BaseResult
from .lp import (VariableSet, Expression, Relation,
Expand All @@ -50,6 +51,7 @@ def __init__(self, **kwargs):

self._variables = {}
self._var_names = ('x'+str(i) for i in count(1))
self._constr_names = ('c'+str(i) for i in count(1))

self._result = None

Expand Down Expand Up @@ -109,12 +111,38 @@ def set(self, names):
set(names) - set(self._variables)))
return Expression({VariableSet(names): 1})

def _add_constraints(self, relation):
"""Add the given relation as one or more constraints
Return a list of the names of the constraints added.
"""
if relation.sense in (
Relation.StrictlyGreater, Relation.StrictlyLess):
raise ValueError(
'Strict relations are invalid in LP-problems: {}'.format(
relation))

expression = relation.expression
names = []
for value_set in expression.value_sets():
values = ((self._variables[variable], value)
for variable, value in value_set)
constr_name = next(self._constr_names)
self._p.add_linear_constraint(
sense=relation.sense, values=values, rhs=-expression.offset,
name=constr_name)
names.append(constr_name)

return names

def add_linear_constraints(self, *relations):
"""Add constraints to the problem
Each constraint is represented by a Relation, and the
expression in that relation can be a set expression.
"""
constraints = []

for relation in relations:
if isinstance(relation, bool):
# A bool in place of a relation is accepted to mean
Expand All @@ -123,19 +151,12 @@ def add_linear_constraints(self, *relations):
# '0 == 0' or '2 >= 3').
if not relation:
raise ValueError('Unsatisfiable relation added')
constraints.append(Constraint(self, None))
else:
if relation.sense in (
Relation.StrictlyGreater, Relation.StrictlyLess):
raise ValueError(
'Strict relations are invalid in LP-problems:'
' {}'.format(relation))

expression = relation.expression
for value_set in expression.value_sets():
values = ((self._variables[variable], value)
for variable, value in value_set)
self._p.add_linear_constraint(
relation.sense, values, -expression.offset)
for name in self._add_constraints(relation):
constraints.append(Constraint(self, name))

return constraints

def set_linear_objective(self, expression):
"""Set linear objective of problem"""
Expand Down Expand Up @@ -172,6 +193,18 @@ def result(self):
return self._result


class Constraint(BaseConstraint):
"""Represents a constraint in a qsoptex.Problem"""

def __init__(self, prob, name):
self._prob = prob
self._name = name

def delete(self):
if self._name is not None:
self._prob._p.delete_linear_constraint(self._name)


class Result(BaseResult):
"""Represents the solution to a qsoptex.Problem
Expand Down
21 changes: 19 additions & 2 deletions psamm/tests/test_cplex.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def test_objective_reset_on_set_linear_objective(self):
self.assertAlmostEqual(result.get_value('y'), 10)

def test_result_to_bool_conversion_on_optimal(self):
'''Run a feasible LP problem and check that the result evaluates to True'''
"""Run a feasible LP problem and check that the result is True"""
prob = self.solver.create_problem()
prob.define('x', 'y', lower=0, upper=10)
prob.add_linear_constraints(prob.var('x') + prob.var('y') <= 12)
Expand All @@ -63,7 +63,7 @@ def test_result_to_bool_conversion_on_optimal(self):
self.assertTrue(result)

def test_result_to_bool_conversion_on_infeasible(self):
'''Run an infeasible LP problem and check that the result evaluates to False'''
"""Run an infeasible LP problem and check that the result is False"""
prob = self.solver.create_problem()
prob.define('x', 'y', 'z', lower=0, upper=10)
prob.add_linear_constraints(2*prob.var('x') == -prob.var('y'),
Expand All @@ -74,6 +74,23 @@ def test_result_to_bool_conversion_on_infeasible(self):
result = prob.solve()
self.assertFalse(result)

def test_constraint_delete(self):
prob = self.solver.create_problem()
prob.define('x', 'y', lower=0, upper=10)
prob.add_linear_constraints(prob.var('x') + prob.var('y') <= 12)
c1, = prob.add_linear_constraints(prob.var('x') <= 5)

prob.set_objective_sense(lp.ObjectiveSense.Maximize)
prob.set_linear_objective(prob.var('x'))

result = prob.solve()
self.assertAlmostEqual(result.get_value('x'), 5)

# Delete constraint
c1.delete()
result = prob.solve()
self.assertAlmostEqual(result.get_value('x'), 10)


if __name__ == '__main__':
unittest.main()
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ setenv =
deps =
coverage
cplex: {env:CPLEX_PYTHON_PACKAGE}
qsoptex: python-qsoptex
qsoptex: python-qsoptex>=0.4
commands =
coverage run -p --branch --omit={envdir},psamm/tests \
./setup.py test
Expand Down

0 comments on commit be2d662

Please sign in to comment.