Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix MIP dual value behaviour #93

Merged
merged 11 commits into from
Apr 6, 2017
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")