mirror of
https://github.com/ANL-CEEESA/MIPLearn.git
synced 2025-12-06 09:28:51 -06:00
Add after_solve_lp callback; make dict keys consistent
This commit is contained in:
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user