Add after_solve_lp callback; make dict keys consistent

master
Alinson S. Xavier 5 years ago
parent 6ae052c8d0
commit 3b61a15ead

@ -21,6 +21,60 @@ class Component(ABC):
strategy. strategy.
""" """
def before_solve_lp(
self,
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,
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.
"""
return
def before_solve_mip( def before_solve_mip(
self, self,
solver: "LearningSolver", solver: "LearningSolver",
@ -41,7 +95,6 @@ class Component(ABC):
""" """
return return
@abstractmethod
def after_solve_mip( def after_solve_mip(
self, self,
solver: "LearningSolver", solver: "LearningSolver",
@ -74,7 +127,7 @@ class Component(ABC):
PrimalSolutionComponent adds the current primal solution. The data must PrimalSolutionComponent adds the current primal solution. The data must
be pickable. be pickable.
""" """
pass return
def fit( def fit(
self, self,

@ -144,8 +144,8 @@ class GurobiSolver(InternalSolver):
if not self.is_infeasible(): if not self.is_infeasible():
opt_value = self.model.objVal opt_value = self.model.objVal
return { return {
"Optimal value": opt_value, "LP value": opt_value,
"Log": log, "LP log": log,
} }
def solve( def solve(
@ -205,9 +205,8 @@ class GurobiSolver(InternalSolver):
"Wallclock time": total_wallclock_time, "Wallclock time": total_wallclock_time,
"Nodes": total_nodes, "Nodes": total_nodes,
"Sense": sense, "Sense": sense,
"Log": log, "MIP log": log,
"Warm start value": ws_value, "Warm start value": ws_value,
"LP value": None,
} }
return stats return stats

@ -22,7 +22,7 @@ from miplearn.instance import Instance
from miplearn.solvers import _RedirectOutput from miplearn.solvers import _RedirectOutput
from miplearn.solvers.internal import InternalSolver from miplearn.solvers.internal import InternalSolver
from miplearn.solvers.pyomo.gurobi import GurobiPyomoSolver from miplearn.solvers.pyomo.gurobi import GurobiPyomoSolver
from miplearn.types import TrainingSample, LearningSolveStats from miplearn.types import TrainingSample, LearningSolveStats, MIPSolveStats
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -85,8 +85,8 @@ class LearningSolver:
use_lazy_cb: bool use_lazy_cb: bool
If true, use native solver callbacks for enforcing lazy constraints, If true, use native solver callbacks for enforcing lazy constraints,
instead of a simple loop. May not be supported by all solvers. instead of a simple loop. May not be supported by all solvers.
solve_lp_first: bool solve_lp: bool
If true, solve LP relaxation first, then solve original MIP. This If true, solve the root LP relaxation before solving the MIP. This
option should be activated if the LP relaxation is not very option should be activated if the LP relaxation is not very
expensive to solve and if it provides good hints for the integer expensive to solve and if it provides good hints for the integer
solution. solution.
@ -103,7 +103,7 @@ class LearningSolver:
mode: str = "exact", mode: str = "exact",
solver: Callable[[], InternalSolver] = None, solver: Callable[[], InternalSolver] = None,
use_lazy_cb: bool = False, use_lazy_cb: bool = False,
solve_lp_first: bool = True, solve_lp: bool = True,
simulate_perfect: bool = False, simulate_perfect: bool = False,
): ):
if solver is None: if solver is None:
@ -113,7 +113,7 @@ class LearningSolver:
self.internal_solver: Optional[InternalSolver] = None self.internal_solver: Optional[InternalSolver] = None
self.mode: str = mode self.mode: str = mode
self.simulate_perfect: bool = simulate_perfect self.simulate_perfect: bool = simulate_perfect
self.solve_lp_first: bool = solve_lp_first self.solve_lp: bool = solve_lp
self.solver_factory: Callable[[], InternalSolver] = solver self.solver_factory: Callable[[], InternalSolver] = solver
self.tee = False self.tee = False
self.use_lazy_cb: bool = use_lazy_cb self.use_lazy_cb: bool = use_lazy_cb
@ -164,6 +164,9 @@ class LearningSolver:
instance.training_data = [] instance.training_data = []
instance.training_data += [training_sample] instance.training_data += [training_sample]
# Initialize stats
stats: LearningSolveStats = {}
# Initialize internal solver # Initialize internal solver
self.tee = tee self.tee = tee
self.internal_solver = self.solver_factory() self.internal_solver = self.solver_factory()
@ -175,22 +178,26 @@ class LearningSolver:
extractor = ModelFeaturesExtractor(self.internal_solver) extractor = ModelFeaturesExtractor(self.internal_solver)
instance.model_features = extractor.extract() instance.model_features = extractor.extract()
# Solve linear relaxation # Solve root LP relaxation
if self.solve_lp_first: if self.solve_lp:
logger.info("Solving LP relaxation...") logger.debug("Running before_solve_lp callbacks...")
for component in self.components.values():
component.before_solve_lp(self, instance, model)
logger.info("Solving root LP relaxation...")
lp_stats = self.internal_solver.solve_lp(tee=tee) lp_stats = self.internal_solver.solve_lp(tee=tee)
stats.update(cast(LearningSolveStats, lp_stats))
training_sample["LP solution"] = self.internal_solver.get_solution() training_sample["LP solution"] = self.internal_solver.get_solution()
training_sample["LP value"] = lp_stats["Optimal value"] training_sample["LP value"] = lp_stats["LP value"]
training_sample["LP log"] = lp_stats["Log"] training_sample["LP log"] = lp_stats["LP log"]
logger.debug("Running after_solve_lp callbacks...")
for component in self.components.values():
component.after_solve_lp(self, instance, model, stats, training_sample)
else: else:
training_sample["LP solution"] = self.internal_solver.get_empty_solution() training_sample["LP solution"] = self.internal_solver.get_empty_solution()
training_sample["LP value"] = 0.0 training_sample["LP value"] = 0.0
# Before-solve callbacks
logger.debug("Running before_solve_mip callbacks...")
for component in self.components.values():
component.before_solve_mip(self, instance, model)
# Define wrappers # Define wrappers
def iteration_cb_wrapper() -> bool: def iteration_cb_wrapper() -> bool:
should_repeat = False should_repeat = False
@ -212,16 +219,19 @@ class LearningSolver:
if self.use_lazy_cb: if self.use_lazy_cb:
lazy_cb = lazy_cb_wrapper lazy_cb = lazy_cb_wrapper
# Before-solve callbacks
logger.debug("Running before_solve_mip callbacks...")
for component in self.components.values():
component.before_solve_mip(self, instance, model)
# Solve MIP # Solve MIP
logger.info("Solving MIP...") logger.info("Solving MIP...")
stats = cast( mip_stats = self.internal_solver.solve(
LearningSolveStats, tee=tee,
self.internal_solver.solve( iteration_cb=iteration_cb_wrapper,
tee=tee, lazy_cb=lazy_cb,
iteration_cb=iteration_cb_wrapper,
lazy_cb=lazy_cb,
),
) )
stats.update(cast(LearningSolveStats, mip_stats))
if "LP value" in training_sample.keys(): if "LP value" in training_sample.keys():
stats["LP value"] = training_sample["LP value"] stats["LP value"] = training_sample["LP value"]
stats["Solver"] = "default" stats["Solver"] = "default"
@ -234,7 +244,7 @@ class LearningSolver:
# Add some information to training_sample # Add some information to training_sample
training_sample["Lower bound"] = stats["Lower bound"] training_sample["Lower bound"] = stats["Lower bound"]
training_sample["Upper bound"] = stats["Upper bound"] training_sample["Upper bound"] = stats["Upper bound"]
training_sample["MIP log"] = stats["Log"] training_sample["MIP log"] = stats["MIP log"]
training_sample["Solution"] = self.internal_solver.get_solution() training_sample["Solution"] = self.internal_solver.get_solution()
# After-solve callbacks # After-solve callbacks

@ -67,8 +67,8 @@ class BasePyomoSolver(InternalSolver):
if not self.is_infeasible(): if not self.is_infeasible():
opt_value = results["Problem"][0]["Lower bound"] opt_value = results["Problem"][0]["Lower bound"]
return { return {
"Optimal value": opt_value, "LP value": opt_value,
"Log": streams[0].getvalue(), "LP log": streams[0].getvalue(),
} }
def _restore_integrality(self) -> None: def _restore_integrality(self) -> None:
@ -114,10 +114,9 @@ class BasePyomoSolver(InternalSolver):
"Upper bound": ub, "Upper bound": ub,
"Wallclock time": total_wallclock_time, "Wallclock time": total_wallclock_time,
"Sense": self._obj_sense, "Sense": self._obj_sense,
"Log": log, "MIP log": log,
"Nodes": node_count, "Nodes": node_count,
"Warm start value": ws_value, "Warm start value": ws_value,
"LP value": None,
} }
return stats return stats

@ -28,8 +28,8 @@ TrainingSample = TypedDict(
LPSolveStats = TypedDict( LPSolveStats = TypedDict(
"LPSolveStats", "LPSolveStats",
{ {
"Optimal value": Optional[float], "LP log": str,
"Log": str, "LP value": Optional[float],
}, },
) )
@ -37,13 +37,12 @@ MIPSolveStats = TypedDict(
"MIPSolveStats", "MIPSolveStats",
{ {
"Lower bound": Optional[float], "Lower bound": Optional[float],
"Upper bound": Optional[float], "MIP log": str,
"Wallclock time": float,
"Nodes": Optional[int], "Nodes": Optional[int],
"Sense": str, "Sense": str,
"Log": str, "Upper bound": Optional[float],
"Wallclock time": float,
"Warm start value": Optional[float], "Warm start value": Optional[float],
"LP value": Optional[float],
}, },
) )
@ -52,21 +51,22 @@ LearningSolveStats = TypedDict(
{ {
"Gap": Optional[float], "Gap": Optional[float],
"Instance": Union[str, int], "Instance": Union[str, int],
"LP log": str,
"LP value": Optional[float], "LP value": Optional[float],
"Log": str,
"Lower bound": Optional[float], "Lower bound": Optional[float],
"MIP log": str,
"Mode": str, "Mode": str,
"Nodes": Optional[int], "Nodes": Optional[int],
"Objective: predicted LB": float,
"Objective: predicted UB": float,
"Primal: free": int,
"Primal: one": int,
"Primal: zero": int,
"Sense": str, "Sense": str,
"Solver": str, "Solver": str,
"Upper bound": Optional[float], "Upper bound": Optional[float],
"Wallclock time": float, "Wallclock time": float,
"Warm start value": Optional[float], "Warm start value": Optional[float],
"Primal: free": int,
"Primal: zero": int,
"Primal: one": int,
"Objective: predicted LB": float,
"Objective: predicted UB": float,
}, },
total=False, total=False,
) )

@ -68,7 +68,7 @@ def test_convert_tight_infeasibility():
solver = LearningSolver( solver = LearningSolver(
solver=GurobiSolver, solver=GurobiSolver,
components=[comp], components=[comp],
solve_lp_first=False, solve_lp=False,
) )
instance = SampleInstance() instance = SampleInstance()
stats = solver.solve(instance) stats = solver.solve(instance)
@ -91,7 +91,7 @@ def test_convert_tight_suboptimality():
solver = LearningSolver( solver = LearningSolver(
solver=GurobiSolver, solver=GurobiSolver,
components=[comp], components=[comp],
solve_lp_first=False, solve_lp=False,
) )
instance = SampleInstance() instance = SampleInstance()
stats = solver.solve(instance) stats = solver.solve(instance)
@ -114,7 +114,7 @@ def test_convert_tight_optimal():
solver = LearningSolver( solver = LearningSolver(
solver=GurobiSolver, solver=GurobiSolver,
components=[comp], components=[comp],
solve_lp_first=False, solve_lp=False,
) )
instance = SampleInstance() instance = SampleInstance()
stats = solver.solve(instance) stats = solver.solve(instance)

@ -93,8 +93,8 @@ def test_internal_solver():
stats = solver.solve_lp() stats = solver.solve_lp()
assert not solver.is_infeasible() assert not solver.is_infeasible()
assert round(stats["Optimal value"], 3) == 1287.923 assert round(stats["LP value"], 3) == 1287.923
assert len(stats["Log"]) > 100 assert len(stats["LP log"]) > 100
solution = solver.get_solution() solution = solver.get_solution()
assert round(solution["x"][0], 3) == 1.000 assert round(solution["x"][0], 3) == 1.000
@ -104,7 +104,7 @@ def test_internal_solver():
stats = solver.solve(tee=True) stats = solver.solve(tee=True)
assert not solver.is_infeasible() assert not solver.is_infeasible()
assert len(stats["Log"]) > 100 assert len(stats["MIP log"]) > 100
assert stats["Lower bound"] == 1183.0 assert stats["Lower bound"] == 1183.0
assert stats["Upper bound"] == 1183.0 assert stats["Upper bound"] == 1183.0
assert stats["Sense"] == "max" assert stats["Sense"] == "max"
@ -198,7 +198,7 @@ def test_infeasible_instance():
stats = solver.solve_lp() stats = solver.solve_lp()
assert solver.get_solution() is None assert solver.get_solution() is None
assert stats["Optimal value"] is None assert stats["LP value"] is None
assert solver.get_value("x", 0) is None assert solver.get_value("x", 0) is None

@ -57,7 +57,7 @@ def test_solve_without_lp():
instance = _get_knapsack_instance(internal_solver) instance = _get_knapsack_instance(internal_solver)
solver = LearningSolver( solver = LearningSolver(
solver=internal_solver, solver=internal_solver,
solve_lp_first=False, solve_lp=False,
) )
solver.solve(instance) solver.solve(instance)
solver.fit([instance]) solver.fit([instance])

@ -29,7 +29,7 @@ def test_benchmark():
benchmark = BenchmarkRunner(test_solvers) benchmark = BenchmarkRunner(test_solvers)
benchmark.fit(train_instances) benchmark.fit(train_instances)
benchmark.parallel_solve(test_instances, n_jobs=2, n_trials=2) benchmark.parallel_solve(test_instances, n_jobs=2, n_trials=2)
assert benchmark.results.values.shape == (12, 17) assert benchmark.results.values.shape == (12, 18)
benchmark.write_csv("/tmp/benchmark.csv") benchmark.write_csv("/tmp/benchmark.csv")
assert os.path.isfile("/tmp/benchmark.csv") assert os.path.isfile("/tmp/benchmark.csv")

Loading…
Cancel
Save