Trained models or other output used by submission processing (script.py),
must be committed in the git repository.
An example is batteryswap_example/planners/best.pickle.
Test submissions in Docker - recommended
Using Docker allows to have exactly the same software versions as the submissions system.
This helps to ensure there are no errors when running in the submission environment.
NOTE: this requires around 20 GB+ of disk space.
Build Docker image
docker build -t batteryswapai-2026-example .
Make submissions and run evaluation (mount your local data folder to /tmp/data)
Exact configuration used (all hardcoded defaults in batteryswap_example/train.py,
so no environment variables are required at submission time):
RUL model: final_model = 'survival' (lifelines Weibull AFT). Chosen by
lowest evaluate_plantotal_cost summed over all scenarios, not by MAE
— MAE only scores observed (non-censored) devices and is blind to the survival
model's censoring-aware conservatism on long-lived devices.
Right-censoring bound: censor_horizon='true' (per-device). 82% of devices
(379/461) have a blank EOL, i.e. are right-censored, and an AFT likelihood
contributes S(C) for each of them — so C must be the time we actually know
the device survived to. The original code used a constant 120 days for every
censored device, although the data shows they were still alive at the dataset
cutoff (~334 days after s_0's start). Measured (sweep_censoring.py, day-caps
trimmed to 32,inf):
censor_horizon
total
early_swap
late_swap
'true'
106,733
41,437
43,220
365.0
112,393
33,596
59,660
240.0
112,551
32,046
60,930
120.0 (old)
112,641
21,724
70,780
The 240/365 arms are controls: 'true' beats equal-magnitude constants by
~5,700, so the win comes from the per-device variation, not merely from a
larger bound. Mechanism: a correct bound spreads predicted RUL out, so the
planner can prioritise genuinely-dying devices instead of treating the whole
fleet as due at once — late_swap falls 70,780 → 43,220. early_swap rises,
but that is a good trade at the evaluator's 20:1 late:early ratio.
Scheduling quantile: OrderedPlanner.SCHEDULE_QUANTILE = 'p10'
(PLANNER_SCHEDULE_QUANTILE default). Re-validated AFTER the censoring fix
(sweep_quantiles.py, trained with censor_horizon='true', day-caps 32,inf):
q
total
early_swap
late_swap
p10
106,733
41,437
43,220
p12
112,046
32,065
56,970
p08
115,759
61,014
32,540
p15
114,331
22,290
73,400
p05
139,788
102,092
13,980
p50
151,106
463
137,790
p10 is a clear, stable minimum (next-best p12 is +5,312). Note the empirical
optimum sits ABOVE the newsvendor anchor q* = early/(early+late) = 0.5/10.5 ≈ p05: perfect-calibration theory says p05, but the day-cap/batching congestion
penalises over-early swaps more than the raw 20:1 ratio implies, so p05 actually
scores 139,788 — trusting theory over measurement would cost ~33k. NOTE: this
default is what the grader uses, because the scheduling quantile is a class
attribute read at import and the grader sets no env var. final_quantilesmust include the chosen quantile or the planner silently falls back to p50.
Planner day-work caps: PLANNER_DAY_CAPS default 24,32,48,inf
(self-tuned per scenario by evaluate_plan against the predicted EOL).
Random seed: seed=0 (default in train_rul_model).
Training data: split='train', limit_scenarios=None (ALL 48 scenarios
feed RUL training; every scenario is the same physical fleet at a +7-day-shifted
start, so this exposes each device at many life-stages rather than duplicating
rows). Measured-best: survival + p10 + all-scenarios sums to 112,641
total_cost across all scenarios, ~16% better than the old limit_scenarios=1
(134,576). The gain is monotonic across 1,2,4,8,16,all (see
sweep_training_data.py), consistent with the RUL model being data-starved at
limit_scenarios=1 (only ~82 observed failures). NOTE: this local harness
evaluates on the same fleet it trains on, so treat the absolute number as
optimistic — the hidden competition devices are the real judge — but the
monotonic trend makes the direction trustworthy.
Files required for a complete, reproducible submission
Runtime (copied into the competition Docker image via Dockerfile):
requirements.txt — fixed by the competition (already includes lifelines,
scikit-learn, etc.).
batteryswap_example/train.py — defines OrderedPlanner + RUL models
(must be importable for unpickling best.pickle).
batteryswap_example/planners/best.pickle — the trained planner artifact.
Repository / reproducibility:
README.md — this file (documents the exact reproduce command).
LICENSE — MIT (edit the copyright holder).
Not part of the grader run and kept out of the submission repo via .gitignore
(the Dockerfile only copies batteryswap_example/ and script.py; these are
local-only tuning/investigation tools that informed the hardcoded defaults in
train.py, plus generated outputs and the organizer-provided dataset):
compare_runs.py, inspect_data.py, investigate_*.py, sweep_*.py
(sweep_quantiles.py, sweep_training_data.py, sweep_censoring.py),
results/, submission.csv (generated), BatterySwapAI-2026-Public/ (dataset).
requirements.dev.txt is kept (referenced by the setup instructions above).