Coverage for pySDC/implementations/convergence_controller_classes/adaptivity.py: 92%

252 statements  

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

1import numpy as np 

2from pySDC.core.convergence_controller import ConvergenceController 

3from pySDC.implementations.convergence_controller_classes.step_size_limiter import ( 

4 StepSizeLimiter, 

5) 

6 

7 

8class AdaptivityBase(ConvergenceController): 

9 """ 

10 Abstract base class for adaptivity from arbitrary local error estimates and step size update rules. 

11 """ 

12 

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

14 """ 

15 Define default parameters here. 

16 

17 Default parameters are: 

18 - control_order (int): The order relative to other convergence controllers 

19 - beta (float): The safety factor 

20 

21 Args: 

22 controller (pySDC.Controller): The controller 

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

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

25 

26 Returns: 

27 (dict): The updated params dictionary 

28 """ 

29 defaults = { 

30 "control_order": -50, 

31 "beta": 0.9, 

32 } 

33 from pySDC.implementations.hooks.log_step_size import LogStepSize 

34 

35 controller.add_hook(LogStepSize) 

36 

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

38 

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

40 """ 

41 Load step size limiters here, if they are desired. 

42 

43 Args: 

44 controller (pySDC.Controller): The controller 

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

46 

47 Returns: 

48 None 

49 """ 

50 step_limiter_keys = ['dt_min', 'dt_max', 'dt_slope_min', 'dt_slope_max', 'dt_rel_min_slope'] 

51 available_keys = [me for me in step_limiter_keys if me in self.params.__dict__.keys()] 

52 

53 if len(available_keys) > 0: 

54 step_limiter_params = {key: self.params.__dict__[key] for key in available_keys} 

55 controller.add_convergence_controller(StepSizeLimiter, params=step_limiter_params, description=description) 

56 

57 return None 

58 

59 def get_new_step_size(self, controller, S, **kwargs): 

60 """ 

61 Determine a step size for the next step from an estimate of the local error of the current step. 

62 

63 Args: 

64 controller (pySDC.Controller): The controller 

65 S (pySDC.Step): The current step 

66 

67 Returns: 

68 None 

69 """ 

70 raise NotImplementedError("Please implement a rule for updating the step size!") 

71 

72 def compute_optimal_step_size(self, beta, dt, e_tol, e_est, order): 

73 """ 

74 Compute the optimal step size for the current step based on the order of the scheme. 

75 This function can be called from `get_new_step_size` for various implementations of adaptivity, but notably not 

76 all! We require to know the order of the error estimate and if we do adaptivity based on the residual, for 

77 instance, we do not know that and we can't use this function. 

78 

79 Args: 

80 beta (float): Safety factor 

81 dt (float): Current step size 

82 e_tol (float): The desired tolerance 

83 e_est (float): The estimated local error 

84 order (int): The order of the local error estimate 

85 

86 Returns: 

87 float: The optimal step size 

88 """ 

89 return beta * dt * (e_tol / e_est) ** (1.0 / order) 

90 

91 def get_local_error_estimate(self, controller, S, **kwargs): 

92 """ 

93 Get the local error estimate for updating the step size. 

94 It does not have to be an error estimate, but could be the residual or something else. 

95 

96 Args: 

97 controller (pySDC.Controller): The controller 

98 S (pySDC.Step): The current step 

99 

100 Returns: 

101 float: The error estimate 

102 """ 

103 raise NotImplementedError("Please implement a way to get the local error") 

104 

105 def determine_restart(self, controller, S, **kwargs): 

106 """ 

107 Check if the step wants to be restarted by comparing the estimate of the local error to a preset tolerance 

108 

109 Args: 

110 controller (pySDC.Controller): The controller 

111 S (pySDC.Step): The current step 

112 

113 Returns: 

114 None 

115 """ 

116 if S.status.iter >= S.params.maxiter: 

117 e_est = self.get_local_error_estimate(controller, S) 

118 if e_est >= self.params.e_tol: 

119 # see if we try to avoid restarts 

120 if self.params.get('avoid_restarts'): 

121 more_iter_needed = max([L.status.iter_to_convergence for L in S.levels]) 

122 k_final = S.status.iter + more_iter_needed 

123 rho = max([L.status.contraction_factor for L in S.levels]) 

124 coll_order = S.levels[0].sweep.coll.order 

125 

126 if rho > 1: 

127 S.status.restart = True 

128 self.log(f"Convergence factor = {rho:.2e} > 1 -> restarting", S) 

129 elif k_final > 2 * S.params.maxiter: 

130 S.status.restart = True 

131 self.log( 

132 f"{more_iter_needed} more iterations needed for convergence -> restart is more efficient", S 

133 ) 

134 elif k_final > coll_order: 

135 S.status.restart = True 

136 self.log( 

137 f"{more_iter_needed} more iterations needed for convergence -> restart because collocation problem would be over resolved", 

138 S, 

139 ) 

140 else: 

141 S.status.force_continue = True 

142 self.log(f"{more_iter_needed} more iterations needed for convergence -> no restart", S) 

143 else: 

144 S.status.restart = True 

145 self.log(f"Restarting: e={e_est:.2e} >= e_tol={self.params.e_tol:.2e}", S) 

146 

147 return None 

148 

149 

150class AdaptivityForConvergedCollocationProblems(AdaptivityBase): 

151 """ 

152 Base class for adaptivity from error estimates that need a converged collocation problem, restarting if it is not. 

153 """ 

154 

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

156 """ 

157 Load interpolation between restarts. 

158 

159 Args: 

160 controller (pySDC.Controller): The controller 

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

162 

163 Returns: 

164 None 

165 """ 

166 super().dependencies(controller, description, **kwargs) 

167 

168 if self.params.interpolate_between_restarts: 

169 from pySDC.implementations.convergence_controller_classes.interpolate_between_restarts import ( 

170 InterpolateBetweenRestarts, 

171 ) 

172 

173 controller.add_convergence_controller( 

174 InterpolateBetweenRestarts, 

175 description=description, 

176 params={}, 

177 ) 

178 if self.params.interpolate_between_restarts: 

179 self.interpolator = controller.convergence_controllers[-1] 

180 return None 

181 

182 def get_convergence(self, controller, S, **kwargs): 

183 """ 

184 Check if the collocation problem is converged. Needs to be implemented by the child class. 

185 

186 Args: 

187 controller (pySDC.Controller): The controller 

188 S (pySDC.Step): The current step 

189 

190 Returns: 

191 bool: Whether the collocation problem is converged 

192 """ 

193 raise NotImplementedError("Please implement a way to check if the collocation problem is converged!") 

194 

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

196 """ 

197 Add a default value for control order to the parameters. 

198 

199 Args: 

200 controller (pySDC.Controller): The controller 

201 params (dict): Parameters for the convergence controller 

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

203 

204 Returns: 

205 dict: Updated parameters 

206 """ 

207 defaults = { 

208 'restol_rel': None, 

209 'e_tol_rel': None, 

210 'restart_at_maxiter': True, 

211 'restol_min': 1e-12, 

212 'restol_max': 1e-5, 

213 'e_tol_min': 1e-10, 

214 'e_tol_max': 1e-5, 

215 'factor_if_not_converged': 4.0, 

216 'residual_max_tol': 1e9, 

217 'interpolate_between_restarts': True, 

218 'abort_at_growing_residual': True, 

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

220 } 

221 if defaults['restol_rel']: 

222 description['level_params']['restol'] = min( 

223 [max([defaults['restol_rel'] * defaults['e_tol'], defaults['restol_min']]), defaults['restol_max']] 

224 ) 

225 elif defaults['e_tol_rel']: 

226 description['level_params']['e_tol'] = min( 

227 [max([defaults['e_tol_rel'] * defaults['e_tol'], defaults['e_tol_min']]), defaults['e_tol_max']] 

228 ) 

229 

230 self.res_last_iter = np.inf 

231 

232 return defaults 

233 

234 def determine_restart(self, controller, S, **kwargs): 

235 """ 

236 Once the collocation problem is converged, restart with a smaller step size if the residual is still above 

237 ``restol`` (with ``restart_at_maxiter``, unless the increment is below ``e_tol``), or restart if the local error 

238 estimate exceeds ``e_tol``. Before convergence, restart with a smaller step size if the residual 

239 grows between iterations (only without parallelism across steps and with ``abort_at_growing_residual``) or 

240 exceeds ``residual_max_tol``. 

241 

242 Args: 

243 controller (pySDC.Controller): The controller 

244 S (pySDC.Step): The current step 

245 

246 Returns: 

247 None 

248 """ 

249 if self.get_convergence(controller, S, **kwargs): 

250 self.res_last_iter = np.inf 

251 

252 L = S.levels[0] 

253 e_tol_converged = ( 

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

255 ) 

256 

257 if ( 

258 self.params.restart_at_maxiter 

259 and S.levels[0].status.residual > S.levels[0].params.restol 

260 and not e_tol_converged 

261 ): 

262 self.trigger_restart_upon_nonconvergence(S) 

263 elif self.get_local_error_estimate(controller, S, **kwargs) > self.params.e_tol: 

264 S.status.restart = True 

265 elif ( 

266 S.status.time_size == 1 

267 and self.res_last_iter < S.levels[0].status.residual 

268 and S.status.iter > 0 

269 and self.params.abort_at_growing_residual 

270 ): 

271 self.trigger_restart_upon_nonconvergence(S) 

272 elif S.levels[0].status.residual > self.params.residual_max_tol: 

273 self.trigger_restart_upon_nonconvergence(S) 

274 

275 self.res_last_iter = S.levels[0].status.residual * 1.0 

276 

277 def trigger_restart_upon_nonconvergence(self, S): 

278 """ 

279 Restart the step and stop iterating, dividing the step size on all levels by ``factor_if_not_converged``. No 

280 interpolation is done for the restarted step. 

281 

282 Args: 

283 S (pySDC.Step): The current step 

284 

285 Returns: 

286 None 

287 """ 

288 S.status.restart = True 

289 S.status.force_done = True 

290 for L in S.levels: 

291 L.status.dt_new = L.params.dt / self.params.factor_if_not_converged 

292 self.log( 

293 f'Collocation problem not converged. Reducing step size to {L.status.dt_new:.2e}', 

294 S, 

295 ) 

296 if self.params.interpolate_between_restarts: 

297 self.interpolator.status.skip_interpolation = True 

298 

299 

300class Adaptivity(AdaptivityBase): 

301 """ 

302 Class to compute time step size adaptively based on embedded error estimate. 

303 

304 We have a version working in non-MPI pipelined SDC, but Adaptivity requires you to know the order of the scheme, 

305 which you can also know for block-Jacobi, but it works differently and it is only implemented for block 

306 Gauss-Seidel so far. 

307 

308 There is an option to reduce restarts if continued iterations could yield convergence in fewer iterations than 

309 restarting based on an estimate of the contraction factor. 

310 Since often only one or two more iterations suffice, this can boost efficiency of adaptivity significantly. 

311 Notice that the computed step size is not effected. 

312 Be aware that this does not work when Hot Rod is enabled, since that requires us to know the order of the scheme in 

313 more detail. Since we reset to the second to last sweep before moving on, we cannot continue to iterate. 

314 Set the reduced restart up by setting a boolean value for "avoid_restarts" in the parameters for the convergence 

315 controller. 

316 The behaviour in multi-step SDC is not well studied and it is unclear if anything useful happens there. 

317 """ 

318 

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

320 """ 

321 Define default parameters here. 

322 

323 Default parameters are: 

324 - control_order (int): The order relative to other convergence controllers 

325 - beta (float): The safety factor 

326 

327 Args: 

328 controller (pySDC.Controller): The controller 

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

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

331 

332 Returns: 

333 (dict): The updated params dictionary 

334 """ 

335 defaults = { 

336 "embedded_error_flavor": 'standard', 

337 "rel_error": False, 

338 } 

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

340 

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

342 """ 

343 Load the embedded error estimator. 

344 

345 Args: 

346 controller (pySDC.Controller): The controller 

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

348 

349 Returns: 

350 None 

351 """ 

352 from pySDC.implementations.convergence_controller_classes.estimate_embedded_error import EstimateEmbeddedError 

353 

354 super().dependencies(controller, description, **kwargs) 

355 

356 controller.add_convergence_controller( 

357 EstimateEmbeddedError.get_implementation(self.params.embedded_error_flavor, self.params.useMPI), 

358 description=description, 

359 params={ 

360 'rel_error': self.params.rel_error, 

361 }, 

362 ) 

363 

364 # load contraction factor estimator if necessary 

365 if self.params.get('avoid_restarts'): 

366 from pySDC.implementations.convergence_controller_classes.estimate_contraction_factor import ( 

367 EstimateContractionFactor, 

368 ) 

369 

370 params = {'e_tol': self.params.e_tol} 

371 controller.add_convergence_controller(EstimateContractionFactor, description=description, params=params) 

372 return None 

373 

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

375 """ 

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

377 For adaptivity, we need to know the order of the scheme. 

378 

379 Args: 

380 controller (pySDC.Controller): The controller 

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

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

383 

384 Returns: 

385 bool: Whether the parameters are compatible 

386 str: The error message 

387 """ 

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

389 return ( 

390 False, 

391 "Adaptivity needs constant order in time and hence restol in the step parameters has to be \ 

392smaller than 0!", 

393 ) 

394 

395 if controller.params.mssdc_jac: 

396 return ( 

397 False, 

398 "Adaptivity needs the same order on all steps, please activate Gauss-Seidel multistep mode!", 

399 ) 

400 

401 if "e_tol" not in params.keys(): 

402 return ( 

403 False, 

404 "Adaptivity needs a local tolerance! Please pass `e_tol` to the parameters for this convergence controller!", 

405 ) 

406 

407 return True, "" 

408 

409 def get_new_step_size(self, controller, S, **kwargs): 

410 """ 

411 Determine a step size for the next step from an embedded estimate of the local error of the current step. 

412 

413 Args: 

414 controller (pySDC.Controller): The controller 

415 S (pySDC.Step): The current step 

416 

417 Returns: 

418 None 

419 """ 

420 # check if we performed the desired amount of sweeps 

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

422 L = S.levels[0] 

423 

424 # compute next step size 

425 order = S.status.iter # embedded error estimate is same order as time marching 

426 

427 e_est = self.get_local_error_estimate(controller, S) 

428 L.status.dt_new = self.compute_optimal_step_size( 

429 self.params.beta, L.params.dt, self.params.e_tol, e_est, order 

430 ) 

431 self.log(f'Adjusting step size from {L.params.dt:.2e} to {L.status.dt_new:.2e}', S) 

432 

433 return None 

434 

435 def get_local_error_estimate(self, controller, S, **kwargs): 

436 """ 

437 Get the embedded error estimate of the finest level of the step. 

438 

439 Args: 

440 controller (pySDC.Controller): The controller 

441 S (pySDC.Step): The current step 

442 

443 Returns: 

444 float: Embedded error estimate 

445 """ 

446 return S.levels[0].status.error_embedded_estimate 

447 

448 

449class AdaptivityRK(Adaptivity): 

450 """ 

451 Adaptivity for Runge-Kutta methods. Basically, we need to change the order in the step size update 

452 """ 

453 

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

455 """ 

456 Add the order of the embedded error estimate of the Runge-Kutta method, from the sweeper's ``get_update_order``, 

457 as default for the parameter ``update_order``. 

458 

459 Args: 

460 controller (pySDC.Controller): The controller 

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

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

463 

464 Returns: 

465 (dict): The updated params dictionary 

466 """ 

467 defaults = {} 

468 defaults['update_order'] = params.get('update_order', description['sweeper_class'].get_update_order()) 

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

470 

471 def get_new_step_size(self, controller, S, **kwargs): 

472 """ 

473 Determine a step size for the next step from an embedded estimate of the local error of the current step. 

474 

475 Args: 

476 controller (pySDC.Controller): The controller 

477 S (pySDC.Step): The current step 

478 

479 Returns: 

480 None 

481 """ 

482 # check if we performed the desired amount of sweeps 

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

484 L = S.levels[0] 

485 

486 # compute next step size 

487 order = self.params.update_order 

488 e_est = self.get_local_error_estimate(controller, S) 

489 L.status.dt_new = self.compute_optimal_step_size( 

490 self.params.beta, L.params.dt, self.params.e_tol, e_est, order 

491 ) 

492 self.log(f'Adjusting step size from {L.params.dt:.2e} to {L.status.dt_new:.2e}', S) 

493 

494 return None 

495 

496 

497class AdaptivityResidual(AdaptivityBase): 

498 """ 

499 Do adaptivity based on residual. 

500 

501 Since we don't know a correlation between the residual and the error (for nonlinear problems), we employ a simpler 

502 rule to update the step size. Instead of giving a local tolerance that we try to hit as closely as possible, we set 

503 two thresholds for the residual. When we exceed the upper one, we reduce the step size by a factor of 2 and if the 

504 residual falls below the lower threshold, we double the step size. 

505 Please setup these parameters as "e_tol" and "e_tol_low". 

506 """ 

507 

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

509 """ 

510 Define default parameters here. 

511 

512 Default parameters are: 

513 - control_order (int): The order relative to other convergence controllers 

514 - e_tol_low (float): Lower absolute threshold for the residual 

515 - e_tol (float): Upper absolute threshold for the residual 

516 - use_restol (bool): Restart if the residual tolerance was not reached 

517 - max_restarts: Override maximum number of restarts 

518 

519 Args: 

520 controller (pySDC.Controller): The controller 

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

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

523 

524 Returns: 

525 (dict): The updated params dictionary 

526 """ 

527 defaults = { 

528 "control_order": -45, 

529 "e_tol_low": 0, 

530 "e_tol": np.inf, 

531 "use_restol": False, 

532 "max_restarts": 99 if "e_tol_low" in params else None, 

533 "allowed_modifications": ['increase', 'decrease'], # what we are allowed to do with the step size 

534 } 

535 return {**defaults, **params} 

536 

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

538 """ 

539 Change maximum number of allowed restarts here. 

540 

541 Args: 

542 controller (pySDC.Controller): The controller 

543 

544 Reutrns: 

545 None 

546 """ 

547 from pySDC.implementations.convergence_controller_classes.basic_restarting import BasicRestarting 

548 

549 if self.params.max_restarts is not None: 

550 conv_controllers = controller.convergence_controllers 

551 restart_cont = [me for me in conv_controllers if BasicRestarting in type(me).__bases__] 

552 

553 if len(restart_cont) == 0: 

554 raise NotImplementedError("Please implement override of maximum number of restarts!") 

555 

556 restart_cont[0].params.max_restarts = self.params.max_restarts 

557 return None 

558 

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

560 """ 

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

562 

563 Args: 

564 controller (pySDC.Controller): The controller 

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

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

567 

568 Returns: 

569 bool: Whether the parameters are compatible 

570 str: The error message 

571 """ 

572 if controller.params.mssdc_jac: 

573 return ( 

574 False, 

575 "Adaptivity needs the same order on all steps, please activate Gauss-Seidel multistep mode!", 

576 ) 

577 

578 return True, "" 

579 

580 def get_new_step_size(self, controller, S, **kwargs): 

581 """ 

582 Determine a step size for the next step. 

583 If we exceed the absolute tolerance of the residual in either direction, we either double or halve the step 

584 size. 

585 

586 Args: 

587 controller (pySDC.Controller): The controller 

588 S (pySDC.Step): The current step 

589 

590 Returns: 

591 None 

592 """ 

593 # check if we performed the desired amount of sweeps 

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

595 L = S.levels[0] 

596 

597 res = self.get_local_error_estimate(controller, S) 

598 

599 dt_planned = L.status.dt_new if L.status.dt_new is not None else L.params.dt 

600 

601 if ( 

602 res > self.params.e_tol or (res > L.params.restol and self.params.use_restol) 

603 ) and 'decrease' in self.params.allowed_modifications: 

604 L.status.dt_new = min([dt_planned, L.params.dt / 2.0]) 

605 self.log(f'Adjusting step size from {L.params.dt:.2e} to {L.status.dt_new:.2e}', S) 

606 elif res < self.params.e_tol_low and 'increase' in self.params.allowed_modifications: 

607 L.status.dt_new = max([dt_planned, L.params.dt * 2.0]) 

608 self.log(f'Adjusting step size from {L.params.dt:.2e} to {L.status.dt_new:.2e}', S) 

609 

610 return None 

611 

612 def get_local_error_estimate(self, controller, S, **kwargs): 

613 """ 

614 Get the residual of the finest level of the step. 

615 

616 Args: 

617 controller (pySDC.Controller): The controller 

618 S (pySDC.Step): The current step 

619 

620 Returns: 

621 float: Embedded error estimate 

622 """ 

623 return S.levels[0].status.residual 

624 

625 

626class AdaptivityCollocation(AdaptivityForConvergedCollocationProblems): 

627 """ 

628 Control the step size via a collocation based estimate of the local error. 

629 The error estimate works by subtracting two solutions to collocation problems with different order. You can 

630 interpolate between collocation methods as much as you want but the adaptive step size selection will always be 

631 based on the last switch of quadrature. 

632 """ 

633 

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

635 """ 

636 Add a default value for control order to the parameters. 

637 

638 Args: 

639 controller (pySDC.Controller): The controller 

640 params (dict): Parameters for the convergence controller 

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

642 

643 Returns: 

644 dict: Updated parameters 

645 """ 

646 defaults = { 

647 "adaptive_coll_params": {}, 

648 "num_colls": 0, 

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

650 "control_order": 220, 

651 } 

652 

653 for key in defaults['adaptive_coll_params'].keys(): 

654 if type(defaults['adaptive_coll_params'][key]) == list: 

655 defaults['num_colls'] = max([defaults['num_colls'], len(defaults['adaptive_coll_params'][key])]) 

656 

657 return defaults 

658 

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

660 """ 

661 Set up empty tuples for the error estimates and orders of the collocation problems solved in the current step. 

662 

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

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

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

666 

667 Args: 

668 controller (pySDC.Controller): The controller 

669 

670 Returns: 

671 None 

672 """ 

673 self.add_status_variable_to_step('coll_error', ()) 

674 self.add_status_variable_to_step('coll_order', ()) 

675 

676 def reset_status_variables(self, controller, **kwargs): 

677 """ 

678 Empty the tuples of error estimates and orders at the start of a step or when restarting. 

679 

680 Args: 

681 controller (pySDC.Controller): The controller 

682 

683 Returns: 

684 None 

685 """ 

686 self.set_step_status_variable('coll_error', ()) 

687 self.set_step_status_variable('coll_order', ()) 

688 

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

690 """ 

691 Load the `EstimateEmbeddedErrorCollocation` convergence controller to estimate the local error by switching 

692 between collocation problems between iterations. 

693 

694 Args: 

695 controller (pySDC.Controller): The controller 

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

697 """ 

698 from pySDC.implementations.convergence_controller_classes.estimate_embedded_error import ( 

699 EstimateEmbeddedErrorCollocation, 

700 ) 

701 

702 super().dependencies(controller, description, **kwargs) 

703 

704 params = {'adaptive_coll_params': self.params.adaptive_coll_params} 

705 controller.add_convergence_controller( 

706 EstimateEmbeddedErrorCollocation, 

707 params=params, 

708 description=description, 

709 ) 

710 

711 def get_convergence(self, controller, S, **kwargs): 

712 """ 

713 The step is converged once all collocation problems have been solved. 

714 

715 Args: 

716 controller (pySDC.Controller): The controller 

717 S (pySDC.Step): The current step 

718 

719 Returns: 

720 bool: Whether all ``num_colls`` collocation problems have been solved 

721 """ 

722 return len(S.status.coll_order) == self.params.num_colls 

723 

724 def get_local_error_estimate(self, controller, S, **kwargs): 

725 """ 

726 Get the collocation based embedded error estimate. 

727 

728 Args: 

729 controller (pySDC.Controller): The controller 

730 S (pySDC.Step): The current step 

731 

732 Returns: 

733 float: Embedded error estimate 

734 """ 

735 if len(S.status.coll_error) > 1: 

736 return S.status.coll_error[-1][1] 

737 else: 

738 return 0.0 

739 

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

741 """ 

742 Get the error estimate and its order if available. 

743 

744 Args: 

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

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

747 """ 

748 if step.status.done: 

749 lvl = step.levels[0] 

750 step.status.coll_error += (lvl.status.error_embedded_estimate_collocation,) 

751 step.status.coll_order += (lvl.sweep.coll.order,) 

752 

753 def get_new_step_size(self, controller, S, **kwargs): 

754 """ 

755 Once all collocation problems have been solved, compute the new step size from the error estimate, using as 

756 order one more than the lower order of the last two collocation methods. 

757 

758 Args: 

759 controller (pySDC.Controller): The controller 

760 S (pySDC.Step): The current step 

761 

762 Returns: 

763 None 

764 """ 

765 if len(S.status.coll_order) == self.params.num_colls: 

766 lvl = S.levels[0] 

767 

768 # compute next step size 

769 order = ( 

770 min(S.status.coll_order[-2::]) + 1 

771 ) # local order of less accurate of the last two collocation problems 

772 e_est = self.get_local_error_estimate(controller, S) 

773 

774 lvl.status.dt_new = self.compute_optimal_step_size( 

775 self.params.beta, lvl.params.dt, self.params.e_tol, e_est, order 

776 ) 

777 self.log(f'Adjusting step size from {lvl.params.dt:.2e} to {lvl.status.dt_new:.2e}', S) 

778 

779 def determine_restart(self, controller, S, **kwargs): 

780 """ 

781 Check if the step wants to be restarted by comparing the estimate of the local error to a preset tolerance 

782 

783 Args: 

784 controller (pySDC.Controller): The controller 

785 S (pySDC.Step): The current step 

786 

787 Returns: 

788 None 

789 """ 

790 if len(S.status.coll_order) == self.params.num_colls: 

791 e_est = self.get_local_error_estimate(controller, S) 

792 if e_est >= self.params.e_tol: 

793 S.status.restart = True 

794 self.log(f"Restarting: e={e_est:.2e} >= e_tol={self.params.e_tol:.2e}", S) 

795 

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

797 """ 

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

799 For adaptivity, we need to know the order of the scheme. 

800 

801 Args: 

802 controller (pySDC.Controller): The controller 

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

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

805 

806 Returns: 

807 bool: Whether the parameters are compatible 

808 str: The error message 

809 """ 

810 if "e_tol" not in params.keys(): 

811 return ( 

812 False, 

813 "Adaptivity needs a local tolerance! Please pass `e_tol` to the parameters for this convergence controller!", 

814 ) 

815 

816 return True, "" 

817 

818 

819class AdaptivityExtrapolationWithinQ(AdaptivityForConvergedCollocationProblems): 

820 """ 

821 Compute the step size adaptively from an error estimate by extrapolation within the quadrature nodes. 

822 

823 This error estimate depends on solving the collocation problem exactly, so make sure you set a sufficient stopping criterion. 

824 """ 

825 

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

827 """ 

828 Add the default ``high_Taylor_order=False`` to the parameters and use the regular convergence check to determine 

829 if the collocation problem is converged. 

830 

831 Args: 

832 controller (pySDC.Controller): The controller 

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

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

835 

836 Returns: 

837 (dict): The updated params dictionary 

838 """ 

839 from pySDC.core.check_convergence import CheckConvergence 

840 

841 defaults = { 

842 'high_Taylor_order': False, 

843 **params, 

844 } 

845 

846 self.check_convergence = CheckConvergence.check_convergence 

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

848 

849 def get_convergence(self, controller, S, **kwargs): 

850 """ 

851 Check if the step is converged with the regular convergence criteria, i.e. residual, increment or maximum number 

852 of iterations. 

853 

854 Args: 

855 controller (pySDC.Controller): The controller 

856 S (pySDC.Step): The current step 

857 

858 Returns: 

859 bool: Whether the step is converged 

860 """ 

861 return self.check_convergence(S) 

862 

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

864 """ 

865 Load the error estimator. 

866 

867 Args: 

868 controller (pySDC.Controller): The controller 

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

870 

871 Returns: 

872 None 

873 """ 

874 from pySDC.implementations.convergence_controller_classes.estimate_extrapolation_error import ( 

875 EstimateExtrapolationErrorWithinQ, 

876 ) 

877 

878 super().dependencies(controller, description, **kwargs) 

879 

880 controller.add_convergence_controller( 

881 EstimateExtrapolationErrorWithinQ, 

882 description=description, 

883 params={'high_Taylor_order': self.params.high_Taylor_order}, 

884 ) 

885 return None 

886 

887 def get_new_step_size(self, controller, S, **kwargs): 

888 """ 

889 Determine a step size for the next step from the error estimate. 

890 

891 Args: 

892 controller (pySDC.Controller): The controller 

893 S (pySDC.Step): The current step 

894 

895 Returns: 

896 None 

897 """ 

898 if self.get_convergence(controller, S, **kwargs): 

899 L = S.levels[0] 

900 

901 # compute next step size 

902 order = L.sweep.coll.num_nodes + 1 if self.params.high_Taylor_order else L.sweep.coll.num_nodes 

903 

904 e_est = self.get_local_error_estimate(controller, S) 

905 L.status.dt_new = self.compute_optimal_step_size( 

906 self.params.beta, L.params.dt, self.params.e_tol, e_est, order 

907 ) 

908 

909 self.log( 

910 f'Error target: {self.params.e_tol:.2e}, error estimate: {e_est:.2e}, update_order: {order}', 

911 S, 

912 level=10, 

913 ) 

914 self.log(f'Adjusting step size from {L.params.dt:.2e} to {L.status.dt_new:.2e}', S) 

915 

916 return None 

917 

918 def get_local_error_estimate(self, controller, S, **kwargs): 

919 """ 

920 Get the embedded error estimate of the finest level of the step. 

921 

922 Args: 

923 controller (pySDC.Controller): The controller 

924 S (pySDC.Step): The current step 

925 

926 Returns: 

927 float: Embedded error estimate 

928 """ 

929 return S.levels[0].status.error_extrapolation_estimate 

930 

931 

932class AdaptivityPolynomialError(AdaptivityForConvergedCollocationProblems): 

933 """ 

934 Compute the step size adaptively from an error estimate by interpolation within the quadrature nodes. 

935 

936 This error estimate depends on solving the collocation problem exactly, so make sure you set a sufficient stopping criterion. 

937 """ 

938 

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

940 """ 

941 Add defaults for the mesh type of the problem, which selects the error estimator, and ``rel_error``, and use the 

942 regular convergence check to determine if the collocation problem is converged. 

943 

944 Args: 

945 controller (pySDC.Controller): The controller 

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

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

948 

949 Returns: 

950 (dict): The updated params dictionary 

951 """ 

952 from pySDC.core.check_convergence import CheckConvergence 

953 

954 defaults = { 

955 'control_order': -50, 

956 'problem_mesh_type': 'numpyesque', 

957 'rel_error': False, 

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

959 **params, 

960 } 

961 

962 self.check_convergence = CheckConvergence.check_convergence 

963 return defaults 

964 

965 def get_convergence(self, controller, S, **kwargs): 

966 """ 

967 Check if the step is converged with the regular convergence criteria, i.e. residual, increment or maximum number 

968 of iterations. 

969 

970 Args: 

971 controller (pySDC.Controller): The controller 

972 S (pySDC.Step): The current step 

973 

974 Returns: 

975 bool: Whether the step is converged 

976 """ 

977 return self.check_convergence(S) 

978 

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

980 """ 

981 Load the error estimator. 

982 

983 Args: 

984 controller (pySDC.Controller): The controller 

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

986 

987 Returns: 

988 None 

989 """ 

990 if self.params.problem_mesh_type.lower() == 'numpyesque': 

991 from pySDC.implementations.convergence_controller_classes.estimate_polynomial_error import ( 

992 EstimatePolynomialError as error_estimation_cls, 

993 ) 

994 elif self.params.problem_mesh_type.lower() == 'firedrake': 

995 from pySDC.implementations.convergence_controller_classes.estimate_polynomial_error import ( 

996 EstimatePolynomialErrorFiredrake as error_estimation_cls, 

997 ) 

998 else: 

999 raise NotImplementedError( 

1000 f'Don\'t know what error estimation class to use for problems with mesh type {self.params.problem_mesh_type}' 

1001 ) 

1002 

1003 super().dependencies(controller, description, **kwargs) 

1004 

1005 controller.add_convergence_controller( 

1006 error_estimation_cls, 

1007 description=description, 

1008 params={ 

1009 'rel_error': self.params.rel_error, 

1010 }, 

1011 ) 

1012 return None 

1013 

1014 def get_new_step_size(self, controller, S, **kwargs): 

1015 """ 

1016 Determine a step size for the next step from the error estimate. 

1017 

1018 Args: 

1019 controller (pySDC.Controller): The controller 

1020 S (pySDC.Step): The current step 

1021 

1022 Returns: 

1023 None 

1024 """ 

1025 if self.get_convergence(controller, S, **kwargs): 

1026 L = S.levels[0] 

1027 

1028 # compute next step size 

1029 order = L.status.order_embedded_estimate 

1030 

1031 e_est = self.get_local_error_estimate(controller, S) 

1032 L.status.dt_new = self.compute_optimal_step_size( 

1033 self.params.beta, L.params.dt, self.params.e_tol, e_est, order 

1034 ) 

1035 

1036 self.log( 

1037 f'Error target: {self.params.e_tol:.2e}, error estimate: {e_est:.2e}, update_order: {order}', 

1038 S, 

1039 level=10, 

1040 ) 

1041 self.log(f'Adjusting step size from {L.params.dt:.2e} to {L.status.dt_new:.2e}', S) 

1042 

1043 return None 

1044 

1045 def get_local_error_estimate(self, controller, S, **kwargs): 

1046 """ 

1047 Get the embedded error estimate of the finest level of the step. 

1048 

1049 Args: 

1050 controller (pySDC.Controller): The controller 

1051 S (pySDC.Step): The current step 

1052 

1053 Returns: 

1054 float: Embedded error estimate 

1055 """ 

1056 return S.levels[0].status.error_embedded_estimate