index.jsx
12.2 kB · jsx · 297 lines
1import { useEffect, useMemo, useRef, useState } from 'react';2import { ready, ink, fit } from '../../lib/mrly.js';3import { stamp, useQuery } from '../../lib/query.js';4import { mount, Page, Row, Slider, Btn, Stats, Stat, Note } from '../../lib/app.jsx';5import { Sketch } from '../../lib/draw.jsx';6import { useSeeds, roll } from '../../lib/select.jsx';7import { board, line, axis, tag } from '../../lib/chart.js';89const m = await ready();10const REACH = 200;11const DENSITY = 30;12const LISTED = 8;13const FIRST = { t: 30, speed: 4, zeros: 10, x: 100 };14const SPANS = [[10, 150], [1, 60], [50, 500]];15const ZEROS = m.zeta_zeros(100);16const [JOIN, SEAM] = m.zeta_seam(REACH, 1000);1718const fixed = (v) => (typeof v === 'number' ? v.toFixed(4) : '');1920function drawn(seed) {21 if (!seed) return FIRST;22 const [t, zeros, x] = roll(seed, SPANS);23 return { ...FIRST, t, zeros, x };24}2526function App() {27 const s = useSeeds();28 const first = useRef(null);29 first.current ??= drawn(s.get());30 const [pick, set] = useQuery(first.current);31 const [head, setHead] = useState(pick.t);32 const [playing, setPlaying] = useState(false);33 const [error, setError] = useState(null);34 const at = useRef(pick.t);35 const now = useRef(pick);36 const path = useRef(null);37 path.current ??= m.zeta_line(0, pick.t, Math.max(1, Math.ceil(pick.t * DENSITY)));38 now.current = pick;3940 const settle = () => stamp({ t: at.current.toFixed(2), speed: now.current.speed, zeros: now.current.zeros, x: now.current.x });4142 const trace = (to) => {43 at.current = to;44 setHead(to);45 try {46 path.current = m.zeta_line(0, to, Math.max(1, Math.ceil(to * DENSITY)));47 setError(null);48 } catch (error) {49 setError(error);50 }51 };5253 useEffect(() => settle(), []);5455 useEffect(() => {56 if (!playing) return;57 let live = true;58 let last = 0;59 const frame = (clock) => {60 if (!live) return;61 requestAnimationFrame(frame);62 const dt = last ? Math.min(0.1, (clock - last) / 1000) : 0;63 last = clock;64 if (!dt) return;65 const to = Math.min(REACH, at.current + now.current.speed * dt);66 try {67 const grown = m.zeta_line(at.current, to, Math.max(1, Math.ceil((to - at.current) * DENSITY)));68 const longer = new Float64Array(path.current.length + grown.length - 4);69 longer.set(path.current);70 longer.set(grown.subarray(4), path.current.length);71 path.current = longer;72 at.current = to;73 setHead(to);74 } catch (error) {75 live = false;76 setError(error);77 setPlaying(false);78 settle();79 return;80 }81 if (to >= REACH) {82 live = false;83 setPlaying(false);84 settle();85 }86 };87 requestAnimationFrame(frame);88 return () => { live = false; };89 }, [playing]);9091 const look = useMemo(() => {92 try {93 const [re, im, z, theta] = m.zeta_at(head);94 return { re, im, z, theta, count: m.zeta_count(head), error: null };95 } catch (error) {96 return { count: 0, error };97 }98 }, [head]);99100 const fold = useMemo(() => {101 try {102 return {103 stair: m.psi_stair(pick.x),104 some: m.psi_formula(pick.x, ZEROS.subarray(0, pick.zeros), 500),105 none: m.psi_formula(pick.x, ZEROS.subarray(0, 0), 500),106 gap: m.psi_gap(pick.x, ZEROS.subarray(0, pick.zeros)),107 error: null,108 };109 } catch (error) {110 return { error };111 }112 }, [pick.x, pick.zeros]);113114 const walk = (canvas) => {115 const [ctx, w, h] = fit(canvas, canvas.clientWidth);116 ctx.fillStyle = ink.deep;117 ctx.fillRect(0, 0, w, h);118 let reach = 2;119 for (let k = 0; k < path.current.length; k += 4) reach = Math.max(reach, Math.abs(path.current[k + 1]), Math.abs(path.current[k + 2]));120 const scale = (Math.min(w, h) / 2 - 14) / reach;121 const px = (re) => w / 2 + re * scale, py = (im) => h / 2 - im * scale;122 ctx.strokeStyle = ink.line;123 ctx.lineWidth = 1;124 ctx.beginPath();125 ctx.moveTo(0, py(0));126 ctx.lineTo(w, py(0));127 ctx.moveTo(px(0), 0);128 ctx.lineTo(px(0), h);129 ctx.stroke();130 ctx.setLineDash([3, 5]);131 ctx.beginPath();132 ctx.arc(px(0), py(0), scale, 0, Math.PI * 2);133 ctx.stroke();134 ctx.setLineDash([]);135 const draw = (from, color, width) => {136 ctx.strokeStyle = color;137 ctx.lineWidth = width;138 ctx.beginPath();139 for (let k = from; k < path.current.length; k += 4) {140 if (k === from) ctx.moveTo(px(path.current[k + 1]), py(path.current[k + 2]));141 else ctx.lineTo(px(path.current[k + 1]), py(path.current[k + 2]));142 }143 ctx.stroke();144 };145 if (path.current.length) {146 draw(0, ink.blue, 1.2);147 draw(Math.max(0, path.current.length - 4 * 2 * DENSITY), ink.fg, 2);148 }149 ctx.strokeStyle = ink.yellow;150 ctx.lineWidth = 2;151 ctx.beginPath();152 ctx.arc(px(0), py(0), 5, 0, Math.PI * 2);153 ctx.stroke();154 if (path.current.length) {155 ctx.fillStyle = ink.orange;156 ctx.beginPath();157 ctx.arc(px(path.current.at(-3)), py(path.current.at(-2)), 4, 0, Math.PI * 2);158 ctx.fill();159 }160 const mono = getComputedStyle(document.body).getPropertyValue('--mono');161 ctx.font = `11px ${mono}`;162 ctx.fillStyle = ink.dim;163 ctx.fillText('1', px(1) + 4, py(0) - 5);164 ctx.fillText('i', px(0) + 5, py(1) - 4);165 };166167 const chart = (canvas) => {168 const b = board(canvas, 220);169 const span = Math.max(head, 1);170 let peak = 1e-9;171 for (let k = 3; k < path.current.length; k += 4) peak = Math.max(peak, Math.abs(path.current[k]));172 b.ctx.strokeStyle = ink.line;173 b.ctx.beginPath();174 b.ctx.moveTo(b.x(0), b.y(0.5));175 b.ctx.lineTo(b.x(1), b.y(0.5));176 b.ctx.stroke();177 b.ctx.strokeStyle = ink.yellow;178 for (const zero of ZEROS) {179 if (zero > head) break;180 b.ctx.beginPath();181 b.ctx.moveTo(b.x(zero / span), b.floor);182 b.ctx.lineTo(b.x(zero / span), b.floor - 10);183 b.ctx.stroke();184 }185 const points = [];186 for (let k = 0; k < path.current.length; k += 4) points.push([path.current[k] / span, 0.5 + 0.5 * path.current[k + 3] / peak]);187 if (points.length > 1) line(b, points, ink.blue);188 axis(b, [[0, '0'], [1, `t = ${head.toFixed(2)}`]]);189 tag(b, 'Z(t), real on the line', ink.blue);190 tag(b, `${look.count} ${look.count === 1 ? 'zero' : 'zeros'}`, ink.yellow, 'right');191 };192193 const stairs = (canvas) => {194 if (fold.error) return;195 const b = board(canvas, 260);196 const x = pick.x, k = pick.zeros;197 let peak = fold.stair.at(-1);198 for (let i = 1; i < fold.some.length; i += 2) peak = Math.max(peak, fold.some[i]);199 peak *= 1.04;200 const fx = (u) => (u - 1) / (x - 1);201 const steps = [];202 for (let n = 1; n <= x; n++) {203 steps.push([fx(n), fold.stair[n - 1] / peak]);204 if (n < x) steps.push([fx(n + 1), fold.stair[n - 1] / peak]);205 }206 const curve = (flat) => {207 const pts = [];208 for (let i = 0; i < flat.length; i += 2) pts.push([fx(flat[i]), flat[i + 1] / peak]);209 return pts;210 };211 b.ctx.save();212 b.ctx.beginPath();213 b.ctx.rect(b.left, b.roof - 4, b.wide, b.tall + 4);214 b.ctx.clip();215 line(b, curve(fold.none), ink.pink, { dash: [4, 4], width: 1 });216 line(b, curve(fold.some), ink.blue);217 line(b, steps, ink.yellow, { width: 2 });218 b.ctx.restore();219 axis(b, [[0, '1'], [1, `x = ${x}`]]);220 let spot = tag(b, 'psi(x)', ink.yellow);221 spot = tag(b, `formula with ${k} ${k === 1 ? 'zero' : 'zeros'}`, ink.blue, 'left', spot + 14);222 tag(b, 'no zeros', ink.pink, 'left', spot + 14);223 };224225 const play = () => {226 if (playing) {227 setPlaying(false);228 settle();229 return;230 }231 if (at.current >= REACH) trace(0);232 setPlaying(true);233 };234235 const shuffle = () => {236 setPlaying(false);237 const [t, zeros, x] = roll(s.next(), SPANS);238 set({ zeros, x });239 trace(t);240 stamp({ t: t.toFixed(2), speed: now.current.speed });241 };242243 const controls = (244 <Row>245 <Slider label="speed" value={pick.speed} min={1} max={20} onChange={(v) => set({ speed: v })} />246 <Slider label="zeros in the formula" value={pick.zeros} min={0} max={100} onChange={(v) => set({ zeros: v })} />247 <Slider label="x" value={pick.x} min={10} max={1000} onChange={(v) => set({ x: v })} />248 <Btn onClick={shuffle}>Randomize</Btn>249 </Row>250 );251252 const next = ZEROS.find((zero) => zero > head);253 const list = Array.from(ZEROS.subarray(0, LISTED), (zero, i) => `${String(i + 1).padStart(2)} ${zero.toFixed(6)}${zero <= head ? ' passed' : ''}`).join('\n');254255 return (256 <Page crumb="zeta" title="Walking the critical line"257 sub={<>The zeta function is a curve you can walk. Put <code>s = 1/2 + it</code> and let <code>t</code> grow: the point loops around the plane and every so often passes straight through the origin, one zero per pass. Those zeros know where the primes are: the staircase below counts the prime powers, and adding the zeros one by one folds a smooth guess into it.</>}258 foot={<>Two engines share the line. Below <code>t = {JOIN}</code> every point is the complex Euler-Maclaurin sum, <code>t</code> plus ten terms closed by seven Bernoulli corrections, good to ten decimals; above it Z(t) comes from the Riemann-Siegel formula, the main sum of <code>floor(sqrt(t / 2pi))</code> cosines and the first four correction terms, with the kernel derivatives taken by central differences, and <code>zeta = Z e^(-i theta)</code>. On this page the two engines never differ by more than {SEAM.toExponential(1)} in Z beyond the join, so the seam is invisible. <code>theta(t)</code> is the argument of <code>Gamma(1/4 + it/2)</code> less <code>t ln(pi) / 2</code>, by Stirling's series after a shift of ten. The zeros are sign changes of Z between Gram points, refined by bisection on the Euler-Maclaurin engine to a billionth, so the six decimals listed are exact; the count below <code>t</code> is the same scan. <code>psi(x)</code> is exact: the sieve adds <code>ln p</code> at every prime power. The blue curve is the von Mangoldt explicit formula <code>x - sum x^rho / rho - ln 2pi - ln(1 - x^-2) / 2</code> cut off at the chosen zeros, each paired with its mirror; the pink curve keeps none of them. At a jump the full formula lands on the midpoint, and more zeros sharpen every step. The zeros come back to the page as numbers, so both curves are Rust; the page only draws. The same primes are sieved on <a href="../primes">primes</a>.</>}259 controls={controls}>260 <div className="arena">261 <div className="panel">262 <h2>The walk <span>{`t = ${head.toFixed(2)}, ${look.count} ${look.count === 1 ? 'pass' : 'passes'} through the origin`}</span></h2>263 <Sketch draw={walk} deps={[head]} role="img" aria-label="The walk, zeta traced in the plane as t grows" />264 <Row>265 <Slider label="t" value={head} min={0} max={REACH} step={0.05} show={head.toFixed(2)} onChange={(v) => { trace(v); settle(); }} />266 <Btn onClick={play}>{playing ? 'Pause' : 'Play'}</Btn>267 </Row>268 </div>269 <div className="panel">270 <h2>Z(t) <span>the signed distance from the origin, zeros in yellow</span></h2>271 <Sketch className="bars" draw={chart} deps={[head]} role="img" aria-label="Z of t, the signed distance from the origin, zeros in yellow" />272 <pre>{list}</pre>273 </div>274 </div>275 <div className="arena">276 <div className="panel">277 <h2>The prime staircase <span>{`${pick.zeros} of ${ZEROS.length} zeros folded in`}</span></h2>278 <Sketch className="bars" draw={stairs} deps={[fold]} role="img" aria-label="The prime staircase against the explicit formula" />279 </div>280 </div>281 <Stats>282 <Stat label="re">{fixed(look.re)}</Stat>283 <Stat label="im">{fixed(look.im)}</Stat>284 <Stat label="Z(t)">{fixed(look.z)}</Stat>285 <Stat label="theta">{fixed(look.theta)}</Stat>286 <Stat label="zeros below t">{look.count}</Stat>287 <Stat label="next zero">{next === undefined ? 'past the list' : next.toFixed(6)}</Stat>288 <Stat label="psi(x)">{fold.error ? '' : fold.stair.at(-1).toFixed(4)}</Stat>289 <Stat label="formula">{fold.error ? '' : fold.some.at(-1).toFixed(4)}</Stat>290 <Stat label="gap">{fold.error ? '' : fold.gap.toFixed(4)}</Stat>291 </Stats>292 <Note error={error ?? look.error ?? fold.error} />293 </Page>294 );295}296297mount(<App />);