Coverage for pySDC/core/check_convergence.py: 97%

70 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-15 06:23 +0000

1import numpy as np 

2 

3from pySDC.core.convergence_controller import ConvergenceController 

4 

5 

6class CheckConvergence(ConvergenceController): 

7 """ 

8 Perform simple checks on convergence for SDC iterations. 

9 

10 Iteration is terminated via one of two criteria: 

11 - Residual tolerance 

12 - Maximum number of iterations 

13 """ 

14 

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

16 """ 

17 Define default parameters here 

18 

19 Args: 

20 controller (pySDC.Controller): The controller 

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

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

23 

24 Returns: 

25 (dict): The updated params dictionary 

26 """ 

27 defaults = {'control_order': +200, 'use_e_tol': 'e_tol' in description['level_params'].keys()} 

28 

29 return {**defaults, **super().setup(controller, params, description, **kwargs)} 

30 

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

32 """ 

33 Load the embedded error estimator if needed. 

34 

35 Args: 

36 controller (pySDC.Controller): The controller 

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

38 

39 Returns: 

40 None 

41 """ 

42 if self.params.useMPI: 

43 self.prepare_MPI_logical_operations() 

44 

45 super().dependencies(controller, description) 

46 

47 if self.params.use_e_tol: 

48 from pySDC.implementations.convergence_controller_classes.estimate_embedded_error import ( 

49 EstimateEmbeddedError, 

50 ) 

51 

52 controller.add_convergence_controller( 

53 EstimateEmbeddedError, 

54 description=description, 

55 ) 

56 

57 return None 

58 

59 @staticmethod 

60 def check_convergence(S, self=None): 

61 """ 

62 Check the convergence of a single step. 

63 Test the residual and max. number of iterations as well as allowing overrides to both stop and continue. 

64 

65 Args: 

66 S (pySDC.Step): The current step 

67 

68 Returns: 

69 bool: Convergence status of the step 

70 """ 

71 # do all this on the finest level 

72 L = S.levels[0] 

73 

74 # get residual and check against prescribed tolerance (plus check number of iterations) 

75 iter_converged = S.status.iter >= S.params.maxiter 

76 res_converged = L.status.residual <= L.params.restol and (S.status.iter > 0 or L.status.sweep > 0) 

77 e_tol_converged = ( 

78 L.status.increment < L.params.e_tol if (L.params.get('e_tol') and L.status.get('increment')) else False 

79 ) 

80 converged = ( 

81 iter_converged or res_converged or e_tol_converged or S.status.force_done 

82 ) and not S.status.force_continue 

83 if converged is None: 

84 converged = False 

85 

86 # print information for debugging 

87 if converged and self: 

88 self.debug( 

89 f'Declared convergence: maxiter reached[{"x" if iter_converged else " "}] restol reached[{"x" if res_converged else " "}] e_tol reached[{"x" if e_tol_converged else " "}]', 

90 S, 

91 ) 

92 return converged 

93 

94 def check_iteration_status(self, controller, S, **kwargs): 

95 """ 

96 Routine to determine whether to stop iterating (currently testing the residual + the max. number of iterations) 

97 

98 Args: 

99 controller (pySDC.Controller.controller): The controller 

100 S (pySDC.Step.step): The current step 

101 

102 Returns: 

103 None 

104 """ 

105 S.status.done = self.check_convergence(S, self) 

106 

107 if "comm" in kwargs.keys(): 

108 self.communicate_convergence(controller, S, **kwargs) 

109 

110 S.status.force_continue = False 

111 

112 return None 

113 

114 def communicate_convergence(self, controller, S, comm=None, **kwargs): 

115 """ 

116 Share convergence status across the block. 

117 

118 Two ways to stop, and the same two whether the block is spread over ranks or sitting in one 

119 process. Either every step has to agree, which is a reduction, or each step waits on its 

120 predecessor, which is a cascade and is what lets an early step finish and drop out. 

121 

122 The only difference between the transports is how the neighbour is reached: an `allreduce` 

123 and point-to-point messages with one step per rank, reading the other steps' status directly 

124 when they are all here. Note the reduction is applied step by step in the second case rather 

125 than all at once as `allreduce` does -- both operations are monotone, so the two agree. 

126 

127 Args: 

128 controller (pySDC.Controller): The controller 

129 S (pySDC.Step.step): The current step 

130 comm (mpi4py.MPI.Intracomm): Communicator, or None when the whole block is in one process 

131 

132 Returns: 

133 None 

134 """ 

135 block = kwargs.get('MS', controller.steps) 

136 

137 if controller.params.all_to_done: 

138 for hook in controller.hooks: 

139 hook.pre_comm(step=S, level_number=0) 

140 

141 if comm is None: 

142 S.status.done = all(T.status.done for T in block) 

143 S.status.force_done = any(T.status.force_done for T in block) 

144 else: 

145 S.status.done = comm.allreduce(sendobj=S.status.done, op=self.MPI_LAND) 

146 S.status.force_done = comm.allreduce(sendobj=S.status.force_done, op=self.MPI_LOR) 

147 

148 for hook in controller.hooks: 

149 hook.post_comm(step=S, level_number=0, add_to_stats=True) 

150 

151 S.status.done = S.status.done or S.status.force_done 

152 

153 else: 

154 if comm is None: 

155 if not S.status.first: 

156 for hook in controller.hooks: 

157 hook.pre_comm(step=S, level_number=0) 

158 S.status.prev_done = S.prev.status.done 

159 for hook in controller.hooks: 

160 hook.post_comm(step=S, level_number=0, add_to_stats=True) 

161 S.status.done = S.status.done and S.status.prev_done 

162 return None 

163 

164 for hook in controller.hooks: 

165 hook.pre_comm(step=S, level_number=0) 

166 

167 # check if an open request of the status send is pending 

168 controller.wait_for_request(request=controller.req_status) 

169 if S.status.force_done: 

170 return None 

171 

172 # recv status 

173 if not S.status.first and not S.status.prev_done: 

174 buff = np.empty(1, dtype=bool) 

175 self.Recv(comm, source=S.status.slot - 1, buffer=[buff, self.MPI_BOOL]) 

176 S.status.prev_done = buff[0] 

177 S.status.done = S.status.done and S.status.prev_done 

178 

179 # send status forward 

180 if not S.status.last: 

181 buff = np.empty(1, dtype=bool) 

182 buff[0] = S.status.done 

183 self.Send(comm, dest=S.status.slot + 1, buffer=[buff, self.MPI_BOOL]) 

184 

185 for hook in controller.hooks: 

186 hook.post_comm(step=S, level_number=0, add_to_stats=True)