BatterySwapAI 2026 β Team limmen
Censoring-aware remaining-useful-life prognosis for CR2477T IoT sensor batteries, coupled to a decision-theoretic replacement scheduler under worker-hour and travel constraints.
Everything needed to reproduce the stored model and the submitted plans is in
this repository. All code here is authored by the participant and MIT
licensed (see LICENSE).
1. Method
Four stages, each addressing a specific structure in the problem:
- Censoring-aware survival model. 82.2% of the 461 training devices never
fail inside the observation window. A Weibull AFT (
lifelines) is fitted on all of them, censored rows included, rather than on the 82 observed failures alone. Supplies the beyond-window survivor tail. - Physics- and DSP-informed features. Distance to the 2.4 V EOL knee,
temperature-compensated voltage (measured within-device coefficient
0.00483 V/Β°C), window minima, local volatility and trend, computed on the
in-band smoothed series that mirrors
batteryswap_public.utils.smooth_series. - Calibrated discrete failure CDF. Six monotonic gradient-boosted classifiers at 7/14/21/28/35/42 days, each isotonic-calibrated on out-of-fold predictions grouped by building, since the scored splits use buildings never seen in training. Monotonicity constraints encode physics sign-priors (lower voltage cannot reduce failure probability).
- Expected-cost decision rule. Per battery, the realised evaluator cost of deferring (failure probability Γ late penalty Γ expected late days, plus a priced emergency round trip) against swapping (survival probability Γ early penalty Γ expected early days, plus wrench time). Buildings are visited only when the summed savings of their due batteries exceed the shared trip cost. Swap timing is the newsvendor critical fractile q* = c_early/(c_early + c_late) = 0.5/10.5 β 0.048 β derived from the cost constants, not tuned.
2. Environment
python -m venv venv && source venv/Scripts/activate
pip install -r requirements.txt
Developed on Python 3.11.9. requirements.txt is the organizers' fixed
competition set, copied verbatim; the submission adds no dependency
beyond it.
3. Data
python download_data.py # from the project repo; or use huggingface_hub directly
Fetches batteryswapaichallenge/BatterySwapAI-2026-Public into data/:
devices.csv, eol_times.csv, battery_metrics.parquet, scenarios.json.
No other dataset is used.
4. Preprocessing β training table
python scripts/build_dense.py --step=2 --start=2025-06-01 --jobs=18 \
--out=artifacts/training_table_era_v6.parquet
One row per (snapshot, battery), features computed strictly from data at or before the snapshot and labels measured after it. Two deliberate choices:
--step=2β snapshots every two days rather than only the 48 shipped scenario starts, which the competition documentation explicitly invites.--start=2025-06-01β restricted to the evaluation era. Training on the full 2022β2026 history scored 2124 against 2065: the older period has a 0.9% in-window failure base rate against 2.3% in the scored era, and isotonic calibration inherits the training base rate.
Runtime β12 min on 18 cores. Deterministic.
5. Training
AQUILA_MEMBERS=5 python scripts/train.py --table=artifacts/training_table_era_v6.parquet
| artifact | contents |
|---|---|
model.pkl |
Weibull AFT, censoring-aware, survivor tail |
clf.pkl |
monotonic GBM, P(EOL β€ 42 d), isotonic-calibrated OOF by building |
hazard.pkl |
six such classifiers at 7β¦42 d β calibrated discrete CDF |
Runtime β15 min. Deterministic given the table (SEED = 20260818; no RNG in
featurisation, AFT fitting is deterministic).
6. Stored-model generation
python scripts/make_submission_repo.py --safety=1.0 --quantile=0.05
Bundles the fitted planner into aquila/planners/best.pickle β the artifact
shipped in this repository β together with the aquila package, script.py
and the Dockerfile.
7. Inference
Exactly as the competition harness runs it:
BATTERYSWAP_DATASET_PATH=../data BATTERYSWAP_SPLITS=train python script.py
script.py unpickles aquila/planners/best.pickle and calls
Planner.plan(timeseries, locations, travel_costs, settings) per scenario,
writing submission.csv. CPU only, single process. Measured 3m37s for 48
scenarios against the 30-minute limit; peak memory well under 32 GB.
The serve path degrades in tiers rather than raising, because one uncaught
exception or a wall-clock overrun would void an entire run: full plan β
voltage-threshold plan built from the raw series alone β defer-all, which is
valid by construction. A time guard (AQUILA_TIME_BUDGET, default 1500 s)
switches remaining scenarios to the fallback rather than overrunning. All
tiers are verified by fault injection.
8. Dependencies, versions, licenses
No pretrained models and no external datasets are used. Every package is from the organizers' fixed environment; versions are those used in development.
| package | version | license |
|---|---|---|
| pandas | 2.2.2 | BSD-3-Clause |
| numpy | 2.2.6 | BSD-3-Clause |
| scikit-learn | 1.7.2 | BSD-3-Clause |
| scipy | 1.13.1 | BSD-3-Clause |
| lifelines | 0.30.3 | MIT |
| statsmodels | 0.14.6 | BSD-3-Clause |
| pyarrow | 16.1.0 | Apache-2.0 |
| fastparquet | 2026.5.0 | Apache-2.0 |
| joblib | 1.5.2 | BSD-3-Clause |
| polars | 1.43.2 | MIT |
| ortools | 9.15.6755 | Apache-2.0 |
| batteryswap_public | 0.3.4 | organizers' package |
All permissive (MIT / BSD / Apache-2.0), consistent with the competition's licensing requirement.
9. Commit history
Each tagged version below is a scored submission, kept intact for
verification. Public-leaderboard total_cost:
| commit | version | score |
|---|---|---|
d6b1dfd |
v1 β AFT + calibrated window classifier | 2132.24 |
f65e62c |
v2 β multi-horizon hazard, temp-compensated features | 2070.04 |
1f7a6d7 |
v3 β tuned ensemble, true trip pricing | 1928.65 |
b057c82 |
v5 β multi-building day chaining | 1816.82 |
416e64b |
v6 β chaining caps from harsh geometry | 1777.22 |
7f5bc19 |
v9 β rider margin 0.45 | 1953.35 |
3be9ee9 |
v10 β safety 1.25 | 1794.94 |
00f5af9 |
v11 β v6 decisions + serve-time safety net | β |
v9 and v10 bracket the swap-count axis from both sides and confirm v6 as a local optimum: removing swaps costs +90.4 late per swap, adding them saves only β35.7. Negative results are documented rather than discarded.