N = 100 #: N atoms
STEPS = 2000 #: STEPS time steps
DT = 0.005 #: DT time step
T0 = 0.5 #: T0 starting temperature (kinetic energy per atom)
BINS = 30
pi = 3.141592653589793
# a square box at reduced density 0.7, atoms started on a square lattice
DENSITY = 0.7
L = sqrt(N / DENSITY)
SIDE = ceil(sqrt(N))
SPACING = L / SIDE
X = zeros(N)
Y = zeros(N)
for i to N {
X[i] = (mod(i, SIDE) + 0.5) * SPACING
Y[i] = (floor(i / SIDE) + 0.5) * SPACING
}
# random velocities, zero total momentum, then scaled to T0
srand(3.0)
VX = zeros(N)
VY = zeros(N)
for i to N {
VX[i] = rand() - 0.5
VY[i] = rand() - 0.5
}
VX = VX - sum(VX) / N
VY = VY - sum(VY) / N
scale = sqrt(2.0 * T0 * N / sum(VX * VX + VY * VY))
VX = scale * VX
VY = scale * VY
#| Minimum-image displacement: wrap a difference into (-L/2, L/2].
def wrap(d) {
return d - L * round(d / L)
}
#| The force on atom i from every other atom, and its potential energy: [fx, fy, u].
def force_on(i) {
dx = wrap(X - X[i])
dy = wrap(Y - Y[i])
r2 = dx * dx + dy * dy
r2[i] = 1.0e30
inv6 = pow(r2, -3.0)
f_over_r = 24.0 * (2.0 * inv6 * inv6 - inv6) / r2
u = 4.0 * (inv6 * inv6 - inv6)
return [-sum(f_over_r * dx), -sum(f_over_r * dy), 0.5 * sum(u)]
}
#| Fill FX and FY with the force on every atom, and return the total potential energy.
FX = zeros(N)
FY = zeros(N)
def compute_forces() {
u = 0.0
for i to N {
f = force_on(i)
FX[i] = f[0]
FY[i] = f[1]
u = u + f[2]
}
return u
}
#| The total kinetic energy of the atoms.
def kinetic() {
return 0.5 * sum(VX * VX + VY * VY)
}
# the run: velocity Verlet, sampling speeds and g(r) over the second half
potential = compute_forces()
E0 = kinetic() + potential
SPEEDS = zeros(N * (STEPS / 2))
G_BINS = 60
R_MAX = 0.5 * L
gcount = zeros(G_BINS)
nsample = 0.0
worst_drift = 0.0
temp_acc = 0.0
FRAME_EVERY = 40
stage("100 Lennard-Jones atoms, periodic box", -0.5, L + 0.5, -0.5, L + 0.5)
segment("bottom", 0.0, 0.0, L, 0.0)
segment("top", 0.0, L, L, L)
segment("left", 0.0, 0.0, 0.0, L)
segment("right", L, 0.0, L, L)
dots("atoms", X, Y, 0.35)
for s to STEPS {
VX = VX + 0.5 * DT * FX
VY = VY + 0.5 * DT * FY
X = X + DT * VX
Y = Y + DT * VY
X = X - L * floor(X / L)
Y = Y - L * floor(Y / L)
potential = compute_forces()
VX = VX + 0.5 * DT * FX
VY = VY + 0.5 * DT * FY
e = kinetic() + potential
worst_drift = max(worst_drift, abs(e - E0))
if s >= STEPS / 2 {
k = s - STEPS / 2
speed = sqrt(VX * VX + VY * VY)
temp_acc = temp_acc + sum(speed * speed) / (2.0 * N)
for i to N {
SPEEDS[k * N + i] = speed[i]
}
if mod(k, 10) == 0.0 {
for i to N {
dx = wrap(X - X[i])
dy = wrap(Y - Y[i])
r = sqrt(dx * dx + dy * dy)
r[i] = 2.0 * R_MAX
gcount = gcount + histogram(r, 0.0, R_MAX, G_BINS)
}
nsample = nsample + 1.0
}
}
if mod(s, FRAME_EVERY) == 0.0 {
dots("atoms", X, Y, 0.35)
frame(DT * FRAME_EVERY)
}
}
momentum = sqrt(sum(VX) * sum(VX) + sum(VY) * sum(VY))
temperature = temp_acc / (STEPS / 2)
mean_speed = sum(SPEEDS) / len(SPEEDS)
maxwell_mean = sqrt(pi * temperature / 2.0)
show E0
show worst_drift
show momentum
show temperature
show mean_speed
show maxwell_mean
# the speed histogram, as a density, against the Maxwell curve at the measured T
V_MAX = 4.0 * sqrt(temperature)
counts = histogram(SPEEDS, 0.0, V_MAX, BINS)
width = V_MAX / BINS
centers = linspace(0.5 * width, V_MAX - 0.5 * width, BINS)
density = counts / (len(SPEEDS) * width)
maxwell = (centers / temperature) * exp(-centers * centers / (2.0 * temperature))
figure("Speeds over the second half of the run, against Maxwell at the measured temperature", "speed", "density")
bars(centers, density, "measured")
plot(centers, maxwell, "Maxwell, 2-D")
# the pair correlation: counts per shell over the ideal-gas expectation
shell_width = R_MAX / G_BINS
r_centers = linspace(0.5 * shell_width, R_MAX - 0.5 * shell_width, G_BINS)
ideal = DENSITY * 2.0 * pi * r_centers * shell_width * N
g = gcount / (nsample * ideal)
peak_r = 0.0
peak_g = 0.0
for b to G_BINS {
if g[b] > peak_g {
peak_g = g[b]
peak_r = r_centers[b]
}
}
show peak_r
show peak_g
figure("The pair correlation g(r)", "r", "g")
plot(r_centers, g, "g(r)")
plot(r_centers, zeros(G_BINS) + 1.0, "an ideal gas")
test "momentum stays at zero to rounding" {
assert_lt(momentum, 1.0e-9)
}
test "the total energy never moves from its starting value by more than a thousandth of the kinetic energy" {
assert_lt(worst_drift, 1.0e-3 * kinetic())
}
test "the mean speed matches the 2-D Maxwell distribution at the measured temperature to two percent" {
assert_near(mean_speed, maxwell_mean, 0.02 * maxwell_mean)
}
test "g(r) peaks near 2^(1/6) sigma, the bottom of the potential, and is above 1 there" {
assert_near(peak_r, 1.122, 0.1)
assert_gt(peak_g, 1.5)
}
# SPDX-License-Identifier: MIT OR Apache-2.0
# SPDX-FileCopyrightText: 2026 The Viv authors