index.jsx
13.1 kB · jsx · 234 lines
1import { useMemo, useState } from 'react';2import { ready, ink } from '../../lib/mrly.js';3import { mount, Page, Group, Pick, Slider, Text, Check, Btn, Stats, Stat, Note } from '../../lib/app.jsx';4import { Grid } from '../../lib/draw.jsx';5import { useSeeds, roll } from '../../lib/select.jsx';6import { useQuery } from '../../lib/query.js';78const m = await ready();910const FIRST = { rule: 110, seed: 'cell', density: 0.35, steps: 128, wrap: false };11const RUNS = [[64, 64], [128, 128], [256, 256], [512, 512]];12const SEEDS = [['cell', 'a single live cell'], ['soup', 'a random soup']];13const SIDE = 64;1415const IDENTITY =16 'Wolfram 1983 numbers a rule by the byte it writes on the eight neighbourhoods, bit `4l + 2c + r` of `N` being the output on `(l, c, r)`. The tree numbers a three-dimensional design by the byte it writes on the eight corners of the cube, bit `i` set when corner `i` is filled, `i = 4 x0 + 2 x1 + x2`. Put `(x0, x1, x2) = (l, c, r)` and the two bytes are the same byte: rule `N` is the design `bang dim 3, code N`, bit for bit, with no translation step. So every invariant of a design is already an invariant of a rule: popcount, the `GF(2)` degree, the genus, the fill fraction that Langton 1990 calls `lambda`. Proved, research/automata.md.';1718const CLASSES =19 'Two groups act on the same byte and they are not the same group. Wolfram equivalence is reflection with conjugation, order 4, and it is a symmetry of the line, so it preserves the dynamics. The cube group is the 48 signed axis permutations of the design, and it is not: a permutation that is not the reflection moves the centre cell off the centre. The two meet exactly in the reflection, so the reductions to 88 classes and to 22 classes are two branches and not a chain. Reversibility survives the cube group anyway, constant on all 22 classes; surjectivity does not, mixed on exactly one class, the 24 rules of the orbit of 30. Proved by witness, research/automata.md.';2021const ADDITIVE =22 'Where the rule is affine over `GF(2)` the diagram from one seed is a plane design of the same tree, one dimension down. Rule 60 is `x_i + x_(i-1)`, the binomial recurrence mod 2, so read rightward from the seed its rows are Pascal mod 2, which is `bang dim 2, code 13`; rule 102 is the same rule mirrored and reads leftward as `bang dim 2, code 14`; rule 90 is `x + x^-1`, and in the sheared frame `j = (t + i)/2` it is `bang dim 2, code 13` again. Each is the unique match among the fill-3 codes 7, 11, 13, 14. The comparison below is recomputed live, cell for cell, against the crate design renderer rather than against a picture.';2324const attempt = (fn) => {25 try {26 return { ...fn(), error: null };27 } catch (error) {28 return { error };29 }30};3132const plain = (grid) => ({ width: grid.width, height: grid.height, types: grid.types });3334const lit = (grid) => {35 let count = 0;36 for (const bit of grid.types) count += bit;37 return count;38};3940const single = (width) => {41 const row = new Uint8Array(width);42 row[(width - 1) / 2] = 1;43 return row;44};4546// THE SHEARED FRAME4748const DEPTH = 2 * (SIDE - 1);4950const READS = { 60: (t, j) => j, 102: (t, j) => j - (SIDE - 1), 90: (t, j) => 2 * j - t };5152const FRAMES = { 60: 'read rightward from the seed', 102: 'read leftward from the seed, the left half of the cone laid out left to right', 90: 'read in the sheared frame j = (t + i)/2' };5354function frame(rule, grid) {55 const centre = (grid.width - 1) / 2;56 const read = READS[rule];57 const types = new Uint8Array(SIDE * SIDE);58 for (let t = 0; t < SIDE; t++) {59 for (let j = 0; j < SIDE; j++) types[t * SIDE + j] = grid.types[t * grid.width + centre + read(t, j)];60 }61 return { width: SIDE, height: SIDE, types };62}6364function App() {65 const seeds = useSeeds();66 const [pick, set] = useQuery(FIRST);67 const [tap, setTap] = useState(0);6869 const rule = Math.max(0, Math.min(255, Math.round(pick.rule) || 0));70 const steps = RUNS.some(([value]) => value === pick.steps) ? pick.steps : 128;71 const width = 2 * steps + 1;72 const turn = (by) => set({ rule: (rule + by + 256) % 256 });7374 const run = useMemo(() => attempt(() => {75 if (pick.seed === 'soup') {76 const row = m.eca_soup(width, pick.density, seeds.get());77 return { grid: plain(m.eca_history(row, rule, steps, pick.wrap)), how: `a soup of ${width} cells at density ${pick.density}` };78 }79 if (pick.wrap) return { grid: plain(m.eca_history(single(width), rule, steps, true)), how: `one live cell on a ring of ${width}` };80 return { grid: plain(m.eca_seed(rule, steps)), how: `one live cell, padded by ${steps} and cropped back to ${width}` };81 }), [rule, steps, pick.seed, pick.density, pick.wrap, tap]);8283 const card = useMemo(() => attempt(() => {84 const read = JSON.parse(m.eca_card(rule));85 const mates = read.b3_orbit.map((mate) => ({ rule: mate, surjective: JSON.parse(m.eca_card(mate)).surjective }));86 return { read, mates, mixed: mates.some((mate) => mate.surjective) && mates.some((mate) => !mate.surjective) };87 }), [rule]);8889 const gasket = useMemo(() => attempt(() => {90 const name = JSON.parse(m.eca_card(rule)).gasket;91 if (!name) return { name: null };92 const code = name.match(/code (\d+)/)[1];93 const design = plain(m.two_grid(code, 2, 6, 0, 2));94 const window = frame(rule, plain(m.eca_seed(rule, DEPTH)));95 let faults = 0;96 for (let at = 0; at < design.types.length; at++) if (design.types[at] !== window.types[at]) faults += 1;97 return { name, code, design, window, faults };98 }), [rule]);99100 const chip = (mate, badge) => (101 <span key={mate} role="button" tabIndex={0} onClick={() => set({ rule: mate })}102 onKeyDown={(event) => { if (event.key === 'Enter') set({ rule: mate }); }}103 style={mate === rule ? { borderColor: 'var(--accent)', color: 'var(--fg)' } : undefined}>104 <b>{mate}</b>{badge ? <i>{badge}</i> : null}105 </span>106 );107108 const stamp = (corners) => (109 <Grid grid={{ width: 4, height: 2, types: Uint8Array.from(corners) }} on={ink.yellow} className=""110 style={{ width: 128, height: 64, borderRadius: 4, imageRendering: 'pixelated', background: 'var(--art)' }}111 role="img" aria-label="the eight corner bits of the rule" />112 );113114 const controls = (115 <>116 <Group name="The rule">117 <Text label="rule 0 to 255" value={String(rule)} onChange={(value) => set({ rule: Math.max(0, Math.min(255, +value.replace(/\D/g, '') || 0)) })} />118 <Btn onClick={() => turn(-1)}>Prev</Btn>119 <Btn onClick={() => turn(1)}>Next</Btn>120 <Btn onClick={() => set({ rule: roll(seeds.next(), [[0, 255]])[0] })}>Randomize</Btn>121 </Group>122 <Group name="The seed">123 <Pick label="seed" value={pick.seed} options={SEEDS} onChange={(value) => set({ seed: value })} />124 <Slider label="soup density" value={pick.density} min={0.05} max={0.95} step={0.01} onChange={(value) => set({ density: value })} />125 <Btn onClick={() => { seeds.next(); setTap(tap + 1); }}>New soup</Btn>126 <Check label="wrap" checked={pick.wrap} onChange={(value) => set({ wrap: value })} />127 </Group>128 <Group name="The run">129 <Pick label="generations" value={steps} options={RUNS} onChange={(value) => set({ steps: +value })} />130 </Group>131 </>132 );133134 const read = card.read;135136 return (137 <Page crumb="wolfram" title="The 256 rules are the 256 cube designs"138 sub="An elementary cellular automaton writes one byte on the eight neighbourhoods of a cell, and a three-dimensional parity design writes one byte on the eight corners of a cube. They are the same byte. Pick a rule and it arrives with a design's card already filled in, and where the rule is additive its own diagram is a plane design of the same tree."139 controls={controls}140 foot={<>Every bit, count and class on this page is computed in the crates and walked through wasm; the page only draws. The single-cell view runs the stable convention, a line padded by the depth and cropped back to the light cone, so the boundary never reaches the window; turning wrap on runs the same seed on a ring instead. Nearby: <a href="../life">life</a> runs birth and survival rules read from named sequences, <a href="../mrlylife">mrlylife</a> runs the same rules on any mask the tree draws, <a href="../universe">the universe</a> is the design gallery these bytes come from. The class lattice, the surjectivity census and the additive bridge are in <a href="/research/automata/">the automata note</a>.</>}>141142 <div className="panel">143 <h2>the space-time diagram <span>time runs down, one row a generation</span></h2>144 <Note error={run.error} />145 {run.error ? null : <Grid grid={run.grid} on={ink.yellow} role="img" aria-label={`Rule ${rule} from ${run.how}`} />}146 {run.error ? null : (147 <Stats>148 <Stat label="rule">{rule}</Stat>149 <Stat label="design">{card.error ? '-' : read.name}</Stat>150 <Stat label="rows">{run.grid.height}</Stat>151 <Stat label="cells across">{run.grid.width}</Stat>152 <Stat label="live cells">{lit(run.grid)}</Stat>153 <span className="dim">{run.how}</span>154 </Stats>155 )}156 <p className="sub">{IDENTITY}</p>157 </div>158159 <div className="arena">160 <div className="panel">161 <h2>the card <span>the design's invariants, read off the same byte</span></h2>162 <Note error={card.error} />163 {card.error ? null : (164 <>165 {stamp(read.corners)}166 <Stats>167 <span className="dim">corner <b>i = 4 x0 + 2 x1 + x2</b>, so the top row is <b>l = 0</b> and the columns run <b>00, 01, 10, 11</b> in <b>(c, r)</b></span>168 <Stat label="name">{read.name}</Stat>169 <Stat label="popcount">{read.popcount}</Stat>170 <Stat label="lambda">{read.lambda.toFixed(3)}</Stat>171 <Stat label="GF(2) degree">{read.degree}</Stat>172 <Stat label="genus">{read.genus}</Stat>173 <span className={`chip ${read.affine ? 'proved' : ''}`}>{read.affine ? 'affine over GF(2)' : 'not affine'}</span>174 <span className={`chip ${read.surjective ? 'verified' : ''}`}>{read.surjective ? 'surjective' : 'not surjective'}</span>175 <span className={`chip ${read.reversible ? 'verified' : ''}`}>{read.reversible ? 'reversible' : 'not reversible'}</span>176 <Stat label="as a birth and survival rule">177 {read.outer_totalistic ? `B${read.outer_totalistic.birth.join('')}/S${read.outer_totalistic.survive.join('')}` : 'not a B/S rule'}178 </Stat>179 </Stats>180 <p className="sub">Exactly 30 of the 256 rules are surjective and exactly 6 are reversible, the degree-one single-axis designs 15, 51, 85, 170, 204, 240 and nothing else. Verified, research/automata.md.</p>181 </>182 )}183 </div>184185 <div className="panel">186 <h2>the classes <span>the cube orbit beside the Wolfram class</span></h2>187 <Note error={card.error} />188 {card.error ? null : (189 <>190 <h2>the cube orbit <span>{read.b3_orbit.length} rules, representative {read.b3_rep}</span></h2>191 <div className="ribbon tight">{card.mates.map((mate) => chip(mate.rule, mate.surjective ? 'onto' : ''))}</div>192 <h2>the Wolfram class <span>{read.wolfram_class.length} rules, representative {read.wolfram_rep}</span></h2>193 <div className="ribbon tight">{read.wolfram_class.map((mate) => chip(mate, ''))}</div>194 <Stats>195 <Stat label="NPN representative">{read.npn_rep}</Stat>196 <span className="dim">a chip is a link, click one to run it</span>197 {card.mixed198 ? <span className="chip refuted">surjectivity is not constant on this orbit, so the cube symmetry is not a dynamical one</span>199 : <span className="chip verified">every rule in this orbit answers surjectivity the same way</span>}200 </Stats>201 <p className="sub">{CLASSES}</p>202 </>203 )}204 </div>205 </div>206207 {gasket.error || !gasket.name ? null : (208 <div className="panel">209 <h2>the gasket <span>{FRAMES[rule]}, against the design the card names</span></h2>210 <div className="arena">211 <div>212 <Grid grid={gasket.window} on={ink.yellow} role="img" aria-label="the diagram cropped to 64 rows" />213 <div className="stats"><span>the diagram, {SIDE} rows</span></div>214 </div>215 <div>216 <Grid grid={gasket.design} on={ink.blue} role="img" aria-label={`the design ${gasket.name} at level 6`} />217 <div className="stats"><span>{gasket.name}, level 6</span></div>218 </div>219 </div>220 <Stats>221 <Stat label="cells compared">{SIDE * SIDE}</Stat>222 <Stat label="cells that differ">{gasket.faults}</Stat>223 <span className={`chip ${gasket.faults === 0 ? 'proved' : 'refuted'}`}>{gasket.faults === 0 ? 'the same grid, cell for cell' : 'two grids'}</span>224 </Stats>225 <p className="sub">{ADDITIVE}</p>226 </div>227 )}228229 <Note error={gasket.error} />230 </Page>231 );232}233234mount(<App />);