mirror of
https://github.com/ANL-CEEESA/MIPLearn.git
synced 2025-12-06 09:28:51 -06:00
Merge branch 'feature/new-py-api' into feature/docs
This commit is contained in:
@@ -710,7 +710,7 @@ class GurobiTestInstanceKnapsack(PyomoTestInstanceKnapsack):
|
||||
self,
|
||||
solver: InternalSolver,
|
||||
model: Any,
|
||||
violation: str,
|
||||
violation_data: Any,
|
||||
) -> None:
|
||||
x0 = model.getVarByName("x[0]")
|
||||
model.cbLazy(x0 <= 0)
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
import logging
|
||||
import time
|
||||
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 tqdm.auto import tqdm
|
||||
|
||||
from miplearn.features.sample import Hdf5Sample, Sample
|
||||
from miplearn.components.component import Component
|
||||
from miplearn.components.dynamic_lazy import DynamicLazyConstraintsComponent
|
||||
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.features.extractor import FeaturesExtractor
|
||||
from miplearn.instance.base import Instance
|
||||
from miplearn.instance.picklegz import PickleGzInstance
|
||||
from miplearn.solvers import _RedirectOutput
|
||||
from miplearn.solvers.internal import InternalSolver
|
||||
from miplearn.solvers.pyomo.gurobi import GurobiPyomoSolver
|
||||
from miplearn.types import LearningSolveStats
|
||||
import gzip
|
||||
import pickle
|
||||
import miplearn
|
||||
from os.path import exists
|
||||
|
||||
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:
|
||||
def __init__(self) -> None:
|
||||
self.solver: Optional[LearningSolver] = None
|
||||
@@ -48,7 +93,7 @@ def _parallel_solve(
|
||||
assert solver is not None
|
||||
assert instances is not None
|
||||
try:
|
||||
stats = solver.solve(
|
||||
stats = solver._solve(
|
||||
instances[idx],
|
||||
discard_output=discard_outputs,
|
||||
)
|
||||
@@ -87,11 +132,6 @@ class LearningSolver:
|
||||
option should be activated if the LP relaxation is not very
|
||||
expensive to solve and if it provides good hints for the integer
|
||||
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__(
|
||||
@@ -101,7 +141,6 @@ class LearningSolver:
|
||||
solver: Optional[InternalSolver] = None,
|
||||
use_lazy_cb: bool = False,
|
||||
solve_lp: bool = True,
|
||||
simulate_perfect: bool = False,
|
||||
extractor: Optional[FeaturesExtractor] = None,
|
||||
extract_lhs: bool = True,
|
||||
extract_sa: bool = True,
|
||||
@@ -118,7 +157,6 @@ class LearningSolver:
|
||||
self.internal_solver: Optional[InternalSolver] = None
|
||||
self.internal_solver_prototype: InternalSolver = solver
|
||||
self.mode: str = mode
|
||||
self.simulate_perfect: bool = simulate_perfect
|
||||
self.solve_lp: bool = solve_lp
|
||||
self.tee = False
|
||||
self.use_lazy_cb: bool = use_lazy_cb
|
||||
@@ -140,6 +178,44 @@ class LearningSolver:
|
||||
discard_output: bool = False,
|
||||
tee: bool = False,
|
||||
) -> 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
|
||||
# -------------------------------------------------------
|
||||
@@ -300,65 +376,25 @@ class LearningSolver:
|
||||
|
||||
def solve(
|
||||
self,
|
||||
instance: Instance,
|
||||
model: Any = None,
|
||||
discard_output: bool = False,
|
||||
arg: Union[Any, List[str]],
|
||||
build_model: Optional[Callable] = None,
|
||||
tee: bool = False,
|
||||
) -> LearningSolveStats:
|
||||
"""
|
||||
Solves the given instance. If trained machine-learning models are
|
||||
available, they will be used to accelerate the solution process.
|
||||
) -> Union[LearningSolveStats, List[LearningSolveStats]]:
|
||||
if isinstance(arg, list):
|
||||
assert build_model is not None
|
||||
stats = []
|
||||
for i in arg:
|
||||
s = self._solve(FileInstanceWrapper(i, build_model), tee=tee)
|
||||
stats.append(s)
|
||||
return stats
|
||||
else:
|
||||
return self._solve(MemoryInstanceWrapper(arg), tee=tee)
|
||||
|
||||
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.
|
||||
"""
|
||||
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 fit(self, filenames: List[str], build_model: Callable) -> None:
|
||||
instances: List[Instance] = [
|
||||
FileInstanceWrapper(f, build_model) for f in filenames
|
||||
]
|
||||
self._fit(instances)
|
||||
|
||||
def parallel_solve(
|
||||
self,
|
||||
@@ -397,7 +433,7 @@ class LearningSolver:
|
||||
"""
|
||||
if n_jobs == 1:
|
||||
return [
|
||||
self.solve(p)
|
||||
self._solve(p)
|
||||
for p in tqdm(
|
||||
instances,
|
||||
disable=not progress,
|
||||
@@ -425,7 +461,7 @@ class LearningSolver:
|
||||
self._restore_miplearn_logger()
|
||||
return stats
|
||||
|
||||
def fit(
|
||||
def _fit(
|
||||
self,
|
||||
training_instances: List[Instance],
|
||||
n_jobs: int = 1,
|
||||
|
||||
@@ -322,8 +322,14 @@ class BasePyomoSolver(InternalSolver):
|
||||
|
||||
# Bounds
|
||||
lb, ub = v.bounds
|
||||
upper_bounds.append(float(ub))
|
||||
lower_bounds.append(float(lb))
|
||||
if ub is not None:
|
||||
upper_bounds.append(float(ub))
|
||||
else:
|
||||
upper_bounds.append(float("inf"))
|
||||
if lb is not None:
|
||||
lower_bounds.append(float(lb))
|
||||
else:
|
||||
lower_bounds.append(-float("inf"))
|
||||
|
||||
# Objective coefficient
|
||||
if v.name in self._obj:
|
||||
@@ -391,7 +397,9 @@ class BasePyomoSolver(InternalSolver):
|
||||
) -> None:
|
||||
if model is None:
|
||||
model = instance.to_model()
|
||||
assert isinstance(model, pe.ConcreteModel)
|
||||
assert isinstance(
|
||||
model, pe.ConcreteModel
|
||||
), f"expected pe.ConcreteModel; found {model.__class__} instead"
|
||||
self.instance = instance
|
||||
self.model = model
|
||||
self.model.extra_constraints = ConstraintList()
|
||||
|
||||
@@ -247,7 +247,7 @@ def run_lazy_cb_tests(solver: InternalSolver) -> None:
|
||||
assert relsol is not None
|
||||
assert relsol[b"x[0]"] is not None
|
||||
if relsol[b"x[0]"] > 0:
|
||||
instance.enforce_lazy_constraint(cb_solver, cb_model, b"cut")
|
||||
instance.enforce_lazy_constraint(cb_solver, cb_model, None)
|
||||
|
||||
solver.set_instance(instance, model)
|
||||
solver.solve(lazy_cb=lazy_cb)
|
||||
|
||||
Reference in New Issue
Block a user