ASSEMBLY-WP-0005-T03: power channels as sub-stocks (GADM-0.3)

- simulations/gadm02/gadm03.py: six domination channels with drivers,
  counters and increasing returns; geometric aggregation; equilibrium
  calibration with stability guard (unit-tested)
- power_channels.py: P1 mostly unsupported over 100 years, except lineage
  hysteresis under the generative regime (lock-in above ~0.3) and market
  path dependence at baseline; P2 supported (critical lineage diffusion
  rises with learning amplification)
- models/GADM-0.3.md; metrics catalog requires channel-level reporting

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
This commit is contained in:
tegwick 2026-10-12 06:39:24 +02:00
parent bd62ace7de
commit 22c0335f07
12 changed files with 492 additions and 6 deletions

View file

@ -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`

View file

@ -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`.

View file

@ -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 |

View file

@ -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.

180
models/GADM-0.3.md Normal file
View file

@ -0,0 +1,180 @@
<!--
Created 2026-10-12 under ASSEMBLY-WP-0005. Sections are added as the
GADM-0.3 tasks are completed. Implemented by simulations/gadm02/gadm03.py;
the code is the reference if the two ever disagree.
-->
# 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
```

View file

@ -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

View file

@ -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

View file

@ -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

View file

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

View file

@ -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 |

View file

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

View file

@ -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"
```