Make all before/solve callbacks receive same parameters

This commit is contained in:
2021-04-02 07:05:16 -05:00
parent 8eb2b63a85
commit 0c687692f7
17 changed files with 201 additions and 189 deletions

View File

@@ -28,18 +28,35 @@ class Component:
solver: "LearningSolver",
instance: Instance,
model: Any,
stats: LearningSolveStats,
features: Features,
training_data: TrainingSample,
) -> None:
"""
Method called by LearningSolver before the root LP relaxation is solved.
Parameters
----------
solver
solver: LearningSolver
The solver calling this method.
instance
instance: Instance
The instance being solved.
model
The concrete optimization model being solved.
stats: LearningSolveStats
A dictionary containing statistics about the solution process, such as
number of nodes explored and running time. Components are free to add
their own statistics here. For example, PrimalSolutionComponent adds
statistics regarding the number of predicted variables. All statistics in
this dictionary are exported to the benchmark CSV file.
features: Features
Features describing the model.
training_data: TrainingSample
A dictionary containing data that may be useful for training machine
learning models and accelerating the solution process. Components are
free to add their own training data here. For example,
PrimalSolutionComponent adds the current primal solution. The data must
be pickable.
"""
return
@@ -49,31 +66,12 @@ class Component:
instance: Instance,
model: Any,
stats: LearningSolveStats,
features: Features,
training_data: TrainingSample,
) -> None:
"""
Method called by LearningSolver after the root LP relaxation is solved.
Parameters
----------
solver: LearningSolver
The solver calling this method.
instance: Instance
The instance being solved.
model: Any
The concrete optimization model being solved.
stats: LearningSolveStats
A dictionary containing statistics about the solution process, such as
number of nodes explored and running time. Components are free to add
their own statistics here. For example, PrimalSolutionComponent adds
statistics regarding the number of predicted variables. All statistics in
this dictionary are exported to the benchmark CSV file.
training_data: TrainingSample
A dictionary containing data that may be useful for training machine
learning models and accelerating the solution process. Components are
free to add their own training data here. For example,
PrimalSolutionComponent adds the current primal solution. The data must
be pickable.
See before_solve_lp for a description of the pameters.
"""
return
@@ -82,18 +80,13 @@ class Component:
solver: "LearningSolver",
instance: Instance,
model: Any,
stats: LearningSolveStats,
features: Features,
training_data: TrainingSample,
) -> None:
"""
Method called by LearningSolver before the MIP is solved.
Parameters
----------
solver
The solver calling this method.
instance
The instance being solved.
model
The concrete optimization model being solved.
See before_solve_lp for a description of the pameters.
"""
return
@@ -103,31 +96,12 @@ class Component:
instance: Instance,
model: Any,
stats: LearningSolveStats,
features: Features,
training_data: TrainingSample,
) -> None:
"""
Method called by LearningSolver after the MIP is solved.
Parameters
----------
solver: LearningSolver
The solver calling this method.
instance: Instance
The instance being solved.
model: Any
The concrete optimization model being solved.
stats: LearningSolveStats
A dictionary containing statistics about the solution process, such as
number of nodes explored and running time. Components are free to add
their own statistics here. For example, PrimalSolutionComponent adds
statistics regarding the number of predicted variables. All statistics in
this dictionary are exported to the benchmark CSV file.
training_data: TrainingSample
A dictionary containing data that may be useful for training machine
learning models and accelerating the solution process. Components are
free to add their own training data here. For example,
PrimalSolutionComponent adds the current primal solution. The data must
be pickable.
See before_solve_lp for a description of the pameters.
"""
return

View File

@@ -33,7 +33,15 @@ class UserCutsComponent(Component):
self.classifier_prototype: Classifier = classifier
self.classifiers: Dict[Any, Classifier] = {}
def before_solve_mip(self, solver, instance, model):
def before_solve_mip(
self,
solver,
instance,
model,
stats,
features,
training_data,
):
instance.found_violated_user_cuts = []
logger.info("Predicting violated user cuts...")
violations = self.predict(instance)
@@ -42,16 +50,6 @@ class UserCutsComponent(Component):
cut = instance.build_user_cut(model, v)
solver.internal_solver.add_constraint(cut)
def after_solve_mip(
self,
solver,
instance,
model,
results,
training_data,
):
pass
def fit(self, training_instances):
logger.debug("Fitting...")
features = InstanceFeaturesExtractor().extract(training_instances)

View File

@@ -33,7 +33,15 @@ class DynamicLazyConstraintsComponent(Component):
self.classifier_prototype: Classifier = classifier
self.classifiers: Dict[Any, Classifier] = {}
def before_solve_mip(self, solver, instance, model):
def before_solve_mip(
self,
solver,
instance,
model,
stats,
features,
training_data,
):
instance.found_violated_lazy_constraints = []
logger.info("Predicting violated lazy constraints...")
violations = self.predict(instance)
@@ -54,16 +62,6 @@ class DynamicLazyConstraintsComponent(Component):
solver.internal_solver.add_constraint(cut)
return True
def after_solve_mip(
self,
solver,
instance,
model,
stats,
training_data,
):
pass
def fit(self, training_instances):
logger.debug("Fitting...")
features = InstanceFeaturesExtractor().extract(training_instances)

View File

@@ -43,7 +43,15 @@ class StaticLazyConstraintsComponent(Component):
self.use_two_phase_gap = use_two_phase_gap
self.violation_tolerance = violation_tolerance
def before_solve_mip(self, solver, instance, model):
def before_solve_mip(
self,
solver,
instance,
model,
stats,
features,
training_data,
):
self.pool = []
if not solver.use_lazy_cb and self.use_two_phase_gap:
logger.info("Increasing gap tolerance to %f", self.large_gap)
@@ -55,16 +63,6 @@ class StaticLazyConstraintsComponent(Component):
if instance.has_static_lazy_constraints():
self._extract_and_predict_static(solver, instance)
def after_solve_mip(
self,
solver,
instance,
model,
stats,
training_data,
):
pass
def iteration_cb(self, solver, instance, model):
if solver.use_lazy_cb:
return False

View File

@@ -52,32 +52,20 @@ class ObjectiveValueComponent(Component):
solver: "LearningSolver",
instance: Instance,
model: Any,
stats: LearningSolveStats,
features: Features,
training_data: TrainingSample,
) -> None:
if self.ub_regressor is not None:
logger.info("Predicting optimal value...")
pred = self.predict([instance])
self._predicted_lb = pred["Upper bound"][0]
self._predicted_ub = pred["Lower bound"][0]
logger.info(
"Predicted values: lb=%.2f, ub=%.2f"
% (
self._predicted_lb,
self._predicted_ub,
)
)
def after_solve_mip(
self,
solver: "LearningSolver",
instance: Instance,
model: Any,
stats: LearningSolveStats,
training_data: TrainingSample,
) -> None:
if self._predicted_ub is not None:
stats["Objective: predicted UB"] = self._predicted_ub
if self._predicted_lb is not None:
stats["Objective: predicted LB"] = self._predicted_lb
predicted_lb = pred["Upper bound"][0]
predicted_ub = pred["Lower bound"][0]
logger.info("Predicted LB=%.2f, UB=%.2f" % (predicted_lb, predicted_ub))
if predicted_ub is not None:
stats["Objective: Predicted UB"] = predicted_ub
if predicted_lb is not None:
stats["Objective: Predicted LB"] = predicted_lb
def fit(self, training_instances: Union[List[str], List[Instance]]) -> None:
self.lb_regressor = self.lb_regressor_prototype.clone()

View File

@@ -62,9 +62,16 @@ class PrimalSolutionComponent(Component):
self.thresholds: Dict[Hashable, Threshold] = {}
self.threshold_prototype = threshold
self.classifier_prototype = classifier
self.stats: Dict[str, float] = {}
def before_solve_mip(self, solver, instance, model):
def before_solve_mip(
self,
solver: "LearningSolver",
instance: Instance,
model: Any,
stats: LearningSolveStats,
features: Features,
training_data: TrainingSample,
) -> None:
if len(self.thresholds) > 0:
logger.info("Predicting MIP solution...")
solution = self.predict(
@@ -72,41 +79,32 @@ class PrimalSolutionComponent(Component):
instance.training_data[-1],
)
# Collect prediction statistics
self.stats["Primal: Free"] = 0
self.stats["Primal: Zero"] = 0
self.stats["Primal: One"] = 0
# Update statistics
stats["Primal: Free"] = 0
stats["Primal: Zero"] = 0
stats["Primal: One"] = 0
for (var, var_dict) in solution.items():
for (idx, value) in var_dict.items():
if value is None:
self.stats["Primal: Free"] += 1
stats["Primal: Free"] += 1
else:
if value < 0.5:
self.stats["Primal: Zero"] += 1
stats["Primal: Zero"] += 1
else:
self.stats["Primal: One"] += 1
stats["Primal: One"] += 1
logger.info(
f"Predicted: free: {self.stats['Primal: Free']}, "
f"zero: {self.stats['Primal: zero']}, "
f"one: {self.stats['Primal: One']}"
f"Predicted: free: {stats['Primal: Free']}, "
f"zero: {stats['Primal: Zero']}, "
f"one: {stats['Primal: One']}"
)
# Provide solution to the solver
assert solver.internal_solver is not None
if self.mode == "heuristic":
solver.internal_solver.fix(solution)
else:
solver.internal_solver.set_warm_start(solution)
def after_solve_mip(
self,
solver: "LearningSolver",
instance: Instance,
model: Any,
stats: LearningSolveStats,
training_data: TrainingSample,
) -> None:
stats.update(self.stats)
def fit_xy(
self,
x: Dict[str, np.ndarray],

View File

@@ -45,8 +45,23 @@ class ConvertTightIneqsIntoEqsStep(Component):
self.check_optimality = check_optimality
self.converted = []
self.original_sense = {}
self.n_restored = 0
self.n_infeasible_iterations = 0
self.n_suboptimal_iterations = 0
def before_solve_mip(
self,
solver,
instance,
model,
stats,
features,
training_data,
):
self.n_restored = 0
self.n_infeasible_iterations = 0
self.n_suboptimal_iterations = 0
def before_solve_mip(self, solver, instance, _):
logger.info("Predicting tight LP constraints...")
x, constraints = DropRedundantInequalitiesStep.x(
instance,
@@ -54,11 +69,8 @@ class ConvertTightIneqsIntoEqsStep(Component):
)
y = self.predict(x)
self.n_converted = 0
self.n_restored = 0
self.n_kept = 0
self.n_infeasible_iterations = 0
self.n_suboptimal_iterations = 0
n_converted = 0
n_kept = 0
for category in y.keys():
for i in range(len(y[category])):
if y[category][i][0] == 1:
@@ -67,11 +79,13 @@ class ConvertTightIneqsIntoEqsStep(Component):
self.original_sense[cid] = s
solver.internal_solver.set_constraint_sense(cid, "=")
self.converted += [cid]
self.n_converted += 1
n_converted += 1
else:
self.n_kept += 1
n_kept += 1
stats["ConvertTight: Kept"] = n_kept
stats["ConvertTight: Converted"] = n_converted
logger.info(f"Converted {self.n_converted} inequalities")
logger.info(f"Converted {n_converted} inequalities")
def after_solve_mip(
self,
@@ -79,12 +93,11 @@ class ConvertTightIneqsIntoEqsStep(Component):
instance,
model,
stats,
features,
training_data,
):
if "slacks" not in training_data.keys():
training_data["slacks"] = solver.internal_solver.get_inequality_slacks()
stats["ConvertTight: Kept"] = self.n_kept
stats["ConvertTight: Converted"] = self.n_converted
stats["ConvertTight: Restored"] = self.n_restored
stats["ConvertTight: Inf iterations"] = self.n_infeasible_iterations
stats["ConvertTight: Subopt iterations"] = self.n_suboptimal_iterations

View File

@@ -46,12 +46,20 @@ class DropRedundantInequalitiesStep(Component):
self.violation_tolerance = violation_tolerance
self.max_iterations = max_iterations
self.current_iteration = 0
self.total_dropped = 0
self.total_restored = 0
self.total_kept = 0
self.total_iterations = 0
self.n_iterations = 0
self.n_restored = 0
def before_solve_mip(self, solver, instance, _):
def before_solve_mip(
self,
solver,
instance,
model,
stats,
features,
training_data,
):
self.n_iterations = 0
self.n_restored = 0
self.current_iteration = 0
logger.info("Predicting redundant LP constraints...")
@@ -62,10 +70,8 @@ class DropRedundantInequalitiesStep(Component):
y = self.predict(x)
self.pool = []
self.total_dropped = 0
self.total_restored = 0
self.total_kept = 0
self.total_iterations = 0
n_dropped = 0
n_kept = 0
for category in y.keys():
for i in range(len(y[category])):
if y[category][i][1] == 1:
@@ -75,10 +81,12 @@ class DropRedundantInequalitiesStep(Component):
obj=solver.internal_solver.extract_constraint(cid),
)
self.pool += [c]
self.total_dropped += 1
n_dropped += 1
else:
self.total_kept += 1
logger.info(f"Extracted {self.total_dropped} predicted constraints")
n_kept += 1
stats["DropRedundant: Kept"] = n_kept
stats["DropRedundant: Dropped"] = n_dropped
logger.info(f"Extracted {n_dropped} predicted constraints")
def after_solve_mip(
self,
@@ -86,18 +94,13 @@ class DropRedundantInequalitiesStep(Component):
instance,
model,
stats,
features,
training_data,
):
if "slacks" not in training_data.keys():
training_data["slacks"] = solver.internal_solver.get_inequality_slacks()
stats.update(
{
"DropRedundant: Kept": self.total_kept,
"DropRedundant: Dropped": self.total_dropped,
"DropRedundant: Restored": self.total_restored,
"DropRedundant: Iterations": self.total_iterations,
}
)
stats["DropRedundant: Iterations"] = self.n_iterations
stats["DropRedundant: Restored"] = self.n_restored
def fit(self, training_instances, n_jobs=1):
x, y = self.x_y(training_instances, n_jobs=n_jobs)
@@ -234,12 +237,12 @@ class DropRedundantInequalitiesStep(Component):
self.pool.remove(c)
solver.internal_solver.add_constraint(c.obj)
if len(constraints_to_add) > 0:
self.total_restored += len(constraints_to_add)
self.n_restored += len(constraints_to_add)
logger.info(
"%8d constraints %8d in the pool"
% (len(constraints_to_add), len(self.pool))
)
self.total_iterations += 1
self.n_iterations += 1
return True
else:
return False

View File

@@ -14,16 +14,14 @@ class RelaxIntegralityStep(Component):
Component that relaxes all integrality constraints before the problem is solved.
"""
def before_solve_mip(self, solver, instance, _):
logger.info("Relaxing integrality...")
solver.internal_solver.relax()
def after_solve_mip(
def before_solve_mip(
self,
solver,
instance,
model,
stats,
features,
training_data,
):
return
logger.info("Relaxing integrality...")
solver.internal_solver.relax()

View File

@@ -178,11 +178,20 @@ class LearningSolver:
extractor = FeaturesExtractor(self.internal_solver)
instance.features = extractor.extract(instance)
callback_args = (
self,
instance,
model,
stats,
instance.features,
training_sample,
)
# Solve root LP relaxation
if self.solve_lp:
logger.debug("Running before_solve_lp callbacks...")
for component in self.components.values():
component.before_solve_lp(self, instance, model)
component.before_solve_lp(*callback_args)
logger.info("Solving root LP relaxation...")
lp_stats = self.internal_solver.solve_lp(tee=tee)
@@ -193,7 +202,7 @@ class LearningSolver:
logger.debug("Running after_solve_lp callbacks...")
for component in self.components.values():
component.after_solve_lp(self, instance, model, stats, training_sample)
component.after_solve_lp(*callback_args)
else:
training_sample["LP solution"] = self.internal_solver.get_empty_solution()
training_sample["LP value"] = 0.0
@@ -222,7 +231,7 @@ class LearningSolver:
# Before-solve callbacks
logger.debug("Running before_solve_mip callbacks...")
for component in self.components.values():
component.before_solve_mip(self, instance, model)
component.before_solve_mip(*callback_args)
# Solve MIP
logger.info("Solving MIP...")
@@ -250,7 +259,7 @@ class LearningSolver:
# After-solve callbacks
logger.debug("Calling after_solve_mip callbacks...")
for component in self.components.values():
component.after_solve_mip(self, instance, model, stats, training_sample)
component.after_solve_mip(*callback_args)
# Write to file, if necessary
if not discard_output and filename is not None:

View File

@@ -59,11 +59,11 @@ LearningSolveStats = TypedDict(
"MIP log": str,
"Mode": str,
"Nodes": Optional[int],
"Objective: predicted LB": float,
"Objective: predicted UB": float,
"Primal: free": int,
"Primal: one": int,
"Primal: zero": int,
"Objective: Predicted LB": float,
"Objective: Predicted UB": float,
"Primal: Free": int,
"Primal: One": int,
"Primal: Zero": int,
"Sense": str,
"Solver": str,
"Upper bound": Optional[float],

View File

@@ -80,7 +80,14 @@ def test_drop_redundant():
component.classifiers = classifiers
# LearningSolver calls before_solve
component.before_solve_mip(solver, instance, None)
component.before_solve_mip(
solver=solver,
instance=instance,
model=None,
stats={},
features=None,
training_data=None,
)
# Should query list of constraints
internal.get_constraint_ids.assert_called_once()
@@ -123,7 +130,14 @@ def test_drop_redundant():
# LearningSolver calls after_solve
training_data = {}
component.after_solve_mip(solver, instance, None, {}, training_data)
component.after_solve_mip(
solver=solver,
instance=instance,
model=None,
stats={},
features=None,
training_data=training_data,
)
# Should query slack for all inequalities
internal.get_inequality_slacks.assert_called_once()
@@ -147,7 +161,14 @@ def test_drop_redundant_with_check_feasibility():
component.classifiers = classifiers
# LearningSolver call before_solve
component.before_solve_mip(solver, instance, None)
component.before_solve_mip(
solver=solver,
instance=instance,
model=None,
stats={},
features=None,
training_data=None,
)
# Assert constraints are extracted
assert internal.extract_constraint.call_count == 2

View File

@@ -86,7 +86,14 @@ def test_lazy_before():
component.classifiers["a"].predict_proba = Mock(return_value=[[0.95, 0.05]])
component.classifiers["b"].predict_proba = Mock(return_value=[[0.02, 0.80]])
component.before_solve_mip(solver, instances[0], models[0])
component.before_solve_mip(
solver=solver,
instance=instances[0],
model=models[0],
stats=None,
features=None,
training_data=None,
)
# Should ask classifier likelihood of each constraint being violated
expected_x_test_a = np.array([[67.0, 21.75, 1287.92]])

View File

@@ -69,7 +69,14 @@ def test_usage_with_solver():
)
# LearningSolver calls before_solve
component.before_solve_mip(solver, instance, None)
component.before_solve_mip(
solver=solver,
instance=instance,
model=None,
stats=None,
features=None,
training_data=None,
)
# Should ask if instance has static lazy constraints
instance.has_static_lazy_constraints.assert_called_once()

View File

@@ -157,11 +157,11 @@ def test_xy_sample_without_lp() -> None:
assert y_actual == y_expected
def test_usage():
def test_usage() -> None:
solver = LearningSolver(components=[ObjectiveValueComponent()])
instance = get_knapsack_instance(GurobiPyomoSolver())
solver.solve(instance)
solver.fit([instance])
stats = solver.solve(instance)
assert stats["Lower bound"] == stats["Objective: predicted LB"]
assert stats["Upper bound"] == stats["Objective: predicted UB"]
assert stats["Lower bound"] == stats["Objective: Predicted LB"]
assert stats["Upper bound"] == stats["Objective: Predicted UB"]

View File

@@ -226,6 +226,6 @@ def test_usage():
solver.solve(instance)
solver.fit([instance])
stats = solver.solve(instance)
assert stats["Primal: free"] == 0
assert stats["Primal: one"] + stats["Primal: zero"] == 10
assert stats["Primal: Free"] == 0
assert stats["Primal: One"] + stats["Primal: Zero"] == 10
assert stats["Lower bound"] == stats["Warm start value"]

View File

@@ -133,7 +133,7 @@ def test_simulate_perfect():
simulate_perfect=True,
)
stats = solver.solve(tmp.name)
assert stats["Lower bound"] == stats["Objective: predicted LB"]
assert stats["Lower bound"] == stats["Objective: Predicted LB"]
def test_gap():