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