index.jsx
10.1 kB · jsx · 188 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, Btn, Stats, Stat, Note } from '../../lib/app.jsx';5import { Pixels, 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 SLIDERS = 9;14const CAP = 32;15const SAMPLES = 241;16const MOMENTS = 161;17const REACH = 4;18const PAD = 14;1920const cap = (side) => Math.min(DEEPEST, m.level_cap(side, 1, CELLS));21const sum = (list) => list.reduce((a, b) => a + b, 0);2223function App() {24 const s = useSeeds();25 const [pick, set] = useQuery({26 code: seeded(s, 2, 3, '69'), side: 3, base: 3, level: 4,27 w: '3,2,3', gamma: 35, curve: 'spectrum',28 });2930 const code = pick.code.trim();31 const level = Math.max(1, Math.min(pick.level, cap(pick.side)));32 const span = pick.side ** level;3334 const corners = useMemo(() => {35 try {36 return { list: Array.from(m.weights_corners(code, pick.side, pick.base)), error: null };37 } catch (error) {38 return { list: [], error };39 }40 }, [code, pick.side, pick.base]);4142 const k = corners.list.length / 2;43 const shares = useMemo(() => {44 const read = pick.w.split(',').map((v) => Math.max(1, Math.min(CAP, Math.round(+v) || 1)));45 return read.length === k ? read : Array(k).fill(1);46 }, [pick.w, k]);47 const key = shares.join(',');48 const whole = sum(shares);4950 const read = useMemo(() => {51 if (!k) return { error: corners.error };52 try {53 const w = Float64Array.from(shares);54 return {55 dims: m.weights_dims(code, pick.side, pick.base, w),56 point: m.weights_point(code, pick.side, pick.base, w, 1),57 spectrum: m.weights_spectrum(code, pick.side, pick.base, w, SAMPLES),58 pressure: m.weights_pressure(code, pick.side, pick.base, w, -REACH, REACH, MOMENTS),59 name: m.name_of(code, 2, pick.base),60 error: null,61 };62 } catch (error) {63 return { error };64 }65 }, [code, pick.side, pick.base, key]);6667 const drawn = useMemo(() => {68 if (!k || read.error) return null;69 const w = Float64Array.from(shares);70 const mass = m.weights_mass(code, pick.side, level, pick.base, w);71 let least = Infinity;72 for (const v of mass) if (v > 0 && v < least) least = v;73 return {74 sheet: m.weights_pixels(code, pick.side, level, pick.base, w, pick.gamma / 100),75 peak: Math.max(...mass),76 least,77 cells: mass.reduce((count, v) => count + (v > 0 ? 1 : 0), 0),78 };79 }, [code, pick.side, pick.base, level, key, pick.gamma]);8081 const curve = (canvas) => {82 if (read.error || !read.dims) return;83 const b = board(canvas, 340, { pad: PAD, top: 20, bottom: 22 });84 const [low, high, one, roof] = Array.from(read.dims);85 if (pick.curve === 'pressure') {86 const xs = [], ys = [];87 for (let i = 0; i < read.pressure.length; i += 2) {88 xs.push(read.pressure[i]);89 ys.push(read.pressure[i + 1]);90 }91 const floor = Math.min(...ys), top = Math.max(...ys);92 const pad = (top - floor) * 0.08 || 0.05;93 const fx = (v) => (v + REACH) / (2 * REACH);94 const fy = (v) => (v - floor + pad) / (top - floor + 2 * pad);95 rules(b, [fx(0), fx(1)], { dash: [2, 4] });96 line(b, [[0, fy(0)], [1, fy(0)]], ink.line, { width: 1 });97 line(b, xs.map((v, i) => [fx(v), fy(ys[i])]), ink.blue, { width: 1.6 });98 line(b, [[fx(0), fy(roof)]], ink.yellow, { dots: 3.6 });99 line(b, [[fx(1), fy(0)]], ink.yellow, { dots: 3.6 });100 axis(b, [[0, `s ${-REACH}`], [0.5, '0'], [1, `${REACH}`]], { wall: true });101 const edge = tag(b, 'tau(s) = log_side sum w^s', ink.blue);102 tag(b, `tau(0) ${roof.toFixed(9)}, tau(1) 0`, ink.yellow, 'left', edge + 12);103 return;104 }105 const xs = [], ys = [];106 for (let i = 0; i < read.spectrum.length; i += 2) {107 xs.push(read.spectrum[i]);108 ys.push(read.spectrum[i + 1]);109 }110 const wide = high - low || 1;111 const fx = (v) => 0.04 + 0.92 * (v - low) / wide;112 const fy = (v) => 0.05 + 0.9 * v / (roof || 1);113 rules(b, [fx(one)], { dash: [2, 4] });114 line(b, [[0, fy(0)], [1, fy(0)]], ink.line, { width: 1 });115 line(b, xs.map((v, i) => [fx(v), fy(ys[i])]), ink.blue, { width: 1.6 });116 line(b, [[fx(read.point[2]), fy(read.point[3])]], ink.yellow, { dots: 4 });117 axis(b, [[0, `alpha ${low.toFixed(6)}`], [1, high.toFixed(6)]], { wall: true });118 const edge = tag(b, 'f(alpha)', ink.blue);119 tag(b, `alpha(1) ${one.toFixed(9)}`, ink.yellow, 'left', edge + 12);120 };121122 const spread = (make) => set({ w: make().join(',') });123 const flat = () => Array(k).fill(1);124 const rim = (v) => v === 0 || v === pick.side - 1;125 const heavy = () => Array.from({ length: k }, (_, i) => (rim(corners.list[2 * i]) && rim(corners.list[2 * i + 1]) ? 8 : 1));126 const drawWeights = () => Array.from(m.random_between(s.next(), Array(k).fill(1), Array(k).fill(CAP)));127128 const controls = (129 <>130 <Group name="Design">131 <Picker dimension={2} bases={[3, 2]} code={pick.code} base={pick.base} seeds={s} onChange={set} />132 <Pick label="side" value={pick.side} options={SIDES.map((v) => [v, v])} onChange={(v) => set({ side: +v, level: Math.min(level, cap(+v)) })} />133 <Slider label="level" value={level} min={1} max={cap(pick.side)} show={`${level}, ${span} by ${span}`} onChange={(v) => set({ level: v })} />134 </Group>135 <Group name={`Weights over ${whole}`}>136 {k <= SLIDERS && shares.map((share, i) => (137 <Slider key={i} label={`(${corners.list[2 * i]},${corners.list[2 * i + 1]})`} value={share} min={1} max={CAP}138 show={`${share}/${whole}`} onChange={(v) => spread(() => shares.map((held, at) => (at === i ? v : held)))} />139 ))}140 <Btn primary onClick={() => spread(flat)}>equal</Btn>141 {k > SLIDERS && <Btn onClick={() => spread(heavy)}>corner-heavy</Btn>}142 {k > SLIDERS && <Btn onClick={() => spread(drawWeights)}>seeded random</Btn>}143 </Group>144 <Group name="Reading">145 <Slider label="gamma" value={pick.gamma} min={10} max={100} show={(pick.gamma / 100).toFixed(2)} onChange={(v) => set({ gamma: v })} />146 <Pick label="curve" value={pick.curve} options={[['spectrum', 'f(alpha)'], ['pressure', 'tau(s)']]} onChange={(v) => set({ curve: v })} />147 </Group>148 </>149 );150151 return (152 <Page crumb="weights" title="The mass side of a weighted design"153 sub={<>Give every filled corner of a design a weight and the cell at the level reached by the digit word <code>f_1 ... f_level</code> carries the mass <code>w_(f_1) ... w_(f_level)</code>. The support never moves, the mass does: drag one weight and the picture darkens on one corner and lights on another while the design stays where it is. The curve beside it is the whole multifractal spectrum, and it is a closed form in the weights alone.</>}154 controls={controls}155 foot={<>The design is the picker's plane code at its side and its residue base; the filled cells of its level-one tile are the corners the sliders weight, listed row by row, and the vector is normalised to sum to one inside the crate. The picture is the mass field at the level, one cell a pixel, read at <code>(mass / peak)^gamma</code> through the ground-blue-yellow ramp, so the gamma is display alone and never a printed number. The pressure is summed with the largest exponent factored out, which is why the curve stays exact out to <code>s = 30</code> where the spectrum's two tails are sampled. Equal weights are the 0/1 design itself: the spectrum collapses to the single point <code>(log_side fill, log_side fill)</code>, which is the box dimension the support has at every weighting. The geometry these weights leave alone is the same design fattened on <a href="../tube">the tube</a>, cropped on <a href="../crop">the crop</a> and laid over the torus on <a href="../modes">the modes</a>. Every mass, every exponent and every point of the curve comes out of the crates through wasm; the page only draws.</>}>156 <p><span className="chip verified">Verified</span> At contraction <code>1/side</code> under the open set condition, which every design satisfies with the unit cell, the pressure equation closes in one line: <code>tau(s) = log_side sum_f w_f^s</code>, with <code>f(alpha) = inf_s (alpha s + tau(s))</code> attained at <code>alpha(s) = -tau'(s)</code>, so the spectrum is explicit in the weights and nothing is fitted. The proof it is read from and the exact tables are on <a href="../../research/weights/">the weights page</a>.</p>157 <div className="arena">158 <div className="panel">159 <h2>The mass field <span>{`level ${level}, ${span} by ${span}`}</span></h2>160 {drawn && <Pixels data={drawn.sheet} role="img" aria-label="The mass field of the weighted design" />}161 </div>162 <div className="panel">163 <h2>{pick.curve === 'pressure' ? 'The pressure' : 'The spectrum'} <span>{pick.curve === 'pressure' ? `tau against s, ${MOMENTS} moments` : `f against alpha, ${SAMPLES} moments`}</span></h2>164 <Sketch draw={curve} deps={[read, pick.curve]} />165 </div>166 </div>167 <Stats>168 <Stat label="design">{read.name ?? ''}</Stat>169 <Stat label="base">{pick.base}</Stat>170 <Stat label="side">{pick.side}</Stat>171 <Stat label="corners">{k}</Stat>172 <Stat label="weights">{`${shares.join(' ')} over ${whole}`}</Stat>173 <Stat label="cells">{drawn ? `${drawn.cells} of ${span * span}` : ''}</Stat>174 </Stats>175 <Stats>176 <Stat label="alpha_min">{read.dims ? read.dims[0].toFixed(9) : ''}</Stat>177 <Stat label="alpha_max">{read.dims ? read.dims[1].toFixed(9) : ''}</Stat>178 <Stat label="alpha(1)">{read.dims ? read.dims[2].toFixed(9) : ''}</Stat>179 <Stat label="tau(0)">{read.dims ? read.dims[3].toFixed(9) : ''}</Stat>180 <Stat label="heaviest cell">{drawn ? drawn.peak.toExponential(6) : ''}</Stat>181 <Stat label="lightest cell">{drawn ? drawn.least.toExponential(6) : ''}</Stat>182 </Stats>183 <Note error={read.error} />184 </Page>185 );186}187188mount(<App />);