assembly-economics/simulations/gadm02/gsa.py
tegwick 144d6b78fc ASSEMBLY-WP-0005-T01: time unit and global sensitivity analysis
- One period = one year; implied timescales documented
- simulations/gadm02/gsa.py: Morris screening + Sobol indices (S1 Saltelli
  2010 centred, ST Jansen) with bootstrap CIs
- GADM-0.2 readings updated: model nearly additive within ±50 %; the
  'reuse as a bundle' interaction reading is refuted; organism governance
  levers confirmed globally; dO flagged as calibration priority

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 01:37:35 +02:00

149 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Global sensitivity analysis of final VFAC: Morris screening + Sobol indices.
Morris (elementary effects, p = 4 levels) screens all parameters. Sobol
first-order (S1, Saltelli 2010 estimator) and total (ST, Jansen estimator)
indices with bootstrap
95 % intervals, are then computed for the top Morris parameters plus the
reuse bundle (rho0, IX, lin_open, std), so the "reuse works as a bundle"
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]
"""
from __future__ import annotations
import random
import statistics
import sys
from pathlib import Path
import gadm02 as g
from scenarios import load_all
OUT = Path(__file__).resolve().parent / "results"
LEVERS = {"m", "L", "lin_open", "std", "disc", "expl", "x_att", "auto", "dshare", "cp", "od"}
BUNDLE = ["rho0", "IX", "lin_open", "std"]
SEED = 20261012
def ranges(base: dict) -> dict[str, tuple[float, float]]:
out = {}
for k, v in base.items():
lo, hi = (0.0, 0.1) if v == 0 else (v * 0.5, v * 1.5)
if k in LEVERS:
lo, hi = max(0.0, lo), min(1.0, hi)
out[k] = (lo, hi)
return out
def model(base: dict, rng: dict, names: list[str], u: list[float], init: dict) -> float:
p = dict(base)
for k, x in zip(names, u):
lo, hi = rng[k]
p[k] = lo + x * (hi - lo)
return g.simulate(p, init)[-1]["VFAC"]
def morris(base, init, rng, r, rnd):
names = list(base)
k, delta = len(names), 2 / 3
effects = {n: [] for n in names}
for _ in range(r):
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):
x[i] += delta
y2 = model(base, rng, names, x, init)
effects[names[i]].append((y2 - y) / delta)
y = y2
res = []
for n, ee in effects.items():
res.append((n, statistics.fmean(abs(e) for e in ee), statistics.fmean(ee), statistics.pstdev(ee)))
res.sort(key=lambda t: -t[1])
return res
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)])
def indices(idx):
a = [fA[j] for j in idx]
b = [fB[j] for j in idx]
var = statistics.pvariance(a + b) or 1e-12
mu = statistics.fmean(a + b)
out = []
for i in range(k):
ab = [fAB[i][j] for j in idx]
# Saltelli 2010 with centred outputs: VFAC has a large mean relative to its
# spread, and centring removes that mean from the product's variance.
s1 = sum((bb - mu) * (x - aa) for aa, bb, x in zip(a, b, ab)) / len(idx) / var
st = sum((aa - x) ** 2 for aa, x in zip(a, ab)) / (2 * len(idx)) / var
out.append((s1, st))
return out
point = indices(range(n))
reps = [indices([rnd.randrange(n) for _ in range(n)]) for _ in range(boot)]
def ci(i, which):
vals = sorted(r[i][which] for r in reps)
return vals[int(0.025 * boot)], vals[int(0.975 * boot) - 1]
return [(names[i], point[i][0], ci(i, 0), point[i][1], ci(i, 1)) for i in range(k)]
def main() -> None:
quick = "--quick" in sys.argv
r, n, top = (4, 32, 6) if quick else (20, 256, 10)
OUT.mkdir(exist_ok=True)
scs = load_all()
lines = [f"# {g.MODEL_VERSION} global sensitivity analysis", "",
"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]).",
f"Morris: {r} trajectories, 4 levels. Sobol: S1 Saltelli (2010, centred), ST Jansen, N = {n},",
f"bootstrap 95 % intervals, seed {SEED}. 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.", ""]
for name in ("baseline", "generative_regime"):
rnd = random.Random(SEED)
sc = scs[name]
base = {**g.PARAMS, **sc["params"]}
rng = ranges(base)
mres = morris(base, sc["init"], rng, r, rnd)
lines += [f"## {sc['title']}", "", "### Morris screening (top 15)", "",
"| Rank | Parameter | μ* | μ | σ |", "|---|---|---|---|---|"]
lines += [f"| {i + 1} | `{p}` | {ms:.4f} | {mu:+.4f} | {sd:.4f} |"
for i, (p, ms, mu, sd) in enumerate(mres[:15])]
mrank = {p: i + 1 for i, (p, *_rest) in enumerate(mres)}
lines += ["", "Reuse bundle Morris ranks: " + ", ".join(f"`{b}` {mrank[b]}" for b in BUNDLE), ""]
names = [p for p, *_rest in mres[:top]] + [b for b in BUNDLE if b not in [p for p, *_r in mres[:top]]]
sres = sobol(base, sc["init"], rng, names, n, rnd)
lines += ["### Sobol indices", "",
"| Parameter | S1 | S1 95 % CI | ST | ST 95 % CI | ST − S1 |", "|---|---|---|---|---|---|"]
for p, s1, c1, st, ct in sorted(sres, key=lambda t: -t[3]):
lines.append(f"| `{p}` | {s1:.3f} | [{c1[0]:.3f}, {c1[1]:.3f}] | {st:.3f} | "
f"[{ct[0]:.3f}, {ct[1]:.3f}] | {st - s1:+.3f} |")
b1 = sum(s1 for p, s1, _c, _st, _ct in sres if p in BUNDLE)
bt = sum(st for p, _s1, _c, st, _ct in sres if p in BUNDLE)
tot1 = sum(s1 for _p, s1, *_r in sres)
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))
print("\n".join(lines))
if __name__ == "__main__":
main()