NeerajCodz commited on
Commit
1552b5a
·
1 Parent(s): 4229df6

feat: HF model repo at v1/v2 root; version selector UI with download submenu; artifacts/ out of git

Browse files
Files changed (33) hide show
  1. .gitignore +3 -18
  2. Dockerfile +1 -2
  3. api/main.py +64 -1
  4. artifacts/v1/results/classical_rul_results.csv +0 -4
  5. artifacts/v1/results/classical_soh_results.csv +0 -11
  6. artifacts/v1/results/dg_itransformer_results.json +0 -9
  7. artifacts/v1/results/ensemble_results.csv +0 -9
  8. artifacts/v1/results/final_rankings.csv +0 -23
  9. artifacts/v1/results/lstm_soh_results.csv +0 -5
  10. artifacts/v1/results/transformer_soh_results.csv +0 -5
  11. artifacts/v1/results/unified_results.csv +0 -23
  12. artifacts/v1/results/vae_lstm_results.json +0 -8
  13. artifacts/v2/results/battery_features.csv +0 -0
  14. artifacts/v2/results/classical_rul_results.csv +0 -4
  15. artifacts/v2/results/classical_soh_results.csv +0 -11
  16. artifacts/v2/results/dg_itransformer_results.json +0 -9
  17. artifacts/v2/results/ensemble_results.csv +0 -9
  18. artifacts/v2/results/final_rankings.csv +0 -23
  19. artifacts/v2/results/lstm_soh_results.csv +0 -5
  20. artifacts/v2/results/transformer_soh_results.csv +0 -5
  21. artifacts/v2/results/unified_results.csv +0 -23
  22. artifacts/v2/results/v2_classical_results.csv +0 -9
  23. artifacts/v2/results/v2_intra_battery.json +0 -9
  24. artifacts/v2/results/v2_model_validation.csv +0 -17
  25. artifacts/v2/results/v2_training_summary.json +0 -14
  26. artifacts/v2/results/v2_validation_report.html +0 -0
  27. artifacts/v2/results/v2_validation_summary.json +0 -11
  28. artifacts/v2/results/vae_lstm_results.json +0 -8
  29. frontend/src/App.tsx +6 -18
  30. frontend/src/api.ts +15 -0
  31. frontend/src/components/VersionSelector.tsx +220 -0
  32. scripts/download_models.py +91 -50
  33. scripts/upload_models_to_hub.py +21 -7
.gitignore CHANGED
@@ -1,23 +1,8 @@
1
  # ─────────────────────────────────────────────────────────────────────────────
2
- # Binary artifacts figures, notebooks, numpy arrays (too large / not needed)
 
3
  # ─────────────────────────────────────────────────────────────────────────────
4
- artifacts/v1/figures/
5
- artifacts/v2/figures/
6
- artifacts/v2/reports/
7
- artifacts/v2/results/*.npz
8
- artifacts/v2/results/*.png
9
- notebooks/
10
- # ─────────────────────────────────────────────────────────────────────────────
11
- # Model artifacts — stored on HF Hub, NOT in git
12
- # Download via: python scripts/download_models.py
13
- # or automatically on Docker startup
14
- # ─────────────────────────────────────────────────────────────────────────────
15
- artifacts/v1/models/
16
- artifacts/v2/models/
17
- artifacts/v1/scalers/
18
- artifacts/v2/scalers/
19
- # Sentinel written by download_models.py
20
- artifacts/.hf_downloaded
21
 
22
  # Python
23
  __pycache__/
 
1
  # ─────────────────────────────────────────────────────────────────────────────
2
+ # Entire artifacts/ lives on HF Hub model repo (NeerajCodz/aiBatteryLifeCycle)
3
+ # Downloaded at Docker startup via scripts/download_models.py
4
  # ─────────────────────────────────────────────────────────────────────────────
5
+ artifacts/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  # Python
8
  __pycache__/
Dockerfile CHANGED
@@ -43,12 +43,11 @@ RUN mkdir -p artifacts/v1/models/classical artifacts/v1/models/deep \
43
  artifacts/v2/scalers artifacts/v2/results artifacts/v2/reports \
44
  artifacts/logs
45
 
46
- # Copy project source
47
  COPY src/ src/
48
  COPY api/ api/
49
  COPY scripts/ scripts/
50
  COPY cleaned_dataset/ cleaned_dataset/
51
- COPY artifacts/ artifacts/
52
 
53
  # Copy built frontend
54
  COPY --from=frontend-build /app/frontend/dist frontend/dist
 
43
  artifacts/v2/scalers artifacts/v2/results artifacts/v2/reports \
44
  artifacts/logs
45
 
46
+ # Copy project source (artifacts/ is NOT in git — downloaded at runtime)
47
  COPY src/ src/
48
  COPY api/ api/
49
  COPY scripts/ scripts/
50
  COPY cleaned_dataset/ cleaned_dataset/
 
51
 
52
  # Copy built frontend
53
  COPY --from=frontend-build /app/frontend/dist frontend/dist
api/main.py CHANGED
@@ -42,10 +42,11 @@ Docker
42
 
43
  from __future__ import annotations
44
 
 
45
  from contextlib import asynccontextmanager
46
  from pathlib import Path
47
 
48
- from fastapi import FastAPI
49
  from fastapi.middleware.cors import CORSMiddleware
50
  from fastapi.staticfiles import StaticFiles
51
  from fastapi.responses import FileResponse
@@ -110,6 +111,68 @@ async def health():
110
  )
111
 
112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  # ── Include routers ──────────────────────────────────────────────────────────
114
  from api.routers.predict import router as predict_router, v1_router
115
  from api.routers.predict_v2 import router as predict_v2_router
 
42
 
43
  from __future__ import annotations
44
 
45
+ import asyncio
46
  from contextlib import asynccontextmanager
47
  from pathlib import Path
48
 
49
+ from fastapi import BackgroundTasks, FastAPI, HTTPException
50
  from fastapi.middleware.cors import CORSMiddleware
51
  from fastapi.staticfiles import StaticFiles
52
  from fastapi.responses import FileResponse
 
111
  )
112
 
113
 
114
+ # ── Version management ───────────────────────────────────────────────────────
115
+ _REGISTRIES = {"v1": registry_v1, "v2": registry_v2}
116
+ _version_status: dict[str, str] = {} # "downloading" | "ready" | "error"
117
+
118
+
119
+ def _artifacts_dir() -> Path:
120
+ return Path(__file__).resolve().parent.parent / "artifacts"
121
+
122
+
123
+ def _version_loaded(version: str) -> bool:
124
+ base = _artifacts_dir() / version / "models" / "classical"
125
+ return any(base.glob("*.joblib")) if base.exists() else False
126
+
127
+
128
+ @app.get("/api/versions", tags=["meta"])
129
+ async def list_versions():
130
+ """Return all known versions with loaded / downloading status."""
131
+ return [
132
+ {
133
+ "id": v,
134
+ "display": f"Version {v[1]}",
135
+ "loaded": _version_loaded(v),
136
+ "model_count": _REGISTRIES[v].model_count,
137
+ "status": _version_status.get(v, "ready" if _version_loaded(v) else "not_downloaded"),
138
+ }
139
+ for v in ["v2", "v1"]
140
+ ]
141
+
142
+
143
+ async def _bg_load_version(version: str) -> None:
144
+ import subprocess, sys as _sys
145
+ try:
146
+ proc = await asyncio.create_subprocess_exec(
147
+ _sys.executable, "scripts/download_models.py", "--version", version,
148
+ stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT,
149
+ )
150
+ await proc.wait()
151
+ if proc.returncode == 0:
152
+ _REGISTRIES[version].load_all()
153
+ _version_status[version] = "ready"
154
+ log.info("Version %s loaded on demand — %d models", version,
155
+ _REGISTRIES[version].model_count)
156
+ else:
157
+ _version_status[version] = "error"
158
+ log.error("download_models.py failed for version %s", version)
159
+ except Exception as exc:
160
+ _version_status[version] = "error"
161
+ log.error("Failed to load version %s: %s", version, exc)
162
+
163
+
164
+ @app.post("/api/versions/{version}/load", tags=["meta"])
165
+ async def load_version(version: str, background_tasks: BackgroundTasks):
166
+ """Download + activate a model version from HF Hub (runs in background)."""
167
+ if version not in _REGISTRIES:
168
+ raise HTTPException(status_code=400, detail=f"Unknown version '{version}'")
169
+ if _version_status.get(version) == "downloading":
170
+ return {"status": "downloading", "version": version}
171
+ _version_status[version] = "downloading"
172
+ background_tasks.add_task(_bg_load_version, version)
173
+ return {"status": "downloading", "version": version}
174
+
175
+
176
  # ── Include routers ──────────────────────────────────────────────────────────
177
  from api.routers.predict import router as predict_router, v1_router
178
  from api.routers.predict_v2 import router as predict_v2_router
artifacts/v1/results/classical_rul_results.csv DELETED
@@ -1,4 +0,0 @@
1
- model,MAE,MSE,RMSE,R2,MAPE,tolerance_acc_5cyc
2
- RandomForest,8.942333980582523,137.23446964660195,11.714711675777682,-0.15748641443851108,120.88277153106779,0.4058252427184466
3
- XGBoost,6.7578043937683105,74.80780029296875,8.649150264214905,0.3690432906150818,90.60895087232073,0.429126213592233
4
- LightGBM,9.174716686966466,125.80377799208308,11.216228331845027,-0.06107572161612751,112.25501529299473,0.35145631067961164
 
 
 
 
 
artifacts/v1/results/classical_soh_results.csv DELETED
@@ -1,11 +0,0 @@
1
- model,MAE,MSE,RMSE,R2,MAPE,tolerance_acc_2pct
2
- RandomForest,4.78051739475878,41.771168089034525,6.463061819991708,0.956679157080545,28.807727531282236,0.29514563106796116
3
- LightGBM,6.909989455704782,89.23030742727897,9.446179514876846,0.9074593240133356,56.94413475231037,0.24854368932038834
4
- XGBoost,8.506303251021016,136.22476526016746,11.67153654238239,0.8587214117403497,72.81562821369698,0.22330097087378642
5
- SVR,7.562130215902278,187.88576938734298,13.707143006014892,0.805143828272144,97.82839183781172,0.32233009708737864
6
- KNN-10,11.665738368704334,265.7682207905257,16.302399234177948,0.7243720041223407,89.92209971367193,0.26019417475728157
7
- KNN-5,11.751580021863887,270.3768092061512,16.44313866651228,0.7195924409938166,88.41470284019084,0.2757281553398058
8
- KNN-20,12.035078655061884,272.741740151536,16.514894494108525,0.71713977312056,101.98653839713165,0.2407766990291262
9
- ElasticNet,15.796038594619795,460.3763913475375,21.45638346384445,0.5225440358554927,144.4728811908027,0.06407766990291262
10
- Lasso,15.830927911593506,462.77574982037044,21.512223265398916,0.5200556632227833,145.46522399976544,0.05242718446601942
11
- Ridge,15.860549029501184,464.1921925903698,21.545119925179574,0.5185866716299998,145.65718178268125,0.05048543689320388
 
 
 
 
 
 
 
 
 
 
 
 
artifacts/v1/results/dg_itransformer_results.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "MAE": 12.886456320718098,
3
- "MSE": 323.38418900793243,
4
- "RMSE": 17.982886003306934,
5
- "R2": 0.12310012848141882,
6
- "MAPE": 69.98234771512423,
7
- "tol_2pct": 0.04827586206896552,
8
- "tol_5pct": 0.2896551724137931
9
- }
 
 
 
 
 
 
 
 
 
 
artifacts/v1/results/ensemble_results.csv DELETED
@@ -1,9 +0,0 @@
1
- model,MAE,MSE,RMSE,R2,MAPE,tol_2pct
2
- Weighted Avg Ensemble,3.737822790616429,37.739016006374186,6.1432089339671805,0.8976655649469139,5.478476687258817,0.3482758620689655
3
- tft,4.732738753085752,46.68825874782447,6.832880706394959,0.8733984854887593,7.499287950788588,0.21379310344827587
4
- Stacking Ensemble,5.76908337148985,60.03540223313905,7.7482515597481125,0.8372059046352616,10.924057540803656,0.12413793103448276
5
- vae_lstm,8.494939970437716,100.78674732190278,10.039260297546965,0.7267031327397879,14.250142040133243,0.09310344827586207
6
- batterygpt,8.020673063309337,129.06954589210812,11.360877866261397,0.6500105074494692,12.874349389916773,0.28620689655172415
7
- vanilla_lstm,10.561354979478219,155.95773910638056,12.488304092485118,0.5770995427937917,14.375050332959946,0.15862068965517243
8
- bidirectional_lstm,11.134867385317946,167.3343794115467,12.935779041540046,0.5462502472468515,17.124593156141298,0.10689655172413794
9
- attention_lstm,14.181327172488002,288.23989200564256,16.977629163273726,0.21839863277892768,24.827876450140888,0.15862068965517243
 
 
 
 
 
 
 
 
 
 
artifacts/v1/results/final_rankings.csv DELETED
@@ -1,23 +0,0 @@
1
- model,MAE,MSE,RMSE,R2,MAPE,tolerance_acc_2pct,tol_2pct,tol_5pct
2
- RandomForest,4.78051739475878,41.771168089034525,6.463061819991708,0.956679157080545,28.80772753128224,0.2951456310679611,,
3
- LightGBM,6.909989455704782,89.23030742727897,9.446179514876846,0.9074593240133356,56.94413475231037,0.2485436893203883,,
4
- Weighted Avg Ensemble,3.737822790616429,37.739016006374186,6.1432089339671805,0.8976655649469139,5.478476687258817,,0.3482758620689655,
5
- TFT,4.732738753085752,46.68825874782447,6.832880706394959,0.8733984854887593,7.499287950788588,,0.2137931034482758,
6
- XGBoost,8.506303251021016,136.22476526016746,11.67153654238239,0.8587214117403497,72.81562821369698,0.2233009708737864,,
7
- Stacking Ensemble,5.76908337148985,60.03540223313905,7.748251559748112,0.8372059046352616,10.924057540803656,,0.1241379310344827,
8
- SVR,7.562130215902278,187.88576938734295,13.707143006014892,0.805143828272144,97.82839183781172,0.3223300970873786,,
9
- VAE-LSTM,8.494939970437716,100.78674732190278,10.039260297546965,0.7267031327397879,14.250142040133243,,0.09310344827586207,
10
- KNN-10,11.665738368704334,265.7682207905257,16.302399234177948,0.7243720041223407,89.92209971367193,0.2601941747572815,,
11
- KNN-5,11.751580021863887,270.3768092061512,16.44313866651228,0.7195924409938166,88.41470284019084,0.2757281553398058,,
12
- KNN-20,12.035078655061884,272.741740151536,16.514894494108525,0.71713977312056,101.98653839713164,0.2407766990291262,,
13
- BatteryGPT,8.020673063309337,129.06954589210812,11.360877866261395,0.6500105074494692,12.874349389916771,,0.2862068965517241,
14
- GRU,9.275809104339835,134.65458890701777,11.604076391812397,0.6348659095727935,15.248492181524448,0.193103448275862,,
15
- Vanilla LSTM,10.56135497947822,155.95773910638056,12.488304092485118,0.5770995427937917,14.375050332959946,0.1586206896551724,,
16
- Bidirectional LSTM,11.134867385317946,167.3343794115467,12.935779041540046,0.5462502472468515,17.124593156141298,0.1068965517241379,,
17
- ElasticNet,15.796038594619796,460.3763913475375,21.45638346384445,0.5225440358554927,144.4728811908027,0.0640776699029126,,
18
- Lasso,15.830927911593506,462.7757498203704,21.51222326539892,0.5200556632227833,145.46522399976544,0.0524271844660194,,
19
- Ridge,15.860549029501184,464.1921925903698,21.545119925179574,0.5185866716299998,145.65718178268125,0.0504854368932038,,
20
- iTransformer,14.544141949115827,275.09890344946007,16.58610573490535,0.2540321967199925,73.33406651321724,,0.0172413793103448,
21
- Attention LSTM,14.181327172488002,288.23989200564256,16.977629163273726,0.2183986327789276,24.827876450140888,0.1586206896551724,,
22
- DG-iTransformer,14.183794754173379,313.5635374005159,17.707725359303375,0.1497301506825386,94.26231665878274,,0.1103448275862069,0.2
23
- Physics iTransformer,16.41156271618159,379.65023683665015,19.48461538847124,-0.0294728537142276,97.60205959509742,,0.0,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
artifacts/v1/results/lstm_soh_results.csv DELETED
@@ -1,5 +0,0 @@
1
- model,MAE,MSE,RMSE,R2,MAPE,tolerance_acc_2pct
2
- GRU,9.275809104339837,134.65458890701777,11.604076391812395,0.6348659095727935,15.248492181524448,0.19310344827586207
3
- Vanilla LSTM,10.561354979478219,155.95773910638056,12.488304092485118,0.5770995427937917,14.375050332959946,0.15862068965517243
4
- Bidirectional LSTM,11.134867385317946,167.3343794115467,12.935779041540046,0.5462502472468515,17.124593156141298,0.10689655172413794
5
- Attention LSTM,14.181327172488002,288.23989200564256,16.977629163273726,0.21839863277892768,24.827876450140888,0.15862068965517243
 
 
 
 
 
 
artifacts/v1/results/transformer_soh_results.csv DELETED
@@ -1,5 +0,0 @@
1
- model,MAE,MSE,RMSE,R2,MAPE,tol_2pct
2
- TFT,9.538144323660262,127.31655440584875,11.283463759229644,0.6547639804432781,18.98017853690174,0.05517241379310345
3
- BatteryGPT,9.614953606189902,135.34729964687295,11.633885836076997,0.6329875309153767,15.44417472623618,0.1793103448275862
4
- iTransformer,9.356759048993766,150.35062176683638,12.261754432659153,0.5923039981808029,14.4907916127095,0.1310344827586207
5
- Physics iTransformer,12.1375468649355,214.65431642999653,14.651085844741901,0.4179358518552816,24.985288391381786,0.07931034482758621
 
 
 
 
 
 
artifacts/v1/results/unified_results.csv DELETED
@@ -1,23 +0,0 @@
1
- model,MAE,MSE,RMSE,R2,MAPE,tolerance_acc_2pct,tol_2pct,tol_5pct
2
- RandomForest,4.78051739475878,41.771168089034525,6.463061819991708,0.956679157080545,28.80772753128224,0.2951456310679611,,
3
- LightGBM,6.909989455704782,89.23030742727897,9.446179514876846,0.9074593240133356,56.94413475231037,0.2485436893203883,,
4
- Weighted Avg Ensemble,3.737822790616429,37.739016006374186,6.1432089339671805,0.8976655649469139,5.478476687258817,,0.3482758620689655,
5
- TFT,4.732738753085752,46.68825874782447,6.832880706394959,0.8733984854887593,7.499287950788588,,0.2137931034482758,
6
- XGBoost,8.506303251021016,136.22476526016746,11.67153654238239,0.8587214117403497,72.81562821369698,0.2233009708737864,,
7
- Stacking Ensemble,5.76908337148985,60.03540223313905,7.748251559748112,0.8372059046352616,10.924057540803656,,0.1241379310344827,
8
- SVR,7.562130215902278,187.88576938734295,13.707143006014892,0.805143828272144,97.82839183781172,0.3223300970873786,,
9
- VAE-LSTM,8.494939970437716,100.78674732190278,10.039260297546965,0.7267031327397879,14.250142040133243,,0.09310344827586207,
10
- KNN-10,11.665738368704334,265.7682207905257,16.302399234177948,0.7243720041223407,89.92209971367193,0.2601941747572815,,
11
- KNN-5,11.751580021863887,270.3768092061512,16.44313866651228,0.7195924409938166,88.41470284019084,0.2757281553398058,,
12
- KNN-20,12.035078655061884,272.741740151536,16.514894494108525,0.71713977312056,101.98653839713164,0.2407766990291262,,
13
- BatteryGPT,8.020673063309337,129.06954589210812,11.360877866261395,0.6500105074494692,12.874349389916771,,0.2862068965517241,
14
- GRU,9.275809104339835,134.65458890701777,11.604076391812397,0.6348659095727935,15.248492181524448,0.193103448275862,,
15
- Vanilla LSTM,10.56135497947822,155.95773910638056,12.488304092485118,0.5770995427937917,14.375050332959946,0.1586206896551724,,
16
- Bidirectional LSTM,11.134867385317946,167.3343794115467,12.935779041540046,0.5462502472468515,17.124593156141298,0.1068965517241379,,
17
- ElasticNet,15.796038594619796,460.3763913475375,21.45638346384445,0.5225440358554927,144.4728811908027,0.0640776699029126,,
18
- Lasso,15.830927911593506,462.7757498203704,21.51222326539892,0.5200556632227833,145.46522399976544,0.0524271844660194,,
19
- Ridge,15.860549029501184,464.1921925903698,21.545119925179574,0.5185866716299998,145.65718178268125,0.0504854368932038,,
20
- iTransformer,14.544141949115827,275.09890344946007,16.58610573490535,0.2540321967199925,73.33406651321724,,0.0172413793103448,
21
- Attention LSTM,14.181327172488002,288.23989200564256,16.977629163273726,0.2183986327789276,24.827876450140888,0.1586206896551724,,
22
- DG-iTransformer,14.183794754173379,313.5635374005159,17.707725359303375,0.1497301506825386,94.26231665878274,,0.1103448275862069,0.2
23
- Physics iTransformer,16.41156271618159,379.65023683665015,19.48461538847124,-0.0294728537142276,97.60205959509742,,0.0,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
artifacts/v1/results/vae_lstm_results.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "MAE": 8.494939970437716,
3
- "MSE": 100.78674732190278,
4
- "RMSE": 10.039260297546965,
5
- "R2": 0.7267031327397879,
6
- "MAPE": 14.250142040133243,
7
- "tol_2pct": 0.09310344827586207
8
- }
 
 
 
 
 
 
 
 
 
artifacts/v2/results/battery_features.csv DELETED
The diff for this file is too large to render. See raw diff
 
artifacts/v2/results/classical_rul_results.csv DELETED
@@ -1,4 +0,0 @@
1
- model,MAE,MSE,RMSE,R2,MAPE,tolerance_acc_5cyc
2
- RandomForest,8.942333980582523,137.23446964660195,11.714711675777682,-0.15748641443851108,120.88277153106779,0.4058252427184466
3
- XGBoost,6.7578043937683105,74.80780029296875,8.649150264214905,0.3690432906150818,90.60895087232073,0.429126213592233
4
- LightGBM,9.174716686966466,125.80377799208308,11.216228331845027,-0.06107572161612751,112.25501529299473,0.35145631067961164
 
 
 
 
 
artifacts/v2/results/classical_soh_results.csv DELETED
@@ -1,11 +0,0 @@
1
- model,MAE,MSE,RMSE,R2,MAPE,tolerance_acc_2pct
2
- RandomForest,4.78051739475878,41.771168089034525,6.463061819991708,0.956679157080545,28.807727531282236,0.29514563106796116
3
- LightGBM,6.909989455704782,89.23030742727897,9.446179514876846,0.9074593240133356,56.94413475231037,0.24854368932038834
4
- XGBoost,8.506303251021016,136.22476526016746,11.67153654238239,0.8587214117403497,72.81562821369698,0.22330097087378642
5
- SVR,7.562130215902278,187.88576938734298,13.707143006014892,0.805143828272144,97.82839183781172,0.32233009708737864
6
- KNN-10,11.665738368704334,265.7682207905257,16.302399234177948,0.7243720041223407,89.92209971367193,0.26019417475728157
7
- KNN-5,11.751580021863887,270.3768092061512,16.44313866651228,0.7195924409938166,88.41470284019084,0.2757281553398058
8
- KNN-20,12.035078655061884,272.741740151536,16.514894494108525,0.71713977312056,101.98653839713165,0.2407766990291262
9
- ElasticNet,15.796038594619795,460.3763913475375,21.45638346384445,0.5225440358554927,144.4728811908027,0.06407766990291262
10
- Lasso,15.830927911593506,462.77574982037044,21.512223265398916,0.5200556632227833,145.46522399976544,0.05242718446601942
11
- Ridge,15.860549029501184,464.1921925903698,21.545119925179574,0.5185866716299998,145.65718178268125,0.05048543689320388
 
 
 
 
 
 
 
 
 
 
 
 
artifacts/v2/results/dg_itransformer_results.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "MAE": 12.886456320718098,
3
- "MSE": 323.38418900793243,
4
- "RMSE": 17.982886003306934,
5
- "R2": 0.12310012848141882,
6
- "MAPE": 69.98234771512423,
7
- "tol_2pct": 0.04827586206896552,
8
- "tol_5pct": 0.2896551724137931
9
- }
 
 
 
 
 
 
 
 
 
 
artifacts/v2/results/ensemble_results.csv DELETED
@@ -1,9 +0,0 @@
1
- model,MAE,MSE,RMSE,R2,MAPE,tol_2pct
2
- Weighted Avg Ensemble,3.737822790616429,37.739016006374186,6.1432089339671805,0.8976655649469139,5.478476687258817,0.3482758620689655
3
- tft,4.732738753085752,46.68825874782447,6.832880706394959,0.8733984854887593,7.499287950788588,0.21379310344827587
4
- Stacking Ensemble,5.76908337148985,60.03540223313905,7.7482515597481125,0.8372059046352616,10.924057540803656,0.12413793103448276
5
- vae_lstm,8.494939970437716,100.78674732190278,10.039260297546965,0.7267031327397879,14.250142040133243,0.09310344827586207
6
- batterygpt,8.020673063309337,129.06954589210812,11.360877866261397,0.6500105074494692,12.874349389916773,0.28620689655172415
7
- vanilla_lstm,10.561354979478219,155.95773910638056,12.488304092485118,0.5770995427937917,14.375050332959946,0.15862068965517243
8
- bidirectional_lstm,11.134867385317946,167.3343794115467,12.935779041540046,0.5462502472468515,17.124593156141298,0.10689655172413794
9
- attention_lstm,14.181327172488002,288.23989200564256,16.977629163273726,0.21839863277892768,24.827876450140888,0.15862068965517243
 
 
 
 
 
 
 
 
 
 
artifacts/v2/results/final_rankings.csv DELETED
@@ -1,23 +0,0 @@
1
- model,MAE,MSE,RMSE,R2,MAPE,tolerance_acc_2pct,tol_2pct,tol_5pct
2
- RandomForest,4.78051739475878,41.771168089034525,6.463061819991708,0.956679157080545,28.80772753128224,0.2951456310679611,,
3
- LightGBM,6.909989455704782,89.23030742727897,9.446179514876846,0.9074593240133356,56.94413475231037,0.2485436893203883,,
4
- Weighted Avg Ensemble,3.737822790616429,37.739016006374186,6.1432089339671805,0.8976655649469139,5.478476687258817,,0.3482758620689655,
5
- TFT,4.732738753085752,46.68825874782447,6.832880706394959,0.8733984854887593,7.499287950788588,,0.2137931034482758,
6
- XGBoost,8.506303251021016,136.22476526016746,11.67153654238239,0.8587214117403497,72.81562821369698,0.2233009708737864,,
7
- Stacking Ensemble,5.76908337148985,60.03540223313905,7.748251559748112,0.8372059046352616,10.924057540803656,,0.1241379310344827,
8
- SVR,7.562130215902278,187.88576938734295,13.707143006014892,0.805143828272144,97.82839183781172,0.3223300970873786,,
9
- VAE-LSTM,8.494939970437716,100.78674732190278,10.039260297546965,0.7267031327397879,14.250142040133243,,0.09310344827586207,
10
- KNN-10,11.665738368704334,265.7682207905257,16.302399234177948,0.7243720041223407,89.92209971367193,0.2601941747572815,,
11
- KNN-5,11.751580021863887,270.3768092061512,16.44313866651228,0.7195924409938166,88.41470284019084,0.2757281553398058,,
12
- KNN-20,12.035078655061884,272.741740151536,16.514894494108525,0.71713977312056,101.98653839713164,0.2407766990291262,,
13
- BatteryGPT,8.020673063309337,129.06954589210812,11.360877866261395,0.6500105074494692,12.874349389916771,,0.2862068965517241,
14
- GRU,9.275809104339835,134.65458890701777,11.604076391812397,0.6348659095727935,15.248492181524448,0.193103448275862,,
15
- Vanilla LSTM,10.56135497947822,155.95773910638056,12.488304092485118,0.5770995427937917,14.375050332959946,0.1586206896551724,,
16
- Bidirectional LSTM,11.134867385317946,167.3343794115467,12.935779041540046,0.5462502472468515,17.124593156141298,0.1068965517241379,,
17
- ElasticNet,15.796038594619796,460.3763913475375,21.45638346384445,0.5225440358554927,144.4728811908027,0.0640776699029126,,
18
- Lasso,15.830927911593506,462.7757498203704,21.51222326539892,0.5200556632227833,145.46522399976544,0.0524271844660194,,
19
- Ridge,15.860549029501184,464.1921925903698,21.545119925179574,0.5185866716299998,145.65718178268125,0.0504854368932038,,
20
- iTransformer,14.544141949115827,275.09890344946007,16.58610573490535,0.2540321967199925,73.33406651321724,,0.0172413793103448,
21
- Attention LSTM,14.181327172488002,288.23989200564256,16.977629163273726,0.2183986327789276,24.827876450140888,0.1586206896551724,,
22
- DG-iTransformer,14.183794754173379,313.5635374005159,17.707725359303375,0.1497301506825386,94.26231665878274,,0.1103448275862069,0.2
23
- Physics iTransformer,16.41156271618159,379.65023683665015,19.48461538847124,-0.0294728537142276,97.60205959509742,,0.0,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
artifacts/v2/results/lstm_soh_results.csv DELETED
@@ -1,5 +0,0 @@
1
- model,MAE,MSE,RMSE,R2,MAPE,tolerance_acc_2pct
2
- GRU,9.275809104339837,134.65458890701777,11.604076391812395,0.6348659095727935,15.248492181524448,0.19310344827586207
3
- Vanilla LSTM,10.561354979478219,155.95773910638056,12.488304092485118,0.5770995427937917,14.375050332959946,0.15862068965517243
4
- Bidirectional LSTM,11.134867385317946,167.3343794115467,12.935779041540046,0.5462502472468515,17.124593156141298,0.10689655172413794
5
- Attention LSTM,14.181327172488002,288.23989200564256,16.977629163273726,0.21839863277892768,24.827876450140888,0.15862068965517243
 
 
 
 
 
 
artifacts/v2/results/transformer_soh_results.csv DELETED
@@ -1,5 +0,0 @@
1
- model,MAE,MSE,RMSE,R2,MAPE,tol_2pct
2
- TFT,9.538144323660262,127.31655440584875,11.283463759229644,0.6547639804432781,18.98017853690174,0.05517241379310345
3
- BatteryGPT,9.614953606189902,135.34729964687295,11.633885836076997,0.6329875309153767,15.44417472623618,0.1793103448275862
4
- iTransformer,9.356759048993766,150.35062176683638,12.261754432659153,0.5923039981808029,14.4907916127095,0.1310344827586207
5
- Physics iTransformer,12.1375468649355,214.65431642999653,14.651085844741901,0.4179358518552816,24.985288391381786,0.07931034482758621
 
 
 
 
 
 
artifacts/v2/results/unified_results.csv DELETED
@@ -1,23 +0,0 @@
1
- model,MAE,MSE,RMSE,R2,MAPE,tolerance_acc_2pct,tol_2pct,tol_5pct
2
- RandomForest,4.78051739475878,41.771168089034525,6.463061819991708,0.956679157080545,28.80772753128224,0.2951456310679611,,
3
- LightGBM,6.909989455704782,89.23030742727897,9.446179514876846,0.9074593240133356,56.94413475231037,0.2485436893203883,,
4
- Weighted Avg Ensemble,3.737822790616429,37.739016006374186,6.1432089339671805,0.8976655649469139,5.478476687258817,,0.3482758620689655,
5
- TFT,4.732738753085752,46.68825874782447,6.832880706394959,0.8733984854887593,7.499287950788588,,0.2137931034482758,
6
- XGBoost,8.506303251021016,136.22476526016746,11.67153654238239,0.8587214117403497,72.81562821369698,0.2233009708737864,,
7
- Stacking Ensemble,5.76908337148985,60.03540223313905,7.748251559748112,0.8372059046352616,10.924057540803656,,0.1241379310344827,
8
- SVR,7.562130215902278,187.88576938734295,13.707143006014892,0.805143828272144,97.82839183781172,0.3223300970873786,,
9
- VAE-LSTM,8.494939970437716,100.78674732190278,10.039260297546965,0.7267031327397879,14.250142040133243,,0.09310344827586207,
10
- KNN-10,11.665738368704334,265.7682207905257,16.302399234177948,0.7243720041223407,89.92209971367193,0.2601941747572815,,
11
- KNN-5,11.751580021863887,270.3768092061512,16.44313866651228,0.7195924409938166,88.41470284019084,0.2757281553398058,,
12
- KNN-20,12.035078655061884,272.741740151536,16.514894494108525,0.71713977312056,101.98653839713164,0.2407766990291262,,
13
- BatteryGPT,8.020673063309337,129.06954589210812,11.360877866261395,0.6500105074494692,12.874349389916771,,0.2862068965517241,
14
- GRU,9.275809104339835,134.65458890701777,11.604076391812397,0.6348659095727935,15.248492181524448,0.193103448275862,,
15
- Vanilla LSTM,10.56135497947822,155.95773910638056,12.488304092485118,0.5770995427937917,14.375050332959946,0.1586206896551724,,
16
- Bidirectional LSTM,11.134867385317946,167.3343794115467,12.935779041540046,0.5462502472468515,17.124593156141298,0.1068965517241379,,
17
- ElasticNet,15.796038594619796,460.3763913475375,21.45638346384445,0.5225440358554927,144.4728811908027,0.0640776699029126,,
18
- Lasso,15.830927911593506,462.7757498203704,21.51222326539892,0.5200556632227833,145.46522399976544,0.0524271844660194,,
19
- Ridge,15.860549029501184,464.1921925903698,21.545119925179574,0.5185866716299998,145.65718178268125,0.0504854368932038,,
20
- iTransformer,14.544141949115827,275.09890344946007,16.58610573490535,0.2540321967199925,73.33406651321724,,0.0172413793103448,
21
- Attention LSTM,14.181327172488002,288.23989200564256,16.977629163273726,0.2183986327789276,24.827876450140888,0.1586206896551724,,
22
- DG-iTransformer,14.183794754173379,313.5635374005159,17.707725359303375,0.1497301506825386,94.26231665878274,,0.1103448275862069,0.2
23
- Physics iTransformer,16.41156271618159,379.65023683665015,19.48461538847124,-0.0294728537142276,97.60205959509742,,0.0,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
artifacts/v2/results/v2_classical_results.csv DELETED
@@ -1,9 +0,0 @@
1
- model,r2,mae,within_5pct
2
- extra_trees,0.9545111863206289,1.325439106747622,99.27007299270073
3
- svr,0.9749418151896808,0.8447528977381709,99.27007299270073
4
- ridge,0.96260007301777,1.238689808585007,99.27007299270073
5
- xgboost,0.9701536078007467,1.276102318230126,98.72262773722628
6
- gradient_boosting,0.9427669688063848,1.4114545626753392,98.54014598540147
7
- knn_k5,0.9303819293635062,1.7036079148305412,97.62773722627736
8
- random_forest,0.9518568236961867,1.6867479514298702,96.71532846715328
9
- lightgbm,0.9560384659829149,1.5661312778770975,95.98540145985402
 
 
 
 
 
 
 
 
 
 
artifacts/v2/results/v2_intra_battery.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "target": "within_5pct >= 95",
3
- "passed_models": 8,
4
- "total_models": 8,
5
- "best_model": "extra_trees",
6
- "best_within_5pct": 99.27007299270073,
7
- "best_r2": 0.9545111863206289,
8
- "notes": "XGBoost\u2192LGB+GB Ensemble, Ridge\u2192ExtraTrees-Scaled, KNN\u2192SVR Ensemble"
9
- }
 
 
 
 
 
 
 
 
 
 
artifacts/v2/results/v2_model_validation.csv DELETED
@@ -1,17 +0,0 @@
1
- model,mae,rmse,r2,within_2pct,within_5pct,passed_95
2
- random_forest,0.9087291273437881,1.969167860523798,0.9768094711831841,83.75912408759125,95.07299270072993,True
3
- svr,1.6349861755580366,3.4846680412456976,0.9273780344781068,82.48175182481752,88.86861313868614,False
4
- xgboost,1.0395094776911078,2.3264169017858896,0.9676316722415876,82.2992700729927,88.86861313868614,False
5
- lightgbm,1.5073643559068495,2.9029505644051135,0.9496007058102836,81.02189781021897,88.13868613138686,False
6
- knn_k20,2.486675866923982,6.500672824933502,0.7472670935266853,84.12408759124088,85.21897810218978,False
7
- knn_k10,2.537245189395581,6.643166546082999,0.7360659298020076,84.48905109489051,84.67153284671532,False
8
- knn_k5,2.5366661790854557,6.661080117527008,0.734640592527769,84.48905109489051,84.67153284671532,False
9
- lasso,6.338197702892737,7.855862262540625,0.63090947633286,13.321167883211679,49.63503649635037,False
10
- ridge,6.354219600702956,7.877916808246509,0.6288341980649439,13.138686131386862,49.63503649635037,False
11
- elasticnet,6.359292593403684,7.8720819252103045,0.6293838121478381,13.503649635036496,49.27007299270073,False
12
- physics_itransformer,11.942222882334457,19.560215899810355,-1.288191997636034,15.875912408759124,44.70802919708029,False
13
- itransformer,18.775672188288702,24.807532802455988,-2.680546617377897,10.948905109489052,25.18248175182482,False
14
- dynamic_graph_itransformer,14.5743662824896,18.019296255058386,-0.9418730093540923,6.204379562043796,17.335766423357665,False
15
- extra_trees,21.583514681220528,25.105434141924782,-2.769473079864784,5.839416058394161,9.48905109489051,False
16
- gradient_boosting,29.48091148539111,32.124687008446905,-5.1719583162447185,0.0,0.18248175182481752,False
17
- best_rul_model,61.6389790255578,62.979116792145476,-22.721290167829615,0.0,0.0,False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
artifacts/v2/results/v2_training_summary.json DELETED
@@ -1,14 +0,0 @@
1
- {
2
- "version": "v2.0",
3
- "timestamp": "2026-02-25T18:16:44.705048",
4
- "total_models": 12,
5
- "passed_models": 5,
6
- "pass_rate_pct": 41.66666666666667,
7
- "best_model": "extra_trees",
8
- "best_within_5pct": 99.27007299270073,
9
- "best_r2": 0.9545111863206289,
10
- "mean_within_5pct": 77.38746958637469,
11
- "train_samples": 2130,
12
- "test_samples": 548,
13
- "batteries": 30
14
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
artifacts/v2/results/v2_validation_report.html DELETED
File without changes
artifacts/v2/results/v2_validation_summary.json DELETED
@@ -1,11 +0,0 @@
1
- {
2
- "timestamp": "2026-02-25T16:31:44.904601",
3
- "test_samples": 548,
4
- "test_batteries": 30,
5
- "total_models_tested": 16,
6
- "models_passed_95pct": 1,
7
- "overall_pass_rate_pct": 6.25,
8
- "best_model": "random_forest",
9
- "best_within_5pct": 95.07299270072993,
10
- "mean_within_5pct": 53.809306569343065
11
- }
 
 
 
 
 
 
 
 
 
 
 
 
artifacts/v2/results/vae_lstm_results.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "MAE": 8.494939970437716,
3
- "MSE": 100.78674732190278,
4
- "RMSE": 10.039260297546965,
5
- "R2": 0.7267031327397879,
6
- "MAPE": 14.250142040133243,
7
- "tol_2pct": 0.09310344827586207
8
- }
 
 
 
 
 
 
 
 
 
frontend/src/App.tsx CHANGED
@@ -5,6 +5,7 @@ import GraphPanel from "./components/GraphPanel";
5
  import RecommendationPanel from "./components/RecommendationPanel";
6
  import MetricsPanel from "./components/MetricsPanel";
7
  import ResearchPaper from "./components/ResearchPaper";
 
8
  import { ToastProvider } from "./components/Toast";
9
  import { getApiVersion, setApiVersion } from "./api";
10
  import { BatteryCharging } from "lucide-react";
@@ -42,24 +43,11 @@ export default function App() {
42
  <h1 className="text-lg font-bold">AI Battery Lifecycle Predictor</h1>
43
  </div>
44
  <div className="flex items-center gap-4">
45
- {/* Version toggle */}
46
- <div className="flex items-center gap-2 bg-gray-800 rounded-lg p-1">
47
- {(["v1", "v2"] as const).map((v) => (
48
- <button
49
- key={v}
50
- onClick={() => handleVersionChange(v)}
51
- className={`px-3 py-1 rounded text-xs font-bold transition-colors ${
52
- apiVersion === v
53
- ? v === "v2"
54
- ? "bg-green-600 text-white"
55
- : "bg-blue-600 text-white"
56
- : "text-gray-400 hover:text-white"
57
- }`}
58
- >
59
- {v.toUpperCase()}
60
- </button>
61
- ))}
62
- </div>
63
  <nav className="flex gap-1">
64
  {tabs.map((t) => (
65
  <button
 
5
  import RecommendationPanel from "./components/RecommendationPanel";
6
  import MetricsPanel from "./components/MetricsPanel";
7
  import ResearchPaper from "./components/ResearchPaper";
8
+ import VersionSelector from "./components/VersionSelector";
9
  import { ToastProvider } from "./components/Toast";
10
  import { getApiVersion, setApiVersion } from "./api";
11
  import { BatteryCharging } from "lucide-react";
 
43
  <h1 className="text-lg font-bold">AI Battery Lifecycle Predictor</h1>
44
  </div>
45
  <div className="flex items-center gap-4">
46
+ {/* Version selector with submenu */}
47
+ <VersionSelector
48
+ activeVersion={apiVersion}
49
+ onSwitch={handleVersionChange}
50
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  <nav className="flex gap-1">
52
  {tabs.map((t) => (
53
  <button
frontend/src/api.ts CHANGED
@@ -66,6 +66,21 @@ export interface ModelVersionGroups {
66
  default_model: string | null;
67
  }
68
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  export interface BatteryVizData {
70
  battery_id: string;
71
  soh_pct: number;
 
66
  default_model: string | null;
67
  }
68
 
69
+ // ── Version management ───────────────────────────────────────────────────────
70
+ export interface VersionInfo {
71
+ id: string; // "v1" | "v2"
72
+ display: string; // "Version 1" | "Version 2"
73
+ loaded: boolean;
74
+ model_count: number;
75
+ status: "ready" | "not_downloaded" | "downloading" | "error";
76
+ }
77
+
78
+ export const fetchVersions = () =>
79
+ baseApi.get<VersionInfo[]>("/versions").then((r) => r.data);
80
+
81
+ export const loadVersion = (version: string) =>
82
+ baseApi.post<{ status: string; version: string }>(`/versions/${version}/load`).then((r) => r.data);
83
+
84
  export interface BatteryVizData {
85
  battery_id: string;
86
  soh_pct: number;
frontend/src/components/VersionSelector.tsx ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * VersionSelector
3
+ *
4
+ * Shows the active API version badge ("Version 2") with a chevron icon.
5
+ * Clicking opens a submenu listing all available versions:
6
+ * - Active version: shown with a check mark
7
+ * - Downloaded but inactive: "Switch" button
8
+ * - Not yet downloaded: Download icon button → triggers server-side download
9
+ * then auto-switches when ready
10
+ */
11
+
12
+ import { useCallback, useEffect, useRef, useState } from "react";
13
+ import {
14
+ ChevronRight, Download, Check, RefreshCw, AlertCircle, Layers,
15
+ } from "lucide-react";
16
+ import { fetchVersions, loadVersion, VersionInfo } from "../api";
17
+
18
+ interface Props {
19
+ activeVersion: "v1" | "v2";
20
+ onSwitch: (v: "v1" | "v2") => void;
21
+ }
22
+
23
+ export default function VersionSelector({ activeVersion, onSwitch }: Props) {
24
+ const [open, setOpen] = useState(false);
25
+ const [versions, setVersions] = useState<VersionInfo[]>([]);
26
+ const [busy, setBusy] = useState<string | null>(null); // version being downloaded
27
+ const menuRef = useRef<HTMLDivElement>(null);
28
+ const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
29
+
30
+ // ── Load version list ────────────────────────────────────────────────────
31
+ const refresh = useCallback(() => {
32
+ fetchVersions()
33
+ .then(setVersions)
34
+ .catch(() => {});
35
+ }, []);
36
+
37
+ useEffect(() => {
38
+ refresh();
39
+ }, [refresh]);
40
+
41
+ // ── Poll while a download is in progress ────────────────────────────────
42
+ useEffect(() => {
43
+ const hasDownloading = versions.some((v) => v.status === "downloading");
44
+ if (hasDownloading && !pollRef.current) {
45
+ pollRef.current = setInterval(refresh, 2500);
46
+ }
47
+ if (!hasDownloading && pollRef.current) {
48
+ clearInterval(pollRef.current);
49
+ pollRef.current = null;
50
+ setBusy(null);
51
+ }
52
+ return () => {
53
+ if (pollRef.current) clearInterval(pollRef.current);
54
+ };
55
+ }, [versions, refresh]);
56
+
57
+ // ── Close on outside click ───────────────────────────────────────────────
58
+ useEffect(() => {
59
+ if (!open) return;
60
+ const handler = (e: MouseEvent) => {
61
+ if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
62
+ setOpen(false);
63
+ }
64
+ };
65
+ document.addEventListener("mousedown", handler);
66
+ return () => document.removeEventListener("mousedown", handler);
67
+ }, [open]);
68
+
69
+ // ── Actions ──────────────────────────────────────────────────────────────
70
+ const handleDownload = async (version: string) => {
71
+ setBusy(version);
72
+ try {
73
+ await loadVersion(version);
74
+ refresh(); // poll will take over
75
+ } catch {
76
+ setBusy(null);
77
+ }
78
+ };
79
+
80
+ const handleSwitch = (version: string) => {
81
+ onSwitch(version as "v1" | "v2");
82
+ setOpen(false);
83
+ };
84
+
85
+ const activeDisplay = versions.find((v) => v.id === activeVersion)?.display
86
+ ?? `Version ${activeVersion[1]}`;
87
+
88
+ // Versions that are NOT the active one
89
+ const others = versions.filter((v) => v.id !== activeVersion);
90
+
91
+ return (
92
+ <div className="relative" ref={menuRef}>
93
+ {/* Badge button */}
94
+ <button
95
+ onClick={() => setOpen((o) => !o)}
96
+ className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold
97
+ transition-colors select-none
98
+ ${activeVersion === "v2"
99
+ ? "bg-green-600 text-white hover:bg-green-500"
100
+ : "bg-blue-600 text-white hover:bg-blue-500"}`}
101
+ title="Switch model version"
102
+ >
103
+ <Layers className="w-3.5 h-3.5 opacity-80" />
104
+ {activeDisplay}
105
+ <ChevronRight
106
+ className={`w-3.5 h-3.5 transition-transform duration-200
107
+ ${open ? "rotate-90" : ""}`}
108
+ />
109
+ </button>
110
+
111
+ {/* Dropdown */}
112
+ {open && (
113
+ <div
114
+ className="absolute right-0 top-full mt-2 w-56
115
+ bg-gray-900 border border-gray-700 rounded-xl shadow-2xl z-50
116
+ overflow-hidden"
117
+ >
118
+ {/* Header */}
119
+ <div className="px-3 py-2 border-b border-gray-700 text-xs text-gray-400 font-medium">
120
+ Model Versions
121
+ </div>
122
+
123
+ {/* Active version row */}
124
+ <div className="flex items-center justify-between px-3 py-2.5 bg-gray-800/50">
125
+ <div>
126
+ <span className="text-sm font-semibold text-white">{activeDisplay}</span>
127
+ <span className="ml-2 text-xs text-green-400">active</span>
128
+ </div>
129
+ <Check className="w-4 h-4 text-green-400 shrink-0" />
130
+ </div>
131
+
132
+ {/* Other versions */}
133
+ {others.length === 0 && (
134
+ <div className="px-3 py-3 text-xs text-gray-500 text-center">
135
+ No other versions available
136
+ </div>
137
+ )}
138
+ {others.map((v) => {
139
+ const isDownloading = v.status === "downloading" || busy === v.id;
140
+ const isError = v.status === "error";
141
+ const canSwitch = v.loaded && !isDownloading;
142
+
143
+ return (
144
+ <div
145
+ key={v.id}
146
+ className="flex items-center justify-between px-3 py-2.5
147
+ hover:bg-gray-800/60 transition-colors"
148
+ >
149
+ <div>
150
+ <span className="text-sm font-medium text-gray-200">{v.display}</span>
151
+ {v.loaded && v.model_count > 0 && (
152
+ <span className="ml-2 text-xs text-gray-500">
153
+ {v.model_count} models
154
+ </span>
155
+ )}
156
+ {isError && (
157
+ <span className="ml-2 text-xs text-red-400">error</span>
158
+ )}
159
+ {isDownloading && (
160
+ <span className="ml-2 text-xs text-yellow-400 animate-pulse">
161
+ downloading…
162
+ </span>
163
+ )}
164
+ </div>
165
+
166
+ <div className="flex items-center gap-1 shrink-0">
167
+ {/* Switch button — visible when loaded */}
168
+ {canSwitch && (
169
+ <button
170
+ onClick={() => handleSwitch(v.id)}
171
+ className="px-2 py-0.5 rounded text-xs font-medium
172
+ bg-blue-700 hover:bg-blue-600 text-white transition-colors"
173
+ title={`Switch to ${v.display}`}
174
+ >
175
+ Load
176
+ </button>
177
+ )}
178
+
179
+ {/* Download button — visible when NOT loaded and NOT downloading */}
180
+ {!v.loaded && !isDownloading && !isError && (
181
+ <button
182
+ onClick={() => handleDownload(v.id)}
183
+ className="p-1.5 rounded-lg bg-gray-700 hover:bg-gray-600
184
+ text-gray-300 hover:text-white transition-colors"
185
+ title={`Download ${v.display} from HF Hub`}
186
+ >
187
+ <Download className="w-3.5 h-3.5" />
188
+ </button>
189
+ )}
190
+
191
+ {/* Spinner while downloading */}
192
+ {isDownloading && (
193
+ <RefreshCw className="w-3.5 h-3.5 text-yellow-400 animate-spin" />
194
+ )}
195
+
196
+ {/* Error retry */}
197
+ {isError && !isDownloading && (
198
+ <button
199
+ onClick={() => handleDownload(v.id)}
200
+ className="p-1.5 rounded-lg bg-gray-700 hover:bg-red-700
201
+ text-red-400 hover:text-white transition-colors"
202
+ title="Retry download"
203
+ >
204
+ <AlertCircle className="w-3.5 h-3.5" />
205
+ </button>
206
+ )}
207
+ </div>
208
+ </div>
209
+ );
210
+ })}
211
+
212
+ {/* Footer hint */}
213
+ <div className="px-3 py-2 border-t border-gray-700 text-xs text-gray-600 text-center">
214
+ Models hosted on Hugging Face Hub
215
+ </div>
216
+ </div>
217
+ )}
218
+ </div>
219
+ );
220
+ }
scripts/download_models.py CHANGED
@@ -2,7 +2,20 @@
2
  Download model artifacts from Hugging Face Hub at container startup.
3
 
4
  Called automatically by the Docker entrypoint before uvicorn starts.
5
- Downloads only if artifacts are missing (idempotent skips already-present files).
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  """
7
 
8
  import os
@@ -14,83 +27,111 @@ from pathlib import Path
14
  # ──────────────────────────────────────────────────────────────────────────────
15
  REPO_ID = "NeerajCodz/aiBatteryLifeCycle"
16
  REPO_TYPE = "model"
17
- # Token read from the HF_TOKEN Space Secret (set in Space Settings Secrets)
18
  # For local use: set HF_TOKEN in your shell or .env before running
19
  HF_TOKEN = os.getenv("HF_TOKEN", "")
20
 
21
- # Project root snapshot_download uses this as local_dir so that files stored
22
- # at "artifacts/v1/..." in the HF repo land at <PROJECT_ROOT>/artifacts/v1/...
23
- # (NOT at <PROJECT_ROOT>/artifacts/artifacts/v1/... which would happen if we
24
- # pointed local_dir at the artifacts/ subfolder directly).
25
- PROJECT_ROOT = Path(__file__).resolve().parent.parent # /app (or repo root locally)
26
- ARTIFACTS_DIR = PROJECT_ROOT / "artifacts"
27
 
28
- # Sentinel file — written after a successful download
29
  SENTINEL = ARTIFACTS_DIR / ".hf_downloaded"
30
 
31
  # ──────────────────────────────────────────────────────────────────────────────
32
 
33
 
34
- def already_downloaded() -> bool:
35
- """Return True only when all three BestEnsemble component models are present.
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
- These three are required for ML simulation to work. Any other models are
38
- optional bonuses, but if these three are absent the Container must download.
39
- """
40
- required = [
41
- ARTIFACTS_DIR / "v2" / "models" / "classical" / "random_forest.joblib",
42
- ARTIFACTS_DIR / "v2" / "models" / "classical" / "xgboost.joblib",
43
- ARTIFACTS_DIR / "v2" / "models" / "classical" / "lightgbm.joblib",
44
- ]
45
- missing = [p for p in required if not p.exists()]
 
 
 
 
 
46
  if missing:
47
  if SENTINEL.exists():
48
- SENTINEL.unlink() # stale sentinel — remove so next run re-downloads
49
- print(f"[download_models] Sentinel was stale ({len(missing)} key models missing) — will re-download")
50
  return False
51
  return True
52
 
53
 
54
- def download_artifacts() -> None:
55
  try:
56
- from huggingface_hub import snapshot_download
57
  except ImportError:
58
- print("[download_models] huggingface_hub not installed — installing now…")
59
  import subprocess
60
- subprocess.check_call([sys.executable, "-m", "pip", "install", "huggingface_hub>=0.23", "-q"])
61
- from huggingface_hub import snapshot_download
62
-
63
- ARTIFACTS_DIR.mkdir(parents=True, exist_ok=True)
64
 
65
- print(f"[download_models] Downloading from {REPO_ID} → {ARTIFACTS_DIR}")
66
 
67
- # Repo is public — only pass token when non-empty.
68
- # Passing an empty string causes a 401 even on public repos.
69
- # IMPORTANT: local_dir must be PROJECT_ROOT (not artifacts/) because the HF
70
- # repo stores files under "artifacts/v1/..." — pointing local_dir at the
71
- # project root makes them land at <root>/artifacts/v1/... as expected.
72
- kwargs: dict = dict(
73
- repo_id=REPO_ID,
74
- repo_type=REPO_TYPE,
75
- local_dir=str(PROJECT_ROOT),
76
- ignore_patterns=["*.png", "*.jpg", "*.pdf", "*.log", "figures/**"],
77
- )
78
- if HF_TOKEN:
79
- kwargs["token"] = HF_TOKEN
 
 
 
 
 
 
 
 
 
80
 
81
- snapshot_download(**kwargs)
82
 
83
- # Write sentinel
84
- SENTINEL.write_text("downloaded\n")
85
- print("[download_models] Artifacts ready")
 
 
 
86
 
 
 
 
 
 
 
87
 
88
- def main():
89
- if already_downloaded():
90
  print("[download_models] Artifacts already present — skipping download")
91
  return
92
 
93
- download_artifacts()
94
 
95
 
96
  if __name__ == "__main__":
 
2
  Download model artifacts from Hugging Face Hub at container startup.
3
 
4
  Called automatically by the Docker entrypoint before uvicorn starts.
5
+ Can also download a specific version on-demand (e.g. from the API).
6
+
7
+ HF model repo layout (v1/ and v2/ at repo root):
8
+ v1/models/classical/*.joblib
9
+ v1/models/deep/*.pt *.keras
10
+ v1/scalers/*.joblib
11
+ v2/models/classical/*.joblib
12
+ v2/models/deep/*.pt *.keras
13
+ v2/scalers/*.joblib
14
+ v2/results/*.json
15
+
16
+ Local layout after download (local_dir = ARTIFACTS_DIR):
17
+ artifacts/v1/...
18
+ artifacts/v2/...
19
  """
20
 
21
  import os
 
27
  # ──────────────────────────────────────────────────────────────────────────────
28
  REPO_ID = "NeerajCodz/aiBatteryLifeCycle"
29
  REPO_TYPE = "model"
30
+ # Token read from the HF_TOKEN Space Secret (set in Space Settings -> Secrets)
31
  # For local use: set HF_TOKEN in your shell or .env before running
32
  HF_TOKEN = os.getenv("HF_TOKEN", "")
33
 
34
+ # HF repo stores v1/ and v2/ at root local_dir=ARTIFACTS_DIR maps them to
35
+ # artifacts/v1/... and artifacts/v2/...
36
+ ARTIFACTS_DIR = Path(__file__).resolve().parent.parent / "artifacts"
 
 
 
37
 
38
+ # Sentinel file — written after a successful full download
39
  SENTINEL = ARTIFACTS_DIR / ".hf_downloaded"
40
 
41
  # ──────────────────────────────────────────────────────────────────────────────
42
 
43
 
44
+ def _hf_kwargs(allow_patterns: list | None = None,
45
+ ignore_patterns: list | None = None) -> dict:
46
+ """Build kwargs for snapshot_download; inject token only when non-empty."""
47
+ kwargs: dict = dict(
48
+ repo_id=REPO_ID,
49
+ repo_type=REPO_TYPE,
50
+ local_dir=str(ARTIFACTS_DIR),
51
+ )
52
+ if allow_patterns:
53
+ kwargs["allow_patterns"] = allow_patterns
54
+ if ignore_patterns:
55
+ kwargs["ignore_patterns"] = ignore_patterns
56
+ if HF_TOKEN:
57
+ kwargs["token"] = HF_TOKEN
58
+ return kwargs
59
 
60
+
61
+ def _key_models(version: str = "v2") -> list:
62
+ base = ARTIFACTS_DIR / version / "models" / "classical"
63
+ return [base / f"{m}.joblib" for m in ("random_forest", "xgboost", "lightgbm")]
64
+
65
+
66
+ def version_loaded(version: str) -> bool:
67
+ """Return True when the given version's key models exist on disk."""
68
+ return all(p.exists() for p in _key_models(version))
69
+
70
+
71
+ def already_downloaded(version: str = "v2") -> bool:
72
+ """Return True only when all three BestEnsemble component models are present."""
73
+ missing = [p for p in _key_models(version) if not p.exists()]
74
  if missing:
75
  if SENTINEL.exists():
76
+ SENTINEL.unlink()
77
+ print(f"[download_models] Sentinel stale ({len(missing)} key models missing) — will re-download")
78
  return False
79
  return True
80
 
81
 
82
+ def _ensure_hub():
83
  try:
84
+ from huggingface_hub import snapshot_download # noqa: F401
85
  except ImportError:
 
86
  import subprocess
87
+ subprocess.check_call([sys.executable, "-m", "pip", "install",
88
+ "huggingface_hub>=0.23", "-q"])
 
 
89
 
 
90
 
91
+ def download_version(version: str) -> None:
92
+ """Download a single version (e.g. 'v1' or 'v2') from HF Hub into artifacts/."""
93
+ _ensure_hub()
94
+ from huggingface_hub import snapshot_download
95
+ ARTIFACTS_DIR.mkdir(parents=True, exist_ok=True)
96
+ print(f"[download_models] Downloading {version}/ from {REPO_ID} -> {ARTIFACTS_DIR}")
97
+ snapshot_download(**_hf_kwargs(
98
+ allow_patterns=[f"{version}/**"],
99
+ ignore_patterns=["*.log"],
100
+ ))
101
+ print(f"[download_models] {version}/ ready")
102
+
103
+
104
+ def download_all() -> None:
105
+ """Download all versions (v1 + v2) from HF Hub."""
106
+ _ensure_hub()
107
+ from huggingface_hub import snapshot_download
108
+ ARTIFACTS_DIR.mkdir(parents=True, exist_ok=True)
109
+ print(f"[download_models] Downloading all versions from {REPO_ID} -> {ARTIFACTS_DIR}")
110
+ snapshot_download(**_hf_kwargs(ignore_patterns=["*.log"]))
111
+ SENTINEL.write_text("downloaded\n")
112
+ print("[download_models] Artifacts ready")
113
 
 
114
 
115
+ def main() -> None:
116
+ import argparse
117
+ parser = argparse.ArgumentParser()
118
+ parser.add_argument("--version", default=None,
119
+ help="Download only this version, e.g. v1 or v2")
120
+ args = parser.parse_args()
121
 
122
+ if args.version:
123
+ if version_loaded(args.version):
124
+ print(f"[download_models] {args.version} already present — skipping")
125
+ else:
126
+ download_version(args.version)
127
+ return
128
 
129
+ # Default: ensure v2 (latest) is present
130
+ if already_downloaded("v2"):
131
  print("[download_models] Artifacts already present — skipping download")
132
  return
133
 
134
+ download_all()
135
 
136
 
137
  if __name__ == "__main__":
scripts/upload_models_to_hub.py CHANGED
@@ -151,28 +151,42 @@ def main():
151
  commit_message="chore: update model card",
152
  )
153
 
154
- # 3. Upload artifacts folder (models + scalers + results + figures + reports)
155
- # Only skip logs everything else (including figures/reports) is preserved on HF Hub
 
156
  for version in ["v1", "v2"]:
157
  version_path = ARTIFACTS / version
158
  if not version_path.exists():
159
  print(f" [skip] {version_path} does not exist")
160
  continue
161
 
162
- print(f"\nUploading artifacts/{version}/ …")
163
  upload_folder(
164
  folder_path=str(version_path),
165
- path_in_repo=f"artifacts/{version}",
166
  repo_id=REPO_ID,
167
  repo_type=REPO_TYPE,
168
  token=HF_TOKEN,
169
  ignore_patterns=["logs/**", "*.log"],
170
- commit_message=f"feat: upload {version} model artifacts (incl. figures & reports)",
171
  run_as_future=False,
172
  )
173
- print(f" [OK] artifacts/{version} uploaded")
174
 
175
- print("\n✅ All artifacts uploaded to", f"https://huggingface.co/{REPO_ID}")
 
 
 
 
 
 
 
 
 
 
 
 
 
176
 
177
 
178
  if __name__ == "__main__":
 
151
  commit_message="chore: update model card",
152
  )
153
 
154
+ # 3. Upload each version directly at repo root: v1/ and v2/ (NOT under artifacts/)
155
+ # This keeps the HF model repo clean download_models.py maps them back into
156
+ # the local artifacts/ folder via local_dir=ARTIFACTS_DIR.
157
  for version in ["v1", "v2"]:
158
  version_path = ARTIFACTS / version
159
  if not version_path.exists():
160
  print(f" [skip] {version_path} does not exist")
161
  continue
162
 
163
+ print(f"\nUploading {version}/ at repo root …")
164
  upload_folder(
165
  folder_path=str(version_path),
166
+ path_in_repo=version, # v1/ or v2/ at repo root
167
  repo_id=REPO_ID,
168
  repo_type=REPO_TYPE,
169
  token=HF_TOKEN,
170
  ignore_patterns=["logs/**", "*.log"],
171
+ commit_message=f"feat: upload {version} artifacts at repo root",
172
  run_as_future=False,
173
  )
174
+ print(f" [OK] {version}/ uploaded")
175
 
176
+ # 4. Remove old artifacts/ tree that may exist from previous uploads
177
+ print("\nCleaning up legacy artifacts/ folder in HF repo (if any) …")
178
+ try:
179
+ api.delete_folder(
180
+ path_in_repo="artifacts",
181
+ repo_id=REPO_ID,
182
+ repo_type=REPO_TYPE,
183
+ commit_message="chore: remove legacy artifacts/ folder (moved to repo root)",
184
+ )
185
+ print(" [OK] artifacts/ removed")
186
+ except Exception as e:
187
+ print(f" [skip] No legacy artifacts/ to remove ({e})")
188
+
189
+ print("\n[OK] All artifacts uploaded to", f"https://huggingface.co/{REPO_ID}")
190
 
191
 
192
  if __name__ == "__main__":