Skip to content

Commit

Permalink
initial implementation of IntegerRelaxThenEnforce
Browse files Browse the repository at this point in the history
  • Loading branch information
bknueven committed Dec 2, 2024
1 parent df452aa commit 43e7559
Show file tree
Hide file tree
Showing 10 changed files with 98 additions and 8 deletions.
1 change: 1 addition & 0 deletions mpisppy/cylinders/xhatlooper_bounder.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def main(self):
logger.debug(f' *got a new one! on rank {self.global_rank}')
logger.debug(f' *localnonants={str(self.localnonants)}')

self.opt._restore_original_fixedness()
self.opt._put_nonant_cache(self.localnonants)
self.opt._restore_nonants()
upperbound, srcsname = xhatter.xhat_looper(scen_limit=scen_limit, restore_nonants=False)
Expand Down
1 change: 1 addition & 0 deletions mpisppy/cylinders/xhatshufflelooper_bounder.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ def _vb(msg):
logger.debug(f' *localnonants={str(self.localnonants)}')

# update the caches
self.opt._restore_original_fixedness()
self.opt._put_nonant_cache(self.localnonants)
self.opt._restore_nonants()

Expand Down
1 change: 1 addition & 0 deletions mpisppy/cylinders/xhatspecific_bounder.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ def main(self):
format(global_rank))
logging.debug(' localnonants={}'.format(str(self.localnonants)))

self.opt._restore_original_fixedness()
self.opt._put_nonant_cache(self.localnonants) # don't really need all caches
self.opt._restore_nonants()
innerbound = xhatter.xhat_tryit(xhat_scenario_dict, restore_nonants=False)
Expand Down
1 change: 1 addition & 0 deletions mpisppy/cylinders/xhatxbar_bounder.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ def main(self):
logging.debug(' and its new! on global rank {}'.\
format(global_rank))
logging.debug(' localnonants={}'.format(str(self.localnonants)))
self.opt._restore_original_fixedness()
self.opt._put_nonant_cache(self.localnonants) # don't really need all caches
self.opt._restore_nonants()
innerbound = xhatter.xhat_tryit(restore_nonants=False)
Expand Down
63 changes: 63 additions & 0 deletions mpisppy/extensions/integer_relax_then_enforce.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
###############################################################################
# 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.
###############################################################################

import time
import pyomo.environ as pyo
import mpisppy.extensions.extension
from mpisppy.utils.sputils import is_persistent
from mpisppy import global_toc

class IntegerRelaxThenEnforce(mpisppy.extensions.extension.Extension):
""" Class for relaxing integer variables, running PH, and then
enforcing the integality constraints after some condition.
"""

def __init__(self, opt):
super().__init__(opt)
self.integer_relaxer = pyo.TransformationFactory('core.relax_integer_vars')
options = opt.options.get("integer_relax_then_enforce_options", {})
# fraction of iterations or time to spend in relaxed mode
self.ratio = options.get("ratio", 0.5)


def pre_iter0(self):
global_toc(f"{self.__class__.__name__}: relaxing integrality constraints", self.opt.cylinder_rank == 0)
for s in self.opt.local_scenarios.values():
self.integer_relaxer.apply_to(s)
self._integers_relaxed = True

def _unrelax_integers(self):
for sub in self.opt.local_subproblems.values():
for sn in sub.scen_list:
s = self.opt.local_scenarios[sn]
subproblem_solver = sub._solver_plugin
vlist = None
if is_persistent(subproblem_solver):
vlist = list(v for v,d in s._relaxed_integer_vars[None].values())
self.integer_relaxer.apply_to(s, options={"undo":True})
if is_persistent(subproblem_solver):
for v in vlist:
subproblem_solver.update_var(v)
self._integers_relaxed = False

def miditer(self):
if not self._integers_relaxed:
return
# time is running out
if self.opt.options["time_limit"] is not None and ( time.perf_counter() - self.opt.start_time ) > (self.opt.options["time_limit"] * self.ratio):
global_toc(f"{self.__class__.__name__}: enforcing integrality constraints, ran so far for more than {self.opt.options['time_limit']*self.ratio} seconds", self.opt.cylinder_rank == 0)
self._unrelax_integers()
# iterations are running out
if self.opt._PHIter > self.opt.options["PHIterLimit"] * self.ratio:
global_toc(f"{self.__class__.__name__}: enforcing integrality constraints, ran so far for {self.opt._PHIter - 1} iterations", self.opt.cylinder_rank == 0)
self._unrelax_integers()
# nearly converged
if self.opt.conv < (self.opt.options["convthresh"] * 1.1):
global_toc(f"{self.__class__.__name__}: Enforcing integrality constraints, PH is nearly converged", self.opt.cylinder_rank == 0)
self._unrelax_integers()
10 changes: 5 additions & 5 deletions mpisppy/extensions/xhatbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,9 @@ def _try_one(self, snamedict, solver_options=None, verbose=False,
sputils.reactivate_objs(s)
# if you hit infeas, return None
if not pyo.check_optimal_termination(results):
self.opt._restore_nonants()
return None
if restore_nonants:
self.opt._restore_nonants()
return None

# feasible xhat found, so finish up 2EF part and return
if verbose and src_rank == self.cylinder_rank:
Expand All @@ -219,9 +220,8 @@ def _try_one(self, snamedict, solver_options=None, verbose=False,

infeasP = self.opt.infeas_prob()
if infeasP != 0.:
# restoring does no harm
# if this solution is infeasible
self.opt._restore_nonants()
if restore_nonants:
self.opt._restore_nonants()
return None
else:
if verbose and src_rank == self.cylinder_rank:
Expand Down
5 changes: 2 additions & 3 deletions mpisppy/extensions/xhatxbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,8 @@ def _vb(msg):

infeasP = self.opt.infeas_prob()
if infeasP != 0.:
# restoring does no harm
# if this solution is infeasible
self.opt._restore_nonants()
if restore_nonants:
self.opt._restore_nonants()
return None
else:
if verbose and self.cylinder_rank == 0:
Expand Down
4 changes: 4 additions & 0 deletions mpisppy/generic_cylinders.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def _parse_args(m):
cfg.ph_args()
cfg.aph_args()
cfg.fixer_args()
cfg.integer_relax_then_enforce_args()
cfg.gapper_args()
cfg.fwph_args()
cfg.lagrangian_args()
Expand Down Expand Up @@ -171,6 +172,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.integer_relax_then_enforce:
vanilla.add_integer_relax_then_enforce(hub_dict, cfg)

if cfg.grad_rho:
ext_classes.append(Gradient_extension)
hub_dict['opt_kwargs']['options']['gradient_extension_options'] = {'cfg': cfg}
Expand Down
8 changes: 8 additions & 0 deletions mpisppy/utils/cfg_vanilla.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from mpisppy.cylinders.hub import APHHub
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
Expand Down Expand Up @@ -204,6 +205,13 @@ def add_fixer(hub_dict,
"id_fix_list_fct": cfg.id_fix_list_fct}
return hub_dict

def add_integer_relax_then_enforce(hub_dict,
cfg,
):
hub_dict = extension_adder(hub_dict,IntegerRelaxThenEnforce)
hub_dict["opt_kwargs"]["options"]["integer_relax_then_enforce_options"] = {"ratio":cfg.integer_relax_then_enforce_ratio}
return hub_dict

def add_reduced_costs_rho(hub_dict, cfg):
hub_dict = extension_adder(hub_dict,ReducedCostsRho)
hub_dict["opt_kwargs"]["options"]["reduced_costs_rho_options"] = {"multiplier" : cfg.reduced_costs_rho_multiplier, "cfg": cfg}
Expand Down
12 changes: 12 additions & 0 deletions mpisppy/utils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,18 @@ def fixer_args(self):
domain=float,
default=1e-2)

def integer_relax_then_enforce_args(self):
self.add_to_config('integer_relax_then_enforce',
description="have an integer relax then enforce extensions",
domain=bool,
default=False)

self.add_to_config('integer_relax_then_enforce_ratio',
description="fraction of time limit or iterations (whichever is sooner) "
"to spend with relaxed integers",
domain=float,
default=0.5)

def reduced_costs_rho_args(self):
self.add_to_config("reduced_costs_rho",
description="have a ReducedCostsRho extension",
Expand Down

0 comments on commit 43e7559

Please sign in to comment.