“Know how to solve every problem that has been solved.” “What I cannot create, I do not understand.” — Richard Feynman

The Hard-Sphere Fluid

Statistical Mechanics

What you need to know first 1 concepts, 1 layers

The requisite-knowledge inventory for this page, bottom-up: the primitives at the base, combined upward until you reach what this page assumes. Skim the layers you already own; start wherever the ground gets unfamiliar.

  1. base
  2. you are here

Take everything away. No charges, no dipoles, no dispersion attraction, no springs — just impenetrable billiard balls of diameter in a box. The potential is zero unless two spheres overlap, in which case it is infinite. There is no energy scale left, which means temperature can do nothing: every allowed configuration has exactly the same energy, and the Boltzmann factor is either 1 or 0. Whatever this system does, it does with entropy alone — and what it does is astonishing. It has a liquid-like fluid, a first-order phase transition, and a crystal, all from counting configurations.

That is why hard spheres are the hydrogen atom of liquids. Metropolis, Rosenbluth, Rosenbluth, Teller and Teller invented Markov chain Monte Carlo in 1953 on hard disks — this system, one dimension down — and the 1957 hard-sphere simulations of Alder and Wainwright and of Wood and Jacobson produced the first purely entropic freezing transition, a result so counterintuitive that a straw poll at a 1957 conference split roughly half against its existence. The coexistence densities settled at packing fractions (fluid) and (FCC crystal) by the thermodynamic-integration work of Hoover and Ree (1968).

The simulation: accept anything that fits

Metropolis Monte Carlo collapses to its minimal form here. Propose a small random displacement of one sphere; if it overlaps nothing, accept; if it overlaps anything, put it back. The Boltzmann factor is 1 or 0 — there is no energy to compute, ever:

@njit(cache=True)
def mc_sweeps(pos, L, nsweeps, dmax, seed):
    """Metropolis NVT for hard spheres (sigma = 1). Returns acceptance rate."""
    np.random.seed(seed)
    N = pos.shape[0]
    acc = 0
    for sweep in range(nsweeps):
        for _ in range(N):
            i = np.random.randint(N)
            trial = pos[i] + dmax * (np.random.random(3) - 0.5)
            trial -= L * np.floor(trial / L)          # periodic wrap
            ok = True
            for j in range(N):
                if j == i:
                    continue
                d = trial - pos[j]
                d -= L * np.round(d / L)              # minimum image
                if d[0]*d[0] + d[1]*d[1] + d[2]*d[2] < 1.0:
                    ok = False                        # overlap: reject
                    break
            if ok:
                pos[i] = trial                        # no overlap: accept
                acc += 1
    return acc / (nsweeps * N)

Everything below runs spheres started on an FCC lattice, equilibrated for 2,000 sweeps and measured over 12,000, at packing fractions from to 0.47 — just short of freezing.

Two pressures, no forces

A hard-sphere pressure cannot come from summing forces — the force is a delta function at contact. The equation of state has to be measured structurally, and the cross-check is that two completely different routes agree:

Route 1 — the contact value. The virial theorem for hard spheres collapses to an exact statement about the pair correlation function at contact:

Histogram (the subject of its own page), extrapolate to from above, and the pressure falls out of pure structure.

Route 2 — virtual compression. Shrink the box (and every coordinate) by a factor just below 1 and ask: would any pair now overlap? The acceptance probability of this virtual volume move gives the excess pressure directly, (Eppenga & Frenkel's trick). No force, no contact extrapolation — just the question "does it still fit?"

Both routes, against the Carnahan-Starling equation of state (1969) — the famous closed form built by noticing the virial coefficients are nearly integers and resumming the guessed pattern:

eta=0.05  Z_contact=1.2246 (-0.23%)  Z_vol=1.2351 (+0.62%)  CS=1.2274  p_ins=6.436e-01  mu_ex=0.441 (CS 0.441)
eta=0.10  Z_contact=1.5289 (+0.50%)  Z_vol=1.4985 (-1.49%)  CS=1.5213  p_ins=3.760e-01  mu_ex=0.978 (CS 0.978)
eta=0.15  Z_contact=1.8941 (-0.51%)  Z_vol=1.9105 (+0.36%)  CS=1.9037  p_ins=1.934e-01  mu_ex=1.643 (CS 1.641)
eta=0.20  Z_contact=2.4080 (+0.07%)  Z_vol=2.3779 (-1.18%)  CS=2.4062  p_ins=8.388e-02  mu_ex=2.478 (CS 2.469)
eta=0.25  Z_contact=3.0787 (+0.15%)  Z_vol=3.0405 (-1.09%)  CS=3.0741  p_ins=2.946e-02  mu_ex=3.525 (CS 3.519)
eta=0.30  Z_contact=3.9733 (-0.01%)  Z_vol=4.1067 (+3.35%)  CS=3.9738  p_ins=7.482e-03  mu_ex=4.895 (CS 4.872)
eta=0.35  Z_contact=5.2269 (+0.41%)  Z_vol=5.1500 (-1.07%)  CS=5.2057  p_ins=1.261e-03  mu_ex=6.676 (CS 6.650)
eta=0.40  Z_contact=6.9084 (-0.25%)  Z_vol=dead (P_acc -> 0)  CS=6.9259  p_ins=1.075e-04  mu_ex=9.138 (CS 9.037)
eta=0.45  Z_contact=9.3608 (-0.25%)  Z_vol=dead (P_acc -> 0)  CS=9.3847  p_ins=5.000e-06  mu_ex=12.206 (CS 12.327)
eta=0.47  Z_contact=10.6208 (-0.37%)  Z_vol=dead (P_acc -> 0)  CS=10.6603  p_ins=4.167e-07  mu_ex=14.691 (CS 13.994)
Top: compressibility factor Z versus packing fraction, Monte Carlo contact-value and virtual-compression routes lying on the Carnahan-Starling curve up to the freezing density. Bottom: percent error of both routes, within about one percent.

Read the two routes separately. The contact route tracks Carnahan-Starling within half a percent all the way to freezing. The volume route agrees with it at low and moderate density — genuinely independent confirmation — and then dies above : a global compression of a dense configuration is rejected essentially every time, the acceptance probability underflows, and the estimator returns nothing rather than something wrong. That is the same exponential death the Widom method suffers below, and it is the recurring failure mode of every "count rare successes" estimator in this subject. Note also what does not depend on: temperature. For hard spheres is the entire thermodynamics — is a function of density alone, because there is no energy scale to compare against.

Widom insertion: the chemical potential from ghosts

Widom (1963) noticed that the excess chemical potential — the free-energy cost of adding one more particle — is an average you can measure without ever actually adding it: , where is the energy a randomly placed ghost particle would feel. For hard spheres the exponential is again 1 or 0, so the whole method reduces to: throw darts, count the fraction that land in a sphere-sized hole.

@njit(cache=True)
def widom_insertions(pos, L, ntrials, seed):
    """Fraction of random ghost insertions that create no overlap."""
    np.random.seed(seed)
    hits = 0
    for _ in range(ntrials):
        t = np.random.random(3) * L                  # a ghost sphere, anywhere
        ok = True
        for j in range(pos.shape[0]):
            d = t - pos[j]
            d -= L * np.round(d / L)
            if d[0]*d[0] + d[1]*d[1] + d[2]*d[2] < 1.0:
                ok = False
                break
        if ok:
            hits += 1
    return hits / ntrials                            # <e^(-beta dU)> = P(no overlap)
Left: insertion probability versus packing fraction on a log scale, falling exponentially through five orders of magnitude. Right: excess chemical potential from Widom insertion agreeing with the Carnahan-Starling prediction.

The right panel is the success: tracks the Carnahan-Starling prediction across the whole fluid. The left panel is the built-in failure mode, and it is worth staring at: the insertion probability falls roughly exponentially with density, into the 10-5 range by . Every decade costs ten times the statistics. Push much past freezing and no laptop lifetime of darts will find a hole — which is precisely why staged-insertion and thermodynamic-integration methods exist. A method whose error bars tell you when it stops working is a good method.

Try First

Each prompt asks a checkable question about the working code or math above — predict an output, derive a sign, state an invariant, find a bug. Commit to an answer before clicking "reveal." That commitment is the whole point: if your answer matched, you understand the piece you were looking at; if it didn't, that's the part worth re-reading.

predict
A colleague runs this same simulation at "twice the temperature." What changes in Z, in g(r), and in the acceptance rate?
why does this work
Freezing costs the system configurational disorder — a crystal seems like the low-entropy option. How can a transition driven by entropy alone produce one?
what if
The Metropolis acceptance rate at with the step size used here is far below the folklore-optimal 50%. Is that a bug?

Reproduce it

scripts/gen_hard_spheres.py regenerates every number and figure on this page and the radial distribution function page in one run (numba does the heavy lifting). The natural next steps in the stat-mech plan: event-driven hard-sphere molecular dynamics — no timestep, jump collision to collision — and jamming, where packings this page politely stops short of get compressed until they lock. Related pages already live: Lennard-Jones MD, Wang-Landau, critical exponents from finite-size scaling.