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 />);