Please upload one PDF file to Gradescope (Entry code: K326P6).
For this homework you will implement code (on PrairieLearn) to perform molecular dynamics simulations using periodic boundary conditions. You specifically should implement functions that determine the coordinates of a particle, assuming periodic boundary conditions, that determine the vector that connects two particles (as well as its length), and functions for forces and energies (please see PrairieLearn for details and instructions).
Often, the goal of atomic simulations is to measure bulk properties of a system. “Bulk” means that one wishes to describe the many-body interactions of an infinite system (the so-called thermodynamic limit). A particle in a bulk system should interact with an infinite volume of particles around it; it cannot be at a surface or boundary, where it will experience different interactions.
Of course, we can simulate only a finite system on a computer, so we must make an approximation to the thermodynamic limit. Typically, the solution is to implement Periodic Boundary Conditions (PBC). This is similar to old video games where, when you come out of one side of the screen, you re-enter on the other side.
The effect of PBC is that a particle in the simulation box ``sees’’ all other particles replicated on all sides of the simulation box, thus creating the effective interactions of a bulk system. A technical limitation of PBC is that there will be artificial spatial correlations on the order of the box size. However, it is a standard simulation technique and this artifact can be analyzed and mitigated by studying the effect of systematically increasing the system size.
It is important to ensure that you are enforcing PBC correctly; it can be tricky to get this right!
Once you have implemented the corresponding functions correctly, you
will implement a molecular dynamics code to model a Lennard-Jones gas.
The Lennard-Jones potential is:
For simplicity, we will work in reduced units (see e.g., p.40 of
Frenkel & Smit). This means lengths are expressed in terms of
We will simulate
Note: You are welcome to spend some time experimenting with other ways to initialize your simulation, but it is non-trivial. For example, starting all your particles at the same spot is a bad idea. The Lennard-Jones potential has a repulsive core, so your system will start with effectively infinite potential energy. Because you are working in the micro-canonical ensemble, energy is conserved, so the kinetic energy will explode and the simulation will be unstable.
You’ll also want to get some output from your simulation. We’re
usually less interested in the precise positions of all the atoms as we
are in some physical properties. The simplest one to examine is the
energy. In case you’ve forgotten, let us remind you precisely what the
energy means in this context: First, the energy of the system is the sum
of the kinetic and potential energies. The kinetic energy is simply the
sum over all atoms of
To save you some effort, you will not have to implement the entire molecular dynamics code. You should peruse the following code to see the flow of logic in a simple molecular dynamics simulation:
import numpy as np
# Everyone will start their gas in the same initial configuration.
# ----------------------------------------------------------------
def InitPositionCubic(Ncube, L):
"""Places Ncube^3 atoms in a cubic box; returns position vector"""
= Ncube**3
N = np.zeros((N,3))
position = L/Ncube
rs = L/2 - rs/2
roffset = 0
n # Note: you can rewrite this using the `itertools.product()` function
for x in range(0, Ncube):
for y in range(0, Ncube):
for z in range(0, Ncube):
if n < N:
0] = rs*x - roffset
position[n, 1] = rs*y - roffset
position[n, 2] = rs*z - roffset
position[n, += 1
n return position
def InitVelocity(N, T0, mass=1., seed=1):
= 3
dim
np.random.seed(seed)# generate N x dim array of random numbers, and shift to be [-0.5, 0.5)
= np.random.random((N,dim))-0.5
velocity = np.sum(velocity, axis=0)/N # get the average along the first axis
sumV -= sumV # subtract off sumV, so there is no net momentum
velocity = np.sum(velocity*velocity) # calculate the total of V^2
KE = np.sqrt(dim*N*T0/(mass*KE)) # calculate a scaling factor
vscale *= vscale # rescale
velocity return velocity
# The simulation will require most of the functions you have already
# implemented above. If it helps you debug, feel free to copy and
# paste the code here.
# We have written the Verlet time-stepping functions for you below,
# `h` is the time step.
# -----------------------------------------------------------------
def VerletNextR(r_t, v_t, a_t, h):
"""Return new positions after one Verlet step"""
# Note that these are vector quantities.
# Numpy loops over the coordinates for us.
= r_t + v_t*h + 0.5*a_t*h*h
r_t_plus_h return r_t_plus_h
def VerletNextV(v_t,a_t,a_t_plus_h,h):
"""Return new velocities after one Verlet step"""
# Note that these are vector quantities.
# Numpy loops over the coordinates for us.
= v_t + 0.5*(a_t + a_t_plus_h)*h
v_t_plus_h return v_t_plus_h
# Main Loop.
# ------------------------------------------------------------------------
# R, V, and A are the position, velocity, and acceleration of the atoms
# respectively. nR, nV, and nA are the next positions, velocities, etc.
# There are missing pieces in the code below that you will need to fill in.
# These are marked below with comments:
def simulate(Ncube, T0, L, M, steps, h):
"""Initialize and run a simulation in a Ncube**3 box, for steps"""
= Ncube**3
N = InitPositionCubic(Ncube, L)
R = InitVelocity(N, T0, M)
V = np.zeros((N,3))
A
= np.zeros(steps)
E
for t in range(0, steps):
= my_kinetic_energy(V, M)
E[t] += ## calculate potential energy contribution
E[t] = ## calculate forces; should be a function that returns an N x 3 array
F = F/M
A = VerletNextR(R, V, A, h)
nR ## from PrairieLearn HW
my_pos_in_box(nR, L)
= ## calculate forces with new positions nR
nF = nF/M
nA = VerletNextV(V, A, nA, h)
nV
# update positions:
= nR, nV
R, V return E
# You may adjust the gas properties here.
# ---------------------------------------
# mass
= 48.0
M
# number of Particles
= 4
Ncube = Ncube**3
N
# box side length
= 4.2323167
L
# temperature
= 0.728 T0
Note: The slowest part of a molecular dynamics simulation is computing the distances and the forces. If you are interested in speeding up your code, consider the following tips.
%lprun
(an example
is here) to see how much time is spent on individual lines in a
function.for
loops can be slow. Whenever possible, allow
numpy
(or python builtin’s) to manipulate vectors for you
rather than looping over all elements. Often, using vectors in
numpy
results in much neater looking code as well.You can vary the time step by changing the variable h just above the Verlet time-stepping functions. Using a time step of 0.01, run your simulation for at least 1000 steps (a run of 1000 steps should take about 10 minutes to complete). Plot an energy trace of the run (the total energy at each time step in the simulation) and include in your report. (As a check on your energy trace, look to see if your total energy remains approximately constant.)
Initially, we used a time step of 0.01, which is probably not optimal and may not even be stable! It is important to be in the habit of ensuring that your time step is neither too small (your system will never evolve from its initial configuration) nor too large (your numerical integration will be unstable and the velocities will explode). Here, our goal is to find the largest time step that gives a stable mean and keeps total energy fluctuations within 5% of the mean. This would be the largest acceptable time step, and you will generally want to work with a time step that is an order of magnitude smaller than this to mitigate the time step error.
Make a graph of the standard deviation of the total energy versus time step size. In other words, you are trying to see how the energy fluctuates with respect to your choice of time step. For this you need to make several simulations using as many as 10 different time steps.
What happens if you pick a very large time step? Why?
From your graph, determine the largest time step one can use and still maintain energy conservation (a stable mean and fluctuations within 5% of the mean).
So far, we have primarily used the velocity Verlet integrator.
This isn’t the most obvious choice. Instead, one might imagine that we
could simply Taylor expand
Prove that the Verlet algorithm is time-reversal invariant. That
is, show that if we use
State two possible problems with finite-precision arithmetic about the Verlet time-stepping algorithm.