mirror of
https://github.com/ANL-CEEESA/MIPLearn.git
synced 2025-12-06 09:28:51 -06:00
Organize test fixtures; handle infeasibility in DropRedundant
This commit is contained in:
@@ -32,7 +32,7 @@ class DropRedundantInequalitiesStep(Component):
|
|||||||
classifier=CountingClassifier(),
|
classifier=CountingClassifier(),
|
||||||
threshold=0.95,
|
threshold=0.95,
|
||||||
slack_tolerance=1e-5,
|
slack_tolerance=1e-5,
|
||||||
check_feasibility=False,
|
check_feasibility=True,
|
||||||
violation_tolerance=1e-5,
|
violation_tolerance=1e-5,
|
||||||
max_iterations=3,
|
max_iterations=3,
|
||||||
):
|
):
|
||||||
@@ -208,6 +208,8 @@ class DropRedundantInequalitiesStep(Component):
|
|||||||
return False
|
return False
|
||||||
if self.current_iteration >= self.max_iterations:
|
if self.current_iteration >= self.max_iterations:
|
||||||
return False
|
return False
|
||||||
|
if solver.internal_solver.is_infeasible():
|
||||||
|
return False
|
||||||
self.current_iteration += 1
|
self.current_iteration += 1
|
||||||
logger.debug("Checking that dropped constraints are satisfied...")
|
logger.debug("Checking that dropped constraints are satisfied...")
|
||||||
constraints_to_add = []
|
constraints_to_add = []
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ class GurobiSolver(InternalSolver):
|
|||||||
self.model.remove(constr)
|
self.model.remove(constr)
|
||||||
return cobj
|
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
|
lhs, sense, rhs, name = cobj
|
||||||
if self.cb_where is not None:
|
if self.cb_where is not None:
|
||||||
lhs_value = lhs.getConstant()
|
lhs_value = lhs.getConstant()
|
||||||
@@ -378,6 +378,7 @@ class GurobiSolver(InternalSolver):
|
|||||||
|
|
||||||
def relax(self) -> None:
|
def relax(self) -> None:
|
||||||
assert self.model is not None
|
assert self.model is not None
|
||||||
|
self.model.update()
|
||||||
self.model = self.model.relax()
|
self.model = self.model.relax()
|
||||||
self._update_vars()
|
self._update_vars()
|
||||||
|
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ class InternalSolver(ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@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.
|
Returns True if the current solution satisfies the given constraint.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -308,7 +308,7 @@ class BasePyomoSolver(InternalSolver):
|
|||||||
def extract_constraint(self, cid: str) -> Constraint:
|
def extract_constraint(self, cid: str) -> Constraint:
|
||||||
raise Exception("Not implemented")
|
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")
|
raise Exception("Not implemented")
|
||||||
|
|
||||||
def is_infeasible(self) -> bool:
|
def is_infeasible(self) -> bool:
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ from unittest.mock import Mock, call
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from miplearn import RelaxIntegralityStep, BasePyomoSolver
|
from miplearn import RelaxIntegralityStep, GurobiSolver
|
||||||
from miplearn.classifiers import Classifier
|
from miplearn.classifiers import Classifier
|
||||||
from miplearn.components.steps.drop_redundant import DropRedundantInequalitiesStep
|
from miplearn.components.steps.drop_redundant import DropRedundantInequalitiesStep
|
||||||
from miplearn.instance import Instance
|
from miplearn.instance import Instance
|
||||||
from miplearn.solvers.internal import InternalSolver
|
from miplearn.solvers.internal import InternalSolver
|
||||||
from miplearn.solvers.learning import LearningSolver
|
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():
|
def _setup():
|
||||||
@@ -30,6 +31,7 @@ def _setup():
|
|||||||
)
|
)
|
||||||
internal.extract_constraint = Mock(side_effect=lambda cid: "<%s>" % cid)
|
internal.extract_constraint = Mock(side_effect=lambda cid: "<%s>" % cid)
|
||||||
internal.is_constraint_satisfied = Mock(return_value=False)
|
internal.is_constraint_satisfied = Mock(return_value=False)
|
||||||
|
internal.is_infeasible = Mock(return_value=False)
|
||||||
|
|
||||||
instance = Mock(spec=Instance)
|
instance = Mock(spec=Instance)
|
||||||
instance.get_constraint_features = Mock(
|
instance.get_constraint_features = Mock(
|
||||||
@@ -399,14 +401,19 @@ def test_x_multiple_solves():
|
|||||||
|
|
||||||
|
|
||||||
def test_usage():
|
def test_usage():
|
||||||
solver = LearningSolver(
|
for internal_solver in [GurobiSolver]:
|
||||||
components=[
|
for instance in [
|
||||||
RelaxIntegralityStep(),
|
get_instance_with_redundancy(internal_solver),
|
||||||
DropRedundantInequalitiesStep(),
|
get_infeasible_instance(internal_solver),
|
||||||
]
|
]:
|
||||||
)
|
solver = LearningSolver(
|
||||||
instance = _get_knapsack_instance(BasePyomoSolver)
|
solver=internal_solver,
|
||||||
# The following should not crash
|
components=[
|
||||||
solver.solve(instance)
|
RelaxIntegralityStep(),
|
||||||
solver.fit([instance])
|
DropRedundantInequalitiesStep(),
|
||||||
solver.solve(instance)
|
],
|
||||||
|
)
|
||||||
|
# The following should not crash
|
||||||
|
solver.solve(instance)
|
||||||
|
solver.fit([instance])
|
||||||
|
solver.solve(instance)
|
||||||
|
|||||||
3
tests/fixtures/__init__.py
vendored
Normal file
3
tests/fixtures/__init__.py
vendored
Normal file
@@ -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.
|
||||||
40
tests/fixtures/infeasible.py
vendored
Normal file
40
tests/fixtures/infeasible.py
vendored
Normal file
@@ -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()
|
||||||
39
tests/fixtures/redundant.py
vendored
Normal file
39
tests/fixtures/redundant.py
vendored
Normal file
@@ -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()
|
||||||
@@ -3,11 +3,8 @@
|
|||||||
# Released under the modified BSD license. See COPYING.md for more details.
|
# Released under the modified BSD license. See COPYING.md for more details.
|
||||||
|
|
||||||
from inspect import isclass
|
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.problems.knapsack import KnapsackInstance, GurobiKnapsackInstance
|
||||||
from miplearn.solvers.gurobi import GurobiSolver
|
from miplearn.solvers.gurobi import GurobiSolver
|
||||||
from miplearn.solvers.internal import InternalSolver
|
from miplearn.solvers.internal import InternalSolver
|
||||||
@@ -16,27 +13,6 @@ from miplearn.solvers.pyomo.gurobi import GurobiPyomoSolver
|
|||||||
from miplearn.solvers.pyomo.xpress import XpressPyomoSolver
|
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):
|
def _is_subclass_or_instance(obj, parent_class):
|
||||||
return isinstance(obj, parent_class) or (
|
return isinstance(obj, parent_class) or (
|
||||||
isclass(obj) and issubclass(obj, parent_class)
|
isclass(obj) and issubclass(obj, parent_class)
|
||||||
@@ -59,12 +35,5 @@ def _get_knapsack_instance(solver):
|
|||||||
assert False
|
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]]:
|
def _get_internal_solvers() -> List[Callable[[], InternalSolver]]:
|
||||||
return [GurobiPyomoSolver, GurobiSolver, XpressPyomoSolver]
|
return [GurobiPyomoSolver, GurobiSolver, XpressPyomoSolver]
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ from miplearn.solvers.pyomo.base import BasePyomoSolver
|
|||||||
from . import (
|
from . import (
|
||||||
_get_knapsack_instance,
|
_get_knapsack_instance,
|
||||||
_get_internal_solvers,
|
_get_internal_solvers,
|
||||||
_get_infeasible_instance,
|
|
||||||
)
|
)
|
||||||
|
from ..fixtures.infeasible import get_infeasible_instance
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ def test_relax():
|
|||||||
|
|
||||||
def test_infeasible_instance():
|
def test_infeasible_instance():
|
||||||
for solver_class in _get_internal_solvers():
|
for solver_class in _get_internal_solvers():
|
||||||
instance = _get_infeasible_instance(solver_class)
|
instance = get_infeasible_instance(solver_class)
|
||||||
solver = solver_class()
|
solver = solver_class()
|
||||||
solver.set_instance(instance)
|
solver.set_instance(instance)
|
||||||
stats = solver.solve()
|
stats = solver.solve()
|
||||||
|
|||||||
Reference in New Issue
Block a user