We again model a box of particles with pairwise Lennard-Jones potential.
For the pairwise interactions:
\[F(r) = \frac{1}{r^{13}} - \frac{1}{r^7} \tag{1},\]
The corresponding potential function is
\[U(r) = \int{-F(r)dr} = \frac{1}{12r^{12}}-\frac{1}{6r^{6}}. \tag{2}\] We can then compute the total potential for the particles in the box. We consider the periodic boundary condition, so the minimum image convention is also applied.
# pairwise force
potential <- function(r2){
# r2: r square (not r to save a square root operation)
r4 = r2*r2
r6 = r4*r2
r12 = r6*r6
u = 1/12/r12 - 1/6/r6
return (u)
}
r_all = seq(0, 4, by = 0.01)
plot(r_all, sapply(r_all, potential), type = "l", col = 1,
xlab = "r", ylab = "U", xlim = c(0,4), ylim = c(-0.2,0.3))
abline(h = 0, lty = 3, col = 2)
# potential between particles i and j
potential_ij <- function(xi, xj, a){
# xi: position of particle i (a vector of size 2)
# xj: position of particle j (a vector of size 2)
# a: box size
dx = xi - xj
dx = dx - round(dx/a)*a # minimum image convention
rij2 = sum(dx*dx)
return(potential(rij2))
}
potential_tot <- function(x_all, a){
# Ntot: number of particles
# x_all: a vector of 2 * Ntot
# a: box size
Ntot = length(x_all)/2
p_tot = 0
for (i in 1:(Ntot-1)){
xi = x_all[(2*i-1):(2*i)]
for (j in (i+1):Ntot){
xj = x_all[(2*j-1):(2*j)]
p_ij = potential_ij(xi,xj, a)
p_tot = p_tot + p_ij
}
}
return(p_tot)
}
Interestingly, we don’t need to consider the kinetic energy in the Boltzmann distribution, as the kinetic energy is only velocity dependent and can be integrated out in the partition function (we will not discuss this Statistical Mechanics topic in details). Thus, in the Metropolis-Hastings algorithm, we just need to consider the particle positions and the potential energy and just omit the velocities and the kinetic energy.
To efficiently sample another configuration of the system, we propose a local move, which involves a randomly selected particle and a small displacement. Such a local move would allow a small change in the total energy; therefore, the proposed move is more likely to be accepted.
Below shows the calculation of the energy differences after a proposed local move. Only potential terms related to the moved particle \(i\) are computed.
dp <- function(x_all, a, x_new, i){
# x_all: a vector f 2 *Ntot
# a: box size
# x_new: proposed position of a particle i
# i: index of the moved particle
xi = x_all[(2*i-1):(2*i)]
Ntot = length(x_all)/2
p_old = 0
p_new = 0
for (j in 1:Ntot){
if(i != j){
xj = x_all[(2*j-1):(2*j)]
p_old = p_old + potential_ij(xi, xj, a)
p_new = p_new + potential_ij(x_new, xj, a)
}
}
return(p_new - p_old)
}
Q: How much improvement is this method in terms of computational efficiency? Is there a way to further improve the efficiency?
Specifying the initial configuration. Note that, in MCMC, only positions are needed. We also make the box size slightly larger (why?).
set.seed(10)
ntot = 25
a = 6.25
grid_x = seq(0, 5, by = 1.25)
grid_y = seq(0, 5, by = 1.25)
mat_xy = as.matrix(expand.grid(grid_x, grid_y))
x0 = c(t(mat_xy))
xy0 = matrix(x0, ncol = 2, byrow = TRUE)
plot(xy0[,1], xy0[,2], type = 'p', xlab = "X", ylab = "Y", xlim = c(0, a), ylim = c(0,a))
potential_tot(x0, a) # initial potential
## [1] -2.24004
Use Metropolis-Hastings algorithm to sample particle configurations.
metropolis_particle <- function(ntot, cal_e, cal_de, x0, dxmax, nstep, temp, a){
# ntot: number of particles
# cal_e: name of the function to compute the total energy
# cal_de: name of the function to compute the energy differences
# x0: initial configuration (a vector of size 2*ntot)
# dxmax: maximum displacement in a proposed move (a vector of size 2)
# nstep: number of MCMC steps
# temp: temperature (scaled)
# a: box size
x = matrix(0, nrow = (nstep + 1), ncol = 2*ntot) # positions
e = numeric(nstep + 1) # total energy
x[1, ] = x0
e[1] = cal_e(x0, a)
num_accept = 0
for(i in 1:nstep){
k = sample(x = ntot, size = 1) # randomly select a particle
dx = runif(n = 2, min = -dxmax, max = dxmax)
x_new = x[i, (2*k-1):(2*k)] + dx
x_new = x_new - floor(x_new/a) * a # periodic boundary condition
de = cal_de(x[i,], a, x_new, k)
if((de < 0) || (runif(1) < exp(-de/temp))){
x[i+1,] = x[i,]
x[i+1, (2*k-1):(2*k)] = x_new
e[i+1] = e[i] + de
num_accept = num_accept + 1
}else{
x[i+1,] = x[i,]
e[i+1] = e[i]
}
}
print(paste0("Acceptance rate ", num_accept/nstep))
return(list(x = x, e = e))
}
MCMC sampling - the initial phase. This is needed as the initial configuration has a higher energy state for \(T = 0.05\). The parameter dxmax was selected to make the acceptance rate around 40% - 50%.
set.seed(10)
dxmax = c(0.25, 0.25)
temp = 0.05
nstep = 5*10^3
results_init = metropolis_particle(ntot = ntot, cal_e = potential_tot, cal_de = dp, x0 = x0,
dxmax = dxmax, nstep = nstep, temp = temp, a = a)
## [1] "Acceptance rate 0.4512"
plot(0:nstep, results_init$e, type = "l", col = 2, lty = 1, xlab = "Step", ylab = "Total Energy", xlim = c(0,nstep))
x1 = results_init$x[nstep + 1,]
A particle configuration after the initial phase of the MCMC sampling.
xy_1 = matrix(x1, ncol = 2, byrow = TRUE)
plot(xy_1[,1], xy_1[,2], type = 'p', xlab = "X", ylab = "Y")
A second phase of MCMC sampling, prepared for the RDF calculation later.
nstep = 1*10^4
results_final = metropolis_particle(ntot = ntot, cal_e = potential_tot, cal_de = dp, x0 = x1,
dxmax = dxmax, nstep = nstep, temp = temp, a = a)
## [1] "Acceptance rate 0.4079"
plot(0:nstep, results_final$e, type = "l", col = 2, lty = 1, xlab = "Step", ylab = "Total Energy", xlim = c(0,nstep))
Q: Can we propose a move that involves displacements of multiple particles?
The RDF is computed by an ensemble average from the second phase of the MCMC sampling.
cal_rdf <- function(ntot, results, a, dr){
# ntot: number of particles
# results: simulation output matrix
# a: box size
# dr: bin size
nt_all = nrow(results)
r_all = seq(dr, a/2, by = dr)
nr = length(r_all)
counts = numeric(nr)
for(nt in c(1:nt_all)){
x_all = results[nt, ]
for(i in 1:ntot){
xi = x_all[2*i-1]
yi = x_all[2*i]
for(j in 1:ntot){
if(i != j){
xj = x_all[2*j-1]
yj = x_all[2*j]
dx = xi - xj
dx = dx - round(dx/a)*a
dy = yi - yj
dy = dy - round(dy/a)*a
rij = sqrt(dx**2 + dy**2)
ind = ceiling(rij/dr)
if(ind <= nr){
counts[ind] = counts[ind] + 1
}
}
}
}
}
r_mean = r_all-0.5*dr
rho = counts/nt_all/ntot/2/pi/dr/r_mean
rho_0 = ntot/a**2
rdf = rho/rho_0
return(list(r = r_mean, rdf = rdf))
}
results_rdf = cal_rdf(ntot = ntot, results = results_final$x, a = a, dr = 0.05)
plot(results_rdf$r, results_rdf$rdf, type = "l", col = 2, xlab = "r", ylab = "rdf")
Q: Compare the results with those from an MD simulation. Which method is more efficient?