Coverage for pySDC/implementations/sweeper_classes/Runge_Kutta.py: 93%
362 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 12:30 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 12:30 +0000
1import numpy as np
2import logging
3from qmat.qcoeff.butcher import RK_SCHEMES
5from pySDC.core.sweeper import Sweeper, _Pars
6from pySDC.core.errors import ParameterError, ProblemError
7from pySDC.core.level import Level
10class ButcherTableau(object):
11 def __init__(self, weights, nodes, matrix):
12 """
13 Initialization routine to get a quadrature matrix out of a Butcher tableau
15 Args:
16 weights (numpy.ndarray): Butcher tableau weights
17 nodes (numpy.ndarray): Butcher tableau nodes
18 matrix (numpy.ndarray): Butcher tableau entries
19 """
20 self.check_method(weights, nodes, matrix)
22 self.tleft = 0.0
23 self.tright = 1.0
24 self.num_nodes = matrix.shape[0]
25 self.weights = weights
27 self.nodes = np.append([0], nodes)
28 self.Qmat = np.zeros([self.num_nodes + 1, self.num_nodes + 1])
29 self.Qmat[1:, 1:] = matrix
31 self.left_is_node = True
32 self.right_is_node = self.nodes[-1] == self.tright
34 # compute distances between the nodes
35 if self.num_nodes > 1:
36 self.delta_m = self.nodes[1:] - self.nodes[:-1]
37 else:
38 self.delta_m = np.zeros(1)
39 self.delta_m[0] = self.nodes[0] - self.tleft
41 # check if the RK scheme is implicit
42 self.implicit = any(matrix[i, i] != 0 for i in range(self.num_nodes))
44 def check_method(self, weights, nodes, matrix):
45 """
46 Check that the method is entered in the correct format
47 """
48 if type(matrix) != np.ndarray:
49 raise ParameterError('Runge-Kutta matrix needs to be supplied as a numpy array!')
50 elif len(np.unique(matrix.shape)) != 1 or len(matrix.shape) != 2:
51 raise ParameterError('Runge-Kutta matrix needs to be a square 2D numpy array!')
53 if type(nodes) != np.ndarray:
54 raise ParameterError('Nodes need to be supplied as a numpy array!')
55 elif len(nodes.shape) != 1:
56 raise ParameterError(f'Incompatible dimension of nodes! Need 1, got {len(nodes.shape)}')
57 elif len(nodes) != matrix.shape[0]:
58 raise ParameterError(f'Incompatible number of nodes! Need {matrix.shape[0]}, got {len(nodes)}')
60 self.check_weights(weights, nodes, matrix)
62 def check_weights(self, weights, nodes, matrix):
63 """
64 Check that the weights of the method are entered in the correct format
65 """
66 if type(weights) != np.ndarray:
67 raise ParameterError('Weights need to be supplied as a numpy array!')
68 elif len(weights.shape) != 1:
69 raise ParameterError(f'Incompatible dimension of weights! Need 1, got {len(weights.shape)}')
70 elif len(weights) != matrix.shape[0]:
71 raise ParameterError(f'Incompatible number of weights! Need {matrix.shape[0]}, got {len(weights)}')
73 @property
74 def globally_stiffly_accurate(self):
75 """
76 Whether the last row of the Butcher matrix equals the weights, such that the last stage is the solution of the
77 step.
79 Returns:
80 bool: True if the method is stiffly accurate
81 """
82 return np.allclose(self.Qmat[-1, 1:], self.weights)
85class ButcherTableauEmbedded(ButcherTableau):
87 def check_weights(self, weights, nodes, matrix):
88 """
89 Check that the weights of the method are entered in the correct format
90 """
91 if type(weights) != np.ndarray:
92 raise ParameterError('Weights need to be supplied as a numpy array!')
93 elif len(weights.shape) != 2:
94 raise ParameterError(f'Incompatible dimension of weights! Need 2, got {len(weights.shape)}')
95 elif len(weights[0]) != matrix.shape[0]:
96 raise ParameterError(f'Incompatible number of weights! Need {matrix.shape[0]}, got {len(weights[0])}')
98 @property
99 def globally_stiffly_accurate(self):
100 """
101 Whether the last row of the Butcher matrix equals the weights of the primary (higher order) method, such that
102 the last stage is the solution of the step.
104 Returns:
105 bool: True if the method is stiffly accurate
106 """
107 return np.allclose(self.Qmat[-1, 1:], self.weights[0])
110class RungeKutta(Sweeper):
111 """
112 Base class for Runge-Kutta methods with lower triangular Butcher tableaux, wrapped in the sweeper interface.
113 Actually, the sweeper idea fits the Runge-Kutta idea when using only lower triangular rules, where solutions
114 at the nodes are successively computed from earlier nodes. However, we only perform a single iteration of this.
116 We have two choices to realise a Runge-Kutta sweeper: We can choose Q = Q_Delta = <Butcher tableau>, but in this
117 implementation, that would lead to a lot of wasted FLOPS from integrating with Q and then with Q_Delta and
118 subtracting the two. For that reason, we built this new sweeper, which does not have a preconditioner.
120 This class only supports lower triangular Butcher tableaux such that the system can be solved with forward
121 substitution. In this way, we don't get the maximum order that we could for the number of stages, but computing the
122 stages is much cheaper. In particular, if the Butcher tableaux is strictly lower triangular, we get an explicit
123 method, which does not require us to solve a system of equations to compute the stages.
125 Please be aware that all fundamental parameters of the Sweeper are ignored. These include
127 - num_nodes
128 - collocation_class
129 - initial_guess
130 - QI
132 All of these variables are either determined by the RK rule, or are not part of an RK scheme.
134 The entries of the Butcher tableau are stored as class attributes.
135 """
137 nodes = None
138 weights = None
139 matrix = None
140 ButcherTableauClass = ButcherTableau
142 def __init__(self, params, level):
143 """
144 Initialization routine for the custom sweeper
146 Args:
147 params: parameters for the sweeper
148 level (pySDC.Level.level): the level that uses this sweeper
149 """
150 # set up logger
151 self.logger = logging.getLogger('sweeper')
153 # check if some parameters are set which only apply to actual sweepers
154 for key in ['initial_guess', 'collocation_class', 'num_nodes']:
155 if key in params:
156 self.logger.warning(f'"{key}" will be ignored by Runge-Kutta sweeper')
158 # set parameters to their actual values
159 self.coll = self.get_Butcher_tableau()
160 params['initial_guess'] = 'zero'
161 params['num_nodes'] = self.coll.num_nodes
163 # disable residual computation by default
164 params['skip_residual_computation'] = params.get(
165 'skip_residual_computation', ('IT_CHECK', 'IT_FINE', 'IT_COARSE', 'IT_UP', 'IT_DOWN')
166 )
168 # check if we can skip some usually unnecessary right hand side evaluations
169 params['eval_rhs_at_right_boundary'] = params.get('eval_rhs_at_right_boundary', False)
171 self.params = _Pars(params)
173 # set level using the setter in order to adapt residual tolerance if needed
174 self.__level = None
175 self.level = level
177 self.parallelizable = False
178 self.QI = self.coll.Qmat
180 @classmethod
181 def get_Q_matrix(cls):
182 """
183 Get the quadrature matrix of the scheme, i.e. the Butcher matrix padded with a leading row and column of zeros
184 for the initial conditions.
186 Returns:
187 numpy.ndarray: the quadrature matrix
188 """
189 return cls.get_Butcher_tableau().Qmat
191 @classmethod
192 def get_Butcher_tableau(cls):
193 """
194 Build the Butcher tableau from the class attributes ``weights``, ``nodes`` and ``matrix``.
196 Returns:
197 ButcherTableau: an instance of ``ButcherTableauClass``
198 """
199 return cls.ButcherTableauClass(cls.weights, cls.nodes, cls.matrix)
201 @classmethod
202 def get_update_order(cls):
203 """
204 Get the order of the lower order method for doing adaptivity. Only applies to embedded methods.
205 """
206 raise NotImplementedError(
207 f"There is not an update order for RK scheme \"{cls.__name__}\" implemented. Maybe it is not an embedded scheme?"
208 )
210 @classmethod
211 def is_embedded(cls):
212 """
213 Whether the scheme is an embedded pair, i.e. uses ``ButcherTableauEmbedded`` and has a secondary solution for
214 error estimation.
216 Returns:
217 bool: True if the scheme is embedded
218 """
219 return cls.ButcherTableauClass == ButcherTableauEmbedded
221 def get_full_f(self, f):
222 """
223 Get the full right hand side as a `mesh` from the right hand side
225 Args:
226 f (dtype_f): Right hand side at a single node
228 Returns:
229 mesh: Full right hand side as a mesh
230 """
231 if type(f).__name__ in ['mesh', 'cupy_mesh', 'firedrake_mesh']:
232 return f
233 elif type(f).__name__.lower() in ['imex_mesh', 'imex_cupy_mesh', 'imex_firedrake_mesh']:
234 return f.impl + f.expl
235 elif f is None:
236 prob = self.level.prob
237 return self.get_full_f(prob.dtype_f(prob.init, val=0))
238 else:
239 raise NotImplementedError(f'Type \"{type(f)}\" not implemented in Runge-Kutta sweeper')
241 def integrate(self):
242 """
243 Integrates the right-hand side
245 Returns:
246 list of dtype_u: containing the integral as values
247 """
249 # get current level and problem
250 lvl = self.level
251 prob = lvl.prob
253 me = []
255 # integrate RHS over all collocation nodes
256 for m in range(1, self.coll.num_nodes + 1):
257 # new instance of dtype_u, initialize values with 0
258 me.append(prob.dtype_u(prob.init, val=0.0))
259 for j in range(1, self.coll.num_nodes + 1):
260 me[-1] += lvl.dt * self.coll.Qmat[m, j] * self.get_full_f(lvl.f[j])
262 return me
264 def update_nodes(self):
265 """
266 Update the u- and f-values at the collocation nodes
268 Returns:
269 None
270 """
272 # get current level and problem
273 lvl = self.level
274 prob = lvl.prob
276 # only if the level has been touched before
277 assert lvl.status.unlocked
278 assert lvl.status.sweep <= 1, "RK schemes are direct solvers. Please perform only 1 iteration!"
280 # get number of collocation nodes for easier access
281 M = self.coll.num_nodes
283 # `solve_system` of a split problem only inverts the implicit part, so an implicit stage would drop the rest
284 if self.coll.implicit and prob.dtype_f.__name__.lower().startswith('imex'):
285 raise ProblemError(
286 f'{type(self).__name__} has implicit stages, but {type(prob).__name__} splits its right hand side. Use an IMEX Runge-Kutta scheme, such as ARK548L2SA, instead.'
287 )
289 for m in range(0, M):
290 # build rhs, consisting of the known values from above and new values from previous nodes (at k+1)
291 rhs = prob.dtype_u(lvl.u[0])
292 for j in range(1, m + 1):
293 rhs += lvl.dt * self.QI[m + 1, j] * self.get_full_f(lvl.f[j])
295 # implicit solve with prefactor stemming from the diagonal of Qd, use previous stage as initial guess
296 if self.QI[m + 1, m + 1] != 0:
297 lvl.u[m + 1] = prob.solve_system(
298 rhs, lvl.dt * self.QI[m + 1, m + 1], lvl.u[m], lvl.time + lvl.dt * self.coll.nodes[m + 1]
299 )
300 else:
301 lvl.u[m + 1] = rhs
303 # update function values (we don't usually need to evaluate the RHS at the solution of the step)
304 if m < M - 1 or not self.coll.globally_stiffly_accurate or self.is_embedded():
305 lvl.f[m + 1] = prob.eval_f(lvl.u[m + 1], lvl.time + lvl.dt * self.coll.nodes[m + 1])
306 else:
307 lvl.f[m + 1] = prob.f_init
309 # indicate presence of new values at this level
310 lvl.status.updated = True
312 return None
314 def compute_end_point(self):
315 """
316 In this Runge-Kutta implementation, the solution to the step is always stored in the last node
317 """
318 lvl = self.level
320 if lvl.f[1] is None:
321 lvl.uend = lvl.prob.dtype_u(lvl.u[0])
322 if self.is_embedded():
323 self.u_secondary = lvl.prob.dtype_u(lvl.u[0])
324 elif self.coll.globally_stiffly_accurate:
325 lvl.uend = lvl.prob.dtype_u(lvl.u[-1])
326 if self.is_embedded():
327 self.u_secondary = lvl.prob.dtype_u(lvl.u[0])
328 for w2, k in zip(self.coll.weights[1], lvl.f[1:], strict=True):
329 self.u_secondary += lvl.dt * w2 * self.get_full_f(k)
330 else:
331 lvl.uend = lvl.prob.dtype_u(lvl.u[0])
332 if type(self.coll) == ButcherTableau:
333 for w, k in zip(self.coll.weights, lvl.f[1:], strict=True):
334 lvl.uend += lvl.dt * w * self.get_full_f(k)
335 elif self.is_embedded():
336 self.u_secondary = lvl.prob.dtype_u(lvl.u[0])
337 for w1, w2, k in zip(self.coll.weights[0], self.coll.weights[1], lvl.f[1:], strict=True):
338 k = self.get_full_f(k)
339 lvl.uend += lvl.dt * w1 * k
340 self.u_secondary += lvl.dt * w2 * k
342 @property
343 def level(self):
344 """
345 Returns the current level
347 Returns:
348 pySDC.Level.level: Current level
349 """
350 return self.__level
352 @level.setter
353 def level(self, lvl):
354 """
355 Sets a reference to the current level (done in the initialization of the level)
357 Args:
358 lvl (pySDC.Level.level): Current level
359 """
360 assert isinstance(lvl, Level), f"You tried to set the sweeper's level with an instance of {type(lvl)}!"
361 if lvl.params.restol > 0:
362 lvl.params.restol = -1
363 self.logger.warning(
364 'Overwriting residual tolerance with -1 because RK methods are direct and hence may not compute a residual at all!'
365 )
367 self.__level = lvl
369 def predict(self):
370 """
371 Predictor to fill values at nodes before first sweep
372 """
374 # get current level and problem
375 lvl = self.level
376 prob = lvl.prob
378 for m in range(1, self.coll.num_nodes + 1):
379 lvl.u[m] = prob.dtype_u(init=prob.init, val=0.0)
381 # indicate that this level is now ready for sweeps
382 lvl.status.unlocked = True
383 lvl.status.updated = True
386class RungeKuttaIMEX(RungeKutta):
387 """
388 Implicit-explicit split Runge Kutta base class. Only supports methods that share the nodes and weights.
389 """
391 matrix_explicit = None
392 weights_explicit = None
393 ButcherTableauClass_explicit = ButcherTableau
395 def __init__(self, params, level):
396 """
397 Initialization routine
399 Args:
400 params: parameters for the sweeper
401 level (pySDC.Level.level): the level that uses this sweeper
402 """
403 super().__init__(params, level)
404 self.coll_explicit = self.get_Butcher_tableau_explicit()
405 self.QE = self.coll_explicit.Qmat
407 def predict(self):
408 """
409 Predictor to fill values at nodes before first sweep
410 """
412 # get current level and problem
413 lvl = self.level
414 prob = lvl.prob
416 for m in range(1, self.coll.num_nodes + 1):
417 lvl.u[m] = prob.dtype_u(init=prob.init, val=0.0)
418 lvl.f[m] = prob.dtype_f(init=prob.init, val=0.0)
420 # indicate that this level is now ready for sweeps
421 lvl.status.unlocked = True
422 lvl.status.updated = True
424 @classmethod
425 def get_Butcher_tableau_explicit(cls):
426 """
427 Build the Butcher tableau of the explicit part from ``weights_explicit``, ``nodes`` and ``matrix_explicit``.
428 Without ``weights_explicit``, the explicit part uses the implicit ``weights``.
430 Returns:
431 ButcherTableau: an instance of ``ButcherTableauClass_explicit``
432 """
433 weights = cls.weights if cls.weights_explicit is None else cls.weights_explicit
434 return cls.ButcherTableauClass_explicit(weights, cls.nodes, cls.matrix_explicit)
436 def integrate(self):
437 """
438 Integrates the right-hand side
440 Returns:
441 list of dtype_u: containing the integral as values
442 """
444 # get current level and problem
445 lvl = self.level
446 prob = lvl.prob
448 me = []
450 # integrate RHS over all collocation nodes
451 for m in range(1, self.coll.num_nodes + 1):
452 # new instance of dtype_u, initialize values with 0
453 me.append(prob.dtype_u(prob.init, val=0.0))
454 for j in range(1, self.coll.num_nodes + 1):
455 me[-1] += lvl.dt * (
456 self.coll.Qmat[m, j] * lvl.f[j].impl + self.coll_explicit.Qmat[m, j] * lvl.f[j].expl
457 )
459 return me
461 def update_nodes(self):
462 """
463 Update the u- and f-values at the collocation nodes
465 Returns:
466 None
467 """
469 # get current level and problem
470 lvl = self.level
471 prob = lvl.prob
473 # only if the level has been touched before
474 assert lvl.status.unlocked
475 assert lvl.status.sweep <= 1, "RK schemes are direct solvers. Please perform only 1 iteration!"
477 # get number of collocation nodes for easier access
478 M = self.coll.num_nodes
480 for m in range(0, M):
481 # build rhs, consisting of the known values from above and new values from previous nodes (at k+1)
482 rhs = lvl.prob.dtype_u(lvl.u[0])
483 for j in range(1, m + 1):
484 rhs += lvl.dt * (self.QI[m + 1, j] * lvl.f[j].impl + self.QE[m + 1, j] * lvl.f[j].expl)
486 # implicit solve with prefactor stemming from the diagonal of Qd, use previous stage as initial guess
487 if self.QI[m + 1, m + 1] != 0:
488 lvl.u[m + 1] = prob.solve_system(
489 rhs, lvl.dt * self.QI[m + 1, m + 1], lvl.u[m], lvl.time + lvl.dt * self.coll.nodes[m + 1]
490 )
491 else:
492 lvl.u[m + 1] = rhs
494 # update function values
495 if (
496 m < M - 1
497 or not (self.coll.globally_stiffly_accurate and self.coll_explicit.globally_stiffly_accurate)
498 or self.is_embedded()
499 ):
500 lvl.f[m + 1] = prob.eval_f(lvl.u[m + 1], lvl.time + lvl.dt * self.coll.nodes[m + 1])
501 else:
502 lvl.f[m + 1] = prob.f_init
504 # indicate presence of new values at this level
505 lvl.status.updated = True
507 return None
509 def compute_end_point(self):
510 """
511 In this Runge-Kutta implementation, the solution to the step is always stored in the last node
512 """
513 lvl = self.level
515 if lvl.f[1] is None:
516 lvl.uend = lvl.prob.dtype_u(lvl.u[0])
517 if self.is_embedded():
518 self.u_secondary = lvl.prob.dtype_u(lvl.u[0])
519 elif self.coll.globally_stiffly_accurate and self.coll_explicit.globally_stiffly_accurate:
520 lvl.uend = lvl.u[-1]
521 if self.is_embedded():
522 self.u_secondary = lvl.prob.dtype_u(lvl.u[0])
523 for w2, w2E, k in zip(self.coll.weights[1], self.coll_explicit.weights[1], lvl.f[1:], strict=True):
524 self.u_secondary += lvl.dt * (w2 * k.impl + w2E * k.expl)
525 else:
526 lvl.uend = lvl.prob.dtype_u(lvl.u[0])
527 if type(self.coll) == ButcherTableau:
528 for w, wE, k in zip(self.coll.weights, self.coll_explicit.weights, lvl.f[1:], strict=True):
529 lvl.uend += lvl.dt * (w * k.impl + wE * k.expl)
530 elif self.is_embedded():
531 self.u_secondary = lvl.u[0].copy()
532 for w1, w2, w1E, w2E, k in zip(
533 self.coll.weights[0],
534 self.coll.weights[1],
535 self.coll_explicit.weights[0],
536 self.coll_explicit.weights[1],
537 lvl.f[1:],
538 strict=True,
539 ):
540 lvl.uend += lvl.dt * (w1 * k.impl + w1E * k.expl)
541 self.u_secondary += lvl.dt * (w2 * k.impl + w2E * k.expl)
544class ForwardEuler(RungeKutta):
545 """
546 Forward Euler. Still a classic.
548 Not very stable first order method.
549 """
551 generator = RK_SCHEMES["FE"]()
552 nodes, weights, matrix = generator.genCoeffs()
555class BackwardEuler(RungeKutta):
556 """
557 Backward Euler. A favorite among true connoisseurs of the heat equation.
559 A-stable first order method.
560 """
562 generator = RK_SCHEMES["BE"]()
563 nodes, weights, matrix = generator.genCoeffs()
566class IMEXEuler(RungeKuttaIMEX):
567 """
568 First-order IMEX Euler with one backward Euler stage, at which both parts of the right hand side are evaluated.
570 This is a Lie splitting of a backward Euler step for fI and a forward Euler step for fE from its result:
571 u1 = u0 + dt fI(u1), u = u1 + dt fE(u1, t + dt). The explicit tableau has node 1 and matrix entry 0, so fE is
572 evaluated at the new time and the implicit stage, not at the start of the step. For the variant with fE at the
573 start of the step, see IMEXEulerStifflyAccurate.
574 """
576 nodes = BackwardEuler.nodes
577 weights = BackwardEuler.weights
579 matrix = BackwardEuler.matrix
580 matrix_explicit = ForwardEuler.matrix
583class IMEXEulerStifflyAccurate(RungeKuttaIMEX):
584 """
585 IMEX Euler with the explicit part evaluated at the start of the step, as a stiffly accurate two-stage method.
586 This implements u = fI^-1(u0 + fE(u0)) rather than IMEXEuler's u = fI^-1(u0) + fE(fI^-1(u0)).
587 This implementation is slightly inefficient with two stages, but the last stage is the solution, making it stiffly
588 accurate and suitable for some DAEs.
589 """
591 nodes = np.array([0, 1])
592 weights = np.array([0, 1])
593 weights_explicit = np.array([1, 0])
595 matrix = np.array([[0, 0], [0, 1]])
596 matrix_explicit = np.array([[0, 0], [1, 0]])
599class CrankNicolson(RungeKutta):
600 """
601 Implicit Runge-Kutta method of second order, A-stable.
602 """
604 generator = RK_SCHEMES["CN"]()
605 nodes, weights, matrix = generator.genCoeffs()
608class ExplicitMidpointMethod(RungeKutta):
609 """
610 Explicit Runge-Kutta method of second order.
611 """
613 generator = RK_SCHEMES["RK2"]()
614 nodes, weights, matrix = generator.genCoeffs()
617class ImplicitMidpointMethod(RungeKutta):
618 """
619 Implicit Runge-Kutta method of second order.
620 """
622 generator = RK_SCHEMES["IMP"]()
623 nodes, weights, matrix = generator.genCoeffs()
626class RK4(RungeKutta):
627 """
628 Explicit Runge-Kutta of fourth order: Everybody's darling.
629 """
631 generator = RK_SCHEMES["RK4"]()
632 nodes, weights, matrix = generator.genCoeffs()
635class Heun_Euler(RungeKutta):
636 """
637 Second order explicit embedded Runge-Kutta method.
638 """
640 ButcherTableauClass = ButcherTableauEmbedded
642 generator = RK_SCHEMES["HEUN"]()
643 nodes, _weights, matrix = generator.genCoeffs()
644 weights = np.zeros((2, len(_weights)))
645 weights[0] = _weights
646 weights[1] = matrix[-1]
648 @classmethod
649 def get_update_order(cls):
650 """
651 Order in dt of the embedded error estimate, i.e. of the local error of forward Euler.
653 Returns:
654 int: 2
655 """
656 return 2
659class Cash_Karp(RungeKutta):
660 """
661 Fifth order explicit embedded Runge-Kutta. See [here](https://doi.org/10.1145/79505.79507).
662 """
664 generator = RK_SCHEMES["CashKarp"]()
665 nodes, weights, matrix = generator.genCoeffs(embedded=True)
666 ButcherTableauClass = ButcherTableauEmbedded
668 @classmethod
669 def get_update_order(cls):
670 """
671 Order in dt of the embedded error estimate, i.e. of the local error of the fourth order secondary method.
673 Returns:
674 int: 5
675 """
676 return 5
679class DIRK43(RungeKutta):
680 """
681 Embedded A-stable diagonally implicit RK pair of order 3 and 4.
683 Taken from [here](https://doi.org/10.1007/BF01934920).
684 """
686 generator = RK_SCHEMES["EDIRK43"]()
687 nodes, weights, matrix = generator.genCoeffs(embedded=True)
688 ButcherTableauClass = ButcherTableauEmbedded
690 @classmethod
691 def get_update_order(cls):
692 """
693 Order in dt of the embedded error estimate, i.e. of the local error of the third order secondary method.
695 Returns:
696 int: 4
697 """
698 return 4
701class DIRK43_2(RungeKutta):
702 """
703 L-stable Diagonally Implicit RK method with four stages of order 3.
704 Taken from [here](https://en.wikipedia.org/wiki/List_of_Runge%E2%80%93Kutta_methods).
705 """
707 generator = RK_SCHEMES["DIRK43"]()
708 nodes, weights, matrix = generator.genCoeffs()
711class EDIRK4(RungeKutta):
712 """
713 Stiffly accurate, fourth-order EDIRK with four stages. Taken from
714 [here](https://ntrs.nasa.gov/citations/20160005923), second one in eq. (216).
715 """
717 generator = RK_SCHEMES["EDIRK4"]()
718 nodes, weights, matrix = generator.genCoeffs()
721class ESDIRK53(RungeKutta):
722 """
723 A-stable embedded RK pair of orders 5 and 3, ESDIRK5(3)6L[2]SA.
724 Taken from [here](https://ntrs.nasa.gov/citations/20160005923)
725 """
727 generator = RK_SCHEMES["ESDIRK53"]()
728 nodes, weights, matrix = generator.genCoeffs(embedded=True)
729 ButcherTableauClass = ButcherTableauEmbedded
731 @classmethod
732 def get_update_order(cls):
733 """
734 Order in dt of the embedded error estimate, i.e. of the local error of the third order secondary method.
736 Returns:
737 int: 4
738 """
739 return 4
742class ESDIRK43(RungeKutta):
743 """
744 A-stable embedded RK pair of orders 4 and 3, ESDIRK4(3)6L[2]SA.
745 Taken from [here](https://ntrs.nasa.gov/citations/20160005923)
746 """
748 generator = RK_SCHEMES["ESDIRK43"]()
749 nodes, weights, matrix = generator.genCoeffs(embedded=True)
750 ButcherTableauClass = ButcherTableauEmbedded
752 @classmethod
753 def get_update_order(cls):
754 """
755 Order in dt of the embedded error estimate, i.e. of the local error of the third order secondary method.
757 Returns:
758 int: 4
759 """
760 return 4
763class ARK548L2SAERK(RungeKutta):
764 """
765 Explicit part of the ARK54 scheme.
766 """
768 generator = RK_SCHEMES["ARK548L2SAERK"]()
769 nodes, weights, matrix = generator.genCoeffs(embedded=True)
770 ButcherTableauClass = ButcherTableauEmbedded
772 @classmethod
773 def get_update_order(cls):
774 """
775 Order in dt of the embedded error estimate, i.e. of the local error of the fourth order secondary method.
777 Returns:
778 int: 5
779 """
780 return 5
783class ARK548L2SAESDIRK(ARK548L2SAERK):
784 """
785 Implicit part of the ARK54 scheme. As Kennedy and Carpenter state, the embedded method has order 4 and the main
786 method order 5. The fifth-order conditions of the embedded method are violated only by about 1e-4, however, so it
787 can look like order 5 in convergence tests at moderate step sizes.
788 """
790 generator_IMP = RK_SCHEMES["ARK548L2SAESDIRK"]()
791 matrix = generator_IMP.Q
794class ARK54(RungeKuttaIMEX):
795 """
796 IMEX Runge-Kutta method ARK5(4)8L[2]SA of Kennedy and Carpenter, combining ARK548L2SAERK and ARK548L2SAESDIRK.
797 Pair of pairs of ARK5(4)8L[2]SA-ERK and ARK5(4)8L[2]SA-ESDIRK from
798 [here](https://doi.org/10.1016/S0168-9274(02)00138-1).
799 """
801 ButcherTableauClass = ButcherTableauEmbedded
802 ButcherTableauClass_explicit = ButcherTableauEmbedded
804 nodes = ARK548L2SAERK.nodes
805 weights = ARK548L2SAERK.weights
807 matrix = ARK548L2SAESDIRK.matrix
808 matrix_explicit = ARK548L2SAERK.matrix
810 @classmethod
811 def get_update_order(cls):
812 """
813 Order in dt of the embedded error estimate, i.e. of the local error of the fourth order secondary method.
815 Returns:
816 int: 5
817 """
818 return 5
821class ARK548L2SAESDIRK2(RungeKutta):
822 """
823 Implicit part of ARK548L2SA: an L-stable, stiffly accurate ESDIRK pair of orders 5 and 4.
824 Stiffly accurate singly diagonally L-stable implicit embedded Runge-Kutta pair of orders 5 and 4 with explicit
825 first stage from [here](https://doi.org/10.1016/j.apnum.2018.10.007).
826 This method is part of the IMEX method ARK548L2SA.
827 """
829 generator = RK_SCHEMES["ARK548L2SAESDIRK2"]()
830 nodes, weights, matrix = generator.genCoeffs(embedded=True)
831 ButcherTableauClass = ButcherTableauEmbedded
833 @classmethod
834 def get_update_order(cls):
835 """
836 Order in dt of the embedded error estimate, i.e. of the local error of the fourth order secondary method.
838 Returns:
839 int: 5
840 """
841 return 5
844class ARK548L2SAERK2(ARK548L2SAESDIRK2):
845 """
846 Explicit part of ARK548L2SA: an explicit embedded Runge-Kutta pair of orders 5 and 4.
847 Taken from [here](https://doi.org/10.1016/j.apnum.2018.10.007).
848 This method is part of the IMEX method ARK548L2SA.
849 """
851 generator_EXP = RK_SCHEMES["ARK548L2SAERK2"]()
852 matrix = generator_EXP.Q
855class ARK548L2SA(RungeKuttaIMEX):
856 """
857 Newer order-5 IMEX Runge-Kutta method ARK5(4)8L[2]SA of Kennedy and Carpenter, an alternative to ARK54.
858 It is based on the explicit method ARK548L2SAERK2 and the implicit method
859 ARK548L2SAESDIRK2 from [here](https://doi.org/10.1016/j.apnum.2018.10.007).
861 According to Kennedy and Carpenter (see reference), the two IMEX RK methods of order 5 are the only ones available
862 as of now. And we are not aware of higher order ones. This one is newer then the other one and apparently better.
863 """
865 ButcherTableauClass = ButcherTableauEmbedded
866 ButcherTableauClass_explicit = ButcherTableauEmbedded
868 nodes = ARK548L2SAERK2.nodes
869 weights = ARK548L2SAERK2.weights
871 matrix = ARK548L2SAESDIRK2.matrix
872 matrix_explicit = ARK548L2SAERK2.matrix
874 @classmethod
875 def get_update_order(cls):
876 """
877 Order in dt of the embedded error estimate, i.e. of the local error of the fourth order secondary method.
879 Returns:
880 int: 5
881 """
882 return 5
885class ARK324L2SAERK(RungeKutta):
886 """
887 Explicit part of ARK32: an explicit embedded Runge-Kutta pair of orders 3 and 2.
888 """
890 generator = RK_SCHEMES["ARK324L2SAERK"]()
891 nodes, weights, matrix = generator.genCoeffs(embedded=True)
892 ButcherTableauClass = ButcherTableauEmbedded
894 @classmethod
895 def get_update_order(cls):
896 """
897 Order in dt of the embedded error estimate, i.e. of the local error of the second order secondary method.
899 Returns:
900 int: 3
901 """
902 return 3
905class ARK324L2SAESDIRK(ARK324L2SAERK):
906 """
907 Implicit part of ARK32: an L-stable, stiffly accurate ESDIRK pair of orders 3 and 2.
908 """
910 generator = RK_SCHEMES["ARK324L2SAESDIRK"]()
911 matrix = generator.Q
914class ARK32(RungeKuttaIMEX):
915 """
916 Embedded IMEX Runge-Kutta method ARK3(2)4L[2]SA of Kennedy and Carpenter, of orders 3 and 2.
917 """
919 ButcherTableauClass = ButcherTableauEmbedded
920 ButcherTableauClass_explicit = ButcherTableauEmbedded
922 nodes = ARK324L2SAESDIRK.nodes
923 weights = ARK324L2SAESDIRK.weights
925 matrix = ARK324L2SAESDIRK.matrix
926 matrix_explicit = ARK324L2SAERK.matrix
928 @classmethod
929 def get_update_order(cls):
930 """
931 Order in dt of the embedded error estimate, i.e. of the local error of the second order secondary method.
933 Returns:
934 int: 3
935 """
936 return 3
939class ARK2(RungeKuttaIMEX):
940 """
941 Second order two stage singly diagonally implicit globally stiffly accurate IMEX RK method with explicit first stage.
942 Can be used to integrate simple DAEs because explicit and implicit part are both stiffly accurate.
943 """
945 generator_IMP = RK_SCHEMES["ARK222EDIRK"]()
946 generator_EXP = RK_SCHEMES["ARK222ERK"]()
948 nodes, weights, matrix = generator_IMP.genCoeffs()
949 _, weights_explicit, matrix_explicit = generator_EXP.genCoeffs()
952class ARK3(RungeKuttaIMEX):
953 """
954 Third order four stage singly diagonally implicit globally stiffly accurate IMEX RK method with explicit first stage.
955 Can be used to integrate simple DAEs because explicit and implicit part are both stiffly accurate.
956 """
958 generator_IMP = RK_SCHEMES["ARK443ESDIRK"]()
959 generator_EXP = RK_SCHEMES["ARK443ERK"]()
961 nodes, weights, matrix = generator_IMP.genCoeffs()
962 _, weights_explicit, matrix_explicit = generator_EXP.genCoeffs()