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
89 lines
4.9 KiB
Python
89 lines
4.9 KiB
Python
"""Endogenous selection weights across the adoption band (ASSEMBLY-WP-0007-T03).
|
||
|
||
Tests (GADM-Sectors-Scope.md § 4; ParameterGrounding.md § 3):
|
||
1. Does domination capture selection itself (market + organism weight share rising with P)?
|
||
2. Is capture slow and incremental (no flip of the weights within a few years)?
|
||
3. Does a plural selection mix make the generative regime more stable against a
|
||
domination shock?
|
||
|
||
Usage: python3 -B simulations/gadm02/selection.py
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
sys.dont_write_bytecode = True
|
||
|
||
import band_probe as bp # noqa: E402
|
||
import gadm05 as g5 # noqa: E402
|
||
import grounding as gr # noqa: E402
|
||
from scenarios import load_all # noqa: E402
|
||
|
||
OUT = Path(__file__).resolve().parent / "results"
|
||
ANCHORS = list(gr.BAND)
|
||
SCEN = ("baseline", "attention_oligopoly", "generative_concentrated_lineages", "generative_regime")
|
||
SHOCK = {"Pd": 0.25, "Pm": 0.15} # domination shock added to the starting state
|
||
|
||
|
||
def max_rate(en: list[dict], fx: list[dict]) -> float:
|
||
"""Fastest change of the *endogenous part* of capture (endogenous minus fixed weights) per year.
|
||
Capture itself also moves with K_Omega (organism weight x K_Omega), even with fixed weights."""
|
||
diff = [(e["t"], e["capture"] - f["capture"]) for e, f in zip(en, fx)]
|
||
return max(abs(b - a) / (tb - ta) for (ta, a), (tb, b) in zip(diff, diff[1:]))
|
||
|
||
|
||
def main() -> None:
|
||
OUT.mkdir(exist_ok=True)
|
||
scs = load_all()
|
||
var = {a: gr.variant(gr.BAND[a]) for a in ANCHORS}
|
||
res = {}
|
||
for a in ANCHORS:
|
||
for sc in SCEN:
|
||
p = {**scs[sc]["params"], **var[a][0]}
|
||
i0 = {**scs[sc]["init"], **var[a][1]}
|
||
fx = g5.simulate_sel(p, i0, endogenous=False)
|
||
en = g5.simulate_sel(p, i0, endogenous=True)
|
||
shock = {k: min(1.0, i0.get(k, g5.g4.INIT[k]) + v) for k, v in SHOCK.items()}
|
||
fxs = g5.simulate_sel(p, {**i0, **shock}, endogenous=False)
|
||
ens = g5.simulate_sel(p, {**i0, **shock}, endogenous=True)
|
||
res[(a, sc)] = {"Vfx": fx[-1]["VFAC"], "Ven": en[-1]["VFAC"], "c0": en[0]["capture"],
|
||
"c100": en[-1]["capture"], "rate": max_rate(en, fx),
|
||
"cfx100": fx[-1]["capture"],
|
||
"loss_fx": fxs[-1]["VFAC"] - fx[-1]["VFAC"], "loss_en": ens[-1]["VFAC"] - en[-1]["VFAC"],
|
||
"sig_fx": fx[-1]["sigma"], "sig_en": en[-1]["sigma"]}
|
||
lines = ["# GADM-0.5-dev: endogenous selection weights across the band", "",
|
||
"Generated by `simulations/gadm02/selection.py` (ASSEMBLY-WP-0007-T03). Priors: weights relax at",
|
||
f"{g5.SEL_PARAMS['sel_lam']}/year; domination strength {g5.SEL_PARAMS['k_cap']}, civic strength "
|
||
f"{g5.SEL_PARAMS['k_civ']}. Capture = share of selection weight held by market + organism channels.",
|
||
f"Shock = {SHOCK} added to the starting state. t = 100.", "",
|
||
"| Scenario | Anchor | VFAC fixed → endogenous | σ fixed → endogenous | capture t0 → t100 (fixed weights at t100) | max endogenous capture change/yr | shock VFAC loss fixed / endogenous |",
|
||
"|---|---|---|---|---|---|---|"]
|
||
for sc in SCEN:
|
||
for a in ANCHORS:
|
||
r = res[(a, sc)]
|
||
lines.append(f"| {scs[sc]['title']} | {a} | {r['Vfx']:.3f} → {r['Ven']:.3f} | {r['sig_fx']:.3f} → "
|
||
f"{r['sig_en']:.3f} | {r['c0']:.2f} → {r['c100']:.2f} ({r['cfx100']:.2f}) | {r['rate']:.4f} | "
|
||
f"{r['loss_fx']:+.3f} / {r['loss_en']:+.3f} |")
|
||
readings = []
|
||
for sc in SCEN:
|
||
readings.append((f"endogenous capture at t100 (endogenous − fixed weights): {scs[sc]['title']}",
|
||
[res[(a, sc)]["c100"] - res[(a, sc)]["cfx100"] for a in ANCHORS]))
|
||
readings.append((f"ΔVFAC endogenous − fixed: {scs[sc]['title']}", [res[(a, sc)]["Ven"] - res[(a, sc)]["Vfx"] for a in ANCHORS]))
|
||
readings.append(("shock loss difference (endogenous − fixed), generative regime",
|
||
[res[(a, 'generative_regime')]["loss_en"] - res[(a, 'generative_regime')]["loss_fx"] for a in ANCHORS]))
|
||
lines += ["", "## Readings and robustness class", "", "| Reading | " + " | ".join(ANCHORS) + " | class |",
|
||
"|---|" + "---|" * (len(ANCHORS) + 1)]
|
||
for name, vals in readings:
|
||
lines.append(f"| {name} | " + " | ".join(f"{v:+.3f}" for v in vals) + f" | {bp.classify(vals, 'sign')} |")
|
||
rate = max(r["rate"] for r in res.values())
|
||
lines += ["", f"Largest change of the endogenous part of capture in any run: {rate:.4f} per year "
|
||
f"({'slow and incremental' if rate < 0.02 else 'FAST'}; grounding test: no flip within a few years).",
|
||
"Rows with ΔVFAC of ±0.000 are classified by sign noise; read them as 'no effect'.", ""]
|
||
(OUT / "selection.md").write_text("\n".join(lines))
|
||
print("\n".join(lines))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|