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