Gradient Descent For The Schrรถdinger Equation With Python

Photo by Ash from Modern Afflatus on Unsplash

Today Iโ€™ll discuss a numerical method that is widely applicable to problems in quantum mechanics and quantum chemistry, although it is rarely found in textbooks. Suppose we want to solve the stationary Schrรถdinger equation in its most general form

where ๐ป is the Hamiltonian operator, ๐œ“_๐‘› is an eigenvector (wave functions) of the Hamiltonian and ๐ธ_๐‘› the corresponding (energy) eigenvalue. We want to calculate all the different eigenvectors with their energies. In particular, we are interested in the ground state, i.e. the eigenvector with the lowest energy.

The method we discuss here is a variant of gradient descent. But itโ€™s gradient descent in disguise. Consider what happens if we apply ๐ป to some arbitrary wave function ๐œ“_init. We can expand ๐œ“ in terms of the (yet unknown) eigenvectors:

where ๐œ“_0 shall denote the eigenvector with the lowest energy and ๐œ“_๐‘ the one with the highest energy. Then we have

because

the term with the largest eigenvalue gets bigger and bigger in comparison with the other terms. So in the limit, only that term survives and the resulting vector lies completely in the direction of ๐œ“_๐‘.

Now, we donโ€™t want the vector with the largest eigenvalue, but the one with the lowest. How can we achieve that? Easy! We just map ๐ป linearly to some other operator, which has the same eigenvectors, but its eigenvalues are reversed. The simplest choice is

where ๐œ– is some constant to be chosen later. When we apply ๐พ to some eigenvalue of ๐ป, we get

In other words, the eigenvectors of ๐ป are also eigenvectors or ๐พ, but with different eigenvalues. For the H-eigenvector ๐œ“_0 with lowest H-eigenvalue ๐ธ_0 we get

so the corresponding K-eigenvalue is less than 1.

For the H-eigenvector ๐œ“_๐‘ with highest H-eigenvalue ๐ธ_๐‘, we get

and the corresponding value is 1.

So we see that the spectrum has been reversed. Now we use the idea above to iterate to the K-eigenvector with highest K-eigenvalue, and this will be the H-eigenvector with lowest H-eigenvalue, the ground state.

The iteration scheme in detail is like that, where the upper index in braces counts iterations:

2. Iterate

where

and N is the normalizing operation

We normalize in each step so that we donโ€™t run out of the range of valid floating point numbers.

Finally, we must choose the step size ๐œ–. In general, it must be

but since we donโ€™t know the energies beforehand, that doesnโ€™t help very much. There are sophisticated methods available to choose an optimal step size, but some experimentation shall suffice for us here.

But why did I call it gradient descent in disguise? Because ๐ป๐œ“ is directly related to the functional derivative of the energy with respect to the wave function:

where ๐›ฟ denotes a functional derivative. So this recipe is really a gradient descent.

Now letโ€™s write a general routine for our gradient descent:

import numpy as np

def gradient_descent(H, n_steps, eps, psi_init, integral):
    
    def normalize(psi):
        return psi / np.sqrt(integral(psi**2))
    
    psi = normalize(psi_init)
        
    for j in range(n_steps):        
        E = integral(psi * H(psi))
        psi = normalize(psi - eps * (H(psi) - E*psi))
    
    E = integral(psi * H(psi))
    
    return psi, E

We kept this function completely general, so it can be applied to all kind of problems. Letโ€™s apply it to the 1D harmonic oscillator, first. We need to implement the Hamiltonian and the integral operator:

from findiff import FinDiff

x = np.linspace(-5, 5, 100)
dx = x[1] - x[0]
laplace = FinDiff(0, dx, 2)

V = 0.5 * x**2

def H(psi):
    return -0.5 * laplace(psi) + V * psi 


def integral(f):
    return np.trapz(f) * dx

Now we can compute the ground state of the harmonic oscillator:

psi = np.exp(-np.abs(x))
psi, E = gradient_descent(H, 100, 0.01, psi, integral)
E

Out: 0.4997480777288626

The exact value should be 0.5.

import matplotlib.pyplot as plt

plt.plot(x, psi)

Or the hydrogen atom:

x = y = z = np.linspace(-10, 10, 80)
dx = dy = dz = x[1] - x[0]
laplace = FinDiff(0, dx, 2) + FinDiff(1, dy, 2) + FinDiff(2, dz, 2)

X, Y, Z = np.meshgrid(x, y, z, indexing='ij')
R = np.sqrt(X**2 + Y**2 + Z**2)
V = -2 / R

def H(psi):
    return -laplace(psi) + V * psi 


def integral(f):
    return np.trapz(np.trapz(np.trapz(f))) * dx * dy * dz
psi = np.exp(-R**2)

psi, E = gradient_descent(H, 600, 0.01, psi, integral)
E

Out: -0.9848171001223128

The exact value should be -1, so the result is quite crude. This is because we didnโ€™t take care of the singularity at the origin. But thatโ€™s another story.

There is much more that we could discuss, like taking into account constraints to calculate excited states. But I will leave that for a separate post. So stay tuned. Thanks for reading!