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