Learning Objectives¶
By the end of this lecture, you should be able to:
- Differentiate between conservative and non-conservative forces and compute forces on a system of particles using potential energy functions.
- Derive the Verlet integration algorithm from the Taylor series expansion of particle positions.
- Implement the Verlet algorithm to simulate the motion of particles interacting via the Lennard-Jones potential.
- Calculate velocities and energies within the Verlet framework to analyze kinetic, potential, and total energy conservation in the system.
Forces¶
To solve Newton’s equations of motion for a system of particles, we need to calculate the forces acting on each particle. These forces are then used to update the positions and velocities over time. Forces can be classified into two main types: conservative forces and non-conservative (dissipative) forces.
Conservative Forces¶
Conservative forces are those for which the work done moving an object between two points is independent of the path taken—it depends only on the initial and final positions. Examples include gravity, electrostatic forces, and elastic spring forces. Conservative forces depend solely on the positions of the particles. For such forces, we can define a potential energy function such that the force is the negative gradient of the potential energy, i.e.,
Since the potential energy is defined up to an arbitrary constant, adding a constant to doesn’t affect the force. In systems with only conservative forces, the total mechanical energy—sum of kinetic energy and potential energy —is conserved, i.e.,
Non-Conservative (Dissipative) Forces¶
Non-conservative or dissipative forces are forces for which the work done depends on the path taken and typically lead to a loss of mechanical energy from the system. This lost mechanical energy is usually transformed into other forms, such as heat. Examples include friction, air resistance, and viscous drag forces. In the presence of non-conservative forces, mechanical energy is not conserved, though the total energy (including all forms) remains constant.
Example: Force Between Two Lennard-Jones Particles¶
To calculate the force between two particles interacting via the Lennard-Jones potential, we use
First, compute the derivative of with respect to , i.e.,
The force vector is then
So, the -component of the force is
Similarly for the - and -components. This force is needed to compute the acceleration and update the positions and velocities of the particles in simulations. Let’s implement the potential energy function and the force function for two Lennard-Jones particles and plot them as functions of the distance between the particles.
import numpy as np
import matplotlib.pyplot as plt
# Lennard-Jones potential energy function
def U(r, epsilon=1.0, sigma=1.0):
return 4 * epsilon * ((sigma / r)**12 - (sigma / r)**6)
# Lennard-Jones force magnitude function
def F(r, epsilon=1.0, sigma=1.0):
return 24 * epsilon * ((2 * (sigma / r)**12) - ((sigma / r)**6)) / r
# Distance between the particles
r = np.linspace(0.8, 2.5, 100)
# Avoid division by zero
r = r[r != 0]
# Plot the potential energy and force as functions of distance
fig, axs = plt.subplots(1, 2, figsize=(12, 4))
# Potential Energy Plot
axs[0].plot(r, U(r))
axs[0].set_xlabel('Distance ($r/\\sigma$)')
axs[0].set_ylabel('Potential Energy $U(r)/\\epsilon$')
axs[0].set_title('Lennard-Jones Potential')
axs[0].axvline(2**(1/6), color='gray', linestyle='--')
axs[0].annotate('$r_{\\rm min}/\\sigma$', xy=(2**(1/6) + 0.05, 1), fontsize=12)
axs[0].set_ylim(-2, 2)
# Force Plot
axs[1].plot(r, F(r))
axs[1].set_xlabel('Distance ($r/\\sigma$)')
axs[1].set_ylabel('Force Magnitude $F(r)\\sigma/\\epsilon$')
axs[1].set_title('Force Between Particles')
axs[1].axvline(2**(1/6), color='gray', linestyle='--')
axs[1].annotate('$r_{\\rm min}/\\sigma$', xy=(2**(1/6) + 0.05, 1.5), fontsize=12)
axs[1].axhline(0, color='gray', linestyle='--')
axs[1].annotate('$F > 0$, repulsive', xy=(2.5, 1), fontsize=12, va='bottom', ha='right')
axs[1].annotate('$F < 0$, attractive', xy=(2.5, -1), fontsize=12, va='top', ha='right')
axs[1].set_ylim(-3, 3)
plt.tight_layout()
plt.show()

The potential energy reaches its minimum at , representing the equilibrium separation. For , the force is positive (repulsive), pushing the particles apart. For , the force is negative (attractive), pulling the particles together.
Verlet Integration¶
The Verlet integration algorithm is a widely used numerical method for integrating Newton’s equations of motion in molecular dynamics simulations. It is particularly favored for its simplicity, time-reversibility, and good energy conservation properties over long simulations.
Derivation of the Verlet Algorithm¶
The Verlet algorithm is derived from the Taylor series expansions of the position of a particle at times and , i.e.,
Here, is the velocity, is the acceleration, and is the third derivative of position with respect to time. Adding the two equations,
Rewriting the equation,
Since acceleration is related to the force by Newton’s second law , we can write
The final Verlet update formula is
This formula allows us to compute the new position using the current position , the previous position , and the force at time .
Calculating Velocities for Kinetic Energy¶
While the Verlet algorithm updates positions efficiently, velocities are also needed to compute kinetic energies and other properties. An estimate of the velocity at time can be obtained using a central difference approximation, i.e.,
This estimation provides velocities consistent with the positions calculated by the Verlet algorithm.
Verlet Integration Algorithm Steps¶
- Set initial positions and .
- Calculate the forces based on the current positions .
- Update positions using the final Verlet update formula.
- Estimate velocities using the central difference approximation for analysis or for calculating kinetic energy.
- Compute kinetic and potential energies based on velocities and positions.
- Advance to the next time step by updating and repeat steps 2–6.
Implementation¶
Let’s implement the Verlet algorithm to simulate the motion of two particles interacting via the Lennard-Jones potential in one dimension. We’ll calculate the forces acting on the particles using the Lennard-Jones potential energy and force functions. The particles will start from rest near the equilibrium separation. We’ll update their positions using the Verlet algorithm, calculate their velocities, and compute the kinetic and potential energies over time.
Code Implementation¶
# Simulation parameters
epsilon = 1.0 # Depth of the potential well
sigma = 1.0 # Distance at which the potential is zero
m = 1.0 # Mass of each particle
dt = 0.005 # Time step size
nsteps = 1000 # Number of time steps
# Initialize position arrays for two particles
positions = np.zeros((nsteps, 2))
# Set initial positions at time t = 0
positions[0] = [0.0, 1.2] # Slightly perturbed from equilibrium
positions[1] = positions[0] # Starting from rest
# Initialize arrays for velocities and forces
velocities = np.zeros((nsteps, 2))
forces = np.zeros((nsteps, 2))
# Initialize arrays for kinetic and potential energies
kinetic_energy = np.zeros(nsteps)
potential_energy = np.zeros(nsteps)
total_energy = np.zeros(nsteps)
# Main Verlet integration loop
for i in range(1, nsteps - 1):
# Compute the distance between the particles
r12 = positions[i, 0] - positions[i, 1]
r12_abs = np.abs(r12)
# Compute the force magnitude and direction
force_mag = F(r12_abs, epsilon, sigma)
force_dir = np.sign(r12)
force = force_mag * force_dir
# Assign forces to each particle (Newton's third law)
forces[i, 0] = force # Force on particle 0
forces[i, 1] = -force # Equal and opposite force on particle 1
# Update positions using the Verlet algorithm
positions[i + 1] = (2 * positions[i] - positions[i - 1] +
(forces[i] / m) * dt**2)
# Estimate velocities using central difference
velocities[i] = (positions[i + 1] - positions[i - 1]) / (2 * dt)
# Compute kinetic energy
kinetic_energy[i] = 0.5 * m * np.sum(velocities[i]**2)
# Compute potential energy
potential_energy[i] = U(r12_abs, epsilon, sigma)
# Compute total energy
total_energy[i] = kinetic_energy[i] + potential_energy[i]
# Time array for plotting
time = np.arange(nsteps) * dt
# Plot the distance between the particles and (kinetic, potential, total) energies over time
fig, axs = plt.subplots(1, 2, figsize=(16, 5))
# Distance plot
axs[0].plot(time[1:-1], np.abs(positions[1:-1, 1] - positions[1:-1, 0]))
axs[0].axhline(2**(1/6) * sigma, color='gray', linestyle='--', label='Equilibrium Distance')
axs[0].set_xlabel('Time')
axs[0].set_ylabel('Distance Between Particles')
axs[0].set_title('Inter-Particle Distance vs. Time')
# Energy plot
axs[1].plot(time[1:-1], kinetic_energy[1:-1], label='Kinetic Energy')
axs[1].plot(time[1:-1], potential_energy[1:-1], label='Potential Energy')
axs[1].plot(time[1:-1], total_energy[1:-1], label='Total Energy', linestyle='--')
axs[1].set_xlabel('Time')
axs[1].set_ylabel('Energy')
axs[1].set_title('Energy vs. Time')
axs[1].legend()
plt.tight_layout()
plt.show()

Observations¶
The left plot shows the distance between the particles as a function of time. The particles exhibit oscillatory motion around the equilibrium separation due to the interplay of attractive and repulsive forces. The right plot shows the kinetic, potential, and total energies of the system as functions of time. The total energy remains approximately constant, showcasing the energy-conserving property of the Verlet algorithm. The kinetic and potential energies oscillate out of phase, indicating the exchange of energy between motion and interaction potential.