You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
MIPLearn/src/python/miplearn/solvers/learning.py

170 lines
5.6 KiB

# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
import logging
from copy import deepcopy
from typing import Optional, List
from p_tqdm import p_map
from .cplex import CPLEXSolver
from .gurobi import GurobiSolver
from .internal import InternalSolver
from .. import (ObjectiveValueComponent,
PrimalSolutionComponent,
LazyConstraintsComponent)
logger = logging.getLogger(__name__)
# Global memory for multiprocessing
SOLVER = [None] # type: List[Optional[LearningSolver]]
INSTANCES = [None] # type: List[Optional[dict]]
def _parallel_solve(instance_idx):
solver = deepcopy(SOLVER[0])
instance = INSTANCES[0][instance_idx]
results = solver.solve(instance)
return {
"Results": results,
"Solution": instance.solution,
"LP solution": instance.lp_solution,
"LP value": instance.lp_value,
"Upper bound": instance.upper_bound,
"Lower bound": instance.lower_bound,
"Violations": instance.found_violations,
}
class LearningSolver:
"""
Mixed-Integer Linear Programming (MIP) solver that extracts information
from previous runs, using Machine Learning methods, to accelerate the
solution of new (yet unseen) instances.
"""
def __init__(self,
components=None,
gap_tolerance=None,
mode="exact",
solver="gurobi",
threads=4,
time_limit=None):
self.components = {}
self.mode = mode
self.internal_solver = None
self.internal_solver_factory = solver
self.threads = threads
self.time_limit = time_limit
self.gap_tolerance = gap_tolerance
self.tee = False
if components is not None:
for comp in components:
self.add(comp)
else:
self.add(ObjectiveValueComponent())
self.add(PrimalSolutionComponent())
self.add(LazyConstraintsComponent())
assert self.mode in ["exact", "heuristic"]
for component in self.components.values():
component.mode = self.mode
def _create_internal_solver(self):
logger.debug("Initializing %s" % self.internal_solver_factory)
if self.internal_solver_factory == "cplex":
solver = CPLEXSolver()
elif self.internal_solver_factory == "gurobi":
solver = GurobiSolver()
elif callable(self.internal_solver_factory):
solver = self.internal_solver_factory()
assert isinstance(solver, InternalSolver)
else:
raise Exception("solver %s not supported" % self.internal_solver_factory)
solver.set_threads(self.threads)
if self.time_limit is not None:
solver.set_time_limit(self.time_limit)
if self.gap_tolerance is not None:
solver.set_gap_tolerance(self.gap_tolerance)
return solver
def solve(self,
instance,
model=None,
tee=False,
relaxation_only=False):
if model is None:
model = instance.to_model()
self.tee = tee
self.internal_solver = self._create_internal_solver()
self.internal_solver.set_instance(instance, model=model)
logger.debug("Solving LP relaxation...")
results = self.internal_solver.solve_lp(tee=tee)
instance.lp_solution = self.internal_solver.get_solution()
instance.lp_value = results["Optimal value"]
logger.debug("Running before_solve callbacks...")
for component in self.components.values():
component.before_solve(self, instance, model)
if relaxation_only:
return results
results = self.internal_solver.solve(tee=tee)
# Read MIP solution and bounds
instance.lower_bound = results["Lower bound"]
instance.upper_bound = results["Upper bound"]
instance.solution = self.internal_solver.get_solution()
logger.debug("Calling after_solve callbacks...")
for component in self.components.values():
component.after_solve(self, instance, model, results)
return results
def parallel_solve(self,
instances,
n_jobs=4,
label="Solve"):
self.internal_solver = None
SOLVER[0] = self
INSTANCES[0] = instances
p_map_results = p_map(_parallel_solve,
list(range(len(instances))),
num_cpus=n_jobs,
desc=label)
results = [p["Results"] for p in p_map_results]
for (idx, r) in enumerate(p_map_results):
instances[idx].solution = r["Solution"]
instances[idx].lp_solution = r["LP solution"]
instances[idx].lp_value = r["LP value"]
instances[idx].lower_bound = r["Lower bound"]
instances[idx].upper_bound = r["Upper bound"]
instances[idx].found_violations = r["Violations"]
return results
def fit(self, training_instances):
if len(training_instances) == 0:
return
for component in self.components.values():
component.fit(training_instances)
def add(self, component):
name = component.__class__.__name__
self.components[name] = component
def __getstate__(self):
self.internal_solver = None
return self.__dict__