Thibaut Forest Claude Fable 5 commited on
Commit ·
34c30a6
1
Parent(s): 4d3b32e
Phase 62: synthetic scaling study (1x/2x/4x, official-mechanism EOLs); count rule scales cleanly; fixed-ref vmin7 dial (default off); alpha-1.0 edge holds at every scale
Browse files- EXPERIMENTS.md +14 -0
- agent_reports_bgkf/artifacts/scale_run.py +147 -0
- agent_reports_bgkf/artifacts/synth.py +102 -0
- agent_reports_bgkf/c3_findings.md +41 -0
- batteryswap_example/submit.py +17 -0
EXPERIMENTS.md
CHANGED
|
@@ -3305,3 +3305,17 @@ zero catches) was on the OLD ordering; whether ft2's ordering converts
|
|
| 3305 |
extra appetite publicly is the one open question a public trigger can
|
| 3306 |
answer. PREPARED: commit 296f8f2 = alpha 1.0 on ft2 (single variable,
|
| 3307 |
selectable for public scoring); head 7d7632c keeps the safe 0.8.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3305 |
extra appetite publicly is the one open question a public trigger can
|
| 3306 |
answer. PREPARED: commit 296f8f2 = alpha 1.0 on ft2 (single variable,
|
| 3307 |
selectable for public scoring); head 7d7632c keeps the safe 0.8.
|
| 3308 |
+
|
| 3309 |
+
## Phase 62 - synthetic scaling study (user directive): CV tested on fleet-size scaling; fixed-reference vmin7 dial shipped
|
| 3310 |
+
|
| 3311 |
+
Synthetic fleets at 1x/2x/4x (461/922/1844 devices) from time-shifted
|
| 3312 |
+
building clones with OFFICIAL-mechanism-derived EOLs (0 mismatches on the
|
| 3313 |
+
checkable set at every scale); deployment-faithful static-path runs;
|
| 3314 |
+
anchor k=1 champ = 1762.5 ~= bgkf 1760.2 (same recall .471). Verdicts:
|
| 3315 |
+
count rule scales cleanly (swaps/death 0.98-1.07); live-rank vmin7_rank
|
| 3316 |
+
decays and inverts at 4x while a train-ECDF fixed-reference form is
|
| 3317 |
+
scale-neutral (shipped as BATTERYSWAP_FT2_FIXEDREF, default 0 - the Docker
|
| 3318 |
+
rehearsal measured 922-device splits = the safe 2x regime); alpha 1.0 best
|
| 3319 |
+
at EVERY scale with recall +7pts held (third independent support for the
|
| 3320 |
+
296f8f2 public A/B); runtime 3.4s/scenario at 4x - no budget risk. Full
|
| 3321 |
+
tables: agent_reports_bgkf/c3_findings.md.
|
agent_reports_bgkf/artifacts/scale_run.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Scaling evaluation: run an arm on a synthetic fleet at scale K.
|
| 2 |
+
|
| 3 |
+
Deployment-faithful: ONE static fit on the real packaged train table
|
| 4 |
+
(submit.py's held-out branch), then the 48 real scenario times planned on
|
| 5 |
+
the synthetic fleet and costed by the real evaluator with derived EOLs.
|
| 6 |
+
|
| 7 |
+
Arms: champ (plain logbag, alpha .8) | ft2 (frontier, alpha .8)
|
| 8 |
+
| ft2a10 (frontier, alpha 1.0)
|
| 9 |
+
Usage: scale_run.py K ARM
|
| 10 |
+
"""
|
| 11 |
+
import sys, time, pickle
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
import numpy as np
|
| 14 |
+
import pandas as pd
|
| 15 |
+
|
| 16 |
+
OURS = Path('/Users/THIF/repos/BatterySwapAI2026-Example')
|
| 17 |
+
sys.path.insert(0, str(OURS))
|
| 18 |
+
from batteryswap_public.utils import load_dataset, iterate_scenarios
|
| 19 |
+
from batteryswap_public.evaluate import evaluate_plan
|
| 20 |
+
from batteryswap_example.features import (
|
| 21 |
+
daily_voltage, daily_all, weekly_betas, master_curve, physics_features,
|
| 22 |
+
features_at_origin, WINDOW_DAYS, BETA_DEFAULT)
|
| 23 |
+
from batteryswap_example.fht_features import daily_corrected, wiener_at_origin
|
| 24 |
+
import batteryswap_example.models_cv as mc
|
| 25 |
+
import batteryswap_example.submit as sub
|
| 26 |
+
|
| 27 |
+
K = int(sys.argv[1])
|
| 28 |
+
ARM = sys.argv[2]
|
| 29 |
+
ALPHA = 1.0 if ARM.endswith('a10') else 0.8
|
| 30 |
+
if ARM == 'ft2fix':
|
| 31 |
+
ALPHA = 0.8
|
| 32 |
+
|
| 33 |
+
with open(f'/tmp/c5/fleet_k{K}.pkl', 'rb') as f:
|
| 34 |
+
FL = pickle.load(f)
|
| 35 |
+
locations = FL['locations']
|
| 36 |
+
for _c in ('start_time', 'end_time'):
|
| 37 |
+
if getattr(locations[_c].dt, 'tz', None) is not None:
|
| 38 |
+
locations[_c] = locations[_c].dt.tz_localize(None)
|
| 39 |
+
eol = FL['eol']
|
| 40 |
+
if getattr(eol.dt, 'tz', None) is not None:
|
| 41 |
+
eol = eol.dt.tz_localize(None)
|
| 42 |
+
travel = FL['travel']
|
| 43 |
+
ts = FL['ts'].set_index(['device_id', 'end_time']).sort_index()
|
| 44 |
+
|
| 45 |
+
_, _, eol_real, scenarios = load_dataset(OURS / 'dataset/train')
|
| 46 |
+
# fleet-wide EOL series shaped like load_dataset's (NaT for survivors);
|
| 47 |
+
# clone-0 devices take the OFFICIAL values (exact), shifted clones the
|
| 48 |
+
# telemetry-derived ones (0 mismatches on the checkable set). The 6 real
|
| 49 |
+
# deaths the derivation misses become permanent low-voltage "ghosts" in
|
| 50 |
+
# shifted clones - noted, and shared by every arm equally.
|
| 51 |
+
eol_syn = eol.reindex(locations['battery'])
|
| 52 |
+
eol_syn.index = locations['battery']
|
| 53 |
+
er = eol_real.copy()
|
| 54 |
+
if getattr(er.dt, 'tz', None) is not None:
|
| 55 |
+
er = er.dt.tz_localize(None)
|
| 56 |
+
c0 = eol_syn.index.intersection(er.index)
|
| 57 |
+
eol_syn.loc[c0] = er.reindex(c0)
|
| 58 |
+
# scenarios with the synthetic travel matrix
|
| 59 |
+
scenarios = [dict(sc, travel_costs=travel) for sc in scenarios]
|
| 60 |
+
GEN = list(iterate_scenarios(locations, ts, eol_syn, scenarios))
|
| 61 |
+
|
| 62 |
+
# ---- one static foreign fit (the deployment path) ----
|
| 63 |
+
base = pd.read_parquet(OURS / 'batteryswap_example/data/train_features.parquet')
|
| 64 |
+
base = base[base['window_closed']]
|
| 65 |
+
model = (mc.SklearnRUL('logbag') if ARM == 'champ' else sub.Ft2RUL()).fit(base)
|
| 66 |
+
if ARM == 'ft2fix':
|
| 67 |
+
# scale hardening: vmin7_rank as a FIXED-reference percentile (train
|
| 68 |
+
# ECDF of v_min_7) instead of a live-fleet rank - fleet-size invariant
|
| 69 |
+
_q = np.sort(base['v_min_7'].dropna().to_numpy())
|
| 70 |
+
_inner = model
|
| 71 |
+
class _Fix:
|
| 72 |
+
def predict(self, rows):
|
| 73 |
+
r = rows.copy()
|
| 74 |
+
v = r['v_min_7'].to_numpy(dtype=float)
|
| 75 |
+
pct = np.searchsorted(_q, v, side='right') / len(_q)
|
| 76 |
+
r['vmin7_rank'] = np.where(np.isfinite(v), pct, np.nan)
|
| 77 |
+
return _inner.predict(r)
|
| 78 |
+
model = _Fix()
|
| 79 |
+
|
| 80 |
+
# ---- per-fleet caches ----
|
| 81 |
+
t0 = time.time()
|
| 82 |
+
daily = daily_voltage(ts)
|
| 83 |
+
dall = daily_all(ts)
|
| 84 |
+
betas = weekly_betas(dall)
|
| 85 |
+
fd = daily_corrected(ts)
|
| 86 |
+
print(f'fleet caches {time.time()-t0:.0f}s', file=sys.stderr, flush=True)
|
| 87 |
+
|
| 88 |
+
p = mc.plan_tsroute
|
| 89 |
+
p.alpha = ALPHA
|
| 90 |
+
p.ec_margin, p.ec_pmax = 0.8, 25.0
|
| 91 |
+
p.split_days = 7
|
| 92 |
+
p.floor_scale, p.recent_deaths = 2.5, 0.0
|
| 93 |
+
|
| 94 |
+
W = pd.Timedelta(days=WINDOW_DAYS)
|
| 95 |
+
recs = []
|
| 96 |
+
for i, (sc, fleet, cut, eol_f) in enumerate(GEN):
|
| 97 |
+
T = pd.Timestamp(sc['start_time'])
|
| 98 |
+
tt = time.time()
|
| 99 |
+
feats = features_at_origin(daily, fleet, T)
|
| 100 |
+
past = betas[betas['week_end'] <= T]['beta']
|
| 101 |
+
beta = float(past.median()) if len(past) >= 20 else BETA_DEFAULT
|
| 102 |
+
dead = eol[eol.notna() & (eol <= T)]
|
| 103 |
+
curve = master_curve(dall[dall['day'] <= T], dead, beta)
|
| 104 |
+
feats = feats.join(physics_features(dall, T, beta, curve, match=False))
|
| 105 |
+
if ARM != 'champ':
|
| 106 |
+
feats = feats.join(wiener_at_origin(
|
| 107 |
+
fd, T.tz_localize(None) if T.tz is not None else T))
|
| 108 |
+
feats = feats.reindex(fleet['battery'])
|
| 109 |
+
t_feat = time.time() - tt
|
| 110 |
+
|
| 111 |
+
pred = model.predict(feats)
|
| 112 |
+
t_pred = time.time() - tt - t_feat
|
| 113 |
+
plan = mc.plan_tsroute(pred, None, fleet, T, 0.05,
|
| 114 |
+
travel_costs=travel, settings=sc['settings'])
|
| 115 |
+
t_plan = time.time() - tt - t_feat - t_pred
|
| 116 |
+
_, _, overall = evaluate_plan(plan, fleet, travel, sc['settings'],
|
| 117 |
+
eol_times=eol_f, start_time=T)
|
| 118 |
+
pday = plan['day']
|
| 119 |
+
if getattr(pday.dt, 'tz', None) is not None:
|
| 120 |
+
pday = pday.dt.tz_localize(None)
|
| 121 |
+
Tn = T.tz_localize(None) if T.tz is not None else T
|
| 122 |
+
inwin = plan[(pday <= Tn + W).values]
|
| 123 |
+
swap_day = inwin.groupby('battery')['day'].min()
|
| 124 |
+
dies = eol[(eol > Tn) & (eol <= Tn + W)
|
| 125 |
+
& eol.index.isin(set(fleet['battery']))]
|
| 126 |
+
caught = int(dies.index.isin(swap_day.index).sum())
|
| 127 |
+
recs.append(dict(overall, origin=i, n_swaps=int(inwin['battery'].nunique()),
|
| 128 |
+
deaths=len(dies), caught=caught,
|
| 129 |
+
t_feat=t_feat, t_pred=t_pred, t_plan=t_plan))
|
| 130 |
+
print(f's_{i}: {overall["total_cost"]:8.1f} deaths {len(dies):3d} '
|
| 131 |
+
f'caught {caught:3d} swaps {recs[-1]["n_swaps"]:3d} '
|
| 132 |
+
f'[f{t_feat:.1f} p{t_pred:.1f} pl{t_plan:.1f}s]',
|
| 133 |
+
file=sys.stderr, flush=True)
|
| 134 |
+
|
| 135 |
+
df = pd.DataFrame(recs)
|
| 136 |
+
df.to_csv(f'/tmp/c5/scale_k{K}_{ARM}.csv', index=False)
|
| 137 |
+
n_dev = locations['battery'].nunique()
|
| 138 |
+
print(f"\nK={K} ARM={ARM} devices={n_dev}")
|
| 139 |
+
print(f" mean total/scenario {df['total_cost'].mean():9.1f} "
|
| 140 |
+
f"per-DEVICE {df['total_cost'].mean()/n_dev:7.3f}")
|
| 141 |
+
print(f" swaps/scen {df['n_swaps'].mean():6.1f} deaths/scen "
|
| 142 |
+
f"{df['deaths'].mean():5.1f} recall {df['caught'].sum()/max(df['deaths'].sum(),1):.3f} "
|
| 143 |
+
f"swaps/death {df['n_swaps'].sum()/max(df['deaths'].sum(),1):.2f}")
|
| 144 |
+
print(f" late/scen {df['late_swap'].mean():7.1f} early/scen "
|
| 145 |
+
f"{df['early_swap'].mean():7.1f}")
|
| 146 |
+
print(f" sec/scenario: feat {df['t_feat'].mean():.1f} pred "
|
| 147 |
+
f"{df['t_pred'].mean():.1f} plan {df['t_plan'].mean():.1f}")
|
agent_reports_bgkf/artifacts/synth.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Synthetic fleet generator for the SCALING evaluation.
|
| 2 |
+
|
| 3 |
+
Fleet at scale k = k time-shifted clones of each real building. Each clone
|
| 4 |
+
building gets ONE shift (preserves within-building death correlation,
|
| 5 |
+
decorrelates clone sets). Ground-truth EOL is DERIVED from the shifted
|
| 6 |
+
telemetry with the official mechanism (7d-median < 2.40, validated 81/81
|
| 7 |
+
by the team repo) - labels stay mechanically consistent with the data.
|
| 8 |
+
Travel: distance between clones = distance between base buildings;
|
| 9 |
+
same-base clone pairs get the median off-diagonal hop.
|
| 10 |
+
|
| 11 |
+
Writes /tmp/c5/fleet_k{K}.pkl with: timeseries, locations, eol, travel fn.
|
| 12 |
+
"""
|
| 13 |
+
import sys, pickle, time
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
import numpy as np
|
| 16 |
+
import pandas as pd
|
| 17 |
+
|
| 18 |
+
TEAM = Path('/tmp/teamrepo')
|
| 19 |
+
OURS = Path('/Users/THIF/repos/BatterySwapAI2026-Example')
|
| 20 |
+
sys.path.insert(0, str(TEAM))
|
| 21 |
+
sys.path.insert(0, str(OURS))
|
| 22 |
+
from batteryswap_public.utils import load_dataset
|
| 23 |
+
from features import daily_panel, eol_from_panel
|
| 24 |
+
|
| 25 |
+
K = int(sys.argv[1]) if len(sys.argv) > 1 else 2
|
| 26 |
+
SEED = 7
|
| 27 |
+
|
| 28 |
+
locations, ts, eol_times, scenarios = load_dataset(OURS / 'dataset/train')
|
| 29 |
+
df = ts.reset_index()
|
| 30 |
+
df['device_id'] = df['device_id'].astype(str)
|
| 31 |
+
dev = pd.read_csv(OURS / 'dataset/train/devices.csv').drop_duplicates('device_id')
|
| 32 |
+
span_lo = df['end_time'].min()
|
| 33 |
+
span_hi = df['end_time'].max()
|
| 34 |
+
|
| 35 |
+
rng = np.random.default_rng(SEED)
|
| 36 |
+
frames, loc_rows = [], []
|
| 37 |
+
for c in range(K):
|
| 38 |
+
for b, sub in dev.groupby('building_id'):
|
| 39 |
+
# clone 0 of each building = the untouched original (scale anchor)
|
| 40 |
+
shift = 0 if c == 0 else int(rng.integers(-120, 121))
|
| 41 |
+
sb = b if c == 0 else f'{b}_c{c}'
|
| 42 |
+
for _, r in sub.iterrows():
|
| 43 |
+
sd = r['device_id'] if c == 0 else f"{r['device_id']}_c{c}"
|
| 44 |
+
d = df[df['device_id'] == r['device_id']].copy()
|
| 45 |
+
d['device_id'] = sd
|
| 46 |
+
d['end_time'] = d['end_time'] + pd.Timedelta(days=shift)
|
| 47 |
+
d = d[(d['end_time'] >= span_lo) & (d['end_time'] <= span_hi)]
|
| 48 |
+
if len(d) < 50:
|
| 49 |
+
continue
|
| 50 |
+
frames.append(d)
|
| 51 |
+
loc_rows.append(dict(battery=sd, building=sb,
|
| 52 |
+
room=(r['room_id'] if c == 0
|
| 53 |
+
else f"{r['room_id']}_c{c}"),
|
| 54 |
+
base_building=b,
|
| 55 |
+
start_time=pd.Timestamp(r['start_time'])
|
| 56 |
+
+ pd.Timedelta(days=shift),
|
| 57 |
+
end_time=pd.Timestamp(r['end_time'])
|
| 58 |
+
+ pd.Timedelta(days=shift)))
|
| 59 |
+
|
| 60 |
+
sts = pd.concat(frames, ignore_index=True)
|
| 61 |
+
locs = pd.DataFrame(loc_rows)
|
| 62 |
+
for col in ('start_time', 'end_time'):
|
| 63 |
+
locs[col] = pd.to_datetime(locs[col], utc=True)
|
| 64 |
+
|
| 65 |
+
t0 = time.time()
|
| 66 |
+
panel = daily_panel(sts.set_index(['device_id', 'end_time']))
|
| 67 |
+
eol = eol_from_panel(panel)
|
| 68 |
+
print(f'k={K}: {locs.battery.nunique()} devices, '
|
| 69 |
+
f'{locs.building.nunique()} buildings, {len(eol)} EOLs '
|
| 70 |
+
f'(panel {time.time()-t0:.0f}s)')
|
| 71 |
+
|
| 72 |
+
# sanity: clone-0 EOLs must match the official file exactly
|
| 73 |
+
official = pd.read_csv(OURS / 'dataset/train/eol_times.csv')
|
| 74 |
+
col = [c for c in official.columns if c != 'device_id'][0]
|
| 75 |
+
truth = pd.to_datetime(official.set_index('device_id')[col]).dt.tz_localize(None)
|
| 76 |
+
c0 = eol[~eol.index.str.contains('_c')]
|
| 77 |
+
both = truth.dropna().index.intersection(c0.index)
|
| 78 |
+
mism = int((pd.to_datetime(c0.reindex(both)).dt.normalize()
|
| 79 |
+
!= truth.reindex(both).dt.normalize()).sum())
|
| 80 |
+
print(f'clone-0 EOL check: {len(both)} deaths, {mism} mismatches')
|
| 81 |
+
|
| 82 |
+
# travel-cost template: base 24x24 matrix from scenario 0
|
| 83 |
+
tc0 = scenarios[0]['travel_costs']
|
| 84 |
+
base_h = tc0.set_index(['from', 'to'])['hours']
|
| 85 |
+
med_hop = tc0.loc[tc0['from'] != tc0['to'], 'hours'].median()
|
| 86 |
+
blds = sorted(locs['building'].unique())
|
| 87 |
+
base_of = locs.drop_duplicates('building').set_index('building')['base_building']
|
| 88 |
+
rows = []
|
| 89 |
+
for i in blds:
|
| 90 |
+
for j in blds:
|
| 91 |
+
if i == j:
|
| 92 |
+
h = 0.0
|
| 93 |
+
else:
|
| 94 |
+
bi, bj = base_of[i], base_of[j]
|
| 95 |
+
h = med_hop if bi == bj else float(base_h.get((bi, bj), med_hop))
|
| 96 |
+
rows.append(dict(**{'from': i, 'to': j}, hours=h))
|
| 97 |
+
travel = pd.DataFrame(rows)
|
| 98 |
+
|
| 99 |
+
with open(f'/tmp/c5/fleet_k{K}.pkl', 'wb') as f:
|
| 100 |
+
pickle.dump(dict(ts=sts, locations=locs, eol=pd.to_datetime(eol),
|
| 101 |
+
travel=travel), f)
|
| 102 |
+
print(f'wrote /tmp/c5/fleet_k{K}.pkl')
|
agent_reports_bgkf/c3_findings.md
CHANGED
|
@@ -167,3 +167,44 @@ trigger can answer it. Prepared as a one-click experiment:
|
|
| 167 |
**commit 296f8f2 = alpha 1.0 on the ft2 frontier** (single variable);
|
| 168 |
head 7d7632c stays at the safe 0.8. Pushes are scoring-inert - 296f8f2
|
| 169 |
only runs if the user triggers it.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
**commit 296f8f2 = alpha 1.0 on the ft2 frontier** (single variable);
|
| 168 |
head 7d7632c stays at the safe 0.8. Pushes are scoring-inert - 296f8f2
|
| 169 |
only runs if the user triggers it.
|
| 170 |
+
|
| 171 |
+
## SCALING EVALUATION (user directive, phase 62): synthetic fleets 1x/2x/4x
|
| 172 |
+
|
| 173 |
+
Construction: fleets of 461/922/1844 devices (24/48/96 buildings) = time-
|
| 174 |
+
shifted clones of real buildings (one shift per clone building; clone-0 =
|
| 175 |
+
the untouched real fleet); ground-truth EOLs DERIVED from the shifted
|
| 176 |
+
telemetry with the official label mechanism (clone-0 check: 0 mismatches
|
| 177 |
+
at every scale). Deployment-faithful runs: one static fit on the shipped
|
| 178 |
+
train table, submit.py's per-scenario feature path, real evaluator,
|
| 179 |
+
synthetic travel matrix. ANCHOR: k=1 champ = 1762.5/scenario ~= the bgkf
|
| 180 |
+
baseline 1760.2 with the same recall (.471) - the instrument calibrates.
|
| 181 |
+
Clone bias affects levels, cancels across scales and arms.
|
| 182 |
+
|
| 183 |
+
| per-device cost | k=1 | k=2 | k=4 | recall k1->k4 |
|
| 184 |
+
|---|---|---|---|---|
|
| 185 |
+
| champ (logbag, a.8) | 3.823 | 3.891 | 4.438 | .471 -> .453 |
|
| 186 |
+
| ft2 (live-rank) | 3.713 | 3.874 | 4.506 | .489 -> .448 |
|
| 187 |
+
| ft2 FIXED-REF vmin7 | 3.736 | 3.848 | 4.443 | .491 -> .457 |
|
| 188 |
+
| ft2 + alpha 1.0 | 3.523 | 3.569 | 4.020 | .545 -> .522 |
|
| 189 |
+
|
| 190 |
+
FINDINGS:
|
| 191 |
+
1. COUNT RULE SCALES CLEANLY: swaps/death 0.98-1.07 at every scale for the
|
| 192 |
+
alpha-.8 arms - sum(p42) tracks prevalence linearly. No calibration
|
| 193 |
+
risk at large fleets.
|
| 194 |
+
2. LIVE-RANK vmin7 DECAYS with fleet size and inverts at 4x. Fixed-
|
| 195 |
+
reference percentile (train ECDF of v_min_7) removes the decay; on the
|
| 196 |
+
standard bgkf it costs +2.1 +/- 14.4 (free at the mean, 8W/27L slightly
|
| 197 |
+
sign-worse on small fleets). SHIPPED AS A DIAL:
|
| 198 |
+
BATTERYSWAP_FT2_FIXEDREF (default 0). The Docker rehearsal MEASURED the
|
| 199 |
+
real splits at 922 devices (= k=2, where live-rank is still fine), so
|
| 200 |
+
the dial stays off; flip it if fleets turn out much larger. (The
|
| 201 |
+
"4000 total" figure is inconsistent with the rehearsal's observation.)
|
| 202 |
+
3. ALPHA 1.0 IS THE BEST ARM AT EVERY SCALE and its edge GROWS absolutely
|
| 203 |
+
with fleet size (recall +7pts held at 4x; swaps/death stable 1.15-1.22).
|
| 204 |
+
Third independent line (bgkf mean, podium operating points, synthetic
|
| 205 |
+
scaling) behind the 296f8f2 public A/B.
|
| 206 |
+
4. RUNTIME IS A NON-ISSUE: 3.4s/scenario at 1844 devices (feat 0.7 + plan
|
| 207 |
+
2.0-2.7) ~= 3 min/split even at 4x fleets, far inside the 20-min budget.
|
| 208 |
+
|
| 209 |
+
Artifacts: /tmp/c5/ (generator synth.py, runner scale_run.py, fleets,
|
| 210 |
+
per-arm CSVs, /tmp/cv_bgkfcand_ft2fix.csv).
|
batteryswap_example/submit.py
CHANGED
|
@@ -212,6 +212,16 @@ class FrontierRUL:
|
|
| 212 |
# any fit error falls back to the legacy champion via _fit_model.
|
| 213 |
FT2 = os.environ.get('BATTERYSWAP_FT2', '1') != '0'
|
| 214 |
FT2_W = float(os.environ.get('BATTERYSWAP_FT2_W', '0.20'))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
|
| 216 |
|
| 217 |
class Ft2RUL:
|
|
@@ -239,9 +249,16 @@ class Ft2RUL:
|
|
| 239 |
self.primary = SklearnRUL(MODEL_KIND).fit(train)
|
| 240 |
self.wiener = (models_cv.WienerFHT().fit(train)
|
| 241 |
if FT2_W > 0 and 'wnr_n' in train.columns else None)
|
|
|
|
|
|
|
| 242 |
return self
|
| 243 |
|
| 244 |
def predict(self, rows):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
if 'vmin7_rank' not in rows.columns:
|
| 246 |
rows = rows.copy()
|
| 247 |
rows['vmin7_rank'] = (rows['v_min_7'].rank(pct=True)
|
|
|
|
| 212 |
# any fit error falls back to the legacy champion via _fit_model.
|
| 213 |
FT2 = os.environ.get('BATTERYSWAP_FT2', '1') != '0'
|
| 214 |
FT2_W = float(os.environ.get('BATTERYSWAP_FT2_W', '0.20'))
|
| 215 |
+
# Scale hardening (phase 62 synthetic scaling study): vmin7_rank as a
|
| 216 |
+
# FIXED-reference percentile (train ECDF of v_min_7) instead of the scored
|
| 217 |
+
# fleet's live rank. Live-rank ft2's edge decays with fleet size and
|
| 218 |
+
# inverts at 4x (per-device 4.506 vs champ 4.438 at 1844 devices); the
|
| 219 |
+
# fixed form is scale-neutral (4.443) and best at 2x (3.848). Cost on the
|
| 220 |
+
# standard bgkf instrument: +2.1 +/- 14.4 (free at the mean, slightly
|
| 221 |
+
# sign-worse on small fleets). Docker rehearsal measured the real splits
|
| 222 |
+
# at 922 devices (= the 2x regime, where live-rank is fine), so this ships
|
| 223 |
+
# OFF - flip to 1 if the scored fleets turn out much larger than 922.
|
| 224 |
+
FT2_FIXEDREF = os.environ.get('BATTERYSWAP_FT2_FIXEDREF', '0') != '0'
|
| 225 |
|
| 226 |
|
| 227 |
class Ft2RUL:
|
|
|
|
| 249 |
self.primary = SklearnRUL(MODEL_KIND).fit(train)
|
| 250 |
self.wiener = (models_cv.WienerFHT().fit(train)
|
| 251 |
if FT2_W > 0 and 'wnr_n' in train.columns else None)
|
| 252 |
+
self._ecdf = (np.sort(train['v_min_7'].dropna().to_numpy())
|
| 253 |
+
if FT2_FIXEDREF and 'v_min_7' in train.columns else None)
|
| 254 |
return self
|
| 255 |
|
| 256 |
def predict(self, rows):
|
| 257 |
+
if self._ecdf is not None and 'v_min_7' in rows.columns:
|
| 258 |
+
rows = rows.copy()
|
| 259 |
+
v = rows['v_min_7'].to_numpy(dtype=float)
|
| 260 |
+
pct = np.searchsorted(self._ecdf, v, side='right') / len(self._ecdf)
|
| 261 |
+
rows['vmin7_rank'] = np.where(np.isfinite(v), pct, np.nan)
|
| 262 |
if 'vmin7_rank' not in rows.columns:
|
| 263 |
rows = rows.copy()
|
| 264 |
rows['vmin7_rank'] = (rows['v_min_7'].rank(pct=True)
|