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