mirror of
https://github.com/ANL-CEEESA/MIPLearn.git
synced 2025-12-06 09:28:51 -06:00
ConvertTight: Detect and fix sub-optimality
This commit is contained in:
@@ -52,10 +52,11 @@ class ConvertTightIneqsIntoEqsStep(Component):
|
|||||||
)
|
)
|
||||||
y = self.predict(x)
|
y = self.predict(x)
|
||||||
|
|
||||||
self.total_converted = 0
|
self.n_converted = 0
|
||||||
self.total_restored = 0
|
self.n_restored = 0
|
||||||
self.total_kept = 0
|
self.n_kept = 0
|
||||||
self.total_iterations = 0
|
self.n_infeasible_iterations = 0
|
||||||
|
self.n_suboptimal_iterations = 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:
|
||||||
@@ -64,17 +65,18 @@ class ConvertTightIneqsIntoEqsStep(Component):
|
|||||||
self.original_sense[cid] = s
|
self.original_sense[cid] = s
|
||||||
solver.internal_solver.set_constraint_sense(cid, "=")
|
solver.internal_solver.set_constraint_sense(cid, "=")
|
||||||
self.converted += [cid]
|
self.converted += [cid]
|
||||||
self.total_converted += 1
|
self.n_converted += 1
|
||||||
else:
|
else:
|
||||||
self.total_kept += 1
|
self.n_kept += 1
|
||||||
logger.info(f"Converted {self.total_converted} inequalities")
|
logger.info(f"Converted {self.n_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()
|
||||||
results["ConvertTight: Kept"] = self.total_kept
|
results["ConvertTight: Kept"] = self.n_kept
|
||||||
results["ConvertTight: Converted"] = self.total_converted
|
results["ConvertTight: Converted"] = self.n_converted
|
||||||
results["ConvertTight: Restored"] = self.total_restored
|
results["ConvertTight: Restored"] = self.n_restored
|
||||||
results["ConvertTight: Iterations"] = self.total_iterations
|
results["ConvertTight: Inf iterations"] = self.n_infeasible_iterations
|
||||||
|
results["ConvertTight: Subopt iterations"] = self.n_suboptimal_iterations
|
||||||
|
|
||||||
def fit(self, training_instances):
|
def fit(self, training_instances):
|
||||||
logger.debug("Extracting x and y...")
|
logger.debug("Extracting x and y...")
|
||||||
@@ -173,21 +175,56 @@ class ConvertTightIneqsIntoEqsStep(Component):
|
|||||||
def iteration_cb(self, solver, instance, model):
|
def iteration_cb(self, solver, instance, model):
|
||||||
if not self.check_converted:
|
if not self.check_converted:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
logger.debug("Checking converted inequalities...")
|
logger.debug("Checking converted inequalities...")
|
||||||
|
is_infeasible, is_suboptimal = False, False
|
||||||
restored = []
|
restored = []
|
||||||
|
|
||||||
|
def check_pi(msense, csense, pi):
|
||||||
|
if csense == "=":
|
||||||
|
return True
|
||||||
|
if msense == "max":
|
||||||
|
if csense == "<":
|
||||||
|
return pi >= 0
|
||||||
|
else:
|
||||||
|
return pi <= 0
|
||||||
|
else:
|
||||||
|
if csense == ">":
|
||||||
|
return pi >= 0
|
||||||
|
else:
|
||||||
|
return pi <= 0
|
||||||
|
|
||||||
|
def restore(cid):
|
||||||
|
nonlocal restored
|
||||||
|
csense = self.original_sense[cid]
|
||||||
|
solver.internal_solver.set_constraint_sense(cid, csense)
|
||||||
|
restored += [cid]
|
||||||
|
|
||||||
if solver.internal_solver.is_infeasible():
|
if solver.internal_solver.is_infeasible():
|
||||||
for cid in self.converted:
|
for cid in self.converted:
|
||||||
f = solver.internal_solver.get_farkas_dual(cid)
|
pi = solver.internal_solver.get_dual(cid)
|
||||||
if abs(f) > 0:
|
if abs(pi) > 0:
|
||||||
s = self.original_sense[cid]
|
is_infeasible = True
|
||||||
solver.internal_solver.set_constraint_sense(cid, s)
|
restore(cid)
|
||||||
restored += [cid]
|
else:
|
||||||
for cid in restored:
|
for cid in self.converted:
|
||||||
self.converted.remove(cid)
|
pi = solver.internal_solver.get_dual(cid)
|
||||||
|
csense = self.original_sense[cid]
|
||||||
|
msense = solver.internal_solver.get_sense()
|
||||||
|
if not check_pi(msense, csense, pi):
|
||||||
|
is_suboptimal = True
|
||||||
|
restore(cid)
|
||||||
|
|
||||||
|
for cid in restored:
|
||||||
|
self.converted.remove(cid)
|
||||||
|
|
||||||
if len(restored) > 0:
|
if len(restored) > 0:
|
||||||
self.total_restored += len(restored)
|
self.n_restored += len(restored)
|
||||||
|
if is_infeasible:
|
||||||
|
self.n_infeasible_iterations += 1
|
||||||
|
if is_suboptimal:
|
||||||
|
self.n_suboptimal_iterations += 1
|
||||||
logger.info(f"Restored {len(restored)} inequalities")
|
logger.info(f"Restored {len(restored)} inequalities")
|
||||||
self.total_iterations += 1
|
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -72,3 +72,26 @@ def test_convert_tight_infeasibility():
|
|||||||
instance = TestInstance()
|
instance = TestInstance()
|
||||||
solver.solve(instance)
|
solver.solve(instance)
|
||||||
assert instance.lower_bound == 5.0
|
assert instance.lower_bound == 5.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_convert_tight_suboptimality():
|
||||||
|
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=[[1, 0]])
|
||||||
|
comp.classifiers["c3"].predict_proba = Mock(return_value=[[0, 1]])
|
||||||
|
|
||||||
|
solver = LearningSolver(
|
||||||
|
solver=GurobiSolver(params={}),
|
||||||
|
components=[comp],
|
||||||
|
solve_lp_first=False,
|
||||||
|
)
|
||||||
|
instance = TestInstance()
|
||||||
|
solver.solve(instance)
|
||||||
|
assert instance.lower_bound == 5.0
|
||||||
|
|||||||
@@ -162,6 +162,12 @@ class GurobiSolver(InternalSolver):
|
|||||||
"Warm start value": self._extract_warm_start_value(log),
|
"Warm start value": self._extract_warm_start_value(log),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_sense(self):
|
||||||
|
if self.model.modelSense == 1:
|
||||||
|
return "min"
|
||||||
|
else:
|
||||||
|
return "max"
|
||||||
|
|
||||||
def get_solution(self):
|
def get_solution(self):
|
||||||
self._raise_if_callback()
|
self._raise_if_callback()
|
||||||
|
|
||||||
@@ -179,9 +185,12 @@ class GurobiSolver(InternalSolver):
|
|||||||
def is_infeasible(self):
|
def is_infeasible(self):
|
||||||
return self.model.status in [self.GRB.INFEASIBLE, self.GRB.INF_OR_UNBD]
|
return self.model.status in [self.GRB.INFEASIBLE, self.GRB.INF_OR_UNBD]
|
||||||
|
|
||||||
def get_farkas_dual(self, cid):
|
def get_dual(self, cid):
|
||||||
c = self.model.getConstrByName(cid)
|
c = self.model.getConstrByName(cid)
|
||||||
return c.farkasDual
|
if self.is_infeasible():
|
||||||
|
return c.farkasDual
|
||||||
|
else:
|
||||||
|
return c.pi
|
||||||
|
|
||||||
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:
|
||||||
|
|||||||
@@ -200,11 +200,20 @@ class InternalSolver(ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_farkas_dual(self, cid):
|
def get_dual(self, cid):
|
||||||
"""
|
"""
|
||||||
If the model is infeasible, returns a portion of the infeasibility certificate
|
If the model is feasible and has been solved to optimality, returns the optimal
|
||||||
corresponding to the given constraint. If the model is feasible, calling this
|
value of the dual variable associated with this constraint. If the model is infeasible,
|
||||||
function raises an error.
|
returns a portion of the infeasibility certificate corresponding to the given constraint.
|
||||||
|
|
||||||
|
Solve must be called prior to this method.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_sense(self):
|
||||||
|
"""
|
||||||
|
Returns the sense of the problem (either "min" or "max").
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -267,5 +267,8 @@ class BasePyomoSolver(InternalSolver):
|
|||||||
def is_infeasible(self):
|
def is_infeasible(self):
|
||||||
raise Exception("Not implemented")
|
raise Exception("Not implemented")
|
||||||
|
|
||||||
def get_farkas_dual(self, cid):
|
def get_dual(self, cid):
|
||||||
|
raise Exception("Not implemented")
|
||||||
|
|
||||||
|
def get_sense(self):
|
||||||
raise Exception("Not implemented")
|
raise Exception("Not implemented")
|
||||||
Reference in New Issue
Block a user