import numpy as np
from pySDC.core.convergence_controller import ConvergenceController
[docs]
class CheckConvergence(ConvergenceController):
"""
Perform simple checks on convergence for SDC iterations.
Iteration is terminated via one of two criteria:
- Residual tolerance
- Maximum number of iterations
"""
[docs]
def setup(self, controller, params, description, **kwargs):
"""
Define default parameters here
Args:
controller (pySDC.Controller): The controller
params (dict): The params passed for this specific convergence controller
description (dict): The description object used to instantiate the controller
Returns:
(dict): The updated params dictionary
"""
defaults = {'control_order': +200, 'use_e_tol': 'e_tol' in description['level_params'].keys()}
return {**defaults, **super().setup(controller, params, description, **kwargs)}
[docs]
def dependencies(self, controller, description, **kwargs):
"""
Load the embedded error estimator if needed.
Args:
controller (pySDC.Controller): The controller
description (dict): The description object used to instantiate the controller
Returns:
None
"""
if self.params.useMPI:
self.prepare_MPI_logical_operations()
super().dependencies(controller, description)
if self.params.use_e_tol:
from pySDC.implementations.convergence_controller_classes.estimate_embedded_error import (
EstimateEmbeddedError,
)
controller.add_convergence_controller(
EstimateEmbeddedError,
description=description,
)
return None
[docs]
@staticmethod
def check_convergence(S, self=None):
"""
Check the convergence of a single step.
Test the residual and max. number of iterations as well as allowing overrides to both stop and continue.
Args:
S (pySDC.Step): The current step
Returns:
bool: Convergence status of the step
"""
# do all this on the finest level
L = S.levels[0]
# get residual and check against prescribed tolerance (plus check number of iterations)
iter_converged = S.status.iter >= S.params.maxiter
res_converged = L.status.residual <= L.params.restol and (S.status.iter > 0 or L.status.sweep > 0)
e_tol_converged = (
L.status.increment < L.params.e_tol if (L.params.get('e_tol') and L.status.get('increment')) else False
)
converged = (
iter_converged or res_converged or e_tol_converged or S.status.force_done
) and not S.status.force_continue
if converged is None:
converged = False
# print information for debugging
if converged and self:
self.debug(
f'Declared convergence: maxiter reached[{"x" if iter_converged else " "}] restol reached[{"x" if res_converged else " "}] e_tol reached[{"x" if e_tol_converged else " "}]',
S,
)
return converged
[docs]
def check_iteration_status(self, controller, S, **kwargs):
"""
Routine to determine whether to stop iterating (currently testing the residual + the max. number of iterations)
Args:
controller (pySDC.Controller.controller): The controller
S (pySDC.Step.step): The current step
Returns:
None
"""
S.status.done = self.check_convergence(S, self)
if "comm" in kwargs.keys():
self.communicate_convergence(controller, S, **kwargs)
S.status.force_continue = False
return None
[docs]
def communicate_convergence(self, controller, S, comm=None, **kwargs):
"""
Share convergence status across the block.
Two ways to stop, and the same two whether the block is spread over ranks or sitting in one
process. Either every step has to agree, which is a reduction, or each step waits on its
predecessor, which is a cascade and is what lets an early step finish and drop out.
The only difference between the transports is how the neighbour is reached: an `allreduce`
and point-to-point messages with one step per rank, reading the other steps' status directly
when they are all here. Note the reduction is applied step by step in the second case rather
than all at once as `allreduce` does -- both operations are monotone, so the two agree.
Args:
controller (pySDC.Controller): The controller
S (pySDC.Step.step): The current step
comm (mpi4py.MPI.Intracomm): Communicator, or None when the whole block is in one process
Returns:
None
"""
block = kwargs.get('MS', controller.steps)
if controller.params.all_to_done:
for hook in controller.hooks:
hook.pre_comm(step=S, level_number=0)
if comm is None:
S.status.done = all(T.status.done for T in block)
S.status.force_done = any(T.status.force_done for T in block)
else:
S.status.done = comm.allreduce(sendobj=S.status.done, op=self.MPI_LAND)
S.status.force_done = comm.allreduce(sendobj=S.status.force_done, op=self.MPI_LOR)
for hook in controller.hooks:
hook.post_comm(step=S, level_number=0, add_to_stats=True)
S.status.done = S.status.done or S.status.force_done
else:
if comm is None:
if not S.status.first:
for hook in controller.hooks:
hook.pre_comm(step=S, level_number=0)
S.status.prev_done = S.prev.status.done
for hook in controller.hooks:
hook.post_comm(step=S, level_number=0, add_to_stats=True)
S.status.done = S.status.done and S.status.prev_done
return None
for hook in controller.hooks:
hook.pre_comm(step=S, level_number=0)
# check if an open request of the status send is pending
controller.wait_for_request(request=controller.req_status)
if S.status.force_done:
return None
# recv status
if not S.status.first and not S.status.prev_done:
buff = np.empty(1, dtype=bool)
self.Recv(comm, source=S.status.slot - 1, buffer=[buff, self.MPI_BOOL])
S.status.prev_done = buff[0]
S.status.done = S.status.done and S.status.prev_done
# send status forward
if not S.status.last:
buff = np.empty(1, dtype=bool)
buff[0] = S.status.done
self.Send(comm, dest=S.status.slot + 1, buffer=[buff, self.MPI_BOOL])
for hook in controller.hooks:
hook.post_comm(step=S, level_number=0, add_to_stats=True)