|
|
@ -5,11 +5,13 @@
|
|
|
|
import logging
|
|
|
|
import logging
|
|
|
|
import time
|
|
|
|
import time
|
|
|
|
import traceback
|
|
|
|
import traceback
|
|
|
|
from typing import Optional, List, Any, cast, Dict, Tuple
|
|
|
|
from typing import Optional, List, Any, cast, Dict, Tuple, Callable, IO, Union
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
from overrides import overrides
|
|
|
|
from p_tqdm import p_map, p_umap
|
|
|
|
from p_tqdm import p_map, p_umap
|
|
|
|
from tqdm.auto import tqdm
|
|
|
|
from tqdm.auto import tqdm
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
from miplearn.features.sample import Hdf5Sample, Sample
|
|
|
|
from miplearn.components.component import Component
|
|
|
|
from miplearn.components.component import Component
|
|
|
|
from miplearn.components.dynamic_lazy import DynamicLazyConstraintsComponent
|
|
|
|
from miplearn.components.dynamic_lazy import DynamicLazyConstraintsComponent
|
|
|
|
from miplearn.components.dynamic_user_cuts import UserCutsComponent
|
|
|
|
from miplearn.components.dynamic_user_cuts import UserCutsComponent
|
|
|
@ -17,15 +19,58 @@ from miplearn.components.objective import ObjectiveValueComponent
|
|
|
|
from miplearn.components.primal import PrimalSolutionComponent
|
|
|
|
from miplearn.components.primal import PrimalSolutionComponent
|
|
|
|
from miplearn.features.extractor import FeaturesExtractor
|
|
|
|
from miplearn.features.extractor import FeaturesExtractor
|
|
|
|
from miplearn.instance.base import Instance
|
|
|
|
from miplearn.instance.base import Instance
|
|
|
|
from miplearn.instance.picklegz import PickleGzInstance
|
|
|
|
|
|
|
|
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 LearningSolveStats
|
|
|
|
from miplearn.types import LearningSolveStats
|
|
|
|
|
|
|
|
import gzip
|
|
|
|
|
|
|
|
import pickle
|
|
|
|
|
|
|
|
import miplearn
|
|
|
|
|
|
|
|
from os.path import exists
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FileInstanceWrapper(Instance):
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
|
|
|
self,
|
|
|
|
|
|
|
|
data_filename: Any,
|
|
|
|
|
|
|
|
build_model: Callable,
|
|
|
|
|
|
|
|
):
|
|
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
|
|
assert data_filename.endswith(".pkl.gz")
|
|
|
|
|
|
|
|
self.filename = data_filename
|
|
|
|
|
|
|
|
self.sample_filename = data_filename.replace(".pkl.gz", ".h5")
|
|
|
|
|
|
|
|
self.sample = Hdf5Sample(
|
|
|
|
|
|
|
|
self.sample_filename,
|
|
|
|
|
|
|
|
mode="r+" if exists(self.sample_filename) else "w",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
self.build_model = build_model
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def to_model(self) -> Any:
|
|
|
|
|
|
|
|
return miplearn.load(self.filename, self.build_model)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def create_sample(self) -> Sample:
|
|
|
|
|
|
|
|
return self.sample
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def get_samples(self) -> List[Sample]:
|
|
|
|
|
|
|
|
return [self.sample]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MemoryInstanceWrapper(Instance):
|
|
|
|
|
|
|
|
def __init__(self, model: Any) -> None:
|
|
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
|
|
assert model is not None
|
|
|
|
|
|
|
|
self.model = model
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@overrides
|
|
|
|
|
|
|
|
def to_model(self) -> Any:
|
|
|
|
|
|
|
|
return self.model
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _GlobalVariables:
|
|
|
|
class _GlobalVariables:
|
|
|
|
def __init__(self) -> None:
|
|
|
|
def __init__(self) -> None:
|
|
|
|
self.solver: Optional[LearningSolver] = None
|
|
|
|
self.solver: Optional[LearningSolver] = None
|
|
|
@ -48,7 +93,7 @@ def _parallel_solve(
|
|
|
|
assert solver is not None
|
|
|
|
assert solver is not None
|
|
|
|
assert instances is not None
|
|
|
|
assert instances is not None
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
stats = solver.solve(
|
|
|
|
stats = solver._solve(
|
|
|
|
instances[idx],
|
|
|
|
instances[idx],
|
|
|
|
discard_output=discard_outputs,
|
|
|
|
discard_output=discard_outputs,
|
|
|
|
)
|
|
|
|
)
|
|
|
@ -87,11 +132,6 @@ class LearningSolver:
|
|
|
|
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.
|
|
|
|
simulate_perfect: bool
|
|
|
|
|
|
|
|
If true, each call to solve actually performs three actions: solve
|
|
|
|
|
|
|
|
the original problem, train the ML models on the data that was just
|
|
|
|
|
|
|
|
collected, and solve the problem again. This is useful for evaluating
|
|
|
|
|
|
|
|
the theoretical performance of perfect ML models.
|
|
|
|
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
def __init__(
|
|
|
@ -101,7 +141,6 @@ class LearningSolver:
|
|
|
|
solver: Optional[InternalSolver] = None,
|
|
|
|
solver: Optional[InternalSolver] = None,
|
|
|
|
use_lazy_cb: bool = False,
|
|
|
|
use_lazy_cb: bool = False,
|
|
|
|
solve_lp: bool = True,
|
|
|
|
solve_lp: bool = True,
|
|
|
|
simulate_perfect: bool = False,
|
|
|
|
|
|
|
|
extractor: Optional[FeaturesExtractor] = None,
|
|
|
|
extractor: Optional[FeaturesExtractor] = None,
|
|
|
|
extract_lhs: bool = True,
|
|
|
|
extract_lhs: bool = True,
|
|
|
|
extract_sa: bool = True,
|
|
|
|
extract_sa: bool = True,
|
|
|
@ -118,7 +157,6 @@ class LearningSolver:
|
|
|
|
self.internal_solver: Optional[InternalSolver] = None
|
|
|
|
self.internal_solver: Optional[InternalSolver] = None
|
|
|
|
self.internal_solver_prototype: InternalSolver = solver
|
|
|
|
self.internal_solver_prototype: InternalSolver = solver
|
|
|
|
self.mode: str = mode
|
|
|
|
self.mode: str = mode
|
|
|
|
self.simulate_perfect: bool = simulate_perfect
|
|
|
|
|
|
|
|
self.solve_lp: bool = solve_lp
|
|
|
|
self.solve_lp: bool = solve_lp
|
|
|
|
self.tee = False
|
|
|
|
self.tee = False
|
|
|
|
self.use_lazy_cb: bool = use_lazy_cb
|
|
|
|
self.use_lazy_cb: bool = use_lazy_cb
|
|
|
@ -140,6 +178,44 @@ class LearningSolver:
|
|
|
|
discard_output: bool = False,
|
|
|
|
discard_output: bool = False,
|
|
|
|
tee: bool = False,
|
|
|
|
tee: bool = False,
|
|
|
|
) -> LearningSolveStats:
|
|
|
|
) -> LearningSolveStats:
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
Solves the given instance. If trained machine-learning models are
|
|
|
|
|
|
|
|
available, they will be used to accelerate the solution process.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The argument `instance` may be either an Instance object or a
|
|
|
|
|
|
|
|
filename pointing to a pickled Instance object.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
This method adds a new training sample to `instance.training_sample`.
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
lp_value will be set to dummy values.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
|
|
----------
|
|
|
|
|
|
|
|
instance: Instance
|
|
|
|
|
|
|
|
The instance to be solved.
|
|
|
|
|
|
|
|
model: Any
|
|
|
|
|
|
|
|
The corresponding Pyomo model. If not provided, it will be created.
|
|
|
|
|
|
|
|
discard_output: bool
|
|
|
|
|
|
|
|
If True, do not write the modified instances anywhere; simply discard
|
|
|
|
|
|
|
|
them. Useful during benchmarking.
|
|
|
|
|
|
|
|
tee: bool
|
|
|
|
|
|
|
|
If true, prints solver log to screen.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
|
|
-------
|
|
|
|
|
|
|
|
LearningSolveStats
|
|
|
|
|
|
|
|
A dictionary of solver statistics containing at least the following
|
|
|
|
|
|
|
|
keys: "Lower bound", "Upper bound", "Wallclock time", "Nodes",
|
|
|
|
|
|
|
|
"Sense", "Log", "Warm start value" and "LP value".
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Additional components may generate additional keys. For example,
|
|
|
|
|
|
|
|
ObjectiveValueComponent adds the keys "Predicted LB" and
|
|
|
|
|
|
|
|
"Predicted UB". See the documentation of each component for more
|
|
|
|
|
|
|
|
details.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
# Generate model
|
|
|
|
# Generate model
|
|
|
|
# -------------------------------------------------------
|
|
|
|
# -------------------------------------------------------
|
|
|
@ -300,65 +376,25 @@ class LearningSolver:
|
|
|
|
|
|
|
|
|
|
|
|
def solve(
|
|
|
|
def solve(
|
|
|
|
self,
|
|
|
|
self,
|
|
|
|
instance: Instance,
|
|
|
|
arg: Union[Any, List[str]],
|
|
|
|
model: Any = None,
|
|
|
|
build_model: Optional[Callable] = None,
|
|
|
|
discard_output: bool = False,
|
|
|
|
|
|
|
|
tee: bool = False,
|
|
|
|
tee: bool = False,
|
|
|
|
) -> LearningSolveStats:
|
|
|
|
) -> Union[LearningSolveStats, List[LearningSolveStats]]:
|
|
|
|
"""
|
|
|
|
if isinstance(arg, list):
|
|
|
|
Solves the given instance. If trained machine-learning models are
|
|
|
|
assert build_model is not None
|
|
|
|
available, they will be used to accelerate the solution process.
|
|
|
|
stats = []
|
|
|
|
|
|
|
|
for i in arg:
|
|
|
|
The argument `instance` may be either an Instance object or a
|
|
|
|
s = self._solve(FileInstanceWrapper(i, build_model), tee=tee)
|
|
|
|
filename pointing to a pickled Instance object.
|
|
|
|
stats.append(s)
|
|
|
|
|
|
|
|
return stats
|
|
|
|
This method adds a new training sample to `instance.training_sample`.
|
|
|
|
else:
|
|
|
|
If a filename is provided, then the file is modified in-place. That is,
|
|
|
|
return self._solve(MemoryInstanceWrapper(arg), tee=tee)
|
|
|
|
the original file is overwritten.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
If `solver.solve_lp_first` is False, the properties lp_solution and
|
|
|
|
|
|
|
|
lp_value will be set to dummy values.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
|
|
|
|
----------
|
|
|
|
|
|
|
|
instance: Instance
|
|
|
|
|
|
|
|
The instance to be solved.
|
|
|
|
|
|
|
|
model: Any
|
|
|
|
|
|
|
|
The corresponding Pyomo model. If not provided, it will be created.
|
|
|
|
|
|
|
|
discard_output: bool
|
|
|
|
|
|
|
|
If True, do not write the modified instances anywhere; simply discard
|
|
|
|
|
|
|
|
them. Useful during benchmarking.
|
|
|
|
|
|
|
|
tee: bool
|
|
|
|
|
|
|
|
If true, prints solver log to screen.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Returns
|
|
|
|
|
|
|
|
-------
|
|
|
|
|
|
|
|
LearningSolveStats
|
|
|
|
|
|
|
|
A dictionary of solver statistics containing at least the following
|
|
|
|
|
|
|
|
keys: "Lower bound", "Upper bound", "Wallclock time", "Nodes",
|
|
|
|
|
|
|
|
"Sense", "Log", "Warm start value" and "LP value".
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Additional components may generate additional keys. For example,
|
|
|
|
def fit(self, filenames: List[str], build_model: Callable) -> None:
|
|
|
|
ObjectiveValueComponent adds the keys "Predicted LB" and
|
|
|
|
instances: List[Instance] = [
|
|
|
|
"Predicted UB". See the documentation of each component for more
|
|
|
|
FileInstanceWrapper(f, build_model) for f in filenames
|
|
|
|
details.
|
|
|
|
]
|
|
|
|
"""
|
|
|
|
self._fit(instances)
|
|
|
|
if self.simulate_perfect:
|
|
|
|
|
|
|
|
if not isinstance(instance, PickleGzInstance):
|
|
|
|
|
|
|
|
raise Exception("Not implemented")
|
|
|
|
|
|
|
|
self._solve(
|
|
|
|
|
|
|
|
instance=instance,
|
|
|
|
|
|
|
|
model=model,
|
|
|
|
|
|
|
|
tee=tee,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
self.fit([instance])
|
|
|
|
|
|
|
|
instance.instance = None
|
|
|
|
|
|
|
|
return self._solve(
|
|
|
|
|
|
|
|
instance=instance,
|
|
|
|
|
|
|
|
model=model,
|
|
|
|
|
|
|
|
discard_output=discard_output,
|
|
|
|
|
|
|
|
tee=tee,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parallel_solve(
|
|
|
|
def parallel_solve(
|
|
|
|
self,
|
|
|
|
self,
|
|
|
@ -397,7 +433,7 @@ class LearningSolver:
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
if n_jobs == 1:
|
|
|
|
if n_jobs == 1:
|
|
|
|
return [
|
|
|
|
return [
|
|
|
|
self.solve(p)
|
|
|
|
self._solve(p)
|
|
|
|
for p in tqdm(
|
|
|
|
for p in tqdm(
|
|
|
|
instances,
|
|
|
|
instances,
|
|
|
|
disable=not progress,
|
|
|
|
disable=not progress,
|
|
|
@ -425,7 +461,7 @@ class LearningSolver:
|
|
|
|
self._restore_miplearn_logger()
|
|
|
|
self._restore_miplearn_logger()
|
|
|
|
return stats
|
|
|
|
return stats
|
|
|
|
|
|
|
|
|
|
|
|
def fit(
|
|
|
|
def _fit(
|
|
|
|
self,
|
|
|
|
self,
|
|
|
|
training_instances: List[Instance],
|
|
|
|
training_instances: List[Instance],
|
|
|
|
n_jobs: int = 1,
|
|
|
|
n_jobs: int = 1,
|
|
|
|