Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ the same model that moves the parts. Left alone, each page plays a guided tour;

| | The machine | What drives it | Checked by |
|---|---|---|---|
| [**Radial**](machines/radial) | The Pratt & Whitney R-2800 Double Wasp, an 18-cylinder twin-row radial: the starter, the first firings missed then caught, the firing order going round the star, one row in section with its master rod and eight articulated rods (whose pistons do not quite follow the master's law), the rows pulled apart, a cam ring lifting a valve, the climb to 2,700 rpm. A C-series CB16 after public sources; not affiliated with Pratt & Whitney. | The crank angle, integrated once from a planned run of the engine speed. Power, torque and manifold pressure follow from it. | `machines/radial/check.mjs` |
| [**Quantum**](machines/quantum) | A photonic quantum computer: a quantum dot emits one photon per laser pulse, fibre delay lines bring them onto a 24-mode chip together, 276 Mach-Zehnder interferometers mix them, superconducting detectors click. A Hong-Ou-Mandel test on one coupler, the Perceval program behind the heaters. Modelled on the public figures of Quandela's Belenos; not affiliated with Quandela. | The physical time τ of the shot, in nanoseconds. The output distribution is computed from the permanents of the chip's unitary. | `machines/quantum/check.mjs` |
| [**Spindl**](machines/spindl) | A 3.5-inch 7,200 rpm desktop hard drive (2 platters, 4 heads): spin-up, the heads loading off the ramp, a seek drawn live, the rotational wait, the 3 nm flying height, the emergency park. A model of a typical drive, not a specific product. | The drive time τ. A request queue (LBA → zone, cylinder, head, sector) is planned once. | `machines/spindl/check.mjs` |

Expand Down Expand Up @@ -66,8 +67,8 @@ Content-Security-Policy that allows the pages' inline scripts by their hashes, c
## Credits

Built with Claude Code (Claude Opus 5.5) and the teardown skill, whose kit, frame capture and sound engine come from
[whaleyxbt/claude-motion](https://github.com/whaleyxbt/claude-motion) (MIT). three.js (MIT). Inter, JetBrains Mono and
Instrument Serif (SIL Open Font License).
[whaleyxbt/claude-motion](https://github.com/whaleyxbt/claude-motion) (MIT). three.js (MIT). Inter, JetBrains Mono,
Instrument Serif, Barlow Condensed and Saira Stencil One (SIL Open Font License).

## License

Expand Down
31 changes: 31 additions & 0 deletions machines/radial/FILM.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# The film

The tour (40 s) is recorded frame by frame from `machines/radial/r2800-film`, the same code as the page, with the
capture tools of the teardown skill ([whaleyxbt/claude-motion](https://github.com/whaleyxbt/claude-motion)). They need
the skill installed (`SKILL_DIR`), headless Chrome with a real GPU, `ffmpeg` and Python 3.

```sh
cd machines/radial
node r2800-shared/make-timeline.mjs # r2800-film/timeline.json (beats) and events.json (every spark, the run at 100 Hz)
node "$SKILL_DIR/sims/build.mjs" ./r2800-film
echo '{"lang":"en"}' > /tmp/en.json # or fr / es
node "$SKILL_DIR/sims/capture.mjs" r2800-film --timeline r2800-film/timeline.json --config /tmp/en.json --size 1600x900 --dpr 1.2 --stills beats --cols 3 --out teardown-out/beats.png
node "$SKILL_DIR/sims/capture.mjs" r2800-film --timeline r2800-film/timeline.json --config /tmp/en.json --size 1600x900 --dpr 1.2 --out teardown-out/silent.mp4
python3 "$SKILL_DIR/run_sfx.py" r2800-shared/score.py # teardown-out/radial-score.wav, mastered to −14 LUFS
ffmpeg -y -i teardown-out/silent.mp4 -i teardown-out/radial-score.wav -map 0:v -map 1:a -c:v copy -c:a aac -b:a 256k -shortest -movflags +faststart teardown-out/radial-en.mp4
```

The share card is a frame of the film without the console, the star from the front at idle:

```sh
echo '{"lang":"en","clean":true}' > /tmp/clean.json
node "$SKILL_DIR/sims/capture.mjs" r2800-film --config /tmp/clean.json --size 1600x900 --dpr 1.2 --still 11 --out teardown-out/og-src.png
ffmpeg -y -i teardown-out/og-src.png -vf "scale=1200:-1,crop=1200:630:0:(ih-630)/2" -map_metadata -1 og.png
```

On macOS, Playwright's headless shell may get no WebGL context; point the capture at Chrome:
`CHROMIUM="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"`.

The sound is built on the same timeline: the starter's whine, a cough for each spark that lights while the engine
catches (the missed ones stay silent), a thud per firing while they come slower than ~12 a second on screen, then a
growl whose pulse is the firing rate on screen, and the propeller's wash.
87 changes: 87 additions & 0 deletions machines/radial/REFERENCES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Radial: references

Radial is a model of a Pratt & Whitney R-2800 Double Wasp, the 18-cylinder twin-row radial of the F6F Hellcat, the
F4U Corsair, the P-47 and, after the war, the Douglas DC-6. It is built from public sources, as a **C-series CB16**
(the DC-6B's engine, single-stage two-speed supercharger, 0.45 : 1 propeller gear), and it is not affiliated with
Pratt & Whitney. Where a figure is not public, the model uses a plausible value, listed below under **[E]**.

The letters match the comments in [`r2800-shared/engine.js`](r2800-shared/engine.js), the one file every pose,
gauge and sound reads its numbers from.

## Sources

- **[F]** FAA, Type Certificate Data Sheet **6A3**, Douglas DC-6A / DC-6B, revision 28
([copy on enginehistory.org](https://www.enginehistory.org/Operations/WaterInjServErrors/6A3Rev28.pdf)). Engine
limits of the P&W Double Wasp CB-16: take-off (two minutes, dry) 2,050 hp at 2,700 rpm and 55.0 inHg; take-off wet
(water-alcohol) 2,400 hp at 2,800 rpm and 59.5 inHg; maximum continuous 1,800 hp at 2,600 rpm and 48.5 inHg;
impeller gear ratios 7.29 : 1 (low) and 8.58 : 1 (high); ".45:1 propeller reduction gearing". The film climbs to
the dry take-off point.
- **[N]** Smithsonian National Air and Space Museum,
[Pratt & Whitney Double Wasp R-2800 CB16](https://airandspace.si.edu/collection-objects/pratt-whitney-double-wasp-r-2800-cb16-2-row-radial-18-engine/nasm_A19580058000)
(A19580058000): weight 1,068 kg (2,350 lb), diameter 52.8 in, length 81.4 in, gear-driven single-stage two-speed
supercharger, 2,400 hp at 2,800 rpm.
- **[W]** K. McCutcheon, [Pratt & Whitney Double Wasp (R-2800)](https://enginehistory.org/Piston/P&W/R-2800/r-2800.shtml),
Aircraft Engine Historical Society: bore 5.75 in, stroke 6.000 in, 2,804.5 in³; two master rod assemblies, each a
one-piece master rod and eight "I"-section articulated rods on knuckle pins; a crankshaft of three steel forgings
joined at the crankpins; forged aluminium heads screwed and shrunk on forged steel barrels, finned aluminium muffs;
one inlet and one exhaust valve per cylinder; valve gear and ignition "substantially the same as the Twin Wasp".
The C series' compression ratio, 6.75 : 1, is from the
[R-2800 article](https://en.wikipedia.org/wiki/Pratt_%26_Whitney_R-2800_Double_Wasp) (Wikipedia, citing the
manufacturer's data).
- **[H]** FAA, *Aviation Maintenance Technician Handbook, Powerplant* (FAA-H-8083-32), the chapters on radial
engines: the firing order numbers of an 18-cylinder twin-row radial are **+11 and −7**, which gives
1-12-5-16-9-2-13-6-17-10-3-14-7-18-11-4-15-8; cylinders numbered clockwise from the top of the rear row, seen from
the rear, odd in the rear row; a nine-cylinder row's cam ring has 4 lobes per track and turns against the crank
(a lobe every 90°, at ⅛ crank speed). Copies of those pages:
[firing order](https://www.aircraftsystemstech.com/p/firingorder-firing-order-of-engine-is.html),
[valve operating mechanism](https://aircraftsystemstech.com/p/valveoperating-mechanism-for.html).

The geometry was built against photographs of real engines (Wikimedia Commons):
[a sectioned R-2800](https://commons.wikimedia.org/wiki/File:Cross_Section_of_a_Pratt_and_Whitney_R2800_Double_Wasp.JPG)
(the profile in section: nose case and reduction gear, both rows, master rods, crank, impeller),
[an R-2800 on its stand](https://commons.wikimedia.org/wiki/File:HARP_Pratt_%26_Whitney_R-2800_Double_Wasp.JPG)
(nose, magnetos, harness ring, pushrod tubes, baffles),
[the R-2800-21 at the National Museum of the USAF](https://commons.wikimedia.org/wiki/File:Pratt_%26_Whitney_R-2800-21_USAF.jpg)
(the star from the front, black intake pipes, aluminium rocker boxes) and
[an R-2800 in profile](https://commons.wikimedia.org/wiki/File:20-09-083-R_2800.jpg) (the long supercharger and
accessory section). They belong to their authors and are not in this repository.

## Estimates **[E]**

Not public, or not found in a primary source; chosen to be plausible:

| Figure | Value | Why |
|---|---|---|
| Master rod, crankpin to piston pin | 292 mm (11.5 in) | about 3.8 crank throws; fits the 1,341 mm diameter with the heads and rocker boxes |
| Knuckle-pin circle | 60.3 mm (2.375 in) | just outside the 92 mm crankpin and its bearing |
| Articulated rod | 231.7 mm (master − knuckle circle) | the usual first choice; the knuckle pins sit at exactly 40° steps (see below) |
| Master rods in cylinders | 13 (rear row), 8 (front row) | not found in the sources above |
| Row spacing along the axis | 210 mm | the reference photos |
| Valve timing | inlet 20° BTDC – 60° ABDC, exhaust 50° BBDC – 20° ATDC | typical of the period; lift 15.9 mm |
| Spark | 25° before top centre | typical |
| Idle, starter cranking | 700 rpm, 140 rpm | typical |
| Propeller | three blades, 4.0 m | a Hamilton Standard of the DC-6B's size |
| Power at other speeds | the propeller absorbs P ∝ N³ through the run, 2,050 hp at 2,700 rpm | a fixed-pitch law; inertia neglected |
| Manifold pressure | bmep = k · MAP − friction (1.2 + 0.6 (N/2,700)² bar) − pumping | k set so that 2,050 hp needs 55.0 inHg exactly; the same law gives 1,702 hp at the METO point (1,800 hp [F], −5.4 %) |
| Reduction gear layout | sun 72 teeth on the crank, ring 88 fixed, carrier on the shaft | gives 0.45 exactly; P&W's tooth counts are not public |

## The model, by hand

- π/4 × 5.75² × 6 × 18 = **2,804.5 in³** (45.96 L).
- 2,050 hp = 1,528.7 kW at 2,700 rpm (282.7 rad/s): **5,407 N·m** at the crank, a brake mean effective pressure of
14.8 bar.
- 18 firings in two turns: 9 × 2,700 / 60 = **405 firings a second**, one every 40° of crank.
- Mean piston speed 2 × 0.1524 m × 45 rev/s = **13.7 m/s**. Propeller 0.45 × 2,700 = 1,215 rpm; impeller
7.29 × 2,700 = 19,683 rpm.
- The cam ring: each tappet needs one lift in 720° of crank; 4 lobes on a ring turning at ⅛ crank speed against it
bring one every 90° × 8 = 720°. The model reads every valve's lift off the turning ring and check.mjs verifies it
equals the valve timing for all 36 valves.
- **The articulated rods.** The master rod's piston follows the plain slider-crank law: stroke 152.40 mm, top centre
when its crankpin points at it. An articulated rod hangs on a knuckle pin that orbits on the master rod's big end,
an ellipse rather than a circle, so its piston does not: with the knuckle pins at exact 40° steps the model gives
strokes from 152.41 to **153.08 mm**, top centres up to **3.7°** early or late and up to 2.45 mm lower, so a
compression ratio down to 6.27 against the master's 6.75. Real engines trim the knuckle-pin positions to even this
out; how P&W did it for the R-2800 is not in the public sources, so the page shows the uncorrected geometry and
says so.

`node machines/radial/check.mjs` recomputes each of these from the code.
99 changes: 99 additions & 0 deletions machines/radial/check.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Recomputes, from the same functions the page draws with, every figure Radial shows, and fails if one leaves the
// range its source gives. node machines/radial/check.mjs (CI runs it on every push).
import {CAM, CYL, DISPL_IN3, DISPL_L, E, FIRE, GEAR, hpOf, LAW, lift, liftFromTiming, ORDER, pinAngle, RATING, ROWS, stateAt} from './r2800-shared/engine.js';
import {DURATION, rpmAt, sparkList, thetaAt} from './r2800-shared/schedule.js';

let bad = 0;
const near = (name, v, lo, hi, unit = '') => {
const ok = v >= lo && v <= hi;
if (!ok) bad++;
console.log(`${ok ? 'ok ' : 'BAD'} ${name.padEnd(52)} ${Number(v.toPrecision(6))} ${unit} [${lo} … ${hi}]`);
};
const mod = (a, m) => ((a % m) + m) % m;

// displacement: π/4 × 5.75² × 6 × 18 = 2,804.5 in³ [W]
near('displacement', DISPL_IN3, 2804, 2805, 'in³');
near('displacement', DISPL_L, 45.9, 46.0, 'L');

// the firing order: +11, −7 [H], each cylinder once in 720°, 40° apart, the rows alternating
const BRIEF = '1-12-5-16-9-2-13-6-17-10-3-14-7-18-11-4-15-8';
near('firing order is ' + BRIEF, ORDER.join('-') === BRIEF ? 1 : 0, 1, 1);
near('cylinders in the order, each once', new Set(ORDER).size, 18, 18);
let alt = 0;
for (let i = 0; i < 18; i++) if (CYL[ORDER[i] - 1].row !== CYL[ORDER[(i + 1) % 18] - 1].row) alt++;
near('consecutive firings in different rows', alt, 18, 18);
const gaps = ORDER.map((c, i) => mod(FIRE[ORDER[(i + 1) % 18]] - FIRE[c], 720));
near('interval between firings (min)', Math.min(...gaps), 40, 40, '°');
near('interval between firings (max)', Math.max(...gaps), 40, 40, '°');
// at its firing TDC the row's crankpin points at the cylinder (the geometry agrees with the order)
let miss = 0;
for (const cy of CYL) miss = Math.max(miss, Math.abs(((mod(pinAngle(cy.row, FIRE[cy.c]) - cy.angle, 360) + 180) % 360) - 180));
near('crankpin on the cylinder at its firing', miss, 0, 1e-9, '°');
// every spark of the film, counted per cylinder over the running part: once per two turns
{
const sp = sparkList();
const t0 = 8;
const t1 = DURATION - 0.5;
const turns = (thetaAt(t1) - thetaAt(t0)) / 360;
const per = CYL.map((cy) => sp.filter((s) => s[1] === cy.c && s[0] >= t0 && s[0] < t1).length);
near('sparks per cylinder per 2 turns (film)', Math.min(...per) / (turns / 2), 0.98, 1.02);
near('', Math.max(...per) / (turns / 2), 0.98, 1.02);
}

// the master rod and the articulated rods, exact: the master's piston follows the plain slider-crank law
const mF = LAW[E.master.front];
near('master stroke (2 × crank throw)', mF.stroke, 152.399, 152.401, 'mm');
near('master TDC at its pin angle', Math.abs(mF.tdcLag), 0, 0.05, '°');
const arts = CYL.filter((c) => c.c !== E.master.front && c.c !== E.master.rear).map((c) => LAW[c.c]);
near('articulated strokes, shortest', Math.min(...arts.map((a) => a.stroke)), 152.4, 153.5, 'mm');
near('articulated strokes, longest', Math.max(...arts.map((a) => a.stroke)), 152.4, 153.5, 'mm');
near('articulated TDC, largest shift', Math.max(...arts.map((a) => Math.abs(a.tdcLag))), 0.1, 6, '°');
near('articulated TDC, lowest below the master', -Math.min(...arts.map((a) => a.dTDC)), 0, 4, 'mm');
near('compression ratio, master', mF.cr, 6.7499, 6.7501);
near('compression ratio, lowest articulated', Math.min(...arts.map((a) => a.cr)), 6.0, 6.75);
for (const row of ['rear', 'front']) {
const ws = ROWS[row].filter((c) => c.c !== E.master[row]).sort((a, b) => Math.abs(LAW[b.c].tdcLag) - Math.abs(LAW[a.c].tdcLag))[0];
const x = LAW[ws.c];
console.log(` ${row} row: master ${E.master[row]}, most different ${ws.c}: stroke ${x.stroke.toFixed(3)} mm, TDC ${x.tdcLag.toFixed(2)}°, ${(-x.dTDC).toFixed(2)} mm lower, CR ${x.cr.toFixed(3)}`);
}

// the cam rings: 4 lobes at ⅛ crank speed against the crank [H]; the lift read off the ring equals the valve timing
near('cam ring speed', CAM.ratio, -0.125, -0.125);
near('lobes × 2 × speed = once per two turns', CAM.lobes * 2 * Math.abs(CAM.ratio), 1, 1);
let err = 0;
let opens = 0;
for (const cy of CYL)
for (const ev of ['in', 'ex']) {
let prev = lift(cy.c, ev, -0.5);
for (let th = 0; th < 720; th += 0.5) {
const a = lift(cy.c, ev, th);
err = Math.max(err, Math.abs(a - liftFromTiming(cy.c, ev, th)));
if (a > 0.5 && prev <= 0.5) opens++;
prev = a;
}
}
near('cam lift vs valve timing (max error)', err, 0, 1e-9);
near('valve openings in 720° (36 valves)', opens, 36, 36);

// power: the take-off point is exact [F], the maximum-continuous point within 7 %
const to = stateAt(RATING.takeoff.rpm);
near('take-off power at 2,700 rpm', to.hp, 2049.9, 2050.1, 'hp');
near('take-off manifold pressure', to.map, 54.99, 55.01, 'inHg');
near('METO 1,800 hp at 2,600 rpm, 48.5″ (model)', hpOf(RATING.meto.map, RATING.meto.rpm), 1800 * 0.93, 1800 * 1.07, 'hp');
near('take-off torque', to.torque, 5400, 5415, 'N·m');
near('firing frequency at 2,700 rpm', to.fireHz, 405, 405, 'Hz');
near('mean piston speed at 2,700 rpm', to.pistonMs, 13.71, 13.72, 'm/s');
near('propeller at 2,700 rpm (0.45 : 1)', to.propRpm, 1215, 1215, 'rpm');
near('impeller at 2,700 rpm (7.29 : 1)', to.impRpm, 19683, 19683, 'rpm');
near('drawn reduction gear 72 / (72 + 88)', 72 / (72 + 88), GEAR.prop, GEAR.prop);
near('idle manifold pressure (model)', stateAt(RATING.idle).map, 8, 16, 'inHg');

// the film's run reaches take-off rpm and comes back to idle
near('film: peak rpm', Math.max(...Array.from({length: 400}, (_, i) => rpmAt(i / 10))), 2690, 2701, 'rpm');
near('film: rpm at the end', rpmAt(DURATION), RATING.idle - 5, RATING.idle + 5, 'rpm');

if (bad) {
console.error(`\n${bad} figure(s) out of range`);
process.exit(1);
}
console.log('\nevery figure in range');
Binary file added machines/radial/og.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions machines/radial/r2800-film/events.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions machines/radial/r2800-film/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Radial, the R-2800 film. Shared code in ../r2800-shared.
import {run} from '../r2800-shared/film.js';

run();
1 change: 1 addition & 0 deletions machines/radial/r2800-film/timeline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"duration":40,"beats":{"views":[1.6,3.3,6.4,12.6,18.6,23.6,28.4,32.6,35.4],"presses":[1.6,3.3,28.4,35.4],"starter":1.6,"ignition":3.3,"disengage":4.6,"climb":29.7,"full":32.7,"back":35.6,"idleAgain":38.6}}
5 changes: 5 additions & 0 deletions machines/radial/r2800-live/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {runLive} from '../r2800-shared/live.js';

// Radial as an interactive page: starter, magnetos, throttle, slow motion, seven subsystems, cutaways, four views,
// orbit and zoom, and an auto tour when left alone.
runLive();
Loading