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

257 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-17 13:41 +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 if self.params.useMPI: 

58 self.prepare_MPI_logical_operations() 

59 

60 return None 

61 

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

63 """ 

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

65 

66 Args: 

67 controller (pySDC.Controller): The controller 

68 S (pySDC.Step): The current step 

69 

70 Returns: 

71 None 

72 """ 

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

74 

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

76 """ 

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

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

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

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

81 

82 Args: 

83 beta (float): Safety factor 

84 dt (float): Current step size 

85 e_tol (float): The desired tolerance 

86 e_est (float): The estimated local error 

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

88 

89 Returns: 

90 float: The optimal step size 

91 """ 

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

93 

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

95 """ 

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

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

98 

99 Args: 

100 controller (pySDC.Controller): The controller 

101 S (pySDC.Step): The current step 

102 

103 Returns: 

104 float: The error estimate 

105 """ 

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

107 

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

109 """ 

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

111 

112 Args: 

113 controller (pySDC.Controller): The controller 

114 S (pySDC.Step): The current step 

115 

116 Returns: 

117 None 

118 """ 

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

120 e_est = self.get_local_error_estimate(controller, S) 

121 if e_est >= self.params.e_tol: 

122 # see if we try to avoid restarts 

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

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

125 k_final = S.status.iter + more_iter_needed 

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

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

128 

129 if rho > 1: 

130 S.status.restart = True 

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

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

133 S.status.restart = True 

134 self.log( 

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

136 ) 

137 elif k_final > coll_order: 

138 S.status.restart = True 

139 self.log( 

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

141 S, 

142 ) 

143 else: 

144 S.status.force_continue = True 

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

146 else: 

147 S.status.restart = True 

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

149 

150 return None 

151 

152 

153class AdaptivityForConvergedCollocationProblems(AdaptivityBase): 

154 """ 

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

156 """ 

157 

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

159 """ 

160 Load interpolation between restarts. 

161 

162 Args: 

163 controller (pySDC.Controller): The controller 

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

165 

166 Returns: 

167 None 

168 """ 

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

170 

171 if self.params.interpolate_between_restarts: 

172 from pySDC.implementations.convergence_controller_classes.interpolate_between_restarts import ( 

173 InterpolateBetweenRestarts, 

174 ) 

175 

176 controller.add_convergence_controller( 

177 InterpolateBetweenRestarts, 

178 description=description, 

179 params={}, 

180 ) 

181 if self.params.interpolate_between_restarts: 

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

183 return None 

184 

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

186 """ 

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

188 

189 Args: 

190 controller (pySDC.Controller): The controller 

191 S (pySDC.Step): The current step 

192 

193 Returns: 

194 bool: Whether the collocation problem is converged 

195 """ 

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

197 

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

199 """ 

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

201 

202 Args: 

203 controller (pySDC.Controller): The controller 

204 params (dict): Parameters for the convergence controller 

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

206 

207 Returns: 

208 dict: Updated parameters 

209 """ 

210 defaults = { 

211 'restol_rel': None, 

212 'e_tol_rel': None, 

213 'restart_at_maxiter': True, 

214 'restol_min': 1e-12, 

215 'restol_max': 1e-5, 

216 'e_tol_min': 1e-10, 

217 'e_tol_max': 1e-5, 

218 'factor_if_not_converged': 4.0, 

219 'residual_max_tol': 1e9, 

220 'interpolate_between_restarts': True, 

221 'abort_at_growing_residual': True, 

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

223 } 

224 if defaults['restol_rel']: 

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

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

227 ) 

228 elif defaults['e_tol_rel']: 

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

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

231 ) 

232 

233 self.res_last_iter = np.inf 

234 

235 return defaults 

236 

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

238 """ 

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

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

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

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

243 exceeds ``residual_max_tol``. 

244 

245 Args: 

246 controller (pySDC.Controller): The controller 

247 S (pySDC.Step): The current step 

248 

249 Returns: 

250 None 

251 """ 

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

253 self.res_last_iter = np.inf 

254 

255 L = S.levels[0] 

256 e_tol_converged = ( 

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

258 ) 

259 

260 if ( 

261 self.params.restart_at_maxiter 

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

263 and not e_tol_converged 

264 ): 

265 self.trigger_restart_upon_nonconvergence(S) 

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

267 S.status.restart = True 

268 elif ( 

269 S.status.time_size == 1 

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

271 and S.status.iter > 0 

272 and self.params.abort_at_growing_residual 

273 ): 

274 self.trigger_restart_upon_nonconvergence(S) 

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

276 self.trigger_restart_upon_nonconvergence(S) 

277 

278 if self.params.useMPI: 

279 # borrow the reduction rather than storing it on the instance: an unbound function in an 

280 # instance attribute shadows the hook of the same name, and needs an explicit `self` that 

281 # nothing else in this API does 

282 from pySDC.core.check_convergence import CheckConvergence 

283 

284 CheckConvergence.communicate_convergence(self, controller, S, **kwargs) 

285 

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

287 

288 def trigger_restart_upon_nonconvergence(self, S): 

289 """ 

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

291 interpolation is done for the restarted step. 

292 

293 Args: 

294 S (pySDC.Step): The current step 

295 

296 Returns: 

297 None 

298 """ 

299 S.status.restart = True 

300 S.status.force_done = True 

301 for L in S.levels: 

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

303 self.log( 

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

305 S, 

306 ) 

307 if self.params.interpolate_between_restarts: 

308 self.interpolator.status.skip_interpolation = True 

309 

310 

311class Adaptivity(AdaptivityBase): 

312 """ 

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

314 

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

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

317 Gauss-Seidel so far. 

318 

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

320 restarting based on an estimate of the contraction factor. 

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

322 Notice that the computed step size is not effected. 

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

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

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

326 controller. 

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

328 """ 

329 

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

331 """ 

332 Define default parameters here. 

333 

334 Default parameters are: 

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

336 - beta (float): The safety factor 

337 

338 Args: 

339 controller (pySDC.Controller): The controller 

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

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

342 

343 Returns: 

344 (dict): The updated params dictionary 

345 """ 

346 defaults = { 

347 "embedded_error_flavor": 'standard', 

348 "rel_error": False, 

349 } 

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

351 

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

353 """ 

354 Load the embedded error estimator. 

355 

356 Args: 

357 controller (pySDC.Controller): The controller 

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

359 

360 Returns: 

361 None 

362 """ 

363 from pySDC.implementations.convergence_controller_classes.estimate_embedded_error import EstimateEmbeddedError 

364 

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

366 

367 controller.add_convergence_controller( 

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

369 description=description, 

370 params={ 

371 'rel_error': self.params.rel_error, 

372 }, 

373 ) 

374 

375 # load contraction factor estimator if necessary 

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

377 from pySDC.implementations.convergence_controller_classes.estimate_contraction_factor import ( 

378 EstimateContractionFactor, 

379 ) 

380 

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

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

383 return None 

384 

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

386 """ 

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

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

389 

390 Args: 

391 controller (pySDC.Controller): The controller 

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

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

394 

395 Returns: 

396 bool: Whether the parameters are compatible 

397 str: The error message 

398 """ 

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

400 return ( 

401 False, 

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

403smaller than 0!", 

404 ) 

405 

406 if controller.params.mssdc_jac: 

407 return ( 

408 False, 

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

410 ) 

411 

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

413 return ( 

414 False, 

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

416 ) 

417 

418 return True, "" 

419 

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

421 """ 

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

423 

424 Args: 

425 controller (pySDC.Controller): The controller 

426 S (pySDC.Step): The current step 

427 

428 Returns: 

429 None 

430 """ 

431 # check if we performed the desired amount of sweeps 

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

433 L = S.levels[0] 

434 

435 # compute next step size 

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

437 

438 e_est = self.get_local_error_estimate(controller, S) 

439 L.status.dt_new = self.compute_optimal_step_size( 

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

441 ) 

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

443 

444 return None 

445 

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

447 """ 

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

449 

450 Args: 

451 controller (pySDC.Controller): The controller 

452 S (pySDC.Step): The current step 

453 

454 Returns: 

455 float: Embedded error estimate 

456 """ 

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

458 

459 

460class AdaptivityRK(Adaptivity): 

461 """ 

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

463 """ 

464 

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

466 """ 

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

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

469 

470 Args: 

471 controller (pySDC.Controller): The controller 

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

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

474 

475 Returns: 

476 (dict): The updated params dictionary 

477 """ 

478 defaults = {} 

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

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

481 

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

483 """ 

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

485 

486 Args: 

487 controller (pySDC.Controller): The controller 

488 S (pySDC.Step): The current step 

489 

490 Returns: 

491 None 

492 """ 

493 # check if we performed the desired amount of sweeps 

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

495 L = S.levels[0] 

496 

497 # compute next step size 

498 order = self.params.update_order 

499 e_est = self.get_local_error_estimate(controller, S) 

500 L.status.dt_new = self.compute_optimal_step_size( 

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

502 ) 

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

504 

505 return None 

506 

507 

508class AdaptivityResidual(AdaptivityBase): 

509 """ 

510 Do adaptivity based on residual. 

511 

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

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

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

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

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

517 """ 

518 

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

520 """ 

521 Define default parameters here. 

522 

523 Default parameters are: 

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

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

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

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

528 - max_restarts: Override maximum number of restarts 

529 

530 Args: 

531 controller (pySDC.Controller): The controller 

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

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

534 

535 Returns: 

536 (dict): The updated params dictionary 

537 """ 

538 defaults = { 

539 "control_order": -45, 

540 "e_tol_low": 0, 

541 "e_tol": np.inf, 

542 "use_restol": False, 

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

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

545 } 

546 return {**defaults, **params} 

547 

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

549 """ 

550 Change maximum number of allowed restarts here. 

551 

552 Args: 

553 controller (pySDC.Controller): The controller 

554 

555 Reutrns: 

556 None 

557 """ 

558 from pySDC.implementations.convergence_controller_classes.basic_restarting import BasicRestarting 

559 

560 if self.params.max_restarts is not None: 

561 conv_controllers = controller.convergence_controllers 

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

563 

564 if len(restart_cont) == 0: 

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

566 

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

568 return None 

569 

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

571 """ 

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

573 

574 Args: 

575 controller (pySDC.Controller): The controller 

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

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

578 

579 Returns: 

580 bool: Whether the parameters are compatible 

581 str: The error message 

582 """ 

583 if controller.params.mssdc_jac: 

584 return ( 

585 False, 

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

587 ) 

588 

589 return True, "" 

590 

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

592 """ 

593 Determine a step size for the next step. 

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

595 size. 

596 

597 Args: 

598 controller (pySDC.Controller): The controller 

599 S (pySDC.Step): The current step 

600 

601 Returns: 

602 None 

603 """ 

604 # check if we performed the desired amount of sweeps 

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

606 L = S.levels[0] 

607 

608 res = self.get_local_error_estimate(controller, S) 

609 

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

611 

612 if ( 

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

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

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

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

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

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

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

620 

621 return None 

622 

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

624 """ 

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

626 

627 Args: 

628 controller (pySDC.Controller): The controller 

629 S (pySDC.Step): The current step 

630 

631 Returns: 

632 float: Embedded error estimate 

633 """ 

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

635 

636 

637class AdaptivityCollocation(AdaptivityForConvergedCollocationProblems): 

638 """ 

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

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

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

642 based on the last switch of quadrature. 

643 """ 

644 

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

646 """ 

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

648 

649 Args: 

650 controller (pySDC.Controller): The controller 

651 params (dict): Parameters for the convergence controller 

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

653 

654 Returns: 

655 dict: Updated parameters 

656 """ 

657 defaults = { 

658 "adaptive_coll_params": {}, 

659 "num_colls": 0, 

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

661 "control_order": 220, 

662 } 

663 

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

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

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

667 

668 return defaults 

669 

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

671 """ 

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

673 

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

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

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

677 

678 Args: 

679 controller (pySDC.Controller): The controller 

680 

681 Returns: 

682 None 

683 """ 

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

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

686 

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

688 """ 

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

690 

691 Args: 

692 controller (pySDC.Controller): The controller 

693 

694 Returns: 

695 None 

696 """ 

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

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

699 

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

701 """ 

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

703 between collocation problems between iterations. 

704 

705 Args: 

706 controller (pySDC.Controller): The controller 

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

708 """ 

709 from pySDC.implementations.convergence_controller_classes.estimate_embedded_error import ( 

710 EstimateEmbeddedErrorCollocation, 

711 ) 

712 

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

714 

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

716 controller.add_convergence_controller( 

717 EstimateEmbeddedErrorCollocation, 

718 params=params, 

719 description=description, 

720 ) 

721 

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

723 """ 

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

725 

726 Args: 

727 controller (pySDC.Controller): The controller 

728 S (pySDC.Step): The current step 

729 

730 Returns: 

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

732 """ 

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

734 

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

736 """ 

737 Get the collocation based embedded error estimate. 

738 

739 Args: 

740 controller (pySDC.Controller): The controller 

741 S (pySDC.Step): The current step 

742 

743 Returns: 

744 float: Embedded error estimate 

745 """ 

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

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

748 else: 

749 return 0.0 

750 

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

752 """ 

753 Get the error estimate and its order if available. 

754 

755 Args: 

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

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

758 """ 

759 if step.status.done: 

760 lvl = step.levels[0] 

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

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

763 

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

765 """ 

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

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

768 

769 Args: 

770 controller (pySDC.Controller): The controller 

771 S (pySDC.Step): The current step 

772 

773 Returns: 

774 None 

775 """ 

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

777 lvl = S.levels[0] 

778 

779 # compute next step size 

780 order = ( 

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

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

783 e_est = self.get_local_error_estimate(controller, S) 

784 

785 lvl.status.dt_new = self.compute_optimal_step_size( 

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

787 ) 

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

789 

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

791 """ 

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

793 

794 Args: 

795 controller (pySDC.Controller): The controller 

796 S (pySDC.Step): The current step 

797 

798 Returns: 

799 None 

800 """ 

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

802 e_est = self.get_local_error_estimate(controller, S) 

803 if e_est >= self.params.e_tol: 

804 S.status.restart = True 

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

806 

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

808 """ 

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

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

811 

812 Args: 

813 controller (pySDC.Controller): The controller 

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

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

816 

817 Returns: 

818 bool: Whether the parameters are compatible 

819 str: The error message 

820 """ 

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

822 return ( 

823 False, 

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

825 ) 

826 

827 return True, "" 

828 

829 

830class AdaptivityExtrapolationWithinQ(AdaptivityForConvergedCollocationProblems): 

831 """ 

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

833 

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

835 """ 

836 

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

838 """ 

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

840 if the collocation problem is converged. 

841 

842 Args: 

843 controller (pySDC.Controller): The controller 

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

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

846 

847 Returns: 

848 (dict): The updated params dictionary 

849 """ 

850 from pySDC.core.check_convergence import CheckConvergence 

851 

852 defaults = { 

853 'high_Taylor_order': False, 

854 **params, 

855 } 

856 

857 self.check_convergence = CheckConvergence.check_convergence 

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

859 

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

861 """ 

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

863 of iterations. 

864 

865 Args: 

866 controller (pySDC.Controller): The controller 

867 S (pySDC.Step): The current step 

868 

869 Returns: 

870 bool: Whether the step is converged 

871 """ 

872 return self.check_convergence(S) 

873 

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

875 """ 

876 Load the error estimator. 

877 

878 Args: 

879 controller (pySDC.Controller): The controller 

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

881 

882 Returns: 

883 None 

884 """ 

885 from pySDC.implementations.convergence_controller_classes.estimate_extrapolation_error import ( 

886 EstimateExtrapolationErrorWithinQ, 

887 ) 

888 

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

890 

891 controller.add_convergence_controller( 

892 EstimateExtrapolationErrorWithinQ, 

893 description=description, 

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

895 ) 

896 return None 

897 

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

899 """ 

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

901 

902 Args: 

903 controller (pySDC.Controller): The controller 

904 S (pySDC.Step): The current step 

905 

906 Returns: 

907 None 

908 """ 

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

910 L = S.levels[0] 

911 

912 # compute next step size 

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

914 

915 e_est = self.get_local_error_estimate(controller, S) 

916 L.status.dt_new = self.compute_optimal_step_size( 

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

918 ) 

919 

920 self.log( 

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

922 S, 

923 level=10, 

924 ) 

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

926 

927 return None 

928 

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

930 """ 

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

932 

933 Args: 

934 controller (pySDC.Controller): The controller 

935 S (pySDC.Step): The current step 

936 

937 Returns: 

938 float: Embedded error estimate 

939 """ 

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

941 

942 

943class AdaptivityPolynomialError(AdaptivityForConvergedCollocationProblems): 

944 """ 

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

946 

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

948 """ 

949 

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

951 """ 

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

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

954 

955 Args: 

956 controller (pySDC.Controller): The controller 

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

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

959 

960 Returns: 

961 (dict): The updated params dictionary 

962 """ 

963 from pySDC.core.check_convergence import CheckConvergence 

964 

965 defaults = { 

966 'control_order': -50, 

967 'problem_mesh_type': 'numpyesque', 

968 'rel_error': False, 

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

970 **params, 

971 } 

972 

973 self.check_convergence = CheckConvergence.check_convergence 

974 return defaults 

975 

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

977 """ 

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

979 of iterations. 

980 

981 Args: 

982 controller (pySDC.Controller): The controller 

983 S (pySDC.Step): The current step 

984 

985 Returns: 

986 bool: Whether the step is converged 

987 """ 

988 return self.check_convergence(S) 

989 

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

991 """ 

992 Load the error estimator. 

993 

994 Args: 

995 controller (pySDC.Controller): The controller 

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

997 

998 Returns: 

999 None 

1000 """ 

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

1002 from pySDC.implementations.convergence_controller_classes.estimate_polynomial_error import ( 

1003 EstimatePolynomialError as error_estimation_cls, 

1004 ) 

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

1006 from pySDC.implementations.convergence_controller_classes.estimate_polynomial_error import ( 

1007 EstimatePolynomialErrorFiredrake as error_estimation_cls, 

1008 ) 

1009 else: 

1010 raise NotImplementedError( 

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

1012 ) 

1013 

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

1015 

1016 controller.add_convergence_controller( 

1017 error_estimation_cls, 

1018 description=description, 

1019 params={ 

1020 'rel_error': self.params.rel_error, 

1021 }, 

1022 ) 

1023 return None 

1024 

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

1026 """ 

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

1028 

1029 Args: 

1030 controller (pySDC.Controller): The controller 

1031 S (pySDC.Step): The current step 

1032 

1033 Returns: 

1034 None 

1035 """ 

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

1037 L = S.levels[0] 

1038 

1039 # compute next step size 

1040 order = L.status.order_embedded_estimate 

1041 

1042 e_est = self.get_local_error_estimate(controller, S) 

1043 L.status.dt_new = self.compute_optimal_step_size( 

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

1045 ) 

1046 

1047 self.log( 

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

1049 S, 

1050 level=10, 

1051 ) 

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

1053 

1054 return None 

1055 

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

1057 """ 

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

1059 

1060 Args: 

1061 controller (pySDC.Controller): The controller 

1062 S (pySDC.Step): The current step 

1063 

1064 Returns: 

1065 float: Embedded error estimate 

1066 """ 

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