Molecular Dynamics and N-body Simulation
Point a computer at the night sky and you can grow your own galaxy. Give it a hundred thousand stars,
tell each one that it is pulled by the gravity of every other, and let time crawl forward in tiny
steps. Spiral arms curl out of nothing; clusters form, merge, and fling stray stars into the dark.
Point that same machinery at a droplet of argon instead — a few thousand atoms nudging one another —
and you watch a liquid boil, a crystal melt, a protein fold. It is the same program. Only
the force law and the length scale change.
That program has a name in two dialects. Astronomers call it an N-body simulation;
chemists and materials scientists call it molecular dynamics (MD). Both do exactly
one thing: take many interacting particles and push them all forward together by
integrating Newton's
laws. This page is about how to do that well — because with millions of steps
stacked end to end, a mediocre integrator quietly destroys your world, and a good one keeps it alive
for as long as you care to watch.
The N-body problem: everybody pulls on everybody
Take N particles with positions \mathbf{r}_1, \dots,
\mathbf{r}_N. The rule of the game is simple to state: each particle feels the
sum of the forces from all the others. For gravity, the pull on particle
i from particle j is
\mathbf{F}_{ij} = -\,G\,m_i m_j\,\frac{\mathbf{r}_i - \mathbf{r}_j}{\lvert \mathbf{r}_i - \mathbf{r}_j \rvert^{3}},
which points from i toward j and falls off as
1/r^2 in strength (the cube in the denominator is one power for the
1/r^2 law and one to normalise the direction vector). The total force on
i, and hence its acceleration
\mathbf{a}_i = \mathbf{F}_i/m_i, is the sum over every other particle:
\mathbf{F}_i = \sum_{j \neq i} \mathbf{F}_{ij}, \qquad \mathbf{a}_i = -\,G \sum_{j \neq i} m_j\,\frac{\mathbf{r}_i - \mathbf{r}_j}{\lvert \mathbf{r}_i - \mathbf{r}_j \rvert^{3}}.
Here is the sting in the tail. To take a single step you must, for every one of the
N particles, add up contributions from the other
N-1. That is N(N-1)/2 distinct pairwise
interactions per step — the cost grows as N^2. Double the number of stars
and you quadruple the work; a thousand-fold bigger simulation is a million times slower. This
O(N^2) wall is the central obstacle of the whole field, and taming it (see
the tree-code digression below) is what makes billion-body cosmology possible.
For N = 2 Newton solved this by hand and got ellipses. For
N = 3 there is no general closed-form solution — the
three-body problem is famously chaotic. So from three particles upward, the computer is not a
convenience; it is the only way through.
The heart of the matter: velocity-Verlet
Once you can compute every particle's acceleration, you need to march the positions forward in time.
The obvious choice — plain Euler, "new position = old position + velocity × step" —
is a disaster over long runs. It has a fatal habit: it steadily leaks or pumps energy.
An Euler-integrated orbit spirals slowly outward (or inward) not because the physics says so, but
because the method quietly adds a little false energy every step. Run it for a million steps and your
tidy ellipse has unwound into a spiral. For a simulation whose whole point is to run for a very long
time, that is fatal.
The workhorse of MD and N-body work is instead velocity-Verlet. It updates position
and velocity in a carefully paired dance, using the acceleration at both the old and the new position:
-
Drift the position using the current velocity and acceleration:
\mathbf{x}_{n+1} = \mathbf{x}_n + \mathbf{v}_n\,\Delta t + \tfrac{1}{2}\,\mathbf{a}_n\,\Delta t^2.
-
Recompute the force at the new positions to get
\mathbf{a}_{n+1} — this is the single, all-important force evaluation
of the step, and it must happen before the velocity is finished.
-
Kick the velocity with the average of the old and new accelerations:
\mathbf{v}_{n+1} = \mathbf{v}_n + \tfrac{1}{2}\,(\mathbf{a}_n + \mathbf{a}_{n+1})\,\Delta t.
Why is this modest-looking scheme the near-universal choice, beating the fancier Runge–Kutta methods
that dominate elsewhere? Four reasons, and they matter:
-
It is symplectic. Verlet preserves the geometric structure of the equations of
motion, so its energy error stays bounded — it wobbles around the true value forever
instead of drifting away. Your orbit stays an orbit for a billion steps.
-
It is time-reversible. Run it forward then flip the velocities and run back, and
you return exactly to the start (to round-off). The physics has that symmetry; so does the method.
-
It is cheap. Just one force evaluation per step. Since computing all
those O(N^2) pairwise forces is where nearly all the time goes, a method
that asks for the force four times a step (like classical RK4) is four times as slow for the same
\Delta t.
-
It is second-order accurate. Halve the step and the per-step error drops by a
factor of four — accurate enough, and paired with the bounded-energy property, it is the sweet
spot for long simulations.
The tension is worth stating plainly: RK4 is more accurate on any single step than Verlet.
But accuracy per step is the wrong thing to optimise for a run of millions of steps. What you want is
for the total energy not to drift — and there, cheap symplectic Verlet crushes
expensive non-symplectic RK4. The next block lets you watch the difference with your own eyes.
Verlet vs. Euler: watch the energy
Here is a body orbiting a fixed mass (units where GM = 1), launched on a
circular orbit of radius 1. The total energy per unit mass,
E = \tfrac{1}{2}v^2 - 1/r, should stay pinned at
-0.5 forever. We integrate the same orbit two ways and print the
energy as it goes. Watch Verlet hold steady while Euler slides away.
// Two-body Kepler orbit, GM = 1. Compare velocity-Verlet with plain Euler.
// State: position (x,y), velocity (vx,vy). Total energy per unit mass:
// E = 0.5*(vx^2 + vy^2) - 1/r should stay constant at -0.5.
function accel(x: number, y: number): [number, number] {
const r = Math.sqrt(x * x + y * y);
const r3 = r * r * r;
return [-x / r3, -y / r3]; // GM = 1
}
function energy(x: number, y: number, vx: number, vy: number): number {
return 0.5 * (vx * vx + vy * vy) - 1 / Math.sqrt(x * x + y * y);
}
const dt = 0.05, steps = 200;
// ---- velocity-Verlet ----
let x = 1, y = 0, vx = 0, vy = 1; // circular orbit: r = 1, speed = 1
let [ax, ay] = accel(x, y);
console.log("velocity-Verlet (E should hold near -0.5):");
for (let n = 1; n <= steps; n++) {
x += vx * dt + 0.5 * ax * dt * dt; // 1) drift with old acceleration
y += vy * dt + 0.5 * ay * dt * dt;
const [nx, ny] = accel(x, y); // 2) force at the NEW position
vx += 0.5 * (ax + nx) * dt; // 3) kick with the averaged accel
vy += 0.5 * (ay + ny) * dt;
ax = nx; ay = ny;
if (n % 40 === 0) console.log(` t=${(n * dt).toFixed(2)} E=${energy(x, y, vx, vy).toFixed(5)}`);
}
// ---- plain Euler, same start ----
x = 1; y = 0; vx = 0; vy = 1;
console.log("plain Euler (E drifts away):");
for (let n = 1; n <= steps; n++) {
const [ex, ey] = accel(x, y);
x += vx * dt; y += vy * dt; // position from OLD velocity
vx += ex * dt; vy += ey * dt; // velocity from OLD position
if (n % 40 === 0) console.log(` t=${(n * dt).toFixed(2)} E=${energy(x, y, vx, vy).toFixed(5)}`);
}
The Verlet energy jitters in the fifth decimal place and comes right back; the Euler energy marches
off in one direction and never returns. Multiply that drift by the millions of steps a real
simulation takes and the Euler orbit is long gone. This single output is the whole argument for
symplectic integrators, made concrete.
From two bodies to many
Scaling up to a genuine N-body system changes nothing about the integrator — it changes only the
force routine, which now has to loop over all pairs. Here are three equal masses released near one
another (with a little "softening" added to the distance so a close encounter can't blow up to
infinity), advanced with the very same velocity-Verlet dance. Notice the inner double loop: that is
the O(N^2) cost in the flesh, doing exactly
N(N-1)/2 = 3 pairwise force calculations per step.
// Three equal masses (m = 1, G = 1) pulled together by gravity, advanced
// with velocity-Verlet. The double loop is the O(N^2) all-pairs force sum.
type Vec = [number, number];
const N = 3;
const pos: Vec[] = [[-1, 0], [1, 0], [0, 0.6]];
const vel: Vec[] = [[0, -0.25], [0, 0.25], [0, 0]];
const soft = 0.15; // softening: avoids infinite forces
function forces(p: Vec[]): Vec[] {
const a: Vec[] = p.map(() => [0, 0] as Vec);
for (let i = 0; i < N; i++) {
for (let j = i + 1; j < N; j++) { // each unordered pair once
const dx = p[j][0] - p[i][0], dy = p[j][1] - p[i][1];
const r2 = dx * dx + dy * dy + soft * soft;
const inv = 1 / (r2 * Math.sqrt(r2)); // 1 / r^3
a[i][0] += dx * inv; a[i][1] += dy * inv; // Newton's 3rd law: equal
a[j][0] -= dx * inv; a[j][1] -= dy * inv; // and opposite, for free
}
}
return a;
}
let acc = forces(pos);
const dt = 0.01;
for (let n = 1; n <= 300; n++) {
for (let i = 0; i < N; i++) { // drift every body
pos[i][0] += vel[i][0] * dt + 0.5 * acc[i][0] * dt * dt;
pos[i][1] += vel[i][1] * dt + 0.5 * acc[i][1] * dt * dt;
}
const nacc = forces(pos); // one force sweep at new positions
for (let i = 0; i < N; i++) { // kick every body
vel[i][0] += 0.5 * (acc[i][0] + nacc[i][0]) * dt;
vel[i][1] += 0.5 * (acc[i][1] + nacc[i][1]) * dt;
}
acc = nacc;
if (n % 100 === 0) {
const s = pos.map((p) => `(${p[0].toFixed(2)}, ${p[1].toFixed(2)})`).join(" ");
console.log(`t=${(n * dt).toFixed(2)} ${s}`);
}
}
The three bodies swing around their common centre in a genuinely three-body dance — no formula could
have told you these coordinates in advance. To simulate a galaxy you change three numbers:
N becomes a hundred thousand, the initial positions become a disc, and the
loop becomes the bottleneck. Everything else is identical.
Molecular dynamics: atoms and the Lennard–Jones potential
Swap gravity for the force between two neutral atoms and you have molecular dynamics. Atoms do not
pull on each other like planets. Push two of them together and they repel ferociously
(their electron clouds refuse to overlap); pull them apart and they feel a gentle
attraction (the fleeting van der Waals stickiness). The classic model that captures
both in one tidy formula is the Lennard–Jones potential:
V(r) = 4\varepsilon\!\left[\left(\frac{\sigma}{r}\right)^{12} - \left(\frac{\sigma}{r}\right)^{6}\right].
The (\sigma/r)^{12} term is the steep repulsive core —
the wall an atom hides behind; the -(\sigma/r)^{6} term is the softer,
longer-ranged attractive tail. Here \varepsilon sets the
depth of the well (how sticky) and \sigma sets the distance scale (how
big). The curve dips to a minimum — the comfortable spacing atoms settle into — at
r = 2^{1/6}\sigma \approx 1.122\,\sigma, where the potential equals
exactly -\varepsilon. The force along the line joining the
atoms is just the downhill slope of this curve:
F(r) = -\frac{dV}{dr} = \frac{24\varepsilon}{r}\!\left[2\left(\frac{\sigma}{r}\right)^{12} - \left(\frac{\sigma}{r}\right)^{6}\right].
In a real MD run you drop a few thousand of these atoms into a box, give them random starting
velocities, and integrate — with velocity-Verlet, of course. Two ideas make such a run physical:
-
Temperature is average kinetic energy. The temperature of the sample is not an
extra ingredient you impose; it simply is the mean kinetic energy of the atoms,
\tfrac{3}{2}k_B T = \langle \tfrac{1}{2}mv^2 \rangle. Heat the box and the
atoms jiggle harder; cool it and they settle into a lattice. "Melting" is nothing more than the
jiggling getting violent enough to break the Lennard–Jones cages.
-
Periodic boundary conditions. A few thousand atoms is a speck; to imitate bulk
matter without walls, an atom that drifts out the right face of the box reappears at the left, and
it feels forces from the nearest copies of its neighbours. The little box thus pretends to be an
infinite crystal, dodging the artefacts a hard container would create.
An atom pair, oscillating in its well
Let us make that Lennard–Jones curve move. Take a single pair of atoms in one dimension (reduced units
\varepsilon = \sigma = 1), start them stretched apart at
r = 1.5 and release from rest. They should fall inward toward the bottom of
the well near 1.122, overshoot, and swing back — a vibrating molecule. The
energy sloshes between kinetic and potential, but the total holds steady, because
Verlet is on the job.
// 1-D Lennard-Jones pair (ε = σ = 1, mass = 1). Released from rest while
// stretched, it oscillates in the well. KE and PE trade off; total holds.
function V(q: number): number {
const s6 = Math.pow(1 / q, 6);
return 4 * (s6 * s6 - s6); // 4[(1/q)^12 - (1/q)^6]
}
function F(q: number): number {
const s6 = Math.pow(1 / q, 6);
return 24 * (2 * s6 * s6 - s6) / q; // -dV/dq
}
let q = 1.5, v = 0; // separation, relative velocity
let a = F(q); // reduced mass = 1
const dt = 0.01;
const rmin = Math.pow(2, 1 / 6); // well bottom
console.log(`well minimum: r = ${rmin.toFixed(3)}, V = ${V(rmin).toFixed(3)}`);
for (let n = 1; n <= 400; n++) {
q += v * dt + 0.5 * a * dt * dt; // velocity-Verlet, as before
const na = F(q);
v += 0.5 * (a + na) * dt;
a = na;
if (n % 50 === 0) {
const ke = 0.5 * v * v, pe = V(q);
console.log(`t=${(n * dt).toFixed(2)} r=${q.toFixed(3)} KE=${ke.toFixed(3)} PE=${pe.toFixed(3)} E=${(ke + pe).toFixed(3)}`);
}
}
Follow the r column: it dives from 1.5 down
through the minimum and back out — an oscillation, exactly like a mass on a spring, only the "spring"
is the curved floor of the Lennard–Jones well. The \text{KE} and
\text{PE} columns trade places while the E
column barely twitches. A whole vibrational spectrum of matter is hiding in that one little loop.
Do not reach for RK4 just because it is "more accurate." The single most common
rookie mistake in N-body/MD work is to pick a high-order, non-symplectic method — usually classical
fourth-order Runge–Kutta — on the reasoning that it is more accurate per step than humble Verlet. It
is. But it is not symplectic, so over a long run its energy slowly and
systematically drifts — leaking or gaining — while cheap Verlet's energy merely wobbles
within a fixed band forever. For a simulation of millions of steps you want boundedness, not per-step
precision, and RK4 also costs four force evaluations a step to Verlet's one. Wrong tool.
Two more traps in the same family:
-
Ordering. Velocity-Verlet insists on the force at the new positions
before the final half-kick to the velocity. Compute the velocity from the old
acceleration alone and you have silently downgraded to plain Euler and thrown away every good
property. The middle line of the loop is not optional.
-
Step size. Even a perfect integrator explodes if \Delta t
is too large. A fast close encounter — two stars nearly colliding, two atoms slamming together —
needs many steps to resolve; step over it in one giant \Delta t and the
particles gain enormous spurious energy and rocket off to infinity. When close approaches matter,
shrink the step (or soften the force, as the three-body code above quietly does).
You cheat — cleverly. The O(N^2) all-pairs sum is hopeless past a few tens
of thousands of bodies, so modern codes never compute it exactly. The trick, due to Josh Barnes and
Piet Hut in 1986, is a tree code. Divide space into a hierarchy of nested cells (an
octree in 3-D). A clump of distant stars, viewed from far enough away, pulls almost exactly like a
single point mass sitting at the clump's centre — so replace the whole clump by that one effective
mass and skip the individual sums. Only nearby particles are treated one by one. This
Barnes–Hut approximation drops the cost from O(N^2) to
O(N \log N), and it is the reason a laptop can now push millions of bodies
and a supercomputer can do the whole observable-universe's worth. (For long-range electrostatics,
MD uses a cousin trick — the "particle-mesh" and fast-multipole methods — for the same reason.)
And a footnote on names: the Verlet scheme is often traced to the astronomer Jean-Baptiste Delambre,
who used essentially this update rule around 1791, and it was rediscovered several
times before the French physicist Loup Verlet put it to work on molecular liquids in
1967 and lent it his name. A two-hundred-year-old idea still running at the heart of
the world's biggest simulations — because good geometry never goes out of date.