Thibaut Forest
Claude Fable 5
Phase 34-35: retire cross-fleet CV, re-screen 72 configs, champion unchanged
7d93fb8 Download batteryswap_example/planner.py from thibautforest/BatterySwapAI2026-Example: direct link, hf CLI and curl.
- Browser
- Download file 6.95 kB
-
https://huggingface.co/thibautforest/BatterySwapAI2026-Example/resolve/main/batteryswap_example/planner.py
- Command line
-
hf download hf://thibautforest/BatterySwapAI2026-Example/batteryswap_example/planner.py
-
curl -L -o planner.py https://huggingface.co/thibautforest/BatterySwapAI2026-Example/resolve/main/batteryswap_example/planner.py
6.95 kB
| """ | |
| Static pickled planner: logistic RUL model + tsroute planning (topsum inclusion, | |
| expected-cost day placement, building-group day search). | |
| train_full() fits it on every closed-window (device, origin) pair and main() | |
| writes planners/best.pickle - the per-split fallback submit.py uses when the | |
| adaptive path fails. Planner knobs mirror the phase-11/14 champion settings | |
| (alpha=0.8, ec_margin=0.8, ec_pmax=25, split_days=7; see models_cv.py and | |
| EXPERIMENTS.md). TopSumPlanner below is the retired v1 champion, kept for | |
| reference and so older pickles that recorded it stay loadable. | |
| Everything the planner needs at plan() time is computed from the arguments the | |
| competition passes it: the truncated timeseries, the locations table (which carries | |
| each device's install date), and the scenario settings. | |
| """ | |
| import pickle | |
| import pandas as pd | |
| from batteryswap_public.interfaces import Planner | |
| from batteryswap_example.features import ( | |
| daily_voltage, features_at_origin, load as load_features, | |
| daily_all, weekly_betas, master_curve, physics_features, BETA_DEFAULT) | |
| from batteryswap_example.models_cv import SklearnRUL | |
| class TopSumPlanner(Planner): | |
| """ | |
| Retired v1 champion (phase 1): adaptive top-sum inclusion + building | |
| batching. Walk-forward CV at selection time: test block s_24..s_41 mean | |
| total_cost 1368 vs voltage-15 2034, defer-all 2512, oracle 186. | |
| Superseded by ECRoutePlanner (tsroute); see EXPERIMENTS.md. | |
| """ | |
| def __init__(self, model: SklearnRUL, alpha: float = 0.8, day_q: float = 0.05, | |
| max_groups_per_day: int = 2): | |
| self.model = model | |
| self.alpha = alpha | |
| self.day_q = day_q | |
| self.max_groups_per_day = max_groups_per_day | |
| def plan(self, battery_data, locations, travel_costs, settings): | |
| window = int(settings.planning_window_days) | |
| origin = battery_data.reset_index()['end_time'].max().normalize() | |
| daily = daily_voltage(battery_data) | |
| feats = features_at_origin(daily, locations, origin) | |
| feats = feats.reindex(locations['battery']) | |
| pred = self.model.predict(feats) | |
| # adaptive inclusion: expected death count tracks prevalence even when the | |
| # probability scale drifts between refits, unlike a fixed threshold | |
| n = int(round(self.alpha * pred['p42'].sum())) | |
| chosen = pred['p42'].nlargest(n).index | |
| offs = pred.loc[chosen, f'day_q{self.day_q}'].round().clip(0, window) | |
| # batching: one visit per building at the group's earliest predicted day, | |
| # capped groups per day (travel is expensive, early days are cheap) | |
| if len(offs): | |
| bldg = locations.set_index('battery')['building'].reindex(offs.index) | |
| group_day = offs.groupby(bldg).min().sort_values() | |
| used, final = {}, {} | |
| for building, day in group_day.items(): | |
| d = int(day) | |
| while used.get(d, 0) >= self.max_groups_per_day and d < window: | |
| d += 1 | |
| used[d] = used.get(d, 0) + 1 | |
| final[building] = d | |
| offs = bldg.map(final).astype(float) | |
| parked_day = origin + pd.Timedelta(days=window + 1) | |
| scheduled = set(offs.index) | |
| parked = [b for b in locations['battery'] if b not in scheduled] | |
| plan = pd.concat([ | |
| pd.DataFrame({'day': origin + pd.to_timedelta(offs.values, unit='D'), | |
| 'battery': offs.index}), | |
| pd.DataFrame({'day': parked_day, 'battery': parked}), | |
| ], ignore_index=True) | |
| return plan.sort_values('day', kind='stable').reset_index(drop=True) | |
| class ECRoutePlanner(Planner): | |
| """ | |
| The pickled fallback planner. Despite the legacy name it calls plan_tsroute: | |
| topsum inclusion (N = alpha * sum(p42)) plus the phase-11 break-even top-up, | |
| expected-cost day placement, and local search over building-group days | |
| against an exact replica of the evaluator's travel/overtime/limit mechanics. | |
| The name dates from the pure expected-cost include/park selection | |
| (plan_ecroute, combined dev+test mean 1816 vs previous champion 1901), | |
| superseded by tsroute in phases 6-14; kept so existing pickles load. | |
| """ | |
| def __init__(self, model: SklearnRUL, beta: float = BETA_DEFAULT, curve=None): | |
| self.model = model | |
| # physics context baked in at fit time: the planner receives no EOL data, | |
| # so the master discharge curve and temperature coefficient travel with it | |
| self.beta = beta | |
| self.curve = curve | |
| def plan(self, battery_data, locations, travel_costs, settings): | |
| from batteryswap_example.models_cv import plan_tsroute | |
| origin = battery_data.reset_index()['end_time'].max().normalize() | |
| daily = daily_voltage(battery_data) | |
| feats = features_at_origin(daily, locations, origin) | |
| phys = physics_features(daily_all(battery_data), origin, self.beta, self.curve) | |
| feats = feats.join(phys).reindex(locations['battery']) | |
| pred = self.model.predict(feats) | |
| plan_tsroute.alpha = 0.8 | |
| plan_tsroute.ec_margin = 0.8 # phase-11 break-even top-up | |
| plan_tsroute.ec_pmax = 25.0 | |
| plan_tsroute.split_days = 7 # phase-14 building day-splitting | |
| return plan_tsroute(pred, None, locations, origin, 0.05, | |
| travel_costs=travel_costs, settings=settings) | |
| def train_full(dataset_path='dataset'): | |
| """Refit the selected model on every fully observed (device, origin) pair.""" | |
| from pathlib import Path | |
| from batteryswap_public.utils import load_dataset | |
| table = load_features(Path(dataset_path)) | |
| train = table[table['window_closed']] | |
| model = SklearnRUL('logistic').fit(train) | |
| # physics context from the full train split | |
| _, timeseries, eol_times, _ = load_dataset(Path(dataset_path) / 'train') | |
| dall = daily_all(timeseries) | |
| betas = weekly_betas(dall)['beta'] | |
| beta = float(betas.median()) if len(betas) >= 20 else BETA_DEFAULT | |
| curve = master_curve(dall, eol_times.dropna(), beta) | |
| print(f'refit on {len(train)} pairs, {train.dies42.sum()} positive, ' | |
| f'origins s_0..s_{train.origin_idx.max()}, beta={beta:.4f}, ' | |
| f'curve={"yes" if curve is not None else "no"}') | |
| return ECRoutePlanner(model, beta=beta, curve=curve) | |
| def main(): | |
| # When run as a script/-m, classes defined here would pickle under __main__ and | |
| # be unloadable by script.py. Rebuild the instance from the imported module so | |
| # the pickle records the importable path. | |
| import os | |
| import batteryswap_example.planner as this_module | |
| dataset_path = os.environ.get('BATTERYSWAP_DATASET_PATH', 'dataset') | |
| planner = this_module.train_full(dataset_path) | |
| path = 'batteryswap_example/planners/best.pickle' | |
| with open(path, 'wb') as f: | |
| pickle.dump(planner, f) | |
| print('planner-save', path) | |
| if __name__ == '__main__': | |
| main() | |