@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
JavaScript
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
};