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
1 change: 1 addition & 0 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 |
|---|---|---|---|
| [**Pepper**](machines/pepper) | The Curta Type I, the mechanical calculator that fits in one hand (8 sliders, a 6-digit counter, an 11-digit result): one stepped drum for every column, subtraction by the ten's complement, carries that run column after column, the carriage shifted to multiply, the clearing ring. A model after the public sources; not affiliated with any maker or rights holder. | The cumulative crank angle θ, and the discrete state between turns (sliders, crank up or down, carriage position). A plan of operations (1 234 × 56, one turn back) is laid out once. | `machines/pepper/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
29 changes: 29 additions & 0 deletions machines/pepper/FILM.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# The film

The tour (38 s) is recorded frame by frame from `machines/pepper/curta-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/pepper
node curta-shared/make-timeline.mjs # curta-film/timeline.json (beats), sounds.json (every tick)
node "$SKILL_DIR/sims/build.mjs" ./curta-film
echo '{"lang":"en"}' > /tmp/en.json # or fr / es
node "$SKILL_DIR/sims/capture.mjs" curta-film --timeline curta-film/timeline.json --config /tmp/en.json --size 1600x900 --dpr 1.2 --stills beats --cols 4 --out teardown-out/beats.png
node "$SKILL_DIR/sims/capture.mjs" curta-film --timeline curta-film/timeline.json --config /tmp/en.json --size 1600x900 --dpr 1.2 --out teardown-out/silent.mp4
python3 "$SKILL_DIR/run_sfx.py" curta-shared/score.py # teardown-out/pepper-score.wav, mastered
ffmpeg -y -i teardown-out/silent.mp4 -i teardown-out/pepper-score.wav -map 0:v -map 1:a -c:v copy -c:a aac -b:a 256k -shortest -movflags +faststart teardown-out/pepper-en.mp4
```

The share card is frame 0 of the film, scaled and cropped to 1200 × 630, without metadata:

```sh
node "$SKILL_DIR/sims/capture.mjs" curta-film --config /tmp/en.json --size 1600x900 --dpr 1.2 --still 0 --out teardown-out/frame0.png
ffmpeg -y -i teardown-out/frame0.png -vf "scale=1200:675,crop=1200:630:0:22" -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"`.

`{"lang":"en","clean":true}` records the machine without the sheet. The kit's film build inlines Inter and
JetBrains Mono only; `curta-film/main.js` brings in Instrument Serif itself.
67 changes: 67 additions & 0 deletions machines/pepper/REFERENCES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Pepper: references

Pepper is a model of the Curta Type I mechanical calculator (Curt Herzstark, Contina AG, Mauren, Liechtenstein, from
1948), built from public sources: encyclopedias, a period article, a teaching page and drawings of the internals. It
is not affiliated with any maker or rights holder, and it is not a measured reproduction: where a figure is not
public, the model uses a plausible value, listed below as an estimate.

The letters match the comments in [`curta-shared/machine.js`](curta-shared/machine.js) (the figures and the
arithmetic) and [`curta-shared/curta.js`](curta-shared/curta.js) (the geometry).

## Sources

- **[W]** Wikipedia, [Curta](https://en.wikipedia.org/wiki/Curta). Type I: 8 setting sliders, a 6-digit revolution
counter, an 11-digit result; Type II: 11 / 8 / 15. 230 g (Type I), 360 g (Type II). About 80,000 Type I and
60,000 Type II made; introduced 1948 (Type II 1954), the last one in 1972 according to Herzstark. German patent
DE 747073 (filed 19 August 1938). Pulling the crank up before turning subtracts; subtraction by nines' complement.
- **[F]** Wikipédia, [Curta](https://fr.wikipedia.org/wiki/Curta). Type 1: 8.5 cm high, 5.3 cm across, 230 g,
~600 parts; made 1948–1970, ~80,000 Type 1. The drum carries a second set of teeth, interleaved, each row with the
nine's complement of its digit, engaged by moving the drum (and the crank, which is fixed to it) a few millimetres
in height; the first column takes the ten's complement. Carries: a pin on each result wheel presses a carry lever
as the wheel passes 9 → 0, the lever shifts a pinion on the next column's shaft into the path of the single tooth
of the carry bell, fixed to the drum; a cascade propagates during the turn; a reset cam lifts the levers at the
start of the next turn; a ratchet stops the crank turning backwards. Clearing: a top disc with a fold-out lever,
two racks, the carriage lifted to disengage the wheels. The carriage lifts, turns, and drops into a position.
- **[V]** "The Curta", a period article reprinted at [vcalc.net](https://vcalc.net/cu-pe-mag.htm): 19 rows of teeth
on the drum (9 for addition, 9 complementary, 1 row of ten teeth for the first digit's true complement, met by an
extra cog on that column's axle), the handle extended to raise the drum for subtraction, the carry by transfer
pin, slide and pinion against a single tooth above the drum; capacity 8 × 6 × 11; 6 carriage positions.
- **[H]** E. Hamann, [Curta](https://public.beuth-hochschule.de/hamann/curta/index.html), Beuth Hochschule Berlin:
53 mm across, 105 mm high (with the crank), 230 g; the carriage turns only when lifted; the crank turns clockwise
only; a negative result shows as the complement of the next power of ten (−12 as 99…9988).
- **[M]** Wikimedia Commons, [Category: Curta Internal Details](https://commons.wikimedia.org/wiki/Category:Curta_Internal_Details)
(drawings by Manuco, CC0): the carry lever's actuation and reset cam, carry propagation, the reset racks, the
doubly stepped drum. The geometry of the carry levers, the digit drums lying on their sides and the racks follows
these drawings.
- Photographs used to build the outside (not redistributed here): Wikimedia Commons
[Curta01.JPG](https://commons.wikimedia.org/wiki/File:Curta01.JPG) (a Type I in the hand) and
[Curta Mechanical computer img 1651.jpg](https://commons.wikimedia.org/wiki/File:Curta_Mechanical_computer_img_1651.jpg)
(front view: knurled base and carriage, slotted body with its sliders and rods, the CURTA band, the crank and the
clearing lever).

Not consulted: the service manuals collected at curta.org (Rick Furr) and Herzstark's patent text, which would
settle the estimates below.

## Checked in `check.mjs`

The registers' capacity (99,999,999,999 / 999,999 / 99,999,999), the film's multiplication (1,234 × 56 = 69,104 in
6 + 5 turns, the counter reading 56), the subtraction by the ten's complement (one turn back in position 2 adds
10¹¹ − 12,340, 10 − 4 teeth on the first column, 9 − d on the others, and leaves 56,764 = 1,234 × 46), the carry
cascade of that turn (columns 5 to 11 in order, and one off the top), …999 + 1 = 0 with 10 carries, the timing of the
carries over a turn, and 3,000 random turns against plain arithmetic modulo 10¹¹ and 10⁶.

## Estimates **[E]**

| Figure | Value | Why |
|---|---|---|
| Stations around the drum | 16, 22.5° apart: 8 slider shafts, 3 carry-only shafts, 5 parked places | The model needs every result wheel to sit over a shaft or a parked place in all 6 carriage positions; the real machine spreads its 8 sliders over a wider arc than the model's 157°. |
| Columns above the 8th slider in subtraction | take 9 teeth, as a slider set to 0 would | The sources say only that a negative result reads as the complement of 10¹¹ (99…9988); how the mechanism feeds those 9s is not described in what was read. The arithmetic is exact either way. |
| Timing over a turn | teeth from 15°, 10° apart; the carry tooth meets station s at 105° + 22.5°·(s − 1); the counter steps at 338° | Chosen so each station's teeth end before its carry and the carries run in order; the sources say only that carries are sequential, during the turn. |
| Row pitch, drum shift | 2.4 mm, 1.2 mm (half a row) | "A few millimetres" [F]; the shift is half the pitch by construction [V]. |
| Carriage lift | 3.2 mm | Not given. |
| Ratchet teeth | 12 per turn | The ratchet is described [F], not its count; it sets the ratchet ticks of the sound. |
| Counter carries | in the same step as its digit | The counter's own carry train is not modelled. |
| Interior proportions | drum Ø 12 mm, shafts on a 9.7 mm radius, digit drums Ø 8.4 mm | Scaled to fit the 53 mm body; not measured. |

The brief asked for the drum to move down when the crank is pulled; the sources say it rises with the crank [V][F],
so it rises here.
125 changes: 125 additions & 0 deletions machines/pepper/check.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Recomputes, from the same functions the page draws with, every figure Pepper shows, and fails if one is wrong:
// the registers' capacity, the multiplication of the film, the subtraction by the ten's complement, a carry cascade
// from …999 + 1, the order and timing of the carries over one turn, and 3,000 random turns against plain arithmetic.
// node machines/pepper/check.mjs (CI runs it on every push).
import {rng} from '../../kit/util.js';
import {CURTA, G, addendOf, createPlan, fromDigits, stationOf, thetaCarry, thetaTooth, toDigits, turnEvents} from './curta-shared/machine.js';
import {SETTING, filmFacts, filmPlan} from './curta-shared/schedule.js';

let bad = 0;
const eq = (name, v, want) => {
const ok = v === want;
if (!ok) bad++;
console.log(`${ok ? 'ok ' : 'BAD'} ${name.padEnd(54)} ${v}${ok ? '' : ` (want ${want})`}`);
};
const near = (name, v, lo, hi, unit = '') => {
const ok = v >= lo && v <= hi;
if (!ok) bad++;
console.log(`${ok ? 'ok ' : 'BAD'} ${name.padEnd(54)} ${v} ${unit} [${lo} … ${hi}]`);
};

// ---- the figures [W][F][H]
eq('setting sliders (Type I)', CURTA.sliders, 8);
eq('counter digits (Type I)', CURTA.counter, 6);
eq('result digits (Type I)', CURTA.result, 11);
eq('Type II: 11 · 8 · 15', `${CURTA.typeII.sliders} · ${CURTA.typeII.counter} · ${CURTA.typeII.result}`, '11 · 8 · 15');
near('diameter', CURTA.diameter, 52, 54, 'mm');
near('height', CURTA.height, 84, 86, 'mm');
near('mass', CURTA.mass, 228, 232, 'g');
near('Type I made', CURTA.made, 80000, 80000);
// capacity: the largest number each register holds
eq('result capacity', 10 ** CURTA.result - 1, 99999999999);
eq('counter capacity', 10 ** CURTA.counter - 1, 999999);
eq('setting capacity', 10 ** CURTA.sliders - 1, 99999999);
// the carriage turns through as many positions as the counter has digits; the setting reaches the top column
eq('carriage positions = counter digits', CURTA.counter, 6);

// ---- the film: 1 234 × 56, then one turn back
const plan = filmPlan();
const f = filmFacts(plan);
eq('setting', f.set, 1234);
eq('6 turns in position 1 (counter)', f.m1, 6);
eq('… then 5 in position 2 (counter)', f.m2, 56);
eq('1 234 × 56', f.r1, 1234 * 56);
eq('one turn back in position 2: 69 104 − 12 340', f.r2, 69104 - 12340);
eq('counter after the subtraction', f.c2, 46);
eq('1 234 × 46 = the result', f.r2, 1234 * 46);
const sub = plan.turns[plan.turns.length - 1];
// the ten's complement: 9 − d in each column, 10 − d in the first, over the 11 columns from the carriage position
const add = addendOf(sub.set, sub.p, -1);
const addV = add.reduce((a, x, i) => a + (x ?? 0) * 10 ** i, 0);
eq('addend = 10^11 − 12 340', addV, 10 ** 11 - 12340);
eq('first column takes 10 − 4 teeth', add[sub.p - 1], 6);
eq('…the column above takes 9 − 3', add[sub.p], 6);
// the carries of that turn: one per column from the 5th to the 11th, in order, and one off the top
eq('carries in the subtraction turn', sub.carries.map((c) => c.col).join(','), '5,6,7,8,9,10,11');
eq('overflow off the 11th column', !!sub.overflow, true);
let order = true;
for (let i = 1; i < sub.carries.length; i++) if (!(sub.carries[i].at > sub.carries[i - 1].at && sub.carries[i].from < sub.carries[i].at && sub.carries[i].from > sub.carries[i - 1].from)) order = false;
eq('carries in order, each after the 9 → 0 that set it', order, true);

// ---- …999 + 1: the longest cascade
{
const reg = new Array(CURTA.result).fill(9);
const ev = turnEvents(reg, new Array(CURTA.counter).fill(0), [1, 0, 0, 0, 0, 0, 0, 0], 1, 1);
eq('99 999 999 999 + 1 = 0 (11 digits)', fromDigits(ev.after), 0);
eq('…with 10 carries', ev.carries.length, 10);
eq('…and the 1 off the top', !!ev.overflow, true);
near('…last carry, crank angle', thetaCarry(11), 330, 330, '°');
}

// ---- the timing over one turn: every station's teeth end before its carry, the carry tooth runs in order, the
// counter steps last, everything inside 360°
{
let ok = true;
for (let s = 1; s <= G.active; s++) {
if (s > 1 && thetaCarry(s) <= thetaCarry(s - 1) + G.carryDur) ok = false;
// a wrap in column s (teeth or carry) always comes before the carry tooth reaches s + 1
if (Math.max(thetaTooth(s, s === 1 ? 9 : 8) + G.toothDur, thetaCarry(s) + G.carryDur) >= thetaCarry(s + 1)) ok = false;
}
eq('teeth, then carries in order, per station', ok, true);
near('carries span (crank angle)', thetaCarry(G.active) - thetaCarry(2), 0, 240, '°');
near('counter step after the last carry', G.counterAt - (thetaCarry(G.active) + G.carryDur), 0, 30, '°');
near('the complement shift = half a row', G.shift / G.rowPitch, 0.5, 0.5);
}

// ---- 3,000 random turns against plain arithmetic, mod 10^11 and 10^6
{
const r = rng(1948);
let ok = true;
let reg = new Array(CURTA.result).fill(0);
let ctr = new Array(CURTA.counter).fill(0);
let R = 0n;
let C = 0;
const M = 10n ** 11n;
for (let k = 0; k < 3000; k++) {
const set = Array.from({length: 8}, () => Math.floor(r() * 10));
const p = 1 + Math.floor(r() * 6);
const sign = r() < 0.4 ? -1 : 1;
const ev = turnEvents(reg, ctr, set, p, sign);
reg = ev.after;
ctr = ev.ctrAfter;
R = (((R + BigInt(sign) * BigInt(fromDigits(set)) * 10n ** BigInt(p - 1)) % M) + M) % M;
C = (((C + sign * 10 ** (p - 1)) % 1e6) + 1e6) % 1e6;
const got = BigInt(reg.reduce((a, x, i) => a + BigInt(x) * 10n ** BigInt(i), 0n));
if (got !== R || fromDigits(ctr) !== C) ok = false;
}
eq('3,000 random turns = arithmetic mod 10^11 / 10^6', ok, true);
}
// the live page's plan gives the same as the film's for the same steps
{
const p2 = createPlan();
p2.add({kind: 'slide', t0: 0, t1: 1, to: SETTING});
p2.add({kind: 'turns', t0: 1, t1: 2, n: 6});
p2.add({kind: 'carriage', t0: 2, t1: 3, to: 2});
p2.add({kind: 'turns', t0: 3, t1: 4, n: 5});
eq('a plan built step by step', fromDigits(p2.end.reg), 69104);
eq('stations: column 1 in position 2 is parked', stationOf(1, 2), 0);
}
void toDigits;

if (bad) {
console.error(`\n${bad} figure(s) wrong`);
process.exit(1);
}
console.log('\nevery figure checks');
9 changes: 9 additions & 0 deletions machines/pepper/curta-film/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Pepper, the explorer film. Shared code in ../curta-shared. The kit's film build inlines Inter and JetBrains Mono
// only; Instrument Serif (the sheet's titles) comes in here, as the site build gives it to the live page.
import serif from '../../../node_modules/@fontsource/instrument-serif/files/instrument-serif-latin-400-normal.woff2';
import {run} from '../curta-shared/film.js';

const style = document.createElement('style');
style.textContent = `@font-face{font-family:'Instrument Serif';font-weight:400;font-style:normal;font-display:block;src:url(data:font/woff2;base64,${serif}) format('woff2');}`;
document.head.appendChild(style);
document.fonts.load("400 20px 'Instrument Serif'").finally(run);
1 change: 1 addition & 0 deletions machines/pepper/curta-film/sounds.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"ratchet":[6.39,6.854,7.218,7.543,7.85,8.151,8.458,8.781,9.143,9.6,10.125,11.999,12.128,12.219,12.292,12.353,12.406,12.454,12.498,12.538,12.575,12.609,12.673,12.702,12.73,12.757,12.783,12.807,12.831,12.854,12.876,12.898,12.919,12.959,12.978,12.997,13.015,13.033,13.05,13.067,13.085,13.103,13.122,13.141,13.181,13.202,13.224,13.246,13.269,13.293,13.317,13.343,13.37,13.398,13.427,13.491,13.525,13.562,13.602,13.646,13.694,13.747,13.808,13.881,13.972,14.101,18.299,18.428,18.519,18.592,18.653,18.706,18.754,18.798,18.838,18.875,18.909,18.973,19.002,19.03,19.057,19.083,19.107,19.131,19.154,19.176,19.198,19.219,19.259,19.278,19.297,19.315,19.333,19.35,19.367,19.385,19.403,19.422,19.441,19.481,19.502,19.524,19.546,19.569,19.593,19.617,19.643,19.67,19.698,19.727,19.791,19.825,19.862,19.902,19.946,19.994,20.047,20.108,20.181,20.272,20.401,23.539,23.813,24.066,24.344,24.777,25.354,26.002,26.71,27.438,28.137,28.712],"home":[10.902,12.642,12.939,13.161,13.457,14.473,18.942,19.239,19.461,19.757,20.773,29.327],"carry":[{"t":12.764,"col":2},{"t":12.783,"col":3},{"t":13.252,"col":2},{"t":13.287,"col":4},{"t":13.646,"col":3},{"t":19.083,"col":4},{"t":19.101,"col":5},{"t":19.319,"col":3},{"t":19.913,"col":3},{"t":19.946,"col":4},{"t":25.203,"col":5},{"t":25.669,"col":6},{"t":26.175,"col":7},{"t":26.71,"col":8},{"t":27.256,"col":9},{"t":27.794,"col":10},{"t":28.303,"col":11}],"overflow":[28.838],"counter":[10.447,12.627,12.93,13.152,13.444,14.217,18.927,19.23,19.452,19.744,20.517,28.979],"slide":[2.698,2.829,2.916,3.047,3.016,3.161,3.306,3.353,3.547,3.739,34.198,34.286,34.344,34.432,34.411,34.507,34.604,34.635,34.765,34.892],"carriageLift":[15.336,31.096],"carriageNotch":[16.769,33.315],"carriageDrop":[17.304,33.616],"crank":[22.25,30.225],"ring":[[31.348,32.916]]}
1 change: 1 addition & 0 deletions machines/pepper/curta-film/timeline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"duration":38,"beats":{"views":[2,4.6,11.2,21.1,23.9,30.4,34.2],"firstTooth":6.2,"turnsEnd":14.6,"carriage":16.2,"subStart":23.8,"cascade":26.175,"overflow":28.838,"clear":32.3,"end":36.5}}
5 changes: 5 additions & 0 deletions machines/pepper/curta-live/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {runLive} from '../curta-shared/live.js';

// Pepper as an interactive page: your hand on the crank, the carriage and the setting drums; the subsystems opened
// one by one; the body solid, glass, cut or exploded; orbit and zoom; and an auto tour when left alone.
runLive();
Loading