Coverage for pySDC/implementations/convergence_controller_classes/estimate_extrapolation_error.py: 98%

163 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-02 12:30 +0000

1import numpy as np 

2from scipy.special import factorial 

3 

4from pySDC.core.convergence_controller import ConvergenceController, Status 

5from pySDC.core.errors import DataError 

6from pySDC.implementations.datatype_classes.mesh import mesh, imex_mesh 

7from pySDC.implementations.hooks.log_extrapolated_error_estimate import LogExtrapolationErrorEstimate 

8 

9 

10class EstimateExtrapolationErrorBase(ConvergenceController): 

11 """ 

12 Abstract base class for error estimates by Taylor extrapolation of solutions and right-hand sides from other times. 

13 

14 This error estimate extrapolates a solution based on Taylor expansions using solutions at other times. Child 

15 classes decide where these come from: EstimateExtrapolationErrorNonMPI uses previous time steps, and 

16 EstimateExtrapolationErrorWithinQ the collocation nodes of the current step. There is no MPI version. 

17 """ 

18 

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

20 """ 

21 Initialization routine 

22 

23 Args: 

24 controller (pySDC.Controller): The controller 

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

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

27 """ 

28 self.prev = Status(["t", "u", "f", "dt"]) # store solutions etc. of previous steps here 

29 self.coeff = Status(["u", "f", "prefactor"]) # store coefficients for extrapolation here 

30 super().__init__(controller, params, description) 

31 controller.add_hook(LogExtrapolationErrorEstimate) 

32 

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

34 """ 

35 The extrapolation based method requires storage of previous values of u, f, t and dt and also requires solving 

36 a linear system of equations to compute the Taylor expansion finite difference style. Here, all variables are 

37 initialized which are needed for this process. 

38 

39 Args: 

40 controller (pySDC.Controller): The controller 

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

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

43 

44 Returns: 

45 dict: Updated parameters with default values 

46 """ 

47 

48 from pySDC.implementations.convergence_controller_classes.hotrod import HotRod 

49 from pySDC.implementations.convergence_controller_classes.adaptivity import ( 

50 Adaptivity, 

51 ) 

52 

53 default_params = { 

54 "control_order": -75, 

55 "use_adaptivity": True in [me == Adaptivity for me in description.get("convergence_controllers", {})], 

56 "use_HotRod": True in [me == HotRod for me in description.get("convergence_controllers", {})], 

57 "order_time_marching": description["step_params"]["maxiter"], 

58 } 

59 

60 new_params = {**default_params, **super().setup(controller, params, description, **kwargs)} 

61 

62 # Do a sufficiently high order Taylor expansion 

63 new_params["Taylor_order"] = new_params["order_time_marching"] + 2 

64 

65 # Estimate and store values from this iteration 

66 new_params["estimate_iter"] = new_params["order_time_marching"] - (1 if new_params["use_HotRod"] else 0) 

67 

68 # Store n values. Since we store u and f, we need only half of each (the +1 is for rounding) 

69 new_params["n"] = (new_params["Taylor_order"] + 1) // 2 

70 new_params["n_per_proc"] = new_params["n"] * 1 

71 

72 return new_params 

73 

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

75 """ 

76 Initialize coefficient variables and add variable to the levels for extrapolated error 

77 

78 Args: 

79 controller (pySDC.controller): The controller 

80 

81 Returns: 

82 None 

83 """ 

84 self.coeff.u = [None] * self.params.n 

85 self.coeff.f = [0.0] * self.params.n 

86 

87 self.add_status_variable_to_level('error_extrapolation_estimate') 

88 

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

90 """ 

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

92 

93 Args: 

94 controller (pySDC.Controller): The controller 

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

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

97 

98 Returns: 

99 bool: Whether the parameters are compatible 

100 str: Error message 

101 """ 

102 if controller.params.mssdc_jac: 

103 return ( 

104 False, 

105 "Extrapolation error estimator needs the same order on all steps, please activate Gauss-Seid\ 

106el multistep mode!", 

107 ) 

108 

109 return True, "" 

110 

111 def store_values(self, S, **kwargs): 

112 """ 

113 Store the required attributes of the step to do the extrapolation. We only care about the last collocation 

114 node on the finest level at the moment. 

115 

116 Args: 

117 S (pySDC.Step): The current step 

118 

119 Returns: 

120 None 

121 """ 

122 # figure out which values are to be replaced by the new ones 

123 if None in self.prev.t: 

124 oldest_val = len(self.prev.t) - len(self.prev.t[self.prev.t == [None]]) 

125 else: 

126 oldest_val = np.argmin(self.prev.t) 

127 

128 # figure out how to store the right hand side 

129 f = S.levels[0].f[-1] 

130 if type(f) == imex_mesh: 

131 self.prev.f[oldest_val] = f.impl + f.expl 

132 elif type(f) == mesh: 

133 self.prev.f[oldest_val] = f 

134 else: 

135 raise DataError(f"Unable to store f from datatype {type(f)}, extrapolation based error estimate only\ 

136 works with types imex_mesh and mesh") 

137 

138 # store the rest of the values 

139 self.prev.u[oldest_val] = S.levels[0].u[-1] 

140 self.prev.t[oldest_val] = S.time + S.dt 

141 self.prev.dt[oldest_val] = S.dt 

142 

143 return None 

144 

145 def get_extrapolation_coefficients(self, t, dt, t_eval): 

146 """ 

147 This function solves a linear system where in the matrix A, the row index reflects the order of the derivative 

148 in the Taylor expansion and the column index reflects the particular step and whether its u or f from that 

149 step. The vector b on the other hand, contains a 1 in the first entry and zeros elsewhere, since we want to 

150 compute the value itself and all the derivatives should vanish after combining the Taylor expansions. This 

151 works to the order of the number of rows and since we want a square matrix for solving, we need the same amount 

152 of columns, which determines the memory overhead, since it is equal to the solutions / rhs that we need in 

153 memory at the time of evaluation. 

154 

155 This is enough to get the extrapolated solution, but if we want to compute the local error, we have to compute 

156 a prefactor. This is based on error accumulation between steps (first step's solution is exact plus 1 LTE, 

157 second solution is exact plus 2 LTE and so on), which can be computed for adaptive step sizes as well. However, 

158 this is only true for linear problems, which means we expect the error estimate to work less well for non-linear 

159 problems. 

160 

161 Since only time differences are important for computing the coefficients, we need to compute this only once when 

162 using constant step sizes. When we allow the step size to change, however, we need to recompute this in every 

163 step, which is activated by the `use_adaptivity` parameter. 

164 

165 Solving for the coefficients requires solving a dense linear system of equations. The number of unknowns is 

166 equal to the order of the Taylor expansion, so this step should be cheap compared to the solves in each SDC 

167 iteration. 

168 

169 The function stores the computed coefficients in the `self.coeff` variables. 

170 

171 Args: 

172 t (list): The list of times at which we have solutions available 

173 dt (list): The step sizes used for computing these solutions (needed for the prefactor) 

174 t_eval (float): The time we want to extrapolate to 

175 

176 Returns: 

177 None 

178 """ 

179 

180 # prepare A matrix 

181 A = np.zeros((self.params.Taylor_order, self.params.Taylor_order)) 

182 A[0, 0 : self.params.n] = 1.0 

183 j = np.arange(self.params.Taylor_order) 

184 inv_facs = 1.0 / factorial(j) 

185 

186 # get the steps backwards from the point of evaluation 

187 idx = np.argsort(t) 

188 steps_from_now = t[idx] - t_eval 

189 

190 # fill A matrix 

191 for i in range(1, self.params.Taylor_order): 

192 # Taylor expansions of the solutions 

193 A[i, : self.params.n] = steps_from_now ** j[i] * inv_facs[i] 

194 

195 # Taylor expansions of the first derivatives a.k.a. right hand side evaluations 

196 A[i, self.params.n : self.params.Taylor_order] = ( 

197 steps_from_now[2 * self.params.n - self.params.Taylor_order :] ** (j[i] - 1) * inv_facs[i - 1] 

198 ) 

199 

200 # prepare rhs 

201 b = np.zeros(self.params.Taylor_order) 

202 b[0] = 1.0 

203 

204 # solve linear system for the coefficients 

205 coeff = np.linalg.solve(A, b) 

206 self.coeff.u = coeff[: self.params.n] 

207 self.coeff.f[self.params.n * 2 - self.params.Taylor_order :] = coeff[self.params.n : self.params.Taylor_order] 

208 

209 # determine prefactor 

210 step_size_ratios = abs(dt[len(dt) - len(self.coeff.u) :] / dt[-1]) ** (self.params.Taylor_order - 1) 

211 inv_prefactor = -sum(step_size_ratios[1:]) - 1.0 

212 for i in range(len(self.coeff.u)): 

213 inv_prefactor += sum(step_size_ratios[1 : i + 1]) * self.coeff.u[i] 

214 self.coeff.prefactor = 1.0 / abs(inv_prefactor) 

215 

216 return None 

217 

218 

219class EstimateExtrapolationErrorNonMPI(EstimateExtrapolationErrorBase): 

220 """ 

221 Implementation of the extrapolation error estimate for the non-MPI controller. 

222 """ 

223 

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

225 """ 

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

227 

228 Args: 

229 controller (pySDC.Controller): The controller 

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

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

232 

233 Returns: 

234 bool: Whether the parameters are compatible 

235 str: Error message 

236 """ 

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

238 return ( 

239 False, 

240 "Extrapolation error needs constant order in time and hence restol in the level parameters \ 

241has to be smaller than 0!", 

242 ) 

243 

244 return super().check_parameters(controller, params, description, **kwargs) 

245 

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

247 """ 

248 Add a no parameter 'no_storage' which decides whether the standard or the no-memory-overhead version is run, 

249 where only values are used for extrapolation which are in memory of other processes 

250 

251 Args: 

252 controller (pySDC.Controller): The controller 

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

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

255 

256 Returns: 

257 dict: Updated parameters with default values 

258 """ 

259 default_params = super().setup(controller, params, description) 

260 

261 non_mpi_defaults = { 

262 "no_storage": False, 

263 } 

264 

265 return {**non_mpi_defaults, **default_params} 

266 

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

268 """ 

269 Initialize storage variables. 

270 

271 Args: 

272 controller (pySDC.controller): The controller 

273 

274 Returns: 

275 None 

276 """ 

277 super().setup_status_variables(controller, **kwargs) 

278 

279 self.prev.t = np.array([None] * self.params.n) 

280 self.prev.dt = np.array([None] * self.params.n) 

281 self.prev.u = [None] * self.params.n 

282 self.prev.f = [None] * self.params.n 

283 

284 return None 

285 

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

287 """ 

288 We perform three key operations here in the last iteration: 

289 - Compute the error estimate 

290 - Compute the coefficients if needed 

291 - Store the values of the step if we pretend not to for the no-memory overhead version 

292 

293 Args: 

294 controller (pySDC.Controller): The controller 

295 S (pySDC.Step): The current step 

296 

297 Returns: 

298 None 

299 """ 

300 if S.status.iter == self.params.estimate_iter: 

301 t_eval = S.time + S.dt 

302 

303 # compute the extrapolation coefficients if needed 

304 if ( 

305 ( 

306 None in self.coeff.u 

307 or self.params.use_adaptivity 

308 or (not self.params.no_storage and S.status.time_size > 1) 

309 ) 

310 and None not in self.prev.t 

311 and t_eval > max(self.prev.t) 

312 ): 

313 self.get_extrapolation_coefficients(self.prev.t, self.prev.dt, t_eval) 

314 

315 # compute the error if we can 

316 if None not in self.coeff.u and None not in self.prev.t: 

317 self.get_extrapolated_error(S) 

318 

319 # store the solution and pretend we didn't because in the non MPI version we take a few shortcuts 

320 if self.params.no_storage: 

321 self.store_values(S) 

322 

323 return None 

324 

325 def prepare_next_block(self, controller, S, size, time, Tend, MS, **kwargs): 

326 """ 

327 If the no-memory-overhead version is used, we need to delete stuff that shouldn't be available. Otherwise, we 

328 need to store all the stuff that we can. 

329 

330 Args: 

331 controller (pySDC.Controller): The controller 

332 S (pySDC.step): The current step 

333 size (int): Number of ranks 

334 time (float): The current time 

335 Tend (float): The final time 

336 MS (list): Active steps 

337 

338 Returns: 

339 None 

340 """ 

341 

342 # delete values that should not be available in the next step 

343 if self.params.no_storage: 

344 self.prev.t = np.array([None] * self.params.n) 

345 self.prev.dt = np.array([None] * self.params.n) 

346 self.prev.u = [None] * self.params.n 

347 self.prev.f = [None] * self.params.n 

348 

349 else: 

350 # decide where we need to restart to store everything up to that point 

351 restarts = [S.status.restart for S in MS] 

352 restart_at = np.where(restarts)[0][0] if True in restarts else len(MS) 

353 

354 # store values in the current block that don't need restarting 

355 if restart_at > S.status.slot: 

356 self.store_values(S) 

357 

358 return None 

359 

360 def get_extrapolated_solution(self, S, **kwargs): 

361 """ 

362 Combine values from previous steps to extrapolate. 

363 

364 Args: 

365 S (pySDC.Step): The current step 

366 

367 Returns: 

368 dtype_u: The extrapolated solution 

369 """ 

370 if len(S.levels) > 1: 

371 raise NotImplementedError("Extrapolated estimate only works on the finest level for now") 

372 

373 # prepare variables 

374 u_ex = S.levels[0].u[-1] * 0.0 

375 idx = np.argsort(self.prev.t) 

376 

377 # see if we have a solution for the current step already stored 

378 if (abs(S.time + S.dt - self.prev.t) < 10.0 * np.finfo(float).eps).any(): 

379 idx_step = idx[np.argmin(abs(self.prev.t - S.time - S.dt))] 

380 else: 

381 idx_step = max(idx) + 1 

382 

383 # make a mask of all the steps we want to include in the extrapolation 

384 mask = np.logical_and(idx < idx_step, idx >= idx_step - self.params.n) 

385 

386 # do the extrapolation by summing everything up 

387 for i in range(self.params.n): 

388 u_ex += self.coeff.u[i] * self.prev.u[idx[mask][i]] + self.coeff.f[i] * self.prev.f[idx[mask][i]] 

389 

390 return u_ex 

391 

392 def get_extrapolated_error(self, S, **kwargs): 

393 """ 

394 The extrapolation estimate combines values of u and f from multiple steps to extrapolate and compare to the 

395 solution obtained by the time marching scheme. 

396 

397 Args: 

398 S (pySDC.Step): The current step 

399 

400 Returns: 

401 None 

402 """ 

403 u_ex = self.get_extrapolated_solution(S) 

404 if u_ex is not None: 

405 difference = u_ex - S.levels[0].u[-1] 

406 

407 # algebraic components of a DAE have no time derivative to extrapolate 

408 diff_mask = getattr(S.levels[0].prob, 'diff_mask', None) 

409 if diff_mask is not None and not all(diff_mask): 

410 difference = difference[diff_mask] 

411 

412 S.levels[0].status.error_extrapolation_estimate = abs(difference) * self.coeff.prefactor 

413 else: 

414 S.levels[0].status.error_extrapolation_estimate = None 

415 

416 

417class EstimateExtrapolationErrorWithinQ(EstimateExtrapolationErrorBase): 

418 """ 

419 Estimate the local error by comparing the converged SDC solution to an extrapolation from other quadrature nodes. 

420 Collocation methods compute a high order solution from a linear combination 

421 of solutions at intermediate time points. While the intermediate solutions (a.k.a. stages) don't share the order of 

422 accuracy with the solution at the end of the interval, for SDC we know that the order is equal to the number of 

423 nodes + 1 (locally). This is because the solution to the collocation problem is a polynomial approximation of order 

424 of the number of nodes. 

425 That means we can do a Taylor expansion around the end point of the interval to higher order and after cancelling 

426 terms just like we are used to with the extrapolation based error estimate across multiple steps, we get an error 

427 estimate that is of the order of accuracy of the stages. 

428 This can be used for adaptivity, for instance, with the nice property that it doesn't matter how we arrived at the 

429 converged collocation solution, as long as we did. We don't rely on knowing the order of accuracy after every sweep, 

430 only after convergence of the collocation problem has been achieved, which we can check from the residual. 

431 """ 

432 

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

434 """ 

435 We need this convergence controller to become active after the check for convergence, because we need the step 

436 to be converged. 

437 

438 Args: 

439 controller (pySDC.Controller): The controller 

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

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

442 

443 Returns: 

444 dict: Updated parameters with default values 

445 """ 

446 from pySDC.core.check_convergence import CheckConvergence 

447 

448 num_nodes = description['sweeper_params']['num_nodes'] 

449 self.comm = description['sweeper_params'].get('comm', None) 

450 if self.comm: 

451 from mpi4py import MPI 

452 

453 self.sum = MPI.SUM 

454 

455 self.check_convergence = CheckConvergence.check_convergence 

456 

457 default_params = { 

458 'Taylor_order': 2 * num_nodes, 

459 'n': num_nodes, 

460 'recompute_coefficients': False, 

461 **params, 

462 } 

463 

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

465 

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

467 """ 

468 Compute the extrapolated error estimate here if the step is converged. 

469 

470 Args: 

471 controller (pySDC.Controller): The controller 

472 S (pySDC.Step): The current step 

473 

474 Returns: 

475 None 

476 """ 

477 

478 if not self.check_convergence(S): 

479 return None 

480 

481 lvl = S.levels[0] 

482 

483 nodes_ = lvl.sweep.coll.nodes * S.dt 

484 nodes = S.time + np.append(0, nodes_[:-1]) 

485 t_eval = S.time + nodes_[-1] 

486 

487 dts = np.append(nodes_[0], nodes_[1:] - nodes_[:-1]) 

488 self.params.Taylor_order = len(nodes) 

489 self.params.n = len(nodes) 

490 

491 # compute the extrapolation coefficients 

492 if None in self.coeff.u or self.params.recompute_coefficients: 

493 self.get_extrapolation_coefficients(nodes, dts, t_eval) 

494 

495 # compute the extrapolated solution 

496 if lvl.f[0] is None: 

497 lvl.f[0] = lvl.prob.eval_f(lvl.u[0], lvl.time) 

498 

499 if type(lvl.f[0]) == imex_mesh: 

500 f = [lvl.f[i].impl + lvl.f[i].expl if self.coeff.f[i] and lvl.f[i] else 0.0 for i in range(len(lvl.f) - 1)] 

501 elif type(lvl.f[0]) == mesh: 

502 f = [lvl.f[i] if self.coeff.f[i] else 0.0 for i in range(len(lvl.f) - 1)] 

503 else: 

504 raise DataError(f"Unable to store f from datatype {type(lvl.f[0])}, extrapolation based error estimate only\ 

505 works with types imex_mesh and mesh") 

506 

507 # compute the error with the weighted sum 

508 if self.comm: 

509 idx = (self.comm.rank + 1) % self.comm.size 

510 sendbuf = self.coeff.u[idx] * lvl.u[idx] + self.coeff.f[idx] * f[idx] 

511 u_ex = lvl.prob.dtype_u(lvl.prob.init, val=0.0) if self.comm.rank == self.comm.size - 1 else None 

512 self.comm.Reduce(sendbuf, u_ex, op=self.sum, root=self.comm.size - 1) 

513 else: 

514 u_ex = lvl.prob.dtype_u(lvl.prob.init, val=0.0) 

515 for i in range(self.params.n): 

516 u_ex += self.coeff.u[i] * lvl.u[i] + self.coeff.f[i] * f[i] 

517 

518 # store the error 

519 if self.comm: 

520 error = ( 

521 abs(u_ex - lvl.u[self.comm.rank + 1]) * self.coeff.prefactor 

522 if self.comm.rank == self.comm.size - 1 

523 else None 

524 ) 

525 lvl.status.error_extrapolation_estimate = self.comm.bcast(error, root=self.comm.size - 1) 

526 else: 

527 lvl.status.error_extrapolation_estimate = abs(u_ex - lvl.u[-1]) * self.coeff.prefactor 

528 return None