technology.js
345 lines · in natura-src-1.0.0.zip as natura/technology.js · a module · its entry in the module reference · its test: technology-test.js
// technology.js -- Natura KNOWLEDGE / TECHNOLOGY (the ontology's CULTURE rung seen through what a people can
// DO, not what they feel). Where SOCIETY spreads a MOOD, this spreads INFORMATION WITH CONTENT: techniques
// that have prerequisites, serve functions, and RECOMBINE. A technique is not a scalar "tech level"; it is a
// discrete id sitting in a hidden TECH-TREE, and a population is a set of agents each HOLDING a set of those
// ids. Knowledge here is a thing that can be invented, taught (imperfectly), bettered, made obsolete, and --
// the uncanny pole this rung exists to show -- LOST. A technique known to too few, whose few holders turn over
// before they pass it on, VANISHES from the world; a people can fall into a dark age and have to re-invent
// what it once knew. Consensus without truth was SOCIETY's ghost; a DARK AGE is this rung's.
//
// The engine of growth is COMBINATORIAL: a new technique is formed by combining >=2 techniques already known,
// and the more a population knows the more pairs it can try, so invention ACCELERATES on itself (cumulative
// culture -- the ratchet). But the same combinatorics needs VARIETY: recombination of a monoculture (everyone
// knowing the same few things) finds little; a DIVERSE population, its knowledge spread across many corners,
// invents faster. And a hidden RECIPE map + PREREQUISITE gates give the tree its shape -- some combos unlock a
// real new tech (gated by lower techs already held), most combine to nothing -- so the growth is structured,
// not a free-for-all, and higher techs genuinely stand on lower ones.
//
// Five coupled mechanisms, none meaningful alone: (1) INVENTION by recombination against a hidden recipe tree;
// (2) DIFFUSION with FIDELITY < 1 (a lesson can fail or garble); (3) OBSOLESCENCE -- a better technique for a
// niche displaces a worse one in USE; (4) LOSS -- turnover + rarity can erase a technique from the whole
// population; (5) CUMULATIVE prerequisites -- a high tech needs the lower techs it is built on. Deterministic
// (rng only from init's seed + ctx.rng; time only from dt/clock), node-testable. UMD.
(function (root, factory) {
if (typeof module !== 'undefined' && module.exports) module.exports = factory();
else root.Technology = factory();
})(typeof self !== 'undefined' ? self : this, function () {
'use strict';
const clamp = (v, a, b) => v < a ? a : v > b ? b : v;
const keyOf = (a, b) => a < b ? (a + ',' + b) : (b + ',' + a); // an unordered pair of tech ids -> a recipe key
// A small deterministic rng we can seed independently of the world stream, so the HIDDEN TECH-TREE (built at
// init) is a fixed property of the config seed and does NOT consume the per-step world rng. mulberry32, same
// family as natura.js, kept local so this module stands alone.
function rngFrom(seed) {
let a = seed >>> 0;
return function () {
a = (a + 0x6D2B79F5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
// ---- the hidden TECH-TREE: the "physics of invention" -------------------------------------------------
// Built once, deterministically from techSeed. Nodes are laid out in TIERS. Tier-0 techs are PRIMITIVES a
// mind can stumble on unaided (fire, stone, fibre...). Every higher tech is the child of a RECIPE: an
// unordered pair of lower techs whose combination invents it, gated by a set of PREREQUISITES (lower techs
// that must ALSO be known to use/build it -- the cumulative floor). Each tech serves a NICHE (a function --
// cutting, heating, carrying...) with a UTILITY in that niche, and higher tiers tend to serve their niche
// BETTER, which is what lets a later tech make an earlier one OBSOLETE. The map from recipe-key -> child is
// the hidden structure; most pairs map to nothing, so invention is a search over a sparse tree.
function buildTree(cfg) {
const rng = rngFrom(cfg.techSeed >>> 0);
const tiers = cfg.tiers, perTier = cfg.perTier, niches = cfg.niches;
const techs = []; // techs[id] = { id, tier, niche, utility, prereqs:[], recipe:[a,b]|null }
const byTier = [];
for (let ti = 0; ti < tiers; ti++) byTier.push([]);
let id = 0;
// tier 0: primitives -- no recipe, no prereqs; a spread of niches so the base is varied
for (let k = 0; k < perTier; k++) {
const niche = k % niches;
techs.push({ id, tier: 0, niche, utility: 0.15 + 0.10 * rng(), prereqs: [], recipe: null });
byTier[0].push(id); id++;
}
const recipeMap = new Map(); // recipe-key "a,b" -> child id (the hidden combination table)
// higher tiers: each new tech is the child of a pair drawn from LOWER tiers, plus prereqs from below.
for (let ti = 1; ti < tiers; ti++) {
for (let k = 0; k < perTier; k++) {
const niche = (ti + k) % niches;
// pick two DISTINCT parents from any lower tier (recombination reaches back down the tree)
const lowerPool = [];
for (let lt = 0; lt < ti; lt++) for (const p of byTier[lt]) lowerPool.push(p);
let a = lowerPool[Math.floor(rng() * lowerPool.length)];
let b = lowerPool[Math.floor(rng() * lowerPool.length)];
let guard = 0;
while (b === a && guard++ < 16) b = lowerPool[Math.floor(rng() * lowerPool.length)];
if (b === a) continue; // degenerate pool (shouldn't happen with perTier>=2)
const rkey = keyOf(a, b);
if (recipeMap.has(rkey)) continue; // that pair is already spoken for -- keep recipes unique
// PREREQUISITES: the two parents, plus (sometimes) one extra lower tech -- the cumulative floor. A
// higher tier tends to demand more, so late techs genuinely stand on a tall stack.
const prereqs = new Set([a, b]);
if (ti >= 2 && rng() < 0.6) prereqs.add(lowerPool[Math.floor(rng() * lowerPool.length)]);
if (ti >= 3 && rng() < 0.5) prereqs.add(lowerPool[Math.floor(rng() * lowerPool.length)]);
// UTILITY climbs with tier (with noise) so later techs can obsolete earlier ones in the same niche
const utility = clamp(0.25 + 0.14 * ti + 0.10 * (rng() - 0.5), 0, 1);
const child = { id, tier: ti, niche, utility, prereqs: Array.from(prereqs), recipe: [a, b] };
techs.push(child);
recipeMap.set(rkey, id);
byTier[ti].push(id);
id++;
}
}
return { techs, recipeMap, byTier, count: id };
}
// make a Technology system. config (all optional):
// population, tiers, perTier, niches, techSeed,
// fidelity (diffusion success prob), inventRate (recombination attempts / agent / day),
// diffuseRate (learning attempts / agent / day), meanContacts (network degree),
// turnover (fraction of agents replaced per day), inherit (fraction of parent knowledge a newborn keeps),
// seedTechs (how many tier-0 primitives each agent starts knowing), obsolescence (bool toggle).
function makeTechnology(config) {
config = config || {};
const P = config.population || 80;
const cfg = {
tiers: config.tiers || 6,
perTier: config.perTier || 6,
niches: config.niches || 5,
techSeed: config.techSeed != null ? config.techSeed : 12345
};
const fidelity = config.fidelity != null ? config.fidelity : 0.85; // a lesson succeeds this often (transmission < 1)
const inventRate = config.inventRate != null ? config.inventRate : 3.0; // recombination ATTEMPTS per agent per sim-day
const diffuseRate = config.diffuseRate != null ? config.diffuseRate : 4.0; // learning attempts per agent per sim-day
const meanContacts = config.meanContacts != null ? config.meanContacts : 8; // network degree (connectivity)
const turnover = config.turnover != null ? config.turnover : 0.0; // fraction of the population replaced per sim-day (death/birth)
const inherit = config.inherit != null ? config.inherit : 0.5; // fraction of a parent's knowledge a newborn keeps
const seedTechs = config.seedTechs != null ? config.seedTechs : 3; // primitives each agent is born knowing at t0
const obsolescence = config.obsolescence != null ? config.obsolescence : true;
const tree = buildTree(cfg);
const T = tree.count;
// does agent a (a Set of known ids) satisfy tech `id`'s prerequisites? (the cumulative floor)
function prereqsMet(known, id) {
const pr = tree.techs[id].prereqs;
for (let i = 0; i < pr.length; i++) if (!known.has(pr[i])) return false;
return true;
}
return {
name: config.name || 'technology',
domain: 'culture',
scale: 'population',
params: config,
init(rng, params, clock) {
const _rng = rng || rngFrom((config.seed || 1) >>> 0);
const agents = [];
for (let a = 0; a < P; a++) {
const known = new Set();
// each agent is born knowing a few PRIMITIVES (tier-0), drawn with variety so the starting
// population is DIVERSE across corners of the base -- the raw material recombination feeds on.
for (let s = 0; s < seedTechs; s++) {
const t0 = tree.byTier[0][Math.floor(_rng() * tree.byTier[0].length)];
known.add(t0);
}
agents.push({ known, use: new Int32Array(cfg.niches).fill(-1), age: _rng() * 1.0, alive: true });
}
// fixed random contact network (undirected), degree ~ meanContacts. Connectivity is a lever the tests
// vary: a well-connected people RETAINS knowledge a fragmented one loses.
const contacts = [];
for (let a = 0; a < P; a++) contacts.push([]);
const targetEdges = Math.round(P * meanContacts / 2);
for (let e = 0; e < targetEdges; e++) {
const u = Math.floor(_rng() * P); let v = Math.floor(_rng() * P);
let guard = 0; while (v === u && guard++ < 8) v = Math.floor(_rng() * P);
if (v === u) continue;
if (contacts[u].indexOf(v) === -1) { contacts[u].push(v); contacts[v].push(u); }
}
const st = {
agents, contacts, T, niches: cfg.niches,
_techs: tree.techs, // the hidden tech-tree node table (recompute reads niche/utility/tier)
// observables (filled by recompute)
knownCount: 0, meanHeld: 0, maxTier: 0, diversity: 0, holders: new Int32Array(T),
invented: 0, lost: 0, diffusions: 0, inUseUtility: 0,
everInvented: new Uint8Array(T), // has this tech EVER existed in the population (for detecting LOSS)
_prevHolders: new Int32Array(T), _prevMaxTier: 0,
_newInventions: [], _lostThisStep: [], evs: [],
_acc: 0, _dacc: 0, _tacc: 0, // fractional accumulators for sub-integer rates
steps: 0
};
// seed everInvented with the primitives the population starts holding
recompute(st);
for (let id = 0; id < T; id++) if (st.holders[id] > 0) st.everInvented[id] = 1;
return st;
},
step(st, ctx, dt) {
const rng = ctx && ctx.rng ? ctx.rng : Math.random;
const dd = clamp((dt || 0) / 86400, 0, 3650); // step length in sim-days (bounded)
const agents = st.agents, contacts = st.contacts;
st.evs.length = 0; st._newInventions.length = 0; st._lostThisStep.length = 0;
st.steps++;
// ---- (4) LOSS / TURNOVER: age the population and replace a fraction. A replaced agent is a DEATH: its
// private knowledge dies with it UNLESS a newborn inherits a fraction, or the tech survives in someone
// else. A tech held by too FEW whose holders all turn over before teaching it VANISHES -- the dark-age
// mechanism. Turnover uses a fractional accumulator so sub-one-per-step rates integrate correctly.
if (turnover > 0 && P > 0) {
st._tacc += turnover * P * dd;
let nReplace = Math.floor(st._tacc);
st._tacc -= nReplace;
nReplace = Math.min(nReplace, P);
for (let r = 0; r < nReplace; r++) {
const victim = Math.floor(rng() * P);
// the newborn inherits a fraction of the victim's knowledge (parent-to-child teaching), the rest is
// lost with the dead. inherit=0 => a clean slate (harshest loss); inherit=1 => nothing is lost.
const oldKnown = agents[victim].known;
const child = new Set();
oldKnown.forEach(id => { if (rng() < inherit) child.add(id); });
agents[victim] = { known: child, use: new Int32Array(cfg.niches).fill(-1), age: 0, alive: true };
}
}
// ---- (2) DIFFUSION: each agent tries to learn a technique from a random CONTACT who knows something it
// doesn't. Transmission has FIDELITY < 1 -- the lesson can simply fail (garble), so knowledge does not
// spread frictionlessly. This is what a well-connected population does that a fragmented one cannot:
// move a rare technique from its few holders to many before those holders turn over.
{
st._dacc += diffuseRate * P * dd;
let nLearn = Math.floor(st._dacc); st._dacc -= nLearn;
nLearn = Math.min(nLearn, P * 40);
for (let l = 0; l < nLearn; l++) {
const a = Math.floor(rng() * P), nbrs = contacts[a];
if (!nbrs.length) continue;
const b = nbrs[Math.floor(rng() * nbrs.length)];
const kb = agents[b].known, ka = agents[a].known;
if (kb.size === 0) continue;
// pick a technique the teacher knows and the learner lacks
let pick = -1, seen = 0;
kb.forEach(id => { if (!ka.has(id)) { seen++; if (rng() < 1 / seen) pick = id; } }); // reservoir pick
if (pick < 0) continue;
// to LEARN a tech you must already hold its prerequisites (you cannot copy a lesson you have no
// floor for) -- diffusion respects the cumulative structure too.
if (!prereqsMet(ka, pick)) continue;
if (rng() < fidelity) { ka.add(pick); st.diffusions++; } // else the lesson failed -- fidelity < 1
}
}
// ---- (1) INVENTION by RECOMBINATION: each agent tries to combine >=2 techniques it KNOWS. If that
// unordered pair matches a hidden recipe AND the agent holds the child's prerequisites, a NEW technique
// is invented (new to that agent; new to the WORLD if never invented before). The number of pairs grows
// as O(k^2) in the agent's known-count k, so a knowledgeable, VARIED population invents combinatorially
// faster -- the ratchet. A monoculture (everyone knowing the same few) has few novel pairs and stalls.
{
st._acc += inventRate * P * dd;
let nTry = Math.floor(st._acc); st._acc -= nTry;
nTry = Math.min(nTry, P * 40);
for (let it = 0; it < nTry; it++) {
const a = Math.floor(rng() * P), ka = agents[a].known;
if (ka.size < 2) continue;
const arr = Array.from(ka);
const i1 = Math.floor(rng() * arr.length);
let i2 = Math.floor(rng() * arr.length);
let g = 0; while (i2 === i1 && g++ < 8) i2 = Math.floor(rng() * arr.length);
if (i2 === i1) continue;
const child = tree.recipeMap.get(keyOf(arr[i1], arr[i2]));
if (child == null) continue; // that combination invents nothing (most don't)
if (ka.has(child)) continue; // already known
if (!prereqsMet(ka, child)) continue; // the cumulative floor isn't met yet
ka.add(child);
st.invented++;
if (!st.everInvented[child]) { st.everInvented[child] = 1; st._newInventions.push(child); }
}
}
recompute(st);
// ---- (4b) detect LOSS at the population scale: a tech that was EVER invented but is now held by NOBODY
// has been lost from the world (a dark age for that technique). Recorded as a running counter + event.
for (let id = 0; id < st.T; id++) {
if (st.everInvented[id] && st.holders[id] === 0) {
// count it once at the moment it drops to zero this step
if (st._prevHolders && st._prevHolders[id] > 0) { st.lost++; st._lostThisStep.push(id); }
}
}
st._prevHolders = st.holders.slice();
// ---- events -----------------------------------------------------------------------------------------
for (const id of st._newInventions) st.evs.push({ kind: 'invention', tech: id, tier: tree.techs[id].tier });
for (const id of st._lostThisStep) st.evs.push({ kind: 'loss', tech: id, tier: tree.techs[id].tier });
// a BREAKTHROUGH: the population reaches a new maximum tier for the first time
if (st.maxTier > (st._prevMaxTier || 0)) { st.evs.push({ kind: 'breakthrough', tier: st.maxTier }); st._prevMaxTier = st.maxTier; }
return st;
},
fields(st) {
return {
knownCount: st.knownCount, // distinct techniques ALIVE in the population right now
meanHeld: st.meanHeld, // mean techniques per agent
maxTier: st.maxTier, // deepest tier reached (cumulative depth)
diversity: st.diversity, // spread of knowledge across agents (recombination fuel)
invented: st.invented, // cumulative invention events
lost: st.lost, // cumulative techniques lost from the whole population (dark ages)
diffusions: st.diffusions, // cumulative successful teachings
inUseUtility: st.inUseUtility, // mean utility of the best-in-niche tech agents actually USE (obsolescence)
holders: st.holders, // per-tech holder counts
T: st.T
};
},
events(st) { return st.evs; },
// exposed for tests / introspection (not part of the coupling contract)
_tree: tree
};
// ---- observables recompute (module-scoped so init + step share it). Also runs OBSOLESCENCE. -------------
function recompute(st) {
const agents = st.agents, T = st.T, niches = st.niches;
const holders = st.holders; holders.fill(0);
let sumHeld = 0;
for (let a = 0; a < agents.length; a++) {
const ka = agents[a].known;
sumHeld += ka.size;
ka.forEach(id => { holders[id]++; });
// ---- (3) OBSOLESCENCE: for each niche, the agent USES the highest-UTILITY tech it knows, so a better
// later technique displaces a worse earlier one in actual use. (5) prereqs are already enforced at
// learn/invent time, so anything an agent holds is usable.
if (obsolescence) {
const use = agents[a].use; use.fill(-1);
const bestU = new Float32Array(niches); bestU.fill(-1);
ka.forEach(id => {
const tk = st._techs[id]; const n = tk.niche;
if (tk.utility > bestU[n]) { bestU[n] = tk.utility; use[n] = id; }
});
}
}
// distinct techs alive + max tier
let known = 0, maxTier = 0;
for (let id = 0; id < T; id++) if (holders[id] > 0) { known++; const tt = st._techs[id].tier; if (tt > maxTier) maxTier = tt; }
st.knownCount = known;
st.meanHeld = agents.length ? sumHeld / agents.length : 0;
st.maxTier = maxTier;
// DIVERSITY: normalized Shannon entropy of the holder distribution over ALIVE techs -- high when knowledge
// is spread across many different techniques (varied), low when it collapses onto a few (a monoculture).
let totH = 0; for (let id = 0; id < T; id++) totH += holders[id];
let H = 0, aliveN = 0;
if (totH > 0) {
for (let id = 0; id < T; id++) { const h = holders[id]; if (h > 0) { aliveN++; const p = h / totH; H -= p * Math.log(p); } }
}
st.diversity = aliveN > 1 ? H / Math.log(aliveN) : 0;
// mean in-use utility across agents+niches (rises as obsolescence upgrades what people actually use)
let uSum = 0, uN = 0;
if (obsolescence) {
for (let a = 0; a < agents.length; a++) { const use = agents[a].use; for (let n = 0; n < niches; n++) { if (use[n] >= 0) { uSum += st._techs[use[n]].utility; uN++; } } }
}
st.inUseUtility = uN ? uSum / uN : 0;
}
}
return { makeTechnology };
});