|
|
@ -2,26 +2,24 @@
|
|
|
|
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
|
|
|
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
|
|
|
|
# Released under the modified BSD license. See COPYING.md for more details.
|
|
|
|
# Released under the modified BSD license. See COPYING.md for more details.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import gzip
|
|
|
|
import logging
|
|
|
|
import logging
|
|
|
|
import pickle
|
|
|
|
|
|
|
|
import os
|
|
|
|
import os
|
|
|
|
|
|
|
|
import pickle
|
|
|
|
import tempfile
|
|
|
|
import tempfile
|
|
|
|
import gzip
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
from copy import deepcopy
|
|
|
|
from copy import deepcopy
|
|
|
|
from typing import Optional, List
|
|
|
|
from typing import Optional, List, Any, IO, cast, BinaryIO, Union
|
|
|
|
|
|
|
|
|
|
|
|
from p_tqdm import p_map
|
|
|
|
from p_tqdm import p_map
|
|
|
|
from tempfile import NamedTemporaryFile
|
|
|
|
|
|
|
|
|
|
|
|
from miplearn.components.cuts import UserCutsComponent
|
|
|
|
from . import RedirectOutput
|
|
|
|
from miplearn.components.lazy_dynamic import DynamicLazyConstraintsComponent
|
|
|
|
from .. import (
|
|
|
|
from miplearn.components.objective import ObjectiveValueComponent
|
|
|
|
ObjectiveValueComponent,
|
|
|
|
from miplearn.components.primal import PrimalSolutionComponent
|
|
|
|
PrimalSolutionComponent,
|
|
|
|
from miplearn.instance import Instance
|
|
|
|
DynamicLazyConstraintsComponent,
|
|
|
|
from miplearn.solvers import RedirectOutput
|
|
|
|
UserCutsComponent,
|
|
|
|
from miplearn.solvers.pyomo.gurobi import GurobiPyomoSolver
|
|
|
|
)
|
|
|
|
from miplearn.types import MIPSolveStats, TrainingSample
|
|
|
|
from ..solvers.internal import InternalSolver
|
|
|
|
|
|
|
|
from ..solvers.pyomo.gurobi import GurobiPyomoSolver
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
@ -117,11 +115,11 @@ class LearningSolver:
|
|
|
|
|
|
|
|
|
|
|
|
def solve(
|
|
|
|
def solve(
|
|
|
|
self,
|
|
|
|
self,
|
|
|
|
instance,
|
|
|
|
instance: Union[Instance, str],
|
|
|
|
model=None,
|
|
|
|
model: Any = None,
|
|
|
|
output="",
|
|
|
|
output: str = "",
|
|
|
|
tee=False,
|
|
|
|
tee: bool = False,
|
|
|
|
):
|
|
|
|
) -> MIPSolveStats:
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
Solves the given instance. If trained machine-learning models are
|
|
|
|
Solves the given instance. If trained machine-learning models are
|
|
|
|
available, they will be used to accelerate the solution process.
|
|
|
|
available, they will be used to accelerate the solution process.
|
|
|
@ -129,20 +127,9 @@ class LearningSolver:
|
|
|
|
The argument `instance` may be either an Instance object or a
|
|
|
|
The argument `instance` may be either an Instance object or a
|
|
|
|
filename pointing to a pickled Instance object.
|
|
|
|
filename pointing to a pickled Instance object.
|
|
|
|
|
|
|
|
|
|
|
|
This method modifies the instance object. Specifically, the following
|
|
|
|
This method adds a new training sample to `instance.training_sample`.
|
|
|
|
properties are set:
|
|
|
|
If a filename is provided, then the file is modified in-place. That is,
|
|
|
|
|
|
|
|
the original file is overwritten.
|
|
|
|
- instance.lp_solution
|
|
|
|
|
|
|
|
- instance.lp_value
|
|
|
|
|
|
|
|
- instance.lower_bound
|
|
|
|
|
|
|
|
- instance.upper_bound
|
|
|
|
|
|
|
|
- instance.solution
|
|
|
|
|
|
|
|
- instance.solver_log
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Additional solver components may set additional properties. Please
|
|
|
|
|
|
|
|
see their documentation for more details. If a filename is provided,
|
|
|
|
|
|
|
|
then the file is modified in-place. That is, the original file is
|
|
|
|
|
|
|
|
overwritten.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
If `solver.solve_lp_first` is False, the properties lp_solution and
|
|
|
|
If `solver.solve_lp_first` is False, the properties lp_solution and
|
|
|
|
lp_value will be set to dummy values.
|
|
|
|
lp_value will be set to dummy values.
|
|
|
@ -192,46 +179,62 @@ class LearningSolver:
|
|
|
|
|
|
|
|
|
|
|
|
def _solve(
|
|
|
|
def _solve(
|
|
|
|
self,
|
|
|
|
self,
|
|
|
|
instance,
|
|
|
|
instance: Union[Instance, str],
|
|
|
|
model=None,
|
|
|
|
model: Any = None,
|
|
|
|
output="",
|
|
|
|
output: str = "",
|
|
|
|
tee=False,
|
|
|
|
tee: bool = False,
|
|
|
|
):
|
|
|
|
) -> MIPSolveStats:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Load instance from file, if necessary
|
|
|
|
filename = None
|
|
|
|
filename = None
|
|
|
|
fileformat = None
|
|
|
|
fileformat = None
|
|
|
|
|
|
|
|
file: Union[BinaryIO, gzip.GzipFile]
|
|
|
|
if isinstance(instance, str):
|
|
|
|
if isinstance(instance, str):
|
|
|
|
filename = instance
|
|
|
|
filename = instance
|
|
|
|
logger.info("Reading: %s" % filename)
|
|
|
|
logger.info("Reading: %s" % filename)
|
|
|
|
if filename.endswith(".gz"):
|
|
|
|
if filename.endswith(".gz"):
|
|
|
|
fileformat = "pickle-gz"
|
|
|
|
fileformat = "pickle-gz"
|
|
|
|
with gzip.GzipFile(filename, "rb") as file:
|
|
|
|
with gzip.GzipFile(filename, "rb") as file:
|
|
|
|
instance = pickle.load(file)
|
|
|
|
instance = pickle.load(cast(IO[bytes], file))
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
fileformat = "pickle"
|
|
|
|
fileformat = "pickle"
|
|
|
|
with open(filename, "rb") as file:
|
|
|
|
with open(filename, "rb") as file:
|
|
|
|
instance = pickle.load(file)
|
|
|
|
instance = pickle.load(cast(IO[bytes], file))
|
|
|
|
|
|
|
|
assert isinstance(instance, Instance)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Generate model
|
|
|
|
if model is None:
|
|
|
|
if model is None:
|
|
|
|
with RedirectOutput([]):
|
|
|
|
with RedirectOutput([]):
|
|
|
|
model = instance.to_model()
|
|
|
|
model = instance.to_model()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Initialize training sample
|
|
|
|
|
|
|
|
training_sample: TrainingSample = {}
|
|
|
|
|
|
|
|
if not hasattr(instance, "training_data"):
|
|
|
|
|
|
|
|
instance.training_data = []
|
|
|
|
|
|
|
|
instance.training_data += [training_sample]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Initialize internal solver
|
|
|
|
self.tee = tee
|
|
|
|
self.tee = tee
|
|
|
|
self.internal_solver = self.solver_factory()
|
|
|
|
self.internal_solver = self.solver_factory()
|
|
|
|
self.internal_solver.set_instance(instance, model)
|
|
|
|
self.internal_solver.set_instance(instance, model)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Solve linear relaxation
|
|
|
|
if self.solve_lp_first:
|
|
|
|
if self.solve_lp_first:
|
|
|
|
logger.info("Solving LP relaxation...")
|
|
|
|
logger.info("Solving LP relaxation...")
|
|
|
|
results = self.internal_solver.solve_lp(tee=tee)
|
|
|
|
stats = self.internal_solver.solve_lp(tee=tee)
|
|
|
|
instance.lp_solution = self.internal_solver.get_solution()
|
|
|
|
training_sample["LP solution"] = self.internal_solver.get_solution()
|
|
|
|
instance.lp_value = results["Optimal value"]
|
|
|
|
training_sample["LP value"] = stats["Optimal value"]
|
|
|
|
|
|
|
|
training_sample["LP log"] = stats["Log"]
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
instance.lp_solution = self.internal_solver.get_empty_solution()
|
|
|
|
training_sample["LP solution"] = self.internal_solver.get_empty_solution()
|
|
|
|
instance.lp_value = 0.0
|
|
|
|
training_sample["LP value"] = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Before-solve callbacks
|
|
|
|
logger.debug("Running before_solve callbacks...")
|
|
|
|
logger.debug("Running before_solve callbacks...")
|
|
|
|
for component in self.components.values():
|
|
|
|
for component in self.components.values():
|
|
|
|
component.before_solve(self, instance, model)
|
|
|
|
component.before_solve(self, instance, model)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Define wrappers
|
|
|
|
def iteration_cb():
|
|
|
|
def iteration_cb():
|
|
|
|
should_repeat = False
|
|
|
|
should_repeat = False
|
|
|
|
for comp in self.components.values():
|
|
|
|
for comp in self.components.values():
|
|
|
@ -247,29 +250,28 @@ class LearningSolver:
|
|
|
|
if self.use_lazy_cb:
|
|
|
|
if self.use_lazy_cb:
|
|
|
|
lazy_cb = lazy_cb_wrapper
|
|
|
|
lazy_cb = lazy_cb_wrapper
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Solve MILP
|
|
|
|
logger.info("Solving MILP...")
|
|
|
|
logger.info("Solving MILP...")
|
|
|
|
stats = self.internal_solver.solve(
|
|
|
|
stats = self.internal_solver.solve(
|
|
|
|
tee=tee,
|
|
|
|
tee=tee,
|
|
|
|
iteration_cb=iteration_cb,
|
|
|
|
iteration_cb=iteration_cb,
|
|
|
|
lazy_cb=lazy_cb,
|
|
|
|
lazy_cb=lazy_cb,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
stats["LP value"] = instance.lp_value
|
|
|
|
if "LP value" in training_sample.keys():
|
|
|
|
|
|
|
|
stats["LP value"] = training_sample["LP value"]
|
|
|
|
|
|
|
|
|
|
|
|
# Read MIP solution and bounds
|
|
|
|
# Read MIP solution and bounds
|
|
|
|
instance.lower_bound = stats["Lower bound"]
|
|
|
|
training_sample["Lower bound"] = stats["Lower bound"]
|
|
|
|
instance.upper_bound = stats["Upper bound"]
|
|
|
|
training_sample["Upper bound"] = stats["Upper bound"]
|
|
|
|
instance.solver_log = stats["Log"]
|
|
|
|
training_sample["MIP log"] = stats["Log"]
|
|
|
|
instance.solution = self.internal_solver.get_solution()
|
|
|
|
training_sample["Solution"] = self.internal_solver.get_solution()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# After-solve callbacks
|
|
|
|
logger.debug("Calling after_solve callbacks...")
|
|
|
|
logger.debug("Calling after_solve callbacks...")
|
|
|
|
training_data = {}
|
|
|
|
|
|
|
|
for component in self.components.values():
|
|
|
|
for component in self.components.values():
|
|
|
|
component.after_solve(self, instance, model, stats, training_data)
|
|
|
|
component.after_solve(self, instance, model, stats, training_sample)
|
|
|
|
|
|
|
|
|
|
|
|
if not hasattr(instance, "training_data"):
|
|
|
|
|
|
|
|
instance.training_data = []
|
|
|
|
|
|
|
|
instance.training_data += [training_data]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Write to file, if necessary
|
|
|
|
if filename is not None and output is not None:
|
|
|
|
if filename is not None and output is not None:
|
|
|
|
output_filename = output
|
|
|
|
output_filename = output
|
|
|
|
if len(output) == 0:
|
|
|
|
if len(output) == 0:
|
|
|
@ -277,11 +279,10 @@ class LearningSolver:
|
|
|
|
logger.info("Writing: %s" % output_filename)
|
|
|
|
logger.info("Writing: %s" % output_filename)
|
|
|
|
if fileformat == "pickle":
|
|
|
|
if fileformat == "pickle":
|
|
|
|
with open(output_filename, "wb") as file:
|
|
|
|
with open(output_filename, "wb") as file:
|
|
|
|
pickle.dump(instance, file)
|
|
|
|
pickle.dump(instance, cast(IO[bytes], file))
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
with gzip.GzipFile(output_filename, "wb") as file:
|
|
|
|
with gzip.GzipFile(output_filename, "wb") as file:
|
|
|
|
pickle.dump(instance, file)
|
|
|
|
pickle.dump(instance, cast(IO[bytes], file))
|
|
|
|
|
|
|
|
|
|
|
|
return stats
|
|
|
|
return stats
|
|
|
|
|
|
|
|
|
|
|
|
def parallel_solve(
|
|
|
|
def parallel_solve(
|
|
|
@ -340,7 +341,7 @@ class LearningSolver:
|
|
|
|
self._restore_miplearn_logger()
|
|
|
|
self._restore_miplearn_logger()
|
|
|
|
return stats
|
|
|
|
return stats
|
|
|
|
|
|
|
|
|
|
|
|
def fit(self, training_instances):
|
|
|
|
def fit(self, training_instances: Union[List[str], List[Instance]]) -> None:
|
|
|
|
if len(training_instances) == 0:
|
|
|
|
if len(training_instances) == 0:
|
|
|
|
return
|
|
|
|
return
|
|
|
|
for component in self.components.values():
|
|
|
|
for component in self.components.values():
|
|
|
|