← Natura·Docs/Modules/Source

daynight-probe.js

54 lines · in natura-src-1.0.0.zip as natura/daynight-probe.js · a probe (a robustness or behaviour check run in node) · its entry in the module reference

// daynight-probe.js -- verify the DAY/NIGHT ACTIVITY verb by RUNNING it (the balance-trap lesson: never reason that
// a behaviour "should" stabilise; run it ON vs OFF, multi-seed, and read the ground truth). Checks (1) it does NOT
// destabilise the ecosystem (populations stay sane vs OFF), and (2) the rhythm actually manifests -- prey move slower
// at night (bedded) and kills concentrate in low light (the nocturnal/crepuscular hunt).
const N = require('./natura.js');
const { makeWeather } = require('./weather.js');
const { makeVegetation } = require('./vegetation.js');
const { makeEcosystem } = require('./ecosystem.js');
const { World, UNITS } = N;
const GRID = 40;

function run(seed, dayNight) {
  const w = World({ seed, systems: [makeWeather({ N: GRID }), makeVegetation({ N: GRID, weather: 'weather', waterK: 13, drainDays: 40 }), makeEcosystem({ N: GRID })] });
  for (let i = 0; i < 60 * 6; i++) w.step(UNITS.HOUR * 4);        // grow vegetation + settle
  const eco = w.get('ecosystem');
  let hmin = 1e9, hmax = 0, hsum = 0, pmin = 1e9, pmax = 0, psum = 0, n = 0;
  let killsLow = 0, killsDay = 0, prevKills = (eco.killsYoung + eco.killsAdult);
  let spdNightSum = 0, spdNightN = 0, spdDaySum = 0, spdDayN = 0;
  for (let d = 0; d < 220; d++) {
    for (let s = 0; s < 6; s++) {
      if (dayNight) eco.__tod = w.clock.dayPhase();   // feed the sim the time of day (else leave inert)
      w.step(UNITS.HOUR * 4);
      const tod = w.clock.dayPhase(), c = Math.cos(tod * 2 * Math.PI), lowLight = 1 - Math.max(0, -c);
      const dk = (eco.killsYoung + eco.killsAdult) - prevKills; prevKills += dk;
      if (dayNight) { if (lowLight > 0.6) killsLow += dk; else killsDay += dk; }
      // sample mean herbivore speed, bucketed by deep-night vs day
      const herbs = eco.agents.filter(a => a.alive && a.sp === 0);
      if (herbs.length) { let sp = 0; for (const a of herbs) sp += Math.hypot(a.vx, a.vy); sp /= herbs.length;
        if (c > 0.5) { spdNightSum += sp; spdNightN++; } else if (c < -0.5) { spdDaySum += sp; spdDayN++; } }
    }
    const p = eco.pop; hmin = Math.min(hmin, p.herbivore); hmax = Math.max(hmax, p.herbivore); hsum += p.herbivore;
    pmin = Math.min(pmin, p.predator); pmax = Math.max(pmax, p.predator); psum += p.predator; n++;
  }
  return {
    hMean: hsum / n, hMin: hmin, hMax: hmax, pMean: psum / n, pMin: pmin, pMax: pmax,
    killsLow, killsDay, spdNight: spdNightN ? spdNightSum / spdNightN : 0, spdDay: spdDayN ? spdDaySum / spdDayN : 0,
    alive: eco.pop.herbivore > 0 && eco.pop.predator > 0
  };
}

let fail = 0; const ok = (c, m) => { if (!c) { console.log('  FAIL:', m); fail++; } else console.log('  ok:', m); };
for (const seed of [3, 7, 11]) {
  console.log('\n== seed ' + seed + ' ==');
  const off = run(seed, false), on = run(seed, true);
  console.log('  OFF: herb ' + off.hMin + '..' + off.hMax + ' (mean ' + off.hMean.toFixed(0) + ')  pred ' + off.pMin + '..' + off.pMax + ' (mean ' + off.pMean.toFixed(0) + ')');
  console.log('  ON : herb ' + on.hMin + '..' + on.hMax + ' (mean ' + on.hMean.toFixed(0) + ')  pred ' + on.pMin + '..' + on.pMax + ' (mean ' + on.pMean.toFixed(0) + ')');
  console.log('  ON rhythm: kills low-light=' + on.killsLow + ' vs daylight=' + on.killsDay + ' | prey speed night=' + on.spdNight.toFixed(4) + ' day=' + on.spdDay.toFixed(4));
  ok(on.alive, 'day/night world stays alive (both species present at end)');
  ok(on.hMean > off.hMean * 0.4 && on.hMean < off.hMean * 2.5, 'herbivore mean stays in the same ballpark as OFF (not destabilised)');
  ok(on.spdNight < on.spdDay * 0.85, 'prey move SLOWER at night than by day (bedded down)');
  ok(on.killsLow > on.killsDay, 'predation concentrates in LOW LIGHT (the nocturnal/crepuscular hunt)');
}
console.log(fail ? '\nFAILED (' + fail + ')' : '\nALL PASS');
process.exit(fail ? 1 : 0);