You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

BatterySwapAI2026-sackriti

Reproducible CPU-only submission repository for the BatterySwapAI 2026 Challenge. The runtime entry point is script.py; it loads the committed planner in batteryswap_sackriti/planners/best.pickle and writes the required submission.csv with the official batteryswap_public helper.

The submitted tree contains no dataset, generated submission, cache, secret, or network dependency. See SUBMISSION_021.md for the method and its evidence, REPRODUCIBILITY.md for the complete rebuild and validation procedure, THIRD_PARTY_NOTICES.md for dependency provenance, and COMPETITION_COMPLIANCE.md before publishing any new commit.

The idea

eol_times is not a latent quantity to be predicted. It is exactly the first day on which the officially smoothed voltage β€” batteryswap_public.utils.smooth_series, the organiser's own function β€” is at or below 2.40 V.

On the public split that rule reproduces the labels perfectly:

Check Result
Observed end-of-life dates reproduced to the day 82 / 82
Batteries that cross 2.40 V but carry no label 0
Batteries with a label that never cross 0

The build re-runs that audit every time and prints the result, so a change in the organiser's data would surface immediately rather than silently.

This makes the task a first-passage problem, not a classification problem. The question is not "is this battery unhealthy?" but "when will this curve first touch this line, and how uncertain is that date?" Submissions 007–017 answered the first question with an Extra Trees classifier, a 0.45 probability threshold and a 20-battery cap. Submission 021 answers the second.

How it works

1. The official smoother, exactly, in 1.6 seconds. A vectorised reimplementation whose output is bit-identical to smooth_series (max absolute difference 0.0 across 336,096 public rows; also checked against the real function in tests/test_crossing.py). Getting it right is subtle β€” the official smoother resamples onto a dense daily grid before dropping days with fewer than five in-band readings, so a sparse day becomes a blank slot rather than disappearing, and the seven-slot rolling median spans seven calendar days rather than seven observations. The obvious implementation loses 9,258 rows and misses a label. Being fast is what makes it affordable to recompute the whole causal state at every scenario.

2. A survival model over the drawdown distribution. Only 82 batteries ever fail, so a classifier for "fails within 42 days" has 82 events to learn from no matter how many features it is given β€” that, not the feature set, is the binding constraint on the earlier submissions. But because the label is a crossing, the event "fails within h days" is identical to "the voltage falls by at least m within h days", where m is the battery's current margin. So the model is trained on that general question with (h, m) supplied as inputs. Now every battery-day is a labelled example at every margin β€” a healthy cell that drops 0.03 V in three weeks is direct evidence about the drawdown law. Training goes from 454 positive scenario rows to 2.0 million labelled questions, and the actual margin is oversampled so the model is sharpest where it is queried.

3. Seasonality is worth three weeks of ageing. The smoothed voltage carries a common seasonal swing of about 0.048 V peak-to-trough, against a typical near-failure decline of 0.0022 V/day. It is why failures cluster in autumn and winter (46 of 82 crossings fall between September and January). The planner estimates a cross-sectional common factor contemporaneously, fits a day-of-year climatology on strictly past data, and tells the model how much of the required fall the calendar will supply on its own over the queried horizon. Worth βˆ’104 on out-of-fold cost by itself.

4. Expected cost, not a threshold. A battery left out of the plan is retired by the evaluator after the window closes, at 10 hour-equivalents per day late β€” roughly 240 hours. A needless swap costs 0.5 per day of remaining life. Both options are priced for every battery from its own survival curve, including the evaluator's assumed end-of-life 30 days after a device's last reading (which makes a needless swap expensive early in the year and cheap late in it). A scheduler then decides selection, timing and routing together, pricing every candidate move with the evaluator's real accounting for travel, room and building changes, overtime and the daily and weekly caps. It plans 8 batteries in one scenario and 24 in another, where the champion always plans about 13 to 17.

5. Nothing label-derived at inference. The 2.40 V constant was read off the public labels, which the hidden split does not provide. The planner does not depend on it: a battery still present at a scenario cut is one the organiser has not retired, so its smoothed voltage has never touched the threshold, and the smallest historical minimum across live batteries bounds it from above. On the public split that bound lands within 0.018 V of 2.40 in every scenario. At inference the smoother, the features, the climatology and the threshold are all recomputed from the scenario's own leakage-safe telemetry cut.

Results

Lower is better. Model folds are disjoint by battery (or by building), so no held-out battery contributes a single training row; every scenario is then replanned from its own leakage-safe cut and scored by the official evaluator on complete plans.

Validation view Submission 016 Submission 021 Delta
Battery-disjoint out-of-fold, 48 scenarios 1735.7828 1237.0882 βˆ’498.6946 (βˆ’28.7%)
Building-disjoint out-of-fold, 48 scenarios 1767.6100 1350.6761 βˆ’416.9339 (βˆ’23.6%)

Both fold structures agree on the direction and rough size of the gain; the building split is the harder test because it also removes the shared local environment.

At a matched workload of 20 batteries per scenario the model misses 1.79 mandatory batteries against the champion's reported 3.65 (3.40 with the Submission 017 rescue). For scale, a planner handed the true end-of-life dates and this same scheduler scores 123.48, so essentially all remaining cost is forecast uncertainty rather than routing.

Development history, each step measured end to end on the battery-disjoint view:

Model Cost Misses/scenario
Drawdown model, base features 1377.19 2.38
plus seasonal common factor 1272.81 2.15
plus drawdown history and first-passage z-scores 1288.10 2.23
plus pre-smoothing band signals and temperature correction 1252.82 2.08
shipped two-seed ensemble 1237.09 2.27

On not overfitting

The hidden split is what counts, so every reported number is out-of-fold and the guards are deliberate:

  • A four-day timing shift scored 1224.57 against the shipped two-day value's 1237.09, and was rejected β€” its neighbours (1237.89 at three days, 1239.63 at five) show it is scheduling discreteness, not signal.
  • Sharpening the survival curve (powers 0.7 to 2.0) and biasing selection (0.8Γ— to 2.2Γ— the cost of a miss) both make the result worse in either direction. The optimum sitting at the neutral value is evidence the model is calibrated rather than tuned.
  • The model is heavily regularised: minimum leaf 200 on 2.0 M rows. That is why its full-fit public replay (716.29) is worse than Submission 017's (575.72) while its out-of-fold score is far better β€” a lightly regularised forest fitted on the public labels reproduces them well and generalises worse. The full-fit replay is a memorisation diagnostic, not a comparison between policies.

Locked production policy

Control Value
Model drawdown survival classifier, gradient-boosted histograms
Base features 122 causal, plus 12 horizon/margin query terms
Trees / learning rate / leaves / min leaf 450 / 0.07 / 63 / 200
Seed ensemble 2 (seeds 0, 1)
Augmented training rows ~2.0 M per seed
Horizon grid (training / query) 12 horizons / 108 horizons to 260 days
End-of-life threshold 2.40 V, re-derived label-free per scenario
Selection rule expected cost of swapping vs. skipping
Candidate gain floor / cap 0.5 h / 120
Timing expected-cost minimising day, shifted 2 days earlier
Scheduler greedy insertion then move/drop local search, 4 passes
Feature history required 560 days
Device batch at inference 64

Runtime

The 48-scenario public replay takes about nine minutes against roughly twenty-five for Submission 017, and peaks at 1.5 GB against the evaluation container's 2 GB cap. Telemetry is walked in device batches because everything except the common factor is per battery; batching is verified to produce byte-identical plans. If a scenario ever raises, the planner degrades to a margin-ranked fallback plan rather than failing the whole submission.

Quick verification

After placing the official split under dataset/train/:

python -m venv venv
source venv/bin/activate
pip install -r requirements.txt -r requirements.dev.txt

python -m batteryswap_sackriti.train_crossing \
  --dataset-path dataset --split train \
  --output-path batteryswap_sackriti/planners/best.pickle
python -m pytest

BATTERYSWAP_DATASET_PATH=dataset BATTERYSWAP_SPLITS=train \
  BATTERYSWAP_SUBMISSION_PATH=submission.csv python script.py
BATTERYSWAP_DATASET_PATH=dataset BATTERYSWAP_SPLITS=train \
  BATTERYSWAP_SUBMISSION_PATH=submission.csv \
  python -m batteryswap_public.metric

submission.csv is a generated result and is intentionally ignored. The repository is self-contained at inference time and does not call APIs, download models, require a GPU, or use external training data.

Earlier submissions

Submissions 007 through 020 are a single line of work: an Extra Trees failure classifier, then reranking, rescue, value-gating and learned-replacement stages layered on top of it. Their artifacts are preserved unchanged:

Artifact SHA-256 Policy
planners/submission_019.pickle c363624d...aff4b agreement-gated value wrapper, replacements disabled
planners/submission_017.pickle 1a6b3e7a...a154c guarded stale-telemetry rescue over the 016 reranker
planners/submission_007.pickle 1ecf0ce1...830ca frozen trajectory model both of the above wrap

batteryswap_sackriti/submission_020.py holds a separate learned-replacement candidate on the same 016 base; it is unpromoted and does not affect the runtime artifact. Their method and validation are recorded in SUBMISSION_016.md, SUBMISSION_017.md, SUBMISSION_019.md, RESEARCH_EVOLUTION.md and EXPERIMENTS.md.

Submission 021 is not another layer on that stack. It replaces the classifier, the threshold and the cap outright, because the label turned out to be a rule rather than a target.

Publishing

Follow PUBLISHING.md. The numbered Hugging Face submission refs are immutable, and the supported clean-history command is:

SUBMISSION_NAME=submission-021 ./scripts/publish_huggingface_clean.sh
Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support