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

68 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-02 12:30 +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 S.status.force_continue = False 

107 

108 return None 

109 

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

111 """ 

112 Share convergence status across the block. 

113 

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

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

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

117 

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

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

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

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

122 

123 Args: 

124 controller (pySDC.Controller): The controller 

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

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

127 

128 Returns: 

129 None 

130 """ 

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

132 

133 if controller.params.all_to_done: 

134 for hook in controller.hooks: 

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

136 

137 if comm is None: 

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

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

140 else: 

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

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

143 

144 for hook in controller.hooks: 

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

146 

147 S.status.done = S.status.done or S.status.force_done 

148 

149 else: 

150 if comm is None: 

151 if not S.status.first: 

152 for hook in controller.hooks: 

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

154 S.status.prev_done = S.prev.status.done 

155 for hook in controller.hooks: 

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

157 S.status.done = S.status.done and S.status.prev_done 

158 return None 

159 

160 for hook in controller.hooks: 

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

162 

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

164 controller.wait_for_request(request=controller.req_status) 

165 if S.status.force_done: 

166 return None 

167 

168 # recv status 

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

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

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

172 S.status.prev_done = buff[0] 

173 S.status.done = S.status.done and S.status.prev_done 

174 

175 # send status forward 

176 if not S.status.last: 

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

178 buff[0] = S.status.done 

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

180 

181 for hook in controller.hooks: 

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