diff --git a/mip/cbc.py b/mip/cbc.py index 9c6e9a95..13824650 100644 --- a/mip/cbc.py +++ b/mip/cbc.py @@ -1405,7 +1405,10 @@ def var_get_column(self, var: "Var") -> Column: def add_constr(self, lin_expr: LinExpr, name: str = ""): # collecting linear expression data - numnz = len(lin_expr.expr) + + # In case of empty linear expression add dummy row + # by setting first index of row explicitly with 0 + numnz = len(lin_expr.expr) or 1 if numnz > self.iidx_space: self.iidx_space = max(numnz, self.iidx_space * 2) @@ -1413,12 +1416,12 @@ def add_constr(self, lin_expr: LinExpr, name: str = ""): self.dvec = ffi.new("double[%d]" % self.iidx_space) # cind = self.iidx - self.iidx = [var.idx for var in lin_expr.expr.keys()] + self.iidx = [var.idx for var in lin_expr.expr.keys()] or [0] # cind = ffi.new("int[]", [var.idx for var in lin_expr.expr.keys()]) # cval = ffi.new("double[]", [coef for coef in lin_expr.expr.values()]) # cval = self.dvec - self.dvec = [coef for coef in lin_expr.expr.values()] + self.dvec = [coef for coef in lin_expr.expr.values()] or [0] # constraint sense and rhs sense = lin_expr.sense.encode("utf-8") diff --git a/mip/constants.py b/mip/constants.py index cf943dc9..c8863fd4 100644 --- a/mip/constants.py +++ b/mip/constants.py @@ -133,6 +133,11 @@ class OptimizationStatus(Enum): CUTOFF = 7 """No feasible solution exists for the current cutoff""" + INF_OR_UNBD = 8 + """Special state for gurobi solver. In some cases gurobi could not + determine if the problem is infeasible or unbounded due to application + of dual reductions (when active) during presolve.""" + OTHER = 10000 diff --git a/mip/gurobi.py b/mip/gurobi.py index c7adcfaa..ba1dde73 100644 --- a/mip/gurobi.py +++ b/mip/gurobi.py @@ -775,7 +775,14 @@ def callback( if status == 3: # INFEASIBLE return OptimizationStatus.INFEASIBLE if status == 4: # INF_OR_UNBD - return OptimizationStatus.UNBOUNDED + # Special case by gurobi, where an additional run has to be made + # to determine infeasibility or unbounded problem + # For this run dual reductions must be disabled + # See gurobi support article online - How do I resolve the error "Model is infeasible or unbounded"? + # self.set_int_param("DualReductions", 0) + # GRBoptimize(self._model) + # return OptimizationStatus.INFEASIBLE if self.get_int_attr("Status") == 3 else OptimizationStatus.UNBOUNDED + return OptimizationStatus.INF_OR_UNBD if status == 5: # UNBOUNDED return OptimizationStatus.UNBOUNDED if status == 6: # CUTOFF diff --git a/mip/model.py b/mip/model.py index ed690cc7..e9297cd1 100644 --- a/mip/model.py +++ b/mip/model.py @@ -331,11 +331,7 @@ def add_constr( raise mip.InvalidLinExpr( "A boolean (true/false) cannot be used as a constraint." ) - # TODO: some tests use empty linear constraints, which ideally should not happen - # if len(lin_expr) == 0: - # raise mip.InvalidLinExpr( - # "An empty linear expression cannot be used as a constraint." - # ) + return self.constrs.add(lin_expr, name, priority) def add_lazy_constr(self: "Model", expr: "mip.LinExpr"): diff --git a/test/mip_test.py b/test/mip_test.py index 06ea3d37..265383f8 100644 --- a/test/mip_test.py +++ b/test/mip_test.py @@ -3,7 +3,7 @@ import pytest import networkx as nx from mip import Model, xsum, OptimizationStatus, MAXIMIZE, BINARY, INTEGER -from mip import ConstrsGenerator, CutPool, maximize, CBC, GUROBI, Column +from mip import ConstrsGenerator, CutPool, maximize, CBC, GUROBI, Column, Constr from os import environ import math @@ -653,3 +653,37 @@ def test_float(solver: str, val: int): assert y.x == float(y) # test linear expressions. assert float(x + y) == (x + y).x + + +@pytest.mark.parametrize("solver", SOLVERS) +def test_empty_useless_constraint_is_considered(solver: str): + m = Model("empty_constraint", solver_name=solver) + x = m.add_var(name="x") + y = m.add_var(name="y") + m.add_constr(xsum([]) <= 1, name="c_empty") # useless, empty constraint + m.add_constr(x + y <= 5, name="c1") + m.add_constr(2 * x + y <= 6, name="c2") + m.objective = maximize(x + 2 * y) + m.optimize() + # check objective + assert m.status == OptimizationStatus.OPTIMAL + assert abs(m.objective.x - 10) < TOL + # check that all names of constraints could be queried + assert {c.name for c in m.constrs} == {"c1", "c2", "c_empty"} + assert all(isinstance(m.constr_by_name(c_name), Constr) for c_name in ("c1", "c2", "c_empty")) + + +@pytest.mark.parametrize("solver", SOLVERS) +def test_empty_contradictory_constraint_is_considered(solver: str): + m = Model("empty_constraint", solver_name=solver) + x = m.add_var(name="x") + y = m.add_var(name="y") + m.add_constr(xsum([]) <= -1, name="c_contra") # contradictory empty constraint + m.add_constr(x + y <= 5, name="c1") + m.objective = maximize(x + 2 * y) + m.optimize() + # assert infeasibility of problem + assert m.status in (OptimizationStatus.INF_OR_UNBD, OptimizationStatus.INFEASIBLE) + # check that all names of constraints could be queried + assert {c.name for c in m.constrs} == {"c1", "c_contra"} + assert all(isinstance(m.constr_by_name(c_name), Constr) for c_name in ("c1", "c_contra"))