Quantum Dynamics

We solve the Schrodinger equation numerically for a Gaussian wavepacket in Morse and harmonic potential. We define our Gaussian wavepacket at t=0 as,

\psi(x,t=0)=\Big(\frac{m\omega}{\pi \hbar}\Big)^{1/4}\mathrm{e}^{- \frac{m \omega}{2 \hbar}(x-x_0)^2}.

One cannot generally obtain the eigenstates of the quantum propagator. We shall therefore numerically simulate the time-dynamics of a wave packet moving in Morse potential using the split operator method in python. In this method, one propagates a wave packet alternately in real space and Fourier space. We first propagate by time-step  \Delta t/2 in position space by solving,

 \mathrm{i}\hbar\frac{\partial \psi(x,t)}{\partial t}=V(x) \psi(x,t) ,

where V(x) is the potential energy surface. The solution to equation above is given by

 \psi(x,t+\Delta t/2)=\mathrm{e}^{-\frac{\mathrm{i} V(x)}{\hbar} \frac{\Delta t}{2}} \psi(x,t)

Use \texttt{numpy} module \texttt{fft.fft} to obtain wavefunction numerically in momentum space. To obtain the p vector, use \texttt{fft.fftfreq}. If \psi is a grid of n samples in real space, \texttt{fft.fft}(\psi) gives you a grid of n numbers in Fourier space, such that the first element corresponds to the zero-frequency mode. It is helpful to checkout the documentation of \texttt{scipy} on Fast Fourier Transform (FFT) to learn about the ordering of the FFT output. With this knowledge, propagate the next full-time step \Delta t in Fourier space by solving,

\mathrm{i}\hbar\frac{\partial \tilde{\psi}(p,t)}{\partial t} =\frac{p^2}{2 m } \tilde{\psi}(p,t),

where \tilde{\psi}(p,t) is simply the wavefunction in Fourier space. The solution to the equation above is given by,

\tilde{\psi}(p,t+\Delta t)=\mathrm{e}^{-\frac{\mathrm{i} p^2 }{2 m \hbar} \Delta t} \tilde{\psi}(p,t)

Finally use \texttt{numpy} module \texttt{fft.ifft} to obtain your wavefunction back in real space, and repeat the propagation in this space to complete the final \Delta t/2 propagation. Therefore, in one iteration of the algorithm, the wave packet propagates by \Delta t as follows,

\psi(x,t+ \Delta t) =\mathrm{e}^{-\frac{\mathrm{i} V(x)}{\hbar} \frac{\Delta t}{2} }\mathcal{F}^{-1}\Big(\mathrm{e}^{-\frac{\mathrm{i} p^2 }{2 m \hbar} \Delta t}\mathcal{F}\big(\mathrm{e}^{-\frac{\mathrm{i} V(x)}{\hbar} \frac{\Delta t}{2} }\psi(x,t)\big)\Big).

For small enough \Delta t, and large number of iterations, the approximate propagator obtained from this method is identical to the exact one. Starting from the initial Gaussian wave packet \psi(x,t=0), we implement the split operator algorithm for the following Morse potential,

V(x)=D\big(1-\mathrm{e}^{-\alpha (x-x_e)}\big)^2.


Using \texttt{ffmpeg} program, one can combine several of the snapshots of the wavepackets into a movie such as the following,

Now we perform a wavepacket propagation using the same code in a harmonic potential defined by,

V(x)=\frac{1}{2} m \omega^2 x^2.

We initialize our wave packet at t=0 with a Gaussian, \psi(x,t=0). What does the time-dynamics of the Gaussian wavepacket in a harmonic potential tell you about your initial state \psi(x,t=0)? How do you explain this interesting observation below?


The time-dynamics of the Gaussian wavepacket in a harmonic oscillator can be seen in the movie below,

Run the code below to perform simulation of a Gaussian wavepacket moving in a two-dimensional Muller-Brown potential energy surface:

Author: Manish J. Thapa
Date: 10.04.2020
ETH Zurich, Switzerland
(Please keep this if you want to modify or use this code!)

from __future__ import division, print_function

import numpy as np
from matplotlib import pyplot as plt
from scipy.fftpack import fft2, ifft2

class propagation(object):
    def __init__(self, xarr, yarr,gauss2d, Vxy, m, hbar=1, dt=0.01 ):
        self.psi_2d = gauss2d #initial wavepacket
        self.xarr = xarr
        self.Vxy = Vxy #potential energy surface
        self.hbar = hbar
        self.m = m
        self.dx = self.xarr[1] - self.xarr[0]
        self.dy = self.yarr[1] - self.yarr[0]

    def evolution(self,steps):
        self.kx = 2 * np.pi * np.fft.fftfreq(len(self.xarr), d=self.dx) #obtain the k vector
        self.ky = 2 * np.pi * np.fft.fftfreq(len(self.yarr), d=self.dy)

        psi_x = self.psi_2d

        for i in range(steps):
            kx2=self.kx * self.kx
            ky2=self.ky * self.ky
            kx2ky2 = np.zeros((len(self.xarr), len(self.yarr)))
            for ixid, ix in enumerate(kx2):
                for iyid, iy in enumerate(ky2):
                    kx2ky2[ixid, iyid] = ix+iy
            halflin = np.exp(-0.5 * 1j * self.Vxy / self.hbar * self.dt)
            fullnonlin=np.exp(-0.5 * 1j * self.hbar /self.m * (kx2ky2) * self.dt - 0.5 * 1j * self.hbar /self.m * (kx2ky2) * self.dt)
            psi_x = psi_x * halflin #half-step linear propagation in x-space
            psi_k = fft2(psi_x)
            psi_k = psi_k*fullnonlin #full-step non-linear propagation in fourier space
            psi_x = ifft2(psi_k)
            psi_x = psi_x * halflin #half-step linear propagation in x-space

#set some global variables

def gauss2d(x,y, x0gauss,y0gauss): #initialize a wavepacket
    kickx = 0 #give a momentum kick in x direction
    kicky = 0 #give a momentum kick in y direction
    return np.exp(-m*omega/2*(x-x0gauss)**2)*np.exp(-m*omega/2*(y-y0gauss)**2)*np.exp(1j*(x-x0gauss)*kickx)*np.exp(1j*(y-y0gauss)*kicky) #normalize as appropriate

A = [-200, -100, -170, 15]
a = [-1, -1, -6.5, 0.7]
b = [0, 0, 11, 0.6]
c = [-10, -10, -6.5, 0.7]
x0 = [1, 0, -0.5, -1]
y0 = [0, 0.5, 1.5, 1]

#grid for the surface (PES)

def potentialMB(x,y): #define PES
   V = 0
   for i in range(4):
      V += A[i] * np.exp(a[i] *(x-x0[i])**2+ b[i]*(x-x0[i])*(y-y0[i]) + c[i]*(y-y0[i])**2)
   return V

for xid,xval in enumerate(Vx):
    for yid,yval in enumerate(Vy):

#some critical parameters to centralize your wavepacket at
if 0: #TS

elif 0: #min 1
    x0gauss = -0.05001082
    y0gauss = 0.4666941

elif 0: #min 2
    x0gauss = -0.558
    y0gauss = 1.442

elif 0:
    x0gauss = -0.81
    y0gauss = 0.95

elif 1:
    x0gauss = -0.327
    y0gauss = 0.579

    x0gauss = -0.6
    y0gauss = 0.5

#grid for your wavepacket to spread across (keep it same as for the PES)
for xid,xval in enumerate(x): #initialize the wavepacket at t=0
    for yid,yval in enumerate(y):
        initGauss[yid,xid]=gauss2d(xval,yval, x0gauss,y0gauss) #check transpose here but I believe it is correct
Z0=np.real(abs(initGauss)**2) #probabilities

#make an object of your propagation class
evolve = propagation(xarr=x,yarr=y,gauss2d=initGauss,Vxy=Vxy.T,hbar=1,m=m,dt=0.001) #check transpose here but I believe it is correct (note that norm is preserved regardless of the value of dt)

from matplotlib import animation

fig = plt.figure(figsize=(8,8))
ax = fig.gca()
ax.set_xlim(min(Vx), max(Vx))
ax.set_ylim(min(Vy), max(Vy))
ax.plot(-0.05001082, 0.4666941, '-bo')
ax.plot(-0.558, 1.442, '-bo')
ax.plot(0.61, 0.03, '-bo')
list=np.linspace(-500,1000,40) #for the contour lines

def movie(i):
    Z = abs(evolve.psi_2d) ** 2
    ax.contourf(Vx, Vy, Vxy.T, cmap='cividis',levels=list)
    ax.contour(Vx, Vy, Vxy.T, cmap='cividis',levels=list)
    ax.contourf(x, y, Z, cmap='Greys', alpha=1.0)
    return ax,

anim = animation.FuncAnimation(fig, movie,interval=3,frames=100)
#anim.save('MB.mp4', fps=3, extra_args=['-vcodec', 'libx264'])


Note: I prepared these simulation results for Advanced Kinetics class taught by Prof. Jeremy O. Richardson at ETH Zurich.