ASSEMBLY-WP-0012 finished: model matching (matching.py, observed series), flip definition and regime filter, synthetic test (negative: flips not yet reliable), FlipRegister, BR-009; residuals to WP-0017
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:
parent
eeb5c28284
commit
597eb4176e
11 changed files with 703 additions and 7 deletions
|
|
@ -38,6 +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)
|
||||
```
|
||||
|
||||
Reality checkpoints (ASSEMBLY-WP-0010): scheduled by activity-core and
|
||||
|
|
|
|||
|
|
@ -21,3 +21,4 @@ that part of the band.
|
|||
| BR-006 | 2026-10-13 (WP-0013-T04) | Legitimacy loop damps adoption (BR-005, AdoptionBand § 3.3) | damping under extractive regimes → amplifying under rights-first levers from the calibrated start (US, AR) | when adoption lowers domination, perceived benefit rises and sentiment accelerates adoption | none structural; the reading is regime-dependent | open: state the loop's effect per regime |
|
||||
| BR-007 | 2026-10-13 (WP-0013-T04) | Rights-first levers bound domination | holds when switching to J1 levers (US: P 0.64 → 0.54) → fails for a jurisdiction already at J1 levers (EU: P 0.65 → 0.77) | calibrated domination comes from levels set abroad that local levers cannot reach | **structural:** cross-border coupling (ASSEMBLY-WP-0011) | open |
|
||||
| BR-008 | 2026-10-13 (WP-0011-T05) | Which levers bound domination (BR-007 follow-up) | closed model: host rights levers → coupled model: only host access rules on foreign operators and the home's levers reach the foreign-run channels | measured channels (Pd, Pmem, Plin, Pi) are operated from the US for EU and AR hosts | **structural, implemented:** cross-border coupling (`world.py`); observed DMA effect on search ≈ 0 (at most a 0.006/yr) | open: lineage access rules (BR-003) |
|
||||
| BR-009 | 2026-10-13 (WP-0012) | Sentiment dynamics of the legitimacy loop | Σ relaxes toward 0.5 + gain × Δ(perceived benefit), so from the measured US Σ 0.285 it rises → measured US sentiment fell 2021–2026 (0.405 → 0.285) while adoption grew fast | the neutral reference 0.5 is an assumption; US adoption also shows no braking by low sentiment (all loop-on candidates fail the US match) | **structural candidate:** sentiment reference anchored to the observed level and a small or lagged adoption elasticity | open: ASSEMBLY-WP-0017 |
|
||||
|
|
|
|||
22
research/FlipRegister.md
Normal file
22
research/FlipRegister.md
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Flip register (append-only)
|
||||
|
||||
Regime changes of jurisdictions, detected by model matching
|
||||
(`research/ModelMatching.md` § 3). **Never edit or delete an entry.**
|
||||
|
||||
**Status 2026-10-13:** no entries. Flip detection has not passed its
|
||||
synthetic test (`research/ModelMatching.md` § 4), so no measured flip may be
|
||||
recorded yet. Simulated flips (regime crossings inside model runs) are
|
||||
recorded in the break register until then (for example BR-005).
|
||||
|
||||
## Entry format
|
||||
|
||||
```markdown
|
||||
### F-YYYY-NNN — <jurisdiction>: <from regime> → <to regime>
|
||||
- Detected: <checkpoint date>, kind measured | simulated, by <script/version>
|
||||
- Evidence: weights before/after, indicators that moved
|
||||
- Consequences: predictions superseded (ids), break-register links
|
||||
```
|
||||
|
||||
## Entries
|
||||
|
||||
(none)
|
||||
104
research/ModelMatching.md
Normal file
104
research/ModelMatching.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# 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.
|
||||
|
||||
Code: `simulations/gadm02/matching.py`. Data: `observed_series.json`.
|
||||
Results: `results/matching.md` (observed), `results/matching_synthetic.md`
|
||||
(synthetic test).
|
||||
|
||||
## 1. Candidates and fit criterion (T01)
|
||||
|
||||
Candidates: band anchor B1–B4 × legitimacy loop off/on, each started from
|
||||
the jurisdiction's calibrated state (`calibrate_data.start_state`). B1 stays
|
||||
in the set with its misfit recorded, so a return to B1-like growth would be
|
||||
visible.
|
||||
|
||||
For each candidate, the adoption indicator is taken as proportional to K_Ω.
|
||||
Two values are fitted: a time offset t0 and a scale s, with
|
||||
`log y = log s + log K_Ω(t0 + τ) + ε` and ε ~ N(0, σ) (σ per series: BTOS
|
||||
0.03, Eurostat 0.05). The calibrated K_Ω level range at the end of the window
|
||||
adds a Gaussian level term. All candidates fit the same number of values, so
|
||||
AIC weights reduce to likelihood weights.
|
||||
|
||||
The sentiment slope (Pew 2021 → 2026) is not scored yet: the points precede
|
||||
the model start (checkpoint 0). It is switched on once there are sentiment
|
||||
points after checkpoint 0.
|
||||
|
||||
## 2. Results on observed data (T02, T04)
|
||||
|
||||
| Jurisdiction | Data | Anchor weights | Loop weights | Best |
|
||||
|---|---|---|---|---|
|
||||
| US | BTOS, 23 biweekly points 2025–2026 | B1 0.00 · B2 0.38 · B3 0.44 · B4 0.18 | off 1.00 · on 0.00 | B3 / loop off (0.44) |
|
||||
| EU | Eurostat 2023–2025 | B1 0.00 · B2 0.38 · B3 0.62 · B4 0.00 | off 0.47 · on 0.53 | B3 / loop off (0.46) |
|
||||
| DE | Eurostat 2023–2025 | B1 0.00 · B2 0.73 · B3 0.27 · B4 0.00 | off 0.33 · on 0.67 | B2 / loop on (0.57) |
|
||||
|
||||
Readings:
|
||||
|
||||
1. **The data rule out B1 everywhere, and B4 for the EU and DE.** The present
|
||||
is B2–B3, consistent with the calibration (`models/Calibration.md` § 3).
|
||||
2. **US adoption is not braked by its low sentiment.** Every loop-on
|
||||
candidate fails for the US, because with measured US sentiment (Σ 0.285)
|
||||
the loop slows adoption far below the observed growth. Either adoption's
|
||||
elasticity to sentiment (`sig_adopt`) is much smaller than 1, or
|
||||
sentiment acts on adoption with a long lag. This is the first data-based
|
||||
constraint on the legitimacy loop.
|
||||
3. **No regime is established yet.** No candidate reaches weight 0.75.
|
||||
Predictions v1 (P-2026-001 … 004) assume growth alignment between B2 and
|
||||
B3, which the matched weights bracket. No predictions v2 are needed (T04).
|
||||
|
||||
## 3. Flipping events (T03)
|
||||
|
||||
A **flipping event** is a change of the regime a jurisdiction operates
|
||||
under, detected when:
|
||||
|
||||
1. the best-weighted candidate changes, and
|
||||
2. the new best holds weight ≥ 0.75 at two consecutive evaluations.
|
||||
|
||||
Before a first regime is established (weight ≥ 0.75), changes are not flips.
|
||||
|
||||
**Measured flips** come from the regime filter on observed data. Every
|
||||
quarter, each regime's dynamics run forward from the current state estimate
|
||||
over the last quarter, the weights are updated with a switching prior (0.02
|
||||
per quarter), and the estimate advances with the best regime. **Simulated
|
||||
flips** are regime crossings inside a model run, for example the B4
|
||||
boom-bust of BR-005. Flips are recorded in `research/FlipRegister.md`.
|
||||
|
||||
## 4. Synthetic test (T06): not yet reliable
|
||||
|
||||
Truth is simulated from a known candidate. In the switch cases it changes
|
||||
regime at t = 4. It is observed biweekly with 3 % log-noise, with level
|
||||
evidence as wide as the real US target. Three seeds per case
|
||||
(`results/matching_synthetic.md`).
|
||||
|
||||
| Indicators | Correct regime at start | False flips (no-switch truth) | Switches detected |
|
||||
|---|---|---|---|
|
||||
| adoption only | 3 of 6 | 3 of 6 | 0 of 6 |
|
||||
| adoption + annual sentiment | 3 of 6 | 3 of 6 | 2 of 6 (delays 0.5 y and 3.25 y) |
|
||||
|
||||
The design went through three stages:
|
||||
|
||||
1. Free re-fitting of t0 and s per window: any candidate matches any segment.
|
||||
2. Alignment fixed after the first window: still path-based, so a regime
|
||||
change starting *now* is not represented.
|
||||
3. A forward filter from the current state, with evidence accumulated across
|
||||
quarters.
|
||||
|
||||
Each stage reduced false flips. None of them made detection reliable.
|
||||
|
||||
**Why:** with adoption alone, neighbouring anchors (B2/B3/B4) differ by less
|
||||
than measurement noise over a quarter. The loop switch leaves almost no trace
|
||||
in adoption. In sentiment it is confounded by BR-009: the sentiment
|
||||
equation reverts toward neutral from the low measured level.
|
||||
|
||||
**Requirements before flips may be declared** (ASSEMBLY-WP-0017):
|
||||
|
||||
- more discriminating indicators: investment (L5) for self-enhancement,
|
||||
sentiment at least twice a year for the loop, and concentration channels
|
||||
(CB1) for domination dynamics;
|
||||
- per-regime state tracking (one state per regime, not one shared estimate);
|
||||
- the sentiment equation fixed (BR-009);
|
||||
- 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.
|
||||
303
simulations/gadm02/matching.py
Normal file
303
simulations/gadm02/matching.py
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
"""Model matching and regime flipping (ASSEMBLY-WP-0012-T01/T02/T03/T06).
|
||||
|
||||
Candidates: band anchor (B1-B4) x legitimacy loop (off/on), each from the
|
||||
jurisdiction's calibrated state (calibrate_data.start_state). Method:
|
||||
research/ModelMatching.md.
|
||||
|
||||
Fit per candidate: the adoption indicator is taken as proportional to K_Omega,
|
||||
so two free values are fitted, a time offset t0 and a scale s
|
||||
(log y = log s + log K_Omega(t0 + tau) + noise, noise sd = the series'
|
||||
log_sigma). The calibrated K_Omega level range at the aligned time
|
||||
(calibration_targets.json) is scored as well, so shape and level both count.
|
||||
`sentiment_ll` scores a sentiment slope, but it is not used yet: the Pew
|
||||
points (2021, 2026) precede the model start (checkpoint 0). It is switched on
|
||||
once the sentiment series has points after checkpoint 0. All candidates have the same
|
||||
number of fitted values, so AIC weights reduce to likelihood weights.
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import random
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
|
||||
import calibrate_data as cd # noqa: E402
|
||||
import gadm04 as g # noqa: E402
|
||||
import grounding as gr # noqa: E402
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
OUT = HERE / "results"
|
||||
SERIES = json.loads((HERE / "observed_series.json").read_text())["series"]
|
||||
CANDIDATES = [(a, lp) for a in ("B1", "B2", "B3", "B4") for lp in ("off", "on")]
|
||||
STEP = 0.1
|
||||
T_PATH = 30.0
|
||||
FLIP_WEIGHT = 0.75
|
||||
SWITCH = 0.02 # prior probability per quarter that the regime changes (filter)
|
||||
_CACHE: dict = {}
|
||||
|
||||
|
||||
def path(jur: str, anchor: str, loop: str) -> list[dict]:
|
||||
key = (jur, anchor, loop)
|
||||
if key not in _CACHE:
|
||||
p, s = cd.start_state(jur, anchor)
|
||||
_CACHE[key] = g.simulate({**p, "sent": 1.0 if loop == "on" else 0.0}, s, T=T_PATH, record_every=1)
|
||||
return _CACHE[key]
|
||||
|
||||
|
||||
def interp(rows: list[dict], t: float, key: str) -> float:
|
||||
x = t / STEP
|
||||
i = min(int(x), len(rows) - 2)
|
||||
w = x - i
|
||||
return (1 - w) * rows[i][key] + w * rows[i + 1][key]
|
||||
|
||||
|
||||
def fit_series(rows: list[dict], points: list, log_sigma: float,
|
||||
fixed: tuple[float, float] | None = None) -> tuple[float, float, float]:
|
||||
"""(log-likelihood, t0, log s) for an indicator proportional to K_Omega.
|
||||
|
||||
fixed = (t0, log s): no fitting, score the points at this alignment and scale. Used after the
|
||||
first evaluation: the calendar fixes how t0 advances and the measurement fixes the scale."""
|
||||
if fixed is not None:
|
||||
t0, ls = fixed
|
||||
if t0 + points[-1][0] > T_PATH - STEP:
|
||||
return -1e9, t0, ls
|
||||
sse = sum((math.log(y) - math.log(max(1e-9, interp(rows, t0 + tau, "Ko"))) - ls) ** 2 for tau, y in points)
|
||||
return -0.5 * sse / log_sigma ** 2, t0, ls
|
||||
span = points[-1][0]
|
||||
best = None
|
||||
t0 = 0.0
|
||||
while t0 + span <= T_PATH - 1:
|
||||
r = [math.log(y) - math.log(max(1e-9, interp(rows, t0 + tau, "Ko"))) for tau, y in points]
|
||||
ls = sum(r) / len(r)
|
||||
sse = sum((x - ls) ** 2 for x in r)
|
||||
if best is None or sse < best[0]:
|
||||
best = (sse, t0, ls)
|
||||
t0 += 0.05
|
||||
sse, t0, ls = best
|
||||
return -0.5 * sse / log_sigma ** 2, t0, ls
|
||||
|
||||
|
||||
def sentiment_ll(rows: list[dict], t0: float, series: dict) -> float:
|
||||
(ta, ya), (tb, yb) = series["points"][0], series["points"][-1]
|
||||
obs = (yb - ya) / (tb - ta)
|
||||
span = tb - ta
|
||||
a = max(0.0, t0 - span) # the sentiment window ends at the adoption alignment
|
||||
mod = (interp(rows, a + span, "Sig") - interp(rows, a, "Sig")) / span
|
||||
return -0.5 * ((mod - obs) * span / series["sigma"]) ** 2
|
||||
|
||||
|
||||
def score(jur: str, rows_for: callable, adoption: dict, sentiment: dict | None = None,
|
||||
level: dict | None = None, fixed: dict | None = None) -> dict:
|
||||
out = {}
|
||||
for a, lp in CANDIDATES:
|
||||
rows = rows_for(jur, a, lp)
|
||||
ll, t0, ls = fit_series(rows, adoption["points"], adoption["log_sigma"],
|
||||
fixed=fixed.get((a, lp)) if fixed else None)
|
||||
if level is not None: # K_Omega level at the end of the observed window
|
||||
ko = interp(rows, t0 + adoption["points"][-1][0], "Ko")
|
||||
ll += -0.5 * cd.z(ko, level) ** 2
|
||||
if sentiment is not None:
|
||||
ll += sentiment_ll(rows, t0, sentiment)
|
||||
out[(a, lp)] = {"ll": ll, "t0": t0, "s": math.exp(ls), "ls": ls}
|
||||
mx = max(v["ll"] for v in out.values())
|
||||
z = sum(math.exp(v["ll"] - mx) for v in out.values())
|
||||
for v in out.values():
|
||||
v["w"] = math.exp(v["ll"] - mx) / z
|
||||
return out
|
||||
|
||||
|
||||
def best(res: dict) -> tuple:
|
||||
return max(res, key=lambda k: res[k]["w"])
|
||||
|
||||
|
||||
def flips(evaluations: list[tuple[float, dict]]) -> list[tuple[float, tuple, tuple]]:
|
||||
"""Flip events: best changes and the new best holds weight >= FLIP_WEIGHT twice in a row."""
|
||||
events, current, pending = [], None, None
|
||||
for t, res in evaluations:
|
||||
b, wb = best(res), res[best(res)]["w"]
|
||||
if current is None:
|
||||
if wb >= FLIP_WEIGHT:
|
||||
current = b
|
||||
continue
|
||||
if b != current and wb >= FLIP_WEIGHT:
|
||||
if pending == b:
|
||||
events.append((t, current, b))
|
||||
current, pending = b, None
|
||||
else:
|
||||
pending = b
|
||||
else:
|
||||
pending = None
|
||||
return events
|
||||
|
||||
|
||||
def fmt(c: tuple) -> str:
|
||||
return f"{c[0]}/loop {c[1]}"
|
||||
|
||||
|
||||
def main_observed() -> None:
|
||||
lines = [f"# {g.MODEL_VERSION}: model matching on observed series", "",
|
||||
"Generated by `simulations/gadm02/matching.py` (ASSEMBLY-WP-0012). Method:",
|
||||
"`research/ModelMatching.md`. Data: `observed_series.json`. Weights are likelihood (AIC) weights",
|
||||
"over the eight candidates; all candidates fit the same two values (t0, s). Each score adds the",
|
||||
"calibrated K_Ω level range at the end of the window. Sentiment is not scored yet (data precede",
|
||||
"checkpoint 0).", ""]
|
||||
for jur, adop, sent in (("US", "US_adoption", None), ("EU", "EU_adoption", None),
|
||||
("DE", "DE_adoption", None)):
|
||||
res = score(jur, path, SERIES[adop], SERIES[sent] if sent else None, level=cd.targets(jur)["Ko"])
|
||||
b = best(res)
|
||||
lines += [f"## {jur}", "", f"Series: {SERIES[adop]['source']}" + (f"; {SERIES[sent]['source']}" if sent else ""),
|
||||
"", "| Candidate | log-likelihood | weight | t0 | K_Ω at window end |", "|---|---|---|---|---|"]
|
||||
for c in sorted(res, key=lambda k: -res[k]["w"]):
|
||||
v = res[c]
|
||||
lines.append(f"| {fmt(c)} | {v['ll']:.1f} | {v['w']:.3f} | {v['t0']:.2f} | "
|
||||
f"{interp(path(jur, *c), v['t0'] + SERIES[adop]['points'][-1][0], 'Ko'):.3f} |")
|
||||
anchors = {}
|
||||
for (a, _lp), v in res.items():
|
||||
anchors[a] = anchors.get(a, 0) + v["w"]
|
||||
loops = {lp: sum(v["w"] for (a, l2), v in res.items() if l2 == lp) for lp in ("off", "on")}
|
||||
lines += ["", f"**Best:** {fmt(b)} (weight {res[b]['w']:.2f}). Anchor weights: "
|
||||
+ ", ".join(f"{a} {w:.2f}" for a, w in anchors.items())
|
||||
+ f". Loop weights: off {loops['off']:.2f}, on {loops['on']:.2f}.", ""]
|
||||
(OUT / "matching.md").write_text("\n".join(lines))
|
||||
print("\n".join(lines))
|
||||
|
||||
|
||||
def synthetic_rows(jur: str, first: tuple, second: tuple | None, t_switch: float) -> list[dict]:
|
||||
"""Truth: candidate `first` until t_switch, then `second` parameters continuing from that state."""
|
||||
p1, s1 = cd.start_state(jur, first[0])
|
||||
r1 = g.simulate({**p1, "sent": 1.0 if first[1] == "on" else 0.0}, s1, T=t_switch, record_every=1)
|
||||
if second is None:
|
||||
return g.simulate({**p1, "sent": 1.0 if first[1] == "on" else 0.0}, s1, T=T_PATH, record_every=1)
|
||||
p2, _s2 = cd.start_state(jur, second[0])
|
||||
state = {k: r1[-1][k] for k in g.STOCKS}
|
||||
r2 = g.simulate({**p2, "sent": 1.0 if second[1] == "on" else 0.0}, state, T=T_PATH - t_switch, record_every=1)
|
||||
for r in r2[1:]:
|
||||
r["t"] = round(r["t"] + t_switch, 6)
|
||||
return r1 + r2[1:]
|
||||
|
||||
|
||||
_PARAMS: dict = {}
|
||||
|
||||
|
||||
def regime_params(jur: str, cand: tuple) -> dict:
|
||||
"""Parameters of a regime (anchor dynamics, loop switch) for jurisdiction `jur`."""
|
||||
if (jur, cand) not in _PARAMS:
|
||||
p, _s = cd.start_state(jur, cand[0])
|
||||
_PARAMS[(jur, cand)] = {**p, "sent": 1.0 if cand[1] == "on" else 0.0}
|
||||
return _PARAMS[(jur, cand)]
|
||||
|
||||
|
||||
def filter_step(jur: str, state: dict, pts: list, scale_ls: float, log_sigma: float,
|
||||
level: dict | None, window: float, sent_pts: list | None = None, sent_sd: float = 0.02) -> dict:
|
||||
"""Score every regime by running its dynamics forward from `state` over the window.
|
||||
|
||||
sent_pts: optional sentiment observations (tau, Sigma) within the window."""
|
||||
out = {}
|
||||
for c in CANDIDATES:
|
||||
rows = g.simulate(regime_params(jur, c), state, T=window, record_every=1)
|
||||
sse = sum((math.log(y) - math.log(max(1e-9, interp(rows, tau, "Ko"))) - scale_ls) ** 2 for tau, y in pts)
|
||||
ll = -0.5 * sse / log_sigma ** 2
|
||||
for tau, y in sent_pts or []:
|
||||
ll += -0.5 * ((interp(rows, tau, "Sig") - y) / sent_sd) ** 2
|
||||
if level is not None:
|
||||
ll += -0.5 * cd.z(rows[-1]["Ko"], level) ** 2
|
||||
out[c] = {"ll": ll, "end": {k: rows[-1][k] for k in g.STOCKS}}
|
||||
mx = max(v["ll"] for v in out.values())
|
||||
zs = sum(math.exp(v["ll"] - mx) for v in out.values())
|
||||
for v in out.values():
|
||||
v["w"] = math.exp(v["ll"] - mx) / zs
|
||||
return out
|
||||
|
||||
|
||||
def synthetic_run(truth: list[dict], start: float, years: float, window: float, seed: int,
|
||||
log_sigma: float = 0.03, sentiment: bool = False) -> list[tuple[float, dict]]:
|
||||
"""Quarterly regime filter on noisy biweekly observations (research/ModelMatching.md § 3).
|
||||
|
||||
First window: whole-path alignment fit (t0, scale) gives the initial state and the scale. Then,
|
||||
every quarter, each regime runs forward from the current state estimate over the last window;
|
||||
the state estimate advances with the best regime."""
|
||||
rnd = random.Random(seed)
|
||||
obs, t = [], start
|
||||
while t <= start + years:
|
||||
obs.append((t, interp(truth, t, "Ko") * 25.0 * math.exp(rnd.gauss(0, log_sigma))))
|
||||
t += 2 / 52
|
||||
sobs = [(start + k, interp(truth, start + k, "Sig") + rnd.gauss(0, 0.02)) for k in range(int(years) + 1)] \
|
||||
if sentiment else []
|
||||
te0 = start + window
|
||||
pts0 = [(tt - start, y) for tt, y in obs if start <= tt <= te0]
|
||||
first = score("US", path, {"points": pts0, "log_sigma": log_sigma},
|
||||
level={"lo": interp(truth, te0, "Ko") - 0.075, "hi": interp(truth, te0, "Ko") + 0.075})
|
||||
b = best(first)
|
||||
scale_ls = first[b]["ls"]
|
||||
t_est = first[b]["t0"] + window
|
||||
rows_b = path("US", *b)
|
||||
i_est = int(round(t_est / STEP))
|
||||
est = {k: rows_b[i_est][k] for k in g.STOCKS}
|
||||
evals = [(te0, first)]
|
||||
step = 0.25
|
||||
te = te0 + step
|
||||
while te <= start + years + 1e-9:
|
||||
pts = [(tt - (te - step), y) for tt, y in obs if te - step - 1e-9 <= tt <= te]
|
||||
ko = interp(truth, te, "Ko")
|
||||
spts = [(tt - (te - step), y) for tt, y in sobs if te - step < tt <= te + 1e-9]
|
||||
res = filter_step("US", est, pts, scale_ls, log_sigma, {"lo": ko - 0.075, "hi": ko + 0.075}, step, spts)
|
||||
prev = evals[-1][1]
|
||||
n = len(CANDIDATES)
|
||||
prior = {c: (1 - SWITCH) * prev[c]["w"] + SWITCH * (1 - prev[c]["w"]) / (n - 1) for c in CANDIDATES}
|
||||
mx = max(res[c]["ll"] for c in CANDIDATES)
|
||||
post = {c: prior[c] * math.exp(res[c]["ll"] - mx) for c in CANDIDATES}
|
||||
zs = sum(post.values())
|
||||
for c in CANDIDATES:
|
||||
res[c]["w"] = post[c] / zs # evidence accumulates across quarters
|
||||
est = res[best(res)]["end"]
|
||||
evals.append((te, res))
|
||||
te += step
|
||||
return evals
|
||||
|
||||
|
||||
def main_synthetic() -> None:
|
||||
lines = [f"# {g.MODEL_VERSION}: matching and flip detection on synthetic data", "",
|
||||
"Generated by `simulations/gadm02/matching.py --synthetic` (ASSEMBLY-WP-0012-T06).",
|
||||
"US calibrated state. Truth simulated from a known candidate (optionally switching regime at",
|
||||
"t_switch), observed biweekly as 25 × K_Ω with log-noise sd 0.03, from t 2 to t 8. Quarterly",
|
||||
"regime filter: the first year fixes the initial state and the scale (whole-path alignment); then",
|
||||
"every quarter each regime runs forward from the current state estimate over the last quarter, and",
|
||||
"the estimate advances with the best regime. Weights accumulate across quarters (forward filter,",
|
||||
f"switch probability {SWITCH} per quarter). Level evidence: true K_Ω ± 0.075 (as wide as the real",
|
||||
f"US target). Flip: new best with weight ≥ {FLIP_WEIGHT} twice in a row.", "",
|
||||
"| Indicators | Truth | Seed | Best at first evaluation | Flips detected (t, from → to) | Verdict |",
|
||||
"|---|---|---|---|---|---|"]
|
||||
cases = [(("B3", "off"), None, None), (("B2", "off"), None, None),
|
||||
(("B2", "off"), ("B4", "off"), 4.0), (("B3", "off"), ("B3", "on"), 4.0)]
|
||||
for first, second, ts in cases:
|
||||
truth = synthetic_rows("US", first, second, ts or T_PATH)
|
||||
for seed, sentiment in ((1, False), (2, False), (3, False), (1, True), (2, True), (3, True)):
|
||||
ev = synthetic_run(truth, 2.0, 6.0, 1.0, seed, sentiment=sentiment)
|
||||
fl = flips(ev)
|
||||
b0 = best(ev[0][1])
|
||||
if second is None:
|
||||
verdict = "ok (no false flip)" if not fl else "FALSE FLIP"
|
||||
else:
|
||||
hit = [e for e in fl if e[0] >= ts and e[2][0] == second[0] and (second[1] == first[1] or e[2][1] == second[1])]
|
||||
verdict = f"detected, delay {hit[0][0] - ts:.2f} y" if hit else "missed"
|
||||
label = fmt(first) + (f" → {fmt(second)} at t {ts}" if second else "")
|
||||
lines.append(f"| {'adoption + annual sentiment' if sentiment else 'adoption'} | {label} | {seed} | {fmt(b0)} | "
|
||||
+ ("; ".join(f"{t:.2f}: {fmt(a)} → {fmt(b)}" for t, a, b in fl) or "none")
|
||||
+ f" | {verdict} |")
|
||||
lines.append("")
|
||||
(OUT / "matching_synthetic.md").write_text("\n".join(lines))
|
||||
print("\n".join(lines))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main_synthetic() if "--synthetic" in sys.argv else main_observed()
|
||||
32
simulations/gadm02/observed_series.json
Normal file
32
simulations/gadm02/observed_series.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"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)",
|
||||
"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]]
|
||||
},
|
||||
"EU_adoption": {
|
||||
"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]]
|
||||
},
|
||||
"DE_adoption": {
|
||||
"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]]
|
||||
},
|
||||
"US_sentiment": {
|
||||
"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]]
|
||||
}
|
||||
}
|
||||
}
|
||||
58
simulations/gadm02/results/matching.md
Normal file
58
simulations/gadm02/results/matching.md
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# GADM-0.4: model matching on observed series
|
||||
|
||||
Generated by `simulations/gadm02/matching.py` (ASSEMBLY-WP-0012). Method:
|
||||
`research/ModelMatching.md`. Data: `observed_series.json`. Weights are likelihood (AIC) weights
|
||||
over the eight candidates; all candidates fit the same two values (t0, s). Each score adds the
|
||||
calibrated K_Ω level range at the end of the window. Sentiment is not scored yet (data precede
|
||||
checkpoint 0).
|
||||
|
||||
## US
|
||||
|
||||
Series: U.S. Census Bureau BTOS Q7 (National.xlsx), periods 2025-24 to 2026-20 (biweekly)
|
||||
|
||||
| Candidate | log-likelihood | weight | t0 | K_Ω at window end |
|
||||
|---|---|---|---|---|
|
||||
| B3/loop off | -8.3 | 0.443 | 1.20 | 0.172 |
|
||||
| B2/loop off | -8.5 | 0.376 | 0.20 | 0.077 |
|
||||
| B4/loop off | -9.2 | 0.176 | 1.90 | 0.278 |
|
||||
| B4/loop on | -12.8 | 0.005 | 0.00 | 0.065 |
|
||||
| B3/loop on | -19.4 | 0.000 | 0.00 | 0.062 |
|
||||
| B1/loop off | -80.7 | 0.000 | 0.00 | 0.054 |
|
||||
| B2/loop on | -82.7 | 0.000 | 3.70 | 0.069 |
|
||||
| B1/loop on | -110.1 | 0.000 | 9.60 | 0.045 |
|
||||
|
||||
**Best:** B3/loop off (weight 0.44). Anchor weights: B1 0.00, B2 0.38, B3 0.44, B4 0.18. Loop weights: off 1.00, on 0.00.
|
||||
|
||||
## EU
|
||||
|
||||
Series: Eurostat isoc_eb_ai, EU27_2020, reference years 2023-2025 (2021: 7.65, before generative AI, excluded)
|
||||
|
||||
| Candidate | log-likelihood | weight | t0 | K_Ω at window end |
|
||||
|---|---|---|---|---|
|
||||
| B3/loop off | -2.2 | 0.456 | 0.50 | 0.198 |
|
||||
| B2/loop on | -2.4 | 0.366 | 0.00 | 0.107 |
|
||||
| B3/loop on | -3.3 | 0.159 | 0.55 | 0.222 |
|
||||
| B2/loop off | -5.6 | 0.016 | 0.00 | 0.098 |
|
||||
| B4/loop off | -7.5 | 0.002 | 1.00 | 0.308 |
|
||||
| B4/loop on | -11.4 | 0.000 | 1.05 | 0.356 |
|
||||
| B1/loop on | -49.8 | 0.000 | 0.00 | 0.061 |
|
||||
| B1/loop off | -58.0 | 0.000 | 0.00 | 0.058 |
|
||||
|
||||
**Best:** B3/loop off (weight 0.46). Anchor weights: B1 0.00, B2 0.38, B3 0.62, B4 0.00. Loop weights: off 0.47, on 0.53.
|
||||
|
||||
## DE
|
||||
|
||||
Series: Eurostat isoc_eb_ai, DE, reference years 2023-2025 (2021: 10.56, excluded)
|
||||
|
||||
| Candidate | log-likelihood | weight | t0 | K_Ω at window end |
|
||||
|---|---|---|---|---|
|
||||
| B2/loop on | -0.1 | 0.573 | 0.00 | 0.110 |
|
||||
| B3/loop off | -1.3 | 0.178 | 0.65 | 0.214 |
|
||||
| B2/loop off | -1.4 | 0.152 | 0.00 | 0.101 |
|
||||
| B3/loop on | -1.9 | 0.097 | 0.70 | 0.240 |
|
||||
| B4/loop off | -8.3 | 0.000 | 1.35 | 0.370 |
|
||||
| B4/loop on | -11.0 | 0.000 | 1.35 | 0.415 |
|
||||
| B1/loop on | -36.2 | 0.000 | 0.00 | 0.063 |
|
||||
| B1/loop off | -43.4 | 0.000 | 0.00 | 0.059 |
|
||||
|
||||
**Best:** B2/loop on (weight 0.57). Anchor weights: B1 0.00, B2 0.73, B3 0.27, B4 0.00. Loop weights: off 0.33, on 0.67.
|
||||
37
simulations/gadm02/results/matching_synthetic.md
Normal file
37
simulations/gadm02/results/matching_synthetic.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# GADM-0.4: matching and flip detection on synthetic data
|
||||
|
||||
Generated by `simulations/gadm02/matching.py --synthetic` (ASSEMBLY-WP-0012-T06).
|
||||
US calibrated state. Truth simulated from a known candidate (optionally switching regime at
|
||||
t_switch), observed biweekly as 25 × K_Ω with log-noise sd 0.03, from t 2 to t 8. Quarterly
|
||||
regime filter: the first year fixes the initial state and the scale (whole-path alignment); then
|
||||
every quarter each regime runs forward from the current state estimate over the last quarter, and
|
||||
the estimate advances with the best regime. Weights accumulate across quarters (forward filter,
|
||||
switch probability 0.02 per quarter). Level evidence: true K_Ω ± 0.075 (as wide as the real
|
||||
US target). Flip: new best with weight ≥ 0.75 twice in a row.
|
||||
|
||||
| Indicators | Truth | Seed | Best at first evaluation | Flips detected (t, from → to) | Verdict |
|
||||
|---|---|---|---|---|---|
|
||||
| adoption | B3/loop off | 1 | B3/loop off | none | ok (no false flip) |
|
||||
| adoption | B3/loop off | 2 | B3/loop off | 4.50: B3/loop off → B4/loop off; 7.75: B4/loop off → B3/loop off | FALSE FLIP |
|
||||
| adoption | B3/loop off | 3 | B3/loop off | 4.75: B4/loop off → B3/loop off | FALSE FLIP |
|
||||
| adoption + annual sentiment | B3/loop off | 1 | B3/loop off | none | ok (no false flip) |
|
||||
| adoption + annual sentiment | B3/loop off | 2 | B3/loop off | 4.50: B3/loop off → B4/loop off; 7.75: B4/loop off → B3/loop off | FALSE FLIP |
|
||||
| adoption + annual sentiment | B3/loop off | 3 | B3/loop off | 4.75: B4/loop off → B3/loop off | FALSE FLIP |
|
||||
| adoption | B2/loop off | 1 | B4/loop on | none | ok (no false flip) |
|
||||
| adoption | B2/loop off | 2 | B3/loop on | 8.00: B3/loop on → B2/loop off | FALSE FLIP |
|
||||
| adoption | B2/loop off | 3 | B3/loop on | none | ok (no false flip) |
|
||||
| adoption + annual sentiment | B2/loop off | 1 | B4/loop on | none | ok (no false flip) |
|
||||
| adoption + annual sentiment | B2/loop off | 2 | B3/loop on | none | ok (no false flip) |
|
||||
| adoption + annual sentiment | B2/loop off | 3 | B3/loop on | 8.00: B2/loop off → B4/loop on | FALSE FLIP |
|
||||
| adoption | B2/loop off → B4/loop off at t 4.0 | 1 | B4/loop on | none | missed |
|
||||
| adoption | B2/loop off → B4/loop off at t 4.0 | 2 | B3/loop on | none | missed |
|
||||
| adoption | B2/loop off → B4/loop off at t 4.0 | 3 | B3/loop on | none | missed |
|
||||
| adoption + annual sentiment | B2/loop off → B4/loop off at t 4.0 | 1 | B4/loop on | none | missed |
|
||||
| adoption + annual sentiment | B2/loop off → B4/loop off at t 4.0 | 2 | B3/loop on | 4.50: B2/loop off → B4/loop off | detected, delay 0.50 y |
|
||||
| adoption + annual sentiment | B2/loop off → B4/loop off at t 4.0 | 3 | B3/loop on | none | missed |
|
||||
| adoption | B3/loop off → B3/loop on at t 4.0 | 1 | B3/loop off | none | missed |
|
||||
| adoption | B3/loop off → B3/loop on at t 4.0 | 2 | B3/loop off | none | missed |
|
||||
| adoption | B3/loop off → B3/loop on at t 4.0 | 3 | B3/loop off | none | missed |
|
||||
| adoption + annual sentiment | B3/loop off → B3/loop on at t 4.0 | 1 | B3/loop off | none | missed |
|
||||
| adoption + annual sentiment | B3/loop off → B3/loop on at t 4.0 | 2 | B3/loop off | 8.00: B3/loop off → B4/loop on | missed |
|
||||
| adoption + annual sentiment | B3/loop off → B3/loop on at t 4.0 | 3 | B3/loop off | 7.25: B4/loop off → B3/loop on | detected, delay 3.25 y |
|
||||
29
simulations/gadm02/test_matching.py
Normal file
29
simulations/gadm02/test_matching.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
"""Tests for the matching engine (ASSEMBLY-WP-0012)."""
|
||||
|
||||
import math
|
||||
import unittest
|
||||
|
||||
import gadm04 as g
|
||||
import matching as m
|
||||
|
||||
|
||||
class TestMatching(unittest.TestCase):
|
||||
def test_fit_recovers_alignment_and_scale(self):
|
||||
rows = g.simulate(T=m.T_PATH, record_every=1)
|
||||
pts = [(tau, 25.0 * m.interp(rows, 3.0 + tau, "Ko")) for tau in (0.0, 0.25, 0.5, 0.75, 1.0)]
|
||||
ll, t0, ls = m.fit_series(rows, pts, 0.03)
|
||||
self.assertAlmostEqual(t0, 3.0, places=6)
|
||||
self.assertAlmostEqual(math.exp(ls), 25.0, places=4)
|
||||
self.assertGreater(ll, -1e-6)
|
||||
|
||||
def test_flip_needs_two_consecutive_strong_evaluations(self):
|
||||
a, b = ("B2", "off"), ("B3", "off")
|
||||
def ev(t, wa):
|
||||
return (t, {a: {"w": wa}, b: {"w": 1 - wa}})
|
||||
self.assertEqual(m.flips([ev(0, 0.9), ev(1, 0.1), ev(2, 0.9)]), [])
|
||||
self.assertEqual(m.flips([ev(0, 0.9), ev(1, 0.1), ev(2, 0.1)]), [(2, a, b)])
|
||||
self.assertEqual(m.flips([ev(0, 0.5), ev(1, 0.1), ev(2, 0.1)]), []) # nothing established yet
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -4,7 +4,7 @@ type: workplan
|
|||
title: "Model matching and regime flipping"
|
||||
domain: communication
|
||||
repo: assembly-economics
|
||||
status: ready
|
||||
status: finished
|
||||
flavor: implementation
|
||||
depends_on: [ASSEMBLY-WP-0013, ASSEMBLY-WP-0015, ASSEMBLY-WP-0010]
|
||||
owner: claude-code
|
||||
|
|
@ -43,11 +43,13 @@ reality-aligned baseline.
|
|||
|
||||
```task
|
||||
id: ASSEMBLY-WP-0012-T01
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "e2c8b5f3-67aa-5f69-b654-b589d990105e"
|
||||
```
|
||||
|
||||
**Done 2026-10-13:** `research/ModelMatching.md` § 1; candidates B1–B4 × loop off/on; likelihood with level term.
|
||||
|
||||
Start with a small candidate set that the data can already separate:
|
||||
anchors B2, B3, B4 × legitimacy loop off/on × the fitted adoption speed k
|
||||
from `calibrate_data.py`. Keep B1 as a candidate with its current misfit
|
||||
|
|
@ -63,11 +65,13 @@ Write `research/ModelMatching.md`.
|
|||
|
||||
```task
|
||||
id: ASSEMBLY-WP-0012-T02
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "a3eae0d5-c5cf-5af2-b419-ab72dfd504f7"
|
||||
```
|
||||
|
||||
**Done 2026-10-13:** `matching.py`, `observed_series.json`, `results/matching.md`. US B3/B2 with loop off; EU B3/B2; DE B2. No regime established (weights < 0.75).
|
||||
|
||||
Build on `calibrate_data.py` (its alignment by growth and the z-scores per
|
||||
target are the first score). Implement `matching.py`: for each jurisdiction, fit each candidate to the
|
||||
checkpoint series (using the WP-0013 calibration routine), score it, and
|
||||
|
|
@ -79,11 +83,13 @@ checkpoint.
|
|||
|
||||
```task
|
||||
id: ASSEMBLY-WP-0012-T03
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "a7a9a42a-8703-56c8-ab0d-b7505e22dfef"
|
||||
```
|
||||
|
||||
**Done 2026-10-13:** flip definition and regime filter (`research/ModelMatching.md` § 3), `research/FlipRegister.md` (empty), `test_matching.py`.
|
||||
|
||||
First flip candidates to watch: B3 → B2 (an adoption slowdown in the BTOS
|
||||
series, compare P-2026-001) and loop off → on (sentiment falling faster than
|
||||
adoption). Define a flipping event: the best-fitting regime changes **and** the
|
||||
|
|
@ -97,11 +103,13 @@ flip register (`research/FlipRegister.md`), linked to the break register.
|
|||
|
||||
```task
|
||||
id: ASSEMBLY-WP-0012-T04
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "20c370c2-2989-54ba-b3a4-0ba056532f88"
|
||||
```
|
||||
|
||||
**Done 2026-10-13:** matched weights bracket the v1 assumption (B2–B3); no predictions v2 needed.
|
||||
|
||||
Use the best-fitting regime, or a weighted ensemble, for the prediction
|
||||
register (`checkpoints/predictions.md`). Each prediction states which
|
||||
regime produced it. After a flip, new predictions use the new regime; old
|
||||
|
|
@ -111,11 +119,13 @@ predictions remain and are scored as they were made.
|
|||
|
||||
```task
|
||||
id: ASSEMBLY-WP-0012-T05
|
||||
status: todo
|
||||
status: cancel
|
||||
priority: medium
|
||||
state_hub_task_id: "88940a83-d078-5346-94da-d49de570f4f3"
|
||||
```
|
||||
|
||||
**Moved 2026-10-13 to ASSEMBLY-WP-0017-T05** (waits for a reliable filter).
|
||||
|
||||
Run matching at every full checkpoint (and optionally at light ones), and
|
||||
report the current regime per jurisdiction and any flip candidates in the
|
||||
checkpoint record. Coordinate the runner change with WP-0010.
|
||||
|
|
@ -124,12 +134,22 @@ checkpoint record. Coordinate the runner change with WP-0010.
|
|||
|
||||
```task
|
||||
id: ASSEMBLY-WP-0012-T06
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "3a3ec337-322e-5b4c-86f7-db3cfe463d6d"
|
||||
```
|
||||
|
||||
**Done 2026-10-13, negative result:** flip detection is not yet reliable (3 of 6 false flips, at most 2 of 6 switches detected). Requirements in `research/ModelMatching.md` § 4; work in ASSEMBLY-WP-0017.
|
||||
|
||||
Before trusting real flips: generate synthetic indicator series from a known
|
||||
regime that switches at a known time, add realistic noise and gaps, and
|
||||
check that matching recovers the regime and detects the flip with an
|
||||
acceptable delay and false-alarm rate.
|
||||
|
||||
## Close-out 2026-10-13
|
||||
|
||||
T01–T04 and T06 are done (`research/ModelMatching.md`, `matching.py`,
|
||||
`research/FlipRegister.md`, BR-009). The synthetic test shows that flip
|
||||
detection is not yet reliable. The residuals are handed off to
|
||||
ASSEMBLY-WP-0017: the sentiment equation, more indicators, a per-regime
|
||||
filter, passing the acceptance test, and checkpoint integration.
|
||||
|
|
|
|||
89
workplans/ASSEMBLY-WP-0017-reliable-flip-detection.md
Normal file
89
workplans/ASSEMBLY-WP-0017-reliable-flip-detection.md
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
---
|
||||
id: ASSEMBLY-WP-0017
|
||||
type: workplan
|
||||
title: "Reliable flip detection: indicators, per-regime filter, sentiment equation"
|
||||
domain: communication
|
||||
repo: assembly-economics
|
||||
status: proposed
|
||||
flavor: residual
|
||||
depends_on: [ASSEMBLY-WP-0012]
|
||||
owner: claude-code
|
||||
topic_slug: coulomb-social
|
||||
created: "2026-10-13"
|
||||
updated: "2026-10-13"
|
||||
---
|
||||
|
||||
# Reliable flip detection
|
||||
|
||||
Residuals of ASSEMBLY-WP-0012. Matching on observed data works
|
||||
(`research/ModelMatching.md` § 2). Flip detection does not yet pass its
|
||||
synthetic test (§ 4): 3 of 6 false flips, at most 2 of 6 switches detected.
|
||||
No flip may be recorded in `research/FlipRegister.md` until T04 passes.
|
||||
|
||||
## Fix the sentiment equation (BR-009)
|
||||
|
||||
```task
|
||||
id: ASSEMBLY-WP-0017-T01
|
||||
status: todo
|
||||
priority: high
|
||||
```
|
||||
|
||||
Σ relaxes toward 0.5 + gain × Δ(perceived benefit), so from the measured US
|
||||
level (0.285) it rises, while measured US sentiment fell 2021–2026. Add a
|
||||
sentiment reference parameter: neutral 0.5 (the default, keeps old results)
|
||||
or anchored to the observed level. Fit `sig_adopt` (and an optional lag) to
|
||||
the US finding that adoption grows fast despite low sentiment. Re-run
|
||||
`legitimacy.py`, `calibrated_readings.py` and `world_scenarios.py`, and
|
||||
record which readings change.
|
||||
|
||||
## More discriminating indicators in the filter
|
||||
|
||||
```task
|
||||
id: ASSEMBLY-WP-0017-T02
|
||||
status: todo
|
||||
priority: high
|
||||
```
|
||||
|
||||
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
|
||||
surveys allow it. Each indicator needs an observation operator and a noise
|
||||
level.
|
||||
|
||||
## Per-regime state tracking
|
||||
|
||||
```task
|
||||
id: ASSEMBLY-WP-0017-T03
|
||||
status: todo
|
||||
priority: medium
|
||||
```
|
||||
|
||||
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
|
||||
state.
|
||||
|
||||
## Pass the synthetic acceptance test
|
||||
|
||||
```task
|
||||
id: ASSEMBLY-WP-0017-T04
|
||||
status: todo
|
||||
priority: high
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## Checkpoint integration (from WP-0012-T05)
|
||||
|
||||
```task
|
||||
id: ASSEMBLY-WP-0017-T05
|
||||
status: wait
|
||||
priority: medium
|
||||
```
|
||||
|
||||
Waits for T04. Then run matching at every full checkpoint (optionally at
|
||||
light ones), report the current regime and any flip candidates in the
|
||||
checkpoint record, and coordinate the runner change with WP-0014 (governed
|
||||
execution profile).
|
||||
Loading…
Add table
Add a link
Reference in a new issue