← Natura·Docs/Modules/Source

economy-test.js

120 lines · in natura-src-1.0.0.zip as natura/economy-test.js · a test

// economy-test.js -- the ECONOMY as real general-equilibrium, every emergence verified by RUNNING (never by
// reasoning): money conserved to the cent, prices that converge and track scarcity, gains from trade over
// self-sufficiency, wealth that concentrates from small differences, and shocks that ripple through prices.
const N = require('./natura.js');
const { makeEconomy } = require('./economy.js');
const { World, UNITS } = N;

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

function run(seed, cfg, periods, mutate) {
  const eco = makeEconomy(cfg || {});
  const w = World({ seed, systems: [eco] });
  const st = w.get('economy');
  if (mutate) mutate(st);                         // scenario hook: set skills / prefs / shocks before running
  const series = [];
  for (let i = 0; i < periods; i++) { w.step(UNITS.DAY); series.push({ gini: st.gini, price: Array.from(st.price), total: st.totalMoney, spec: w.fields().economy.specialization }); }
  return { st, f: w.fields().economy, series, eco };
}
const autarkyWelfare = (st) => { let tot = 0; for (let a = 0; a < st.N; a++) { let u = 0; for (let g = 0; g < st.G; g++) { const c = st.pref[a][g] * st.skill[a][g]; u += st.pref[a][g] * Math.log(c + 1e-6); } tot += u; } return tot; };
const tradeWelfare = (st) => { let t = 0; for (let a = 0; a < st.N; a++) t += st.util[a]; return t; };

console.log('economy assertions:');

// 1. DETERMINISM
{
  const a = run(5, { agents: 80 }, 60).f, b = run(5, { agents: 80 }, 60).f;
  let same = a.gini === b.gini && a.meanUtil === b.meanUtil;
  for (let g = 0; g < a.G; g++) if (a.price[g] !== b.price[g]) same = false;
  ok(same, 'same seed -> identical economy (reproducible)');
}

// 2. MONEY CONSERVED -- the clearing is EXACT (p*supply == money-chasing-good), so total money never moves; only
//    its distribution does. The correctness oracle: if this drifts, the market is quietly minting or burning cash.
{
  const { series } = run(7, { agents: 120, goods: 5 }, 200);
  const m0 = series[1].total, mL = series[series.length - 1].total;
  ok(Math.abs(mL - m0) / m0 < 1e-6, 'total money is CONSERVED to the cent across 200 periods (' + m0.toFixed(2) + ' -> ' + mL.toFixed(2) + ')');
}

// 3. PRICES CONVERGE -- the tatonnement finds an equilibrium; late price moves are far smaller than early ones.
{
  const { series } = run(3, { agents: 120, goods: 4, luckSig: 0 }, 160);   // isolate the deterministic tatonnement (luck off)
  const chg = (i) => { let s = 0; for (let g = 0; g < series[i].price.length; g++) s += Math.abs(series[i].price[g] - series[i - 1].price[g]) / (series[i - 1].price[g] + 1e-9); return s; };
  let early = 0, late = 0; for (let i = 2; i < 8; i++) early += chg(i); for (let i = series.length - 6; i < series.length; i++) late += chg(i);
  ok(late < early * 0.25, 'prices CONVERGE to equilibrium (late price-churn ' + late.toFixed(3) + ' << early ' + early.toFixed(3) + ')');
}

// 4. PRICE reflects SCARCITY -- a good everyone WANTS but few can MAKE well ends up dear; an easily-made,
//    little-wanted good ends up cheap. Constructed: good 0 = wanted+hard, good 1 = ignored+easy.
{
  const { st } = run(9, { agents: 120, goods: 2 }, 120, (st) => {
    for (let a = 0; a < st.N; a++) {
      st.pref[a][0] = 0.8; st.pref[a][1] = 0.2;                 // everyone mostly wants good 0
      st.skill[a][0] = 0.4 + 0.3 * (a / st.N);                 // but is poor at making it (scarce)
      st.skill[a][1] = 1.4 + 0.3 * (a / st.N);                 // and good at the good they want less (abundant)
    }
  });
  ok(st.price[0] > st.price[1] * 1.5, 'the WANTED-but-SCARCE good is dear, the ignored-but-abundant one cheap (price ' + st.price[0].toFixed(2) + ' vs ' + st.price[1].toFixed(2) + ')');
}

// 5. GAINS FROM TRADE -- the whole point of a market. Let people specialise and TRADE and total welfare beats a
//    world where each makes only what it will itself consume (autarky). Measured by RUNNING both.
{
  const { st } = run(11, { agents: 150, goods: 4 }, 150);
  const trade = tradeWelfare(st), auto = autarkyWelfare(st);
  ok(trade > auto + 1e-3, 'TRADE beats self-sufficiency -- specialisation + exchange raise total welfare (trade ' + trade.toFixed(1) + ' > autarky ' + auto.toFixed(1) + ')');
}

// 6. WEALTH CONCENTRATES -- everyone starts with the SAME money. A pure competitive LABOUR market equalises
//    incomes, so it stays fairly equal; add CAPITAL (wealth funds better production) and idiosyncratic LUCK
//    (returns are noisy, and noise compounds through savings -- a Kesten process) and wealth pulls apart into a
//    heavy-tailed distribution. Both proven by RUNNING and compared.
{
  const plain = run(13, { agents: 220, goods: 4, luckSig: 0, capBoost: 0 }, 280).f.gini;   // pure labour market: modest
  const full = run(13, { agents: 220, goods: 4, luckSig: 0.6 }, 280).f.gini;                // + capital + luck: heavy-tailed
  ok(full > 0.15, 'WEALTH CONCENTRATES from an EQUAL start into a heavy-tailed distribution (Gini ' + full.toFixed(2) + ')');
  ok(full > plain + 0.05, 'CAPITAL + LUCK amplify inequality far beyond a pure labour market (Gini ' + plain.toFixed(2) + ' -> ' + full.toFixed(2) + ')');
}

// 7. SPECIALISATION -- the market SORTS people toward their comparative advantage; the labour-concentration
//    index rises well above the jack-of-all-goods baseline (1/G).
{
  const { series, f } = run(17, { agents: 150, goods: 4 }, 150);
  const base = 1 / f.G;
  ok(f.specialization > base + 0.12 && f.specialization > series[3].spec, 'the market drives SPECIALISATION (concentration ' + f.specialization.toFixed(2) + ' vs a diversified ' + base.toFixed(2) + ')');
}

// 8. A SHOCK RIPPLES THROUGH PRICES -- run to equilibrium, then choke the SUPPLY of a good (a strike/embargo);
//    its price spikes. And a pure DEMAND shock (a craze) spikes a price WITHOUT breaking money conservation.
{
  // supply choke on good 0
  const w2 = World({ seed: 19, systems: [makeEconomy({ agents: 120, goods: 4 })] }); const s2 = w2.get('economy');
  for (let i = 0; i < 130; i++) w2.step(UNITS.DAY); const preShock = s2.price[0];
  s2.closedGoods = new Set([0]); for (let i = 0; i < 25; i++) w2.step(UNITS.DAY); const postShock = s2.price[0];
  ok(postShock > preShock * 1.5, 'a SUPPLY choke spikes the good\'s price (' + preShock.toFixed(2) + ' -> ' + postShock.toFixed(2) + ')');

  // demand craze for good 1, and money must stay conserved through it
  const w3 = World({ seed: 23, systems: [makeEconomy({ agents: 120, goods: 4 })] }); const s3 = w3.get('economy');
  for (let i = 0; i < 130; i++) w3.step(UNITS.DAY); const m0 = s3.totalMoney, pB = s3.price[1];
  const sp = new Float64Array(4); sp.fill(1); sp[1] = 4;                 // a craze for good 1
  s3.shockPref = sp; let peak1 = 0; for (let i = 0; i < 25; i++) { w3.step(UNITS.DAY); peak1 = Math.max(peak1, s3.price[1]); }
  ok(peak1 > pB * 1.4, 'a DEMAND craze spikes the craved good\'s price before supply catches up (' + pB.toFixed(2) + ' -> peak ' + peak1.toFixed(2) + ')');
  ok(Math.abs(s3.totalMoney - m0) / m0 < 1e-6, 'money stays CONSERVED through the demand shock (a craze moves demand, does not mint money)');
}

// 9. ACTIVITY (a coupling input) -- when the workforce is suppressed (sick / locked down), REAL output falls in
//    proportion, but money stays conserved and prices hold: it scales production AND consumption together, so it
//    is a real contraction, not a price shock. This is the channel a pandemic drives the economy through.
{
  const full = run(27, { agents: 150, goods: 4, luckSig: 0 }, 120);
  const half = run(27, { agents: 150, goods: 4, luckSig: 0 }, 120, (st) => { st.activity = 0.5; });
  ok(half.f.output < full.f.output * 0.62 && half.f.output > full.f.output * 0.38, 'ACTIVITY 0.5 roughly HALVES real output (' + full.f.output.toFixed(1) + ' -> ' + half.f.output.toFixed(1) + ')');
  ok(Math.abs(half.f.totalMoney - full.f.totalMoney) / full.f.totalMoney < 1e-6, 'money stays CONSERVED under the contraction (a real shock, not a monetary one)');
  let priceClose = true; for (let g = 0; g < 4; g++) if (Math.abs(half.f.price[g] - full.f.price[g]) / full.f.price[g] > 0.06) priceClose = false;
  ok(priceClose, 'and relative PRICES hold (production and consumption both scale, so it is output that moves, not prices)');
}

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