0

I'm trying to implement a simple molecular dynamic simulation in Python for N atoms with periodic boundary conditions using minimum image convention. Here is my code:

import numpy as np
import matplotlib.pyplot as plt
from scipy.constants import Boltzmann

# Constants
mass_of_argon = 39.948  # amu
dt = 0.1
num_steps = 5000
n = 2
d = 4.0
epsilon = 0.0103  # eV
sigma = 3.405  # A
kb = 8.617333262e-5  # eV/K
T_in = 95
cutoff = 2.5 * sigma
cutoff2 = cutoff ** 2  

N = n**3
L = n * d


def initialize_positions(n, d):
    pos = np.zeros((N, 3))
    count = 0
    for i in range(n):
        for j in range(n):
            for k in range(n):
                pos[count] = np.array([i * d, j * d, k * d])
                count += 1
    return pos


def initialize_velocities(T, N):
    vel = np.random.randn(N, 3)  
    vel -= np.mean(vel, axis=0)  
    T_current = mass_of_argon * np.sum(vel**2) / (3 * kb * N)
    vel *= np.sqrt(T / T_current)  
    return vel


def get_acc_pot(X, L):
    forces = np.zeros((N, 3))
    potential_energy = np.zeros(N)
    D = X[None, :, :] - X[:, None, :]
    D -= np.rint(D / L) * L  
    D2 = np.sum(D**2, axis=-1)
    D2_safe = np.maximum(D2, 1e-10)  
    DS2 = np.divide(sigma**2, D2_safe)
    valid = D2 < cutoff2  
    DS6 = np.where(valid, DS2**3, 0) 
    
    for i in range(N - 1):
        for j in range(i + 1, N):
            r = D[i, j, :]
            r2 = D2[i, j]
            rs6 = DS6[i, j]
            F = np.where(valid[i, j], 48 * epsilon * rs6 * r * (rs6 - 0.5) / r2, 0)
            forces[i, :] -= F
            forces[j, :] += F
            potential_energy[i] += np.where(valid[i, j], 4 * epsilon * (rs6 * (rs6 - 1)), 0)
            potential_energy[j] += np.where(valid[i, j], 4 * epsilon * (rs6 * (rs6 - 1)), 0)
    
    return forces / mass_of_argon, potential_energy


def update_pos_vel(X, V, A, dt, L):
    X_new = X + V * dt + 0.5 * A * dt**2
    X_new %= L  
    A_new, potential_energy = get_acc_pot(X_new, L)
    V_new = V + 0.5 * (A + A_new) * dt
    return X_new, V_new, A_new, potential_energy


def K_energy(V):
    kinetic_energy = 0.5 * mass_of_argon * np.sum(V**2, axis=1)
    return kinetic_energy


def run_md(dt, num_steps, T):
    pos = initialize_positions(n, d)
    vel = initialize_velocities(T, N)
    acc, pot_energy = get_acc_pot(pos, L)
    pos_history = np.zeros((num_steps, N, 3))
    vel_history = np.zeros((num_steps, N, 3))
    ke_history = np.zeros((num_steps, N))
    pe_history = np.zeros((num_steps, N))
    
    for step in range(num_steps):
        pos, vel, acc, pot_energy = update_pos_vel(pos, vel, acc, dt, L)
        pos_history[step] = pos
        vel_history[step] = vel
        pe_history[step] = pot_energy
        ke_history[step] = K_energy(vel)
    
    return pos_history, vel_history, ke_history, pe_history


pos_hist, vel_hist, ke_hist, pe_hist = run_md(dt, num_steps, T_in)

def plot_energy(i):
    t_vals = np.linspace(0, dt * num_steps, num_steps)
    plt.figure(figsize=(10, 5))
    plt.plot(t_vals, ke_hist[:, i], label=f'Kinetic energy for atom {i+1}')
    plt.plot(t_vals, pe_hist[:, i], label=f'Potential energy for atom {i+1}')
    plt.plot(t_vals, ke_hist[:, i] + pe_hist[:, i], label=f'Potential energy for atom {i+1}')
    plt.xlabel('Time (ps)')
    plt.ylabel('Energy (eV)')
    plt.legend()
    plt.title(f'Energy evolution for Atom {i+1}')
    plt.show()

def plot_x(i):
    t_vals = np.linspace(0, dt * num_steps, num_steps)
    plt.figure(figsize=(10, 5))
    plt.plot(t_vals, pos_hist[:, i, 0], label='x-Position for atom {i+1}')
    plt.xlabel('Time (ps)')
    plt.ylabel('Position (Å)')
    plt.legend()
    plt.title(f'x-position evolution for Atom {i+1}')
    plt.show()

def plot_v(i):
    t_vals = np.linspace(0, dt * num_steps, num_steps)
    plt.figure(figsize=(10, 5))
    plt.plot(t_vals, vel_hist[:, i, 0], label='x-velocity')
    plt.xlabel('Time (ps)')
    plt.ylabel('Velocity (Å/ps)')
    plt.legend()
    plt.title(f'x-Velocity evolution for Atom {i+1}')
    plt.show()


plot_energy(1)
plot_x(1)
plot_v(1)

The problem is my implementation of min image convention does not seem to work properly, although I made sure to include it both before and after updating the positions (as you can see in get_acc_pot function). It is instead making a weird jump when reaching the boundary! Here is a plot of the position of a random atom:

enter image description here

It is so hard to spot where I went wrong when I have been working on the same code for multiple days and thought that a second eye might be a good idea! Any tips/advice is much appreciated!:)

4
  • Can you elaborate a bit more about what you expect to happen. Right now, you set the position as X_new %= L in update_pos_vel, which means that the position X of the atom will have L subtracted from it if X>L. Commented Feb 7, 2025 at 16:21
  • @Slavensky I'm trying to implement periodic boundary conditions so that if the atom leaves the boundary of the system it appears from the other side, i.e if it leaves from the bottom boundary it just pops out from the upper one! But that does not seem to be whats actually happening.. Commented Feb 7, 2025 at 16:58
  • Well, from the position plot above, it looks like it does exactly that: The position of atom 2 becomes larger than $L=8$, so 8 should be retracted from its position, thus the position will jump from 8 to 0. What is the difference between that behaviour and the one you describe? Commented Feb 7, 2025 at 18:09
  • @Slavensky Yes technically it does, but the idea was for it to "disappear" when hitting the upper boundary, and reappear from the lower one as in PBC instead of making this reflection behaviour Commented Feb 7, 2025 at 18:21

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.