From b81815d35bb513ae81134660eb9c75262b77fd46 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Fri, 27 Oct 2023 10:34:33 -0500 Subject: [PATCH] Lazy: Minor fixes; make it compatible with Pyomo --- miplearn/collectors/basic.py | 9 +++--- miplearn/solvers/abstract.py | 8 ++++- miplearn/solvers/gurobi.py | 46 ++++++++++++++++------------ miplearn/solvers/pyomo.py | 37 ++++++++++++++++++++-- tests/fixtures/tsp-n20-00000.h5 | Bin 65915 -> 50503 bytes tests/fixtures/tsp-n20-00000.mps.gz | Bin 6238 -> 0 bytes tests/fixtures/tsp-n20-00000.pkl.gz | Bin 1145 -> 1145 bytes tests/fixtures/tsp-n20-00001.h5 | Bin 30392 -> 40852 bytes tests/fixtures/tsp-n20-00001.mps.gz | Bin 5371 -> 5371 bytes tests/fixtures/tsp-n20-00001.pkl.gz | Bin 1134 -> 1134 bytes tests/fixtures/tsp-n20-00002.h5 | Bin 31312 -> 42772 bytes tests/fixtures/tsp-n20-00002.mps.gz | Bin 5653 -> 5653 bytes tests/fixtures/tsp-n20-00002.pkl.gz | Bin 1134 -> 1134 bytes tests/test_lazy_pyomo.py | 44 ++++++++++++++++++++++++++ 14 files changed, 116 insertions(+), 28 deletions(-) delete mode 100644 tests/fixtures/tsp-n20-00000.mps.gz create mode 100644 tests/test_lazy_pyomo.py diff --git a/miplearn/collectors/basic.py b/miplearn/collectors/basic.py index 0bec085..d7e190e 100644 --- a/miplearn/collectors/basic.py +++ b/miplearn/collectors/basic.py @@ -4,6 +4,8 @@ import json import os +import sys + from io import StringIO from os.path import exists from typing import Callable, List @@ -57,11 +59,8 @@ class BasicCollector: model.extract_after_mip(h5) # Add lazy constraints to model - if ( - hasattr(model, "lazy_enforce") - and model.lazy_enforce is not None - ): - model.lazy_enforce(model, model.lazy_constrs_, "aot") + if model.lazy_enforce is not None: + model.lazy_enforce(model, model.lazy_constrs_) h5.put_scalar("mip_lazy", repr(model.lazy_constrs_)) # Save MPS file diff --git a/miplearn/solvers/abstract.py b/miplearn/solvers/abstract.py index cf985f7..986716e 100644 --- a/miplearn/solvers/abstract.py +++ b/miplearn/solvers/abstract.py @@ -3,7 +3,7 @@ # Released under the modified BSD license. See COPYING.md for more details. from abc import ABC, abstractmethod -from typing import Optional, Dict, Callable +from typing import Optional, Dict, Callable, Hashable, List, Any import numpy as np @@ -16,9 +16,15 @@ class AbstractModel(ABC): _supports_node_count = False _supports_solution_pool = False + WHERE_DEFAULT = "default" + WHERE_CUTS = "cuts" + WHERE_LAZY = "lazy" + def __init__(self) -> None: self.lazy_enforce: Optional[Callable] = None self.lazy_separate: Optional[Callable] = None + self.lazy_constrs_: Optional[List[Any]] = None + self.where = self.WHERE_DEFAULT @abstractmethod def add_constrs( diff --git a/miplearn/solvers/gurobi.py b/miplearn/solvers/gurobi.py index 63917d9..31dd1d9 100644 --- a/miplearn/solvers/gurobi.py +++ b/miplearn/solvers/gurobi.py @@ -12,6 +12,27 @@ from miplearn.h5 import H5File from miplearn.solvers.abstract import AbstractModel +def _gurobi_callback(model: AbstractModel, where: int) -> None: + assert model.lazy_separate is not None + assert model.lazy_enforce is not None + assert model.lazy_constrs_ is not None + if where == GRB.Callback.MIPSOL: + model.where = model.WHERE_LAZY + violations = model.lazy_separate(model) + model.lazy_constrs_.extend(violations) + model.lazy_enforce(model, violations) + model.where = model.WHERE_DEFAULT + + +def _gurobi_add_constr(gp_model: gp.Model, where: str, constr: Any) -> None: + if where == AbstractModel.WHERE_LAZY: + gp_model.cbLazy(constr) + elif where == AbstractModel.WHERE_CUTS: + gp_model.cbCut(constr) + else: + gp_model.addConstr(constr) + + class GurobiModel(AbstractModel): _supports_basis_status = True _supports_sensitivity_analysis = True @@ -24,11 +45,10 @@ class GurobiModel(AbstractModel): lazy_separate: Optional[Callable] = None, lazy_enforce: Optional[Callable] = None, ) -> None: + super().__init__() self.lazy_separate = lazy_separate self.lazy_enforce = lazy_enforce self.inner = inner - self.lazy_constrs_: Optional[List[Any]] = None - self.where = "default" def add_constrs( self, @@ -55,12 +75,7 @@ class GurobiModel(AbstractModel): stats["Added constraints"] += nconstrs def add_constr(self, constr: Any) -> None: - if self.where == "lazy": - self.inner.cbLazy(constr) - elif self.where == "cut": - self.inner.cbCut(constr) - else: - self.inner.addConstr(constr) + _gurobi_add_constr(self.inner, self.where, constr) def extract_after_load(self, h5: H5File) -> None: """ @@ -136,19 +151,12 @@ class GurobiModel(AbstractModel): def optimize(self) -> None: self.lazy_constrs_ = [] - def callback(m: gp.Model, where: int) -> None: - assert self.lazy_separate is not None - assert self.lazy_constrs_ is not None - assert self.lazy_enforce is not None - if where == GRB.Callback.MIPSOL: - self.where = "lazy" - violations = self.lazy_separate(self) - self.lazy_constrs_.extend(violations) - self.lazy_enforce(self, violations) - self.where = "default" + def callback(_: gp.Model, where: int) -> None: + _gurobi_callback(self, where) if self.lazy_enforce is not None: - self.inner.Params.lazyConstraints = 1 + self.inner.setParam("PreCrush", 1) + self.inner.setParam("LazyConstraints", 1) self.inner.optimize(callback) else: self.inner.optimize() diff --git a/miplearn/solvers/pyomo.py b/miplearn/solvers/pyomo.py index fd6b91a..60d2a11 100644 --- a/miplearn/solvers/pyomo.py +++ b/miplearn/solvers/pyomo.py @@ -2,10 +2,11 @@ # Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved. # Released under the modified BSD license. See COPYING.md for more details. from numbers import Number -from typing import Optional, Dict, List, Any, Tuple, Union +from typing import Optional, Dict, List, Any, Tuple, Callable import numpy as np import pyomo +import pyomo.environ as pe from pyomo.core import Objective, Var, Suffix from pyomo.core.base import _GeneralVarData from pyomo.core.expr.numeric_expr import SumExpression, MonomialTermExpression @@ -13,11 +14,18 @@ from scipy.sparse import coo_matrix from miplearn.h5 import H5File from miplearn.solvers.abstract import AbstractModel -import pyomo.environ as pe +from miplearn.solvers.gurobi import _gurobi_callback, _gurobi_add_constr class PyomoModel(AbstractModel): - def __init__(self, model: pe.ConcreteModel, solver_name: str = "gurobi_persistent"): + def __init__( + self, + model: pe.ConcreteModel, + solver_name: str = "gurobi_persistent", + lazy_separate: Optional[Callable] = None, + lazy_enforce: Optional[Callable] = None, + ): + super().__init__() self.inner = model self.solver_name = solver_name self.solver = pe.SolverFactory(solver_name) @@ -26,11 +34,20 @@ class PyomoModel(AbstractModel): self.solver.set_instance(model) self.results: Optional[Dict] = None self._is_warm_start_available = False + self.lazy_separate = lazy_separate + self.lazy_enforce = lazy_enforce + self.lazy_constrs_: Optional[List[Any]] = None if not hasattr(self.inner, "dual"): self.inner.dual = Suffix(direction=Suffix.IMPORT) self.inner.rc = Suffix(direction=Suffix.IMPORT) self.inner.slack = Suffix(direction=Suffix.IMPORT) + def add_constr(self, constr: Any) -> None: + assert ( + self.solver_name == "gurobi_persistent" + ), "Callbacks are currently only supported on gurobi_persistent" + _gurobi_add_constr(self.solver, self.where, constr) + def add_constrs( self, var_names: np.ndarray, @@ -114,6 +131,20 @@ class PyomoModel(AbstractModel): self.solver.update_var(var) def optimize(self) -> None: + self.lazy_constrs_ = [] + + if self.lazy_separate is not None: + assert ( + self.solver_name == "gurobi_persistent" + ), "Callbacks are currently only supported on gurobi_persistent" + + def callback(_: Any, __: Any, where: int) -> None: + _gurobi_callback(self, where) + + self.solver.set_gurobi_param("PreCrush", 1) + self.solver.set_gurobi_param("LazyConstraints", 1) + self.solver.set_callback(callback) + if self.is_persistent: self.results = self.solver.solve( tee=True, diff --git a/tests/fixtures/tsp-n20-00000.h5 b/tests/fixtures/tsp-n20-00000.h5 index 159a981087d348fc8259e08f4afc26ce47b1bf96..40929bd4008de161a85bac45f6e85338263ecf09 100644 GIT binary patch delta 1510 zcmey}#B#igd4i0F`%wlkFn~}DrrWIe{XJZQm=xySvQC{2#1ML-OSMG936?crIYp2d z0#81mCp%eyiGAYl2NHiXHYDOv^IT%D8S!S)9d$4`BhwGN2`4 zlh?DXV|g+$l5uh(s~0wTeztWi{FlFJZa&Dy&B63!%jR=@7L4c$4FqK;8wxy^&~twt zh|MLN4Fp*jB_?$iJi{hC8KgZ^=()s+zO1cS75otkn7l)H3ybD~32!$Sh}1EmIbic2 zF`)VP(`Q%W&@MZxSBeI$fTVRu&9H3<|xyI#+#JwtoyEqlhaJ?cSRW5-& zEI}r0al0b%?7_t&SWQ^qp2YOz$YckD1rqnz{LWxgvw4pP(2+upH<_`?f*r}>bwy&g zOvf&q3RJwWNN9M@=EJ67vw#oK{GQC9aICUAzCc;Cj^GTevL1dw*#%NcyCJfm;u3); zgEHt8|0^s%F1{_@oD(ojlMf*YQn*>5iiMHI=l1@uizTYrCSOo~BIWMv@52a8!Y_bn z83aIV5IAJCIZtX9vv5FBs&i3kafU)kzCu}IPHC!w;pU&R8(25@1Y~Klb3qhC3j=Eb E0BkvPsQ>@~ delta 1529 zcmX@!#r(U8WrB=GH6tSf2pE7#1_rT&%v^sDmmnsExTDsor3b82A@oF-Y6+fz_4~nc ziXbrro_s)0cCr8y`^4W5Bo00dh{U7jxx}N(f*v>&giTImUdK|g{&B@*aTXIkgashW zfR=1)J<-koHWW=Mw(+uP?!>;E!0q9WSdQ{b7jG^QsbfNO zz~(<Z+$`2Ki+S^<)`hIRP%#iSfo*f4R2!QJHq$l-XaODde8;U~Y_iZ8)}6dfdzVC1 z*K!UViozz}(^)6MCVJ&KP6fYo*GW_^JD!eB!R7;cKu7#Gb9#YI7Muc%4Ax03TcXs3 zQ-Po1I+k~e+7+9OZnAP|dE0|Mer)UJ$L73@yr!HC3^r1bAYlRqhCL&Wz_0?kWBSxR zoH(sHWW7!zf={vyr-FMn>m>eMJhBO^0!SLNoczmnoy2sXOsw&{`G6hJ5truPwZ~yZ z*knIPp2>C&izJTnGd{+nXq`lU^!Zsh6=XTBlei_^P=ZatW&>xSBW6sl&c-G?IY7%` za*fLsiONphLYxX_xL%Qvb=`bnOF_k zyvGCR$YZbnZNX{+$O;axD-!?qzdVdnfr|GPi78swv8Mjb0zN?V)#h$}f=&Bo9bceq zl`%49de({I9U+eUSUMIVWJ6CLcl)q;Ru96$>NF^}7|9 c7E4sKP5#r!vH8uGXo1BxQUXxU4}Ovu0Oj>odjJ3c diff --git a/tests/fixtures/tsp-n20-00000.mps.gz b/tests/fixtures/tsp-n20-00000.mps.gz deleted file mode 100644 index 4699bf6cedfec2e218135c4ec4d8651ab6748dbb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6238 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 BYJ>m) delta 1467 zcmbQTpJ~Ti#tAYSJIWZqzyLxqNPXP$$KS&xh)JRJfORSYPjso4IM#9S1z1iIAr4YA z`GB76WC14jiN7C6RGi~{f=A7BiE@`FT!w^APGw%la{uy-=*i+NCVUu{giT)0vX15G zV|DJyiL74OYC8YNx^J8=3W&=SMMu|j^3x}}D zP6lbu6nZXk@pG0vRt0~=0w(Ve-oo;Wx3#G)^MX(vSIY0~Os`iTeQP^amQL8(7oAxe=wF_Tf$Dt@}GCv#7QtNx{0!Hz{8jgJ-DK3u zD*VC79_0ImfJ`vW!0^F-vZ6)8B#T+ROE?%9Y@{GT!~_hRsf;+n#tP`9?o{91IBh&+ zy-tG7Zf7b^1@~;$Nz`WUxPw&zBu(i~erLN&g4u5BJZy?KAFu;DVn55b&Ddmt$uexR zk;6KPE=$>~I2HIgu9Jv5?OKRaL6*}x3CVS(ir5rvHgEZZ%VDesbGuS6^ZbLUrMnl*u20U=zu58`oFQsf{h(R>v18o20Y*B34-s vKcH+7bD1DE*~y>`J;nbD%R!CrD>vr^n8~9T92@dk7+GXIS1K*uQNRWOmo_(L diff --git a/tests/fixtures/tsp-n20-00001.mps.gz b/tests/fixtures/tsp-n20-00001.mps.gz index 907d7cd0095c1a615258c0091274f97fc189243e..7f8ad968d699967df65de09723f2394d18dd5a7e 100644 GIT binary patch delta 15 WcmeyZ`CF4szMF$%`mK#@Uqt{jAO<=B delta 15 WcmeyZ`CF4szMF%i^uR{8uOa|3(gpqi diff --git a/tests/fixtures/tsp-n20-00001.pkl.gz b/tests/fixtures/tsp-n20-00001.pkl.gz index 9286666df2896903a453c2bccca2f2bba72dee9b..46b37702f67283ae0b6bb64ad5950534072fc252 100644 GIT binary patch delta 15 WcmaFI@s5K{zMF$%`mK#@87u%TFa<#X delta 15 WcmaFI@s5K{zMF%i^uR{83>E+@;spf& diff --git a/tests/fixtures/tsp-n20-00002.h5 b/tests/fixtures/tsp-n20-00002.h5 index 7f209f240dc538b175047ae94c6cade08cff3105..84c534ce8255e3012d33772dd2b8fbf213df3b8a 100644 GIT binary patch delta 1511 zcmcccg>lL`rU^0{BFh=TzyLxq+?APd$lt>yh)H4kE$dVSp6F67;reOYT(F!XLL8)K z@&P^B$pTF56MsLD*wcT`5RaPY672<@hjAzfo1DtLj>T@Oi_~Os785=UOTs3vXIaOx zQ6S{YsTfTtM_d_$mYt4uE0Q0cCw+sbBUTcJ(k#9u-QP6g;C;B?5zjb zWG92PX9_)+=ufx1fmOjDv4F`tgtxE+^1Rg9Tp&`%gyw+Ff5d?1|L5XZhC{pT16Lc z*c5C&pa*nB$NO^ZUOb=|HrdEvorJ!8@N#V0fvyaj>}R-+W!a-IGd39=W#!uEWDgF8 zk2#yqn=>-N6ZR~WWpv3OnAV+a9Agi8oIm;rvg96brQ_h ziFr5`WI3&q*k8N#C^iL~4V-}vXkL75KQ`IP0a^x=Yh12K{A4b2z^Pz{>lKNTIXzEt zD%j$7MPlajuL;-`Y+m3FbikKT2On&*;1I}i;+ZVxxk$n@-~J$0MIcvlcwLcDymYl4 zrver4D-!)ruTI6LV6%V^&=IN^*>teV>i7a>g_zxbVU_jp1IqF`S-iw1I~kN^r}$rC z*^;wMcXLj_IVJR>!K0XkktMB)|H9&c61K?=%1@-+o&9|nf$8@FFd>5ghz$a}v^VET pg)j>T6s0;Bl@@0xl;kUvCFYc-Dj06wEW3eqb5FnuC3a|;008=sZi@f_ delta 1449 zcmbPoj_JY|#tAYS0aXlOU;v>QHmOvc_V;iJVp1qQV4aG<6J4q$j&&S-0hUulh=bHj zKAMX+`l{{da^i+2_J?fVUyRhtYbO) zSe<)vBC8iRd49HaEHYkIS2iDHbLB)=U?3e zlR?@ug`P_|tw_6rRly&zfXO?Ax3J9goY}LvK%|Zd%>kSLhyl&lQ?&VmL%ZzcG8Vnb z_Ts#g3nbP^tmG2N#G+uQlZ`0l-@mS~JS{#bPCg0OpC$S-Q%Uzrb ze(A20&`41^hE2id1A0J5a4N0bicJ>ih_J~<2J0kdX2~VtRN!a0j>X~QfybMSjE+@;spf& diff --git a/tests/test_lazy_pyomo.py b/tests/test_lazy_pyomo.py new file mode 100644 index 0000000..a96f32e --- /dev/null +++ b/tests/test_lazy_pyomo.py @@ -0,0 +1,44 @@ +# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization +# Copyright (C) 2020-2023, UChicago Argonne, LLC. All rights reserved. +# Released under the modified BSD license. See COPYING.md for more details. +import logging +from typing import Any, Hashable, List + +import pyomo.environ as pe + +from miplearn.solvers.pyomo import PyomoModel + +logger = logging.getLogger(__name__) + + +def _build_model() -> PyomoModel: + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(0, 5), domain=pe.Integers) + m.obj = pe.Objective(expr=-m.x) + m.cons = pe.ConstraintList() + + def lazy_separate(model: PyomoModel) -> List[Hashable]: + model.solver.cbGetSolution(vars=[m.x]) + if m.x.value > 0.5: + return [m.x.value] + else: + return [] + + def lazy_enforce(model: PyomoModel, violations: List[Any]) -> None: + for v in violations: + model.add_constr(m.cons.add(m.x <= round(v - 1))) + + return PyomoModel( + m, + "gurobi_persistent", + lazy_separate=lazy_separate, + lazy_enforce=lazy_enforce, + ) + + +def test_pyomo_callback() -> None: + model = _build_model() + model.optimize() + assert model.lazy_constrs_ is not None + assert len(model.lazy_constrs_) > 0 + assert model.inner.x.value == 0.0