Arithmetics with floating point numbers is not exact, causing acumulation of numerical error.
In modern computers, the floating point is presented as Sign ∗ Mantisa ∗ Exponent
.
The largest and the smallest floating point number depends on the type. Most often we will
use python float
, which needs 8bytes=64bits
and can store numbers between 2.22507e-308 to
1.79769e+308. It is composed of roughly: 9-bits exponent, 54-bits mantisa, 1-bit sign.
The overflow error occurs if we want to store x>1.7976910308 and underflow when
x<2.2250710−308. These numbers are sufficiently large/small that usually do not cause problems.
If they do, we should probably work with logarithms of the numbers (log
representation).
The roundoff error is the hardest problem to avoid, which occurs when : 1+ϵ==1.
For 64-bit float
it occurs around (only!) 210−16.
for i in range(1,10):
x=1+i*1e-16-1
print('1 + '+str(i)+'e-6 -1 = ', x)
1 + 1e-6 -1 = 0.0 1 + 2e-6 -1 = 2.220446049250313e-16 1 + 3e-6 -1 = 2.220446049250313e-16 1 + 4e-6 -1 = 4.440892098500626e-16 1 + 5e-6 -1 = 4.440892098500626e-16 1 + 6e-6 -1 = 6.661338147750939e-16 1 + 7e-6 -1 = 6.661338147750939e-16 1 + 8e-6 -1 = 8.881784197001252e-16 1 + 9e-6 -1 = 8.881784197001252e-16
Many numerical methods rely on iterations, which cause so called error accumulation, and just within a few steps the numerical error makes result useless.
Typical example is the three term linear recurrence relation, xn+1=anxn+bnxn−1, which can very quickly become unstable. Here an and bn are some real or complex numbers. The three points recurrence relation is related to the finite difference equation for the general second order differential equation. But is very commonly used to obtain special functions, such as Bessel functions, associated Legendre functions, regular Coulomb wave functions, etc.
For constant coefficient, an=a and bn=b, the exact solution can be found by the zeros of the characteristic polynomial. More specifically, we search for the solution in the form an=Crn, which gives rn+1=arn+brn−1 which is solved by characteristic polynomial of the form r2−ar−b=0. For n-point recurence relation we would have characteristic polynomial of the n−1 power. For the three points recurence relation, we have thus two solutions r1,2=12a±12√a2+4b. The generic solution is than of the form an=C1rn1+C2rn2. If a2+4b=0, and the characteristic polynomial has only one solution, the solution of the tree-point recurrence is instead: an=C1rn1+C2nrn1.
Even when coefficients are not constant, the n-term linear recurrence relation has exactly n−1 solutions. They can not be found analytically or exactly, but we know that they exist. However, only one solution out of n−1 is easy to follow, namely, the one that grows the fastest in the direction of recurence.
For three-point recurence relation, we have two solutions: one is typically increasing and one is decreasing.
Hence for three point recurrence relation, this instability can be easily fixed by so-called Miller’s algorithm. If iteration in one direction is unstable, it is likely stable in the opposite direction.
Examples of three point recurrence relation include:
Bessel Functinons jl+1(x)=2l+1xjl(x)−jl−1(x) Modified Bessel Functions jl+1(x)=−2l+1xjl(x)+jl−1(x) Legendre Polynomials (n+1)Pn+1(x)=(2n+1)xPn(x)−nPn−1(x) Associated Legendre polynomials (l−m+1)Pml+1(x)=(2l+1)xPml(x)−(l+m)Pml−1 2mxPml(x)=−√1−x2[Pm+1l(x)+(l+m)(l−m+1)Pm−1l(x)] in terms of them the spherical harmonics are given Yl,m(θ,ϕ)=√(2l+1)(l−m)!4π(l+m)!Pml(cosθ)eimϕ, which are used to solve the 3D partial differential equation of the form ∇2ψ(θ,ϕ)+λψ(θ,ϕ)=0. Confluent Hypergeometric Series (b−n)Mn−1+(2n−b+x)Mn−nMn+1=0 with solution Mn=M(n,b;x).
The spherica Bessel functions satisfy the two point recurrence relation:
jl+1(x)=2l+1xjl(x)−jl−1(x)with the initial condition j0(x)=sinxx and j1(x)=sinxx2−cosxx
This recurrence relation should be sufficient to compute jl(x) for any l and any x. However, as we will see below, the numeric iteration becomes extremely unstable for x≲.
Any three term linear recurrence relation must have exactly two solutions. In case of Bessel functions j_l(x), we also have Neumann functions n_l(x), which satisfy exactly the same recurrence relation, but have different initial condition. If n_l(x) is much larger than j_l(x) the iteration will very quickly start to follow n_l(x) even though we started with j_l(x). As is well known n_l(x) are diverging at small x while j_l(x)\propto x^l are very small. Obviously small x and large l are challenging for this algorithm.
To refresh our memory, spherical Bessel functions are solutions of the radial Schroedinger equation, as well as the Electromagnetic waves in a cylindrical symmetry, heat conduction in a cylindrical object, and many other applications.
They are the solution of the following differential equation:
\left[-\frac{1}{2}\frac{d^2}{dr^2}+\frac{l(l+1)}{2 r^2}\right]\left(r j_l(r)\right) = E \left(r j_l(r)\right)We will first iterate the recurrence relation for j_l(x) starting at l=0. This is called upward recurrence. If this process proves to be unstable, we will demonstrate that downward recurrence becomes stable and serves as the solution to this problem.
We will evaluate bessel upward recurrence using the formula
\begin{equation} j_{l+1}(x) = \frac{2l+1}{x} j_l - j_{l-1} \end{equation}from scipy import *
from numpy import *
def bessel_upward(l,x):
"returns array of j_i from i=0 to i=l, including l"
res = zeros(l+1)
if abs(x)<1e-30: # first take care of the special case, which is numerically hard.
res[0]=1.
return res
j0 = sin(x)/x
res[0]=j0
if l==0: return res
j1 = j0/x - cos(x)/x
res[1] = j1
for i in range(1,l):
j2 = (2*i+1)/x*j1 - j0 # (j2,j1,j0)==(j_{i+1},j_i,j_{i-1})
res[i+1]=j2 # store j_{i+1}
j0,j1 = j1,j2 # prepare for the next step (j_{i-1},j_i) <- (j_i,j_{i+1})
return res
from scipy import special
l=10
x=0.1
dat0 = bessel_upward(l,x)
dat1 = special.spherical_jn(range(l+1),x)
diff = dat0-dat1
print("%4s %16s %16s %16s %16s" % ('l','exact','upward','abs-error','rel-error'))
for i in range(len(dat0)):
print("%4d %16.13f %16.13f %16.13f %16.8g" % (i,dat1[i],dat0[i],diff[i], diff[i]/dat1[i]))
l exact upward abs-error rel-error 0 0.9983341664683 0.9983341664683 0.0000000000000 0 1 0.0333000119026 0.0333000119026 0.0000000000000 2.9172516e-15 2 0.0006661906084 0.0006661906084 0.0000000000000 6.4359747e-12 3 0.0000095185197 0.0000095185199 0.0000000000002 2.2510964e-08 4 0.0000001057720 0.0000001057870 0.0000000000150 0.00014176421 5 0.0000000009616 0.0000000023109 0.0000000013493 1.4031448 6 0.0000000000074 0.0000001484162 0.0000001484088 20061.914 7 0.0000000000000 0.0000192917991 0.0000192917990 3.9116462e+08 8 0.0000000000000 0.0028936214469 0.0028936214469 9.9738775e+12 9 0.0000000000000 0.4918963541799 0.4918963541799 3.2213554e+17 10 0.0000000000000 93.4574136727252 93.4574136727252 1.2852544e+22
What happens with upward recurrence?
x=0.1
j9 = special.spherical_jn(9,x)
j10 = special.spherical_jn(10,x)
j11 = special.spherical_jn(11,x)
j11a = (2*10+1)/x * j10 - j9 # upward
j9a = (2*10+1)/x * j10 - j11 # downward
print('part1 upward or downward=', (2*10+1)/x*j10)
print('part2 upward=', -j9)
print('part2 downward=', -j11)
print('j9=', j9a, 'j9_exact=', j9, 'abs-err=', j9a-j9, 'rel-err=', (j9a-j9)/j9)
print('j11=', j11a, 'j11_exact=',j11, 'abs-err=', j11a-j11, 'rel-err=', (j11a-j11)/j11)
part1 upward or downward= 1.5270173093098784e-18 part2 upward= -1.5269856934948229e-18 part2 downward= -3.16158150515107e-23 j9= 1.526985693494827e-18 j9_exact= 1.5269856934948229e-18 abs-err= 4.044452883213195e-33 rel-err= 2.648651457864433e-15 j11= 3.161581505555956e-23 j11_exact= 3.16158150515107e-23 abs-err= 4.048855109557025e-33 rel-err= 1.280642331364966e-10
Now we will use recurrence:
\begin{eqnarray} j_{l-1} = (2l+1)/x j_l - j_{l+1} \end{eqnarray}Because the recurrence relation is linear and homogeneous, we are allowed to multiply all accumulated values j_l with an arbitrary constant, and they will still represent solution of the same recurrence relation. We can use this to rescale all values j_l. We know j_0=\frac{\sin(x)}{x}, hence we can use this to rescale all j_l in the series.
We can start with j_{l_{max}+1}=0 and j_{l_{max}}=1 as we know that the values of j_l fall of quickly, and we are allowed to make substantial error at large enough l.
def bessel_downward(l,x):
"downward recursion"
if abs(x)<1e-20: # again take care of the special case where the algorithm would be unstable.
res = zeros(l+1)
res[0]=1
return res
lstart = l + int(sqrt(10*l)) # this is a reasonable l_max
j2 = 0.
j1 = 1.
res = [] # [j_l,j_{l-1},.....j_0]
for i in range(lstart,0,-1):
j0 = (2*i+1)/x * j1 - j2
if i-1<=l : res.append(j0)
j2,j1 = j1,j0
res.reverse() # reverse the list: [j0,j1,....j_l]
true_j0 = sin(x)/x
res = array(res) * true_j0/res[0]
return res
l=10
x=0.1
dat0 = bessel_downward(l,x)
dat1 = special.spherical_jn(range(l+1),x)
diff=dat0-dat1
print("%4s %16s %16s %16s %16s" % ('l','exact','downward','abs-error','rel-error'))
for i in range(len(dat0)):
print("%4d %16.13f %16.13f %16.13f %16.8g" % (i,dat1[i],dat0[i],diff[i], diff[i]/dat1[i]))
l exact downward abs-error rel-error 0 0.9983341664683 0.9983341664683 0.0000000000000 0 1 0.0333000119026 0.0333000119026 -0.0000000000000 -1.4586258e-15 2 0.0006661906084 0.0006661906084 -0.0000000000000 -9.7647925e-16 3 0.0000095185197 0.0000095185197 -0.0000000000000 -1.6017819e-15 4 0.0000001057720 0.0000001057720 -0.0000000000000 -2.7527846e-15 5 0.0000000009616 0.0000000009616 -0.0000000000000 -2.1504626e-16 6 0.0000000000074 0.0000000000074 0.0000000000000 8.7358062e-16 7 0.0000000000000 0.0000000000000 -0.0000000000000 -1.6634916e-15 8 0.0000000000000 0.0000000000000 -0.0000000000000 -1.1895996e-15 9 0.0000000000000 0.0000000000000 -0.0000000000000 -1.8918939e-15 10 0.0000000000000 0.0000000000000 -0.0000000000000 -4.5522754e-15
def bessel_j(l,x):
"combines upward and downward recursion"
if l<=x : return bessel_upward(l,x)
# If x is largel we only need upward recursion. However, when x is small, we need both the
# upward and downward. The upward for l<x and downward for l>x.
lcritical = int(x)
if lcritical<=0 : return bessel_downward(l,x) # for very small x we only need downward recursion
_ju_ = bessel_upward(lcritical-1,x) # upward works
_jd_ = bessel_downward(l,x)
return hstack( (_ju_, _jd_[lcritical:]) )
l=10
x=0.1
dat0 = bessel_upward(l,x)
dat1 = bessel_downward(l,x)
dat2 = bessel_j(l,x)
date = special.spherical_jn(range(l+1),x)
diff = date-dat2
#print('difference=', date-dat2)
#print('updard-diff=', date-dat0)
#print('down-diff=', date-dat1)
print("%4s %16s %16s %16s %16s" % ('l','exact','combined','abs-error','rel-error'))
for i in range(len(dat0)):
print("%4d %16.13f %16.13f %16.13f %16.8g" % (i, date[i], dat2[i], diff[i], diff[i]/date[i]))
l exact combined abs-error rel-error 0 0.9983341664683 0.9983341664683 0.0000000000000 0 1 0.0333000119026 0.0333000119026 0.0000000000000 1.4586258e-15 2 0.0006661906084 0.0006661906084 0.0000000000000 9.7647925e-16 3 0.0000095185197 0.0000095185197 0.0000000000000 1.6017819e-15 4 0.0000001057720 0.0000001057720 0.0000000000000 2.7527846e-15 5 0.0000000009616 0.0000000009616 0.0000000000000 2.1504626e-16 6 0.0000000000074 0.0000000000074 -0.0000000000000 -8.7358062e-16 7 0.0000000000000 0.0000000000000 0.0000000000000 1.6634916e-15 8 0.0000000000000 0.0000000000000 0.0000000000000 1.1895996e-15 9 0.0000000000000 0.0000000000000 0.0000000000000 1.8918939e-15 10 0.0000000000000 0.0000000000000 0.0000000000000 4.5522754e-15
from pylab import *
%matplotlib inline
l=15
x = linspace(1e-6,50,100)
dat0 = array([bessel_upward(l,t) for t in x])
dat1 = array([bessel_downward(l,t) for t in x])
dat2 = array([bessel_j(l,t) for t in x])
date = array([special.spherical_jn(range(l+1),t) for t in x])
for i in range(5,l,3):
semilogy(x, abs((dat2-date)/date))
title('combination of up and down recursion')
ylim([1e-20,1e-11])
show()
for i in range(5,l,3):
semilogy(x, abs((dat1-date)/date))
title('downward recursion')
show()
for i in range(5,l,3):
semilogy(x, abs((dat0-date)/date))
title('upward recursion')
show()
We want to compute the series of integrals, defined by K_n(z,\alpha) = \int_0^1 dx \frac{x^n}{z+\alpha x} when n=0,1,...n_{max}=10.
These occur, for example, when we evaluate matrix elements of the following correlation function <P_n(x)|G_(x)|P_m(x)>, where P_n(x) is Legendre polynomial, and G(x)=\frac{1}{z-x} is the Green's function.
We can derive recurrence relation by noting K_{n+1}(z,\alpha) = \int_0^1 dx \frac{x^n(x+z/\alpha) - x^n z/\alpha}{z+\alpha x}= \frac{1}{\alpha} \int_0^1 dx x^n -\frac{z}{\alpha}\int_0^1 dx \frac{x^n}{z+\alpha x} which gives K_{n+1} = \frac{1}{\alpha(n+1)}-\frac{z}{\alpha} K_n
We can also calculate K_0: K_0=\frac{1}{\alpha} \log(1+\alpha/z)
Starting for K_0 you can compute K_n up to n_{max} using recurrence. This works quite well for |\alpha/z|\gtrapprox 1.
Choosing z and \alpha so that |\alpha/z|\ll 1 (for example \alpha/z=10^{-4}) verify that upward recurrence is unstable.
Implement downward recurrence for \alpha/z<1/2. Since recurrence relation is not homogeneous, we can not start with arbitrary value and later normalize result. We thus need to start with very accurate value of K_{n_{max}}. Use power series expansion for K_{n_{max}} in powers of (\alpha/z)^k, and evaluate as many terms as needed to achieve desired accuracy (say 10^{-12}).
We can derive the power series in \alpha/z by Taylor expansion: K_n(z,\alpha) = \frac{1}{z}\int_0^1 dx \frac{x^n}{1+\alpha/z x}= \frac{1}{z} \sum_{k=0}^{\infty}(-\alpha/z)^k \int_0^1 dx x^{n+k}= \frac{1}{z} \sum_{k=0}^{\infty}\frac{(-\alpha/z)^k}{n+k+1} For n large and |\alpha/z|<1/2 this series is is converging well.
The first order recurrence relation can be solved analytically by the algorithm described in wikipedia:Recurrence relation
The solution is:
K_n = \left(-\frac{z}{\alpha}\right)^n\left[K_0 + \frac{1}{\alpha}\sum_{k=1}^{n} \frac{\left(-\frac{\alpha}{z}\right)^k}{k}\right]By noting that Taylor expansion of \log(1+x)=-\sum_{k=1}^{\infty} \frac{(-x)^k}{k}, we see that K_0=-\frac{1}{\alpha}\sum_{k=1}^\infty \frac{\left(-\frac{\alpha}{z}\right)^k}{k} and inserting this expression into previous formula for K_n gives Taylor expansion that we derived for small \alpha/z above.
You can use this expression to check your implementation of recurrence relation.