← Natura·Docs/Modules/Source

economy.js

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

// economy.js -- Natura/EXISTA ECONOMY (the SOCIETY domain's material verbs: producing, exchanging, pricing,
// distributing). A market, run as real general-equilibrium: agents who each make what they are relatively best
// at given the going prices, spend their earnings on what they want, and a price for every good that RISES
// where money chases scarcity and FALLS where goods pile up -- with no auctioneer, only the crowd clearing
// itself. Out of nothing but heterogeneous skill and taste comes the whole of it: prices that track scarcity,
// SPECIALISATION by comparative advantage, GAINS FROM TRADE over self-sufficiency, and -- from differences too
// small to see -- a WEALTH distribution that concentrates. Money is conserved to the cent (the clearing is
// exact), so nothing here is faked. NOT nature: this is the human world, the same emergence machinery pointed
// at a market instead of a meadow.
//
// A Natura system (init/step/fields/events), deterministic + node-testable. UMD.
(function (root, factory) {
  if (typeof module !== 'undefined' && module.exports) module.exports = factory();
  else root.Economy = factory();
})(typeof self !== 'undefined' ? self : this, function () {
  'use strict';

  const clamp = (v, a, b) => v < a ? a : v > b ? b : v;

  // Gini coefficient of a nonnegative array (0 = perfect equality, ->1 = one holder). O(n log n).
  function gini(xs) {
    const a = Array.from(xs).filter(v => v >= 0).slice().sort((p, q) => p - q), n = a.length;
    if (n === 0) return 0;
    let cum = 0, s = 0; for (let i = 0; i < n; i++) { cum += a[i]; s += cum; }
    if (cum <= 1e-12) return 0;
    return clamp((n + 1 - 2 * (s / cum)) / n, 0, 1);
  }

  function makeEconomy(config) {
    config = config || {};
    const N = config.agents || 120;                 // households/firms
    const G = config.goods || 4;                    // kinds of good
    const consume = config.consume != null ? config.consume : 0.72;   // fraction of wealth spent each period (rest saved -> wealth can accumulate)
    const adapt = config.adapt != null ? config.adapt : 0.25;          // how fast labour RE-ALLOCATES toward the profitable good (gradual -> damps the cobweb; price still clears EXACTLY)
    const softness = config.softness != null ? config.softness : 4;    // labour-allocation sharpness (higher = sharper specialisation)
    const learn = config.learn != null ? config.learn : 0.03;          // LEARNING BY DOING: productivity in a good rises with cumulative experience of it (a saturating learning curve) -> early advantages SNOWBALL into inequality + path dependence
    const capBoost = config.capBoost != null ? config.capBoost : 0.9;  // CAPITAL: wealth funds better means of production, so the well-off produce more, earn more, and pull further ahead (r > g -> inequality compounds beyond mere skill)
    const capHalf = config.capHalf != null ? config.capHalf : 25;      // the wealth at which the capital boost is half its maximum (saturating, so it never runs away to a single owner)
    const luckSig = config.luckSig != null ? config.luckSig : 0.4;     // idiosyncratic LUCK: multiplicative per-period productivity shocks that, compounding through savings + capital, spread wealth into a heavy-tailed distribution (a Kesten process -- the real engine of inequality)
    const money0 = config.money0 != null ? config.money0 : 10;

    return {
      name: config.name || 'economy',
      domain: 'society',
      scale: 'collective',
      params: config,

      init(rng) {
        const skill = [], pref = [];                // skill[a][g], pref[a][g]
        for (let a = 0; a < N; a++) {
          const sk = new Float64Array(G), pr = new Float64Array(G);
          let ps = 0;
          for (let g = 0; g < G; g++) { sk[g] = 0.35 + 1.65 * rng(); pr[g] = 0.15 + rng(); ps += pr[g]; }
          for (let g = 0; g < G; g++) pr[g] /= ps;   // preferences are a budget share -> sum to 1 (Cobb-Douglas)
          skill.push(sk); pref.push(pr);
        }
        const money = new Float64Array(N); money.fill(money0);
        const price = new Float64Array(G); price.fill(1);
        return {
          N, G, skill, pref, money, price,
          share: Array.from({ length: N }, () => { const s = new Float64Array(G); s.fill(1 / G); return s; }),   // labour allocation (starts diversified, adapts)
          _ew: Array.from({ length: N }, () => new Float64Array(G)),      // scratch: effective (shock-warped, renormalised) budget shares
          _tgt: new Float64Array(G),                                     // scratch: the target labour allocation this period
          exp: Array.from({ length: N }, () => new Float64Array(G)),      // cumulative experience per (agent, good) -> learning-by-doing
          _ske: Array.from({ length: N }, () => new Float64Array(G)),     // scratch: this period's effective skill (skill x learning), reused by production + income so money conserves
          produced: new Float64Array(G), demand: new Float64Array(G),
          util: new Float64Array(N),                 // last-period utility (log Cobb-Douglas)
          shockPref: null,                           // optional demand shock (per-good multiplier), set by a scenario
          closedGoods: null,                         // optional supply shock: goods no one may produce
          activity: 1,                               // economy-wide ACTIVITY multiplier (a coupling input: 1 = normal, <1 = a contraction as workers fall sick / stay home). Scales production AND consumption together, so money conserves and prices hold -- only REAL output moves.
          gini: 0, meanUtil: 0, totalMoney: N * money0, tPeriod: 0, evs: []
        };
      },

      step(st, ctx) {
        const { N, G, skill, pref, money, price, share } = st;
        const rng = ctx && ctx.rng ? ctx.rng : null;   // seeded world rng; if absent, luck is off (stays deterministic)
        const act = st.activity != null ? clamp(st.activity, 0, 1) : 1;   // economy-wide activity (a coupling input)
        st.evs.length = 0;
        const supply = st.produced; supply.fill(0);
        const closed = st.closedGoods;               // a set of goods no one may produce (a supply shock)

        // --- 1. PRODUCTION: each agent moves its labour GRADUALLY toward where value (skill x price) is highest.
        //        The target share ~ (skill*price)^softness (graded comparative advantage); labour re-allocates only
        //        a step `adapt` toward it each period, which damps the cobweb so prices settle instead of exploding.
        const tgt = st._tgt, exp = st.exp;
        for (let a = 0; a < N; a++) {
          const sk = skill[a], sh = share[a], ex = exp[a], ske = st._ske[a]; let tot = 0;
          const luck = rng ? Math.exp(luckSig * (2 * rng() - 1)) : 1;    // this agent's fortune this period (multiplicative)
          const cap = (1 + capBoost * money[a] / (money[a] + capHalf)) * luck;   // wealth-as-capital (saturating) x luck
          for (let g = 0; g < G; g++) ske[g] = sk[g] * (1 + learn * ex[g] / (1 + 0.05 * ex[g])) * cap * act;   // effective skill: learning curve x capital x activity (a sick/locked-down workforce produces less)
          for (let g = 0; g < G; g++) { const v = (closed && closed.has(g)) ? 0 : Math.pow(Math.max(1e-9, ske[g] * price[g]), softness); tgt[g] = v; tot += v; }
          for (let g = 0; g < G; g++) { const t = tot > 0 ? tgt[g] / tot : 0; sh[g] = (1 - adapt) * sh[g] + adapt * t; supply[g] += sh[g] * ske[g]; }
        }

        // --- 2. NOMINAL DEMAND: each agent means to spend a fraction `consume` of its wealth, split across goods
        //        by its Cobb-Douglas budget shares -- optionally warped by a demand shock, then RENORMALISED per
        //        agent so it still spends its whole budget (a shock MOVES demand between goods, never creates money).
        const sp = st.shockPref, ew = st._ew;
        for (let a = 0; a < N; a++) { const pr = pref[a], w = ew[a]; let s = 0;
          for (let g = 0; g < G; g++) { w[g] = sp ? pr[g] * sp[g] : pr[g]; s += w[g]; }
          for (let g = 0; g < G; g++) w[g] = s > 0 ? w[g] / s : 0; }
        const dnom = st.demand; dnom.fill(0);
        for (let a = 0; a < N; a++) { const budget = consume * money[a] * act, w = ew[a]; for (let g = 0; g < G; g++) dnom[g] += budget * w[g]; }

        // --- 3. CLEARING PRICE: the price that exactly clears each good is the money chasing it over the quantity
        //        of it -- p = Dnom / supply. No auctioneer; the crowd sets it. A good no one makes gets its price
        //        BID UP (scarcity signal) so producers are drawn to it next period.
        for (let g = 0; g < G; g++) {
          if (supply[g] > 1e-9) price[g] = dnom[g] / supply[g];   // the EXACT market-clearing price (keeps money conserved)
          else price[g] = price[g] * 1.5;                         // absent good -> scarcity -> price climbs until someone supplies it
          price[g] = clamp(price[g], 1e-4, 1e6);
        }

        // --- 4. TRADE + WEALTH: at the (clearing) prices, each agent sells its output for income and spends its
        //        consumption budget on goods that EXIST. Money each agent ends with = what it kept + what it earned
        //        - what it spent. Because p*supply == Dnom on every traded good, total earned == total spent EXACTLY,
        //        so the whole economy's money is conserved; only its DISTRIBUTION moves.
        let totalMoney = 0, sumUtil = 0;
        for (let a = 0; a < N; a++) {
          const sh = share[a], pr = pref[a], w = ew[a], ske = st._ske[a];
          let income = 0; for (let g = 0; g < G; g++) if (supply[g] > 1e-9) income += sh[g] * ske[g] * price[g];   // earn only on goods that SOLD, at the SAME effective skill used for supply -> money conserved
          let spent = 0, u = 0;
          for (let g = 0; g < G; g++) {
            const tradable = supply[g] > 1e-9;
            const budG = consume * money[a] * act * w[g];           // spend the effective (possibly shocked) budget share, scaled by activity
            const qty = tradable ? budG / price[g] : 0;             // what it can buy of g
            if (tradable) spent += budG;                            // money for absent goods is simply not spent (kept)
            u += pr[g] * Math.log(qty + 1e-6);                      // WELFARE uses TRUE preferences (log Cobb-Douglas)
          }
          money[a] = money[a] - spent + income;
          st.util[a] = u; sumUtil += u; totalMoney += money[a];
        }

        // experience grows with the labour spent this period (learning-by-doing, applied AFTER trade so supply +
        // income shared one effective skill; the well-practised pull ahead -> compounding path-dependent inequality)
        for (let a = 0; a < N; a++) { const ex = exp[a], sh = share[a]; for (let g = 0; g < G; g++) ex[g] += sh[g]; }

        // --- 5. observables + events
        st.gini = gini(money); st.meanUtil = sumUtil / N; st.totalMoney = totalMoney; st.tPeriod++;
        if (st.gini > 0.45) st.evs.push({ kind: 'stratified', gini: st.gini });
        for (let g = 0; g < G; g++) if (price[g] > 3) st.evs.push({ kind: 'dear', good: g, price: price[g] });
        return st;
      },

      fields(st) {
        // the specialisation index: mean Herfindahl of each agent's labour allocation (1/G = jack-of-all-goods,
        // 1 = fully specialised). A read of how far the market has sorted people into their comparative advantage.
        let herf = 0; for (let a = 0; a < st.N; a++) { let h = 0; for (let g = 0; g < st.G; g++) h += st.share[a][g] * st.share[a][g]; herf += h; }
        let out = 0; for (let g = 0; g < st.G; g++) out += st.produced[g];   // total REAL output (goods made) -- the thing that falls in a contraction
        return {
          N: st.N, G: st.G, price: st.price, produced: st.produced, demand: st.demand,
          money: st.money, gini: st.gini, meanUtil: st.meanUtil, totalMoney: st.totalMoney,
          output: out, activity: st.activity,
          specialization: herf / st.N, tPeriod: st.tPeriod
        };
      },

      events(st) { return st.evs; },

      // ---- scenario helpers (a reader may reach in for a controlled experiment; used by the tests) ----
      _gini: gini
    };
  }

  return { makeEconomy };
});