ASSEMBLY-WP-0017 T02/T03 done, T04 in progress: IMM multi-indicator regime filter (0 false flips, accelerations detected in 1-1.5 y, slowdowns only after 2.5-3.75 y); observed investment and discovery series

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
This commit is contained in:
tegwick 2026-10-13 21:35:28 +02:00
parent 65584be429
commit 0f6ef9fb3b
6 changed files with 481 additions and 20 deletions

View file

@ -38,7 +38,7 @@ cd simulations/gadm02 && python3 -B calibrate_data.py && python3 -B calibrated_r
cd simulations/gadm02 && for j in US EU; do python3 -B gsa.py --model gadm04 --jobs 8 --start $j; done # GSA around calibrated starts, ~30 min
cd simulations/gadm02 && python3 -B cross_border.py # coupled world US/EU/AR from the calibrated start (models/CrossBorder.md)
cd simulations/gadm02 && python3 -B world_scenarios.py # contexts X1–X4, hypothesis re-test, local vs. global
cd simulations/gadm02 && python3 -B matching.py && python3 -B matching.py --synthetic # model matching; flip-detection test (~10 min)
cd simulations/gadm02 && python3 -B matching.py && python3 -B matching.py --imm # model matching; IMM flip-detection acceptance test (~6 min)
```
Reality checkpoints (ASSEMBLY-WP-0010): scheduled by activity-core and

View file

@ -1,8 +1,9 @@
# Model matching and regime flipping
**Status:** v0.1 (2026-10-13), ASSEMBLY-WP-0012. Matching on observed data
works. **Flip detection is not yet reliable** (§ 4). No flip may be declared
from real data until the synthetic test passes.
**Status:** v0.2 (2026-10-13), ASSEMBLY-WP-0012 and WP-0017. Matching on
observed data works. The IMM filter (§ 4.1) detects accelerations reliably,
but detects slowdowns only after 2.5–3.75 years. The acceptance test is not
yet passed, so no measured flip may be declared yet.
Code: `simulations/gadm02/matching.py`. Data: `observed_series.json`.
Results: `results/matching.md` (observed), `results/matching_synthetic.md`
@ -108,3 +109,47 @@ equation reverts toward neutral from the low measured level.
- acceptance criteria: at least 5 of 6 regimes correct at the start, no
false flips in 6 no-switch runs, and at least 4 of 6 switches detected
within 1.5 years.
### 4.1 IMM filter with several indicators (WP-0017-T02/T03)
`matching.py --imm` (`results/matching_imm.md`):
- **Filter:** an interacting-multiple-model filter. Each regime keeps its own
state, and before each quarter the regime states are mixed by the
switching probabilities (0.10 per quarter). Each regime then runs forward,
and the weights update with the likelihood of all indicators in the
quarter.
- **Indicators:** adoption (biweekly, log sd 0.03), gross organism
investment (quarterly, log sd 0.05), discovery channel Pd (monthly, sd
0.01) and sentiment (twice a year, sd 0.02).
- **Sentiment:** anchored (BR-009 fix).
| Case | Result |
|---|---|
| anchor correct at the first evaluation (no-switch truth) | 6 of 6 |
| false flips (no-switch truth) | 0 of 6 |
| acceleration B2 → B4 | detected 3 of 3, delay 1.0–1.5 years |
| slowdown B3 → B2 | right direction 3 of 3, but delay 2.5–3.75 years (outside 1.5) |
| loop switch off → on | 0 of 3 |
Acceptance (anchor switches ≥ 4 of 6 within 1.5 years): **not passed** (3 of
6).
Readings:
1. **Accelerations are visible fast. Slowdowns are not.** After a slowdown,
the paths diverge slowly from a shared state. For the real data: telling
B2 from B3 in the US (P-2026-001) will take roughly 2.5–4 years of
observations, not one.
2. **The loop state is not identifiable while sentiment is flat.** With
sentiment anchored, switching the loop on changes nothing visible until
perceived benefit moves. The loop is a conditional regime: it can be
matched only after a shock to perceived benefit.
3. **More indicators remove false flips; they do not speed up slowdown
detection.** With adoption alone the IMM filter had 3 of 6 false flips. With
all four indicators it had none.
Observed series for the real filter: US adoption (BTOS), US investment (L5,
quarterly; operator rough), US discovery channel (StatCounter, monthly), EU
and DE adoption (annual). Sentiment points after checkpoint 0 do not exist
yet.

View file

@ -18,7 +18,8 @@ Flip: the best candidate changes and the new best has weight >= FLIP_WEIGHT
in two consecutive evaluations (research/ModelMatching.md § 3).
Usage: python3 -B simulations/gadm02/matching.py # match observed series
python3 -B simulations/gadm02/matching.py --synthetic # recovery and flip test
python3 -B simulations/gadm02/matching.py --synthetic # recovery and flip test (single-state filter)
python3 -B simulations/gadm02/matching.py --imm # IMM filter, several indicators (WP-0017)
"""
from __future__ import annotations
@ -299,5 +300,156 @@ def main_synthetic() -> None:
print("\n".join(lines))
# --- WP-0017-T02/T03: interacting-multiple-model filter with several indicators -----------------
def observe(rows: list[dict], t: float, kind: str, dO: float = None) -> float:
"""Model value of an indicator at time t (rows recorded every STEP)."""
if kind == "adopt":
return interp(rows, t, "Ko")
if kind == "invest": # gross organism investment: dK_Omega/dt + dO K_Omega
dO = g.PARAMS["dO"] if dO is None else dO
a, b = interp(rows, max(0.0, t - STEP), "Ko"), interp(rows, t, "Ko")
return max(1e-9, (b - a) / STEP + dO * b)
if kind == "pd":
return interp(rows, t, "Pd")
if kind == "sig":
return interp(rows, t, "Sig")
raise KeyError(kind)
NOISE = {"adopt": ("log", 0.03), "invest": ("log", 0.05), "pd": ("abs", 0.01), "sig": ("abs", 0.02)}
CADENCE = {"adopt": 2 / 52, "invest": 0.25, "pd": 1 / 12, "sig": 0.5}
def obs_ll(rows: list[dict], obs: list, scales: dict) -> float:
ll = 0.0
for tau, kind, y in obs:
mode, sd = NOISE[kind]
m_ = observe(rows, tau, kind)
if mode == "log":
ll += -0.5 * ((math.log(y) - math.log(max(1e-9, m_)) - scales[kind]) / sd) ** 2
else:
ll += -0.5 * ((y - m_) / sd) ** 2
return ll
IMM_SWITCH = 0.10 # switching prior per quarter for the IMM filter (tested in main_imm)
def imm_run(truth: list[dict], start: float, years: float, seed: int, kinds: tuple,
switch: float = IMM_SWITCH, step: float = 0.25) -> list[tuple[float, dict]]:
"""IMM regime filter (research/ModelMatching.md § 4.1).
Initial states and log-scales from a whole-path alignment of the first year of adoption data;
then every quarter: mix regime states by switching probabilities, run each regime from its
mixed state, update weights by the likelihood of all indicators in the quarter."""
rnd = random.Random(seed)
obs = []
for kind in kinds:
t = start
while t <= start + years + 1e-9:
mode, sd = NOISE[kind]
v = observe(truth, t, kind)
y = v * 25.0 * math.exp(rnd.gauss(0, sd)) if kind == "adopt" else \
v * 3.0 * math.exp(rnd.gauss(0, sd)) if kind == "invest" else v + rnd.gauss(0, sd)
obs.append((t, kind, y))
t += CADENCE[kind]
te0 = start + 1.0
first_pts = [(t - start, y) for t, k, y in obs if k == "adopt" and t <= te0]
ko = interp(truth, te0, "Ko")
first = score("US", path, {"points": first_pts, "log_sigma": NOISE["adopt"][1]},
level={"lo": ko - 0.075, "hi": ko + 0.075})
b = best(first)
t_b = first[b]["t0"]
scales = {"adopt": first[b]["ls"]}
inv = [(t - start, y) for t, k, y in obs if k == "invest" and t <= te0]
if inv:
rb = path("US", *b)
scales["invest"] = sum(math.log(y) - math.log(observe(rb, t_b + tau, "invest")) for tau, y in inv) / len(inv)
states = {}
for c in CANDIDATES:
rc = path("US", *c)
i = min(len(rc) - 1, int(round((first[c]["t0"] + 1.0) / STEP)))
states[c] = {k: rc[i][k] for k in g.STOCKS}
w = {c: first[c]["w"] for c in CANDIDATES}
evals = [(te0, {c: {"w": w[c]} for c in CANDIDATES})]
n = len(CANDIDATES)
pi = {(i, c): (1 - switch) if i == c else switch / (n - 1) for i in CANDIDATES for c in CANDIDATES}
te = te0 + step
while te <= start + years + 1e-9:
window = [(t - (te - step), k, y) for t, k, y in obs if te - step < t <= te + 1e-9]
cbar = {c: sum(pi[(i, c)] * w[i] for i in CANDIDATES) for c in CANDIDATES}
lls, ends = {}, {}
for c in CANDIDATES:
mu = {i: pi[(i, c)] * w[i] / cbar[c] for i in CANDIDATES}
x0 = {k: sum(mu[i] * states[i][k] for i in CANDIDATES) for k in g.STOCKS}
rows = g.simulate(regime_params("US", c), x0, T=step, record_every=1)
lls[c] = obs_ll(rows, window, scales)
ends[c] = {k: rows[-1][k] for k in g.STOCKS}
mx = max(lls.values())
post = {c: cbar[c] * math.exp(lls[c] - mx) for c in CANDIDATES}
zs = sum(post.values())
w = {c: post[c] / zs for c in CANDIDATES}
states = ends
evals.append((te, {c: {"w": w[c]} for c in CANDIDATES}))
te += step
return evals
def main_imm() -> None:
kinds = ("adopt", "invest", "pd", "sig")
lines = [f"# {g.MODEL_VERSION}: IMM regime filter on synthetic data", "",
"Generated by `simulations/gadm02/matching.py --imm` (ASSEMBLY-WP-0017-T02/T03/T04).",
"US calibrated state, sentiment anchored (BR-009 fix). Truth from a known candidate, optionally",
"switching at t 4; observed from t 2 to t 8. Indicators and noise: adoption biweekly (log sd 0.03),",
"investment quarterly (log sd 0.05), discovery channel Pd monthly (sd 0.01), sentiment twice a",
f"year (sd 0.02). IMM filter, switch probability {IMM_SWITCH} per quarter. Flip: new best with weight",
f"≥ {FLIP_WEIGHT} twice in a row.", "",
"**Acceptance (anchor regimes):** anchor correct at the first evaluation in ≥ 5 of 6 no-switch runs,",
"no false flips in 6 no-switch runs, and ≥ 4 of 6 anchor switches detected within 1.5 years. The",
"loop switch is reported separately: with sentiment anchored, switching the loop on is invisible",
"until perceived benefit moves.", "",
"| Truth | Seed | Best at start | Flips (t, from → to) | Verdict |", "|---|---|---|---|---|"]
correct = false = det = 0
loop_det = 0
cases = [(("B3", "off"), None, None, "none"), (("B2", "on"), None, None, "none"),
(("B2", "off"), ("B4", "off"), 4.0, "anchor"), (("B3", "off"), ("B2", "off"), 4.0, "anchor"),
(("B3", "off"), ("B3", "on"), 4.0, "loop")]
for first_c, second, ts, kind in cases:
truth = synthetic_rows("US", first_c, second, ts or T_PATH)
for seed in (1, 2, 3):
ev = imm_run(truth, 2.0, 6.0, seed, kinds)
fl = flips(ev)
b0 = best(ev[0][1])
if kind == "none":
correct += b0[0] == first_c[0]
false += bool(fl)
verdict = ("anchor correct" if b0[0] == first_c[0] else "anchor wrong") + ("; FALSE FLIP" if fl else "")
else:
if kind == "anchor":
hit = [e for e in fl if ts <= e[0] <= ts + 1.5 and e[2][0] == second[0]]
det += bool(hit)
else:
hit = [e for e in fl if ts <= e[0] <= ts + 1.5 and e[2][1] == second[1]]
loop_det += bool(hit)
verdict = f"detected, delay {hit[0][0] - ts:.2f} y" if hit else "missed"
label = fmt(first_c) + (f" → {fmt(second)} at t {ts}" if second else "")
lines.append(f"| {label} | {seed} | {fmt(b0)} | "
+ ("; ".join(f"{t:.2f}: {fmt(a)} → {fmt(b)}" for t, a, b in fl) or "none")
+ f" | {verdict} |")
ok = correct >= 5 and false == 0 and det >= 4
lines += ["", f"Anchor correct at start {correct}/6, false flips {false}/6, anchor switches detected {det}/6, "
f"loop switches detected {loop_det}/3. Acceptance: {'**passed**' if ok else '**not passed**'}.", ""]
(OUT / "matching_imm.md").write_text("\n".join(lines))
print("\n".join(lines))
if __name__ == "__main__":
main_synthetic() if "--synthetic" in sys.argv else main_observed()
if "--imm" in sys.argv:
main_imm()
elif "--synthetic" in sys.argv:
main_synthetic()
else:
main_observed()

View file

@ -2,31 +2,256 @@
"note": "Observed indicator series for model matching (ASSEMBLY-WP-0012). Values as fetched for checkpoint 0 and the 2026-10-13 review; t in years relative to the first point. Append new points at each checkpoint; do not edit old ones.",
"series": {
"US_adoption": {
"indicator": "L1", "unit": "% of employer firms using AI (last two weeks)",
"indicator": "L1",
"unit": "% of employer firms using AI (last two weeks)",
"source": "U.S. Census Bureau BTOS Q7 (National.xlsx), periods 2025-24 to 2026-20 (biweekly)",
"log_sigma": 0.03,
"points": [[0.0, 17.3], [0.0385, 17.2], [0.0769, 17.8], [0.1154, 17.7], [0.1538, 18.2], [0.1923, 18.9],
[0.2308, 17.5], [0.2692, 18.2], [0.3077, 18.9], [0.3462, 19.1], [0.3846, 19.8], [0.4231, 19.8],
[0.4615, 19.5], [0.5, 20.6], [0.5385, 20.6], [0.5769, 20.6], [0.6154, 21.7], [0.6538, 21.5],
[0.6923, 21.8], [0.7308, 22.4], [0.7692, 23.2], [0.8077, 23.8], [0.8462, 23.8]]
"points": [
[
0.0,
17.3
],
[
0.0385,
17.2
],
[
0.0769,
17.8
],
[
0.1154,
17.7
],
[
0.1538,
18.2
],
[
0.1923,
18.9
],
[
0.2308,
17.5
],
[
0.2692,
18.2
],
[
0.3077,
18.9
],
[
0.3462,
19.1
],
[
0.3846,
19.8
],
[
0.4231,
19.8
],
[
0.4615,
19.5
],
[
0.5,
20.6
],
[
0.5385,
20.6
],
[
0.5769,
20.6
],
[
0.6154,
21.7
],
[
0.6538,
21.5
],
[
0.6923,
21.8
],
[
0.7308,
22.4
],
[
0.7692,
23.2
],
[
0.8077,
23.8
],
[
0.8462,
23.8
]
]
},
"EU_adoption": {
"indicator": "L2", "unit": "% of enterprises (10+ employees) using at least one AI technology",
"indicator": "L2",
"unit": "% of enterprises (10+ employees) using at least one AI technology",
"source": "Eurostat isoc_eb_ai, EU27_2020, reference years 2023-2025 (2021: 7.65, before generative AI, excluded)",
"log_sigma": 0.05,
"points": [[0.0, 8.06], [1.0, 13.48], [2.0, 19.95]]
"points": [
[
0.0,
8.06
],
[
1.0,
13.48
],
[
2.0,
19.95
]
]
},
"DE_adoption": {
"indicator": "L2", "unit": "% of enterprises (10+ employees) using at least one AI technology",
"indicator": "L2",
"unit": "% of enterprises (10+ employees) using at least one AI technology",
"source": "Eurostat isoc_eb_ai, DE, reference years 2023-2025 (2021: 10.56, excluded)",
"log_sigma": 0.05,
"points": [[0.0, 11.55], [1.0, 19.75], [2.0, 25.97]]
"points": [
[
0.0,
11.55
],
[
1.0,
19.75
],
[
2.0,
25.97
]
]
},
"US_sentiment": {
"indicator": "L6", "unit": "Sigma = 0.5 + (excited - concerned)/200",
"indicator": "L6",
"unit": "Sigma = 0.5 + (excited - concerned)/200",
"source": "Pew Research: 2021 (37 % concerned, 18 % excited) and June 2026 (52 %, 9 %)",
"sigma": 0.03,
"points": [[0.0, 0.405], [5.0, 0.285]]
"points": [
[
0.0,
0.405
],
[
5.0,
0.285
]
]
},
"US_investment": {
"indicator": "L5",
"unit": "billions of USD, SAAR",
"source": "BEA private fixed investment in information processing equipment (FRED Y033RC1Q027SBEA), 2024 Q3 to 2026 Q2. Operator: proportional to gross organism investment; rough, because the series covers all information-processing equipment, not AI only",
"log_sigma": 0.05,
"points": [
[
0.0,
1543.157
],
[
0.25,
1534.174
],
[
0.5,
1610.088
],
[
0.75,
1651.61
],
[
1.0,
1709.98
],
[
1.25,
1754.081
],
[
1.5,
1836.848
],
[
1.75,
1922.516
]
]
},
"US_discovery": {
"indicator": "CB1 / P2",
"unit": "leading search engine share (Google), fraction",
"source": "StatCounter Global Stats, US, monthly 2025-10 to 2026-09. Operator: Pd = leading search share (as in calibration)",
"sigma": 0.01,
"points": [
[
0.0,
0.851
],
[
0.0833,
0.8507
],
[
0.1667,
0.8448
],
[
0.25,
0.8391
],
[
0.3333,
0.8417
],
[
0.4167,
0.85
],
[
0.5,
0.8516
],
[
0.5833,
0.8582
],
[
0.6667,
0.8668
],
[
0.75,
0.8661
],
[
0.8333,
0.8601
],
[
0.9167,
0.8519
]
]
}
}
}

View file

@ -0,0 +1,33 @@
# GADM-0.4: IMM regime filter on synthetic data
Generated by `simulations/gadm02/matching.py --imm` (ASSEMBLY-WP-0017-T02/T03/T04).
US calibrated state, sentiment anchored (BR-009 fix). Truth from a known candidate, optionally
switching at t 4; observed from t 2 to t 8. Indicators and noise: adoption biweekly (log sd 0.03),
investment quarterly (log sd 0.05), discovery channel Pd monthly (sd 0.01), sentiment twice a
year (sd 0.02). IMM filter, switch probability 0.1 per quarter. Flip: new best with weight
≥ 0.75 twice in a row.
**Acceptance (anchor regimes):** anchor correct at the first evaluation in ≥ 5 of 6 no-switch runs,
no false flips in 6 no-switch runs, and ≥ 4 of 6 anchor switches detected within 1.5 years. The
loop switch is reported separately: with sentiment anchored, switching the loop on is invisible
until perceived benefit moves.
| Truth | Seed | Best at start | Flips (t, from → to) | Verdict |
|---|---|---|---|---|
| B3/loop off | 1 | B3/loop off | none | anchor correct |
| B3/loop off | 2 | B3/loop off | none | anchor correct |
| B3/loop off | 3 | B3/loop off | none | anchor correct |
| B2/loop on | 1 | B2/loop off | none | anchor correct |
| B2/loop on | 2 | B2/loop off | none | anchor correct |
| B2/loop on | 3 | B2/loop off | none | anchor correct |
| B2/loop off → B4/loop off at t 4.0 | 1 | B2/loop off | 5.00: B2/loop on → B4/loop off | detected, delay 1.00 y |
| B2/loop off → B4/loop off at t 4.0 | 2 | B2/loop off | 5.50: B2/loop off → B4/loop off | detected, delay 1.50 y |
| B2/loop off → B4/loop off at t 4.0 | 3 | B2/loop off | 5.25: B2/loop off → B4/loop off | detected, delay 1.25 y |
| B3/loop off → B2/loop off at t 4.0 | 1 | B3/loop off | 7.75: B3/loop off → B2/loop on | missed |
| B3/loop off → B2/loop off at t 4.0 | 2 | B3/loop off | 7.00: B3/loop off → B2/loop off | missed |
| B3/loop off → B2/loop off at t 4.0 | 3 | B3/loop off | 6.50: B3/loop off → B2/loop off | missed |
| B3/loop off → B3/loop on at t 4.0 | 1 | B3/loop off | none | missed |
| B3/loop off → B3/loop on at t 4.0 | 2 | B3/loop off | none | missed |
| B3/loop off → B3/loop on at t 4.0 | 3 | B3/loop off | none | missed |
Anchor correct at start 6/6, false flips 0/6, anchor switches detected 3/6, loop switches detected 0/3. Acceptance: **not passed**.

View file

@ -48,11 +48,13 @@ record which readings change.
```task
id: ASSEMBLY-WP-0017-T02
status: todo
status: done
priority: high
state_hub_task_id: "57035a4b-7165-52f6-b77b-b7e6476a0e79"
```
**Done 2026-10-13:** indicators adoption, investment, Pd and sentiment in the filter (`observe`, `obs_ll`); US investment (L5) and US discovery (StatCounter) series added to `observed_series.json`.
Add to `filter_step` and to `observed_series.json`: investment (L5,
quarterly; separates self-enhancement and B4), concentration channels (CB1,
monthly; domination dynamics), and sentiment at least twice a year where
@ -63,11 +65,13 @@ level.
```task
id: ASSEMBLY-WP-0017-T03
status: todo
status: done
priority: medium
state_hub_task_id: "a8b12642-c842-581d-840d-5e1a1c731d0c"
```
**Done 2026-10-13:** IMM filter (`imm_run`): one state per regime, mixed by switching probabilities. False flips 3/6 → 0/6 (with all indicators).
Today the filter carries one shared state estimate, advanced by the best
regime. Track one state per regime (interacting multiple-model filter), so
that a regime that becomes true later is not judged from another regime's
@ -77,11 +81,13 @@ state.
```task
id: ASSEMBLY-WP-0017-T04
status: todo
status: progress
priority: high
state_hub_task_id: "5e4d8270-8499-5635-98d4-55f3186258d2"
```
**2026-10-13, not passed:** anchors correct 6/6, false flips 0/6, B2 → B4 detected 3/3 within 1.0–1.5 y, B3 → B2 detected 3/3 but only after 2.5–3.75 y (acceptance needs ≥ 4/6 within 1.5 y). Next: a slowdown-sensitive indicator (for example the growth rate of investment, or adoption *intentions*, BTOS Q24) or a longer acceptance window for slowdowns. The criterion must not be loosened without a reason recorded here.
Acceptance: at least 5 of 6 regimes correct at the first evaluation, no false
flips in 6 no-switch runs, and at least 4 of 6 switches detected within 1.5
years (`matching.py --synthetic`). Add the criteria as a slow test.