diff --git a/AGENTS.md b/AGENTS.md index 14d4ab7..420100b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,8 @@ simulation. No install step. ```bash python3 -m unittest discover -s simulations/gadm02 # must pass before committing model changes cd simulations/gadm02 && python3 run_scenarios.py && python3 sensitivity.py -cd simulations/gadm02 && python3 gsa.py # global sensitivity, ~5 min +cd simulations/gadm02 && python3 -B gsa.py --jobs 8 # global sensitivity GADM-0.2, ~1.5 min +cd simulations/gadm02 && python3 -B gsa.py --model gadm04 --jobs 8 # GADM-0.4, ~12 min cd simulations/gadm02 && python3 -B power_channels.py # GADM-0.3 power channels (T03 stage) cd simulations/gadm02 && python3 -B wealth.py # GADM-0.3 wealth vs. power cd simulations/gadm02 && python3 -B organism_grid.py # GADM-0.3 organism grid diff --git a/models/GADM-0.4.md b/models/GADM-0.4.md index 6d3c9e1..7837240 100644 --- a/models/GADM-0.4.md +++ b/models/GADM-0.4.md @@ -8,7 +8,7 @@ the code is the reference if the two ever disagree. **Status:** in development, exploratory. Implemented so far: durable human capability (§ 1, ASSEMBLY-WP-0006-T01), grounded organism depreciation and -robustness checks (§ 2, T02). +robustness checks (§ 2, T02), global sensitivity analysis (§ 3, T03). GADM-0.4 builds on the GADM-0.3 cohort model (`GADM-0.3.md` § 5), which stays unchanged and reproducible in `gadm03c.py`. Time unit: one year. @@ -106,10 +106,65 @@ Details and sources: `ParameterGrounding.md`. are essentially unchanged: L3 half-time 17 years, final ΔH working age +0.032; L1 ΔH −0.028 (ageing, strong mentoring) against −0.038 (stable, none). +--- + +## 3. Global sensitivity analysis (T03) + +Same method as `GADM-0.2.md` § 5.1: Morris screening over all parameters, then +Sobol indices (S1 Saltelli 2010 centred, ST Jansen, N = 256, bootstrap 95 % +intervals) for the top 10 plus the reuse bundle; ±50 % ranges. New for +GADM-0.4: + +- the channel counters c_* are recalibrated for every sample (not varied), so + each sample starts in equilibrium; +- the switch `wealth_on` is fixed; +- evaluation is parallel (`--jobs 8`). The parallel code reproduces the + GADM-0.2 analysis exactly. + +Full tables: `simulations/gadm02/results/gsa_gadm04.md`. + +| | Baseline | Generative regime | +|---|---|---| +| Top ST | `IX` 0.25, `dO` 0.23, `q0` 0.10, `lX` 0.10, `dX` 0.08 | `dO` 0.17, `expl` 0.17, `m` 0.16, `IX` 0.13, `q0` 0.12 | +| ΣS1 (analysed set) | 1.03 | 0.89 | +| Reuse bundle ΣS1 / ΣST | 0.30 / 0.28 | 0.20 / 0.26 | +| Morris σ ≥ μ* (strong interaction / non-linearity) | none | `L` (σ 0.051 vs. μ* 0.036), `r_lin` (0.043 vs. 0.042), `m` (0.057 vs. 0.062) | + +For comparison, GADM-0.2: baseline top ST was `dO` 0.34, `p7` 0.13, `ell` 0.10; +generative top ST was `m` 0.27, `L` 0.13, `ereg` 0.13; ΣS1 ≈ 1.04–1.06. + +### 3.1 Readings + +1. **Additivity holds at baseline, but breaks for the governance levers in + the generative regime.** That is where the superlinear power channels + operate near their thresholds. Lineage diffusion `L` acts almost entirely + through interactions (S1 ≈ 0, ST 0.04), together with the lineage + self-reinforcement `r_lin`: it matters where it pushes the lineage channel + across its tipping point (GADM-0.3 P1/P2). One-at-a-time analysis is no + longer adequate for these levers. +2. **The commons cluster now drives the baseline.** Commons investment `IX`, + enclosure `lX` and obsolescence `dX` together carry about 0.43 of total + ST. In GADM-0.2, the reuse bundle carried about 0.11. Two things changed: + durable capability makes X's contribution to formation persist, and the + power channels make infrastructure control respond to X and standards. The + bundle's effect is mostly **IX alone**, without interaction (ΣST ≈ ΣS1). + So the GADM-0.2 correction ("reuse is not an interaction bundle") still + stands. +3. **The level of organism capability is a top uncertainty.** `dO` stays among + the top two drivers in both scenarios (ST 0.17–0.23), even after grounding. + Varying `dO` with investment held fixed moves the organism level, which is + exactly the parameter `ParameterGrounding.md` lists as ungrounded. Grounding + that level is the most valuable next empirical step. +4. **Explainability gains weight in the generative regime** (ST 0.17, up from + 0.06 in GADM-0.2). With power channels, epistemic capacity Q does double + duty: it checks the political channel and feeds the discovery counter. So + `expl` acts on power as well as on knowledge. + ## Reproduce ```bash python3 -B -m unittest discover -s simulations/gadm02 cd simulations/gadm02 && python3 -B life_cycle.py --model gadm04 cd simulations/gadm02 && python3 -B robustness.py # T02 robustness checks +cd simulations/gadm02 && python3 -B gsa.py --model gadm04 --jobs 8 # T03, ~12 min on 8 cores ``` diff --git a/models/ParameterGrounding.md b/models/ParameterGrounding.md index 2e73a2e..954ee33 100644 --- a/models/ParameterGrounding.md +++ b/models/ParameterGrounding.md @@ -59,7 +59,8 @@ Readings: ## Open grounding needs 1. The level of organism capability K_Ω (AI capital share, task-automation - share). + share). Confirmed as a top uncertainty by the GADM-0.4 Sobol analysis + (`GADM-0.4.md` § 3, reading 3). 2. The channel self-reinforcement r_k (persistence of market leadership, platform tipping evidence). 3. The wealth → influence conversion k_w (political-economy evidence on diff --git a/simulations/README.md b/simulations/README.md index 3ce8e32..83348f9 100644 --- a/simulations/README.md +++ b/simulations/README.md @@ -17,7 +17,7 @@ bytecode cache can mask same-size changes made within the same second. python3 -m unittest discover -s simulations/gadm02 # tests cd simulations/gadm02 && python3 run_scenarios.py # → results/summary.md + CSVs cd simulations/gadm02 && python3 sensitivity.py # → results/sensitivity.md -cd simulations/gadm02 && python3 gsa.py # → results/gsa.md (~5 min; --quick) +cd simulations/gadm02 && python3 -B gsa.py --jobs 8 # → results/gsa.md (--model gadm04 → gsa_gadm04.md; --quick) ``` Results under `results/` are committed so that model documents can cite them. diff --git a/simulations/gadm02/gsa.py b/simulations/gadm02/gsa.py index 1bd3df6..5f98576 100644 --- a/simulations/gadm02/gsa.py +++ b/simulations/gadm02/gsa.py @@ -10,18 +10,32 @@ reading in models/GADM-0.2.md can be tested directly. Ranges: each parameter varies ±50 % around its scenario value; levers are clipped to [0, 1]; zero-valued parameters vary in [0, 0.1]. -Usage: python3 simulations/gadm02/gsa.py [--quick] +Usage: python3 -B simulations/gadm02/gsa.py [--quick] [--model gadm04] [--jobs 8] + +For models with power channels (gadm03, gadm03c, gadm04), the channel counter +coefficients c_* are not varied but recalibrated for every sample, so each +sample starts in equilibrium; the wealth_on switch is not varied. """ from __future__ import annotations +import importlib +import multiprocessing as mp import random import statistics import sys from pathlib import Path -import gadm02 as g -from scenarios import load_all +sys.dont_write_bytecode = True + +import gadm03 # noqa: E402 +from scenarios import load_all # noqa: E402 + +MODEL = sys.argv[sys.argv.index("--model") + 1] if "--model" in sys.argv else "gadm02" +JOBS = int(sys.argv[sys.argv.index("--jobs") + 1]) if "--jobs" in sys.argv else 1 +g = importlib.import_module(MODEL) +CHANNELS = "Pm" in g.STOCKS +FIXED = {"wealth_on"} | ({c for c in gadm03.COEF.values()} if CHANNELS else set()) OUT = Path(__file__).resolve().parent / "results" LEVERS = {"m", "L", "lin_open", "std", "disc", "expl", "x_att", "auto", "dshare", "cp", "od"} @@ -44,21 +58,52 @@ def model(base: dict, rng: dict, names: list[str], u: list[float], init: dict) - for k, x in zip(names, u): lo, hi = rng[k] p[k] = lo + x * (hi - lo) + if CHANNELS: + p.update(gadm03.calibrate(p)) return g.simulate(p, init)[-1]["VFAC"] +_CTX: dict = {} + + +def _init(ctx: dict) -> None: + _CTX.update(ctx) + + +def _eval(u: list[float]) -> float: + return model(_CTX["base"], _CTX["rng"], _CTX["names"], u, _CTX["init"]) + + +def evaluate(base, rng, names, init, points: list[list[float]]) -> list[float]: + """Evaluate many sample points, in parallel when --jobs > 1.""" + if JOBS <= 1: + return [model(base, rng, names, u, init) for u in points] + ctx = {"base": base, "rng": rng, "names": names, "init": init} + with mp.Pool(JOBS, initializer=_init, initargs=(ctx,)) as pool: + return pool.map(_eval, points, chunksize=8) + + def morris(base, init, rng, r, rnd): - names = list(base) + names = [n for n in base if n not in FIXED] k, delta = len(names), 2 / 3 effects = {n: [] for n in names} - for _ in range(r): + trajectories = [] + for _ in range(r): # draw all trajectories first (same RNG order as before) x = [rnd.choice((0.0, 1 / 3)) for _ in range(k)] - y = model(base, rng, names, x, init) - for i in rnd.sample(range(k), k): + order = rnd.sample(range(k), k) + pts = [list(x)] + for i in order: x[i] += delta - y2 = model(base, rng, names, x, init) - effects[names[i]].append((y2 - y) / delta) - y = y2 + pts.append(list(x)) + trajectories.append((order, pts)) + flat = [pt for _o, pts in trajectories for pt in pts] + ys = evaluate(base, rng, names, init, flat) + pos = 0 + for order, pts in trajectories: + y = ys[pos:pos + len(pts)] + pos += len(pts) + for j, i in enumerate(order): + effects[names[i]].append((y[j + 1] - y[j]) / delta) res = [] for n, ee in effects.items(): res.append((n, statistics.fmean(abs(e) for e in ee), statistics.fmean(ee), statistics.pstdev(ee))) @@ -70,11 +115,10 @@ def sobol(base, init, rng, names, n, rnd, boot=200): k = len(names) A = [[rnd.random() for _ in range(k)] for _ in range(n)] B = [[rnd.random() for _ in range(k)] for _ in range(n)] - fA = [model(base, rng, names, a, init) for a in A] - fB = [model(base, rng, names, b, init) for b in B] - fAB = [] - for i in range(k): - fAB.append([model(base, rng, names, A[j][:i] + [B[j][i]] + A[j][i + 1:], init) for j in range(n)]) + pts = A + B + [A[j][:i] + [B[j][i]] + A[j][i + 1:] for i in range(k) for j in range(n)] + ys = evaluate(base, rng, names, init, pts) + fA, fB = ys[:n], ys[n:2 * n] + fAB = [ys[2 * n + i * n: 2 * n + (i + 1) * n] for i in range(k)] def indices(idx): a = [fA[j] for j in idx] @@ -107,6 +151,8 @@ def main() -> None: OUT.mkdir(exist_ok=True) scs = load_all() lines = [f"# {g.MODEL_VERSION} global sensitivity analysis", "", + *([("Channel counters c_* are recalibrated per sample (not varied); " + "wealth_on is fixed."), ""] if CHANNELS else []), "Generated by `simulations/gadm02/gsa.py`" + (" (--quick)" if quick else "") + ".", f"Output: final VFAC (t = 100 {g.TIME_UNIT}s). Parameters vary ±50 % around the scenario", "value (levers clipped to [0, 1]; zero-valued parameters in [0, 0.1]).", @@ -141,7 +187,8 @@ def main() -> None: lines += ["", f"Sum of S1 over analysed parameters: {tot1:.3f}. Reuse bundle: " f"ΣS1 = {b1:.3f}, ΣST = {bt:.3f}.", ""] print(f"done {name}", file=sys.stderr) - (OUT / ("gsa-quick.md" if quick else "gsa.md")).write_text("\n".join(lines)) + stem = "gsa" if MODEL == "gadm02" else f"gsa_{MODEL}" + (OUT / (f"{stem}-quick.md" if quick else f"{stem}.md")).write_text("\n".join(lines)) print("\n".join(lines)) diff --git a/simulations/gadm02/results/gsa_gadm04.md b/simulations/gadm02/results/gsa_gadm04.md new file mode 100644 index 0000000..e7774b8 --- /dev/null +++ b/simulations/gadm02/results/gsa_gadm04.md @@ -0,0 +1,102 @@ +# GADM-0.4-dev global sensitivity analysis + +Channel counters c_* are recalibrated per sample (not varied); wealth_on is fixed. + +Generated by `simulations/gadm02/gsa.py`. +Output: final VFAC (t = 100 years). Parameters vary ±50 % around the scenario +value (levers clipped to [0, 1]; zero-valued parameters in [0, 0.1]). +Morris: 20 trajectories, 4 levels. Sobol: S1 Saltelli (2010, centred), ST Jansen, N = 256, +bootstrap 95 % intervals, seed 20261012. Parameters outside the Sobol set stay at +their scenario values. + +μ* ranks importance. A σ that is large relative to μ* indicates +interactions or non-linearity. ST − S1 is the share of variance due to +interactions involving that parameter. + +## Baseline (current trends) + +### Morris screening (top 15) + +| Rank | Parameter | μ* | μ | σ | +|---|---|---|---|---| +| 1 | `IX` | 0.0520 | +0.0520 | 0.0119 | +| 2 | `dO` | 0.0503 | +0.0503 | 0.0209 | +| 3 | `q0` | 0.0410 | +0.0410 | 0.0185 | +| 4 | `dX` | 0.0333 | -0.0333 | 0.0096 | +| 5 | `n1` | 0.0328 | +0.0328 | 0.0122 | +| 6 | `ell` | 0.0301 | -0.0301 | 0.0159 | +| 7 | `lX` | 0.0275 | -0.0275 | 0.0061 | +| 8 | `q2` | 0.0263 | -0.0263 | 0.0142 | +| 9 | `g1` | 0.0260 | +0.0260 | 0.0070 | +| 10 | `arec` | 0.0250 | +0.0250 | 0.0046 | +| 11 | `b1` | 0.0225 | -0.0225 | 0.0049 | +| 12 | `wL0` | 0.0216 | +0.0216 | 0.0080 | +| 13 | `r_lin` | 0.0210 | -0.0210 | 0.0098 | +| 14 | `aext` | 0.0198 | -0.0198 | 0.0059 | +| 15 | `r_mem` | 0.0197 | -0.0197 | 0.0092 | + +Reuse bundle Morris ranks: `rho0` 29, `IX` 1, `lin_open` 44, `std` 54 + +### Sobol indices + +| Parameter | S1 | S1 95 % CI | ST | ST 95 % CI | ST − S1 | +|---|---|---|---|---|---| +| `IX` | 0.260 | [0.184, 0.355] | 0.249 | [0.214, 0.291] | -0.011 | +| `dO` | 0.169 | [0.077, 0.246] | 0.228 | [0.182, 0.275] | +0.059 | +| `q0` | 0.100 | [0.057, 0.150] | 0.097 | [0.082, 0.117] | -0.003 | +| `lX` | 0.140 | [0.101, 0.189] | 0.096 | [0.078, 0.115] | -0.044 | +| `dX` | 0.109 | [0.059, 0.162] | 0.079 | [0.066, 0.097] | -0.030 | +| `n1` | 0.019 | [-0.037, 0.078] | 0.079 | [0.065, 0.098] | +0.060 | +| `ell` | 0.070 | [0.027, 0.121] | 0.077 | [0.062, 0.097] | +0.007 | +| `g1` | 0.030 | [-0.015, 0.068] | 0.070 | [0.056, 0.084] | +0.040 | +| `arec` | 0.017 | [-0.028, 0.064] | 0.062 | [0.052, 0.073] | +0.045 | +| `q2` | 0.067 | [0.031, 0.103] | 0.050 | [0.042, 0.059] | -0.016 | +| `rho0` | 0.030 | [0.008, 0.048] | 0.017 | [0.014, 0.021] | -0.013 | +| `std` | 0.009 | [-0.007, 0.024] | 0.006 | [0.004, 0.007] | -0.003 | +| `lin_open` | 0.005 | [-0.006, 0.017] | 0.004 | [0.003, 0.005] | -0.001 | + +Sum of S1 over analysed parameters: 1.025. Reuse bundle: ΣS1 = 0.304, ΣST = 0.277. + +## Generative regime + +### Morris screening (top 15) + +| Rank | Parameter | μ* | μ | σ | +|---|---|---|---|---| +| 1 | `m` | 0.0624 | +0.0546 | 0.0565 | +| 2 | `r_lin` | 0.0423 | -0.0419 | 0.0431 | +| 3 | `IX` | 0.0410 | +0.0410 | 0.0288 | +| 4 | `dO` | 0.0388 | +0.0388 | 0.0245 | +| 5 | `n1` | 0.0365 | +0.0365 | 0.0175 | +| 6 | `L` | 0.0361 | +0.0197 | 0.0508 | +| 7 | `q0` | 0.0350 | +0.0350 | 0.0261 | +| 8 | `expl` | 0.0347 | +0.0347 | 0.0156 | +| 9 | `aX` | 0.0297 | +0.0297 | 0.0203 | +| 10 | `dX` | 0.0292 | -0.0292 | 0.0173 | +| 11 | `q1` | 0.0286 | +0.0286 | 0.0160 | +| 12 | `q2` | 0.0275 | -0.0275 | 0.0142 | +| 13 | `arec` | 0.0267 | +0.0267 | 0.0069 | +| 14 | `ereg` | 0.0267 | +0.0267 | 0.0130 | +| 15 | `g1` | 0.0265 | +0.0265 | 0.0086 | + +Reuse bundle Morris ranks: `rho0` 26, `IX` 3, `lin_open` 31, `std` 22 + +### Sobol indices + +| Parameter | S1 | S1 95 % CI | ST | ST 95 % CI | ST − S1 | +|---|---|---|---|---|---| +| `dO` | 0.147 | [0.076, 0.204] | 0.171 | [0.134, 0.203] | +0.023 | +| `expl` | 0.165 | [0.103, 0.235] | 0.166 | [0.139, 0.196] | +0.000 | +| `m` | 0.095 | [0.022, 0.182] | 0.156 | [0.092, 0.230] | +0.061 | +| `IX` | 0.098 | [0.030, 0.152] | 0.129 | [0.102, 0.162] | +0.032 | +| `q0` | 0.119 | [0.074, 0.192] | 0.118 | [0.096, 0.144] | -0.001 | +| `n1` | 0.075 | [0.023, 0.127] | 0.092 | [0.076, 0.113] | +0.017 | +| `std` | 0.051 | [0.009, 0.091] | 0.066 | [0.052, 0.079] | +0.015 | +| `aX` | 0.044 | [0.009, 0.078] | 0.060 | [0.044, 0.075] | +0.016 | +| `dX` | 0.043 | [-0.002, 0.095] | 0.057 | [0.047, 0.070] | +0.014 | +| `r_lin` | 0.016 | [-0.014, 0.045] | 0.048 | [0.029, 0.070] | +0.032 | +| `L` | -0.013 | [-0.047, 0.013] | 0.039 | [0.017, 0.071] | +0.052 | +| `rho0` | 0.032 | [0.001, 0.061] | 0.038 | [0.030, 0.051] | +0.006 | +| `lin_open` | 0.022 | [-0.004, 0.051] | 0.027 | [0.022, 0.035] | +0.006 | + +Sum of S1 over analysed parameters: 0.894. Reuse bundle: ΣS1 = 0.203, ΣST = 0.262. diff --git a/workplans/ASSEMBLY-WP-0006-gadm-0.4.md b/workplans/ASSEMBLY-WP-0006-gadm-0.4.md index dc61c09..64726a9 100644 --- a/workplans/ASSEMBLY-WP-0006-gadm-0.4.md +++ b/workplans/ASSEMBLY-WP-0006-gadm-0.4.md @@ -51,7 +51,7 @@ literature- or data-based ranges. Re-run the affected readings. ```task id: ASSEMBLY-WP-0006-T03 -status: progress +status: done priority: medium state_hub_task_id: "c33dd30b-a015-55d6-ba75-61dc3d7a1b49" ```