diff --git a/AGENTS.md b/AGENTS.md index ce657ca..75888e6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,6 +41,7 @@ cd simulations/gadm02 && python3 -B world_scenarios.py # contexts X1–X4, hyp cd simulations/gadm02 && python3 -B exports.py # export channel and the "Argentina may thrive" hypothesis cd simulations/gadm02 && python3 -B nonrival.py # GADM-0.5-dev rival vs. non-rival output across the band cd simulations/gadm02 && python3 -B eco_classes.py # GADM-0.5-dev ecological resource classes +cd simulations/gadm02 && python3 -B selection.py # GADM-0.5-dev endogenous selection weights cd simulations/gadm02 && python3 -B matching.py && python3 -B matching.py --imm # model matching; IMM flip-detection acceptance test (~6 min) ``` diff --git a/models/GADM-0.5.md b/models/GADM-0.5.md index fcf7789..3218467 100644 --- a/models/GADM-0.5.md +++ b/models/GADM-0.5.md @@ -117,3 +117,51 @@ years. it within the horizon. The effect is conservative: sink regeneration is fitted so that each share starts at rest, so the lever acts only on the growth of the load. + +## 3. Endogenous selection weights (T03) + +`gadm05.simulate_sel`: the five selection weights (market, science, +community, public, organism) relax at 0.05 per year toward targets pushed by +domination and civic capacity: + +- **Domination:** market concentration Pm raises the market weight, and + discovery concentration Pd raises the organism weight (strength 2). +- **Civic capacity:** epistemic capacity Q raises science, and commons X + raise community (strength 1). + +**Capture** is the share of selection weight held by the market and organism +channels. Results are in `selection.py` and `results/selection.md`. + +| Reading (endogenous − fixed weights, t 100) | B1 | B2 | B3 | B4 | class | +|---|---|---|---|---|---| +| capture, attention oligopoly | +0.114 | +0.131 | +0.138 | +0.141 | robust | +| capture, baseline | +0.023 | +0.109 | +0.115 | +0.115 | quantitative shift | +| capture, generative regime | −0.240 | −0.211 | −0.189 | −0.161 | robust | +| ΔVFAC, any scenario | ≤ 0.002 in absolute value | | | | no effect | + +Largest change in the weights themselves: 0.0044 per year. + +Readings: + +1. **Domination captures selection, and the generative regime de-captures + it.** Both are robust across the band. Capture is slow and incremental, so + the grounding test passes: no flip within a few years. +2. **Capture of selection is not a material channel for VFAC in GADM.** The + five quality signals are of similar size, so moving weight between them + hardly changes selection quality σ. A plural selection mix also does not + make the generative regime more stable against a domination shock. +3. **What would make it material:** captured channels that deliver + systematically worse quality. An example is organism selection under + discovery concentration, `O = Q (1 − disc)`, which falls only through the + lever `disc`, not through the discovery channel Pd. Linking O to Pd is the + candidate change. It is recorded here and not implemented, because no + reading depends on it yet. + +## 4. Open: demand composition (T04, refined) + +T01 found no genuine decoupling, because non-rival output only adds to rival +consumption (BR-010). T02 locates the generative regime's ecological cost in +the sinks. Both point to the same missing structure: a **two-good demand +split**, rival and non-rival consumption, with substitution between them +(dematerialization). This is narrower than the four-sector option in the scope +note, and it is the question that opens T04. diff --git a/models/ParameterGrounding.md b/models/ParameterGrounding.md index 139df8a..d5ed82c 100644 --- a/models/ParameterGrounding.md +++ b/models/ParameterGrounding.md @@ -115,6 +115,11 @@ least on materials (−0.018). New priors to ground: the class starting levels, the load shares, and material replenishment (reserve growth). Which class binds next depends on the replenishment. +**T03 result (2026-10-13, `models/GADM-0.5.md` § 3):** test passed. The +largest change in the selection weights is 0.0044 per year, so capture is slow +and incremental. New priors to ground: the adjustment speed 0.05 per year and +the strengths k_cap 2 and k_civ 1. + ## 4. Remaining open grounding needs 1. Commons investment `IX`: needs a mapping concept, not just data. diff --git a/simulations/gadm02/gadm05.py b/simulations/gadm02/gadm05.py index 7d019b6..dda19f1 100644 --- a/simulations/gadm02/gadm05.py +++ b/simulations/gadm02/gadm05.py @@ -114,3 +114,58 @@ def simulate_eco(params: dict | None = None, init: dict | None = None, eco: dict else min(1.0, max(0.0, s[k] + dt * d[k]))) for k in STOCKS_ECO} s["E"] = eco_E(s) return rows + + +# --- ASSEMBLY-WP-0007-T03: endogenous selection weights ---------------------------------------- +# Selection quality sigma mixes five channels (market M, science Q, community N(1-P), public S, +# organism O) with weights wm, ws, wc, wp, wo. Here the weights are a slow state: each relaxes at +# rate sel_lam toward a target pushed by domination (market concentration Pm raises the market +# weight, discovery concentration Pd raises the organism weight) and by civic capacity (epistemic +# capacity Q raises science, commons X raise community). Scope: GADM-Sectors-Scope.md § 4. +SEL_PARAMS = {"sel_lam": 0.05, "k_cap": 2.0, "k_civ": 1.0} +WEIGHTS = ("wm", "ws", "wc", "wp", "wo") + + +def sel_targets(s: dict, w0: dict, s0: dict, q: dict) -> dict: + return {"wm": w0["wm"] * max(0.0, 1 + q["k_cap"] * (s["Pm"] - s0["Pm"])), + "wo": w0["wo"] * max(0.0, 1 + q["k_cap"] * (s["Pd"] - s0["Pd"])), + "ws": w0["ws"] * max(0.0, 1 + q["k_civ"] * (s["Q"] - s0["Q"])), + "wc": w0["wc"] * max(0.0, 1 + q["k_civ"] * (s["X"] - s0["X"])), + "wp": w0["wp"]} + + +def capture(w: dict, Ko: float) -> float: + """Share of selection weight held by market and organism channels.""" + wo = w["wo"] * Ko + return (w["wm"] + wo) / (w["wm"] + w["ws"] + w["wc"] + w["wp"] + wo) + + +def simulate_sel(params: dict | None = None, init: dict | None = None, sel: dict | None = None, + endogenous: bool = True, T: float = 100.0, dt: float = 0.1, record_every: int = 10) -> list[dict]: + """GADM-0.5 with endogenous selection weights (fixed weights if endogenous=False).""" + q = {**SEL_PARAMS, **(sel or {})} + p = {**PARAMS, **(params or {})} + s = {**g4.INIT, **(init or {})} + if p["sent"] and p["sig_pb0"] is None: + _d, a0 = g4.derivatives(s, p) + p = {**p, "sig_pb0": g4.perceived_benefit(s, a0, g4.legislated(s, p))} + w0 = {k: p[k] for k in WEIGHTS} + w = dict(w0) + s0 = dict(s) + rows = [] + steps = int(round(T / dt)) + for i in range(steps + 1): + d, a = g4.derivatives(s, {**p, **w}) + if i % record_every == 0: + row = {"t": round(i * dt, 6), **s, "P": g4.g3.total_p(s), "G": a["Gagg"], + **{k: a[k] for k in ("H", "A", "Ystar", "dem", "B", "V", "Vsel", "sigma")}, + **w, "capture": capture(w, s["Ko"])} + row["VFAC"] = g4.g2.vfac(row) + rows.append(row) + if i < steps: + if endogenous: + tg = sel_targets(s, w0, s0, q) + w = {k: max(0.0, w[k] + dt * q["sel_lam"] * (tg[k] - w[k])) for k in WEIGHTS} + 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 g4.STOCKS} + return rows diff --git a/simulations/gadm02/results/selection.md b/simulations/gadm02/results/selection.md new file mode 100644 index 0000000..398f0e1 --- /dev/null +++ b/simulations/gadm02/results/selection.md @@ -0,0 +1,41 @@ +# GADM-0.5-dev: endogenous selection weights across the band + +Generated by `simulations/gadm02/selection.py` (ASSEMBLY-WP-0007-T03). Priors: weights relax at +0.05/year; domination strength 2.0, civic strength 1.0. Capture = share of selection weight held by market + organism channels. +Shock = {'Pd': 0.25, 'Pm': 0.15} 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 | +|---|---|---|---|---|---|---| +| Baseline (current trends) | B1 | 0.411 → 0.411 | 0.418 → 0.426 | 0.42 → 0.45 (0.43) | 0.0021 | +0.003 / +0.003 | +| Baseline (current trends) | B2 | 0.353 → 0.353 | 0.366 → 0.389 | 0.42 → 0.55 (0.44) | 0.0020 | -0.000 / -0.000 | +| Baseline (current trends) | B3 | 0.337 → 0.337 | 0.326 → 0.341 | 0.42 → 0.60 (0.48) | 0.0023 | +0.000 / +0.000 | +| Baseline (current trends) | B4 | 0.311 → 0.311 | 0.284 → 0.290 | 0.42 → 0.64 (0.53) | 0.0027 | +0.000 / +0.000 | +| Attention oligopoly | B1 | 0.338 → 0.339 | 0.357 → 0.380 | 0.42 → 0.54 (0.42) | 0.0017 | +0.000 / +0.000 | +| Attention oligopoly | B2 | 0.302 → 0.302 | 0.311 → 0.326 | 0.42 → 0.57 (0.44) | 0.0020 | +0.000 / +0.000 | +| Attention oligopoly | B3 | 0.289 → 0.289 | 0.264 → 0.264 | 0.42 → 0.61 (0.47) | 0.0026 | +0.000 / -0.000 | +| Attention oligopoly | B4 | 0.265 → 0.266 | 0.212 → 0.201 | 0.42 → 0.66 (0.52) | 0.0035 | +0.000 / -0.000 | +| Generative regime, concentrated lineages | B1 | 0.528 → 0.529 | 0.530 → 0.465 | 0.42 → 0.17 (0.44) | 0.0041 | -0.001 / -0.000 | +| Generative regime, concentrated lineages | B2 | 0.474 → 0.475 | 0.479 → 0.474 | 0.42 → 0.38 (0.47) | 0.0036 | -0.004 / -0.003 | +| Generative regime, concentrated lineages | B3 | 0.426 → 0.426 | 0.439 → 0.455 | 0.42 → 0.48 (0.51) | 0.0031 | -0.000 / -0.000 | +| Generative regime, concentrated lineages | B4 | 0.389 → 0.390 | 0.414 → 0.424 | 0.42 → 0.51 (0.55) | 0.0027 | -0.000 / -0.000 | +| Generative regime | B1 | 0.685 → 0.685 | 0.660 → 0.690 | 0.42 → 0.23 (0.47) | 0.0044 | -0.000 / -0.000 | +| Generative regime | B2 | 0.661 → 0.659 | 0.612 → 0.652 | 0.42 → 0.29 (0.50) | 0.0043 | -0.000 / -0.001 | +| Generative regime | B3 | 0.641 → 0.639 | 0.579 → 0.618 | 0.42 → 0.34 (0.53) | 0.0040 | -0.000 / -0.001 | +| Generative regime | B4 | 0.577 → 0.577 | 0.507 → 0.521 | 0.42 → 0.39 (0.55) | 0.0038 | -0.000 / -0.000 | + +## Readings and robustness class + +| Reading | B1 | B2 | B3 | B4 | class | +|---|---|---|---|---|---| +| endogenous capture at t100 (endogenous − fixed weights): Baseline (current trends) | +0.023 | +0.109 | +0.115 | +0.115 | quantitative shift | +| ΔVFAC endogenous − fixed: Baseline (current trends) | +0.000 | +0.000 | -0.000 | -0.000 | QUALITATIVE BREAK | +| endogenous capture at t100 (endogenous − fixed weights): Attention oligopoly | +0.114 | +0.131 | +0.138 | +0.141 | robust | +| ΔVFAC endogenous − fixed: Attention oligopoly | +0.000 | +0.000 | +0.000 | +0.000 | quantitative shift | +| endogenous capture at t100 (endogenous − fixed weights): Generative regime, concentrated lineages | -0.265 | -0.092 | -0.028 | -0.034 | quantitative shift | +| ΔVFAC endogenous − fixed: Generative regime, concentrated lineages | +0.001 | +0.000 | -0.000 | +0.000 | QUALITATIVE BREAK | +| endogenous capture at t100 (endogenous − fixed weights): Generative regime | -0.240 | -0.211 | -0.189 | -0.161 | robust | +| ΔVFAC endogenous − fixed: Generative regime | -0.001 | -0.001 | -0.002 | -0.000 | quantitative shift | +| shock loss difference (endogenous − fixed), generative regime | -0.000 | -0.000 | -0.000 | -0.000 | quantitative shift | + +Largest change of the endogenous part of capture in any run: 0.0044 per year (slow and incremental; grounding test: no flip within a few years). +Rows with ΔVFAC of ±0.000 are classified by sign noise; read them as 'no effect'. diff --git a/simulations/gadm02/selection.py b/simulations/gadm02/selection.py new file mode 100644 index 0000000..5d82dd6 --- /dev/null +++ b/simulations/gadm02/selection.py @@ -0,0 +1,89 @@ +"""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() diff --git a/workplans/ASSEMBLY-WP-0007-gadm-sectors-goods.md b/workplans/ASSEMBLY-WP-0007-gadm-sectors-goods.md index 1498212..f752696 100644 --- a/workplans/ASSEMBLY-WP-0007-gadm-sectors-goods.md +++ b/workplans/ASSEMBLY-WP-0007-gadm-sectors-goods.md @@ -67,11 +67,16 @@ floor. Find which class binds first per scenario (scoping note § 2). ```task id: ASSEMBLY-WP-0007-T03 -status: todo +status: done priority: medium state_hub_task_id: "e58601b5-ca84-59c5-8022-8dcfd084728a" ``` +**Done 2026-10-13:** `gadm05.simulate_sel`, `selection.py`, `models/GADM-0.5.md` +§ 3. Domination captures selection (attention oligopoly +0.11 to +0.14), and +the generative regime de-captures it (−0.16 to −0.24); both robust. Capture is +slow (grounding test passed) and has no material effect on VFAC. + Make the selection-channel weights a slow state pushed by domination channels and governance levers. Test capture of selection (scoping note § 4). @@ -79,10 +84,25 @@ and governance levers. Test capture of selection (scoping note § 4). ```task id: ASSEMBLY-WP-0007-T04 -status: wait +status: todo priority: low state_hub_task_id: "c3dbbe24-4378-55cf-91c1-9e2788646728" ``` Only if T01–T03 raise a question that needs sector composition (scoping note § 3). + +**Opened 2026-10-13, refined:** T01 and T02 raise one such question. Genuine +decoupling needs non-rival consumption to *replace* rival consumption (BR-010), +and the generative regime's ecological cost sits in the sinks. Implement a +**two-good demand split**: rival and non-rival consumption with a substitution +elasticity and a preference shift driven by non-rival quality (X, Q). Do not +implement the full four-sector model. Tests: + +1. Can VFAC rise while physical throughput falls, for reasons other than + organism decline? +2. Does the generative regime's sink breach move out of the 100-year + horizon? + +Ground the elasticity in dematerialization evidence, for example the material +footprint per unit of GDP over time (Eurostat env_ac_rme, UNEP IRP).