Thibaut Forest
Claude Fable 5
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
34c30a6
Raw History Blame Contribute Delete
6.32 kB
"""Scaling evaluation: run an arm on a synthetic fleet at scale K.
Deployment-faithful: ONE static fit on the real packaged train table
(submit.py's held-out branch), then the 48 real scenario times planned on
the synthetic fleet and costed by the real evaluator with derived EOLs.
Arms: champ (plain logbag, alpha .8) | ft2 (frontier, alpha .8)
| ft2a10 (frontier, alpha 1.0)
Usage: scale_run.py K ARM
"""
import sys, time, pickle
from pathlib import Path
import numpy as np
import pandas as pd
OURS = Path('/Users/THIF/repos/BatterySwapAI2026-Example')
sys.path.insert(0, str(OURS))
from batteryswap_public.utils import load_dataset, iterate_scenarios
from batteryswap_public.evaluate import evaluate_plan
from batteryswap_example.features import (
daily_voltage, daily_all, weekly_betas, master_curve, physics_features,
features_at_origin, WINDOW_DAYS, BETA_DEFAULT)
from batteryswap_example.fht_features import daily_corrected, wiener_at_origin
import batteryswap_example.models_cv as mc
import batteryswap_example.submit as sub
K = int(sys.argv[1])
ARM = sys.argv[2]
ALPHA = 1.0 if ARM.endswith('a10') else 0.8
if ARM == 'ft2fix':
ALPHA = 0.8
with open(f'/tmp/c5/fleet_k{K}.pkl', 'rb') as f:
FL = pickle.load(f)
locations = FL['locations']
for _c in ('start_time', 'end_time'):
if getattr(locations[_c].dt, 'tz', None) is not None:
locations[_c] = locations[_c].dt.tz_localize(None)
eol = FL['eol']
if getattr(eol.dt, 'tz', None) is not None:
eol = eol.dt.tz_localize(None)
travel = FL['travel']
ts = FL['ts'].set_index(['device_id', 'end_time']).sort_index()
_, _, eol_real, scenarios = load_dataset(OURS / 'dataset/train')
# fleet-wide EOL series shaped like load_dataset's (NaT for survivors);
# clone-0 devices take the OFFICIAL values (exact), shifted clones the
# telemetry-derived ones (0 mismatches on the checkable set). The 6 real
# deaths the derivation misses become permanent low-voltage "ghosts" in
# shifted clones - noted, and shared by every arm equally.
eol_syn = eol.reindex(locations['battery'])
eol_syn.index = locations['battery']
er = eol_real.copy()
if getattr(er.dt, 'tz', None) is not None:
er = er.dt.tz_localize(None)
c0 = eol_syn.index.intersection(er.index)
eol_syn.loc[c0] = er.reindex(c0)
# scenarios with the synthetic travel matrix
scenarios = [dict(sc, travel_costs=travel) for sc in scenarios]
GEN = list(iterate_scenarios(locations, ts, eol_syn, scenarios))
# ---- one static foreign fit (the deployment path) ----
base = pd.read_parquet(OURS / 'batteryswap_example/data/train_features.parquet')
base = base[base['window_closed']]
model = (mc.SklearnRUL('logbag') if ARM == 'champ' else sub.Ft2RUL()).fit(base)
if ARM == 'ft2fix':
# scale hardening: vmin7_rank as a FIXED-reference percentile (train
# ECDF of v_min_7) instead of a live-fleet rank - fleet-size invariant
_q = np.sort(base['v_min_7'].dropna().to_numpy())
_inner = model
class _Fix:
def predict(self, rows):
r = rows.copy()
v = r['v_min_7'].to_numpy(dtype=float)
pct = np.searchsorted(_q, v, side='right') / len(_q)
r['vmin7_rank'] = np.where(np.isfinite(v), pct, np.nan)
return _inner.predict(r)
model = _Fix()
# ---- per-fleet caches ----
t0 = time.time()
daily = daily_voltage(ts)
dall = daily_all(ts)
betas = weekly_betas(dall)
fd = daily_corrected(ts)
print(f'fleet caches {time.time()-t0:.0f}s', file=sys.stderr, flush=True)
p = mc.plan_tsroute
p.alpha = ALPHA
p.ec_margin, p.ec_pmax = 0.8, 25.0
p.split_days = 7
p.floor_scale, p.recent_deaths = 2.5, 0.0
W = pd.Timedelta(days=WINDOW_DAYS)
recs = []
for i, (sc, fleet, cut, eol_f) in enumerate(GEN):
T = pd.Timestamp(sc['start_time'])
tt = time.time()
feats = features_at_origin(daily, fleet, T)
past = betas[betas['week_end'] <= T]['beta']
beta = float(past.median()) if len(past) >= 20 else BETA_DEFAULT
dead = eol[eol.notna() & (eol <= T)]
curve = master_curve(dall[dall['day'] <= T], dead, beta)
feats = feats.join(physics_features(dall, T, beta, curve, match=False))
if ARM != 'champ':
feats = feats.join(wiener_at_origin(
fd, T.tz_localize(None) if T.tz is not None else T))
feats = feats.reindex(fleet['battery'])
t_feat = time.time() - tt
pred = model.predict(feats)
t_pred = time.time() - tt - t_feat
plan = mc.plan_tsroute(pred, None, fleet, T, 0.05,
travel_costs=travel, settings=sc['settings'])
t_plan = time.time() - tt - t_feat - t_pred
_, _, overall = evaluate_plan(plan, fleet, travel, sc['settings'],
eol_times=eol_f, start_time=T)
pday = plan['day']
if getattr(pday.dt, 'tz', None) is not None:
pday = pday.dt.tz_localize(None)
Tn = T.tz_localize(None) if T.tz is not None else T
inwin = plan[(pday <= Tn + W).values]
swap_day = inwin.groupby('battery')['day'].min()
dies = eol[(eol > Tn) & (eol <= Tn + W)
& eol.index.isin(set(fleet['battery']))]
caught = int(dies.index.isin(swap_day.index).sum())
recs.append(dict(overall, origin=i, n_swaps=int(inwin['battery'].nunique()),
deaths=len(dies), caught=caught,
t_feat=t_feat, t_pred=t_pred, t_plan=t_plan))
print(f's_{i}: {overall["total_cost"]:8.1f} deaths {len(dies):3d} '
f'caught {caught:3d} swaps {recs[-1]["n_swaps"]:3d} '
f'[f{t_feat:.1f} p{t_pred:.1f} pl{t_plan:.1f}s]',
file=sys.stderr, flush=True)
df = pd.DataFrame(recs)
df.to_csv(f'/tmp/c5/scale_k{K}_{ARM}.csv', index=False)
n_dev = locations['battery'].nunique()
print(f"\nK={K} ARM={ARM} devices={n_dev}")
print(f" mean total/scenario {df['total_cost'].mean():9.1f} "
f"per-DEVICE {df['total_cost'].mean()/n_dev:7.3f}")
print(f" swaps/scen {df['n_swaps'].mean():6.1f} deaths/scen "
f"{df['deaths'].mean():5.1f} recall {df['caught'].sum()/max(df['deaths'].sum(),1):.3f} "
f"swaps/death {df['n_swaps'].sum()/max(df['deaths'].sum(),1):.2f}")
print(f" late/scen {df['late_swap'].mean():7.1f} early/scen "
f"{df['early_swap'].mean():7.1f}")
print(f" sec/scenario: feat {df['t_feat'].mean():.1f} pred "
f"{df['t_pred'].mean():.1f} plan {df['t_plan'].mean():.1f}")