Files
web_server/html/js/mobile-automata.mjs
2025-11-21 23:27:02 +00:00

312 lines
8.0 KiB
JavaScript

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);
}