Implement lazy callbacks & two-phase gap

This commit is contained in:
2020-09-25 06:02:07 -05:00
parent 86e7b1981f
commit a221740ac5
10 changed files with 288 additions and 75 deletions

View File

@@ -13,13 +13,25 @@ logger = logging.getLogger(__name__)
class GurobiSolver(InternalSolver):
def __init__(self, params=None):
def __init__(self,
params=None,
lazy_cb_frequency=1,
):
"""
An InternalSolver backed by Gurobi's Python API (without Pyomo).
Parameters
----------
params
Parameters to pass to Gurobi. For example, params={"MIPGap": 1e-3}
sets the gap tolerance to 1e-3.
lazy_cb_frequency
If 1, calls lazy constraint callbacks whenever an integer solution
is found. If 2, calls it also at every node, after solving the
LP relaxation of that node.
"""
if params is None:
params = {}
# params = {
# "LazyConstraints": 1,
# "PreCrush": 1,
# }
from gurobipy import GRB
self.GRB = GRB
self.instance = None
@@ -27,8 +39,16 @@ class GurobiSolver(InternalSolver):
self.params = params
self._all_vars = None
self._bin_vars = None
self.cb_where = None
assert lazy_cb_frequency in [1, 2]
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]
def set_instance(self, instance, model=None):
self._raise_if_callback()
if model is None:
model = instance.to_model()
self.instance = instance
@@ -36,6 +56,10 @@ class GurobiSolver(InternalSolver):
self.model.update()
self._update_vars()
def _raise_if_callback(self):
if self.cb_where is not None:
raise Exception("method cannot be called from a callback")
def _update_vars(self):
self._all_vars = {}
self._bin_vars = {}
@@ -63,6 +87,7 @@ class GurobiSolver(InternalSolver):
self.model.setParam(name, value)
def solve_lp(self, tee=False):
self._raise_if_callback()
self._apply_params()
streams = [StringIO()]
if tee:
@@ -83,7 +108,24 @@ class GurobiSolver(InternalSolver):
"Log": log
}
def solve(self, tee=False, iteration_cb=None):
def solve(self,
tee=False,
iteration_cb=None,
lazy_cb=None):
self._raise_if_callback()
def cb_wrapper(cb_model, cb_where):
try:
self.cb_where = cb_where
if cb_where in self.lazy_cb_where:
lazy_cb(self, self.model)
except:
logger.exception("callback error")
finally:
self.cb_where = None
if lazy_cb:
self.params["LazyConstraints"] = 1
self._apply_params()
total_wallclock_time = 0
total_nodes = 0
@@ -95,7 +137,10 @@ class GurobiSolver(InternalSolver):
while True:
logger.debug("Solving MIP...")
with RedirectOutput(streams):
self.model.optimize()
if lazy_cb is None:
self.model.optimize()
else:
self.model.optimize(cb_wrapper)
total_wallclock_time += self.model.runtime
total_nodes += int(self.model.nodeCount)
should_repeat = iteration_cb()
@@ -114,6 +159,8 @@ class GurobiSolver(InternalSolver):
}
def get_solution(self):
self._raise_if_callback()
solution = {}
for (varname, vardict) in self._all_vars.items():
solution[varname] = {}
@@ -121,7 +168,22 @@ class GurobiSolver(InternalSolver):
solution[varname][idx] = var.x
return solution
def get_value(self, var_name, index):
var = self._all_vars[var_name][index]
return self._get_value(var)
def _get_value(self, var):
if self.cb_where == self.GRB.Callback.MIPSOL:
return self.model.cbGetSolution(var)
elif self.cb_where == self.GRB.Callback.MIPNODE:
return self.model.cbGetNodeRel(var)
elif self.cb_where is None:
return var.x
else:
raise Exception("get_value cannot be called from cb_where=%s" % self.cb_where)
def get_variables(self):
self._raise_if_callback()
variables = {}
for (varname, vardict) in self._all_vars.items():
variables[varname] = []
@@ -132,12 +194,18 @@ class GurobiSolver(InternalSolver):
def add_constraint(self, constraint, name=""):
if type(constraint) is tuple:
lhs, sense, rhs, name = constraint
logger.debug(lhs, sense, rhs)
self.model.addConstr(lhs, sense, rhs, name)
if self.cb_where in [self.GRB.Callback.MIPSOL, self.GRB.Callback.MIPNODE]:
self.model.cbLazy(lhs, sense, rhs)
else:
self.model.addConstr(lhs, sense, rhs, name)
else:
self.model.addConstr(constraint, name=name)
if self.cb_where in [self.GRB.Callback.MIPSOL, self.GRB.Callback.MIPNODE]:
self.model.cbLazy(constraint)
else:
self.model.addConstr(constraint, name=name)
def set_warm_start(self, solution):
self._raise_if_callback()
count_fixed, count_total = 0, 0
for (varname, vardict) in solution.items():
for (idx, value) in vardict.items():
@@ -149,11 +217,13 @@ class GurobiSolver(InternalSolver):
(count_fixed, count_total))
def clear_warm_start(self):
self._raise_if_callback()
for (varname, vardict) in self._all_vars:
for (idx, var) in vardict.items():
var[idx].start = self.GRB.UNDEFINED
def fix(self, solution):
self._raise_if_callback()
for (varname, vardict) in solution.items():
for (idx, value) in vardict.items():
if value is None:
@@ -164,10 +234,12 @@ class GurobiSolver(InternalSolver):
var.ub = value
def get_constraint_ids(self):
self._raise_if_callback()
self.model.update()
return [c.ConstrName for c in self.model.getConstrs()]
def extract_constraint(self, cid):
self._raise_if_callback()
constr = self.model.getConstrByName(cid)
cobj = (self.model.getRow(constr),
constr.sense,
@@ -178,29 +250,41 @@ class GurobiSolver(InternalSolver):
def is_constraint_satisfied(self, cobj, tol=1e-5):
lhs, sense, rhs, name = cobj
lhs_value = lhs.getValue()
if self.cb_where is not None:
lhs_value = lhs.getConstant()
for i in range(lhs.size()):
var = lhs.getVar(i)
coeff = lhs.getCoeff(i)
lhs_value += self._get_value(var) * coeff
else:
lhs_value = lhs.getValue()
if sense == "<":
return lhs_value <= rhs + tol
elif sense == ">":
return lhs_value >= rhs - tol
elif sense == "=":
return abs(rhs - lhs_value) < tol
return abs(rhs - lhs_value) < abs(tol)
else:
raise Exception("Unknown sense: %s" % sense)
def set_branching_priorities(self, priorities):
self._raise_if_callback()
logger.warning("set_branching_priorities not implemented")
def set_threads(self, threads):
self._raise_if_callback()
self.params["Threads"] = threads
def set_time_limit(self, time_limit):
self._raise_if_callback()
self.params["TimeLimit"] = time_limit
def set_node_limit(self, node_limit):
self._raise_if_callback()
self.params["NodeLimit"] = node_limit
def set_gap_tolerance(self, gap_tolerance):
self._raise_if_callback()
self.params["MIPGap"] = gap_tolerance
def _extract_warm_start_value(self, log):