Hello all, new to the forums but not new to CGOL. Recently made this tool to explore some questions I was working out, it didn't quite work out the way I thought but it turned out to be interesting anyhow. When I was done, thought I would share it with the one community that might find it interesting. Right now the chirality lives in the tag field, not in the macroscopic geometry, I will iterate it some more but since I don't know how to code I had to have an AI do it for me with my guidance and instruction so no guarantees on how good the coding itself is, but I did run it and it seems to do the things I needed it to do.
s I might be retreading ground y'all have tread. The name was chosen early in the process reflecting what I was looking at/for in the first place.
Chiral Life is ordinary Conway Life (B3/S23) with an additional algebraic tag channel. Live cells carry tags from one of three algebras:
Binary (±1) — commutative and associative control
Quaternion group Q₈ — associative but non-commutative
Octonions — non-commutative and non-associative
On birth (exactly three live neighbours) the new cell’s tag is the ordered product of its three parents’ tags. Two parent-ordering conventions are supported:Lab-frame: absolute atan2 ordering (coordinate-dependent) Cyclic geometric: intrinsic cyclic order obtained by cutting at the unique largest angular gap (falls back to lab-frame only on rare ties)
The central experiment tests whether the full dynamical system (geometry + tags) is equivariant under spatial symmetry
S:E(S(X)) =? S(E(X))E(S(X))\ \stackrel{?}{=}\ S(E(X))E(S(X))\ \stackrel{?}{=}\ S(E(X))
where S is either vertical reflection or 90° rotation. The original configuration and its transformed copy evolve independently. Spatial mismatch (occupancy) and tag mismatch are measured separately at every generation.Key observations from the interactive explorer and the automated 64-run matrix (100 generations each):Binary remains fully equivariant under both symmetries and both orderings.
Non-commutative algebras produce tag mismatches, often on the first relevant birth.
Purely geometric observables (population, bounding-box area, live-cell support) stay synchronized; the chirality lives in the tag field.
Cyclic ordering reduces or eliminates some lab-frame artefacts, especially under rotation.
An interactive single-file HTML demo is available that includes live entropy/MI read-outs, separate spatial vs tag mismatch metrics, and a one-click experiment matrix with JSON export.Forum post body (copy-paste ready):
Chiral Life – algebraic tags on B3/S23 and a clean equivariance test
Ordinary Life controls occupancy. A separate tag channel lives in
binary / Q8 / octonions. Births take the ordered product of the three
parents.
Two ordering modes:
• lab-frame (absolute atan2)
• cyclic geometric (largest angular gap defines the cut)
We test E(S(X)) ?= S(E(X)) for S = reflection or 90° rotation,
measuring spatial mismatch and tag mismatch independently.
Binary stays equivariant (control). Non-commutative algebras break
tag equivariance, often at generation 1, while the underlying geometry
remains perfectly synchronized. Cyclic ordering removes some
coordinate artefacts that appear with pure atan2.
Single-file interactive demo + automated 64-run matrix (JSON export)
attached / linked below.
Happy to hear thoughts, alternative orderings, or ideas for letting
the tags feed back into the Life rule.
(You can attach the HTML or host it somewhere and link it.)2.
Small code tweaks (recommended before posting)These are low-risk polish items:
Surface cyclic ambiguities in the summary in summarizeResults, after the per-group counts, add something like:js
const totalAmb = results.reduce((s,r) => s + (r.cyclicAmbiguities||0), 0);
lines.push(`Total cyclic-order ambiguities across all runs: ${totalAmb}`);
Slightly friendlier status text while the matrix runs
Change the running message to include a rough progress hint or just keep it as-is (it is already clear).
Optional “quick” matrix
Add a second button or a parameter so users can run 30 generations instead of 100 if they want a faster preview. Not essential.
Guard against empty results in the copy button
Already present — good.
Tiny numerical stability
In the cyclic gap comparison you already use 1e-12; that is fine.
No correctness bugs stood out on review. The separation of spatial vs tag mismatch and the restore-after-matrix logic are solid.3. Interpreting the first full set of resultsOnce you hit “run experiments” you should see a clear pattern. Here is the expected signature (based on the mathematics and our earlier Python probes):Expected patternAlgebra
Ordering
Symmetry
Spatial mismatch
Tag mismatch
Notes
Binary
any
any
none
none
Control
Quaternion
lab
reflect
none / rare
early (often gen 1)
Orientation reversal
Quaternion
lab
rot90
none / rare
early–moderate
Lab-frame artefact possible
Quaternion
cyclic
reflect
none
early
Genuine chirality
Quaternion
cyclic
rot90
none
later or fewer
Cyclic should be closer to equivariant
Octonion
either
either
none
early + possible later re-agreements
Non-associativity adds extra variation
How to read the numbersfirstSpatialMismatch === null (or very late) on almost every run confirms the geometric rule itself is equivariant.
firstTagMismatch early for Q8/octonion + reflection is the main positive signal of algebraic chirality.
Lower tag-mismatch rate (or later onset) under cyclic ordering, especially for rotation, shows that the lab-frame branch cut was contributing artificial effects.
finalMI dropping below the individual entropies quantifies the permanent loss of correspondence between a configuration and its transform.
Non-zero cyclicAmbiguities should be rare; if a particular pattern shows many, that geometry has frequent tied gaps.
Code: Select all
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Chiral Life Explorer — Symmetry Experiment</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
background: #1a1a1a;
color: #e8e8e6;
margin: 0;
padding: 24px;
}
.wrap {
max-width: 900px;
margin: 0 auto;
}
h1 {
font-size: 20px;
font-weight: 500;
margin-bottom: 4px;
}
.sub {
color: #999;
font-size: 13px;
line-height: 1.5;
margin-bottom: 20px;
}
.controls {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 16px;
flex-wrap: wrap;
}
select,
button {
background: #2a2a2a;
color: #e8e8e6;
border: 1px solid #444;
border-radius: 6px;
padding: 8px 12px;
font-size: 13px;
cursor: pointer;
}
button:hover,
select:hover {
background: #333;
}
button.active {
background: #3a5a8a;
border-color: #4a7ac0;
}
button.run {
background: #315f3b;
border-color: #4c8b58;
}
canvas {
width: 100%;
aspect-ratio: 1.5;
background: #0f0f0f;
border-radius: 8px;
display: block;
border: 1px solid #333;
}
.info {
display: flex;
gap: 16px;
margin-top: 10px;
font-size: 12px;
color: #999;
flex-wrap: wrap;
}
.period {
margin-left: auto;
font-weight: 500;
color: #7bc47f;
}
.legend {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin-top: 10px;
font-size: 11px;
}
.swatch {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 6px;
border-radius: 4px;
background: #2a2a2a;
}
.dot {
width: 8px;
height: 8px;
border-radius: 2px;
}
.note {
font-size: 12px;
color: #888;
margin-top: 16px;
line-height: 1.6;
padding: 12px;
background: #222;
border-radius: 6px;
}
.experiment {
margin-top: 20px;
background: #202020;
border: 1px solid #333;
border-radius: 8px;
padding: 14px;
}
.experiment h2 {
font-size: 14px;
font-weight: 500;
margin: 0 0 8px;
}
.experiment p {
color: #999;
font-size: 12px;
line-height: 1.5;
margin: 6px 0 12px;
}
.metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 8px;
}
.metric {
background: #292929;
border-radius: 6px;
padding: 9px;
}
.metric .label {
color: #888;
font-size: 10px;
text-transform: uppercase;
letter-spacing: .04em;
}
.metric .value {
color: #e8e8e6;
font-size: 15px;
margin-top: 3px;
}
#experimentStatus {
margin-top: 10px;
color: #7bc47f;
font-size: 12px;
}
#experimentOutput {
margin-top: 10px;
max-height: 360px;
overflow: auto;
background: #111;
border-radius: 6px;
padding: 10px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 10px;
line-height: 1.5;
color: #aaa;
white-space: pre;
}
</style>
</head>
<body>
<div class="wrap">
<h1>Chiral Life Explorer — Symmetry Experiment</h1>
<div class="sub">
Conway Life B3/S23 controls occupancy. A separate algebraic tag channel
assigns tags to births. The experiment tests whether evolution commutes
with spatial symmetry:
E(S(X)) =? S(E(X)).
</div>
<div class="controls">
<select id="algebra">
<option value="binary">Binary (GF(2))</option>
<option value="quaternion">Quaternion (Q8)</option>
<option value="octonion">Octonion</option>
</select>
<select id="pattern">
<option value="glider">Glider</option>
<option value="blinker">Blinker</option>
<option value="block">Block (still life)</option>
<option value="rpent">R-pentomino</option>
</select>
<select id="symmetry">
<option value="reflect">Reflection</option>
<option value="rot90">90° rotation</option>
</select>
<select id="ordering">
<option value="lab">Parent order: lab-frame</option>
<option value="cyclic">Parent order: cyclic geometric</option>
</select>
<button id="groupBtn" style="display:none;">
grouping: left
</button>
<button id="playBtn">play</button>
<button id="stepBtn">step</button>
<button id="resetBtn">reset</button> <button id="runBtn" class="run">
run experiments
</button>
<button id="copyBtn">
copy results
</button>
</div>
<canvas id="grid"></canvas><div class="info">
<span id="genLabel">gen 0</span> <span id="popLabel"></span> <span id="entropyLabel"></span> <span id="reflectionLabel"></span> <span class="period" id="periodLabel"></span></div>
<div class="legend" id="legend"></div>
<div style="margin-top:16px;">
<div style="
font-size:12px;
color:#999;
margin-bottom:6px;
">
Bounding-box area over time
</div>
<canvas
id="bboxChart"
style="
width:100%;
height:80px;
background:#0f0f0f;
border-radius:8px;
border:1px solid #333;
display:block;
" ></canvas>
<div
id="bboxLabel"
style="
font-size:12px;
color:#999;
margin-top:4px;
"
></div>
</div>
<div class="experiment">
<h2>Equivariance experiment</h2>
<p>
The original state and its transformed copy evolve independently.
At every generation we compare the transformed original against the
independently evolved transformed state. Spatial mismatch and tag
mismatch are measured separately.
</p>
<div class="metrics">
<div class="metric">
<div class="label">First mismatch</div>
<div class="value" id="firstMismatchMetric">none</div>
</div>
<div class="metric">
<div class="label">First spatial mismatch</div>
<div class="value" id="firstSpatialMetric">none</div>
</div>
<div class="metric">
<div class="label">First tag mismatch</div>
<div class="value" id="firstTagMetric">none</div>
</div>
<div class="metric">
<div class="label">Spatial mismatch now</div>
<div class="value" id="spatialMetric">0</div>
</div>
<div class="metric">
<div class="label">Tag mismatch now</div>
<div class="value" id="tagMetric">0</div>
</div>
<div class="metric">
<div class="label">Common live cells</div>
<div class="value" id="commonMetric">0</div>
</div> </div>
<div id="experimentStatus">
Ready.
</div>
<pre id="experimentOutput"></pre>
</div>
<div class="note" id="note"></div>
</div>
<script>
// ============================================================
// CHIRAL LIFE EXPLORER
// ============================================================
//
// Spatial dynamics:
// Conway Life B3/S23
//
// Tag dynamics:
// GF(2)
// Q8
// Octonions
//
// Main experimental question:
//
// E(S(X)) ?= S(E(X))
//
// where S is:
// reflection
// 90-degree rotation
//
// Two parent-ordering modes:
//
// lab:
// absolute atan2 ordering
//
// cyclic:
// intrinsic cyclic ordering of the three parent directions.
// The largest angular gap defines the cyclic cut.
//
// The distinction is important:
// a global atan2 branch cut can create apparent rotational effects
// in a noncommutative algebra that are artifacts of the coordinate
// convention rather than genuine spatial chirality.
//
// ============================================================
// ============================================================
// Quaternion arithmetic
// ============================================================
function qMul(q1,q2) {
const [a1,b1,c1,d1] = q1;
const [a2,b2,c2,d2] = q2;
return [
a1*a2 - b1*b2 - c1*c2 - d1*d2,
a1*b2 + b1*a2 + c1*d2 - d1*c2,
a1*c2 - b1*d2 + c1*a2 + d1*b2,
a1*d2 + b1*c2 - c1*b2 + d1*a2
];
}
function qConj(q) {
return [
q[0],
-q[1],
-q[2],
-q[3]
];
}
function qAdd(a,b) {
return a.map((x,i) => x+b);
}
function qSub(a,b) {
return a.map((x,i) => x-b);
}
function qEq(a,b) {
return a.every((x,i) => x === b);
}
const Q8 = {
'1': [1,0,0,0],
'-1': [-1,0,0,0],
'i': [0,1,0,0],
'-i': [0,-1,0,0],
'j': [0,0,1,0],
'-j': [0,0,-1,0],
'k': [0,0,0,1],
'-k': [0,0,0,-1]
};
const Q8_NAMES = Object.keys(Q8);
function qName(q) {
for (const [name,value] of Object.entries(Q8)) {
if (qEq(q,value)) {
return name;
}
}
return 'other';
}
// ============================================================
// Octonions — Cayley-Dickson doubling of quaternions
// ============================================================
function oMul(o1,o2) {
const [a,b] = o1;
const [c,d] = o2;
const real =
qSub(
qMul(a,c),
qMul(qConj(d),b)
);
const imag =
qAdd(
qMul(d,a),
qMul(b,qConj(c))
);
return [real,imag];
}
function oNeg(o) {
return [
o[0].map(x => -x),
o[1].map(x => -x)
];
}
function oEq(a,b) {
return (
a[0].every((x,i) => x === b[0]) &&
a[1].every((x,i) => x === b[1])
);
}
const ONE_Q = [1,0,0,0];
const ZERO_Q = [0,0,0,0];
function oBasis(idx) {
if (idx === 0) {
return [
ONE_Q.slice(),
ZERO_Q.slice()
];
}
if (idx <= 3) {
const q = [0,0,0,0];
q[idx] = 1;
return [
q,
ZERO_Q.slice()
];
}
if (idx === 4) {
return [
ZERO_Q.slice(),
ONE_Q.slice()
];
}
const q = [0,0,0,0];
q[idx-4] = 1;
return [
ZERO_Q.slice(),
q
];
}
const O_NAMES = [
'1',
'e1',
'e2',
'e3',
'e4',
'e5',
'e6',
'e7'
];
const O_BASIS =
O_NAMES.map((_,i) => oBasis(i));
function oName(o) {
for (let i=0;i<8;i++) {
if (oEq(o,O_BASIS)) {
return O_NAMES;
}
if (oEq(o,oNeg(O_BASIS))) {
return '-' + O_NAMES;
}
}
return 'other';
}
// ============================================================
// Algebra sanity checks
// ============================================================
console.assert(
qName(qMul(Q8.i,Q8.i)) === '-1',
'Q8: i*i = -1'
);
console.assert(
qName(qMul(Q8.i,Q8.j)) === 'k',
'Q8: i*j = k'
);
console.assert(
qName(qMul(Q8.j,Q8.i)) === '-k',
'Q8: j*i = -k'
);
console.assert(
oName(oMul(O_BASIS[1],O_BASIS[1])) === '-1',
'Octonion: e1*e1 = -1'
);
console.assert(
oName(oMul(O_BASIS[1],O_BASIS[2])) === 'e3',
'Octonion: e1*e2 = e3'
);
// ============================================================
// Global state
// ============================================================
const SIZE = 50;
let grid;
let tags;
let mirrorGrid;
let mirrorTags;
let gen = 0;
let playing = false;
let timer = null;
let algebra = 'binary';
let pattern = 'glider';
let grouping = 'left';
let symmetry = 'reflect';
let orderingMode = 'lab';
let seenConfigs;
let periodMsg = '';
let cycleStartGen = null;
let bboxHistory = [];
let firstMismatch = null;
let firstSpatialMismatch = null;
let firstTagMismatch = null;
let mismatchHistory = [];
let differenceHistory = [];
// ============================================================
// Grid helpers
// ============================================================
function makeGrid() {
return Array.from(
{length:SIZE},
() => new Array(SIZE).fill(0)
);
}
function cloneTag(t) {
if (algebra === 'binary') {
return t;
}
if (algebra === 'quaternion') {
return t.slice();
}
return [
t[0].slice(),
t[1].slice()
];
}
function cloneTags(t) {
const out = {};
for (const [key,value] of Object.entries(t)) {
out[key] = cloneTag(value);
}
return out;
}
// ============================================================
// Tag values
// ============================================================
function tagValue(name) {
if (algebra === 'binary') {
return name === '+' ? 1 : -1;
}
if (algebra === 'quaternion') {
return Q8[name].slice();
}
const negative =
name.startsWith('-');
const base =
negative
? name.slice(1)
: name;
const idx =
O_NAMES.indexOf(base);
if (idx < 0) {
throw new Error(
'Unknown octonion: ' + name
);
}
return negative
? oNeg(O_BASIS[idx])
: [
O_BASIS[idx][0].slice(),
O_BASIS[idx][1].slice()
];
}
// ============================================================
// Spatial transforms
// ============================================================
function reflectCoord(r,c) {
return [
r,
SIZE-1-c
];
}
// Clockwise 90 degrees.
function rot90Coord(r,c) {
return [
c,
SIZE-1-r
];
}
// Counterclockwise 90 degrees.
function rot90InvCoord(r,c) {
return [
SIZE-1-c,
r
];
}
function applyTransform(
sourceGrid,
sourceTags,
transformFn
) {
const g = makeGrid();
const t = {};
for (let r=0;r<SIZE;r++) {
for (let c=0;c<SIZE;c++) {
if (!sourceGrid[r][c]) {
continue;
}
const [
rr,
cc
] = transformFn(r,c);
g[rr][cc] = 1;
t[`${rr},${cc}`] =
cloneTag(
sourceTags[`${r},${c}`]
);
}
}
return {
grid:g,
tags:t
};
}
function transformState(
sourceGrid,
sourceTags
) {
if (symmetry === 'reflect') {
return applyTransform(
sourceGrid,
sourceTags,
reflectCoord
);
}
return applyTransform(
sourceGrid,
sourceTags,
rot90Coord
);
}
function inverseTransformState(
sourceGrid,
sourceTags
) {
if (symmetry === 'reflect') {
return applyTransform(
sourceGrid,
sourceTags,
reflectCoord
);
}
return applyTransform(
sourceGrid,
sourceTags,
rot90InvCoord
);
}
// ============================================================
// Pattern loading
// ============================================================
function loadPattern() {
gen = 0;
grid = makeGrid();
tags = {};
mirrorGrid = makeGrid();
mirrorTags = {};
seenConfigs = new Map();
periodMsg = '';
cycleStartGen = null;
firstMismatch = null;
firstSpatialMismatch = null;
firstTagMismatch = null;
mismatchHistory = [];
differenceHistory = [];
bboxHistory = [];
const ox = 15;
const oy = 15;
let cells;
let tagNames;
if (pattern === 'glider') {
cells = [
[0,1],
[1,2],
[2,0],
[2,1],
[2,2]
];
tagNames =
algebra === 'binary'
? ['+','+','-','+','-']
: algebra === 'quaternion'
? ['1','i','j','k','-1']
: ['1','e1','e2','e4','e6'];
}
else if (pattern === 'blinker') {
cells = [
[0,0],
[1,0],
[2,0]
];
tagNames =
algebra === 'binary'
? ['+','-','+']
: algebra === 'quaternion'
? ['i','j','k']
: ['e1','e2','e4'];
}
else if (pattern === 'block') {
cells = [
[0,0],
[0,1],
[1,0],
[1,1]
];
tagNames =
algebra === 'binary'
? ['+','-','+','-']
: algebra === 'quaternion'
? ['i','j','k','-1']
: ['e1','e2','e4','e7'];
}
else {
cells = [
[0,1],
[0,2],
[1,0],
[1,1],
[2,1]
];
tagNames =
algebra === 'binary'
? ['+','-','+','-','+']
: algebra === 'quaternion'
? ['1','i','j','k','-1']
: ['1','e1','e2','e4','e6'];
}
cells.forEach(
([dx,dy],i) => {
const r = ox + dx;
const c = oy + dy;
grid[r][c] = 1;
tags[`${r},${c}`] =
tagValue(
tagNames[
i % tagNames.length
]
);
}
);
const transformed =
transformState(
grid,
tags
);
mirrorGrid =
transformed.grid;
mirrorTags =
transformed.tags;
seenConfigs.set(
relConfig(grid,tags),
0
);
bboxHistory.push(
computeBBoxArea(grid)
);
const diff =
currentDifference();
differenceHistory.push(diff);
mismatchHistory.push(
isMismatch(diff)
? 1
: 0
);
}
// ============================================================
// Equality
// ============================================================
function tagsEqual(a,b) {
if (algebra === 'binary') {
return a === b;
}
if (algebra === 'quaternion') {
return qEq(a,b);
}
return oEq(a,b);
}
function statesEqual(
g1,t1,
g2,t2
) {
for (let r=0;r<SIZE;r++) {
for (let c=0;c<SIZE;c++) {
if (g1[r][c] !== g2[r][c]) {
return false;
}
if (g1[r][c]) {
if (
!tagsEqual(
t1[`${r},${c}`],
t2[`${r},${c}`]
)
) {
return false;
}
}
}
}
return true;
}
// ============================================================
// Quantitative state difference
// ============================================================
function stateDifference(
g1,t1,
g2,t2
) {
let spatialMismatch = 0;
let tagMismatch = 0;
let commonLive = 0;
let unionLive = 0;
for (let r=0;r<SIZE;r++) {
for (let c=0;c<SIZE;c++) {
const a =
g1[r][c] !== 0;
const b =
g2[r][c] !== 0;
if (a || b) {
unionLive++;
}
if (a !== b) {
spatialMismatch++;
continue;
}
if (a && b) {
commonLive++;
if (
!tagsEqual(
t1[`${r},${c}`],
t2[`${r},${c}`]
)
) {
tagMismatch++;
}
}
}
}
return {
spatialMismatch,
tagMismatch,
commonLive,
unionLive
};
}
function currentDifference() {
const transformedOriginal =
transformState(
grid,
tags
);
return stateDifference(
mirrorGrid,
mirrorTags,
transformedOriginal.grid,
transformedOriginal.tags
);
}
function isMismatch(diff) {
return (
diff.spatialMismatch > 0 ||
diff.tagMismatch > 0
);
}
// ============================================================
// Relative configuration / cycle detection
// ============================================================
function relConfig(
g=grid,
t=tags
) {
const cells = [];
for (let r=0;r<SIZE;r++) {
for (let c=0;c<SIZE;c++) {
if (g[r][c]) {
cells.push([r,c]);
}
}
}
if (!cells.length) {
return 'EXTINCT';
}
const minR =
Math.min(
...cells.map(x => x[0])
);
const minC =
Math.min(
...cells.map(x => x[1])
);
return cells
.map(([r,c]) => {
const value =
t[`${r},${c}`];
const serialized =
algebra === 'binary'
? String(value)
: JSON.stringify(value);
return (
`${r-minR},` +
`${c-minC},` +
`${serialized}`
);
})
.sort()
.join('|');
}
// ============================================================
// Parent geometry
// ============================================================
//
// parentInfo entries:
// [dr, dc, tag]
//
// The three parents occupy three of the eight Moore-neighbor
// directions.
//
// Two ordering conventions:
//
// 1. lab
// Absolute atan2 ordering.
// This is deliberately frame-dependent.
//
// 2. cyclic
// Sort by geometric angle, then remove the arbitrary angular
// branch cut by choosing the largest angular gap as the cut.
//
// This is the appropriate intrinsic cyclic ordering when the
// largest gap is unique.
//
// For tied largest gaps, the local geometry does not select a
// unique starting point. We therefore use the lab convention
// only for that rare ambiguous case and count it.
// ============================================================
let cyclicAmbiguities = 0;
function normalizedAngle(dr,dc) {
let a =
Math.atan2(
dc,
dr
);
if (a < 0) {
a += Math.PI*2;
}
return a;
}
function labOrderedParents(
parentInfo
) {
return parentInfo
.slice()
.sort((a,b) => {
const aa =
Math.atan2(
a[1],
a[0]
);
const ab =
Math.atan2(
b[1],
b[0]
);
if (aa !== ab) {
return aa-ab;
}
if (a[0] !== b[0]) {
return a[0]-b[0];
}
return a[1]-b[1];
});
}
function cyclicOrderedParents(
parentInfo
) {
const entries =
parentInfo
.map(p => ({
p,
angle:
normalizedAngle(
p[0],
p[1]
)
}))
.sort(
(a,b) =>
a.angle-b.angle
);
if (entries.length <= 1) {
return entries.map(x => x.p);
}
const gaps = [];
for (
let i=0;
i<entries.length;
i++
) {
const a =
entries.angle;
const b =
entries[
(i+1) % entries.length
].angle;
let gap = b-a;
if (gap <= 0) {
gap += Math.PI*2;
}
gaps.push(gap);
}
const maxGap =
Math.max(...gaps);
const winners =
gaps.filter(
g =>
Math.abs(g-maxGap)
< 1e-12
);
if (winners.length !== 1) {
cyclicAmbiguities++;
return labOrderedParents(
parentInfo
);
}
const cut =
gaps.indexOf(maxGap);
const start =
(cut+1) %
entries.length;
const result = [];
for (
let k=0;
k<entries.length;
k++
) {
result.push(
entries[
(start+k) %
entries.length
].p
);
}
return result;
}
function orderedParents(
parentInfo
) {
if (orderingMode === 'lab') {
return labOrderedParents(
parentInfo
);
}
return cyclicOrderedParents(
parentInfo
);
}
// ============================================================
// Tag composition
// ============================================================
function combineTags(
vals
) {
if (algebra === 'binary') {
const negatives =
vals.filter(
v => v < 0
).length;
return (
negatives % 2 === 0
? 1
: -1
);
}
if (algebra === 'quaternion') {
let result =
vals[0];
for (
let k=1;
k<vals.length;
k++
) {
result =
qMul(
result,
vals[k]
);
}
return result;
}
if (vals.length !== 3) {
throw new Error(
'Octonion birth expects exactly 3 parents.'
);
}
if (grouping === 'left') {
return oMul(
oMul(
vals[0],
vals[1]
),
vals[2]
);
}
return oMul(
vals[0],
oMul(
vals[1],
vals[2]
)
);
}
// ============================================================
// One Life step
// ============================================================
function evolveOne(
sourceGrid,
sourceTags
) {
const next =
makeGrid();
const nextTags =
{};
for (let i=0;i<SIZE;i++) {
for (let j=0;j<SIZE;j++) {
let live = 0;
const parentInfo = [];
for (let di=-1;di<=1;di++) {
for (let dj=-1;dj<=1;dj++) {
if (
di === 0 &&
dj === 0
) {
continue;
}
const ni =
(i+di+SIZE)
% SIZE;
const nj =
(j+dj+SIZE)
% SIZE;
if (
sourceGrid[ni][nj]
) {
live++;
parentInfo.push([
di,
dj,
sourceTags[
`${ni},${nj}`
]
]);
}
}
}
// Survival
if (
sourceGrid[i][j]
) {
if (
live === 2 ||
live === 3
) {
next[i][j] = 1;
nextTags[
`${i},${j}`
] =
cloneTag(
sourceTags[
`${i},${j}`
]
);
}
continue;
}
// Birth
if (live === 3) {
next[i][j] = 1;
const ordered =
orderedParents(
parentInfo
);
nextTags[
`${i},${j}`
] =
combineTags(
ordered.map(
p => p[2]
)
);
}
}
}
return {
grid:next,
tags:nextTags
};
}
// ============================================================
// Main step
// ============================================================
function step() {
const originalNext =
evolveOne(
grid,
tags
);
const mirrorNext =
evolveOne(
mirrorGrid,
mirrorTags
);
grid =
originalNext.grid;
tags =
originalNext.tags;
mirrorGrid =
mirrorNext.grid;
mirrorTags =
mirrorNext.tags;
gen++;
// Period detection on original state.
const rc =
relConfig(
grid,
tags
);
if (
seenConfigs.has(rc) &&
periodMsg === ''
) {
const previous =
seenConfigs.get(rc);
periodMsg =
`period ${gen-previous} ` +
`(gen ${gen} matches gen ${previous})`;
cycleStartGen =
previous;
}
if (!seenConfigs.has(rc)) {
seenConfigs.set(
rc,
gen
);
}
bboxHistory.push(
computeBBoxArea(
grid
)
);
const diff =
currentDifference();
differenceHistory.push(
diff
);
const mismatch =
isMismatch(diff)
? 1
: 0;
mismatchHistory.push(
mismatch
);
if (
mismatch &&
firstMismatch === null
) {
firstMismatch = gen;
}
if (
diff.spatialMismatch > 0 &&
firstSpatialMismatch === null
) {
firstSpatialMismatch = gen;
}
if (
diff.tagMismatch > 0 &&
firstTagMismatch === null
) {
firstTagMismatch = gen;
}
}
// ============================================================
// Bounding box
// ============================================================
function computeBBoxArea(
g=grid
) {
let minR = SIZE;
let maxR = -1;
let minC = SIZE;
let maxC = -1;
let pop = 0;
for (let r=0;r<SIZE;r++) {
for (let c=0;c<SIZE;c++) {
if (g[r][c]) {
pop++;
minR =
Math.min(
minR,
r
);
maxR =
Math.max(
maxR,
r
);
minC =
Math.min(
minC,
c
);
maxC =
Math.max(
maxC,
c
);
}
}
}
if (!pop) {
return 0;
}
return (
(maxR-minR+1) *
(maxC-minC+1)
);
}
// ============================================================
// Tag entropy
// ============================================================
function tagToStr(t) {
if (t == null) {
return 'dead';
}
if (algebra === 'binary') {
return t > 0
? '+'
: '-';
}
if (algebra === 'quaternion') {
return qName(t);
}
return oName(t);
}
function tagEntropy(
tagMap
) {
const vals =
Object.values(
tagMap
).map(
tagToStr
);
if (!vals.length) {
return 0;
}
const count = {};
vals.forEach(
v =>
count[v] =
(count[v] || 0) + 1
);
const total =
vals.length;
let H = 0;
for (
const n of Object.values(count)
) {
const p =
n / total;
H -=
p * Math.log2(p);
}
return H;
}
// ============================================================
// Mutual information
//
// We align the independently evolved transformed state back
// into the original coordinate frame.
//
// This is measured over the union of positions that are live
// in either state, with "dead" treated as a symbol.
//
// This statistic therefore measures correspondence of the
// complete local tag/occupancy symbol, not tag information
// conditioned only on common-live positions.
// ============================================================
function mutualInformation() {
const aligned =
inverseTransformState(
mirrorGrid,
mirrorTags
);
const positions =
new Set([
...Object.keys(tags),
...Object.keys(
aligned.tags
)
]);
if (!positions.size) {
return 0;
}
const xs = [];
const ys = [];
for (
const pos of positions
) {
xs.push(
tags[pos] != null
? tagToStr(tags[pos])
: 'dead'
);
ys.push(
aligned.tags[pos] != null
? tagToStr(
aligned.tags[pos]
)
: 'dead'
);
}
const total =
xs.length;
function count(arr) {
const out = {};
arr.forEach(
x =>
out[x] =
(out[x] || 0) + 1
);
return out;
}
const cx = count(xs);
const cy = count(ys);
const cxy = {};
for (
let i=0;
i<xs.length;
i++
) {
const key =
xs[i] + '|' + ys[i];
cxy[key] =
(cxy[key] || 0) + 1;
}
function entropy(cnt) {
let H = 0;
for (
const n of Object.values(cnt)
) {
const p =
n / total;
H -=
p * Math.log2(p);
}
return H;
}
return (
entropy(cx) +
entropy(cy) -
entropy(cxy)
);
}
// ============================================================
// Colours
// ============================================================
const PALETTE = [
'#378ADD',
'#D85A30',
'#1D9E75',
'#D4537E',
'#7F77DD',
'#BA7517',
'#639922',
'#e8e8e6'
];
const Q_PALETTE = {
'1':'#378ADD',
'-1':'#D85A30',
'i':'#1D9E75',
'-i':'#D4537E',
'j':'#7F77DD',
'-j':'#BA7517',
'k':'#639922',
'-k':'#e8e8e6'
};
function colorFor(t) {
if (algebra === 'binary') {
return t > 0
? '#378ADD'
: '#D85A30';
}
if (algebra === 'quaternion') {
return (
Q_PALETTE[
qName(t)
] ||
'#999'
);
}
const name =
oName(t);
const base =
name.startsWith('-')
? name.slice(1)
: name;
return (
PALETTE[
O_NAMES.indexOf(base)
] ||
'#999'
);
}
// ============================================================
// Drawing
// ============================================================
function draw() {
const canvas =
document.getElementById(
'grid'
);
const rect =
canvas.getBoundingClientRect();
const w =
rect.width > 0
? rect.width
: 700;
const h =
rect.height > 0
? rect.height
: 467;
canvas.width =
w*2;
canvas.height =
h*2;
const ctx =
canvas.getContext(
'2d'
);
const cell =
canvas.width / SIZE;
ctx.clearRect(
0,
0,
canvas.width,
canvas.height
);
let pop = 0;
for (let r=0;r<SIZE;r++) {
for (let c=0;c<SIZE;c++) {
if (grid[r][c]) {
pop++;
ctx.fillStyle =
colorFor(
tags[
`${r},${c}`
]
);
ctx.fillRect(
c*cell,
r*cell,
cell-1,
cell-1
);
}
}
}
let transformedPop = 0;
for (let r=0;r<SIZE;r++) {
for (let c=0;c<SIZE;c++) {
if (mirrorGrid[r][c]) {
transformedPop++;
}
}
}
document
.getElementById(
'genLabel'
)
.textContent =
'gen ' + gen;
document
.getElementById(
'popLabel'
)
.textContent =
`${pop} live ` +
`(transformed copy: ${transformedPop})`;
document
.getElementById(
'periodLabel'
)
.textContent =
periodMsg;
document
.getElementById(
'entropyLabel'
)
.textContent =
`H=${tagEntropy(tags).toFixed(2)} ` +
`MI=${mutualInformation().toFixed(2)}`;
updateExperimentMetrics();
drawBBoxChart();
}
// ============================================================
// Experiment metrics UI
// ============================================================
function updateExperimentMetrics() {
const diff =
differenceHistory.length
? differenceHistory[
differenceHistory.length-1
]
: {
spatialMismatch:0,
tagMismatch:0,
commonLive:0
};
document
.getElementById(
'firstMismatchMetric'
)
.textContent =
firstMismatch === null
? 'none'
: firstMismatch;
document
.getElementById(
'firstSpatialMetric'
)
.textContent =
firstSpatialMismatch === null
? 'none'
: firstSpatialMismatch;
document
.getElementById(
'firstTagMetric'
)
.textContent =
firstTagMismatch === null
? 'none'
: firstTagMismatch;
document
.getElementById(
'spatialMetric'
)
.textContent =
diff.spatialMismatch;
document
.getElementById(
'tagMetric'
)
.textContent =
diff.tagMismatch;
document
.getElementById(
'commonMetric'
)
.textContent =
diff.commonLive;
const symName =
symmetry === 'reflect'
? 'reflection'
: '90° rotation';
const orderingName =
orderingMode === 'lab'
? 'lab-frame'
: 'cyclic';
const status =
document.getElementById(
'reflectionLabel'
);
if (
firstMismatch === null
) {
status.textContent =
`${symName}: equivariant so far ` +
`(${orderingName})`;
status.style.color =
'#7bc47f';
} else {
status.textContent =
`${symName} mismatch first at ` +
`gen ${firstMismatch}`;
status.style.color =
'#D85A30';
}
}
// ============================================================
// Bounding-box chart
// ============================================================
function drawBBoxChart() {
const canvas =
document.getElementById(
'bboxChart'
);
const rect =
canvas.getBoundingClientRect();
const w =
rect.width > 0
? rect.width
: 700;
const h =
rect.height > 0
? rect.height
: 80;
canvas.width =
w*2;
canvas.height =
h*2;
const ctx =
canvas.getContext(
'2d'
);
ctx.clearRect(
0,
0,
canvas.width,
canvas.height
);
if (
bboxHistory.length < 2
) {
return;
}
const maxVal =
Math.max(
...bboxHistory,
1
);
const n =
bboxHistory.length;
if (
cycleStartGen !== null
) {
const x0 =
canvas.width *
(
cycleStartGen /
Math.max(
n-1,
1
)
);
ctx.fillStyle =
'rgba(123,196,127,0.12)';
ctx.fillRect(
x0,
0,
canvas.width-x0,
canvas.height
);
}
ctx.strokeStyle =
'#7bc47f';
ctx.lineWidth = 3;
ctx.beginPath();
bboxHistory.forEach(
(v,i) => {
const x =
canvas.width *
(
i /
Math.max(
n-1,
1
)
);
const y =
canvas.height -
(
v/maxVal
) *
canvas.height *
0.85 -
canvas.height*0.05;
if (i === 0) {
ctx.moveTo(x,y);
} else {
ctx.lineTo(x,y);
}
}
);
ctx.stroke();
const label =
document.getElementById(
'bboxLabel'
);
const recent =
bboxHistory.slice(-8);
label.textContent =
`recent areas: ` +
recent.join(', ') +
(
cycleStartGen !== null
? ' (shaded = detected period)'
: ''
);
}
// ============================================================
// Legend
// ============================================================
function updateLegend() {
const leg =
document.getElementById(
'legend'
);
leg.innerHTML = '';
if (algebra === 'binary') {
leg.innerHTML = `
<span class="swatch">
<span class="dot"
style="background:#378ADD"></span>
+1
</span>
<span class="swatch">
<span class="dot"
style="background:#D85A30"></span>
-1
</span>
`;
return;
}
if (algebra === 'quaternion') {
leg.innerHTML =
Q8_NAMES.map(
n => `
<span class="swatch">
<span
class="dot"
style="
background:${Q_PALETTE[n]}
"
></span>
${n}
</span>
`
).join('');
return;
}
leg.innerHTML =
O_NAMES.map(
(n,i) => `
<span class="swatch">
<span
class="dot"
style="
background:${PALETTE[i]}
"
></span>
${n}
</span>
`
).join('');
}
// ============================================================
// Notes
// ============================================================
const notes = {
binary:
'GF(2) control: commutative and associative. ' +
'Reflection and rotation are expected to remain equivariant.',
quaternion:
'Q8: associative but noncommutative. ' +
'Reflection reverses geometric orientation; rotation preserves it. ' +
'The ordering control distinguishes lab-frame from cyclic geometry.',
octonion:
'Octonions are noncommutative and nonassociative. ' +
'Grouping and spatial ordering are independent experimental variables.'
};
document
.getElementById('note')
.textContent =
notes[algebra];
// ============================================================
// Automated experiment runner
// ============================================================
//
// Default matrix:
//
// 3 algebras
// 4 patterns
// 2 symmetries
// 2 ordering modes
// 2 octonion groupings
//
// = 64 runs total.
//
// Non-octonion groupings collapse to one setting.
//
// Each run evolves independently for 100 generations.
//
// Results are returned as a plain JS array and displayed in
// the experimentOutput panel.
// ============================================================
function runExperiment({
generations = 100
} = {}) {
const originalAlgebra =
algebra;
const originalPattern =
pattern;
const originalSymmetry =
symmetry;
const originalOrdering =
orderingMode;
const originalGrouping =
grouping;
const results = [];
const algebras = [
'binary',
'quaternion',
'octonion'
];
const patterns = [
'glider',
'blinker',
'block',
'rpent'
];
const symmetries = [
'reflect',
'rot90'
];
const orderings = [
'lab',
'cyclic'
];
for (
const alg of algebras
) {
for (
const pat of patterns
) {
for (
const sym of symmetries
) {
for (
const order of orderings
) {
const groupings =
alg === 'octonion'
? ['left','right']
: ['left'];
for (
const grp of groupings
) {
algebra =
alg;
pattern =
pat;
symmetry =
sym;
orderingMode =
order;
grouping =
grp;
cyclicAmbiguities = 0;
loadPattern();
for (
let g=0;
g<generations;
g++
) {
step();
}
const finalDiff =
differenceHistory[
differenceHistory.length-1
];
const maxSpatial =
Math.max(
...differenceHistory.map(
d =>
d.spatialMismatch
)
);
const maxTag =
Math.max(
...differenceHistory.map(
d =>
d.tagMismatch
)
);
const maxTotal =
Math.max(
...differenceHistory.map(
d =>
d.spatialMismatch +
d.tagMismatch
)
);
const firstSpatial =
differenceHistory.findIndex(
d =>
d.spatialMismatch > 0
);
const firstTag =
differenceHistory.findIndex(
d =>
d.tagMismatch > 0
);
const firstAny =
differenceHistory.findIndex(
d =>
isMismatch(d)
);
const finalMI =
mutualInformation();
const result = {
algebra: alg,
pattern: pat,
symmetry: sym,
ordering: order,
grouping: grp,
generations,
firstMismatch:
firstAny < 0
? null
: firstAny,
firstSpatialMismatch:
firstSpatial < 0
? null
: firstSpatial,
firstTagMismatch:
firstTag < 0
? null
: firstTag,
maxSpatialMismatch:
maxSpatial,
maxTagMismatch:
maxTag,
maxTotalMismatch:
maxTotal,
finalSpatialMismatch:
finalDiff.spatialMismatch,
finalTagMismatch:
finalDiff.tagMismatch,
finalCommonLive:
finalDiff.commonLive,
finalUnionLive:
finalDiff.unionLive,
finalEntropy:
tagEntropy(tags),
finalMI,
cyclicAmbiguities
};
results.push(
result
);
}
}
}
}
}
// Restore interactive state.
algebra =
originalAlgebra;
pattern =
originalPattern;
symmetry =
originalSymmetry;
orderingMode =
originalOrdering;
grouping =
originalGrouping;
loadPattern();
updateLegend();
draw();
return results;
}
// ============================================================
// Experiment summary
// ============================================================
function summarizeResults(
results
) {
const lines = [];
lines.push(
'CHIRAL LIFE EXPERIMENT RESULTS'
);
lines.push(
'=============================='
);
lines.push('');
lines.push(
`runs: ${results.length}`
);
lines.push(
`generations/run: ` +
`${results[0]?.generations ?? 0}`
);
lines.push('');
// Group by algebra/symmetry/ordering.
const groups = {};
for (
const r of results
) {
const key =
[
r.algebra,
r.symmetry,
r.ordering,
r.grouping
].join('/');
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(r);
}
for (
const [key,rows]
of Object.entries(groups)
) {
const any =
rows.filter(
r =>
r.firstMismatch !== null
).length;
const spatial =
rows.filter(
r =>
r.firstSpatialMismatch !== null
).length;
const tag =
rows.filter(
r =>
r.firstTagMismatch !== null
).length;
lines.push(
`${key}:`
);
lines.push(
` any mismatch: ${any}/${rows.length}`
);
lines.push(
` spatial mismatch: ${spatial}/${rows.length}`
);
lines.push(
` tag mismatch: ${tag}/${rows.length}`
);
lines.push('');
}
return lines.join('\n');
}
// ============================================================
// JSON export
// ============================================================
let lastExperimentResults = [];
function experimentJSON() {
return JSON.stringify(
{
generatedAt:
new Date().toISOString(),
model: {
rule: 'B3/S23',
gridSize: SIZE,
algebraSet: [
'binary',
'quaternion',
'octonion'
],
symmetrySet: [
'reflect',
'rot90'
],
orderingSet: [
'lab',
'cyclic'
],
octonionGroupings: [
'left',
'right'
]
},
results:
lastExperimentResults
},
null,
2
);
}
// ============================================================
// UI: algebra
// ============================================================
document
.getElementById(
'algebra'
)
.addEventListener(
'change',
e => {
algebra =
e.target.value;
document
.getElementById(
'groupBtn'
)
.style.display =
algebra === 'octonion'
? 'inline-block'
: 'none';
document
.getElementById(
'note'
)
.textContent =
notes[algebra];
updateLegend();
loadPattern();
draw();
}
);
// ============================================================
// UI: pattern
// ============================================================
document
.getElementById(
'pattern'
)
.addEventListener(
'change',
e => {
pattern =
e.target.value;
loadPattern();
draw();
}
);
// ============================================================
// UI: symmetry
// ============================================================
document
.getElementById(
'symmetry'
)
.addEventListener(
'change',
e => {
symmetry =
e.target.value;
loadPattern();
draw();
}
);
// ============================================================
// UI: ordering
// ============================================================
document
.getElementById(
'ordering'
)
.addEventListener(
'change',
e => {
orderingMode =
e.target.value;
loadPattern();
draw();
}
);
// ============================================================
// UI: octonion grouping
// ============================================================
document
.getElementById(
'groupBtn'
)
.addEventListener(
'click',
function() {
grouping =
grouping === 'left'
? 'right'
: 'left';
this.textContent =
'grouping: ' +
grouping;
loadPattern();
draw();
}
);
// ============================================================
// UI: step
// ============================================================
document
.getElementById(
'stepBtn'
)
.addEventListener(
'click',
() => {
step();
draw();
}
);
// ============================================================
// UI: reset
// ============================================================
document
.getElementById(
'resetBtn'
)
.addEventListener(
'click',
() => {
loadPattern();
draw();
}
);
// ============================================================
// UI: play
// ============================================================
document
.getElementById(
'playBtn'
)
.addEventListener(
'click',
function() {
playing =
!playing;
this.textContent =
playing
? 'pause'
: 'play';
this.className =
playing
? 'active'
: '';
if (playing) {
timer =
setInterval(
() => {
step();
draw();
},
150
);
} else {
clearInterval(
timer
);
}
}
);
// ============================================================
// UI: experiment runner
// ============================================================
document
.getElementById(
'runBtn'
)
.addEventListener(
'click',
() => {
const button =
document.getElementById(
'runBtn'
);
const status =
document.getElementById(
'experimentStatus'
);
const output =
document.getElementById(
'experimentOutput'
);
button.disabled = true;
status.textContent =
'Running 100-generation experiment matrix...';
output.textContent =
'Working...';
setTimeout(
() => {
try {
lastExperimentResults =
runExperiment({
generations:100
});
const summary =
summarizeResults(
lastExperimentResults
);
output.textContent =
summary +
'\n\n' +
'FULL JSON AVAILABLE VIA ' +
'copy results.\n\n' +
JSON.stringify(
lastExperimentResults,
null,
2
);
status.textContent =
`Complete: ` +
`${lastExperimentResults.length} runs. ` +
`Cyclic-order ambiguities are reported ` +
`per run.`;
}
catch (err) {
console.error(err);
status.textContent =
'Experiment failed: ' +
err.message;
output.textContent =
err.stack || String(err);
}
finally {
button.disabled = false;
}
},
30
);
}
);
// ============================================================
// UI: copy results
// ============================================================
document
.getElementById(
'copyBtn'
)
.addEventListener(
'click',
async () => {
if (
!lastExperimentResults.length
) {
document
.getElementById(
'experimentStatus'
)
.textContent =
'Run experiments first.';
return;
}
const text =
experimentJSON();
try {
await navigator.clipboard.writeText(
text
);
document
.getElementById(
'experimentStatus'
)
.textContent =
'Full JSON copied to clipboard.';
}
catch (err) {
console.log(text);
document
.getElementById(
'experimentStatus'
)
.textContent =
'Clipboard unavailable; full JSON is in the console.';
}
}
);
// ============================================================
// Browser console helpers
// ============================================================
//
// Grok can also use:
//
// runExperiment({generations:100})
//
// or:
//
// console.table(lastExperimentResults)
//
// and:
//
// console.log(experimentJSON())
//
// ============================================================
window.runExperiment =
runExperiment;
window.experimentJSON =
experimentJSON;
window.getExperimentResults =
() =>
lastExperimentResults;
// ============================================================
// Initial state
// ============================================================
updateLegend();
loadPattern();
requestAnimationFrame(
() =>
requestAnimationFrame(
draw
)
);
window.addEventListener(
'resize',
draw
);
</script>
</body>
</html>