File size: 6,316 Bytes
34c30a6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | """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}")
|