Annus Mirabilis · Interactive critical edition in preparation
Drift-diffusion balance and the Stokes-Einstein relation
Push suspended particles with a steady force and see drift and diffusion balance.
An executable model
The drift-diffusion balance laboratory
Static worked example
Predict before the numbers
If you double the external force pulling on the particles, does the steady-state diffusion coefficient D double, halve, or stay unchanged?
Predict before the numbers
In a hypothetical world where single molecular collisions cannot move particles (kicks off, m = 0), what happens to particles under a constant downward force?
The result appears when you choose, say you have one in mind, or skip.
Test it: calculate with twice the force
The accepted run has thermal D = 0.42858 μm²/s and drift velocity 0.42858 μm/s. With mismatched kicks, the chosen kick strength is a separate model assumption; a transient profile is not an equilibrium measurement.
The explanation is available with or without a prediction. These are model consequences, not experimental proof.
Net flux J = Jdrift + Jdiff = 0.035929 m⁻²s⁻¹
Accepted result: Drift-diffusion balance steady state transient; diffusivity 0.42858 μm²/s.
The force cancellation principle
In §3, Einstein equates the directional Stokes drift with the opposing Brownian diffusion. Notice how the magnitude of the applied force F drops out completely from the inferred diffusion coefficient:
Osmotic decay length (thermodynamic)
λosm = kB T / |F|
1 μm
Calculated directly from the force-balance exponential profile.
Kinetic decay length (stepper)
λkin = Dkicks / (μ |F|)
1 μm
Measured from the steady-state concentration slope.
Equilibrium Agreement (m = 1.0): The kinetic kicks exactly match the thermal expectation. The two routes to D agree, confirming Einstein’s relation D = μ k_B T = 0.42858 μm²/s.
Inspect and export the accepted dataset
All 100 cells are available below (cell width Δx = 0.1 μm). The numerical profile is a normalized coordinate probability density; its cell masses sum to one. The osmotic reference is evaluated at cell centers.
Full cell dataset (100 rows)
| Cell | Position x (μm) | Numerical density (m⁻¹) | Osmotic density (m⁻¹) |
|---|---|---|---|
| 1 | 0.05 | 47183 | 47.73 |
| 2 | 0.15 | 51648 | 52.75 |
| 3 | 0.25 | 56042 | 58.297 |
| 4 | 0.35 | 60324 | 64.428 |
| 5 | 0.45 | 64453 | 71.204 |
| 6 | 0.55 | 68395 | 78.693 |
| 7 | 0.65 | 72119 | 86.969 |
| 8 | 0.75 | 75600 | 96.116 |
| 9 | 0.85 | 78822 | 106.22 |
| 10 | 0.95 | 81771 | 117.4 |
| 11 | 1.05 | 84444 | 129.74 |
| 12 | 1.15 | 86839 | 143.39 |
| 13 | 1.25 | 88964 | 158.47 |
| 14 | 1.35 | 90829 | 175.13 |
| 15 | 1.45 | 92448 | 193.55 |
| 16 | 1.55 | 93839 | 213.91 |
| 17 | 1.65 | 95020 | 236.41 |
| 18 | 1.75 | 96013 | 261.27 |
| 19 | 1.85 | 96839 | 288.75 |
| 20 | 1.95 | 97518 | 319.12 |
| 21 | 2.05 | 98070 | 352.68 |
| 22 | 2.15 | 98514 | 389.77 |
| 23 | 2.25 | 98867 | 430.76 |
| 24 | 2.35 | 99145 | 476.07 |
| 25 | 2.45 | 99362 | 526.13 |
| 26 | 2.55 | 99528 | 581.47 |
| 27 | 2.65 | 99655 | 642.62 |
| 28 | 2.75 | 99750 | 710.21 |
| 29 | 2.85 | 99821 | 784.9 |
| 30 | 2.95 | 99873 | 867.45 |
| 31 | 3.05 | 99911 | 958.68 |
| 32 | 3.15 | 99938 | 1059.5 |
| 33 | 3.25 | 99958 | 1170.9 |
| 34 | 3.35 | 99971 | 1294.1 |
| 35 | 3.45 | 99981 | 1430.2 |
| 36 | 3.55 | 99987 | 1580.6 |
| 37 | 3.65 | 99992 | 1746.8 |
| 38 | 3.75 | 99994 | 1930.5 |
| 39 | 3.85 | 99996 | 2133.6 |
| 40 | 3.95 | 99998 | 2358 |
| 41 | 4.05 | 99999 | 2606 |
| 42 | 4.15 | 99999 | 2880 |
| 43 | 4.25 | 99999 | 3182.9 |
| 44 | 4.35 | 100000 | 3517.7 |
| 45 | 4.45 | 100000 | 3887.6 |
| 46 | 4.55 | 100000 | 4296.5 |
| 47 | 4.65 | 100000 | 4748.4 |
| 48 | 4.75 | 100000 | 5247.8 |
| 49 | 4.85 | 100000 | 5799.7 |
| 50 | 4.95 | 100000 | 6409.6 |
| 51 | 5.05 | 100000 | 7083.7 |
| 52 | 5.15 | 100000 | 7828.7 |
| 53 | 5.25 | 100000 | 8652.1 |
| 54 | 5.35 | 100000 | 9562 |
| 55 | 5.45 | 100000 | 10568 |
| 56 | 5.55 | 100000 | 11679 |
| 57 | 5.65 | 100000 | 12907 |
| 58 | 5.75 | 100000 | 14265 |
| 59 | 5.85 | 100000 | 15765 |
| 60 | 5.95 | 100000 | 17423 |
| 61 | 6.05 | 100000 | 19256 |
| 62 | 6.15 | 100000 | 21281 |
| 63 | 6.25 | 100000 | 23519 |
| 64 | 6.35 | 100000 | 25992 |
| 65 | 6.45 | 100000 | 28726 |
| 66 | 6.55 | 100000 | 31747 |
| 67 | 6.65 | 100000 | 35086 |
| 68 | 6.75 | 100000 | 38776 |
| 69 | 6.85 | 100000 | 42854 |
| 70 | 6.95 | 100010 | 47361 |
| 71 | 7.05 | 100010 | 52342 |
| 72 | 7.15 | 100010 | 57847 |
| 73 | 7.25 | 100020 | 63931 |
| 74 | 7.35 | 100030 | 70654 |
| 75 | 7.45 | 100050 | 78085 |
| 76 | 7.55 | 100070 | 86297 |
| 77 | 7.65 | 100110 | 95373 |
| 78 | 7.75 | 100160 | 105400 |
| 79 | 7.85 | 100230 | 116490 |
| 80 | 7.95 | 100340 | 128740 |
| 81 | 8.05 | 100480 | 142280 |
| 82 | 8.15 | 100680 | 157240 |
| 83 | 8.25 | 100960 | 173780 |
| 84 | 8.35 | 101340 | 192060 |
| 85 | 8.45 | 101850 | 212260 |
| 86 | 8.55 | 102530 | 234580 |
| 87 | 8.65 | 103430 | 259250 |
| 88 | 8.75 | 104620 | 286520 |
| 89 | 8.85 | 106160 | 316650 |
| 90 | 8.95 | 108150 | 349950 |
| 91 | 9.05 | 110700 | 386760 |
| 92 | 9.15 | 113930 | 427430 |
| 93 | 9.25 | 117990 | 472390 |
| 94 | 9.35 | 123070 | 522070 |
| 95 | 9.45 | 129360 | 576980 |
| 96 | 9.55 | 137110 | 637660 |
| 97 | 9.65 | 146580 | 704720 |
| 98 | 9.75 | 158070 | 778840 |
| 99 | 9.85 | 171940 | 860750 |
| 100 | 9.95 | 188580 | 951270 |
Push every suspended particle the same way with a small steady force and they drift and pile up, while their random jostling spreads them back out. When the two balance, the strength of the jostling is fixed by the temperature and the drag of the liquid alone, whatever the force was.
Section 3 imagines a force K acting along x on every suspended particle. In equilibrium the force is balanced by osmotic pressure: Kν = ∂p/∂x, with p = (RT/N)ν, so the density falls off exponentially over a length (RT/N)/K. Einstein then reads the same equilibrium as two opposite processes. The force drives each sphere at K/(6πkP), carrying νK/(6πkP) particles through unit area each second, and diffusion carries D ∂ν/∂x back; for the two to cancel everywhere, D = (RT/N)/(6πkP). The force does not appear in the result. The instrument takes a sphere of radius 0.5 μm in water at 20 °C, with viscosity 1.002 mPa·s, and a force of 4.05 fN, chosen so that the decay length kBT/F is 1 μm. The drag gives a drift of 0.429 μm/s and D = 0.429 μm2/s, and the density in a 10 μm box relaxes towards that 1 μm exponential. With the jostling switched off, Nägeli's objection, only the drift is left and the particles pile against the wall; with it doubled, the profile stretches to 2 μm, twice what osmotic balance allows.
Take unit cross-section and a force K on each particle along x. The suspended particles exert an osmotic pressure p = (RT/N)ν, like a gas of ν particles per unit volume. For a thin slab to stay at rest, the force on its particles, Kν per unit volume, must be matched by the change of pressure across it: Kν = dp/dx = (RT/N) dν/dx. So ν changes by the same fraction, K/(RT/N), over every unit of length, which makes the profile an exponential with decay length (RT/N)/K = kBT/K. With kBT = 1.381 × 10−23 × 293.15 = 4.047 × 10−21 J and K = 4.047 × 10−15 N, that length is 10−6 m, 1 μm. Now the moving picture. Stokes's law gives a sphere of radius P, in a liquid of viscosity k, the speed K/(6πkP) under the force K. Here 6πkP = 6 × 3.1416 × 0.001002 × 0.5 × 10−6 = 9.44 × 10−9 kg/s, so the drift speed is 4.047 × 10−15/(9.44 × 10−9) = 4.29 × 10−7 m/s, or 0.429 μm/s, and ν such spheres per unit volume carry νK/(6πkP) across unit area each second. Diffusion carries D dν/dx the other way. In equilibrium the two cancel: νK/(6πkP) = D dν/dx. Put in dν/dx = νK/(kBT) from the pressure balance: νK/(6πkP) = DνK/(kBT). Both ν and K cancel, leaving D = kBT/(6πkP) = 4.047 × 10−21/(9.44 × 10−9) = 4.29 × 10−13 m2/s. The same D follows from any force at all, which is why Einstein can use an imagined one. The flow around the sphere is extremely slow, with a Reynolds number of 2 × 10−7, so Stokes's law applies. In the box the density starts uniform and, after 1 s, the drift still outweighs the returning diffusion, so it is still settling towards the exponential.
Modern qualifications
Einstein wrote K for the force, ν for the number of particles per unit volume, k for the viscosity and P for the radius. The force is a device of the argument, and it drops out of the result. The same relation, D = (RT/N)/(6πkP), was found independently by Sutherland in 1905 and is often called the Stokes–Einstein relation. Nägeli had argued in 1879 that molecular impacts are far too small to move a visible particle; the branch with the jostling switched off shows what the balance would look like without them, drift alone with nothing to hold the particles off the wall. Stokes's law for the drag on a sphere dates from 1851.
Show the code
The site’s own reference code for this law, in TypeScript. It computes the numbers above when the page runs it, and it is what the page uses wherever a FrankenSim result is not available.
driftDiffusionFrames1d · src/physics/reference/diffusion/driftDiffusion.ts · this site’s source, as built · sha256:af1cc1a77a820260026aa7f774161b211ece08205f7c21eac6447d1ab98ce6d1
This function computes the listed outputs when it runs.
In words
An explicit finite-volume solve of the drift-diffusion equation on a one-dimensional grid with zero-flux walls. It returns every frame's density, the cell width, the drift velocity, the Peclet number that says which of the two transports dominates, and the stability ratio the step was accepted under. At the defaults of this laboratory the Peclet number is 3.90, so drift is a few times stronger than diffusion, and the stability ratio is 0.0052, far inside its limit of 1.
Implementation
export function driftDiffusionFrames1d(
p: DriftDiffusionParameters,
budget = REFERENCE_FTCS_BUDGET,
): Computation<DriftDiffusionFrames> {
const {
cells,
width,
frames,
stepsPerFrame,
dt,
kickDiffusivity,
mobility,
force,
temperature,
profile,
set,
} = p;
if (!integer(cells, 3) || !integer(frames, 1) || !integer(stepsPerFrame, 1)) {
return invalid(
["cells", "frames", "stepsPerFrame"],
"Use at least three cells and positive whole-number frame and step counts.",
);
}
if (
![width, dt, kickDiffusivity, mobility, force, temperature].every(Number.isFinite) ||
width <= 0 ||
dt <= 0 ||
kickDiffusivity < 0 ||
mobility <= 0 ||
temperature <= 0
) {
return invalid(
["width", "dt", "kickDiffusivity", "mobility", "force", "temperature"],
"Use positive width, dt, mobility, and temperature, nonnegative kick diffusivity, and a finite force.",
);
}
const D = kickDiffusivity;
const u = mobility * force;
const dx = width / cells;
if (!Number.isFinite(u) || !Number.isFinite(1 / dx) || dx <= 0) {
return invalid(
["width", "cells", "mobility", "force"],
"The drift speed and cell scale must be representable.",
);
}
const work = cells * (frames - 1) * stepsPerFrame;
if (work > budget.workUnits || cells * frames * 8 > budget.allocationBytes) {
return {
kind: "outcome",
outcome: {
outcome: "budget-exhausted",
message: executionOutcomeRegistry["budget-exhausted"].message,
retry: executionOutcomeRegistry["budget-exhausted"].retry,
requested: { workUnits: work, allocationBytes: cells * frames * 8 },
allowed: { ...budget },
},
};
}
const speeds = transportSpeeds(D, u, dx);
const rate = (speeds.right + speeds.left) / dx;
const sigma = dt * rate;
if (!Number.isFinite(rate) || !Number.isFinite(sigma)) {
return invalid(
["dt", "width", "kickDiffusivity", "force"],
"The explicit transport rate must be finite.",
);
}
if (sigma > 1) {
const dtMax = 1 / rate;
const driftOnly = D === 0;
return {
kind: "refused",
refusal: makeRefusal(
driftOnly ? "drift-cfl-exceeded" : "drift-diffusion-unstable",
{
parameterIds: driftOnly ? ["dt", "force"] : ["dt"],
capabilityId: "diffusion.driftDiffusionFrames1d",
},
{
details: driftOnly
? { courant: sigma, limit: 1, dtMax }
: { ratio: sigma, limit: 1, dtMax },
rankedRepairs: [
{
label: "Reduce the time step to the explicit scheme's positivity limit.",
action: { parameterId: "dt", value: dtMax },
},
{
label: "Use a coarser spatial grid.",
action: { parameterId: "cells", value: Math.max(3, Math.floor(cells / 2)) },
},
...(driftOnly
? [
{
label: "Use a weaker force.",
action: { parameterId: "force", value: force / 2 },
},
]
: []),
],
},
),
};
}
const field = initialProfile(cells, dx, width, D, u, force, temperature, profile, set);
if (!field) {
return invalid(
["profile"],
"The initial profile must have a finite, positive, representable mass.",
);
}
const values = new Float64Array(cells * frames);
values.set(field);
// Reuse numerical buffers. Face objects are needed only for the accepted final frame.
const flux = new Float64Array(cells + 1);
const next = new Float64Array(cells);
for (let frame = 1; frame < frames; frame++) {
for (let s = 0; s < stepsPerFrame; s++) {
const step = advance(field, flux, next, dx, dt, speeds);
if (step.kind === "refused") return step;
}
values.set(field, frame * cells);
}
const pe = D === 0 ? (u === 0 ? 0 : Math.sign(u) * Number.POSITIVE_INFINITY) : u / (D / dx);
return {
kind: "accepted",
data: Object.freeze({
values,
shape: Object.freeze([frames, cells] as const),
dx,
peclet: pe,
sigma,
driftVelocity: u,
// Diagnostics describe this frame, not the field before its last time step.
faceFlux: Object.freeze(faceFlux(field, speeds, D, u)),
boundary: "zero-flux",
ownerId: "diffusion.driftDiffusionFrames1d",
modelIdentity: D === 0 ? DRIFT_ONLY_NO_KICKS : "drift-diffusion",
}),
};
}The site’s own reference code for this law, in TypeScript. It computes the numbers above when the page runs it, and it is what the page uses wherever a FrankenSim result is not available.
equilibriumBalance · src/physics/reference/diffusion/routeA.ts · this site’s source, as built · sha256:02f6431cff2c11ada5c1ef25dedbc116f66ce0316e1eb872d33caf237f0751a4
This function computes the listed outputs when it runs.
In words
Paper 2's move is to demand that a force-driven drift and a diffusive spread cancel in equilibrium, and to read a diffusivity out of the demand. This computes both sides: the mobility route gives D = mu k_B T from Stokes's mobility and the temperature, and the balance route gives the diffusivity the cancellation requires. With the kicks set to mu k_B T both come to 3.1485 times 10^-13 m^2/s and the function reports agreement.
Implementation
export function equilibriumBalance(
{
force,
temperature,
eta,
a,
kickDiffusivity,
}: {
force: number;
temperature: number;
eta: number;
a: number;
kickDiffusivity: number;
profile?: string;
},
set: ConstantSet,
): EquilibriumBalance | RouteAEvaluation {
if (
![force, temperature, eta, a, kickDiffusivity].every(Number.isFinite) ||
temperature <= 0 ||
eta <= 0 ||
a <= 0 ||
kickDiffusivity < 0
) {
return outside(
"diffusionCoefficient",
"equilibriumBalance",
"T > 0, eta > 0, a > 0, D_kicks >= 0",
"Use positive temperature, viscosity, and radius.",
"input",
set,
);
}
const muEval = stokesMobility(eta, a);
if (muEval.result.status !== "value") return muEval;
const mu = muEval.result.value as number;
const kT = thermalConstant(set).value * temperature;
const mobilityD = mu * kT;
const relation: ScientificResult = Object.freeze({
...identity("diffusionCoefficient", "equilibriumBalance"),
status: "symbolic",
expressionRef: "D = mu k_B T",
unspecifiedSymbols: Object.freeze(["D", "mu", "k_B", "T"]),
});
if (force === 0) {
return {
mobilityD: number("diffusionCoefficient", "equilibriumBalance", mobilityD, set),
balanceD: wrap({
...identity("kickDiffusivity", "equilibriumBalance"),
status: "not-applicable",
reason:
"0/0 is not an evaluation; the equilibrium construction relates two descriptions of the same state",
}),
relation,
agree: null,
cancellationFactors: null,
kickStrength: kickDiffusivity / mobilityD,
};
}
const lambdaKin = kickDiffusivity / (mu * Math.abs(force));
const balanceD = mu * Math.abs(force) * lambdaKin;
const m = kickDiffusivity / mobilityD;
return {
mobilityD: number("diffusionCoefficient", "equilibriumBalance", mobilityD, set),
balanceD: number("kickDiffusivity", "equilibriumBalance", balanceD, set),
relation,
agree: Math.abs(m - 1) <= 1e-9,
cancellationFactors: Object.freeze([`mu*|F|`, `k_B T / |F|`]),
kickStrength: m,
};
}The site’s own reference code for this law, in TypeScript. It computes the numbers above when the page runs it, and it is what the page uses wherever a FrankenSim result is not available.
decayLengths · src/physics/reference/diffusion/routeA.ts · this site’s source, as built · sha256:eeb06fece93e833b80eacacbe1cdf8b8e0ee05a74c6d22604b7c5c9bdc098917
This function computes the listed outputs when it runs.
In words
The osmotic length is k_B T over the force; the kinetic length is the kick diffusivity over the mobility times the force. They are the two accounts of the same equilibrium profile, and they are equal exactly when the kicks carry the diffusivity the temperature demands. With the kicks set to mu k_B T both come to 4.006 times 10^-7 m and the returned kick strength is 1.
Implementation
export function decayLengths(
{
force,
temperature,
kickDiffusivity,
mobility,
}: { force: number; temperature: number; kickDiffusivity: number; mobility: number },
set: ConstantSet,
): DecayLengths {
const na = (q: Quantity, reason: string) =>
wrap({
...identity(q, "decayLengths"),
status: "not-applicable",
reason,
});
if (
![force, temperature, kickDiffusivity, mobility].every(Number.isFinite) ||
temperature <= 0 ||
kickDiffusivity < 0 ||
mobility <= 0
) {
const bad = outside(
"osmoticDecayLength",
"decayLengths",
"T > 0, mobility > 0, D_kicks >= 0",
"Use a positive temperature and mobility and a nonnegative kick diffusivity.",
"input",
set,
);
return { osmotic: bad, kinetic: bad, kickStrength: null };
}
if (force === 0) {
const reason = "no force, no gradient: the equilibrium profile is uniform";
return {
osmotic: na("osmoticDecayLength", reason),
kinetic: na("kineticDecayLength", reason),
kickStrength: null,
};
}
const kT = thermalConstant(set).value * temperature;
const osmotic = kT / Math.abs(force);
const kinetic = kickDiffusivity / (mobility * Math.abs(force));
const m = kickDiffusivity / (mobility * kT);
return {
osmotic: number("osmoticDecayLength", "decayLengths", osmotic, set),
kinetic: number("kineticDecayLength", "decayLengths", kinetic, set),
kickStrength: m,
};
}The site’s own reference code for this law, in TypeScript. It computes the numbers above when the page runs it, and it is what the page uses wherever a FrankenSim result is not available.
osmoticEquilibriumProfile · src/physics/reference/diffusion/routeA.ts · this site’s source, as built · sha256:8c77120faf0ef1bb8539709ace90b0689625ded8b03fc3958f4f580a1608e557
This function computes the listed outputs when it runs.
In words
An exponential profile with a decay length of k_B T over the force, normalised so that the whole box holds the particles it was given. The function is evaluated at one position rather than over a grid, and it mirrors for a force pointing the other way rather than carrying a sign through the exponent. At zero force it returns the uniform density directly, which is the same expression in the limit and avoids dividing by a decay length that has gone to infinity.
Implementation
export function osmoticEquilibriumProfile(
x: number,
width: number,
force: number,
temperature: number,
total: number,
set: ConstantSet,
): RouteAEvaluation {
if (
![x, width, force, temperature, total].every(Number.isFinite) ||
width <= 0 ||
temperature <= 0 ||
total < 0 ||
x < 0 ||
x > width
) {
return outside(
"probabilityDensity",
"osmoticEquilibriumProfile",
"box [0, W], T > 0, N_tot >= 0",
"Evaluate inside a positive-width box at positive temperature.",
"input",
set,
);
}
if (force === 0)
return number("probabilityDensity", "osmoticEquilibriumProfile", total / width, set);
const absL = Math.abs((thermalConstant(set).value * temperature) / force);
// n = N_tot (1/λ) e^{x/λ} / (e^{W/λ} - 1) for F > 0, mirrored for F < 0.
// Rewrite as (N_tot/λ) e^{z-w} / (1 - e^{-w}) so large W/λ never overflows an intermediate exp.
const z = force > 0 ? x / absL : (width - x) / absL;
const w = width / absL;
const denom = -Math.expm1(-w);
if (!(denom > 0)) {
return outside(
"probabilityDensity",
"osmoticEquilibriumProfile",
"W/lambda underflow",
"The normalized profile's denominator is not a positive finite number.",
"numerical",
set,
);
}
const logN = Math.log(total / absL) + (z - w) - Math.log(denom);
if (!Number.isFinite(logN) || logN > 700) {
return outside(
"probabilityDensity",
"osmoticEquilibriumProfile",
"W/lambda too large",
"The normalized profile overflowed binary64 even in log space.",
"numerical",
set,
);
}
return number("probabilityDensity", "osmoticEquilibriumProfile", Math.exp(logN), set);
}The explanation
Full explanation
The force makes the particles drift and pile up, and their random motion spreads them back out. At balance the diffusion coefficient is fixed by the temperature and the drag of the liquid alone, whatever the force was.
Show every step of the investigation
Change the force and watch the steady profile steepen while the diffusion coefficient stays put; then change the temperature, viscosity or radius and see it move. The balance is section 3's route to the Stokes-Einstein relation.
An explanatory model, not an observation of nature. This embed starts from the laboratory’s worked defaults, not a saved run. Presentation options change the surrounding guide, never the numerical inputs.