Analytical Introduction

We are solving a 1D Schrödinger equation (in atomic units): $$ \left(-{1\over 2\mu} {d^2\over dx^2} + V(x)\right)\psi(x) = E\psi(x) $$ For example for double well: $$ V(x) = D_0 {(x^2 - a^2)^2\over a^4} $$ we want to get:

We follow the Appendix 2 in [1], which explains how to use sine basis to solve the problem.

[1] Horáček, J., Gemperle, F., Meyer, H.-D. (1996). Calculation of dissociative attachment of electrons to diatomic molecules by the Schwinger–Lanczos approach. The Journal of Chemical Physics, 104(21), 8433. doi:10.1063/1.471593

Basis functions


In [1]:
%pylab inline
from sympy.interactive import init_printing
init_printing()
from sympy import sqrt, sin, pi, var, integrate, Symbol, S, Integral
var("L x mu")
i = Symbol("i", integer=True)
j = Symbol("j", integer=True)
def phik(k, x):
    return sqrt(2/L)*sin(k*pi*x/L)  # domain is [0, L]


Welcome to pylab, a matplotlib-based Python environment [backend: module://IPython.zmq.pylab.backend_inline].
For more information, type 'help(pylab)'.

The basis functions are $\phi_i = \langle x|i\rangle$:


In [2]:
phik(i, x)


Out[2]:
$$\sqrt{2} \sqrt{\frac{1}{L}} \sin{\left (\frac{\pi i x}{L} \right )}$$

Operator $T$

Let's calculate the operator $T_{ij}=-{1\over 2\mu}\langle i|\nabla^2|j\rangle$.

The diagonal terms $T_{ii}$ are:


In [3]:
-S(1)/(2*mu) * Integral(phik(i, x) * phik(i, x).diff(x, 2), (x, 0, L))


Out[3]:
$$- \frac{\int_{0}^{L} - 2 \frac{\pi^{2} i^{2} \sin^{2}{\left (\frac{\pi i x}{L} \right )}}{L^{3}}\, dx}{2 \mu}$$

In [4]:
_.doit()


Out[4]:
$$\frac{\pi^{2} i^{2}}{2 L^{2} \mu}$$

The off diagonal terms are zero:


In [5]:
-S(1)/(2*mu) * Integral(phik(i, x) * phik(j, x).diff(x, 2), (x, 0, L))


Out[5]:
$$- \frac{\int_{0}^{L} - 2 \frac{\pi^{2} j^{2} \sin{\left (\frac{\pi i x}{L} \right )} \sin{\left (\frac{\pi j x}{L} \right )}}{L^{3}}\, dx}{2 \mu}$$

In [6]:
_.doit()


Out[6]:
$$0$$

In [7]:
integrate(phik(1, x)*phik(2, x), (x, 0, L))


Out[7]:
$$0$$

In [8]:
integrate(phik(1, x)*phik(3, x), (x, 0, L))


Out[8]:
$$0$$

Which is equal to the solution in [1]: $$ T_{ij} = \cases{{1\over 2\mu}\left[i\pi\over L\right]^2 &if $i=j$;\cr 0,&otherwise.\cr} $$

Operator $X$

Let's calculate the operator $X_{ij}=\langle i|x|j\rangle$.

The diagonal terms $X_{ii}$ are:


In [9]:
Integral(phik(i, x) * x * phik(i, x), (x, 0, L))


Out[9]:
$$\int_{0}^{L} 2 \frac{x \sin^{2}{\left (\frac{\pi i x}{L} \right )}}{L}\, dx$$

In [10]:
_.doit()


Out[10]:
$$\frac{1}{2} L$$

But notice that:


In [11]:
Integral(phik(i, x) * x * phik(i, x), (x, -L/2, L/2))


Out[11]:
$$\int_{- \frac{1}{2} L}^{\frac{1}{2} L} 2 \frac{x \sin^{2}{\left (\frac{\pi i x}{L} \right )}}{L}\, dx$$

In [12]:
_.doit()


Out[12]:
$$0$$

The off diagonal terms are:


In [13]:
Integral(phik(i, x) * x * phik(j, x), (x, 0, L))


Out[13]:
$$\int_{0}^{L} 2 \frac{x \sin{\left (\frac{\pi i x}{L} \right )} \sin{\left (\frac{\pi j x}{L} \right )}}{L}\, dx$$

In [14]:
_.doit()


Out[14]:
$$4 \frac{\left(-1\right)^{i} \left(-1\right)^{j} L i j}{\pi^{2} i^{4} - 2 \pi^{2} i^{2} j^{2} + \pi^{2} j^{4}} - 4 \frac{L i j}{\pi^{2} i^{4} - 2 \pi^{2} i^{2} j^{2} + \pi^{2} j^{4}}$$

In [15]:
_.simplify()


Out[15]:
$$4 \frac{L i j \left(\left(-1\right)^{i + j} -1\right)}{\pi^{2} \left(i^{4} - 2 i^{2} j^{2} + j^{4}\right)}$$

Which is equal to the solution in [1]: $$ X_{ij} = \cases{-{8ijL\over \pi^2 (i+j)^2(i-j)^2} &if $i+j$ is odd;\cr 0,&otherwise.\cr} $$

Numerical Solution

Authors of the code:

  • Jiří Eliášek (adarith@gmail.com)
  • Ondřej Maršálek (ondrej.marsalek@gmail.com)

Original repository: https://github.com/OndrejMarsalek/Fourier-DVR-1D


In [16]:
import numpy as np

def _eig_sorted(M):
    """Convenience wrapper over numpy.linalg.eig.

    :param M: matrix
    :type M: 2D numpy array

    Returns eigenvalues and eigenvectors of M sorted by eigevalue magnitude.

    """

    val, vec = np.linalg.eig(M)
    idx_sort = val.argsort()

    return val[idx_sort], vec[:, idx_sort]

class Domain_Fourier_DVR_1D(object):
    """
    Solves the Schroedinger equation on a finite one-dimensional interval using
    the discrete variable representation method with the Fourier sine basis:

    phi_k = sqrt(2 / (b-a)) * sin(k * pi * (x-a) / (b-a)), k=1..n_DVR

    For details, see for example Appendix 2 of:
    J. Chem. Phys. 104, 8433 (1996)
    http://dx.doi.org/10.1063/1.471593
    """

    def __init__(self, a, b, n_DVR):
        """Constructs the domain with given end points and basis size.

        :param a: lower bound of domain
        :param b: upper bound of domain
        :param n_DVR: number of basis functions

        """

        # store domain parameters
        self._a = a
        self._b = b
        self._n_DVR = n_DVR

        # no previous calculation was performed
        self._m = None
        self._V = None

        # update spectral decomposition of the position operator
        self._update_X_in_Fourier_basis()

    def _update_X_in_Fourier_basis(self):
        """Decompose the position operator in the Fourier sine basis.

        Eigenvalues and eigenvectors of the position operator
        in the Fourier sine basis are stored in `self`.

        """

        a = self._a
        b = self._b
        n_DVR = self._n_DVR

        # construct position operator in Fourier sine basis
        i = np.arange(1, n_DVR+1, dtype=float)[:, np.newaxis].repeat(n_DVR, axis=1)
        j = i.transpose()
        ipj = i + j
        ipjmod1 = (ipj) % 2
        div = np.where(ipjmod1,  ((i-j) * ipj)**2, 1)
        fact = -8.0 * (b-a) / np.pi**2
        X_four = fact * np.where(ipjmod1, (i*j) / div, 0.0)

        x, phi_four = _eig_sorted(X_four)
        x = x + (a+b) / 2.0

        self._x = x
        self._phi_four = phi_four

    def _update_T_four(self, m):
        """Build the kinetic energy operator and store it in `self`.

        :param m: mass

        """

        if self._m is None or (m != self._m):

            self._m = m

            l = self._b - self._a
            t = (0.5 / m) * (np.pi / l)**2 * np.arange(1, self._n_DVR + 1)**2
            self._T_four = np.diagflat(t)

    def _update_V_four(self, V):
        """Build the potential energy operator and store it in `self`.

        :param V: potential energy - real-space vectorized function

        """

        if self._V is None or (V != self._V):
            self._V = V
            phi_four = self._phi_four
            V_x = np.diagflat(V(self._x))
            self._V_four = np.dot(np.dot(phi_four, V_x), phi_four.transpose())

    def solve(self, m, V):
        """Solve the Schroedinger equation on this domain.

        :param m: mass
        :param V: potential energy - real-space vectoried function

        Returns eigenenergies and eigenstates of the Hamiltonian sorted
        by eigenenergy magnitude.

        """

        # update kinetic energy and potential operators, if needed
        self._update_T_four(m)
        self._update_V_four(V)

        # construct the Hamiltonian
        H_four = self._T_four + self._V_four

        # solve
        E, psi_four = _eig_sorted(H_four)

        return E, psi_four


    def grid(self, x, psi_four):
        """Evaluate states on a real-space grid.

        :param x: real-space grid - 1D array
        :param psi_four: states in Fourier basis

        Returns states on grid x.

        """

        n_DVR, n_out = psi_four.shape

        if n_DVR != self._n_DVR:
            data = (self._n_DVR, n_DVR)
            err = 'Wrong dimension of states. Expected %d, got %d.' % data
            raise ValueError(err)

        # convenience
        a = self._a
        b = self._b

        # flag points outside the [a, b] interval
        outside = np.logical_and((x > a), (x < b))

        # construct Fourier sine basis functions on real-space grid
        norm = np.sqrt(2 / (b-a))
        four_1_grid = np.exp(1.0j * np.pi * (x-a) / (b-a))
        four_1_grid *= outside
        k = np.arange(1, n_DVR + 1, dtype=complex)
        four_grid = norm * np.imag(four_1_grid[:, np.newaxis]**k)

        # project states to real-space grid
        psi_grid = np.dot(four_grid, psi_four).transpose()

        return psi_grid

Double Well

$$ V(x) = D_0 {(x^2 - a^2)^2\over a^4} $$

In [17]:
# settings
m = 1.0
x_min = -5.0
x_max = 5.0
n_DVR = 200 # increase this for convergence
n_g = 601
D0 = 43.0
a = 1.5
V = lambda x: (D0 / a**4) * (x**2 - a**2)**2
n_plot = 15
scale = 4.0

# solve
domain = Domain_Fourier_DVR_1D(x_min, x_max, n_DVR)
E, E_four = domain.solve(m, V)

# evaluate eigenstates on grid
x = np.linspace(x_min, x_max, n_g)
psi_x = domain.grid(x, E_four[:,:n_plot])

# print energies
for i, e in enumerate(E[:n_plot]):
    print '%3i %18.12f' % (i, e)

# plot eigenstates
for i in range(n_plot):
    plot([x[0], x[-1]], [E[i], E[i]], '--', color='gray')
plot(x, V(x), 'k-', lw=2)
for i in range(n_plot):
    plot(x, scale * psi_x[i] + E[i])
xlim(-3, 3)
ylim(-E[0], E[n_plot])
xlabel("$x$ [a.u.]")
ylabel("E [a.u.]")
tight_layout()


  0     6.066347889246
  1     6.066349114579
  2    17.690611617659
  3    17.690825196654
  4    28.400361191828
  5    28.414779898742
  6    37.628396407232
  7    38.044703742463
  8    44.176291626709
  9    47.060445838845
 10    51.953289448631
 11    56.940860226219
 12    62.439975247803
 13    68.281211247166
 14    74.447117421066

Harmonic Oscillator


In [18]:
# settings
m = 1.0
x_min = -15.0
x_max = 15.0
n_DVR = 300 # increase this for convergence
n_g = 1001
k = 1.0
V = lambda x: 0.5 * k * x * x
n_plot = 50
scale = 1.0

# solve
domain = Domain_Fourier_DVR_1D(x_min, x_max, n_DVR)
E, E_four = domain.solve(m, V)

# evaluate eigenstates on grid
x = np.linspace(x_min, x_max, n_g)
psi_x = domain.grid(x, E_four[:,:n_plot])

# print energies
for i, e in enumerate(E[:n_plot]):
    print '%3i %12.6f' % (i, e)

# plot eigenstates
subplots_adjust(left=0.05, right=0.95,
                    bottom=0.05, top=0.95)
plot(x, V(x), 'k-', lw=2)
for i in range(n_plot):
    plot([x[0], x[-1]], [E[i], E[i]], '--', color='gray')
for i in range(n_plot):
    plot(x, scale * psi_x[i] + E[i])
xlim(x_min, x_max)
ylim(-E[0], E[n_plot])
xlabel("$x$ [a.u.]")
ylabel("E [a.u.]");


  0     0.500000
  1     1.500000
  2     2.500000
  3     3.500000
  4     4.500000
  5     5.500000
  6     6.500000
  7     7.500000
  8     8.500000
  9     9.500000
 10    10.500000
 11    11.500000
 12    12.500000
 13    13.500000
 14    14.500000
 15    15.500000
 16    16.500000
 17    17.500000
 18    18.500000
 19    19.500000
 20    20.500000
 21    21.500000
 22    22.500000
 23    23.500000
 24    24.500000
 25    25.500000
 26    26.500000
 27    27.500000
 28    28.500000
 29    29.500000
 30    30.500000
 31    31.500000
 32    32.500000
 33    33.500000
 34    34.500000
 35    35.500000
 36    36.500000
 37    37.500000
 38    38.500000
 39    39.500000
 40    40.500000
 41    41.500000
 42    42.500000
 43    43.500000
 44    44.500000
 45    45.500000
 46    46.500000
 47    47.500000
 48    48.500000
 49    49.500000

In [18]: