function integerDigits(n, b = 10, length = null) { // Get the list of digits in base b const digits = []; while (n > 0) { digits.push(n % b); n = Math.floor(n / b); } digits.reverse(); // Reverse the list to get digits in big endian order // Pad with zeros if length is specified if (length !== null) { const padding = Array(Math.max(0, length - digits.length)).fill(0); return padding.concat(digits); } return digits; } function* cartesianProduct(...arrays) { // Generator for cartesian product if (arrays.length === 0) { yield []; return; } const [first, ...rest] = arrays; if (rest.length === 0) { for (const item of first) { yield [item]; } } else { for (const item of first) { for (const combo of cartesianProduct(...rest)) { yield [item, ...combo]; } } } } function tuplesFromList(lst, n) { const arrays = Array(n).fill(lst); return Array.from(cartesianProduct(...arrays)); } function tuplesFromMultipleLists(...lists) { return Array.from(cartesianProduct(...lists)); } function flattenTuples(tuples) { return tuples.flat(); } function partition(lst, n) { const result = []; for (let i = 0; i < lst.length; i += n) { result.push(lst.slice(i, i + n)); } return result; } function pick(pickList, lst) { const trues = []; const falses = []; for (let i = 0; i < pickList.length; i++) { if (pickList[i]) { trues.push(lst[i]); } else { falses.push(lst[i]); } } return [trues, falses]; } function factorial(n) { if (n < 0) return NaN; if (n === 0 || n === 1) return 1; let result = 1; for (let i = 2; i <= n; i++) { result *= i; } return result; } function unrankPermutation(r, lst) { const n = lst.length; r -= 1; // Convert r to 0-indexed const permutation = []; const availableElements = [...lst]; for (let i = n; i > 0; i--) { const fact = factorial(i - 1); // (n-1)! const index = Math.floor(r / fact); // Find the index of the current element permutation.push(availableElements.splice(index, 1)[0]); // Add the element and remove it from available r %= fact; // Update r to find the next element } return permutation; } export function toMaRule(sn, dn, n, k) { if (n < 1 || n % 2 === 0) { throw new Error("n must be >= 1 and odd"); } const inputs = tuplesFromList([...Array(k).keys()], n); const directions = integerDigits(dn, 2, Math.pow(k, n)).map((x) => Math.pow(-1, x), ); const snDigits = integerDigits(sn, k, n * Math.pow(k, n)); const outputs = partition(snDigits, n); const rules = {}; for (let i = 0; i < inputs.length; i++) { rules[JSON.stringify(inputs[i])] = [outputs[i], directions[i]]; } return rules; } export function toReversibleMaRule(bn, pn, n, k) { if (n < 1 || n % 2 === 0) { throw new Error("n must be >= 1 and odd"); } const inputs = tuplesFromList([...Array(k).keys()], n); const blockers = tuplesFromList([...Array(k).keys()], n - 2); const blockSelect = pick(integerDigits(bn, 2, Math.pow(k, n - 2)), blockers); const rightBlockers = blockSelect[0]; const leftBlockers = blockSelect[1]; const twoFair = tuplesFromList([...Array(k).keys()], 2); const leftOutputs = tuplesFromMultipleLists(leftBlockers, twoFair).map( (x) => [flattenTuples(x), -1], ); const rightOutputs = tuplesFromMultipleLists(twoFair, rightBlockers).map( (x) => [flattenTuples(x), 1], ); const outputs = [...leftOutputs, ...rightOutputs]; const rankedOutputs = unrankPermutation(pn, outputs); const rules = {}; for (let i = 0; i < inputs.length; i++) { rules[JSON.stringify(inputs[i])] = rankedOutputs[i]; } return rules; } export function maStep(rules, state, r) { /** * Apply one step of the mobile automaton rules * * Args: * rules (object): Dictionary of rules where key is input tuple and value is [output_tuple, direction] * state (array): [list, head] where list is current state and head is current position * r (number): Radius of the neighborhood (window size = 2r + 1) * * Returns: * array: [new_list, new_head] or [[], -1] if out of bounds */ const [currentList, head] = state; // Check bounds if (head - r <= 0 || head + r >= currentList.length) { return [[], -1]; } // Get the window of elements centered at head const window = currentList.slice(head - r, head + r + 1); // Apply rule const ruleKey = JSON.stringify(window); const [newWindow, direction] = rules[ruleKey]; // Create new list with replaced elements const newList = [...currentList]; for (let i = 0; i < newWindow.length; i++) { newList[head - r + i] = newWindow[i]; } return [newList, head + direction]; } export function ma(rules, initialState, t) { /** * Perform t steps of the mobile automaton * * Args: * rules (object): Dictionary of rules * initialState (array): Initial [list, head] state * t (number): Number of steps to perform * * Returns: * array: List of states at each time step */ // Calculate radius from first rule key length const firstKey = Object.keys(rules)[0]; const r = JSON.parse(firstKey).length / 2; const states = [initialState]; let currentState = initialState; for (let i = 0; i < t; i++) { currentState = maStep(rules, currentState, r); states.push(currentState); // Stop if we hit an invalid state if (currentState[0].length === 0) { break; } } return states; } export function cyclicMaStep(rules, state, r) { /** * Cyclic version: indexing wraps around the array. */ const [currentList, head] = state; const n = currentList.length; // --- Cyclic window extraction --- const window = []; for (let i = -r; i <= r; i++) { window.push(currentList[(head + i + n) % n]); } // Apply rule const ruleKey = JSON.stringify(window); const [newWindow, direction] = rules[ruleKey]; // --- Cyclic writeback --- const newList = [...currentList]; for (let offset = 0; offset < newWindow.length; offset++) { newList[(head - r + offset + n) % n] = newWindow[offset]; } // Move head cyclically const newHead = (head + direction + n) % n; return [newList, newHead]; } export function cyclicMa(rules, initialState, t) { /** * Perform t steps of the mobile automaton * * Args: * rules (object): Dictionary of rules * initialState (array): Initial [list, head] state * t (number): Number of steps to perform * * Returns: * array: List of states at each time step */ // Calculate radius from first rule key length const firstKey = Object.keys(rules)[0]; const r = JSON.parse(firstKey).length / 2; const states = [initialState]; let currentState = initialState; for (let i = 0; i < t; i++) { currentState = cyclicMaStep(rules, currentState, r); states.push(currentState); // Stop if we hit an invalid state if (currentState[0].length === 0) { break; } } return states; } export function renderToCanvas(canvas, width, height, sn = 13252213, dn = 180) { const r = 1; const rules = toMaRule(sn, dn, 2 * r + 1, 2); let states = Array.from({ length: height }, () => Array(width).fill(0)); let head = Math.floor(width / 2) % width; let row_num = 0; const ctx = canvas.getContext("2d"); const img = ctx.createImageData(width, height); const data = img.data; function step() { // calculate new state let [newState, newHead] = cyclicMaStep(rules, [states[row_num], head], r); states[row_num] = newState; // write row to ImageData for (let x = 0; x < width; x++) { const idx = (row_num * width + x) * 4; const val = newState[x] ? 255 : 0; data[idx] = val; data[idx + 1] = val; data[idx + 2] = val; data[idx + 3] = 255; } // update canvas (only this row) ctx.putImageData(img, 0, row_num, 0, 0, width, 1); // advance row and head row_num = (row_num + 1) % height; head = newHead; requestAnimationFrame(step); } requestAnimationFrame(step); }