Make all before/solve callbacks receive same parameters

master
Alinson S. Xavier 5 years ago
parent 8eb2b63a85
commit 0c687692f7

@ -28,31 +28,12 @@ class Component:
solver: "LearningSolver",
instance: Instance,
model: Any,
) -> None:
"""
Method called by LearningSolver before the root LP relaxation is solved.
Parameters
----------
solver
The solver calling this method.
instance
The instance being solved.
model
The concrete optimization model being solved.
"""
return
def after_solve_lp(
self,
solver: "LearningSolver",
instance: Instance,
model: Any,
stats: LearningSolveStats,
features: Features,
training_data: TrainingSample,
) -> None:
"""
Method called by LearningSolver after the root LP relaxation is solved.
Method called by LearningSolver before the root LP relaxation is solved.
Parameters
----------
@ -60,7 +41,7 @@ class Component:
The solver calling this method.
instance: Instance
The instance being solved.
model: Any
model
The concrete optimization model being solved.
stats: LearningSolveStats
A dictionary containing statistics about the solution process, such as
@ -68,6 +49,8 @@ class Component:
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
@ -77,23 +60,33 @@ class Component:
"""
return
def after_solve_lp(
self,
solver: "LearningSolver",
instance: Instance,
model: Any,
stats: LearningSolveStats,
features: Features,
training_data: TrainingSample,
) -> None:
"""
Method called by LearningSolver after the root LP relaxation is solved.
See before_solve_lp for a description of the pameters.
"""
return
def before_solve_mip(
self,
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

@ -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)

@ -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)

@ -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

@ -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()

@ -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],

@ -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

@ -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

@ -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()

@ -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:

@ -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],

@ -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

@ -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]])

@ -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()

@ -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"]

@ -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"]

@ -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():

Loading…
Cancel
Save