Why this LJ molecular dynamics result doesn't converge?

I am doing a molecular dynamics simulation of Leonard Jones 6-12 potential. But instead of converging to a particular value. It always stays between -5.58 to -5.62. The standard value is -5.517. The problem is that however long it runs (I have tested up to 30000 steps) it does not converge to even 1 digit after the decimal point. It changes between -5.58 to -5.62 but never reaches -5.517. My timestep is 0.005 (as given in reference value). All my other parameters are the same as the value in reference. I am using a velocity-Verlet algorithm. The standard reference is available here.

I have used velocity rescaling and that's why kinetic energy remains almost constant, at least up to 2 digits after decimal always. Total energy change is quite small after a few steps, the change is mainly due to fluctuation in potential energy i.e. in the range -5.58 to -5.62.

The code to reproduce the error is below.

import numpy as np
import matplotlib.pyplot as plt
from numba import jit
import os
import sys

# All globals are compile time constants
# recompilation needed if you change this values
# Better way: hand a tuple of all needed vars to the functions
# params=(NSteps,deltat,temp,DumpFreq,epsilon,DIM,N,density,Rcutoff)

# Setting up the simulation
NSteps =100000 # Number of steps
deltat = 0.005 # Time step in reduced time units
temp   = 0.851# #Reduced temperature
DumpFreq = 100 # Save the position to file every DumpFreq steps
epsilon = 1.0 # LJ parameter for the energy between particles
DIM     =3
N       =500
density =0.776
Rcutoff =3

params=(NSteps,deltat,temp,DumpFreq,epsilon,DIM,N,density,Rcutoff)

#----------------------Function Definitions---------------------

#------------------Initialise Configuration--------

error_model=True
#If it is really required to search for division by zeros (additional cost)?

@jit(nopython=True,error_model="numpy")
def initialise_config(N,DIM,density):
velocity = (np.random.randn(N,DIM)-0.5)

# Set initial momentum to zero
COM_V = np.sum(velocity)/N     #Center of mass velocity
velocity = velocity - COM_V    # Fix any center-of-mass drift

# Calculate initial kinetic energy
k_energy=0
for i in range (N):
k_energy+=np.dot(velocity[i],velocity[i])
vscale=np.sqrt(DIM*temp/k_energy)
velocity*=vscale

#wrong array initialization (use tuple)
#Initialize with zeroes
coords = np.zeros((N,DIM))

# Get the cooresponding box size
L = (N/density)**(1.0/DIM)

""" Find the lowest perfect cube greater than or equal to the number of
particles"""
nCube = 2

while (nCube**3 < N):
nCube = nCube + 1

# Assign particle positions
ip=-1
x=0
y=0
z=0

for i in range(0,nCube):
for j in range(0,nCube):
for k in range(0,nCube):
if(ip<N):
x=(i+0.5)*(L/nCube)
y=(j+0.5)*(L/nCube)
z=(k+0.5)*(L/nCube)
coords[ip]=np.array([x,y,z])
ip=ip+1
else:
break
return (coords/L),velocity,L

@jit(nopython=True)
def wrap(pos,L):
'''Apply perodic boundary conditions.'''

#correct array indexing
for i in range (len(pos)):
for k in range(DIM):
if (pos[i,k]>0.5):
pos[i,k]=pos[i,k]-1
if (pos[i,k]<-0.5):
pos[i,k]=pos[i,k]+1

return (pos)

@jit(nopython=True,error_model="numpy")
def LJ_Forces(pos,acc,epsilon,L,DIM,N):
# Compute forces on positions using the Lennard-Jones potential
# Uses double nested loop which is slow O(N^2) time unsuitable for large systems
Sij = np.zeros(DIM) # Box scaled units
Rij = np.zeros(DIM) # Real space units

#Set all variables to zero
ene_pot = np.zeros(N)
acc = acc*0
virial=0.0

# Loop over all pairs of particles
for i in range(N-1):
for j in range(i+1,N): #i+1 to N ensures we do not double count
Sij = pos[i]-pos[j] # Distance in box scaled units
for l in range(DIM): # Periodic interactions
if (np.abs(Sij[l])>0.5):
Sij[l] = Sij[l] - np.copysign(1.0,Sij[l]) # If distance is greater than 0.5  (scaled units) then subtract 0.5 to find periodic interaction distance.

Rij   = L*Sij # Scale the box to the real units in this case reduced LJ units
Rsqij = np.dot(Rij,Rij) # Calculate the square of the distance

if(Rsqij < Rcutoff**2):
# Calculate LJ potential inside cutoff
# We calculate parts of the LJ potential at a time to improve the efficieny of the computation (most important for compiled code)
rm2      = 1.0/Rsqij # 1/r^2
rm6      = rm2**3
forcefact=(rm2**4)*(rm6-0.5) # 1/r^6
phi      =4*(rm6**2-rm6)

ene_pot[i] = ene_pot[i]+0.5*phi # Accumulate energy

ene_pot[j] = ene_pot[j]+0.5*phi # Accumulate energy

virial     = virial-forcefact*Rsqij # Virial is needed to calculate the pressure
acc[i]     = acc[i]+forcefact*Sij # Accumulate forces
acc[j]     = acc[j]-forcefact*Sij # (Fji=-Fij)
#summing at the end np.sum(ene_pot)
return 48*acc, np.sum(ene_pot)/N, -virial/DIM # return the acceleration vector, potential energy and virial coefficient

@jit(nopython=True,error_model="numpy")
def Calculate_Temperature(vel,L,DIM,N):

ene_kin = 0.0

for i in range(N):
real_vel = L*vel[i]
ene_kin = ene_kin + 0.5*np.dot(real_vel,real_vel)

ene_kin_aver = 1.0*ene_kin/N
temperature = 2.0*ene_kin_aver/DIM

return ene_kin_aver,temperature

# Main MD loop
@jit(nopython=True,error_model="numpy")
def main(params):
NSteps,deltat,temp,DumpFreq,epsilon,DIM,N,density,Rcutoff=params
# Vectors to store parameter values at each step

ene_kin_aver = np.ones(NSteps)
ene_pot_aver = np.ones(NSteps)
temperature = np.ones(NSteps)
virial = np.ones(NSteps)
pressure = np.ones(NSteps)

pos,vel,L = initialise_config(N,DIM,density)
acc = (np.random.randn(N,DIM)-0.5)
volume=L**3

# Open file which we will save the outputs to
# Unsupported operations have to be in an objectmode block
# or simply write the outputs at the end in a pure Python function
"""
if os.path.exists('energy2'):
os.remove('energy2')
f = open('traj.xyz', 'w')
"""
for k in range(NSteps):

# Refold positions according to periodic boundary conditions
pos=wrap(pos,L)

# r(t+dt) modify positions according to velocity and acceleration
pos = pos + deltat*vel + 0.5*(deltat**2.0)*acc # Step 1

# Calculate temperature
ene_kin_aver[k],temperature[k] = Calculate_Temperature(vel,L,DIM,N)

# Rescale velocities and take half step
chi = np.sqrt(temp/temperature[k])
vel = chi*vel + 0.5*deltat*acc # v(t+dt/2) Step 2

# Compute forces a(t+dt),ene_pot,virial
acc, ene_pot_aver[k], virial[k] = LJ_Forces(pos,acc,epsilon,L,DIM,N) # Step 3

# Complete the velocity step
vel = vel + 0.5*deltat*acc # v(t+dt/2) Step 4

# Calculate temperature
ene_kin_aver[k],temperature[k] = Calculate_Temperature(vel,L,DIM,N)

# Calculate pressure
pressure[k]= density*temperature[k] + virial[k]/volume

# Print output to file every DumpFreq number of steps
"""
if(k%DumpFreq==0): # The % symbol is the modulus so if the Step is a whole multiple of DumpFreq then print the values

f.write("%s\n" %(N)) # Write the number of particles to file
# Write all of the quantities at this step to the file
f.write("Energy %s, Temperature %.5f\n" %(ene_kin_aver[k]+ene_pot_aver[k],temperature[k]))
for n in range(N): # Write the positions to file
f.write("X"+" ")
for l in range(DIM):
f.write(str(pos[n][l]*L)+" ")
f.write("\n")

#Simple prints without formating are supported
"""
if (k%150==0):
print("step: ",k,"KE: ",ene_kin_aver[k],"PE :",ene_pot_aver[k],"\n Total Energy: ",ene_kin_aver[k]+ene_pot_aver[k])
#print("\rStep: {0} KE: {1}   PE: {2} Energy:  {3}".format(k, ene_kin_aver[k], ene_pot_aver[k],ene_kin_aver[k]+ene_pot_aver[k]))
#sys.stdout.write("\rStep: {0} KE: {1}   PE: {2} Energy:  {3}".format(k, ene_kin_aver[k], ene_pot_aver[k],ene_kin_aver[k]+ene_pot_aver[k]))
#sys.stdout.flush()

return ene_kin_aver, ene_pot_aver, temperature, pressure, pos

#------------------------------------------------------
ene_kin_aver, ene_pot_aver, temperature, pressure, pos = main(params)

# Plot all of the quantities
def plot():
plt.figure(figsize=[7,12])
plt.rc('xtick', labelsize=15)
plt.rc('ytick', labelsize=15)
plt.subplot(4, 1, 1)
plt.plot(ene_kin_aver,'k-')
plt.ylabel(r"$$E_{K}", fontsize=20) plt.subplot(4, 1, 2) plt.plot(ene_pot_aver,'k-') plt.ylabel(r"$$E_{P}$$", fontsize=20) plt.subplot(4, 1, 3) plt.plot(temperature,'k-') plt.ylabel(r"$$T$$", fontsize=20) plt.subplot(4, 1, 4) plt.plot(pressure,'k-') plt.ylabel(r"$$P\$", fontsize=20)
plt.show()

plot()



I have tried with different timesteps but no result. I think I have used reduced units everywhere. Epsilon is taken as 1 and sigma is never used (always taken as 1). Box size is the length of the cube grid required (L) to make a system of required reduced density (density as a parameter in the code). It is calculated by the system and no. of particles and density (reduced) is taken as input.

I would also like to know how to calculate the radial distribution function using this code. I know its definition but can't understand how to implement it in code. As far as I can understand, my origin will be (0,0,0) as I have at first in initialise_config function made all the coordinates with respect to the center of mass. But how to implement a function to efficiently calculate the number of particles at a distance $$r$$? Making a histogram corresponding to the radial distance of each LJ particle from origin may work but I am not sure how it will produce the negative parts in the $$\rho$$ axis in the following plot.
.

Any help in implementing this will be much appreciated. I don't have any clue on how to sort these 2 problems.

• I would suggest that you first test your algoright with a simpler problem. For example, try with a spring-mass system that has an analytic solution that you can use to compare. – nicoguaro Mar 31 at 16:32
• @nicoguaro Thank you. I shall try MD with the spring-mass system. But here the algo used is standard velocity verlet . For comparison, in this case, I am using reference data (all my parameters are same as the reference) mmlapps.nist.gov/srs/LJ_PURE/md.htm – Endeavour Apr 1 at 6:39
• The point is to test only the integrator. If you are not able to solve for a simpler problem I doubt you are capable of a more complex one. – nicoguaro Apr 1 at 13:40