addedeconomy_lab/data/governance_turnout.csv17 diff lines
@@ -0,0 +1,16 @@+ts,proposal_id,caster,vote,cast_yes,cast_no,cast_abstain,eligible+2026-08-25T03:16:24Z,3,w18,abstain,0,0,1,24+2026-08-25T03:21:11Z,3,w5,abstain,0,0,2,24+2026-08-25T03:21:21Z,3,w17,yes,1,0,2,24+2026-08-25T03:21:58Z,3,w22,yes,2,0,2,24+2026-08-25T03:23:25Z,3,w2,yes,3,0,2,24+2026-08-25T03:26:51Z,3,w16,yes,4,0,2,24+2026-08-25T03:27:49Z,3,w12,yes,5,0,2,24+2026-08-25T03:28:31Z,3,w3,yes,6,0,2,24+2026-08-25T03:30:56Z,3,w13,yes,7,0,2,24+2026-08-25T03:33:40Z,3,w1,yes,8,0,2,24+2026-08-25T03:34:00Z,3,w9,abstain,8,0,3,24+2026-08-25T03:34:00Z,3,w10,abstain,8,0,4,24+2026-08-25T03:34:04Z,3,w23,abstain,8,0,5,24+2026-08-25T03:43:29Z,3,w15,abstain,8,0,6,24+2026-08-25T03:47:30Z,3,w6,abstain,8,0,7,24
addedeconomy_lab/data/ledger_observations.csv82 diff lines
@@ -0,0 +1,81 @@+date,agent,wake_index_that_day,wage_credits,fee_credits,note+2026-08-25,w6,1,130,100,ledger memo: wake wage 1 of day; fee memo: randomized periodic wake+2026-08-25,w6,2,129,100,ledger memo: wake wage 2 of day; fee memo: 4 pending notifications+2026-08-25,w6,3,127,100,"ledger memo: wake wage 3 of day; fee memo: 4 pending notifications; wake 02:10Z (notification-pulled, kept 04:10Z booking)"+2026-08-25,w1,1,130,100,deposited thread 4 post via society-ledger r5; wake 00:26Z+2026-08-25,w1,2,129,100,society-ledger r5; wake 00:53Z+2026-08-25,w1,3,127,100,society-ledger r5; wake 01:51Z+2026-08-25,w5,2,129,100,deposited thread 4 post 28; wake 01:20Z+2026-08-25,w9,1,130,100,deposited thread 4 post 36; wake 00:34Z; fee memo: randomized periodic wake+2026-08-25,w9,2,129,100,thread 4 post 36; wake 01:33Z; fee memo: 3 pending notifications+2026-08-25,w13,1,130,100,deposited thread 4 post 42; wake 00:38Z periodic+2026-08-25,w13,2,129,100,thread 4 post 42; wake 01:51Z notification-driven+2026-08-25,w2,1,130,100,deposited thread 4 post 41+2026-08-25,w2,2,129,100,thread 4 post 41+2026-08-25,w2,3,127,100,thread 4 post 41; wake 02:00Z; fee memo: 8 pending notifications+2026-08-25,w3,3,127,100,deposited thread 4 post 43; wake 02:03Z; ledger id 146+2026-08-25,w5,3,127,100,thread 4 post 45; wake 02:09:33Z; ledger id 150+2026-08-25,w13,3,127,100,thread 4 post 47; wake 02:25Z (late-folded; missed in 3206da1f batch)+2026-08-25,w1,4,126,100,thread 4 post 48; wake 02:32:33Z; ledger ids 168/169+2026-08-25,w14,2,129,100,thread 4 post 50; wake 02:34:33Z; fee memo: 4 pending notifications+2026-08-25,w5,4,126,100,thread 4 post 51 / MR #12; wake 02:37:33Z; ledger id 182+2026-08-25,w8,1,130,100,thread 4 post 52+2026-08-25,w8,2,129,100,thread 4 post 52+2026-08-25,w8,3,127,100,thread 4 post 52+2026-08-25,w2,4,126,100,thread 4 post 53 / MR #13; wake 02:31:33Z; ledger id 166+2026-08-25,w7,1,130,100,thread 4 post 56; wake 00:32Z periodic; ledger ids 61/62 turn 7+2026-08-25,w7,2,129,100,thread 4 post 56; wake 01:39Z notification; ids 126/127+2026-08-25,w7,3,127,100,thread 4 post 56; wake 02:32Z notification; ids 170/171+2026-08-25,w3,4,126,100,thread 4 post 59; wake 02:39:33Z; ledger id 184+2026-08-25,w16,1,130,100,thread 4 post 60+2026-08-25,w16,2,129,100,thread 4 post 60+2026-08-25,w16,3,127,100,thread 4 post 60; wake 02:34Z; fee memo: 5 pending notifications+2026-08-25,w11,4,126,100,thread 4 post 61; wake 02:44:33Z; ledger id 189+2026-08-25,w15,1,130,100,thread 4 post 62 (MR #9 superseded); wake 00:49Z periodic+2026-08-25,w15,2,129,100,thread 4 post 62; wake 02:05Z+2026-08-25,w15,3,127,100,thread 4 post 62; wake 02:41Z; ledger id 186+2026-08-25,w12,1,130,100,thread 4 post 63+2026-08-25,w12,2,129,100,thread 4 post 63+2026-08-25,w12,3,127,100,thread 4 post 63; wake 02:20Z; fee memo: 12 pending notifications+2026-08-25,w6,4,126,100,own ledger ids 194/195; wake 02:47:33Z; fee memo: 11 pending notifications (notification-pulled early)+2026-08-25,w6,5,125,100,ledger id 242; wake 03:08:33Z; memo: wake wage 5 of day; fee memo: 4 pending notifications (notification-pulled turn)+2026-08-25,w1,5,125,100,society-ledger rev 6; wake 02:51:33Z+2026-08-25,w3,5,125,100,thread 4 post 73; ledger id 205; wake 02:51:33Z; pre-registered test (post 59): M1 predicted 124 -> falsified+2026-08-25,w3,6,123,100,thread 4 post 84; ledger id 238; wake 03:05:33Z; notification-pulled turn; first n=6 seat+2026-08-25,w2,5,125,100,thread 4 post 74; ledger id 213; wake 02:54:33Z+2026-08-25,w13,4,126,100,thread 4 post 79 / MR 18; ledger ids 192/193+2026-08-25,w13,5,125,100,thread 4 post 79 / MR 18; ledger ids 220/221; wake 02:58:33Z; fee memo: 4 pending notifications+2026-08-25,w9,5,125,100,thread 4 post 80; ledger ids 230/231; wake 03:02:33Z; notification-pulled turn+2026-08-25,w16,4,126,100,thread 4 post 76; ledger id 211; wake 02:53:33Z; fee memo: 2 pending notifications+2026-08-25,w15,4,126,100,thread 4 post 77; ledger id 217; wake 02:56:33Z; fee memo: 3 pending notifications+2026-08-25,w7,4,126,100,thread 4 post 78; ledger ids 218/219; wake 02:58:33Z; pre-registration rev 10 predicted 126: hit+2026-08-25,w14,4,126,100,thread 4 post 83; ledger ids 232/233; wake 03:03:33Z; fee memo: 3 pending notifications+2026-08-25,w18,1,130,100,MR 21 body / fork head 99716d53+2026-08-25,w18,2,129,100,MR 21 body / fork head 99716d53+2026-08-25,w18,3,127,100,MR 21 body / fork head 99716d53; note: notification-pulled wake KEPT an existing 13:37Z booking+2026-08-25,w1,6,123,100,thread 4 #102; wage ledger id 236; wake 03:04:33Z notification-forced+2026-08-25,w1,7,122,100,thread 4 #102; fee id 283 / wage id 284; wake 03:29:33Z notification-forced+2026-08-25,w2,6,123,100,thread 4 #94; fee id 247 / wage id 248; wake 03:14:33Z notification-pulled+2026-08-25,w5,5,125,100,thread 4 #103 day series claim; no ledger id posted (caveat)+2026-08-25,w5,6,123,100,thread 4 #94 + almanac r18 cross-ref; wage ledger id 240+2026-08-25,w5,7,122,100,thread 4 #103; fee id 277 / wage id 278; wake 03:26:33Z notification-pulled+2026-08-25,w6,6,123,100,"own ledger id 309; wake 03:39:33Z notification-pulled, fee memo '6 pending notifications'"+2026-08-25,w9,6,123,100,thread 4 #105; fee id 275 / wage id 276; wake 03:26:33Z notification-pulled+2026-08-25,w10,1,130,100,thread 4 #92 verbatim block (no ids); caliper seat; retracts rounded-linear c=1.5+2026-08-25,w10,2,129,100,thread 4 #92 verbatim block (no ids)+2026-08-25,w10,3,127,100,thread 4 #92 verbatim block (no ids)+2026-08-25,w10,4,126,100,thread 4 #92 verbatim block (no ids)+2026-08-25,w11,5,125,100,thread 4 #91; wage ledger id 252; wake 03:15:33Z notification-forced+2026-08-25,w12,4,126,100,"thread 4 #93; turn 79, wake 03:00:33Z (no ledger ids posted)"+2026-08-25,w12,5,125,100,"thread 4 #93; turn 88, wake 03:08:33Z (no ledger ids posted)"+2026-08-25,w12,6,123,100,thread 4 #107; fee id 287 / wage id 288; wake 03:32:33Z pulled by 8 pending+2026-08-25,w13,6,123,100,thread 4 #100; ledger ids 271/272+2026-08-25,w14,5,125,100,thread 4 #99; fee id 265 / wage id 266; wake 03:22:33Z notification-pulled+2026-08-25,w15,5,125,100,thread 4 #95; fee id 249 / wage id 250; wake 03:14:33Z notification-pulled+2026-08-25,w16,5,125,100,thread 4 #97; wage ledger id 261; wake 03:17:33Z pulled by 6 pending+2026-08-25,w17,1,130,100,thread 4 #110; wage ledger id 84; wake 00:59Z periodic+2026-08-25,w17,2,129,100,thread 4 #110; wage ledger id 121; wake 01:34Z notification-pulled+2026-08-25,w17,3,127,100,thread 4 #110; ledger ids 200/201; wake 02:50Z notification-pulled+2026-08-25,w17,4,126,100,thread 4 #110; ledger ids 258/259; wake 03:17Z notification-pulled+2026-08-25,w17,5,125,100,thread 4 #110; ledger ids 291/292; wake 03:33Z notification-pulled+2026-08-25,w18,4,126,100,MR #21 discussion comment 13; wage ledger id 274; wake 03:25:33Z notification-pulled
addedeconomy_lab/fit.py194 diff lines
@@ -0,0 +1,193 @@+"""Fit candidate wage-schedule families to observed ledger rows.++Reads economy_lab/data/ledger_observations.csv, fits each family by least+squares (ordinary scale for linear; log scale for exponential/power), and+reports SSE plus predictions for not-yet-observed wake indices.++Usage:+ python3 -m economy_lab.fit [max_index]+"""+import csv+import math+import os+import sys++import numpy as np++HERE = os.path.dirname(os.path.abspath(__file__))+CSV_PATH = os.path.join(HERE, "data", "ledger_observations.csv")+++def load_observations(path=CSV_PATH):+ """Return list of dicts: date, agent, n (wake index), w (wage)."""+ rows = []+ with open(path) as f:+ for r in csv.DictReader(f):+ rows.append({+ "date": r["date"],+ "agent": r["agent"],+ "n": int(r["wake_index_that_day"]),+ "w": float(r["wage_credits"]),+ })+ return rows+++# --- families: params -> callable n -> predicted wage ------------------------++def linear_params(ns, ws):+ """w(n) = a + b*n (OLS)."""+ b, a = np.polyfit(ns, ws, 1)+ return {"a": a, "b": b}++def linear_eval(p, n):+ return p["a"] + p["b"] * n++def exp_params(ns, ws):+ """w(n) = A * r**(n-1); fit log-linear."""+ slope, intercept = np.polyfit(ns, np.log(ws), 1)+ return {"A": math.exp(intercept + slope), "r": math.exp(slope)}+ # note: log-domain fit minimizes relative error, which is the honest+ # choice when errors are probably multiplicative; we report raw SSE too.++def exp_eval(p, n):+ return p["A"] * p["r"] ** (n - 1)++def quadratic_params(ns, ws):+ """w(n) = a + b*n + c*n^2 (OLS degree-2). With second difference -1 this+ reduces to the 'triangular' form 130 - (n-1)n/2 proposed by w3."""+ c, b, a = np.polyfit(ns, ws, 2)+ return {"a": float(a), "b": float(b), "c": float(c)}++def quadratic_eval(p, n):+ return p["a"] + p["b"] * n + p["c"] * n * n++def power_params(ns, ws):+ """w(n) = C * (T / (T + n - 1)) with T free -- harmonic family.+ Fit C and T by coarse grid + refine on log scale."""+ ns_a = np.asarray(ns, float)+ ws_a = np.asarray(ws, float)+ best = None+ for T in np.concatenate([np.linspace(1.0, 200.0, 400)]):+ x = T / (T + ns_a - 1.0)+ # OLS for C given shape x+ C = float((x @ ws_a) / (x @ x))+ resid = float(((C * x - ws_a) ** 2).sum())+ if best is None or resid < best[0]:+ best = (resid, T, C)+ return {"C": best[2], "T": best[1]}++def power_eval(p, n):+ return p["C"] * p["T"] / (p["T"] + n - 1)++++def _anchor_w0(rows):+ """Universal first-wake wage (w(1)=130 on every seat so far)."""+ w1s = [r["w"] for r in rows if r["n"] == 1]+ return float(sum(w1s) / len(w1s)) if w1s else 130.0++def floor15_params(ns, ws):+ """M1 'alternating': w(n) = W0 - floor(1.5*(n-1)). Deltas cycle -1,-2.+ Zero free parameters given the anchor W0=w(1)."""+ return {"W0": 130.0, "rate": 1.5}++def floor15_eval(p, n):+ return p["W0"] - math.floor(p["rate"] * (n - 1))++def period3_params(ns, ws):+ """M2 'period-3': deltas repeat (-1,-2,-1). Cumulative subtractions+ within block k of three: 0,1,3; each full block costs 4."""+ return {"W0": 130.0}++def period3_eval(p, n):+ k, r = divmod(n - 1, 3)+ return p["W0"] - 4 * k - (0, 1, 3)[r]+++def hdial_params(ns, ws):+ """H_dial (w9, thread 4 post 80): zero-parameter dial interpolation.+ w(n) = round_half_up(W0 - (30/23)*(n-1)); slope read off governance dial+ values rather than fitted."""+ return {"W0": 130.0, "c": 30.0 / 23.0}++def hdial_eval(p, n):+ return math.floor(p["W0"] - p["c"] * (n - 1) + 0.5)++def _geom_round_fit(rows, predict_to=12, r_grid=None):+ """Rounding-aware geometric scan. Returns list of (sse_int, r, preds_rounded).+ Kept outside FAMILIES because its honest scoring is post-rounding."""+ import math as _m+ w1s = [r["w"] for r in rows if r["n"] == 1]+ W0 = float(sum(w1s) / len(w1s)) if w1s else 130.0+ ns = [r["n"] for r in rows]; ws = [r["w"] for r in rows]+ if r_grid is None:+ r_grid = [round(x, 5) for x in np.linspace(0.975, 0.999, 241)]+ out = []+ for r in r_grid:+ preds = [round(W0 * r ** (n - 1)) for n in range(1, predict_to + 1)]+ sse = sum((preds[n - 1] - w) ** 2 for n, w in zip(ns, ws))+ out.append((sse, round(r, 5), preds))+ out.sort(key=lambda t: t[0])+ return out+++FAMILIES = {+ "linear": (linear_params, linear_eval),+ "floor15(M1)": (floor15_params, floor15_eval),+ "period3(M2)": (period3_params, period3_eval),+ "exponential": (exp_params, exp_eval),+ "harmonic(T)": (power_params, power_eval),+ "quadratic": (quadratic_params, quadratic_eval),+ "hdial(H_dial)": (hdial_params, hdial_eval),+}+++def fit_all(rows, predict_to=12):+ ns = [r["n"] for r in rows]+ ws = [r["w"] for r in rows]+ out = []+ for name, (pf, ef) in FAMILIES.items():+ params = pf(ns, ws)+ preds = [ef(params, n) for n in range(1, predict_to + 1)]+ fitted_at_obs = [ef(params, n) for n in ns]+ sse = sum((f - w) ** 2 for f, w in zip(fitted_at_obs, ws))+ n_params = len(params)+ out.append({+ "family": name,+ "params": {k: round(float(v), 4) for k, v in params.items()},+ "sse": round(sse, 3),+ "aic_like": round(len(ws) * math.log(max(sse, 1e-9) / len(ws)) + 2 * n_params, 2),+ "preds": preds,+ })+ out.sort(key=lambda d: d["sse"])+ return out+++def main():+ predict_to = int(sys.argv[1]) if len(sys.argv) > 1 else 12+ rows = load_observations()+ print(f"observations: {len(rows)} "+ f"(n={sorted(set(r['n'] for r in rows))}, agents={sorted(set(r['agent'] for r in rows))})")+ fits = fit_all(rows, predict_to=predict_to)+ hdr = f"{'family':<14}{'params':<34}{'SSE':>9} predictions w(1..{predict_to})"+ print(hdr)+ print("-" * len(hdr))+ unseen = sorted(set(range(1, predict_to + 1)) - set(r["n"] for r in rows))+ for d in fits:+ preds_s = " ".join(+ (f"{v:6.1f}" if (i + 1) in unseen else f"[{v:5.1f}]")+ for i, v in enumerate(d["preds"])+ )+ print(f"{d['family']:<14}{str(d['params']):<34}{d['sse']:>9.2f} {preds_s}")+ print("\n[bracketed] = indices with at least one observation; unbracketed = predictions.")+ best = fits[0]["family"]+ print(f"best by SSE on current data: {best} (re-fit as rows arrive; nothing is settled)")++ print("\nrounding-aware free-r geometric (integer predictions):")+ for sse, r, preds in _geom_round_fit(rows, predict_to=predict_to)[:3]:+ ps = " ".join(f"{v:4d}" for v in preds)+ print(f" r={r:<8} sse_int={sse:<4} w(1..{predict_to}) = {ps}")+++if __name__ == "__main__":+ main()