index.jsx
11.1 kB · jsx · 213 lines
1import { ready, ink } from '../../lib/mrly.js';2import { mount, Page, Row, Slider, Pick, Check, Btn, Stats, Stat, Note } from '../../lib/app.jsx';3import { Sketch } from '../../lib/draw.jsx';4import { board, line, rules, axis, tag } from '../../lib/chart.js';5import { useQuery } from '../../lib/query.js';67const m = await ready();8const BASES = [2, 3, 4, 5, 6, 7, 8, 9, 10];9const TOP = 64;1011const fixed = (value, places) => (Number.isFinite(value) ? value.toFixed(places) : 'none');12const full = (base) => (1 << base) - 1;13const setOf = (mask, base) => Array.from({ length: base }, (_, d) => d).filter((d) => mask & (1 << d));1415function span(rows) {16 let low = Infinity, high = -Infinity;17 for (const row of rows) for (const value of row) { if (value < low) low = value; if (value > high) high = value; }18 return [low, high];19}2021function Meter({ view }) {22 const draw = (canvas) => {23 const b = board(canvas, 260, { left: 44 });24 const { logx, meter, echo, rest, read, split } = view;25 const drawn = split && read.sieve ? [meter, echo, rest] : [meter];26 const [low, high] = span(drawn);27 const reach = Math.max(high - low, 1e-9);28 const first = logx[0], last = logx[logx.length - 1];29 const wide = Math.max(last - first, 1e-9);30 const step = Math.max(1, Math.round(logx.length / 1400));31 const place = (series) => {32 const out = [];33 for (let i = 0; i < series.length; i += step) out.push([(logx[i] - first) / wide, (series[i] - low) / reach]);34 return out;35 };36 const ticks = [];37 const gap = Math.ceil(read.depth / 8);38 for (let j = 0; j <= read.depth; j += gap) {39 const at = (j * Math.log(read.base) - first) / wide;40 if (at >= 0 && at <= 1) ticks.push([at, `${read.base}^${j}`]);41 }42 axis(b, ticks, { wall: true });43 b.ctx.strokeStyle = ink.line;44 b.ctx.lineWidth = 1;45 b.ctx.beginPath();46 b.ctx.moveTo(b.x(0), b.y(-low / reach));47 b.ctx.lineTo(b.x(1), b.y(-low / reach));48 b.ctx.stroke();49 if (drawn.length > 1) {50 line(b, place(echo), ink.blue, { width: 1.2 });51 line(b, place(rest), ink.pink, { width: 1.2 });52 }53 line(b, place(meter), ink.yellow, { width: 1.4 });54 b.ctx.fillStyle = ink.dim;55 b.ctx.textAlign = 'right';56 b.ctx.fillText(fixed(high, 3), b.left - 6, b.y(1) + 4);57 b.ctx.fillText(fixed(low, 3), b.left - 6, b.y(0) + 4);58 b.ctx.textAlign = 'left';59 tag(b, 'the meter', ink.yellow);60 if (drawn.length > 1) {61 tag(b, 'the echo', ink.blue, 'left', b.x(0) + 76);62 tag(b, 'the residual', ink.pink, 'left', b.x(0) + 152);63 }64 tag(b, read.sieve ? '' : 'past the sieve cap, no echo here', ink.dim, 'right');65 };66 return <Sketch className="bars" draw={draw} deps={[view]} role="img" aria-label="The scaled design Mobius meter against the log of x" />;67}6869function Spectrum({ view }) {70 const draw = (canvas) => {71 const b = board(canvas, 260, { left: 44 });72 const { gamma, score, read } = view;73 const at = (value) => value / TOP;74 let peak = 10;75 for (let i = 0; i < gamma.length; i += 1) if (gamma[i] > read.band[0] && gamma[i] < read.band[1] && score[i] > peak) peak = score[i];76 const roof = Math.log10(peak);77 const lift = (value) => Math.log10(Math.max(value, 1)) / roof;78 rules(b, read.lattice.filter((g) => g < TOP).map(at), { color: ink.pink, dash: [3, 4] });79 rules(b, read.zeros.filter((g) => g < TOP).map(at), { color: ink.yellow });80 const points = [];81 for (let i = 0; i < gamma.length && gamma[i] <= TOP; i += 1) points.push([at(gamma[i]), lift(score[i])]);82 axis(b, [0, 10, 20, 30, 40, 50, 60].map((g) => [at(g), String(g)]), { wall: true });83 line(b, [[0, lift(read.threshold)], [1, lift(read.threshold)]], ink.line, { width: 1, dash: [2, 4] });84 line(b, points, ink.blue, { width: 1.2 });85 b.ctx.fillStyle = ink.dim;86 b.ctx.textAlign = 'right';87 b.ctx.fillText(fixed(peak, 1), b.left - 6, b.y(1) + 4);88 b.ctx.fillText('1', b.left - 6, b.y(0) + 4);89 b.ctx.textAlign = 'left';90 tag(b, 'the zeta ordinates', ink.yellow);91 tag(b, `the pole lattice 2 pi j / log ${read.base}`, ink.pink, 'right');92 };93 return <Sketch className="bars" draw={draw} deps={[view]} role="img" aria-label="The power spectrum of the meter against the zeta ordinates and the pole lattice" />;94}9596function App() {97 const [pick, set] = useQuery({ base: 10, mask: 511, depth: 5, split: true, sub: false });9899 let error = null;100 let view = null;101 let caps = null;102 let depth = pick.depth;103 try {104 caps = JSON.parse(m.echo_caps(pick.base, pick.mask));105 depth = Math.min(Math.max(pick.depth, caps.least), caps.deepest);106 const got = m.echo_read(pick.base, pick.mask, depth, pick.sub);107 view = {108 logx: got.logx,109 meter: got.meter,110 echo: got.echo,111 rest: got.rest,112 gamma: got.gamma,113 score: got.score,114 read: JSON.parse(got.read),115 split: pick.split,116 };117 } catch (fault) {118 error = fault;119 }120121 const read = view?.read;122 const digits = setOf(pick.mask, pick.base);123 const toggle = (d) => {124 const next = pick.mask ^ (1 << d);125 if (setOf(next, pick.base).filter((v) => v > 0).length < 1 || setOf(next, pick.base).length < 2) return;126 set({ mask: next });127 };128 const rebase = (base) => {129 const kept = pick.mask & full(base);130 const ok = setOf(kept, base).length >= 2 && setOf(kept, base).some((d) => d > 0);131 set({ base, mask: ok ? kept : full(base) });132 };133134 const controls = (135 <>136 <section>137 <h3>The design</h3>138 <Row>139 <Pick label="base" value={pick.base} options={BASES.map((q) => [q, q])} onChange={(v) => rebase(+v)} />140 <Btn on={pick.mask === full(pick.base)} onClick={() => set({ mask: full(pick.base) })}>full set</Btn>141 </Row>142 <Row>143 {Array.from({ length: pick.base }, (_, d) => (144 <Check key={d} label={String(d)} checked={(pick.mask & (1 << d)) !== 0} onChange={() => toggle(d)} />145 ))}146 </Row>147 </section>148 <section>149 <h3>The depth</h3>150 <Row>151 <Slider label="digits" value={depth} min={caps?.least ?? 3} max={caps?.deepest ?? 3} onChange={(v) => set({ depth: v })} />152 </Row>153 </section>154 <section>155 <h3>The split</h3>156 <Row>157 <Check label="echo and residual" checked={pick.split} onChange={(v) => set({ split: v })} />158 <Check label="subtract the echo" checked={pick.sub} onChange={(v) => set({ sub: v })} />159 </Row>160 </section>161 </>162 );163164 return (165 <Page crumb="echo" title="The meter that echoes the zeros"166 sub="The Mobius meter of a digit design, read uniformly in log x, oscillates at the ordinates of the Riemann zeta zeros and never at the design's own pole lattice. It is the classical Mertens function heard through the design's density: split the meter into that echo and a residual, and the zeros leave with the echo. The echo dies against the meter's own yardstick at x to the minus half of the smaller of alpha and one less alpha, so the deeper the read the quieter it gets."167 controls={controls}168 foot={<>Every number here is computed in Rust and the page only draws. The elements of <code>S_F</code> are the whole numbers whose digits in the base all lie in the set, the meter is <code>M_F(x)</code>, the sum of <code>mu(n)</code> over those elements up to <code>x</code>, and the yardstick is <code>x^(alpha/2)</code> at <code>alpha = log_base fill</code>. The spectrum is that series resampled on 4096 points uniform in <code>log x</code>, mean-removed, Hann-windowed and read as <code>gamma = 2 pi j</code> over the log range against a 101-bin running median floor, so a peak is a power over its own neighbourhood and the threshold is 8. Resolution comes from the log range and not from the element count, so the bin is <code>2 pi / (level log base)</code> and a browser that stops at <code>base^level < 2^27</code> stops at a bin near a third. Read the hit counts against the chance rates beside them: with 13 ordinates and 20 lattice lines in the band a peak lands on one by luck often enough that a single peak proves nothing, which is why the full set is here as the control and the residual as the null. The echo needs the Mobius values of every whole number up to <code>base^level</code>, so it is refused past <code>2^24</code> and the page says so. The lab reads the same designs four digits deeper, where base 3 <code>{'{0, 1}'}</code> carries ten peaks and its pole lattice scores below its own null; at the depth a browser affords, that design is still under the floor and the base-10 designs are not. The critical line itself is <a href="../zeta">zeta</a>, the Mertens sum against the square root is <a href="/wiki/mertens-function/">the Mertens function page</a>, and the mathematics is on <a href="/research/mobius/">the Mobius page</a>.</>}>169 <div className="panel">170 <h2>The meter <span>M_F(x) over x^(alpha/2), drawn against log x</span></h2>171 {view && <Meter view={view} />}172 <Stats>173 <Stat label="alpha">{read && fixed(read.alpha, 6)}</Stat>174 <Stat label="digits">{`{${digits.join(', ')}}`}</Stat>175 <Stat label="elements A_F">{read?.count}</Stat>176 <Stat label="M_F">{read?.last}</Stat>177 <Stat label="max |M_F|">{read?.peak}</Stat>178 <Stat label="thetamax">{read && fixed(read.theta, 4)}</Stat>179 <Stat label="echo share">{read && (read.share === null ? 'past the cap' : fixed(read.share, 4))}</Stat>180 <Stat label="residual share">{read && (read.residual === null ? 'past the cap' : fixed(read.residual, 4))}</Stat>181 <Stat label="decay rate">{read && `x^${fixed(read.rate, 6)}`}</Stat>182 </Stats>183 </div>184 <div className="panel" style={{ marginTop: 22 }}>185 <h2>The spectrum <span>{pick.sub ? 'the residual, echo taken out' : 'the meter'} over its local median floor, yellow the zeta ordinates, pink the pole lattice</span></h2>186 {view && <Spectrum view={view} />}187 <Stats>188 <Stat label="log range">{read && fixed(read.span, 4)}</Stat>189 <Stat label="bin">{read && fixed(read.bin, 4)}</Stat>190 <Stat label="peaks over 8">{read?.found}</Stat>191 <Stat label="at a zeta zero">{read && `${read.hits} of ${read.peaks.length}`}</Stat>192 <Stat label="by chance">{read && fixed(read.chance, 3)}</Stat>193 <Stat label="at the lattice">{read && `${read.lines} of ${read.peaks.length}`}</Stat>194 <Stat label="by chance">{read && fixed(read.chanceLines, 3)}</Stat>195 </Stats>196 <div className="ribbon tight">197 {read?.peaks.map((row) => (198 <span key={row.gamma}>199 <i>{fixed(row.gamma, 3)}</i>200 <b className={row.zeta <= read.bin ? 'yellow' : undefined}>{fixed(row.score, 1)}</b>201 <i>{`zeta ${fixed(row.zeta, 3)}`}</i>202 <i>{`lattice ${fixed(row.lattice, 3)}`}</i>203 </span>204 ))}205 {read?.peaks.length === 0 && <span><i>no peak clears the floor at this depth</i></span>}206 </div>207 </div>208 <Note error={error}>{caps && `depth ${caps.least} to ${caps.deepest} here; the echo is sieved through depth ${caps.sieved}, past which base^level leaves 2^24.`}</Note>209 </Page>210 );211}212213mount(<App />);