Skip to content

Commit 61d29d5

Browse files
authored
Merge pull request #164 from brownbaerchen/rk-sweeper
Added Cash-Karp method to do embedded Runge-Kutta 5(4)
2 parents e63c6fb + bf74069 commit 61d29d5

File tree

7 files changed

+300
-34
lines changed

7 files changed

+300
-34
lines changed

pySDC/implementations/convergence_controller_classes/adaptivity.py

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def get_new_step_size(self, controller, S):
6767
'''
6868
raise NotImplementedError('Please implement a rule for updating the step size!')
6969

70-
def compute_optimatal_step_size(self, beta, dt, e_tol, e_est, order):
70+
def compute_optimal_step_size(self, beta, dt, e_tol, e_est, order):
7171
'''
7272
Compute the optimal step size for the current step based on the order of the scheme.
7373
This function can be called from `get_new_step_size` for various implementations of adaptivity, but notably not
@@ -192,8 +192,8 @@ def get_new_step_size(self, controller, S):
192192
# compute next step size
193193
order = S.status.iter # embedded error estimate is same order as time marching
194194
e_est = self.get_local_error_estimate(controller, S)
195-
L.status.dt_new = self.compute_optimatal_step_size(self.params.beta, L.params.dt, self.params.e_tol, e_est,
196-
order)
195+
L.status.dt_new = self.compute_optimal_step_size(self.params.beta, L.params.dt, self.params.e_tol, e_est,
196+
order)
197197
self.log(f'Adjusting step size from {L.params.dt:.2e} to {L.status.dt_new:.2e}', S)
198198

199199
return None
@@ -212,6 +212,55 @@ def get_local_error_estimate(self, controller, S, **kwargs):
212212
return S.levels[0].status.error_embedded_estimate
213213

214214

215+
class AdaptivityRK(Adaptivity):
216+
'''
217+
Adaptivity for Runge-Kutta methods. Basically, we need to change the order in the step size update
218+
'''
219+
def check_parameters(self, controller, params, description):
220+
'''
221+
Check whether parameters are compatible with whatever assumptions went into the step size functions etc.
222+
For adaptivity, we need to know the order of the scheme.
223+
224+
Args:
225+
controller (pySDC.Controller): The controller
226+
params (dict): The params passed for this specific convergence controller
227+
description (dict): The description object used to instantiate the controller
228+
229+
Returns:
230+
bool: Whether the parameters are compatible
231+
str: The error message
232+
'''
233+
if 'update_order' not in params.keys():
234+
return False, 'Adaptivity needs an order for the update rule! Please set some up in \
235+
description[\'convergence_control_params\'][\'update_order\']!'
236+
237+
return super(AdaptivityRK, self).check_parameters(controller, params, description)
238+
239+
def get_new_step_size(self, controller, S):
240+
'''
241+
Determine a step size for the next step from an embedded estimate of the local error of the current step.
242+
243+
Args:
244+
controller (pySDC.Controller): The controller
245+
S (pySDC.Step): The current step
246+
247+
Returns:
248+
None
249+
'''
250+
# check if we performed the desired amount of sweeps
251+
if S.status.iter == S.params.maxiter:
252+
L = S.levels[0]
253+
254+
# compute next step size
255+
order = self.params.update_order
256+
e_est = self.get_local_error_estimate(controller, S)
257+
L.status.dt_new = self.compute_optimal_step_size(self.params.beta, L.params.dt, self.params.e_tol, e_est,
258+
order)
259+
self.log(f'Adjusting step size from {L.params.dt:.2e} to {L.status.dt_new:.2e}', S)
260+
261+
return None
262+
263+
215264
class AdaptivityResidual(AdaptivityBase):
216265
'''
217266
Do adaptivity based on residual.

pySDC/implementations/convergence_controller_classes/estimate_embedded_error.py

Lines changed: 32 additions & 7 deletions
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
'''
@@ -14,7 +16,7 @@ class EstimateEmbeddedError(ConvergenceController):
1416

1517
def setup(self, controller, params, description):
1618
'''
17-
Add a default value for control order to the parameters
19+
Add a default value for control order to the parameters and check if we are using a Runge-Kutta sweeper
1820
1921
Args:
2022
controller (pySDC.Controller): The controller
@@ -24,11 +26,12 @@ def setup(self, controller, params, description):
2426
Returns:
2527
dict: Updated parameters
2628
'''
27-
return {'control_order': -80, **params}
29+
sweeper_type = 'RK' if RungeKutta in description['sweeper_class'].__bases__ else 'SDC'
30+
return {'control_order': -80, 'sweeper_type': sweeper_type, **params}
2831

2932
def dependencies(self, controller, description):
3033
'''
31-
Load the convergence controller that stores the solution of the last sweep
34+
Load the convergence controller that stores the solution of the last sweep unless we are doing Runge-Kutta
3235
3336
Args:
3437
controller (pySDC.Controller): The controller
@@ -37,9 +40,32 @@ def dependencies(self, controller, description):
3740
Returns:
3841
None
3942
'''
40-
controller.add_convergence_controller(StoreUOld, description=description)
43+
if RungeKutta not in description['sweeper_class'].__bases__:
44+
controller.add_convergence_controller(StoreUOld, description=description)
4145
return None
4246

47+
def estimate_embedded_error_serial(self, L):
48+
'''
49+
Estimate the serial embedded error, which may need to be modified for a parallel estimate.
50+
51+
Depending on the type of sweeper, the lower order solution is stored in a different place.
52+
53+
Args:
54+
L (pySDC.level): The level
55+
56+
Returns:
57+
dtype_u: The embedded error estimate
58+
'''
59+
if self.params.sweeper_type == 'RK':
60+
# lower order solution is stored in the second to last entry of L.u
61+
return abs(L.u[-2] - L.u[-1])
62+
elif self.params.sweeper_type == 'SDC':
63+
# order rises by one between sweeps, making this so ridiculously easy
64+
return abs(L.uold[-1] - L.u[-1])
65+
else:
66+
raise NotImplementedError(f'Don\'t know how to estimate embedded error for sweeper type \
67+
{self.params.sweeper_type}')
68+
4369

4470
class EstimateEmbeddedErrorNonMPI(EstimateEmbeddedError):
4571

@@ -84,10 +110,9 @@ def post_iteration_processing(self, controller, S):
84110
raise NotImplementedError('Embedded error estimate only works for serial multi-level or parallel single \
85111
level')
86112

87-
if S.status.iter > 1:
113+
if S.status.iter > 1 or self.params.sweeper_type == 'RK':
88114
for L in S.levels:
89-
# order rises by one between sweeps, making this so ridiculously easy
90-
temp = abs(L.uold[-1] - L.u[-1])
115+
temp = self.estimate_embedded_error_serial(L)
91116
L.status.error_embedded_estimate = max([abs(temp - self.buffers.e_em_last), np.finfo(float).eps])
92117

93118
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: 45 additions & 9 deletions
Large diffs are not rendered by default.

pySDC/projects/Resilience/accuracy_check.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,16 +141,20 @@ def plot(res, ax, k):
141141
color = plt.rcParams['axes.prop_cycle'].by_key()['color'][k - 2]
142142

143143
for i in range(len(keys)):
144+
if all([me == 0. for me in res[keys[i]]]):
145+
continue
144146
order = get_accuracy_order(res, key=keys[i])
145147
if keys[i] == 'e_embedded':
146148
label = rf'$k={{{np.mean(order):.2f}}}$'
147-
assert np.isclose(np.mean(order), k, atol=3e-1), f'Expected embedded error estimate to have order {k} \
149+
assert np.isclose(np.mean(order), k, atol=4e-1), f'Expected embedded error estimate to have order {k} \
148150
but got {np.mean(order):.2f}'
149151

150152
elif keys[i] == 'e_extrapolated':
151153
label = None
152154
assert np.isclose(np.mean(order), k + 1, rtol=3e-1), f'Expected extrapolation error estimate to have order \
153155
{k+1} but got {np.mean(order):.2f}'
156+
else:
157+
label = None
154158
ax.loglog(res['dt'], res[keys[i]], color=color, ls=ls[i], label=label)
155159

156160
ax.set_xlabel(r'$\Delta t$')
@@ -198,11 +202,12 @@ def plot_orders(ax, ks, serial, Tend_fixed=None, custom_description=None, prob=r
198202
plot_order(res, ax, k)
199203

200204

201-
def plot_all_errors(ax, ks, serial, Tend_fixed=None, custom_description=None, prob=run_piline):
205+
def plot_all_errors(ax, ks, serial, Tend_fixed=None, custom_description=None, prob=run_piline, dt_list=None,
206+
custom_controller_params=None):
202207
for i in range(len(ks)):
203208
k = ks[i]
204209
res = multiple_runs(k=k, serial=serial, Tend_fixed=Tend_fixed, custom_description=custom_description,
205-
prob=prob)
210+
prob=prob, dt_list=dt_list, custom_controller_params=custom_controller_params)
206211

207212
# visualize results
208213
plot(res, ax, k)

0 commit comments

Comments
 (0)