index.jsx
11.1 kB · jsx · 202 lines
1import { useMemo } from 'react';2import { ready } from '../../lib/mrly.js';3import { useQuery } from '../../lib/query.js';4import { mount, Page, Row, Pick, Slider, Check, Text, Btn, Stats, Stat, Note, Group } from '../../lib/app.jsx';5import { Picker, useSeeds } from '../../lib/select.jsx';6import { Pins, Ratios, Differences, Digits, Terms } from '../../lib/series.jsx';78const m = await ready();9const BUDGET = '500000';10const DEPTH = 6;11const HEADS = 3;12const LABELS = '4000';13const DIMS = [1, 2, 3, 4];14const BASES = [2, 3, 4, 5];15const MEASURES = [...m.ledger_measures()];16const OPS = [...m.blend_ops()];17const PAIRED = ['add', 'sub', 'hadamard', 'cauchy'];18const VIEWS = [['pins', 'pin plot'], ['log', 'log plot'], ['ratios', 'ratios'], ['differences', 'difference triangle'], ['digits', 'digit heatmap']];1920function listed(dimension, base) {21 try {22 return [...m.ledger_designs(dimension, base)];23 } catch {24 return null;25 }26}2728const SPACES = new Map(DIMS.map((d) => [d, BASES.filter((b) => listed(d, b))]));2930function gallery(dimension) {31 try {32 m.universe(dimension);33 return true;34 } catch {35 return false;36 }37}3839const GALLERY = DIMS.filter(gallery);4041function digits(base) {42 return [...new Set([2, 3, 10, base])].sort((a, b) => a - b);43}4445function shown(value) {46 return value === null || value === undefined ? 'none' : Number(value).toFixed(6);47}4849function View({ row, view, dig, label, name }) {50 if (view === 'ratios') return <Ratios values={row.ratios} start={row.start} label={`${label}, each term against the one before`} role="img" aria-label={`${name}, each term against the one before`} />;51 if (view === 'differences') return <Differences rows={row.differences} label={`${label}, each row the differences of the row above`} role="img" aria-label={`${name}, the difference triangle`} />;52 if (view === 'digits') return <Digits terms={row.terms} start={row.start} base={dig} label={label} role="img" aria-label={`${name}, a heatmap of the digits of every term`} />;53 return <Pins terms={row.terms} start={row.start} log={view === 'log'} label={label} role="img" aria-label={`${name}, every term standing on its index`} />;54}5556function Rule({ row }) {57 return (58 <Stats>59 <Stat label="order">{row.order ?? 'none fits'}</Stat>60 <Stat label="rule">{row.recurrence || 'no linear rule fits these terms'}</Stat>61 <Stat label="characteristic">{row.polynomial || 'none'}</Stat>62 <Stat label="largest positive real root">{shown(row.root)}</Stat>63 <Stat label="growth">{shown(row.growth)}</Stat>64 <Stat label="exponent">{shown(row.exponent)}</Stat>65 <Stat label="read from">{row.growth_from}</Stat>66 </Stats>67 );68}6970function App() {71 const s = useSeeds();72 const [pick, set] = useQuery({73 code: '7', dimension: 2, base: 2, measure: 'surface', axis: 'level', count: 12,74 view: 'pins', dig: 2, on: false, op: 'hadamard', arg: 2, scode: '3', smeasure: 'fills',75 });7677 const move = (patch) => {78 const dimension = patch.dimension ?? pick.dimension;79 const wanted = patch.base ?? pick.base;80 const bases = SPACES.get(dimension) ?? [];81 const base = bases.includes(wanted) ? wanted : bases[0] ?? wanted;82 const list = listed(dimension, base) ?? [];83 const keep = (code) => {84 try {85 m.name_of(code, dimension, base);86 return code;87 } catch {88 return list.at(-1) ?? code;89 }90 };91 s.drop();92 set({ ...patch, base, code: keep(pick.code.trim()), scode: keep(pick.scode) });93 };9495 const read = useMemo(() => {96 try {97 return { row: JSON.parse(m.blend_series(pick.code.trim(), pick.dimension, pick.base, pick.measure, pick.axis, pick.count, BUDGET, DEPTH)), error: null };98 } catch (error) {99 return { row: null, error };100 }101 }, [pick.code, pick.dimension, pick.base, pick.measure, pick.axis, pick.count]);102103 const family = useMemo(() => {104 try {105 return JSON.parse(m.blend_family(pick.dimension, pick.base, pick.smeasure, pick.axis, HEADS, LABELS));106 } catch {107 return [];108 }109 }, [pick.dimension, pick.base, pick.smeasure, pick.axis]);110111 const blend = useMemo(() => {112 if (!pick.on || !read.row) return { row: null, mate: null, error: null };113 try {114 const mate = JSON.parse(m.blend_series(pick.scode.trim(), pick.dimension, pick.base, pick.smeasure, pick.axis, pick.count, BUDGET, DEPTH));115 return { row: JSON.parse(m.blend_mix(read.row.terms, mate.terms, pick.op, pick.arg, DEPTH)), mate, error: null };116 } catch (error) {117 return { row: null, mate: null, error };118 }119 }, [pick.on, read.row, pick.scode, pick.smeasure, pick.op, pick.arg]);120121 const row = read.row;122 const label = row ? `${row.axis === 'level' ? 'level L' : 'side k'} from ${row.start}` : '';123 const mates = family.map((one) => [one.code, `${one.code} · ${one.terms.join(', ') || 'no terms'}`]);124 const options = family.some((one) => one.code === pick.scode) ? mates : [[pick.scode, pick.scode], ...mates];125126 const controls = (127 <>128 <Group name="The sequence">129 {GALLERY.includes(pick.dimension) ? <Picker dimension={pick.dimension} bases={[pick.base]} code={pick.code} seeds={s} onChange={set} /> : (130 <span className="set">131 <Text label="code" value={pick.code} onChange={(v) => { s.drop(); set({ code: v }); }} />132 <Btn onClick={() => set({ code: m.random_code(pick.dimension, pick.base, s.next()) })}>Randomize</Btn>133 </span>134 )}135 <Pick label="dimension" value={pick.dimension} options={DIMS} onChange={(v) => move({ dimension: +v })} />136 <Pick label="base" value={pick.base} options={SPACES.get(pick.dimension) ?? [pick.base]} onChange={(v) => move({ base: +v })} />137 <Pick label="measure" value={pick.measure} options={MEASURES} onChange={(v) => set({ measure: v })} />138 <Pick label="axis" value={pick.axis} options={[['level', 'level L'], ['side', 'odd side 2k - 1']]} onChange={(v) => set({ axis: v })} />139 <Slider label="terms" value={pick.count} min={4} max={32} onChange={(v) => set({ count: v })} />140 </Group>141 <Group name="The view">142 <Pick label="view" value={pick.view} options={VIEWS} onChange={(v) => set({ view: v })} />143 {pick.view === 'digits' && <Pick label="digit base" value={pick.dig} options={digits(pick.base)} onChange={(v) => set({ dig: +v })} />}144 </Group>145 <Group name="The mix">146 <Check label="mix a second sequence" checked={pick.on} onChange={(v) => set({ on: v })} />147 {pick.on && (148 <span className="set">149 <Pick label="second design" value={pick.scode} options={options} onChange={(v) => set({ scode: v })} />150 <Pick label="its measure" value={pick.smeasure} options={MEASURES} onChange={(v) => set({ smeasure: v })} />151 <Pick label="operation" value={pick.op} options={OPS} onChange={(v) => set({ op: v })} />152 <Slider label="argument" value={pick.arg} min={-8} max={12} onChange={(v) => set({ arg: v })} />153 </span>154 )}155 </Group>156 </>157 );158159 return (160 <Page crumb="plot" title="Every sequence the designs write, drawn"161 sub="The ledger lists these sequences; this page draws one. Pick a design, a measure and an axis and the terms stand up as a pin plot, a log plot, a ratio strip, a difference triangle or a heatmap of their digits, with the linear recurrence they satisfy read out beneath. Then mix in a second sequence and watch the rule the blend inherits."162 foot={<>A sequence is one design, one measure and one axis, the same key the <a href="../sequences">ledger</a> holds, so a row there links straight into this page. The pin plot stands every term on its index and the log plot lifts the tall ones back into view; the ratio strip colours each term against the one before, the difference triangle takes differences until the rows run out, and the heatmap spells every term in a chosen base. Under the drawing is the rule: the smallest linear constant-coefficient recurrence every term satisfies, hunted modulo three primes, rebuilt as exact fractions and verified on every term before it is shown, with its monic characteristic polynomial and the largest real root of that polynomial. The growth is that root where a rule fits and a least-squares slope of the log terms over the tail where none does, a fit and not a rule, and the exponent is the decades a term gains per step, which is the slope the log plot draws. Order and growth are honest only to the terms in hand: a rule needs twice its order plus two terms to be trusted, so a short read finds no rule where a longer one would. The mixer takes a second key from the same space and axis and runs a term operation over the two, adding, subtracting, multiplying term by term or convolving them, or dropping, thinning, differencing, summing and scaling the first alone; the mixed sequence carries its own rule, because sequences satisfying linear recurrences are closed under every one of these. The registry behind both pages is written up in <a href="/research/sequences/">the sequences note</a>, and their formal census, with the fill law and the exposed-face recurrence, is the <a href="/papers/sequence-census/">sequence-census paper</a>. Every number on this page is computed in Rust; the page only draws.</>}163 controls={controls}>164 <div className={pick.on ? 'arena' : undefined}>165 <div className="panel">166 <h2>The sequence <span>{row ? row.name : 'nothing read'}</span></h2>167 {row && <View row={row} view={pick.view} dig={pick.dig} label={label} name={`The sequence ${row.name}`} />}168 {row && <Terms terms={row.terms} start={row.start} capped={row.capped} />}169 {row && (170 <Stats>171 <Stat label="closed form">{row.closed || 'none known'}</Stat>172 <Stat label="record">{row.oeis ? <a href={`https://oeis.org/${row.oeis}`} target="_blank" rel="noopener">{row.oeis}</a> : 'none'}</Stat>173 <Stat label="status">{row.tag || 'unmatched'}</Stat>174 <Stat label="terms">{row.capped ? `${row.terms.length}, to the budget` : row.terms.length}</Stat>175 </Stats>176 )}177 {row && <Rule row={row} />}178 <Note error={read.error} />179 </div>180 {pick.on && (181 <div className="panel">182 <h2>The mix <span>{blend.mate ? `${pick.op} of this and ${blend.mate.name}` : pick.op}</span></h2>183 {blend.row && <View row={blend.row} view={pick.view} dig={pick.dig} label={`${PAIRED.includes(pick.op) ? 'both sequences' : 'the first sequence'}, index from 0`} name={`The mix, the ${pick.op} of the two sequences`} />}184 {blend.row && <Terms terms={blend.row.terms} />}185 {blend.mate && (186 <Stats>187 <Stat label="second">{blend.mate.name}</Stat>188 <Stat label="its closed form">{blend.mate.closed || 'none known'}</Stat>189 <Stat label="its growth">{shown(blend.mate.growth)}</Stat>190 <Stat label="argument">{PAIRED.includes(pick.op) ? 'unused' : pick.arg}</Stat>191 </Stats>192 )}193 {blend.row && <Rule row={blend.row} />}194 <Note error={blend.error} />195 </div>196 )}197 </div>198 </Page>199 );200}201202mount(<App />);