Skip to content

Commit d536051

Browse files
author
Thomas Baumann
committed
Added Cash-Karp method to do embedded Runge-Kutta 5(4)
1 parent 7851cc8 commit d536051

File tree

5 files changed

+197
-21
lines changed

5 files changed

+197
-21
lines changed

pySDC/implementations/convergence_controller_classes/estimate_embedded_error.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from pySDC.core.ConvergenceController import ConvergenceController, Pars
44
from pySDC.implementations.convergence_controller_classes.store_uold import StoreUOld
55

6+
from pySDC.implementations.sweeper_classes.Runge_Kutta import RungeKutta
7+
68

79
class EstimateEmbeddedError(ConvergenceController):
810
'''
@@ -40,6 +42,24 @@ def dependencies(self, controller, description):
4042
controller.add_convergence_controller(StoreUOld, description=description)
4143
return None
4244

45+
def estimate_embedded_error_serial(self, L):
46+
'''
47+
Estimate the serial embedded error, which may need to be modified for a parallel estimate.
48+
49+
Depending on the type of sweeper, the lower order solution is stored in a different place.
50+
51+
Args:
52+
L (pySDC.level): The level
53+
54+
Returns:
55+
dtype_u: The embedded error estimate
56+
'''
57+
sweeper = L.sweep
58+
if RungeKutta in type(sweeper).__bases__:
59+
return abs(L.u[-2] - L.u[-1])
60+
else:
61+
return abs(L.uold[-1] - L.u[-1])
62+
4363

4464
class EstimateEmbeddedErrorNonMPI(EstimateEmbeddedError):
4565

@@ -87,7 +107,7 @@ def post_iteration_processing(self, controller, S):
87107
if S.status.iter > 1:
88108
for L in S.levels:
89109
# order rises by one between sweeps, making this so ridiculously easy
90-
temp = abs(L.uold[-1] - L.u[-1])
110+
temp = self.estimate_embedded_error_serial(L)
91111
L.status.error_embedded_estimate = max([abs(temp - self.buffers.e_em_last), np.finfo(float).eps])
92112

93113
self.buffers.e_em_last = temp * 1.

pySDC/implementations/sweeper_classes/Runge_Kutta.py

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
class ButcherTableau(object):
1010
def __init__(self, weights, nodes, matrix):
1111
"""
12-
Initialization routine for an collocation object
12+
Initialization routine to get a quadrature matrix out of a Butcher tableau
1313
1414
Args:
1515
weights (numpy.ndarray): Butcher tableau weights
@@ -61,6 +61,64 @@ def __init__(self, weights, nodes, matrix):
6161
self.implicit = any([matrix[i, i] != 0 for i in range(self.num_nodes - 1)])
6262

6363

64+
class ButcherTableauEmbedded(object):
65+
def __init__(self, weights, nodes, matrix):
66+
"""
67+
Initialization routine to get a quadrature matrix out of a Butcher tableau for embedded RK methods.
68+
69+
Be aware that the method that generates the final solution should be in the first row of the weights matrix.
70+
71+
Args:
72+
weights (numpy.ndarray): Butcher tableau weights
73+
nodes (numpy.ndarray): Butcher tableau nodes
74+
matrix (numpy.ndarray): Butcher tableau entries
75+
"""
76+
# check if the arguments have the correct form
77+
if type(matrix) != np.ndarray:
78+
raise ParameterError('Runge-Kutta matrix needs to be supplied as a numpy array!')
79+
elif len(np.unique(matrix.shape)) != 1 or len(matrix.shape) != 2:
80+
raise ParameterError('Runge-Kutta matrix needs to be a square 2D numpy array!')
81+
82+
if type(weights) != np.ndarray:
83+
raise ParameterError('Weights need to be supplied as a numpy array!')
84+
elif len(weights.shape) != 2:
85+
raise ParameterError(f'Incompatible dimension of weights! Need 2, got {len(weights.shape)}')
86+
elif len(weights[0]) != matrix.shape[0]:
87+
raise ParameterError(f'Incompatible number of weights! Need {matrix.shape[0]}, got {len(weights[0])}')
88+
89+
if type(nodes) != np.ndarray:
90+
raise ParameterError('Nodes need to be supplied as a numpy array!')
91+
elif len(nodes.shape) != 1:
92+
raise ParameterError(f'Incompatible dimension of nodes! Need 1, got {len(nodes.shape)}')
93+
elif len(nodes) != matrix.shape[0]:
94+
raise ParameterError(f'Incompatible number of nodes! Need {matrix.shape[0]}, got {len(nodes)}')
95+
96+
# Set number of nodes, left and right interval boundaries
97+
self.num_nodes = matrix.shape[0] + 2
98+
self.tleft = 0.
99+
self.tright = 1.
100+
101+
self.nodes = np.append(np.append([0], nodes), [1, 1])
102+
self.weights = weights
103+
self.Qmat = np.zeros([self.num_nodes + 1, self.num_nodes + 1])
104+
self.Qmat[1:-2, 1:-2] = matrix
105+
self.Qmat[-1, 1:-2] = weights[0] # this is for computing the higher order solution
106+
self.Qmat[-2, 1:-2] = weights[1] # this is for computing the lower order solution
107+
108+
self.left_is_node = True
109+
self.right_is_node = self.nodes[-1] == self.tright
110+
111+
# compute distances between the nodes
112+
if self.num_nodes > 1:
113+
self.delta_m = self.nodes[1:] - self.nodes[:-1]
114+
else:
115+
self.delta_m = np.zeros(1)
116+
self.delta_m[0] = self.nodes[0] - self.tleft
117+
118+
# check if the RK scheme is implicit
119+
self.implicit = any([matrix[i, i] != 0 for i in range(self.num_nodes - 2)])
120+
121+
64122
class RungeKutta(generic_implicit):
65123
"""
66124
Runge-Kutta scheme that fits the interface of a sweeper.
@@ -215,3 +273,34 @@ def __init__(self, params):
215273
matrix[3, 2] = 1.
216274
params['butcher_tableau'] = ButcherTableau(weights, nodes, matrix)
217275
super(RK4, self).__init__(params)
276+
277+
278+
class Heun_Euler(RungeKutta):
279+
'''
280+
Second order explicit embedded Runge-Kutta
281+
'''
282+
def __init__(self, params):
283+
nodes = np.array([0, 1])
284+
weights = np.array([[0.5, 0.5], [1, 0]])
285+
matrix = np.zeros((2, 2))
286+
matrix[1, 0] = 1
287+
params['butcher_tableau'] = ButcherTableauEmbedded(weights, nodes, matrix)
288+
super(Heun_Euler, self).__init__(params)
289+
290+
291+
class Cash_Karp(RungeKutta):
292+
'''
293+
Fifth order explicit embedded Runge-Kutta
294+
'''
295+
def __init__(self, params):
296+
nodes = np.array([0, 0.2, 0.3, 0.6, 1., 7. / 8.])
297+
weights = np.array([[37. / 378., 0., 250. / 621., 125. / 594., 0., 512. / 1771.],
298+
[2825. / 27648., 0., 18575. / 48384., 13525. / 55296., 277. / 14336., 1. / 4.]])
299+
matrix = np.zeros((6, 6))
300+
matrix[1, 0] = 1. / 5.
301+
matrix[2, :2] = [3. / 40., 9. / 40.]
302+
matrix[3, :3] = [0.3, -0.9, 1.2]
303+
matrix[4, :4] = [-11. / 54., 5. / 2., -70. / 27., 35. / 27.]
304+
matrix[5, :5] = [1631. / 55296., 175. / 512., 575. / 13824., 44275. / 110592., 253. / 4096.]
305+
params['butcher_tableau'] = ButcherTableauEmbedded(weights, nodes, matrix)
306+
super(Cash_Karp, self).__init__(params)

pySDC/playgrounds/Runge_Kutta/Runge-Kutta-Methods.ipynb

Lines changed: 44 additions & 9 deletions
Large diffs are not rendered by default.

pySDC/projects/Resilience/test_Runge_Kutta_sweeper.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,19 @@
55
from pySDC.projects.Resilience.dahlquist import run_dahlquist, plot_stability
66

77
from pySDC.projects.Resilience.advection import run_advection
8-
from pySDC.projects.Resilience.vdp import run_vdp
8+
from pySDC.projects.Resilience.vdp import run_vdp, plot_step_sizes
99

10-
from pySDC.implementations.sweeper_classes.Runge_Kutta import RK1, RK4, MidpointMethod, CrankNicholson
10+
from pySDC.implementations.sweeper_classes.Runge_Kutta import RK1, RK4, MidpointMethod, CrankNicholson, Cash_Karp
11+
from pySDC.implementations.convergence_controller_classes.adaptivity import Adaptivity
12+
from pySDC.helpers.stats_helper import get_sorted
1113

1214

1315
colors = {
1416
RK1: 'blue',
1517
MidpointMethod: 'red',
1618
RK4: 'orange',
1719
CrankNicholson: 'purple',
20+
Cash_Karp: 'teal',
1821
}
1922

2023

@@ -38,10 +41,11 @@ def plot_order(sweeper, prob, dt_list, description=None, ax=None, Tend_fixed=Non
3841
MidpointMethod: 3,
3942
RK4: 5,
4043
CrankNicholson: 3,
44+
Cash_Karp: 6,
4145
}
4246
numerical_order = float(ax.get_lines()[-1].get_label()[7:])
4347
expected_order = orders.get(sweeper, numerical_order)
44-
assert np.isclose(numerical_order, expected_order, atol=2.5e-1),\
48+
assert np.isclose(numerical_order, expected_order, atol=2.6e-1),\
4549
f"Expected order {expected_order}, got {numerical_order}!"
4650

4751
# decorate
@@ -80,11 +84,11 @@ def plot_all_stability():
8084
fig, axs = plt.subplots(1, 2, figsize=(11, 5))
8185

8286
impl = [True, False]
83-
sweepers = [[RK1, MidpointMethod, CrankNicholson], [RK1, MidpointMethod, RK4]]
87+
sweepers = [[RK1, MidpointMethod, CrankNicholson], [RK1, MidpointMethod, RK4, Cash_Karp]]
8488
titles = ['implicit', 'explicit']
85-
re = np.linspace(-3, 3, 400)
86-
im = np.linspace(-3, 3, 400)
87-
crosshair = [True, False, False]
89+
re = np.linspace(-4, 4, 400)
90+
im = np.linspace(-4, 4, 400)
91+
crosshair = [True, False, False, False]
8892

8993
for j in range(len(impl)):
9094
for i in range(len(sweepers[j])):
@@ -102,15 +106,42 @@ def plot_all_orders(prob, dt_list, Tend, sweepers):
102106

103107

104108
def test_vdp():
105-
Tend = 5e-2
106-
plot_all_orders(run_vdp, Tend * 2.**(-np.arange(8)), Tend, [RK1, MidpointMethod, CrankNicholson, RK4])
109+
Tend = 7e-2
110+
plot_all_orders(run_vdp, Tend * 2.**(-np.arange(8)), Tend, [RK1, MidpointMethod, CrankNicholson, RK4, Cash_Karp])
107111

108112

109113
def test_advection():
110114
plot_all_orders(run_advection, 1.e-3 * 2.**(-np.arange(8)), None, [RK1, MidpointMethod, CrankNicholson])
111115

112116

117+
def test_embedded_method():
118+
sweeper = Cash_Karp
119+
fig, ax = plt.subplots(1, 1)
120+
121+
# change only the things in the description that we need for adaptivity
122+
adaptivity_params = dict()
123+
adaptivity_params['e_tol'] = 1e-7
124+
125+
convergence_controllers = dict()
126+
convergence_controllers[Adaptivity] = adaptivity_params
127+
128+
description = dict()
129+
description['convergence_controllers'] = convergence_controllers
130+
description['sweeper_class'] = sweeper
131+
132+
custom_controller_params = {'logger_level': 40}
133+
134+
stats, _, _ = run_vdp(description, 1, custom_controller_params=custom_controller_params)
135+
plot_step_sizes(stats, ax)
136+
137+
fig.tight_layout()
138+
139+
dt_last = get_sorted(stats, type='dt')[-1][1]
140+
assert np.isclose(dt_last, 0.11091455589374277), "Cash-Karp has computed a different last step size than before!"
141+
142+
113143
if __name__ == '__main__':
144+
test_embedded_method()
114145
test_vdp()
115146
test_advection()
116147
plot_all_stability()
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import pytest
22

3-
from pySDC.projects.Resilience.test_Runge_Kutta_sweeper import test_vdp, test_advection
3+
from pySDC.projects.Resilience.test_Runge_Kutta_sweeper import test_vdp, test_advection, test_embedded_method
44

55
def test_main():
66
test_vdp()
77
test_advection()
8+
test_embedded_method()

0 commit comments

Comments
 (0)