diff --git a/miplearn/components/steps/drop_redundant.py b/miplearn/components/steps/drop_redundant.py index f22cb6c..ba21ee6 100644 --- a/miplearn/components/steps/drop_redundant.py +++ b/miplearn/components/steps/drop_redundant.py @@ -32,7 +32,7 @@ class DropRedundantInequalitiesStep(Component): classifier=CountingClassifier(), threshold=0.95, slack_tolerance=1e-5, - check_feasibility=False, + check_feasibility=True, violation_tolerance=1e-5, max_iterations=3, ): @@ -208,6 +208,8 @@ class DropRedundantInequalitiesStep(Component): return False if self.current_iteration >= self.max_iterations: return False + if solver.internal_solver.is_infeasible(): + return False self.current_iteration += 1 logger.debug("Checking that dropped constraints are satisfied...") constraints_to_add = [] diff --git a/miplearn/solvers/gurobi.py b/miplearn/solvers/gurobi.py index 2cced0f..b6ab663 100644 --- a/miplearn/solvers/gurobi.py +++ b/miplearn/solvers/gurobi.py @@ -342,7 +342,7 @@ class GurobiSolver(InternalSolver): self.model.remove(constr) return cobj - def is_constraint_satisfied(self, cobj, tol=1e-5): + def is_constraint_satisfied(self, cobj, tol=1e-6): lhs, sense, rhs, name = cobj if self.cb_where is not None: lhs_value = lhs.getConstant() @@ -378,6 +378,7 @@ class GurobiSolver(InternalSolver): def relax(self) -> None: assert self.model is not None + self.model.update() self.model = self.model.relax() self._update_vars() diff --git a/miplearn/solvers/internal.py b/miplearn/solvers/internal.py index d64457f..89543aa 100644 --- a/miplearn/solvers/internal.py +++ b/miplearn/solvers/internal.py @@ -173,7 +173,7 @@ class InternalSolver(ABC): pass @abstractmethod - def is_constraint_satisfied(self, cobj: Constraint) -> bool: + def is_constraint_satisfied(self, cobj: Constraint, tol: float = 1e-6) -> bool: """ Returns True if the current solution satisfies the given constraint. """ diff --git a/miplearn/solvers/pyomo/base.py b/miplearn/solvers/pyomo/base.py index f6f7849..ebed5c0 100644 --- a/miplearn/solvers/pyomo/base.py +++ b/miplearn/solvers/pyomo/base.py @@ -308,7 +308,7 @@ class BasePyomoSolver(InternalSolver): def extract_constraint(self, cid: str) -> Constraint: raise Exception("Not implemented") - def is_constraint_satisfied(self, cobj: Constraint) -> bool: + def is_constraint_satisfied(self, cobj: Constraint, tol: float = 1e-6) -> bool: raise Exception("Not implemented") def is_infeasible(self) -> bool: diff --git a/tests/components/steps/test_drop_redundant.py b/tests/components/steps/test_drop_redundant.py index d673b1a..5ff5215 100644 --- a/tests/components/steps/test_drop_redundant.py +++ b/tests/components/steps/test_drop_redundant.py @@ -6,13 +6,14 @@ from unittest.mock import Mock, call import numpy as np -from miplearn import RelaxIntegralityStep, BasePyomoSolver +from miplearn import RelaxIntegralityStep, GurobiSolver from miplearn.classifiers import Classifier from miplearn.components.steps.drop_redundant import DropRedundantInequalitiesStep from miplearn.instance import Instance from miplearn.solvers.internal import InternalSolver from miplearn.solvers.learning import LearningSolver -from tests.solvers import _get_knapsack_instance +from tests.fixtures.infeasible import get_infeasible_instance +from tests.fixtures.redundant import get_instance_with_redundancy def _setup(): @@ -30,6 +31,7 @@ def _setup(): ) internal.extract_constraint = Mock(side_effect=lambda cid: "<%s>" % cid) internal.is_constraint_satisfied = Mock(return_value=False) + internal.is_infeasible = Mock(return_value=False) instance = Mock(spec=Instance) instance.get_constraint_features = Mock( @@ -399,14 +401,19 @@ def test_x_multiple_solves(): def test_usage(): - solver = LearningSolver( - components=[ - RelaxIntegralityStep(), - DropRedundantInequalitiesStep(), - ] - ) - instance = _get_knapsack_instance(BasePyomoSolver) - # The following should not crash - solver.solve(instance) - solver.fit([instance]) - solver.solve(instance) + for internal_solver in [GurobiSolver]: + for instance in [ + get_instance_with_redundancy(internal_solver), + get_infeasible_instance(internal_solver), + ]: + solver = LearningSolver( + solver=internal_solver, + components=[ + RelaxIntegralityStep(), + DropRedundantInequalitiesStep(), + ], + ) + # The following should not crash + solver.solve(instance) + solver.fit([instance]) + solver.solve(instance) diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py new file mode 100644 index 0000000..13c148b --- /dev/null +++ b/tests/fixtures/__init__.py @@ -0,0 +1,3 @@ +# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization +# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. diff --git a/tests/fixtures/infeasible.py b/tests/fixtures/infeasible.py new file mode 100644 index 0000000..29a9684 --- /dev/null +++ b/tests/fixtures/infeasible.py @@ -0,0 +1,40 @@ +# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization +# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. + +from typing import Any + +from pyomo import environ as pe + +from miplearn.instance import Instance +from miplearn.solvers.gurobi import GurobiSolver +from miplearn.solvers.pyomo.base import BasePyomoSolver +from tests.solvers import _is_subclass_or_instance + + +class InfeasiblePyomoInstance(Instance): + def to_model(self) -> pe.ConcreteModel: + model = pe.ConcreteModel() + model.x = pe.Var([0], domain=pe.Binary) + model.OBJ = pe.Objective(expr=model.x[0], sense=pe.maximize) + model.eq = pe.Constraint(expr=model.x[0] >= 2) + return model + + +class InfeasibleGurobiInstance(Instance): + def to_model(self) -> Any: + import gurobipy as gp + from gurobipy import GRB + + model = gp.Model() + x = model.addVars(1, vtype=GRB.BINARY, name="x") + model.addConstr(x[0] >= 2) + model.setObjective(x[0]) + return model + + +def get_infeasible_instance(solver): + if _is_subclass_or_instance(solver, BasePyomoSolver): + return InfeasiblePyomoInstance() + if _is_subclass_or_instance(solver, GurobiSolver): + return InfeasibleGurobiInstance() diff --git a/tests/fixtures/redundant.py b/tests/fixtures/redundant.py new file mode 100644 index 0000000..a9e9ae6 --- /dev/null +++ b/tests/fixtures/redundant.py @@ -0,0 +1,39 @@ +# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization +# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. +from typing import Any + +from miplearn import Instance, BasePyomoSolver, GurobiSolver +import pyomo.environ as pe + +from tests.solvers import _is_subclass_or_instance + + +class PyomoInstanceWithRedundancy(Instance): + def to_model(self) -> pe.ConcreteModel: + model = pe.ConcreteModel() + model.x = pe.Var([0, 1], domain=pe.Binary) + model.OBJ = pe.Objective(expr=model.x[0] + model.x[1], sense=pe.maximize) + model.eq1 = pe.Constraint(expr=model.x[0] + model.x[1] <= 1) + model.eq2 = pe.Constraint(expr=model.x[0] + model.x[1] <= 2) + return model + + +class GurobiInstanceWithRedundancy(Instance): + def to_model(self) -> Any: + import gurobipy as gp + from gurobipy import GRB + + model = gp.Model() + x = model.addVars(2, vtype=GRB.BINARY, name="x") + model.addConstr(x[0] + x[1] <= 1) + model.addConstr(x[0] + x[1] <= 2) + model.setObjective(x[0] + x[1], GRB.MAXIMIZE) + return model + + +def get_instance_with_redundancy(solver): + if _is_subclass_or_instance(solver, BasePyomoSolver): + return PyomoInstanceWithRedundancy() + if _is_subclass_or_instance(solver, GurobiSolver): + return GurobiInstanceWithRedundancy() diff --git a/tests/solvers/__init__.py b/tests/solvers/__init__.py index ec6343b..b1c4f0b 100644 --- a/tests/solvers/__init__.py +++ b/tests/solvers/__init__.py @@ -3,11 +3,8 @@ # Released under the modified BSD license. See COPYING.md for more details. from inspect import isclass -from typing import List, Callable, Any +from typing import List, Callable -from pyomo import environ as pe - -from miplearn.instance import Instance from miplearn.problems.knapsack import KnapsackInstance, GurobiKnapsackInstance from miplearn.solvers.gurobi import GurobiSolver from miplearn.solvers.internal import InternalSolver @@ -16,27 +13,6 @@ from miplearn.solvers.pyomo.gurobi import GurobiPyomoSolver from miplearn.solvers.pyomo.xpress import XpressPyomoSolver -class InfeasiblePyomoInstance(Instance): - def to_model(self) -> pe.ConcreteModel: - model = pe.ConcreteModel() - model.x = pe.Var([0], domain=pe.Binary) - model.OBJ = pe.Objective(expr=model.x[0], sense=pe.maximize) - model.eq = pe.Constraint(expr=model.x[0] >= 2) - return model - - -class InfeasibleGurobiInstance(Instance): - def to_model(self) -> Any: - import gurobipy as gp - from gurobipy import GRB - - model = gp.Model() - x = model.addVars(1, vtype=GRB.BINARY, name="x") - model.addConstr(x[0] >= 2) - model.setObjective(x[0]) - return model - - def _is_subclass_or_instance(obj, parent_class): return isinstance(obj, parent_class) or ( isclass(obj) and issubclass(obj, parent_class) @@ -59,12 +35,5 @@ def _get_knapsack_instance(solver): assert False -def _get_infeasible_instance(solver): - if _is_subclass_or_instance(solver, BasePyomoSolver): - return InfeasiblePyomoInstance() - if _is_subclass_or_instance(solver, GurobiSolver): - return InfeasibleGurobiInstance() - - def _get_internal_solvers() -> List[Callable[[], InternalSolver]]: return [GurobiPyomoSolver, GurobiSolver, XpressPyomoSolver] diff --git a/tests/solvers/test_internal_solver.py b/tests/solvers/test_internal_solver.py index 47f9dca..b34942d 100644 --- a/tests/solvers/test_internal_solver.py +++ b/tests/solvers/test_internal_solver.py @@ -14,8 +14,8 @@ from miplearn.solvers.pyomo.base import BasePyomoSolver from . import ( _get_knapsack_instance, _get_internal_solvers, - _get_infeasible_instance, ) +from ..fixtures.infeasible import get_infeasible_instance logger = logging.getLogger(__name__) @@ -186,7 +186,7 @@ def test_relax(): def test_infeasible_instance(): for solver_class in _get_internal_solvers(): - instance = _get_infeasible_instance(solver_class) + instance = get_infeasible_instance(solver_class) solver = solver_class() solver.set_instance(instance) stats = solver.solve()