Skip to content

Commit

Permalink
Catch Cplex errors and raise optlang.SolverError (#96)
Browse files Browse the repository at this point in the history
* feat: add `SolverError` exception

This introduces the unified optlang error that should be reraised anywhere that
solver specific errors can occur. Third party code can then rely on catching the
optlang error only.

* feat: catch Cplex errors and raise SolverError

* feat: add infeasible MIP test cases

* fix: correct uncovered problems in properties

* feat: add test cases for unsolved LP

* chore: fix flake8 errors
  • Loading branch information
Midnighter authored and KristianJensen committed Apr 6, 2017
1 parent 7a15952 commit eae7c1d
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 32 deletions.
87 changes: 56 additions & 31 deletions optlang/cplex_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,19 @@
import os
from six.moves import StringIO

log = logging.getLogger(__name__)

import sympy
from sympy.core.add import _unevaluated_Add
from sympy.core.mul import _unevaluated_Mul
from sympy.core.singleton import S
import cplex
from cplex.exceptions import CplexSolverError

from optlang import interface
from optlang.util import inheritdocstring, TemporaryFilename
from optlang.expression_parsing import parse_optimization_expression
from optlang.exceptions import SolverError

log = logging.getLogger(__name__)

Zero = S.Zero
One = S.One
Expand Down Expand Up @@ -190,17 +193,21 @@ def type(self, value):
super(Variable, Variable).type.fset(self, value)

def _get_primal(self):
primal_from_solver = self.problem.problem.solution.get_values(self.name)
return primal_from_solver
try:
return self.problem.problem.solution.get_values(self.name)
except CplexSolverError as err:
raise SolverError(str(err))

@property
def dual(self):
if self.problem is not None:
if self.problem.is_integer:
raise ValueError("Dual values are not well-defined for integer problems")
return self.problem.problem.solution.get_reduced_costs(self.name)
else:
if self.problem is None:
return None
if self.problem.is_integer:
raise ValueError("Dual values are not well-defined for integer problems")
try:
return self.problem.problem.solution.get_reduced_costs(self.name)
except CplexSolverError as err:
raise SolverError(str(err))

@interface.Variable.name.setter
def name(self, value):
Expand Down Expand Up @@ -254,21 +261,24 @@ def problem(self, value):

@property
def primal(self):
if self.problem is not None:
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:
if self.problem is None:
return None
try:
# return self._round_primal_to_bounds(primal_from_solver) # Test assertions fail
return self.problem.problem.solution.get_activity_levels(self.name)
except CplexSolverError as err:
raise SolverError(str(err))

@property
def dual(self):
if self.problem is not None:
if self.problem.is_integer:
raise ValueError("Dual values are not well-defined for integer problems")
return self.problem.problem.solution.get_dual_values(self.name)
else:
if self.problem is None:
return None
if self.problem.is_integer:
raise ValueError("Dual values are not well-defined for integer problems")
try:
return self.problem.problem.solution.get_dual_values(self.name)
except CplexSolverError as err:
raise SolverError(str(err))

@interface.Constraint.name.setter
def name(self, value):
Expand Down Expand Up @@ -335,10 +345,12 @@ def __init__(self, expression, sloppy=False, **kwargs):

@property
def value(self):
if getattr(self, 'problem', None) is not None:
return self.problem.problem.solution.get_objective_value()
else:
if getattr(self, 'problem', None) is None:
return None
try:
return self.problem.problem.solution.get_objective_value()
except CplexSolverError as err:
raise SolverError(str(err))

@interface.Objective.direction.setter
def direction(self, value):
Expand Down Expand Up @@ -711,32 +723,42 @@ def _set_objective_direction(self, direction):

@property
def primal_values(self):
primal_values = collections.OrderedDict()
for variable, primal in zip(self.variables, self.problem.solution.get_values()):
primal_values[variable.name] = variable._round_primal_to_bounds(primal)
try:
primal_values = collections.OrderedDict(
(variable.name, variable._round_primal_to_bounds(primal))
for variable, primal in zip(self.variables, self.problem.solution.get_values())
)
except CplexSolverError as err:
raise SolverError(str(err))
return primal_values

@property
def reduced_costs(self):
if self.is_integer:
raise ValueError("Dual values are not well-defined for integer problems")
else:
try:
return collections.OrderedDict(
zip((variable.name for variable in self.variables), self.problem.solution.get_reduced_costs()))
except CplexSolverError as err:
raise SolverError(str(err))

@property
def constraint_values(self):
return collections.OrderedDict(
zip((constraint.name for constraint in self.constraints), self.problem.solution.get_activity_levels()))

try:
return collections.OrderedDict(
zip((constraint.name for constraint in self.constraints), self.problem.solution.get_activity_levels()))
except CplexSolverError as err:
raise SolverError(str(err))

@property
def shadow_prices(self):
if self.is_integer:
raise ValueError("Dual values are not well-defined for integer problems")
else:
try:
return collections.OrderedDict(
zip((constraint.name for constraint in self.constraints), self.problem.solution.get_dual_values()))
except CplexSolverError as err:
raise SolverError(str(err))

@property
def is_integer(self):
Expand All @@ -751,7 +773,10 @@ def to_lp(self):
return lp_form

def _optimize(self):
self.problem.solve()
try:
self.problem.solve()
except CplexSolverError as err:
raise SolverError(str(err))
cplex_status = self.problem.solution.get_status()
self._original_status = self.problem.solution.get_status_string()
status = _CPLEX_STATUS_TO_STATUS[cplex_status]
Expand Down
7 changes: 7 additions & 0 deletions optlang/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
# limitations under the License.


class SolverError(Exception):
"""Reraise solver specific errors with this unified optlang error instead."""

def __init__(self, message, **kwargs):
super(SolverError, self).__init__(message, **kwargs)


class ContainerAlreadyContains(Exception):
"""
This exception is raised when the name of an object being added to a Container is already
Expand Down
130 changes: 129 additions & 1 deletion optlang/tests/test_cplex_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ def test_fail(self):

import nose
from optlang.tests import abstract_test_cases
from optlang.exceptions import SolverError
from optlang.interface import OPTIMAL, INFEASIBLE

from optlang.cplex_interface import Variable, Constraint, Model, Objective
from optlang import cplex_interface
Expand Down Expand Up @@ -443,7 +445,7 @@ def test_non_convex_obj(self):
obj = Objective(self.x1 * self.x2, direction="min")
model.objective = obj
model.configuration.solution_target = "convex"
self.assertRaises(CplexSolverError, model.optimize)
self.assertRaises(SolverError, model.optimize)
model.configuration.solution_target = "local"
model.configuration.qp_method = "barrier"
model.optimize()
Expand Down Expand Up @@ -497,6 +499,132 @@ def test_qp_non_convex(self):
model.optimize()
self.assertAlmostEqual(model.objective.value, 2441.999999971)

class InfeasibleMIPTestCase(unittest.TestCase):

interface = cplex_interface

def setUp(self):
model = self.interface.Model()
x = self.interface.Variable('x', lb=0, ub=10)
y = self.interface.Variable('y', lb=0, ub=10)
k = self.interface.Variable('k', type='binary')
i = self.interface.Variable('i', type='binary')
constr1 = self.interface.Constraint(1. * x + y, lb=3, name="constr1")
constr2 = self.interface.Constraint(x - k, ub=-10, name="constr2")
constr3 = self.interface.Constraint(y - i, ub=-10, name="constr3")
obj = self.interface.Objective(2 * x + y)
model.add(x)
model.add(y)
model.add(k)
model.add(i)
model.add(constr1)
model.add(constr2)
model.add(constr3)
model.objective = obj
model.optimize()
self.model = model
self.continuous_var = x
self.binary_var = k
self.constraint = constr1

def test_infeasible(self):
self.assertEqual(self.model.status, INFEASIBLE)

def test_objective_value(self):
with self.assertRaises(SolverError) as context:
self.model.objective.value
self.assertIn("CPLEX Error 1217", str(context.exception))

def test_variable_primal(self):
with self.assertRaises(SolverError) as context:
self.continuous_var.primal
self.assertIn("CPLEX Error 1217", str(context.exception))

def test_binary_variable_primal(self):
with self.assertRaises(SolverError) as context:
self.binary_var.primal
self.assertIn("CPLEX Error 1217", str(context.exception))

def test_constraint_primal(self):
with self.assertRaises(SolverError) as context:
self.constraint.primal
self.assertIn("CPLEX Error 1217", str(context.exception))

def test_primal_values(self):
with self.assertRaises(SolverError) as context:
self.model.primal_values
self.assertIn("CPLEX Error 1217", str(context.exception))

def test_constraint_values(self):
with self.assertRaises(SolverError) as context:
self.model.constraint_values
self.assertIn("CPLEX Error 1217", str(context.exception))


class UnsolvedTestCase(unittest.TestCase):

interface = cplex_interface

def setUp(self):
model = self.interface.Model()
x = self.interface.Variable('x', lb=0, ub=10)
constr1 = self.interface.Constraint(1. * x, lb=3, name="constr1")
obj = self.interface.Objective(2 * x)
model.add(x)
model.add(constr1)
model.objective = obj
self.model = model
self.continuous_var = x
self.constraint = constr1

def test_status(self):
self.assertIs(self.model.status, None)

def test_objective_value(self):
with self.assertRaises(SolverError) as context:
self.model.objective.value
self.assertIn("CPLEX Error 1217", str(context.exception))

def test_variable_primal(self):
with self.assertRaises(SolverError) as context:
self.continuous_var.primal
self.assertIn("CPLEX Error 1217", str(context.exception))

def test_variable_dual(self):
with self.assertRaises(SolverError) as context:
self.continuous_var.dual
self.assertIn("CPLEX Error 1217", str(context.exception))

def test_constraint_primal(self):
with self.assertRaises(SolverError) as context:
self.constraint.primal
self.assertIn("CPLEX Error 1217", str(context.exception))

def test_constraint_dual(self):
with self.assertRaises(SolverError) as context:
self.constraint.dual
self.assertIn("CPLEX Error 1217", str(context.exception))

def test_primal_values(self):
with self.assertRaises(SolverError) as context:
self.model.primal_values
self.assertIn("CPLEX Error 1217", str(context.exception))

def test_constraint_values(self):
with self.assertRaises(SolverError) as context:
self.model.constraint_values
self.assertIn("CPLEX Error 1217", str(context.exception))

def test_reduced_costs(self):
with self.assertRaises(SolverError) as context:
self.model.reduced_costs
self.assertIn("CPLEX Error 1217", str(context.exception))

def test_shadow_prices(self):
with self.assertRaises(SolverError) as context:
self.model.shadow_prices
self.assertIn("CPLEX Error 1217", str(context.exception))


if __name__ == '__main__':
nose.runmodule()

0 comments on commit eae7c1d

Please sign in to comment.