diff --git a/.custodian-brief.md b/.custodian-brief.md index 35efb91..6d1d134 100644 --- a/.custodian-brief.md +++ b/.custodian-brief.md @@ -2,16 +2,15 @@ # Custodian Brief — assembly-economics **Domain:** communication -**Last synced:** 2026-10-11 23:37 UTC +**Last synced:** 2026-10-12 04:34 UTC **State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* ## Active Workstreams ### GADM-0.3: global sensitivity, power channels, cohorts -Progress: 1/6 done | workplan_id: `23e33ee7-cb27-52bc-819c-704f44f91663` +Progress: 2/6 done | workplan_id: `23e33ee7-cb27-52bc-819c-704f44f91663` **Open tasks:** -- · Verify the literature comparison `5c77c522` - · Power channels as sub-stocks `274ce388` - · Separate wealth from power `99e702d7` - · Strengthen attention dynamics and the full organism grid `f56e20b4` diff --git a/AGENTS.md b/AGENTS.md index b66db01..96a3924 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,8 +20,11 @@ simulation. No install step. 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 power_channels.py # GADM-0.3 power channels ``` +Use `python3 -B` after quick edits; a stale bytecode cache once masked a change. + After changing `simulations/` or `scenarios/`, regenerate the results and update the affected readings in `models/GADM-0.2.md`. diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index ecc5022..6ae515b 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -41,7 +41,7 @@ | task | ASSEMBLY-WP-0004-T08 | done | — | workplans/ASSEMBLY-WP-0004-gadm-0.2-modeling.md | | task | ASSEMBLY-WP-0004-T09 | done | — | workplans/ASSEMBLY-WP-0004-gadm-0.2-modeling.md | | task | ASSEMBLY-WP-0005-T01 | done | — | workplans/ASSEMBLY-WP-0005-gadm-0.3.md | -| task | ASSEMBLY-WP-0005-T02 | todo | — | workplans/ASSEMBLY-WP-0005-gadm-0.3.md | +| task | ASSEMBLY-WP-0005-T02 | done | — | workplans/ASSEMBLY-WP-0005-gadm-0.3.md | | task | ASSEMBLY-WP-0005-T03 | todo | — | workplans/ASSEMBLY-WP-0005-gadm-0.3.md | | task | ASSEMBLY-WP-0005-T04 | todo | — | workplans/ASSEMBLY-WP-0005-gadm-0.3.md | | task | ASSEMBLY-WP-0005-T05 | todo | — | workplans/ASSEMBLY-WP-0005-gadm-0.3.md | diff --git a/metrics/MetricsCatalog.md b/metrics/MetricsCatalog.md index 7e7c5cc..5c8feca 100644 --- a/metrics/MetricsCatalog.md +++ b/metrics/MetricsCatalog.md @@ -130,6 +130,12 @@ levels. | B | rate of self-preferencing and exclusion events | | Su | perceived ability to contest decisions by dominant actors | +**Reporting requirement (GADM-0.3):** report domination per channel +(market, infrastructure, discovery/attention, membrane, lineage, +political), not only in aggregate. In GADM-0.3, a single dominant channel +(discovery at 0.80) coexisted with an aggregate P of only 0.32 +(`models/GADM-0.3.md` § 2.6). + **Goodhart risk:** measuring wealth instead of power. GADM models power separately from wealth: control of discovery, membranes and infrastructure can be extreme at moderate wealth concentration. diff --git a/models/GADM-0.3.md b/models/GADM-0.3.md new file mode 100644 index 0000000..632b62a --- /dev/null +++ b/models/GADM-0.3.md @@ -0,0 +1,180 @@ + + +# Generative Assembly Dynamics Model — GADM-0.3 + +**Status:** in progress, exploratory. Implemented so far: power channels +(§ 2, ASSEMBLY-WP-0005-T03). Planned: wealth separated from power (T04), +attention dynamics and the organism grid (T05), cohorts (T06). + +GADM-0.3 builds on GADM-0.2 (`GADM-0.2.md`). Everything not described here is +unchanged; the implementation reuses `gadm02.py`. Time unit: one year. + +--- + +## 1. Why channels + +GADM-0.2's sensitivity analysis showed that the membrane-control and +lineage-diffusion levers dominate outcomes. A single P stock cannot show +*which* channel drives domination, or whether channels behave differently. +`GADM-0.3-Candidates.md` § 2.2 proposed splitting P into channel sub-stocks. + +--- + +## 2. Power channels (T03) + +### 2.1 Channels + +| Code | Channel | Driver d_k | Counter C_k | +|---|---|---|---| +| `Pm` | market | p₁ V_sel (1 − B): private returns of success | B + N: broad claims and entry | +| `Pi` | infrastructure | p_i K (1 − std): capacity under closed standards | X · std + 0.1: interoperable commons | +| `Pd` | discovery / attention | p₂ disc (0.5 + 0.5 K_Ω) | (1 − disc) Q + 0.1: plural discovery | +| `Pmem` | membrane | p₃ K_Ω (1 − m) | m + 0.1: user/community control | +| `Plin` | lineage | p₄ K_Ω (1 − L) | L + 0.1: lineage diffusion | +| `Ppol` | political / institutional | p_pol · P_econ · (1 − B) | Q S: checks | + +`P_econ` aggregates the five economic channels. The political channel +converts economic control into political influence. + +### 2.2 Dynamics + +\[ +\dot P_k = (d_k + r_k P_k^2)(1-P_k) - c_k C_k P_k +\] + +The superlinear term \(r_k P_k^2\) represents increasing returns to control. +It allows thresholds: with drivers held fixed, a channel can have a low and a +high stable equilibrium separated by an unstable one. + +For the lineage channel, self-reinforcement grows with learning with use +(loop R7): \(r_{lin} = r(1 + lw \cdot K_\Omega Q)\). + +### 2.3 Aggregation + +\[ +P = 1 - \prod_k (1-P_k)^{1/6} +\] + +One fully dominant channel gives P = 1, and equal channels give P equal to +that common value. + +*Rejected alternative:* the plain product \(1-\prod_k(1-P_k)\) compounds +moderate channels (six channels at 0.5 give P = 0.98) and drove P to 1 in +almost every scenario. + +### 2.4 Calibration + +- Initial channels: market 0.50, infrastructure 0.45, discovery 0.50, + membrane 0.05, lineage 0.05, political 0.50 (total P ≈ 0.37; GADM-0.2: + 0.40). Membrane and lineage start low because organism capability starts at + 0.05. +- r_k = 0.3 for all channels (0.4 for discovery). This is a uniform + assumption, not an estimate. +- The counter coefficients c_k are computed by `calibrate()` so that every + channel starts in equilibrium in the baseline. + +**A calibration pitfall, found and guarded against.** The first calibration +put the market, membrane and lineage channels exactly on their *unstable* +root, so small drift decided which way each went. A channel's starting +equilibrium is stable when \(P_0 \ge 0.5\) or \(r < d/(P_0^2(1-2P_0))\). The +initial values above satisfy this, and a unit test now checks that every +calibrated channel starts at a stable equilibrium. + +### 2.5 Scenario results + +Final values at t = 100 years: + +| Scenario | VFAC (0.2 → 0.3) | P (0.2 → 0.3) | market | infra | discovery | membrane | lineage | political | +|---|---|---|---|---|---|---|---|---| +| Baseline | 0.461 → 0.345 | 0.52 → 0.79 | 0.80 | 0.74 | 0.69 | 0.79 | 0.84 | 0.83 | +| Automated capitalism | 0.317 → 0.248 | 0.73 → 0.89 | 0.90 | 0.78 | 0.81 | 0.91 | 0.93 | 0.92 | +| Dividend abundance | 0.445 → 0.338 | 0.54 → 0.79 | 0.76 | 0.76 | 0.70 | 0.80 | 0.86 | 0.82 | +| Universal venture access | 0.487 → 0.371 | 0.52 → 0.78 | 0.79 | 0.74 | 0.69 | 0.79 | 0.84 | 0.80 | +| Commons-rich | 0.544 → 0.466 | 0.48 → 0.70 | 0.73 | **0.05** | 0.61 | 0.79 | 0.86 | 0.75 | +| Attention oligopoly | 0.372 → 0.296 | 0.69 → 0.86 | 0.81 | 0.76 | **0.91** | 0.91 | 0.83 | 0.87 | +| Generative regime | 0.650 → **0.692** | 0.31 → **0.14** | 0.39 | 0.05 | 0.08 | 0.09 | 0.14 | 0.04 | +| — concentrated lineages | 0.530 → 0.452 | 0.50 → 0.70 | 0.68 | 0.08 | 0.08 | **0.86** | **0.95** | 0.60 | +| — attention extraction | 0.575 → 0.599 | 0.41 → 0.32 | 0.27 | 0.06 | **0.80** | 0.08 | 0.12 | 0.11 | + +### 2.6 Readings + +These are model behaviour under assumed structure, not claims about +economies. + +1. **Domination polarizes.** With channels and increasing returns, every + non-generative regime ends with P between 0.70 and 0.89 (GADM-0.2: + 0.48–0.73), while the generative regime falls to 0.14. The VFAC spread + widens accordingly. This sharpening comes directly from the assumed + self-reinforcement r. +2. **Each regime defeats only the channels it targets.** The commons-rich + regime removes infrastructure control (0.74 → 0.05) but leaves the other + five channels high. Dividends move none of them. Only the generative + regime, which acts on several levers at once, keeps all channels low. +3. **Organism channels spill into politics.** In the concentrated-lineages + variant, membrane and lineage control (0.86, 0.95) raise political control + to 0.60, even though market, infrastructure and discovery channels stay + low. +4. **Single-channel domination is visible, not hidden.** In the + attention-extraction variant, discovery control reaches 0.80 while total P + is only 0.32. The channel view shows a concentration that the aggregate + understates. Report channels, not just P. + +### 2.7 Hypotheses P1 and P2 + +Full tables: `simulations/gadm02/results/power_channels.md`. + +**P1: channels become self-sustaining above a threshold. Mostly not +supported over 100 years.** Sweeping each channel's initial value, the final +value is the same for most channels, whatever the start: drifting drivers +dominate. There are two exceptions: + +- **Lineage, generative regime: hysteresis.** If lineage control starts + above about 0.25–0.30, it locks in at 0.68, despite high lineage diffusion + (L = 0.8). Below that, it stays at about 0.15. +- **Market, baseline:** only a market that starts near zero stays low (0.22). + Any start above 0.15 ends at about 0.8. + +**P2: membrane/lineage control plus learning with use forms a +natural-monopoly regime unless L exceeds a critical value. Supported.** The +critical L (final lineage control below 0.5) rises with learning +amplification `lw`: + +| lw | Baseline | Generative regime | +|---|---|---| +| 0 | 0.50 | 0.60 | +| 2 (default) | 0.60 | 0.80 | +| 4 | 0.70 | 0.85 | + +### 2.8 Implications for the research program + +- **Timing matters (WS-12, transition dynamics).** The lineage hysteresis + means that lineage governance has to come *before* concentration. A + generative regime introduced after lineages concentrate does not undo it in + this model. +- **Stronger organism learning raises the governance bar.** The more + organisms learn from use, the more lineage diffusion is needed (P2). This + ties WS-13 (organism lens) to WS-07 (power). +- **Report channels, not just P** (reading 4). Added to the metrics catalog + as a requirement for the domination bundle. + +### 2.9 Limitations + +- r_k is uniform and assumed. The channel counters include ad-hoc floors + (+0.1) so that no counter vanishes entirely. +- The baseline market channel starts in its high basin (start 0.50, threshold + about 0.42). The initial split across channels is a modeling choice. +- Thresholds were computed with other stocks moving. Frozen-state roots + (`channel_roots()`) differ from long-run behaviour. +- Python pitfall seen during development: a stale bytecode cache can mask + quick same-size edits. Use `python3 -B` after rapid edits. + +## Reproduce + +```bash +python3 -B -m unittest discover -s simulations/gadm02 +cd simulations/gadm02 && python3 -B power_channels.py +``` diff --git a/models/README.md b/models/README.md index a3b3925..50b1c92 100644 --- a/models/README.md +++ b/models/README.md @@ -11,6 +11,7 @@ exploratory: they generate hypotheses and are not established results. | [InformationOrganismsInGADM.md](InformationOrganismsInGADM.md) | mapping note v0.1 | ASSEMBLY-WP-0003-T03; input for GADM-0.2 (ASSEMBLY-WP-0004) | | [GADM-CLD.md](GADM-CLD.md) | v0.1 | causal-loop diagram, loops, signed link table (ASSEMBLY-WP-0004-T01/T02) | | [GADM-0.2.md](GADM-0.2.md) | GADM-0.2 | extensions decision, equations, scenario and sensitivity findings (ASSEMBLY-WP-0004) | +| [GADM-0.3.md](GADM-0.3.md) | in progress | power channels and hypotheses P1/P2 (ASSEMBLY-WP-0005-T03) | | [GADM-0.3-Candidates.md](GADM-0.3-Candidates.md) | outline | life-cycle and power extensions, deferred items (ASSEMBLY-WP-0004-T08) | ## Conventions diff --git a/simulations/README.md b/simulations/README.md index ede06c8..fa1a138 100644 --- a/simulations/README.md +++ b/simulations/README.md @@ -6,8 +6,10 @@ model version it implements. | Directory | Model | Entry points | |-----------|-------|--------------| | `gadm02/` | GADM-0.2 (`models/GADM-0.2.md`) | `run_scenarios.py`, `sensitivity.py`, `gsa.py`, `test_gadm02.py` | +| `gadm02/gadm03.py` | GADM-0.3 power channels (`models/GADM-0.3.md`) | `power_channels.py`, `test_gadm03.py` | -Python 3.10+, standard library only. +Python 3.10+, standard library only. Use `python3 -B` after quick edits: a stale +bytecode cache can mask same-size changes made within the same second. ```bash python3 -m unittest discover -s simulations/gadm02 # tests diff --git a/simulations/gadm02/gadm03.py b/simulations/gadm02/gadm03.py new file mode 100644 index 0000000..17166d8 --- /dev/null +++ b/simulations/gadm02/gadm03.py @@ -0,0 +1,138 @@ +"""GADM-0.3 (power channels): domination P split into six channel sub-stocks. + +Implements models/GADM-0.3.md § 2 (ASSEMBLY-WP-0005-T03). Everything except P +is GADM-0.2 unchanged (gadm02.py is reused). P becomes an auxiliary: + + P = 1 - prod_k (1 - P_k) ** (1/6) (equal-weight geometric aggregation) + +The plain product 1 - prod(1 - P_k) was tried first and rejected: with six +channels it compounds moderate values (six channels at 0.5 give P = 0.98) and +drove P to 1 in almost every scenario. The weighted form keeps the property +that one fully dominant channel yields P = 1, without compounding. + +Each channel follows + dP_k = (d_k + r_k * P_k**2) * (1 - P_k) - c_k * C_k * P_k +with driver d_k, superlinear self-reinforcement r_k * P_k**2 (increasing +returns to control, which allows threshold behaviour, hypothesis P1) and a +counter C_k scaled by c_k. The c_k are calibrated so that every channel starts +in equilibrium in the baseline (see calibrate()). +""" + +from __future__ import annotations + +import math + +import gadm02 as g2 + +MODEL_VERSION = "GADM-0.3-power" +TIME_UNIT = g2.TIME_UNIT + +CHANNELS = ("Pm", "Pi", "Pd", "Pmem", "Plin", "Ppol") +CHANNEL_NAMES = { + "Pm": "market", "Pi": "infrastructure", "Pd": "discovery/attention", + "Pmem": "membrane", "Plin": "lineage", "Ppol": "political/institutional", +} +STOCKS = tuple(k for k in g2.STOCKS if k != "P") + CHANNELS + +INIT = {k: v for k, v in g2.INIT.items() if k != "P"} +INIT.update({"Pm": 0.50, "Pi": 0.45, "Pd": 0.50, "Pmem": 0.05, "Plin": 0.05, "Ppol": 0.50}) + +PARAMS = dict(g2.PARAMS) +PARAMS.update({ + # drivers + "pi": 0.06, # infrastructure control grows with capacity K where standards are closed + "ppol": 0.10, # economic control converts into political influence + # self-reinforcement (increasing returns to control) + "r_m": 0.30, "r_i": 0.30, "r_d": 0.40, "r_mem": 0.30, "r_lin": 0.30, "r_pol": 0.30, + "lw": 2.0, # learning-with-use amplification of lineage self-reinforcement (R7) + # counter coefficients: calibrated, see calibrate() + "c_m": 0.0668, "c_i": 0.3117, "c_d": 0.2815, "c_mem": 0.1354, "c_lin": 0.1372, "c_pol": 0.3411, +}) + + +def total_p(s: dict) -> float: + return 1 - math.prod((1 - s[k]) ** (1 / len(CHANNELS)) for k in CHANNELS) + + +def _channel_terms(s: dict, p: dict, a: dict) -> dict: + """Return {channel: (driver, r_eff, counter)}.""" + Ko, Q, S, X, N = s["Ko"], s["Q"], s["S"], s["X"], s["N"] + B, vsel, K = a["B"], a["Vsel"], a["K"] + econ_ch = ("Pm", "Pi", "Pd", "Pmem", "Plin") + econ = 1 - math.prod((1 - s[k]) ** (1 / len(econ_ch)) for k in econ_ch) + return { + "Pm": (p["p1"] * vsel * (1 - B), p["r_m"], B + N), + "Pi": (p["pi"] * K * (1 - p["std"]), p["r_i"], X * p["std"] + 0.1), + "Pd": (p["p2"] * p["disc"] * (0.5 + 0.5 * Ko), p["r_d"], (1 - p["disc"]) * Q + 0.1), + "Pmem": (p["p3"] * Ko * (1 - p["m"]), p["r_mem"], p["m"] + 0.1), + "Plin": (p["p4"] * Ko * (1 - p["L"]), p["r_lin"] * (1 + p["lw"] * Ko * Q), p["L"] + 0.1), + "Ppol": (p["ppol"] * econ * (1 - B), p["r_pol"], Q * S), + } + + +COEF = {"Pm": "c_m", "Pi": "c_i", "Pd": "c_d", "Pmem": "c_mem", "Plin": "c_lin", "Ppol": "c_pol"} + + +def derivatives(s: dict, p: dict) -> tuple[dict, dict]: + s2 = {k: v for k, v in s.items() if k not in CHANNELS} + s2["P"] = total_p(s) + d, a = g2.derivatives(s2, p) + d.pop("P") + for k, (drv, r, ctr) in _channel_terms(s, p, a).items(): + d[k] = (drv + r * s[k] ** 2) * (1 - s[k]) - p[COEF[k]] * ctr * s[k] + return d, a + + +def channel_roots(s: dict, p: dict, k: str, n: int = 10000) -> list[float]: + """Equilibria of channel k with every other stock frozen at state s.""" + s2 = {x: v for x, v in s.items() if x not in CHANNELS} + s2["P"] = total_p(s) + drv, r, ctr = _channel_terms(s, p, g2.auxiliaries(s2, p))[k] + c = p[COEF[k]] + f = lambda x: (drv + r * x * x) * (1 - x) - c * ctr * x # noqa: E731 + xs = [i / n for i in range(1, n)] + return sorted({round(b, 3) for a, b in zip(xs, xs[1:]) if f(a) * f(b) <= 0}) + + +def channel_slope(s: dict, p: dict, k: str, h: float = 1e-5) -> float: + """d(dP_k)/dP_k at state s; negative means the channel is locally stable.""" + up, dn = dict(s), dict(s) + up[k] += h + dn[k] -= h + return (derivatives(up, p)[0][k] - derivatives(dn, p)[0][k]) / (2 * h) + + +def calibrate(params: dict | None = None, init: dict | None = None) -> dict: + """Counter coefficients that put every channel in equilibrium at t = 0.""" + p = {**PARAMS, **(params or {})} + s = {**INIT, **(init or {})} + s2 = {k: v for k, v in s.items() if k not in CHANNELS} + s2["P"] = total_p(s) + a = g2.auxiliaries(s2, p) + out = {} + for k, (drv, r, ctr) in _channel_terms(s, p, a).items(): + out[COEF[k]] = round((drv + r * s[k] ** 2) * (1 - s[k]) / (ctr * s[k]), 4) + return out + + +def simulate(params: dict | None = None, init: dict | None = None, + T: float = 100.0, dt: float = 0.1, record_every: int = 10) -> list[dict]: + p = {**PARAMS, **(params or {})} + s = {**INIT, **(init or {})} + unknown = set(p) - set(PARAMS) + if unknown: + raise KeyError(f"unknown parameters: {sorted(unknown)}") + rows = [] + steps = int(round(T / dt)) + for i in range(steps + 1): + d, a = derivatives(s, p) + if i % record_every == 0: + row = {"t": round(i * dt, 6), **s, "P": total_p(s), + **{k: a[k] for k in ("Ystar", "B", "V", "Vsel", "sigma")}} + vs = {**row} + row["VFAC"] = g2.vfac(vs) + row["VFAC01"] = g2.vfac(vs, with_attention=False) + rows.append(row) + if i < steps: + s = {k: min(1.0, max(0.0, s[k] + dt * d[k])) for k in STOCKS} + return rows diff --git a/simulations/gadm02/power_channels.py b/simulations/gadm02/power_channels.py new file mode 100644 index 0000000..a93387b --- /dev/null +++ b/simulations/gadm02/power_channels.py @@ -0,0 +1,67 @@ +"""Hypothesis tests P1 and P2 for the GADM-0.3 power channels. + +P1: each channel becomes self-sustaining above a threshold. Test: sweep the + channel's initial value with everything else at the scenario initial state, + and look for a jump in its final value (bistability). +P2: membrane/lineage channels plus learning with use (R7) form a natural- + monopoly regime unless lineage diffusion L exceeds a critical value. Test: + sweep L for several learning amplifications lw. + +Usage: python3 simulations/gadm02/power_channels.py +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +sys.dont_write_bytecode = True + +import gadm03 as g # noqa: E402 +from scenarios import load_all # noqa: E402 + +OUT = Path(__file__).resolve().parent / "results" +GRID = [i / 20 for i in range(21)] + + +def p1(sc: dict) -> list[str]: + lines = ["| Channel | Final value for initial 0.0, 0.2, …, 1.0 | Largest jump (initial) | Path-dependent |", + "|---|---|---|---|"] + for k in g.CHANNELS: + finals = [g.simulate(sc["params"], {**sc["init"], k: x})[-1][k] for x in GRID] + jumps = [(GRID[i], GRID[i + 1], finals[i + 1] - finals[i]) for i in range(len(GRID) - 1)] + big = max(jumps, key=lambda j: j[2]) + bistable = max(finals) - min(finals) > 0.3 # end state depends on the start + thr = f"{big[0]:.2f}–{big[1]:.2f}" if bistable else "—" + spark = " ".join(f"{v:.2f}" for v in finals[::4]) + lines.append(f"| {g.CHANNEL_NAMES[k]} (`{k}`) | {spark} | {thr} | {'yes' if bistable else 'no'} |") + return lines + + +def p2(sc: dict) -> list[str]: + lines = ["| lw (learning amplification) | final `Plin` for L = 0.0, 0.2, …, 1.0 | critical L (Plin < 0.5) |", + "|---|---|---|"] + for lw in (0.0, 2.0, 4.0): + finals = [g.simulate({**sc["params"], "L": L, "lw": lw}, sc["init"])[-1]["Plin"] for L in GRID] + crit = next((L for L, v in zip(GRID, finals) if v < 0.5), None) + lines.append(f"| {lw:.0f} | " + " ".join(f"{v:.2f}" for v in finals[::4]) + + f" | {crit:.2f} |" if crit is not None else f"| {lw:.0f} | " + + " ".join(f"{v:.2f}" for v in finals[::4]) + " | none ≤ 1 |") + return lines + + +def main() -> None: + OUT.mkdir(exist_ok=True) + scs = load_all() + lines = [f"# {g.MODEL_VERSION}: power-channel hypotheses P1 and P2", "", + "Generated by `simulations/gadm02/power_channels.py`. Final values at t = 100 years.", ""] + for name in ("baseline", "generative_regime"): + sc = scs[name] + lines += [f"## {sc['title']}", "", "### P1: thresholds per channel", ""] + p1(sc) + [""] + lines += ["### P2: lineage diffusion vs. learning with use", ""] + p2(sc) + [""] + (OUT / "power_channels.md").write_text("\n".join(lines)) + print("\n".join(lines)) + + +if __name__ == "__main__": + main() diff --git a/simulations/gadm02/results/power_channels.md b/simulations/gadm02/results/power_channels.md new file mode 100644 index 0000000..1bcce64 --- /dev/null +++ b/simulations/gadm02/results/power_channels.md @@ -0,0 +1,45 @@ +# GADM-0.3-power: power-channel hypotheses P1 and P2 + +Generated by `simulations/gadm02/power_channels.py`. Final values at t = 100 years. + +## Baseline (current trends) + +### P1: thresholds per channel + +| Channel | Final value for initial 0.0, 0.2, …, 1.0 | Largest jump (initial) | Path-dependent | +|---|---|---|---| +| market (`Pm`) | 0.22 0.77 0.80 0.80 0.80 0.80 | 0.10–0.15 | yes | +| infrastructure (`Pi`) | 0.74 0.74 0.74 0.74 0.74 0.74 | — | no | +| discovery/attention (`Pd`) | 0.68 0.69 0.69 0.69 0.69 0.69 | — | no | +| membrane (`Pmem`) | 0.79 0.79 0.79 0.79 0.79 0.79 | — | no | +| lineage (`Plin`) | 0.84 0.84 0.84 0.84 0.84 0.84 | — | no | +| political/institutional (`Ppol`) | 0.83 0.83 0.83 0.83 0.83 0.83 | — | no | + +### P2: lineage diffusion vs. learning with use + +| lw (learning amplification) | final `Plin` for L = 0.0, 0.2, …, 1.0 | critical L (Plin < 0.5) | +|---|---|---| +| 0 | 0.96 0.85 0.70 0.15 0.04 0.00 | 0.50 | +| 2 | 0.96 0.89 0.79 0.45 0.05 0.00 | 0.60 | +| 4 | 0.97 0.91 0.83 0.74 0.05 0.00 | 0.70 | + +## Generative regime + +### P1: thresholds per channel + +| Channel | Final value for initial 0.0, 0.2, …, 1.0 | Largest jump (initial) | Path-dependent | +|---|---|---|---| +| market (`Pm`) | 0.34 0.35 0.38 0.40 0.40 0.40 | — | no | +| infrastructure (`Pi`) | 0.05 0.05 0.05 0.05 0.05 0.05 | — | no | +| discovery/attention (`Pd`) | 0.08 0.08 0.08 0.08 0.08 0.08 | — | no | +| membrane (`Pmem`) | 0.09 0.09 0.09 0.09 0.09 0.09 | — | no | +| lineage (`Plin`) | 0.14 0.16 0.68 0.68 0.68 0.68 | 0.25–0.30 | yes | +| political/institutional (`Ppol`) | 0.04 0.04 0.04 0.04 0.04 0.04 | — | no | + +### P2: lineage diffusion vs. learning with use + +| lw (learning amplification) | final `Plin` for L = 0.0, 0.2, …, 1.0 | critical L (Plin < 0.5) | +|---|---|---| +| 0 | 0.96 0.86 0.73 0.42 0.09 0.00 | 0.60 | +| 2 | 0.98 0.92 0.86 0.79 0.14 0.00 | 0.80 | +| 4 | 0.98 0.95 0.91 0.86 0.81 0.00 | 0.85 | diff --git a/simulations/gadm02/test_gadm03.py b/simulations/gadm02/test_gadm03.py new file mode 100644 index 0000000..6672249 --- /dev/null +++ b/simulations/gadm02/test_gadm03.py @@ -0,0 +1,45 @@ +"""Sanity tests for the GADM-0.3 power-channel model. + +Usage: python3 -B -m unittest discover -s simulations/gadm02 +""" + +import unittest + +import gadm03 as g +from scenarios import load_all + + +class TestGADM03(unittest.TestCase): + def test_stocks_stay_bounded_in_all_scenarios(self): + for name, sc in load_all().items(): + for row in g.simulate(sc["params"], sc["init"]): + for k in g.STOCKS + ("P",): + self.assertGreaterEqual(row[k], 0.0, (name, k)) + self.assertLessEqual(row[k], 1.0, (name, k)) + + def test_calibration_matches_params(self): + self.assertEqual(g.calibrate(), {c: g.PARAMS[c] for c in g.COEF.values()}) + + def test_initial_channels_are_stable_equilibria(self): + # Guards against calibrating a channel onto its unstable (threshold) root. + d, _ = g.derivatives(dict(g.INIT), g.PARAMS) + for k in g.CHANNELS: + self.assertAlmostEqual(d[k], 0.0, places=3, msg=k) + self.assertLess(g.channel_slope(dict(g.INIT), g.PARAMS, k), 0.0, k) + + def test_one_dominant_channel_gives_full_domination(self): + s = {k: 0.0 for k in g.CHANNELS} + s["Plin"] = 1.0 + self.assertAlmostEqual(g.total_p(s), 1.0) + + def test_aggregation_does_not_compound_equal_channels(self): + s = {k: 0.5 for k in g.CHANNELS} + self.assertAlmostEqual(g.total_p(s), 0.5) + + def test_unknown_parameter_rejected(self): + with self.assertRaises(KeyError): + g.simulate({"not_a_param": 1.0}) + + +if __name__ == "__main__": + unittest.main() diff --git a/workplans/ASSEMBLY-WP-0005-gadm-0.3.md b/workplans/ASSEMBLY-WP-0005-gadm-0.3.md index 52392ba..bdc374d 100644 --- a/workplans/ASSEMBLY-WP-0005-gadm-0.3.md +++ b/workplans/ASSEMBLY-WP-0005-gadm-0.3.md @@ -52,7 +52,7 @@ inheritable capital (§ 7, claim 2). ```task id: ASSEMBLY-WP-0005-T03 -status: todo +status: done priority: medium state_hub_task_id: "274ce388-33da-590e-aa34-fabb0c058f89" ```