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}")