assembly-economics/simulations/gadm02/gadm03c.py
tegwick b6a0d7b3ff ASSEMBLY-WP-0005-T06: life-cycle cohorts; finish WP-0005; add WP-0006
- gadm03c.py: four cohorts with demography, cohort capability, mentoring,
  care load on attention; stationary at replacement fertility (tested)
- life_cycle.py: L1 supported after fixing an inert mentoring term; L2
  conditional on who carries care per person, care relief via participation
  claims protects carers' attention; L3 not supported (H lacks a durable
  component)
- GADM-0.3.md complete; ASSEMBLY-WP-0006 (GADM-0.4, proposed residual)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1494246@bnt-lap001
Assistant-Session: caf6de6f-f9e4-47ce-a584-49baa271fa4b
2026-10-12 07:38:22 +02:00

141 lines
5.9 KiB
Python

"""GADM-0.3 with life-cycle cohorts (ASSEMBLY-WP-0005-T06).
Extends gadm03 (power channels, wealth, recalibrated attention). Human
capability H and attention headroom A become cohort quantities; everything else
is unchanged and sees population-weighted aggregates:
cohorts: ch (children, 0-19), w (working age, 20-64),
s (seniors, 65-79), d (very old / dependent, 80+)
Population counts N_c evolve by births (b * N_w), ageing (1/20, 1/45, 1/15 per
year) and deaths of the very old (1/8 per year). With b = 1/45 the age
structure is stationary (shares ≈ 23 / 51 / 17 / 9 %).
Intensive cohort quantities follow their own dynamics plus a mixing term when
people age in: dX_c += (inflow_c / N_c) * (X_from - X_c).
Effective agency G stays a single aggregate in this version.
Mentoring (T_g) is an inflow proportional to seniors' capability and
working-age headroom. A first version transferred only max(0, Hs - Hw); since
seniors' capability decays below working-age capability, that term was inert
(L1 showed zero effect) and was replaced.
"""
from __future__ import annotations
import gadm02 as g2
import gadm03 as g3
MODEL_VERSION = "GADM-0.3-cohorts"
TIME_UNIT = g3.TIME_UNIT
COHORTS = ("ch", "w", "s", "d")
AGE_RATE = {"ch": 1 / 20, "w": 1 / 45, "s": 1 / 15}
DEATH_D = 1 / 8
STOCKS = tuple(k for k in g3.STOCKS if k not in ("H", "A")) + (
"Nch", "Nw", "Ns", "Nd", "Hch", "Hw", "Hs", "Hd", "Aw", "As")
INIT = {k: v for k, v in g3.INIT.items() if k not in ("H", "A")}
INIT.update({
# stationary age structure for b = 1/45 (shares of a population of 1)
"Nch": 0.2273, "Nw": 0.5114, "Ns": 0.1705, "Nd": 0.0909,
"Hch": 0.40, "Hw": 0.55, "Hs": 0.55, "Hd": 0.40, # aggregate H ≈ 0.50
"Aw": 0.45, "As": 0.65, # aggregate adult A ≈ 0.50
})
PARAMS = dict(g3.PARAMS)
PARAMS.update({
"birth": 1 / 45, # births per working-age person per year (1/45 = replacement)
"edu": 1.0, # education intensity for children (multiplier)
"tg": 0.05, # intergenerational transfer: seniors' mentoring of working age
"tgc": 0.03, # seniors' mentoring of children
"dHs": 1.5, # capability depreciation multiplier, seniors
"dHd": 3.0, # capability depreciation multiplier, very old
"learn_s": 0.5, # learning multiplier for seniors
"care_load": 0.12, # attention cost of care per unit of care need
"care_sen": 0.3, # share of informal care provided by seniors
"care_relief": 3.0, # relief of informal care per unit of participation claims cp
})
def shares(s: dict) -> dict:
tot = s["Nch"] + s["Nw"] + s["Ns"] + s["Nd"]
return {c: s["N" + c] / tot for c in COHORTS}
def aggregates(s: dict) -> dict:
n = shares(s)
H = sum(n[c] * s["H" + c] for c in COHORTS)
A = (n["w"] * s["Aw"] + n["s"] * s["As"]) / (n["w"] + n["s"])
return {"H": H, "A": A, **{"n_" + c: n[c] for c in COHORTS}}
def derivatives(s: dict, p: dict) -> tuple[dict, dict]:
agg = aggregates(s)
base = {k: v for k, v in s.items() if k in g3.STOCKS}
base["H"], base["A"] = agg["H"], agg["A"]
d3, a = g3.derivatives(base, p)
d = {k: v for k, v in d3.items() if k not in ("H", "A")}
S, B, X, Ko, m = s["S"], a["B"], s["X"], s["Ko"], p["m"]
n = shares(s)
Nch, Nw, Ns, Nd = s["Nch"], s["Nw"], s["Ns"], s["Nd"]
# demography
births = p["birth"] * Nw
flow = {"ch": AGE_RATE["ch"] * Nch, "w": AGE_RATE["w"] * Nw, "s": AGE_RATE["s"] * Ns}
d["Nch"] = births - flow["ch"]
d["Nw"] = flow["ch"] - flow["w"]
d["Ns"] = flow["w"] - flow["s"]
d["Nd"] = flow["s"] - DEATH_D * Nd
# human capability per cohort (GADM-0.2 form, cohort-specific modifiers)
social = p["h0"] * (0.5 * S + 0.5 * B)
desk = p["desk"] * Ko * (1 - m)
Hch, Hw, Hs, Hd = s["Hch"], s["Hw"], s["Hs"], s["Hd"]
d["Hch"] = (p["edu"] * (social + p["hT"] * X) + p["hA"] * s["Aw"] + p["tgc"] * n["s"] / max(n["ch"], 1e-9) * Hs) \
* (1 - Hch) - p["dH"] * Hch
d["Hw"] = (social + p["hA"] * s["Aw"] + p["hT"] * X) * (1 - Hw) - (p["dH"] + desk) * Hw \
+ p["tg"] * n["s"] / max(n["w"], 1e-9) * Hs * (1 - Hw) \
+ flow["ch"] / Nw * (Hch - Hw)
d["Hs"] = p["learn_s"] * (social + p["hA"] * s["As"] + p["hT"] * X) * (1 - Hs) \
- (p["dHs"] * p["dH"] + desk) * Hs + flow["w"] / Ns * (Hw - Hs)
d["Hd"] = -p["dHd"] * p["dH"] * Hd + flow["s"] / Nd * (Hs - Hd)
# attention headroom: extraction (as GADM-0.3) plus informal care load
care_need = 0.5 * n["ch"] + 1.0 * n["d"]
relief = max(0.0, 1 - p["care_relief"] * p["cp"])
load = p["care_load"] * care_need * relief
ext = p["aext"] * p["x_att"] * (0.2 + Ko)
rec = p["arec"] * (0.4 + 0.6 * B)
d["Aw"] = rec * (1 - s["Aw"]) - (ext + load * (1 - p["care_sen"]) / n["w"]) * s["Aw"] \
+ flow["ch"] / Nw * (0.5 - s["Aw"]) # young adults enter with A = 0.5
d["As"] = rec * (1 - s["As"]) - (ext + load * p["care_sen"] / n["s"]) * s["As"] \
+ flow["w"] / Ns * (s["Aw"] - s["As"])
a = {**a, **agg, "care_need": care_need}
return d, a
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, "P": g3.total_p(s),
**{k: a[k] for k in ("H", "A", "n_ch", "n_w", "n_s", "n_d", "care_need",
"Ystar", "B", "V", "Vsel")}}
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