Implement lazy callbacks & two-phase gap

pull/3/head
Alinson S. Xavier 5 years ago
parent 86e7b1981f
commit a221740ac5

@ -24,3 +24,6 @@ class Component(ABC):
def after_iteration(self, solver, instance, model): def after_iteration(self, solver, instance, model):
return False return False
def on_lazy_callback(self, solver, instance, model):
return

@ -21,14 +21,29 @@ class LazyConstraint:
class StaticLazyConstraintsComponent(Component): class StaticLazyConstraintsComponent(Component):
def __init__(self, def __init__(self,
classifier=CountingClassifier(), classifier=CountingClassifier(),
threshold=0.05): threshold=0.05,
use_two_phase_gap=True,
large_gap=1e-2,
violation_tolerance=-0.5,
):
self.threshold = threshold self.threshold = threshold
self.classifier_prototype = classifier self.classifier_prototype = classifier
self.classifiers = {} self.classifiers = {}
self.pool = [] self.pool = []
self.original_gap = None
self.large_gap = large_gap
self.is_gap_large = False
self.use_two_phase_gap = use_two_phase_gap
self.violation_tolerance = violation_tolerance
def before_solve(self, solver, instance, model): def before_solve(self, solver, instance, model):
self.pool = [] self.pool = []
if not solver.use_lazy_cb and self.use_two_phase_gap:
logger.info("Increasing gap tolerance to %f", self.large_gap)
self.original_gap = solver.gap_tolerance
self.is_gap_large = True
solver.internal_solver.set_gap_tolerance(self.large_gap)
instance.found_violated_lazy_constraints = [] instance.found_violated_lazy_constraints = []
if instance.has_static_lazy_constraints(): if instance.has_static_lazy_constraints():
self._extract_and_predict_static(solver, instance) self._extract_and_predict_static(solver, instance)
@ -37,21 +52,39 @@ class StaticLazyConstraintsComponent(Component):
pass pass
def after_iteration(self, solver, instance, model): def after_iteration(self, solver, instance, model):
logger.info("Finding violated lazy constraints...") if solver.use_lazy_cb:
return False
else:
should_repeat = self._check_and_add(instance, solver)
if should_repeat:
return True
else:
if self.is_gap_large:
logger.info("Restoring gap tolerance to %f", self.original_gap)
solver.internal_solver.set_gap_tolerance(self.original_gap)
self.is_gap_large = False
return True
else:
return False
def on_lazy_callback(self, solver, instance, model):
self._check_and_add(instance, solver)
def _check_and_add(self, instance, solver):
logger.debug("Finding violated lazy constraints...")
constraints_to_add = [] constraints_to_add = []
for c in self.pool: for c in self.pool:
if not solver.internal_solver.is_constraint_satisfied(c.obj): if not solver.internal_solver.is_constraint_satisfied(c.obj,
tol=self.violation_tolerance):
constraints_to_add.append(c) constraints_to_add.append(c)
for c in constraints_to_add: for c in constraints_to_add:
self.pool.remove(c) self.pool.remove(c)
solver.internal_solver.add_constraint(c.obj) solver.internal_solver.add_constraint(c.obj)
instance.found_violated_lazy_constraints += [c.cid] instance.found_violated_lazy_constraints += [c.cid]
if len(constraints_to_add) > 0: if len(constraints_to_add) > 0:
logger.info("Added %d lazy constraints back into the model" % len(constraints_to_add)) logger.info("%8d lazy constraints added %8d in the pool" % (len(constraints_to_add), len(self.pool)))
logger.info("Lazy constraint pool has %d constraints" % len(self.pool))
return True return True
else: else:
logger.info("Found no violated lazy constraints")
return False return False
def fit(self, training_instances): def fit(self, training_instances):
@ -92,7 +125,7 @@ class StaticLazyConstraintsComponent(Component):
obj=solver.internal_solver.extract_constraint(cid)) obj=solver.internal_solver.extract_constraint(cid))
constraints[category] += [c] constraints[category] += [c]
self.pool.append(c) self.pool.append(c)
logger.info("Extracted %d lazy constraints" % len(self.pool)) logger.info("%8d lazy constraints extracted" % len(self.pool))
logger.info("Predicting required lazy constraints...") logger.info("Predicting required lazy constraints...")
n_added = 0 n_added = 0
for (category, x_values) in x.items(): for (category, x_values) in x.items():
@ -108,8 +141,7 @@ class StaticLazyConstraintsComponent(Component):
self.pool.remove(c) self.pool.remove(c)
solver.internal_solver.add_constraint(c.obj) solver.internal_solver.add_constraint(c.obj)
instance.found_violated_lazy_constraints += [c.cid] instance.found_violated_lazy_constraints += [c.cid]
logger.info("Added %d lazy constraints back into the model" % n_added) logger.info("%8d lazy constraints added %8d in the pool" % (n_added, len(self.pool)))
logger.info("Lazy constraint pool has %d constraints" % len(self.pool))
def _collect_constraints(self, train_instances): def _collect_constraints(self, train_instances):
constraints = {} constraints = {}

@ -13,6 +13,9 @@ from miplearn.classifiers import Classifier
def test_usage_with_solver(): def test_usage_with_solver():
solver = Mock(spec=LearningSolver) solver = Mock(spec=LearningSolver)
solver.use_lazy_cb = False
solver.gap_tolerance = 1e-4
internal = solver.internal_solver = Mock(spec=InternalSolver) internal = solver.internal_solver = Mock(spec=InternalSolver)
internal.get_constraint_ids = Mock(return_value=["c1", "c2", "c3", "c4"]) internal.get_constraint_ids = Mock(return_value=["c1", "c2", "c3", "c4"])
internal.extract_constraint = Mock(side_effect=lambda cid: "<%s>" % cid) internal.extract_constraint = Mock(side_effect=lambda cid: "<%s>" % cid)
@ -37,7 +40,8 @@ def test_usage_with_solver():
"c4": "type-b", "c4": "type-b",
}[cid]) }[cid])
component = StaticLazyConstraintsComponent(threshold=0.90) component = StaticLazyConstraintsComponent(threshold=0.90,
use_two_phase_gap=False)
component.classifiers = { component.classifiers = {
"type-a": Mock(spec=Classifier), "type-a": Mock(spec=Classifier),
"type-b": Mock(spec=Classifier), "type-b": Mock(spec=Classifier),

@ -7,40 +7,43 @@ import logging
import time import time
import sys import sys
if sys.stdout.isatty(): class TimeFormatter():
def __init__(self, start_time, log_colors):
self.start_time = start_time
self.log_colors = log_colors
def format(self, record):
if record.levelno >= logging.ERROR:
color = self.log_colors["red"]
elif record.levelno >= logging.WARNING:
color = self.log_colors["yellow"]
else:
color = self.log_colors["green"]
return "%s[%12.3f]%s %s" % (color,
record.created - self.start_time,
self.log_colors["reset"],
record.getMessage())
def setup_logger(start_time=None,
force_color=False):
if start_time is None:
start_time = time.time()
if sys.stdout.isatty() or force_color:
log_colors = { log_colors = {
"green": '\033[92m', "green": '\033[92m',
"yellow": '\033[93m', "yellow": '\033[93m',
"red": '\033[91m', "red": '\033[91m',
"reset": '\033[0m', "reset": '\033[0m',
} }
else: else:
log_colors = { log_colors = {
"green": "", "green": "",
"yellow": "", "yellow": "",
"red": "", "red": "",
"reset": "", "reset": "",
} }
class TimeFormatter():
def __init__(self, start_time):
self.start_time = start_time
def format(self, record):
if record.levelno >= logging.ERROR:
color = log_colors["red"]
elif record.levelno >= logging.WARNING:
color = log_colors["yellow"]
else:
color = log_colors["green"]
return "%s[%12.3f]%s %s" % (color,
record.created - self.start_time,
log_colors["reset"],
record.getMessage())
def setup_logger(start_time):
handler = logging.StreamHandler() handler = logging.StreamHandler()
handler.setFormatter(TimeFormatter(start_time)) handler.setFormatter(TimeFormatter(start_time, log_colors))
logging.getLogger().addHandler(handler) logging.getLogger().addHandler(handler)
logging.getLogger("miplearn").setLevel(logging.INFO) logging.getLogger("miplearn").setLevel(logging.INFO)
lg = logging.getLogger("miplearn") lg = logging.getLogger("miplearn")

@ -13,13 +13,25 @@ logger = logging.getLogger(__name__)
class GurobiSolver(InternalSolver): 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: if params is None:
params = {} params = {}
# params = {
# "LazyConstraints": 1,
# "PreCrush": 1,
# }
from gurobipy import GRB from gurobipy import GRB
self.GRB = GRB self.GRB = GRB
self.instance = None self.instance = None
@ -27,8 +39,16 @@ class GurobiSolver(InternalSolver):
self.params = params self.params = params
self._all_vars = None self._all_vars = None
self._bin_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): def set_instance(self, instance, model=None):
self._raise_if_callback()
if model is None: if model is None:
model = instance.to_model() model = instance.to_model()
self.instance = instance self.instance = instance
@ -36,6 +56,10 @@ class GurobiSolver(InternalSolver):
self.model.update() self.model.update()
self._update_vars() 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): def _update_vars(self):
self._all_vars = {} self._all_vars = {}
self._bin_vars = {} self._bin_vars = {}
@ -63,6 +87,7 @@ class GurobiSolver(InternalSolver):
self.model.setParam(name, value) self.model.setParam(name, value)
def solve_lp(self, tee=False): def solve_lp(self, tee=False):
self._raise_if_callback()
self._apply_params() self._apply_params()
streams = [StringIO()] streams = [StringIO()]
if tee: if tee:
@ -83,7 +108,24 @@ class GurobiSolver(InternalSolver):
"Log": log "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() self._apply_params()
total_wallclock_time = 0 total_wallclock_time = 0
total_nodes = 0 total_nodes = 0
@ -95,7 +137,10 @@ class GurobiSolver(InternalSolver):
while True: while True:
logger.debug("Solving MIP...") logger.debug("Solving MIP...")
with RedirectOutput(streams): with RedirectOutput(streams):
if lazy_cb is None:
self.model.optimize() self.model.optimize()
else:
self.model.optimize(cb_wrapper)
total_wallclock_time += self.model.runtime total_wallclock_time += self.model.runtime
total_nodes += int(self.model.nodeCount) total_nodes += int(self.model.nodeCount)
should_repeat = iteration_cb() should_repeat = iteration_cb()
@ -114,6 +159,8 @@ class GurobiSolver(InternalSolver):
} }
def get_solution(self): def get_solution(self):
self._raise_if_callback()
solution = {} solution = {}
for (varname, vardict) in self._all_vars.items(): for (varname, vardict) in self._all_vars.items():
solution[varname] = {} solution[varname] = {}
@ -121,7 +168,22 @@ class GurobiSolver(InternalSolver):
solution[varname][idx] = var.x solution[varname][idx] = var.x
return solution 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): def get_variables(self):
self._raise_if_callback()
variables = {} variables = {}
for (varname, vardict) in self._all_vars.items(): for (varname, vardict) in self._all_vars.items():
variables[varname] = [] variables[varname] = []
@ -132,12 +194,18 @@ class GurobiSolver(InternalSolver):
def add_constraint(self, constraint, name=""): def add_constraint(self, constraint, name=""):
if type(constraint) is tuple: if type(constraint) is tuple:
lhs, sense, rhs, name = constraint lhs, sense, rhs, name = constraint
logger.debug(lhs, sense, rhs) 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) self.model.addConstr(lhs, sense, rhs, name)
else:
if self.cb_where in [self.GRB.Callback.MIPSOL, self.GRB.Callback.MIPNODE]:
self.model.cbLazy(constraint)
else: else:
self.model.addConstr(constraint, name=name) self.model.addConstr(constraint, name=name)
def set_warm_start(self, solution): def set_warm_start(self, solution):
self._raise_if_callback()
count_fixed, count_total = 0, 0 count_fixed, count_total = 0, 0
for (varname, vardict) in solution.items(): for (varname, vardict) in solution.items():
for (idx, value) in vardict.items(): for (idx, value) in vardict.items():
@ -149,11 +217,13 @@ class GurobiSolver(InternalSolver):
(count_fixed, count_total)) (count_fixed, count_total))
def clear_warm_start(self): def clear_warm_start(self):
self._raise_if_callback()
for (varname, vardict) in self._all_vars: for (varname, vardict) in self._all_vars:
for (idx, var) in vardict.items(): for (idx, var) in vardict.items():
var[idx].start = self.GRB.UNDEFINED var[idx].start = self.GRB.UNDEFINED
def fix(self, solution): def fix(self, solution):
self._raise_if_callback()
for (varname, vardict) in solution.items(): for (varname, vardict) in solution.items():
for (idx, value) in vardict.items(): for (idx, value) in vardict.items():
if value is None: if value is None:
@ -164,10 +234,12 @@ class GurobiSolver(InternalSolver):
var.ub = value var.ub = value
def get_constraint_ids(self): def get_constraint_ids(self):
self._raise_if_callback()
self.model.update() self.model.update()
return [c.ConstrName for c in self.model.getConstrs()] return [c.ConstrName for c in self.model.getConstrs()]
def extract_constraint(self, cid): def extract_constraint(self, cid):
self._raise_if_callback()
constr = self.model.getConstrByName(cid) constr = self.model.getConstrByName(cid)
cobj = (self.model.getRow(constr), cobj = (self.model.getRow(constr),
constr.sense, constr.sense,
@ -178,29 +250,41 @@ class GurobiSolver(InternalSolver):
def is_constraint_satisfied(self, cobj, tol=1e-5): def is_constraint_satisfied(self, cobj, tol=1e-5):
lhs, sense, rhs, name = cobj lhs, sense, rhs, name = cobj
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() lhs_value = lhs.getValue()
if sense == "<": if sense == "<":
return lhs_value <= rhs + tol return lhs_value <= rhs + tol
elif sense == ">": elif sense == ">":
return lhs_value >= rhs - tol return lhs_value >= rhs - tol
elif sense == "=": elif sense == "=":
return abs(rhs - lhs_value) < tol return abs(rhs - lhs_value) < abs(tol)
else: else:
raise Exception("Unknown sense: %s" % sense) raise Exception("Unknown sense: %s" % sense)
def set_branching_priorities(self, priorities): def set_branching_priorities(self, priorities):
self._raise_if_callback()
logger.warning("set_branching_priorities not implemented") logger.warning("set_branching_priorities not implemented")
def set_threads(self, threads): def set_threads(self, threads):
self._raise_if_callback()
self.params["Threads"] = threads self.params["Threads"] = threads
def set_time_limit(self, time_limit): def set_time_limit(self, time_limit):
self._raise_if_callback()
self.params["TimeLimit"] = time_limit self.params["TimeLimit"] = time_limit
def set_node_limit(self, node_limit): def set_node_limit(self, node_limit):
self._raise_if_callback()
self.params["NodeLimit"] = node_limit self.params["NodeLimit"] = node_limit
def set_gap_tolerance(self, gap_tolerance): def set_gap_tolerance(self, gap_tolerance):
self._raise_if_callback()
self.params["MIPGap"] = gap_tolerance self.params["MIPGap"] = gap_tolerance
def _extract_warm_start_value(self, log): def _extract_warm_start_value(self, log):

@ -119,7 +119,7 @@ class InternalSolver(ABC):
pass pass
@abstractmethod @abstractmethod
def solve(self, tee=False, iteration_cb=None): def solve(self, tee=False, iteration_cb=None, lazy_cb=None):
""" """
Solves the currently loaded instance. After this method finishes, Solves the currently loaded instance. After this method finishes,
the best solution found can be retrieved by calling `get_solution`. the best solution found can be retrieved by calling `get_solution`.
@ -132,7 +132,15 @@ class InternalSolver(ABC):
instead, InternalSolver enters a loop, where `solve` and `iteration_cb` instead, InternalSolver enters a loop, where `solve` and `iteration_cb`
are called alternatively. To stop the loop, `iteration_cb` should are called alternatively. To stop the loop, `iteration_cb` should
return False. Any other result causes the solver to loop again. return False. Any other result causes the solver to loop again.
tee: bool lazy_cb: (internal_solver, model) -> None
This function is called whenever the solver finds a new candidate
solution and can be used to add lazy constraints to the model. Only
two operations within the callback are allowed:
- Querying the value of a variable, through `get_value(var, idx)`
- Querying if a constraint is satisfied, through `is_constraint_satisfied(cobj)`
- Adding a new constraint to the problem, through `add_constraint`
Additional operations may be allowed by specific subclasses.
tee: Bool
If true, prints the solver log to the screen. If true, prints the solver log to the screen.
Returns Returns
@ -144,6 +152,13 @@ class InternalSolver(ABC):
""" """
pass pass
@abstractmethod
def get_value(self, var_name, index):
"""
Returns the current value of a decision variable.
"""
pass
@abstractmethod @abstractmethod
def get_constraint_ids(self): def get_constraint_ids(self):
""" """

@ -31,34 +31,61 @@ def _parallel_solve(instance_idx):
"Solution": instance.solution, "Solution": instance.solution,
"LP solution": instance.lp_solution, "LP solution": instance.lp_solution,
"Violated lazy constraints": instance.found_violated_lazy_constraints, "Violated lazy constraints": instance.found_violated_lazy_constraints,
"Violated user cuts": instance.found_violated_user_cuts, #"Violated user cuts": instance.found_violated_user_cuts,
} }
class LearningSolver: 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):
""" """
Mixed-Integer Linear Programming (MIP) solver that extracts information Mixed-Integer Linear Programming (MIP) solver that extracts information
from previous runs, using Machine Learning methods, to accelerate the from previous runs and uses Machine Learning methods to accelerate the
solution of new (yet unseen) instances. solution of new (yet unseen) instances.
Parameters Parameters
---------- ----------
components
Set of components in the solver. By default, includes:
- ObjectiveValueComponent
- PrimalSolutionComponent
- DynamicLazyConstraintsComponent
- UserCutsComponent
gap_tolerance
Relative MIP gap tolerance. By default, 1e-4.
mode
If "exact", solves problem to optimality, keeping all optimality
guarantees provided by the MIP solver. If "heuristic", uses machine
learning more agressively, and may return suboptimal solutions.
solver
The internal MIP solver to use. Can be either "cplex", "gurobi", a
solver class such as GurobiSolver, or a solver instance such as
GurobiSolver().
threads
Maximum number of threads to use. If None, uses solver default.
time_limit
Maximum running time in seconds. If None, uses solver default.
node_limit
Maximum number of branch-and-bound nodes to explore. If None, uses
solver default.
use_lazy_cb
If True, uses lazy callbacks to enforce lazy constraints, instead of
a simple solver loop. This functionality may not supported by
all internal MIP solvers.
solve_lp_first: bool solve_lp_first: bool
If true, solve LP relaxation first, then solve original MILP. This If true, solve LP relaxation first, then solve original MILP. This
option should be activated if the LP relaxation is not very option should be activated if the LP relaxation is not very
expensive to solve and if it provides good hints for the integer expensive to solve and if it provides good hints for the integer
solution. solution.
""" """
def __init__(self,
components=None,
gap_tolerance=None,
mode="exact",
solver="gurobi",
threads=None,
time_limit=None,
node_limit=None,
solve_lp_first=True):
self.components = {} self.components = {}
self.mode = mode self.mode = mode
self.internal_solver = None self.internal_solver = None
@ -69,6 +96,7 @@ class LearningSolver:
self.tee = False self.tee = False
self.node_limit = node_limit self.node_limit = node_limit
self.solve_lp_first = solve_lp_first self.solve_lp_first = solve_lp_first
self.use_lazy_cb = use_lazy_cb
if components is not None: if components is not None:
for comp in components: for comp in components:
@ -122,14 +150,12 @@ class LearningSolver:
- instance.lower_bound - instance.lower_bound
- instance.upper_bound - instance.upper_bound
- instance.solution - instance.solution
- instance.found_violated_lazy_constraints
- instance.solver_log - instance.solver_log
Additional solver components may set additional properties. Please Additional solver components may set additional properties. Please
see their documentation for more details. see their documentation for more details.
If `solve_lp_first` is False, the properties lp_solution and lp_value If `solver.solve_lp_first` is False, the properties lp_solution and
will be set to dummy values. lp_value will be set to dummy values.
Parameters Parameters
---------- ----------
@ -175,13 +201,23 @@ class LearningSolver:
def iteration_cb(): def iteration_cb():
should_repeat = False should_repeat = False
for component in self.components.values(): for comp in self.components.values():
if component.after_iteration(self, instance, model): if comp.after_iteration(self, instance, model):
should_repeat = True should_repeat = True
return should_repeat return should_repeat
def lazy_cb_wrapper(cb_solver, cb_model):
for comp in self.components.values():
comp.on_lazy_callback(self, instance, model)
lazy_cb = None
if self.use_lazy_cb:
lazy_cb = lazy_cb_wrapper
logger.info("Solving MILP...") logger.info("Solving MILP...")
results = self.internal_solver.solve(tee=tee, iteration_cb=iteration_cb) results = self.internal_solver.solve(tee=tee,
iteration_cb=iteration_cb,
lazy_cb=lazy_cb)
results["LP value"] = instance.lp_value results["LP value"] = instance.lp_value
# Read MIP solution and bounds # Read MIP solution and bounds
@ -217,7 +253,7 @@ class LearningSolver:
instances[idx].lower_bound = r["Results"]["Lower bound"] instances[idx].lower_bound = r["Results"]["Lower bound"]
instances[idx].upper_bound = r["Results"]["Upper bound"] instances[idx].upper_bound = r["Results"]["Upper bound"]
instances[idx].found_violated_lazy_constraints = r["Violated lazy constraints"] instances[idx].found_violated_lazy_constraints = r["Violated lazy constraints"]
instances[idx].found_violated_user_cuts = r["Violated user cuts"] #instances[idx].found_violated_user_cuts = r["Violated user cuts"]
instances[idx].solver_log = r["Results"]["Log"] instances[idx].solver_log = r["Results"]["Log"]
return results return results

@ -57,6 +57,10 @@ class BasePyomoSolver(InternalSolver):
solution[str(var)][index] = var[index].value solution[str(var)][index] = var[index].value
return solution return solution
def get_value(self, var_name, index):
var = self._varname_to_var[var_name]
return var[index].value
def get_variables(self): def get_variables(self):
variables = {} variables = {}
for var in self.model.component_objects(Var): for var in self.model.component_objects(Var):
@ -137,7 +141,12 @@ class BasePyomoSolver(InternalSolver):
self._pyomo_solver.add_constraint(constraint) self._pyomo_solver.add_constraint(constraint)
self._update_constrs() self._update_constrs()
def solve(self, tee=False, iteration_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 total_wallclock_time = 0
streams = [StringIO()] streams = [StringIO()]
if tee: if tee:

@ -7,13 +7,13 @@ from miplearn.problems.knapsack import KnapsackInstance, GurobiKnapsackInstance
def _get_instance(solver): def _get_instance(solver):
if issubclass(solver, BasePyomoSolver): if issubclass(solver, BasePyomoSolver) or isinstance(solver, BasePyomoSolver):
return KnapsackInstance( return KnapsackInstance(
weights=[23., 26., 20., 18.], weights=[23., 26., 20., 18.],
prices=[505., 352., 458., 220.], prices=[505., 352., 458., 220.],
capacity=67., capacity=67.,
) )
if issubclass(solver, GurobiSolver): if issubclass(solver, GurobiSolver) or isinstance(solver, GurobiSolver):
return GurobiKnapsackInstance( return GurobiKnapsackInstance(
weights=[23., 26., 20., 18.], weights=[23., 26., 20., 18.],
prices=[505., 352., 458., 220.], prices=[505., 352., 458., 220.],

@ -0,0 +1,27 @@
# 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 . import _get_instance
from ... import GurobiSolver
logger = logging.getLogger(__name__)
def test_lazy_cb():
solver = GurobiSolver()
instance = _get_instance(solver)
model = instance.to_model()
def lazy_cb(cb_solver, cb_model):
logger.info("x[0] = %.f" % cb_solver.get_value("x", 0))
cobj = (cb_model.getVarByName("x[0]") * 1.0, "<", 0.0, "cut")
if not cb_solver.is_constraint_satisfied(cobj):
cb_solver.add_constraint(cobj)
solver.set_instance(instance, model)
solver.solve(lazy_cb=lazy_cb)
solution = solver.get_solution()
assert solution["x"][0] == 0.0
Loading…
Cancel
Save