Skip to content

Commit

Permalink
fix: allow sympy.Number to be used as bounds (#35)
Browse files Browse the repository at this point in the history
* fix: allow sympy.Number to be used as bounds

Both variable and constraint bounds can now be set to sympy.Number types

* test: add a bit more testing

* fix: typo
  • Loading branch information
KristianJensen authored and phantomas1234 committed Nov 8, 2016
1 parent 5860afc commit f8cef9c
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 7 deletions.
4 changes: 2 additions & 2 deletions optlang/cplex_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -694,12 +694,12 @@ def _optimize(self):

def _set_variable_bounds_on_problem(self, var_lb, var_ub):
lb = [
(var.name, -cplex.infinity) if val is None else (var.name, val) for var, val in var_lb
(var.name, -cplex.infinity) if val is None else (var.name, float(val)) for var, val in var_lb
]
if len(lb) > 0:
self.problem.variables.set_lower_bounds(lb)
ub = [
(var.name, cplex.infinity) if val is None else (var.name, val) for var, val in var_ub
(var.name, cplex.infinity) if val is None else (var.name, float(val)) for var, val in var_ub
]
if len(ub) > 0:
self.problem.variables.set_upper_bounds(ub)
Expand Down
10 changes: 5 additions & 5 deletions optlang/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
from sympy.core.assumptions import _assume_rules
from sympy.core.facts import FactKB
from sympy.core.expr import Expr
from optlang.util import parse_expr, expr_to_json
from optlang.util import parse_expr, expr_to_json, is_numeric

from .container import Container

Expand Down Expand Up @@ -111,7 +111,7 @@ class Variable(sympy.Symbol):

@staticmethod
def __test_valid_lower_bound(type, value, name):
if not (value is None or isinstance(value, (float, int))):
if not (value is None or is_numeric(value)):
raise TypeError("Variable bounds must be numeric or None.")
if value is not None:
if type == 'integer' and value % 1 != 0.:
Expand All @@ -124,7 +124,7 @@ def __test_valid_lower_bound(type, value, name):

@staticmethod
def __test_valid_upper_bound(type, value, name):
if not (value is None or isinstance(value, (float, int))):
if not (value is None or is_numeric(value)):
raise TypeError("Variable bounds must be numeric or None.")
if value is not None:
if type == 'integer' and value % 1 != 0.:
Expand Down Expand Up @@ -583,13 +583,13 @@ class Constraint(OptimizationExpression):
_INDICATOR_CONSTRAINT_SUPPORT = True

def _check_valid_lower_bound(self, value):
if not (value is None or isinstance(value, (int, float))):
if not (value is None or is_numeric(value)):
raise TypeError("Constraint bounds must be numeric or None, not {}".format(type(value)))
if value is not None and getattr(self, "ub", None) is not None and value > self.ub:
raise ValueError("Cannot set a lower bound that is greater than the upper bound.")

def _check_valid_upper_bound(self, value):
if not (value is None or isinstance(value, (int, float))):
if not (value is None or is_numeric(value)):
raise TypeError("Constraint bounds must be numeric or None, not {}".format(type(value)))
if value is not None and getattr(self, "lb", None) is not None and value < self.lb:
raise ValueError("Cannot set an upper bound that is less than the lower bound.")
Expand Down
31 changes: 31 additions & 0 deletions optlang/tests/abstract_test_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import json
import copy
import os
import sympy

__test__ = False

Expand Down Expand Up @@ -79,8 +80,10 @@ def test_setting_lower_bound_higher_than_upper_bound_raises(self):

def test_setting_nonnumerical_bounds_raises(self):
self.assertRaises(TypeError, setattr, self.var, "lb", "Ministrone")
self.assertRaises(TypeError, setattr, self.var, "ub", "Ministrone")
self.model.add(self.var)
self.assertRaises(TypeError, setattr, self.model.variables[0], 'lb', 'Chicken soup')
self.assertRaises(TypeError, setattr, self.model.variables[0], 'ub', 'Chicken soup')

@abc.abstractmethod
def test_changing_variable_names_is_reflected_in_the_solver(self):
Expand All @@ -98,6 +101,10 @@ def test_setting_bounds(self):
self.model.objective.direction = "min"
self.model.optimize()
self.assertEqual(self.var.primal, -3)
self.var.lb = sympy.Number(-4) # Sympy numbers should be valid bounds
self.model.optimize()
self.assertEqual(self.var.primal, -4)


def test_set_bounds_to_none(self):
model = self.model
Expand Down Expand Up @@ -180,11 +187,35 @@ def test_setting_lower_bound_higher_than_upper_bound_raises(self):

self.assertRaises(ValueError, self.interface.Constraint, 0, lb=0, ub=-1)

def test_setting_bounds(self):
var = self.interface.Variable("test", lb=-10)
c = self.interface.Constraint(var, lb=0)
model = self.interface.Model()
obj = self.interface.Objective(var)
model.add(c)
model.objective = obj

c.ub = 5
model.optimize()
self.assertEqual(var.primal, 5)
c.ub = 4
model.optimize()
self.assertEqual(var.primal, 4)
c.lb = -3
model.objective.direction = "min"
model.optimize()
self.assertEqual(var.primal, -3)
c.lb = sympy.Number(-4) # Sympy numbers should be valid bounds
model.optimize()
self.assertEqual(var.primal, -4)

def test_setting_nonnumerical_bounds_raises(self):
var = self.interface.Variable("test")
constraint = self.interface.Constraint(var, lb=0)
self.assertRaises(TypeError, setattr, constraint, "lb", "noodle soup")
self.assertRaises(TypeError, setattr, self.model.constraints[0], 'lb', 'Chicken soup')
self.assertRaises(TypeError, setattr, constraint, "ub", "noodle soup")
self.assertRaises(TypeError, setattr, self.model.constraints[0], 'ub', 'Chicken soup')

def test_set_constraint_bounds_to_none(self):
model = self.interface.Model()
Expand Down
7 changes: 7 additions & 0 deletions optlang/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,13 @@ def method_inheritdocstring(mthd):
pass


def is_numeric(obj):
if isinstance(obj, (int, float)) or getattr(obj, "is_Number", False):
return True
else:
return False


def expr_to_json(expr):
"""
Converts a Sympy expression to a json-compatible tree-structure.
Expand Down

0 comments on commit f8cef9c

Please sign in to comment.