Coverage for pySDC/implementations/convergence_controller_classes/adaptivity.py: 93%
257 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-17 13:41 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-17 13:41 +0000
1import numpy as np
2from pySDC.core.convergence_controller import ConvergenceController
3from pySDC.implementations.convergence_controller_classes.step_size_limiter import (
4 StepSizeLimiter,
5)
8class AdaptivityBase(ConvergenceController):
9 """
10 Abstract base class for adaptivity from arbitrary local error estimates and step size update rules.
11 """
13 def setup(self, controller, params, description, **kwargs):
14 """
15 Define default parameters here.
17 Default parameters are:
18 - control_order (int): The order relative to other convergence controllers
19 - beta (float): The safety factor
21 Args:
22 controller (pySDC.Controller): The controller
23 params (dict): The params passed for this specific convergence controller
24 description (dict): The description object used to instantiate the controller
26 Returns:
27 (dict): The updated params dictionary
28 """
29 defaults = {
30 "control_order": -50,
31 "beta": 0.9,
32 }
33 from pySDC.implementations.hooks.log_step_size import LogStepSize
35 controller.add_hook(LogStepSize)
37 return {**defaults, **super().setup(controller, params, description, **kwargs)}
39 def dependencies(self, controller, description, **kwargs):
40 """
41 Load step size limiters here, if they are desired.
43 Args:
44 controller (pySDC.Controller): The controller
45 description (dict): The description object used to instantiate the controller
47 Returns:
48 None
49 """
50 step_limiter_keys = ['dt_min', 'dt_max', 'dt_slope_min', 'dt_slope_max', 'dt_rel_min_slope']
51 available_keys = [me for me in step_limiter_keys if me in self.params.__dict__.keys()]
53 if len(available_keys) > 0:
54 step_limiter_params = {key: self.params.__dict__[key] for key in available_keys}
55 controller.add_convergence_controller(StepSizeLimiter, params=step_limiter_params, description=description)
57 if self.params.useMPI:
58 self.prepare_MPI_logical_operations()
60 return None
62 def get_new_step_size(self, controller, S, **kwargs):
63 """
64 Determine a step size for the next step from an estimate of the local error of the current step.
66 Args:
67 controller (pySDC.Controller): The controller
68 S (pySDC.Step): The current step
70 Returns:
71 None
72 """
73 raise NotImplementedError("Please implement a rule for updating the step size!")
75 def compute_optimal_step_size(self, beta, dt, e_tol, e_est, order):
76 """
77 Compute the optimal step size for the current step based on the order of the scheme.
78 This function can be called from `get_new_step_size` for various implementations of adaptivity, but notably not
79 all! We require to know the order of the error estimate and if we do adaptivity based on the residual, for
80 instance, we do not know that and we can't use this function.
82 Args:
83 beta (float): Safety factor
84 dt (float): Current step size
85 e_tol (float): The desired tolerance
86 e_est (float): The estimated local error
87 order (int): The order of the local error estimate
89 Returns:
90 float: The optimal step size
91 """
92 return beta * dt * (e_tol / e_est) ** (1.0 / order)
94 def get_local_error_estimate(self, controller, S, **kwargs):
95 """
96 Get the local error estimate for updating the step size.
97 It does not have to be an error estimate, but could be the residual or something else.
99 Args:
100 controller (pySDC.Controller): The controller
101 S (pySDC.Step): The current step
103 Returns:
104 float: The error estimate
105 """
106 raise NotImplementedError("Please implement a way to get the local error")
108 def determine_restart(self, controller, S, **kwargs):
109 """
110 Check if the step wants to be restarted by comparing the estimate of the local error to a preset tolerance
112 Args:
113 controller (pySDC.Controller): The controller
114 S (pySDC.Step): The current step
116 Returns:
117 None
118 """
119 if S.status.iter >= S.params.maxiter:
120 e_est = self.get_local_error_estimate(controller, S)
121 if e_est >= self.params.e_tol:
122 # see if we try to avoid restarts
123 if self.params.get('avoid_restarts'):
124 more_iter_needed = max([L.status.iter_to_convergence for L in S.levels])
125 k_final = S.status.iter + more_iter_needed
126 rho = max([L.status.contraction_factor for L in S.levels])
127 coll_order = S.levels[0].sweep.coll.order
129 if rho > 1:
130 S.status.restart = True
131 self.log(f"Convergence factor = {rho:.2e} > 1 -> restarting", S)
132 elif k_final > 2 * S.params.maxiter:
133 S.status.restart = True
134 self.log(
135 f"{more_iter_needed} more iterations needed for convergence -> restart is more efficient", S
136 )
137 elif k_final > coll_order:
138 S.status.restart = True
139 self.log(
140 f"{more_iter_needed} more iterations needed for convergence -> restart because collocation problem would be over resolved",
141 S,
142 )
143 else:
144 S.status.force_continue = True
145 self.log(f"{more_iter_needed} more iterations needed for convergence -> no restart", S)
146 else:
147 S.status.restart = True
148 self.log(f"Restarting: e={e_est:.2e} >= e_tol={self.params.e_tol:.2e}", S)
150 return None
153class AdaptivityForConvergedCollocationProblems(AdaptivityBase):
154 """
155 Base class for adaptivity from error estimates that need a converged collocation problem, restarting if it is not.
156 """
158 def dependencies(self, controller, description, **kwargs):
159 """
160 Load interpolation between restarts.
162 Args:
163 controller (pySDC.Controller): The controller
164 description (dict): The description object used to instantiate the controller
166 Returns:
167 None
168 """
169 super().dependencies(controller, description, **kwargs)
171 if self.params.interpolate_between_restarts:
172 from pySDC.implementations.convergence_controller_classes.interpolate_between_restarts import (
173 InterpolateBetweenRestarts,
174 )
176 controller.add_convergence_controller(
177 InterpolateBetweenRestarts,
178 description=description,
179 params={},
180 )
181 if self.params.interpolate_between_restarts:
182 self.interpolator = controller.convergence_controllers[-1]
183 return None
185 def get_convergence(self, controller, S, **kwargs):
186 """
187 Check if the collocation problem is converged. Needs to be implemented by the child class.
189 Args:
190 controller (pySDC.Controller): The controller
191 S (pySDC.Step): The current step
193 Returns:
194 bool: Whether the collocation problem is converged
195 """
196 raise NotImplementedError("Please implement a way to check if the collocation problem is converged!")
198 def setup(self, controller, params, description, **kwargs):
199 """
200 Add a default value for control order to the parameters.
202 Args:
203 controller (pySDC.Controller): The controller
204 params (dict): Parameters for the convergence controller
205 description (dict): The description object used to instantiate the controller
207 Returns:
208 dict: Updated parameters
209 """
210 defaults = {
211 'restol_rel': None,
212 'e_tol_rel': None,
213 'restart_at_maxiter': True,
214 'restol_min': 1e-12,
215 'restol_max': 1e-5,
216 'e_tol_min': 1e-10,
217 'e_tol_max': 1e-5,
218 'factor_if_not_converged': 4.0,
219 'residual_max_tol': 1e9,
220 'interpolate_between_restarts': True,
221 'abort_at_growing_residual': True,
222 **super().setup(controller, params, description, **kwargs),
223 }
224 if defaults['restol_rel']:
225 description['level_params']['restol'] = min(
226 [max([defaults['restol_rel'] * defaults['e_tol'], defaults['restol_min']]), defaults['restol_max']]
227 )
228 elif defaults['e_tol_rel']:
229 description['level_params']['e_tol'] = min(
230 [max([defaults['e_tol_rel'] * defaults['e_tol'], defaults['e_tol_min']]), defaults['e_tol_max']]
231 )
233 self.res_last_iter = np.inf
235 return defaults
237 def determine_restart(self, controller, S, **kwargs):
238 """
239 Once the collocation problem is converged, restart with a smaller step size if the residual is still above
240 ``restol`` (with ``restart_at_maxiter``, unless the increment is below ``e_tol``), or restart if the local error
241 estimate exceeds ``e_tol``. Before convergence, restart with a smaller step size if the residual
242 grows between iterations (only without parallelism across steps and with ``abort_at_growing_residual``) or
243 exceeds ``residual_max_tol``.
245 Args:
246 controller (pySDC.Controller): The controller
247 S (pySDC.Step): The current step
249 Returns:
250 None
251 """
252 if self.get_convergence(controller, S, **kwargs):
253 self.res_last_iter = np.inf
255 L = S.levels[0]
256 e_tol_converged = (
257 L.status.increment < L.params.e_tol if (L.params.get('e_tol') and L.status.get('increment')) else False
258 )
260 if (
261 self.params.restart_at_maxiter
262 and S.levels[0].status.residual > S.levels[0].params.restol
263 and not e_tol_converged
264 ):
265 self.trigger_restart_upon_nonconvergence(S)
266 elif self.get_local_error_estimate(controller, S, **kwargs) > self.params.e_tol:
267 S.status.restart = True
268 elif (
269 S.status.time_size == 1
270 and self.res_last_iter < S.levels[0].status.residual
271 and S.status.iter > 0
272 and self.params.abort_at_growing_residual
273 ):
274 self.trigger_restart_upon_nonconvergence(S)
275 elif S.levels[0].status.residual > self.params.residual_max_tol:
276 self.trigger_restart_upon_nonconvergence(S)
278 if self.params.useMPI:
279 # borrow the reduction rather than storing it on the instance: an unbound function in an
280 # instance attribute shadows the hook of the same name, and needs an explicit `self` that
281 # nothing else in this API does
282 from pySDC.core.check_convergence import CheckConvergence
284 CheckConvergence.communicate_convergence(self, controller, S, **kwargs)
286 self.res_last_iter = S.levels[0].status.residual * 1.0
288 def trigger_restart_upon_nonconvergence(self, S):
289 """
290 Restart the step and stop iterating, dividing the step size on all levels by ``factor_if_not_converged``. No
291 interpolation is done for the restarted step.
293 Args:
294 S (pySDC.Step): The current step
296 Returns:
297 None
298 """
299 S.status.restart = True
300 S.status.force_done = True
301 for L in S.levels:
302 L.status.dt_new = L.params.dt / self.params.factor_if_not_converged
303 self.log(
304 f'Collocation problem not converged. Reducing step size to {L.status.dt_new:.2e}',
305 S,
306 )
307 if self.params.interpolate_between_restarts:
308 self.interpolator.status.skip_interpolation = True
311class Adaptivity(AdaptivityBase):
312 """
313 Class to compute time step size adaptively based on embedded error estimate.
315 We have a version working in non-MPI pipelined SDC, but Adaptivity requires you to know the order of the scheme,
316 which you can also know for block-Jacobi, but it works differently and it is only implemented for block
317 Gauss-Seidel so far.
319 There is an option to reduce restarts if continued iterations could yield convergence in fewer iterations than
320 restarting based on an estimate of the contraction factor.
321 Since often only one or two more iterations suffice, this can boost efficiency of adaptivity significantly.
322 Notice that the computed step size is not effected.
323 Be aware that this does not work when Hot Rod is enabled, since that requires us to know the order of the scheme in
324 more detail. Since we reset to the second to last sweep before moving on, we cannot continue to iterate.
325 Set the reduced restart up by setting a boolean value for "avoid_restarts" in the parameters for the convergence
326 controller.
327 The behaviour in multi-step SDC is not well studied and it is unclear if anything useful happens there.
328 """
330 def setup(self, controller, params, description, **kwargs):
331 """
332 Define default parameters here.
334 Default parameters are:
335 - control_order (int): The order relative to other convergence controllers
336 - beta (float): The safety factor
338 Args:
339 controller (pySDC.Controller): The controller
340 params (dict): The params passed for this specific convergence controller
341 description (dict): The description object used to instantiate the controller
343 Returns:
344 (dict): The updated params dictionary
345 """
346 defaults = {
347 "embedded_error_flavor": 'standard',
348 "rel_error": False,
349 }
350 return {**defaults, **super().setup(controller, params, description, **kwargs)}
352 def dependencies(self, controller, description, **kwargs):
353 """
354 Load the embedded error estimator.
356 Args:
357 controller (pySDC.Controller): The controller
358 description (dict): The description object used to instantiate the controller
360 Returns:
361 None
362 """
363 from pySDC.implementations.convergence_controller_classes.estimate_embedded_error import EstimateEmbeddedError
365 super().dependencies(controller, description, **kwargs)
367 controller.add_convergence_controller(
368 EstimateEmbeddedError.get_implementation(self.params.embedded_error_flavor, self.params.useMPI),
369 description=description,
370 params={
371 'rel_error': self.params.rel_error,
372 },
373 )
375 # load contraction factor estimator if necessary
376 if self.params.get('avoid_restarts'):
377 from pySDC.implementations.convergence_controller_classes.estimate_contraction_factor import (
378 EstimateContractionFactor,
379 )
381 params = {'e_tol': self.params.e_tol}
382 controller.add_convergence_controller(EstimateContractionFactor, description=description, params=params)
383 return None
385 def check_parameters(self, controller, params, description, **kwargs):
386 """
387 Check whether parameters are compatible with whatever assumptions went into the step size functions etc.
388 For adaptivity, we need to know the order of the scheme.
390 Args:
391 controller (pySDC.Controller): The controller
392 params (dict): The params passed for this specific convergence controller
393 description (dict): The description object used to instantiate the controller
395 Returns:
396 bool: Whether the parameters are compatible
397 str: The error message
398 """
399 if description["level_params"].get("restol", -1.0) >= 0:
400 return (
401 False,
402 "Adaptivity needs constant order in time and hence restol in the step parameters has to be \
403smaller than 0!",
404 )
406 if controller.params.mssdc_jac:
407 return (
408 False,
409 "Adaptivity needs the same order on all steps, please activate Gauss-Seidel multistep mode!",
410 )
412 if "e_tol" not in params.keys():
413 return (
414 False,
415 "Adaptivity needs a local tolerance! Please pass `e_tol` to the parameters for this convergence controller!",
416 )
418 return True, ""
420 def get_new_step_size(self, controller, S, **kwargs):
421 """
422 Determine a step size for the next step from an embedded estimate of the local error of the current step.
424 Args:
425 controller (pySDC.Controller): The controller
426 S (pySDC.Step): The current step
428 Returns:
429 None
430 """
431 # check if we performed the desired amount of sweeps
432 if S.status.iter == S.params.maxiter:
433 L = S.levels[0]
435 # compute next step size
436 order = S.status.iter # embedded error estimate is same order as time marching
438 e_est = self.get_local_error_estimate(controller, S)
439 L.status.dt_new = self.compute_optimal_step_size(
440 self.params.beta, L.params.dt, self.params.e_tol, e_est, order
441 )
442 self.log(f'Adjusting step size from {L.params.dt:.2e} to {L.status.dt_new:.2e}', S)
444 return None
446 def get_local_error_estimate(self, controller, S, **kwargs):
447 """
448 Get the embedded error estimate of the finest level of the step.
450 Args:
451 controller (pySDC.Controller): The controller
452 S (pySDC.Step): The current step
454 Returns:
455 float: Embedded error estimate
456 """
457 return S.levels[0].status.error_embedded_estimate
460class AdaptivityRK(Adaptivity):
461 """
462 Adaptivity for Runge-Kutta methods. Basically, we need to change the order in the step size update
463 """
465 def setup(self, controller, params, description, **kwargs):
466 """
467 Add the order of the embedded error estimate of the Runge-Kutta method, from the sweeper's ``get_update_order``,
468 as default for the parameter ``update_order``.
470 Args:
471 controller (pySDC.Controller): The controller
472 params (dict): The params passed for this specific convergence controller
473 description (dict): The description object used to instantiate the controller
475 Returns:
476 (dict): The updated params dictionary
477 """
478 defaults = {}
479 defaults['update_order'] = params.get('update_order', description['sweeper_class'].get_update_order())
480 return {**defaults, **super().setup(controller, params, description, **kwargs)}
482 def get_new_step_size(self, controller, S, **kwargs):
483 """
484 Determine a step size for the next step from an embedded estimate of the local error of the current step.
486 Args:
487 controller (pySDC.Controller): The controller
488 S (pySDC.Step): The current step
490 Returns:
491 None
492 """
493 # check if we performed the desired amount of sweeps
494 if S.status.iter == S.params.maxiter:
495 L = S.levels[0]
497 # compute next step size
498 order = self.params.update_order
499 e_est = self.get_local_error_estimate(controller, S)
500 L.status.dt_new = self.compute_optimal_step_size(
501 self.params.beta, L.params.dt, self.params.e_tol, e_est, order
502 )
503 self.log(f'Adjusting step size from {L.params.dt:.2e} to {L.status.dt_new:.2e}', S)
505 return None
508class AdaptivityResidual(AdaptivityBase):
509 """
510 Do adaptivity based on residual.
512 Since we don't know a correlation between the residual and the error (for nonlinear problems), we employ a simpler
513 rule to update the step size. Instead of giving a local tolerance that we try to hit as closely as possible, we set
514 two thresholds for the residual. When we exceed the upper one, we reduce the step size by a factor of 2 and if the
515 residual falls below the lower threshold, we double the step size.
516 Please setup these parameters as "e_tol" and "e_tol_low".
517 """
519 def setup(self, controller, params, description, **kwargs):
520 """
521 Define default parameters here.
523 Default parameters are:
524 - control_order (int): The order relative to other convergence controllers
525 - e_tol_low (float): Lower absolute threshold for the residual
526 - e_tol (float): Upper absolute threshold for the residual
527 - use_restol (bool): Restart if the residual tolerance was not reached
528 - max_restarts: Override maximum number of restarts
530 Args:
531 controller (pySDC.Controller): The controller
532 params (dict): The params passed for this specific convergence controller
533 description (dict): The description object used to instantiate the controller
535 Returns:
536 (dict): The updated params dictionary
537 """
538 defaults = {
539 "control_order": -45,
540 "e_tol_low": 0,
541 "e_tol": np.inf,
542 "use_restol": False,
543 "max_restarts": 99 if "e_tol_low" in params else None,
544 "allowed_modifications": ['increase', 'decrease'], # what we are allowed to do with the step size
545 }
546 return {**defaults, **params}
548 def setup_status_variables(self, controller, **kwargs):
549 """
550 Change maximum number of allowed restarts here.
552 Args:
553 controller (pySDC.Controller): The controller
555 Reutrns:
556 None
557 """
558 from pySDC.implementations.convergence_controller_classes.basic_restarting import BasicRestarting
560 if self.params.max_restarts is not None:
561 conv_controllers = controller.convergence_controllers
562 restart_cont = [me for me in conv_controllers if BasicRestarting in type(me).__bases__]
564 if len(restart_cont) == 0:
565 raise NotImplementedError("Please implement override of maximum number of restarts!")
567 restart_cont[0].params.max_restarts = self.params.max_restarts
568 return None
570 def check_parameters(self, controller, params, description, **kwargs):
571 """
572 Check whether parameters are compatible with whatever assumptions went into the step size functions etc.
574 Args:
575 controller (pySDC.Controller): The controller
576 params (dict): The params passed for this specific convergence controller
577 description (dict): The description object used to instantiate the controller
579 Returns:
580 bool: Whether the parameters are compatible
581 str: The error message
582 """
583 if controller.params.mssdc_jac:
584 return (
585 False,
586 "Adaptivity needs the same order on all steps, please activate Gauss-Seidel multistep mode!",
587 )
589 return True, ""
591 def get_new_step_size(self, controller, S, **kwargs):
592 """
593 Determine a step size for the next step.
594 If we exceed the absolute tolerance of the residual in either direction, we either double or halve the step
595 size.
597 Args:
598 controller (pySDC.Controller): The controller
599 S (pySDC.Step): The current step
601 Returns:
602 None
603 """
604 # check if we performed the desired amount of sweeps
605 if S.status.iter == S.params.maxiter:
606 L = S.levels[0]
608 res = self.get_local_error_estimate(controller, S)
610 dt_planned = L.status.dt_new if L.status.dt_new is not None else L.params.dt
612 if (
613 res > self.params.e_tol or (res > L.params.restol and self.params.use_restol)
614 ) and 'decrease' in self.params.allowed_modifications:
615 L.status.dt_new = min([dt_planned, L.params.dt / 2.0])
616 self.log(f'Adjusting step size from {L.params.dt:.2e} to {L.status.dt_new:.2e}', S)
617 elif res < self.params.e_tol_low and 'increase' in self.params.allowed_modifications:
618 L.status.dt_new = max([dt_planned, L.params.dt * 2.0])
619 self.log(f'Adjusting step size from {L.params.dt:.2e} to {L.status.dt_new:.2e}', S)
621 return None
623 def get_local_error_estimate(self, controller, S, **kwargs):
624 """
625 Get the residual of the finest level of the step.
627 Args:
628 controller (pySDC.Controller): The controller
629 S (pySDC.Step): The current step
631 Returns:
632 float: Embedded error estimate
633 """
634 return S.levels[0].status.residual
637class AdaptivityCollocation(AdaptivityForConvergedCollocationProblems):
638 """
639 Control the step size via a collocation based estimate of the local error.
640 The error estimate works by subtracting two solutions to collocation problems with different order. You can
641 interpolate between collocation methods as much as you want but the adaptive step size selection will always be
642 based on the last switch of quadrature.
643 """
645 def setup(self, controller, params, description, **kwargs):
646 """
647 Add a default value for control order to the parameters.
649 Args:
650 controller (pySDC.Controller): The controller
651 params (dict): Parameters for the convergence controller
652 description (dict): The description object used to instantiate the controller
654 Returns:
655 dict: Updated parameters
656 """
657 defaults = {
658 "adaptive_coll_params": {},
659 "num_colls": 0,
660 **super().setup(controller, params, description, **kwargs),
661 "control_order": 220,
662 }
664 for key in defaults['adaptive_coll_params'].keys():
665 if type(defaults['adaptive_coll_params'][key]) == list:
666 defaults['num_colls'] = max([defaults['num_colls'], len(defaults['adaptive_coll_params'][key])])
668 return defaults
670 def setup_status_variables(self, controller, **kwargs):
671 """
672 Set up empty tuples for the error estimates and orders of the collocation problems solved in the current step.
674 These live on the step rather than on this object, since there is one instance of this class per controller,
675 which is a whole block of steps when running with more than one process. They are tuples, so that the same empty
676 value can be set on all steps and appending to it on one step does not change it on the others.
678 Args:
679 controller (pySDC.Controller): The controller
681 Returns:
682 None
683 """
684 self.add_status_variable_to_step('coll_error', ())
685 self.add_status_variable_to_step('coll_order', ())
687 def reset_status_variables(self, controller, **kwargs):
688 """
689 Empty the tuples of error estimates and orders at the start of a step or when restarting.
691 Args:
692 controller (pySDC.Controller): The controller
694 Returns:
695 None
696 """
697 self.set_step_status_variable('coll_error', ())
698 self.set_step_status_variable('coll_order', ())
700 def dependencies(self, controller, description, **kwargs):
701 """
702 Load the `EstimateEmbeddedErrorCollocation` convergence controller to estimate the local error by switching
703 between collocation problems between iterations.
705 Args:
706 controller (pySDC.Controller): The controller
707 description (dict): The description object used to instantiate the controller
708 """
709 from pySDC.implementations.convergence_controller_classes.estimate_embedded_error import (
710 EstimateEmbeddedErrorCollocation,
711 )
713 super().dependencies(controller, description, **kwargs)
715 params = {'adaptive_coll_params': self.params.adaptive_coll_params}
716 controller.add_convergence_controller(
717 EstimateEmbeddedErrorCollocation,
718 params=params,
719 description=description,
720 )
722 def get_convergence(self, controller, S, **kwargs):
723 """
724 The step is converged once all collocation problems have been solved.
726 Args:
727 controller (pySDC.Controller): The controller
728 S (pySDC.Step): The current step
730 Returns:
731 bool: Whether all ``num_colls`` collocation problems have been solved
732 """
733 return len(S.status.coll_order) == self.params.num_colls
735 def get_local_error_estimate(self, controller, S, **kwargs):
736 """
737 Get the collocation based embedded error estimate.
739 Args:
740 controller (pySDC.Controller): The controller
741 S (pySDC.Step): The current step
743 Returns:
744 float: Embedded error estimate
745 """
746 if len(S.status.coll_error) > 1:
747 return S.status.coll_error[-1][1]
748 else:
749 return 0.0
751 def post_iteration_processing(self, controller, step, **kwargs):
752 """
753 Get the error estimate and its order if available.
755 Args:
756 controller (pySDC.Controller.controller): The controller
757 step (pySDC.Step.step): The current step
758 """
759 if step.status.done:
760 lvl = step.levels[0]
761 step.status.coll_error += (lvl.status.error_embedded_estimate_collocation,)
762 step.status.coll_order += (lvl.sweep.coll.order,)
764 def get_new_step_size(self, controller, S, **kwargs):
765 """
766 Once all collocation problems have been solved, compute the new step size from the error estimate, using as
767 order one more than the lower order of the last two collocation methods.
769 Args:
770 controller (pySDC.Controller): The controller
771 S (pySDC.Step): The current step
773 Returns:
774 None
775 """
776 if len(S.status.coll_order) == self.params.num_colls:
777 lvl = S.levels[0]
779 # compute next step size
780 order = (
781 min(S.status.coll_order[-2::]) + 1
782 ) # local order of less accurate of the last two collocation problems
783 e_est = self.get_local_error_estimate(controller, S)
785 lvl.status.dt_new = self.compute_optimal_step_size(
786 self.params.beta, lvl.params.dt, self.params.e_tol, e_est, order
787 )
788 self.log(f'Adjusting step size from {lvl.params.dt:.2e} to {lvl.status.dt_new:.2e}', S)
790 def determine_restart(self, controller, S, **kwargs):
791 """
792 Check if the step wants to be restarted by comparing the estimate of the local error to a preset tolerance
794 Args:
795 controller (pySDC.Controller): The controller
796 S (pySDC.Step): The current step
798 Returns:
799 None
800 """
801 if len(S.status.coll_order) == self.params.num_colls:
802 e_est = self.get_local_error_estimate(controller, S)
803 if e_est >= self.params.e_tol:
804 S.status.restart = True
805 self.log(f"Restarting: e={e_est:.2e} >= e_tol={self.params.e_tol:.2e}", S)
807 def check_parameters(self, controller, params, description, **kwargs):
808 """
809 Check whether parameters are compatible with whatever assumptions went into the step size functions etc.
810 For adaptivity, we need to know the order of the scheme.
812 Args:
813 controller (pySDC.Controller): The controller
814 params (dict): The params passed for this specific convergence controller
815 description (dict): The description object used to instantiate the controller
817 Returns:
818 bool: Whether the parameters are compatible
819 str: The error message
820 """
821 if "e_tol" not in params.keys():
822 return (
823 False,
824 "Adaptivity needs a local tolerance! Please pass `e_tol` to the parameters for this convergence controller!",
825 )
827 return True, ""
830class AdaptivityExtrapolationWithinQ(AdaptivityForConvergedCollocationProblems):
831 """
832 Compute the step size adaptively from an error estimate by extrapolation within the quadrature nodes.
834 This error estimate depends on solving the collocation problem exactly, so make sure you set a sufficient stopping criterion.
835 """
837 def setup(self, controller, params, description, **kwargs):
838 """
839 Add the default ``high_Taylor_order=False`` to the parameters and use the regular convergence check to determine
840 if the collocation problem is converged.
842 Args:
843 controller (pySDC.Controller): The controller
844 params (dict): The params passed for this specific convergence controller
845 description (dict): The description object used to instantiate the controller
847 Returns:
848 (dict): The updated params dictionary
849 """
850 from pySDC.core.check_convergence import CheckConvergence
852 defaults = {
853 'high_Taylor_order': False,
854 **params,
855 }
857 self.check_convergence = CheckConvergence.check_convergence
858 return {**defaults, **super().setup(controller, params, description, **kwargs)}
860 def get_convergence(self, controller, S, **kwargs):
861 """
862 Check if the step is converged with the regular convergence criteria, i.e. residual, increment or maximum number
863 of iterations.
865 Args:
866 controller (pySDC.Controller): The controller
867 S (pySDC.Step): The current step
869 Returns:
870 bool: Whether the step is converged
871 """
872 return self.check_convergence(S)
874 def dependencies(self, controller, description, **kwargs):
875 """
876 Load the error estimator.
878 Args:
879 controller (pySDC.Controller): The controller
880 description (dict): The description object used to instantiate the controller
882 Returns:
883 None
884 """
885 from pySDC.implementations.convergence_controller_classes.estimate_extrapolation_error import (
886 EstimateExtrapolationErrorWithinQ,
887 )
889 super().dependencies(controller, description, **kwargs)
891 controller.add_convergence_controller(
892 EstimateExtrapolationErrorWithinQ,
893 description=description,
894 params={'high_Taylor_order': self.params.high_Taylor_order},
895 )
896 return None
898 def get_new_step_size(self, controller, S, **kwargs):
899 """
900 Determine a step size for the next step from the error estimate.
902 Args:
903 controller (pySDC.Controller): The controller
904 S (pySDC.Step): The current step
906 Returns:
907 None
908 """
909 if self.get_convergence(controller, S, **kwargs):
910 L = S.levels[0]
912 # compute next step size
913 order = L.sweep.coll.num_nodes + 1 if self.params.high_Taylor_order else L.sweep.coll.num_nodes
915 e_est = self.get_local_error_estimate(controller, S)
916 L.status.dt_new = self.compute_optimal_step_size(
917 self.params.beta, L.params.dt, self.params.e_tol, e_est, order
918 )
920 self.log(
921 f'Error target: {self.params.e_tol:.2e}, error estimate: {e_est:.2e}, update_order: {order}',
922 S,
923 level=10,
924 )
925 self.log(f'Adjusting step size from {L.params.dt:.2e} to {L.status.dt_new:.2e}', S)
927 return None
929 def get_local_error_estimate(self, controller, S, **kwargs):
930 """
931 Get the embedded error estimate of the finest level of the step.
933 Args:
934 controller (pySDC.Controller): The controller
935 S (pySDC.Step): The current step
937 Returns:
938 float: Embedded error estimate
939 """
940 return S.levels[0].status.error_extrapolation_estimate
943class AdaptivityPolynomialError(AdaptivityForConvergedCollocationProblems):
944 """
945 Compute the step size adaptively from an error estimate by interpolation within the quadrature nodes.
947 This error estimate depends on solving the collocation problem exactly, so make sure you set a sufficient stopping criterion.
948 """
950 def setup(self, controller, params, description, **kwargs):
951 """
952 Add defaults for the mesh type of the problem, which selects the error estimator, and ``rel_error``, and use the
953 regular convergence check to determine if the collocation problem is converged.
955 Args:
956 controller (pySDC.Controller): The controller
957 params (dict): The params passed for this specific convergence controller
958 description (dict): The description object used to instantiate the controller
960 Returns:
961 (dict): The updated params dictionary
962 """
963 from pySDC.core.check_convergence import CheckConvergence
965 defaults = {
966 'control_order': -50,
967 'problem_mesh_type': 'numpyesque',
968 'rel_error': False,
969 **super().setup(controller, params, description, **kwargs),
970 **params,
971 }
973 self.check_convergence = CheckConvergence.check_convergence
974 return defaults
976 def get_convergence(self, controller, S, **kwargs):
977 """
978 Check if the step is converged with the regular convergence criteria, i.e. residual, increment or maximum number
979 of iterations.
981 Args:
982 controller (pySDC.Controller): The controller
983 S (pySDC.Step): The current step
985 Returns:
986 bool: Whether the step is converged
987 """
988 return self.check_convergence(S)
990 def dependencies(self, controller, description, **kwargs):
991 """
992 Load the error estimator.
994 Args:
995 controller (pySDC.Controller): The controller
996 description (dict): The description object used to instantiate the controller
998 Returns:
999 None
1000 """
1001 if self.params.problem_mesh_type.lower() == 'numpyesque':
1002 from pySDC.implementations.convergence_controller_classes.estimate_polynomial_error import (
1003 EstimatePolynomialError as error_estimation_cls,
1004 )
1005 elif self.params.problem_mesh_type.lower() == 'firedrake':
1006 from pySDC.implementations.convergence_controller_classes.estimate_polynomial_error import (
1007 EstimatePolynomialErrorFiredrake as error_estimation_cls,
1008 )
1009 else:
1010 raise NotImplementedError(
1011 f'Don\'t know what error estimation class to use for problems with mesh type {self.params.problem_mesh_type}'
1012 )
1014 super().dependencies(controller, description, **kwargs)
1016 controller.add_convergence_controller(
1017 error_estimation_cls,
1018 description=description,
1019 params={
1020 'rel_error': self.params.rel_error,
1021 },
1022 )
1023 return None
1025 def get_new_step_size(self, controller, S, **kwargs):
1026 """
1027 Determine a step size for the next step from the error estimate.
1029 Args:
1030 controller (pySDC.Controller): The controller
1031 S (pySDC.Step): The current step
1033 Returns:
1034 None
1035 """
1036 if self.get_convergence(controller, S, **kwargs):
1037 L = S.levels[0]
1039 # compute next step size
1040 order = L.status.order_embedded_estimate
1042 e_est = self.get_local_error_estimate(controller, S)
1043 L.status.dt_new = self.compute_optimal_step_size(
1044 self.params.beta, L.params.dt, self.params.e_tol, e_est, order
1045 )
1047 self.log(
1048 f'Error target: {self.params.e_tol:.2e}, error estimate: {e_est:.2e}, update_order: {order}',
1049 S,
1050 level=10,
1051 )
1052 self.log(f'Adjusting step size from {L.params.dt:.2e} to {L.status.dt_new:.2e}', S)
1054 return None
1056 def get_local_error_estimate(self, controller, S, **kwargs):
1057 """
1058 Get the embedded error estimate of the finest level of the step.
1060 Args:
1061 controller (pySDC.Controller): The controller
1062 S (pySDC.Step): The current step
1064 Returns:
1065 float: Embedded error estimate
1066 """
1067 return S.levels[0].status.error_embedded_estimate