Skip to content

Commit

Permalink
Fix MIP dual value behaviour (#93)
Browse files Browse the repository at this point in the history
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

* fix: cplex problem type change back to LP

When going from MILP to LP cplex problem type needs to be explicitly
changed back to LP

* feat: Model is_integer method

* test: add tests for is_integer and integer duals

* fix: flake8

* test: move tests to cplex interface for now

* fix: add reason to skipTest

* chore: raise ValueErrors for duals in IPs

* chore: raise ValueError when getting IP duals (gurobi)

* feat: is_integer in gurobi

* test: add test for getting duals in batch for MIP

* fix: remove unnecessary checks
  • Loading branch information
KristianJensen authored Apr 6, 2017
1 parent 2ec31da commit 7a15952
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 26 deletions.
43 changes: 33 additions & 10 deletions optlang/cplex_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,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 @@ -175,6 +181,12 @@ 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):
Expand All @@ -184,8 +196,8 @@ def _get_primal(self):
@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
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:
return None
Expand Down Expand Up @@ -252,22 +264,23 @@ 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 self.problem.problem.solution.get_dual_values(self.name)
else:
return None

@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 @@ -705,8 +718,11 @@ def primal_values(self):

@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")
else:
return collections.OrderedDict(
zip((variable.name for variable in self.variables), self.problem.solution.get_reduced_costs()))

@property
def constraint_values(self):
Expand All @@ -716,8 +732,15 @@ def constraint_values(self):

@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")
else:
return collections.OrderedDict(
zip((constraint.name for constraint in self.constraints), self.problem.solution.get_dual_values()))

@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 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
9 changes: 9 additions & 0 deletions optlang/gurobi_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ def 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 self._internal_variable.getAttr('RC')
else:
return None
Expand Down Expand Up @@ -253,6 +255,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 self._internal_constraint.Pi
else:
return None
Expand Down Expand Up @@ -638,6 +642,11 @@ def _remove_constraints(self, constraints):
for internal_constraint in internal_constraints:
self.problem.remove(internal_constraint)

@property
def is_integer(self):
self.problem.update()
return self.problem.NumIntVars > 0


if __name__ == '__main__':
x = Variable('x', lb=0, ub=10)
Expand Down
4 changes: 4 additions & 0 deletions optlang/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -1246,6 +1246,10 @@ def shadow_prices(self):
# Fallback, if nothing faster is available
return collections.OrderedDict([(constraint.name, constraint.dual) for constraint in self.constraints])

@property
def is_integer(self):
return any(var.type in ("integer", "binary") for var in self.variables)

def __str__(self):
if hasattr(self, "to_lp"):
return self.to_lp()
Expand Down
77 changes: 77 additions & 0 deletions optlang/tests/abstract_test_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import copy
import os
import sympy
from functools import partial

__test__ = False

Expand Down Expand Up @@ -697,6 +698,82 @@ def test_constraint_set_linear_coefficients(self):
self.assertEqual(model.optimize(), optlang.interface.OPTIMAL)
self.assertAlmostEqual(x.primal, 0.5)

def test_is_integer(self):
model = self.model
self.assertFalse(model.is_integer)

model.variables[0].type = "integer"
self.assertTrue(model.is_integer)

model.variables[0].type = "continuous"
model.variables[1].type = "binary"
self.assertTrue(model.is_integer)

model.variables[1].type = "continuous"
self.assertFalse(model.is_integer)

def test_integer_variable_dual(self):
model = self.interface.Model()
x = self.interface.Variable("x", lb=0)
y = self.interface.Variable("y", lb=0)
c = self.interface.Constraint(x + y, ub=1)
model.add(c)
model.objective = self.interface.Objective(x)

model.optimize()
self.assertEqual(y.dual, -1)

x.type = "integer"
model.optimize()
self.assertRaises(ValueError, partial(getattr, y, "dual"))

x.type = "continuous"
model.optimize()
self.assertEqual(y.dual, -1)

def test_integer_constraint_dual(self):
model = self.interface.Model()
x = self.interface.Variable("x")
c = self.interface.Constraint(x, ub=1)
model.add(c)
model.objective = self.interface.Objective(x)

model.optimize()
self.assertEqual(c.dual, 1)

x.type = "integer"
model.optimize()
self.assertRaises(ValueError, partial(getattr, c, "dual"))

x.type = "continuous"
model.optimize()
self.assertEqual(c.dual, 1)

def test_integer_batch_duals(self):
model = self.interface.Model()
x = self.interface.Variable("x")
c = self.interface.Constraint(x, ub=1)
model.add(c)
model.objective = self.interface.Objective(x)
model.optimize()

self.assertEqual(model.reduced_costs[x.name], 0)
self.assertEqual(model.shadow_prices[c.name], 1)

x.type = "integer"
model.optimize()

with self.assertRaises(ValueError):
model.reduced_costs
with self.assertRaises(ValueError):
model.shadow_prices

x.type = "continuous"
model.optimize()

self.assertEqual(model.reduced_costs[x.name], 0)
self.assertEqual(model.shadow_prices[c.name], 1)


@six.add_metaclass(abc.ABCMeta)
class AbstractConfigurationTestCase(unittest.TestCase):
Expand Down
12 changes: 12 additions & 0 deletions optlang/tests/test_scipy_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,3 +288,15 @@ def test_scipy_coefficient_dict(self):
model.add(c)
model.objective = obj
self.assertEqual(model.optimize(), optlang.interface.OPTIMAL)

def test_is_integer(self):
self.skipTest("No integers with scipy")

def test_integer_variable_dual(self):
self.skipTest("No duals with scipy")

def test_integer_constraint_dual(self):
self.skipTest("No duals with scipy")

def test_integer_batch_duals(self):
self.skipTest("No duals with scipy")

0 comments on commit 7a15952

Please sign in to comment.