jonnor commited on
Commit
a3449bd
·
1 Parent(s): d33fe39

Update for datetime plans

Browse files
.gitignore CHANGED
@@ -1,3 +1,5 @@
1
  submission.csv
2
  data/
3
  dataset/
 
 
 
1
  submission.csv
2
  data/
3
  dataset/
4
+
5
+ venv/
README.md CHANGED
@@ -2,9 +2,25 @@
2
  license: mit
3
  ---
4
 
 
5
 
 
 
6
 
7
- # Setting up virtual environment
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  On Linux / Mac OS / Windows Subsystem for Linux (WSL)
10
  ```
@@ -17,7 +33,10 @@ Install dependencies
17
  pip install -r requirements.txt -r requirements.dev.txt
18
  ```
19
 
20
- # Develop in virtual environment
 
 
 
21
 
22
 
23
  Run the training
@@ -25,14 +44,16 @@ Run the training
25
  python batteryswap_example/train.py
26
  ```
27
 
28
- # Submitting trained models
 
 
29
 
30
  Trained models or other output used by submission processing (`script.py`),
31
  must be committed in the git repository.
32
  An example is `batteryswap_example/planners/best.pickle`.
33
 
34
 
35
- # Test submissions in Docker - recommended
36
 
37
  Using Docker allows to have exactly the same software versions as the submissions system.
38
 
@@ -55,4 +76,10 @@ Copy submission.csv out of container
55
  docker cp batteryswapai:/app/submission.csv ./submission.csv
56
  ```
57
 
 
 
 
 
 
 
58
 
 
2
  license: mit
3
  ---
4
 
5
+ # Example Model for BatterySwapAI 2026
6
 
7
+ This repository shows how to create a solution for the
8
+ [BatterySwapAI 2026 challenge](https://www.nora.ai/competitions/batteryswapai/batteryswapai2026.html).
9
 
10
+ It contains working example code that can be submitted as-is,
11
+ along with tools to check the solution before submitting.
12
+
13
+ # Setup
14
+
15
+ ## Prerequisites
16
+
17
+ You need to have the following installed
18
+
19
+ - git
20
+ - Python 3.10+
21
+ - Docker
22
+
23
+ ## Setting up virtual environment
24
 
25
  On Linux / Mac OS / Windows Subsystem for Linux (WSL)
26
  ```
 
33
  pip install -r requirements.txt -r requirements.dev.txt
34
  ```
35
 
36
+
37
+ # Developing
38
+
39
+ ## Develop in virtual environment
40
 
41
 
42
  Run the training
 
44
  python batteryswap_example/train.py
45
  ```
46
 
47
+ # Making submissions
48
+
49
+ ## Submitting trained models
50
 
51
  Trained models or other output used by submission processing (`script.py`),
52
  must be committed in the git repository.
53
  An example is `batteryswap_example/planners/best.pickle`.
54
 
55
 
56
+ ## Test submissions in Docker - recommended
57
 
58
  Using Docker allows to have exactly the same software versions as the submissions system.
59
 
 
76
  docker cp batteryswapai:/app/submission.csv ./submission.csv
77
  ```
78
 
79
+ ## Create new submission
80
+
81
+ NOTE: remember to commit and push your changes to the HuggingFace model repository.
82
+
83
+ Use `New submission` in the competition application to submit your current code for evaluation.
84
+
85
 
batteryswap_example/planners/best.pickle CHANGED
Binary files a/batteryswap_example/planners/best.pickle and b/batteryswap_example/planners/best.pickle differ
 
batteryswap_example/train.py CHANGED
@@ -2,34 +2,221 @@
2
  import pandas
3
  import numpy
4
  import pickle
 
 
 
5
 
6
- class OrderedPlanner():
7
- def __init__(self):
8
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
  def plan(self, battery_data, locations, travel_costs, settings):
11
 
12
- # Order by voltage
13
- latest_voltage = battery_data.groupby(['device_id'])['voltage'].last()
14
- locations = locations.copy().set_index('battery')
15
- locations['latest_voltage'] = latest_voltage
16
- order = locations.sort_values('latest_voltage')
 
 
 
17
 
18
- # Do one swap per day
19
- days = numpy.arange(len(order))
 
 
 
 
 
 
 
 
20
  plan = pandas.DataFrame({
21
  'day': days,
22
  'battery': order.index,
23
  })
 
 
 
24
  return plan
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  def main():
27
- planner = OrderedPlanner()
28
- planner_path = 'planner.pickle'
 
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  with open(planner_path, "wb") as f:
31
  pickle.dump(planner, f)
32
  print('planner-save', planner_path)
33
 
 
34
  if __name__ == '__main__':
35
  main()
 
2
  import pandas
3
  import numpy
4
  import pickle
5
+ from typing import Optional, Sequence
6
+ from pathlib import Path
7
+ import pathlib
8
 
9
+
10
+ from sklearn.dummy import DummyRegressor
11
+ import pandas as pd
12
+ import numpy
13
+ import numpy as np
14
+ import pandas
15
+ from pydantic import Field
16
+ from pydantic_settings import BaseSettings, SettingsConfigDict
17
+ import structlog
18
+
19
+ from batteryswap_public.interfaces import Planner, RULModel
20
+ from batteryswap_public.utils import load_dataset, iterate_scenarios
21
+ from batteryswap_public.evaluate import evaluate_plan, check_plan_valid
22
+
23
+ log = structlog.get_logger()
24
+
25
+
26
+ class OrderedPlanner(Planner):
27
+ def __init__(self, rul_estimator):
28
+ self.rul_estimator = rul_estimator
29
 
30
  def plan(self, battery_data, locations, travel_costs, settings):
31
 
32
+ # Remaining Useful Life estimation
33
+ # FIXME: consider balance of over/under-estimation
34
+ percentile = 'p50'
35
+ rul = self.rul_estimator.predict(battery_data)
36
+ rul_days = rul[percentile]
37
+ # convert to a date
38
+ start_time = battery_data.reset_index()['end_time'].max()
39
+ predict_eol = start_time + pandas.to_timedelta(rul_days, unit='D')
40
 
41
+ loc = locations.copy().set_index('battery')
42
+ loc['eol_time'] = predict_eol
43
+ order = loc.sort_values('eol_time', ascending=True)
44
+
45
+ # Planner
46
+ # Stupid heuristic: Do one swap per day
47
+ # FIXME: take travel distances into account
48
+ # FIXME: take co-location into account
49
+ # FIXME: take daily and weekly limits into account
50
+ days = start_time + pandas.to_timedelta(numpy.arange(len(order)), unit='D')
51
  plan = pandas.DataFrame({
52
  'day': days,
53
  'battery': order.index,
54
  })
55
+
56
+ check_plan_valid(plan, locations, start_time=start_time)
57
+
58
  return plan
59
 
60
+
61
+ class DummyRULModel(RULModel):
62
+ # RUL model that predicts (no information rate)
63
+ # FIXME: make a model that actually uses the data to improve predictions
64
+
65
+ def __init__(
66
+ self,
67
+ time_col: str = 'end_time',
68
+ group_col: str = 'device_id',
69
+ value_cols: Sequence[str] = ('voltage', 'temperature'),
70
+ quantiles: Sequence[float] = (0.5, ),
71
+ ):
72
+ self.time_col = time_col
73
+ self.group_col = group_col
74
+ self.value_cols = list(value_cols)
75
+ self.quantiles = sorted(quantiles)
76
+ self.quantile_cols = [f"p{round(q * 100):02d}" for q in self.quantiles]
77
+
78
+ self.model = None
79
+ self.use_total_elapsed_days = True
80
+
81
+ def _compute_features(self, unit_df: pd.DataFrame) -> np.ndarray:
82
+ unit_df = unit_df.sort_values(self.time_col)
83
+
84
+ all_stats = {}
85
+ # FIXME: actually compute some features
86
+
87
+
88
+ feature_names = sorted(all_stats.keys())
89
+ self._feature_names_ = feature_names # stored for inspection/debugging
90
+ return np.array([all_stats[k] for k in feature_names], dtype=float)
91
+
92
+ def _build_feature_matrix(self, timeseries: pd.DataFrame) -> tuple[np.ndarray, list]:
93
+ rows, ids = [], []
94
+ for unit_id, unit_df in timeseries.groupby(self.group_col):
95
+ if len(unit_df) < 2:
96
+ continue
97
+ rows.append(self._compute_features(unit_df))
98
+ ids.append(unit_id)
99
+ return np.vstack(rows), ids
100
+
101
+ def fit(self, timeseries: pd.DataFrame, rul: pd.Series):
102
+ X, ids = self._build_feature_matrix(timeseries)
103
+ y = np.array([rul[unit_id] for unit_id in ids])
104
+
105
+ # FIXME: actually use an estimator that learns
106
+ self.model = DummyRegressor(strategy='median')
107
+ self.model.fit(X, y)
108
+ return self
109
+
110
+ def predict(self, timeseries: pd.DataFrame) -> pd.DataFrame:
111
+ rows, ids = [], []
112
+ for unit_id, unit_df in timeseries.groupby(self.group_col):
113
+ if len(unit_df) < 2:
114
+ raise ValueError(f"too short unit={unit_id} len={len(unit_df)}")
115
+ rows.append(self._compute_features(unit_df))
116
+ ids.append(unit_id)
117
+
118
+ X = np.vstack(rows)
119
+
120
+ # every quantile column just gets the single point prediction.
121
+ point_pred = self.model.predict(X)
122
+ preds = {col: point_pred for col in self.quantile_cols}
123
+
124
+ out = pd.DataFrame(preds, index=pd.Index(ids, name=self.group_col))
125
+ return out[self.quantile_cols]
126
+
127
+
128
+ def train_rul_model(locations, timeseries, eol_times, scenarios):
129
+
130
+ # Collect training data
131
+ # FIXME: train/validate/test split to estimate generalized predictive performance
132
+ gen = iterate_scenarios(locations, timeseries, eol_times, scenarios)
133
+ cut_dfs = []
134
+ cut_ruls = []
135
+ for scenario, locs, cut, eol in gen:
136
+
137
+ cut = cut.reset_index()
138
+ cut['device_id'] = cut['device_id'].astype(str) + scenario['name']
139
+ cut = cut.set_index(['device_id', 'end_time'])
140
+ cut_dfs.append(cut)
141
+ plan_start = pandas.Timestamp(scenario['start_time'])
142
+
143
+ # Convert EOL datetimes to RUL in days relative to planning time
144
+ rul_days = (eol - plan_start) / pandas.Timedelta(days=1)
145
+ rul_days.index = pandas.Series(rul_days.index) + scenario['name']
146
+ cut_ruls.append(rul_days)
147
+
148
+ assert set(rul_days.index) == set(cut.index.get_level_values('device_id'))
149
+
150
+
151
+
152
+ # Train a RUL prediction model
153
+ rul_model = DummyRULModel()
154
+ X = pandas.concat(cut_dfs)
155
+ Y = pandas.concat(cut_ruls)
156
+
157
+ print(X.head())
158
+ print(Y.head())
159
+
160
+ rul_model.fit(X, Y)
161
+
162
+ # FIXME: do model selection
163
+
164
+ return rul_model
165
+
166
+
167
+ class Config(BaseSettings):
168
+ """
169
+ Automatically provides command-line argument support for specified fields
170
+ """
171
+ model_config = SettingsConfigDict(
172
+ env_prefix="",
173
+ cli_parse_args=True,
174
+ cli_ignore_unknown_args=True,
175
+ )
176
+
177
+ dataset_path: Path = Path("dataset")
178
+ split : str = 'train'
179
+
180
  def main():
181
+ cfg = Config()
182
+
183
+ dataset_path = pathlib.Path(cfg.dataset_path)
184
 
185
+ split_path = dataset_path / cfg.split
186
+ locations, timeseries, eol_times, scenarios = load_dataset(split_path)
187
+
188
+ log.info('evaluate-load-data', path=dataset_path)
189
+
190
+
191
+ # Prediction model training
192
+ rul_model = train_rul_model(locations, timeseries, eol_times, scenarios)
193
+
194
+
195
+ # Evaluate on the planning scenarios
196
+ gen = iterate_scenarios(locations, timeseries, eol_times, scenarios)
197
+ for scenario, locs, cut, eol in gen:
198
+ scenario_name = scenario['name']
199
+ travel_costs = scenario['travel_costs']
200
+ settings = scenario['settings']
201
+
202
+ planner = OrderedPlanner(rul_model)
203
+ plan = planner.plan(cut, locs, travel_costs, settings)
204
+
205
+ start_time = pandas.Timestamp(scenario['start_time'])
206
+
207
+ transitions, daily, overall = evaluate_plan(plan, locs, travel_costs, settings, eol_times=eol, start_time=start_time)
208
+
209
+ print('scores', scenario_name, overall)
210
+
211
+
212
+ # Save best planner
213
+ planner = OrderedPlanner(rul_model)
214
+
215
+ planner_path = 'batteryswap_example/planners/best.pickle'
216
  with open(planner_path, "wb") as f:
217
  pickle.dump(planner, f)
218
  print('planner-save', planner_path)
219
 
220
+
221
  if __name__ == '__main__':
222
  main()