Source code for implementations.convergence_controller_classes.adaptive_alpha

import numpy as np

from pySDC.core.convergence_controller import ConvergenceController


[docs] class AdaptiveAlpha(ConvergenceController): r""" Choose the ParaDiag :math:`\alpha` adaptively from the residual. ParaDiag replaces the time-stepping matrix by an :math:`\alpha`-circulant approximation, and :math:`\alpha` trades two error sources against each other: a small value approximates the original problem better, but conditions the diagonalization worse, so round-off and inexact inner solves contaminate the result. A single fixed :math:`\alpha` therefore has to be a compromise for the whole run, even though the balance shifts as the residual falls. This convergence controller updates :math:`\alpha` after every iteration instead, following the strategy in `Čaklović et al. <https://doi.org/10.2140/camcos.2023.18.55>`_: .. math:: \gamma = L (3 \epsilon + \tau), \quad \alpha_{k} = \sqrt{\frac{\gamma r_k}{e_k}}, \quad e_{k+1} = 2 \sqrt{\gamma e_k r_k}, with :math:`L` the number of steps in the block, :math:`\epsilon` machine precision, :math:`\tau` the inner solver tolerance, :math:`r_k` the residual and :math:`e_k` a running bound on the error. :math:`\gamma` is the accuracy floor: there is no point pushing :math:`\alpha` below the level at which round-off and the inner solver dominate anyway. The residual is reduced over the whole block, so every rank computes the same :math:`\alpha` and the controllers stay in step. """
[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': +300, # accuracy floor: round-off plus whatever the inner solver leaves behind 'inner_tol': 0.0, # initial bound on the error, before we have seen a residual 'e0': 1.0, # keep alpha in a sane range no matter what the residual does 'alpha_min': 1e-12, 'alpha_max': 1.0, } return {**defaults, **super().setup(controller, params, description, **kwargs)}
[docs] def setup_status_variables(self, controller, **kwargs): """ Start the alpha history, which spans the whole run rather than a single block. Args: controller (pySDC.Controller): The controller Returns: None """ self.alphas = [] return None
[docs] def reset_status_variables(self, controller, **kwargs): """ Reset the error bound at the start of every block. Args: controller (pySDC.Controller): The controller Returns: None """ self.e = self.params.e0 return None
[docs] def get_gamma(self, controller): r""" The accuracy floor :math:`\gamma = L (3 \epsilon + \tau)`. Args: controller (pySDC.Controller): The controller Returns: float: gamma """ eps = np.finfo(complex).eps return controller.n_steps * (3 * eps + self.params.inner_tol)
[docs] def post_iteration_processing_block(self, controller, **kwargs): r""" Compute the next :math:`\alpha` from the residual of the whole block. This is a block hook rather than a per-step one because :math:`\alpha` belongs to the block: it parametrises the transform across the steps, so it has to advance once per iteration no matter how the block was decomposed. Args: controller (pySDC.Controller): The controller Returns: None """ # the residual of the composite problem is the largest one across the block residual = max(step.levels[0].status.residual for step in controller.steps) comm = kwargs.get('comm', None) if comm is not None: residual = comm.allreduce(residual, op=self.MPI_MAX) if residual <= 0: return None gamma = self.get_gamma(controller) alpha = np.sqrt(gamma * residual / self.e) self.e = 2 * np.sqrt(gamma * self.e * residual) controller.params.alpha = min(max(alpha, self.params.alpha_min), self.params.alpha_max) self.debug(f'Set alpha to {controller.params.alpha:.3e} from residual {residual:.3e}', controller.steps[0]) # keep the history around; it is what you want to look at when tuning self.alphas.append(controller.params.alpha) return None
[docs] def dependencies(self, controller, description, **kwargs): """ Prepare the MPI reduction we need for the block residual. 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_datatypes() from mpi4py import MPI self.MPI_MAX = MPI.MAX super().dependencies(controller, description, **kwargs) return None