mirror of
https://github.com/ANL-CEEESA/MIPLearn.git
synced 2025-12-06 09:28:51 -06:00
ConvertTight: Detect and fix infeasibility
This commit is contained in:
@@ -20,6 +20,7 @@ from .components.steps.convert_tight import ConvertTightIneqsIntoEqsStep
|
|||||||
from .components.steps.relax_integrality import RelaxIntegralityStep
|
from .components.steps.relax_integrality import RelaxIntegralityStep
|
||||||
from .components.steps.drop_redundant import DropRedundantInequalitiesStep
|
from .components.steps.drop_redundant import DropRedundantInequalitiesStep
|
||||||
|
|
||||||
|
from .classifiers import Classifier, Regressor
|
||||||
from .classifiers.adaptive import AdaptiveClassifier
|
from .classifiers.adaptive import AdaptiveClassifier
|
||||||
from .classifiers.threshold import MinPrecisionThreshold
|
from .classifiers.threshold import MinPrecisionThreshold
|
||||||
|
|
||||||
|
|||||||
@@ -32,11 +32,15 @@ class ConvertTightIneqsIntoEqsStep(Component):
|
|||||||
classifier=CountingClassifier(),
|
classifier=CountingClassifier(),
|
||||||
threshold=0.95,
|
threshold=0.95,
|
||||||
slack_tolerance=0.0,
|
slack_tolerance=0.0,
|
||||||
|
check_converted=False,
|
||||||
):
|
):
|
||||||
self.classifiers = {}
|
self.classifiers = {}
|
||||||
self.classifier_prototype = classifier
|
self.classifier_prototype = classifier
|
||||||
self.threshold = threshold
|
self.threshold = threshold
|
||||||
self.slack_tolerance = slack_tolerance
|
self.slack_tolerance = slack_tolerance
|
||||||
|
self.check_converted = check_converted
|
||||||
|
self.converted = []
|
||||||
|
self.original_sense = {}
|
||||||
|
|
||||||
def before_solve(self, solver, instance, _):
|
def before_solve(self, solver, instance, _):
|
||||||
logger.info("Predicting tight LP constraints...")
|
logger.info("Predicting tight LP constraints...")
|
||||||
@@ -47,14 +51,15 @@ class ConvertTightIneqsIntoEqsStep(Component):
|
|||||||
return_constraints=True,
|
return_constraints=True,
|
||||||
)
|
)
|
||||||
y = self.predict(x)
|
y = self.predict(x)
|
||||||
n_converted = 0
|
|
||||||
for category in y.keys():
|
for category in y.keys():
|
||||||
for i in range(len(y[category])):
|
for i in range(len(y[category])):
|
||||||
if y[category][i][0] == 1:
|
if y[category][i][0] == 1:
|
||||||
cid = constraints[category][i]
|
cid = constraints[category][i]
|
||||||
|
s = solver.internal_solver.get_constraint_sense(cid)
|
||||||
|
self.original_sense[cid] = s
|
||||||
solver.internal_solver.set_constraint_sense(cid, "=")
|
solver.internal_solver.set_constraint_sense(cid, "=")
|
||||||
n_converted += 1
|
self.converted += [cid]
|
||||||
logger.info(f"Converted {n_converted} inequalities into equalities")
|
logger.info(f"Converted {len(self.converted)} inequalities")
|
||||||
|
|
||||||
def after_solve(self, solver, instance, model, results):
|
def after_solve(self, solver, instance, model, results):
|
||||||
instance.slacks = solver.internal_solver.get_inequality_slacks()
|
instance.slacks = solver.internal_solver.get_inequality_slacks()
|
||||||
@@ -152,3 +157,23 @@ class ConvertTightIneqsIntoEqsStep(Component):
|
|||||||
else:
|
else:
|
||||||
tn += 1
|
tn += 1
|
||||||
return classifier_evaluation_dict(tp, tn, fp, fn)
|
return classifier_evaluation_dict(tp, tn, fp, fn)
|
||||||
|
|
||||||
|
def iteration_cb(self, solver, instance, model):
|
||||||
|
if not self.check_converted:
|
||||||
|
return False
|
||||||
|
logger.debug("Checking converted inequalities...")
|
||||||
|
restored = []
|
||||||
|
if solver.internal_solver.is_infeasible():
|
||||||
|
for cid in self.converted:
|
||||||
|
f = solver.internal_solver.get_farkas_dual(cid)
|
||||||
|
if abs(f) > 0:
|
||||||
|
s = self.original_sense[cid]
|
||||||
|
solver.internal_solver.set_constraint_sense(cid, s)
|
||||||
|
restored += [cid]
|
||||||
|
for cid in restored:
|
||||||
|
self.converted.remove(cid)
|
||||||
|
if len(restored) > 0:
|
||||||
|
logger.info(f"Restored {len(restored)} inequalities")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
from miplearn import LearningSolver, GurobiSolver
|
from miplearn import LearningSolver, GurobiSolver, Instance, Classifier
|
||||||
from miplearn.components.steps.convert_tight import ConvertTightIneqsIntoEqsStep
|
from miplearn.components.steps.convert_tight import ConvertTightIneqsIntoEqsStep
|
||||||
from miplearn.components.steps.relax_integrality import RelaxIntegralityStep
|
from miplearn.components.steps.relax_integrality import RelaxIntegralityStep
|
||||||
from miplearn.problems.knapsack import GurobiKnapsackInstance
|
from miplearn.problems.knapsack import GurobiKnapsackInstance
|
||||||
|
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
|
||||||
def test_convert_tight_usage():
|
def test_convert_tight_usage():
|
||||||
instance = GurobiKnapsackInstance(
|
instance = GurobiKnapsackInstance(
|
||||||
@@ -32,3 +34,41 @@ def test_convert_tight_usage():
|
|||||||
|
|
||||||
# Objective value should be the same
|
# Objective value should be the same
|
||||||
assert instance.upper_bound == original_upper_bound
|
assert instance.upper_bound == original_upper_bound
|
||||||
|
|
||||||
|
|
||||||
|
class TestInstance(Instance):
|
||||||
|
def to_model(self):
|
||||||
|
import gurobipy as grb
|
||||||
|
from gurobipy import GRB
|
||||||
|
|
||||||
|
m = grb.Model("model")
|
||||||
|
x1 = m.addVar(name="x1")
|
||||||
|
x2 = m.addVar(name="x2")
|
||||||
|
m.setObjective(x1 + 2 * x2, grb.GRB.MAXIMIZE)
|
||||||
|
m.addConstr(x1 <= 2, name="c1")
|
||||||
|
m.addConstr(x2 <= 2, name="c2")
|
||||||
|
m.addConstr(x1 + x2 <= 3, name="c2")
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
def test_convert_tight_infeasibility():
|
||||||
|
comp = ConvertTightIneqsIntoEqsStep(
|
||||||
|
check_converted=True,
|
||||||
|
)
|
||||||
|
comp.classifiers = {
|
||||||
|
"c1": Mock(spec=Classifier),
|
||||||
|
"c2": Mock(spec=Classifier),
|
||||||
|
"c3": Mock(spec=Classifier),
|
||||||
|
}
|
||||||
|
comp.classifiers["c1"].predict_proba = Mock(return_value=[[0, 1]])
|
||||||
|
comp.classifiers["c2"].predict_proba = Mock(return_value=[[0, 1]])
|
||||||
|
comp.classifiers["c3"].predict_proba = Mock(return_value=[[1, 0]])
|
||||||
|
|
||||||
|
solver = LearningSolver(
|
||||||
|
solver=GurobiSolver(params={}),
|
||||||
|
components=[comp],
|
||||||
|
solve_lp_first=False,
|
||||||
|
)
|
||||||
|
instance = TestInstance()
|
||||||
|
solver.solve(instance)
|
||||||
|
assert instance.lower_bound == 5.0
|
||||||
@@ -33,6 +33,7 @@ class GurobiSolver(InternalSolver):
|
|||||||
"""
|
"""
|
||||||
if params is None:
|
if params is None:
|
||||||
params = {}
|
params = {}
|
||||||
|
params["InfUnbdInfo"] = True
|
||||||
from gurobipy import GRB
|
from gurobipy import GRB
|
||||||
|
|
||||||
self.GRB = GRB
|
self.GRB = GRB
|
||||||
@@ -132,7 +133,6 @@ class GurobiSolver(InternalSolver):
|
|||||||
if iteration_cb is None:
|
if iteration_cb is None:
|
||||||
iteration_cb = lambda: False
|
iteration_cb = lambda: False
|
||||||
while True:
|
while True:
|
||||||
logger.debug("Solving MIP...")
|
|
||||||
with RedirectOutput(streams):
|
with RedirectOutput(streams):
|
||||||
if lazy_cb is None:
|
if lazy_cb is None:
|
||||||
self.model.optimize()
|
self.model.optimize()
|
||||||
@@ -176,6 +176,13 @@ class GurobiSolver(InternalSolver):
|
|||||||
var = self._all_vars[var_name][index]
|
var = self._all_vars[var_name][index]
|
||||||
return self._get_value(var)
|
return self._get_value(var)
|
||||||
|
|
||||||
|
def is_infeasible(self):
|
||||||
|
return self.model.status in [self.GRB.INFEASIBLE, self.GRB.INF_OR_UNBD]
|
||||||
|
|
||||||
|
def get_farkas_dual(self, cid):
|
||||||
|
c = self.model.getConstrByName(cid)
|
||||||
|
return c.farkasDual
|
||||||
|
|
||||||
def _get_value(self, var):
|
def _get_value(self, var):
|
||||||
if self.cb_where == self.GRB.Callback.MIPSOL:
|
if self.cb_where == self.GRB.Callback.MIPSOL:
|
||||||
return self.model.cbGetSolution(var)
|
return self.model.cbGetSolution(var)
|
||||||
@@ -280,6 +287,10 @@ class GurobiSolver(InternalSolver):
|
|||||||
c = self.model.getConstrByName(cid)
|
c = self.model.getConstrByName(cid)
|
||||||
c.Sense = sense
|
c.Sense = sense
|
||||||
|
|
||||||
|
def get_constraint_sense(self, cid):
|
||||||
|
c = self.model.getConstrByName(cid)
|
||||||
|
return c.Sense
|
||||||
|
|
||||||
def set_constraint_rhs(self, cid, rhs):
|
def set_constraint_rhs(self, cid, rhs):
|
||||||
c = self.model.getConstrByName(cid)
|
c = self.model.getConstrByName(cid)
|
||||||
c.RHS = rhs
|
c.RHS = rhs
|
||||||
|
|||||||
@@ -191,6 +191,23 @@ class InternalSolver(ABC):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def is_infeasible(self):
|
||||||
|
"""
|
||||||
|
Returns True if the model has been proved to be infeasible.
|
||||||
|
Must be called after solve.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_farkas_dual(self, cid):
|
||||||
|
"""
|
||||||
|
If the model is infeasible, returns a portion of the infeasibility certificate
|
||||||
|
corresponding to the given constraint. If the model is feasible, calling this
|
||||||
|
function raises an error.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def is_constraint_satisfied(self, cobj):
|
def is_constraint_satisfied(self, cobj):
|
||||||
pass
|
pass
|
||||||
@@ -199,6 +216,10 @@ class InternalSolver(ABC):
|
|||||||
def set_constraint_sense(self, cid, sense):
|
def set_constraint_sense(self, cid, sense):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_constraint_sense(self, cid):
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def set_constraint_rhs(self, cid, rhs):
|
def set_constraint_rhs(self, cid, rhs):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -258,5 +258,14 @@ class BasePyomoSolver(InternalSolver):
|
|||||||
def set_constraint_sense(self, cid, sense):
|
def set_constraint_sense(self, cid, sense):
|
||||||
raise Exception("Not implemented")
|
raise Exception("Not implemented")
|
||||||
|
|
||||||
|
def get_constraint_sense(self, cid):
|
||||||
|
raise Exception("Not implemented")
|
||||||
|
|
||||||
def set_constraint_rhs(self, cid, rhs):
|
def set_constraint_rhs(self, cid, rhs):
|
||||||
raise Exception("Not implemented")
|
raise Exception("Not implemented")
|
||||||
|
|
||||||
|
def is_infeasible(self):
|
||||||
|
raise Exception("Not implemented")
|
||||||
|
|
||||||
|
def get_farkas_dual(self, cid):
|
||||||
|
raise Exception("Not implemented")
|
||||||
Reference in New Issue
Block a user