- 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
141 lines
5.9 KiB
Python
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
|