Skip to content

Commit

Permalink
Consider constraints with empty linexpr correctly (#237)
Browse files Browse the repository at this point in the history
Closes #213
  • Loading branch information
sebheger authored Jan 19, 2024
1 parent 7592c6c commit 78ad7e4
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 10 deletions.
9 changes: 6 additions & 3 deletions mip/cbc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1405,20 +1405,23 @@ 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)
self.iidx = ffi.new("int[%d]" % self.iidx_space)
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")
Expand Down
5 changes: 5 additions & 0 deletions mip/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
9 changes: 8 additions & 1 deletion mip/gurobi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 1 addition & 5 deletions mip/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down
36 changes: 35 additions & 1 deletion test/mip_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"))

0 comments on commit 78ad7e4

Please sign in to comment.