Simulation backends for atooms

This is a demo of running a molecular dynamics simulation in the NVT ensemble for the 80-20 Kob-Ansersen binary mixture using the backend shipped with atooms-dynamics. We use the symplectic Nosé-Poincaré algorithm to equilibrate the system at normal liquid conditions. The interactions are computed using the built-in Fortran 90 code shipped with atooms-models.

This is explicitly written down as a monolitic piece of notebook.

Setup

Import the necessary modules and turn off verbosity

import numpy
import matplotlib
import matplotlib.pyplot as plt
import atooms.core
from atooms.system import Thermostat
from atooms.simulation import Simulation, Scheduler, write_trajectory, store
from atooms.dynamics import NosePoincare, VelocityVerlet
from atooms.trajectory import TrajectoryXYZ

atooms.core.progress.active = False
atooms.core.utils.setup_logging(level=40, update=True)

Create the starting configuration

We create a small system of \(N=100\) particles at the canonical density \(\rho=1.2\) and composition 80-20

from atooms.system import System
system = System(N=100)
system.density = 1.2
system.composition = {'A': 80, 'B': 20}
print(system)
system composed by N=100 particles
with chemical composition C={'A': 80, 'B': 20}
with chemical concentration x={'A': 0.8, 'B': 0.2}
enclosed in a cubic box at number density rho=1.200000

Define the interaction model

We look for the interaction model of the Kob-Andersen mixture in the atooms.models database.

from atooms import models
from pprint import pprint
pprint(models.get('kob_andersen'))
{'cutoff': [{'parameters': {'rcut': [[2.5, 2.0], [2.0, 2.2]]},
             'type': 'cut_shift'}],
 'potential': [{'parameters': {'epsilon': [[1.0, 1.5], [1.5, 0.5]],
                               'sigma': [[1.0, 0.8], [0.8, 0.88]]},
                'type': 'lennard_jones'}],
 'reference': 'W. Kob and H. C. Andersen, Phys. Rev. Lett. 73, 1376 (1994)'}

We define a new model, changing the cutoff to a smoother one, and set up the interaction

model = {'cutoff': [{'parameters': {'rcut': [[2.5, 2.0], [2.0, 2.2]]},
                     'type': 'linear_cut_shift'}],
         'potential': [{'parameters': {'epsilon': [[1.0, 1.5], [1.5, 0.5]],
                                       'sigma': [[1.0, 0.8], [0.8, 0.88]]},
                        'type': 'lennard_jones'}],
}
system.species_layout = 'F'  # This ensures species are Fortran-styled
system.interaction = models.f90.Interaction(model)
# system.interaction.neighbor_list = f90.VerletList(skin=0.3)
>>>

Run the simulation

We create a simulation backend that integrates the equation of motions using the Nosé-Poincaré algorithm at a temperature \(T=2.0\). The simulation s fairly short because at this temperature the system is a normal liquid and thermalies rapidly. We also add

system.set_temperature(2.0)
system.thermostat = Thermostat(temperature=2.0, mass=5.0)
bck = NosePoincare(system, timestep=0.002)
sim = Simulation(bck)
data = {}
# This adds a callback to store every 100 steps several system attributes in the data dictionary
# Check the atooms.simulation.observers.store() doc for more info.
sim.add(store, 100, ['steps', 'potential energy per particle',
                     'kinetic energy per particle', 'temperature'], data)

Run the simulation and show a snapshot of the final state of the system (this requires ovito)

sim.run(int(1e4))

# Temporarily set alphabetical species for ovito
system.species_layout = 'A'
# system.show('ovito', outfile='_images/config.png')
system.species_layout = 'F'
_images/config.png

Check that the kinetic temperature looks good

plt.plot(data['steps'], data['temperature'], 'o', label='Kinetic temperature')
plt.plot(data['steps'], [system.thermostat.temperature]*len(data['steps']), '-', label='Thermostat temperature')
plt.xlabel('Steps')
_images/kinetic.png

We now do a production run in which we store configurations in a trajectory file, written in xyz format. This time we store the configurations using an exponential sampling, which allows to resolve both short and long time dynamics. This will take about a minute.

th = TrajectoryXYZ('/tmp/config.xyz', 'w')
bck = VelocityVerlet(system, timestep=0.006)
sim = Simulation(bck)
# This adds a callback to write the system in the th trajectory using an exponential schedule
sim.add(write_trajectory, Scheduler(block=[2**n for n in range(10)]),
                                    variables=['species', 'pos', 'vel'], trajectory=th)
data = {}
sim.add(store, 100, ['steps', 'potential energy per particle',
                     'total energy per particle', 'temperature'], data)
sim.run(int(1e5))
th.close()

Chech that the total energy is conserved

plt.plot(data['steps'], data['total energy per particle'], '-', label='Total')
plt.plot(data['steps'], data['potential energy per particle'], '-', label='Potential')
plt.xlabel('Steps')
_images/total.png

Analyze the trajectory file

The trajectory file can be analyzed using the atooms-postprocessing package. We compute the velocity auto-correlation function \(Z(t)\) over a short time grid

from atooms.postprocessing import VelocityAutocorrelation

with TrajectoryXYZ('/tmp/config.xyz') as th:
    cf = VelocityAutocorrelation(th, tgrid=th.times[:9])
    cf.compute()
plt.plot(cf.grid, cf.value, '-o')
plt.xlabel('t')
plt.ylabel('Z(t)')
_images/vacf.png

Here we compute instead the mean square displacement

from atooms.postprocessing import MeanSquareDisplacement

with TrajectoryXYZ('/tmp/config.xyz') as th:
    cf = MeanSquareDisplacement(th)
    cf.compute()
plt.plot(cf.grid[:20], cf.value[:20], '-o')
plt.xlabel('t')
plt.ylabel('MSD')
_images/msd.png