mirror of
https://github.com/ANL-CEEESA/MIPLearn.git
synced 2025-12-06 09:28:51 -06:00
Reformat source code with Black; add pre-commit hooks and CI checks
This commit is contained in:
@@ -13,10 +13,11 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GurobiSolver(InternalSolver):
|
||||
def __init__(self,
|
||||
params=None,
|
||||
lazy_cb_frequency=1,
|
||||
):
|
||||
def __init__(
|
||||
self,
|
||||
params=None,
|
||||
lazy_cb_frequency=1,
|
||||
):
|
||||
"""
|
||||
An InternalSolver backed by Gurobi's Python API (without Pyomo).
|
||||
|
||||
@@ -33,6 +34,7 @@ class GurobiSolver(InternalSolver):
|
||||
if params is None:
|
||||
params = {}
|
||||
from gurobipy import GRB
|
||||
|
||||
self.GRB = GRB
|
||||
self.instance = None
|
||||
self.model = None
|
||||
@@ -44,8 +46,7 @@ class GurobiSolver(InternalSolver):
|
||||
if lazy_cb_frequency == 1:
|
||||
self.lazy_cb_where = [self.GRB.Callback.MIPSOL]
|
||||
else:
|
||||
self.lazy_cb_where = [self.GRB.Callback.MIPSOL,
|
||||
self.GRB.Callback.MIPNODE]
|
||||
self.lazy_cb_where = [self.GRB.Callback.MIPSOL, self.GRB.Callback.MIPNODE]
|
||||
|
||||
def set_instance(self, instance, model=None):
|
||||
self._raise_if_callback()
|
||||
@@ -70,14 +71,15 @@ class GurobiSolver(InternalSolver):
|
||||
idx = [0]
|
||||
else:
|
||||
name = m.group(1)
|
||||
idx = tuple(int(k) if k.isdecimal() else k
|
||||
for k in m.group(2).split(","))
|
||||
idx = tuple(
|
||||
int(k) if k.isdecimal() else k for k in m.group(2).split(",")
|
||||
)
|
||||
if len(idx) == 1:
|
||||
idx = idx[0]
|
||||
if name not in self._all_vars:
|
||||
self._all_vars[name] = {}
|
||||
self._all_vars[name][idx] = var
|
||||
if var.vtype != 'C':
|
||||
if var.vtype != "C":
|
||||
if name not in self._bin_vars:
|
||||
self._bin_vars[name] = {}
|
||||
self._bin_vars[name][idx] = var
|
||||
@@ -103,15 +105,9 @@ class GurobiSolver(InternalSolver):
|
||||
for (idx, var) in vardict.items():
|
||||
var.vtype = self.GRB.BINARY
|
||||
log = streams[0].getvalue()
|
||||
return {
|
||||
"Optimal value": self.model.objVal,
|
||||
"Log": log
|
||||
}
|
||||
return {"Optimal value": self.model.objVal, "Log": log}
|
||||
|
||||
def solve(self,
|
||||
tee=False,
|
||||
iteration_cb=None,
|
||||
lazy_cb=None):
|
||||
def solve(self, tee=False, iteration_cb=None, lazy_cb=None):
|
||||
self._raise_if_callback()
|
||||
|
||||
def cb_wrapper(cb_model, cb_where):
|
||||
@@ -133,7 +129,7 @@ class GurobiSolver(InternalSolver):
|
||||
if tee:
|
||||
streams += [sys.stdout]
|
||||
if iteration_cb is None:
|
||||
iteration_cb = lambda : False
|
||||
iteration_cb = lambda: False
|
||||
while True:
|
||||
logger.debug("Solving MIP...")
|
||||
with RedirectOutput(streams):
|
||||
@@ -187,7 +183,9 @@ class GurobiSolver(InternalSolver):
|
||||
elif self.cb_where is None:
|
||||
return var.x
|
||||
else:
|
||||
raise Exception("get_value cannot be called from cb_where=%s" % self.cb_where)
|
||||
raise Exception(
|
||||
"get_value cannot be called from cb_where=%s" % self.cb_where
|
||||
)
|
||||
|
||||
def get_variables(self):
|
||||
self._raise_if_callback()
|
||||
@@ -220,8 +218,10 @@ class GurobiSolver(InternalSolver):
|
||||
if value is not None:
|
||||
count_fixed += 1
|
||||
self._all_vars[varname][idx].start = value
|
||||
logger.info("Setting start values for %d variables (out of %d)" %
|
||||
(count_fixed, count_total))
|
||||
logger.info(
|
||||
"Setting start values for %d variables (out of %d)"
|
||||
% (count_fixed, count_total)
|
||||
)
|
||||
|
||||
def clear_warm_start(self):
|
||||
self._raise_if_callback()
|
||||
@@ -248,10 +248,7 @@ class GurobiSolver(InternalSolver):
|
||||
def extract_constraint(self, cid):
|
||||
self._raise_if_callback()
|
||||
constr = self.model.getConstrByName(cid)
|
||||
cobj = (self.model.getRow(constr),
|
||||
constr.sense,
|
||||
constr.RHS,
|
||||
constr.ConstrName)
|
||||
cobj = (self.model.getRow(constr), constr.sense, constr.RHS, constr.ConstrName)
|
||||
self.model.remove(constr)
|
||||
return cobj
|
||||
|
||||
@@ -316,7 +313,7 @@ class GurobiSolver(InternalSolver):
|
||||
value = matches[0]
|
||||
return value
|
||||
|
||||
def __getstate__(self):
|
||||
def __getstate__(self):
|
||||
return {
|
||||
"params": self.params,
|
||||
"lazy_cb_where": self.lazy_cb_where,
|
||||
@@ -324,6 +321,7 @@ class GurobiSolver(InternalSolver):
|
||||
|
||||
def __setstate__(self, state):
|
||||
from gurobipy import GRB
|
||||
|
||||
self.params = state["params"]
|
||||
self.lazy_cb_where = state["lazy_cb_where"]
|
||||
self.GRB = GRB
|
||||
@@ -331,4 +329,4 @@ class GurobiSolver(InternalSolver):
|
||||
self.model = None
|
||||
self._all_vars = None
|
||||
self._bin_vars = None
|
||||
self.cb_where = None
|
||||
self.cb_where = None
|
||||
|
||||
@@ -222,4 +222,3 @@ class InternalSolver(ABC):
|
||||
for idx in indices:
|
||||
solution[var][idx] = 0.0
|
||||
return solution
|
||||
|
||||
|
||||
@@ -12,10 +12,12 @@ from copy import deepcopy
|
||||
from typing import Optional, List
|
||||
from p_tqdm import p_map
|
||||
|
||||
from .. import (ObjectiveValueComponent,
|
||||
PrimalSolutionComponent,
|
||||
DynamicLazyConstraintsComponent,
|
||||
UserCutsComponent)
|
||||
from .. import (
|
||||
ObjectiveValueComponent,
|
||||
PrimalSolutionComponent,
|
||||
DynamicLazyConstraintsComponent,
|
||||
UserCutsComponent,
|
||||
)
|
||||
from .pyomo.cplex import CplexPyomoSolver
|
||||
from .pyomo.gurobi import GurobiPyomoSolver
|
||||
|
||||
@@ -43,16 +45,18 @@ def _parallel_solve(idx):
|
||||
|
||||
|
||||
class LearningSolver:
|
||||
def __init__(self,
|
||||
components=None,
|
||||
gap_tolerance=1e-4,
|
||||
mode="exact",
|
||||
solver="gurobi",
|
||||
threads=None,
|
||||
time_limit=None,
|
||||
node_limit=None,
|
||||
solve_lp_first=True,
|
||||
use_lazy_cb=False):
|
||||
def __init__(
|
||||
self,
|
||||
components=None,
|
||||
gap_tolerance=1e-4,
|
||||
mode="exact",
|
||||
solver="gurobi",
|
||||
threads=None,
|
||||
time_limit=None,
|
||||
node_limit=None,
|
||||
solve_lp_first=True,
|
||||
use_lazy_cb=False,
|
||||
):
|
||||
"""
|
||||
Mixed-Integer Linear Programming (MIP) solver that extracts information
|
||||
from previous runs and uses Machine Learning methods to accelerate the
|
||||
@@ -142,28 +146,30 @@ class LearningSolver:
|
||||
solver.set_node_limit(self.node_limit)
|
||||
return solver
|
||||
|
||||
def solve(self,
|
||||
instance,
|
||||
model=None,
|
||||
output="",
|
||||
tee=False):
|
||||
def solve(
|
||||
self,
|
||||
instance,
|
||||
model=None,
|
||||
output="",
|
||||
tee=False,
|
||||
):
|
||||
"""
|
||||
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.
|
||||
filename pointing to a pickled Instance object.
|
||||
|
||||
This method modifies the instance object. Specifically, the following
|
||||
properties are set:
|
||||
|
||||
|
||||
- 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
|
||||
@@ -197,7 +203,7 @@ class LearningSolver:
|
||||
"Predicted UB". See the documentation of each component for more
|
||||
details.
|
||||
"""
|
||||
|
||||
|
||||
filename = None
|
||||
fileformat = None
|
||||
if isinstance(instance, str):
|
||||
@@ -211,7 +217,7 @@ class LearningSolver:
|
||||
fileformat = "pickle"
|
||||
with open(filename, "rb") as file:
|
||||
instance = pickle.load(file)
|
||||
|
||||
|
||||
if model is None:
|
||||
model = instance.to_model()
|
||||
|
||||
@@ -248,9 +254,11 @@ class LearningSolver:
|
||||
lazy_cb = lazy_cb_wrapper
|
||||
|
||||
logger.info("Solving MILP...")
|
||||
results = self.internal_solver.solve(tee=tee,
|
||||
iteration_cb=iteration_cb,
|
||||
lazy_cb=lazy_cb)
|
||||
results = self.internal_solver.solve(
|
||||
tee=tee,
|
||||
iteration_cb=iteration_cb,
|
||||
lazy_cb=lazy_cb,
|
||||
)
|
||||
results["LP value"] = instance.lp_value
|
||||
|
||||
# Read MIP solution and bounds
|
||||
@@ -262,7 +270,7 @@ class LearningSolver:
|
||||
logger.debug("Calling after_solve callbacks...")
|
||||
for component in self.components.values():
|
||||
component.after_solve(self, instance, model, results)
|
||||
|
||||
|
||||
if filename is not None and output is not None:
|
||||
output_filename = output
|
||||
if len(output) == 0:
|
||||
@@ -280,36 +288,38 @@ class LearningSolver:
|
||||
def parallel_solve(self, instances, n_jobs=4, label="Solve", output=[]):
|
||||
"""
|
||||
Solves multiple instances in parallel.
|
||||
|
||||
|
||||
This method is equivalent to calling `solve` for each item on the list,
|
||||
but it processes multiple instances at the same time. Like `solve`, this
|
||||
method modifies each instance in place. Also like `solve`, a list of
|
||||
filenames may be provided.
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
instances: [miplearn.Instance] or [str]
|
||||
The instances to be solved
|
||||
n_jobs: int
|
||||
Number of instances to solve in parallel at a time.
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
Returns a list of dictionaries, with one entry for each provided instance.
|
||||
This dictionary is the same you would obtain by calling:
|
||||
|
||||
|
||||
[solver.solve(p) for p in instances]
|
||||
|
||||
|
||||
"""
|
||||
self.internal_solver = None
|
||||
self._silence_miplearn_logger()
|
||||
SOLVER[0] = self
|
||||
OUTPUTS[0] = output
|
||||
INSTANCES[0] = instances
|
||||
results = p_map(_parallel_solve,
|
||||
list(range(len(instances))),
|
||||
num_cpus=n_jobs,
|
||||
desc=label)
|
||||
results = p_map(
|
||||
_parallel_solve,
|
||||
list(range(len(instances))),
|
||||
num_cpus=n_jobs,
|
||||
desc=label,
|
||||
)
|
||||
stats = []
|
||||
for (idx, (s, instance)) in enumerate(results):
|
||||
stats.append(s)
|
||||
@@ -330,12 +340,12 @@ class LearningSolver:
|
||||
def _silence_miplearn_logger(self):
|
||||
miplearn_logger = logging.getLogger("miplearn")
|
||||
self.prev_log_level = miplearn_logger.getEffectiveLevel()
|
||||
miplearn_logger.setLevel(logging.WARNING)
|
||||
|
||||
miplearn_logger.setLevel(logging.WARNING)
|
||||
|
||||
def _restore_miplearn_logger(self):
|
||||
miplearn_logger = logging.getLogger("miplearn")
|
||||
miplearn_logger.setLevel(self.prev_log_level)
|
||||
|
||||
miplearn_logger.setLevel(self.prev_log_level)
|
||||
|
||||
def __getstate__(self):
|
||||
self.internal_solver = None
|
||||
return self.__dict__
|
||||
|
||||
@@ -81,8 +81,10 @@ class BasePyomoSolver(InternalSolver):
|
||||
count_fixed += 1
|
||||
if count_fixed > 0:
|
||||
self._is_warm_start_available = True
|
||||
logger.info("Setting start values for %d variables (out of %d)" %
|
||||
(count_fixed, count_total))
|
||||
logger.info(
|
||||
"Setting start values for %d variables (out of %d)"
|
||||
% (count_fixed, count_total)
|
||||
)
|
||||
|
||||
def clear_warm_start(self):
|
||||
for var in self._all_vars:
|
||||
@@ -134,17 +136,19 @@ class BasePyomoSolver(InternalSolver):
|
||||
count_fixed += 1
|
||||
var[index].fix(solution[varname][index])
|
||||
self._pyomo_solver.update_var(var[index])
|
||||
logger.info("Fixing values for %d variables (out of %d)" %
|
||||
(count_fixed, count_total))
|
||||
logger.info(
|
||||
"Fixing values for %d variables (out of %d)"
|
||||
% (
|
||||
count_fixed,
|
||||
count_total,
|
||||
)
|
||||
)
|
||||
|
||||
def add_constraint(self, constraint):
|
||||
self._pyomo_solver.add_constraint(constraint)
|
||||
self._update_constrs()
|
||||
|
||||
def solve(self,
|
||||
tee=False,
|
||||
iteration_cb=None,
|
||||
lazy_cb=None):
|
||||
def solve(self, tee=False, iteration_cb=None, lazy_cb=None):
|
||||
if lazy_cb is not None:
|
||||
raise Exception("lazy callback not supported")
|
||||
total_wallclock_time = 0
|
||||
@@ -158,8 +162,10 @@ class BasePyomoSolver(InternalSolver):
|
||||
while True:
|
||||
logger.debug("Solving MIP...")
|
||||
with RedirectOutput(streams):
|
||||
results = self._pyomo_solver.solve(tee=True,
|
||||
warmstart=self._is_warm_start_available)
|
||||
results = self._pyomo_solver.solve(
|
||||
tee=True,
|
||||
warmstart=self._is_warm_start_available,
|
||||
)
|
||||
total_wallclock_time += results["Solver"][0]["Wallclock time"]
|
||||
should_repeat = iteration_cb()
|
||||
if not should_repeat:
|
||||
@@ -192,9 +198,7 @@ class BasePyomoSolver(InternalSolver):
|
||||
return value
|
||||
|
||||
def _extract_node_count(self, log):
|
||||
return int(self.__extract(log,
|
||||
self._get_node_count_regexp(),
|
||||
default=1))
|
||||
return int(self.__extract(log, self._get_node_count_regexp(), default=1))
|
||||
|
||||
def set_threads(self, threads):
|
||||
key = self._get_threads_option_name()
|
||||
@@ -249,4 +253,4 @@ class BasePyomoSolver(InternalSolver):
|
||||
raise Exception("not implemented")
|
||||
|
||||
def get_constraint_slacks(self):
|
||||
raise Exception("not implemented")
|
||||
raise Exception("not implemented")
|
||||
|
||||
@@ -20,7 +20,7 @@ class CplexPyomoSolver(BasePyomoSolver):
|
||||
{"mip_display": 5} to increase the log verbosity.
|
||||
"""
|
||||
super().__init__()
|
||||
self._pyomo_solver = pe.SolverFactory('cplex_persistent')
|
||||
self._pyomo_solver = pe.SolverFactory("cplex_persistent")
|
||||
self._pyomo_solver.options["randomseed"] = randint(low=0, high=1000).rvs()
|
||||
self._pyomo_solver.options["mip_display"] = 4
|
||||
if options is not None:
|
||||
|
||||
@@ -15,8 +15,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GurobiPyomoSolver(BasePyomoSolver):
|
||||
def __init__(self,
|
||||
options=None):
|
||||
def __init__(self, options=None):
|
||||
"""
|
||||
Creates a new Gurobi solver, accessed through Pyomo.
|
||||
|
||||
@@ -27,7 +26,7 @@ class GurobiPyomoSolver(BasePyomoSolver):
|
||||
{"Threads": 4} to set the number of threads.
|
||||
"""
|
||||
super().__init__()
|
||||
self._pyomo_solver = pe.SolverFactory('gurobi_persistent')
|
||||
self._pyomo_solver = pe.SolverFactory("gurobi_persistent")
|
||||
self._pyomo_solver.options["Seed"] = randint(low=0, high=1000).rvs()
|
||||
if options is not None:
|
||||
for (key, value) in options.items():
|
||||
@@ -56,6 +55,7 @@ class GurobiPyomoSolver(BasePyomoSolver):
|
||||
|
||||
def set_branching_priorities(self, priorities):
|
||||
from gurobipy import GRB
|
||||
|
||||
for varname in priorities.keys():
|
||||
var = self._varname_to_var[varname]
|
||||
for (index, priority) in priorities[varname].items():
|
||||
|
||||
@@ -9,20 +9,22 @@ from miplearn.problems.knapsack import KnapsackInstance, GurobiKnapsackInstance
|
||||
|
||||
def _get_instance(solver):
|
||||
def _is_subclass_or_instance(solver, parentClass):
|
||||
return isinstance(solver, parentClass) or (isclass(solver) and issubclass(solver, parentClass))
|
||||
return isinstance(solver, parentClass) or (
|
||||
isclass(solver) and issubclass(solver, parentClass)
|
||||
)
|
||||
|
||||
if _is_subclass_or_instance(solver, BasePyomoSolver):
|
||||
return KnapsackInstance(
|
||||
weights=[23., 26., 20., 18.],
|
||||
prices=[505., 352., 458., 220.],
|
||||
capacity=67.,
|
||||
weights=[23.0, 26.0, 20.0, 18.0],
|
||||
prices=[505.0, 352.0, 458.0, 220.0],
|
||||
capacity=67.0,
|
||||
)
|
||||
|
||||
if _is_subclass_or_instance(solver, GurobiSolver):
|
||||
return GurobiKnapsackInstance(
|
||||
weights=[23., 26., 20., 18.],
|
||||
prices=[505., 352., 458., 220.],
|
||||
capacity=67.,
|
||||
weights=[23.0, 26.0, 20.0, 18.0],
|
||||
prices=[505.0, 352.0, 458.0, 220.0],
|
||||
capacity=67.0,
|
||||
)
|
||||
|
||||
assert False
|
||||
|
||||
@@ -16,6 +16,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def test_redirect_output():
|
||||
import sys
|
||||
|
||||
original_stdout = sys.stdout
|
||||
io = StringIO()
|
||||
with RedirectOutput([io]):
|
||||
@@ -31,36 +32,42 @@ def test_internal_solver_warm_starts():
|
||||
model = instance.to_model()
|
||||
solver = solver_class()
|
||||
solver.set_instance(instance, model)
|
||||
solver.set_warm_start({
|
||||
"x": {
|
||||
0: 1.0,
|
||||
1: 0.0,
|
||||
2: 0.0,
|
||||
3: 1.0,
|
||||
solver.set_warm_start(
|
||||
{
|
||||
"x": {
|
||||
0: 1.0,
|
||||
1: 0.0,
|
||||
2: 0.0,
|
||||
3: 1.0,
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
stats = solver.solve(tee=True)
|
||||
assert stats["Warm start value"] == 725.0
|
||||
|
||||
solver.set_warm_start({
|
||||
"x": {
|
||||
0: 1.0,
|
||||
1: 1.0,
|
||||
2: 1.0,
|
||||
3: 1.0,
|
||||
solver.set_warm_start(
|
||||
{
|
||||
"x": {
|
||||
0: 1.0,
|
||||
1: 1.0,
|
||||
2: 1.0,
|
||||
3: 1.0,
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
stats = solver.solve(tee=True)
|
||||
assert stats["Warm start value"] is None
|
||||
|
||||
solver.fix({
|
||||
"x": {
|
||||
0: 1.0,
|
||||
1: 0.0,
|
||||
2: 0.0,
|
||||
3: 1.0,
|
||||
solver.fix(
|
||||
{
|
||||
"x": {
|
||||
0: 1.0,
|
||||
1: 0.0,
|
||||
2: 0.0,
|
||||
3: 1.0,
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
stats = solver.solve(tee=True)
|
||||
assert stats["Lower bound"] == 725.0
|
||||
assert stats["Upper bound"] == 725.0
|
||||
|
||||
@@ -20,11 +20,13 @@ def test_learning_solver():
|
||||
for internal_solver in _get_internal_solvers():
|
||||
logger.info("Solver: %s" % internal_solver)
|
||||
instance = _get_instance(internal_solver)
|
||||
solver = LearningSolver(time_limit=300,
|
||||
gap_tolerance=1e-3,
|
||||
threads=1,
|
||||
solver=internal_solver,
|
||||
mode=mode)
|
||||
solver = LearningSolver(
|
||||
time_limit=300,
|
||||
gap_tolerance=1e-3,
|
||||
threads=1,
|
||||
solver=internal_solver,
|
||||
mode=mode,
|
||||
)
|
||||
|
||||
solver.solve(instance)
|
||||
assert instance.solution["x"][0] == 1.0
|
||||
@@ -74,37 +76,36 @@ def test_solve_fit_from_disk():
|
||||
filenames = []
|
||||
for k in range(3):
|
||||
instance = _get_instance(internal_solver)
|
||||
with tempfile.NamedTemporaryFile(suffix=".pkl",
|
||||
delete=False) as file:
|
||||
with tempfile.NamedTemporaryFile(suffix=".pkl", delete=False) as file:
|
||||
filenames += [file.name]
|
||||
pickle.dump(instance, file)
|
||||
|
||||
|
||||
# Test: solve
|
||||
solver = LearningSolver(solver=internal_solver)
|
||||
solver.solve(filenames[0])
|
||||
with open(filenames[0], "rb") as file:
|
||||
instance = pickle.load(file)
|
||||
assert hasattr(instance, "solution")
|
||||
|
||||
|
||||
# Test: parallel_solve
|
||||
solver.parallel_solve(filenames)
|
||||
for filename in filenames:
|
||||
with open(filename, "rb") as file:
|
||||
instance = pickle.load(file)
|
||||
assert hasattr(instance, "solution")
|
||||
|
||||
|
||||
# Test: solve (with specified output)
|
||||
output = [f + ".out" for f in filenames]
|
||||
solver.solve(filenames[0], output=output[0])
|
||||
assert os.path.isfile(output[0])
|
||||
|
||||
|
||||
# Test: parallel_solve (with specified output)
|
||||
solver.parallel_solve(filenames, output=output)
|
||||
for filename in output:
|
||||
assert os.path.isfile(filename)
|
||||
|
||||
|
||||
# Delete temporary files
|
||||
for filename in filenames:
|
||||
os.remove(filename)
|
||||
for filename in output:
|
||||
os.remove(filename)
|
||||
os.remove(filename)
|
||||
|
||||
Reference in New Issue
Block a user