Numerov’s method
Numerov’s method1 (also called Cowell’s method) is a numerical method to solve ordinary differential equations of second order in which the first-order term does not appear. It is a fourth-order linear multistep method. The method is implicit, but can be made explicit if the differential equation is linear. Numerov’s method was developed by the Russian astronomer Boris Vasil’evich Numerov.
Given the differential equation of this form
where
Similarly, we have
By summing the two equations, we have
To get an expression for
If we substitute this into the proceeding equation, we get
or equivalently
This yields the Numerov’s method if we ignore the term of order
where
With this relation, we can now find the value of
Case 1. Hydrogen-like atom
The Schrödinger equation for hydrogen-like atom is 2
Here, we assume that there is only one electron and thus neglecting the Coulomb
interaction between the electrons. By variable separation, the wavefunction can
be written as
Introducing the dimensionless variables
where
The equation for
The boundary conditions are
1. From known energy eigenvalue to the wavefunctions
As is well known that the energy levels of hydrogen-like atoms are
Let’s first show that given the correct energy levels, we can obtain the corresponding wavefunctions.
CLICK TO SHOW THE CODE!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#!/usr/bin/env python
import numpy as np
from scipy.integrate import odeint, simps
def radial_schrod_deriv(u, r, l, E, Z=1):
'''
The radial Schrödinger equation for hydrogen-like atom is
u''(x) - [(l(l+1)/x^2) - (2Z/x) - E]u(x) = 0
Since it is a second-order equation, i.e.
y'' + g(x)y(x) = 0
To use scipy.integrate.odeint, we can turn this equation into two
first-order equation by defining a new dependent variables
y'(x) = z(x)
z'(x) = -g(x)y(x)
Then we can solve this system of ODEs using "odeint" wit list.
'''
y, z = u
return np.array([
z,
((l*(l+1) / r**2) - (2*Z / r) - E) * y
])
def radial_wfc_scipy(r0, n=1, l=0, Z=1, direction='F', du=0.1):
'''
Get the radial wavefunction by integrating the equation with
scipy.integrate.odeint.
'''
assert direction.upper() in ['F', 'B']
E = -Z**2 / n**2
# forward integration
if direction.upper() == 'F':
ur = odeint(radial_schrod_deriv, [0.0, du], r0, args=(l, E, Z))[:,0]
# back integration
else:
ur = odeint(radial_schrod_deriv, [0.0, -du], r0[::-1], args=(l, E, Z))[:,0][::-1]
# normalization
ur /= np.sqrt(simps(ur**2, x=r0))
return ur
def radial_wfc_numerov(r0, n=1, l=0, Z=1, du=0.001):
'''
Numerov algorithm
[12 - 10f(n)]*y(n) - y(n-1)*f(n-1)
y(n+1) = ------------------------------------
f(n+1)
where
f(n) = 1 + (h**2 / 12)*g(n)
g(n) = [E + (2*Z / x) - l*(l+1) / x**2]
'''
ur = np.zeros(r0.size)
fn = np.zeros(r0.size)
E = -Z**2 / n**2
ur[-1] = 0.0
ur[-2] = du
dr = r0[1] - r0[0]
h12 = dr**2 / 12.
gn = (E + 2*Z / r0 - l*(l+1) / r0**2)
fn = 1. + h12 * gn
for ii in range(r0.size - 3, -1, -1):
ur[ii] = (12 - 10*fn[ii+1]) * ur[ii+1] - \
ur[ii+2] * fn[ii+2]
ur[ii] /= fn[ii]
# normalization
ur /= np.sqrt(simps(ur**2, x=r0))
return ur
In the code above, we have defined two integration method to integrate the radial Schrödinger equation
-
radial_wfc_scipy
usesodeint
implemented inscipy
.3 -
radial_wfc_numerov
uses the Numerov’s method. -
We implemented forward/backward integration scheme in
radial_wfc_scipy
function, where forward integration means integrating from and backward integraton starts from . -
Of course we can not integrate from
, we used a large enough cutoff and assumes .
CLICK TO SHOW THE CODE!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
################################################################################
# Define the radial grid
r0 = np.linspace(1E-10, 20, 500)
Z = 1
l = 0
n = 1
# forward integration from u(0)
ur1 = radial_wfc_scipy(r0, n, l, direction='F')
# backward integration from u(\infty)
ur2 = radial_wfc_scipy(r0, n, l, direction='B')
# backward integration from u(\infty) with Numerov method
ur3 = radial_wfc_numerov(r0, n, l)
################################################################################
import matplotlib as mpl
import matplotlib.pyplot as plt
plt.style.use('ggplot')
fig = plt.figure(
figsize=(7.2, 3.0),
dpi=300
)
axes = [plt.subplot(1,2,ii+1) for ii in range(2)]
################################################################################
ax = axes[0]
ax.plot(
r0, ur1,
ls='none',
ms=4, marker='o', mfc='white', mew=1.0,
zorder=1,
label=r'Forward integration',
)
ax.plot(
r0, ur2,
ls='none',
ms=4, marker='o', mfc='white', mew=1.0,
zorder=1,
label=r'Backward integration',
)
ax.plot(r0, 2*r0*np.exp(-r0), lw=1.0, color='cyan', zorder=2, label=r'Exact: $2r\cdot e^{-r}$')
ax.set_xlabel(r'$r$ [$a_0$]', labelpad=5)
ax.set_ylabel(r'$u(r)$ [a.u.]', labelpad=5)
ylim = list(ax.get_ylim())
ylim[1] = 1.0
ax.set_ylim(ylim)
ax.legend(loc='best', fontsize='small')
#################################################################################
ax = axes[1]
ax.semilogy(
r0, 2*r0*np.exp(-r0) - ur2,
ls='-', alpha=0.8,
zorder=1,
label=r'scipy.integrate.odeint',
)
ax.semilogy(
r0, 2*r0*np.exp(-r0) - ur3,
ls=':', alpha=0.8,
zorder=1,
label=r'Numerov',
)
ax.legend(loc='lower right', fontsize='small')
ax.set_xlabel(r'$r$ [$a_0$]', labelpad=5)
ax.set_ylabel(r'Error', labelpad=5)
plt.tight_layout()
plt.savefig('fig1.png')
plt.show()
We then try to get the radial wavefunction of hydrogen (
-
It turns out that the forward integration of the equation is not stable, as can be seen in the left panel of Figure 1.
-
It is better to use logarithmic mesh for radial variable rather than linear. Radial functions need smaller number of points in logarithmic mesh. For example, use
np.logspace(1E-6, 2, 500)
to generate the radial grid and pass to theradial_wfc_scipy
function. Note that Numerov’s method only support linear mesh, as can be seen in the method section.
One can show that the Numerov’s method also works fine for higher excited
states. For example, below we compare the radial wavefunction
2. Unknown energy levels
If we don NOT know the correct energy levels, we can utilize the boundary condition to get the energy. Recall that the boundary condition is
where
-
Start wite a guess energy, e.g.
. In practice, the guess energy should be smaller than the smallest potential energy. In our case, should be smaller than . With the guess energy , integrate the equation and get the value of the function at , which we will denote as . Meanwhile, Set another energy . -
Increase
by an amount and get a new energy . Integrate the equation to get the value at , which we will denote as . -
Go back to step 2 if
. -
At this step, we should have the correct energy enclosed in
. Use root finding method, e.g.scipy.optimize.brentq
to get the correct energy.
CLICK TO SHOW THE CODE!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#!/usr/bin/env python
import numpy as np
from scipy.integrate import odeint, simps
from scipy.optimize import brentq
def radial_wfc_at0(E, r0, n=1, l=0, Z=1, du=0.01):
'''
Numerov algorithm
[12 - 10f(n)]*y(n) - y(n-1)*f(n-1)
y(n+1) = ------------------------------------
f(n+1)
where
f(n) = 1 + (h**2 / 12)*g(n)
g(n) = [E + (2*Z / x) - l*(l+1) / x**2]
here, we use reverse integration from the other end.
'''
ur = np.zeros_like(r0)
ur[-1] = 0.0
ur[-2] = du
dr = r0[1] - r0[0]
h12 = dr**2 / 12.
gn = E + 2*Z / r0 - l*(l+1) / r0**2
fn = 1. + h12 * gn
for ii in range(gn.size - 3, -1, -1):
ur[ii] = (12 - 10*fn[ii+1]) * ur[ii+1] - \
ur[ii+2] * fn[ii+2]
ur[ii] /= fn[ii]
# normalization
ur /= np.sqrt(simps(ur**2, x=r0))
# now extrapolate the wavefunction to u(0)
# the first derivative equals at ur[0]
# (u0 - ur[0]) / (0 - r0[0]) = (ur[1] - ur[0]) / (r0[1] - r0[0])
u0 = ur[0] + (ur[1] - ur[0]) * (0 - r0[0]) / dr
return u0
if __name__ == "__main__":
r0 = np.linspace(1E-6, 30, 3000)
E_lower = E_upper = -1.1
dE = 0.10
u1 = radial_wfc_at0(E_lower, r0, n=1)
while True:
E_upper += dE
u2 = radial_wfc_at0(E_upper, r0, n=1)
if u1 * u2 < 0:
break
E = brentq(radial_wfc_at0, E_lower, E_upper, args=(r0, 1, 0, 1, 0.001))
print(E)