← Natura·Docs/Modules/Source

weather-test.js

93 lines · in natura-src-1.0.0.zip as natura/weather-test.js · a test

// weather-test.js -- assertions for the Weather system. node weather-test.js.
const N = require('./natura.js');
const { makeWeather } = require('./weather.js');
const { World, UNITS } = N;

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

// a test terrain: a western OCEAN, then land rising to a N-S RIDGE at x=0, then land to the east.
// prevailing wind blows EAST -> moist sea air should rain on the ridge's WEST (windward) slope and
// leave the EAST (lee) slope dry: an orographic rain-shadow.
function terrain(x, y) {
  if (x < -0.55) return -0.2;                         // western ocean
  const ridge = 0.6 * Math.exp(-((x) * (x)) / 0.03);  // ridge at x=0
  return 0.15 + ridge;
}
const cfg = { N: 48, elevation: terrain, seaLevel: 0 };
const gx = (i, Nn) => (i / (Nn - 1)) * 2 - 1;

function runWorld(seed, steps) {
  const w = World({ seed, systems: [makeWeather(cfg)] });
  for (let i = 0; i < steps; i++) w.step(UNITS.HOUR);
  return w;
}
function meanBand(F, Nn, xlo, xhi) {   // mean of a field over an x-band (all y), land only
  let s = 0, n = 0;
  for (let j = 0; j < Nn; j++) for (let i = 0; i < Nn; i++) {
    const x = gx(i, Nn); if (x >= xlo && x <= xhi) { s += F[j * Nn + i]; n++; }
  }
  return n ? s / n : 0;
}

console.log('weather system assertions:');

// finite + no blowup over a long run
{
  const w = runWorld(7, 200);
  const st = w.get('weather');
  let nan = false, mx = 0;
  for (let k = 0; k < st.moist.length; k++) { if (!isFinite(st.moist[k]) || !isFinite(st.temp[k]) || !isFinite(st.hum[k])) nan = true; mx = Math.max(mx, st.moist[k]); }
  ok(!nan, 'no NaN/Infinity over 200 steps');
  ok(mx > 0, 'some moisture accumulated (it rained somewhere)');
  ok(mx < 1e6, 'moisture bounded (no runaway)');
}

// determinism
{
  const a = runWorld(11, 60).get('weather'), b = runWorld(11, 60).get('weather');
  let same = true; for (let k = 0; k < a.moist.length; k++) if (a.moist[k] !== b.moist[k]) { same = false; break; }
  ok(same, 'same seed -> identical moisture field (reproducible)');
}

// THE key realism test: orographic RAIN-SHADOW. windward (west, x<0) wetter than lee (east, x>0).
{
  const w = runWorld(3, 300);
  const st = w.get('weather');
  const windward = meanBand(st.moist, st.N, -0.45, -0.1);   // west slope, into the wind
  const lee = meanBand(st.moist, st.N, 0.1, 0.45);           // east slope, in the shadow
  ok(windward > lee * 1.3, 'orographic rain-shadow: windward moisture > lee (windward ' + windward.toFixed(3) + ' vs lee ' + lee.toFixed(3) + ')');
  console.log('  windward moisture ' + windward.toFixed(3) + ', lee moisture ' + lee.toFixed(3) + ' (ratio ' + (windward / (lee + 1e-6)).toFixed(2) + 'x)');
}

// temperature responds to elevation (colder on the ridge than the lowland) and stays sane
{
  const st = runWorld(5, 40).get('weather');
  const ridge = meanBand(st.temp, st.N, -0.06, 0.06);        // over the ridge crest
  const low = meanBand(st.temp, st.N, 0.6, 0.95);            // eastern lowland
  ok(ridge < low, 'the ridge is colder than the lowland (lapse rate): ridge ' + ridge.toFixed(1) + ' vs low ' + low.toFixed(1));
}

// the pressure systems MOVE (dynamic weather) + storms can fire as events
{
  const w = World({ seed: 9, systems: [makeWeather(cfg)] });
  const s0 = w.get('weather').sys.map(p => ({ x: p.x, y: p.y }));
  let anyEvent = false;
  for (let i = 0; i < 120; i++) { const r = w.step(UNITS.HOUR); if (r.events.length) anyEvent = true; }
  const s1 = w.get('weather').sys;
  const moved = s1.some((p, k) => Math.abs(p.x - s0[k].x) > 0.05 || Math.abs(p.y - s0[k].y) > 0.05);
  ok(moved, 'pressure systems drift over time (dynamic weather)');
  ok(anyEvent, 'storm events fire when a strong low is inside the domain');
}

// COUPLING shape: the moisture field is exposed for other systems to read, keyed by system name
{
  const w = World({ seed: 1, systems: [makeWeather(cfg)] });
  w.step(UNITS.HOUR);
  const f = w.fields();
  ok(f.weather && f.weather.moisture && f.weather.moisture.length === 48 * 48, 'weather exposes a moisture grid field for coupling');
  ok(f.weather.rain && f.weather.wind === undefined && f.weather.windU, 'exposes rain + wind components + land mask');
}

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