assembly-economics/simulations/gadm02/selection.py
tegwick 80055bc700 ASSEMBLY-WP-0007-T03: endogenous selection weights (capture slow; robust capture/de-capture; no material VFAC effect); T04 opened as two-good demand split
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
2026-10-13 23:15:04 +02:00

89 lines
4.9 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.

"""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()