diff --git a/doc/src/extensions.rst b/doc/src/extensions.rst index a4aae4d47..81e497dfc 100644 --- a/doc/src/extensions.rst +++ b/doc/src/extensions.rst @@ -74,7 +74,7 @@ is an example of its use in ``examples.sizes.sizes_demo.py`` fixer.py ^^^^^^^^ -This extension provides methods for fixing variables (usually integers) for +This extension provides methods for fixing nonanticipative variables (usually integers) for which all scenarios have agreed for some number of iterations. There is an example of its use in ``examples.sizes.sizes_demo.py`` also in ``examples.sizes.uc_ama.py``. The ``uc_ama`` example illustrates @@ -89,6 +89,36 @@ to be on the ``Config`` object so the amalgamator can find it. So if you don't want to fix a variable at iteration zero, provide a tolerance, but set all count values to ``None``. +reduced_cost_fixer +^^^^^^^^^^^^^^^^^^ + +This extension provides methods for fixing nonanticipative variables based on their expected +reduced cost as calculated by the ReducedCostSpoke. The aggressiveness of the +fixing can be controled through the ``zero_rc_tol`` parameter (reduced costs +with magnitude below this value will be considered 0 and not eligible for fixing) +and the ``fix_fraction_target`` paramemters, which set a maximum fraction of +nonanticipative variables to be fixed based on expected reduced costs. These two +parameters iteract with each other -- the expected reduced costs are sorted by +magnitude, and if the `fix_fraction_target`` percental is below ``zero_rc_tol``, +then fewer than ``fix_fraction_target`` variables will be fixed. Further, to +have a defined expected reduced cost, all nonant variable values *must be* at +the same bound in the ReducedCostSpoke. + +Variables will be unfixed if they no longer meet the expected reduced cost +criterion for fixing, e.g., the variable's expected reduced cost became too +low or the variable was not at its bound in every subproblem in the ReducedCostSpoke. + +relaxed_ph_fixer +^^^^^^^^^^^^^^^^ + +This extension will fix nonanticipative variables at their bound if they are at +their bound in the RelaxedPHSpoke for that subproblem. It will similarily unfix +nonanticipative variables which are not at their bounds in the RelaxedPHSpoke. +Because different nonanticipative variables are fixed in different suproblems, +it will also unfix nonanticipative variables if their value is *not* at the the current +consensus solution xbar (because the variable was not fixed in a different subproblem +and therefore came off its bound). + xhat ^^^^ diff --git a/doc/src/hubs.rst b/doc/src/hubs.rst index 45bcfbe8b..aea2bfbad 100644 --- a/doc/src/hubs.rst +++ b/doc/src/hubs.rst @@ -47,6 +47,13 @@ this value (default 1e-1). If only the binary terms should be approximated, the option `linearize_binary_proximal_terms` can be used. +PHNonant +-------- +This is exactly like the PH hub, except it does not send W values. +It can be useful when some other cylinder is providing W values +(e.g., RelaxedPHSpoke). + + lshaped ------- @@ -72,6 +79,15 @@ also supplies x and/or W values at every iteration, and is largely based on the PH implementation. It utilizes a constant step size rule based on `rho` unless modified by an extension. +FWPH +---- + +The Frank-Wolfe progressive hedging hub can be used with most spokes +because it supplies x and/or W values as part of its solution process. +While FWPH is not known to converge to a primal solution for a SMIP, it +often discovers excellent incumbent values along the way (when paired with +an xhat spoke). + Hub Convergers -------------- diff --git a/doc/src/spokes.rst b/doc/src/spokes.rst index 0d3480ab6..819d61f66 100644 --- a/doc/src/spokes.rst +++ b/doc/src/spokes.rst @@ -92,8 +92,9 @@ Reduced Costs The reduced cost spoke is equivalent to the Lagrangian spoke, except that it relaxes all integrality contraints in the subproblems. This enables the computation of reduced costs -for the first stage variables, which can be used for bound tightening or heuristic fixing -in the hub. +for the nonanticipative variables, which can be used for provable bound tightening, which +is shared to all cylinders. The reduced cost can also be used for heuristic fixing in the +hub via the reduced_cost_fixer extension. Inner Bounds @@ -171,4 +172,17 @@ General cross scenario ^^^^^^^^^^^^^^ -Passes cross scenario cuts. +Computes and passes cross scenario cuts. + +relaxed_ph +^^^^^^^^^^ + +For S-MIPs, runs progressive hedging on the linear programming relaxation +of the scenario subproblems. Provides Ws and "relaxed nonants", the former +of which can be utilized for Lagrangian to compute lower bounds, and the later +of which can be utilized to inform fixings for the hub via the relaxed_ph_fixer +extention. This method can be effective when the subproblem linear programming +relaxation is "strong". + +The option ``relaxed_ph_rescale_rho_factor``, which defaults to 1, rescales +rho for this spoke between iteration 0 and iteration 1. diff --git a/mpisppy/cylinders/fwph_spoke.py b/mpisppy/cylinders/fwph_spoke.py index d2eee80f0..e81529b8b 100644 --- a/mpisppy/cylinders/fwph_spoke.py +++ b/mpisppy/cylinders/fwph_spoke.py @@ -15,9 +15,6 @@ class FrankWolfeOuterBound(mpisppy.cylinders.spoke.OuterBoundSpoke): def main(self): self.opt.fwph_main() - def is_converged(self): - return self.got_kill_signal() - def sync(self): # The FWPH spoke can call "sync" before it # even starts doing anything, so its possible diff --git a/mpisppy/cylinders/hub.py b/mpisppy/cylinders/hub.py index 384069e9f..f6e564e88 100644 --- a/mpisppy/cylinders/hub.py +++ b/mpisppy/cylinders/hub.py @@ -225,11 +225,21 @@ def sync_bounds(self): self.send_boundsout() -class PHHub(Hub): - - send_fields = (*Hub.send_fields, Field.NONANT, Field.DUALS) +class PHNonantHub(Hub): + """ + Like PHHub, but only sends nonants and omits Ws. To be used + when another cylinder is supplying Ws (like RelaxedPHSpoke). + Could be removed when mpi-sppy supports pointing consuming + spokes like Lagrangian to a specific dual (W) buffer. + """ + + send_fields = (*Hub.send_fields, Field.NONANT, ) receive_fields = (*Hub.receive_fields,) + @property + def nonant_field(self): + return Field.NONANT + def setup_hub(self): ## Generate some warnings if nothing is giving bounds if not self.receive_field_spcomms[Field.OBJECTIVE_OUTER_BOUND]: @@ -316,17 +326,27 @@ def send_nonants(self): """ Gather nonants and send them to the appropriate spokes """ ci = 0 ## index to self.nonant_send_buffer - nonant_send_buffer = self.send_buffers[Field.NONANT] + nonant_send_buffer = self.send_buffers[self.nonant_field] for k, s in self.opt.local_scenarios.items(): for xvar in s._mpisppy_data.nonant_indices.values(): nonant_send_buffer[ci] = xvar._value ci += 1 logging.debug("hub is sending X nonants={}".format(nonant_send_buffer)) - self.put_send_buffer(nonant_send_buffer, Field.NONANT) + self.put_send_buffer(nonant_send_buffer, self.nonant_field) return + def send_ws(self): + """ Nonant hub; do not send Ws + """ + pass + + +class PHHub(PHNonantHub): + send_fields = (*PHNonantHub.send_fields, Field.DUALS, ) + receive_fields = (*PHNonantHub.receive_fields,) + def send_ws(self): """ Send dual weights to the appropriate spokes """ @@ -339,6 +359,7 @@ def send_ws(self): return + class LShapedHub(Hub): send_fields = (*Hub.send_fields, Field.NONANT,) diff --git a/mpisppy/cylinders/relaxed_ph_spoke.py b/mpisppy/cylinders/relaxed_ph_spoke.py new file mode 100644 index 000000000..5c7815a1b --- /dev/null +++ b/mpisppy/cylinders/relaxed_ph_spoke.py @@ -0,0 +1,57 @@ +############################################################################### +# mpi-sppy: MPI-based Stochastic Programming in PYthon +# +# Copyright (c) 2024, Lawrence Livermore National Security, LLC, Alliance for +# Sustainable Energy, LLC, The Regents of the University of California, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md for +# full copyright and license information. +############################################################################### + +from mpisppy.cylinders.spwindow import Field +from mpisppy.cylinders.spoke import Spoke +from mpisppy.cylinders.hub import PHHub + +import pyomo.environ as pyo + +class RelaxedPHSpoke(Spoke, PHHub): + + send_fields = (*Spoke.send_fields, Field.DUALS, Field.RELAXED_NONANT, ) + receive_fields = (*Spoke.receive_fields, ) + + @property + def nonant_field(self): + return Field.RELAXED_NONANT + + def send_boundsout(self): + # overwrite PHHub.send_boundsout (not a hub) + return + + def update_rho(self): + rho_factor = self.opt.options.get("relaxed_ph_rho_factor", 1.0) + if rho_factor == 1.0: + return + for s in self.opt.local_scenarios.values(): + for rho in s._mpisppy_model.rho.values(): + rho._value = rho_factor * rho._value + + def main(self): + # relax the integers + integer_relaxer = pyo.TransformationFactory("core.relax_integer_vars") + for s in self.opt.local_scenarios.values(): + integer_relaxer.apply_to(s) + + # setup, PH Iter0 + smoothed = self.options.get('smoothed', 0) + attach_prox = True + self.opt.PH_Prep(attach_prox=attach_prox, attach_smooth = smoothed) + trivial_bound = self.opt.Iter0() + if self.opt._can_update_best_bound(): + self.opt.best_bound_obj_val = trivial_bound + + # update the rho + self.update_rho() + + # rest of PH + self.opt.iterk_loop() + + return self.opt.conv, None, trivial_bound diff --git a/mpisppy/cylinders/spoke.py b/mpisppy/cylinders/spoke.py index 7bbe7d073..759b0ec37 100644 --- a/mpisppy/cylinders/spoke.py +++ b/mpisppy/cylinders/spoke.py @@ -29,6 +29,12 @@ def got_kill_signal(self): self.get_receive_buffer(shutdown_buf, Field.SHUTDOWN, 0, synchronize=False) return self.allreduce_or(shutdown_buf[0] == 1.0) + def is_converged(self): + """ Alias for got_kill_signal; useful for algorithms working as both + hub and spoke + """ + return self.got_kill_signal() + @abc.abstractmethod def main(self): """ diff --git a/mpisppy/cylinders/spwindow.py b/mpisppy/cylinders/spwindow.py index 3d8eb8e78..6dbfd1cdc 100644 --- a/mpisppy/cylinders/spwindow.py +++ b/mpisppy/cylinders/spwindow.py @@ -20,6 +20,7 @@ class Field(enum.IntEnum): SHUTDOWN=-1000 NONANT=1 DUALS=2 + RELAXED_NONANT=3 BEST_OBJECTIVE_BOUNDS=100 # Both inner and outer bounds from the hub. Layout: [OUTER INNER ID] OBJECTIVE_INNER_BOUND=101 OBJECTIVE_OUTER_BOUND=102 @@ -42,6 +43,7 @@ class Field(enum.IntEnum): Field.SHUTDOWN : 1, Field.NONANT : _field_length_components.local_nonant_length, Field.DUALS : _field_length_components.local_nonant_length, + Field.RELAXED_NONANT : _field_length_components.local_nonant_length, Field.BEST_OBJECTIVE_BOUNDS : 2, Field.OBJECTIVE_INNER_BOUND : 1, Field.OBJECTIVE_OUTER_BOUND : 1, diff --git a/mpisppy/extensions/relaxed_ph_fixer.py b/mpisppy/extensions/relaxed_ph_fixer.py new file mode 100644 index 000000000..a6471d757 --- /dev/null +++ b/mpisppy/extensions/relaxed_ph_fixer.py @@ -0,0 +1,119 @@ +############################################################################### +# mpi-sppy: MPI-based Stochastic Programming in PYthon +# +# Copyright (c) 2024, Lawrence Livermore National Security, LLC, Alliance for +# Sustainable Energy, LLC, The Regents of the University of California, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md for +# full copyright and license information. +############################################################################### +from mpisppy.extensions.extension import Extension +from mpisppy.utils.sputils import is_persistent + +from mpisppy.cylinders.spwindow import Field + +class RelaxedPHFixer(Extension): + + def __init__(self, spobj): + super().__init__(spobj) + + ph_options = spobj.options + ph_fixer_options = ph_options.get("relaxed_ph_fixer_options", {}) + self.bound_tol = ph_fixer_options.get("bound_tol", 1e-4) + self.verbose = ph_fixer_options.get("verbose", True) + self.debug = ph_fixer_options.get("debug", False) + + self._heuristic_fixed_vars = {} + self._current_relaxed_nonants = None + + def pre_iter0(self): + self._modeler_fixed_nonants = set() + self.nonant_length = self.opt.nonant_length + for k,s in self.opt.local_scenarios.items(): + for ndn_i, xvar in s._mpisppy_data.nonant_indices.items(): + if xvar.fixed: + self._modeler_fixed_nonants.add(ndn_i) + + for k,sub in self.opt.local_subproblems.items(): + self._heuristic_fixed_vars[k] = 0 + + def iter0_post_solver_creation(self): + # wait for relaxed iter0: + if self.relaxed_nonant_buf.id() == 0: + while not self.opt.spcomm.get_receive_buffer(self.relaxed_nonant_buf, Field.RELAXED_NONANT, self.relaxed_ph_spoke_index): + continue + self.relaxed_ph_fixing(self.relaxed_nonant_buf.value_array(), pre_iter0=True) + + def register_receive_fields(self): + spcomm = self.opt.spcomm + relaxed_ph_ranks = spcomm.fields_to_ranks[Field.RELAXED_NONANT] + assert len(relaxed_ph_ranks) == 1 + index = relaxed_ph_ranks[0] + + self.relaxed_ph_spoke_index = index + + self.relaxed_nonant_buf = spcomm.register_recv_field( + Field.RELAXED_NONANT, + self.relaxed_ph_spoke_index, + ) + + return + + def miditer(self): + self.opt.spcomm.get_receive_buffer( + self.relaxed_nonant_buf, + Field.RELAXED_NONANT, + self.relaxed_ph_spoke_index, + ) + self.relaxed_ph_fixing(self.relaxed_nonant_buf.value_array(), pre_iter0=False) + return + + def relaxed_ph_fixing(self, relaxed_solution, pre_iter0 = False): + + for k, sub in self.opt.local_subproblems.items(): + raw_fixed_this_iter = 0 + persistent_solver = is_persistent(sub._solver_plugin) + for sn in sub.scen_list: + s = self.opt.local_scenarios[sn] + for ci, (ndn_i, xvar) in enumerate(s._mpisppy_data.nonant_indices.items()): + if ndn_i in self._modeler_fixed_nonants: + continue + if xvar in s._mpisppy_data.all_surrogate_nonants: + continue + relaxed_val = relaxed_solution[ci] + xvar_value = xvar._value + update_var = False + if not pre_iter0 and xvar.fixed: + if (relaxed_val - xvar.lb) > self.bound_tol and (xvar.ub - relaxed_val) > self.bound_tol: + xvar.unfix() + update_var = True + raw_fixed_this_iter -= 1 + if self.debug and self.opt.cylinder_rank == 0: + print(f"{k}: unfixing var {xvar.name}; {relaxed_val=} is off bounds {(xvar.lb, xvar.ub)}") + # in case somebody else unfixs a variable in another rank... + xb = s._mpisppy_model.xbars[ndn_i]._value + if abs(xb - xvar_value) > self.bound_tol: + xvar.unfix() + update_var = True + raw_fixed_this_iter -= 1 + if self.debug and self.opt.cylinder_rank == 0: + print(f"{k}: unfixing var {xvar.name}; xbar {xb} differs from the fixed value {xvar_value}") + elif (relaxed_val - xvar.lb <= self.bound_tol) and (pre_iter0 or (xvar_value - xvar.lb <= self.bound_tol)): + xvar.fix(xvar.lb) + if self.debug and self.opt.cylinder_rank == 0: + print(f"{k}: fixing var {xvar.name} to lb {xvar.lb}; {relaxed_val=}, var value is {xvar_value}") + update_var = True + raw_fixed_this_iter += 1 + elif (xvar.ub - relaxed_val <= self.bound_tol) and (pre_iter0 or (xvar.ub - xvar_value <= self.bound_tol)): + xvar.fix(xvar.ub) + if self.debug and self.opt.cylinder_rank == 0: + print(f"{k}: fixing var {xvar.name} to ub {xvar.ub}; {relaxed_val=}, var value is {xvar_value}") + update_var = True + raw_fixed_this_iter += 1 + + if update_var and persistent_solver: + sub._solver_plugin.update_var(xvar) + + # Note: might count incorrectly with bundling? + self._heuristic_fixed_vars[k] += raw_fixed_this_iter + if self.verbose: + print(f"{k}: total unique vars fixed by heuristic: {int(round(self._heuristic_fixed_vars[k]))}/{self.nonant_length}") diff --git a/mpisppy/generic_cylinders.py b/mpisppy/generic_cylinders.py index bb778656d..3677080e0 100644 --- a/mpisppy/generic_cylinders.py +++ b/mpisppy/generic_cylinders.py @@ -80,8 +80,11 @@ def _parse_args(m): cfg.aph_args() cfg.subgradient_args() cfg.fixer_args() + cfg.relaxed_ph_fixer_args() cfg.integer_relax_then_enforce_args() cfg.gapper_args() + cfg.ph_nonant_args() + cfg.relaxed_ph_args() cfg.fwph_args() cfg.lagrangian_args() cfg.ph_ob_args() @@ -190,6 +193,14 @@ def _do_decomp(module, cfg, scenario_creator, scenario_creator_kwargs, scenario_ rho_setter = rho_setter, all_nodenames = all_nodenames, ) + elif cfg.ph_nonant_hub: + hub_dict = vanilla.ph_nonant_hub(*beans, + scenario_creator_kwargs=scenario_creator_kwargs, + ph_extensions=None, + ph_converger=ph_converger, + rho_setter = rho_setter, + all_nodenames = all_nodenames, + ) else: # Vanilla PH hub hub_dict = vanilla.ph_hub(*beans, @@ -228,6 +239,9 @@ def _do_decomp(module, cfg, scenario_creator, scenario_creator_kwargs, scenario_ if cfg.rc_fixer: vanilla.add_reduced_costs_fixer(hub_dict, cfg) + if cfg.relaxed_ph_fixer: + vanilla.add_relaxed_ph_fixer(hub_dict, cfg) + if cfg.integer_relax_then_enforce: vanilla.add_integer_relax_then_enforce(hub_dict, cfg) @@ -336,7 +350,20 @@ def _do_decomp(module, cfg, scenario_creator, scenario_creator_kwargs, scenario_ vanilla.add_coeff_rho(ph_ob_spoke, cfg) if cfg.sensi_rho: vanilla.add_sensi_rho(ph_ob_spoke, cfg) - + + # relaxed ph spoke + if cfg.relaxed_ph: + relaxed_ph_spoke = vanilla.relaxed_ph_spoke(*beans, + scenario_creator_kwargs=scenario_creator_kwargs, + rho_setter = rho_setter, + all_nodenames = all_nodenames, + ) + if cfg.sep_rho: + vanilla.add_sep_rho(relaxed_ph_spoke, cfg) + if cfg.coeff_rho: + vanilla.add_coeff_rho(relaxed_ph_spoke, cfg) + if cfg.sensi_rho: + vanilla.add_sensi_rho(relaxed_ph_spoke, cfg) # subgradient outer bound spoke if cfg.subgradient: @@ -385,6 +412,8 @@ def _do_decomp(module, cfg, scenario_creator, scenario_creator_kwargs, scenario_ list_of_spoke_dict.append(lagrangian_spoke) if cfg.ph_ob: list_of_spoke_dict.append(ph_ob_spoke) + if cfg.relaxed_ph: + list_of_spoke_dict.append(relaxed_ph_spoke) if cfg.subgradient: list_of_spoke_dict.append(subgradient_spoke) if cfg.xhatshuffle: diff --git a/mpisppy/utils/cfg_vanilla.py b/mpisppy/utils/cfg_vanilla.py index 60648ebc9..dde292844 100644 --- a/mpisppy/utils/cfg_vanilla.py +++ b/mpisppy/utils/cfg_vanilla.py @@ -35,13 +35,15 @@ from mpisppy.cylinders.slam_heuristic import SlamMaxHeuristic, SlamMinHeuristic from mpisppy.cylinders.cross_scen_spoke import CrossScenarioCutSpoke from mpisppy.cylinders.reduced_costs_spoke import ReducedCostsSpoke -from mpisppy.cylinders.hub import PHHub, SubgradientHub, APHHub, FWPHHub +from mpisppy.cylinders.relaxed_ph_spoke import RelaxedPHSpoke +from mpisppy.cylinders.hub import PHNonantHub, PHHub, SubgradientHub, APHHub, FWPHHub from mpisppy.extensions.extension import MultiExtension from mpisppy.extensions.fixer import Fixer 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 from mpisppy.extensions.reduced_costs_rho import ReducedCostsRho +from mpisppy.extensions.relaxed_ph_fixer import RelaxedPHFixer from mpisppy.extensions.sep_rho import SepRho from mpisppy.extensions.coeff_rho import CoeffRho from mpisppy.extensions.sensi_rho import SensiRho @@ -144,6 +146,35 @@ def ph_hub( add_timed_mipgap(hub_dict, cfg) return hub_dict +def ph_nonant_hub( + cfg, + scenario_creator, + scenario_denouement, + all_scenario_names, + scenario_creator_kwargs=None, + ph_extensions=None, + extension_kwargs=None, + ph_converger=None, + rho_setter=None, + variable_probability=None, + all_nodenames=None, +): + hub_dict = ph_hub( + cfg, + scenario_creator, + scenario_denouement, + all_scenario_names, + scenario_creator_kwargs=scenario_creator_kwargs, + ph_extensions=ph_extensions, + extension_kwargs=extension_kwargs, + ph_converger=ph_converger, + rho_setter=rho_setter, + variable_probability=variable_probability, + all_nodenames=all_nodenames, + ) + # use PHNonantHub instead of PHHub + hub_dict["hub_class"] = PHNonantHub + return hub_dict def aph_hub(cfg, scenario_creator, @@ -346,6 +377,18 @@ def add_reduced_costs_fixer(hub_dict, return hub_dict +def add_relaxed_ph_fixer(hub_dict, + cfg, + ): + #WARNING: Do not use without a reduced_costs_spoke spoke + hub_dict = extension_adder(hub_dict, RelaxedPHFixer) + + hub_dict["opt_kwargs"]["options"]["relaxed_ph_fixer_options"] = { + "bound_tol": cfg.relaxed_ph_fixer_tol, + } + + return hub_dict + def add_wxbar_read_write(hub_dict, cfg): """ Add the wxbar read and write extensions to the hub_dict @@ -686,6 +729,44 @@ def subgradient_spoke( return subgradient_spoke +def relaxed_ph_spoke( + cfg, + scenario_creator, + scenario_denouement, + all_scenario_names, + scenario_creator_kwargs=None, + rho_setter=None, + all_nodenames=None, + ph_extensions=None, + extension_kwargs=None, +): + relaxed_ph_spoke = _PHBase_spoke_foundation( + RelaxedPHSpoke, + cfg, + scenario_creator, + scenario_denouement, + all_scenario_names, + scenario_creator_kwargs=scenario_creator_kwargs, + rho_setter=rho_setter, + all_nodenames=all_nodenames, + ph_extensions=ph_extensions, + extension_kwargs=extension_kwargs, + ) + options = relaxed_ph_spoke["opt_kwargs"]["options"] + if cfg.relaxed_ph_rescale_rho_factor is not None: + options["relaxed_ph_rho_factor"] = cfg.relaxed_ph_rescale_rho_factor + + # make sure this spoke doesn't hit the time or iteration limit + options["time_limit"] = None + options["PHIterLimit"] = cfg.max_iterations * 1_000_000 + options["display_progress"] = False + options["display_convergence_detail"] = False + + add_ph_tracking(relaxed_ph_spoke, cfg, spoke=True) + + return relaxed_ph_spoke + + def xhatlooper_spoke( cfg, scenario_creator, diff --git a/mpisppy/utils/config.py b/mpisppy/utils/config.py index 38abcaea8..7ba0d40a1 100644 --- a/mpisppy/utils/config.py +++ b/mpisppy/utils/config.py @@ -461,6 +461,13 @@ def subgradient_args(self): domain=bool, default=False) + def ph_nonant_args(self): + + self.add_to_config(name="ph_nonant_hub", + description="Use PH Hub which only supplies nonants (and not Ws) (default False)", + domain=bool, + default=False) + def fixer_args(self): self.add_to_config('fixer', @@ -471,7 +478,19 @@ def fixer_args(self): self.add_to_config("fixer_tol", description="fixer bounds tolerance (default 1e-4)", domain=float, - default=1e-2) + default=1e-4) + + def relaxed_ph_fixer_args(self): + + self.add_to_config('relaxed_ph_fixer', + description="have a relaxed PH fixer extension ", + domain=bool, + default=False) + + self.add_to_config("relaxed_ph_fixer_tol", + description="relaxed PH fixer bounds tolerance (default 1e-4)", + domain=float, + default=1e-4) def integer_relax_then_enforce_args(self): self.add_to_config('integer_relax_then_enforce', @@ -707,6 +726,18 @@ def ph_ob_args(self): default=False) + def relaxed_ph_args(self): + + self.add_to_config("relaxed_ph", + description="have a relaxed PH spoke", + domain=bool, + default=False) + self.add_to_config("relaxed_ph_rescale_rho_factor", + description="Used to rescale rho initially (default=1.0)", + domain=float, + default=1.0) + + def xhatlooper_args(self): self.add_to_config('xhatlooper',