Temporarily remove native solver callbacks; add iteration_cb

pull/3/head
Alinson S. Xavier 5 years ago
parent e731f46b72
commit 425ea2b7cc

@ -24,7 +24,7 @@ from .instance import Instance
from .solvers.pyomo.base import BasePyomoSolver from .solvers.pyomo.base import BasePyomoSolver
from .solvers.pyomo.cplex import CplexPyomoSolver from .solvers.pyomo.cplex import CplexPyomoSolver
from .solvers.pyomo.gurobi import GurobiPyomoSolver from .solvers.pyomo.gurobi import GurobiPyomoSolver
from .solvers.guroby import GurobiSolver from .solvers.gurobi import GurobiSolver
from .solvers.internal import InternalSolver from .solvers.internal import InternalSolver
from .solvers.learning import LearningSolver from .solvers.learning import LearningSolver

@ -30,6 +30,7 @@ class UserCutsComponent(Component):
self.classifiers = {} self.classifiers = {}
def before_solve(self, solver, instance, model): def before_solve(self, solver, instance, model):
instance.found_violated_user_cuts = []
logger.info("Predicting violated user cuts...") logger.info("Predicting violated user cuts...")
violations = self.predict(instance) violations = self.predict(instance)
logger.info("Enforcing %d user cuts..." % len(violations)) logger.info("Enforcing %d user cuts..." % len(violations))

@ -30,6 +30,7 @@ class LazyConstraintsComponent(Component):
self.classifiers = {} self.classifiers = {}
def before_solve(self, solver, instance, model): def before_solve(self, solver, instance, model):
instance.found_violated_lazy_constraints = []
logger.info("Predicting violated lazy constraints...") logger.info("Predicting violated lazy constraints...")
violations = self.predict(instance) violations = self.predict(instance)
logger.info("Enforcing %d lazy constraints..." % len(violations)) logger.info("Enforcing %d lazy constraints..." % len(violations))

@ -269,7 +269,8 @@ class GurobiKnapsackInstance(KnapsackInstance):
n = len(self.weights) n = len(self.weights)
x = model.addVars(n, vtype=GRB.BINARY, name="x") x = model.addVars(n, vtype=GRB.BINARY, name="x")
model.addConstr(gp.quicksum(x[i] * self.weights[i] model.addConstr(gp.quicksum(x[i] * self.weights[i]
for i in range(n)) <= self.capacity) for i in range(n)) <= self.capacity,
"eq_capacity")
model.setObjective(gp.quicksum(x[i] * self.prices[i] model.setObjective(gp.quicksum(x[i] * self.prices[i]
for i in range(n)), GRB.MAXIMIZE) for i in range(n)), GRB.MAXIMIZE)
return model return model

@ -23,52 +23,52 @@ def test_generator():
assert np.std(d) > 0 assert np.std(d) > 0
def test_instance(): # def test_instance():
n_cities = 4 # n_cities = 4
distances = np.array([ # distances = np.array([
[0., 1., 2., 1.], # [0., 1., 2., 1.],
[1., 0., 1., 2.], # [1., 0., 1., 2.],
[2., 1., 0., 1.], # [2., 1., 0., 1.],
[1., 2., 1., 0.], # [1., 2., 1., 0.],
]) # ])
instance = TravelingSalesmanInstance(n_cities, distances) # instance = TravelingSalesmanInstance(n_cities, distances)
for solver_name in ['gurobi', 'cplex']: # for solver_name in ['gurobi', 'cplex']:
solver = LearningSolver(solver=solver_name) # solver = LearningSolver(solver=solver_name)
solver.solve(instance) # solver.solve(instance)
x = instance.solution["x"] # x = instance.solution["x"]
assert x[0,1] == 1.0 # assert x[0,1] == 1.0
assert x[0,2] == 0.0 # assert x[0,2] == 0.0
assert x[0,3] == 1.0 # assert x[0,3] == 1.0
assert x[1,2] == 1.0 # assert x[1,2] == 1.0
assert x[1,3] == 0.0 # assert x[1,3] == 0.0
assert x[2,3] == 1.0 # assert x[2,3] == 1.0
assert instance.lower_bound == 4.0 # assert instance.lower_bound == 4.0
assert instance.upper_bound == 4.0 # assert instance.upper_bound == 4.0
#
#
def test_subtour(): # def test_subtour():
n_cities = 6 # n_cities = 6
cities = np.array([ # cities = np.array([
[0., 0.], # [0., 0.],
[1., 0.], # [1., 0.],
[2., 0.], # [2., 0.],
[3., 0.], # [3., 0.],
[0., 1.], # [0., 1.],
[3., 1.], # [3., 1.],
]) # ])
distances = squareform(pdist(cities)) # distances = squareform(pdist(cities))
instance = TravelingSalesmanInstance(n_cities, distances) # instance = TravelingSalesmanInstance(n_cities, distances)
for solver_name in ['gurobi', 'cplex']: # for solver_name in ['gurobi', 'cplex']:
solver = LearningSolver(solver=solver_name) # solver = LearningSolver(solver=solver_name)
solver.solve(instance) # solver.solve(instance)
assert hasattr(instance, "found_violated_lazy_constraints") # assert hasattr(instance, "found_violated_lazy_constraints")
assert hasattr(instance, "found_violated_user_cuts") # assert hasattr(instance, "found_violated_user_cuts")
x = instance.solution["x"] # x = instance.solution["x"]
assert x[0,1] == 1.0 # assert x[0,1] == 1.0
assert x[0,4] == 1.0 # assert x[0,4] == 1.0
assert x[1,2] == 1.0 # assert x[1,2] == 1.0
assert x[2,3] == 1.0 # assert x[2,3] == 1.0
assert x[3,5] == 1.0 # assert x[3,5] == 1.0
assert x[4,5] == 1.0 # assert x[4,5] == 1.0
solver.fit([instance]) # solver.fit([instance])
solver.solve(instance) # solver.solve(instance)

@ -26,7 +26,6 @@ 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._varname_to_var = None
def set_instance(self, instance, model=None): def set_instance(self, instance, model=None):
if model is None: if model is None:
@ -83,45 +82,30 @@ class GurobiSolver(InternalSolver):
"Log": log "Log": log
} }
def solve(self, tee=False): def solve(self, tee=False, iteration_cb=None):
self.instance.found_violated_lazy_constraints = [] total_wallclock_time = 0
self.instance.found_violated_user_cuts = [] total_nodes = 0
streams = [StringIO()] streams = [StringIO()]
if tee: if tee:
streams += [sys.stdout] streams += [sys.stdout]
if iteration_cb is None:
iteration_cb = lambda : False
while True:
logger.debug("Solving MIP...")
with RedirectOutput(streams):
self.model.optimize()
total_wallclock_time += self.model.runtime
total_nodes += int(self.model.nodeCount)
should_repeat = iteration_cb()
if not should_repeat:
break
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() log = streams[0].getvalue()
return { return {
"Lower bound": self.model.objVal, "Lower bound": self.model.objVal,
"Upper bound": self.model.objBound, "Upper bound": self.model.objBound,
"Wallclock time": self.model.runtime, "Wallclock time": total_wallclock_time,
"Nodes": int(self.model.nodeCount), "Nodes": total_nodes,
"Sense": ("min" if self.model.modelSense == 1 else "max"), "Sense": ("min" if self.model.modelSense == 1 else "max"),
"Log": log, "Log": log,
"Warm start value": self._extract_warm_start_value(log), "Warm start value": self._extract_warm_start_value(log),
@ -143,8 +127,13 @@ class GurobiSolver(InternalSolver):
variables[varname] += [idx] variables[varname] += [idx]
return variables return variables
def add_constraint(self, constraint): def add_constraint(self, constraint, name=""):
self.model.addConstr(constraint) if type(constraint) is tuple:
lhs, sense, rhs, name = constraint
logger.debug(lhs, sense, rhs)
self.model.addConstr(lhs, sense, rhs, name)
else:
self.model.addConstr(constraint, name=name)
def set_warm_start(self, solution): def set_warm_start(self, solution):
count_fixed, count_total = 0, 0 count_fixed, count_total = 0, 0
@ -172,6 +161,31 @@ class GurobiSolver(InternalSolver):
var.lb = value var.lb = value
var.ub = value var.ub = value
def get_constraints_ids(self):
self.model.update()
return [c.ConstrName for c in self.model.getConstrs()]
def extract_constraint(self, cid):
constr = self.model.getConstrByName(cid)
cobj = (self.model.getRow(constr),
constr.sense,
constr.RHS,
constr.ConstrName)
self.model.remove(constr)
return cobj
def is_constraint_satisfied(self, cobj, tol=1e-5):
lhs, sense, rhs, name = cobj
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
else:
raise Exception("Unknown sense: %s" % sense)
def set_branching_priorities(self, priorities): def set_branching_priorities(self, priorities):
logger.warning("set_branching_priorities not implemented") logger.warning("set_branching_priorities not implemented")

@ -119,13 +119,19 @@ class InternalSolver(ABC):
pass pass
@abstractmethod @abstractmethod
def solve(self, tee=False): def solve(self, tee=False, iteration_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`.
Parameters Parameters
---------- ----------
iteration_cb: function
By default, InternalSolver makes a single call to the native `solve`
method and returns the result. If an iteration callback is provided
instead, InternalSolver enters a loop, where `solve` and `iteration_cb`
are called alternatively. To stop the loop, `iteration_cb` should
return False. Any other result causes the solver to loop again.
tee: bool tee: bool
If true, prints the solver log to the screen. If true, prints the solver log to the screen.
@ -138,25 +144,25 @@ class InternalSolver(ABC):
""" """
pass pass
# @abstractmethod @abstractmethod
def get_constraint_names(self): def get_constraints_ids(self):
""" """
Returns a list of strings, containing the name of each constraint in the Returns a list of ids, which uniquely identify each constraint in the model.
model.
""" """
pass pass
# @abstractmethod @abstractmethod
def extract_constraint(self, cname): def extract_constraint(self, cid):
""" """
Removes a given constraint from the model and returns an object `c` which Removes a given constraint from the model and returns an object `cobj` which
can be used to verify if the removed constraint is still satisfied by can be used to verify if the removed constraint is still satisfied by
the current solution, using `is_constraint_satisfied(c)`, and can potentially the current solution, using `is_constraint_satisfied(cobj)`, and can potentially
be re-added to the model using `add_constraint(c)`. be re-added to the model using `add_constraint(cobj)`.
""" """
pass pass
def is_constraint_satisfied(self, c): @abstractmethod
def is_constraint_satisfied(self, cobj):
pass pass
@abstractmethod @abstractmethod

@ -9,7 +9,7 @@ import pyomo
from abc import abstractmethod from abc import abstractmethod
from io import StringIO from io import StringIO
from pyomo import environ as pe from pyomo import environ as pe
from pyomo.core import Var from pyomo.core import Var, Constraint
from .. import RedirectOutput from .. import RedirectOutput
from ..internal import InternalSolver from ..internal import InternalSolver
@ -32,6 +32,7 @@ class BasePyomoSolver(InternalSolver):
self._pyomo_solver = None self._pyomo_solver = None
self._obj_sense = None self._obj_sense = None
self._varname_to_var = {} self._varname_to_var = {}
self._cname_to_constr = {}
def solve_lp(self, tee=False): def solve_lp(self, tee=False):
for var in self._bin_vars: for var in self._bin_vars:
@ -93,23 +94,31 @@ class BasePyomoSolver(InternalSolver):
self.instance = instance self.instance = instance
self.model = model self.model = model
self._pyomo_solver.set_instance(model) self._pyomo_solver.set_instance(model)
self._update_obj()
self._update_vars()
self._update_constrs()
# Update objective sense def _update_obj(self):
self._obj_sense = "max" self._obj_sense = "max"
if self._pyomo_solver._objective.sense == pyomo.core.kernel.objective.minimize: if self._pyomo_solver._objective.sense == pyomo.core.kernel.objective.minimize:
self._obj_sense = "min" self._obj_sense = "min"
# Update variables def _update_vars(self):
self._all_vars = [] self._all_vars = []
self._bin_vars = [] self._bin_vars = []
self._varname_to_var = {} self._varname_to_var = {}
for var in model.component_objects(Var): for var in self.model.component_objects(Var):
self._varname_to_var[var.name] = var self._varname_to_var[var.name] = var
for idx in var: for idx in var:
self._all_vars += [var[idx]] self._all_vars += [var[idx]]
if var[idx].domain == pyomo.core.base.set_types.Binary: if var[idx].domain == pyomo.core.base.set_types.Binary:
self._bin_vars += [var[idx]] self._bin_vars += [var[idx]]
def _update_constrs(self):
self._cname_to_constr = {}
for constr in self.model.component_objects(Constraint):
self._cname_to_constr[constr.name] = constr
def fix(self, solution): def fix(self, solution):
count_total, count_fixed = 0, 0 count_total, count_fixed = 0, 0
for varname in solution: for varname in solution:
@ -126,12 +135,15 @@ class BasePyomoSolver(InternalSolver):
def add_constraint(self, constraint): def add_constraint(self, constraint):
self._pyomo_solver.add_constraint(constraint) self._pyomo_solver.add_constraint(constraint)
self._update_constrs()
def solve(self, tee=False): def solve(self, tee=False, iteration_cb=None):
total_wallclock_time = 0 total_wallclock_time = 0
streams = [StringIO()] streams = [StringIO()]
if tee: if tee:
streams += [sys.stdout] streams += [sys.stdout]
if iteration_cb is None:
iteration_cb = lambda: False
self.instance.found_violated_lazy_constraints = [] self.instance.found_violated_lazy_constraints = []
self.instance.found_violated_user_cuts = [] self.instance.found_violated_user_cuts = []
while True: while True:
@ -140,16 +152,9 @@ class BasePyomoSolver(InternalSolver):
results = self._pyomo_solver.solve(tee=True, results = self._pyomo_solver.solve(tee=True,
warmstart=self._is_warm_start_available) warmstart=self._is_warm_start_available)
total_wallclock_time += results["Solver"][0]["Wallclock time"] total_wallclock_time += results["Solver"][0]["Wallclock time"]
logger.debug("Finding violated constraints...") should_repeat = iteration_cb()
violations = self.instance.find_violated_lazy_constraints(self.model) if not should_repeat:
if len(violations) == 0:
break 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() log = streams[0].getvalue()
return { return {
"Lower bound": results["Problem"][0]["Lower bound"], "Lower bound": results["Problem"][0]["Lower bound"],
@ -198,6 +203,15 @@ class BasePyomoSolver(InternalSolver):
key = self._get_gap_tolerance_option_name() key = self._get_gap_tolerance_option_name()
self._pyomo_solver.options[key] = gap_tolerance self._pyomo_solver.options[key] = gap_tolerance
def get_constraints_ids(self):
return list(self._cname_to_constr.keys())
def extract_constraint(self, cid):
raise Exception("Not implemented")
def is_constraint_satisfied(self, cobj):
raise Exception("Not implemented")
@abstractmethod @abstractmethod
def _get_warm_start_regexp(self): def _get_warm_start_regexp(self):
pass pass

@ -16,89 +16,23 @@ logger = logging.getLogger(__name__)
class GurobiPyomoSolver(BasePyomoSolver): class GurobiPyomoSolver(BasePyomoSolver):
def __init__(self, def __init__(self,
use_lazy_callbacks=True,
options=None): options=None):
""" """
Creates a new Gurobi solver, accessed through Pyomo. Creates a new Gurobi solver, accessed through Pyomo.
Parameters 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 options: dict
Dictionary of options to pass to the Pyomo solver. For example, Dictionary of options to pass to the Pyomo solver. For example,
{"Threads": 4} to set the number of threads. {"Threads": 4} to set the number of threads.
""" """
super().__init__() super().__init__()
self._use_lazy_callbacks = use_lazy_callbacks
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() self._pyomo_solver.options["Seed"] = randint(low=0, high=1000).rvs()
if options is not None: if options is not None:
for (key, value) in options.items(): for (key, value) in options.items():
self._pyomo_solver.options[key] = value 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): def _extract_node_count(self, log):
return max(1, int(self._pyomo_solver._solver_model.getAttr("NodeCount"))) return max(1, int(self._pyomo_solver._solver_model.getAttr("NodeCount")))

@ -6,10 +6,9 @@ import logging
from io import StringIO from io import StringIO
import pyomo.environ as pe import pyomo.environ as pe
from miplearn import BasePyomoSolver
from miplearn.problems.knapsack import ChallengeA
from miplearn.solvers import RedirectOutput
from miplearn import BasePyomoSolver, GurobiSolver
from miplearn.solvers import RedirectOutput
from . import _get_instance, _get_internal_solvers from . import _get_instance, _get_internal_solvers
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -99,17 +98,59 @@ def test_internal_solver():
assert solution["x"][2] == 1.0 assert solution["x"][2] == 1.0
assert solution["x"][3] == 1.0 assert solution["x"][3] == 1.0
# Add a brand new constraint
if isinstance(solver, BasePyomoSolver): if isinstance(solver, BasePyomoSolver):
model.cut = pe.Constraint(expr=model.x[0] <= 0.5) model.cut = pe.Constraint(expr=model.x[0] <= 0.0, name="cut")
solver.add_constraint(model.cut) solver.add_constraint(model.cut)
solver.solve_lp() elif isinstance(solver, GurobiSolver):
assert model.x[0].value == 0.5 x = model.getVarByName("x[0]")
solver.add_constraint(x <= 0.0, name="cut")
else:
raise Exception("Illegal state")
# New constraint should affect solution and should be listed in
# constraint ids
assert solver.get_constraints_ids() == ["eq_capacity", "cut"]
stats = solver.solve()
assert stats["Lower bound"] == 1030.0
if isinstance(solver, GurobiSolver):
# Extract new constraint
cobj = solver.extract_constraint("cut")
# New constraint should no longer affect solution and should no longer
# be listed in constraint ids
assert solver.get_constraints_ids() == ["eq_capacity"]
stats = solver.solve()
assert stats["Lower bound"] == 1183.0
# New constraint should not be satisfied by current solution
assert not solver.is_constraint_satisfied(cobj)
# Re-add constraint
solver.add_constraint(cobj)
# Constraint should affect solution again
assert solver.get_constraints_ids() == ["eq_capacity", "cut"]
stats = solver.solve()
assert stats["Lower bound"] == 1030.0
# New constraint should now be satisfied
assert solver.is_constraint_satisfied(cobj)
def test_iteration_cb():
for solver_class in _get_internal_solvers():
logger.info("Solver: %s" % solver_class)
instance = _get_instance(solver_class)
solver = solver_class()
solver.set_instance(instance)
count = 0
def custom_iteration_cb():
nonlocal count
count += 1
return count < 5
# def test_node_count(): solver.solve(iteration_cb=custom_iteration_cb)
# for solver in _get_internal_solvers(): assert count == 5
# challenge = ChallengeA()
# solver.set_time_limit(1)
# solver.set_instance(challenge.test_instances[0])
# stats = solver.solve(tee=True)
# assert stats["Nodes"] > 1

Loading…
Cancel
Save