A freight train is not a vehicle. It is a chain of 83 vehicles, a mile long, driven from one end, and every link in it carries a couple of inches of lost motion before it carries anything at all. Add those inches up and the train has 5.21 m of free slack — seventeen feet of nothing, distributed, that has to be taken up before the back of the train finds out what the front decided.
Everything a locomotive engineer is taught is downstream of that. Not from the physics being subtle — it is a spring with a dead zone — but from the state that matters being invisible. The cab has a speedometer, an ammeter, a brake pipe gauge and a throttle. It has no instrument that says where the slack is sitting, and the slack is the thing that decides whether the next handle movement is uneventful or parts the train.
So this is a mechanic with one piece of hidden state and four levers that all disturb it.
What is actually being simulated
src/run-in.mjs is three locomotives and eighty loaded cars — 1530.6 m over
the couplers, 12,070 short tons — as 83 masses on a line, with no renderer in
it. Each car gets position, velocity, and a brake cylinder; each of the 82
couplers gets:
- free slack, 63.5 mm of travel carrying exactly zero force;
- then draft gear, 85 mm of travel at 20 MN/m with friction damping at 0.26 of critical, which resists and never pulls back the other way;
- then steel, at 240 MN/m, once the gear is bottomed.
A knuckle parts in tension at 1600 kN (360 kip) and the train is called jackknifed if any coupler is squeezed past 2200 kN (495 kip). Parting tears the brake pipe open, so a break is also an emergency application on both halves, which is what happens on the railroad and is why a parted train usually stops rather than rolls away.
Four forces act on each car: its coupler at each end, gravity resolved on the grade under that car — which is the whole point, since a mile-long train is on several grades at once — a Davis-type rolling and air resistance fitted to the usual lb/ton figures, and its own brakes.
The brakes are the part people get wrong. A service reduction is not a signal sent to the train; it is air leaving a pipe, and it walks rearward at 274 m/s (900 ft/s), reaching each car at its own distance divided by that. Each car’s cylinder then builds with a 5 s time constant and releases with a 22 s one. And the handle is a ratchet: a conventional freight brake applies in stages and releases only all at once, so every set is a commitment you cannot partially undo.
The locomotives are power-limited above a few mph, which produces the trap
that caught the controller first: tractive effort goes as power over speed, so
the slower and heavier you are, the less throttle the drawbar can afford.
maxNotchFor() is that budget, and it tightens as you lose speed.
Checks a wrong model fails
Before believing any number below, scripts/measure.mjs checks what a wrong
model gets wrong:
| check | result |
|---|---|
| same script, same run | 2315.297729 m twice |
| momentum drift, couplers ringing with no external force | 0.000000% |
| ticks where the tail brake led the head | 0 |
| coupler forces pointing the wrong way | 0 |
| deepest draft gear travel reached | 75.0 mm of 85 mm |
| peak coupler force at h = 1/200, 1/500, 1/1000, 1/2000 s | 861, 861, 861, 861 kN |
The peak force is the claim this piece makes most of, so it is the quantity checked against the integrator step rather than a trajectory. It does not move.
What the model says
1. The hill is already most of the knuckle. Notch 8 on the 0.9% grade settles at 18.00 mph with 1160 kN in the head coupler, which is 72.5% of a knuckle with nothing dynamic happening at all. There is no throttle setting that is “safe”; there is a steady pull that leaves 27.5% for everything else.
2. The brake pipe is not a wire. Move the handle to full service and the head car starts to bite 0.58 s later, the last car 6.08 s later — a gap of 5.50 s, which is 278 m/s down the train. For those seconds the back of the train is braking less than the front, and the difference peaks at 2.03 mph tail-over-head. That closes 5.00 m of the 5.21 m of free slack available. One ordinary brake application very nearly uses the whole train’s worth of play.
3. The knuckle is a speed limit, not a force limit. Drive the head end into a standing train and the head coupler reads 196 kN at 0.22 mph, 550 kN at 1.12 mph, 1096 kN at 2.24 mph, and parts at 3.27 mph of closing. The chain’s impedance is √(k·m) and the force is just that times the closing speed, so the dangerous quantity is a velocity difference between two ends of a mile-long object — and the only speed instrument in the cab reads one end, to 1 mph.
4. The same application, two hidden states. Full service at 25 mph on level track, nothing else touched:
| peak buff | peak draft | |
|---|---|---|
| slack stretched | 861 kN | 636 kN |
| slack bunched | 452 kN | 214 kN |
1.91×, from a difference the engineer cannot see and did not choose. The stretched train has 5.21 m to run in through; the bunched one has none.
5. Where you apply the brake beats how hard. This is the one worth the piece. Match the two brakes on total retarding force — 1200 kN either way, same train, same speed, same slack — and change only where it is applied:
| run-in | speed lost in 44 s | |
|---|---|---|
| full dynamic brake, all 1200 kN at the head | 1181 kN | 10.18 mph |
| 11.8 psi set, the same 1200 kN spread over 80 cars | 435 kN | 11.05 mph |
2.7× the internal force for slightly less deceleration. The locomotive’s own brake is a point force on a flexible object; the air brake is a body force. They are the same number on a spreadsheet and not remotely the same event.
Over the summit it is worse. With the head on the 1.3% down and 69% of the train still climbing, taking the throttle off in one move costs a 526 kN run-in; feathering it one notch every 6 s costs 482 kN, which is a 9% improvement for a great deal of care. Reaching for full dynamic instead costs 1559 kN — 97% of a knuckle. The graduated reduction everybody teaches is worth a tenth of what simply choosing the other brake is worth.
6. Two speeds of sound. Open the throttle and watch when each coupler first loads up. With the train stretched the arrival curve is a straight line at 246 m/s and the last coupler knows in 6.08 s. With the train sitting in its slack, the same movement propagates at 150 m/s and takes 10.08 s — because every gap has to be physically closed at whatever relative speed the chain can muster. Press C in the demo and the curve is drawn across the coupler strip.
7. Stretch braking is a real trade and not a free one. An 18 psi set on the 1.3% descent, throttle to idle, lets 54.9% of the couplers go loose and makes a 350 kN run-in. Holding notch 2 in against the same set keeps every coupler engaged and the run-in down to 210 kN — but raises the steady draft from 344 kN to 581 kN. You do not remove the force. You move it from a transient you cannot predict to a tension you can read on a gauge, which is a much better place to keep it.
Playing it
Three roads: Climb (0.9% up, over the summit, 1.3% down, stop with the head end in a 100 m box), Sag (down into the bottom and back out, where the slack runs in and then straight back out again), and Yard, which is flat and has no objective other than finding out what a handle movement costs.
↑↓ work one combined lever from DB8 through idle to N8.
→ sets the air in stages and ← releases it — all of it,
because that is the only release the valve has. space is an
emergency. [ and ] start the train bunched or stretched
so the two can be compared, and H hands it to engineer(), the
textbook controller in src/, which writes through the same setNotch and
setReduction the keys do.
The display is three readings of the same instant: the train on the railroad, coloured by what each car’s couplers are carrying; every coupler as a bar, draft up toward the knuckle line and buff down toward the jackknife line, with a dot showing where inside its free slack each one is sitting; and the brake cylinders, head at the right, so an application can be watched walking rearward down the train.
The textbook engineer gets the Climb run in 697 s, stops 22 m short of the mark and inside the box, peaks at 1151 kN of draft (72% of a knuckle) and 951 kN of buff, and never once uses the dynamic brake. It took four rules to get there, and it found every one of them by breaking the train first:
- never set the air while the train is draped over a summit, because every car still on the climb answers that application by hanging back, and the coupler at the top pays for all of them;
- never set the air on a climb at all, because the hill is already the brake, and the two of them together are what the head coupler has to hold;
- treat the drawbar as a budget the throttle spends — and spends faster the slower you go, because effort is power over speed;
- and release on the far side of a sag rather than dragging the brakes up the hill, because the release is all-or-nothing and the recharge wants minutes.
Reusing it
src/run-in.mjs is framework-free ES modules with no DOM in it. new Train({cars, locos, profile, headAt, speed, slack}), then step(dt, {notch, reduction, emergency}). Profile takes grade breakpoints and interpolates
between them, so summits and sags are real vertical curves rather than kinks.
arrivalTimes() and waveSpeed() are the wave probe; engineer() is the
controller; state() is a flat snapshot for a HUD.
One honest caveat about the numbers. The constants are the published ballpark figures for North American practice — 286k cars, 2.5 in of slack per coupler, 360 kip knuckles, 900 ft/s service propagation — and not a calibration against any particular railroad’s data. The model is also strictly one-dimensional: there are no curves in it, so the jackknife limit is standing in for a lateral problem this model cannot actually see. What survives that is the structure: free slack makes the response bimodal, propagation delay turns one handle movement into a velocity difference, and a point force and a body force of equal size are different events. Those do not depend on the constants. The particular kilonewtons do, and they will move if you move them.







