index.jsx

12.8 kB · jsx · 243 lines

1import { useMemo } from 'react';2import { ready, ink } from '../../../lib/mrly.js';3import { board, axis, rules, tag } from '../../../lib/chart.js';4import { mount, Page, Group, Pick, Slider, Stats, Stat, Note } from '../../../lib/app.jsx';5import { Sketch } from '../../../lib/draw.jsx';6import { useQuery } from '../../../lib/query.js';78const m = await ready();9const LOW = 6;10const HIGH = 16;11const CELLS = 720;12const LEAST = 48;13const STEPS = 1000;14const FLOOR = 0.7;15const FIRST = { level: 16, x: 14348906, focus: 12766858, zoom: 16, three: 15, four: 12 };1617const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, Math.round(v)));18const whole = (n) => n.toLocaleString('en');19const down = (v) => (Math.floor(v * 1e6) / 1e6).toFixed(6);20const up = (v) => (Math.ceil(v * 1e6) / 1e6).toFixed(6);21const key = (row) => `${row.three},${row.four}`;22const kind = (row) => (row.clean ? 'clean' : row.copy ? 'gap copy' : 'mixed');2324const attempt = (fn) => {25  try {26    return { value: fn(), error: null };27  } catch (error) {28    return { value: null, error };29  }30};3132function paint(b, fills) {33  const { ctx } = b;34  const n = fills.length;35  const step = b.wide / n;36  const gap = step >= 6 ? 1 : step >= 3 ? 0.5 : 0;37  ctx.fillStyle = ink.deep;38  ctx.fillRect(b.x(0), b.roof, b.wide, b.tall);39  ctx.fillStyle = ink.blue;40  fills.forEach((f, i) => {41    if (!f) return;42    ctx.globalAlpha = f;43    ctx.fillRect(b.x(i / n) + gap / 2, b.roof, Math.max(step - gap, 0.5), b.tall);44  });45  ctx.globalAlpha = 1;46}4748function App() {49  const [pick, set] = useQuery(FIRST);50  const level = clamp(pick.level, LOW, HIGH);51  const top = 3 ** level;52  const x = clamp(pick.x, 1, top);53  const zmax = Math.max(0, Math.floor(Math.log2((x + 1) / LEAST)));54  const zoom = clamp(pick.zoom, 0, zmax);55  const span = Math.max(Math.min(LEAST, x + 1), Math.floor((x + 1) / 2 ** zoom));56  const focus = clamp(pick.focus, 0, x);57  const lo = clamp(focus - Math.floor(span / 2), 0, x + 1 - span);58  const hi = lo + span;5960  const read = useMemo(() => attempt(() => JSON.parse(m.sumset_read(level, x))), [level, x]);61  const envelope = useMemo(() => attempt(() => m.sumset_envelope(level, CELLS)), [level]);62  const census = useMemo(() => attempt(() => JSON.parse(m.sumset_pairs(level))), [level]);63  const pairs = census.value ?? [];64  const chosen = pairs.find((row) => row.three === pick.three && row.four === pick.four);65  const here = read.value;66  const gaps = pairs.filter((row) => row.gap);6768  const choose = (row) => set({ three: row.three, four: row.four, x: row.gap ? row.gap[1] : row.largest, focus: row.largest });69  const logx = (f) => clamp(Math.exp(Math.max(0, Math.min(1, f)) * Math.log(top)), 1, top);7071  const overview = (canvas) => {72    const b = board(canvas, 64, { top: 10, bottom: 10 });73    paint(b, m.sumset_strip(level, 0, x + 1, Math.max(1, Math.min(x + 1, Math.floor(b.wide)))));74    const a = b.x(lo / (x + 1)), c = b.x(hi / (x + 1));75    b.ctx.strokeStyle = ink.yellow;76    b.ctx.lineWidth = 2;77    b.ctx.strokeRect(a - 1, b.roof - 5, Math.max(2, c - a) + 2, b.tall + 10);78  };7980  const closeup = (canvas) => {81    const b = board(canvas, 76, { top: 8, bottom: 24 });82    paint(b, m.sumset_strip(level, lo, hi, Math.max(1, Math.min(span, Math.floor(b.wide)))));83    axis(b, [[0, whole(lo)], [1, whole(hi - 1)]]);84  };8586  const density = (canvas) => {87    const b = board(canvas, 320, { top: 24, bottom: 24 });88    const { ctx } = b;89    const fx = (v) => Math.log(v) / Math.log(top);90    const fy = (v) => (v - FLOOR) / (1 - FLOOR);91    for (const row of gaps) {92      const a = b.x(fx(row.gap[0])), c = b.x(fx(row.gap[1] + 1));93      ctx.fillStyle = row === chosen ? ink.yellow : ink.line;94      ctx.globalAlpha = row === chosen ? 0.35 : 1;95      ctx.fillRect(a, b.roof, Math.max(1.5, c - a), b.tall);96    }97    ctx.globalAlpha = 1;98    ctx.strokeStyle = ink.line;99    ctx.fillStyle = ink.dim;100    ctx.textAlign = 'right';101    for (const v of [0.8, 0.9, 1]) {102      ctx.beginPath();103      ctx.moveTo(b.x(0), b.y(fy(v)));104      ctx.lineTo(b.x(1), b.y(fy(v)));105      ctx.stroke();106      ctx.fillText(v.toFixed(1), b.x(1) - 2, b.y(fy(v)) - 3);107    }108    ctx.textAlign = 'left';109    const band = envelope.value;110    if (band) {111      const w = b.wide / CELLS;112      ctx.fillStyle = ink.blue;113      for (let i = 0; i < CELLS; i++) {114        const low = band[2 * i], high = band[2 * i + 1];115        if (Number.isNaN(low)) continue;116        const y0 = b.y(fy(high)), y1 = b.y(fy(Math.max(low, FLOOR)));117        ctx.fillRect(b.x(i / CELLS), y0, Math.max(1, w + 0.3), Math.max(1.5, y1 - y0));118      }119    }120    rules(b, [fx(x)], { color: ink.pink, width: 1.5 });121    if (here) {122      ctx.fillStyle = ink.pink;123      ctx.beginPath();124      ctx.arc(b.x(fx(x)), b.y(fy(Math.max(here.density, FLOOR))), 4, 0, Math.PI * 2);125      ctx.fill();126    }127    const marks = [];128    for (let k = 4; k < level; k += 4) marks.push([k / level, `3^${k}`]);129    axis(b, [[0, '1'], ...marks, [1, `3^${level}`]]);130    const edge = tag(b, 'D(x), least to greatest over each pixel', ink.blue);131    tag(b, 'gaps', ink.dim, 'left', edge + 14);132  };133134  const energy = (canvas) => {135    const b = board(canvas, 240, { top: 24, bottom: 24 });136    const { ctx } = b;137    const n = pairs.length;138    if (!n) return;139    const ceiling = Math.ceil(Math.max(...pairs.map((row) => row.ratio)) * 4 + 1) / 4;140    const fx = (i) => (i + 0.5) / n;141    const fy = (q) => (q - 1) / (ceiling - 1);142    ctx.strokeStyle = ink.line;143    ctx.fillStyle = ink.dim;144    for (let q = 1; q <= ceiling; q += 0.5) {145      ctx.beginPath();146      ctx.moveTo(b.x(0), b.y(fy(q)));147      ctx.lineTo(b.x(1), b.y(fy(q)));148      ctx.stroke();149      if (q < ceiling) ctx.fillText(q.toFixed(1), b.x(0), b.y(fy(q)) - 3);150    }151    pairs.forEach((row, i) => {152      const px = b.x(fx(i)), py = b.y(fy(row.ratio));153      ctx.beginPath();154      ctx.arc(px, py, row === chosen ? 6 : 4.5, 0, Math.PI * 2);155      if (row.copy) {156        ctx.strokeStyle = ink.dim;157        ctx.lineWidth = 1.5;158        ctx.stroke();159      } else {160        ctx.fillStyle = row.clean ? ink.yellow : ink.blue;161        ctx.fill();162      }163      if (row === chosen) {164        ctx.strokeStyle = ink.fg;165        ctx.lineWidth = 1.5;166        ctx.beginPath();167        ctx.arc(px, py, 9, 0, Math.PI * 2);168        ctx.stroke();169      }170    });171    axis(b, [[fx(0), `k = ${pairs[0].three}`], [fx(n - 1), `k = ${pairs[n - 1].three}`]]);172    const one = tag(b, 'clean', ink.yellow);173    const two = tag(b, 'mixed', ink.blue, 'left', one + 12);174    tag(b, 'gap copy', ink.dim, 'left', two + 12);175  };176177  const controls = (178    <>179      <Group name="Height">180        <Slider label="level" value={level} min={LOW} max={HIGH} show={`3^${level}`} onChange={(v) => set({ level: v, x: Math.min(x, 3 ** v), focus: Math.min(focus, 3 ** v) })} />181      </Group>182      <Group name="Cursor">183        <Slider label="x" value={Math.round((STEPS * Math.log(x)) / Math.log(top))} min={0} max={STEPS} show={whole(x)} onChange={(v) => set({ x: logx(v / STEPS) })} />184        <Pick label="gap" value={chosen?.gap ? key(chosen) : ''} options={[['', 'jump to a gap'], ...gaps.map((row) => [key(row), `(${row.three}, ${row.four}) at ${row.scale.toFixed(3)}`])]}185          onChange={(v) => { const row = gaps.find((r) => key(r) === v); if (row) choose(row); }} />186      </Group>187      <Group name="Zoom">188        <Slider label="zoom" value={zoom} min={0} max={zmax} show={`${whole(span)} integers`} onChange={(v) => set({ zoom: v })} />189      </Group>190    </>191  );192193  return (194    <Page crumb="sumset" title="Three plus four: Erdos problem 125"195      sub={<>Add a number whose base-3 digits are all <code>0</code> or <code>1</code> to one whose base-4 digits are. Just below each near meeting of a power of 3 and a power of 4 the sums <code>S</code> miss a whole run of integers, so the share <code>D(x)</code> of lit integers up to <code>x</code> dips, and along ever closer meetings its lower limit is 0. Whether it returns above one fixed share at arbitrarily large <code>x</code> is open. Jump to a gap, zoom into the strip and watch.</>}196      controls={controls}197      foot={<>The strip is one bit per integer up to <code>3^{level}</code>, built in Rust: the members of <code>A</code> set directly, then each power of 4 folded in by one shift-or pass. Each strip pixel is shaded by the share of lit integers it covers, and the density chart draws the least and the greatest <code>D(x)</code> over each pixel of <code>log x</code>, so every dip shows at its true depth. The energy <code>E(k, m)</code> is summed over the <code>3^m</code> digit strings a difference in <code>B_m</code> can be, each weighted by <code>2</code> to the zero digits it has in base 4 and in balanced ternary. Every count, density, gap and energy comes out of the crates through wasm; the page only draws. The proofs and the census to <code>3^22</code> are on <a href="/research/cobham/">two bases</a>, section Object S.</>}>198      <p className="banner"><span className="chip conjecture">Open</span> Is the upper density of <code>S</code> positive: does <code>D(x)</code> return above one fixed share at arbitrarily large <code>x</code>?</p>199      <p className="sub"><span className="chip proved">Proved</span> No sum lands in the gap <code>(d, min(3^k, 4^m))</code>, <code>d = (3^k - 1)/2 + (4^m - 1)/3</code> the largest sum of <code>A_k + B_m</code>. Where <code>4^m/3^k</code> is near 1 that gap is a sixth of the scale, and iterated along ever closer coincidences it drives the lower density to 0, the answer to the question Erdos asked, checked in Lean on the problem page.</p>200      <div className="panel">201        <h2>S up to x <span>{`${whole(x + 1)} integers from 0, the yellow box the window below`}</span></h2>202        <Sketch draw={overview} deps={[level, x, lo, hi]} onSeek={(f) => set({ focus: clamp(f * x, 0, x) })} role="img" aria-label="The sumset up to x as a strip, each pixel shaded by the share of members" />203        <h2>the window <span>{`${whole(lo)} to ${whole(hi - 1)}, one cell an integer once they fit`}</span></h2>204        <Sketch draw={closeup} deps={[level, lo, hi]} role="img" aria-label="A window of the sumset, lit integers in blue" />205        {here && (206          <Stats>207            <Stat label="x">{whole(x)}</Stat>208            <Stat label="card(S meet [1, x])">{whole(here.count)}</Stat>209            <Stat label="D(x)">{down(here.density)}</Stat>210            <Stat label="x in S">{here.member ? 'yes' : 'no'}</Stat>211          </Stats>212        )}213        <Note error={read.error ?? envelope.error ?? census.error} />214      </div>215      <div className="arena">216        <div className="panel">217          <h2>the density <span>{`D(x) on a log scale from 1 to 3^${level}`}</span></h2>218          <Sketch draw={density} deps={[level, x, envelope.value, chosen]} onSeek={(f) => set({ x: logx(f) })} role="img" aria-label="The density of the sumset against log x, dipping at every shaded gap" />219          <p className="sub">Each grey band is a gap <code>(d, min(3^k, 4^m))</code>, open when <code>4^m/3^k</code> lies between about <code>3/4</code> and <code>3/2</code>. <code>D</code> falls through the band and bottoms out just below <code>min(3^k, 4^m)</code>; what it does after the last band drawn is the open question. Click to move <code>x</code>.</p>220        </div>221        <div className="panel">222          <h2>the energy ratio <span>{`Q(k, m) at ${pairs.length} pairs from k = 6`}</span></h2>223          <Sketch draw={energy} deps={[pairs, chosen]} onSeek={(f) => pairs.length && choose(pairs[clamp(f * pairs.length - 0.5, 0, pairs.length - 1)])} role="img" aria-label="The energy ratio Q at every pair of levels" />224          <p className="sub"><code>Q(k, m) = E(k, m) (d + 1)/4^(k+m)</code> weighs how often two sums of <code>A_k + B_m</code> collide against a flat spread, and Cauchy-Schwarz gives <code>card(A_k + B_m) &gt;= (d + 1)/Q</code>. <span className="chip proved">Proved</span> <code>Q</code> is unbounded, yet for every <code>eps &gt; 0</code> it is below <code>3^(eps k)</code> at every large <code>k</code> and every <code>m</code> with <code>1/3 &lt;= 4^m/3^k &lt; 4</code>, the window every pair here sits in. <span className="chip conjecture">Conjecture</span> <code>Q</code> stays bounded along one infinite chain of pairs, which would put the upper density at least <code>1/Q</code>. Click a dot to pick its pair.</p>225          {chosen && (226            <Stats>227              <Stat label="pair">{`(${chosen.three}, ${chosen.four}), ${kind(chosen)}`}</Stat>228              <Stat label="4^m/3^k">{up(chosen.scale)}</Stat>229              <Stat label="d">{whole(chosen.largest)}</Stat>230              <Stat label="gap">{chosen.gap ? `${whole(chosen.gap[0])} to ${whole(chosen.gap[1])}` : 'none'}</Stat>231              <Stat label="E">{chosen.energy}</Stat>232              <Stat label="Q">{up(chosen.ratio)}</Stat>233              <Stat label="1/Q">{down(chosen.bound)}</Stat>234              <Stat label="fill to d">{down(chosen.fill)}</Stat>235            </Stats>236          )}237        </div>238      </div>239    </Page>240  );241}242243mount(<App />);