← Natura·Docs/Modules/Source

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);