← Natura·Docs/Modules/Source

quickstart-test.js

117 lines · in natura-src-1.0.0.zip as natura/quickstart-test.js · a test

// quickstart-test.js -- the QUICKSTART from the Natura page (neap.systems/natura/), run as a test: two library
// systems (weather, vegetation) and one defined here, composed into one World, stepped, and a coupled field read.
// The page shows the code between the "example" markers below exactly as it is here, so the page cannot drift from
// code that runs. Then assertions: the coupling is real (plants grow only on the weather's rain, and a rain-shadow
// greens the windward slope), the events arrive, the run is deterministic, and the same files load the way a
// browser loads them (<script> tags defining the globals Natura, Weather, Vegetation). node quickstart-test.js
'use strict';
const Natura = require('./natura.js');
const Weather = require('./weather.js');
const Vegetation = require('./vegetation.js');

function example() {
// --- example: begin ---
const { World, UNITS } = Natura;

// Two systems from the library. A module exports a factory; the factory returns a system definition.
// The map is the square [-1,1] x [-1,1]: here a ridge runs north-south down the middle, with sea at the corners.
const ground = (x, y) => 0.5 * Math.exp(-(x * x) / 0.05) - 0.3 * (x * x + y * y) + 0.1;
const weather = Weather.makeWeather({ N: 32, elevation: ground });   // exposes rain, temp, wind, land ...
const plants = Vegetation.makeVegetation({ N: 32 });                // reads ctx.fields.weather

// A system of your own has the same shape as every system in the library.
const gauge = {
  name: 'gauge', domain: 'physical', scale: 'local',
  init(rng, params, clock) { return { total: 0, lastHour: 0 }; },   // rng: this system's own seeded stream
  step(s, ctx, dt) {
    const w = ctx.fields.weather;             // the weather's fields as they were before this tick
    if (!w) return s;
    let sum = 0;
    for (let k = 0; k < w.rain.length; k++) sum += w.rain[k];
    s.lastHour = sum;
    s.total += sum;
    return s;
  },
  fields(s) { return { totalRain: s.total }; },                         // what others may read
  events(s) { return s.lastHour > 15 ? [{ kind: 'downpour', rain: s.lastHour }] : []; }
};

// A world is a seed and a stack of systems on one clock. Same seed, same steps: the same world.
const world = World({ seed: 7, t0: 0, systems: [weather, plants, gauge] });
const events = world.run(24 * 90, UNITS.HOUR);   // 90 days in hourly steps; returns every event

// Read the fields: grid fields are Float32Array(N*N), row-major, cell (i,j) at index j*N+i.
const f = world.fields();
const N = f.vegetation.N, green = f.vegetation.biomass;
let west = 0, east = 0, nw = 0, ne = 0;
for (let j = 0; j < N; j++) for (let i = 0; i < N; i++) {
  const k = j * N + i, x = i / (N - 1) * 2 - 1;
  if (!f.vegetation.land[k]) continue;
  if (x < -0.15) { west += green[k]; nw++; } else if (x > 0.15) { east += green[k]; ne++; }
}
console.log('after', world.clock.t / UNITS.DAY, 'days (' + world.clock.season() + ')');
console.log('plants, windward west:', (west / nw).toFixed(3), ' lee east:', (east / ne).toFixed(3));
console.log('rain gauged:', f.gauge.totalRain.toFixed(0), ' downpours:', events.filter(e => e.kind === 'downpour').length,
            ' storm events:', events.filter(e => e.kind === 'storm').length);
// --- example: end ---
  return { world, f, events, west: west / nw, east: east / ne, N };
}

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

console.log('quickstart (the example as the page shows it):');
const A = example();
console.log('\nquickstart assertions:');
ok(A.f.vegetation.biomass instanceof Float32Array && A.f.vegetation.biomass.length === A.N * A.N,
   'vegetation.biomass is a Float32Array of N*N');
ok(Array.from(A.f.vegetation.biomass).every(Number.isFinite), 'every biomass value is finite');
ok(A.world.clock.t === 90 * Natura.UNITS.DAY, 'the clock advanced 90 days');
ok(A.west > 3 * A.east && A.west > 0.1,
   'coupling: the windward (west) slope is greener than the lee, a rain-shadow: ' + A.west.toFixed(3) + ' vs ' + A.east.toFixed(3));
ok(A.f.gauge.totalRain > 0, 'the gauge read the weather\'s rain through ctx.fields');
const dp = A.events.filter(e => e.kind === 'downpour');
ok(dp.length > 0 && dp.every(e => e.system === 'gauge' && typeof e.t === 'number'),
   'events arrive tagged with their system and time: ' + dp.length + ' downpours');
ok(A.events.some(e => e.system === 'weather' && e.kind === 'storm'), 'the weather system emits its own storm events');

// Without the weather there is no rain in ctx.fields, so the plants never grow: the coupling is the only source.
{
  const w = Natura.World({ seed: 7, systems: [Vegetation.makeVegetation({ N: 32 })] });
  w.run(24 * 30, Natura.UNITS.HOUR);
  ok(Array.from(w.fields().vegetation.biomass).every(v => v === 0), 'vegetation alone (no weather) grows nothing');
}

// Determinism: the same seed and the same steps give the same world; another seed gives another.
{
  const make = (seed) => {
    const w = Natura.World({ seed, systems: [Weather.makeWeather({ N: 32, elevation: (x, y) => 0.3 - 0.4 * (x * x + y * y) }),
                                             Vegetation.makeVegetation({ N: 32 })] });
    w.run(24 * 20, Natura.UNITS.HOUR);
    return Array.from(w.fields().vegetation.biomass);
  };
  const a = make(11), b = make(11), c = make(12);
  ok(a.every((v, k) => v === b[k]), 'same seed, same steps -> identical biomass, cell for cell');
  ok(a.some((v, k) => v !== c[k]), 'a different seed -> a different world');
}

// The browser path: run the same three files as <script> tags would (no module, a global object as `self`),
// and use the globals they define.
{
  const vm = require('vm'), fs = require('fs'), path = require('path');
  const ctx = vm.createContext({ console });
  vm.runInContext('var self = this;', ctx);
  for (const f of ['natura.js', 'weather.js', 'vegetation.js']) {
    vm.runInContext(fs.readFileSync(path.join(__dirname, f), 'utf8'), ctx, { filename: f });
  }
  ok(ctx.Natura && typeof ctx.Natura.World === 'function', 'natura.js as a <script> defines the global Natura');
  ok(ctx.Weather && typeof ctx.Weather.makeWeather === 'function', 'weather.js as a <script> defines the global Weather');
  ok(ctx.Vegetation && typeof ctx.Vegetation.makeVegetation === 'function', 'vegetation.js as a <script> defines the global Vegetation');
  const bw = vm.runInContext(
    'var w = Natura.World({ seed: 7, systems: [Weather.makeWeather({ N: 16 }), Vegetation.makeVegetation({ N: 16 })] });' +
    'w.run(48, Natura.UNITS.HOUR); w.fields().vegetation.N', ctx);
  ok(bw === 16, 'a World built from the browser globals steps and exposes its fields');
}

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