E10.2Numerical stability of an integral solved by recursion
The integral
suggests a recursion relation obtained by integration by parts:
terminating with \(I_0 = e-1\). However, this algorithm, applied "forwards" for increasing \(n\) is numerically unstable since small errors (such as floating point rounding errors) are magnified at each step: if the error in \(I_n\) is \(\epsilon_n\) such that the estimated value of \(I_n\) is \(I_n' + \epsilon_n\) then
and hence \(|\epsilon_n| = n!\epsilon_{0}\). Even if the error in \(\epsilon_0\) is small, that in \(\epsilon_n\) is larger by a factor \(n!\) which can be huge.
The numerically stable solution, in this case, is to apply the recursion backwards for decreasing \(n\):
That is, errors in \(I_n\) are reduced on each step of the recursion. One can even start the algorithm at \(I'_N = 0\) and providing enough steps are taken between \(N\) and the desired \(n\) it will converge on the correct \(I_n\).
import numpy as np
import matplotlib.pyplot as plt
def Iforward(n):
if n == 0:
return np.e - 1
return np.e - n * Iforward(n - 1)
def Ibackward(n):
if n >= 99:
return 0
return (np.e - Ibackward(n + 1)) / (n + 1)
N = 35
Iforward = [np.e - 1]
for n in range(1, N + 1):
Iforward.append(np.e - n * Iforward[n - 1])
Ibackward = [0] * (N + 1)
for n in range(N - 1, -1, -1):
Ibackward[n] = (np.e - Ibackward[n + 1]) / (n + 1)
n = range(N + 1)
plt.plot(n, Iforward, label="Forward algorithm")
plt.plot(n, Ibackward, label="Backward algorithm")
plt.ylim(-0.5, 2)
plt.xlabel("$n$")
plt.ylabel("$I(n)$")
plt.legend()
plt.show()
The figure below shows the forwards algorithm becoming extremely unstable for \(n>16\) and fluctuating between very large positive and negative values; conversely, the backwards algorithm is well-behaved.