From ba96338d2d10a1d15c2e37da415e44411e226e67 Mon Sep 17 00:00:00 2001 From: Alinson S Xavier Date: Thu, 24 Sep 2020 14:30:29 -0500 Subject: [PATCH] Small fixes to lazy constraints --- miplearn/components/lazy_static.py | 37 +++++++++++++------ miplearn/components/tests/test_lazy_static.py | 4 +- miplearn/solvers/gurobi.py | 14 ++++--- miplearn/solvers/internal.py | 2 +- miplearn/solvers/learning.py | 32 ++++++++-------- miplearn/solvers/pyomo/base.py | 2 +- .../solvers/tests/test_internal_solver.py | 6 +-- 7 files changed, 58 insertions(+), 39 deletions(-) diff --git a/miplearn/components/lazy_static.py b/miplearn/components/lazy_static.py index 39004dd..289eb2d 100644 --- a/miplearn/components/lazy_static.py +++ b/miplearn/components/lazy_static.py @@ -28,6 +28,7 @@ class StaticLazyConstraintsComponent(Component): self.pool = [] def before_solve(self, solver, instance, model): + self.pool = [] instance.found_violated_lazy_constraints = [] if instance.has_static_lazy_constraints(): self._extract_and_predict_static(solver, instance) @@ -36,21 +37,28 @@ class StaticLazyConstraintsComponent(Component): pass def after_iteration(self, solver, instance, model): - logger.debug("Finding violated (static) lazy constraints...") - n_added = 0 + logger.info("Finding violated lazy constraints...") + constraints_to_add = [] for c in self.pool: if not solver.internal_solver.is_constraint_satisfied(c.obj): - self.pool.remove(c) - solver.internal_solver.add_constraint(c.obj) - instance.found_violated_lazy_constraints += [c.cid] - n_added += 1 - if n_added > 0: - logger.debug(" %d violations found" % n_added) + constraints_to_add.append(c) + for c in constraints_to_add: + self.pool.remove(c) + solver.internal_solver.add_constraint(c.obj) + instance.found_violated_lazy_constraints += [c.cid] + if len(constraints_to_add) > 0: + logger.info("Added %d lazy constraints back into the model" % len(constraints_to_add)) + logger.info("Lazy constraint pool has %d constraints" % len(self.pool)) return True else: + logger.info("Found no violated lazy constraints") return False def fit(self, training_instances): + training_instances = [t + for t in training_instances + if hasattr(t, "found_violated_lazy_constraints")] + logger.debug("Extracting x and y...") x = self.x(training_instances) y = self.y(training_instances) @@ -72,11 +80,10 @@ class StaticLazyConstraintsComponent(Component): def _extract_and_predict_static(self, solver, instance): x = {} constraints = {} - for cid in solver.internal_solver.get_constraint_names(): + logger.info("Extracting lazy constraints...") + for cid in solver.internal_solver.get_constraint_ids(): if instance.is_constraint_lazy(cid): category = instance.get_lazy_constraint_category(cid) - if category not in self.classifiers: - continue if category not in x: x[category] = [] constraints[category] = [] @@ -85,16 +92,24 @@ class StaticLazyConstraintsComponent(Component): obj=solver.internal_solver.extract_constraint(cid)) constraints[category] += [c] self.pool.append(c) + logger.info("Extracted %d lazy constraints" % len(self.pool)) + logger.info("Predicting required lazy constraints...") + n_added = 0 for (category, x_values) in x.items(): + if category not in self.classifiers: + continue if isinstance(x_values[0], np.ndarray): x[category] = np.array(x_values) proba = self.classifiers[category].predict_proba(x[category]) for i in range(len(proba)): if proba[i][1] > self.threshold: + n_added += 1 c = constraints[category][i] self.pool.remove(c) solver.internal_solver.add_constraint(c.obj) instance.found_violated_lazy_constraints += [c.cid] + logger.info("Added %d lazy constraints back into the model" % n_added) + logger.info("Lazy constraint pool has %d constraints" % len(self.pool)) def _collect_constraints(self, train_instances): constraints = {} diff --git a/miplearn/components/tests/test_lazy_static.py b/miplearn/components/tests/test_lazy_static.py index 34e52f7..c4b651d 100644 --- a/miplearn/components/tests/test_lazy_static.py +++ b/miplearn/components/tests/test_lazy_static.py @@ -14,7 +14,7 @@ from miplearn.classifiers import Classifier def test_usage_with_solver(): solver = Mock(spec=LearningSolver) internal = solver.internal_solver = Mock(spec=InternalSolver) - internal.get_constraint_names = 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.is_constraint_satisfied = Mock(return_value=False) @@ -59,7 +59,7 @@ def test_usage_with_solver(): instance.has_static_lazy_constraints.assert_called_once() # Should ask internal solver for a list of constraints in the model - internal.get_constraint_names.assert_called_once() + internal.get_constraint_ids.assert_called_once() # Should ask if each constraint in the model is lazy instance.is_constraint_lazy.assert_has_calls([ diff --git a/miplearn/solvers/gurobi.py b/miplearn/solvers/gurobi.py index b47c789..0587e0f 100644 --- a/miplearn/solvers/gurobi.py +++ b/miplearn/solvers/gurobi.py @@ -15,10 +15,11 @@ logger = logging.getLogger(__name__) class GurobiSolver(InternalSolver): def __init__(self, params=None): if params is None: - params = { - "LazyConstraints": 1, - "PreCrush": 1, - } + params = {} + # params = { + # "LazyConstraints": 1, + # "PreCrush": 1, + # } from gurobipy import GRB self.GRB = GRB self.instance = None @@ -83,6 +84,7 @@ class GurobiSolver(InternalSolver): } def solve(self, tee=False, iteration_cb=None): + self._apply_params() total_wallclock_time = 0 total_nodes = 0 streams = [StringIO()] @@ -122,7 +124,7 @@ class GurobiSolver(InternalSolver): def get_variables(self): variables = {} for (varname, vardict) in self._all_vars.items(): - variables[varname] = {} + variables[varname] = [] for (idx, var) in vardict.items(): variables[varname] += [idx] return variables @@ -161,7 +163,7 @@ class GurobiSolver(InternalSolver): var.lb = value var.ub = value - def get_constraints_ids(self): + def get_constraint_ids(self): self.model.update() return [c.ConstrName for c in self.model.getConstrs()] diff --git a/miplearn/solvers/internal.py b/miplearn/solvers/internal.py index a004166..85bad83 100644 --- a/miplearn/solvers/internal.py +++ b/miplearn/solvers/internal.py @@ -145,7 +145,7 @@ class InternalSolver(ABC): pass @abstractmethod - def get_constraints_ids(self): + def get_constraint_ids(self): """ Returns a list of ids, which uniquely identify each constraint in the model. """ diff --git a/miplearn/solvers/learning.py b/miplearn/solvers/learning.py index 1cc7919..d8ae4b4 100644 --- a/miplearn/solvers/learning.py +++ b/miplearn/solvers/learning.py @@ -40,6 +40,14 @@ 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. + + Parameters + ---------- + 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. """ def __init__(self, @@ -49,7 +57,8 @@ class LearningSolver: solver="gurobi", threads=None, time_limit=None, - node_limit=None): + node_limit=None, + solve_lp_first=True): self.components = {} self.mode = mode self.internal_solver = None @@ -59,6 +68,7 @@ class LearningSolver: self.gap_tolerance = gap_tolerance self.tee = False self.node_limit = node_limit + self.solve_lp_first = solve_lp_first if components is not None: for comp in components: @@ -84,21 +94,23 @@ class LearningSolver: else: solver = self.internal_solver_factory if self.threads is not None: + logger.info("Setting threads to %d" % self.threads) solver.set_threads(self.threads) if self.time_limit is not None: + logger.info("Setting time limit to %f" % self.time_limit) solver.set_time_limit(self.time_limit) if self.gap_tolerance is not None: + logger.info("Setting gap tolerance to %f" % self.gap_tolerance) solver.set_gap_tolerance(self.gap_tolerance) if self.node_limit is not None: + logger.info("Setting node limit to %d" % self.node_limit) solver.set_node_limit(self.node_limit) return solver def solve(self, instance, model=None, - tee=False, - relaxation_only=False, - solve_lp_first=True): + tee=False): """ Solves the given instance. If trained machine-learning models are available, they will be used to accelerate the solution process. @@ -127,13 +139,6 @@ class LearningSolver: 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 ------- @@ -155,7 +160,7 @@ class LearningSolver: self.internal_solver = self._create_internal_solver() self.internal_solver.set_instance(instance, model) - if solve_lp_first: + if self.solve_lp_first: logger.info("Solving LP relaxation...") results = self.internal_solver.solve_lp(tee=tee) instance.lp_solution = self.internal_solver.get_solution() @@ -168,9 +173,6 @@ class LearningSolver: for component in self.components.values(): component.before_solve(self, instance, model) - if relaxation_only: - return results - def iteration_cb(): should_repeat = False for component in self.components.values(): diff --git a/miplearn/solvers/pyomo/base.py b/miplearn/solvers/pyomo/base.py index 9326eaf..89bb48e 100644 --- a/miplearn/solvers/pyomo/base.py +++ b/miplearn/solvers/pyomo/base.py @@ -203,7 +203,7 @@ class BasePyomoSolver(InternalSolver): key = self._get_gap_tolerance_option_name() self._pyomo_solver.options[key] = gap_tolerance - def get_constraints_ids(self): + def get_constraint_ids(self): return list(self._cname_to_constr.keys()) def extract_constraint(self, cid): diff --git a/miplearn/solvers/tests/test_internal_solver.py b/miplearn/solvers/tests/test_internal_solver.py index ddbda58..3e811ef 100644 --- a/miplearn/solvers/tests/test_internal_solver.py +++ b/miplearn/solvers/tests/test_internal_solver.py @@ -110,7 +110,7 @@ def test_internal_solver(): # New constraint should affect solution and should be listed in # constraint ids - assert solver.get_constraints_ids() == ["eq_capacity", "cut"] + assert solver.get_constraint_ids() == ["eq_capacity", "cut"] stats = solver.solve() assert stats["Lower bound"] == 1030.0 @@ -120,7 +120,7 @@ def test_internal_solver(): # New constraint should no longer affect solution and should no longer # be listed in constraint ids - assert solver.get_constraints_ids() == ["eq_capacity"] + assert solver.get_constraint_ids() == ["eq_capacity"] stats = solver.solve() assert stats["Lower bound"] == 1183.0 @@ -131,7 +131,7 @@ def test_internal_solver(): solver.add_constraint(cobj) # Constraint should affect solution again - assert solver.get_constraints_ids() == ["eq_capacity", "cut"] + assert solver.get_constraint_ids() == ["eq_capacity", "cut"] stats = solver.solve() assert stats["Lower bound"] == 1030.0