diff --git a/mpisppy/utils/cfg_vanilla.py b/mpisppy/utils/cfg_vanilla.py index ae0de202..4f1b8729 100644 --- a/mpisppy/utils/cfg_vanilla.py +++ b/mpisppy/utils/cfg_vanilla.py @@ -18,9 +18,13 @@ import mpisppy.utils.sputils as sputils def _hasit(cfg, argname): - # aside: Config objects act like a dict or an object TBD: so why the and? - return cfg.get(argname) is not None and cfg[argname] is not None and \ - cfg[argname] + # aside: Config objects act like a dict or an object + val = cfg.get(argname) + # For boolean flags, require True. For non-bool values, treat any non-None + # value as "set". + if isinstance(val, bool): + return val + return val is not None def shared_options(cfg): shoptions = { @@ -69,6 +73,24 @@ def shared_options(cfg): return shoptions +def apply_solver_specs(name, spoke, cfg): + options = spoke["opt_kwargs"]["options"] + if _hasit(cfg, name+"_solver_name"): + options["solver_name"] = cfg.get(name+"_solver_name") + if _hasit(cfg, name+"_solver_options"): + odict = sputils.option_string_to_dict(cfg.get(name+"_solver_options")) + options["iter0_solver_options"] = odict + options["iterk_solver_options"] = copy.deepcopy(odict) + if _hasit(cfg, name+"_iter0_mipgap"): + options["iter0_solver_options"]["mipgap"] = cfg.get(name+"_iter0_mipgap") + if _hasit(cfg, name+"_iterk_mipgap"): + options["iterk_solver_options"]["mipgap"] = cfg.get(name+"_iterk_mipgap") + # re-apply max_solver_threads since we may have over-written the + # iter*_solver_options above. + if _hasit(cfg, "max_solver_threads"): + options["iter0_solver_options"]["threads"] = cfg.max_solver_threads + options["iterk_solver_options"]["threads"] = cfg.max_solver_threads + def add_multistage_options(cylinder_dict,all_nodenames,branching_factors): cylinder_dict = copy.deepcopy(cylinder_dict) if branching_factors is not None: @@ -653,12 +675,7 @@ def lagrangian_spoke( ph_extensions=ph_extensions, extension_kwargs=extension_kwargs, ) - if cfg.lagrangian_iter0_mipgap is not None: - lagrangian_spoke["opt_kwargs"]["options"]["iter0_solver_options"]\ - ["mipgap"] = cfg.lagrangian_iter0_mipgap - if cfg.lagrangian_iterk_mipgap is not None: - lagrangian_spoke["opt_kwargs"]["options"]["iterk_solver_options"]\ - ["mipgap"] = cfg.lagrangian_iterk_mipgap + apply_solver_specs("lagrangian", lagrangian_spoke, cfg) add_ph_tracking(lagrangian_spoke, cfg, spoke=True) return lagrangian_spoke @@ -689,6 +706,7 @@ def reduced_costs_spoke( extension_kwargs=extension_kwargs, ) + apply_solver_specs("reduced_costs", rc_spoke, cfg) add_ph_tracking(rc_spoke, cfg, spoke=True) return rc_spoke @@ -760,12 +778,7 @@ def subgradient_spoke( extension_kwargs=extension_kwargs, ) subgradient_spoke["opt_class"] = Subgradient - if cfg.subgradient_iter0_mipgap is not None: - subgradient_spoke["opt_kwargs"]["options"]["iter0_solver_options"]\ - ["mipgap"] = cfg.subgradient_iter0_mipgap - if cfg.subgradient_iterk_mipgap is not None: - subgradient_spoke["opt_kwargs"]["options"]["iterk_solver_options"]\ - ["mipgap"] = cfg.subgradient_iterk_mipgap + apply_solver_specs("subgradient", subgradient_spoke, cfg) if cfg.subgradient_rho_multiplier is not None: subgradient_spoke["opt_kwargs"]["options"]["subgradient_rho_multiplier"]\ = cfg.subgradient_rho_multiplier @@ -809,6 +822,7 @@ def ph_dual_spoke( options = ph_dual_spoke["opt_kwargs"]["options"] if cfg.ph_dual_rescale_rho_factor is not None: options["rho_factor"] = cfg.ph_dual_rescale_rho_factor + apply_solver_specs("ph_dual", ph_dual_spoke, cfg) # make sure this spoke doesn't hit the time or iteration limit options["time_limit"] = None @@ -848,6 +862,7 @@ def relaxed_ph_spoke( options = relaxed_ph_spoke["opt_kwargs"]["options"] if cfg.relaxed_ph_rescale_rho_factor is not None: options["rho_factor"] = cfg.relaxed_ph_rescale_rho_factor + apply_solver_specs("relaxed_ph", relaxed_ph_spoke, cfg) # make sure this spoke doesn't hit the time or iteration limit options["time_limit"] = None diff --git a/mpisppy/utils/config.py b/mpisppy/utils/config.py index d8616dd8..5250caea 100644 --- a/mpisppy/utils/config.py +++ b/mpisppy/utils/config.py @@ -169,17 +169,33 @@ def _bad_options(msg): _bad_options("--rc-fixer requires --reduced-costs") def add_solver_specs(self, prefix=""): - sstr = f"{prefix}_solver" if prefix != "" else "solver" + sstr = f"{prefix}_solver" if prefix else "solver" + if prefix: + prefix += " " self.add_to_config(f"{sstr}_name", - description= "solver name (default None)", + description= f"{prefix}solver name (default None)", domain = str, default=None) self.add_to_config(f"{sstr}_options", - description= "solver options; space delimited with = for values (default None)", + description= f"{prefix}solver options; space delimited with = for values (default None)", domain = str, default=None) + def add_mipgap_specs(self, prefix=""): + sstr = f"{prefix}_" if prefix else "" + if prefix: + prefix += " " + self.add_to_config(f"{sstr}iter0_mipgap", + description=f"{prefix}mip gap option for iteration 0 (default None)", + domain=float, + default=None) + + self.add_to_config(f"{sstr}iterk_mipgap", + description=f"{prefix}mip gap option non-zero iterations (default None)", + domain=float, + default=None) + def _common_args(self): raise RuntimeError("_common_args is no longer used. See comments at top of config.py") @@ -211,7 +227,7 @@ def popular_args(self): domain=bool, default=False) - self.add_solver_specs(prefix="") + self.add_solver_specs() self.add_to_config("seed", description="Seed for random numbers (default is 1134)", @@ -456,16 +472,7 @@ def timed_mipgap_args(self): default="0.05:600") def mip_options(self): - - self.add_to_config("iter0_mipgap", - description="mip gap option for iteration 0 (default None)", - domain=float, - default=None) - - self.add_to_config("iterk_mipgap", - description="mip gap option non-zero iterations (default None)", - domain=float, - default=None) + self.add_mipgap_specs() def aph_args(self): @@ -660,15 +667,8 @@ def lagrangian_args(self): domain=bool, default=False) - self.add_to_config("lagrangian_iter0_mipgap", - description="lgr. iter0 solver option mipgap (default None)", - domain=float, - default=None) - - self.add_to_config("lagrangian_iterk_mipgap", - description="lgr. iterk solver option mipgap (default None)", - domain=float, - default=None) + self.add_solver_specs("lagrangian") + self.add_mipgap_specs("lagrangian") def reduced_costs_args(self): @@ -677,6 +677,8 @@ def reduced_costs_args(self): description="have a reduced costs spoke", domain=bool, default=False) + + self.add_solver_specs("reduced_costs") self.add_to_config('rc_verbose', description="verbose output for reduced costs", @@ -731,16 +733,7 @@ def lagranger_args(self): description="have a special lagranger spoke", domain=bool, default=False) - - self.add_to_config("lagranger_iter0_mipgap", - description="lagranger iter0 mipgap (default None)", - domain=float, - default=None) - - self.add_to_config("lagranger_iterk_mipgap", - description="lagranger iterk mipgap (default None)", - domain=float, - default=None) + self.add_mipgap_specs("lagranger") self.add_to_config("lagranger_rho_rescale_factors_json", description="json file: rho rescale factors (default None)", @@ -755,15 +748,8 @@ def subgradient_bounder_args(self): domain=bool, default=False) - self.add_to_config("subgradient_iter0_mipgap", - description="lgr. iter0 solver option mipgap (default None)", - domain=float, - default=None) - - self.add_to_config("subgradient_iterk_mipgap", - description="lgr. iterk solver option mipgap (default None)", - domain=float, - default=None) + self.add_solver_specs("subgradient") + self.add_mipgap_specs("subgradient") self.add_to_config("subgradient_rho_multiplier", description="rescale rho (update step size) by this factor", @@ -785,6 +771,7 @@ def relaxed_ph_args(self): description="Used to rescale rho initially (default=1.0)", domain=float, default=1.0) + self.add_solver_specs("relaxed_ph") def ph_dual_args(self): @@ -793,6 +780,9 @@ def ph_dual_args(self): description="have a dual PH spoke", domain=bool, default=False) + + self.add_solver_specs("ph_dual") + self.add_to_config("ph_dual_rescale_rho_factor", description="Used to rescale rho initially (default=0.1)", domain=float, diff --git a/mpisppy/utils/solver_spec.py b/mpisppy/utils/solver_spec.py index 1cb1fca7..d0ed4e1b 100644 --- a/mpisppy/utils/solver_spec.py +++ b/mpisppy/utils/solver_spec.py @@ -66,7 +66,7 @@ def solver_specification(cfg, prefix="", name_required=True): solver_name = cfg[name_idx] options_idx = "solver_options" if sroot == "" else f"{sroot}_solver_options" ostr = cfg.get(options_idx) - solver_options = sputils.option_string_to_dict(ostr) # will return None for None + solver_options = sputils.option_string_to_dict(ostr) break else: if name_required: diff --git a/mpisppy/utils/sputils.py b/mpisppy/utils/sputils.py index c7bff5e4..d0014b8a 100644 --- a/mpisppy/utils/sputils.py +++ b/mpisppy/utils/sputils.py @@ -653,7 +653,7 @@ def convert_value_string_to_number(s): solver_options = dict() if ostr is None or ostr == "": - return None + return solver_options for this_option_string in ostr.split(): this_option_pieces = this_option_string.strip().split("=") if len(this_option_pieces) == 2: