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
4.34 kB
"""Synthetic fleet generator for the SCALING evaluation.
Fleet at scale k = k time-shifted clones of each real building. Each clone
building gets ONE shift (preserves within-building death correlation,
decorrelates clone sets). Ground-truth EOL is DERIVED from the shifted
telemetry with the official mechanism (7d-median < 2.40, validated 81/81
by the team repo) - labels stay mechanically consistent with the data.
Travel: distance between clones = distance between base buildings;
same-base clone pairs get the median off-diagonal hop.
Writes /tmp/c5/fleet_k{K}.pkl with: timeseries, locations, eol, travel fn.
"""
import sys, pickle, time
from pathlib import Path
import numpy as np
import pandas as pd
TEAM = Path('/tmp/teamrepo')
OURS = Path('/Users/THIF/repos/BatterySwapAI2026-Example')
sys.path.insert(0, str(TEAM))
sys.path.insert(0, str(OURS))
from batteryswap_public.utils import load_dataset
from features import daily_panel, eol_from_panel
K = int(sys.argv[1]) if len(sys.argv) > 1 else 2
SEED = 7
locations, ts, eol_times, scenarios = load_dataset(OURS / 'dataset/train')
df = ts.reset_index()
df['device_id'] = df['device_id'].astype(str)
dev = pd.read_csv(OURS / 'dataset/train/devices.csv').drop_duplicates('device_id')
span_lo = df['end_time'].min()
span_hi = df['end_time'].max()
rng = np.random.default_rng(SEED)
frames, loc_rows = [], []
for c in range(K):
for b, sub in dev.groupby('building_id'):
# clone 0 of each building = the untouched original (scale anchor)
shift = 0 if c == 0 else int(rng.integers(-120, 121))
sb = b if c == 0 else f'{b}_c{c}'
for _, r in sub.iterrows():
sd = r['device_id'] if c == 0 else f"{r['device_id']}_c{c}"
d = df[df['device_id'] == r['device_id']].copy()
d['device_id'] = sd
d['end_time'] = d['end_time'] + pd.Timedelta(days=shift)
d = d[(d['end_time'] >= span_lo) & (d['end_time'] <= span_hi)]
if len(d) < 50:
continue
frames.append(d)
loc_rows.append(dict(battery=sd, building=sb,
room=(r['room_id'] if c == 0
else f"{r['room_id']}_c{c}"),
base_building=b,
start_time=pd.Timestamp(r['start_time'])
+ pd.Timedelta(days=shift),
end_time=pd.Timestamp(r['end_time'])
+ pd.Timedelta(days=shift)))
sts = pd.concat(frames, ignore_index=True)
locs = pd.DataFrame(loc_rows)
for col in ('start_time', 'end_time'):
locs[col] = pd.to_datetime(locs[col], utc=True)
t0 = time.time()
panel = daily_panel(sts.set_index(['device_id', 'end_time']))
eol = eol_from_panel(panel)
print(f'k={K}: {locs.battery.nunique()} devices, '
f'{locs.building.nunique()} buildings, {len(eol)} EOLs '
f'(panel {time.time()-t0:.0f}s)')
# sanity: clone-0 EOLs must match the official file exactly
official = pd.read_csv(OURS / 'dataset/train/eol_times.csv')
col = [c for c in official.columns if c != 'device_id'][0]
truth = pd.to_datetime(official.set_index('device_id')[col]).dt.tz_localize(None)
c0 = eol[~eol.index.str.contains('_c')]
both = truth.dropna().index.intersection(c0.index)
mism = int((pd.to_datetime(c0.reindex(both)).dt.normalize()
!= truth.reindex(both).dt.normalize()).sum())
print(f'clone-0 EOL check: {len(both)} deaths, {mism} mismatches')
# travel-cost template: base 24x24 matrix from scenario 0
tc0 = scenarios[0]['travel_costs']
base_h = tc0.set_index(['from', 'to'])['hours']
med_hop = tc0.loc[tc0['from'] != tc0['to'], 'hours'].median()
blds = sorted(locs['building'].unique())
base_of = locs.drop_duplicates('building').set_index('building')['base_building']
rows = []
for i in blds:
for j in blds:
if i == j:
h = 0.0
else:
bi, bj = base_of[i], base_of[j]
h = med_hop if bi == bj else float(base_h.get((bi, bj), med_hop))
rows.append(dict(**{'from': i, 'to': j}, hours=h))
travel = pd.DataFrame(rows)
with open(f'/tmp/c5/fleet_k{K}.pkl', 'wb') as f:
pickle.dump(dict(ts=sts, locations=locs, eol=pd.to_datetime(eol),
travel=travel), f)
print(f'wrote /tmp/c5/fleet_k{K}.pkl')