index.jsx
17.8 kB · jsx · 369 lines
1import { Fragment, useEffect, useMemo, useReducer, useRef, useState } from 'react';2import { ready, ink } from '../../lib/mrly.js';3import { stamp, useQuery } from '../../lib/query.js';4import { mount, Page, Row, Pick, Btn, Stats, Stat, Note, Group } from '../../lib/app.jsx';5import { Grid, Markup, Sketch } from '../../lib/draw.jsx';6import { useSeeds, roll } from '../../lib/select.jsx';7import { board, bars, axis, tag } from '../../lib/chart.js';8import { Pins, Staircase, Ratios, Differences, Digits, Terms } from '../../lib/series.jsx';910const m = await ready();11const TERMS = 8;12const STEP = 4;13const DEPTH = 6;14const BUDGET = '500000';15const TASTE = '20000';16const CHUNK = 50;17const DIMS = [1, 2, 3, 4];18const BASES = [2, 3, 4, 5];19const RECORDS = JSON.parse(m.ledger_records());20const MEASURES = [...m.ledger_measures()];21const BUILT = m.ledger_build('closed', TERMS);2223function chip(text) {24 return text ? <span className={`chip ${text.toLowerCase()}`}>{text}</span> : '';25}2627function badge(row) {28 if (!row.oeis) return '';29 return (30 <>31 <a className="badge" href={`https://oeis.org/${row.oeis}`} target="_blank" rel="noopener">{row.oeis}</a>32 {row.shift ? <> <span className="shift">{row.shift > 0 ? '+' : ''}{row.shift}</span></> : null}33 </>34 );35}3637function read(code, d, b, measures, cells) {38 const out = [];39 for (const measure of measures) {40 for (const way of ['level', 'side']) {41 try {42 out.push({ ...JSON.parse(m.ledger_row(code, d, b, measure, way, TERMS, cells)), extra: 'typed' });43 } catch {44 continue;45 }46 }47 }48 return out;49}5051function reading(name, cells) {52 const named = name.match(/^(bang_[^.]+)\.(\w+)\.(\w+)$/);53 if (!named) return null;54 try {55 const bang = JSON.parse(m.name_parse(named[1]));56 return JSON.parse(m.ledger_row(bang.code, bang.dim, bang.base, named[2], named[3], TERMS, cells));57 } catch {58 return null;59 }60}6162function fits(row, f) {63 return (!f.measure || row.measure === f.measure) && (!f.d || row.d === f.d) && (!f.b || row.q === f.b);64}6566function keyed(f) {67 const out = [];68 for (const found of JSON.parse(m.ledger_identify(f.q))) {69 const record = RECORDS.find((r) => r.id === found.id);70 if (!record?.key || JSON.parse(m.ledger_search(record.key, '', 0, 0, 0, 1)).total) continue;71 const row = reading(record.key, BUDGET);72 if (row && fits(row, f)) out.push({ ...row, extra: 'record' });73 }74 return out;75}7677function typed(f) {78 const measures = f.measure ? [f.measure] : MEASURES;79 if (/\./.test(f.q)) {80 const row = reading(f.q, TASTE);81 return row ? [{ ...row, extra: 'typed' }] : [];82 }83 if (!/^\d+$/.test(f.q)) return [];84 const b = f.b || BASES[0];85 const dims = f.d ? [f.d] : DIMS;86 for (const d of dims) {87 try {88 m.name_of(f.q, d, b);89 } catch {90 continue;91 }92 return read(f.q, d, b, measures, TASTE);93 }94 return [];95}9697function idle() {98 return new Promise((resolve) => (window.requestIdleCallback ? requestIdleCallback(resolve, { timeout: 100 }) : setTimeout(resolve, 0)));99}100101function Found({ terms }) {102 const key = terms.slice(0, TERMS).join(', ');103 const found = useMemo(() => JSON.parse(m.ledger_identify(key)), [key]);104 if (!found.length) return 'no curated record holds these terms';105 return found.slice(0, 3).map((r, i) => (106 <Fragment key={r.id}>107 {i ? <br /> : null}108 <a href={`https://oeis.org/${r.id}`} target="_blank" rel="noopener">{r.id}</a> at index {r.shift}: {r.name}109 </Fragment>110 ));111}112113function Design({ row }) {114 const made = useMemo(() => {115 try {116 if (row.d === 2) {117 const level = m.level_cap(row.number, 2, 60000);118 return { title: `side ${row.number}, level ${level}`, art: <Grid grid={m.two_grid(row.code, row.number, level, 0, row.q)} on={ink.blue} role="img" aria-label={`The design ${row.code}, side ${row.number}, level ${level}`} /> };119 }120 if (row.d === 3) {121 const level = m.level_cap(row.number, 3, 8000);122 return { title: `side ${row.number}, level ${level}, isometric`, art: <Markup svg={m.hex_svg(row.code, row.number, level, row.q, 'iso', 4)} role="img" aria-label={`The design ${row.code}, side ${row.number}, level ${level}, isometric`} /> };123 }124 if (row.d === 1) {125 const level = m.level_cap(row.number, 1, 729);126 const cells = m.ledger_profile(row.code, 1, row.q, row.number, level);127 return { title: `side ${row.number}, level ${level}, the strip`, art: <Grid grid={{ width: cells.length, height: 1, types: Uint8Array.from(cells) }} on={ink.blue} style={{ height: 48 }} role="img" aria-label={`The design ${row.code}, side ${row.number}, level ${level}, the strip`} /> };128 }129 const level = m.level_cap(row.number, row.d, 600000);130 const counts = m.ledger_profile(row.code, row.d, row.q, row.number, level).map(Number);131 const draw = (canvas) => {132 const b = board(canvas, 220);133 bars(b, counts, { color: ink.pink, inset: 0 });134 axis(b, [[0, 'first plane'], [1, 'last plane']]);135 tag(b, `cells on every diagonal plane, level ${level}`, ink.dim);136 };137 return { title: `side ${row.number}, level ${level}, the diagonal profile`, art: <Sketch className="bars" draw={draw} deps={[counts]} role="img" aria-label={`The design ${row.code}, side ${row.number}, level ${level}, the diagonal profile`} /> };138 } catch (error) {139 return { title: '', art: null, error };140 }141 }, [row.name]);142143 return (144 <div className="panel">145 <h2>The design <span>{made.title}</span></h2>146 <div>{made.art}</div>147 <Stats>148 <Stat label="code">{row.code}</Stat>149 <Stat label="space">{`dimension ${row.d}, base ${row.q}`}</Stat>150 <Stat label="measure">{row.measure}</Stat>151 <Stat label="axis">{row.axis === 'level' ? `level L at side ${row.number}` : 'odd side 2k - 1 at level 1'}</Stat>152 </Stats>153 <Note error={made.error} />154 </div>155 );156}157158const VIEWS = [['pins', 'pin plot'], ['steps', 'step function'], ['sums', 'partial sums'], ['ratios', 'ratios'], ['differences', 'difference triangle'], ['digits', 'digit heatmap']];159const BLENDED = ['ratios', 'differences'];160161const bases = (q) => [...new Set([2, 3, 10, q])].sort((a, b) => a - b);162163const plotted = (row) => `../plot/?code=${row.code}&dimension=${row.d}&base=${row.q}&measure=${row.measure}&axis=${row.axis}&count=${row.terms.length}`;164165function View({ row, view, dig, blend }) {166 const label = `${row.axis === 'level' ? 'level L' : 'side k'} from ${row.start}`;167 const named = (what) => `The terms of ${row.name}, ${what}`;168 if (view === 'digits') return <Digits terms={row.terms} start={row.start} base={dig} label={label} role="img" aria-label={named('a heatmap of the digits of every term')} />;169 if (view === 'steps') return <Staircase terms={row.terms} start={row.start} label={label} role="img" aria-label={named('as a step function')} />;170 if (view === 'sums') return <Staircase terms={row.terms} start={row.start} sums label={label} role="img" aria-label={named('as partial sums')} />;171 if (view === 'ratios') return blend ? <Ratios values={blend.ratios} start={row.start} label={`${label}, each term against the one before`} role="img" aria-label={named('each term against the one before')} /> : null;172 if (view === 'differences') return blend ? <Differences rows={blend.differences} label={`${label}, each row the differences of the row above`} role="img" aria-label={named('the difference triangle')} /> : null;173 return <Pins terms={row.terms} start={row.start} label={label} role="img" aria-label={named('every term standing on its index')} />;174}175176function App() {177 const s = useSeeds();178 const [pick, set] = useQuery({ q: '', measure: '', dimension: '', base: '', rows: 25, page: '', open: '', view: 'pins', dig: 2 });179 const [count, setCount] = useState(BUILT);180 const [tier, setTier] = useState('closed');181 const [built, grew] = useReducer((x) => x + 1, 0);182 const [picked, setPicked] = useState(null);183 const [capped, setCapped] = useState(false);184 const [note, setNote] = useState(null);185 const latest = useRef(null);186 latest.current = picked;187188 const view = useMemo(() => {189 const f = { q: pick.q.trim(), measure: pick.measure, d: +pick.dimension, b: +pick.base, rows: +pick.rows };190 try {191 let at = +pick.page;192 let hits = JSON.parse(m.ledger_search(f.q, f.measure, f.d, f.b, at, f.rows));193 if (at && !hits.rows.length) {194 at = 0;195 hits = JSON.parse(m.ledger_search(f.q, f.measure, f.d, f.b, at, f.rows));196 }197 const terms = /^[\d,\s-]+$/.test(f.q) && !/^\d+$/.test(f.q);198 const extra = at ? [] : terms ? keyed(f) : typed(f);199 return { f, at, hits, extra, terms, error: null };200 } catch (error) {201 return { f, at: +pick.page, hits: null, extra: [], terms: false, error };202 }203 }, [pick.q, pick.measure, pick.dimension, pick.base, pick.rows, pick.page, built]);204205 const now = useRef(view);206 now.current = view;207208 const blend = useMemo(() => {209 if (!picked || !BLENDED.includes(pick.view)) return null;210 try {211 return { row: JSON.parse(m.blend_series(picked.code, picked.d, picked.q, picked.measure, picked.axis, picked.terms.length, BUDGET, DEPTH)), error: null };212 } catch (error) {213 return { row: null, error };214 }215 }, [picked, pick.view]);216217 const choose = (row) => {218 setPicked(row);219 latest.current = row;220 setCapped(false);221 setNote(null);222 set({ open: row.name });223 };224225 const shuffle = (seed) => {226 const f = now.current.f;227 const total = JSON.parse(m.ledger_search(f.q, f.measure, f.d, f.b, 0, 1)).total;228 if (!total) return;229 const [at] = roll(seed, [[0, total - 1]]);230 const row = JSON.parse(m.ledger_search(f.q, f.measure, f.d, f.b, at, 1)).rows[0];231 set({ page: Math.floor(at / f.rows) || '' });232 choose(row);233 };234235 const deeper = () => {236 const row = picked;237 setNote(null);238 try {239 const asked = row.terms.length + STEP;240 const more = m.ledger_terms(row.code, row.d, row.q, row.measure, row.axis, asked, BUDGET);241 if (more.length <= row.terms.length) {242 setPicked({ ...row, capped: true });243 setCapped(true);244 } else {245 setPicked({ ...row, terms: more, capped: more.length < asked });246 }247 } catch (error) {248 setNote(error);249 }250 };251252 useEffect(() => {253 const f = now.current.f;254 stamp({ q: f.q, measure: f.measure, dimension: f.d || '', base: f.b || '', rows: f.rows, page: now.current.at || '' });255 if (pick.open && !s.get()) {256 const row = reading(pick.open, BUDGET);257 if (row) choose(row);258 else set({ open: '' });259 }260 let live = true;261 (async () => {262 for (const name of ['convolved', 'side']) {263 let state;264 do {265 await idle();266 if (!live) return;267 state = JSON.parse(m.ledger_grow(name, TERMS, CHUNK));268 setCount(state.rows);269 setTier(`${name} ${state.done} of ${state.total}`);270 } while (state.done < state.total);271 grew();272 }273 setTier('complete');274 if (s.get() && !latest.current) shuffle(s.get());275 })();276 return () => { live = false; };277 }, []);278279 useEffect(() => { if (view.at !== +pick.page) set({ page: view.at || '' }); }, [view]);280281 const f = view.f;282 const listed = view.error ? [] : [...view.extra, ...view.hits.rows];283284 const controls = (285 <>286 <Group name="Search">287 <label>search <input type="text" style={{ flex: 1, minWidth: 0 }} value={pick.q} placeholder="6, 42, 306 or carpet or A000567 or 23" onChange={(e) => { s.drop(); set({ q: e.target.value, page: '' }); }} /></label>288 <Pick label="measure" value={pick.measure} options={[['', 'any'], ...MEASURES]} onChange={(v) => set({ measure: v, page: '' })} />289 <Pick label="dimension" value={pick.dimension} options={[['', 'any'], ...DIMS]} onChange={(v) => set({ dimension: v, page: '' })} />290 <Pick label="base" value={pick.base} options={[['', 'any'], ...BASES]} onChange={(v) => set({ base: v, page: '' })} />291 <Pick label="rows" value={pick.rows} options={[10, 25, 50, 100]} onChange={(v) => set({ rows: +v, page: '' })} />292 </Group>293 <Group name="Seed">294 <Btn onClick={() => shuffle(s.next())}>Randomize</Btn>295 </Group>296 {picked && (297 <Group name="The terms">298 <Pick label="view" value={pick.view} options={VIEWS} onChange={(v) => set({ view: v })} />299 {pick.view === 'digits' ? <Pick label="digit base" value={pick.dig} options={bases(picked.q)} onChange={(v) => set({ dig: +v })} /> : null}300 </Group>301 )}302 </>303 );304305 return (306 <Page crumb="sequences" title="Every sequence the designs write"307 sub="A design fills cells, and counted level by level or side by side the counts make an integer sequence: its fills, its voids, its exposed faces, the cells on its deepest diagonal plane, the pieces of its slice. This is the ledger of all of them, read live from the crates. Type a few terms and find which design writes them, or type a name, a record, or any code."308 foot={<>A sequence is one design, one measure and one axis. The level axis grows the fractal level by level at the smallest side the base allows; the side axis holds level one and widens the odd side. The name is a sequence name of its own kind, so the sponge's surface by level is <code>sequence_dim=3_code=23_measure=surface_axis=level</code>. The closed tier comes first, the fills, voids and exposed faces that close in a formula; the convolved tier, the diagonal profile's peak and its height count, and the side grid tier, the vertices, edges, faces, Euler characteristic and slice census read off a rendered grid, build behind the page while it stays live, and the counter says how far they are. A record badge names the OEIS entry whose terms hold the row's, and the number after it is the record's index of the row's first term less the ledger's; the status is the record's own where the record names this design, and a collision to explain where it does not. Deeper reads more terms within a budget of cells a term, and stops where the budget or the width of a number stops it. The terms of a picked row are drawn as a pin plot, a step function, its partial sums, the ratio of each term to the one before, the difference triangle or a digit heatmap in a chosen base, and the ribbon under them prints every term. The picked row opens whole in <a href="../plot">plot</a>, which reads the same key and adds the smallest linear recurrence its terms satisfy, the characteristic polynomial, the growth and a second sequence to mix in. The table lists the least code of every orbit; a typed code of any spelling, canonical or not, is read the same way and shown first, and the odd-side rows are code specific, so the orbit mates of one design read different polynomials. Every number on this page is computed in Rust; the page only draws. The ledger itself, with its closed forms and status per row, is the <a href="/research/sequences/">sequences note</a>, and the formal census of these rows is the <a href="/papers/sequence-census/">sequence-census paper</a>.</>}309 controls={controls}>310 <div className="panel">311 <h2>The ledger <span>{view.terms ? <Found terms={f.q.split(/[\s,]+/).filter(Boolean)} /> : f.q ? `matching "${f.q}"` : 'every row'}</span></h2>312 <Stats>313 <Stat label="rows built">{count}</Stat>314 <Stat label="tier">{tier}</Stat>315 <Stat label="hits">{view.error ? '' : view.hits.total + (view.extra.length ? ` + ${view.extra.length} ${view.extra[0].extra}` : '')}</Stat>316 <Stat label="page">{view.at + 1}</Stat>317 <span>318 <button disabled={view.at === 0} onClick={() => set({ page: (view.at - 1) || '' })}>prev</button>{' '}319 <button disabled={!!view.error || (view.at + 1) * f.rows >= view.hits.total} onClick={() => set({ page: view.at + 1 })}>next</button>320 </span>321 </Stats>322 <div className="scroll">323 <table>324 <thead><tr><th>name</th><th>first terms</th><th>closed form</th><th>record</th><th>status</th></tr></thead>325 <tbody>326 {listed.map((row, i) => {327 const shown = picked && picked.name === row.name ? picked : row;328 return (329 <tr key={`${i}:${row.name}`} className={picked?.name === row.name ? 'on' : undefined} onClick={() => choose(row)}>330 <td className="mono">{row.name}{row.extra ? <> {chip(row.extra)}</> : null}</td>331 <td><Terms terms={shown.terms} capped={shown.capped} tight /></td>332 <td className="mono">{row.closed}</td>333 <td>{badge(row)}</td>334 <td>{chip(row.tag)}</td>335 </tr>336 );337 })}338 </tbody>339 </table>340 </div>341 <Note error={view.error} />342 </div>343 {picked && (344 <div className="arena">345 <Design row={picked} />346 <div className="panel">347 <h2>The terms <span>{picked.name}</span></h2>348 <Row>349 <a className="badge" href={plotted(picked)}>draw this row in plot</a>350 </Row>351 <View row={picked} view={pick.view} dig={pick.dig} blend={blend?.row} />352 <Terms terms={picked.terms} start={picked.start} capped={picked.capped} />353 <Stats>354 <Stat label="closed form">{picked.closed || 'none known'}</Stat>355 <Stat label="record">{badge(picked) || 'none'}</Stat>356 <Stat label="status">{chip(picked.tag) || 'unmatched'}</Stat>357 <Stat label="terms">{picked.capped ? `${picked.terms.length}, to the budget` : picked.terms.length}</Stat>358 <span><button disabled={capped} onClick={deeper}>{capped ? 'at the budget' : 'deeper'}</button></span>359 </Stats>360 <p className="foot" style={{ marginTop: 12, paddingTop: 10 }}><Found terms={picked.terms} /></p>361 <Note error={note ?? blend?.error} />362 </div>363 </div>364 )}365 </Page>366 );367}368369mount(<App />);