← Natura·Docs/Modules/Source

stellar.js

88 lines · in natura-src-1.0.0.zip as natura/stellar.js · a module · its entry in the module reference · its test: stellar-test.js

// stellar.js -- Natura STELLAR (SYSTEMS.md #11, ALGORITHMS.md): the deep-time energy source. A star's
// whole life told as a function of AGE in billions of years -- the slow main-sequence brightening, the
// swelling into a red giant, and the long cooling of the white-dwarf ember afterward. Everything is
// ANALYTIC in the clock time (no integration over a Gyr), so it is exact and resumable at any moment of
// the star's life. It exposes the star's luminosity, radius, temperature, and phase, and the INSOLATION
// at a planet's orbit -- which is the boundary condition the whole climate hangs from: a brightening sun
// marches the habitable zone outward (frozen -> temperate -> scorched over eons), and the biome system,
// reading that trend, shifts the living world to match. The cosmic rung of the scale ladder.
//
// Reads only the clock (at eon scale). Deterministic + node-testable. UMD (node test / window.Stellar).
(function (root, factory) {
  if (typeof module !== 'undefined' && module.exports) module.exports = factory();
  else root.Stellar = factory();
})(typeof self !== 'undefined' ? self : this, function () {
  'use strict';

  // stellar state at an age (Gyr), for a star of `mass` solar masses. Solar-normalized units
  // (L_sun, R_sun, T in K with T_sun=5772). A compact but qualitatively faithful evolution track.
  function evolve(ageGyr, mass, planetDist) {
    const tMs = 10 / Math.pow(mass, 2.5);              // main-sequence lifetime (~10 Gyr for the Sun)
    const L0 = Math.pow(mass, 3.5);                    // zero-age main-sequence luminosity (mass-luminosity)
    const tRg = 1.0 * Math.pow(mass, -1);             // red-giant duration (~1 Gyr for the Sun)
    let L, R, phase;
    if (ageGyr < tMs) {                                // MAIN SEQUENCE: slow ~2x brightening over the life
      L = L0 * (0.7 + 0.7 * (ageGyr / tMs));
      R = Math.pow(mass, 0.8);
      phase = 'main-sequence';
    } else if (ageGyr < tMs + tRg) {                   // RED GIANT: L climbs ~1000x, the star swells hugely
      const frac = (ageGyr - tMs) / tRg;               // 0 -> 1 (the RGB tip)
      const Lend = L0 * 1.4;
      L = Lend * Math.pow(2000 / 1.4, frac);
      R = Math.pow(mass, 0.8) * Math.pow(180, frac);
      phase = 'red-giant';
    } else {                                           // WHITE DWARF: the ember, small + hot, cooling for Gyr
      const tau = 0.3, aw = ageGyr - (tMs + tRg);
      L = 1.0 * Math.pow(tau / (aw + tau), 1.3);
      R = 0.013 * Math.pow(mass, -1 / 3);              // ~Earth-sized, denser for more mass
      phase = 'white-dwarf';
    }
    const T = 5772 * Math.pow(Math.max(1e-9, L) / (R * R), 0.25);   // Stefan-Boltzmann: L = R^2 (T/Tsun)^4
    const insol = L / (planetDist * planetDist);       // insolation at the planet (present Sun ~ 1)
    return { L, R, T, phase, insol };
  }

  // make a Stellar system. config: { mass(solar), age0Gyr(present age), planetDist(AU), name }.
  function makeStellar(config) {
    config = config || {};
    const mass = config.mass != null ? config.mass : 1.0;
    const age0 = config.age0Gyr != null ? config.age0Gyr : 4.6;    // default: the Sun's present age
    const planetDist = config.planetDist != null ? config.planetDist : 1.0;

    return {
      name: config.name || 'stellar',
      domain: 'cosmic',
      scale: 'stellar',
      params: config,

      init(rng, params, clock) {
        const a0 = age0 + (clock ? clock.t / (1e9 * 365 * 86400) : 0);
        const s = evolve(a0, mass, planetDist);
        return { age: a0, cur: s, prevPhase: s.phase, peakL: s.L, firedPeak: false };
      },

      step(st, ctx, dt) {
        const age = age0 + ctx.clock.t / (1e9 * 365 * 86400);       // present age + elapsed, in Gyr
        const s = evolve(age, mass, planetDist);
        st.events = [];
        if (s.phase !== st.prevPhase) {
          if (s.phase === 'red-giant') st.events.push({ kind: 'leaveMainSequence', age: +age.toFixed(3) });
          if (s.phase === 'white-dwarf') st.events.push({ kind: 'whiteDwarf', age: +age.toFixed(3) });
          st.prevPhase = s.phase;
        }
        // the red-giant luminosity peak (the RGB tip), edge-triggered once
        if (s.phase === 'red-giant') { if (s.L > st.peakL) st.peakL = s.L; }
        else if (st.prevPhase === 'white-dwarf' && !st.firedPeak && st.peakL > 100) { st.events.push({ kind: 'redGiantPeak', peakL: +st.peakL.toFixed(0) }); st.firedPeak = true; }
        st.age = age; st.cur = s;
        return st;
      },

      // OUT: luminosity, radius, temperature, phase, and INSOLATION (the climate's deep-time boundary condition)
      fields(st) { return { luminosity: st.cur.L, radius: st.cur.R, temperature: st.cur.T,
                            phase: st.cur.phase, insolation: st.cur.insol, ageGyr: st.age }; },
      events(st) { return st.events || []; }
    };
  }

  return { makeStellar, evolve };
});