index.jsx

9.1 kB · jsx · 161 lines

1import { useMemo } from 'react';2import { ready, ink } from '../../lib/mrly.js';3import { board, line, axis, rules, tag } from '../../lib/chart.js';4import { mount, Page, Group, Pick, Slider, Check, Stats, Stat, Note } from '../../lib/app.jsx';5import { Signs, Sketch } from '../../lib/draw.jsx';6import { useQuery } from '../../lib/query.js';7import { useSeeds, seeded, Picker } from '../../lib/select.jsx';89const m = await ready();10const SIDES = [2, 3, 5];11const DEEPEST = 6;12const CELLS = 729;13const SAMPLES = 200;14const SWEEP = 2001;15const PAD = 14;16const band = () => ({ plus: ink.fg, minus: ink.blue, empty: ink.deep });1718const cap = (side) => Math.min(DEEPEST, m.level_cap(side, 1, CELLS));1920function App() {21  const s = useSeeds();22  const [pick, set] = useQuery({23    code: seeded(s, 2, 3, '495'), side: 3, base: 3, level: 5, eps: 7, closed: true,24  });2526  const code = pick.code.trim();27  const level = Math.max(1, Math.min(pick.level, cap(pick.side)));28  const span = pick.side ** level;29  const reach = Math.max(1, span / pick.side);30  const eps = Math.max(1, Math.min(pick.eps, reach));3132  const built = useMemo(() => {33    try {34      return {35        dist: m.tube_distance(code, pick.side, level, pick.base),36        pairs: m.tube_profile(code, pick.side, level, pick.base, SAMPLES),37        digits: m.modes_digits(code, pick.side, pick.base),38        d: m.dimension(code, pick.side, 2, pick.base),39        inside: m.tube_class(code, pick.side, pick.base),40        name: m.name_of(code, 2, pick.base),41        error: null,42      };43    } catch (error) {44      return { dist: null, pairs: null, digits: 0, d: 0, inside: false, name: '', error };45    }46  }, [code, pick.side, level, pick.base]);4748  const limit = useMemo(() => {49    if (!built.inside) return null;50    const gap = 1 - 1 / pick.side;51    let low = Infinity, high = -Infinity;52    for (let i = 0; i < SWEEP; i += 1) {53      const g = m.tube_closed(pick.side, built.digits, 1 / pick.side + (gap * i) / (SWEEP - 1));54      low = Math.min(low, g);55      high = Math.max(high, g);56    }57    return { low, high, swing: (100 * (high - low)) / low };58  }, [built.inside, built.digits, pick.side]);5960  const cells = useMemo(() => {61    if (!built.dist) return null;62    return { width: span, height: span, types: Uint8Array.from(built.dist, (v) => (v === 0 ? 0 : v <= eps ? 1 : 2)) };63  }, [built.dist, eps]);6465  const volume = useMemo(66    () => (built.dist ? m.tube_volume(code, pick.side, level, pick.base, eps) : null),67    [built.dist, eps],68  );6970  const yellow = useMemo(() => {71    if (!limit || !built.pairs || !pick.closed) return null;72    const out = [];73    for (let k = 0; k < built.pairs.length; k += 2) out.push(m.tube_closed(pick.side, built.digits, Math.exp(-built.pairs[k])));74    return out;75  }, [limit, built.pairs, pick.closed]);7677  const profile = (canvas) => {78    const pairs = built.pairs;79    if (!pairs || pairs.length < 4) return;80    const b = board(canvas, 340, { pad: PAD, top: 20, bottom: 22 });81    const us = [], ms = [];82    for (let k = 0; k < pairs.length; k += 2) {83      us.push(pairs[k]);84      ms.push(pairs[k + 1]);85    }86    const [u0, u1] = [us[0], us.at(-1)];87    const seen = yellow ? ms.concat(yellow) : ms;88    const floor = Math.min(...seen), roof = Math.max(...seen);89    const pad = (roof - floor) * 0.08 || 0.05;90    const fx = (u) => (u - u0) / (u1 - u0);91    const fy = (v) => (v - floor + pad) / (roof - floor + 2 * pad);92    const step = Math.log(pick.side);93    const marks = [];94    for (let n = Math.ceil(u0 / step); n * step <= u1; n += 1) marks.push(fx(n * step));95    rules(b, marks, { dash: [2, 4] });96    rules(b, [fx(Math.log(span / eps))], { color: ink.pink });97    if (yellow) {98      line(b, [[0, fy(limit.low)], [1, fy(limit.low)]], ink.dim, { width: 1, dash: [3, 5] });99      line(b, [[0, fy(limit.high)], [1, fy(limit.high)]], ink.dim, { width: 1, dash: [3, 5] });100      line(b, us.map((u, i) => [fx(u), fy(yellow[i])]), ink.yellow, { width: 1.6 });101    }102    line(b, us.map((u, i) => [fx(u), fy(ms[i])]), ink.blue, { width: 1.6 });103    axis(b, [[0, `ln 1/eps ${u0.toFixed(2)}`], [1, u1.toFixed(2)]], { wall: true });104    const edge = tag(b, 'M measured', ink.blue);105    if (yellow) tag(b, `G limit, swing ${limit.swing.toFixed(5)}%`, ink.yellow, 'left', edge + 12);106  };107108  const controls = (109    <>110      <Group name="Design">111        <Picker dimension={2} bases={[3, 2]} code={pick.code} base={pick.base} seeds={s} onChange={set} />112        <Pick label="side" value={pick.side} options={SIDES.map((v) => [v, v])} onChange={(v) => set({ side: +v, level: Math.min(level, cap(+v)) })} />113        <Slider label="level" value={level} min={1} max={cap(pick.side)} show={`${level}, ${span} by ${span}`} onChange={(v) => set({ level: v })} />114      </Group>115      <Group name="Radius">116        <Slider label="eps" value={eps} min={1} max={reach} show={`${eps} cells, ${(eps / span).toFixed(6)} of the side`} onChange={(v) => set({ eps: v })} />117      </Group>118      <Group name="Limit">119        <Check label="closed form" checked={pick.closed} onChange={(v) => set({ closed: v })} />120      </Group>121    </>122  );123124  return (125    <Page crumb="tube" title="The inner tube of a design and what it refuses to settle on"126      sub={<>Fatten a design by <code>eps</code> and measure the area it swallows: that is the inner tube <code>V(eps)</code>. Divide by the power the dimension asks for and you get the Minkowski content reading <code>M(eps)</code>, which should settle down if the design has a length in its own dimension. Drag the radius and watch it not settle: it circles the same profile forever, once per factor of the base.</>}127      controls={controls}128      foot={<>The left panel is an exact Euclidean distance transform of the grid at the level in Rust, the two-pass lower envelope of parabolas, so every cell carries its true distance in cell widths to the nearest filled cell; the band is that field thresholded at the radius, and the tube area is the field read again, each cell carrying the share of itself the radius reaches. No hole lemma enters, so every design the picker offers gets a tube and a profile. The yellow curve is the other route and applies only to designs whose holes are isolated interior squares with their boundaries in the set: there the complement splits level by level into <code>fill^(m-1)</code> open squares of side <code>base^(-m)</code>, the inner parallel area of a square of side <code>s</code> is <code>4 eps s - 4 eps^2</code> until <code>2 eps</code> passes <code>s</code> and <code>s^2</code> after, and the two geometric tails close in the form the page draws. The measured curve sits below the limit by about <code>(4/5) eps^(log(fill)/log(base) - 1)</code>, the cost of a finite grid, and climbs onto it as the radius shrinks. The dashed rules are the powers of the base, one period of the profile apart. The same design counted inside a shape rather than fattened is <a href="../crop">the crop</a>, and its mask laid over the torus is <a href="../modes">the modes</a>. Every distance, area and profile value comes out of the crates through wasm; the page only draws.</>}>129      <p><span className="chip proved">Proved</span> The Sierpinski carpet is not Minkowski measurable: its tube is the exact hole sum, <code>M(eps)</code> runs onto a log-periodic <code>G(t)</code> with <code>G(1/3) = G(1) = 379/280</code>, and the swing between its maximum and its minimum is <code>0.36625%</code>, above zero, so no limit exists. The proof, the class it opens and the sponge it does not reach are on <a href="/research/dimensions/">the dimensions page</a>.</p>130      <div className="arena">131        <div className="panel">132          <h2>The design and its tube <span>{`level ${level}, radius ${eps} of ${span} cells`}</span></h2>133          {cells && <Signs grid={cells} hues={band()} role="img" aria-label="The design in the foreground with its inner tube band in blue" />}134        </div>135        <div className="panel">136          <h2>The Minkowski profile <span>{built.pairs && built.pairs.length ? `M against ln 1/eps, ${SAMPLES} radii` : 'the grid is too coarse to resolve a range of radii'}</span></h2>137          <Sketch draw={profile} deps={[built.pairs, yellow, eps, span]} />138        </div>139      </div>140      <Stats>141        <Stat label="design">{built.name}</Stat>142        <Stat label="base">{pick.base}</Stat>143        <Stat label="side">{pick.side}</Stat>144        <Stat label="digits">{built.digits}</Stat>145        <Stat label="dimension d">{built.d ? built.d.toFixed(9) : ''}</Stat>146        <Stat label="grid">{`${span} by ${span}`}</Stat>147      </Stats>148      <Stats>149        <Stat label="eps">{`${eps} cells, ${(eps / span).toFixed(9)}`}</Stat>150        <Stat label="V in cells">{volume === null ? '' : volume.toFixed(3)}</Stat>151        <Stat label="V of the square">{volume === null ? '' : (volume / (span * span)).toFixed(9)}</Stat>152        <Stat label="closed form">{built.inside ? 'the design is in the class' : 'outside the class, measured only'}</Stat>153        <Stat label="G band">{limit ? `${limit.low.toFixed(9)} to ${limit.high.toFixed(9)}` : ''}</Stat>154        <Stat label="swing">{limit ? `${limit.swing.toFixed(6)}%` : ''}</Stat>155      </Stats>156      <Note error={built.error} />157    </Page>158  );159}160161mount(<App />);