Note: this post was generated from an iPython notebook. You can download the notebook from github and execute all the code yourself.
Internal stability deals with the growth of errors (such as roundoff) introduced at the Runge-Kutta stages during a single Runge-Kutta step. It is usually important only for methods with a large number of stages, since that is when the internal amplification factors can be large. An excellent explanation of internal stability is given in this paper. Here we demonstrate some tools for studying internal stability in NodePy.
First, let’s load a couple of RK methods:
from nodepy import rk
reload(rk)
rk4 = rk.loadRKM('RK44')
ssprk4 = rk.loadRKM('SSP104')
print rk4
print ssprk4
Classical RK4
The original four-stage, fourth-order method of Kutta
0 |
1/2 | 1/2
1/2 | 0 1/2
1 | 0 0 1
_____|____________________
| 1/6 1/3 1/3 1/6
SSPRK(10,4)
The optimal ten-stage, fourth order SSP Runge-Kutta method
0 |
1/6 | 1/6
1/3 | 1/6 1/6
1/2 | 1/6 1/6 1/6
2/3 | 1/6 1/6 1/6 1/6
1/3 | 1/15 1/15 1/15 1/15 1/15
1/2 | 1/15 1/15 1/15 1/15 1/15 1/6
2/3 | 1/15 1/15 1/15 1/15 1/15 1/6 1/6
5/6 | 1/15 1/15 1/15 1/15 1/15 1/6 1/6 1/6
1 | 1/15 1/15 1/15 1/15 1/15 1/6 1/6 1/6 1/6
_____|____________________________________________________________
| 1/10 1/10 1/10 1/10 1/10 1/10 1/10 1/10 1/10 1/10
First we can use NodePy to plot the region of absolute stability for each method. The absolute stability region is the set
where \(\phi(z)\) is the stability function of the method:
If we solve \(u'(t) = \lambda u\) with a given method, then \(z=\lambda \Delta t\) must lie inside this region or the computation will be unstable.
p,q = rk4.stability_function()
print p
h1=rk4.plot_stability_region()
4 3 2
0.04167 x + 0.1667 x + 0.5 x + 1 x + 1
p,q = ssprk4.stability_function()
print p
h2=ssprk4.plot_stability_region()
10 9 8 7 6
3.969e-09 x + 2.381e-07 x + 6.43e-06 x + 0.0001029 x + 0.00108 x
5 4 3 2
+ 0.00787 x + 0.04167 x + 0.1667 x + 0.5 x + 1 x + 1
The stability function tells us by how much errors from one step are amplified in the next one. This is important since we introduce truncation errors at every step. However, we also introduce roundoff errors at the each stage within a step. Internal stability tells us about the growth of those. Internal stability is typically less important than (step-by-step) absolute stability for two reasons:
Nevertheless, in methods with many stages, internal stability can play a key role.
Questions: In the solution of PDEs, large spatial truncation errors enter at each stage. Does this mean internal stability becomes more significant? How does this relate to stiff accuracy analysis and order reduction?
We can write the equations of a Runge-Kutta method compactly as
where \(y\) is the vector of stage values, \(u^n\) is the previous step solution, \(e\) is a vector with all entries equal to 1, \(h\) is the step size, \(A\) and \(b\) are the coefficients in the Butcher tableau, and \(F(y)\) is the vector of stage derivatives. In floating point arithmetic, roundoff errors will be made at each stage. Representing these errors by a vector \(r\), we have
Considering the test problem \(F(y)=\lambda y\) and solving for \(y\) gives
where \(z=h\lambda\). Substituting this result in the equation for \(u^{n+1}\) gives
Here \(\psi(z)\) is the stability function of the method, that we already encountered above. Meanwhile, the vector \(\theta(z)\) contains the internal stability functions that govern the amplification of roundoff errors \(r\) within a step:
Let’s compute \(\theta\) for the classical RK4 method:
theta=rk4.internal_stability_polynomials()
theta
[poly1d([1/24, 1/12, 1/6, 1/6, 0], dtype=object), poly1d([1/12, 1/6, 1/3, 0], dtype=object), poly1d([1/6, 1/3, 0], dtype=object), poly1d([1/6, 0], dtype=object)]
for theta_j in theta:
print theta_j
4 3 2
0.04167 x + 0.08333 x + 0.1667 x + 0.1667 x
3 2
0.08333 x + 0.1667 x + 0.3333 x
2
0.1667 x + 0.3333 x
0.1667 x
Thus the roundoff errors in the first stage are amplified by a factor \(z^4/24 + z^3/12 + z^2/6 + z/6\), while the errors in the last stage are amplified by a factor \(z/6\).
Usually internal stability is unimportant since it relates to amplification of roundoff errors, which are very small. Let’s think about when things can go wrong in terms of internal instability. If \(|\theta(z)|\) is of the order \(1/\epsilon_{machine}\), then roundoff errors could be amplified so much that they destroy the accuracy of the computation. More specifically, we should be concerned if \(|\theta(z)|\) is of the order \(tol/\epsilon_{machine}\) where \(tol\) is our desired error tolerance. Of course, we only care about values of \(z\) that lie inside the absolute stability region \(S\), since internal stability won’t matter if the computation is not absolutely stable.
We can get some idea about the amplification of stage errors by plotting the curves \(|\theta(z)|=1\) along with the stability region. Ideally these curves will all lie outside the stability region, so that all stage errors are damped.
rk4.internal_stability_plot()
ssprk4.internal_stability_plot()
For both methods, we see that some of the curves intersect the absolute stability region, so some stage errors are amplified. But by how much? We’d really like to know the maximum amplification of the stage errors under the condition of absolute stability. We therefore define the maximum internal amplification factor \(M\):
print rk4.maximum_internal_amplification()
print ssprk4.maximum_internal_amplification()
2.15239281554
4.04399941143
We see that both methods have small internal amplification factors, so internal stability is not a concern in either case. This is not surprising for the method with only four stages; it is a surprisingly good property of the method with ten stages.
Questions: Do SSP RK methods always (necessarily) have small amplification factors? Can we prove it?
Now let’s look at some methods with many stages.
The paper of Verwer, Hundsdorfer, and Sommeijer deals with RKC
methods, which can have very many stages. The construction of these
methods is implemented in NodePy, so let’s take a look at them. The
functions RKC1(s)
and RKC2(s)
construct RKC
methods of order 1 and 2, respectively, with \(s\) stages.
s=4
rkc = rk.RKC1(s)
print rkc
RKC41
0 |
1/16 | 1/16
1/4 | 1/8 1/8
9/16 | 3/16 1/4 1/8
______|________________________
| 1/4 3/8 1/4 1/8
rkc.internal_stability_plot()
It looks like there could be some significant internal amplification here. Let’s see:
rkc.maximum_internal_amplification()
11.760869405962685
Nothing catastrophic. Let’s try a larger value of \(s\):
s=20
rkc = rk.RKC1(s)
rkc.maximum_internal_amplification()
42.665327220219126
As promised, these methods seem to have good internal stability properties. What about the second-order methods?
s=20
rkc = rk.RKC2(s)
rkc.maximum_internal_amplification()
106.69110992619214
Again, nothing catastrophic. We could take \(s\) much larger than 20, but the calculations get to be rather slow (in Python) and since we’re using floating point arithmetic, the accuracy deteriorates.
Remark: we could do the calculations in exact arithmetic using Sympy, but things would get even slower. Perhaps there are some optimizations that could be done to speed this up. Or perhaps we should use Mathematica if we need to do this kind of thing.
Remark 2: of course, for the RKC methods the internal stability polynomials are shifted Chebyshev polynomials. So we could evaluate them directly in a stable manner using the three-term recurrence (or perhaps scipy’s special functions library). This would also be a nice check on the calculations above.
Three other classes of methods with many stages have been implemented in NodePy:
s=20
ssprk = rk.SSPRK2(s)
ssprk.internal_stability_plot()
ssprk.maximum_internal_amplification()
2.0212921484995547
s=25 # # of stages
ssprk = rk.SSPRK3(s)
ssprk.internal_stability_plot()
ssprk.maximum_internal_amplification()
3.8049237837215397
The SSP methods seem to have excellent internal stability properties.
p=6 #order
idc = rk.DC(p-1)
print len(idc)
idc.internal_stability_plot()
idc.maximum_internal_amplification()
26
6.4140166271998815
IDC methods also seem to have excellent internal stability.
p=6 #order
ex = rk.extrap(p)
print len(ex)
ex.internal_stability_plot()
ex.maximum_internal_amplification()
16
6
Not so good. Let’s try a method with even more stages (this next computation will take a while; go stretch your legs).
p=10 #order
ex = rk.extrap(p)
print len(ex)
ex.maximum_internal_amplification()
46
28073.244376758907
Now we’re starting to see something that might cause trouble, especially since such high order extrapolation methods are usually used when extremely tight error tolerances are required. Internal amplification will cause a loss of about 5 digits of accuracy here, so the best we can hope for is about 10 digits of accuracy in double precision. Higher order extrapolation methods will make things even worse. How large are their amplification factors? (Really long calculation here…)
pmax = 12
ampfac = np.zeros(pmax+1)
for p in range(1,pmax+1):
ex = rk.extrap(p)
ampfac[p] = ex.maximum_internal_amplification()
print p, ampfac[p]
1 1.99777378912
2 2.40329384375
3
5.07204078733
4
17.747335803
5
69.62805786
6
97.6097450835
7
346.277441462
8
1467.40356089
9
6344.16303534
10
28073.2443768
11
126011.586473
12
169897.662582
[]
semilogy(ampfac,linewidth=3)
[]
We see roughly geometric growth of the internal amplification factor as a function of the order \(p\). It seems clear that very high order extrapolation methods applied to problems with high accuracy requirements will fall victim to internal stability issues.