Coverage for pySDC/implementations/convergence_controller_classes/adaptivity.py: 92%
252 statements
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-12 02:45 +0000
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-12 02:45 +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 return None
59 def get_new_step_size(self, controller, S, **kwargs):
60 """
61 Determine a step size for the next step from an estimate of the local error of the current step.
63 Args:
64 controller (pySDC.Controller): The controller
65 S (pySDC.Step): The current step
67 Returns:
68 None
69 """
70 raise NotImplementedError("Please implement a rule for updating the step size!")
72 def compute_optimal_step_size(self, beta, dt, e_tol, e_est, order):
73 """
74 Compute the optimal step size for the current step based on the order of the scheme.
75 This function can be called from `get_new_step_size` for various implementations of adaptivity, but notably not
76 all! We require to know the order of the error estimate and if we do adaptivity based on the residual, for
77 instance, we do not know that and we can't use this function.
79 Args:
80 beta (float): Safety factor
81 dt (float): Current step size
82 e_tol (float): The desired tolerance
83 e_est (float): The estimated local error
84 order (int): The order of the local error estimate
86 Returns:
87 float: The optimal step size
88 """
89 return beta * dt * (e_tol / e_est) ** (1.0 / order)
91 def get_local_error_estimate(self, controller, S, **kwargs):
92 """
93 Get the local error estimate for updating the step size.
94 It does not have to be an error estimate, but could be the residual or something else.
96 Args:
97 controller (pySDC.Controller): The controller
98 S (pySDC.Step): The current step
100 Returns:
101 float: The error estimate
102 """
103 raise NotImplementedError("Please implement a way to get the local error")
105 def determine_restart(self, controller, S, **kwargs):
106 """
107 Check if the step wants to be restarted by comparing the estimate of the local error to a preset tolerance
109 Args:
110 controller (pySDC.Controller): The controller
111 S (pySDC.Step): The current step
113 Returns:
114 None
115 """
116 if S.status.iter >= S.params.maxiter:
117 e_est = self.get_local_error_estimate(controller, S)
118 if e_est >= self.params.e_tol:
119 # see if we try to avoid restarts
120 if self.params.get('avoid_restarts'):
121 more_iter_needed = max([L.status.iter_to_convergence for L in S.levels])
122 k_final = S.status.iter + more_iter_needed
123 rho = max([L.status.contraction_factor for L in S.levels])
124 coll_order = S.levels[0].sweep.coll.order
126 if rho > 1:
127 S.status.restart = True
128 self.log(f"Convergence factor = {rho:.2e} > 1 -> restarting", S)
129 elif k_final > 2 * S.params.maxiter:
130 S.status.restart = True
131 self.log(
132 f"{more_iter_needed} more iterations needed for convergence -> restart is more efficient", S
133 )
134 elif k_final > coll_order:
135 S.status.restart = True
136 self.log(
137 f"{more_iter_needed} more iterations needed for convergence -> restart because collocation problem would be over resolved",
138 S,
139 )
140 else:
141 S.status.force_continue = True
142 self.log(f"{more_iter_needed} more iterations needed for convergence -> no restart", S)
143 else:
144 S.status.restart = True
145 self.log(f"Restarting: e={e_est:.2e} >= e_tol={self.params.e_tol:.2e}", S)
147 return None
150class AdaptivityForConvergedCollocationProblems(AdaptivityBase):
151 """
152 Base class for adaptivity from error estimates that need a converged collocation problem, restarting if it is not.
153 """
155 def dependencies(self, controller, description, **kwargs):
156 """
157 Load interpolation between restarts.
159 Args:
160 controller (pySDC.Controller): The controller
161 description (dict): The description object used to instantiate the controller
163 Returns:
164 None
165 """
166 super().dependencies(controller, description, **kwargs)
168 if self.params.interpolate_between_restarts:
169 from pySDC.implementations.convergence_controller_classes.interpolate_between_restarts import (
170 InterpolateBetweenRestarts,
171 )
173 controller.add_convergence_controller(
174 InterpolateBetweenRestarts,
175 description=description,
176 params={},
177 )
178 if self.params.interpolate_between_restarts:
179 self.interpolator = controller.convergence_controllers[-1]
180 return None
182 def get_convergence(self, controller, S, **kwargs):
183 """
184 Check if the collocation problem is converged. Needs to be implemented by the child class.
186 Args:
187 controller (pySDC.Controller): The controller
188 S (pySDC.Step): The current step
190 Returns:
191 bool: Whether the collocation problem is converged
192 """
193 raise NotImplementedError("Please implement a way to check if the collocation problem is converged!")
195 def setup(self, controller, params, description, **kwargs):
196 """
197 Add a default value for control order to the parameters.
199 Args:
200 controller (pySDC.Controller): The controller
201 params (dict): Parameters for the convergence controller
202 description (dict): The description object used to instantiate the controller
204 Returns:
205 dict: Updated parameters
206 """
207 defaults = {
208 'restol_rel': None,
209 'e_tol_rel': None,
210 'restart_at_maxiter': True,
211 'restol_min': 1e-12,
212 'restol_max': 1e-5,
213 'e_tol_min': 1e-10,
214 'e_tol_max': 1e-5,
215 'factor_if_not_converged': 4.0,
216 'residual_max_tol': 1e9,
217 'interpolate_between_restarts': True,
218 'abort_at_growing_residual': True,
219 **super().setup(controller, params, description, **kwargs),
220 }
221 if defaults['restol_rel']:
222 description['level_params']['restol'] = min(
223 [max([defaults['restol_rel'] * defaults['e_tol'], defaults['restol_min']]), defaults['restol_max']]
224 )
225 elif defaults['e_tol_rel']:
226 description['level_params']['e_tol'] = min(
227 [max([defaults['e_tol_rel'] * defaults['e_tol'], defaults['e_tol_min']]), defaults['e_tol_max']]
228 )
230 self.res_last_iter = np.inf
232 return defaults
234 def determine_restart(self, controller, S, **kwargs):
235 """
236 Once the collocation problem is converged, restart with a smaller step size if the residual is still above
237 ``restol`` (with ``restart_at_maxiter``, unless the increment is below ``e_tol``), or restart if the local error
238 estimate exceeds ``e_tol``. Before convergence, restart with a smaller step size if the residual
239 grows between iterations (only without parallelism across steps and with ``abort_at_growing_residual``) or
240 exceeds ``residual_max_tol``.
242 Args:
243 controller (pySDC.Controller): The controller
244 S (pySDC.Step): The current step
246 Returns:
247 None
248 """
249 if self.get_convergence(controller, S, **kwargs):
250 self.res_last_iter = np.inf
252 L = S.levels[0]
253 e_tol_converged = (
254 L.status.increment < L.params.e_tol if (L.params.get('e_tol') and L.status.get('increment')) else False
255 )
257 if (
258 self.params.restart_at_maxiter
259 and S.levels[0].status.residual > S.levels[0].params.restol
260 and not e_tol_converged
261 ):
262 self.trigger_restart_upon_nonconvergence(S)
263 elif self.get_local_error_estimate(controller, S, **kwargs) > self.params.e_tol:
264 S.status.restart = True
265 elif (
266 S.status.time_size == 1
267 and self.res_last_iter < S.levels[0].status.residual
268 and S.status.iter > 0
269 and self.params.abort_at_growing_residual
270 ):
271 self.trigger_restart_upon_nonconvergence(S)
272 elif S.levels[0].status.residual > self.params.residual_max_tol:
273 self.trigger_restart_upon_nonconvergence(S)
275 self.res_last_iter = S.levels[0].status.residual * 1.0
277 def trigger_restart_upon_nonconvergence(self, S):
278 """
279 Restart the step and stop iterating, dividing the step size on all levels by ``factor_if_not_converged``. No
280 interpolation is done for the restarted step.
282 Args:
283 S (pySDC.Step): The current step
285 Returns:
286 None
287 """
288 S.status.restart = True
289 S.status.force_done = True
290 for L in S.levels:
291 L.status.dt_new = L.params.dt / self.params.factor_if_not_converged
292 self.log(
293 f'Collocation problem not converged. Reducing step size to {L.status.dt_new:.2e}',
294 S,
295 )
296 if self.params.interpolate_between_restarts:
297 self.interpolator.status.skip_interpolation = True
300class Adaptivity(AdaptivityBase):
301 """
302 Class to compute time step size adaptively based on embedded error estimate.
304 We have a version working in non-MPI pipelined SDC, but Adaptivity requires you to know the order of the scheme,
305 which you can also know for block-Jacobi, but it works differently and it is only implemented for block
306 Gauss-Seidel so far.
308 There is an option to reduce restarts if continued iterations could yield convergence in fewer iterations than
309 restarting based on an estimate of the contraction factor.
310 Since often only one or two more iterations suffice, this can boost efficiency of adaptivity significantly.
311 Notice that the computed step size is not effected.
312 Be aware that this does not work when Hot Rod is enabled, since that requires us to know the order of the scheme in
313 more detail. Since we reset to the second to last sweep before moving on, we cannot continue to iterate.
314 Set the reduced restart up by setting a boolean value for "avoid_restarts" in the parameters for the convergence
315 controller.
316 The behaviour in multi-step SDC is not well studied and it is unclear if anything useful happens there.
317 """
319 def setup(self, controller, params, description, **kwargs):
320 """
321 Define default parameters here.
323 Default parameters are:
324 - control_order (int): The order relative to other convergence controllers
325 - beta (float): The safety factor
327 Args:
328 controller (pySDC.Controller): The controller
329 params (dict): The params passed for this specific convergence controller
330 description (dict): The description object used to instantiate the controller
332 Returns:
333 (dict): The updated params dictionary
334 """
335 defaults = {
336 "embedded_error_flavor": 'standard',
337 "rel_error": False,
338 }
339 return {**defaults, **super().setup(controller, params, description, **kwargs)}
341 def dependencies(self, controller, description, **kwargs):
342 """
343 Load the embedded error estimator.
345 Args:
346 controller (pySDC.Controller): The controller
347 description (dict): The description object used to instantiate the controller
349 Returns:
350 None
351 """
352 from pySDC.implementations.convergence_controller_classes.estimate_embedded_error import EstimateEmbeddedError
354 super().dependencies(controller, description, **kwargs)
356 controller.add_convergence_controller(
357 EstimateEmbeddedError.get_implementation(self.params.embedded_error_flavor, self.params.useMPI),
358 description=description,
359 params={
360 'rel_error': self.params.rel_error,
361 },
362 )
364 # load contraction factor estimator if necessary
365 if self.params.get('avoid_restarts'):
366 from pySDC.implementations.convergence_controller_classes.estimate_contraction_factor import (
367 EstimateContractionFactor,
368 )
370 params = {'e_tol': self.params.e_tol}
371 controller.add_convergence_controller(EstimateContractionFactor, description=description, params=params)
372 return None
374 def check_parameters(self, controller, params, description, **kwargs):
375 """
376 Check whether parameters are compatible with whatever assumptions went into the step size functions etc.
377 For adaptivity, we need to know the order of the scheme.
379 Args:
380 controller (pySDC.Controller): The controller
381 params (dict): The params passed for this specific convergence controller
382 description (dict): The description object used to instantiate the controller
384 Returns:
385 bool: Whether the parameters are compatible
386 str: The error message
387 """
388 if description["level_params"].get("restol", -1.0) >= 0:
389 return (
390 False,
391 "Adaptivity needs constant order in time and hence restol in the step parameters has to be \
392smaller than 0!",
393 )
395 if controller.params.mssdc_jac:
396 return (
397 False,
398 "Adaptivity needs the same order on all steps, please activate Gauss-Seidel multistep mode!",
399 )
401 if "e_tol" not in params.keys():
402 return (
403 False,
404 "Adaptivity needs a local tolerance! Please pass `e_tol` to the parameters for this convergence controller!",
405 )
407 return True, ""
409 def get_new_step_size(self, controller, S, **kwargs):
410 """
411 Determine a step size for the next step from an embedded estimate of the local error of the current step.
413 Args:
414 controller (pySDC.Controller): The controller
415 S (pySDC.Step): The current step
417 Returns:
418 None
419 """
420 # check if we performed the desired amount of sweeps
421 if S.status.iter == S.params.maxiter:
422 L = S.levels[0]
424 # compute next step size
425 order = S.status.iter # embedded error estimate is same order as time marching
427 e_est = self.get_local_error_estimate(controller, S)
428 L.status.dt_new = self.compute_optimal_step_size(
429 self.params.beta, L.params.dt, self.params.e_tol, e_est, order
430 )
431 self.log(f'Adjusting step size from {L.params.dt:.2e} to {L.status.dt_new:.2e}', S)
433 return None
435 def get_local_error_estimate(self, controller, S, **kwargs):
436 """
437 Get the embedded error estimate of the finest level of the step.
439 Args:
440 controller (pySDC.Controller): The controller
441 S (pySDC.Step): The current step
443 Returns:
444 float: Embedded error estimate
445 """
446 return S.levels[0].status.error_embedded_estimate
449class AdaptivityRK(Adaptivity):
450 """
451 Adaptivity for Runge-Kutta methods. Basically, we need to change the order in the step size update
452 """
454 def setup(self, controller, params, description, **kwargs):
455 """
456 Add the order of the embedded error estimate of the Runge-Kutta method, from the sweeper's ``get_update_order``,
457 as default for the parameter ``update_order``.
459 Args:
460 controller (pySDC.Controller): The controller
461 params (dict): The params passed for this specific convergence controller
462 description (dict): The description object used to instantiate the controller
464 Returns:
465 (dict): The updated params dictionary
466 """
467 defaults = {}
468 defaults['update_order'] = params.get('update_order', description['sweeper_class'].get_update_order())
469 return {**defaults, **super().setup(controller, params, description, **kwargs)}
471 def get_new_step_size(self, controller, S, **kwargs):
472 """
473 Determine a step size for the next step from an embedded estimate of the local error of the current step.
475 Args:
476 controller (pySDC.Controller): The controller
477 S (pySDC.Step): The current step
479 Returns:
480 None
481 """
482 # check if we performed the desired amount of sweeps
483 if S.status.iter == S.params.maxiter:
484 L = S.levels[0]
486 # compute next step size
487 order = self.params.update_order
488 e_est = self.get_local_error_estimate(controller, S)
489 L.status.dt_new = self.compute_optimal_step_size(
490 self.params.beta, L.params.dt, self.params.e_tol, e_est, order
491 )
492 self.log(f'Adjusting step size from {L.params.dt:.2e} to {L.status.dt_new:.2e}', S)
494 return None
497class AdaptivityResidual(AdaptivityBase):
498 """
499 Do adaptivity based on residual.
501 Since we don't know a correlation between the residual and the error (for nonlinear problems), we employ a simpler
502 rule to update the step size. Instead of giving a local tolerance that we try to hit as closely as possible, we set
503 two thresholds for the residual. When we exceed the upper one, we reduce the step size by a factor of 2 and if the
504 residual falls below the lower threshold, we double the step size.
505 Please setup these parameters as "e_tol" and "e_tol_low".
506 """
508 def setup(self, controller, params, description, **kwargs):
509 """
510 Define default parameters here.
512 Default parameters are:
513 - control_order (int): The order relative to other convergence controllers
514 - e_tol_low (float): Lower absolute threshold for the residual
515 - e_tol (float): Upper absolute threshold for the residual
516 - use_restol (bool): Restart if the residual tolerance was not reached
517 - max_restarts: Override maximum number of restarts
519 Args:
520 controller (pySDC.Controller): The controller
521 params (dict): The params passed for this specific convergence controller
522 description (dict): The description object used to instantiate the controller
524 Returns:
525 (dict): The updated params dictionary
526 """
527 defaults = {
528 "control_order": -45,
529 "e_tol_low": 0,
530 "e_tol": np.inf,
531 "use_restol": False,
532 "max_restarts": 99 if "e_tol_low" in params else None,
533 "allowed_modifications": ['increase', 'decrease'], # what we are allowed to do with the step size
534 }
535 return {**defaults, **params}
537 def setup_status_variables(self, controller, **kwargs):
538 """
539 Change maximum number of allowed restarts here.
541 Args:
542 controller (pySDC.Controller): The controller
544 Reutrns:
545 None
546 """
547 from pySDC.implementations.convergence_controller_classes.basic_restarting import BasicRestarting
549 if self.params.max_restarts is not None:
550 conv_controllers = controller.convergence_controllers
551 restart_cont = [me for me in conv_controllers if BasicRestarting in type(me).__bases__]
553 if len(restart_cont) == 0:
554 raise NotImplementedError("Please implement override of maximum number of restarts!")
556 restart_cont[0].params.max_restarts = self.params.max_restarts
557 return None
559 def check_parameters(self, controller, params, description, **kwargs):
560 """
561 Check whether parameters are compatible with whatever assumptions went into the step size functions etc.
563 Args:
564 controller (pySDC.Controller): The controller
565 params (dict): The params passed for this specific convergence controller
566 description (dict): The description object used to instantiate the controller
568 Returns:
569 bool: Whether the parameters are compatible
570 str: The error message
571 """
572 if controller.params.mssdc_jac:
573 return (
574 False,
575 "Adaptivity needs the same order on all steps, please activate Gauss-Seidel multistep mode!",
576 )
578 return True, ""
580 def get_new_step_size(self, controller, S, **kwargs):
581 """
582 Determine a step size for the next step.
583 If we exceed the absolute tolerance of the residual in either direction, we either double or halve the step
584 size.
586 Args:
587 controller (pySDC.Controller): The controller
588 S (pySDC.Step): The current step
590 Returns:
591 None
592 """
593 # check if we performed the desired amount of sweeps
594 if S.status.iter == S.params.maxiter:
595 L = S.levels[0]
597 res = self.get_local_error_estimate(controller, S)
599 dt_planned = L.status.dt_new if L.status.dt_new is not None else L.params.dt
601 if (
602 res > self.params.e_tol or (res > L.params.restol and self.params.use_restol)
603 ) and 'decrease' in self.params.allowed_modifications:
604 L.status.dt_new = min([dt_planned, L.params.dt / 2.0])
605 self.log(f'Adjusting step size from {L.params.dt:.2e} to {L.status.dt_new:.2e}', S)
606 elif res < self.params.e_tol_low and 'increase' in self.params.allowed_modifications:
607 L.status.dt_new = max([dt_planned, L.params.dt * 2.0])
608 self.log(f'Adjusting step size from {L.params.dt:.2e} to {L.status.dt_new:.2e}', S)
610 return None
612 def get_local_error_estimate(self, controller, S, **kwargs):
613 """
614 Get the residual of the finest level of the step.
616 Args:
617 controller (pySDC.Controller): The controller
618 S (pySDC.Step): The current step
620 Returns:
621 float: Embedded error estimate
622 """
623 return S.levels[0].status.residual
626class AdaptivityCollocation(AdaptivityForConvergedCollocationProblems):
627 """
628 Control the step size via a collocation based estimate of the local error.
629 The error estimate works by subtracting two solutions to collocation problems with different order. You can
630 interpolate between collocation methods as much as you want but the adaptive step size selection will always be
631 based on the last switch of quadrature.
632 """
634 def setup(self, controller, params, description, **kwargs):
635 """
636 Add a default value for control order to the parameters.
638 Args:
639 controller (pySDC.Controller): The controller
640 params (dict): Parameters for the convergence controller
641 description (dict): The description object used to instantiate the controller
643 Returns:
644 dict: Updated parameters
645 """
646 defaults = {
647 "adaptive_coll_params": {},
648 "num_colls": 0,
649 **super().setup(controller, params, description, **kwargs),
650 "control_order": 220,
651 }
653 for key in defaults['adaptive_coll_params'].keys():
654 if type(defaults['adaptive_coll_params'][key]) == list:
655 defaults['num_colls'] = max([defaults['num_colls'], len(defaults['adaptive_coll_params'][key])])
657 return defaults
659 def setup_status_variables(self, controller, **kwargs):
660 """
661 Set up empty tuples for the error estimates and orders of the collocation problems solved in the current step.
663 These live on the step rather than on this object, since there is one instance of this class per controller,
664 which is a whole block of steps when running with more than one process. They are tuples, so that the same empty
665 value can be set on all steps and appending to it on one step does not change it on the others.
667 Args:
668 controller (pySDC.Controller): The controller
670 Returns:
671 None
672 """
673 self.add_status_variable_to_step('coll_error', ())
674 self.add_status_variable_to_step('coll_order', ())
676 def reset_status_variables(self, controller, **kwargs):
677 """
678 Empty the tuples of error estimates and orders at the start of a step or when restarting.
680 Args:
681 controller (pySDC.Controller): The controller
683 Returns:
684 None
685 """
686 self.set_step_status_variable('coll_error', ())
687 self.set_step_status_variable('coll_order', ())
689 def dependencies(self, controller, description, **kwargs):
690 """
691 Load the `EstimateEmbeddedErrorCollocation` convergence controller to estimate the local error by switching
692 between collocation problems between iterations.
694 Args:
695 controller (pySDC.Controller): The controller
696 description (dict): The description object used to instantiate the controller
697 """
698 from pySDC.implementations.convergence_controller_classes.estimate_embedded_error import (
699 EstimateEmbeddedErrorCollocation,
700 )
702 super().dependencies(controller, description, **kwargs)
704 params = {'adaptive_coll_params': self.params.adaptive_coll_params}
705 controller.add_convergence_controller(
706 EstimateEmbeddedErrorCollocation,
707 params=params,
708 description=description,
709 )
711 def get_convergence(self, controller, S, **kwargs):
712 """
713 The step is converged once all collocation problems have been solved.
715 Args:
716 controller (pySDC.Controller): The controller
717 S (pySDC.Step): The current step
719 Returns:
720 bool: Whether all ``num_colls`` collocation problems have been solved
721 """
722 return len(S.status.coll_order) == self.params.num_colls
724 def get_local_error_estimate(self, controller, S, **kwargs):
725 """
726 Get the collocation based embedded error estimate.
728 Args:
729 controller (pySDC.Controller): The controller
730 S (pySDC.Step): The current step
732 Returns:
733 float: Embedded error estimate
734 """
735 if len(S.status.coll_error) > 1:
736 return S.status.coll_error[-1][1]
737 else:
738 return 0.0
740 def post_iteration_processing(self, controller, step, **kwargs):
741 """
742 Get the error estimate and its order if available.
744 Args:
745 controller (pySDC.Controller.controller): The controller
746 step (pySDC.Step.step): The current step
747 """
748 if step.status.done:
749 lvl = step.levels[0]
750 step.status.coll_error += (lvl.status.error_embedded_estimate_collocation,)
751 step.status.coll_order += (lvl.sweep.coll.order,)
753 def get_new_step_size(self, controller, S, **kwargs):
754 """
755 Once all collocation problems have been solved, compute the new step size from the error estimate, using as
756 order one more than the lower order of the last two collocation methods.
758 Args:
759 controller (pySDC.Controller): The controller
760 S (pySDC.Step): The current step
762 Returns:
763 None
764 """
765 if len(S.status.coll_order) == self.params.num_colls:
766 lvl = S.levels[0]
768 # compute next step size
769 order = (
770 min(S.status.coll_order[-2::]) + 1
771 ) # local order of less accurate of the last two collocation problems
772 e_est = self.get_local_error_estimate(controller, S)
774 lvl.status.dt_new = self.compute_optimal_step_size(
775 self.params.beta, lvl.params.dt, self.params.e_tol, e_est, order
776 )
777 self.log(f'Adjusting step size from {lvl.params.dt:.2e} to {lvl.status.dt_new:.2e}', S)
779 def determine_restart(self, controller, S, **kwargs):
780 """
781 Check if the step wants to be restarted by comparing the estimate of the local error to a preset tolerance
783 Args:
784 controller (pySDC.Controller): The controller
785 S (pySDC.Step): The current step
787 Returns:
788 None
789 """
790 if len(S.status.coll_order) == self.params.num_colls:
791 e_est = self.get_local_error_estimate(controller, S)
792 if e_est >= self.params.e_tol:
793 S.status.restart = True
794 self.log(f"Restarting: e={e_est:.2e} >= e_tol={self.params.e_tol:.2e}", S)
796 def check_parameters(self, controller, params, description, **kwargs):
797 """
798 Check whether parameters are compatible with whatever assumptions went into the step size functions etc.
799 For adaptivity, we need to know the order of the scheme.
801 Args:
802 controller (pySDC.Controller): The controller
803 params (dict): The params passed for this specific convergence controller
804 description (dict): The description object used to instantiate the controller
806 Returns:
807 bool: Whether the parameters are compatible
808 str: The error message
809 """
810 if "e_tol" not in params.keys():
811 return (
812 False,
813 "Adaptivity needs a local tolerance! Please pass `e_tol` to the parameters for this convergence controller!",
814 )
816 return True, ""
819class AdaptivityExtrapolationWithinQ(AdaptivityForConvergedCollocationProblems):
820 """
821 Compute the step size adaptively from an error estimate by extrapolation within the quadrature nodes.
823 This error estimate depends on solving the collocation problem exactly, so make sure you set a sufficient stopping criterion.
824 """
826 def setup(self, controller, params, description, **kwargs):
827 """
828 Add the default ``high_Taylor_order=False`` to the parameters and use the regular convergence check to determine
829 if the collocation problem is converged.
831 Args:
832 controller (pySDC.Controller): The controller
833 params (dict): The params passed for this specific convergence controller
834 description (dict): The description object used to instantiate the controller
836 Returns:
837 (dict): The updated params dictionary
838 """
839 from pySDC.core.check_convergence import CheckConvergence
841 defaults = {
842 'high_Taylor_order': False,
843 **params,
844 }
846 self.check_convergence = CheckConvergence.check_convergence
847 return {**defaults, **super().setup(controller, params, description, **kwargs)}
849 def get_convergence(self, controller, S, **kwargs):
850 """
851 Check if the step is converged with the regular convergence criteria, i.e. residual, increment or maximum number
852 of iterations.
854 Args:
855 controller (pySDC.Controller): The controller
856 S (pySDC.Step): The current step
858 Returns:
859 bool: Whether the step is converged
860 """
861 return self.check_convergence(S)
863 def dependencies(self, controller, description, **kwargs):
864 """
865 Load the error estimator.
867 Args:
868 controller (pySDC.Controller): The controller
869 description (dict): The description object used to instantiate the controller
871 Returns:
872 None
873 """
874 from pySDC.implementations.convergence_controller_classes.estimate_extrapolation_error import (
875 EstimateExtrapolationErrorWithinQ,
876 )
878 super().dependencies(controller, description, **kwargs)
880 controller.add_convergence_controller(
881 EstimateExtrapolationErrorWithinQ,
882 description=description,
883 params={'high_Taylor_order': self.params.high_Taylor_order},
884 )
885 return None
887 def get_new_step_size(self, controller, S, **kwargs):
888 """
889 Determine a step size for the next step from the error estimate.
891 Args:
892 controller (pySDC.Controller): The controller
893 S (pySDC.Step): The current step
895 Returns:
896 None
897 """
898 if self.get_convergence(controller, S, **kwargs):
899 L = S.levels[0]
901 # compute next step size
902 order = L.sweep.coll.num_nodes + 1 if self.params.high_Taylor_order else L.sweep.coll.num_nodes
904 e_est = self.get_local_error_estimate(controller, S)
905 L.status.dt_new = self.compute_optimal_step_size(
906 self.params.beta, L.params.dt, self.params.e_tol, e_est, order
907 )
909 self.log(
910 f'Error target: {self.params.e_tol:.2e}, error estimate: {e_est:.2e}, update_order: {order}',
911 S,
912 level=10,
913 )
914 self.log(f'Adjusting step size from {L.params.dt:.2e} to {L.status.dt_new:.2e}', S)
916 return None
918 def get_local_error_estimate(self, controller, S, **kwargs):
919 """
920 Get the embedded error estimate of the finest level of the step.
922 Args:
923 controller (pySDC.Controller): The controller
924 S (pySDC.Step): The current step
926 Returns:
927 float: Embedded error estimate
928 """
929 return S.levels[0].status.error_extrapolation_estimate
932class AdaptivityPolynomialError(AdaptivityForConvergedCollocationProblems):
933 """
934 Compute the step size adaptively from an error estimate by interpolation within the quadrature nodes.
936 This error estimate depends on solving the collocation problem exactly, so make sure you set a sufficient stopping criterion.
937 """
939 def setup(self, controller, params, description, **kwargs):
940 """
941 Add defaults for the mesh type of the problem, which selects the error estimator, and ``rel_error``, and use the
942 regular convergence check to determine if the collocation problem is converged.
944 Args:
945 controller (pySDC.Controller): The controller
946 params (dict): The params passed for this specific convergence controller
947 description (dict): The description object used to instantiate the controller
949 Returns:
950 (dict): The updated params dictionary
951 """
952 from pySDC.core.check_convergence import CheckConvergence
954 defaults = {
955 'control_order': -50,
956 'problem_mesh_type': 'numpyesque',
957 'rel_error': False,
958 **super().setup(controller, params, description, **kwargs),
959 **params,
960 }
962 self.check_convergence = CheckConvergence.check_convergence
963 return defaults
965 def get_convergence(self, controller, S, **kwargs):
966 """
967 Check if the step is converged with the regular convergence criteria, i.e. residual, increment or maximum number
968 of iterations.
970 Args:
971 controller (pySDC.Controller): The controller
972 S (pySDC.Step): The current step
974 Returns:
975 bool: Whether the step is converged
976 """
977 return self.check_convergence(S)
979 def dependencies(self, controller, description, **kwargs):
980 """
981 Load the error estimator.
983 Args:
984 controller (pySDC.Controller): The controller
985 description (dict): The description object used to instantiate the controller
987 Returns:
988 None
989 """
990 if self.params.problem_mesh_type.lower() == 'numpyesque':
991 from pySDC.implementations.convergence_controller_classes.estimate_polynomial_error import (
992 EstimatePolynomialError as error_estimation_cls,
993 )
994 elif self.params.problem_mesh_type.lower() == 'firedrake':
995 from pySDC.implementations.convergence_controller_classes.estimate_polynomial_error import (
996 EstimatePolynomialErrorFiredrake as error_estimation_cls,
997 )
998 else:
999 raise NotImplementedError(
1000 f'Don\'t know what error estimation class to use for problems with mesh type {self.params.problem_mesh_type}'
1001 )
1003 super().dependencies(controller, description, **kwargs)
1005 controller.add_convergence_controller(
1006 error_estimation_cls,
1007 description=description,
1008 params={
1009 'rel_error': self.params.rel_error,
1010 },
1011 )
1012 return None
1014 def get_new_step_size(self, controller, S, **kwargs):
1015 """
1016 Determine a step size for the next step from the error estimate.
1018 Args:
1019 controller (pySDC.Controller): The controller
1020 S (pySDC.Step): The current step
1022 Returns:
1023 None
1024 """
1025 if self.get_convergence(controller, S, **kwargs):
1026 L = S.levels[0]
1028 # compute next step size
1029 order = L.status.order_embedded_estimate
1031 e_est = self.get_local_error_estimate(controller, S)
1032 L.status.dt_new = self.compute_optimal_step_size(
1033 self.params.beta, L.params.dt, self.params.e_tol, e_est, order
1034 )
1036 self.log(
1037 f'Error target: {self.params.e_tol:.2e}, error estimate: {e_est:.2e}, update_order: {order}',
1038 S,
1039 level=10,
1040 )
1041 self.log(f'Adjusting step size from {L.params.dt:.2e} to {L.status.dt_new:.2e}', S)
1043 return None
1045 def get_local_error_estimate(self, controller, S, **kwargs):
1046 """
1047 Get the embedded error estimate of the finest level of the step.
1049 Args:
1050 controller (pySDC.Controller): The controller
1051 S (pySDC.Step): The current step
1053 Returns:
1054 float: Embedded error estimate
1055 """
1056 return S.levels[0].status.error_embedded_estimate