Move python files to root folder; remove built docs

This commit is contained in:
2020-08-29 11:42:02 -05:00
parent 741af8506b
commit 5663ced0be
116 changed files with 8 additions and 12408 deletions

View File

@@ -0,0 +1,32 @@
# 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
import sys
logger = logging.getLogger(__name__)
class RedirectOutput:
def __init__(self, streams):
self.streams = streams
def write(self, data):
for stream in self.streams:
stream.write(data)
def flush(self):
for stream in self.streams:
stream.flush()
def __enter__(self):
self._original_stdout = sys.stdout
self._original_stderr = sys.stderr
sys.stdout = self
sys.stderr = self
return self
def __exit__(self, _type, _value, _traceback):
sys.stdout = self._original_stdout
sys.stderr = self._original_stderr

209
miplearn/solvers/guroby.py Normal file
View File

@@ -0,0 +1,209 @@
# 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 re
import sys
import logging
from io import StringIO
from . import RedirectOutput
from .internal import InternalSolver
logger = logging.getLogger(__name__)
class GurobiSolver(InternalSolver):
def __init__(self, params=None):
if params is None:
params = {
"LazyConstraints": 1,
"PreCrush": 1,
}
from gurobipy import GRB
self.GRB = GRB
self.instance = None
self.model = None
self.params = params
self._all_vars = None
self._bin_vars = None
self._varname_to_var = None
def set_instance(self, instance, model=None):
if model is None:
model = instance.to_model()
self.instance = instance
self.model = model
self.model.update()
self._update_vars()
def _update_vars(self):
self._all_vars = {}
self._bin_vars = {}
for var in self.model.getVars():
m = re.search(r"([^[]*)\[(.*)\]", var.varName)
if m is None:
name = var.varName
idx = [0]
else:
name = m.group(1)
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 name not in self._bin_vars:
self._bin_vars[name] = {}
self._bin_vars[name][idx] = var
def _apply_params(self):
for (name, value) in self.params.items():
self.model.setParam(name, value)
def solve_lp(self, tee=False):
self._apply_params()
streams = [StringIO()]
if tee:
streams += [sys.stdout]
for (varname, vardict) in self._bin_vars.items():
for (idx, var) in vardict.items():
var.vtype = self.GRB.CONTINUOUS
var.lb = 0.0
var.ub = 1.0
with RedirectOutput(streams):
self.model.optimize()
for (varname, vardict) in self._bin_vars.items():
for (idx, var) in vardict.items():
var.vtype = self.GRB.BINARY
log = streams[0].getvalue()
return {
"Optimal value": self.model.objVal,
"Log": log
}
def solve(self, tee=False):
self.instance.found_violated_lazy_constraints = []
self.instance.found_violated_user_cuts = []
streams = [StringIO()]
if tee:
streams += [sys.stdout]
def cb(cb_model, cb_where):
try:
# User cuts
if cb_where == self.GRB.Callback.MIPNODE:
logger.debug("Finding violated cutting planes...")
violations = self.instance.find_violated_user_cuts(cb_model)
self.instance.found_violated_user_cuts += violations
logger.debug(" %d found" % len(violations))
for v in violations:
cut = self.instance.build_user_cut(cb_model, v)
cb_model.cbCut(cut)
# Lazy constraints
if cb_where == self.GRB.Callback.MIPSOL:
logger.debug("Finding violated lazy constraints...")
violations = self.instance.find_violated_lazy_constraints(cb_model)
self.instance.found_violated_lazy_constraints += violations
logger.debug(" %d found" % len(violations))
for v in violations:
cut = self.instance.build_lazy_constraint(cb_model, v)
cb_model.cbLazy(cut)
except Exception as e:
logger.error(e)
with RedirectOutput(streams):
self.model.optimize(cb)
log = streams[0].getvalue()
return {
"Lower bound": self.model.objVal,
"Upper bound": self.model.objBound,
"Wallclock time": self.model.runtime,
"Nodes": int(self.model.nodeCount),
"Sense": ("min" if self.model.modelSense == 1 else "max"),
"Log": log,
"Warm start value": self._extract_warm_start_value(log),
}
def get_solution(self):
solution = {}
for (varname, vardict) in self._all_vars.items():
solution[varname] = {}
for (idx, var) in vardict.items():
solution[varname][idx] = var.x
return solution
def get_variables(self):
variables = {}
for (varname, vardict) in self._all_vars.items():
variables[varname] = {}
for (idx, var) in vardict.items():
variables[varname] += [idx]
return variables
def add_constraint(self, constraint):
self.model.addConstr(constraint)
def set_warm_start(self, solution):
count_fixed, count_total = 0, 0
for (varname, vardict) in solution.items():
for (idx, value) in vardict.items():
count_total += 1
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))
def clear_warm_start(self):
for (varname, vardict) in self._all_vars:
for (idx, var) in vardict.items():
var[idx].start = self.GRB.UNDEFINED
def fix(self, solution):
for (varname, vardict) in solution.items():
for (idx, value) in vardict.items():
if value is None:
continue
var = self._all_vars[varname][idx]
var.vtype = self.GRB.CONTINUOUS
var.lb = value
var.ub = value
def set_branching_priorities(self, priorities):
logger.warning("set_branching_priorities not implemented")
def set_threads(self, threads):
self.params["Threads"] = threads
def set_time_limit(self, time_limit):
self.params["TimeLimit"] = time_limit
def set_node_limit(self, node_limit):
self.params["NodeLimit"] = node_limit
def set_gap_tolerance(self, gap_tolerance):
self.params["MIPGap"] = gap_tolerance
def _extract_warm_start_value(self, log):
ws = self.__extract(log, "MIP start with objective ([0-9.e+-]*)")
if ws is not None:
ws = float(ws)
return ws
def __extract(self, log, regexp, default=None):
value = default
for line in log.splitlines():
matches = re.findall(regexp, line)
if len(matches) == 0:
continue
value = matches[0]
return value
def __getstate__(self):
return self.params
def __setstate__(self, state):
self.params = state

View File

@@ -0,0 +1,164 @@
# 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 abc import ABC, abstractmethod
logger = logging.getLogger(__name__)
class InternalSolver(ABC):
"""
Abstract class representing the MIP solver used internally by LearningSolver.
"""
@abstractmethod
def solve_lp(self, tee=False):
"""
Solves the LP relaxation of the currently loaded instance. After this
method finishes, the solution can be retrieved by calling `get_solution`.
Parameters
----------
tee: bool
If true, prints the solver log to the screen.
Returns
-------
dict
A dictionary of solver statistics containing the following keys:
"Optimal value".
"""
pass
@abstractmethod
def get_solution(self):
"""
Returns current solution found by the solver.
If called after `solve`, returns the best primal solution found during
the search. If called after `solve_lp`, returns the optimal solution
to the LP relaxation.
The solution is a dictionary `sol`, where the optimal value of `var[idx]`
is given by `sol[var][idx]`.
"""
pass
@abstractmethod
def set_warm_start(self, solution):
"""
Sets the warm start to be used by the solver.
The solution should be a dictionary following the same format as the
one produced by `get_solution`. Only one warm start is supported.
Calling this function when a warm start already exists will
remove the previous warm start.
"""
pass
@abstractmethod
def clear_warm_start(self):
"""
Removes any existing warm start from the solver.
"""
pass
@abstractmethod
def set_instance(self, instance, model=None):
"""
Loads the given instance into the solver.
Parameters
----------
instance: miplearn.Instance
The instance to be loaded.
model:
The concrete optimization model corresponding to this instance
(e.g. JuMP.Model or pyomo.core.ConcreteModel). If not provided,
it will be generated by calling `instance.to_model()`.
"""
pass
@abstractmethod
def fix(self, solution):
"""
Fixes the values of a subset of decision variables.
The values should be provided in the dictionary format generated by
`get_solution`. Missing values in the solution indicate variables
that should be left free.
"""
pass
@abstractmethod
def set_branching_priorities(self, priorities):
"""
Sets the branching priorities for the given decision variables.
When the MIP solver needs to decide on which variable to branch, variables
with higher priority are picked first, given that they are fractional.
Ties are solved arbitrarily. By default, all variables have priority zero.
The priorities should be provided in the dictionary format generated by
`get_solution`. Missing values indicate variables whose priorities
should not be modified.
"""
pass
@abstractmethod
def add_constraint(self, constraint):
"""
Adds a single constraint to the model.
"""
pass
@abstractmethod
def solve(self, tee=False):
"""
Solves the currently loaded instance. After this method finishes,
the best solution found can be retrieved by calling `get_solution`.
Parameters
----------
tee: bool
If true, prints the solver log to the screen.
Returns
-------
dict
A dictionary of solver statistics containing the following keys:
"Lower bound", "Upper bound", "Wallclock time", "Nodes", "Sense",
"Log" and "Warm start value".
"""
pass
@abstractmethod
def set_threads(self, threads):
pass
@abstractmethod
def set_time_limit(self, time_limit):
pass
@abstractmethod
def set_node_limit(self, node_limit):
pass
@abstractmethod
def set_gap_tolerance(self, gap_tolerance):
pass
@abstractmethod
def get_variables(self):
pass
def get_empty_solution(self):
solution = {}
for (var, indices) in self.get_variables().items():
solution[var] = {}
for idx in indices:
solution[var][idx] = 0.0
return solution

View File

@@ -0,0 +1,227 @@
# 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 .. import (ObjectiveValueComponent,
PrimalSolutionComponent,
LazyConstraintsComponent,
UserCutsComponent)
from .pyomo.cplex import CplexPyomoSolver
from .pyomo.gurobi import GurobiPyomoSolver
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,
"Violated lazy constraints": instance.found_violated_lazy_constraints,
"Violated user cuts": instance.found_violated_user_cuts,
}
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=None,
time_limit=None,
node_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
self.node_limit = node_limit
if components is not None:
for comp in components:
self.add(comp)
else:
self.add(ObjectiveValueComponent())
self.add(PrimalSolutionComponent())
self.add(LazyConstraintsComponent())
self.add(UserCutsComponent())
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 = CplexPyomoSolver()
elif self.internal_solver_factory == "gurobi":
solver = GurobiPyomoSolver()
elif callable(self.internal_solver_factory):
solver = self.internal_solver_factory()
else:
solver = self.internal_solver_factory
if self.threads is not None:
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)
if self.node_limit is not None:
solver.set_node_limit(self.node_limit)
return solver
def solve(self,
instance,
model=None,
tee=False,
relaxation_only=False,
solve_lp_first=True):
"""
Solves the given instance. If trained machine-learning models are
available, they will be used to accelerate the solution process.
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.found_violated_lazy_constraints
- instance.solver_log
Additional solver components may set additional properties. Please
see their documentation for more details.
If `solve_lp_first` is False, the properties lp_solution and lp_value
will be set to dummy values.
Parameters
----------
instance: miplearn.Instance
The instance to be solved
model: pyomo.core.ConcreteModel
The corresponding Pyomo model. If not provided, it will be created.
tee: bool
If true, prints solver log to screen.
relaxation_only: bool
If true, solve only the root LP relaxation.
solve_lp_first: bool
If true, solve LP relaxation first, then solve original MILP. This
option should be activated if the LP relaxation is not very
expensive to solve and if it provides good hints for the integer
solution.
Returns
-------
dict
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 model is None:
model = instance.to_model()
self.tee = tee
self.internal_solver = self._create_internal_solver()
self.internal_solver.set_instance(instance, model)
if solve_lp_first:
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"]
else:
instance.lp_solution = self.internal_solver.get_empty_solution()
instance.lp_value = 0.0
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)
results["LP value"] = instance.lp_value
# Read MIP solution and bounds
instance.lower_bound = results["Lower bound"]
instance.upper_bound = results["Upper bound"]
instance.solver_log = results["Log"]
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["Results"]["LP value"]
instances[idx].lower_bound = r["Results"]["Lower bound"]
instances[idx].upper_bound = r["Results"]["Upper bound"]
instances[idx].found_violated_lazy_constraints = r["Violated lazy constraints"]
instances[idx].found_violated_user_cuts = r["Violated user cuts"]
instances[idx].solver_log = r["Results"]["Log"]
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__

View File

@@ -0,0 +1,3 @@
# 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.

View File

@@ -0,0 +1,223 @@
# 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 re
import sys
import logging
import pyomo
from abc import abstractmethod
from io import StringIO
from pyomo import environ as pe
from pyomo.core import Var
from .. import RedirectOutput
from ..internal import InternalSolver
from ...instance import Instance
logger = logging.getLogger(__name__)
class BasePyomoSolver(InternalSolver):
"""
Base class for all Pyomo solvers.
"""
def __init__(self):
self.instance = None
self.model = None
self._all_vars = None
self._bin_vars = None
self._is_warm_start_available = False
self._pyomo_solver = None
self._obj_sense = None
self._varname_to_var = {}
def solve_lp(self, tee=False):
for var in self._bin_vars:
lb, ub = var.bounds
var.setlb(lb)
var.setub(ub)
var.domain = pyomo.core.base.set_types.Reals
self._pyomo_solver.update_var(var)
results = self._pyomo_solver.solve(tee=tee)
for var in self._bin_vars:
var.domain = pyomo.core.base.set_types.Binary
self._pyomo_solver.update_var(var)
return {
"Optimal value": results["Problem"][0]["Lower bound"],
}
def get_solution(self):
solution = {}
for var in self.model.component_objects(Var):
solution[str(var)] = {}
for index in var:
solution[str(var)][index] = var[index].value
return solution
def get_variables(self):
variables = {}
for var in self.model.component_objects(Var):
variables[str(var)] = []
for index in var:
variables[str(var)] += [index]
return variables
def set_warm_start(self, solution):
self.clear_warm_start()
count_total, count_fixed = 0, 0
for var_name in solution:
var = self._varname_to_var[var_name]
for index in solution[var_name]:
count_total += 1
var[index].value = solution[var_name][index]
if solution[var_name][index] is not None:
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))
def clear_warm_start(self):
for var in self._all_vars:
if not var.fixed:
var.value = None
self._is_warm_start_available = False
def set_instance(self, instance, model=None):
if model is None:
model = instance.to_model()
assert isinstance(instance, Instance)
assert isinstance(model, pe.ConcreteModel)
self.instance = instance
self.model = model
self._pyomo_solver.set_instance(model)
# Update objective sense
self._obj_sense = "max"
if self._pyomo_solver._objective.sense == pyomo.core.kernel.objective.minimize:
self._obj_sense = "min"
# Update variables
self._all_vars = []
self._bin_vars = []
self._varname_to_var = {}
for var in model.component_objects(Var):
self._varname_to_var[var.name] = var
for idx in var:
self._all_vars += [var[idx]]
if var[idx].domain == pyomo.core.base.set_types.Binary:
self._bin_vars += [var[idx]]
def fix(self, solution):
count_total, count_fixed = 0, 0
for varname in solution:
for index in solution[varname]:
var = self._varname_to_var[varname]
count_total += 1
if solution[varname][index] is None:
continue
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))
def add_constraint(self, constraint):
self._pyomo_solver.add_constraint(constraint)
def solve(self, tee=False):
total_wallclock_time = 0
streams = [StringIO()]
if tee:
streams += [sys.stdout]
self.instance.found_violated_lazy_constraints = []
self.instance.found_violated_user_cuts = []
while True:
logger.debug("Solving MIP...")
with RedirectOutput(streams):
results = self._pyomo_solver.solve(tee=True,
warmstart=self._is_warm_start_available)
total_wallclock_time += results["Solver"][0]["Wallclock time"]
logger.debug("Finding violated constraints...")
violations = self.instance.find_violated_lazy_constraints(self.model)
if len(violations) == 0:
break
self.instance.found_violated_lazy_constraints += violations
logger.debug(" %d violations found" % len(violations))
for v in violations:
cut = self.instance.build_lazy_constraint(self.model, v)
self.add_constraint(cut)
log = streams[0].getvalue()
return {
"Lower bound": results["Problem"][0]["Lower bound"],
"Upper bound": results["Problem"][0]["Upper bound"],
"Wallclock time": total_wallclock_time,
"Nodes": self._extract_node_count(log),
"Sense": self._obj_sense,
"Log": log,
"Warm start value": self._extract_warm_start_value(log),
}
@staticmethod
def __extract(log, regexp, default=None):
value = default
for line in log.splitlines():
matches = re.findall(regexp, line)
if len(matches) == 0:
continue
value = matches[0]
return value
def _extract_warm_start_value(self, log):
value = self.__extract(log, self._get_warm_start_regexp())
if value is not None:
value = float(value)
return value
def _extract_node_count(self, log):
return int(self.__extract(log,
self._get_node_count_regexp(),
default=1))
def set_threads(self, threads):
key = self._get_threads_option_name()
self._pyomo_solver.options[key] = threads
def set_time_limit(self, time_limit):
key = self._get_time_limit_option_name()
self._pyomo_solver.options[key] = time_limit
def set_node_limit(self, node_limit):
key = self._get_node_limit_option_name()
self._pyomo_solver.options[key] = node_limit
def set_gap_tolerance(self, gap_tolerance):
key = self._get_gap_tolerance_option_name()
self._pyomo_solver.options[key] = gap_tolerance
@abstractmethod
def _get_warm_start_regexp(self):
pass
@abstractmethod
def _get_node_count_regexp(self):
pass
@abstractmethod
def _get_threads_option_name(self):
pass
@abstractmethod
def _get_time_limit_option_name(self):
pass
@abstractmethod
def _get_node_limit_option_name(self):
pass
@abstractmethod
def _get_gap_tolerance_option_name(self):
pass

View File

@@ -0,0 +1,49 @@
# 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.
from pyomo import environ as pe
from scipy.stats import randint
from .base import BasePyomoSolver
class CplexPyomoSolver(BasePyomoSolver):
def __init__(self, options=None):
"""
Creates a new CPLEX solver, accessed through Pyomo.
Parameters
----------
options: dict
Dictionary of options to pass to the Pyomo solver. For example,
{"mip_display": 5} to increase the log verbosity.
"""
super().__init__()
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:
for (key, value) in options.items():
self._pyomo_solver.options[key] = value
def _get_warm_start_regexp(self):
return "MIP start .* with objective ([0-9.e+-]*)\\."
def _get_node_count_regexp(self):
return "^[ *] *([0-9]+)"
def _get_threads_option_name(self):
return "threads"
def _get_time_limit_option_name(self):
return "timelimit"
def _get_node_limit_option_name(self):
return "mip_limits_nodes"
def _get_gap_tolerance_option_name(self):
return "mip_tolerances_mipgap"
def set_branching_priorities(self, priorities):
raise NotImplementedError

View File

@@ -0,0 +1,129 @@
# 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 sys
import logging
from io import StringIO
from pyomo import environ as pe
from scipy.stats import randint
from .base import BasePyomoSolver
from .. import RedirectOutput
logger = logging.getLogger(__name__)
class GurobiPyomoSolver(BasePyomoSolver):
def __init__(self,
use_lazy_callbacks=True,
options=None):
"""
Creates a new Gurobi solver, accessed through Pyomo.
Parameters
----------
use_lazy_callbacks: bool
If true, lazy constraints will be enforced via lazy callbacks.
Otherwise, they will be enforced via a simple solve-check loop.
options: dict
Dictionary of options to pass to the Pyomo solver. For example,
{"Threads": 4} to set the number of threads.
"""
super().__init__()
self._use_lazy_callbacks = use_lazy_callbacks
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():
self._pyomo_solver.options[key] = value
def solve(self, tee=False):
if self._use_lazy_callbacks:
return self._solve_with_callbacks(tee)
else:
return super().solve(tee)
def _solve_with_callbacks(self, tee):
from gurobipy import GRB
def cb(cb_model, cb_opt, cb_where):
try:
# User cuts
if cb_where == GRB.Callback.MIPNODE:
logger.debug("Finding violated cutting planes...")
cb_opt.cbGetNodeRel(self._all_vars)
violations = self.instance.find_violated_user_cuts(cb_model)
self.instance.found_violated_user_cuts += violations
logger.debug(" %d found" % len(violations))
for v in violations:
cut = self.instance.build_user_cut(cb_model, v)
cb_opt.cbCut(cut)
# Lazy constraints
if cb_where == GRB.Callback.MIPSOL:
cb_opt.cbGetSolution(self._all_vars)
logger.debug("Finding violated lazy constraints...")
violations = self.instance.find_violated_lazy_constraints(cb_model)
self.instance.found_violated_lazy_constraints += violations
logger.debug(" %d found" % len(violations))
for v in violations:
cut = self.instance.build_lazy_constraint(cb_model, v)
cb_opt.cbLazy(cut)
except Exception as e:
logger.error(e)
self._pyomo_solver.options["LazyConstraints"] = 1
self._pyomo_solver.options["PreCrush"] = 1
self._pyomo_solver.set_callback(cb)
self.instance.found_violated_lazy_constraints = []
self.instance.found_violated_user_cuts = []
streams = [StringIO()]
if tee:
streams += [sys.stdout]
with RedirectOutput(streams):
results = self._pyomo_solver.solve(tee=True,
warmstart=self._is_warm_start_available)
self._pyomo_solver.set_callback(None)
log = streams[0].getvalue()
return {
"Lower bound": results["Problem"][0]["Lower bound"],
"Upper bound": results["Problem"][0]["Upper bound"],
"Wallclock time": results["Solver"][0]["Wallclock time"],
"Nodes": self._extract_node_count(log),
"Sense": self._obj_sense,
"Log": log,
"Warm start value": self._extract_warm_start_value(log),
}
def _extract_node_count(self, log):
return max(1, int(self._pyomo_solver._solver_model.getAttr("NodeCount")))
def _get_warm_start_regexp(self):
return "MIP start with objective ([0-9.e+-]*)"
def _get_node_count_regexp(self):
return None
def _get_threads_option_name(self):
return "Threads"
def _get_time_limit_option_name(self):
return "TimeLimit"
def _get_node_limit_option_name(self):
return "NodeLimit"
def _get_gap_tolerance_option_name(self):
return "MIPGap"
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():
gvar = self._pyomo_solver._pyomo_var_to_solver_var_map[var[index]]
gvar.setAttr(GRB.Attr.BranchPriority, int(round(priority)))

View File

@@ -0,0 +1,26 @@
# 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.
from miplearn import BasePyomoSolver, GurobiSolver, GurobiPyomoSolver, CplexPyomoSolver
from miplearn.problems.knapsack import KnapsackInstance, GurobiKnapsackInstance
def _get_instance(solver):
if issubclass(solver, BasePyomoSolver):
return KnapsackInstance(
weights=[23., 26., 20., 18.],
prices=[505., 352., 458., 220.],
capacity=67.,
)
if issubclass(solver, GurobiSolver):
return GurobiKnapsackInstance(
weights=[23., 26., 20., 18.],
prices=[505., 352., 458., 220.],
capacity=67.,
)
assert False
def _get_internal_solvers():
return [GurobiPyomoSolver, CplexPyomoSolver, GurobiSolver]

View File

@@ -0,0 +1,115 @@
# 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 io import StringIO
import pyomo.environ as pe
from miplearn import BasePyomoSolver
from miplearn.problems.knapsack import ChallengeA
from miplearn.solvers import RedirectOutput
from . import _get_instance, _get_internal_solvers
logger = logging.getLogger(__name__)
def test_redirect_output():
import sys
original_stdout = sys.stdout
io = StringIO()
with RedirectOutput([io]):
print("Hello world")
assert sys.stdout == original_stdout
assert io.getvalue() == "Hello world\n"
def test_internal_solver_warm_starts():
for solver_class in _get_internal_solvers():
logger.info("Solver: %s" % solver_class)
instance = _get_instance(solver_class)
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,
}
})
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,
}
})
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,
}
})
stats = solver.solve(tee=True)
assert stats["Lower bound"] == 725.0
assert stats["Upper bound"] == 725.0
def test_internal_solver():
for solver_class in _get_internal_solvers():
logger.info("Solver: %s" % solver_class)
instance = _get_instance(solver_class)
model = instance.to_model()
solver = solver_class()
solver.set_instance(instance, model)
stats = solver.solve_lp()
assert round(stats["Optimal value"], 3) == 1287.923
solution = solver.get_solution()
assert round(solution["x"][0], 3) == 1.000
assert round(solution["x"][1], 3) == 0.923
assert round(solution["x"][2], 3) == 1.000
assert round(solution["x"][3], 3) == 0.000
stats = solver.solve(tee=True)
assert len(stats["Log"]) > 100
assert stats["Lower bound"] == 1183.0
assert stats["Upper bound"] == 1183.0
assert stats["Sense"] == "max"
assert isinstance(stats["Wallclock time"], float)
assert isinstance(stats["Nodes"], int)
solution = solver.get_solution()
assert solution["x"][0] == 1.0
assert solution["x"][1] == 0.0
assert solution["x"][2] == 1.0
assert solution["x"][3] == 1.0
if isinstance(solver, BasePyomoSolver):
model.cut = pe.Constraint(expr=model.x[0] <= 0.5)
solver.add_constraint(model.cut)
solver.solve_lp()
assert model.x[0].value == 0.5
# def test_node_count():
# for solver in _get_internal_solvers():
# challenge = ChallengeA()
# solver.set_time_limit(1)
# solver.set_instance(challenge.test_instances[0])
# stats = solver.solve(tee=True)
# assert stats["Nodes"] > 1

View File

@@ -0,0 +1,67 @@
# 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
import pickle
import tempfile
from miplearn import LazyConstraintsComponent
from miplearn import LearningSolver
from . import _get_instance, _get_internal_solvers
logger = logging.getLogger(__name__)
def test_learning_solver():
for mode in ["exact", "heuristic"]:
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.solve(instance)
assert instance.solution["x"][0] == 1.0
assert instance.solution["x"][1] == 0.0
assert instance.solution["x"][2] == 1.0
assert instance.solution["x"][3] == 1.0
assert instance.lower_bound == 1183.0
assert instance.upper_bound == 1183.0
assert round(instance.lp_solution["x"][0], 3) == 1.000
assert round(instance.lp_solution["x"][1], 3) == 0.923
assert round(instance.lp_solution["x"][2], 3) == 1.000
assert round(instance.lp_solution["x"][3], 3) == 0.000
assert round(instance.lp_value, 3) == 1287.923
assert instance.found_violated_lazy_constraints == []
assert instance.found_violated_user_cuts == []
assert len(instance.solver_log) > 100
solver.fit([instance])
solver.solve(instance)
# Assert solver is picklable
with tempfile.TemporaryFile() as file:
pickle.dump(solver, file)
def test_parallel_solve():
for internal_solver in _get_internal_solvers():
instances = [_get_instance(internal_solver) for _ in range(10)]
solver = LearningSolver(solver=internal_solver)
results = solver.parallel_solve(instances, n_jobs=3)
assert len(results) == 10
for instance in instances:
assert len(instance.solution["x"].keys()) == 4
def test_add_components():
solver = LearningSolver(components=[])
solver.add(LazyConstraintsComponent())
solver.add(LazyConstraintsComponent())
assert len(solver.components) == 1
assert "BranchPriorityComponent" in solver.components