index.jsx
15.1 kB · jsx · 263 lines
1import { useMemo } from 'react';2import { ready, ink } from '../../../lib/mrly.js';3import { useQuery } from '../../../lib/query.js';4import { mount, Page, Row, Pick, Slider, Btn, Stats, Stat, Note } from '../../../lib/app.jsx';5import { Grid, Sketch } from '../../../lib/draw.jsx';6import { Picker, useSeeds, seeded } from '../../../lib/select.jsx';7import { board, line, axis, tag } from '../../../lib/chart.js';89const m = await ready();10const DIMS = [[1, 'dim 1, the line'], [2, 'dim 2, the square'], [3, 'dim 3, the cube']];11const START = { 1: '1', 2: '7', 3: '23' };12const SIDES = [['odd', 'odd sides 3, 5, 7, ...'], ['even', 'even sides 2, 4, 6, ...']];13const HUES = ['blue', 'orange', 'green', 'pink'];14const LETTERS = 12;15const REACH = 6;16const STOPS = 240;1718const attempt = (fn) => {19 try {20 return { value: fn(), error: null };21 } catch (error) {22 return { value: null, error };23 }24};2526const ratio = ([p, q]) => (q === 1 ? String(p) : `${p}/${q}`);27const digits = (i, dim) => i.toString(2).padStart(dim, '0');28const ones = (i) => [...i.toString(2)].filter((c) => c === '1').length;29const fixed = (value, places = 12) => (value === null || value === undefined ? 'none' : value.toFixed(places));3031function roots(list) {32 const out = [];33 for (const [re, im] of list) {34 if (im > 0) continue;35 out.push(im < 0 ? `${re.toFixed(6)} +- ${(-im).toFixed(6)}i` : re.toFixed(6));36 }37 return out.join(', ');38}3940function points(walk, reach, at) {41 const out = [];42 for (let i = 0; i < walk.length; i += 4) out.push([Math.log10(walk[i]) / reach, walk[i + at]]);43 return out;44}4546function span(values, extra) {47 let low = Math.min(...values, ...extra);48 let high = Math.max(...values, ...extra);49 if (high - low < 1e-9) [low, high] = [low - 1, high + 1];50 const pad = (high - low) * 0.08;51 return [low - pad, high + pad];52}5354function plot(b, series, low, high) {55 for (const [list, color, options] of series) {56 line(b, list.filter(([, y]) => y >= low && y <= high).map(([x, y]) => [x, (y - low) / (high - low)]), color, options);57 }58}5960function decades(reach) {61 return Array.from({ length: reach + 1 }, (_, k) => [k / reach, k === 0 ? '1' : `10^${k}`]);62}6364function App() {65 const seeds = useSeeds();66 const first = useMemo(() => {67 const asked = +(new URLSearchParams(location.search).get('dim') ?? 2);68 const dim = START[asked] ? asked : 2;69 return { dim, code: seeded(seeds, dim, 2, START[dim]), letters: 3, reach: REACH - 1, sides: 'odd' };70 }, []);71 const [q, set] = useQuery(first);72 const dim = START[q.dim] ? q.dim : 2;73 const even = q.sides === 'even';74 const code = q.code.trim();75 const letters = Math.max(1, Math.min(LETTERS, q.letters));76 const reach = Math.max(1, Math.min(REACH, q.reach));77 const cap = useMemo(() => m.staircase_cap(even), [even]);78 const drawn = Math.min(letters, cap);7980 const read = useMemo(() => attempt(() => JSON.parse(m.staircase_read(code, dim, letters, even))), [code, dim, letters, even]);81 const walk = useMemo(() => attempt(() => m.staircase_walk(code, dim, 10 ** reach, STOPS, even)), [code, dim, reach, even]);82 const picture = useMemo(() => attempt(() => m.staircase_grid(code, dim, drawn, even)), [code, dim, drawn, even]);83 const r = read.value;84 const w = walk.value;85 const last = w && w.length ? { level: w[w.length - 4], value: w[w.length - 3], law: w[w.length - 2], scaled: w[w.length - 1] } : null;86 const target = r ? (even ? w?.[1] : r.constant) : null;8788 const toggle = (i) => {89 const next = attempt(() => (BigInt(code) ^ (1n << BigInt(i))).toString()).value;90 if (next === null) return;91 seeds.drop();92 set({ code: next });93 };9495 const shift = (value) => {96 seeds.drop();97 set({ dim: +value, code: START[+value] });98 };99100 const settle = (canvas) => {101 const b = board(canvas, 260, { top: 30, bottom: 24, left: 18, right: 18 });102 if (!r || !w) return;103 const walked = points(w, reach, 1);104 const law = points(w, reach, 2);105 const [low, high] = span(walked.map(([, y]) => y), [target]);106 const level = (target - low) / (high - low);107 line(b, [[0, level], [1, level]], ink.green, { width: 1, dash: [5, 4] });108 plot(b, [[law, ink.orange, { width: 1.5, dash: [2, 3] }], [walked, ink.blue, { width: 2 }]], low, high);109 axis(b, decades(reach));110 tag(b, `${even ? 'the walk, constant ' : 'C = '}${fixed(target)}`, ink.green, 'right', b.x(1), Math.max(b.roof + 10, Math.min(b.floor - 4, b.y(level) - 6)));111 tag(b, even ? 'R_L (2^dim/w)^L on even sides' : 'renormalised fill; dotted, the law C (1 + c_1/L)', ink.dim);112 };113114 const correction = (canvas) => {115 const b = board(canvas, 220, { top: 30, bottom: 24, left: 18, right: 18 });116 if (!r || !w) return;117 const scaled = points(w, reach, 3);118 const marks = even ? [] : [r.c1];119 const tail = reach > 1 ? scaled.filter(([x]) => x >= 1 / reach) : scaled;120 const [low, high] = span(tail.map(([, y]) => y), marks);121 if (!even) {122 const level = (r.c1 - low) / (high - low);123 line(b, [[0, level], [1, level]], ink.orange, { width: 1, dash: [5, 4] });124 tag(b, `c_1 = ${ratio(r.correction)} = ${fixed(r.c1, 6)}`, ink.orange, 'right', b.x(1), Math.max(b.roof + 10, Math.min(b.floor - 4, b.y(level) - 6)));125 }126 plot(b, [[scaled, ink.blue, { width: 2 }]], low, high);127 axis(b, decades(reach));128 tag(b, even ? 'L times the gap to the constant: zero at every level' : 'L times the relative gap to C', ink.dim);129 };130131 const controls = (132 <Row>133 <Pick label="dim" value={dim} options={DIMS} onChange={shift} />134 <Picker dimension={dim} code={q.code} onChange={(v) => set(v)} seeds={seeds} />135 <Pick label="sides" value={q.sides} options={SIDES} onChange={(v) => set({ sides: v })} />136 <Slider label="letters" value={letters} min={1} max={LETTERS} onChange={(v) => set({ letters: v })} />137 <Slider label="walk to" value={reach} min={1} max={REACH} show={`10^${reach}`} onChange={(v) => set({ reach: v })} />138 </Row>139 );140141 const corners = Array.from({ length: 2 ** dim }, (_, i) => i);142 const filled = (i) => attempt(() => (BigInt(code) >> BigInt(i)) & 1n).value === 1n;143 const rows = r ? r.levels : [];144 const word = rows.length ? rows[drawn - 1].word : null;145146 return (147 <Page crumb="staircase" title="Pi on the staircase"148 sub={<>A design is a set of corners of the square, the line or the cube. Draw it at side 3: a cell is filled when the parities of its coordinates name a filled corner. Then put a copy of the side-5 drawing into every filled cell, then side 7, one letter a level. Each letter keeps about <code>w/2^dim</code> of its box, plus or minus a little. Divide out <code>(w/2^dim)^L</code> and the slow power <code>L^drift</code>, and what is left settles on a constant, often pi in disguise: <code>pi/4</code>, <code>cosh(pi/2)/2</code>, <code>3 pi/(4 Gamma(1/3))</code>. Click corners and watch the curve land.</>}149 foot={<>A corner with <code>j</code> odd coordinates fills <code>n^(dim-j) (n-1)^j</code> cells of side <code>2n - 1</code>, since each axis holds <code>n</code> even and <code>n - 1</code> odd positions; so a design with <code>a_j</code> such corners fills <code>P_F(n) = sum_j a_j n^(dim-j) (n-1)^j</code>, a polynomial whose roots <code>r_i</code> decide everything. The row word of sides <code>3, 5, ..., 2L+1</code> has fill ratio exactly <code>(w/2^dim)^L prod_i Gamma(L+2-r_i)/Gamma(2-r_i) / (Gamma(L+3/2)/Gamma(3/2))^dim</code>, hence <code>(w/2^dim)^L L^drift C (1 + c_1/L + O(L^-2))</code> with <code>drift = dim/2 - mean</code>, <code>C = Gamma(3/2)^dim / prod_i Gamma(2 - r_i)</code> and <code>c_1 = dim/8 + drift - var/2</code>, mean and var of the odd count over the corners. The drift is 0 exactly when the corners hold as many odd coordinates as even ones. At even side every letter fills <code>w/2^dim</code> on the nose. The two parity designs fill <code>(N^dim -+ 1)/2</code> at odd side <code>N</code>, so their constants are the Wallis sieve products <code>prod_(N odd >= 3) (1 -+ N^-dim)</code>. Where the roots outside <code>0, 1/2, 1</code> pair as <code>r, 1 - r</code>, <code>Gamma(z) Gamma(1 - z) = pi / sin(pi z)</code> turns <code>C</code> into <code>(sqrt(pi)/2)^(m_0 + m_1) prod_pairs sin(pi r)/(4 r (1-r))</code>, and a design times its mirror always pairs. The proofs are on <a href="/research/notes/pi/">pi</a> and <a href="/research/notes/magic/">magic</a>. The Gamma form, the closed form, the reflection, the Wallis product and every walk are crate calls through wasm; the page only draws.</>}150 controls={controls}>151 <div className="arena">152 <div className="panel">153 <h2>The corners <span>click one to fill or empty it</span></h2>154 <div className="ribbon">155 {corners.map((i) => (156 <span key={i} role="button" tabIndex={0} aria-pressed={filled(i)} className={filled(i) ? undefined : 'dim'}157 style={filled(i) ? { color: `var(--${HUES[ones(i)]})`, borderColor: `var(--${HUES[ones(i)]})` } : undefined}158 onClick={() => toggle(i)} onKeyDown={(event) => { if (event.key === 'Enter') toggle(i); }}>159 <b>{digits(i, dim)}</b><i>{filled(i) ? `in, ${ones(i)} odd` : 'out'}</i>160 </span>161 ))}162 </div>163 {r ? (164 <Stats>165 <Stat label="corners">{`${r.corners} of ${r.box}`}</Stat>166 <Stat label="by odd count a_j">{`(${r.profile.join(', ')})`}</Stat>167 <Stat label="coordinates even, odd">{`${r.coordinates[0]}, ${r.coordinates[1]}`}</Stat>168 <Stat label="drift">{even ? 'none on even sides' : ratio(r.drift)}</Stat>169 <Stat label="roots of P_F">{roots(r.roots)}</Stat>170 </Stats>171 ) : null}172 <Note error={read.error} />173 </div>174175 <div className="panel">176 <h2>The word <span>{dim === 3 ? `its floor layer, the plane word of code ${r?.floor ?? ''}` : dim === 1 ? 'one band per prefix, each at the full side' : `the first ${drawn} letters`}</span></h2>177 {picture.value ? <Grid grid={picture.value} on={ink.blue} role="img" aria-label="The first letters of the row word, filled cells inked" /> : null}178 <p className="sub">{word ? `Sides ${rows.slice(0, drawn).map((row) => row.side).join(' x ')} = ${word[0]}: ${word[1]} of ${word[2]} cells filled.${letters > cap ? ` The picture stops at ${cap} letters; the table runs on.` : ''}` : ''}</p>179 <Note error={picture.error} />180 </div>181 </div>182183 <div className="arena" style={{ marginTop: 22 }}>184 <div className="panel">185 <h2>The settle <span>{even ? 'even sides: nothing to divide out' : `the renormalised fill walked to L = 10^${reach}`}</span></h2>186 <Sketch draw={settle} deps={[r, w, reach, even]} className="bars" role="img" aria-label="The renormalised fill against the level on a log scale, settling on its constant" />187 {last && r ? (188 <Stats>189 <Stat label={`at L = ${last.level}`}>{fixed(last.value)}</Stat>190 <Stat label={even ? 'the even walk' : 'law C (1 + c_1/L)'}>{fixed(last.law)}</Stat>191 <Stat label="L times the gap">{fixed(last.scaled, 6)}</Stat>192 </Stats>193 ) : null}194 <Note error={walk.error} />195 </div>196 <div className="panel">197 <h2>The correction <span>{even ? 'the even word has none' : 'the gap closes like c_1/L'}</span></h2>198 <Sketch draw={correction} deps={[r, w, reach, even]} className="bars" role="img" aria-label="The level times the relative gap to the constant, settling on the first correction" />199 <p className="sub">{even200 ? `On even sides every letter fills exactly w/2^dim of its box, so the walk stands at ${fixed(w?.[1] ?? null)} from the first letter: no drift and no constant. Switch back to odd sides to bring both back.`201 : r ? `Multiply the gap by L and it flattens onto c_1 = dim/8 + drift - var/2 = ${ratio(r.correction)}: the constant is checked against the walk, not fitted to it.` : ''}</p>202 </div>203 </div>204205 {r ? (206 <div className="arena" style={{ marginTop: 22 }}>207 <div className="panel">208 <h2>The constant <span>{`code ${r.code}, dim ${r.dimension}${even ? ', on odd sides' : ''}`}</span></h2>209 <Stats>210 <Stat label="Gamma form at the roots">{fixed(r.constant)}</Stat>211 <Stat label="closed form">{r.closed}</Stat>212 <Stat label="by reflection alone">{r.reflection === null ? 'does not reduce' : fixed(r.reflection)}</Stat>213 {r.parity ? <Stat label={`Wallis sieve, prod_(N odd >= 3) (1 ${r.parity.odd ? '-' : '+'} N^-${r.dimension})`}>{fixed(r.parity.value)}</Stat> : null}214 </Stats>215 <p className="sub">{r.reflection === null216 ? 'A root outside 0, 1/2 and 1 has no partner 1 - r, so a Gamma value stays in the constant.'217 : 'Every root outside 0, 1/2 and 1 pairs with 1 - r, and the reflection formula turns each pair into sin(pi r)/(4 r (1-r)), so no Gamma value is left.'}218 {r.parity ? ` This is a parity design: at odd side N it fills (N^dim ${r.parity.odd ? '-' : '+'} 1)/2, half a Wallis sieve letter, so its constant is a Wallis sieve product.` : ''}</p>219 </div>220 <div className="panel">221 <h2>The mirror <span>flip every coordinate</span></h2>222 <Stats>223 <Stat label="mirror code">{r.mirror.code}</Stat>224 <Stat label="its closed form">{r.mirror.closed}</Stat>225 <Stat label="its constant">{fixed(r.mirror.constant)}</Stat>226 <Stat label="the two constants multiplied">{fixed(r.mirror.both)}</Stat>227 <Stat label="prod sin(pi r)/(4 r (1-r))">{fixed(r.mirror.product)}</Stat>228 </Stats>229 <p className="sub">The mirror's roots are the <code>1 - r_i</code>, so a design times its mirror pairs every root and reduces by reflection, <code>pi/4</code> at a root 0 or 1.</p>230 <Row><Btn onClick={() => { seeds.drop(); set({ code: r.mirror.code }); }}>Load the mirror</Btn></Row>231 </div>232 </div>233 ) : null}234235 {r ? (236 <div className="panel" style={{ marginTop: 22 }}>237 <h2>The letters <span>{even ? 'every share is w/2^dim' : 'each share is P_F(n) over side^dim'}</span></h2>238 <div className="scroll">239 <table>240 <thead><tr><th>L</th><th>side</th><th>fill</th><th>of</th><th>share</th><th>R_L</th><th>{even ? 'R_L (2^dim/w)^L' : 'renormalised'}</th><th>{even ? 'the even walk' : 'C (1 + c_1/L)'}</th></tr></thead>241 <tbody>242 {rows.map((row) => (243 <tr key={row.level}>244 <td className="num">{row.level}</td>245 <td className="num">{row.side}</td>246 <td className="num">{row.fill}</td>247 <td className="num">{row.cells}</td>248 <td className="num">{row.share.toFixed(6)}</td>249 <td className="num">{row.ratio.toExponential(6)}</td>250 <td className="num">{fixed(row.settle, 9)}</td>251 <td className="num">{fixed(row.law, 9)}</td>252 </tr>253 ))}254 </tbody>255 </table>256 </div>257 </div>258 ) : null}259 </Page>260 );261}262263mount(<App />);