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:

Why is this modest-looking scheme the near-universal choice, beating the fancier Runge–Kutta methods that dominate elsewhere? Four reasons, and they matter:

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:

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:

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.