Molecular Dynamics Equilibration Step

:slight_smile: ello! I am currently working on an assignment where I have to simulate an ideal gas using fortran 95 and the Lennard-Jones potential.
After programming a lattice of 500 particles (using a fcc) and shifting them a bit (to simulate a gas) I want to reach an equilibrium point using verlet to forward the time. My issue has encountered when for some reason, teh equilibration code doesn’t calculate properly the energies. I was expecting a small dent in the total energy that I was going to correct, but after fixing my energy at -640, I have that by applying this algorithm I get an evolution of the system at energy -300. I have searched and tried everything but can’t seem to get what the error is. I belive both the crea red and potljsubroutine calculate the enrgy correctly, so I believe now that the problem can be when I grab the different positions and velocities ? I am going to paste my current code and would really appreciate some help on this topic. Thank you!!!

PD: Sorry for mistakes in writting, english isn’t my first language.

Equilibration program:

program equilibrio
    use def_precision         
    use variables_comunes
    use verlet_mod

    implicit none
    integer :: np, kcuenta, kk, kpaso, ktotal
    real(kind=doblep), allocatable :: rx(:), ry(:), rz(:), vx(:), vy(:), vz(:), ax(:), ay(:), az(:)
    real(kind=doblep):: ecin, epot, etot, factor, dfiv, d2fiv, tiempo, xnp

    ! Abrir el archivo 'variables.dat' para lectura
    open(unit=10, file='variables.dat')

    ! Leer datos
    read(10, '(i8)') np                   
    read(10, '(f12.6)') pl                
    read(10, '(f12.6)') rc                
    read(10, '(f12.6, 2x, f12.6)') vol, dens 
    read(10, '(f12.6, 2x, f12.6, 2x, f12.6)') etot, ecin, epot  

    close(10)

    ! Asignar tamaño dinámico a los arreglos basado en np
    allocate(rx(np), ry(np), rz(np), vx(np), vy(np), vz(np), ax(np), ay(np), az(np))

    ! Comprobar si los datos leĂ­dos son correctos
    print *, "Energía total: ", etot, "Energía cinética: ", ecin, "Energía potencial: ", epot

    ! Leer datos de posiciones, velocidades y aceleraciones
    open(20, file='resultados.dat', form='unformatted')
    read(20) rx, ry, rz, vx, vy, vz, ax, ay, az
    close(20)

    ! InicializaciĂłn de variables de tiempo y factores
    dt = 1.0d-04 
    dt12 = dt / 2.0d0
    dt2 = dt * dt / 2.0d0
    rc2 = rc * rc
    xnp = dble(np)
    factor = pi * xnp * xnp / (vol * rc * rc * rc)
    corr_ener = 8.d00 * factor * (1.d00 / (3.d00 * rc**6) - 1.d00) / 3.d00
    corr_suma_rvp = 16.d00 * factor * (-2.d00 / (3.d00 * rc**6) + 1.d00)
    corr_sum_r2vpp = 16.d00 * factor * (26.d00 / (3.d00 * rc**6) - 7.d00)

    ! Bucle principal de integraciĂłn
    kpaso = 1
    ktotal = 50
    kcuenta = 0
    do kk = 1, ktotal
        call verlet(np, rx, ry, rz, vx, vy, vz, ax, ay, az, epot, ecin, dfiv, d2fiv)
        etot = ecin + epot

        ! Grabar datos cada kpaso pasos
        if (mod(kk, kpaso) == 0) then
            kcuenta = kcuenta + 1
            tiempo = dble((kcuenta - 1) * kpaso) * dt 
            open(unit=10, file='simulacion.dat', access='append')
            write(10, 9001) tiempo, etot, ecin, epot
            close(10)
        end if
    end do
    write(*,*) 'grabados', kcuenta, 'pasos'

    ! Escribir la configuraciĂłn final
    open(20, file='resultados.dat', form='unformatted')
    write(20) rx, ry, rz, vx, vy, vz, ax, ay, az
    close(20)

9001 format(1pe13.6, 2x, e13.6, 2x, e13.6, 2x, e13.6)

end program equilibrio

Verlet program:

module verlet_mod
    use def_precision
    use variables_comunes
    use subrutinas 

    contains 
    subroutine verlet(np, rx, ry, rz, vx, vy, vz, ax, ay, az, epot, ecin, dfiv, d2fiv)

        implicit none
        !Algoritmo velocidad de Verlet
        !Preparado para 500 particulas con el parametro npmax
        !The Verlet algorithm is the numerical method we used to calculate updates to particle 
        !position based on the total force of each particle. The algorithm is generally used to 
        !integrate Newton’s equations of motion

        !DeclaraciĂłn de variables:
        !Variables de entrada:
        !   np : numero de particulas en el sistema
        !   rx, ry, rz : posiciones de las particulas
        !   vx, vy, vz : velocidades de las particulas
        !   ax, ay, az : aceleraciones de las particulas
        !   dfiv :: dervada primera del potencial respecto el volumen
        !   d2fiv :: derivada segunda del potencial respecto del volumen

        integer(kind=entero), intent(in) :: np
        real(kind=doblep), intent(inout) :: epot
        real(kind=doblep), dimension(npmax), intent(inout):: rx, ry, rz, vx, vy, vz, ax, ay, az
        real(kind=doblep), intent(out) :: dfiv, d2fiv, ecin

        rx=rx+vx*dt+ax*dt2
        ry=ry+vy*dt+ay*dt2
        rz=rz+vz*dt+az*dt2
    
        vx=vx+ax*dt12
        vy=vy+ay*dt12
        vz=vz+az*dt12
        !print*, 'epot dins verlet', epot
        call potlj(np, rx, ry, rz, ax, ay, az, epot, dfiv, d2fiv)
        vx=vx+ax*dt12
        vy=vy+ay*dt12
        vz=vz+az*dt12

        ecin=0.5d00*(sum(vx*vx)+sum(vy*vy)+sum(vz*vz))

        return
     end subroutine verlet
end module verlet_mod

The module where the subroutine of teh Lennard-Jones potential is kept:

module subrutinas
    use def_precision
    use variables_comunes  ! Usa las variables comunes necesarias para la subrutina
    implicit none
    contains

    !SUBRUTINA DEL POTENCIAL DE LENNARD-JONES
    subroutine potlj(np, rx, ry, rz, ax, ay, az, epot, dfiv, d2fiv)
        implicit none
        ! Esta subrutina calcula la energĂ­a potencial total del sistema (epot), las fuerzas entre partĂ­culas
        ! (almacenadas en las aceleraciones ax, ay, az), y las derivadas primera (dfiv) y segunda (d2fiv) del
        ! potencial de Lennard-Jones respecto al volumen.
        
        integer (kind=entero), intent(in) :: np
        real (kind=doblep), dimension(:), intent(in) :: rx, ry, rz
        real (kind=doblep), dimension(:), intent(out) :: ax, ay, az
        real (kind=doblep), intent(out) :: dfiv, d2fiv
        real(kind=doblep), intent(out) :: epot

        ! Variables locales:
        integer (kind=entero) :: i, j   ! ĂŤndices para recorrer pares de partĂ­culas
        real (kind=doblep) :: rrx, rry, rrz, rijx, rijy, rijz   ! Variables para almacenar posiciones relativas
        real (kind=doblep) :: dis2, fmod, a2, a6, a12, aux1    ! Variables para cálculos de distancias, fuerzas, etc.

        ! InicializaciĂłn de acumuladores:
        epot = 0.d00
        dfiv = 0.d00   ! Primera derivada inicializada a 0
        d2fiv = 0.d00  ! Segunda derivada inicializada a 0
        ax = 0.d00     ! Aceleraciones inicializadas a 0
        ay = 0.d00
        az = 0.d00
        !print*, rc2
        ! Bucle doble para recorrer todos los pares de partĂ­culas i-j
        do i = 1, np-1
            rrx = rx(i)  ! Coordenadas de la partĂ­cula i
            rry = ry(i)
            rrz = rz(i)

            do j = i + 1, np
                ! Cálculo de las distancias relativas entre las partículas i y j
                rijx = rrx - rx(j)
                rijy = rry - ry(j)
                rijz = rrz - rz(j)

                ! AplicaciĂłn de condiciones periĂłdicas de contorno para evitar efectos de borde:
                ! Se ajustan las distancias para que las partículas interactúen con sus imágenes más cercanas.
                rijx = rijx - pl * dnint(rijx * pli)
                rijy = rijy - pl * dnint(rijy * pli)
                rijz = rijz - pl * dnint(rijz * pli)

                ! Cálculo de la distancia al cuadrado entre las partículas i y j
                dis2 = (rijx * rijx) + (rijy * rijy) + (rijz * rijz)

                ! Solo calculamos la interacciĂłn si la distancia es menor o igual al radio de corte (rc)
                if (dis2 <= rc2) then
                    a2 = 1.d00 / dis2     ! Inverso de la distancia al cuadrado
                    a6 = a2 * a2 * a2     ! a6 = (1/r^2)^3 = 1/r^6
                    a12 = a6 * a6         ! a12 = (1/r^6)^2 = 1/r^12
                    ! Actualizamos la energĂ­a potencial usando el potencial de Lennard-Jones (epot):
                    epot = epot + (a12 - a6)
                    
                    ! Auxiliar para el cálculo de las derivadas y las fuerzas:
                    aux1 = (2.d00 * a12) + a6
                    
                    ! Suma de las derivadas del potencial respecto al volumen
                    dfiv = dfiv - aux1
                    d2fiv = d2fiv + 26.d00 * a12 - 7.d00 * a6

                    ! Magnitud de la fuerza (fmod) entre las partĂ­culas i y j
                    fmod = aux1 * a2
                    
                    ! Actualizamos las aceleraciones de las partĂ­culas i y j
                    ax(i) = ax(i) + fmod * rijx
                    ay(i) = ay(i) + fmod * rijy
                    az(i) = az(i) + fmod * rijz
                    ax(j) = ax(j) - fmod * rijx
                    ay(j) = ay(j) - fmod * rijy
                    az(j) = az(j) - fmod * rijz
                end if
            end do
        end do

        ! Una vez calculada la interacciĂłn entre todos los pares de partĂ­culas, aplicamos factores comunes:
        epot = 4.d00 * epot + corr_ener           ! Factor de correcciĂłn de energĂ­a potencial
        dfiv = 24.d00 * dfiv + corr_suma_rvp      ! CorrecciĂłn de la primera derivada
        d2fiv = 24.d00 * d2fiv + corr_sum_r2vpp   ! CorrecciĂłn de la segunda derivada

        ! Escalamos las aceleraciones calculadas:
        ax = 24.d00 * ax
        ay = 24.d00 * ay
        az = 24.d00 * az

        ! Finalmente, calculamos las derivadas del potencial respecto al volumen:
        d2fiv = (d2fiv - 2.d00 * dfiv) / (9.d00 * vol * vol)   
        dfiv = dfiv / (3.d00 * vol)                           

        return
    end subroutine potlj
end module subrutinas

Creation of the lattice (and where i store the different values of r, v, a)

PROGRAM crea_red
! Este programa genera un sistema de particulas en una red cubica, aplica un random shift a sus posiciones,
! asigna unas velocidades aleatorias y computa su energia. Se escriben los resultados en un fichero txt.
   
    use def_precision         ! Modulo con las definiciones de precisiĂłn 
    use variables_comunes      ! Modulo con variables comunas
    use subrutinas             ! Modulo que contiene subrutinas: en este caso contiene la subrutina del potencial de Lennard-Jones
    use random_module       ! Modulo para generar numeros aleatorios

    implicit none

    ! DEFINICION DE LAS VARIABLES USADAS:

    integer(kind=entero):: np, i, j, k, iflag, idum
    real(kind=doblep), dimension(npmax):: rx, ry, rz, vx, vy, vz, ax, ay, az
    !Arrays para almacenar posiciones (rx, ry, rz), velocidades (vx, vy, vz) y aceleraciones (ax, ay, az)
    real(kind=doblep):: xnp, pa, pma, factor, px, py, pz, ecin, etotq, ecinq, epot, dfiv, d2fiv
    
    !   xnp: nĂşmero total de partĂ­culas (como double)
    !   pa: parámetro de red (espaciado entre partículas en la red cúbica)
    !   pma: mitad del parámetro de red
    !   factor: factor de escala usado en la correcciĂłn de energĂ­a
    !   ecin: energía cinética
    !   etotq: energĂ­a total establecida (en mi caso es de -640)
    !   ecinq: energía cinética deseada tras ajustar la energía total
    !   dfiv :: dervada primera del potencial respecto el volumen
    !   d2fiv :: derivada segunda del potencial respecto del volumen

    !Valores necesarios para el calculo
    pl=10.d0
    rc=5.d0
    idum = -1 !Para que inicialice el mi_random
    xnp = dble(npmax)
    pli=1.d00/pl
    vol = pl*pl*pl
    dens = xnp/vol
    rc2 = rc*rc
    pa = pl/dble(numk)
    pma = pa/2.d00
    factor = pi*xnp*xnp/(vol*rc*rc*rc)

    !Correcciones de la energĂ­a relacionadas con el potencial Lennard-Jones
    corr_ener=8.d00*factor*(1.d00/(3.d00*rc**6)-1.d00)/3.d00
    corr_suma_rvp = 16.d00*factor*(-2.d00/(3.d00*rc**6)+1.d00)
    corr_sum_r2vpp = 16.d00 * factor*(26.d00/(3.d00*rc**6)-7.d00)

    print*, corr_ener, corr_sum_r2vpp, corr_suma_rvp
    
!! POSICIONES:

    !Colocamos las particulas y las voy contando
    np=0 !Inicializamos
    do i = 0, numk-1
        do j = 0, numk-1
            do k = 0, numk-1
                if (np < npmax) then
                    np = np + 1
                    rx(np) = 0.d00 + dble(i) * pa
                    ry(np) = 0.d00 + dble(j) * pa
                    rz(np) = 0.d00 + dble(k) * pa
                end if
                if (np < npmax) then
                    np = np + 1
                    rx(np) = pma + dble(i) * pa
                    ry(np) = 0.d00 + dble(j) * pa
                    rz(np) = pma + dble(k) * pa
                end if
                if (np < npmax) then
                    np = np + 1
                    rx(np) = 0.d00 + dble(i) * pa
                    ry(np) = pma + dble(j) * pa
                    rz(np) = pma + dble(k) * pa
                end if
                if (np < npmax) then
                    np = np + 1
                    rx(np) = pma + dble(i) * pa
                    ry(np) = pma + dble(j) * pa
                    rz(np) = 0.d00 + dble(k) * pa
                end if

            end do
        end do
    end do
    

    write(*,*) "Se han colocado en la red", np, "particulas"
    iflag = 64  ! Establezco aquĂ­ un numero para iniciar el generador de numeros aleatorios
                ! También podría introducirlo por consola pero de esta manera es más rápido para ver los resultados

    ! Call the mi_random function
    px = generator_random(idum)

! Aplicar perturbaciones aleatorias a las posiciones de las partĂ­culas
    do i=1, np
        rx(i) = rx(i)+(2.d00*generator_random(iflag)-1.d00)*0.1d0*pma
        ry(i) = ry(i)+(2.d00*generator_random(iflag)-1.d00)*0.1d0*pma
        rz(i) = rz(i)+(2.d00*generator_random(iflag)-1.d00)*0.1d0*pma
    enddo
    

    ! Calcular la energĂ­a potencial del sistema usando un potencial Lennard-Jones
    call potlj (np,rx,ry,rz,ax,ay,az,epot,dfiv,d2fiv)

!! VELOCIDADES:
! Asignar velocidades aleatorias entre -1 y 1 para cada partĂ­cula
do i = 1, np
    vx(i) = 2.d00*generator_random(iflag)-1.d00
    vy(i) = 2.d00*generator_random(iflag)-1.d00
    vz(i) = 2.d00*generator_random(iflag)-1.d00
enddo

!Calculamos los momentos
px = sum(vx)
py = sum(vy) 
pz = sum(vz) 

!TendrĂ­an que ser 0 (Velocidad CM = 0) -> Corregimos para que sea nulo
px = px/xnp  !Velocidad promedio
py = py/xnp
pz = pz/xnp
vx = vx-px !Restamos esta velocidad promedio para equilibrar el sistema
vy = vy-py
vz = vz-pz

! Recalculamos con los ajustes
px = sum(vx)
py = sum(vy)
pz = sum(vz)
ecin = 0.5d00 * (sum(vx*vx)+sum(vy*vy)+sum(vz*vz))

!Imprimimos valores por consola:
write(*,*)
write(*,*) 'Momentos:', px, py, pz
write(*,*) 'Energía cinética:', ecin, "Energía potencial:", epot, "Energía total:", ecin+epot

! Ajustamos la energĂ­a total al valor definido por el usuario 
    !write(*,*) "¿Qué energía total desea fijar?"
    !read(*,*) etotq
    etotq = -640.d0
    ecinq = etotq - epot
    if (ecinq.le.0.d00) then 
        write(*,*) "Error: energía cinética negativa" ! Nos aseguramos que la energía cinética no puede ser negativa
        stop
    end if
    !Ajustamos las velocidades en consecuencia de esta energĂ­a
    factor = dsqrt(ecinq / ecin)
    vx = factor * vx
    vy = factor * vy
    vz = factor * vz

!Recalculamos para verificar
px = sum(vx)
py = sum(vy)
pz = sum(vz)
write(*,*)
write(*,*) 'Despues de modificar la energĂ­a:'
write(*,*) 'Momentos:', px, py, pz
write(*,*) 'Energía cinética:', ecinq, "Energía potencial:", epot, "Energía total:", ecinq+epot

! FICHEROS:

open (10, file='variables.dat')
! Abrimos el archivo 'variables.dat' para escribir las variables principales en formato de texto 
write(10, '(i8)') np               ! NĂşmero de partĂ­culas
write(10, '(f12.6)') pl            ! Lado de la caja
write(10, '(f12.6)') rc             ! Radio de corte del potencial
write(10, '(f12.6, 2x, f12.6)') vol, dens  ! Volumen y Densidad
write(10, '(f12.6, 2x, f12.6, 2x, f12.6)') ecinq + epot, ecinq, epot  ! EnergĂ­as

close(10)
! Abrimos otro archivo llamado 'resultados.dat' para guardar los datos en formato binario 
open(20, file='resultados.dat', form='unformatted') 
    write(20) rx,ry,rz,vx,vy,vz,ax,ay,az
close(20)

stop

END PROGRAM crea_red

Common variables:

module variables_comunes

use def_precision

!preparado para 500 partĂ­culas

!podemos modificar npmax con un numero k adecuado :: npmax = 4*k**3

! k = 1, 2, 3, 4, 5, 6...

! npmax = 4, 32, 108, 256, 500, 864 ...

implicit none

integer(kind=entero), parameter :: npmax=500

integer(kind=entero), parameter :: numk=5

real(kind=doblep), parameter :: pi=3.141592653589d00

real(kind=doblep):: pl, pli, vol, dens, rc, rc2, dt, dt12, dt2

real(kind=doblep):: corr_ener=0.d00, corr_suma_rvp=0.d00, corr_sum_r2vpp=0.d00

end module variables_comunes

1 Like

If you post it on github and make it fpm installable, then people can run it. Most people don’t have time to manually copy & paste and compile things from your post.

3 Likes

Is it possible to post a plot (figure) of the total energy as a function of simulation time (e.g., by generating a PNG image file using Gnuplot and upload it to your first post)? As far as I experienced, the total energy can show an initial increase (or decrease) depending on the initial configuration and then reach a near constant value (with some fluctuations). So, I guess it is not too surprising even if the total energy is somewhat different from that of the initial configuration (e.g., when the latter has some “atomic clash” and the accuracy of the integrator is poor at the initial stage).

3 Likes

That’s right. The initial position is typically random, which is not equilibrated, so the energy typically decreases until you reach equilibrium, then it will oscillate. When you choose a time step, make sure you get several points on every oscillation of the energy to resolve everything.This will be apparent from the plot.

Yes, I was aware that I would have this but I don’t think the error is due to that.
Initially I have that the total energy is -640, the kinetic 1011 and the potential -1650.
On the FIRST iteration I have a prominent decrease.

0.000000E+00  -3.051933E+02   9.914800E+02  -1.296673E+03
 1.000000E-04  -3.053454E+02   9.913274E+02  -1.296673E+03
 2.000000E-04  -3.054983E+02   9.911742E+02  -1.296672E+03
 3.000000E-04  -3.056520E+02   9.910205E+02  -1.296672E+03

These are the first iterations where the first column is time, the second etot, the third, ecin and lastly epot

1 Like

I’ve tried running your code a bit, but cannot find some necessary files… (definitions and random numbers). Maybe attach all the necessary files in the first post or somewhere (so that people can download & try them out directly)?

For the difference of energies at the first iteration, it seems that the program calls verlet() and then prints energies, so at least one step is run from the initial configuration (correct…?). To get more info, how about calling potlj() immediately after reading the initial configuration + calculate ecin = 0.5d0 * sum( vx**2 + vy**2 + vz**2 ), to make sure that they agree with the values computed via crea_red? If they agree, we can probably focus on the first iteration of verlet() (and also the computation of forces or accelerations in potlj()).

These are all the codes I am using. The crea_red program defines the lattice and initial values of the energy (I am fixing it at -640) and velocities.
subrutinas.f90 (4.5 KB)
mi_random.f90 (1.9 KB)
equilibrio.f90 (2.4 KB)
crea_red.f90 (6.3 KB)
verlet.f90 (1.6 KB)

I followed your suggestion and I ended up having a value of 0.0 when calling potlj and a different value for ecin at the equilibration program. This shouldn’t happen because at the crea red program I calculated the potential energy using the same rx,ry,rz no? Am I not reading them right? Why is this happening?
Thank you for your imput.

1 Like

Sometimes errors become obvious after visualization. It may require a little effort to write an export procedure, but then you can use mature tools such as VMD or Paraview,

I still can’t manage to fix this.
I now have a value for the epot after calling (I wasn’t defining rc2) it but I still have a value much smaller than the initial.

I’ve downloaded and tried compiling your codes (in the thread above), but still the following modules seem missing… (so I cannot run it yet).

use def_precision
use variables_comunes  

As for why the epot becomes 0, I initially imagined that potlj() needs some initialization of related variables (possibly defined in variables_comunes?) but such an initialization seems not performed in the above test (in equilibrio.f90) nor verlet(). But I cannot say for sure unless I read the code more carefully… (with variables_comunes).

Anyway, some other approaches for debugging are:

  • When importing variables from modules, use the only statement such that
    the reader can easily understand what variables are imported, e.g.
     use my_mod, only: foo, baa, ...
    
  • Try using compiler options for various checks, e.g.:
    gfortran -fcheck=all mycode.f90
    ifort -check all mycode.f90
    
  • Print key variables as much as possible (e.g., rx(1) and rx(np)) to see they have expected values.
  • Etc, etc, …

There might also be some other issues (e.g. typos like ay → ax!), but at the moment, I guess there are some other reasons (most likely the need for some initialization?)

In the case of gfortran, it may also be useful to try:

gfortran -fcheck=all -Wall -Wextra -finit-real=snan -finit-integer=-999 mycode.f90

which may reveal some problem if there are some uninitialized variables etc.

With these things it’s always best to start simple. Take just two particles bound by the LJ potential. Run a short simulation and visualise the trajectory. Is the total energy conserved?

1 Like

Here is what you can do:

  • post your code on github and make it easy to build (ideally fpm)
  • use two particles
  • plot kinetic, potential and total energy

The kinetic and potential will change, but total energy should be constant. Ensure your time step is several points per cycle of the potential energy to resolve everything.