Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 14913@bnt-lap001 Assistant-Session: add7ef69-b901-4756-9a67-34cf1125d96f
234 lines
11 KiB
Python
234 lines
11 KiB
Python
"""GADM-0.4 (in development): durable human capability (ASSEMBLY-WP-0006-T01).
|
|
|
|
Extends gadm03c (cohorts). Each cohort's capability is split into
|
|
|
|
D_c durable capability: formed mainly in childhood by education,
|
|
depreciates slowly, carried along when people age;
|
|
C_c current capability: the GADM-0.3 cohort dynamics, with learning
|
|
efficiency scaled by durable capability ("learning begets learning").
|
|
|
|
Effective cohort capability, which the rest of the model sees, is
|
|
H_c = 0.5 * D_c + 0.5 * C_c.
|
|
|
|
GADM-0.3 had no durable component, so working-age capability forgot childhood
|
|
within about 8 years (L3 not supported). gadm03c stays unchanged so the
|
|
GADM-0.3 results remain reproducible.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import gadm02 as g2
|
|
import gadm03 as g3
|
|
import gadm03c as c
|
|
|
|
MODEL_VERSION = "GADM-0.4"
|
|
TIME_UNIT = c.TIME_UNIT
|
|
COHORTS = c.COHORTS
|
|
CUR = {k: "C" + k for k in COHORTS} # current capability stocks
|
|
DUR = {k: "D" + k for k in COHORTS} # durable capability stocks
|
|
|
|
STOCKS = tuple(k for k in c.STOCKS if not (k.startswith("H") and k[1:] in COHORTS)) \
|
|
+ tuple(CUR.values()) + tuple(DUR.values()) + ("Gw", "Gs", "Sig", "Lg")
|
|
|
|
INIT = {k: v for k, v in c.INIT.items() if k in STOCKS}
|
|
for k in COHORTS:
|
|
INIT[CUR[k]] = c.INIT["H" + k] # start with D = C = GADM-0.3 H, so H_c is unchanged
|
|
INIT[DUR[k]] = c.INIT["H" + k]
|
|
# T04: wealth concentration and cohort attention start at their own equilibria
|
|
# (all other stocks fixed at INIT); computed by equilibrium_init() and checked
|
|
# by a unit test. GADM-0.3 used W = 0.5, Aw = 0.45, As = 0.65.
|
|
INIT.update({"W": 0.1826, "Aw": 0.5406, "As": 0.5216})
|
|
# T05: cohort agency, started at its equilibrium (equilibrium_init(("Gw", "Gs")))
|
|
INIT.update({"Gw": 0.5373, "Gs": 0.5214})
|
|
# WP-0009-T03: sentiment and legislative stance start neutral
|
|
INIT.update({"Sig": 0.5, "Lg": 0.5})
|
|
|
|
PARAMS = dict(c.PARAMS)
|
|
PARAMS.update({
|
|
"hform": 0.15, # durable formation in childhood (scaled by edu)
|
|
"hformX": 0.04, # contribution of commons X to durable formation
|
|
"dD": 0.01, # durable depreciation, children and working age (per year)
|
|
"dDs": 0.02, # seniors
|
|
"dDd": 0.04, # very old
|
|
"ll": 0.005, # lifelong durable formation for working-age adults
|
|
"lbl": 1.0, # strength of "learning begets learning" (0 = off)
|
|
# T02 grounding: organism capability depreciates like software / R&D / AI
|
|
# hardware (geometric 0.15-0.35 per year), not 0.06. Investment, learning and
|
|
# seeding are rescaled by 3.33 so the baseline organism path stays as before
|
|
# (K_Omega ~0.46 at t=25, ~0.37 at t=100): evidence changes the timescale.
|
|
"dO": 0.20, "aO": 0.50, "ell": 0.83, "oseed": 0.033,
|
|
# T05: effective agency per cohort (working age, seniors)
|
|
"cohort_G": 1.0, # 0 = aggregate G as in GADM-0.3 (Gw, Gs inert)
|
|
"gentry0": 0.30, # agency of young adults entering working age ...
|
|
"gentryD": 0.50, # ... plus this times children's durable capability
|
|
"g3s": 0.5, # seniors' restartability weight (relative to working age)
|
|
"b2s": 1.5, # seniors' exposure to operator lock-in (digital exclusion)
|
|
"gcare": 0.02, # care support (relief from participation claims) raising senior agency
|
|
# WP-0009: self-enhancement of organism capability (AI accelerating its own
|
|
# development); scales organism investment and learning by (1 + selfenh * K_Omega).
|
|
# 0 = off (default); used by the aggressive band anchor B4.
|
|
"selfenh": 0.0,
|
|
# WP-0009-T03: sentiment and legitimacy loop (research/ScenarioBandMethod.md § 3).
|
|
# Sigma (Sig) moves toward 0.5 + sig_gain * (perceived net benefit - its initial
|
|
# value); it scales organism adoption. Legislative stance Lg follows Sigma with
|
|
# lag leg_lag (years) and sets deployment permissiveness, membrane rights m and
|
|
# explainability expl. 0 = off (default): Sig and Lg stay inert.
|
|
"sent": 0.0,
|
|
"sig_rate": 0.2, # 1/years: Sigma adjusts over about 5 years
|
|
"sig_gain": 2.0, # sentiment response to a change in perceived benefit
|
|
"sig_disp": 0.5, # weight of displacement K_Omega * (1 - m) in perceived benefit
|
|
"sig_adopt": 1.0, # adoption elasticity to sentiment
|
|
"leg_lag": 5.0, # years from sentiment to legislation
|
|
"leg_perm": 1.0, # permissiveness: adoption scale per unit of stance above neutral
|
|
"leg_m": 0.5, # membrane rights tightening per unit of stance below neutral
|
|
"leg_expl": 0.5, # explainability tightening per unit of stance below neutral
|
|
"sig_pb0": None, # reference perceived benefit; None = value at the initial state
|
|
})
|
|
|
|
|
|
def equilibrium_init(keys=("W", "Aw", "As"), steps: int = 20000, h: float = 0.05,
|
|
params: dict | None = None) -> dict:
|
|
"""Relax `keys` to equilibrium with every other stock frozen at INIT."""
|
|
s = dict(INIT)
|
|
p = {**PARAMS, **(params or {})}
|
|
for _ in range(steps):
|
|
d, _a = derivatives(s, p)
|
|
for k in keys:
|
|
s[k] = min(1.0, max(0.0, s[k] + h * d[k]))
|
|
return {k: round(s[k], 4) for k in keys}
|
|
|
|
|
|
def calibrate(params: dict | None = None, init: dict | None = None) -> dict:
|
|
"""Channel counter coefficients giving channel equilibrium at GADM-0.4's INIT."""
|
|
s0 = {**INIT, **(init or {})}
|
|
agg = c.aggregates({**s0, **effective(s0)})
|
|
macro = {k: v for k, v in s0.items() if k in g3.STOCKS}
|
|
macro.update({"H": agg["H"], "A": agg["A"]})
|
|
return g3.calibrate({**PARAMS, **(params or {})}, macro)
|
|
|
|
|
|
def effective(s: dict) -> dict:
|
|
return {"H" + k: 0.5 * s[DUR[k]] + 0.5 * s[CUR[k]] for k in COHORTS}
|
|
|
|
|
|
def agency(s: dict) -> float:
|
|
"""Adult-weighted effective agency."""
|
|
n = c.shares(s)
|
|
return (n["w"] * s["Gw"] + n["s"] * s["Gs"]) / (n["w"] + n["s"])
|
|
|
|
|
|
def perceived_benefit(s: dict, a: dict, p: dict) -> float:
|
|
"""Perceived net benefit (T03): broad claims, agency, attention and absence of
|
|
domination, minus displacement by operator-controlled organisms."""
|
|
return 0.3 * a["B"] + 0.3 * agency(s) + 0.2 * a["A"] + 0.2 * (1 - g3.total_p(s)) \
|
|
- p["sig_disp"] * s["Ko"] * (1 - p["m"])
|
|
|
|
|
|
def legislated(s: dict, p: dict) -> dict:
|
|
"""Parameters with legislation levers shifted by the legislative stance Lg."""
|
|
if not p["sent"]:
|
|
return p
|
|
gap = 0.5 - s["Lg"] # > 0: tightening after negative sentiment
|
|
clip = lambda v: min(1.0, max(0.0, v)) # noqa: E731
|
|
return {**p, "m": clip(p["m"] + p["leg_m"] * gap), "expl": clip(p["expl"] + p["leg_expl"] * gap)}
|
|
|
|
|
|
def derivatives(s: dict, p: dict) -> tuple[dict, dict]:
|
|
p = legislated(s, p)
|
|
eff = effective(s)
|
|
sc = {k: v for k, v in s.items() if k in c.STOCKS}
|
|
sc.update(eff)
|
|
if p["cohort_G"]:
|
|
sc["G"] = agency(s)
|
|
# current-capability dynamics: GADM-0.3 cohort form, evaluated on current capability
|
|
cur_state = dict(sc)
|
|
cur_state.update({"H" + k: s[CUR[k]] for k in COHORTS})
|
|
dc, a = c.derivatives(cur_state, p)
|
|
# macro feedback must see effective capability: recompute non-capability flows
|
|
d_eff, a = c.derivatives(sc, p)
|
|
d = {k: v for k, v in d_eff.items() if not (k.startswith("H") and k[1:] in COHORTS)}
|
|
if p["selfenh"]:
|
|
Ko = s["Ko"]
|
|
grow = (p["aO"] * a["Vsel"] * a["dem"] + p["ell"] * Ko * a["dem"] * s["Q"]) * (1 - Ko)
|
|
d["Ko"] += p["selfenh"] * Ko * grow
|
|
if p["sent"]:
|
|
# sentiment and permissiveness scale the organism inflow, not its depreciation
|
|
scale = max(0.0, 1 + p["sig_adopt"] * 2 * (s["Sig"] - 0.5)) \
|
|
* max(0.0, 1 + p["leg_perm"] * 2 * (s["Lg"] - 0.5))
|
|
inflow = d["Ko"] + p["dO"] * s["Ko"]
|
|
d["Ko"] = scale * inflow - p["dO"] * s["Ko"]
|
|
pb = perceived_benefit(s, a, p)
|
|
pb0 = pb if p["sig_pb0"] is None else p["sig_pb0"]
|
|
target = min(1.0, max(0.0, 0.5 + p["sig_gain"] * (pb - pb0)))
|
|
d["Sig"] = p["sent"] * p["sig_rate"] * (target - s["Sig"])
|
|
d["Lg"] = p["sent"] * (s["Sig"] - s["Lg"]) / p["leg_lag"]
|
|
else:
|
|
d["Sig"] = d["Lg"] = 0.0
|
|
|
|
n = c.shares(s)
|
|
flow = {"ch": c.AGE_RATE["ch"] * s["Nch"], "w": c.AGE_RATE["w"] * s["Nw"], "s": c.AGE_RATE["s"] * s["Ns"]}
|
|
S, B, X = s["S"], a["B"], s["X"]
|
|
D0 = 0.5
|
|
for k in COHORTS:
|
|
# learning-begets-learning: scale the cohort's net current-capability change
|
|
# by its durable capability relative to the initial reference level
|
|
scale = 1 + p["lbl"] * (s[DUR[k]] - D0)
|
|
d[CUR[k]] = dc["H" + k] * (scale if dc["H" + k] > 0 else 1.0)
|
|
|
|
Dch, Dw, Ds, Dd = (s[DUR[k]] for k in COHORTS)
|
|
births = p["birth"] * s["Nw"]
|
|
d[DUR["ch"]] = p["edu"] * (p["hform"] * (0.5 * S + 0.5 * B) + p["hformX"] * X) * (1 - Dch) - p["dD"] * Dch \
|
|
- births / s["Nch"] * Dch # newborns enter with no durable capability
|
|
d[DUR["w"]] = p["ll"] * (1 - Dw) - p["dD"] * Dw + flow["ch"] / s["Nw"] * (Dch - Dw)
|
|
d[DUR["s"]] = -p["dDs"] * Ds + flow["w"] / s["Ns"] * (Dw - Ds)
|
|
d[DUR["d"]] = -p["dDd"] * Dd + flow["s"] / s["Nd"] * (Ds - Dd)
|
|
|
|
# T05: cohort agency (GADM-0.2 form with age-specific modifiers)
|
|
if p["cohort_G"]:
|
|
P, F, Ko, m = g3.total_p(s), s["F"], s["Ko"], p["m"]
|
|
Gw, Gs = s["Gw"], s["Gs"]
|
|
common = p["g1"] * B + p["g2"] * X * (1 - P) + p["g4"] * Ko * m
|
|
relief = min(1.0, p["care_relief"] * p["cp"])
|
|
g_entry = p["gentry0"] + p["gentryD"] * Dch
|
|
d["Gw"] = (common + p["g3"] * F) * (1 - Gw) \
|
|
- (p["dG"] + p["b1"] * P + p["b2"] * Ko * (1 - m)) * Gw \
|
|
+ flow["ch"] / s["Nw"] * (g_entry - Gw)
|
|
d["Gs"] = (common + p["g3s"] * p["g3"] * F + p["gcare"] * relief) * (1 - Gs) \
|
|
- (p["dG"] + p["b1"] * P + p["b2s"] * p["b2"] * Ko * (1 - m)) * Gs \
|
|
+ flow["w"] / s["Ns"] * (Gw - Gs)
|
|
d["G"] = 0.0
|
|
else:
|
|
d["Gw"] = d["Gs"] = 0.0
|
|
return d, {**a, **eff, "n_ch": n["ch"], "n_w": n["w"], "n_s": n["s"], "n_d": n["d"],
|
|
"Gagg": agency(s) if p["cohort_G"] else s["G"]}
|
|
|
|
|
|
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)}")
|
|
if p["sent"] and p["sig_pb0"] is None:
|
|
# T03: perceived benefit is judged against the starting situation
|
|
_d, a0 = derivatives(s, p)
|
|
p = {**p, "sig_pb0": perceived_benefit(s, a0, legislated(s, p))}
|
|
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, "P": g3.total_p(s), "G": a["Gagg"],
|
|
**{k: a[k] for k in ("H", "A", "n_ch", "n_w", "n_s", "n_d", "care_need",
|
|
"Ystar", "B", "V", "Vsel", "Hch", "Hw", "Hs", "Hd")}}
|
|
row["VFAC"] = g2.vfac(row)
|
|
rows.append(row)
|
|
if i < steps:
|
|
s = {k: (max(0.0, s[k] + dt * d[k]) if k.startswith("N")
|
|
else min(1.0, max(0.0, s[k] + dt * d[k]))) for k in STOCKS}
|
|
return rows
|
|
|
|
|
|
# T04: calibrate channel counters at this model's own initial state
|
|
PARAMS.update(calibrate())
|