niche.js
90 lines · in natura-src-1.0.0.zip as natura/niche.js · a module · its entry in the module reference · its test: niche-test.js
// niche.js -- Natura NICHE CONSTRUCTION (ONTOLOGY-FOLD-IN #2, "the deepest living-world move left"): life that
// REMAKES its world and then lives in the changed world -- the inverse of "weather changes the world it falls
// on." A population of BUILDERS (beavers, corals, worms, termites) writes a PERSISTENT structure into a field
// (a dam, a reef, a mound), and that structure RAISES the local habitat, which raises the builders' own carrying
// capacity -- so building lifts the population far above what the bare ground would hold (ecosystem engineering,
// a positive feedback). And the structure OUTLASTS its builders: kill the population and the dam still holds,
// the reef still stands, decaying only slowly -- the world keeps the mark of a life long after it is gone (the
// legacy). Field-based (builder density + built-structure + derived habitat), deterministic, node-testable.
// Couples out via its `habitat`/`built` fields (other life colonises the engineered ground). UMD.
(function (root, factory) {
if (typeof module !== 'undefined' && module.exports) module.exports = factory();
else root.Niche = factory();
})(typeof self !== 'undefined' ? self : this, function () {
'use strict';
const clamp = (v, a, b) => v < a ? a : v > b ? b : v;
// config: { N, base (bare-ground capacity), buildGain (built -> habitat lift), buildRate, decayDays
// (how slowly structures crumble unmaintained), growth, mortality, disperse, builtMax, popMax, seed, maxDtDays }
function makeNiche(config) {
config = config || {};
const N = config.N || 48;
const base = config.base != null ? config.base : 0.12; // capacity of BARE ground (builders barely hang on)
const buildGain = config.buildGain != null ? config.buildGain : 0.9; // how much a built structure raises local habitat
const buildRate = config.buildRate != null ? config.buildRate : 0.12; // structure added per unit builder per day
const decayDays = config.decayDays != null ? config.decayDays : 420; // structure e-folding decay when unmaintained
const growth = config.growth != null ? config.growth : 0.35; // builder growth rate/day
const mortality = config.mortality != null ? config.mortality : 0.05; // builder death rate/day
const disperse = config.disperse != null ? config.disperse : 0.05; // builder spread to neighbours
const builtMax = config.builtMax != null ? config.builtMax : 1.5;
const popMax = config.popMax != null ? config.popMax : 1.5;
const seedPop = config.seed != null ? config.seed : 0.15; // starting builders in a central patch (0 = none)
const maxDd = config.maxDtDays != null ? config.maxDtDays : 2;
const suitCfg = config.suitability != null ? config.suitability : 1; // where the ground SUITS building (valleys/wet); 0..1 multiplier on habitat. default 1 = uniform
const gx = (i) => (i / (N - 1)) * 2 - 1;
return {
name: config.name || 'niche',
domain: 'life',
scale: 'ecological',
params: config,
init() {
const pop = new Float32Array(N * N), built = new Float32Array(N * N), suit = new Float32Array(N * N);
for (let j = 0; j < N; j++) for (let i = 0; i < N; i++) suit[j * N + i] = clamp(typeof suitCfg === 'function' ? suitCfg(gx(i), gx(j)) : suitCfg, 0, 1);
if (seedPop > 0) { const c = (N / 2) | 0; for (let dj = -1; dj <= 1; dj++) for (let di = -1; di <= 1; di++) { const k = (c + dj) * N + (c + di); if (k >= 0 && k < N * N) pop[k] = seedPop; } }
return { N, pop, built, suit, meanPop: 0, meanBuilt: 0, event: null };
},
step(st, ctx, dt) {
const dd = clamp(dt / 86400, 0, maxDd); if (dd <= 0) return st;
const { pop, built } = st;
const decayF = Math.exp(-dd / decayDays);
const np = new Float32Array(N * N);
let est = 0;
const suit = st.suit;
for (let k = 0; k < N * N; k++) {
const hab = (base + buildGain * built[k]) * suit[k]; // the structure MAKES the habitat, where the ground SUITS it (valleys, wet ground)
const p = pop[k];
np[k] = clamp(p + growth * p * (1 - p / Math.max(hab, 1e-3)) * dd - mortality * p * dd, 0, popMax); // logistic toward the engineered capacity
const b = clamp(built[k] * decayF + buildRate * np[k] * dd, 0, builtMax); // builders add structure; it crumbles slowly
if (built[k] < 0.5 && b >= 0.5) est++;
built[k] = b;
}
// the builders spread into neighbouring ground (discretized Laplacian on the pre-dispersal field)
if (disperse > 0) {
for (let j = 0; j < N; j++) for (let i = 0; i < N; i++) {
const k = j * N + i; let acc = 0, n = 0;
if (i > 0) { acc += np[k - 1] - np[k]; n++; } if (i < N - 1) { acc += np[k + 1] - np[k]; n++; }
if (j > 0) { acc += np[k - N] - np[k]; n++; } if (j < N - 1) { acc += np[k + N] - np[k]; n++; }
pop[k] = n ? clamp(np[k] + disperse * dd * (acc / n), 0, popMax) : np[k];
}
} else { for (let k = 0; k < N * N; k++) pop[k] = np[k]; }
let sp = 0, sb = 0; for (let k = 0; k < N * N; k++) { sp += pop[k]; sb += built[k]; }
st.meanPop = sp / (N * N); st.meanBuilt = sb / (N * N); st.event = est > 0 ? 'engineered' : null;
return st;
},
// OUT: builder density, the built structure, and the derived HABITAT (other life reads this to colonise the engineered ground)
fields(st) {
const hab = new Float32Array(st.N * st.N);
for (let k = 0; k < hab.length; k++) hab[k] = base + buildGain * st.built[k];
return { N: st.N, pop: st.pop, built: st.built, habitat: hab, suit: st.suit, meanPop: st.meanPop, meanBuilt: st.meanBuilt };
},
events(st) { return st.event ? [{ kind: st.event }] : []; }
};
}
return { makeNiche };
});