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