Numerov's Method
Differential Equations
Background
The Numerov method can be used for functions of the form
Steps
- Taylor expand the general differential equation
- Apply to discrete space
- Use forward and backward step to cancel odd terms
- Do some mathematical magic to get it in "nice" terms
Begin by using the formula for a Taylor expansion.
Set and leaving us with:
We now discretize the space using the following rule . If we plug in we get and consequently . This means we can rewrite as :
Where we adopt the shorthand that , giving us:
From this we make the observation that:
Similarly, if we want to take a step backwards we can replace every with :
The odd terms are the only ones that change sign. Next, we can add the two expressions together and rearranging:
From the statement of the problem , we have . So in continuous variables we have:
and in discrete variables we have:
Remember that the second derivative can be approximated as a finite difference scheme:
Let , , and . Now plug these terms into the finite difference scheme:
Converting to discrete space:
Now that we have an expression for we can substitute this back into:
leaving the following equation:
Cancelling :
This is a very unwieldy expression so let's try to see if we can pull together some common terms with subscript , and . The right side has the same form as the finite difference formula, we can probably match these terms up with the :
The first bracketed term
becomes
The second bracketed term
becomes
and finally
becomes
Each of these similar terms can be absorbed into a new variable :
simplifying our unwieldy expression to:
Substituting , we arrive at our final answer:
We need to remember that:
Practical Considerations
To implement the method there are a few practical considerations, for example:
- How do we start the calculation method without knowing the value of the first step?
We take two small values and set them equal to and , let and . Then:
So now we have taken the first two steps and can use this formula:
for the case .
In short, you need to give the Numerov method the first and second step, the first step can be zero and the second step should be a small positive number.
Simplified Numerov
Numerov's Method can be simplified for differential equations where , giving:
The term simply loses the terms.
Generalized Numerov
Note: these routines were taken directly from Kristjan Haule's LAPW code
Numerov's method presumes that the differential equation is of the form:
Which is not too far off from the infinite square well hamiltonian, with the function actually being a constant :
Feel free to mess with the "constant" to change the result. Note that this is not a valid wavefunction because it is not normalized.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
def set_publication_style():
"""Set publication-quality matplotlib style."""
mpl.rcParams.update({
'font.family': 'serif',
'font.size': 12,
'axes.labelsize': 14,
'axes.titlesize': 16,
'axes.linewidth': 1.2,
'axes.labelpad': 8,
'axes.titlepad': 10,
'xtick.labelsize': 12,
'ytick.labelsize': 12,
'xtick.direction': 'in',
'ytick.direction': 'in',
'xtick.top': True,
'ytick.right': True,
'xtick.major.size': 6,
'ytick.major.size': 6,
'xtick.major.width': 1.2,
'ytick.major.width': 1.2,
'legend.fontsize': 12,
'legend.frameon': False,
'lines.linewidth': 2,
'lines.markersize': 6,
'figure.dpi': 100,
'savefig.dpi': 300,
'savefig.bbox': 'tight'
})
set_publication_style()
# Simplified Numerov (for s(x) = 0)
def Numerov(F, dx, f0=0.0, f1=1e-3):
Nmax = len(F)
dx = float(dx)
Solution = np.zeros(Nmax, dtype=float)
Solution[0] = f0
Solution[1] = f1
h2 = dx*dx;
h12 = h2/12;
w0 = (1-h12*F[0])*Solution[0];
Fx = F[1];
w1 = (1-h12*Fx)*Solution[1];
Phi = Solution[1];
w2 = 0.0
for i in range(2, Nmax):
w2 = 2*w1 - w0 + h2*Phi*Fx;
w0 = w1;
w1 = w2;
Fx = F[i];
Phi = w2/(1-h12*Fx);
Solution[i] = Phi;
return Solution
# Generalized Numerov (for s(x) != 0)
def NumerovGen(F, U, dx, f0=0.0, f1=1e-3):
Nmax = len(F)
dx = float(dx)
Solution = np.zeros(Nmax, dtype=float)
Solution[0] = f0
Solution[1] = f1
h2 = dx * dx
h12 = h2 / 12
w0 = Solution[0] * (1 - h12 * F[0]) - h12 * U[0]
w1 = Solution[1] * (1 - h12 * F[1]) - h12 * U[1]
Phi = Solution[1]
for i in range(2, Nmax):
Fx = F[i]
Ux = U[i]
w2 = 2 * w1 - w0 + h2 * (Phi * Fx + Ux)
w0 = w1
w1 = w2
Phi = (w2 + h12 * Ux) / (1 - h12 * Fx)
Solution[i] = Phi
return Solution
# Example usage: Infinite square well
constant = -10.0
x = np.linspace(0,1,100)
isw_rhs = constant*np.ones(len(x))
sol = Numerov(isw_rhs, x[1]-x[0], 0.00, 0.0001)
plt.plot(x, sol)
plt.savefig('figures/numerov_solution.png', dpi=300, bbox_inches='tight')
plt.show() Visualization
The following plot shows the solution obtained using Numerov's method:
C++ Implementation
The following C++ code implements both standard and generalized Numerov methods:
#include <vector>
#include <cmath>
#include <iostream>
// Helper function: linspace
std::vector<double> linspace(double start, double end, size_t num) {
std::vector<double> result(num);
double dx = (end - start) / (num - 1);
for (size_t i = 0; i < num; ++i) {
result[i] = start + i * dx;
}
return result;
}
// Coulomb potential
double coulombPotential(double r, double Z) {
if (r < 1e-10) return 0.0; // Avoid singularity
return -Z / r;
}
/* =======================
Standard Numerov
======================= */
std::vector<double>
Numerov(const std::vector<double>& F,
double dx,
double f0,
double f1)
{
const std::size_t Nmax = F.size();
std::vector<double> Solution(Nmax, 0.0);
Solution[0] = f0;
Solution[1] = f1;
const double h2 = dx * dx;
const double h12 = h2 / 12.0;
double w0 = (1.0 - h12 * F[0]) * Solution[0];
double Fx = F[1];
double w1 = (1.0 - h12 * Fx) * Solution[1];
double Phi = Solution[1];
for (std::size_t i = 2; i < Nmax; ++i)
{
double w2 = 2.0 * w1 - w0 + h2 * Phi * Fx;
w0 = w1;
w1 = w2;
Fx = F[i];
Phi = w2 / (1.0 - h12 * Fx);
Solution[i] = Phi;
}
return Solution;
}
/* =======================
Generalized Numerov
======================= */
std::vector<double>
NumerovGen(const std::vector<double>& F,
const std::vector<double>& U,
double dx,
double f0,
double f1)
{
const std::size_t Nmax = F.size();
std::vector<double> Solution(Nmax, 0.0);
Solution[0] = f0;
Solution[1] = f1;
const double h2 = dx * dx;
const double h12 = h2 / 12.0;
double w0 = Solution[0] * (1.0 - h12 * F[0]) - h12 * U[0];
double w1 = Solution[1] * (1.0 - h12 * F[1]) - h12 * U[1];
double Phi = Solution[1];
for (std::size_t i = 2; i < Nmax; ++i)
{
const double Fx = F[i];
const double Ux = U[i];
double w2 = 2.0 * w1 - w0 + h2 * (Phi * Fx + Ux);
w0 = w1;
w1 = w2;
Phi = (w2 + h12 * Ux) / (1.0 - h12 * Fx);
Solution[i] = Phi;
}
return Solution;
}
// Radial Schrödinger equation RHS
std::vector<double> radialRHS(
double E,
double l,
const std::vector<double>& R,
const std::vector<double>& Veff
) {
const std::size_t N = R.size();
std::vector<double> RHS(N, 0.0);
for (std::size_t i = 0; i < N; ++i) {
double r = R[i];
if (r < 1e-10) r = 1e-10; // Avoid division by zero
RHS[i] = 2.0 * (-E + 0.5 * l * (l + 1.0) / (r * r) + Veff[i]);
}
return RHS;
}
// Example usage for radial Schrödinger equation
int main() {
std::vector<double> r = linspace(0.01, 20.0, 10000); // Start slightly away from origin
std::vector<double> Veff(r.size());
// Set up potential (e.g., Coulomb potential for hydrogen)
for (size_t i = 0; i < Veff.size(); i++) {
Veff[i] = coulombPotential(r[i], 1.0); // Z = 1
}
// Compute RHS for given energy E and angular momentum l
double E = -0.5; // Energy in atomic units
double l = 0.0; // Angular momentum quantum number
std::vector<double> rhs = radialRHS(E, l, r, Veff);
// Solve using Numerov
double dx = r[1] - r[0];
std::vector<double> solution = Numerov(rhs, dx, 0.0, 1e-3);
// Print some results
std::cout << "Solution at r=0.01: " << solution[0] << std::endl;
std::cout << "Solution at r=1.0: " << solution[100] << std::endl;
std::cout << "Solution at r=10.0: " << solution[5000] << std::endl;
return 0;
}