Coverage for pySDC/implementations/convergence_controller_classes/adaptive_alpha.py: 100%
36 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-25 20:28 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-25 20:28 +0000
1import numpy as np
3from pySDC.core.convergence_controller import ConvergenceController
6class AdaptiveAlpha(ConvergenceController):
7 r"""
8 Choose the ParaDiag :math:`\alpha` adaptively from the residual.
10 ParaDiag replaces the time-stepping matrix by an :math:`\alpha`-circulant approximation, and
11 :math:`\alpha` trades two error sources against each other: a small value approximates the original
12 problem better, but conditions the diagonalization worse, so round-off and inexact inner solves
13 contaminate the result. A single fixed :math:`\alpha` therefore has to be a compromise for the whole
14 run, even though the balance shifts as the residual falls.
16 This convergence controller updates :math:`\alpha` after every iteration instead, following the
17 strategy in `Čaklović et al. <https://doi.org/10.2140/camcos.2023.18.55>`_:
19 .. math::
20 \gamma = L (3 \epsilon + \tau), \quad
21 \alpha_{k} = \sqrt{\frac{\gamma r_k}{e_k}}, \quad
22 e_{k+1} = 2 \sqrt{\gamma e_k r_k},
24 with :math:`L` the number of steps in the block, :math:`\epsilon` machine precision, :math:`\tau`
25 the inner solver tolerance, :math:`r_k` the residual and :math:`e_k` a running bound on the error.
26 :math:`\gamma` is the accuracy floor: there is no point pushing :math:`\alpha` below the level at
27 which round-off and the inner solver dominate anyway.
29 The residual is reduced over the whole block, so every rank computes the same :math:`\alpha` and the
30 controllers stay in step.
31 """
33 def setup(self, controller, params, description, **kwargs):
34 """
35 Define default parameters here.
37 Args:
38 controller (pySDC.Controller): The controller
39 params (dict): The params passed for this specific convergence controller
40 description (dict): The description object used to instantiate the controller
42 Returns:
43 (dict): The updated params dictionary
44 """
45 defaults = {
46 'control_order': +300,
47 # accuracy floor: round-off plus whatever the inner solver leaves behind
48 'inner_tol': 0.0,
49 # initial bound on the error, before we have seen a residual
50 'e0': 1.0,
51 # keep alpha in a sane range no matter what the residual does
52 'alpha_min': 1e-12,
53 'alpha_max': 1.0,
54 }
55 return {**defaults, **super().setup(controller, params, description, **kwargs)}
57 def setup_status_variables(self, controller, **kwargs):
58 """
59 Start the alpha history, which spans the whole run rather than a single block.
61 Args:
62 controller (pySDC.Controller): The controller
64 Returns:
65 None
66 """
67 self.alphas = []
68 return None
70 def reset_status_variables(self, controller, **kwargs):
71 """
72 Reset the error bound at the start of every block.
74 Args:
75 controller (pySDC.Controller): The controller
77 Returns:
78 None
79 """
80 self.e = self.params.e0
81 return None
83 def get_gamma(self, controller):
84 r"""
85 The accuracy floor :math:`\gamma = L (3 \epsilon + \tau)`.
87 Args:
88 controller (pySDC.Controller): The controller
90 Returns:
91 float: gamma
92 """
93 eps = np.finfo(complex).eps
94 return controller.n_steps * (3 * eps + self.params.inner_tol)
96 def post_iteration_processing_block(self, controller, **kwargs):
97 r"""
98 Compute the next :math:`\alpha` from the residual of the whole block.
100 This is a block hook rather than a per-step one because :math:`\alpha` belongs to the block:
101 it parametrises the transform across the steps, so it has to advance once per iteration no
102 matter how the block was decomposed.
104 Args:
105 controller (pySDC.Controller): The controller
107 Returns:
108 None
109 """
110 # the residual of the composite problem is the largest one across the block
111 residual = max(step.levels[0].status.residual for step in controller.steps)
113 comm = kwargs.get('comm', None)
114 if comm is not None:
115 residual = comm.allreduce(residual, op=self.MPI_MAX)
117 if residual <= 0:
118 return None
120 gamma = self.get_gamma(controller)
121 alpha = np.sqrt(gamma * residual / self.e)
122 self.e = 2 * np.sqrt(gamma * self.e * residual)
124 controller.params.alpha = min(max(alpha, self.params.alpha_min), self.params.alpha_max)
125 self.debug(f'Set alpha to {controller.params.alpha:.3e} from residual {residual:.3e}', controller.steps[0])
127 # keep the history around; it is what you want to look at when tuning
128 self.alphas.append(controller.params.alpha)
130 return None
132 def dependencies(self, controller, description, **kwargs):
133 """
134 Prepare the MPI reduction we need for the block residual.
136 Args:
137 controller (pySDC.Controller): The controller
138 description (dict): The description object used to instantiate the controller
140 Returns:
141 None
142 """
143 if self.params.useMPI:
144 self.prepare_MPI_datatypes()
145 from mpi4py import MPI
147 self.MPI_MAX = MPI.MAX
149 super().dependencies(controller, description, **kwargs)
150 return None