index.jsx
12.0 kB · jsx · 253 lines
1import { useEffect, useMemo, useReducer, useRef, useState } from 'react';2import { ready, ink, fit } from '../../lib/mrly.js';3import { mount, Page, Row, Pick, Slider, Text, Btn, Stats, Stat, Note, Group } from '../../lib/app.jsx';4import { Sketch } from '../../lib/draw.jsx';5import { useQuery } from '../../lib/query.js';6import { useSeeds, roll } from '../../lib/select.jsx';7import { board, bars, line, axis, tag } from '../../lib/chart.js';89const m = await ready();10const TOPS = [100, 1000, 10000, 100000, 1000000];11const FIRST = { n: '360', limit: 100, top: 10000, detect: 169 };12const STEPS = +(new URLSearchParams(location.search).get('steps') ?? 0);1314const shuffle = (seed) => {15 const [n, limit, top, detect] = roll(seed, [[2, 1000000], [10, 400], [0, TOPS.length - 1], [3, 199]]);16 return { n: String(n), limit, top: TOPS[top], detect: detect | 1 };17};1819const first = (seeds) => (seeds.get() ? shuffle(seeds.get()) : FIRST);2021function App() {22 const s = useSeeds();23 const [look, set] = useQuery(first(s));24 const [boot] = useState(() => {25 try {26 const made = new m.Sieve(look.limit);27 let at = 0;28 for (let k = 0; k < STEPS; k++) at = made.step();29 return { sieve: made, at, error: null };30 } catch (error) {31 return { sieve: null, at: 0, error };32 }33 });34 const sieve = useRef(boot.sieve);35 const [current, setCurrent] = useState(boot.at);36 const [error, setError] = useState(boot.error);37 const [running, setRunning] = useState(false);38 const [tick, force] = useReducer((x) => x + 1, 0);39 const kept = useRef({ pile: null, data: null, trial: null });4041 const view = useMemo(() => {42 try {43 const pile = JSON.parse(m.factor(look.n.trim()));44 const data = JSON.parse(m.prime_chart(look.top, 400));45 const trial = JSON.parse(m.carpet_witness(look.detect));46 kept.current = { pile, data, trial };47 return { ...kept.current, error: null };48 } catch (error) {49 return { ...kept.current, error };50 }51 }, [look.n, look.top, look.detect]);5253 const reset = (limit) => {54 setRunning(false);55 setCurrent(0);56 try {57 sieve.current = new m.Sieve(limit);58 setError(null);59 } catch (error) {60 setError(error);61 }62 force();63 };6465 const step = () => {66 if (!sieve.current || sieve.current.done()) return;67 setCurrent(sieve.current.step());68 if (sieve.current.done()) setRunning(false);69 force();70 };7172 useEffect(() => {73 if (!running) return;74 step();75 const timer = setInterval(step, 600);76 return () => clearInterval(timer);77 }, [running]);7879 const random = () => {80 const next = shuffle(s.next());81 set(next);82 reset(next.limit);83 };8485 const sheet = (canvas) => {86 if (!sieve.current) return;87 const types = sieve.current.types(), limit = types.length - 1, mark = sieve.current.rank() + 1;88 const cols = limit > 100 ? 20 : 10, rows = Math.ceil(limit / cols);89 const cell = canvas.clientWidth / cols;90 const [ctx, w, h] = fit(canvas, Math.ceil(rows * cell));91 const mono = getComputedStyle(document.body).getPropertyValue('--mono');92 ctx.fillStyle = ink.deep;93 ctx.fillRect(0, 0, w, h);94 ctx.font = `${Math.min(13, cell * 0.42)}px ${mono}`;95 ctx.textAlign = 'center';96 ctx.textBaseline = 'middle';97 for (let n = 1; n <= limit; n++) {98 const t = types[n], x = ((n - 1) % cols) * cell, y = Math.floor((n - 1) / cols) * cell;99 const lit = n === current || t === mark;100 ctx.fillStyle = n === current ? ink.blue : t === mark ? ink.orange : t === 1 ? ink.yellow : t ? ink.line : ink.panel;101 ctx.fillRect(x + 1, y + 1, cell - 2, cell - 2);102 if (cell >= 15) {103 ctx.fillStyle = lit || t === 1 ? ink.bg : ink.dim;104 ctx.fillText(n, x + cell / 2, y + cell / 2 + 1);105 }106 }107 };108109 const stones = (canvas) => {110 const pile = view.pile;111 if (!pile) return;112 const rects = pile.rectangles, n = pile.n;113 const width = canvas.clientWidth, few = n <= 60;114 const stone = few ? (width - 80) / n : 0;115 const rise = few ? rects.reduce((sum, [a]) => sum + a * stone + 8, 0) : 240;116 const [ctx, w, h] = fit(canvas, Math.max(Math.ceil(rise), 60));117 ctx.fillStyle = ink.deep;118 ctx.fillRect(0, 0, w, h);119 const mono = getComputedStyle(document.body).getPropertyValue('--mono');120 ctx.font = `11px ${mono}`;121 if (few) {122 let y = 4;123 for (const [a, b] of rects) {124 ctx.fillStyle = rects.length === 1 ? ink.yellow : ink.blue;125 for (let i = 0; i < a; i++) {126 for (let j = 0; j < b; j++) {127 ctx.beginPath();128 ctx.arc((j + 0.5) * stone, y + (i + 0.5) * stone, stone * 0.36, 0, Math.PI * 2);129 ctx.fill();130 }131 }132 ctx.fillStyle = ink.fg;133 ctx.textAlign = 'right';134 ctx.fillText(`${a} by ${b}`, w - 4, y + a * stone / 2 + 4);135 y += a * stone + 8;136 }137 return;138 }139 const span = Math.log(n);140 const px = (v) => 8 + (w - 16) * Math.log(v) / span;141 const py = (v) => h - 20 - (h - 36) * Math.log(v) / span;142 rects.forEach(([a, b], k) => {143 const x = px(b), y = Math.min(py(a), h - 24);144 ctx.fillStyle = rects.length === 1 ? ink.yellow : ink.blue;145 ctx.globalAlpha = 0.18;146 ctx.fillRect(8, y, x - 8, h - 20 - y);147 ctx.globalAlpha = 1;148 ctx.strokeStyle = ctx.fillStyle;149 ctx.strokeRect(8.5, y + 0.5, x - 8, h - 20 - y);150 if (k === rects.length - 1 || k === 0) {151 ctx.fillStyle = ink.fg;152 ctx.textAlign = k ? 'right' : 'left';153 ctx.fillText(`${a} by ${b}`, k ? x - 4 : 12, y - 5);154 }155 });156 ctx.fillStyle = ink.dim;157 ctx.textAlign = 'left';158 ctx.fillText('1', 8, h - 6);159 ctx.textAlign = 'right';160 ctx.fillText(`${n} stones, sides on a log scale`, w - 8, h - 6);161 };162163 const chart = (canvas) => {164 const data = view.data;165 if (!data) return;166 const b = board(canvas, 220);167 const top = data.x.at(-1), last = data.x.length - 1;168 const peak = Math.max(data.li.at(-1), data.pi.at(-1), data.ratio.at(-1));169 const trace = (column) => column.map((v, k) => [data.x[k] / top, Math.max(0, v) / peak]);170 line(b, trace(data.ratio), ink.pink, { dash: [4, 4] });171 line(b, trace(data.li), ink.blue);172 line(b, trace(data.pi), ink.yellow, { width: 2 });173 axis(b, [[0, '0'], [1, String(top)]]);174 let x = tag(b, `pi(x) ${data.pi[last]}`, ink.yellow);175 x = tag(b, `x / ln x ${data.ratio[last].toFixed(1)}`, ink.pink, 'left', x + 14);176 tag(b, `li(x) ${data.li[last].toFixed(1)}`, ink.blue, 'left', x + 14);177 };178179 const witness = (canvas) => {180 const trial = view.trial;181 if (!trial) return;182 const b = board(canvas, 220);183 const count = trial.scales.length;184 bars(b, trial.row, { color: ink.yellow });185 const every = Math.max(1, Math.round(count / 8));186 axis(b, trial.scales.map((scale, k) => [(k + 0.5) / count, scale]).filter((_, k) => k % every === 0));187 if (trial.prime) tag(b, `${trial.n}: every bar is exactly zero, prime`, ink.green);188 else tag(b, `${trial.n}: largest ${trial.max.toFixed(4)} at scale ${trial.at}`, ink.yellow);189 };190191 const done = sieve.current ? sieve.current.done() : false;192 const pile = view.pile, data = view.data, trial = view.trial;193194 const controls = (195 <>196 <Group name="The sieve">197 <Slider label="sieve up to" value={look.limit} min={10} max={400} onChange={(v) => { set({ limit: v }); reset(v); }} />198 <Btn onClick={() => { setRunning(false); step(); }}>Step</Btn>199 <Btn onClick={() => { if (sieve.current?.done()) reset(look.limit); setRunning(!running); }}>{running ? 'Pause' : 'Play'}</Btn>200 <Btn onClick={() => reset(look.limit)}>Reset</Btn>201 </Group>202 <Group name="The numbers">203 <Text label="stones" value={look.n} onChange={(v) => set({ n: v })} />204 <Pick label="count to" value={look.top} options={TOPS.map((t) => [t, t])} onChange={(v) => set({ top: +v })} />205 <Slider label="scale on trial" value={look.detect} min={3} max={199} step={2} onChange={(v) => set({ detect: v })} />206 </Group>207 <Group name="Seed">208 <Btn onClick={random}>Randomize</Btn>209 </Group>210 </>211 );212213 return (214 <Page crumb="primes" title="Numbers that will not split"215 sub="Take a handful of stones and try to lay them out as a rectangle. Twelve stones make three: one by twelve, two by six, three by four. Thirteen stones make one long row and nothing else, so thirteen is prime. Below, the sieve crosses out every number that splits, the stones show the rectangles of any number you type, the chart counts the primes up the number line, and the carpet stack finds the primes with no arithmetic at all."216 foot={<>The sieve is the one Eratosthenes ran: the next untouched number is prime, and its multiples from its square onward are struck. The stones are the divisors of a number paired below and above its square root; a single rectangle means prime. <code>pi(x)</code> counts the primes up to <code>x</code>; <code>x / ln x</code> and <code>li(x)</code> are the two classic guesses, the second summed by the Ramanujan series. The witness is exact: the carpet layer at scale <code>n</code> lights every cell of an <code>n</code> by <code>n</code> grid except those whose row and column are both odd, two layers are correlated on their common grid in whole numbers, and the correlation is exactly zero precisely when the two odd scales share no factor. So the row of an odd <code>n</code> is clear exactly when <code>n</code> is prime, and the smallest composite signal over the odd scales is the square of thirteen. The stack that sums these layers is the carpet preset of <a href="../moire">moire</a>; the same primes peak the novelty of the <a href="../farey">Farey stack</a>. Where pi comes out of that stack as a counted number is in <a href="/research/pi/">the pi note</a>, and the correlation law the witness rests on is the <a href="/papers/moire-correlation-laws/">moire correlation laws paper</a>.</>}217 controls={controls}>218 <div className="arena">219 <div className="panel">220 <h2>The sieve <span>{done ? `done, ${sieve.current.count()} primes in yellow` : current ? `${current} strikes its multiples in orange` : 'blue is the prime in hand'}</span></h2>221 <Sketch draw={sheet} deps={[tick, current]} role="img" aria-label="The sieve, every number up to the limit in a grid" />222 </div>223 <div className="panel">224 <h2>The stones <span>{pile && pile.rectangles.slice(0, 8).map(([a, b]) => `${a}×${b}`).join(' ') + (pile.rectangles.length > 8 ? ' …' : '')}</span></h2>225 <Sketch draw={stones} deps={[pile]} role="img" aria-label="The stones, one rectangle per divisor pair" />226 </div>227 <div className="panel">228 <h2>Counting primes <span>the staircase and its two guesses</span></h2>229 <Sketch draw={chart} deps={[data]} className="bars" role="img" aria-label="Counting primes, the staircase and its two guesses" />230 </div>231 <div className="panel">232 <h2>The witness <span>one bar per earlier odd scale</span></h2>233 <Sketch draw={witness} deps={[trial]} className="bars" role="img" aria-label="The witness, one bar per earlier odd scale" />234 </div>235 </div>236 <Stats>237 <Stat label="prime in hand">{current || (done ? 'none left' : 'none yet')}</Stat>238 <Stat label="struck">{sieve.current?.struck()}</Stat>239 <Stat label="found">{sieve.current?.count()}</Stat>240 <Stat label="primes up to the top">{data?.pi.at(-1)}</Stat>241 <Stat label="x / ln x">{data?.ratio.at(-1).toFixed(1)}</Stat>242 <Stat label="li(x)">{data?.li.at(-1).toFixed(1)}</Stat>243 <Stat label="factors">{pile && (pile.factors.length ? pile.factors.map(([p, e]) => (e > 1 ? `${p}^${e}` : p)).join(' · ') : 'none')}</Stat>244 <Stat label="verdict">{pile && (pile.prime ? `${pile.n} is prime, one row only` : `${pile.n} makes ${pile.rectangles.length} rectangles`)}</Stat>245 <Stat label="largest correlation">{trial?.max.toFixed(7)}</Stat>246 <Stat label="at scale">{trial && (trial.at || 'nowhere')}</Stat>247 </Stats>248 <Note error={error ?? view.error} />249 </Page>250 );251}252253mount(<App />);