Coverage for pySDC / implementations / convergence_controller_classes / hotrod.py: 87%

38 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-13 09:00 +0000

1import numpy as np 

2 

3from pySDC.core.convergence_controller import ConvergenceController 

4 

5 

6class HotRod(ConvergenceController): 

7 """ 

8 Class that incorporates the Hot Rod detector [1] for soft faults. Based on comparing two estimates of the local 

9 error. 

10 

11 Default control order is -40. 

12 

13 See for the reference: 

14 [1]: Lightweight and Accurate Silent Data Corruption Detection in Ordinary Differential Equation Solvers, 

15 Guhur et al. 2016, Springer. DOI: https://doi.org/10.1007/978-3-319-43659-3_47 

16 """ 

17 

18 def setup(self, controller, params, description, **kwargs): 

19 """ 

20 Setup default values for crucial parameters. 

21 

22 Args: 

23 controller (pySDC.Controller): The controller 

24 params (dict): The params passed for this specific convergence controller 

25 description (dict): The description object used to instantiate the controller 

26 

27 Returns: 

28 dict: The updated params 

29 """ 

30 default_params = { 

31 "HotRod_tol": np.inf, 

32 "control_order": -40, 

33 "no_storage": False, 

34 } 

35 return {**default_params, **super().setup(controller, params, description, **kwargs)} 

36 

37 def dependencies(self, controller, description, **kwargs): 

38 """ 

39 Load the dependencies of Hot Rod, which are the two error estimators 

40 

41 Args: 

42 controller (pySDC.Controller): The controller 

43 description (dict): The description object used to instantiate the controller 

44 

45 Returns: 

46 None 

47 """ 

48 from pySDC.implementations.convergence_controller_classes.estimate_embedded_error import EstimateEmbeddedError 

49 

50 controller.add_convergence_controller( 

51 EstimateEmbeddedError.get_implementation(flavor='linearized', useMPI=self.params.useMPI), 

52 description=description, 

53 ) 

54 if not self.params.useMPI: 

55 from pySDC.implementations.convergence_controller_classes.estimate_extrapolation_error import ( 

56 EstimateExtrapolationErrorNonMPI, 

57 ) 

58 from pySDC.implementations.convergence_controller_classes.basic_restarting import BasicRestartingNonMPI 

59 

60 controller.add_convergence_controller( 

61 EstimateExtrapolationErrorNonMPI, description=description, params={'no_storage': self.params.no_storage} 

62 ) 

63 controller.add_convergence_controller( 

64 BasicRestartingNonMPI, 

65 description=description, 

66 params={'restart_from_first_step': True}, 

67 ) 

68 else: 

69 raise NotImplementedError("Don't know how to estimate extrapolated error with MPI") 

70 

71 def check_parameters(self, controller, params, description, **kwargs): 

72 """ 

73 Check whether parameters are compatible with whatever assumptions went into the step size functions etc. 

74 

75 Args: 

76 controller (pySDC.Controller): The controller 

77 params (dict): The params passed for this specific convergence controller 

78 description (dict): The description object used to instantiate the controller 

79 

80 Returns: 

81 bool: Whether the parameters are compatible 

82 str: Error message 

83 """ 

84 if self.params.HotRod_tol == np.inf: 

85 controller.logger.warning("Hot Rod needs a detection threshold, which is now set to infinity, such that a \ 

86restart is never triggered!") 

87 

88 if description["step_params"].get("restol", -1.0) >= 0: 

89 return ( 

90 False, 

91 "Hot Rod needs constant order in time and hence restol in the step parameters has to be \ 

92smaller than 0!", 

93 ) 

94 

95 if controller.params.mssdc_jac: 

96 return ( 

97 False, 

98 "Hot Rod needs the same order on all steps, please activate Gauss-Seidel multistep mode!", 

99 ) 

100 

101 return True, "" 

102 

103 def determine_restart(self, controller, S, MS, **kwargs): 

104 """ 

105 Check if the difference between the error estimates exceeds the allowed tolerance 

106 

107 Args: 

108 controller (pySDC.Controller): The controller 

109 S (pySDC.Step): The current step 

110 MS (list): List of steps 

111 

112 Returns: 

113 None 

114 """ 

115 # we determine whether to restart only on the last sweep 

116 if S.status.iter < S.params.maxiter: 

117 return None 

118 

119 for L in S.levels: 

120 if None not in [ 

121 L.status.error_extrapolation_estimate, 

122 L.status.error_embedded_estimate, 

123 ]: 

124 diff = abs(L.status.error_extrapolation_estimate - L.status.error_embedded_estimate) 

125 if diff > self.params.HotRod_tol: 

126 S.status.restart = True 

127 self.log( 

128 f"Triggering restart: e_em={L.status.error_embedded_estimate:.2e}, e_ex={L.status.error_extrapolation_estimate:.2e} -> delta={diff:.2e}, tol={self.params.HotRod_tol:.2e}", 

129 S, 

130 ) 

131 else: 

132 self.debug( 

133 f"Not triggering restart: e_em={L.status.error_embedded_estimate:.2e}, e_ex={L.status.error_extrapolation_estimate:.2e} -> delta={diff:.2e}, tol={self.params.HotRod_tol:.2e}", 

134 S, 

135 ) 

136 

137 return None 

138 

139 def post_iteration_processing(self, controller, S, **kwargs): 

140 """ 

141 Throw away the final sweep to match the error estimates. 

142 

143 Args: 

144 controller (pySDC.Controller): The controller 

145 S (pySDC.Step): The current step 

146 

147 Returns: 

148 None 

149 """ 

150 if S.status.iter == S.params.maxiter: 

151 for L in S.levels: 

152 L.u[:] = L.uold[:] 

153 

154 return None