Skip to content

Commit 78ad7e4

Browse files
authored
Consider constraints with empty linexpr correctly (#237)
Closes #213
1 parent 7592c6c commit 78ad7e4

File tree

5 files changed

+55
-10
lines changed

5 files changed

+55
-10
lines changed

mip/cbc.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1405,20 +1405,23 @@ def var_get_column(self, var: "Var") -> Column:
14051405

14061406
def add_constr(self, lin_expr: LinExpr, name: str = ""):
14071407
# collecting linear expression data
1408-
numnz = len(lin_expr.expr)
1408+
1409+
# In case of empty linear expression add dummy row
1410+
# by setting first index of row explicitly with 0
1411+
numnz = len(lin_expr.expr) or 1
14091412

14101413
if numnz > self.iidx_space:
14111414
self.iidx_space = max(numnz, self.iidx_space * 2)
14121415
self.iidx = ffi.new("int[%d]" % self.iidx_space)
14131416
self.dvec = ffi.new("double[%d]" % self.iidx_space)
14141417

14151418
# cind = self.iidx
1416-
self.iidx = [var.idx for var in lin_expr.expr.keys()]
1419+
self.iidx = [var.idx for var in lin_expr.expr.keys()] or [0]
14171420

14181421
# cind = ffi.new("int[]", [var.idx for var in lin_expr.expr.keys()])
14191422
# cval = ffi.new("double[]", [coef for coef in lin_expr.expr.values()])
14201423
# cval = self.dvec
1421-
self.dvec = [coef for coef in lin_expr.expr.values()]
1424+
self.dvec = [coef for coef in lin_expr.expr.values()] or [0]
14221425

14231426
# constraint sense and rhs
14241427
sense = lin_expr.sense.encode("utf-8")

mip/constants.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ class OptimizationStatus(Enum):
133133
CUTOFF = 7
134134
"""No feasible solution exists for the current cutoff"""
135135

136+
INF_OR_UNBD = 8
137+
"""Special state for gurobi solver. In some cases gurobi could not
138+
determine if the problem is infeasible or unbounded due to application
139+
of dual reductions (when active) during presolve."""
140+
136141
OTHER = 10000
137142

138143

mip/gurobi.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -775,7 +775,14 @@ def callback(
775775
if status == 3: # INFEASIBLE
776776
return OptimizationStatus.INFEASIBLE
777777
if status == 4: # INF_OR_UNBD
778-
return OptimizationStatus.UNBOUNDED
778+
# Special case by gurobi, where an additional run has to be made
779+
# to determine infeasibility or unbounded problem
780+
# For this run dual reductions must be disabled
781+
# See gurobi support article online - How do I resolve the error "Model is infeasible or unbounded"?
782+
# self.set_int_param("DualReductions", 0)
783+
# GRBoptimize(self._model)
784+
# return OptimizationStatus.INFEASIBLE if self.get_int_attr("Status") == 3 else OptimizationStatus.UNBOUNDED
785+
return OptimizationStatus.INF_OR_UNBD
779786
if status == 5: # UNBOUNDED
780787
return OptimizationStatus.UNBOUNDED
781788
if status == 6: # CUTOFF

mip/model.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -331,11 +331,7 @@ def add_constr(
331331
raise mip.InvalidLinExpr(
332332
"A boolean (true/false) cannot be used as a constraint."
333333
)
334-
# TODO: some tests use empty linear constraints, which ideally should not happen
335-
# if len(lin_expr) == 0:
336-
# raise mip.InvalidLinExpr(
337-
# "An empty linear expression cannot be used as a constraint."
338-
# )
334+
339335
return self.constrs.add(lin_expr, name, priority)
340336

341337
def add_lazy_constr(self: "Model", expr: "mip.LinExpr"):

test/mip_test.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import pytest
44
import networkx as nx
55
from mip import Model, xsum, OptimizationStatus, MAXIMIZE, BINARY, INTEGER
6-
from mip import ConstrsGenerator, CutPool, maximize, CBC, GUROBI, Column
6+
from mip import ConstrsGenerator, CutPool, maximize, CBC, GUROBI, Column, Constr
77
from os import environ
88
import math
99

@@ -653,3 +653,37 @@ def test_float(solver: str, val: int):
653653
assert y.x == float(y)
654654
# test linear expressions.
655655
assert float(x + y) == (x + y).x
656+
657+
658+
@pytest.mark.parametrize("solver", SOLVERS)
659+
def test_empty_useless_constraint_is_considered(solver: str):
660+
m = Model("empty_constraint", solver_name=solver)
661+
x = m.add_var(name="x")
662+
y = m.add_var(name="y")
663+
m.add_constr(xsum([]) <= 1, name="c_empty") # useless, empty constraint
664+
m.add_constr(x + y <= 5, name="c1")
665+
m.add_constr(2 * x + y <= 6, name="c2")
666+
m.objective = maximize(x + 2 * y)
667+
m.optimize()
668+
# check objective
669+
assert m.status == OptimizationStatus.OPTIMAL
670+
assert abs(m.objective.x - 10) < TOL
671+
# check that all names of constraints could be queried
672+
assert {c.name for c in m.constrs} == {"c1", "c2", "c_empty"}
673+
assert all(isinstance(m.constr_by_name(c_name), Constr) for c_name in ("c1", "c2", "c_empty"))
674+
675+
676+
@pytest.mark.parametrize("solver", SOLVERS)
677+
def test_empty_contradictory_constraint_is_considered(solver: str):
678+
m = Model("empty_constraint", solver_name=solver)
679+
x = m.add_var(name="x")
680+
y = m.add_var(name="y")
681+
m.add_constr(xsum([]) <= -1, name="c_contra") # contradictory empty constraint
682+
m.add_constr(x + y <= 5, name="c1")
683+
m.objective = maximize(x + 2 * y)
684+
m.optimize()
685+
# assert infeasibility of problem
686+
assert m.status in (OptimizationStatus.INF_OR_UNBD, OptimizationStatus.INFEASIBLE)
687+
# check that all names of constraints could be queried
688+
assert {c.name for c in m.constrs} == {"c1", "c_contra"}
689+
assert all(isinstance(m.constr_by_name(c_name), Constr) for c_name in ("c1", "c_contra"))

0 commit comments

Comments
 (0)