ADOPT campaign-2 frontier ft_vmin7r_wnr20: BATTERYSWAP_FT2 default ON
Browse filesFt2RUL = logbag on FEATURES+vmin7_rank, 0.20 value-preserving WienerFHT
order blend (order remapped onto primary's own sorted p42 values; count/
pricing/timing untouched). bgkf v5: 1688.4 vs 1760.2 (-71.8 +/- 23.5,
3.05 sigma, Wilcoxon .037, swaps flat, count-clean; reproduced exactly by
3 lanes). Worst-case within-test-fleet vmin7 recompute still -60.8 (2.3
sigma). Deployment parity vs the CV frontier: bit-exact (max cell diff 0).
wnr_m/s2/n merged into the shipped train table; split-path wiener params
computed past-only from the scored split's own telemetry (0.1s/scenario).
Soft degradation: missing wnr -> pure vm7 order; fit error -> legacy
champion. BATTERYSWAP_FT2=0 reverts to the measured-1661.45 config.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""General composer on cv.run_bgkf: registers vm7 variant + custom aux kinds
|
| 2 |
+
in-process, then runs any (kind, variant, blend) combo. No repo edits.
|
| 3 |
+
Usage: compose.py <label> kind=<k> [variant=base] [blend=<k2>:<w>]"""
|
| 4 |
+
import sys, time
|
| 5 |
+
sys.path.insert(0, '/Users/THIF/repos/BatterySwapAI2026-Example')
|
| 6 |
+
import os
|
| 7 |
+
os.chdir('/Users/THIF/repos/BatterySwapAI2026-Example')
|
| 8 |
+
import numpy as np, pandas as pd
|
| 9 |
+
from batteryswap_example import models_cv as mc
|
| 10 |
+
from batteryswap_example import cv
|
| 11 |
+
|
| 12 |
+
label = sys.argv[1]
|
| 13 |
+
kind, variant, blend = 'logbag', 'base', None
|
| 14 |
+
for a in sys.argv[2:]:
|
| 15 |
+
k, v = a.split('=', 1)
|
| 16 |
+
if k == 'kind':
|
| 17 |
+
kind = v
|
| 18 |
+
elif k == 'variant':
|
| 19 |
+
variant = v
|
| 20 |
+
elif k == 'blend':
|
| 21 |
+
b, w = v.split(':')
|
| 22 |
+
blend = (b, float(w))
|
| 23 |
+
|
| 24 |
+
cv.VARIANTS['vm7'] = lambda t: cv.BASE_FEATURES + ['vmin7_rank']
|
| 25 |
+
mc.MODELS['aux_ho'] = lambda: mc.LogBagFeats(['hot_90', 'oob_90'])
|
| 26 |
+
mc.MODELS['aux_hot'] = lambda: mc.LogBagFeats(['hot_90'])
|
| 27 |
+
mc.MODELS['aux_oob'] = lambda: mc.LogBagFeats(['oob_90'])
|
| 28 |
+
mc.MODELS['aux_ho30'] = lambda: mc.LogBagFeats(['hot_90', 'oob_90'],
|
| 29 |
+
n_estimators=30)
|
| 30 |
+
mc.MODELS['aux_ho50'] = lambda: mc.LogBagFeats(['hot_90', 'oob_90'],
|
| 31 |
+
n_estimators=50)
|
| 32 |
+
|
| 33 |
+
t0 = time.time()
|
| 34 |
+
df = cv.run_bgkf(kind=kind, variant=variant, blend=blend)
|
| 35 |
+
df['cand'] = label
|
| 36 |
+
df.to_csv(f'/tmp/cv_bgkfcand_{label}.csv', index=False)
|
| 37 |
+
base = pd.read_csv('/tmp/cv_bgkf.csv').groupby('origin')[
|
| 38 |
+
['total_cost', 'n_swaps']].sum()
|
| 39 |
+
d = df.groupby('origin')[['total_cost', 'n_swaps']].sum()
|
| 40 |
+
delta = (d['total_cost'] - base['total_cost']).dropna()
|
| 41 |
+
se = delta.std() / np.sqrt(len(delta))
|
| 42 |
+
print(f'{label}: SCORE {d["total_cost"].mean():.1f} '
|
| 43 |
+
f'delta {delta.mean():+.1f} SE {se:.1f} '
|
| 44 |
+
f'{int((delta<0).sum())}W/{int((delta>0).sum())}L '
|
| 45 |
+
f'swaps {d["n_swaps"].mean():.1f} [{time.time()-t0:.0f}s]')
|
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""md-agent candidate models (rebuilt post-outage). Registered by run.py."""
|
| 2 |
+
import numpy as np
|
| 3 |
+
import pandas as pd
|
| 4 |
+
from sklearn.impute import SimpleImputer
|
| 5 |
+
from sklearn.preprocessing import StandardScaler, RobustScaler
|
| 6 |
+
from sklearn.pipeline import make_pipeline
|
| 7 |
+
from sklearn.linear_model import LogisticRegression
|
| 8 |
+
from sklearn.ensemble import BaggingClassifier, HistGradientBoostingClassifier
|
| 9 |
+
|
| 10 |
+
import batteryswap_example.models_cv as mc
|
| 11 |
+
|
| 12 |
+
# risk-direction of p42 in each feature (0 = unconstrained)
|
| 13 |
+
MONO = {
|
| 14 |
+
'v_last': -1, 'v_drop': +1, 'slope_30': -1, 'slope_90': -1,
|
| 15 |
+
'v_min_30': -1, 'staleness': +1, 'age_days': +1, 'slope_lastk': -1,
|
| 16 |
+
'drop_recent': +1, 'below_frac': +1, 'accel': -1, 'n_30': 0,
|
| 17 |
+
'tau_projected': -1,
|
| 18 |
+
'vrel': -1, 'vmin7_rank': -1, 'r_vlast': -1, 'r_tau': -1,
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class _DaysMixin:
|
| 23 |
+
def _fit_days(self, train):
|
| 24 |
+
dying = train[train['dies42'] == 1]
|
| 25 |
+
self.days = mc.DayQuantiles().fit(dying[mc.FEATURES], dying['days_to_eol'])
|
| 26 |
+
|
| 27 |
+
def _out(self, rows, p42):
|
| 28 |
+
out = self.days.predict(rows[mc.FEATURES])
|
| 29 |
+
out['p42'] = p42
|
| 30 |
+
return out
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class HGBMono(_DaysMixin):
|
| 34 |
+
"""HistGradientBoosting with monotonic constraints (risk-direction by name)."""
|
| 35 |
+
|
| 36 |
+
def __init__(self, **kw):
|
| 37 |
+
self.kw = dict(max_iter=200, max_depth=3, learning_rate=0.1,
|
| 38 |
+
min_samples_leaf=20, random_state=0)
|
| 39 |
+
self.kw.update(kw)
|
| 40 |
+
|
| 41 |
+
def fit(self, train):
|
| 42 |
+
feats = list(mc.FEATURES)
|
| 43 |
+
cst = [MONO.get(f, 0) for f in feats]
|
| 44 |
+
self.feats = feats
|
| 45 |
+
self.clf = HistGradientBoostingClassifier(monotonic_cst=cst, **self.kw)
|
| 46 |
+
self.clf.fit(train[feats], train['dies42'])
|
| 47 |
+
self._fit_days(train)
|
| 48 |
+
return self
|
| 49 |
+
|
| 50 |
+
def predict(self, rows):
|
| 51 |
+
return self._out(rows, self.clf.predict_proba(rows[self.feats])[:, 1])
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class MeanBlend(_DaysMixin):
|
| 55 |
+
"""Mean of logbag p42 and a partner model's p42."""
|
| 56 |
+
|
| 57 |
+
def __init__(self, partner, w=0.5):
|
| 58 |
+
self.partner_factory = partner
|
| 59 |
+
self.w = w
|
| 60 |
+
|
| 61 |
+
def fit(self, train):
|
| 62 |
+
self.a = mc.SklearnRUL('logbag').fit(train)
|
| 63 |
+
self.b = self.partner_factory().fit(train)
|
| 64 |
+
return self
|
| 65 |
+
|
| 66 |
+
def predict(self, rows):
|
| 67 |
+
out = self.a.predict(rows)
|
| 68 |
+
out['p42'] = ((1 - self.w) * out['p42']
|
| 69 |
+
+ self.w * self.b.predict(rows)['p42'])
|
| 70 |
+
return out
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
class MeanBlendCM(MeanBlend):
|
| 74 |
+
"""Mean blend, count-matched: p42 rescaled so sum(p42) equals logbag's."""
|
| 75 |
+
|
| 76 |
+
def predict(self, rows):
|
| 77 |
+
pa = self.a.predict(rows)
|
| 78 |
+
pb = self.b.predict(rows)['p42']
|
| 79 |
+
blend = (1 - self.w) * pa['p42'] + self.w * pb
|
| 80 |
+
s = blend.sum()
|
| 81 |
+
if s > 0:
|
| 82 |
+
blend = (blend * (pa['p42'].sum() / s)).clip(upper=1.0)
|
| 83 |
+
out = pa
|
| 84 |
+
out['p42'] = blend
|
| 85 |
+
return out
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
class OrderBlend3(_DaysMixin):
|
| 89 |
+
"""Value-preserving 3-way ORDER blend: logbag p42 values kept (count
|
| 90 |
+
machinery untouched), ordering = weighted mean of pct-ranks of
|
| 91 |
+
logbag / partner2 / partner3."""
|
| 92 |
+
|
| 93 |
+
def __init__(self, p2, w2, p3=None, w3=0.0):
|
| 94 |
+
self.p2f, self.w2 = p2, w2
|
| 95 |
+
self.p3f, self.w3 = p3, w3
|
| 96 |
+
|
| 97 |
+
def fit(self, train):
|
| 98 |
+
self.a = mc.SklearnRUL('logbag').fit(train)
|
| 99 |
+
self.b = self.p2f().fit(train)
|
| 100 |
+
self.c = self.p3f().fit(train) if self.p3f is not None else None
|
| 101 |
+
return self
|
| 102 |
+
|
| 103 |
+
def predict(self, rows):
|
| 104 |
+
out = self.a.predict(rows)
|
| 105 |
+
w1 = 1.0 - self.w2 - self.w3
|
| 106 |
+
r = (w1 * out['p42'].rank(pct=True)
|
| 107 |
+
+ self.w2 * self.b.predict(rows)['p42'].rank(pct=True))
|
| 108 |
+
if self.c is not None:
|
| 109 |
+
r = r + self.w3 * self.c.predict(rows)['p42'].rank(pct=True)
|
| 110 |
+
vals = np.sort(out['p42'].values)[::-1]
|
| 111 |
+
out['p42'] = pd.Series(
|
| 112 |
+
vals, index=r.sort_values(ascending=False).index).reindex(out.index)
|
| 113 |
+
return out
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def register():
|
| 117 |
+
M = mc.MODELS
|
| 118 |
+
M['md_hgb_d2'] = lambda: HGBMono(max_depth=2, l2_regularization=1.0)
|
| 119 |
+
M['md_hgb_d2l2'] = lambda: HGBMono(max_depth=2, l2_regularization=2.0)
|
| 120 |
+
M['md_hgb_d2l10'] = lambda: HGBMono(max_depth=2, l2_regularization=10.0)
|
| 121 |
+
M['md_hgb_d1'] = lambda: HGBMono(max_depth=1, l2_regularization=1.0,
|
| 122 |
+
learning_rate=0.05, max_iter=400)
|
| 123 |
+
M['md_hgb_d1l25'] = lambda: HGBMono(max_depth=1, l2_regularization=5.0,
|
| 124 |
+
learning_rate=0.05, max_iter=400)
|
| 125 |
+
M['md_bl_hgbd2'] = lambda: MeanBlend(M['md_hgb_d2'], 0.5)
|
| 126 |
+
M['md_blcm_hgbd2'] = lambda: MeanBlendCM(M['md_hgb_d2'], 0.5)
|
| 127 |
+
M['md_blcm_hgbd1'] = lambda: MeanBlendCM(M['md_hgb_d1'], 0.5)
|
| 128 |
+
# 3-way order blend on the frontier recipe: logbag .55 / hgb .25 / wiener .20
|
| 129 |
+
M['md_ord3_d2'] = lambda: OrderBlend3(M['md_hgb_d2'], 0.25, mc.WienerFHT, 0.20)
|
| 130 |
+
M['md_ord3_d1l25'] = lambda: OrderBlend3(M['md_hgb_d1l25'], 0.25,
|
| 131 |
+
mc.WienerFHT, 0.20)
|
| 132 |
+
M['md_ord3_d2w15'] = lambda: OrderBlend3(M['md_hgb_d2'], 0.15, mc.WienerFHT, 0.20)
|
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Learning-to-rank partners for the bgkf value-preserving blend API.
|
| 2 |
+
|
| 3 |
+
All rankers: .fit(train_df)->self, .predict(rows)->DataFrame['p42'] where
|
| 4 |
+
p42 is any monotone score (used as rank source only).
|
| 5 |
+
|
| 6 |
+
Pair schemes (groups are the comparison sets deployment makes):
|
| 7 |
+
'building' : pairs within (origin_idx, building) -- REFUTED pre-crash: worst
|
| 8 |
+
'origin' : pairs within origin_idx (cross-building comparisons included)
|
| 9 |
+
Weighting:
|
| 10 |
+
'none' : all pairs weight 1
|
| 11 |
+
'days' : pair weight ~ urgency of the dying device,
|
| 12 |
+
1 + (WINDOW-days_to_eol)/WINDOW (sooner death = costlier miss)
|
| 13 |
+
"""
|
| 14 |
+
import numpy as np
|
| 15 |
+
import pandas as pd
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
from sklearn.impute import SimpleImputer
|
| 18 |
+
from sklearn.preprocessing import StandardScaler
|
| 19 |
+
from sklearn.linear_model import LogisticRegression
|
| 20 |
+
|
| 21 |
+
DATASET = Path('/Users/THIF/repos/BatterySwapAI2026-Example/dataset')
|
| 22 |
+
WINDOW = 42.0
|
| 23 |
+
|
| 24 |
+
F13 = ['v_last', 'v_drop', 'slope_30', 'slope_90', 'v_min_30',
|
| 25 |
+
'staleness', 'age_days',
|
| 26 |
+
'slope_lastk', 'drop_recent', 'below_frac', 'accel', 'n_30',
|
| 27 |
+
'tau_projected']
|
| 28 |
+
FV = F13 + ['vrel', 'vmin7_rank']
|
| 29 |
+
|
| 30 |
+
_DEV_B = None
|
| 31 |
+
def dev_building():
|
| 32 |
+
global _DEV_B
|
| 33 |
+
if _DEV_B is None:
|
| 34 |
+
_DEV_B = (pd.read_csv(DATASET / 'train' / 'devices.csv')
|
| 35 |
+
.drop_duplicates('device_id')
|
| 36 |
+
.set_index('device_id')['building_id'])
|
| 37 |
+
return _DEV_B
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def add_group_ranks(df, feats, group):
|
| 41 |
+
out = df.copy()
|
| 42 |
+
out['g_rvlast'] = df.groupby(group)['v_last'].rank(pct=True)
|
| 43 |
+
out['g_rtau'] = df.groupby(group)['tau_projected'].rank(pct=True)
|
| 44 |
+
return out, feats + ['g_rvlast', 'g_rtau']
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def build_pairs(df, mode, rng, max_neg_per_pos=50):
|
| 48 |
+
"""Return (i_idx, j_idx, days_i) positional indices into df where i is the
|
| 49 |
+
dying device, j the surviving one, within the comparison group."""
|
| 50 |
+
b = df['device_id'].map(dev_building())
|
| 51 |
+
if mode == 'building':
|
| 52 |
+
keys = list(zip(df['origin_idx'].values, b.values))
|
| 53 |
+
else:
|
| 54 |
+
keys = list(df['origin_idx'].values)
|
| 55 |
+
gmap = {}
|
| 56 |
+
for p, k in enumerate(keys):
|
| 57 |
+
gmap.setdefault(k, []).append(p)
|
| 58 |
+
y = df['dies42'].values
|
| 59 |
+
days = df['days_to_eol'].values
|
| 60 |
+
ii, jj, dd = [], [], []
|
| 61 |
+
for k, idxs in gmap.items():
|
| 62 |
+
idxs = np.asarray(idxs)
|
| 63 |
+
p_ = idxs[y[idxs] == 1]
|
| 64 |
+
n_ = idxs[y[idxs] == 0]
|
| 65 |
+
if len(p_) == 0 or len(n_) == 0:
|
| 66 |
+
continue
|
| 67 |
+
for i in p_:
|
| 68 |
+
n_sel = n_
|
| 69 |
+
if len(n_) > max_neg_per_pos:
|
| 70 |
+
n_sel = rng.choice(n_, size=max_neg_per_pos, replace=False)
|
| 71 |
+
ii.extend([i] * len(n_sel))
|
| 72 |
+
jj.extend(n_sel)
|
| 73 |
+
dd.extend([days[i]] * len(n_sel))
|
| 74 |
+
return np.asarray(ii), np.asarray(jj), np.asarray(dd)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def pair_weights(dd, weighting):
|
| 78 |
+
if weighting == 'days':
|
| 79 |
+
w = 1.0 + np.clip((WINDOW - dd) / WINDOW, 0, 1)
|
| 80 |
+
return w / w.mean()
|
| 81 |
+
return np.ones(len(dd))
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
class _Prep:
|
| 85 |
+
def fit(self, X):
|
| 86 |
+
self.imp = SimpleImputer(strategy='median').fit(X)
|
| 87 |
+
self.sc = StandardScaler().fit(self.imp.transform(X))
|
| 88 |
+
return self
|
| 89 |
+
def transform(self, X):
|
| 90 |
+
return self.sc.transform(self.imp.transform(X))
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
class PairwiseLogistic:
|
| 94 |
+
"""Linear scorer fit by logistic regression on standardized pair
|
| 95 |
+
differences (symmetric +/- augmentation, no intercept)."""
|
| 96 |
+
|
| 97 |
+
def __init__(self, feats=F13, mode='building', weighting='none',
|
| 98 |
+
use_group_ranks=False, C=1.0, seed=0, max_neg_per_pos=50):
|
| 99 |
+
self.feats0 = list(feats)
|
| 100 |
+
self.mode, self.weighting = mode, weighting
|
| 101 |
+
self.ugr = use_group_ranks
|
| 102 |
+
self.C, self.seed, self.mnp = C, seed, max_neg_per_pos
|
| 103 |
+
|
| 104 |
+
def _table(self, df, group):
|
| 105 |
+
feats = list(self.feats0)
|
| 106 |
+
if self.ugr:
|
| 107 |
+
df, feats = add_group_ranks(df, feats, group)
|
| 108 |
+
return df, feats
|
| 109 |
+
|
| 110 |
+
def fit(self, train):
|
| 111 |
+
rng = np.random.default_rng(self.seed)
|
| 112 |
+
b = train['device_id'].map(dev_building())
|
| 113 |
+
grp = ([train['origin_idx'], b] if self.mode == 'building'
|
| 114 |
+
else train['origin_idx'])
|
| 115 |
+
df, feats = self._table(train, grp)
|
| 116 |
+
self.feats = feats
|
| 117 |
+
self.prep = _Prep().fit(df[feats])
|
| 118 |
+
X = self.prep.transform(df[feats])
|
| 119 |
+
ii, jj, dd = build_pairs(df, self.mode, rng, self.mnp)
|
| 120 |
+
D = X[ii] - X[jj]
|
| 121 |
+
w = pair_weights(dd, self.weighting)
|
| 122 |
+
Xp = np.vstack([D, -D])
|
| 123 |
+
yp = np.r_[np.ones(len(D)), np.zeros(len(D))]
|
| 124 |
+
wp = np.r_[w, w]
|
| 125 |
+
self.clf = LogisticRegression(fit_intercept=False, C=self.C,
|
| 126 |
+
max_iter=2000)
|
| 127 |
+
self.clf.fit(Xp, yp, sample_weight=wp)
|
| 128 |
+
return self
|
| 129 |
+
|
| 130 |
+
def predict(self, rows):
|
| 131 |
+
df = rows.copy()
|
| 132 |
+
feats = list(self.feats0)
|
| 133 |
+
if self.ugr:
|
| 134 |
+
df['g_rvlast'] = df['v_last'].rank(pct=True)
|
| 135 |
+
df['g_rtau'] = df['tau_projected'].rank(pct=True)
|
| 136 |
+
feats = feats + ['g_rvlast', 'g_rtau']
|
| 137 |
+
X = self.prep.transform(df[feats])
|
| 138 |
+
s = X @ self.clf.coef_.ravel()
|
| 139 |
+
return pd.DataFrame({'p42': s}, index=rows.index)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
class RankNet:
|
| 143 |
+
"""Small MLP RankNet, N-seed bag, pairwise logistic loss on within-group
|
| 144 |
+
pairs. Deterministic per seed; torch threads pinned to 2."""
|
| 145 |
+
|
| 146 |
+
def __init__(self, feats=F13, mode='building', weighting='none',
|
| 147 |
+
use_group_ranks=False, seeds=(0, 1, 2), hidden=(32, 16),
|
| 148 |
+
epochs=300, lr=3e-3, wd=1e-3, max_neg_per_pos=50):
|
| 149 |
+
self.feats0 = list(feats)
|
| 150 |
+
self.mode, self.weighting = mode, weighting
|
| 151 |
+
self.ugr = use_group_ranks
|
| 152 |
+
self.seeds, self.hidden = seeds, hidden
|
| 153 |
+
self.epochs, self.lr, self.wd = epochs, lr, wd
|
| 154 |
+
self.mnp = max_neg_per_pos
|
| 155 |
+
|
| 156 |
+
def fit(self, train):
|
| 157 |
+
import torch
|
| 158 |
+
torch.set_num_threads(2)
|
| 159 |
+
b = train['device_id'].map(dev_building())
|
| 160 |
+
grp = ([train['origin_idx'], b] if self.mode == 'building'
|
| 161 |
+
else train['origin_idx'])
|
| 162 |
+
df = train
|
| 163 |
+
feats = list(self.feats0)
|
| 164 |
+
if self.ugr:
|
| 165 |
+
df, feats = add_group_ranks(df, feats, grp)
|
| 166 |
+
self.feats = feats
|
| 167 |
+
self.prep = _Prep().fit(df[feats])
|
| 168 |
+
X = self.prep.transform(df[feats]).astype(np.float32)
|
| 169 |
+
rng = np.random.default_rng(0)
|
| 170 |
+
ii, jj, dd = build_pairs(df, self.mode, rng, self.mnp)
|
| 171 |
+
w = pair_weights(dd, self.weighting).astype(np.float32)
|
| 172 |
+
Xi = torch.tensor(X[ii]); Xj = torch.tensor(X[jj])
|
| 173 |
+
wt = torch.tensor(w)
|
| 174 |
+
self.nets = []
|
| 175 |
+
d_in = X.shape[1]
|
| 176 |
+
for seed in self.seeds:
|
| 177 |
+
torch.manual_seed(seed)
|
| 178 |
+
layers, prev = [], d_in
|
| 179 |
+
for h in self.hidden:
|
| 180 |
+
layers += [torch.nn.Linear(prev, h), torch.nn.ReLU()]
|
| 181 |
+
prev = h
|
| 182 |
+
layers += [torch.nn.Linear(prev, 1)]
|
| 183 |
+
net = torch.nn.Sequential(*layers)
|
| 184 |
+
opt = torch.optim.Adam(net.parameters(), lr=self.lr,
|
| 185 |
+
weight_decay=self.wd)
|
| 186 |
+
lossf = torch.nn.BCEWithLogitsLoss(weight=wt)
|
| 187 |
+
target = torch.ones(len(ii))
|
| 188 |
+
for _ in range(self.epochs):
|
| 189 |
+
opt.zero_grad()
|
| 190 |
+
diff = (net(Xi) - net(Xj)).squeeze(-1)
|
| 191 |
+
loss = lossf(diff, target)
|
| 192 |
+
loss.backward()
|
| 193 |
+
opt.step()
|
| 194 |
+
net.eval()
|
| 195 |
+
self.nets.append(net)
|
| 196 |
+
return self
|
| 197 |
+
|
| 198 |
+
def predict(self, rows):
|
| 199 |
+
import torch
|
| 200 |
+
torch.set_num_threads(2)
|
| 201 |
+
df = rows.copy()
|
| 202 |
+
feats = list(self.feats0)
|
| 203 |
+
if self.ugr:
|
| 204 |
+
df['g_rvlast'] = df['v_last'].rank(pct=True)
|
| 205 |
+
df['g_rtau'] = df['tau_projected'].rank(pct=True)
|
| 206 |
+
feats = feats + ['g_rvlast', 'g_rtau']
|
| 207 |
+
X = self.prep.transform(df[feats]).astype(np.float32)
|
| 208 |
+
with torch.no_grad():
|
| 209 |
+
ss = [net(torch.tensor(X)).squeeze(-1).numpy()
|
| 210 |
+
for net in self.nets]
|
| 211 |
+
s = np.mean([pd.Series(s_).rank(pct=True).values for s_ in ss], axis=0)
|
| 212 |
+
return pd.DataFrame({'p42': s}, index=rows.index)
|
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""3-way value-preserving order injection on the vm7+wiener frontier base.
|
| 2 |
+
Usage: run3w.py LABEL A B C [NSEEDS SEED0 FEATS]
|
| 3 |
+
final order = A*logbag(vm7)_rank + B*wiener_rank + C*ranknet_rank,
|
| 4 |
+
injected at harness weight 1.0 so values (and counts) stay the primary
|
| 5 |
+
logbag(vm7)'s. (A,B,C)=(0.8,0.2,0) reproduces ft_vmin7r_wnr20 exactly.
|
| 6 |
+
RankNet: within-origin pairs, days weighting (the champion-base survivor),
|
| 7 |
+
FEATS f13|fvm7 (fvm7 = F13 + vmin7_rank, per coordinator).
|
| 8 |
+
"""
|
| 9 |
+
import sys, time, os
|
| 10 |
+
sys.path.insert(0, '/tmp/rk2')
|
| 11 |
+
sys.path.insert(0, '/Users/THIF/repos/BatterySwapAI2026-Example')
|
| 12 |
+
import numpy as np
|
| 13 |
+
import pandas as pd
|
| 14 |
+
import rankers as rk
|
| 15 |
+
import batteryswap_example.cv as cv
|
| 16 |
+
import batteryswap_example.models_cv as mc
|
| 17 |
+
|
| 18 |
+
label = sys.argv[1]
|
| 19 |
+
A, B, C = (float(x) for x in sys.argv[2:5])
|
| 20 |
+
nseeds = int(sys.argv[5]) if len(sys.argv) > 5 else 7
|
| 21 |
+
seed0 = int(sys.argv[6]) if len(sys.argv) > 6 else 0
|
| 22 |
+
feats_name = sys.argv[7] if len(sys.argv) > 7 else 'fvm7'
|
| 23 |
+
D = float(sys.argv[8]) if len(sys.argv) > 8 else 0.0
|
| 24 |
+
feats = {'f13': rk.F13,
|
| 25 |
+
'fvm7': rk.F13 + ['vmin7_rank'],
|
| 26 |
+
'fvm7h': rk.F13 + ['vmin7_rank', 'hot_90', 'oob_90']}[feats_name]
|
| 27 |
+
|
| 28 |
+
cv.VARIANTS['vm7'] = lambda t: cv.BASE_FEATURES + ['vmin7_rank']
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class ThreeWay:
|
| 32 |
+
def fit(self, train):
|
| 33 |
+
self.lb = mc.SklearnRUL('logbag').fit(train)
|
| 34 |
+
self.wn = mc.MODELS['wiener']().fit(train) if B > 0 else None
|
| 35 |
+
self.rk = (rk.RankNet(feats=feats, mode='origin', weighting='days',
|
| 36 |
+
seeds=tuple(range(seed0, seed0 + nseeds)))
|
| 37 |
+
.fit(train) if C > 0 else None)
|
| 38 |
+
self.ho = (mc.LogBagFeats(['hot_90', 'oob_90']).fit(train)
|
| 39 |
+
if D > 0 else None)
|
| 40 |
+
return self
|
| 41 |
+
|
| 42 |
+
def predict(self, rows):
|
| 43 |
+
s = A * self.lb.predict(rows)['p42'].rank(pct=True)
|
| 44 |
+
if self.wn is not None:
|
| 45 |
+
s = s + B * self.wn.predict(rows)['p42'].rank(pct=True)
|
| 46 |
+
if self.rk is not None:
|
| 47 |
+
s = s + C * self.rk.predict(rows)['p42'].rank(pct=True)
|
| 48 |
+
if self.ho is not None:
|
| 49 |
+
s = s + D * self.ho.predict(rows)['p42'].rank(pct=True)
|
| 50 |
+
return pd.DataFrame({'p42': s.values}, index=rows.index)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
mc.MODELS[label] = ThreeWay
|
| 54 |
+
t0 = time.time()
|
| 55 |
+
df = cv.run_bgkf(kind='logbag', variant='vm7', blend=(label, 1.0))
|
| 56 |
+
df['cand'] = label
|
| 57 |
+
out = f'/tmp/cv_bgkfcand_{label}.csv'
|
| 58 |
+
df.to_csv(out, index=False)
|
| 59 |
+
|
| 60 |
+
d = df.groupby('origin')[['total_cost', 'n_swaps']].sum()
|
| 61 |
+
line = (f"{label} (A={A} B={B} C={C} D={D} s{nseeds}+{seed0} {feats_name}): "
|
| 62 |
+
f"SCORE {d['total_cost'].mean():.2f} swaps {d['n_swaps'].mean():.2f}")
|
| 63 |
+
if os.path.exists('/tmp/cv_bgkf.csv'):
|
| 64 |
+
base = pd.read_csv('/tmp/cv_bgkf.csv').groupby('origin')[
|
| 65 |
+
['total_cost', 'n_swaps']].sum()
|
| 66 |
+
delta = (d['total_cost'] - base['total_cost']).dropna()
|
| 67 |
+
se = delta.std() / np.sqrt(len(delta))
|
| 68 |
+
line += (f" delta {delta.mean():+.2f} SE {se:.2f} "
|
| 69 |
+
f"W/L {int((delta<0).sum())}W/{int((delta>0).sum())}L")
|
| 70 |
+
else:
|
| 71 |
+
line += " [baseline missing - pair later]"
|
| 72 |
+
print(f"\n{line} [{time.time()-t0:.0f}s] -> {out}")
|
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a5be48b744effea696e1443bff9b00b0ebab496920e83a3b823737e67563a16f
|
| 3 |
+
size 7434713
|
|
@@ -194,7 +194,82 @@ class FrontierRUL:
|
|
| 194 |
return out
|
| 195 |
|
| 196 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
def _fit_model(train):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
return (FrontierRUL() if FRONTIER else SklearnRUL(MODEL_KIND)).fit(train)
|
| 199 |
# Inclusion appetite, tuned by local CV: alpha sweep 0.7-1.3 on dev has a clear
|
| 200 |
# minimum at 0.8 (beats every alternative on 11-12 of 16 origins), confirmed on
|
|
@@ -387,6 +462,18 @@ def adaptive_split_plans(split, split_path, log=print, deadline=None):
|
|
| 387 |
locations, timeseries, eol_times, scenarios = load_best_effort(split_path, log)
|
| 388 |
scen_starts = [pd.Timestamp(s['start_time']) for s in scenarios]
|
| 389 |
daily = daily_voltage(timeseries)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
cert_days = cert_confirm_days(daily) if CERT_FIRST else pd.Series(dtype='datetime64[ns]')
|
| 391 |
dall = daily_all(timeseries)
|
| 392 |
betas = weekly_betas(dall)
|
|
@@ -529,6 +616,12 @@ def adaptive_split_plans(split, split_path, log=print, deadline=None):
|
|
| 529 |
dead = eol_times[eol_times.notna() & (eol_times <= T)]
|
| 530 |
curve = master_curve(dall[dall['day'] <= T], dead, beta)
|
| 531 |
feats = feats.join(physics_features(dall, T, beta, curve, match=False))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
feats = feats.reindex(locs['battery'])
|
| 533 |
|
| 534 |
pred = model.predict(feats)
|
|
|
|
| 194 |
return out
|
| 195 |
|
| 196 |
|
| 197 |
+
# CAMPAIGN-2 FRONTIER (2026-08-23, building-GroupKFold v5 instrument):
|
| 198 |
+
# ft_vmin7r_wnr20 = logbag on FEATURES + vmin7_rank (fleet pct-rank of the
|
| 199 |
+
# 7d-min voltage, computed by features_at_origin over whatever fleet it
|
| 200 |
+
# ranks - own-fleet semantics in deployment) with a 0.20 value-preserving
|
| 201 |
+
# WienerFHT order blend. THE SCORE 1688.4 vs champion 1760.2 (-71.8 +/-
|
| 202 |
+
# 23.5, 3.05 sigma, Wilcoxon p=.037, swaps 9.58 flat, count-clean by every
|
| 203 |
+
# control; components additive: wiener -50.3, vmin7_rank -23.2; reproduced
|
| 204 |
+
# independently by three lanes). Worst-case reference-shift bound: with
|
| 205 |
+
# vmin7_rank recomputed WITHIN each ~5-building test fleet the stack still
|
| 206 |
+
# reads -60.8 +/- 26.3 (2.31 sigma); the scored fleets are full ~922-device
|
| 207 |
+
# populations, much closer to the shared-reference case. Value-preserving
|
| 208 |
+
# means the blended rank ORDER is mapped back onto the primary's own sorted
|
| 209 |
+
# p42 values: count/pricing/timing machinery see the primary's exact p42
|
| 210 |
+
# distribution (the phase-56/58 shipping bar). Both components degrade
|
| 211 |
+
# softly: missing wnr_* columns drop the wiener source (pure vm7 logbag),
|
| 212 |
+
# any fit error falls back to the legacy champion via _fit_model.
|
| 213 |
+
FT2 = os.environ.get('BATTERYSWAP_FT2', '1') != '0'
|
| 214 |
+
FT2_W = float(os.environ.get('BATTERYSWAP_FT2_W', '0.20'))
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
class Ft2RUL:
|
| 218 |
+
"""Faithful deployment of cv.run_bgkf(kind='logbag', variant='vm7',
|
| 219 |
+
blend=('wiener', 0.20)): primary = SklearnRUL('logbag') fit with
|
| 220 |
+
FEATURES + vmin7_rank (the CV sets mc.FEATURES globally; here the swap
|
| 221 |
+
is scoped to this class's fits and predicts), partner = WienerFHT
|
| 222 |
+
(fit under the same feature list, exactly as the CV's global swap did),
|
| 223 |
+
blended rank order mapped onto the primary's own sorted p42 values,
|
| 224 |
+
timing columns staying with their device."""
|
| 225 |
+
|
| 226 |
+
def _swap(self):
|
| 227 |
+
class _S:
|
| 228 |
+
def __enter__(s):
|
| 229 |
+
s.old = models_cv.FEATURES
|
| 230 |
+
if 'vmin7_rank' not in models_cv.FEATURES:
|
| 231 |
+
models_cv.FEATURES = models_cv.FEATURES + ['vmin7_rank']
|
| 232 |
+
return s
|
| 233 |
+
def __exit__(s, *a):
|
| 234 |
+
models_cv.FEATURES = s.old
|
| 235 |
+
return _S()
|
| 236 |
+
|
| 237 |
+
def fit(self, train):
|
| 238 |
+
with self._swap():
|
| 239 |
+
self.primary = SklearnRUL(MODEL_KIND).fit(train)
|
| 240 |
+
self.wiener = (models_cv.WienerFHT().fit(train)
|
| 241 |
+
if FT2_W > 0 and 'wnr_n' in train.columns else None)
|
| 242 |
+
return self
|
| 243 |
+
|
| 244 |
+
def predict(self, rows):
|
| 245 |
+
if 'vmin7_rank' not in rows.columns:
|
| 246 |
+
rows = rows.copy()
|
| 247 |
+
rows['vmin7_rank'] = (rows['v_min_7'].rank(pct=True)
|
| 248 |
+
if 'v_min_7' in rows.columns else np.nan)
|
| 249 |
+
with self._swap():
|
| 250 |
+
pred = self.primary.predict(rows)
|
| 251 |
+
rk = pred['p42'].rank(pct=True)
|
| 252 |
+
if self.wiener is not None and 'wnr_n' in rows.columns:
|
| 253 |
+
# inside the swap: the wiener's DayQuantiles was fit under
|
| 254 |
+
# the same swapped feature list (exactly the CV's global set)
|
| 255 |
+
p2 = self.wiener.predict(rows)
|
| 256 |
+
rk = (1 - FT2_W) * rk + FT2_W * p2['p42'].rank(pct=True)
|
| 257 |
+
vals = np.sort(pred['p42'].values)[::-1]
|
| 258 |
+
out = pred.copy()
|
| 259 |
+
out['p42'] = pd.Series(
|
| 260 |
+
vals, index=rk.sort_values(ascending=False).index
|
| 261 |
+
).reindex(out.index)
|
| 262 |
+
return out
|
| 263 |
+
|
| 264 |
+
|
| 265 |
def _fit_model(train):
|
| 266 |
+
if FT2:
|
| 267 |
+
try:
|
| 268 |
+
return Ft2RUL().fit(train)
|
| 269 |
+
except Exception:
|
| 270 |
+
# deadline-day discipline: a broken frontier fit degrades to the
|
| 271 |
+
# measured champion, never to a crash
|
| 272 |
+
traceback.print_exc()
|
| 273 |
return (FrontierRUL() if FRONTIER else SklearnRUL(MODEL_KIND)).fit(train)
|
| 274 |
# Inclusion appetite, tuned by local CV: alpha sweep 0.7-1.3 on dev has a clear
|
| 275 |
# minimum at 0.8 (beats every alternative on 11-12 of 16 origins), confirmed on
|
|
|
|
| 462 |
locations, timeseries, eol_times, scenarios = load_best_effort(split_path, log)
|
| 463 |
scen_starts = [pd.Timestamp(s['start_time']) for s in scenarios]
|
| 464 |
daily = daily_voltage(timeseries)
|
| 465 |
+
ft2_daily = None
|
| 466 |
+
if FT2:
|
| 467 |
+
# per-scenario Wiener (drift, variance) for the ft2 order blend -
|
| 468 |
+
# computed from THIS split's own past-only telemetry, same estimator
|
| 469 |
+
# the training cache used. Any failure just drops the wiener source.
|
| 470 |
+
try:
|
| 471 |
+
from batteryswap_example.fht_features import (
|
| 472 |
+
daily_corrected, wiener_at_origin)
|
| 473 |
+
ft2_daily = daily_corrected(timeseries)
|
| 474 |
+
except Exception as e:
|
| 475 |
+
log(f' WARNING ft2 wiener daily build failed ({e}); '
|
| 476 |
+
f'order blend drops the wiener source')
|
| 477 |
cert_days = cert_confirm_days(daily) if CERT_FIRST else pd.Series(dtype='datetime64[ns]')
|
| 478 |
dall = daily_all(timeseries)
|
| 479 |
betas = weekly_betas(dall)
|
|
|
|
| 616 |
dead = eol_times[eol_times.notna() & (eol_times <= T)]
|
| 617 |
curve = master_curve(dall[dall['day'] <= T], dead, beta)
|
| 618 |
feats = feats.join(physics_features(dall, T, beta, curve, match=False))
|
| 619 |
+
if ft2_daily is not None:
|
| 620 |
+
try:
|
| 621 |
+
feats = feats.join(wiener_at_origin(ft2_daily, T))
|
| 622 |
+
except Exception as e:
|
| 623 |
+
log(f' WARNING {scenario["name"]}: ft2 wiener params failed '
|
| 624 |
+
f'({e}); wiener source dropped this scenario')
|
| 625 |
feats = feats.reindex(locs['battery'])
|
| 626 |
|
| 627 |
pred = model.predict(feats)
|