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