Coverage for pySDC/implementations/convergence_controller_classes/estimate_embedded_error.py: 78%

115 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, Pars 

4from pySDC.implementations.convergence_controller_classes.store_uold import StoreUOld 

5 

6from pySDC.implementations.sweeper_classes.Runge_Kutta import RungeKutta 

7 

8 

9class EstimateEmbeddedError(ConvergenceController): 

10 """ 

11 Estimate the local error as the difference between two solutions of different order, such as two consecutive sweeps. 

12 The embedded error is obtained by computing two solutions of different accuracy and pretending the more accurate 

13 one is an exact solution from the point of view of the less accurate solution. In practice, we like to compute the 

14 solutions with different order methods, meaning that in SDC we can just subtract two consecutive sweeps, as long as 

15 you make sure your preconditioner is compatible, which you have to just try out... 

16 """ 

17 

18 @classmethod 

19 def get_implementation(cls, flavor='standard', useMPI=False): 

20 """ 

21 Retrieve the implementation for a specific flavor of this class. 

22 

23 Args: 

24 flavor (str): The implementation that you want 

25 useMPI (bool): Whether to return the MPI version, only relevant for the `'linearized'` flavor 

26 

27 Returns: 

28 cls: The child class that implements the desired flavor 

29 """ 

30 if flavor == 'standard': 

31 return cls 

32 elif flavor == 'linearized': 

33 if useMPI: 

34 return EstimateEmbeddedErrorLinearizedMPI 

35 else: 

36 return EstimateEmbeddedErrorLinearizedNonMPI 

37 elif flavor == 'collocation': 

38 return EstimateEmbeddedErrorCollocation 

39 else: 

40 raise NotImplementedError(f'Flavor {flavor} of EstimateEmbeddedError is not implemented!') 

41 

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

43 """ 

44 Add a default value for control order to the parameters and check if we are using a Runge-Kutta sweeper 

45 

46 Args: 

47 controller (pySDC.Controller): The controller 

48 params (dict): Parameters for the convergence controller 

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

50 

51 Returns: 

52 dict: Updated parameters 

53 """ 

54 sweeper_type = 'SDC' 

55 if RungeKutta in description['sweeper_class'].__mro__: 

56 sweeper_type = 'RK' 

57 elif 'SweeperMPI' in [me.__name__ for me in description['sweeper_class'].__mro__]: 

58 sweeper_type = 'MPI' 

59 return { 

60 "control_order": -80, 

61 "sweeper_type": sweeper_type, 

62 "rel_error": False, 

63 **super().setup(controller, params, description, **kwargs), 

64 } 

65 

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

67 """ 

68 Load the convergence controller that stores the solution of the last sweep unless we are doing Runge-Kutta. 

69 Add the hook for recording the error. 

70 

71 Args: 

72 controller (pySDC.Controller): The controller 

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

74 

75 Returns: 

76 None 

77 """ 

78 if RungeKutta not in description["sweeper_class"].__bases__: 

79 controller.add_convergence_controller(StoreUOld, description=description) 

80 

81 from pySDC.implementations.hooks.log_embedded_error_estimate import LogEmbeddedErrorEstimate 

82 

83 controller.add_hook(LogEmbeddedErrorEstimate) 

84 return None 

85 

86 def estimate_embedded_error_serial(self, L): 

87 """ 

88 Estimate the serial embedded error, which may need to be modified for a parallel estimate. 

89 

90 Depending on the type of sweeper, the lower order solution is stored in a different place. 

91 

92 Args: 

93 L (pySDC.level): The level 

94 

95 Returns: 

96 dtype_u: The embedded error estimate 

97 """ 

98 if self.params.sweeper_type == "RK": 

99 L.sweep.compute_end_point() 

100 if self.params.rel_error: 

101 return abs(L.uend - L.sweep.u_secondary) / abs(L.uend) 

102 else: 

103 return abs(L.uend - L.sweep.u_secondary) 

104 elif self.params.sweeper_type == "SDC": 

105 # order rises by one between sweeps 

106 if self.params.rel_error: 

107 return abs(L.uold[-1] - L.u[-1]) / abs(L.u[-1]) 

108 else: 

109 return abs(L.uold[-1] - L.u[-1]) 

110 elif self.params.sweeper_type == 'MPI': 

111 comm = L.sweep.comm 

112 if self.params.rel_error: 

113 return comm.bcast( 

114 abs(L.uold[comm.rank + 1] - L.u[comm.rank + 1]) / abs(L.u[comm.rank + 1]), root=comm.size - 1 

115 ) 

116 else: 

117 return comm.bcast(abs(L.uold[comm.rank + 1] - L.u[comm.rank + 1]), root=comm.size - 1) 

118 else: 

119 raise NotImplementedError(f"Don't know how to estimate embedded error for sweeper type \ 

120\"{self.params.sweeper_type}\"") 

121 

122 def setup_status_variables(self, controller, **kwargs): 

123 """ 

124 Add the embedded error to the level status 

125 

126 Args: 

127 controller (pySDC.Controller): The controller 

128 """ 

129 self.add_status_variable_to_level('error_embedded_estimate') 

130 self.add_status_variable_to_level('increment') 

131 

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

133 """ 

134 Estimate the local error here. 

135 

136 If you are doing MSSDC, this is the global error within the block in Gauss-Seidel mode. 

137 In Jacobi mode, I haven't thought about what this is. 

138 

139 Args: 

140 controller (pySDC.Controller): The controller 

141 S (pySDC.Step): The current step 

142 

143 Returns: 

144 None 

145 """ 

146 

147 if S.status.iter > 0 or self.params.sweeper_type == "RK": 

148 for L in S.levels: 

149 L.status.error_embedded_estimate = max([self.estimate_embedded_error_serial(L), np.finfo(float).eps]) 

150 L.status.increment = L.status.error_embedded_estimate * 1 

151 self.debug(f'L.status.error_embedded_estimate={L.status.error_embedded_estimate:.5e}', S) 

152 

153 return None 

154 

155 

156class EstimateEmbeddedErrorLinearizedNonMPI(EstimateEmbeddedError): 

157 """ 

158 Local error in a block of steps as the embedded estimate minus that of the step before, for the non-MPI controller. 

159 """ 

160 

161 def __init__(self, controller, params, description, **kwargs): 

162 """ 

163 Initialisation routine. Add the buffers for communication. 

164 

165 Args: 

166 controller (pySDC.Controller): The controller 

167 params (dict): Parameters for the convergence controller 

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

169 """ 

170 super().__init__(controller, params, description, **kwargs) 

171 self.buffers = Pars({'e_em_last': 0.0}) 

172 

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

174 """ 

175 Add option for averaging the local errors. 

176 

177 Args: 

178 controller (pySDC.Controller): The controller 

179 params (dict): Parameters for the convergence controller 

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

181 

182 Returns: 

183 dict: Updated parameters 

184 """ 

185 return { 

186 'averaged': False, 

187 **super().setup(controller, params, description, **kwargs), 

188 } 

189 

190 def reset_buffers_nonMPI(self, controller, **kwargs): 

191 """ 

192 Reset buffers for imitated communication. 

193 

194 Args: 

195 controller (pySDC.controller): The controller 

196 

197 Returns: 

198 None 

199 """ 

200 self.buffers.e_em_last = 0.0 

201 return None 

202 

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

204 """ 

205 Compute embedded error estimate on the last node of each level 

206 In serial this is the local error, but in block Gauss-Seidel MSSDC this is a semi-global error in each block 

207 

208 Args: 

209 controller (pySDC.Controller): The controller 

210 S (pySDC.Step): The current step 

211 

212 Returns: 

213 None 

214 """ 

215 if len(S.levels) > 1 and len(controller.MS) > 1: 

216 raise NotImplementedError("Embedded error estimate only works for serial multi-level or parallel single \ 

217level") 

218 

219 if S.status.iter > 0 or self.params.sweeper_type == "RK": 

220 if self.params.averaged: 

221 averaging = float(S.status.slot + 1) 

222 else: 

223 averaging = 1.0 

224 

225 for L in S.levels: 

226 temp = self.estimate_embedded_error_serial(L) 

227 L.status.error_embedded_estimate = max( 

228 [abs(temp - self.buffers.e_em_last) / averaging, np.finfo(float).eps] 

229 ) 

230 

231 if not self.params.averaged: 

232 self.buffers.e_em_last = temp * 1.0 

233 

234 return None 

235 

236 

237class EstimateEmbeddedErrorLinearizedMPI(EstimateEmbeddedError): 

238 """ 

239 Local error in a block of steps as the embedded estimate minus that of the step before, for the MPI controller. 

240 """ 

241 

242 def __init__(self, controller, params, description, **kwargs): 

243 """ 

244 Initialisation routine. Add the buffers for communication. 

245 

246 Args: 

247 controller (pySDC.Controller): The controller 

248 params (dict): Parameters for the convergence controller 

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

250 """ 

251 super().__init__(controller, params, description, **kwargs) 

252 self.buffers = Pars({'e_em_last': 0.0}) 

253 

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

255 """ 

256 Compute embedded error estimate on the last node of each level 

257 In serial this is the local error, but in block Gauss-Seidel MSSDC this is a semi-global error in each block 

258 

259 Args: 

260 controller (pySDC.Controller): The controller 

261 S (pySDC.Step): The current step 

262 

263 Returns: 

264 None 

265 """ 

266 comm = kwargs['comm'] 

267 

268 if S.status.iter > 0 or self.params.sweeper_type == "RK": 

269 for L in S.levels: 

270 # get accumulated local errors from previous steps 

271 if not S.status.first: 

272 if not S.status.prev_done: 

273 self.buffers.e_em_last = self.recv(comm, S.status.slot - 1) 

274 else: 

275 self.buffers.e_em_last = 0.0 

276 

277 # estimate accumulated local error 

278 temp = self.estimate_embedded_error_serial(L) 

279 

280 # estimate local error as difference of accumulated errors 

281 L.status.error_embedded_estimate = max([abs(temp - self.buffers.e_em_last), np.finfo(float).eps]) 

282 

283 # send the accumulated local errors forward 

284 if not S.status.last: 

285 self.send(comm, dest=S.status.slot + 1, data=temp, blocking=True) 

286 

287 return None 

288 

289 

290class EstimateEmbeddedErrorCollocation(ConvergenceController): 

291 """ 

292 Estimates an embedded error based on changing the underlying quadrature rule. The error estimate is stored as 

293 `error_embedded_estimate_collocation` in the status of the level. Note that we only compute the estimate on the 

294 finest level. The error is stored as a tuple with the first index denoting to which iteration it belongs. This 

295 is useful since the error estimate is not available immediately after, but only when the next collocation problem 

296 is converged to make sure the two solutions are of different accuracy. 

297 

298 Changing the collocation method between iterations happens using the `AdaptiveCollocation` convergence controller. 

299 Please refer to that for documentation on how to use this. Just pass the parameters for that convergence controller 

300 as `adaptive_coll_params` to the parameters for this one and they will be passed on when the `AdaptiveCollocation` 

301 convergence controller is automatically added while loading dependencies. 

302 """ 

303 

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

305 """ 

306 Add a default value for control order to the parameters 

307 

308 Args: 

309 controller (pySDC.Controller): The controller 

310 params (dict): Parameters for the convergence controller 

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

312 

313 Returns: 

314 dict: Updated parameters 

315 """ 

316 defaults = { 

317 "control_order": 210, 

318 "adaptive_coll_params": {}, 

319 **super().setup(controller, params, description, **kwargs), 

320 } 

321 return defaults 

322 

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

324 """ 

325 Load the `AdaptiveCollocation` convergence controller to switch between collocation problems between iterations. 

326 

327 Args: 

328 controller (pySDC.Controller): The controller 

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

330 """ 

331 from pySDC.implementations.convergence_controller_classes.adaptive_collocation import AdaptiveCollocation 

332 

333 controller.add_convergence_controller( 

334 AdaptiveCollocation, params=self.params.adaptive_coll_params, description=description 

335 ) 

336 from pySDC.implementations.hooks.log_embedded_error_estimate import LogEmbeddedErrorEstimate 

337 

338 controller.add_hook(LogEmbeddedErrorEstimate) 

339 

340 def post_iteration_processing(self, controller, step, **kwargs): 

341 """ 

342 Compute the embedded error as the difference between the interpolated and the current solution on the finest 

343 level. 

344 

345 Args: 

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

347 step (pySDC.Step.step): The current step 

348 """ 

349 if step.status.done: 

350 lvl = step.levels[0] 

351 lvl.sweep.compute_end_point() 

352 step.status.coll_u += (lvl.uend,) 

353 step.status.coll_iter += (step.status.iter,) 

354 

355 if len(step.status.coll_u) > 1: 

356 lvl.status.error_embedded_estimate_collocation = ( 

357 step.status.coll_iter[-2], 

358 max([np.finfo(float).eps, abs(step.status.coll_u[-1] - step.status.coll_u[-2])]), 

359 ) 

360 

361 def setup_status_variables(self, *args, **kwargs): 

362 """ 

363 Add the embedded error variable to the levels and add status variables for the collocation problems solved in 

364 the current step. 

365 

366 These live on the step rather than on this object, since there is one instance of this class per controller, 

367 which is a whole block of steps when running with more than one process. They are tuples, so that the same empty 

368 value can be set on all steps and appending to it on one step does not change it on the others. 

369 

370 Args: 

371 controller (pySDC.Controller): The controller 

372 """ 

373 self.add_status_variable_to_step('coll_u', ()) # the solutions of converged collocation problems 

374 self.add_status_variable_to_step('coll_iter', ()) # the iteration in which the solution converged 

375 

376 self.add_status_variable_to_level('error_embedded_estimate_collocation') 

377 

378 def reset_status_variables(self, *args, **kwargs): 

379 """ 

380 Forget the collocation problems solved in the previous step, or the previous attempt of this step. 

381 

382 Args: 

383 controller (pySDC.Controller): The controller 

384 """ 

385 self.set_step_status_variable('coll_u', ()) 

386 self.set_step_status_variable('coll_iter', ())