From a26e38bfc12950d5edee4563ae09a392fc1e1dc0 Mon Sep 17 00:00:00 2001 From: Bernard Knueven Date: Mon, 12 May 2025 10:39:53 -0600 Subject: [PATCH 1/7] add automatic option for gapper --- mpisppy/cylinders/hub.py | 23 ------------- mpisppy/cylinders/spcommunicator.py | 20 ++++++++++++ mpisppy/extensions/mipgapper.py | 50 +++++++++++++++++++++++------ mpisppy/generic_cylinders.py | 22 +++---------- mpisppy/utils/cfg_vanilla.py | 18 ++++++++++- mpisppy/utils/config.py | 11 +++++++ 6 files changed, 93 insertions(+), 51 deletions(-) diff --git a/mpisppy/cylinders/hub.py b/mpisppy/cylinders/hub.py index 384069e9f..3e4905d3c 100644 --- a/mpisppy/cylinders/hub.py +++ b/mpisppy/cylinders/hub.py @@ -74,29 +74,6 @@ def clear_latest_chars(self): self.latest_ib_char = None self.latest_ob_char = None - def compute_gaps(self): - """ Compute the current absolute and relative gaps, - using the current self.BestInnerBound and self.BestOuterBound - """ - if self.opt.is_minimizing: - abs_gap = self.BestInnerBound - self.BestOuterBound - else: - abs_gap = self.BestOuterBound - self.BestInnerBound - - ## define by the best solution, as is common - nano = float("nan") # typing aid - if ( - abs_gap != nano - and abs_gap != float("inf") - and abs_gap != float("-inf") - and self.BestOuterBound != nano - and self.BestOuterBound != 0 - ): - rel_gap = abs_gap / abs(self.BestOuterBound) - else: - rel_gap = float("inf") - return abs_gap, rel_gap - def get_update_string(self): if self.latest_ib_char is None and \ self.latest_ob_char is None: diff --git a/mpisppy/cylinders/spcommunicator.py b/mpisppy/cylinders/spcommunicator.py index a0348aebd..b22cd2099 100644 --- a/mpisppy/cylinders/spcommunicator.py +++ b/mpisppy/cylinders/spcommunicator.py @@ -495,3 +495,23 @@ def initialize_bound_values(self): self.BestOuterBound = inf self._inner_bound_update = lambda new, old : (new > old) self._outer_bound_update = lambda new, old : (new < old) + + def compute_gaps(self): + """ Compute the current absolute and relative gaps, + using the current self.BestInnerBound and self.BestOuterBound + """ + if self.opt.is_minimizing: + abs_gap = self.BestInnerBound - self.BestOuterBound + else: + abs_gap = self.BestOuterBound - self.BestInnerBound + + if abs_gap != inf: + rel_gap = ( abs_gap / + max(1e-10, + abs(self.BestOuterBound), + abs(self.BestInnerBound), + ) + ) + else: + rel_gap = inf + return abs_gap, rel_gap diff --git a/mpisppy/extensions/mipgapper.py b/mpisppy/extensions/mipgapper.py index ded606ee7..2896e8ebe 100644 --- a/mpisppy/extensions/mipgapper.py +++ b/mpisppy/extensions/mipgapper.py @@ -11,6 +11,7 @@ extension. """ +from mpisppy import global_toc import mpisppy.extensions.extension class Gapper(mpisppy.extensions.extension.Extension): @@ -19,13 +20,25 @@ def __init__(self, ph): self.ph = ph self.cylinder_rank = self.ph.cylinder_rank self.gapperoptions = self.ph.options["gapperoptions"] # required - self.mipgapdict = self.gapperoptions["mipgapdict"] + self.mipgapdict = self.gapperoptions.get("mipgapdict", None) + self.starting_mipgap = self.gapperoptions.get("starting_mipgap", None) + self.mipgap_ratio = self.gapperoptions.get("mipgap_ratio", None) self.verbose = self.ph.options["verbose"] \ - or self.gapperoptions["verbose"] - - def _vb(self, str): - if self.verbose and self.cylinder_rank == 0: - print ("(rank0) mipgapper:" + str) + or self.gapperoptions.get("verbose", True) + self.verbose = True + self._check_options() + + def _check_options(self): + if self.mipgapdict is None and self.starting_mipgap is None: + raise RuntimeError("Need to either set a mipgapdict or a starting_mipgap for Gapper") + if self.mipgapdict is not None and self.starting_mipgap is not None: + raise RuntimeError("Gapper: Either use a mipgapdict or automatic mode, not both.") + # exactly one is not None + return + + def _vb(self, msg): + if self.verbose: + global_toc(f"{self.__class__.__name__}: {msg}", self.cylinder_rank == 0) def set_mipgap(self, mipgap): """ set the mipgap @@ -33,22 +46,39 @@ def set_mipgap(self, mipgap): float (mipgap): the gap to set """ oldgap = None + mipgap = float(mipgap) if "mipgap" in self.ph.current_solver_options: oldgap = self.ph.current_solver_options["mipgap"] - self._vb("Changing mipgap from "+str(oldgap)+" to "+str(mipgap)) - self.ph.current_solver_options["mipgap"] = float(mipgap) + if oldgap is None or oldgap != mipgap: + oldgap_str = f"{oldgap}" if oldgap is None else f"{oldgap*100:.3f}%" + self._vb(f"Changing mipgap from {oldgap_str} to {mipgap*100:.3f}%") + # no harm in unconditionally setting this, and covers iteration 1 + self.ph.current_solver_options["mipgap"] = mipgap def pre_iter0(self): if self.mipgapdict is None: - return - if 0 in self.mipgapdict: + # spcomm not yet set in `__init__`, so check this here + if self.ph.spcomm is None: + raise RuntimeError("Automatic gapper can only be used with cylinders -- needs both an upper bound and lower bound cylinder") + self.set_mipgap(self.starting_mipgap) + elif 0 in self.mipgapdict: self.set_mipgap(self.mipgapdict[0]) def post_iter0(self): return + def _autoset_mipgap(self): + abs_gap, rel_gap = self.ph.spcomm.compute_gaps() + cylinder_gap = rel_gap * self.mipgap_ratio + if cylinder_gap < self.starting_mipgap: + self.set_mipgap(cylinder_gap) + # current_solver_options changes in iteration 1 + elif self.ph._PHIter == 1: + self.set_mipgap(self.starting_mipgap) + def miditer(self): if self.mipgapdict is None: + self._autoset_mipgap() return PHIter = self.ph._PHIter if PHIter in self.mipgapdict: diff --git a/mpisppy/generic_cylinders.py b/mpisppy/generic_cylinders.py index bb778656d..54a9f5ffe 100644 --- a/mpisppy/generic_cylinders.py +++ b/mpisppy/generic_cylinders.py @@ -26,8 +26,6 @@ from mpisppy.convergers.primal_dual_converger import PrimalDualConverger from mpisppy.extensions.extension import MultiExtension, Extension -from mpisppy.extensions.fixer import Fixer -from mpisppy.extensions.mipgapper import Gapper from mpisppy.extensions.norm_rho_updater import NormRhoUpdater from mpisppy.extensions.primal_dual_rho import PrimalDualRho from mpisppy.extensions.gradient_extension import Gradient_extension @@ -207,24 +205,14 @@ def _do_decomp(module, cfg, scenario_creator, scenario_creator_kwargs, scenario_ # Extend and/or correct the vanilla dictionary ext_classes = list() # TBD: add cross_scenario_cuts, which also needs a cylinder - if cfg.mipgaps_json is not None: - ext_classes.append(Gapper) - with open(cfg.mipgaps_json) as fin: - din = json.load(fin) - mipgapdict = {int(i): din[i] for i in din} - hub_dict["opt_kwargs"]["options"]["gapperoptions"] = { - "verbose": cfg.verbose, - "mipgapdict": mipgapdict - } + + if cfg.mipgaps_json is not None or cfg.starting_mipgap is not None: + vanilla.add_gapper(hub_dict, cfg) if cfg.fixer: # cfg_vanilla takes care of the fixer_tol? assert hasattr(module, "id_fix_list_fct"), "id_fix_list_fct required for --fixer" - ext_classes.append(Fixer) - hub_dict["opt_kwargs"]["options"]["fixeroptions"] = { - "verbose": cfg.verbose, - "boundtol": cfg.fixer_tol, - "id_fix_list_fct": module.id_fix_list_fct, - } + vanilla.add_fixer(hub_dict, cfg) + if cfg.rc_fixer: vanilla.add_reduced_costs_fixer(hub_dict, cfg) diff --git a/mpisppy/utils/cfg_vanilla.py b/mpisppy/utils/cfg_vanilla.py index 60648ebc9..bca53e784 100644 --- a/mpisppy/utils/cfg_vanilla.py +++ b/mpisppy/utils/cfg_vanilla.py @@ -12,6 +12,7 @@ IDIOM: we feel free to have unused dictionary entries.""" import copy +import json # Hub and spoke SPBase classes from mpisppy.phbase import PHBase @@ -38,6 +39,7 @@ from mpisppy.cylinders.hub import PHHub, SubgradientHub, APHHub, FWPHHub from mpisppy.extensions.extension import MultiExtension from mpisppy.extensions.fixer import Fixer +from mpisppy.extensions.mipgapper import Gapper from mpisppy.extensions.integer_relax_then_enforce import IntegerRelaxThenEnforce from mpisppy.extensions.cross_scen_extension import CrossScenarioExtension from mpisppy.extensions.reduced_costs_fixer import ReducedCostsFixer @@ -285,12 +287,26 @@ def extension_adder(hub_dict,ext_class): hub_dict["opt_kwargs"]["extensions"] = MultiExtension return hub_dict +def add_gapper(hub_dict, cfg): + hub_dict = extension_adder(hub_dict, Gapper) + if cfg.mipgaps_json is not None: + with open(cfg.mipgaps_json) as fin: + din = json.load(fin) + mipgapdict = {int(i): din[i] for i in din} + else: + mipgapdict = None + hub_dict["opt_kwargs"]["options"]["gapperoptions"] = { + "verbose": cfg.verbose, + "mipgapdict": mipgapdict, + "starting_mipgap": cfg.starting_mipgap, + "mipgap_ratio" : cfg.mipgap_ratio, + } def add_fixer(hub_dict, cfg, ): hub_dict = extension_adder(hub_dict,Fixer) - hub_dict["opt_kwargs"]["options"]["fixeroptions"] = {"verbose":False, + hub_dict["opt_kwargs"]["options"]["fixeroptions"] = {"verbose":cfg.verbose, "boundtol": cfg.fixer_tol, "id_fix_list_fct": cfg.id_fix_list_fct} return hub_dict diff --git a/mpisppy/utils/config.py b/mpisppy/utils/config.py index 38abcaea8..d49e7fe42 100644 --- a/mpisppy/utils/config.py +++ b/mpisppy/utils/config.py @@ -535,6 +535,17 @@ def gapper_args(self): domain=str, default=None) + self.add_to_config('starting_mipgap', + description="Sets automatic gapper mode and the starting and minimum mipgap", + domain=float, + default=None) + + self.add_to_config('mipgap_ratio', + description="The ratio of the overall relative optimality gap to the subproblem " + "mipgaps. This should be less than 1 for the algorithm to make progress. " + "(default = 0.1)", + domain=float, + default=0.1) def fwph_args(self): From 45a35b8c365523e9225893984e95e49c3957b8e1 Mon Sep 17 00:00:00 2001 From: Bernard Knueven Date: Mon, 12 May 2025 15:28:06 -0600 Subject: [PATCH 2/7] adding support of Lagrangian gapper --- mpisppy/cylinders/lagrangian_bounder.py | 5 +++-- mpisppy/cylinders/spoke.py | 6 ++++++ mpisppy/cylinders/subgradient_bounder.py | 3 ++- mpisppy/extensions/mipgapper.py | 9 ++++++--- mpisppy/generic_cylinders.py | 3 +++ mpisppy/utils/cfg_vanilla.py | 12 ++++++++---- mpisppy/utils/config.py | 19 ++++++++++++------- 7 files changed, 40 insertions(+), 17 deletions(-) diff --git a/mpisppy/cylinders/lagrangian_bounder.py b/mpisppy/cylinders/lagrangian_bounder.py index 0eac149e5..c3eaff54e 100644 --- a/mpisppy/cylinders/lagrangian_bounder.py +++ b/mpisppy/cylinders/lagrangian_bounder.py @@ -82,10 +82,11 @@ def main(self, need_solution=False): if extensions: self.opt.extobject.pre_iter0() - self.dk_iter = 1 + self.opt._PHIter = 0 self.trivial_bound = self.lagrangian(need_solution=need_solution, warmstart=sputils.WarmstartStatus.CHECK) if extensions: self.opt.extobject.post_iter0() + self.opt._PHIter += 1 self.opt.current_solver_options = self.opt.iterk_solver_options @@ -104,6 +105,6 @@ def main(self, need_solution=False): self.send_bound(bound) if extensions: self.opt.extobject.enditer_after_sync() - self.dk_iter += 1 + self.opt._PHIter += 1 else: self.do_while_waiting_for_new_Ws(need_solution=need_solution, warmstart=True) diff --git a/mpisppy/cylinders/spoke.py b/mpisppy/cylinders/spoke.py index 7bbe7d073..20757e9a9 100644 --- a/mpisppy/cylinders/spoke.py +++ b/mpisppy/cylinders/spoke.py @@ -85,6 +85,12 @@ def bound(self): return self._bound[0] def send_bound(self, value): + if self.bound_type() == Field.OBJECTIVE_INNER_BOUND: + self.BestInnerBound = self.InnerBoundUpdate(value) + elif self.bound_type() == Field.OBJECTIVE_OUTER_BOUND: + self.BestOuterBound = self.OuterBoundUpdate(value) + else: + raise RuntimeError(f"Unexpected bound_type {self.bound_type()}") self._append_trace(value) self._bound[0] = value self.put_send_buffer(self._bound, self.bound_type()) diff --git a/mpisppy/cylinders/subgradient_bounder.py b/mpisppy/cylinders/subgradient_bounder.py index 612afb8e7..6c77f2698 100644 --- a/mpisppy/cylinders/subgradient_bounder.py +++ b/mpisppy/cylinders/subgradient_bounder.py @@ -21,7 +21,7 @@ def main(self): if extensions: self.opt.extobject.pre_iter0() - self.dk_iter = 1 + self.opt._PHIter = 0 self.trivial_bound = self.lagrangian() if extensions: self.opt.extobject.post_iter0() @@ -30,6 +30,7 @@ def main(self): if extensions: self.opt.extobject.post_iter0_after_sync() + self.opt._PHIter += 0 self.opt.current_solver_options = self.opt.iterk_solver_options # update rho / alpha diff --git a/mpisppy/extensions/mipgapper.py b/mpisppy/extensions/mipgapper.py index 2896e8ebe..b701d720c 100644 --- a/mpisppy/extensions/mipgapper.py +++ b/mpisppy/extensions/mipgapper.py @@ -30,15 +30,15 @@ def __init__(self, ph): def _check_options(self): if self.mipgapdict is None and self.starting_mipgap is None: - raise RuntimeError("Need to either set a mipgapdict or a starting_mipgap for Gapper") + raise RuntimeError(f"{self.ph._get_cylinder_name()}: Need to either set a mipgapdict or a starting_mipgap for Gapper") if self.mipgapdict is not None and self.starting_mipgap is not None: - raise RuntimeError("Gapper: Either use a mipgapdict or automatic mode, not both.") + raise RuntimeError(f"{self.ph._get_cylinder_name()} Gapper: Either use a mipgapdict or automatic mode, not both.") # exactly one is not None return def _vb(self, msg): if self.verbose: - global_toc(f"{self.__class__.__name__}: {msg}", self.cylinder_rank == 0) + global_toc(f"{self.ph._get_cylinder_name()} {self.__class__.__name__}: {msg}", self.cylinder_rank == 0) def set_mipgap(self, mipgap): """ set the mipgap @@ -68,8 +68,11 @@ def post_iter0(self): return def _autoset_mipgap(self): + self.ph.spcomm.receive_innerbounds() + self.ph.spcomm.receive_outerbounds() abs_gap, rel_gap = self.ph.spcomm.compute_gaps() cylinder_gap = rel_gap * self.mipgap_ratio + # global_toc(f"{self.ph._get_cylinder_name()}: {self.ph.spcomm.BestInnerBound=}, {self.ph.spcomm.BestOuterBound=}", self.ph.cylinder_rank == 0) if cylinder_gap < self.starting_mipgap: self.set_mipgap(cylinder_gap) # current_solver_options changes in iteration 1 diff --git a/mpisppy/generic_cylinders.py b/mpisppy/generic_cylinders.py index 54a9f5ffe..57797c216 100644 --- a/mpisppy/generic_cylinders.py +++ b/mpisppy/generic_cylinders.py @@ -80,6 +80,7 @@ def _parse_args(m): cfg.fixer_args() cfg.integer_relax_then_enforce_args() cfg.gapper_args() + cfg.gapper_args(name="lagrangian") cfg.fwph_args() cfg.lagrangian_args() cfg.ph_ob_args() @@ -310,6 +311,8 @@ def _do_decomp(module, cfg, scenario_creator, scenario_creator_kwargs, scenario_ rho_setter = rho_setter, all_nodenames = all_nodenames, ) + if cfg.lagrangian_starting_mipgap is not None: + vanilla.add_gapper(lagrangian_spoke, cfg, "lagrangian") # ph outer bounder spoke if cfg.ph_ob: diff --git a/mpisppy/utils/cfg_vanilla.py b/mpisppy/utils/cfg_vanilla.py index bca53e784..4368b6e44 100644 --- a/mpisppy/utils/cfg_vanilla.py +++ b/mpisppy/utils/cfg_vanilla.py @@ -287,19 +287,23 @@ def extension_adder(hub_dict,ext_class): hub_dict["opt_kwargs"]["extensions"] = MultiExtension return hub_dict -def add_gapper(hub_dict, cfg): +def add_gapper(hub_dict, cfg, name=None): hub_dict = extension_adder(hub_dict, Gapper) - if cfg.mipgaps_json is not None: + if name is None and cfg.mipgaps_json is not None: with open(cfg.mipgaps_json) as fin: din = json.load(fin) mipgapdict = {int(i): din[i] for i in din} else: mipgapdict = None + if name is None: + name = "" + else: + name = name + "_" hub_dict["opt_kwargs"]["options"]["gapperoptions"] = { "verbose": cfg.verbose, "mipgapdict": mipgapdict, - "starting_mipgap": cfg.starting_mipgap, - "mipgap_ratio" : cfg.mipgap_ratio, + "starting_mipgap": getattr(cfg, f"{name}starting_mipgap"), + "mipgap_ratio" : getattr(cfg, f"{name}mipgap_ratio"), } def add_fixer(hub_dict, diff --git a/mpisppy/utils/config.py b/mpisppy/utils/config.py index d49e7fe42..22c0261b7 100644 --- a/mpisppy/utils/config.py +++ b/mpisppy/utils/config.py @@ -528,19 +528,24 @@ def coeff_rho_args(self): default=1.0) - def gapper_args(self): + def gapper_args(self, name=None): + if name is None: + name = "" + else: + name = name+"_" - self.add_to_config('mipgaps_json', - description="path to json file with a mipgap schedule for PH iterations", - domain=str, - default=None) + if name == "": + self.add_to_config('mipgaps_json', + description="path to json file with a mipgap schedule for PH iterations", + domain=str, + default=None) - self.add_to_config('starting_mipgap', + self.add_to_config(f'{name}starting_mipgap', description="Sets automatic gapper mode and the starting and minimum mipgap", domain=float, default=None) - self.add_to_config('mipgap_ratio', + self.add_to_config(f'{name}mipgap_ratio', description="The ratio of the overall relative optimality gap to the subproblem " "mipgaps. This should be less than 1 for the algorithm to make progress. " "(default = 0.1)", From b3cb027f95bf6dc2a4001ebf42fef5f5b0a49629 Mon Sep 17 00:00:00 2001 From: Bernard Knueven Date: Tue, 13 May 2025 13:40:09 -0600 Subject: [PATCH 3/7] fix subgradient iteration count --- mpisppy/cylinders/subgradient_bounder.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mpisppy/cylinders/subgradient_bounder.py b/mpisppy/cylinders/subgradient_bounder.py index 6c77f2698..b6cb3df1b 100644 --- a/mpisppy/cylinders/subgradient_bounder.py +++ b/mpisppy/cylinders/subgradient_bounder.py @@ -30,7 +30,7 @@ def main(self): if extensions: self.opt.extobject.post_iter0_after_sync() - self.opt._PHIter += 0 + self.opt._PHIter += 1 self.opt.current_solver_options = self.opt.iterk_solver_options # update rho / alpha @@ -53,3 +53,4 @@ def main(self): self.send_bound(bound) if extensions: self.opt.extobject.enditer_after_sync() + self.opt._PHIter += 1 From 81128c3f3b3b477a47330af1e62f32e01006c76e Mon Sep 17 00:00:00 2001 From: bknueven <30801372+bknueven@users.noreply.github.com> Date: Fri, 16 May 2025 12:28:40 -0600 Subject: [PATCH 4/7] Better variable names --- mpisppy/extensions/mipgapper.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mpisppy/extensions/mipgapper.py b/mpisppy/extensions/mipgapper.py index b701d720c..72d0a5199 100644 --- a/mpisppy/extensions/mipgapper.py +++ b/mpisppy/extensions/mipgapper.py @@ -70,11 +70,11 @@ def post_iter0(self): def _autoset_mipgap(self): self.ph.spcomm.receive_innerbounds() self.ph.spcomm.receive_outerbounds() - abs_gap, rel_gap = self.ph.spcomm.compute_gaps() - cylinder_gap = rel_gap * self.mipgap_ratio + _, problem_rel_gap = self.ph.spcomm.compute_gaps() + subproblem_rel_gap = problem_rel_gap * self.mipgap_ratio # global_toc(f"{self.ph._get_cylinder_name()}: {self.ph.spcomm.BestInnerBound=}, {self.ph.spcomm.BestOuterBound=}", self.ph.cylinder_rank == 0) - if cylinder_gap < self.starting_mipgap: - self.set_mipgap(cylinder_gap) + if subproblem_rel_gap < self.starting_mipgap: + self.set_mipgap(subproblem_rel_gap) # current_solver_options changes in iteration 1 elif self.ph._PHIter == 1: self.set_mipgap(self.starting_mipgap) From 8dde00d2c2e7d08205a191429f7cc6fdaca072b6 Mon Sep 17 00:00:00 2001 From: bknueven <30801372+bknueven@users.noreply.github.com> Date: Fri, 16 May 2025 12:29:06 -0600 Subject: [PATCH 5/7] NFC: adding comments on _PHIter --- mpisppy/cylinders/lagrangian_bounder.py | 1 + mpisppy/cylinders/subgradient_bounder.py | 1 + 2 files changed, 2 insertions(+) diff --git a/mpisppy/cylinders/lagrangian_bounder.py b/mpisppy/cylinders/lagrangian_bounder.py index c3eaff54e..edae8bf4f 100644 --- a/mpisppy/cylinders/lagrangian_bounder.py +++ b/mpisppy/cylinders/lagrangian_bounder.py @@ -82,6 +82,7 @@ def main(self, need_solution=False): if extensions: self.opt.extobject.pre_iter0() + # setting this for PH extensions used by this Spoke self.opt._PHIter = 0 self.trivial_bound = self.lagrangian(need_solution=need_solution, warmstart=sputils.WarmstartStatus.CHECK) if extensions: diff --git a/mpisppy/cylinders/subgradient_bounder.py b/mpisppy/cylinders/subgradient_bounder.py index b6cb3df1b..3017169ba 100644 --- a/mpisppy/cylinders/subgradient_bounder.py +++ b/mpisppy/cylinders/subgradient_bounder.py @@ -21,6 +21,7 @@ def main(self): if extensions: self.opt.extobject.pre_iter0() + # setting this for PH extensions used by this Spoke self.opt._PHIter = 0 self.trivial_bound = self.lagrangian() if extensions: From 663a953dd0239011bd3d1f3651235894fd551c0a Mon Sep 17 00:00:00 2001 From: Bernard Knueven Date: Fri, 16 May 2025 15:30:35 -0600 Subject: [PATCH 6/7] removing verbose option from gapper --- mpisppy/extensions/mipgapper.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/mpisppy/extensions/mipgapper.py b/mpisppy/extensions/mipgapper.py index 72d0a5199..c3287b2de 100644 --- a/mpisppy/extensions/mipgapper.py +++ b/mpisppy/extensions/mipgapper.py @@ -23,9 +23,6 @@ def __init__(self, ph): self.mipgapdict = self.gapperoptions.get("mipgapdict", None) self.starting_mipgap = self.gapperoptions.get("starting_mipgap", None) self.mipgap_ratio = self.gapperoptions.get("mipgap_ratio", None) - self.verbose = self.ph.options["verbose"] \ - or self.gapperoptions.get("verbose", True) - self.verbose = True self._check_options() def _check_options(self): @@ -36,9 +33,8 @@ def _check_options(self): # exactly one is not None return - def _vb(self, msg): - if self.verbose: - global_toc(f"{self.ph._get_cylinder_name()} {self.__class__.__name__}: {msg}", self.cylinder_rank == 0) + def _print_msg(self, msg): + global_toc(f"{self.ph._get_cylinder_name()} {self.__class__.__name__}: {msg}", self.cylinder_rank == 0) def set_mipgap(self, mipgap): """ set the mipgap @@ -51,7 +47,7 @@ def set_mipgap(self, mipgap): oldgap = self.ph.current_solver_options["mipgap"] if oldgap is None or oldgap != mipgap: oldgap_str = f"{oldgap}" if oldgap is None else f"{oldgap*100:.3f}%" - self._vb(f"Changing mipgap from {oldgap_str} to {mipgap*100:.3f}%") + self._print_msg(f"Changing mipgap from {oldgap_str} to {mipgap*100:.3f}%") # no harm in unconditionally setting this, and covers iteration 1 self.ph.current_solver_options["mipgap"] = mipgap From 4969232a11d6127e05efe81672fc918b20325add Mon Sep 17 00:00:00 2001 From: Bernard Knueven Date: Fri, 16 May 2025 15:35:24 -0600 Subject: [PATCH 7/7] adding documentation --- doc/src/extensions.rst | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/doc/src/extensions.rst b/doc/src/extensions.rst index 81e497dfc..92b0ac1a1 100644 --- a/doc/src/extensions.rst +++ b/doc/src/extensions.rst @@ -69,7 +69,17 @@ This is a good extension to look at as a first example. It takes a dictionary with iteration numbers and mipgaps as input and changes the mipgap at the corresponding iterations. The dictionary is provided in the options dictionary in ``["gapperoptions"]["mipgapdict"]``. There -is an example of its use in ``examples.sizes.sizes_demo.py`` +is an example of its use in ``examples.sizes.sizes_demo.py``. + +Instead of an options dictionary, when run with cylinders the options +``["gapperoptions"]["starting_mipgap"]`` and ``["gapperoptions"]["mipgap_ratio"]`` +can be set. The ``starting_mipgap`` will be the initial value used, +and as the cylinders close the relative optimality gap the extension will set the subproblem +mipgaps as the ``min(starting_mipgap, mipgap_ratio * problem_ratio)``, where +the ``problem_ratio`` is the relative optimality gap on the overall problem +as computed by the cylinders. + +This extension can also be used with the Lagrangian and subgradient spokes. fixer.py ^^^^^^^^