UNPKG

@thi.ng/cellular

Version:

Highly customizable 1D cellular automata, shared env, multiple rules, arbitrary sized/shaped neighborhoods, short term memory, cell states etc.

244 lines (243 loc) 7.43 kB
import { rotateTyped } from "@thi.ng/arrays/rotate"; import { isBigInt } from "@thi.ng/checks/is-bigint"; import { assert } from "@thi.ng/errors/assert"; import { SYSTEM } from "@thi.ng/random/system"; import { repeat } from "@thi.ng/transducers"; import { map } from "@thi.ng/transducers/map"; import { mapcat } from "@thi.ng/transducers/mapcat"; import { max } from "@thi.ng/transducers/max"; import { pluck } from "@thi.ng/transducers/pluck"; import { repeatedly } from "@thi.ng/transducers/repeatedly"; import { transduce } from "@thi.ng/transducers/transduce"; const $0 = BigInt(0); const $1 = BigInt(1); const $32 = BigInt(32); const WOLFRAM3 = [ [-1, 0], [0, 0], [1, 0] ]; const WOLFRAM5 = [[-2, 0], ...WOLFRAM3, [2, 0]]; const WOLFRAM7 = [[-3, 0], ...WOLFRAM5, [3, 0]]; class MultiCA1D { constructor(configs, width, wrap = true) { this.width = width; this.wrap = wrap; this.configs = configs.map(__compileSpec); this.rows = transduce( mapcat((c) => map((k) => k[1], c.kernel)), max(), configs ) + 1; this.numStates = transduce(pluck("states"), max(), configs); assert( this.numStates >= 2 && this.numStates <= 256, "num states must be in [2..256] range" ); this.resize(width); } configs; rows; numStates; mask; gens; prob; get current() { return this.gens[1]; } get previous() { return this.gens[2 % this.gens.length]; } clear() { this.gens.forEach((g) => g.fill(0)); this.mask.fill(0); this.prob.fill(1); } clearTarget(target) { this._getTarget(target)[0].fill(target === "prob" ? 1 : 0); } resize(width) { this.width = width; this.mask = new Uint8Array(width); this.gens = [...repeatedly(() => new Uint8Array(width), this.rows + 1)]; this.prob = new Float32Array(width).fill(1); } /** * Sets a parametric pattern in the current generation or mask array. * * @param target - target buffer ID to apply pattern * @param width - number of consecutive cells per segment * @param stride - number of cells between each pattern segment * @param val - start cell value per segment * @param inc - cell value increment * @param offset - start cell offset */ setPattern(target, width, stride, val = 1, inc = 0, offset = 0) { const [dest, num] = this._getTarget(target); for (let x = offset, w = this.width; x < w; x += stride) { for (let k = 0, v = val; k < width; k++, v += inc) { dest[x + k] = v % num; } } return this; } /** * Sets cells in current generation array to a random state using given * `probability` and optional PRNG * ([`IRandom`](https://docs.thi.ng/umbrella/random/interfaces/IRandom.html) * instance). * * @param target * @param prob * @param rnd */ setNoise(target, prob = 0.5, rnd = SYSTEM) { const [dest, num] = this._getTarget(target); const fn = target === "prob" ? () => rnd.float() : () => rnd.int() % num; for (let x = 0, width = this.width; x < width; x++) { if (rnd.probability(prob)) dest[x] = fn(); } return this; } /** * Computes a single new generation using current cell states and mask only * (no consideration for cell update probabilities, use * {@link MultiCA1D.updateProbabilistic} for that instead). Als see * {@link MultiCA1D.updateImage} for batch updates. */ update() { const { width, gens, configs, mask } = this; const [next, curr] = gens; for (let x = 0; x < width; x++) { next[x] = this.computeCell(configs[mask[x]], x, curr[x]); } gens.unshift(gens.pop()); } /** * Same as {@link MultiCA1D.update}, but also considering cell update * probabilities stored in the {@link MultiCA1D.prob} array. * * @param rnd */ updateProbabilistic(rnd = SYSTEM) { const { width, prob, gens, configs, mask } = this; const [next, curr] = gens; for (let x = 0; x < width; x++) { next[x] = rnd.probability(prob[x]) ? this.computeCell(configs[mask[x]], x, curr[x]) : curr[x]; } gens.unshift(gens.pop()); } /** * Computes (but doesn't apply) the new state for a single cell. * * @param config - CA configuration * @param x - cell index * @param val - current cell value */ computeCell({ rule, kernel, weights, fn }, x, val) { const { width, gens, wrap } = this; let sum = $0; for (let i = 0, n = kernel.length; i < n; i++) { const k = kernel[i]; let xx = x + k[0]; if (wrap) { if (xx < 0) xx += width; else if (xx >= width) xx -= width; } else if (xx < 0 || xx >= width) continue; const y = k[1]; if (y >= 0 && gens[1 + y][xx] !== 0) sum += weights[i]; } return rule & $1 << sum ? fn(val) : 0; } /** * Batch version of {@link MultiCA1D.update} to compute an entire image of * given `height` (and assumed to be the same width as this CA instance has * been configured to). Fills given `pixels` array with consecutive * generations. * * @remarks * Via the provided options object, per-generation & per-cell perturbance * settings can be provided for cell states, mask and cell update * probabilities. The latter are only considered if the * {@link UpdateImageOpts1D.probabilistic} option is enabled. This can be * helpful to sporadically introduce noise into the sim, break constant * patterns and/or produce more varied/complex outputs. * * See {@link UpdateImageOpts1D} for further options. * * @param pixels * @param height * @param opts */ updateImage(pixels, height, opts = {}) { assert( pixels.length >= this.width * height, "target pixel buffer too small" ); const { probabilistic = false, rnd = SYSTEM, cells, mask, prob, onupdate } = opts; const $ = (id, conf) => { conf && conf.perturb && rnd.probability(conf.perturb) && this.setNoise(id, conf.density || 0.05, rnd); }; for (let y = 0; y < height; y++) { $("cells", cells); $("mask", mask); $("prob", prob); probabilistic ? this.updateProbabilistic(rnd) : this.update(); onupdate?.(this, y); pixels.set(this.current, y * this.width); } } rotate(target, dir) { if (target === "all") { rotateTyped(this.current, dir); rotateTyped(this.mask, dir); rotateTyped(this.prob, dir); } else { rotateTyped(this._getTarget(target)[0], dir); } } _getTarget(target) { return target === "cells" ? [this.current, this.numStates] : target === "mask" ? [this.mask, this.configs.length] : [this.prob, 1]; } } const __compileSpec = ({ rule, kernel, positional, states, reset }) => { const max2 = states - 1; return { kernel, states, rule: isBigInt(rule) ? rule : BigInt(rule), weights: positional !== false ? kernel.map((_, i) => BigInt(2) ** BigInt(i)) : [...repeat($1, kernel.length)], fn: reset !== false ? (y) => ++y >= states ? 0 : y : (y) => ++y >= max2 ? max2 : y }; }; const randomRule1D = (kernelSize, rnd = SYSTEM) => { const n = BigInt(2 ** kernelSize); let id = $0; for (let i = $0; i < n; i += $32) { id <<= $32; let mask = n - i; if (mask > $32) mask = $32; id |= BigInt(rnd.int()) & ($1 << mask) - $1; } return id; }; export { MultiCA1D, WOLFRAM3, WOLFRAM5, WOLFRAM7, randomRule1D };