assembly-economics/simulations/gadm02/gadm02.py

189 lines
8.6 KiB
Python

"""GADM-0.2 toy simulation (normalized stocks, Euler integration).
Implements models/GADM-0.2.md. Every stock lives in [0, 1]. Inflows are scaled
by headroom (1 - x) and outflows by level (x), so stocks stay bounded without
clipping. This is a hypothesis generator, not a forecast (GADM-0.1 § 27).
Standard library only, deterministic.
"""
from __future__ import annotations
import math
MODEL_VERSION = "GADM-0.2"
TIME_UNIT = "year" # one period = one year (ASSEMBLY-WP-0005-T01); rates are per year
STOCKS = ("H", "X", "Kp", "Ko", "F", "G", "N", "Q", "P", "E", "S", "A")
INIT = {
"H": 0.50, # human capability
"X": 0.40, # assembly commons
"Kp": 0.50, # physical / conventional productive capacity
"Ko": 0.05, # information-organism capability (K_Omega)
"F": 0.40, # risk capacity
"G": 0.50, # effective agency
"N": 0.50, # diversity & composability
"Q": 0.50, # epistemic capacity
"P": 0.40, # domination concentration
"E": 0.50, # ecological headroom
"S": 0.50, # social cohesion
"A": 0.50, # attention headroom (unclaimed, self-directed attention)
}
PARAMS = {
# experimentation V (GADM-0.1 § 5) + organism term gated by Q (M2)
"v0": 2.5, "eH": 0.5, "eX": 0.5, "eF": 0.3, "eG": 0.4, "eN": 0.3,
"eQ": 0.3, "eP": 0.5, "vo": 0.4,
# selection channel weights (§ 6) + organism channel O
"wm": 0.35, "ws": 0.20, "wc": 0.15, "wp": 0.15, "wo": 0.30,
# claims on output (§ 9) + participation claims C_p
"wL0": 0.60, "auto": 0.6, "dshare": 0.10, "cp": 0.0, "od": 0.20,
# ASSEMBLY-WP-0016-T04: foreign demand for organism-enabled output (exports), dem += xport * K_Omega.
# 0 = closed economy (default; all earlier results unchanged).
"xport": 0.0,
# ASSEMBLY-WP-0007-T01: rival / non-rival output. phi_r = share of organism output that is rival
# (physical, limited by ecological headroom); 1 = GADM up to 0.4 (all output scaled by sqrt(E)).
# xn = rate at which non-rival output, as far as it is openly inheritable, flows into commons X.
"phi_r": 1.0, "xn": 0.0,
# ASSEMBLY-WP-0018-T04: organism selection quality falls with discovery concentration,
# O = Q (1 - disc) (1 - o_pd * Pd). 0 = GADM up to 0.5 (default).
"o_pd": 0.0,
# commons (§ 7)
"aX": 0.30, "rho0": 0.30, "rhoO": 0.40, "IX": 0.02, "dX": 0.04,
"lX": 0.06, "lXo": 0.06,
# productive capacity (§ 8) and organism capability (mapping note § 1)
"aK": 0.35, "dK": 0.05, "aO": 0.15, "ell": 0.25, "oseed": 0.01, "dO": 0.06,
# human capability (§ 11)
"h0": 0.08, "hA": 0.04, "hT": 0.03, "dH": 0.05, "desk": 0.06,
# risk capacity
"f0": 0.03, "fY": 0.08, "dF": 0.06,
# effective agency (§ 12)
"g1": 0.08, "g2": 0.05, "g3": 0.04, "g4": 0.04, "dG": 0.04,
"b1": 0.08, "b2": 0.06,
# diversity & composability (§ 16)
"n1": 0.30, "n2": 0.05, "dN": 0.04, "n3": 0.06, "n4": 0.08,
# epistemic capacity (§ 15)
"q0": 0.04, "q1": 0.05, "qo": 0.06, "dQ": 0.03, "q2": 0.06, "qx": 0.08,
"q3": 0.10,
# domination (§ 13) + membrane control and lineage concentration
"p1": 0.10, "p2": 0.06, "p3": 0.06, "p4": 0.06,
"p5": 0.10, "p6": 0.05, "p7": 0.06,
# ecology (§ 14)
"ereg": 0.05, "e1": 0.06, "e2": 0.03, "thr": 1.0,
# cohesion (§ 17), relaxation rate
"srate": 0.20,
# attention headroom (mapping note § 4)
"arec": 0.10, "aext": 0.12,
# institutional / organism-regime levers (scenario axes)
"m": 0.30, # membrane control held by users/communities (0..1)
"L": 0.30, # lineage diffusion (0 concentrated .. 1 diffuse)
"lin_open": 0.30, # openly inheritable lineages (commons vs enclosure)
"std": 0.50, # open standards / interoperability
"disc": 0.40, # discovery concentration
"expl": 0.40, # explainability / provenance enforcement
"x_att": 0.50, # attention-extraction regime (0 protected .. 1 extraction)
}
FLOORS = {"G": (">=", 0.20), "E": (">=", 0.20), "P": ("<=", 0.70), "A": (">=", 0.20)}
def auxiliaries(s: dict, p: dict) -> dict:
"""Flows and auxiliaries for state s under parameters p."""
H, X, Kp, Ko, F, G, N, Q, P, E, S, A = (s[k] for k in STOCKS)
K = 1 - (1 - Kp) * (1 - Ko)
phi = p.get("phi_r", 1.0)
y_r = (1 - (1 - Kp) * (1 - phi * Ko)) * math.sqrt(E) # rival output, limited by ecology
y_n = (1 - phi) * Ko * (1 - Kp) # non-rival output (sum = K at E = 1)
ystar = y_r + y_n
wl = p["wL0"] * (1 - p["auto"] * Ko)
cap = 1 - wl
priv = cap * max(0.0, 1 - p["dshare"] - p["cp"])
B = min(1.0, wl + cap * (p["dshare"] + p["cp"]) + priv * p["od"])
dem = (0.4 + 0.6 * B) * (0.6 + 0.4 * S) + p.get("xport", 0.0) * Ko
v_h = p["v0"] * (H ** p["eH"]) * (X ** p["eX"]) * (F ** p["eF"]) * (G ** p["eG"]) \
* (N ** p["eN"]) * (Q ** p["eQ"]) * ((1 - P) ** p["eP"])
V = min(1.0, v_h + p["vo"] * Ko * Q)
M = math.sqrt(A * dem)
O = Q * (1 - p["disc"]) * (1 - p.get("o_pd", 0.0) * s.get("Pd_ch", 0.0))
w = [p["wm"], p["ws"], p["wc"], p["wp"], p["wo"] * Ko]
q = [M, Q, N * (1 - P), S, O]
sigma = sum(a * b for a, b in zip(w, q)) / sum(w)
vsel = V * sigma
rho = min(1.0, p["rho0"] + p["rhoO"] * Ko * p["lin_open"])
return {"K": K, "Ystar": ystar, "Yr": y_r, "Yn": y_n, "B": B, "dem": dem, "V": V, "sigma": sigma,
"Vsel": vsel, "rho": rho, "wl": wl}
def derivatives(s: dict, p: dict) -> tuple[dict, dict]:
H, X, Kp, Ko, F, G, N, Q, P, E, S, A = (s[k] for k in STOCKS)
a = auxiliaries(s, p)
B, dem, V, vsel, rho = a["B"], a["dem"], a["V"], a["Vsel"], a["rho"]
m, L, lo, std = p["m"], p["L"], p["lin_open"], p["std"]
d = {}
d["X"] = (p["aX"] * rho * vsel + p["IX"] + p.get("xn", 0.0) * a["Yn"] * lo) * (1 - X) \
- (p["dX"] + p["lX"] * P + p["lXo"] * Ko * (1 - lo)) * X
d["Kp"] = p["aK"] * vsel * dem * (1 - Kp) - p["dK"] * Kp
d["Ko"] = (p["aO"] * vsel * dem + p["ell"] * Ko * dem * Q + p["oseed"]) * (1 - Ko) \
- p["dO"] * Ko
d["H"] = (p["h0"] * (0.5 * S + 0.5 * B) + p["hA"] * A + p["hT"] * X) * (1 - H) \
- (p["dH"] + p["desk"] * Ko * (1 - m)) * H
d["F"] = (p["f0"] + p["fY"] * a["Ystar"] * B) * (1 - F) - p["dF"] * F
d["G"] = (p["g1"] * B + p["g2"] * X * (1 - P) + p["g3"] * F + p["g4"] * Ko * m) * (1 - G) \
- (p["dG"] + p["b1"] * P + p["b2"] * Ko * (1 - m)) * G
d["N"] = (p["n1"] * V * (1 - P) + p["n2"] * X * std) * (1 - N) \
- (p["dN"] + p["n3"] * P + p["n4"] * Ko * (1 - L)) * N
d["Q"] = (p["q0"] + p["q1"] * X + p["qo"] * Ko * p["expl"]) * (1 - Q) \
- (p["dQ"] + p["q2"] * P + p["qx"] * Ko * (1 - p["expl"]) + p["q3"] * max(0.0, V - Q)) * Q
d["P"] = (p["p1"] * vsel * (1 - B) + p["p2"] * p["disc"] + p["p3"] * Ko * (1 - m)
+ p["p4"] * Ko * (1 - L)) * (1 - P) \
- (p["p5"] * Q * S + p["p6"] * X * std + p["p7"] * B) * P
d["E"] = p["ereg"] * (1 - E) - (p["e1"] * Kp * dem * p["thr"] + p["e2"] * Ko) * E
s_target = (G ** 0.4) * ((1 - P) ** 0.3) * (B ** 0.2) * (A ** 0.1)
d["S"] = p["srate"] * (s_target - S)
d["A"] = p["arec"] * (0.4 + 0.6 * B) * (1 - A) - p["aext"] * p["x_att"] * (0.2 + Ko) * A
return d, a
def vfac(s: dict, with_attention: bool = True) -> float:
"""Geometric-mean VFAC (equal exponents). Diagnostic, not a welfare function."""
f = [s["H"], s["X"], s["G"], s["N"], s["Q"], s["S"], s["E"], 1 - s["P"]]
if with_attention:
f.append(s["A"])
if min(f) <= 0.0:
return 0.0 # a collapsed foundational subsystem collapses the diagnostic
return math.exp(sum(math.log(x) for x in f) / len(f))
def simulate(params: dict | None = None, init: dict | None = None,
T: float = 100.0, dt: float = 0.1, record_every: int = 10) -> list[dict]:
p = {**PARAMS, **(params or {})}
s = {**INIT, **(init or {})}
unknown = set(p) - set(PARAMS)
if unknown:
raise KeyError(f"unknown parameters: {sorted(unknown)}")
rows = []
steps = int(round(T / dt))
for i in range(steps + 1):
d, a = derivatives(s, p)
if i % record_every == 0:
row = {"t": round(i * dt, 6), **s, **{k: a[k] for k in ("Ystar", "B", "V", "Vsel", "sigma")}}
row["VFAC"] = vfac(s)
row["VFAC01"] = vfac(s, with_attention=False)
rows.append(row)
if i < steps:
s = {k: min(1.0, max(0.0, s[k] + dt * d[k])) for k in STOCKS}
return rows
def floor_violations(rows: list[dict]) -> dict:
"""Share of recorded periods in which each constitutional floor is violated."""
out = {}
for k, (op, lim) in FLOORS.items():
bad = [r for r in rows if (r[k] < lim if op == ">=" else r[k] > lim)]
out[k] = len(bad) / len(rows)
return out