File size: 6,946 Bytes
b89e779 7d93fb8 b89e779 7d93fb8 b89e779 fc9c286 7d93fb8 b89e779 7d93fb8 b89e779 fc9c286 7d93fb8 fc9c286 7d93fb8 fc9c286 4fb1dc8 2ba75d6 fc9c286 b89e779 fc9c286 b89e779 fc9c286 b89e779 fc9c286 b89e779 | 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 149 150 151 152 153 154 155 156 157 158 | """
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()
|