diff --git a/optlang/cplex_interface.py b/optlang/cplex_interface.py index 9124ac56..dbfa6b47 100644 --- a/optlang/cplex_interface.py +++ b/optlang/cplex_interface.py @@ -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""" @@ -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): @@ -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 @@ -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): @@ -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): @@ -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() diff --git a/optlang/glpk_interface.py b/optlang/glpk_interface.py index 589a652b..3994f489 100644 --- a/optlang/glpk_interface.py +++ b/optlang/glpk_interface.py @@ -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 @@ -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 @@ -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 @@ -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): @@ -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 diff --git a/optlang/gurobi_interface.py b/optlang/gurobi_interface.py index 043d6aed..977d3ed3 100644 --- a/optlang/gurobi_interface.py +++ b/optlang/gurobi_interface.py @@ -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 @@ -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 @@ -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) diff --git a/optlang/interface.py b/optlang/interface.py index 9dac49d7..8005f211 100644 --- a/optlang/interface.py +++ b/optlang/interface.py @@ -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() diff --git a/optlang/tests/abstract_test_cases.py b/optlang/tests/abstract_test_cases.py index d4d67c48..4e64415b 100644 --- a/optlang/tests/abstract_test_cases.py +++ b/optlang/tests/abstract_test_cases.py @@ -26,6 +26,7 @@ import copy import os import sympy +from functools import partial __test__ = False @@ -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): diff --git a/optlang/tests/test_scipy_interface.py b/optlang/tests/test_scipy_interface.py index f3ccc4a9..302c3f1a 100644 --- a/optlang/tests/test_scipy_interface.py +++ b/optlang/tests/test_scipy_interface.py @@ -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")