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