From 60d9a68485b8ed0e5ae38bfc94943712fc900be4 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Fri, 2 Feb 2024 10:15:06 -0600 Subject: [PATCH] Solver: Make attributes private; ensure we're not calling them directly Helps with Julia/JuMP integration. --- miplearn/collectors/basic.py | 3 +- miplearn/components/lazy/mem.py | 4 +-- miplearn/solvers/abstract.py | 26 +++++++++----- miplearn/solvers/gurobi.py | 62 ++++++++++++++++----------------- miplearn/solvers/pyomo.py | 26 +++++++------- tests/test_lazy_pyomo.py | 4 +-- 6 files changed, 66 insertions(+), 59 deletions(-) diff --git a/miplearn/collectors/basic.py b/miplearn/collectors/basic.py index 4c6010d..697fbb3 100644 --- a/miplearn/collectors/basic.py +++ b/miplearn/collectors/basic.py @@ -68,8 +68,7 @@ class BasicCollector: if self.write_mps: # Add lazy constraints to model - if model.lazy_enforce is not None: - model.lazy_enforce(model, model.lazy_) + model._lazy_enforce_collected() # Save MPS file model.write(mps_filename) diff --git a/miplearn/components/lazy/mem.py b/miplearn/components/lazy/mem.py index f054de4..06ac005 100644 --- a/miplearn/components/lazy/mem.py +++ b/miplearn/components/lazy/mem.py @@ -24,10 +24,8 @@ class MemorizingLazyComponent(_BaseMemorizingConstrComponent): model: AbstractModel, stats: Dict[str, Any], ) -> None: - if model.lazy_enforce is None: - return assert self.constrs_ is not None violations = self.predict("Predicting violated lazy constraints...", test_h5) logger.info(f"Enforcing {len(violations)} constraints ahead-of-time...") - model.lazy_enforce(model, violations) + model.lazy_enforce(violations) stats["Lazy Constraints: AOT"] = len(violations) diff --git a/miplearn/solvers/abstract.py b/miplearn/solvers/abstract.py index 6341c85..c6eb313 100644 --- a/miplearn/solvers/abstract.py +++ b/miplearn/solvers/abstract.py @@ -21,14 +21,14 @@ class AbstractModel(ABC): WHERE_LAZY = "lazy" def __init__(self) -> None: - self.lazy_enforce: Optional[Callable] = None - self.lazy_separate: Optional[Callable] = None - self.lazy_: Optional[List[Any]] = None - self.cuts_enforce: Optional[Callable] = None - self.cuts_separate: Optional[Callable] = None - self.cuts_: Optional[List[Any]] = None - self.cuts_aot_: Optional[List[Any]] = None - self.where = self.WHERE_DEFAULT + self._lazy_enforce: Optional[Callable] = None + self._lazy_separate: Optional[Callable] = None + self._lazy: Optional[List[Any]] = None + self._cuts_enforce: Optional[Callable] = None + self._cuts_separate: Optional[Callable] = None + self._cuts: Optional[List[Any]] = None + self._cuts_aot: Optional[List[Any]] = None + self._where = self.WHERE_DEFAULT @abstractmethod def add_constrs( @@ -85,3 +85,13 @@ class AbstractModel(ABC): def set_cuts(self, cuts: List) -> None: self.cuts_aot_ = cuts + + def lazy_enforce(self, violations: List[Any]) -> None: + if self._lazy_enforce is not None: + self._lazy_enforce(self, violations) + + def _lazy_enforce_collected(self) -> None: + """Adds all lazy constraints identified in the callback as actual model constraints. Useful for generating + a final MPS file with the constraints that were required in this run.""" + if self._lazy_enforce is not None: + self._lazy_enforce(self, self._lazy) diff --git a/miplearn/solvers/gurobi.py b/miplearn/solvers/gurobi.py index 18634b3..021c043 100644 --- a/miplearn/solvers/gurobi.py +++ b/miplearn/solvers/gurobi.py @@ -21,36 +21,36 @@ def _gurobi_callback(model: AbstractModel, gp_model: gp.Model, where: int) -> No assert isinstance(gp_model, gp.Model) # Lazy constraints - if model.lazy_separate is not None: - assert model.lazy_enforce is not None - assert model.lazy_ is not None + if model._lazy_separate is not None: + assert model._lazy_enforce is not None + assert model._lazy is not None if where == GRB.Callback.MIPSOL: - model.where = model.WHERE_LAZY - violations = model.lazy_separate(model) + model._where = model.WHERE_LAZY + violations = model._lazy_separate(model) if len(violations) > 0: - model.lazy_.extend(violations) - model.lazy_enforce(model, violations) + model._lazy.extend(violations) + model._lazy_enforce(model, violations) # User cuts - if model.cuts_separate is not None: - assert model.cuts_enforce is not None - assert model.cuts_ is not None + if model._cuts_separate is not None: + assert model._cuts_enforce is not None + assert model._cuts is not None if where == GRB.Callback.MIPNODE: status = gp_model.cbGet(GRB.Callback.MIPNODE_STATUS) if status == GRB.OPTIMAL: - model.where = model.WHERE_CUTS - if model.cuts_aot_ is not None: - violations = model.cuts_aot_ - model.cuts_aot_ = None + model._where = model.WHERE_CUTS + if model._cuts_aot is not None: + violations = model._cuts_aot + model._cuts_aot = None logger.info(f"Enforcing {len(violations)} cuts ahead-of-time...") else: - violations = model.cuts_separate(model) + violations = model._cuts_separate(model) if len(violations) > 0: - model.cuts_.extend(violations) - model.cuts_enforce(model, violations) + model._cuts.extend(violations) + model._cuts_enforce(model, violations) # Cleanup - model.where = model.WHERE_DEFAULT + model._where = model.WHERE_DEFAULT def _gurobi_add_constr(gp_model: gp.Model, where: str, constr: Any) -> None: @@ -64,11 +64,11 @@ def _gurobi_add_constr(gp_model: gp.Model, where: str, constr: Any) -> None: def _gurobi_set_required_params(model: AbstractModel, gp_model: gp.Model) -> None: # Required parameters for lazy constraints - if model.lazy_enforce is not None: + if model._lazy_enforce is not None: gp_model.setParam("PreCrush", 1) gp_model.setParam("LazyConstraints", 1) # Required parameters for user cuts - if model.cuts_enforce is not None: + if model._cuts_enforce is not None: gp_model.setParam("PreCrush", 1) @@ -87,10 +87,10 @@ class GurobiModel(AbstractModel): cuts_enforce: Optional[Callable] = None, ) -> None: super().__init__() - self.lazy_separate = lazy_separate - self.lazy_enforce = lazy_enforce - self.cuts_separate = cuts_separate - self.cuts_enforce = cuts_enforce + self._lazy_separate = lazy_separate + self._lazy_enforce = lazy_enforce + self._cuts_separate = cuts_separate + self._cuts_enforce = cuts_enforce self.inner = inner def add_constrs( @@ -118,7 +118,7 @@ class GurobiModel(AbstractModel): stats["Added constraints"] += nconstrs def add_constr(self, constr: Any) -> None: - _gurobi_add_constr(self.inner, self.where, constr) + _gurobi_add_constr(self.inner, self._where, constr) def extract_after_load(self, h5: H5File) -> None: """ @@ -168,10 +168,10 @@ class GurobiModel(AbstractModel): except AttributeError: pass self._extract_after_mip_solution_pool(h5) - if self.lazy_ is not None: - h5.put_scalar("mip_lazy", json.dumps(self.lazy_)) - if self.cuts_ is not None: - h5.put_scalar("mip_cuts", json.dumps(self.cuts_)) + if self._lazy is not None: + h5.put_scalar("mip_lazy", json.dumps(self._lazy)) + if self._cuts is not None: + h5.put_scalar("mip_cuts", json.dumps(self._cuts)) def fix_variables( self, @@ -196,8 +196,8 @@ class GurobiModel(AbstractModel): stats["Fixed variables"] = n_fixed def optimize(self) -> None: - self.lazy_ = [] - self.cuts_ = [] + self._lazy = [] + self._cuts = [] def callback(_: gp.Model, where: int) -> None: _gurobi_callback(self, self.inner, where) diff --git a/miplearn/solvers/pyomo.py b/miplearn/solvers/pyomo.py index 3493f67..55c819a 100644 --- a/miplearn/solvers/pyomo.py +++ b/miplearn/solvers/pyomo.py @@ -34,10 +34,10 @@ class PyomoModel(AbstractModel): super().__init__() self.inner = model self.solver_name = solver_name - self.lazy_separate = lazy_separate - self.lazy_enforce = lazy_enforce - self.cuts_separate = cuts_separate - self.cuts_enforce = cuts_enforce + self._lazy_separate = lazy_separate + self._lazy_enforce = lazy_enforce + self._cuts_separate = cuts_separate + self._cuts_enforce = cuts_enforce self.solver = pe.SolverFactory(solver_name) self.is_persistent = hasattr(self.solver, "set_instance") if self.is_persistent: @@ -53,8 +53,8 @@ class PyomoModel(AbstractModel): assert ( self.solver_name == "gurobi_persistent" ), "Callbacks are currently only supported on gurobi_persistent" - if self.where in [AbstractModel.WHERE_CUTS, AbstractModel.WHERE_LAZY]: - _gurobi_add_constr(self.solver, self.where, constr) + if self._where in [AbstractModel.WHERE_CUTS, AbstractModel.WHERE_LAZY]: + _gurobi_add_constr(self.solver, self._where, constr) else: # outside callbacks, add_constr shouldn't do anything, as the constraint # has already been added to the ConstraintList object @@ -129,10 +129,10 @@ class PyomoModel(AbstractModel): h5.put_scalar("mip_obj_value", obj_value) h5.put_scalar("mip_obj_bound", obj_bound) h5.put_scalar("mip_gap", self._gap(obj_value, obj_bound)) - if self.lazy_ is not None: - h5.put_scalar("mip_lazy", repr(self.lazy_)) - if self.cuts_ is not None: - h5.put_scalar("mip_cuts", repr(self.cuts_)) + if self._lazy is not None: + h5.put_scalar("mip_lazy", repr(self._lazy)) + if self._cuts is not None: + h5.put_scalar("mip_cuts", repr(self._cuts)) def fix_variables( self, @@ -147,10 +147,10 @@ class PyomoModel(AbstractModel): self.solver.update_var(var) def optimize(self) -> None: - self.lazy_ = [] - self.cuts_ = [] + self._lazy = [] + self._cuts = [] - if self.lazy_enforce is not None or self.cuts_enforce is not None: + if self._lazy_enforce is not None or self._cuts_enforce is not None: assert ( self.solver_name == "gurobi_persistent" ), "Callbacks are currently only supported on gurobi_persistent" diff --git a/tests/test_lazy_pyomo.py b/tests/test_lazy_pyomo.py index 27d3de2..55eae5e 100644 --- a/tests/test_lazy_pyomo.py +++ b/tests/test_lazy_pyomo.py @@ -39,6 +39,6 @@ def _build_model() -> PyomoModel: def test_pyomo_callback() -> None: model = _build_model() model.optimize() - assert model.lazy_ is not None - assert len(model.lazy_) > 0 + assert model._lazy is not None + assert len(model._lazy) > 0 assert model.inner.x.value == 0.0