Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Ok-landscape
GitHub Repository: Ok-landscape/computational-pipeline
Path: blob/main/latex-templates/templates/chemistry/molecular_dynamics.tex
75 views
unlisted
1
\documentclass[11pt,a4paper]{article}
2
3
% Essential packages
4
\usepackage[utf8]{inputenc}
5
\usepackage[T1]{fontenc}
6
\usepackage{amsmath,amssymb,amsthm}
7
\usepackage{graphicx}
8
\usepackage{float}
9
\usepackage{booktabs}
10
\usepackage{hyperref}
11
\usepackage{xcolor}
12
\usepackage{pythontex}
13
\usepackage[margin=1in]{geometry}
14
15
% Custom commands
16
\newcommand{\dd}{\mathrm{d}}
17
\newcommand{\ee}{\mathrm{e}}
18
\newcommand{\vect}[1]{\mathbf{#1}}
19
20
% Document metadata
21
\title{Molecular Dynamics Simulation:\\From Lennard-Jones Potentials to Thermodynamic Properties}
22
\author{Computational Chemistry}
23
\date{\today}
24
25
\begin{document}
26
27
\maketitle
28
29
\begin{abstract}
30
Molecular dynamics (MD) simulations provide atomistic insight into the behavior of matter by solving Newton's equations of motion for systems of interacting particles. This document develops a complete MD simulation framework, starting from the Lennard-Jones potential for pairwise interactions, implementing the velocity Verlet integration algorithm, and extracting thermodynamic properties including temperature, pressure, and radial distribution functions. We simulate a simple Lennard-Jones fluid, demonstrating phase behavior and energy conservation.
31
\end{abstract}
32
33
\tableofcontents
34
\newpage
35
36
%------------------------------------------------------------------------------
37
\section{Introduction: Simulating Matter at the Atomic Scale}
38
%------------------------------------------------------------------------------
39
40
Molecular dynamics simulations bridge the gap between quantum mechanical descriptions of molecular interactions and macroscopic thermodynamic properties. By numerically solving Newton's equations of motion for a system of $N$ particles, we can:
41
42
\begin{itemize}
43
\item Observe molecular-level dynamics in real time
44
\item Calculate thermodynamic properties from first principles
45
\item Study phase transitions and transport phenomena
46
\item Investigate protein folding, chemical reactions, and material properties
47
\end{itemize}
48
49
The fundamental algorithm is straightforward:
50
\begin{enumerate}
51
\item Define interatomic potentials that describe particle interactions
52
\item Initialize positions and velocities
53
\item Compute forces on each particle: $\vect{F}_i = -\nabla_i U$
54
\item Integrate equations of motion: $\vect{F}_i = m_i \ddot{\vect{r}}_i$
55
\item Repeat, collecting statistics along the trajectory
56
\end{enumerate}
57
58
\begin{pycode}
59
import numpy as np
60
import matplotlib.pyplot as plt
61
from mpl_toolkits.mplot3d import Axes3D
62
63
# Set random seed for reproducibility
64
np.random.seed(42)
65
66
# Style configuration
67
plt.rcParams['figure.figsize'] = (10, 6)
68
plt.rcParams['font.size'] = 11
69
plt.rcParams['axes.labelsize'] = 12
70
plt.rcParams['axes.titlesize'] = 14
71
\end{pycode}
72
73
%------------------------------------------------------------------------------
74
\section{The Lennard-Jones Potential}
75
%------------------------------------------------------------------------------
76
77
The Lennard-Jones (LJ) potential is the most widely used model for van der Waals interactions between neutral atoms:
78
79
\begin{equation}
80
U_{\text{LJ}}(r) = 4\varepsilon \left[\left(\frac{\sigma}{r}\right)^{12} - \left(\frac{\sigma}{r}\right)^{6}\right]
81
\label{eq:lennard-jones}
82
\end{equation}
83
84
where:
85
\begin{itemize}
86
\item $\varepsilon$ is the depth of the potential well (energy scale)
87
\item $\sigma$ is the distance at which $U = 0$ (length scale)
88
\item $r$ is the interparticle distance
89
\end{itemize}
90
91
The $r^{-12}$ term models Pauli repulsion at short range, while the $r^{-6}$ term models the attractive London dispersion force. The potential has a minimum at $r_{\text{min}} = 2^{1/6}\sigma \approx 1.122\sigma$ with depth $-\varepsilon$.
92
93
\subsection{Force Derivation}
94
95
The force between particles is the negative gradient of the potential:
96
97
\begin{equation}
98
F_{\text{LJ}}(r) = -\frac{\dd U_{\text{LJ}}}{\dd r} = \frac{24\varepsilon}{r}\left[2\left(\frac{\sigma}{r}\right)^{12} - \left(\frac{\sigma}{r}\right)^{6}\right]
99
\label{eq:lj-force}
100
\end{equation}
101
102
In vector form, the force on particle $i$ due to particle $j$ is:
103
104
\begin{equation}
105
\vect{F}_{ij} = F_{\text{LJ}}(r_{ij}) \frac{\vect{r}_{ij}}{r_{ij}}
106
\end{equation}
107
108
\begin{pycode}
109
# Lennard-Jones potential and force
110
def lennard_jones_potential(r, epsilon=1.0, sigma=1.0):
111
"""Lennard-Jones pair potential."""
112
sr6 = (sigma / r)**6
113
sr12 = sr6**2
114
return 4 * epsilon * (sr12 - sr6)
115
116
def lennard_jones_force(r, epsilon=1.0, sigma=1.0):
117
"""Magnitude of Lennard-Jones force."""
118
sr6 = (sigma / r)**6
119
sr12 = sr6**2
120
return 24 * epsilon / r * (2 * sr12 - sr6)
121
122
# Plot potential and force
123
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
124
125
r = np.linspace(0.9, 3.0, 500)
126
U = lennard_jones_potential(r)
127
F = lennard_jones_force(r)
128
129
# Left: Potential
130
ax = axes[0]
131
ax.plot(r, U, 'b-', linewidth=2)
132
ax.axhline(0, color='gray', linestyle='--', alpha=0.5)
133
ax.axvline(2**(1/6), color='red', linestyle=':', alpha=0.7, label=r'$r_{min} = 2^{1/6}\sigma$')
134
135
# Mark key points
136
r_min = 2**(1/6)
137
ax.plot(r_min, -1, 'ro', markersize=8)
138
ax.annotate(r'$(-\varepsilon)$', xy=(r_min, -1), xytext=(r_min + 0.2, -0.7),
139
fontsize=11, color='red')
140
ax.plot(1.0, 0, 'go', markersize=8)
141
ax.annotate(r'$\sigma$', xy=(1.0, 0), xytext=(0.85, 0.5), fontsize=11, color='green')
142
143
ax.set_xlabel(r'Distance $r/\sigma$')
144
ax.set_ylabel(r'Potential $U/\varepsilon$')
145
ax.set_title('Lennard-Jones Potential')
146
ax.set_xlim(0.9, 3.0)
147
ax.set_ylim(-1.5, 2.0)
148
ax.legend()
149
ax.grid(True, alpha=0.3)
150
151
# Right: Force
152
ax = axes[1]
153
ax.plot(r, F, 'r-', linewidth=2)
154
ax.axhline(0, color='gray', linestyle='--', alpha=0.5)
155
ax.axvline(2**(1/6), color='blue', linestyle=':', alpha=0.7)
156
157
ax.fill_between(r[F > 0], 0, F[F > 0], alpha=0.3, color='red', label='Repulsive')
158
ax.fill_between(r[F < 0], 0, F[F < 0], alpha=0.3, color='blue', label='Attractive')
159
160
ax.set_xlabel(r'Distance $r/\sigma$')
161
ax.set_ylabel(r'Force $F\sigma/\varepsilon$')
162
ax.set_title('Lennard-Jones Force')
163
ax.set_xlim(0.9, 3.0)
164
ax.set_ylim(-3, 5)
165
ax.legend()
166
ax.grid(True, alpha=0.3)
167
168
plt.tight_layout()
169
plt.savefig('molecular_dynamics_potential.pdf', bbox_inches='tight')
170
plt.close()
171
\end{pycode}
172
173
\begin{figure}[H]
174
\centering
175
\includegraphics[width=\textwidth]{molecular_dynamics_potential.pdf}
176
\caption{The Lennard-Jones potential and its derivative. Left: The potential energy curve shows strong repulsion at short range ($r < \sigma$), a minimum at $r_{\text{min}} = 2^{1/6}\sigma$ with depth $-\varepsilon$, and asymptotic approach to zero at large distances. Right: The force is repulsive (positive) for $r < r_{\text{min}}$ and attractive (negative) for $r > r_{\text{min}}$. At the equilibrium distance $r_{\text{min}}$, the force is zero.}
177
\label{fig:lj-potential}
178
\end{figure}
179
180
%------------------------------------------------------------------------------
181
\section{Reduced Units}
182
%------------------------------------------------------------------------------
183
184
MD simulations typically use \textit{reduced units} where $\varepsilon$, $\sigma$, and the particle mass $m$ are set to unity. All quantities are then expressed in terms of these fundamental scales:
185
186
\begin{table}[H]
187
\centering
188
\begin{tabular}{lcc}
189
\toprule
190
Quantity & Reduced Unit & For Argon \\
191
\midrule
192
Length & $\sigma$ & 3.4 \AA \\
193
Energy & $\varepsilon$ & $1.65 \times 10^{-21}$ J \\
194
Mass & $m$ & $6.63 \times 10^{-26}$ kg \\
195
Time & $\sigma\sqrt{m/\varepsilon}$ & 2.15 ps \\
196
Temperature & $\varepsilon/k_B$ & 120 K \\
197
Pressure & $\varepsilon/\sigma^3$ & 42 MPa \\
198
\bottomrule
199
\end{tabular}
200
\caption{Reduced units and their values for liquid argon.}
201
\label{tab:units}
202
\end{table}
203
204
%------------------------------------------------------------------------------
205
\section{The Velocity Verlet Algorithm}
206
%------------------------------------------------------------------------------
207
208
The velocity Verlet algorithm is the most popular integrator for MD due to its time-reversibility, symplectic nature, and good energy conservation:
209
210
\begin{align}
211
\vect{r}(t + \Delta t) &= \vect{r}(t) + \vect{v}(t)\Delta t + \frac{1}{2}\vect{a}(t)\Delta t^2 \label{eq:verlet-r}\\
212
\vect{v}(t + \Delta t) &= \vect{v}(t) + \frac{1}{2}[\vect{a}(t) + \vect{a}(t + \Delta t)]\Delta t \label{eq:verlet-v}
213
\end{align}
214
215
where $\vect{a} = \vect{F}/m$. The algorithm proceeds as:
216
\begin{enumerate}
217
\item Calculate forces at time $t$
218
\item Update positions using Eq.~\eqref{eq:verlet-r}
219
\item Calculate forces at time $t + \Delta t$
220
\item Update velocities using Eq.~\eqref{eq:verlet-v}
221
\end{enumerate}
222
223
\begin{pycode}
224
class LennardJonesSimulation:
225
"""
226
Molecular dynamics simulation of a Lennard-Jones fluid.
227
228
Uses reduced units: epsilon = sigma = mass = 1
229
"""
230
231
def __init__(self, n_particles, box_length, temperature, dt=0.002, cutoff=2.5):
232
"""
233
Initialize the simulation.
234
235
Parameters:
236
-----------
237
n_particles : int
238
Number of particles
239
box_length : float
240
Side length of cubic simulation box
241
temperature : float
242
Initial temperature (reduced units)
243
dt : float
244
Time step (reduced units)
245
cutoff : float
246
Potential cutoff distance
247
"""
248
self.n = n_particles
249
self.L = box_length
250
self.T = temperature
251
self.dt = dt
252
self.rc = cutoff
253
self.rc2 = cutoff**2
254
255
# Potential shift for continuity at cutoff
256
self.U_shift = 4 * ((1/cutoff)**12 - (1/cutoff)**6)
257
258
# Initialize positions on a lattice
259
self.positions = self._init_lattice()
260
261
# Initialize velocities from Maxwell-Boltzmann distribution
262
self.velocities = self._init_velocities()
263
264
# Force array
265
self.forces = np.zeros_like(self.positions)
266
267
# Observables
268
self.potential_energy = 0
269
self.kinetic_energy = 0
270
self.virial = 0
271
272
def _init_lattice(self):
273
"""Place particles on a simple cubic lattice."""
274
n_side = int(np.ceil(self.n**(1/3)))
275
spacing = self.L / n_side
276
positions = []
277
278
for i in range(n_side):
279
for j in range(n_side):
280
for k in range(n_side):
281
if len(positions) < self.n:
282
positions.append([
283
(i + 0.5) * spacing,
284
(j + 0.5) * spacing,
285
(k + 0.5) * spacing
286
])
287
288
return np.array(positions)
289
290
def _init_velocities(self):
291
"""Initialize velocities from Maxwell-Boltzmann distribution."""
292
velocities = np.random.randn(self.n, 3) * np.sqrt(self.T)
293
294
# Remove center of mass motion
295
velocities -= np.mean(velocities, axis=0)
296
297
# Scale to exact temperature
298
ke = 0.5 * np.sum(velocities**2)
299
current_T = 2 * ke / (3 * self.n)
300
velocities *= np.sqrt(self.T / current_T)
301
302
return velocities
303
304
def _minimum_image(self, dr):
305
"""Apply minimum image convention for periodic boundaries."""
306
return dr - self.L * np.round(dr / self.L)
307
308
def compute_forces(self):
309
"""
310
Compute forces, potential energy, and virial.
311
312
Uses the minimum image convention for periodic boundaries.
313
"""
314
self.forces.fill(0)
315
self.potential_energy = 0
316
self.virial = 0
317
318
for i in range(self.n - 1):
319
for j in range(i + 1, self.n):
320
dr = self.positions[j] - self.positions[i]
321
dr = self._minimum_image(dr)
322
r2 = np.sum(dr**2)
323
324
if r2 < self.rc2:
325
r2_inv = 1 / r2
326
r6_inv = r2_inv**3
327
r12_inv = r6_inv**2
328
329
# Potential (shifted)
330
U = 4 * (r12_inv - r6_inv) - self.U_shift
331
self.potential_energy += U
332
333
# Force magnitude / r
334
f_over_r = 24 * r2_inv * (2 * r12_inv - r6_inv)
335
336
# Force vector
337
f = f_over_r * dr
338
self.forces[i] -= f
339
self.forces[j] += f
340
341
# Virial for pressure calculation
342
self.virial += f_over_r * r2
343
344
def velocity_verlet_step(self):
345
"""Perform one velocity Verlet integration step."""
346
# Half-step velocity update
347
self.velocities += 0.5 * self.forces * self.dt
348
349
# Full position update
350
self.positions += self.velocities * self.dt
351
352
# Apply periodic boundary conditions
353
self.positions = self.positions % self.L
354
355
# Compute new forces
356
self.compute_forces()
357
358
# Complete velocity update
359
self.velocities += 0.5 * self.forces * self.dt
360
361
# Calculate kinetic energy
362
self.kinetic_energy = 0.5 * np.sum(self.velocities**2)
363
364
def get_temperature(self):
365
"""Calculate instantaneous temperature."""
366
return 2 * self.kinetic_energy / (3 * self.n)
367
368
def get_pressure(self):
369
"""Calculate pressure using virial theorem."""
370
density = self.n / self.L**3
371
T = self.get_temperature()
372
return density * T + self.virial / (3 * self.L**3)
373
374
def get_total_energy(self):
375
"""Calculate total energy."""
376
return self.kinetic_energy + self.potential_energy
377
378
def apply_thermostat(self, target_T):
379
"""Velocity rescaling thermostat."""
380
current_T = self.get_temperature()
381
if current_T > 0:
382
scale = np.sqrt(target_T / current_T)
383
self.velocities *= scale
384
385
# Run a short simulation
386
np.random.seed(42)
387
n_particles = 108 # 4x4x4 FCC-like arrangement
388
density = 0.8 # Reduced density (liquid state)
389
box_length = (n_particles / density)**(1/3)
390
temperature = 1.0 # Reduced temperature
391
392
sim = LennardJonesSimulation(n_particles, box_length, temperature)
393
sim.compute_forces()
394
395
# Equilibration with thermostat
396
n_equil = 500
397
for _ in range(n_equil):
398
sim.velocity_verlet_step()
399
if _ % 50 == 0:
400
sim.apply_thermostat(temperature)
401
402
# Production run (NVE - constant energy)
403
n_steps = 2000
404
energies = {'kinetic': [], 'potential': [], 'total': []}
405
temperatures = []
406
pressures = []
407
408
for step in range(n_steps):
409
sim.velocity_verlet_step()
410
411
energies['kinetic'].append(sim.kinetic_energy / n_particles)
412
energies['potential'].append(sim.potential_energy / n_particles)
413
energies['total'].append(sim.get_total_energy() / n_particles)
414
temperatures.append(sim.get_temperature())
415
pressures.append(sim.get_pressure())
416
417
# Convert to arrays
418
for key in energies:
419
energies[key] = np.array(energies[key])
420
temperatures = np.array(temperatures)
421
pressures = np.array(pressures)
422
time_array = np.arange(n_steps) * sim.dt
423
\end{pycode}
424
425
%------------------------------------------------------------------------------
426
\section{Simulation Results}
427
%------------------------------------------------------------------------------
428
429
\subsection{Energy Conservation}
430
431
A hallmark of a good MD integrator is energy conservation in the microcanonical (NVE) ensemble. The velocity Verlet algorithm achieves excellent long-term energy conservation.
432
433
\begin{pycode}
434
# Energy conservation plot
435
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
436
437
# Top left: Energy components
438
ax = axes[0, 0]
439
ax.plot(time_array, energies['kinetic'], 'r-', alpha=0.7, label='Kinetic')
440
ax.plot(time_array, energies['potential'], 'b-', alpha=0.7, label='Potential')
441
ax.plot(time_array, energies['total'], 'k-', linewidth=2, label='Total')
442
443
ax.set_xlabel('Time (reduced units)')
444
ax.set_ylabel('Energy per particle')
445
ax.set_title('Energy Components')
446
ax.legend()
447
ax.grid(True, alpha=0.3)
448
449
# Top right: Energy conservation detail
450
ax = axes[0, 1]
451
E_total = energies['total']
452
E_mean = np.mean(E_total)
453
E_drift = (E_total - E_total[0]) * 100 / np.abs(E_mean) # Percentage drift
454
455
ax.plot(time_array, E_drift, 'k-', linewidth=1)
456
ax.axhline(0, color='gray', linestyle='--', alpha=0.5)
457
458
ax.set_xlabel('Time (reduced units)')
459
ax.set_ylabel('Energy drift (%)')
460
ax.set_title(f'Energy Conservation (drift: {np.std(E_drift):.4f}%)')
461
ax.grid(True, alpha=0.3)
462
463
# Bottom left: Temperature fluctuations
464
ax = axes[1, 0]
465
ax.plot(time_array, temperatures, 'r-', alpha=0.7)
466
ax.axhline(np.mean(temperatures), color='k', linestyle='--',
467
label=f'Mean: {np.mean(temperatures):.3f}')
468
ax.fill_between(time_array,
469
np.mean(temperatures) - np.std(temperatures),
470
np.mean(temperatures) + np.std(temperatures),
471
alpha=0.2, color='red')
472
473
ax.set_xlabel('Time (reduced units)')
474
ax.set_ylabel('Temperature')
475
ax.set_title(f'Temperature (std: {np.std(temperatures):.3f})')
476
ax.legend()
477
ax.grid(True, alpha=0.3)
478
479
# Bottom right: Pressure fluctuations
480
ax = axes[1, 1]
481
ax.plot(time_array, pressures, 'b-', alpha=0.7)
482
ax.axhline(np.mean(pressures), color='k', linestyle='--',
483
label=f'Mean: {np.mean(pressures):.3f}')
484
485
ax.set_xlabel('Time (reduced units)')
486
ax.set_ylabel('Pressure')
487
ax.set_title(f'Pressure (std: {np.std(pressures):.3f})')
488
ax.legend()
489
ax.grid(True, alpha=0.3)
490
491
plt.tight_layout()
492
plt.savefig('molecular_dynamics_energy.pdf', bbox_inches='tight')
493
plt.close()
494
\end{pycode}
495
496
\begin{figure}[H]
497
\centering
498
\includegraphics[width=\textwidth]{molecular_dynamics_energy.pdf}
499
\caption{Thermodynamic properties during MD simulation. Top left: Energy components showing the exchange between kinetic and potential energy while total energy remains constant. Top right: Energy drift as a percentage of mean energy, demonstrating excellent conservation by the velocity Verlet integrator. Bottom left: Temperature fluctuations around the mean---these are expected in the NVE ensemble and decrease as $1/\sqrt{N}$. Bottom right: Pressure fluctuations; the mean pressure includes both ideal gas and virial contributions.}
500
\label{fig:energy}
501
\end{figure}
502
503
\subsection{Radial Distribution Function}
504
505
The radial distribution function (RDF) $g(r)$ measures the probability of finding a particle at distance $r$ from another particle, relative to an ideal gas. It provides structural information about the fluid:
506
507
\begin{equation}
508
g(r) = \frac{V}{N^2} \left\langle \sum_i \sum_{j \neq i} \delta(r - r_{ij}) \right\rangle
509
\end{equation}
510
511
\begin{pycode}
512
def compute_rdf(positions, box_length, n_bins=100, r_max=None):
513
"""
514
Compute the radial distribution function.
515
516
Parameters:
517
-----------
518
positions : ndarray
519
Particle positions (N x 3)
520
box_length : float
521
Simulation box size
522
n_bins : int
523
Number of histogram bins
524
r_max : float
525
Maximum distance (default: half box length)
526
"""
527
if r_max is None:
528
r_max = box_length / 2
529
530
n = len(positions)
531
dr = r_max / n_bins
532
hist = np.zeros(n_bins)
533
534
for i in range(n - 1):
535
for j in range(i + 1, n):
536
dpos = positions[j] - positions[i]
537
dpos = dpos - box_length * np.round(dpos / box_length)
538
r = np.sqrt(np.sum(dpos**2))
539
540
if r < r_max:
541
bin_idx = int(r / dr)
542
hist[bin_idx] += 2 # Count both i-j and j-i
543
544
# Normalize
545
r_edges = np.linspace(0, r_max, n_bins + 1)
546
r_centers = 0.5 * (r_edges[:-1] + r_edges[1:])
547
shell_volumes = 4 * np.pi * r_centers**2 * dr
548
549
density = n / box_length**3
550
g_r = hist / (n * density * shell_volumes)
551
552
return r_centers, g_r
553
554
# Collect RDF samples
555
rdf_samples = []
556
for _ in range(200):
557
sim.velocity_verlet_step()
558
if _ % 10 == 0:
559
r_vals, g_vals = compute_rdf(sim.positions, sim.L)
560
rdf_samples.append(g_vals)
561
562
g_mean = np.mean(rdf_samples, axis=0)
563
g_std = np.std(rdf_samples, axis=0)
564
565
# Plot RDF
566
fig, ax = plt.subplots(figsize=(10, 6))
567
568
ax.plot(r_vals, g_mean, 'b-', linewidth=2, label='Simulation')
569
ax.fill_between(r_vals, g_mean - g_std, g_mean + g_std, alpha=0.3, color='blue')
570
ax.axhline(1, color='gray', linestyle='--', alpha=0.7, label='Ideal gas')
571
572
# Mark key features
573
peaks = [1.0, 1.95] # Approximate peak positions for LJ liquid
574
for i, p in enumerate(peaks):
575
idx = np.argmin(np.abs(r_vals - p))
576
if g_mean[idx] > 1:
577
ax.annotate(f'{i+1}st shell', xy=(r_vals[idx], g_mean[idx]),
578
xytext=(r_vals[idx] + 0.2, g_mean[idx] + 0.5),
579
arrowprops=dict(arrowstyle='->', color='red'),
580
fontsize=10, color='red')
581
582
ax.set_xlabel(r'Distance $r/\sigma$')
583
ax.set_ylabel(r'$g(r)$')
584
ax.set_title(f'Radial Distribution Function ($\\rho^* = {density:.1f}$, $T^* = {temperature:.1f}$)')
585
ax.legend()
586
ax.grid(True, alpha=0.3)
587
ax.set_xlim(0, sim.L / 2)
588
ax.set_ylim(0, 3.5)
589
590
plt.tight_layout()
591
plt.savefig('molecular_dynamics_rdf.pdf', bbox_inches='tight')
592
plt.close()
593
\end{pycode}
594
595
\begin{figure}[H]
596
\centering
597
\includegraphics[width=0.9\textwidth]{molecular_dynamics_rdf.pdf}
598
\caption{Radial distribution function $g(r)$ for the Lennard-Jones fluid at liquid density ($\rho^* = 0.8$) and temperature ($T^* = 1.0$). The first peak near $r = \sigma$ corresponds to the first coordination shell of nearest neighbors. The second peak indicates the second shell. At large distances, $g(r) \to 1$ (ideal gas behavior). The oscillatory structure is characteristic of liquids, contrasting with the delta-function peaks of solids and the uniform $g(r) = 1$ of gases.}
599
\label{fig:rdf}
600
\end{figure}
601
602
\subsection{Phase Diagram Exploration}
603
604
The Lennard-Jones system exhibits gas, liquid, and solid phases. The phase depends on reduced density $\rho^* = \rho\sigma^3$ and reduced temperature $T^* = k_BT/\varepsilon$.
605
606
\begin{pycode}
607
# Scan different state points
608
state_points = [
609
(0.1, 2.0, 'Gas'),
610
(0.8, 1.0, 'Liquid'),
611
(1.0, 0.5, 'Solid'),
612
]
613
614
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
615
616
for ax, (density, temp, phase) in zip(axes, state_points):
617
# Quick simulation at this state point
618
np.random.seed(42)
619
n_test = 64
620
L_test = (n_test / density)**(1/3)
621
622
sim_test = LennardJonesSimulation(n_test, L_test, temp)
623
sim_test.compute_forces()
624
625
# Short equilibration
626
for _ in range(300):
627
sim_test.velocity_verlet_step()
628
if _ % 20 == 0:
629
sim_test.apply_thermostat(temp)
630
631
# Plot 3D snapshot
632
pos = sim_test.positions
633
ax.scatter(pos[:, 0], pos[:, 1], c=pos[:, 2], cmap='viridis',
634
s=500, alpha=0.7, edgecolors='white')
635
636
ax.set_xlim(0, L_test)
637
ax.set_ylim(0, L_test)
638
ax.set_xlabel('x')
639
ax.set_ylabel('y')
640
ax.set_title(f'{phase}\n($\\rho^* = {density}$, $T^* = {temp}$)')
641
ax.set_aspect('equal')
642
ax.grid(True, alpha=0.3)
643
644
plt.tight_layout()
645
plt.savefig('molecular_dynamics_phases.pdf', bbox_inches='tight')
646
plt.close()
647
\end{pycode}
648
649
\begin{figure}[H]
650
\centering
651
\includegraphics[width=\textwidth]{molecular_dynamics_phases.pdf}
652
\caption{Snapshots of the Lennard-Jones system in different phases (2D projection, colored by z-coordinate). Left: Gas phase at low density---particles are well-separated with no persistent structure. Center: Liquid phase at intermediate density---particles form a disordered but cohesive structure with short-range order. Right: Solid phase at high density and low temperature---particles arrange in an ordered crystalline lattice. These phases emerge naturally from the interplay between kinetic energy (temperature) and potential energy (density).}
653
\label{fig:phases}
654
\end{figure}
655
656
%------------------------------------------------------------------------------
657
\section{Conclusions}
658
%------------------------------------------------------------------------------
659
660
This document has presented a complete molecular dynamics simulation framework:
661
662
\begin{enumerate}
663
\item \textbf{Lennard-Jones potential}: The $12$-$6$ form captures essential physics of neutral atom interactions, with repulsion at short range and attraction at longer distances.
664
665
\item \textbf{Velocity Verlet integration}: This symplectic integrator provides excellent energy conservation, enabling accurate NVE simulations over long timescales.
666
667
\item \textbf{Thermodynamic properties}: Temperature, pressure, and energy are directly calculable from particle positions and velocities. The virial theorem connects microscopic forces to macroscopic pressure.
668
669
\item \textbf{Structural analysis}: The radial distribution function reveals the characteristic short-range order of liquids, distinguishing them from gases and solids.
670
671
\item \textbf{Phase behavior}: By varying density and temperature, the simulation captures gas, liquid, and solid phases---all emerging from the same simple interatomic potential.
672
\end{enumerate}
673
674
The simulated system of \pyc{print(n_particles)} particles showed:
675
\begin{itemize}
676
\item Energy conservation to \pyc{print(f'{np.std(E_drift):.4f}')}{\%}
677
\item Mean temperature: \pyc{print(f'{np.mean(temperatures):.3f}')} (target: \pyc{print(f'{temperature:.1f}')})
678
\item Mean pressure: \pyc{print(f'{np.mean(pressures):.3f}')} (reduced units)
679
\item First RDF peak at $r \approx \sigma$, characteristic of liquid structure
680
\end{itemize}
681
682
Extensions of this framework include more realistic potentials (EAM for metals, force fields for biomolecules), constant temperature and pressure ensembles, long-range electrostatics (Ewald summation), and reactive MD for chemical transformations.
683
684
%------------------------------------------------------------------------------
685
\section{References}
686
%------------------------------------------------------------------------------
687
688
\begin{enumerate}
689
\item Allen, M.P. \& Tildesley, D.J. (2017). \textit{Computer Simulation of Liquids}, 2nd ed. Oxford University Press.
690
691
\item Frenkel, D. \& Smit, B. (2002). \textit{Understanding Molecular Simulation}, 2nd ed. Academic Press.
692
693
\item Rapaport, D.C. (2004). \textit{The Art of Molecular Dynamics Simulation}, 2nd ed. Cambridge University Press.
694
695
\item Lennard-Jones, J.E. (1924). On the Determination of Molecular Fields. \textit{Proc. R. Soc. Lond. A}, 106(738), 463-477.
696
697
\item Verlet, L. (1967). Computer ``Experiments'' on Classical Fluids. \textit{Physical Review}, 159(1), 98-103.
698
699
\item Hansen, J.P. \& Verlet, L. (1969). Phase Transitions of the Lennard-Jones System. \textit{Physical Review}, 184(1), 151-161.
700
\end{enumerate}
701
702
\end{document}
703
704