Molecular Dynamics#

import matplotlib.pyplot as plt
import ipywidgets as widgets
import numpy as np
import plotly.express as px

"Cu atoms"

"Water molecules"

Is MD just Newton’s laws applied on big systems?#

Not quite: Noble prize in Chemistry 2013 just for MD

  • Classical molecular dynamics (MD) is a powerful computational technique for studying complex molecular systems.

  • Applications span wide range including proteins, polymers, inorganic and organic materials.

  • Alos molecular dynamics simulation is being used in a complimentary way to the analysis of experimental data coming from NMR, IR, UV spectroscopies and elastic-scattering techniques, such as small angle scattering or diffraction.

  • 2013 Noble Lectures by M Karplus, A Warshell, M Levitt

Timescales and Lengthscales#

  • Classical Molecular Dynamics can access a hiearrchy of time-scales from pico seconds to microseconds.

  • It is also possible to go beyond the time scale of brute force MD byb emplying clever enhanced sampling techniques.

Energy function (force fields) used in classical MD#

Non-bonded interactions: Van der Waals#

@widgets.interact(sig=(1,2, 0.1),eps=(0.1,3))
def plot_lj(sig=1, eps=1):
    
    r  = np.linspace(0.5, 3, 1000)
    lj = 4 * eps *  ( (sig/r)**12 -(sig/r)**6 )
    
    plt.plot(r, lj, lw=3)
    plt.plot(r, -4*eps*(sig/r)**6, '-',lw=3)
    plt.plot(r,  4*eps*(sig/r)**12,'-',lw=3)

    plt.ylim([-3,3])
    plt.xlabel(r'$r$')
    plt.ylabel(r'$U(r)$')
    plt.legend(['LJ', 'Attr-LJ','Repuls-LJ' ])
    plt.show()
@widgets.interact(kappa=(0.1,5))
def plot_lj(kappa=0.1):
    
    r    = np.linspace(1, 5, 1000)
    
    E_DH = np.exp(-kappa*r)/r

    plt.plot(r, E_DH, lw=3)

    plt.ylim([0,1])
    plt.legend(['Debye-Huckel potential'])
    plt.xlabel(r'$r$')
    plt.ylabel(r'$U(r)$')

    plt.grid(True)
    plt.show()
@widgets.interact(kappa=(0.1,5))
def plot_harmonic(k=0.1):
    
    r    = np.linspace(1, 5, 1000)

    plt.grid(True)
    plt.show()

Integrating equations of motion#

No thanks, Euler#

The simplest integrating scheme for ODEs is the Euler’s method. Given the n-dimensional vectors from the ODE standard form. the Euler rule amounts to writing down equation in finite difference form.

dydt=f(t,y)
dy(t)dty(tn+1)y(tn)h
yn+1yn+hf(tn,yn)

Much better integrators are known under the names of Runge Kutta, 2nd, 4th, 6th … order

def euler(y, f, t, h):   
    """Euler integrator: Returns new y at t+h.
    """
    return y + h * f(t, y)

def rk2(y, f, t, h):
    """Runge-Kutta RK2 midpoint"""
    
    k1 = f(t, y)
    k2 = f(t + 0.5*h, y + 0.5*h*k1)
    
    return y + h*k2

def rk4(y, f, t, h):
    """Runge-Kutta RK4"""
    
    k1 = f(t, y)
    k2 = f(t + 0.5*h, y + 0.5*h*k1)
    k3 = f(t + 0.5*h, y + 0.5*h*k2)
    k4 = f(t + h, y + h*k3)
    
    return y + h/6 * (k1 + 2*k2 + 2*k3 + k4)

Verlet algortihm#

Taylor expansion of position r(t) after timestep Δt we obtain forward and backward Euler schems

rt+Δt=rt+vtΔt+12atΔt2+O(Δt3)
rtΔt=rtvtΔt+12atΔt2+O(Δt3)
  • In 1967 Loup Verlet introduced a new algorithm into molecular dynamics simulations which preserves energy is accurate and efficient.

  • Summing the two taylor expansion above we get a updating scheme which is an order of mangnitude more accurate

rt+Δt=2rtrtΔt+atΔt2+O(Δt4)
vt=rt+ΔtrtΔt2Δt+O(Δt2)
  • Velocity is not needed to update the positions. But we still need them to set the temperature.

  • Terms of order O(Δt3) cancel in position giving position an accuracy of order O(Δt4)

  • To update the position we need positions in the past at two different time points! This is is not very efficient.

Velocity Verlet updating scheme#

  • A better updating scheme has been proposed known as Velocity-Verlet (VV) which stores positions, velocities and accelerations at the same time. Time stepping backward expansion r(tΔt+Δt) and summing with the forward Tayloer expansions we get Velocity Verlet updating scheme:

vt+Δt=vt+12(at+at+Δt)Δt+O(Δt3)

Substituting forces a=Fm instead of acelration we get

rt+Δt=rt+vtΔt+Ft2mΔt2
vt+Δt=vt+Ft+Ft+Δt2mΔt

Velocity Verlet Algorithm#

1. Evaluate the forces by plugging initial positions into force-field

FtU(r)r|r(t)

2. Position update:

rt+Δt=rt+vΔt+Ft2mΔt2

3. Partial update of velocity

vv+Ft2mΔt

3. Force/acceleration evalutation at a new position

Ft+Δt=U(r)r|r(t+Δt)

4. Full update of velocity

vv+Ft+Δt2mΔt

Alternative implementaitons of VV#

  • There is an identical to Velocity Verlet integration scheme known as the Leapfrog algorithm. which differens in the way of implementation.

  • In the Leap-frog velocities are calculated at half-timestep Δt/2.

vt+Δt/2=vtΔt/2+Ft2mΔt
rt+Δt=rt+vt+Δt/2Δt
  • One disadvantage of the leap frog approach is that the velocities are not known at the same time as the positions, making it difficult to evaluate the total energy (kinetic + potential) at any one point in time.

def velv(y, f, t, h):
    """Velocity Verlet for solving differential equations. 
    A little inefficient since same force (first and last) is evaluated twice!
    """
    
    # 1. Evluate force
    F = f(t, y)
    
    # 2, Velocity partial update
    y[1] += 0.5*h * F[1]

    # 3. Full step position
    y[0] += h*y[1]
    
    # 4. Force re-eval
    F = f(t+h, y)
    
    # 5. Full step velocity 
    y[1] += 0.5*h * F[1]  

    return y

Comparison of RK and Verlet#

def f(t, y):
    ''' Define a simple harmonic potential'''
    
    return np.array([y[1], -y[0]])
y  = np.array([1., 1.])
pos, vel = [], []
t  = 0
h = 0.1 

for i in range(1000):
    
    y = velv(y, f, t, h) # Change integration method venv, euler, rk2, rk4
    
    t+=h
    
    pos.append(y[0])
    vel.append(y[1])

fig, ax = plt.subplots(ncols=3, nrows=1, figsize=(12,3))

pos, vel =np.array(pos), np.array(vel)
ax[0].plot(pos, vel)
ax[1].plot(pos)
ax[1].plot(vel)
ax[2].plot(0.5*pos**2 + 0.5*vel**2)
[<matplotlib.lines.Line2D at 0x7f0c20689e20>]
../_images/f2783a64eea31bb07afdb17c607c32a06b991b394823e0999df3accc6954550d.png

Ensemble averages#

Ensemble vs time average and ergodicity#

A=1TprodTeqTeq+TprodA(t)

Kinetic, Potential and Total Energies#

  • Kinetic, potential and total energies are flcutuation quantities in the Molecualr dynamics simulations.

  • Energy conservation should still hold: The mean of total energy should be stable

KEt=ipt22m
PEt=ijuij(rt)
E=KE+PEconst

Temperature#

According to equipariting result of equilibrium statistical mechanics in the NVT ensmeble

ipt22m=32NkBT
Tt=23NkBKEt

Pressure#

The microscopic expression for the pressure can be derived in the NVE ensemble using

P=13ViN[pi2m+Fi(r)ri(t)]

Molecular Dynaamics of Classical Harmonic Oscillator#

NVE#

def time_step_1D(pos, vel, F, en_force):
    '''Velocity Verlet update of velocities, positions and forces
    pos         (float):      position
    vel         (float):      velocity
    F           (float):        force
    en_force (function): a function whichb computes potential energy and force (derivative)
    '''
    
    vel   += 0.5 * F * dt
    
    pos   += vel * dt
    
    pe, F  = en_force(pos)
    
    vel   += 0.5 * F * dt

    return pos, vel, F, pe
def md_nve_1d(x, v, dt, t_max, en_force):
    '''Minimalistic MD code applied to a harmonic oscillator'''
    
    times, pos, vel, KE, PE  = [], [], [], [], []
    
    #1. Intialize force
    pe, F = en_force(x) 
    
    for step in range(int(t_max/dt)):
        
        x, v, F, pe = time_step_1D(x, v, F, en_force)
        
        pos.append(x), vel.append(v), KE.append(0.5*v*v), PE.append(pe)    
    
    return np.array(pos), np.array(vel), np.array(KE), np.array(PE)
#----parameters of simulation----
k     = 3 
x0    = 1 
v0    = 0
dt    = 0.01 * 2*np.pi/np.sqrt(k) #A good timestep determined by using oscillator frequency
t_max = 1000

def ho_en_force(x, k=k):
    '''Force field of harmonic oscillator:
    returns potential energy and force'''
    
    return k*x**2, -k*x
pos, vel, KE, PE = md_nve_1d(x0, v0, dt, t_max, ho_en_force)
fig, ax =plt.subplots(ncols=2)

ax[0].hist(pos);
ax[1].hist(vel, color='orange');
ax[0].set_ylabel('P(x)')
ax[1].set_ylabel('P(v)')

fig.tight_layout()
../_images/9a1a7a7eb048d94c6eef29496697b0656092c1cd937ba1dab3f45d456dcc37ef.png

Molecualr Dynamics of Harmonic oscillator (NVT)#

Newton’s equation of motion

F=mx¨=xU

Langevin equation

F=xUλx˙+η(t)

Overdamped Langevin equation, mv˙=0

λx˙=xU+η(t)

Fluctuation-dissipation theorem

η(t)η(t)=2λkBTδ(tt)

Velocity updating when coupled to a thermal heat bath

vt+Δt=vteλΔt+(kBT)1/2(1e2γΔt)1/2N(0,1)
def langevin_md_1d(x, v, dt, kBT, gamma, t_max, en_force):
    '''Langevin dynamics applied to 1D potentials
    Using integration scheme known as BA-O-AB.
    INPUT: Any 1D function with its parameters
    '''
    
    times, pos, vel, KE, PE  = [], [], [], [], []
    
    t = 0  
    for step in range(int(t_max/dt)):
        
        #B-step
        pe, F = en_force(x)
        v    += F*dt/2
        
        #A-step
        x += v*dt/2

        #O-step
        v = v*np.exp(-gamma*dt) + np.sqrt(1-np.exp(-2*gamma*dt)) * np.sqrt(kBT) * normal()
        
        #A-step
        x +=  v*dt/2
        
        #B-step
        pe, F = en_force(x)
        v    +=  F*dt/2
        
        ### Save output 
        times.append(t), pos.append(x), vel.append(v), KE.append(0.5*v*v), PE.append(pe)    
    
    return np.array(times), np.array(pos), np.array(vel), np.array(KE), np.array(PE)
# Ininital conditions
x     = 0.1
v     = 0.5

# Input parameters of simulation
kBT   = 0.25
gamma = 10
dt    = 0.01
t_max = 10000
freq  = 10

times, pos, vel, KE, PE = langevin_md_1d(x, v, dt, kBT, gamma, t_max, ho_en_force)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[15], line 12
      9 t_max = 10000
     10 freq  = 10
---> 12 times, pos, vel, KE, PE = langevin_md_1d(x, v, dt, kBT, gamma, t_max, ho_en_force)

Cell In[14], line 20, in langevin_md_1d(x, v, dt, kBT, gamma, t_max, en_force)
     17 x += v*dt/2
     19 #O-step
---> 20 v = v*np.exp(-gamma*dt) + np.sqrt(1-np.exp(-2*gamma*dt)) * np.sqrt(kBT) * normal()
     22 #A-step
     23 x +=  v*dt/2

NameError: name 'normal' is not defined
def gaussian_x(x, k, kBT): 
    return  np.exp(-k*(x**2)/(2*kBT)) / np.sqrt(2*np.pi*kBT/k)
    
def gaussian_v(v, kBT):   
    return  np.exp(-(v**2)/(2*kBT))  / np.sqrt(2*np.pi*kBT) 

fig, ax =plt.subplots(nrows=1, ncols=2, figsize=(9,4))

bins=50

x = np.linspace(min(pos), max(pos), bins)
ax[0].hist(pos, bins=bins, density=True) 
ax[0].plot(x, gaussian_x(x, k, kBT), lw=2, color='k')

v = np.linspace(min(vel), max(vel), bins)
ax[1].hist(vel, bins=bins, density=True, color='orange') 
ax[1].plot(v, gaussian_v(v, kBT), lw=2, color='k')

ax[0].set_ylabel('P(x)')
ax[1].set_ylabel('P(v)')

fig.tight_layout()
../_images/e58f764fe57e7f7e2d1141e9bbe8bea3d5ed07cebf4ab5896d30b7741cbfcc77.png

Double well potential#

def double_well(x, k=1, a=3):
    
    energy = 0.25*k*((x-a)**2) * ((x+a)**2)
    force = -k*x*(x-a)*(x+a)
    
    return energy, force

@widgets.interact(k=(0.1, 1), a=(0.1,3))
def plot_harm_force(k=1, a=3):
    
    x = np.linspace(-6,6,1000)
    
    energy, force = double_well(x, k, a) 
    
    plt.plot(x, energy, '-o',lw=3)
    plt.plot(x, force, '-', lw=3, alpha=0.5)
    
    plt.ylim(-20,40)
    plt.grid(True)
    plt.legend(['$U(x)$', '$F=-\partial_x U(x)$'], fontsize=15)
# Ininital conditions
x     = 0.1
v     = 0.5

# Input parameters of simulation
kBT   = 5 # vary this
gamma = 0.1 # vary this

dt    = 0.05
t_max = 10000
freq  = 10

times, pos, vel, KE, PE = langevin_md_1d(x, v, dt, kBT, gamma, t_max, double_well)
fig, ax =plt.subplots(nrows=1, ncols=2, figsize=(13,5))

x = np.linspace(min(pos), max(pos), 50)


ax[0].plot(pos)
ax[1].hist(pos, bins=50, density=True, alpha=0.5);

v = np.linspace(min(vel), max(vel),50)
../_images/14be70aa5c5e75db8f95c844829b52ccfb6800e6dfed293e79a3c16c62330eb8.png

Additional resoruces for learning MD#

For a quick dive consult the following lecture notes:

The best resource for comprehensive stuyd remains the timeless classic by Daan Frankel

Problems#

1D harmonic potential#

Given a 1D potential U(x) write code to carry out molecular dynamics

U(x)=Ax2+Bx3+Cx4
  • Using langevin dynamics to simulate the system in NVT ensemble.

  • Your input should read in A, B, C parameters, starting point and velocity, timestep and number of iterations, kBT, gamma.

  • Output should be the positions which should then be histogrammed to obtain probability distribution

  • Use A=-1, B = 0.0, C = 1.0. Try various initial positions. Look for some interesting regions to sample by adjusting initital positions.

  • Use A=-1, B = -1, C = 1.0. Again try various initial strting positions.

Beads and springs#

Simulate a chain of harmonic oscillators with the following potential energy function

U(x1,x2,...)=jpj22m+12Kj(xjxj1)2
  • Take the particle at x0=0 fixed. Carry out constant T MD using langevin dynamics

  • Try N = 2, N=10 particles and obtain equilibrium distributions of xjxj1 for various temperatures.