index.jsx

17.3 kB · jsx · 292 lines

1import { useEffect, useState } from 'react';2import { ready, ink, fit } from '../../lib/mrly.js';3import { mount, Page, Row, Pick, Slider, Check, Btn, Stat, Note } from '../../lib/app.jsx';4import { Grid, Signs, Sketch } from '../../lib/draw.jsx';5import { Picker, useSeeds } from '../../lib/select.jsx';6import { useQuery, share } from '../../lib/query.js';78const m = await ready();9const ROUNDS = 8;10const SIDE = 512;11const LEVELS = 9;12const RATES = 64;1314const FIRST = { round: 6, length: 256, level: 6, fold: 'sign', code: '9', side: 2, base: 2, depth: 3 };1516const GALLERY =17  'Three of the four lifts fold, and the crate checks each one against the Kronecker power of its own corner tile rather than trusting the picture. The corner tile is the only candidate worth testing: if a grid is a tile folded L times then its corner block is that tile with every bit flipped by (L - 1) t00, and folding the block L times flips the grid again by L (L - 1) t00, which is even. So a grid folds if and only if it folds from its own corner, and no search is needed.';1819const FILTER =20  'The filter has a closed form in both folds, and the closed form is not the Thue-Morse grid. Under the and fold the next level is this level blown up and masked by the base tile, so the difference is this level blown up and masked by the tile complement. Under the plus-minus fold the next level is this level blown up and exclusive-ored with the repeated tile, so the difference is the repeated tile alone. Either way the filter keeps the last digit and throws every other one, so its output repeats with period equal to the tile side while the Thue-Morse grid does not, and the two differ at every side past the tile. Sharper on the plus-minus fold at tile side two: the disagreement is exactly half the sites at every side four and beyond, for every one of the sixteen designs, because the low digits fix a residue class and inside each class the high digits carry opposite letters exactly half the time. Half is the score a coin gets, so the filter output is no evidence of Thue-Morse at all. The resemblance is real and it is generic rather than special: both pictures are digit rules, so both are Kronecker powers of one small tile, and the eye reads any plus-minus speckle at the finest scale as Thue-Morse. Proved here, and pinned in the crate at every code and level the page draws.';2122const attempt = (fn) => {23  try {24    return { ...fn(), error: null };25  } catch (error) {26    return { error };27  }28};2930const hue = (bit) => (bit ? ink.blue : ink.orange);3132const ribbon = (rows, length) => (canvas) => {33  const tall = rows.length * 26 + 4;34  const [ctx, w] = fit(canvas, tall);35  ctx.clearRect(0, 0, w, tall);36  const step = (w - 16) / length;37  rows.forEach((row, r) => {38    for (let i = 0; i < row.length; i++) {39      ctx.fillStyle = row[i];40      ctx.fillRect(8 + i * step, 2 + r * 26, Math.max(1, step - 1), 22);41    }42  });43};4445function spread(runs) {46  const out = [];47  for (const run of runs) for (let k = 0; k < run; k++) out.push(run);48  return out;49}5051function tile(bits) {52  return { width: 2, height: 2, types: Uint8Array.from(bits) };53}5455function verdict(row) {56  if (row.twin) return <span className="chip proved">the same grid as {row.twin}</span>;57  if (row.folds) return <span className="chip proved">a Kronecker power</span>;58  return <span className="chip refuted">no Kronecker power</span>;59}6061function App() {62  const shared = useSeeds();63  const [pick, set] = useQuery(FIRST);64  const [playing, setPlaying] = useState(false);65  const [parity, setParity] = useState(false);6667  const clamp = (value, low, high) => Math.max(low, Math.min(value, high));68  const round = clamp(pick.round, 1, ROUNDS);69  const level = clamp(pick.level, 1, LEVELS);70  const length = clamp(pick.length, 16, 4096);71  const cap = m.level_cap(pick.side, 1, SIDE);72  const depth = clamp(pick.depth, 1, Math.max(1, cap - 1));7374  useEffect(() => {75    if (!playing) return;76    const tick = setInterval(() => set({ round: round >= ROUNDS ? 1 : round + 1 }), 700);77    return () => clearInterval(tick);78  }, [playing, round]);7980  const word = attempt(() => ({ read: JSON.parse(m.morse_word(length)) }));8182  const seed = attempt(() => {83    const stage = Array.from(m.morse_stage(round));84    const digits = JSON.parse(m.morse_word(stage.length)).digits;85    return { stage, digits, agree: stage.every((bit, i) => bit === digits[i]) };86  });8788  const gallery = attempt(() => ({89    rows: JSON.parse(m.morse_gallery(level)),90    grids: ['parity', 'and', 'xor', 'sum'].map((kind) => m.morse_lift(kind, level)),91  }));9293  const filter = attempt(() => ({94    read: JSON.parse(m.morse_filter(pick.code, pick.side, pick.base, depth, pick.fold)),95    coarse: pick.fold === 'sign' ? m.morse_signs(pick.code, pick.side, pick.base, depth) : m.two_grid(pick.code, pick.side, depth, 0, pick.base),96    fine: pick.fold === 'sign' ? m.morse_signs(pick.code, pick.side, pick.base, depth + 1) : m.two_grid(pick.code, pick.side, depth + 1, 0, pick.base),97    difference: m.morse_difference(pick.code, pick.side, pick.base, depth, pick.fold),98  }));99100  const rates = attempt(() => ({ read: JSON.parse(m.magic_rates(['3', '7'], [2, 2], [2, 2], 'thue-morse', RATES)) }));101102  const wordLink = share({103    l0code: '3', l0base: 2, l0n: 2, l1code: '7', l1base: 2, l1n: 2,104    view: 'nest', compare: 'swap', chart: 'exponent', schedule: 'thue-morse', length: RATES,105  });106107  const letters = () => {108    const { stage, digits, agree } = seed;109    const top = parity ? digits : stage;110    const low = parity ? stage : digits;111    return (112      <>113        <Sketch draw={ribbon([top.map(hue), low.map(hue)], top.length)} deps={[top, low, parity]} role="img" aria-label="The word beside the parity of the binary digit sum" />114        <div className="stats">115          <span><span className="swatch" style={{ background: ink.orange }}></span> letter 0, plus one</span>116          <span><span className="swatch" style={{ background: ink.blue }}></span> letter 1, minus one</span>117          <Stat label="letters">{top.length}</Stat>118          <span>{parity ? 'top by bit parity, below by substitution' : 'top by substitution, below by bit parity'}</span>119          <span className={`chip ${agree ? 'proved' : 'refuted'}`}>{agree ? 'the two rules agree letter for letter' : 'the two rules differ'}</span>120        </div>121      </>122    );123  };124125  const runs = () => {126    const { read } = word;127    const lengths = spread(read.runs).slice(0, read.length);128    return (129      <>130        <Sketch draw={ribbon([read.digits.map(hue), lengths.map((run) => (run === 1 ? ink.yellow : ink.pink))], read.length)} deps={[read]} role="img" aria-label="The runs" />131        <div className="stats">132          <span><span className="swatch" style={{ background: ink.yellow }}></span> a run of one</span>133          <span><span className="swatch" style={{ background: ink.pink }}></span> a run of two</span>134          <Stat label="longest run">{read.longest}</Stat>135          <Stat label="runs of one">{read.singles}</Stat>136          <Stat label="runs of two">{read.doubles}</Stat>137          <Stat label="ones">{read.ones}</Stat>138          <span className={`chip ${read.cube_free ? 'proved' : 'refuted'}`}>{read.cube_free ? 'no 000 and no 111' : 'a cube appears'}</span>139        </div>140        <h2>the run boundaries <span>one wherever a letter differs from the next</span></h2>141        <Sketch draw={ribbon([read.boundary.map(hue), read.doubling.map(hue)], read.boundary.length)} deps={[read]} role="img" aria-label="The run boundaries" />142        <div className="stats">143          <span>top, the boundary word of Thue-Morse</span>144          <span>below, the period-doubling word grown by 1 to 10 and 0 to 11</span>145          <span className={`chip ${read.doubling_agree ? 'proved' : 'refuted'}`}>{read.doubling_agree ? 'the same word' : 'two words'}</span>146        </div>147      </>148    );149  };150151  const card = (row, grid) => (152    <div className="panel" key={row.name}>153      <h2>{row.formula} <span>side {row.side}</span></h2>154      <Signs grid={grid} role="img" aria-label={row.formula} />155      <div className="stats">156        {verdict(row)}157        {row.folds ? <span>base tile <Signs grid={tile(row.tile)} className="" style={{ width: 22, height: 22, borderRadius: 4, verticalAlign: 'middle', imageRendering: 'pixelated' }} role="img" aria-label="the base tile" /></span> : null}158        {row.design ? <Stat label="the plus-minus render of">{m.name_of(row.design, 2, 2)}</Stat> : null}159        {row.folds ? null : <span>differs from its corner fold at <b>{row.faults}</b> of <b>{row.side * row.side}</b> sites, first at row <b>{row.first[0]}</b> column <b>{row.first[1]}</b></span>}160      </div>161    </div>162  );163164  const pane = (label, grid, on, note) => (165    <div key={label}>166      {pick.fold === 'sign' ? <Signs grid={grid} role="img" aria-label={label} /> : <Grid grid={grid} on={on} role="img" aria-label={label} />}167      <div className="stats"><span>{label}</span><span className="dim">side {grid.width}{note}</span></div>168    </div>169  );170171  const box = () => {172    const { read } = filter;173    return (174      <>175        <div className="arena">176          {pane('the level', filter.coarse, ink.yellow, ', drawn at the width of the next, which is the blow-up')}177          {pane('the next level', filter.fine, ink.yellow, '')}178          {pane('the difference', filter.difference, ink.pink, '')}179        </div>180        <div className="stats">181          <span className={`chip ${read.closed_exact ? 'proved' : 'refuted'}`}>{read.closed_exact ? 'exactly' : 'not'} {read.form}</span>182          <span className={`chip ${read.morse_exact ? 'proved' : 'refuted'}`}>{read.morse_exact ? 'the Thue-Morse grid' : 'not the Thue-Morse grid'}</span>183          {read.morse_faults === null ? <span className="dim">the Thue-Morse grid lives at side two, so a side-{read.number} tile has nothing to compare against</span>184            : <span>differs from Thue-Morse at <b>{read.morse_faults}</b> of <b>{read.cells}</b> sites, a share of <b>{(read.morse_faults / read.cells).toFixed(4)}</b></span>}185          {read.morse_faults !== null && read.morse_faults * 2 === read.cells ? <span className="chip refuted">exactly half the sites, the score a coin gets</span> : null}186          <Stat label="lit">{read.lit}</Stat>187          {read.morse_tile ? <span className="chip verified">this design reads plus-minus as the Thue-Morse tile</span> : null}188        </div>189      </>190    );191  };192193  const controls = (194    <>195      <section>196        <h3>The word</h3>197        <Row>198          <Slider label="substitution rounds" value={round} min={1} max={ROUNDS} show={`${round}, ${1 << round} letters`} onChange={(v) => { setPlaying(false); set({ round: v }); }} />199          <Btn on={playing} onClick={() => setPlaying(!playing)}>{playing ? 'Stop' : 'Play'}</Btn>200          <Check label="bit parity on top" checked={parity} onChange={setParity} />201        </Row>202      </section>203      <section>204        <h3>The lifts</h3>205        <Row>206          <Slider label="level" value={level} min={1} max={LEVELS} show={`${level}, side ${1 << level}`} onChange={(v) => set({ level: v })} />207        </Row>208      </section>209      <section>210        <h3>The runs</h3>211        <Row>212          <Pick label="letters" value={length} options={[[64, 64], [128, 128], [256, 256], [512, 512], [1024, 1024]]} onChange={(v) => set({ length: +v })} />213        </Row>214      </section>215      <section>216        <h3>The difference filter</h3>217        <Row>218          <span className="set">219            <Picker dimension={2} bases={[2, 3]} code={pick.code} base={pick.base} seeds={shared}220              onChange={(values) => set({ ...values, ...(values.base && pick.side < values.base ? { side: values.base } : {}) })} />221          </span>222          <Pick label="tile side" value={pick.side} options={[[2, 2], [3, 3], [5, 5]]} onChange={(v) => set({ side: Math.max(+v, pick.base) })} />223          <Slider label="level" value={depth} min={1} max={Math.max(1, cap - 1)} onChange={(v) => set({ depth: v })} />224          <Pick label="fold" value={pick.fold} options={[['sign', 'plus-minus, the exclusive or'], ['design', 'the design, the and']]} onChange={(v) => set({ fold: v })} />225        </Row>226      </section>227    </>228  );229230  return (231    <Page crumb="morse" title="The Thue-Morse word is one digit rule built twice"232      sub="The most famous aperiodic sequence is a mrly object. Its letter is a digit rule, the same move every design makes; its famous plane pattern is the Kronecker power of one plus-minus tile; and it is the schedule along which the tree computed a component exponent exactly."233      controls={controls}234      foot={<>Every letter, grid, run and verdict below comes out of the crates through wasm; the page only draws. The plus-minus render is the first on the site: a warm cell is plus one, a cool cell is minus one, and a dark cell is empty. Links: <a href={`../words${wordLink}`}>the words</a> drives a word by this schedule, <a href="../moire">moire</a> stacks one design over its scales, <a href="../sequences">the sequences</a> holds the ledger the designs write. A rule that changes with the scale is a word, and the grammar of one is in <a href="/research/magic/">the magic words note</a>.</>}>235236      <div className="panel">237        <h2>the word <span>0 to 01, 1 to 10, beside the parity of the binary digit sum</span></h2>238        <Note error={seed.error} />239        {seed.error ? null : letters()}240        <p className="sub">The digit rule is `t(n)`, the parity of the count of one-bits of `n`. The substitution grows the same word from a single 0 by doubling: every 0 becomes 01 and every 1 becomes 10. Digits are the recursion, so the two constructions are one construction seen twice, and the chip above is a live comparison rather than a claim.</p>241      </div>242243      <div className="arena">244        {gallery.error ? null : gallery.rows.map((row, at) => card(row, gallery.grids[at]))}245      </div>246      <Note error={gallery.error} />247      <p className="sub">{GALLERY}</p>248      <p className="sub">The first lift is the sign grid `(-1)^(popcount(i) + popcount(j))`, the Kronecker power of the two-by-two tile with plus one on its diagonal, and that tile is a mrly design read plus-minus. The second is the Walsh-Hadamard pattern, the gasket read the same way, which is the spectrometer's world. The third is not a third grid at all: `popcount(i xor j)` and `popcount(i) + popcount(j)` agree modulo two, so `t` carries exclusive or to exclusive or and the third lift is the first. The fourth carries, and carrying is not a digit rule, so it does not fold; its grid is constant along every antidiagonal instead, which is a Hankel pattern and never a Kronecker power past side two.</p>249250      <div className="panel">251        <h2>the runs <span>cube-freeness made visible</span></h2>252        <Note error={word.error} />253        {word.error ? null : runs()}254        <p className="sub">Because `t(2n) = t(n)` and `t(2n+1) = 1 - t(n)`, the word changes at every even place, so no run reaches three: `000` and `111` never appear. The word that marks where the runs break is the period-doubling word, and the page checks that identity term by term at every length it draws. Verified in the crates, and stated in research/connectivity.md at every length to `2^20`.</p>255      </div>256257      <div className="arena">258        <div className="panel">259          <h2>the schedule <span>Thue-Morse as a word over two letters</span></h2>260          <p className="sub">Read the letters as designs rather than as bits and the word becomes a schedule: one design per level, the domino where the letter is 0 and the gasket where it is 1. That is exactly what <a href={`../words${wordLink}`}>the words</a> draws, with the component exponent charted along the schedule and its periodic control beside it.</p>261          <div className="stats">262            <Stat label="letters">the domino and the gasket at side two</Stat>263            <span className="chip proved">order-blind at interior frequency</span>264          </div>265          <p className="sub">Open <a href={`../words${wordLink}`}>the words at this schedule</a>.</p>266        </div>267        <div className="panel">268          <h2>the exponent <span>the value the tree computed exactly</span></h2>269          <Note error={rates.error} />270          {rates.error ? null : (271            <div className="stats">272              <Stat label="interior exponent, log two units">{rates.read.limit.toFixed(15)}</Stat>273              <Stat label="the prefix rate at length 64">{rates.read.rows[rates.read.length - 1][0].toFixed(9)}</Stat>274              <Stat label="the periodic control">{rates.read.control[rates.read.length - 1].toFixed(9)}</Stat>275              <span className="chip proved">(1/2) log 6, Proved</span>276            </div>277          )}278          <p className="sub">Along Thue-Morse over a gasket-and-domino pair the component exponent is exactly `(1/2) log 6`, with a two-sided certificate rather than a fit: the word has no three equal letters in a row, which caps the sandwich suffix, and it is balanced, which pins the letter counts to within one half of `L/2`. The aperiodicity earns nothing extra here, because the exponent depends on the letter frequencies alone, so a periodic word of the same frequencies returns the same number. Proved, research/connectivity.md.</p>279        </div>280      </div>281282      <div className="panel">283        <h2>the difference filter <span>a level exclusive-ored with its own next level</span></h2>284        <Note error={filter.error} />285        {filter.error ? null : box()}286        <p className="sub">{FILTER}</p>287      </div>288    </Page>289  );290}291292mount(<App />);