index.jsx

10.1 kB · jsx · 190 lines

1import { useMemo, useState } from 'react';2import { ready, ink } from '../../lib/mrly.js';3import { mount, Page, Row, Pick, Slider, Stats, Stat, Note } from '../../lib/app.jsx';4import { Pixels, Signs, Sketch } from '../../lib/draw.jsx';5import { board, line, axis, tag } from '../../lib/chart.js';6import { useQuery } from '../../lib/query.js';7import { Ramp } from '../../lib/select.jsx';89const m = await ready();10const CARPET = '23';11const SIZES = [128, 192, 256, 320, 384];12const hues = () => ({ plus: ink.yellow, minus: ink.blue, empty: ink.deep });1314const fixed = (value, places) => (Number.isFinite(value) ? value.toFixed(places) : 'none');1516function Arm({ read }) {17  const rows = read.rows;18  const draw = (canvas) => {19    const b = board(canvas, 240);20    const n = rows.length;21    const at = (i) => (i + 0.5) / n;22    const step = Math.max(1, Math.ceil(n / 12));23    axis(b, rows.map((row, i) => [at(i), String(row.n)]).filter((_, i) => i % step === 0), { wall: true });24    b.ctx.strokeStyle = ink.line;25    b.ctx.lineWidth = 1;26    b.ctx.beginPath();27    b.ctx.moveTo(b.x(0), b.y(0.5));28    b.ctx.lineTo(b.x(1), b.y(0.5));29    b.ctx.stroke();30    line(b, rows.map((row, i) => [at(i), row.law]), ink.blue, { width: 1.5 });31    line(b, rows.map((row, i) => [at(i), row.ink]), ink.yellow, { width: 0, dots: 2.6 });32    tag(b, 'the counted ink of the band, one dot a layer', ink.yellow);33    tag(b, '1/2 + chi_8(n)/(2n)', ink.blue, 'right');34  };35  return <Sketch className="bars" draw={draw} deps={[read]} role="img" aria-label="The band ink at every odd layer against the closed form" />;36}3738function Fall({ read }) {39  const draw = (canvas) => {40    const b = board(canvas, 240, { left: 46 });41    const walk = read.walk;42    if (walk.length < 2) {43      tag(b, 'two rungs are needed before the decay reads', ink.dim);44      return;45    }46    const model = walk.map(([count]) => [count, -Math.log(count) / 4 + read.constant]);47    const lows = walk.map(([, value]) => value).concat(model.map(([, value]) => value));48    const low = Math.min(...lows), high = Math.max(...lows);49    const span = Math.max(high - low, 1e-9);50    const first = Math.log(walk[0][0]), last = Math.log(walk[walk.length - 1][0]);51    const reach = Math.max(last - first, 1e-9);52    const place = ([count, value]) => [(Math.log(count) - first) / reach, (value - low) / span];53    line(b, model.map(place), ink.blue, { width: 1.5, dash: [4, 4] });54    line(b, walk.map(place), ink.yellow, { width: 1.8 });55    axis(b, [[0, String(walk[0][0])], [0.5, 'L'], [1, String(walk[walk.length - 1][0])]], { wall: true });56    b.ctx.fillStyle = ink.dim;57    b.ctx.textAlign = 'right';58    b.ctx.fillText(fixed(high, 3), b.left - 6, b.y(1) + 4);59    b.ctx.fillText(fixed(low, 3), b.left - 6, b.y(0) + 4);60    b.ctx.textAlign = 'left';61    tag(b, 'L times the excess, read against ln L', ink.yellow);62    tag(b, read.slope === null ? 'the L/2 window mixes the parities here' : `slope ${read.slope.toFixed(6)} against ${read.target.toFixed(6)}`, ink.blue, 'right');63  };64  return <Sketch className="bars" draw={draw} deps={[read]} role="img" aria-label="The scaled excess against the log of the layer count" />;65}6667function App() {68  const [pick, set] = useQuery({ layers: 28, half: 0, size: 256 });69  const [look, setLook] = useState({ ramp: 'fire', levels: 24, invert: false });7071  let error = null;72  let view = null;73  let arm = null;74  let fall = null;75  try {76    const field = m.star_field(CARPET, pick.layers, pick.size);77    let low = Infinity, high = -Infinity;78    for (const value of field) if (!Number.isNaN(value)) { low = Math.min(low, value); high = Math.max(high, value); }79    view = {80      pixels: m.paint_span(field, pick.size, low, high, look.ramp, look.levels, look.invert),81      band: m.star_band(CARPET, pick.layers, pick.size, pick.half),82      low,83      high,84    };85    arm = JSON.parse(m.star_layers(CARPET, pick.layers, pick.half));86    fall = JSON.parse(m.star_decay(CARPET, pick.layers, pick.half));87  } catch (fault) {88    error = fault;89  }9091  const ribbon = useMemo(() => (arm ? arm.rows.slice(0, 12) : []), [arm]);92  const deepest = arm ? arm.rows[arm.rows.length - 1] : null;9394  const controls = (95    <>96      <section>97        <h3>The stack</h3>98        <Row>99          <Slider label="layers L" value={pick.layers} min={4} max={128} step={2} onChange={(v) => set({ layers: v })} />100          <Pick label="size" value={pick.size} options={SIZES.map((v) => [v, v])} onChange={(v) => set({ size: +v })} />101        </Row>102      </section>103      <section>104        <h3>The band</h3>105        <Row>106          <Slider label="half-width W" value={pick.half} min={0} max={16} step={1} show={`${pick.half} cells`} onChange={(v) => set({ half: v })} />107        </Row>108      </section>109      <section>110        <h3>Colour</h3>111        <Row>112          <Ramp value={look} onChange={(patch) => setLook({ ...look, ...patch })} />113        </Row>114      </section>115    </>116  );117118  return (119    <Page crumb="star" title="The ghost star at the hexagon's centre"120      sub="Stack the hexagonal cuts of a carpet cube, one per odd side, and a six-armed star stands at the centre. It is not there in the limit: it is the gap between an arm that carries no 1/(2n) term and a background that does, and it fades like (ln L)/L. How fast depends on how you measure it."121      controls={controls}122      foot={<>The picture resamples every layer onto one square, which is the ideal frame; the numbers below never resample, reading each layer on the cube's own cells, which is the cell frame. That difference is the whole point: the same star decays at <b>-0.1807</b> in the ideal frame, <b>-0.1252</b> in the lattice frame and exactly <b>-1/4</b> on the arm in the cell frame, and widening the band walks the coefficient again. Only the order <code>(ln L)/L</code>, the sign, and the cell frame's <code>-1/4</code> survive the change of frame, and only <code>-1/4</code> has a closed form beside it. At odd <code>L</code> the constant shifts by <code>1/8</code>, so the slider steps by two and stays even. That same character term traps the sliding window: it cancels between <code>L/2</code> and <code>L</code> only when <code>L</code> is divisible by four, so at <code>L = 2 mod 4</code> the slope is refused rather than reported wrong, and what you read instead is the other branch of the <code>1/L^2</code> term. The stack the layers come from is <a href="../moire">moire</a> and <a href="../volume">volume</a>, the mesh they are counted on is <a href="../slices">slices</a>, and the ink law the background uses is read back from the corners in <a href="../spectrometer">spectrometer</a>. The width readout beside the slope is the closed form <code>-(K + b)/(4(2K + 1))</code> at <code>K = floor(W/2)</code>, so an odd width is never a new band: <code>x - y</code> is even on the cut, and <code>W</code> and <code>W - 1</code> read one point set and one coefficient. The mathematics, its proofs and the frames it was measured in are on <a href="/research/hexagon/">the hexagon page</a>. Every number here is computed in Rust; the page only draws.</>}>123      <div className="arena">124        <div className="panel">125          <h2>The stack <span>{pick.layers} cut layers on one square, the mean ink at every sample</span></h2>126          {view && <Pixels data={view.pixels} />}127          <Stats>128            <Stat label="layers">{pick.layers}</Stat>129            <Stat label="deepest side">{fall?.deepest}</Stat>130            <Stat label="ink range">{view && `${fixed(view.low, 4)} to ${fixed(view.high, 4)}`}</Stat>131          </Stats>132        </div>133        <div className="panel">134          <h2>The band <span>yellow where the star is read, blue the background it is read against</span></h2>135          {view && <Signs grid={view.band} hues={hues()} />}136          <Stats>137            <Stat label="half-width">{`${pick.half} cells`}</Stat>138            <Stat label="arm">{fall?.arm ? 'the exact diameter x = y' : 'a band about it'}</Stat>139          </Stats>140        </div>141      </div>142      <div className="panel" style={{ marginTop: 22 }}>143        <h2>The arm <span>the band's ink at every odd layer against the closed form 1/2 + chi_8(n)/(2n)</span></h2>144        {arm && <Arm read={arm} />}145        <div className="ribbon">146          {ribbon.map((row) => (147            <span key={row.n}>148              <i>n {row.n}</i>149              <b className={row.exact ? 'yellow' : undefined}>{`${row.numer}/${row.denom}`}</b>150              <i>{`law ${row.lawNumer}/${row.lawDenom}`}</i>151            </span>152          ))}153        </div>154        <Stats>155          <Stat label="exact">{arm && `${arm.exact} of ${arm.layers} layers`}</Stat>156          <Stat label="chi_8 at the deepest">{deepest && `${deepest.chi > 0 ? '+' : ''}${deepest.chi}`}</Stat>157          <Stat label="arm ink">{deepest && fixed(deepest.ink, 6)}</Stat>158          <Stat label="hexagon ink">{deepest && fixed(deepest.hex, 6)}</Stat>159          <Stat label="excess">{deepest && fixed(deepest.excess, 6)}</Stat>160        </Stats>161      </div>162      <div className="panel" style={{ marginTop: 22 }}>163        <h2>The decay <span>L times the mean excess, which is -(ln L)/4 + C on the arm in the cell frame</span></h2>164        {fall && <Fall read={fall} />}165        <Stats>166          <Stat label="excess * L">{fall && fixed(fall.scaled, 6)}</Stat>167          <Stat label="plus (ln L)/4">{fall && fixed(fall.logged, 10)}</Stat>168          <Stat label="C">{fall && fixed(fall.constant, 10)}</Stat>169          <Stat label="slope against ln L">{fall && (fall.slope === null ? 'needs L divisible by four' : fixed(fall.slope, 6))}</Stat>170          <Stat label="the width law">{fall && fixed(fall.target, 6)}</Stat>171          <Stat label="L mod 4">{fall?.branch}</Stat>172          <Stat label="residual * L^2">{fall && fixed(fall.residual, 8)}</Stat>173          <Stat label="predicted">{fall && (fall.predicted === null ? 'odd L has none' : fixed(fall.predicted, 8))}</Stat>174        </Stats>175        <div className="ribbon tight">176          {fall && fall.rows.map((row) => (177            <span key={row.layers}>178              <i>L {row.layers}</i>179              <b>{fixed(row.scaled, 6)}</b>180              <i>{fixed(row.logged, 8)}</i>181            </span>182          ))}183        </div>184      </div>185      <Note error={error} />186    </Page>187  );188}189190mount(<App />);