Skip to content

Commit

Permalink
1.1.4
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.

* Fix MIP dual behaviour

Raise a ValueError when trying to get constraint or variable dual values for an integer problem (instead of returning None)

* Fix: Adds functionality to automatically change the type of the cplex problem when changing variable types

* Change Module Loading Procedure

This fixes Issue #90 where, if any module fails to load, optlang itself cannot load.

* feat: cplex 12.7.1 compatibility
  • Loading branch information
KristianJensen authored Apr 6, 2017
1 parent 1948683 commit 25667ea
Show file tree
Hide file tree
Showing 9 changed files with 391 additions and 75 deletions.
54 changes: 37 additions & 17 deletions optlang/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from __future__ import absolute_import

import logging
import traceback
from optlang._version import get_versions
from optlang.util import list_available_solvers
from optlang.interface import statuses
Expand All @@ -26,26 +27,45 @@

log = logging.getLogger(__name__)

# Dictionary of available solvers
available_solvers = list_available_solvers()

# Load classes from preferred solver interface
if available_solvers['CPLEX']:
from optlang.cplex_interface import Model, Variable, Constraint, Objective
elif available_solvers["GUROBI"]:
from optlang.gurobi_interface import Model, Variable, Constraint, Objective
elif available_solvers['GLPK']:
from optlang.glpk_interface import Model, Variable, Constraint, Objective
elif available_solvers['SCIPY']:
from optlang.scipy_interface import Model, Variable, Constraint, Objective
else:
log.error('No solvers available.')

# Import all available solver interfaces
# Try to load each solver.
if available_solvers['GLPK']:
from optlang import glpk_interface
try:
from optlang import glpk_interface
except Exception:
log.error('GLPK is available but could not load with error:\n ' + str(traceback.format_exc()).strip().replace('\n','\n '))

if available_solvers['CPLEX']:
from optlang import cplex_interface
try:
from optlang import cplex_interface
except Exception:
log.error('CPLEX is available but could not load with error:\n ' + str(traceback.format_exc()).strip().replace('\n','\n '))

if available_solvers['GUROBI']:
from optlang import gurobi_interface
try:
from optlang import gurobi_interface
except Exception:
log.error('GUROBI is available but could not load with error:\n ' + str(traceback.format_exc()).strip().replace('\n','\n '))

if available_solvers['SCIPY']:
from optlang import scipy_interface
try:
from optlang import scipy_interface
except Exception:
log.error('SCIPY is available but could not load with error:\n ' + str(traceback.format_exc()).strip().replace('\n','\n '))


# Go through and find the best solver that loaded. Load that one as the default
for engine_str in ['cplex_interface','gurobi_interface','glpk_interface','scipy_interface']:
# Must check globals since not all interface variables will be defined
if engine_str in globals():
engine=globals()[engine_str]
Model = engine.Model
Variable = engine.Variable
Constraint = engine.Constraint
Objective = engine.Objective
break
else:
# We were unable to find any valid solvers
log.error('No solvers were available and/or loadable.')
121 changes: 85 additions & 36 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 @@ -114,8 +117,9 @@

# Check if each status is supported by the current cplex version
_CPLEX_STATUS_TO_STATUS = {}
_solution = cplex.Cplex().solution
for status_name, optlang_status in _STATUS_MAP.items():
cplex_status = getattr(cplex.Cplex.solution.status, status_name, None)
cplex_status = getattr(_solution.status, status_name, None)
if cplex_status is not None:
_CPLEX_STATUS_TO_STATUS[cplex_status] = optlang_status

Expand All @@ -131,6 +135,12 @@
[(val, key) for key, val in six.iteritems(_CPLEX_VTYPE_TO_VTYPE)]
)

_CPLEX_MIP_TYPES_TO_CONTINUOUS = {
cplex.Cplex.problem_type.MILP: cplex.Cplex.problem_type.LP,
cplex.Cplex.problem_type.MIQP: cplex.Cplex.problem_type.QP,
cplex.Cplex.problem_type.MIQCP: cplex.Cplex.problem_type.QCP
}


def _constraint_lb_and_ub_to_cplex_sense_rhs_and_range_value(lb, ub):
"""Helper function used by Constraint and Model"""
Expand Down Expand Up @@ -174,20 +184,30 @@ def type(self, value):
", ".join(_VTYPE_TO_CPLEX_VTYPE.keys())
)
self.problem.problem.variables.set_types(self.name, cplex_kind)
if value == "continuous" and not self.problem.is_integer:
print("hmm")
cplex_type = self.problem.problem.get_problem_type()
if cplex_type in _CPLEX_MIP_TYPES_TO_CONTINUOUS:
print("yes")
self.problem.problem.set_problem_type(_CPLEX_MIP_TYPES_TO_CONTINUOUS[cplex_type])
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.problem.get_problem_type() == self.problem.problem.problem_type.MILP: # cplex cannot determine reduced costs for MILP problems ...
return None
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 @@ -241,32 +261,36 @@ 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:
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):
old_name = getattr(self, 'name', None)
self._name = 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:
self.problem.problem.linear_constraints.set_names(old_name, value)
self.problem.constraints.update_key(old_name)
self.problem.problem.linear_constraints.set_names(self.name, value)
self.problem.constraints.update_key(self.name)
self._name = value

@interface.Constraint.lb.setter
def lb(self, value):
Expand Down Expand Up @@ -321,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 @@ -697,26 +723,46 @@ 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):
return collections.OrderedDict(
zip((variable.name for variable in self.variables), self.problem.solution.get_reduced_costs()))
if self.is_integer:
raise ValueError("Dual values are not well-defined for integer problems")
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):
return collections.OrderedDict(
zip((constraint.name for constraint in self.constraints), self.problem.solution.get_dual_values()))
if self.is_integer:
raise ValueError("Dual values are not well-defined for integer problems")
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):
return (self.problem.variables.get_num_integer() + self.problem.variables.get_num_binary()) > 0

def to_lp(self):
self.update()
Expand All @@ -727,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
34 changes: 18 additions & 16 deletions optlang/glpk_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ def _get_primal(self):
@property
def dual(self):
if self.problem:
if self.problem.is_integer:
raise ValueError("Dual values are not well-defined for integer problems")
return glp_get_col_dual(self.problem.problem, self._index)
else:
return None
Expand Down Expand Up @@ -228,6 +230,8 @@ def primal(self):
@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 glp_get_row_dual(self.problem.problem, self._index)
else:
return None
Expand Down Expand Up @@ -593,14 +597,11 @@ def primal_values(self):

@property
def reduced_costs(self):
reduced_costs = collections.OrderedDict()
is_mip = self._glpk_is_mip()
for index, variable in enumerate(self.variables):
if is_mip:
value = None
else:
value = glp_get_col_dual(self.problem, index + 1)
reduced_costs[variable.name] = value
if self.is_integer:
raise ValueError("Dual values are not well-defined for integer problems")
reduced_costs = collections.OrderedDict(
(var.name, glp_get_col_dual(self.problem, index + 1)) for index, var in enumerate(self.variables)
)
return reduced_costs

@property
Expand All @@ -617,14 +618,11 @@ def constraint_values(self):

@property
def shadow_prices(self):
is_mip = self._glpk_is_mip()
shadow_prices = collections.OrderedDict()
for index, constraint in enumerate(self.constraints):
if is_mip:
value = None
else:
value = glp_get_row_dual(self.problem, index + 1)
shadow_prices[constraint.name] = value
if self.is_integer:
raise ValueError("Dual values are not well-defined for integer problems")
shadow_prices = collections.OrderedDict(
(constraint.name, glp_get_row_dual(self.problem, index + 1)) for index, constraint in enumerate(self.constraints)
)
return shadow_prices

def to_lp(self):
Expand Down Expand Up @@ -776,6 +774,10 @@ def _glpk_set_col_bounds(self, variable):
def _glpk_is_mip(self):
return glp_get_num_int(self.problem) > 0

@property
def is_integer(self):
return self._glpk_is_mip()

def _glpk_set_row_bounds(self, constraint):
if constraint.lb is None and constraint.ub is None:
# 0.'s are ignored
Expand Down
Loading

0 comments on commit 25667ea

Please sign in to comment.