diff --git a/optlang/cplex_interface.py b/optlang/cplex_interface.py index 40bc1c65..7a05f7b5 100644 --- a/optlang/cplex_interface.py +++ b/optlang/cplex_interface.py @@ -35,6 +35,7 @@ from sympy.core.singleton import S import cplex from optlang import interface +from optlang.util import inheritdocstring Zero = S.Zero One = S.One @@ -146,8 +147,8 @@ def _constraint_lb_and_ub_to_cplex_sense_rhs_and_range_value(lb, ub): return sense, rhs, range_value +@six.add_metaclass(inheritdocstring) class Variable(interface.Variable): - """CPLEX variable interface.""" def __init__(self, name, *args, **kwargs): super(Variable, self).__init__(name, **kwargs) @@ -195,8 +196,8 @@ def dual(self): return None +@six.add_metaclass(inheritdocstring) class Constraint(interface.Constraint): - """CPLEX solver interface""" _INDICATOR_CONSTRAINT_SUPPORT = True @@ -243,7 +244,9 @@ def problem(self, value): @property def primal(self): if self.problem is not None: - return self.problem.problem.solution.get_activity_levels(self.name) + primal_from_solver = self.problem.problem.solution.get_activity_levels(self.name) + #return self._round_primal_to_bounds(primal_from_solver) # Test assertions fail + return primal_from_solver else: return None @@ -254,46 +257,55 @@ def dual(self): else: return None - # TODO: Refactor to use properties - def __setattr__(self, name, value): - try: - old_name = self.name # TODO: This is a hack - except AttributeError: - pass - super(Constraint, self).__setattr__(name, value) - if getattr(self, 'problem', None): + @interface.Constraint.name.setter + def name(self, value): + if getattr(self, 'problem', None) is not None: + if self.indicator_variable is not None: + raise NotImplementedError( + "Unfortunately, the CPLEX python bindings don't support changing an indicator constraint's name" + ) + else: + # TODO: the following needs to deal with quadratic constraints + self.problem.problem.linear_constraints.set_names(self.name, value) + self._name = value - if name == 'name': - if self.indicator_variable is not None: - raise NotImplementedError("Unfortunately, the CPLEX python bindings don't support changing an indicator constraint's name") - else: - # TODO: the following needs to deal with quadratic constraints - self.problem.problem.linear_constraints.set_names(old_name, value) - - elif name == 'lb' or name == 'ub': - if self.indicator_variable is not None: - raise NotImplementedError("Unfortunately, the CPLEX python bindings don't support changing an indicator constraint's bounds") - if name == 'lb': - if self.ub is not None and value > self.ub: - raise ValueError( - "Lower bound %f is larger than upper bound %f in constraint %s" % - (value, self.ub, self) - ) - sense, rhs, range_value = _constraint_lb_and_ub_to_cplex_sense_rhs_and_range_value(value, self.ub) - elif name == 'ub': - if self.lb is not None and value < self.lb: - raise ValueError( - "Upper bound %f is less than lower bound %f in constraint %s" % - (value, self.lb, self) - ) - sense, rhs, range_value = _constraint_lb_and_ub_to_cplex_sense_rhs_and_range_value(self.lb, value) - if self.is_Linear: - self.problem.problem.linear_constraints.set_rhs(self.name, rhs) - self.problem.problem.linear_constraints.set_senses(self.name, sense) - self.problem.problem.linear_constraints.set_range_values(self.name, range_value) - - elif name == 'expression': - pass + @interface.Constraint.lb.setter + def lb(self, value): + if getattr(self, 'problem', None) is not None: + if self.indicator_variable is not None: + raise NotImplementedError( + "Unfortunately, the CPLEX python bindings don't support changing an indicator constraint's bounds" + ) + if self.ub is not None and value > self.ub: + raise ValueError( + "Lower bound %f is larger than upper bound %f in constraint %s" % + (value, self.ub, self) + ) + sense, rhs, range_value = _constraint_lb_and_ub_to_cplex_sense_rhs_and_range_value(value, self.ub) + if self.is_Linear: + self.problem.problem.linear_constraints.set_rhs(self.name, rhs) + self.problem.problem.linear_constraints.set_senses(self.name, sense) + self.problem.problem.linear_constraints.set_range_values(self.name, range_value) + self._lb = value + + @interface.Constraint.ub.setter + def ub(self, value): + if getattr(self, 'problem', None) is not None: + if self.indicator_variable is not None: + raise NotImplementedError( + "Unfortunately, the CPLEX python bindings don't support changing an indicator constraint's bounds" + ) + if self.lb is not None and value < self.lb: + raise ValueError( + "Upper bound %f is less than lower bound %f in constraint %s" % + (value, self.lb, self) + ) + sense, rhs, range_value = _constraint_lb_and_ub_to_cplex_sense_rhs_and_range_value(self.lb, value) + if self.is_Linear: + self.problem.problem.linear_constraints.set_rhs(self.name, rhs) + self.problem.problem.linear_constraints.set_senses(self.name, sense) + self.problem.problem.linear_constraints.set_range_values(self.name, range_value) + self._ub = value def __iadd__(self, other): # if self.problem is not None: @@ -308,6 +320,7 @@ def __iadd__(self, other): return self +@six.add_metaclass(inheritdocstring) class Objective(interface.Objective): def __init__(self, *args, **kwargs): super(Objective, self).__init__(*args, **kwargs) @@ -316,17 +329,16 @@ def __init__(self, *args, **kwargs): def value(self): return self.problem.problem.solution.get_objective_value() - def __setattr__(self, name, value): - - if getattr(self, 'problem', None): - if name == 'direction': - self.problem.problem.objective.set_sense( - {'min': self.problem.problem.objective.sense.minimize, 'max': self.problem.problem.objective.sense.maximize}[value]) - super(Objective, self).__setattr__(name, value) - else: - super(Objective, self).__setattr__(name, value) + @interface.Objective.direction.setter + def direction(self, value): + if getattr(self, 'problem', None) is not None: + self.problem.problem.objective.set_sense( + {'min': self.problem.problem.objective.sense.minimize, + 'max': self.problem.problem.objective.sense.maximize}[value]) + super(Objective, self).__setattr__("direction", value) +@six.add_metaclass(inheritdocstring) class Configuration(interface.MathematicalProgrammingConfiguration): def __init__(self, lp_method='primal', tolerance=1e-9, presolve=False, verbosity=0, timeout=None, @@ -500,6 +512,7 @@ def qp_method(self, value): self._qp_method = value +@six.add_metaclass(inheritdocstring) class Model(interface.Model): def __init__(self, problem=None, *args, **kwargs): diff --git a/optlang/glpk_interface.py b/optlang/glpk_interface.py index a7c96c95..17a9940d 100644 --- a/optlang/glpk_interface.py +++ b/optlang/glpk_interface.py @@ -28,6 +28,8 @@ from sympy.core.add import _unevaluated_Add from sympy.core.mul import _unevaluated_Mul +from optlang.util import inheritdocstring + import logging log = logging.getLogger(__name__) @@ -67,8 +69,8 @@ ) +@six.add_metaclass(inheritdocstring) class Variable(interface.Variable): - """...""" def __init__(self, name, index=None, *args, **kwargs): super(Variable, self).__init__(name, **kwargs) @@ -126,19 +128,15 @@ def dual(self): else: return None - def __setattr__(self, name, value): - try: - old_name = self.name # TODO: This is a hack - except AttributeError: - pass - super(Variable, self).__setattr__(name, value) - if getattr(self, 'problem', None): - if name == 'name': - glp_set_col_name(self.problem.problem, glp_find_col(self.problem.problem, old_name), str(value)) + @interface.Variable.name.setter + def name(self, value): + if getattr(self, 'problem', None) is not None: + glp_set_col_name(self.problem.problem, glp_find_col(self.problem.problem, self.name), str(value)) + self._name = value +@six.add_metaclass(inheritdocstring) class Constraint(interface.Constraint): - """GLPK solver interface""" _INDICATOR_CONSTRAINT_SUPPORT = False @@ -183,6 +181,24 @@ def _set_coefficients_low_level(self, variables_coefficients_dict): else: raise Exception('_set_coefficients_low_level works only if a constraint is associated with a solver instance.') + @interface.Constraint.lb.setter + def lb(self, value): + self._lb = value + if self.problem is not None: + self.problem._glpk_set_row_bounds(self) + + @interface.Constraint.ub.setter + def ub(self, value): + self._ub = value + if self.problem is not None: + self.problem._glpk_set_row_bounds(self) + + @interface.OptimizationExpression.name.setter + def name(self, value): + if self.problem is not None: + glp_set_row_name(self.problem.problem, glp_find_row(self.problem.problem, self.name), str(value)) + self._name = value + @property def problem(self): return self._problem @@ -211,7 +227,9 @@ def index(self): @property def primal(self): if self.problem is not None: - return glp_get_row_prim(self.problem.problem, self.index) + primal_from_solver = glp_get_row_prim(self.problem.problem, self.index) + #return self._round_primal_to_bounds(primal_from_solver) # Test assertions fail + return primal_from_solver else: return None @@ -235,18 +253,6 @@ def problem(self, value): else: self._problem = value - def __setattr__(self, name, value): - try: - old_name = self.name # TODO: This is a hack - except AttributeError: - pass - super(Constraint, self).__setattr__(name, value) - if getattr(self, 'problem', None): - if name == 'name': - glp_set_row_name(self.problem.problem, glp_find_row(self.problem.problem, old_name), str(value)) - elif name == 'lb' or name == 'ub': - self.problem._glpk_set_row_bounds(self) - def __iadd__(self, other): # if self.problem is not None: # self.problem._add_to_constraint(self.index, other) @@ -284,6 +290,7 @@ def __idiv__(self, other): return self +@six.add_metaclass(inheritdocstring) class Objective(interface.Objective): def __init__(self, *args, **kwargs): super(Objective, self).__init__(*args, **kwargs) @@ -310,15 +317,11 @@ def value(self): else: return glp_get_obj_val(self.problem.problem) - def __setattr__(self, name, value): - - if getattr(self, 'problem', None): - if name == 'direction': - glp_set_obj_dir(self.problem.problem, - {'min': GLP_MIN, 'max': GLP_MAX}[value]) - super(Objective, self).__setattr__(name, value) - else: - super(Objective, self).__setattr__(name, value) + @interface.Objective.direction.setter + def direction(self, value): + if getattr(self, 'problem', None) is not None: + glp_set_obj_dir(self.problem.problem, {'min': GLP_MIN, 'max': GLP_MAX}[value]) + super(Objective, self).__setattr__("objective", value) def __iadd__(self, other): self.problem = None @@ -349,8 +352,8 @@ def __idiv__(self, other): return self +@six.add_metaclass(inheritdocstring) class Configuration(interface.MathematicalProgrammingConfiguration): - """docstring for Configuration""" def __init__(self, presolve=False, verbosity=0, timeout=None, *args, **kwargs): super(Configuration, self).__init__(*args, **kwargs) @@ -433,8 +436,9 @@ def timeout(self, value): self._set_timeout(value) self._timeout = value + +@six.add_metaclass(inheritdocstring) class Model(interface.Model): - """GLPK solver interface""" def __init__(self, problem=None, *args, **kwargs): diff --git a/optlang/interface.py b/optlang/interface.py index 3b84aac9..1b5f637b 100644 --- a/optlang/interface.py +++ b/optlang/interface.py @@ -135,6 +135,7 @@ def __init__(self, name, lb=None, ub=None, type="continuous", problem=None, *arg raise ValueError( 'Variable names cannot contain whitespace characters. "%s" contains whitespace character "%s".' % ( name, char)) + self._name = name sympy.Symbol.__init__(name, *args, **kwargs) #TODO: change this back to use super self._lb = lb self._ub = ub @@ -147,6 +148,14 @@ def __init__(self, name, lb=None, ub=None, type="continuous", problem=None, *arg self._type = type self.problem = problem + @property + def name(self): + return self._name + + @name.setter + def name(self, value): + self._name = value + @property def lb(self): return self._lb @@ -278,15 +287,23 @@ def __init__(self, expression, name=None, problem=None, sloppy=False, *args, **k name = str(name) super(OptimizationExpression, self).__init__(*args, **kwargs) + self._problem = problem if sloppy: self._expression = expression else: self._expression = self._canonicalize(expression) if name is None: - self.name = str(uuid.uuid1()) + self._name = str(uuid.uuid1()) else: - self.name = name - self._problem = problem + self._name = name + + @property + def name(self): + return self._name + + @name.setter + def name(self, value): + self._name = value @property def problem(self): @@ -439,13 +456,29 @@ def clone(cls, constraint, model=None, **kwargs): name=constraint.name, sloppy=True, **kwargs) def __init__(self, expression, lb=None, ub=None, indicator_variable=None, active_when=1, *args, **kwargs): - self.lb = lb - self.ub = ub + self._lb = lb + self._ub = ub + super(Constraint, self).__init__(expression, *args, **kwargs) self.__check_valid_indicator_variable(indicator_variable) self.__check_valid_active_when(active_when) self._indicator_variable = indicator_variable self._active_when = active_when - super(Constraint, self).__init__(expression, *args, **kwargs) + + @property + def lb(self): + return self._lb + + @lb.setter + def lb(self, value): + self._lb = value + + @property + def ub(self): + return self._ub + + @ub.setter + def ub(self, value): + self._ub = value @property def indicator_variable(self): @@ -508,6 +541,17 @@ def primal(self): def dual(self): return None + def _round_primal_to_bounds(self, primal, tolerance=1e-5): + if (self.lb is None or primal >= self.lb) and (self.ub is None or primal <= self.ub): + return primal + else: + if (primal <= self.lb) and ((self.lb - primal) <= tolerance): + return self.lb + elif (primal >= self.ub) and ((self.ub - primal) >= -tolerance): + return self.ub + else: + raise AssertionError('The primal value %s returned by the solver is out of bounds for variable %s (lb=%s, ub=%s)' % (primal, self.name, self.lb, self.ub)) + class Objective(OptimizationExpression): """Objective function. diff --git a/optlang/util.py b/optlang/util.py index 2563aea0..3e934e8c 100644 --- a/optlang/util.py +++ b/optlang/util.py @@ -23,6 +23,7 @@ log = logging.getLogger(__name__) import tempfile +import inspect from subprocess import check_output @@ -134,6 +135,35 @@ def list_available_solvers(): return solvers +def inheritdocstring(name, bases, attrs): + """Use as metaclass to inherit class and method docstrings from parent. + Adapted from http://stackoverflow.com/questions/13937500/inherit-a-parent-class-docstring-as-doc-attribute""" + if '__doc__' not in attrs or not attrs["__doc__"]: + # create a temporary 'parent' to (greatly) simplify the MRO search + temp = type('temporaryclass', bases, {}) + for cls in inspect.getmro(temp): + if cls.__doc__ is not None: + attrs['__doc__'] = cls.__doc__ + break + + for attr_name, attr in attrs.items(): + if not attr.__doc__: + for cls in inspect.getmro(temp): + try: + if getattr(cls, attr_name).__doc__ is not None: + attr.__doc__ = getattr(cls, attr_name).__doc__ + break + except (AttributeError, TypeError): + continue + + return type(name, bases, attrs) + + +def method_inheritdocstring(mthd): + """Use as decorator on a method to inherit doc from parent method of same name""" + if not mthd.__doc__: + pass + if __name__ == '__main__': from swiglpk import glp_create_prob, glp_read_lp, glp_get_num_rows diff --git a/tests/test_cplex_interface.py b/tests/test_cplex_interface.py index e10f6285..05ed16c8 100644 --- a/tests/test_cplex_interface.py +++ b/tests/test_cplex_interface.py @@ -116,6 +116,13 @@ def test_setting_nonnumerical_bounds_raises(self): model = Model(problem=problem) self.assertRaises(Exception, setattr, model.constraints[0], 'lb', 'Chicken soup') + def test_setting_bounds(self): + value = 42 + self.constraint.ub = value + self.assertEqual(self.constraint.ub, value) + self.constraint.lb = value + self.assertEqual(self.constraint.lb, value) + class SolverTestCase(unittest.TestCase): def setUp(self):