“Know how to solve every problem that has been solved.” “What I cannot create, I do not understand.” — Richard Feynman

Numerov's Method

Differential Equations

What you need to know first 2 concepts, 2 layers

The requisite-knowledge inventory for this page, bottom-up: the primitives at the base, combined upward until you reach what this page assumes. Skim the layers you already own; start wherever the ground gets unfamiliar.

  1. base
  2. L1
  3. you are here

Background

The Numerov method can be used for functions of the form

Steps

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:

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:

Numerov Method Solution

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;
}