← Natura·Docs/Modules/Source

commons.js

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

// commons.js -- Natura THE COMMONS (society / resource): a renewable common-pool resource and the tragedy that
// takes it, or the governance that saves it. A shared stock (a fishery, a pasture, an aquifer) GROWS logistically
// but collapses through a depensation floor if drawn too low; harvesters each expand effort while it pays, so under
// OPEN ACCESS the stock is drawn past the floor and crashes -- the tragedy of the commons, no one's fault and
// everyone's. Add Ostrom's governance -- a quota, monitoring, and graduated sanctions that build COMPLIANCE and
// curb effort -- and the same commons settles instead onto a sustained high yield. Distinct from economy.js (price)
// and speculation.js (bubbles): this is the shared-resource feedback. Deterministic mean-field (no rng). UMD.
(function (root, factory) {
  if (typeof module !== 'undefined' && module.exports) module.exports = factory();
  else root.Commons = factory();
})(typeof self !== 'undefined' ? self : this, function () {
  'use strict';
  const clamp = (v, a, b) => v < a ? a : v > b ? b : v;

  // config: { mode('open'|'managed'), r, q, floor, mort, quota, monitor, sanction, cost, effortGain, E0, S0, maxDtDays, name }
  function makeCommons(config) {
    config = config || {};
    const mode = config.mode || 'open';
    const r = config.r != null ? config.r : 0.9;          // intrinsic regrowth rate
    const q = config.q != null ? config.q : 1.1;          // catchability (harvest per unit effort per unit stock)
    const floor = config.floor != null ? config.floor : 0.14;  // depensation threshold -- below it recruitment fails
    const mort = config.mort != null ? config.mort : 0.10;     // background mortality (makes the floor a real trap)
    const quota = config.quota != null ? config.quota : 0.16;  // managed: the sustainable-ish cap on harvest
    const monitor = config.monitor != null ? config.monitor : 0.6;
    const sanction = config.sanction != null ? config.sanction : 0.8;
    const cost = config.cost != null ? config.cost : 0.30;     // cost per unit effort (below q*S it pays to expand)
    const effortGain = config.effortGain != null ? config.effortGain : 0.5;
    const maxDd = config.maxDtDays != null ? config.maxDtDays : 2;

    return {
      name: config.name || 'commons',
      domain: 'society',
      scale: 'regional',
      params: config,

      init() { return { S: config.S0 != null ? config.S0 : 0.9, E: config.E0 != null ? config.E0 : 0.15, c: 0, harvest: 0 }; },

      step(st, ctx, dt) {
        const dd = clamp(dt / 86400, 0, maxDd); if (dd <= 0) return st;
        const S = st.S;
        const hill = (S * S) / (S * S + floor * floor);           // recruitment choke below the depensation floor
        const growth = r * S * (1 - S) * hill - mort * S * (1 - hill);   // net regrowth (negative in the trap)
        // desired harvest from current effort; governance caps + curbs it
        const desired = q * st.E * S;
        let harvest;
        if (mode === 'managed') {
          const ct = (monitor * sanction) / (monitor * sanction + 0.08), crate = monitor * sanction + 0.08;   // equilibrium compliance + approach rate
          st.c = clamp(st.c + (ct - st.c) * (1 - Math.exp(-crate * dd)), 0, 1);              // stable exponential approach (dd-robust) -- compliance builds under monitoring+sanctions
          harvest = st.c * Math.min(desired, quota) + (1 - st.c) * desired;                  // compliers respect the quota; the rest overharvest
        } else { harvest = desired; }                                                        // open access: no cap
        st.S = clamp(S + (growth - harvest) * dd, 0, 1);
        // effort chases profit (q*S - cost); managed sanctions add an expected penalty that curbs expansion
        let profit = q * st.S - cost;
        if (mode === 'managed') profit -= monitor * sanction * 0.55;                         // the deterrent
        st.E = Math.max(0.02, st.E + effortGain * profit * st.E * dd);
        st.harvest = harvest;
        return st;
      },

      // OUT: stock (0..1 of carrying capacity), effort, compliance, harvest/yield, collapsed flag
      fields(st) {
        return { stock: +st.S.toFixed(4), effort: +st.E.toFixed(4), compliance: +st.c.toFixed(3),
          harvest: +st.harvest.toFixed(4), yield: +st.harvest.toFixed(4), collapsed: st.S < floor * 0.6 };
      },
      events(st) {
        const out = [];
        if (st.S < floor * 0.6) out.push({ type: 'collapse', stock: +st.S.toFixed(3) });
        else if (st.S > 0.4 && st.harvest > 0.02) out.push({ type: 'sustained', stock: +st.S.toFixed(3), yield: +st.harvest.toFixed(3) });
        return out;
      }
    };
  }
  return { makeCommons };
});