technology-test.js
282 lines · in natura-src-1.0.0.zip as natura/technology-test.js · a test
// technology-test.js -- the KNOWLEDGE / TECHNOLOGY system, proven by RUNNING (society-test.js discipline: no
// emergence is asserted from reasoning; each is measured off a live run and the numbers are printed). The
// system spreads INFORMATION WITH CONTENT -- techniques with prerequisites, niches, and hidden recipes -- so
// the emergences are the ones a mere scalar "tech level" can never show:
// (a) cumulative innovation ACCELERATES -- more known techs -> more recombinations -> super-linear growth;
// (b) a SUPERIOR technique SWEEPS the population on an adoption S-curve (diffusion + obsolescence);
// (c) knowledge can be LOST -- a small / high-turnover / poorly-connected people falls into a DARK AGE that
// a large, well-connected one does not (compare final distinct-tech counts);
// (d) DIVERSITY of held ideas raises the discovery frontier -- a monoculture, recombining forever, reaches
// only the few techs its shared subset unlocks; a varied population reaches many more.
// Plus the invariants: determinism, boundedness, the cumulative PREREQUISITE floor, and transmission fidelity.
const N = require('./natura.js');
const { makeTechnology } = require('./technology.js');
const { World, UNITS } = N;
let pass = 0, fail = 0;
const ok = (c, m) => { if (c) pass++; else { fail++; console.log(' FAIL: ' + m); } };
// convenience: run a lone Technology world and return { world, sys, field-at-end }
function makeWorld(cfg, seed) {
const sys = makeTechnology(cfg);
const w = World({ seed: seed == null ? 1 : seed, systems: [sys] });
return { w, sys };
}
console.log('technology assertions:');
// ---------------------------------------------------------------------------------------------------------
// 0. DETERMINISM -- same seed + config -> byte-identical trajectory (the whole point of the framework). The
// hidden tech-tree, the invention/diffusion/loss draws, every observable must reproduce.
{
function fp(seed) {
const { w } = makeWorld({ population: 60, turnover: 0.02, inventRate: 0.5, diffuseRate: 2 }, seed);
for (let d = 0; d < 150; d++) w.step(UNITS.DAY);
const f = w.fields().technology;
return [f.knownCount, f.invented, f.lost, f.diffusions, f.maxTier, f.diversity.toFixed(6), f.inUseUtility.toFixed(6)].join('|');
}
const a = fp(9), b = fp(9), c = fp(10);
ok(a === b, 'same seed -> identical technology trajectory (known/invented/lost/diffusions/tier/diversity all reproduce)');
ok(a !== c, 'a different seed gives a different trajectory (the rng actually drives the run)');
console.log(' determinism fingerprint (seed 9): ' + a);
}
// ---------------------------------------------------------------------------------------------------------
// 1. BOUNDED + FINITE -- every observable stays finite and in range; holder counts never exceed the
// population; knownCount never exceeds the tree size.
{
const { w, sys } = makeWorld({ population: 100, turnover: 0.03, inventRate: 0.6, diffuseRate: 3 }, 5);
const T = sys._tree.count;
for (let d = 0; d < 300; d++) w.step(UNITS.DAY);
const f = w.fields().technology;
let good = isFinite(f.meanHeld) && isFinite(f.inUseUtility)
&& f.knownCount >= 0 && f.knownCount <= T
&& f.diversity >= -1e-9 && f.diversity <= 1 + 1e-9
&& f.inUseUtility >= -1e-9 && f.inUseUtility <= 1 + 1e-9;
for (let id = 0; id < T; id++) if (f.holders[id] < 0 || f.holders[id] > 100) good = false;
ok(good, 'all observables finite + in range; holders <= population; knownCount <= tree size (' + f.knownCount + '/' + T + ')');
}
// ---------------------------------------------------------------------------------------------------------
// (a) CUMULATIVE INNOVATION ACCELERATES -- the ratchet. Recombination makes new pairs available as the
// known-count grows, so the distinct-tech count climbs SUPER-LINEARLY before it saturates against the
// tree ceiling. Two complementary measurements: (a1) the discovery frontier grows SUPER-LINEARLY in the
// amount of starting knowledge -- a SCALING LAW, because reachable recipes ~ pairs ~ k^2, so doubling the
// inputs MORE than doubles the discoveries; (a2) run in time, the distinct-tech count climbs strongly and
// the population reaches deep tiers it could never reach from primitives alone (the tiers unlock in order).
{
// (a1) SCALING LAW: give the population a shared pool of k distinct primitives (spread across agents) and
// count how many distinct techs it can ever discover. reachable ~ pairs, so frontier should be CONVEX in k.
const base = { population: 150, tiers: 7, perTier: 12, niches: 6, inventRate: 1.0, diffuseRate: 4, fidelity: 0.95, seedTechs: 0, meanContacts: 12 };
const t0 = makeTechnology(base)._tree.byTier[0];
function frontier(k) {
const sys = makeTechnology(base); const w = World({ seed: 41, systems: [sys] }); const st = w.get('technology');
for (let a = 0; a < base.population; a++) { const kn = st.agents[a].known; kn.clear(); const off = (a * 3) % Math.max(1, k); for (let s = 0; s < Math.min(k, 4); s++) kn.add(t0[(off + s) % k]); }
for (let i = 0; i < k; i++) st.agents[i % base.population].known.add(t0[i]); // collectively hold all k
for (let d = 0; d < 300; d++) w.step(UNITS.DAY);
return w.fields().technology.knownCount;
}
const f3 = frontier(3), f6 = frontier(6), f12 = frontier(12);
ok(f6 > 2 * f3 && f12 > 2 * f6, 'the discovery frontier grows SUPER-LINEARLY in starting knowledge -- doubling the primitives MORE than doubles the techs reached (k=3 -> ' + f3 + ', k=6 -> ' + f6 + ', k=12 -> ' + f12 + '; a combinatorial ratchet)');
ok(f12 / f6 > f6 / f3, 'the acceleration itself accelerates -- the frontier multiplier RISES as knowledge grows (x' + (f6 / f3).toFixed(1) + ' then x' + (f12 / f6).toFixed(1) + ' per doubling)');
console.log(' ratchet scaling law: k=3 -> ' + f3 + ' techs | k=6 -> ' + f6 + ' | k=12 -> ' + f12);
}
{
// (a2) IN TIME: a lone population climbs from a few primitives to a rich, DEEP tech base, reaching tiers it
// could not touch at the start (each tier's recipes only open once the tier below is known -- cumulative).
const cfg = { population: 100, tiers: 9, perTier: 10, niches: 7, inventRate: 0.5, diffuseRate: 3, fidelity: 0.85, seedTechs: 4, meanContacts: 10 };
const { w, sys } = makeWorld(cfg, 3);
const T = sys._tree.count;
const known0 = w.fields().technology.knownCount;
let tierEarly = 0;
for (let d = 0; d < 500; d++) { w.step(UNITS.DAY); if (d === 4) tierEarly = w.fields().technology.maxTier; } // day 5: still climbing
const f = w.fields().technology;
ok(f.knownCount > known0 * 3, 'a population climbs from a few seeds to a rich tech base over time (' + known0 + ' -> ' + f.knownCount + ' distinct techs, tree ' + T + ')');
ok(f.maxTier >= 6 && f.maxTier > tierEarly, 'the tech tree is climbed TIER BY TIER -- deep techs are reached only after their floors are (maxTier ' + tierEarly + ' @day5 -> ' + f.maxTier + ' @end; a high tech cannot appear before its floor)');
console.log(' time-climb: known ' + known0 + ' -> ' + f.knownCount + ' | maxTier @5d=' + tierEarly + ' -> ' + f.maxTier);
}
// ---------------------------------------------------------------------------------------------------------
// (b) A SUPERIOR TECHNIQUE SWEEPS THE POPULATION on an adoption S-CURVE. Plant one tech in 2 agents (everyone
// already holds its prerequisites, so we isolate the spread of THIS tech), turn invention off, and let
// DIFFUSION carry it. The adoption fraction traces a sigmoid: a slow start (few teachers), an accelerating
// shoulder (the mid-window gain exceeds the early one), and saturation near 1 (a sweep). We also confirm
// OBSOLESCENCE: once learned, the superior tech is the one agents actually USE in its niche.
{
const cfg = { population: 200, tiers: 6, perTier: 8, niches: 6, inventRate: 0, diffuseRate: 0.08, fidelity: 0.9, seedTechs: 4, meanContacts: 5 };
const sys = makeTechnology(cfg); const w = World({ seed: 11, systems: [sys] }); const st = w.get('technology');
const tree = sys._tree, T = tree.count;
let planted = -1; for (let id = 0; id < T; id++) if (tree.techs[id].tier === 1) { planted = id; break; }
const pr = tree.techs[planted].prereqs, niche = tree.techs[planted].niche;
// everyone gets the prerequisites (so anyone CAN learn it); 2 agents get the tech itself (the seed carriers)
for (let a = 0; a < cfg.population; a++) pr.forEach(p => st.agents[a].known.add(p));
for (let a = 0; a < 2; a++) st.agents[a].known.add(planted);
st.everInvented[planted] = 1;
const frac = [];
for (let d = 0; d < 640; d++) { w.step(UNITS.DAY); frac.push(w.fields().technology.holders[planted] / cfg.population); }
const at = (d) => frac[d];
const early = at(119) - at(39), mid = at(239) - at(119), late = at(639) - at(239);
ok(at(639) > 0.9, 'the superior technique SWEEPS the population -- near-total adoption (final ' + (at(639) * 100).toFixed(0) + '% of agents hold it, from a 2-agent seed)');
ok(mid > early && mid > late, 'adoption is an S-CURVE -- the mid-window gain exceeds both the slow start and the saturating tail (early +' + (early * 100).toFixed(0) + '%, mid +' + (mid * 100).toFixed(0) + '%, late +' + (late * 100).toFixed(0) + '%)');
// obsolescence: the swept tech is now the in-use tech for its niche in agents that hold it and nothing better
let usingIt = 0, holdIt = 0;
for (let a = 0; a < cfg.population; a++) { if (st.agents[a].known.has(planted)) { holdIt++; if (st.agents[a].use[niche] === planted) usingIt++; } }
ok(holdIt > 0 && usingIt > 0, 'OBSOLESCENCE in use -- the swept tech becomes the tech agents actually USE in its niche (' + usingIt + '/' + holdIt + ' holders use it)');
console.log(' sweep S-curve: adoption every 80d ' + [39, 119, 199, 279, 359].map(d => (frac[d] * 100).toFixed(0) + '%').join(' -> '));
}
// ---------------------------------------------------------------------------------------------------------
// (b2) OBSOLESCENCE proper: a LATER, higher-utility tech for the SAME niche displaces an earlier one in use.
// Give agents two techs in one niche (a weak tier-0 and a strong tier-1); the mean in-use utility must
// reflect the BETTER one, and turning obsolescence off must change nothing about what is known but lower
// the in-use utility (people no longer upgrade what they use).
{
const cfg = { population: 40, tiers: 4, perTier: 6, niches: 4, inventRate: 0, diffuseRate: 0, seedTechs: 0 };
// find two techs sharing ONE niche with different utility; hand every agent BOTH (plus prereqs), then read
// which one each agent actually USES in that niche -- the better must displace the worse.
function inUse(obsolescence) {
const sys = makeTechnology(Object.assign({}, cfg, { obsolescence })); const w = World({ seed: 44, systems: [sys] }); const st = w.get('technology');
const tree = sys._tree;
let lo = -1, hi = -1, niche = -1;
for (let n = 0; n < cfg.niches; n++) {
const inN = []; for (let id = 0; id < tree.count; id++) if (tree.techs[id].niche === n) inN.push(id);
if (inN.length >= 2) { inN.sort((a, b) => tree.techs[a].utility - tree.techs[b].utility); lo = inN[0]; hi = inN[inN.length - 1]; niche = n; break; }
}
for (let a = 0; a < cfg.population; a++) { st.agents[a].known.clear(); (tree.techs[hi].prereqs || []).forEach(p => st.agents[a].known.add(p)); st.agents[a].known.add(lo); st.agents[a].known.add(hi); }
w.step(UNITS.DAY);
// count how many agents USE the better tech (vs the worse) in that specific niche
let useHi = 0, useLo = 0;
for (let a = 0; a < cfg.population; a++) { const u = st.agents[a].use[niche]; if (u === hi) useHi++; else if (u === lo) useLo++; }
return { useHi, useLo, loU: tree.techs[lo].utility, hiU: tree.techs[hi].utility, meanUtil: w.fields().technology.inUseUtility };
}
const on = inUse(true), off = inUse(false);
ok(on.useHi === 40 && on.useLo === 0, 'the BETTER tech DISPLACES the worse in use -- every agent that holds both USES the higher-utility tech in the niche (' + on.useHi + '/' + 40 + ' use the better, ' + on.useLo + ' the worse; util ' + on.hiU.toFixed(2) + ' vs ' + on.loU.toFixed(2) + ')');
ok(off.meanUtil === 0, 'obsolescence OFF is a clean toggle -- nothing is marked in-use (in-use utility ' + off.meanUtil.toFixed(3) + ')');
console.log(' obsolescence: better util ' + on.hiU.toFixed(2) + ' displaces weaker ' + on.loU.toFixed(2) + ' in ' + on.useHi + '/40 agents');
}
// ---------------------------------------------------------------------------------------------------------
// (c) KNOWLEDGE CAN BE LOST -- the DARK AGE. A SMALL, poorly-connected, high-turnover population cannot move
// its rare techniques off their few holders before those holders die, so techniques VANISH from the whole
// world (holders drop to zero after having existed). A LARGE, well-connected population under the SAME
// turnover retains far more: the same death rate, but knowledge is spread widely enough to survive it.
// Compare final distinct-tech counts and the cumulative LOSS counter.
{
const common = { tiers: 7, perTier: 8, niches: 6, fidelity: 0.7, inventRate: 1.2, diffuseRate: 3, seedTechs: 4, turnover: 0.05, inherit: 0.2 };
function runPop(o) {
const { w } = makeWorld(Object.assign({}, common, o), 5);
for (let d = 0; d < 400; d++) w.step(UNITS.DAY);
const f = w.fields().technology; return { known: f.knownCount, lost: f.lost, maxTier: f.maxTier };
}
const small = runPop({ population: 18, meanContacts: 2 }); // few minds, fragmented network
const large = runPop({ population: 200, meanContacts: 10 }); // many minds, well connected
ok(large.known > small.known + 5, 'a LARGE, well-connected people RETAINS more knowledge than a small, fragmented one under identical turnover (' + large.known + ' vs ' + small.known + ' distinct techs)');
ok(small.lost > large.lost, 'the small fragmented people suffers MORE LOSS -- techniques vanish from the world (' + small.lost + ' techs lost to a DARK AGE vs ' + large.lost + ' in the large one)');
ok(small.lost > 0, 'knowledge is genuinely LOST -- techniques that once existed are held by NOBODY (' + small.lost + ' techniques erased from the small population)');
console.log(' dark age: small(N18,deg2) known=' + small.known + ' lost=' + small.lost + ' | large(N200,deg10) known=' + large.known + ' lost=' + large.lost);
}
// ---------------------------------------------------------------------------------------------------------
// (c2) TURNOVER + INHERITANCE is the knob: hold size/connectivity fixed and raise turnover, and loss rises;
// let newborns INHERIT more of a parent's knowledge and loss falls. Isolates the forgetting mechanism.
{
const base = { population: 40, tiers: 7, perTier: 8, niches: 6, fidelity: 0.7, inventRate: 1.5, diffuseRate: 2.5, seedTechs: 4, meanContacts: 4 };
function loss(turnover, inherit) {
const { w } = makeWorld(Object.assign({}, base, { turnover, inherit }), 6);
for (let d = 0; d < 300; d++) w.step(UNITS.DAY);
return w.fields().technology.lost;
}
const calm = loss(0.005, 0.2), churny = loss(0.06, 0.2);
const churnyInherit = loss(0.06, 0.9);
ok(churny > calm, 'higher TURNOVER erases more knowledge -- more deaths outrun transmission (' + calm + ' lost at low turnover -> ' + churny + ' at high)');
ok(churnyInherit < churny, 'stronger INHERITANCE (newborns keep more of a parent) protects against loss (' + churny + ' lost at inherit=0.2 -> ' + churnyInherit + ' at inherit=0.9, same churn)');
console.log(' forgetting: lost calm=' + calm + ' churn=' + churny + ' churn+inherit=' + churnyInherit);
}
// ---------------------------------------------------------------------------------------------------------
// (d) DIVERSITY RAISES THE DISCOVERY FRONTIER -- recombination needs VARIETY. Two populations start with the
// SAME AMOUNT of knowledge (same techs-per-agent), diffusion OFF so nothing mixes, but one is DIVERSE
// (each agent a different varied subset of primitives) and one a MONOCULTURE (every agent the same
// subset). The monoculture can only ever invent the few recipes its single shared subset unlocks -- it
// reinvents the same handful across every agent; the diverse population reaches many more distinct techs.
{
const base = { population: 120, tiers: 6, perTier: 10, niches: 6, inventRate: 0.8, diffuseRate: 0, seedTechs: 0 };
const t0 = makeTechnology(base)._tree.byTier[0];
const K = 5;
function run(diverse, seed) {
const sys = makeTechnology(base); const w = World({ seed, systems: [sys] }); const st = w.get('technology');
for (let a = 0; a < base.population; a++) {
const k = st.agents[a].known; k.clear();
if (diverse) { for (let s = 0; s < K; s++) k.add(t0[(a * 7 + s * 3) % t0.length]); } // varied per-agent subset
else { for (let s = 0; s < K; s++) k.add(t0[s]); } // identical subset for all
}
const startDiv = w.fields().technology.diversity;
for (let d = 0; d < 120; d++) w.step(UNITS.DAY);
const f = w.fields().technology;
return { known: f.knownCount, invented: f.invented, startDiv };
}
const div = run(true, 21), mono = run(false, 21);
ok(div.known > mono.known * 2, 'a DIVERSE population reaches a far larger discovery frontier than a MONOCULTURE with the same knowledge-per-agent (' + div.known + ' vs ' + mono.known + ' distinct techs)');
ok(mono.invented > 0 && div.known > mono.known, 'the monoculture INVENTS plenty but discovers FEW distinct techs -- it reinvents the same recipes; variety is what reaches new corners of the tree (mono ' + mono.invented + ' invention-events yet only ' + mono.known + ' distinct techs)');
// robustness across seeds
let robust = true;
for (const s of [22, 23, 24]) { if (!(run(true, s).known > run(false, s).known * 2)) robust = false; }
ok(robust, 'diversity > monoculture holds across seeds (the effect is structural, not a lucky draw)');
console.log(' diversity: diverse-known=' + div.known + ' mono-known=' + mono.known + ' (mono invention-events ' + mono.invented + ')');
}
// ---------------------------------------------------------------------------------------------------------
// 2. THE CUMULATIVE PREREQUISITE FLOOR -- a high tech is NEVER known without the lower techs it stands on.
// Scan a fully-developed population: every tech any agent holds must have all its prerequisites held too.
{
const { w, sys } = makeWorld({ population: 120, tiers: 7, perTier: 8, niches: 6, inventRate: 1.5, diffuseRate: 4, seedTechs: 4 }, 13);
const tree = sys._tree;
for (let d = 0; d < 300; d++) w.step(UNITS.DAY);
const st = w.get('technology');
let violations = 0, checked = 0, deepHeld = 0;
for (let a = 0; a < st.agents.length; a++) {
const known = st.agents[a].known;
known.forEach(id => {
checked++; if (tree.techs[id].tier >= 3) deepHeld++;
const pr = tree.techs[id].prereqs;
for (let i = 0; i < pr.length; i++) if (!known.has(pr[i])) violations++;
});
}
ok(violations === 0, 'the CUMULATIVE FLOOR holds -- no agent ever holds a tech without ALL its prerequisites (' + checked + ' holdings checked, ' + violations + ' violations)');
ok(deepHeld > 0, 'the tree is genuinely climbed -- agents hold deep (tier >= 3) techs built on tall prerequisite stacks (' + deepHeld + ' deep holdings)');
}
// ---------------------------------------------------------------------------------------------------------
// 3. FIDELITY < 1 -- transmission is imperfect. With a low fidelity, a tech spreads SLOWER (more lessons fail)
// than with a high fidelity, all else equal. Proven by the adoption speed of a planted tech.
{
function spreadSpeed(fidelity) {
const cfg = { population: 150, tiers: 6, perTier: 8, niches: 6, inventRate: 0, diffuseRate: 0.15, fidelity, seedTechs: 4, meanContacts: 6 };
const sys = makeTechnology(cfg); const w = World({ seed: 17, systems: [sys] }); const st = w.get('technology');
const tree = sys._tree; let planted = -1; for (let id = 0; id < tree.count; id++) if (tree.techs[id].tier === 1) { planted = id; break; }
const pr = tree.techs[planted].prereqs;
for (let a = 0; a < cfg.population; a++) pr.forEach(p => st.agents[a].known.add(p));
for (let a = 0; a < 3; a++) st.agents[a].known.add(planted);
st.everInvented[planted] = 1;
let days = 0; for (; days < 2000; days++) { w.step(UNITS.DAY); if (w.fields().technology.holders[planted] / cfg.population > 0.5) break; }
return days;
}
const hi = spreadSpeed(0.95), lo = spreadSpeed(0.35);
ok(lo > hi, 'FIDELITY matters -- a garbly (low-fidelity) channel spreads a tech to half the people SLOWER than a faithful one (' + hi + ' days at fidelity 0.95 vs ' + lo + ' days at 0.35)');
console.log(' fidelity: days-to-50% adoption hi-fidelity=' + hi + ' lo-fidelity=' + lo);
}
// ---------------------------------------------------------------------------------------------------------
// 4. EVENTS fire -- invention, breakthrough (a new deepest tier), and loss are emitted for a coupled system to
// react to. A rich developing-then-collapsing run should emit all three kinds.
{
const { w } = makeWorld({ population: 30, tiers: 7, perTier: 8, niches: 6, inventRate: 1.5, diffuseRate: 2, fidelity: 0.7, seedTechs: 4, meanContacts: 3, turnover: 0.06, inherit: 0.15 }, 8);
const kinds = {};
for (let d = 0; d < 400; d++) { w.step(UNITS.DAY); for (const e of w.events()) kinds[e.kind] = (kinds[e.kind] || 0) + 1; }
ok((kinds.invention || 0) > 0 && (kinds.breakthrough || 0) > 0, 'INVENTION + BREAKTHROUGH events fire as the tree is climbed (' + (kinds.invention || 0) + ' inventions, ' + (kinds.breakthrough || 0) + ' breakthroughs)');
ok((kinds.loss || 0) > 0, 'LOSS events fire when techniques vanish (' + (kinds.loss || 0) + ' loss events -- a coupled system could feel the dark age)');
console.log(' events: ' + JSON.stringify(kinds));
}
console.log('\n' + pass + ' passed, ' + fail + ' failed');
process.exit(fail ? 1 : 0);