← Natura·Docs/Modules/Source

tectonics-test.js

77 lines · in natura-src-1.0.0.zip as natura/tectonics-test.js · a test

// tectonics-test.js -- assertions for Tectonics (drifting plates build mountains at convergent margins).
const N = require('./natura.js');
const { makeTectonics } = require('./tectonics.js');
const { World, UNITS } = N;
const YEAR = UNITS.YEAR, MYR = 1e6 * YEAR;

let pass = 0, fail = 0;
const ok = (c, m) => { if (c) pass++; else { fail++; console.log('  FAIL: ' + m); } };

function run(seed, myrPerStep, steps, cfg) {
  const w = World({ seed, systems: [makeTectonics(Object.assign({ N: 64, elevation: () => 0.05 }, cfg))] });
  for (let i = 0; i < steps; i++) w.step(myrPerStep * MYR);
  return w.get('tectonics');
}
// recompute the convergence field the same way the system does, to classify boundaries
function convergence(st) {
  const { N: n, plate, seeds } = st, conv = new Float32Array(n * n);
  for (let j = 0; j < n; j++) for (let i = 0; i < n; i++) {
    const pA = plate[j * n + i], vA = seeds[pA]; let c = 0;
    for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
      if (!dx && !dy) continue; const ni = i + dx, nj = j + dy; if (ni < 0 || ni >= n || nj < 0 || nj >= n) continue;
      const pB = plate[nj * n + ni]; if (pB === pA) continue; const vB = seeds[pB], nl = Math.hypot(dx, dy);
      c += ((vA.vx - vB.vx) * dx + (vA.vy - vB.vy) * dy) / nl;
    }
    conv[j * n + i] = c;
  }
  return conv;
}
const mean = (xs) => xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0;

console.log('tectonics assertions:');

// 1. DETERMINISM
{
  const a = run(5, 4, 12).elev, b = run(5, 4, 12).elev;
  let same = true; for (let k = 0; k < 64 * 64; k++) if (Math.abs(a[k] - b[k]) > 1e-9) { same = false; break; }
  ok(same, 'same seed -> identical plates + heightfield (reproducible)');
}

// 2. MOUNTAINS at CONVERGENT margins: after tens of Myr the converging boundaries stand higher than the
//    rifting ones, and the flat craton has grown real relief
{
  const st = run(5, 4, 12);                                         // ~48 Myr from a flat 0.05 craton
  const conv = convergence(st), elev = st.elev;
  const upE = [], downE = [];
  for (let k = 0; k < 64 * 64; k++) { if (conv[k] > 0.15) upE.push(elev[k]); else if (conv[k] < -0.15) downE.push(elev[k]); }
  let mx = 0; for (let k = 0; k < 64 * 64; k++) mx = Math.max(mx, elev[k]);
  ok(mx > 0.2, 'a flat craton grows real mountains (max elevation ' + mx.toFixed(2) + ' from 0.05)');
  ok(upE.length && downE.length && mean(upE) > mean(downE) + 0.05, 'convergent margins stand higher than rifting ones (' + mean(upE).toFixed(2) + ' vs ' + mean(downE).toFixed(2) + ')');
}

// 3. It EVOLVES: the heightfield genuinely changes over deep time (it is not the static starting craton)
{
  const st = run(7, 4, 10);
  let changed = 0; for (let k = 0; k < 64 * 64; k++) if (Math.abs(st.elev[k] - 0.05) > 0.02) changed++;
  ok(changed > 100, 'the ground evolves over Myr (' + changed + ' cells moved from the starting height)');
}

// 4. The crust is PARTITIONED into plates
{
  const st = run(7, 4, 2, { plates: 5 }); const s = new Set();
  for (let k = 0; k < 64 * 64; k++) s.add(st.plate[k]);
  ok(s.size >= 3, 'the crust is broken into drifting plates (' + s.size + ' present)');
}

// 5. ISOSTASY: a highland with NO convergence under it (a plate interior) erodes back toward baseline
{
  const w = World({ seed: 5, systems: [makeTectonics({ N: 64, elevation: () => 0.05 })] });
  const st = w.get('tectonics'), conv = convergence(st);
  let idx = -1; for (let k = 0; k < 64 * 64; k++) if (conv[k] === 0) { idx = k; break; }   // a plate-interior cell, no uplift
  if (idx >= 0) st.elev[idx] = 0.6;                                 // drop an artificial highland on it
  for (let i = 0; i < 12; i++) w.step(4 * MYR);                     // ~48 Myr
  ok(idx >= 0 && st.elev[idx] < 0.55, 'isostasy: an interior highland erodes back down (0.60 -> ' + (idx >= 0 ? st.elev[idx].toFixed(2) : 'n/a') + ')');
}

console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);