Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main' into lagrangian_subgradient
Browse files Browse the repository at this point in the history
  • Loading branch information
bknueven committed Apr 29, 2024
2 parents 5698ee8 + c5a7344 commit f386355
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 98 deletions.
55 changes: 21 additions & 34 deletions mpisppy/cylinders/lagranger_bounder.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,16 @@
import json
import csv
import mpisppy.cylinders.spoke
from mpisppy.cylinders.lagrangian_bounder import _LagrangianMixin

class LagrangerOuterBound(mpisppy.cylinders.spoke.OuterBoundNonantSpoke):
class LagrangerOuterBound(_LagrangianMixin, mpisppy.cylinders.spoke.OuterBoundNonantSpoke):
"""Indepedent Lagrangian that takes x values as input and updates its own W.
"""
converger_spoke_char = 'A'

def lagrangian_prep(self):
verbose = self.opt.options['verbose']
# Scenarios are created here
self.opt.PH_Prep(attach_prox=False)
self.opt._reenable_W()
self.opt._create_solvers()
super().lagrangian_prep()
if "lagranger_rho_rescale_factors_json" in self.opt.options and\
self.opt.options["lagranger_rho_rescale_factors_json"] is not None:
with open(self.opt.options["lagranger_rho_rescale_factors_json"], "r") as fin:
Expand All @@ -35,21 +33,7 @@ def _lagrangian(self, iternum):
if self.rho_rescale_factors is not None\
and iternum in self.rho_rescale_factors:
self._rescale_rho(self.rho_rescale_factors[iternum])
teeme = False
if "tee-rank0-solves" in self.opt.options and self.opt.cylinder_rank == 0:
teeme = self.opt.options['tee-rank0-solves']

self.opt.solve_loop(
solver_options=self.opt.current_solver_options,
dtiming=False,
gripe=True,
tee=teeme,
verbose=verbose
)

# Compute the resulting bound
return self.opt.Ebound(verbose)

return self.lagrangian()

def _rescale_rho(self,rf):
# IMPORTANT: the scalings accumulate.
Expand All @@ -75,11 +59,14 @@ def _write_W_and_xbar(self, iternum):

def _update_weights_and_solve(self, iternum):
# Work with the nonants that we have (and we might not have any yet).
extensions = self.opt.extensions is not None
self.opt._put_nonant_cache(self.localnonants)
self.opt._restore_nonants()
verbose = self.opt.options["verbose"]
self.opt.Compute_Xbar(verbose=verbose)
self.opt.Update_W(verbose=verbose)
if extensions:
self.opt.extobject.miditer()
## writes Ws here
self._write_W_and_xbar(iternum)
return self._lagrangian(iternum)
Expand All @@ -89,30 +76,30 @@ def main(self):
rho_setter = None
if hasattr(self.opt, 'rho_setter'):
rho_setter = self.opt.rho_setter
extensions = self.opt.extensions is not None

self.lagrangian_prep()

if extensions:
self.opt.extobject.pre_iter0()
self.A_iter = 1
self.trivial_bound = self._lagrangian(0)
if extensions:
self.opt.extobject.post_iter0()

self.bound = self.trivial_bound
if extensions:
self.opt.extobject.post_iter0_after_sync()

self.opt.current_solver_options = self.opt.iterk_solver_options

while not self.got_kill_signal():
# because of aph, do not check for new data, just go for it
self.bound = self._update_weights_and_solve(self.A_iter)
bound = self._update_weights_and_solve(self.A_iter)
if extensions:
self.opt.extobject.enditer()
if bound is not None:
self.bound = bound
if extensions:
self.opt.extobject.enditer_after_sync()
self.A_iter += 1

def finalize(self):
'''
Do one final lagrangian pass with the final
PH weights. Useful for when PH convergence
and/or iteration limit is the cause of termination
'''
self.final_bound = self._update_weights_and_solve(self.A_iter)
self.bound = self.final_bound
if self.opt.extensions is not None and \
hasattr(self.opt.extobject, 'post_everything'):
self.opt.extobject.post_everything()
return self.final_bound
44 changes: 27 additions & 17 deletions mpisppy/cylinders/lagrangian_bounder.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
# This software is distributed under the 3-clause BSD License.
import mpisppy.cylinders.spoke

class LagrangianOuterBound(mpisppy.cylinders.spoke.OuterBoundWSpoke):

converger_spoke_char = 'L'
class _LagrangianMixin:

def lagrangian_prep(self):
verbose = self.opt.options['verbose']
Expand Down Expand Up @@ -37,22 +35,18 @@ def lagrangian(self):
on the fact that the OPT thread is solving the same
models, and hence would detect both of those things on its
own--the Lagrangian spoke doesn't need to check again. '''
return self.opt.Ebound(verbose)

def finalize(self):
self.final_bound = self.bound
if self.opt.extensions is not None and \
hasattr(self.opt.extobject, 'post_everything'):
self.opt.extobject.post_everything()
return self.final_bound

# Compute the resulting bound, checking to be sure
# the weights came from the same PH iteration
serial_number = self.get_serial_number()
bound, extra_sums = self.opt.Ebound(verbose, extra_sum_terms=[serial_number])
serial_number_sum = int(round(extra_sums[0]))
class LagrangianOuterBound(_LagrangianMixin, mpisppy.cylinders.spoke.OuterBoundWSpoke):

total = int(self.cylinder_comm.Get_size())*serial_number
if total == serial_number_sum:
return bound
elif self.cylinder_rank == 0:
# TODO: this whole check can probably be removed as its done
# within `got_kill_signal`. Leaving it for now as an
# additional check.
raise RuntimeError("Lagrangian spokes unexpectly out of snyc")
return None
converger_spoke_char = 'L'

def _set_weights_and_solve(self):
self.opt.W_from_flat_list(self.localWs) # Sets the weights
Expand All @@ -64,22 +58,36 @@ def main(self):
if hasattr(self.opt, 'rho_setter'):
rho_setter = self.opt.rho_setter
verbose = self.opt.options['verbose']
extensions = self.opt.extensions is not None

self.lagrangian_prep()

if extensions:
self.opt.extobject.pre_iter0()
self.dk_iter = 1
self.trivial_bound = self.lagrangian()
if extensions:
self.opt.extobject.post_iter0()

self.opt.current_solver_options = self.opt.iterk_solver_options

self.bound = self.trivial_bound
if extensions:
self.opt.extobject.post_iter0_after_sync()

while not self.got_kill_signal():
if self.new_Ws:
if extensions:
self.opt.extobject.miditer()
bound = self._set_weights_and_solve()
if extensions:
self.opt.extobject.enditer()
if bound is not None:
self.bound = bound
if extensions:
self.opt.extobject.enditer_after_sync()
self.dk_iter += 1
<<<<<<< HEAD
elif self.opt.options.get("subgradient_while_waiting", False):
# compute a subgradient step
self.opt.Compute_Xbar(verbose)
Expand All @@ -94,3 +102,5 @@ def finalize(self):
hasattr(self.opt.extobject, 'post_everything'):
self.opt.extobject.post_everything()
return self.final_bound
=======
>>>>>>> upstream/main
69 changes: 22 additions & 47 deletions mpisppy/cylinders/subgradient_bounder.py
Original file line number Diff line number Diff line change
@@ -1,77 +1,52 @@
# Copyright 2020 by B. Knueven, D. Mildebrath, C. Muir, J-P Watson, and D.L. Woodruff
# This software is distributed under the 3-clause BSD License.
import mpisppy.cylinders.spoke
from mpisppy.cylinders.lagrangian_bounder import _LagrangianMixin

class SubgradientOuterBound(mpisppy.cylinders.spoke.OuterBoundSpoke):
class SubgradientOuterBound(_LagrangianMixin, mpisppy.cylinders.spoke.OuterBoundSpoke):

converger_spoke_char = 'G'

def lagrangian_prep(self):
verbose = self.opt.options['verbose']
# Split up PH_Prep? Prox option is important for APH.
# Seems like we shouldn't need the Lagrangian stuff, so attach_prox=False
# Scenarios are created here
self.opt.PH_Prep(attach_prox=False)
self.opt._reenable_W()
self.opt._create_solvers()

def lagrangian(self):
verbose = self.opt.options['verbose']
# This is sort of a hack, but might help folks:
if "ipopt" in self.opt.options["solver_name"]:
print("\n WARNING: An ipopt solver will not give outer bounds\n")
teeme = False
if "tee-rank0-solves" in self.opt.options:
teeme = self.opt.options['tee-rank0-solves']

self.opt.solve_loop(
solver_options=self.opt.current_solver_options,
dtiming=False,
gripe=True,
tee=teeme,
verbose=verbose
)
''' DTM (dlw edits): This is where PHBase Iter0 checks for scenario
probabilities that don't sum to one and infeasibility and
will send a kill signal if needed. For now we are relying
on the fact that the OPT thread is solving the same
models, and hence would detect both of those things on its
own--the Lagrangian spoke doesn't need to check again. '''
return self.opt.Ebound(verbose)

def main(self):
# The rho_setter should be attached to the opt object
rho_setter = None
if hasattr(self.opt, 'rho_setter'):
rho_setter = self.opt.rho_setter
extensions = self.opt.extensions is not None
verbose = self.opt.options['verbose']

self.lagrangian_prep()

if extensions:
self.opt.extobject.pre_iter0()
self.dk_iter = 1
self.trivial_bound = self.lagrangian()
if extensions:
self.opt.extobject.post_iter0()

self.bound = self.trivial_bound
if extensions:
self.opt.extobject.post_iter0_after_sync()

self.opt.current_solver_options = self.opt.iterk_solver_options

# update rho / alpha
if self.opt.options.get('subgradient_rho_multiplier') is not None:
rf = self.opt.options['subgradient_rho_multiplier']
for scenario in self.opt.local_scenarios.values():
for ndn_i in scenario._mpisppy_model.rho:
scenario._mpisppy_model.rho[ndn_i] *= rf

self.trivial_bound = self.lagrangian()

self.opt.current_solver_options = self.opt.iterk_solver_options

self.bound = self.trivial_bound

while not self.got_kill_signal():
# compute a subgradient step
self.opt.Compute_Xbar(verbose)
self.opt.Update_W(verbose)
if extensions:
self.opt.extobject.miditer()
bound = self.lagrangian()
if extensions:
self.opt.extobject.enditer()
if bound is not None:
self.bound = bound

def finalize(self):
self.final_bound = self.bound
if self.opt.extensions is not None and \
hasattr(self.opt.extobject, 'post_everything'):
self.opt.extobject.post_everything()
return self.final_bound
if extensions:
self.opt.extobject.enditer_after_sync()

0 comments on commit f386355

Please sign in to comment.