Skip to content
VryuzPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

⚡ PowerIQ — Unified Battery Health & Site-Load Intelligence

Team: 102.3 · Vayu AI Studio Hackathon · Track: Move-It

🔗 Live demo — deployed on Vayu ML Service, both modes running with live simulated telemetry.

UI: Streamlit Gateway: FastAPI Streaming: Kafka Model: scikit--learn Container: Docker Deploy: Vayu ML Service

🔴 Problem statement and outcome

Problem: Battery-swap networks rotate packs by guesswork because no one fuses per-pack health data across a fleet, and fleet charging depots discover their shared transformer is overloaded only after it trips — tripped breakers and blacked-out blocks are a documented, recurring failure, not a hypothetical. In both cases the sensor telemetry needed to prevent the problem already exists; it's just never fused with a site-level forecast and turned into an automated action.

Outcome: One Vayu-native pipeline, two configurable modes, sharing the same telemetry schema, the same two-model structure (a per-unit health regressor + a site-level forecaster), and the same closed loop: simulated sensors → Kafka → FastAPI ingestion → model inference → an automated action published back over Kafka → the simulator visibly reacting — with a live Streamlit dashboard judges can drive themselves, including manual override, a kill switch, and a one-click test trigger to force the loop end to end.

User story

Rakesh runs EV infrastructure on the outskirts of Pune — a 40-charger fleet depot overnight, and a small network of battery-swap stations for the delivery riders who pass through by day. Both run into the same wall: he can see the numbers, but only after something's already gone wrong. A batch of vans plugs in at once and the shared 500 kVA transformer trips before he gets a warning — the same failure mode that blacked out a residential block near a real Pune EV hub. A pack keeps getting swapped to riders until one of them complains their range dropped, and a station runs a queue during peak hours while another sits on inventory it doesn't need. He's managing all of it on manual caps and gut-feel rotation, because nothing fuses the sensor data he already has into a decision he can act on in time.

PowerIQ closes that gap with one engine and a mode switch. On the depot side, it forecasts site load 15 minutes ahead and auto-throttles the most recently connected stall the moment the forecast crosses 85% of the transformer limit — before a trip, not after. On the swap side, it scores every pack's health on ingestion, auto-retires anything below a safe threshold, and forecasts swap demand per station so it can recommend moving packs before a queue forms. Same telemetry shape, same two-model structure, same automated-action pattern — just "stall" vs. "pack" and a different action taken.

Before: Rakesh finds out from a tripped breaker or a frustrated rider. After: a live gauge and forecast tell him what's about to happen, and the system acts on it automatically, with a manual override and kill switch next to it if he wants to step in.

(Full version with sourcing/market data: 09_docs/user_story.md, the standalone 1-page deliverable.)

📦 Submission materials

File What it is
submission/team5_102.3.pdf Solution document — problem, approach, platform services used, results
submission/team5_102.3.mp4 Demo video — both modes closing the loop live, judge-triggered

📚 Documentation index

Doc Covers
This README Problem, architecture, quick start, deploy steps, model results
USAGE.md Local setup, running both APIs + dashboard, known fixes
09_docs/API_REFERENCE.md Every endpoint on both ingestion APIs — request/response shapes
09_docs/ENVIRONMENT_VARIABLES.md Every config var, its default, and what breaks if it's wrong
09_docs/DEPLOYMENT_RUNBOOK.md Master deploy runbook — depot + swap, unified, one linear pass from “wire it to Vayu” to “judge can open a URL”
09_docs/user_story.md Standalone 1-page user story for both modes
01_dataset/README.md / README_swap.md Dataset cards — source, generation method, grounding assumptions
00_vayu_workspaces/ → 08_deploy/ One README per numbered pipeline stage, each scoped to that stage only
boo.md Full original proposal (market sizing, competitive analysis, roadmap)

What's built vs. what's left to deploy

Deliverable Where it lives Status
Historical telemetry in Object Storage 01_dataset/ (depot + swap CSVs) Uploaded
Both models trained, registered, MLflow-logged 05_model_registry/register_*.py Code ready — run against your MLFLOW_TRACKING_URI
Kafka telemetry-in + command-out live 03_vayu_kafka/ Code ready — confirm with test_connection.py
Model Serving endpoints (cloud inference) — deployed
Dashboard live at a public URL 07_build_app/app_unified.py deployed
Docker images built, signed, pushed 07_build_app/Dockerfile.* done
Simulator publishes + reacts over Kafka simulator.py, simulator_swap.py Built, unit-verified
Judge can trigger the loop from the dashboard sidebar "Judge Testing" panel Built
Observability panel (telemetry rate, command success) sidebar "Live Metrics" panel Built
Safety guard (kill switch, concurrency cap) both ingestion APIs Built
User story 09_docs/user_story.md Written

Wiring diagram (sense → think → act, closed over Kafka)

DEPOT MODE
  simulator.py (fake ESP32, per-stall telemetry)
        │  POST /ingest every 1-5s
        ▼
  ingestion_api.py  ──publish_reading──▶  Kafka topic: telemetry-in
        │  forecast site load 15min ahead
        │  (Vayu Model Serving, falls back to local forecast_model.joblib)
        ▼
  overload_risk? ──yes──▶ _apply_throttle() ──publish_command──▶ Kafka topic: command-out
        │                                                              │
        │ /forecast /state /alerts /metrics (polled)                  │ consumed by
        ▼                                                              ▼
  app_unified.py (Streamlit)                              simulator.py kafka_command_loop()
  live gauge, per-stall bars, kill switch,                trickle-charges the throttled
  manual Throttle/Release override                        stall instead of full draw
                                                            (device visibly reacts)

SWAP-STATION MODE — identical shape, "pack" instead of "stall"
  simulator_swap.py ──▶ ingestion_api_swap.py ──▶ Kafka telemetry-in
        │ SoH score (health model) + per-station demand forecast
        ▼
  low SoH? ──yes──▶ _apply_retire() ──publish_command──▶ Kafka command-out
        │                                                     │
        ▼                                                     ▼
  app_unified.py (Swap Station mode)                simulator_swap.py kafka_command_loop()
  station heat-bands, SoH histogram,                 pulls the pack from circulation
  redistribution recs, manual Retire/Un-retire       (device visibly reacts)

Both modes share one Kafka client (03_vayu_kafka/kafka_client.py), one telemetry schema, and one closed-loop pattern: an automated decision (_apply_throttle / _apply_retire) is the single code path used by both the automatic threshold-crossing trigger and the dashboard's manual override button, and always publishes to command-out when KAFKA_ENABLED=true.

📁 Project layout

poweriq/
├── 00_vayu_workspaces/README.md
├── 01_dataset/
│   ├── depot_telemetry.csv           # depot training data
│   ├── pack_health.csv, swap_demand.csv  # swap-mode training data
│   └── README.md, README_swap.md     # dataset cards
├── 02_vayu_mlflow/README.md
├── 03_vayu_kafka/
│   ├── kafka_client.py               # producer/consumer, shared by both modes
│   ├── create_topic.py               # provisions telemetry-in + command-out
│   └── test_connection.py            # verifies Kafka auth on both topics
├── 04_starter-kit/
│   ├── train_forecast_model.py       # depot site-load forecaster
│   ├── train_health_model.py         # swap pack SoH regressor
│   └── train_swap_forecast_model.py  # swap demand forecaster
├── 05_model_registry/                # trained models + metadata + register_*.py
├── 07_build_app/
│   ├── config.py                     # env-var config for both modes
│   ├── simulator.py, simulator_swap.py
│   ├── ingestion_api.py, ingestion_api_swap.py
│   ├── app.py                        # depot-only dashboard (fallback)
│   ├── app_unified.py                # depot + swap, mode toggle — deploy this
│   ├── Dockerfile.ingestion, Dockerfile.ingestion-swap
│   ├── Dockerfile.dashboard, Dockerfile.dashboard-unified
│   └── requirements-ingest.txt, requirements-dashboard.txt
├── 08_deploy/image-signing/sign_image.py
├── 09_docs/user_story.md
├── requirements.txt
├── .env.example
└── boo.md                            # full proposal

⚡ Quick start

pip install -r requirements.txt

# depot mode
cd 07_build_app
python ingestion_api.py                        # terminal 1 — http://localhost:8000
python simulator.py --stalls 20 --interval 3    # terminal 2
streamlit run app_unified.py                    # terminal 3 — http://localhost:8501

# swap mode (run alongside depot mode, or on its own)
python ingestion_api_swap.py                              # terminal 4 — http://localhost:8001
python simulator_swap.py --packs-per-station 20 --interval 3   # terminal 5

Force a depot overload to see the alert path fire:

python -c "
import requests
for i in range(40):
    requests.post('http://localhost:8000/ingest', json={
        'unit_id': f'stall-{i:03d}', 'unit_type': 'stall',
        'timestamp': '2026-07-11T09:00:00+00:00',
        'voltage': 230.0, 'current': 90.0,
        'temperature': 35.0, 'cycle_count': 10, 'station_ambient_temp': 35.0,
    })
print(requests.get('http://localhost:8000/forecast').json())
"

Or use the dashboard sidebar's Judge Testing panel — it calls the same /trigger_test endpoint in both modes without needing a terminal.

Deploying to Vayu

  1. Copy .env.example to .env and fill in the Kafka/S3/Postgres passwords from your Access Guide. Set KAFKA_ENABLED=true once running inside the Vayu Workspace — the Kafka broker is only reachable from there.
  2. Run python 03_vayu_kafka/test_connection.py from inside the Workspace to confirm both telemetry-in and command-out topics are reachable. If you only have one provisioned topic, set KAFKA_COMMAND_TOPIC=<your topic> to multiplex both directions over it (messages carry a message_type field so they stay distinguishable).
  3. Upload 01_dataset/depot_telemetry.csv (and the swap CSVs) to Object Storage via S3 Browser.
  4. Log training runs to MLflow and register both models:
    export MLFLOW_TRACKING_URI=<your Vayu MLflow URL>
    python 05_model_registry/register_model.py            # depot — logs 3 runs
    python 05_model_registry/register_health_model.py     # swap SoH
    python 05_model_registry/register_swap_forecast_model.py  # swap demand
  5. Deploy each registered model via Model Serving, then set PREDICT_URL / HEALTH_PREDICT_URL / SWAP_PREDICT_URL in .env (or as ML Service env vars) so forecasts come from the cloud endpoint instead of the local .joblib fallback.
  6. Build, sign, and push the containers:
    docker login image-registry-vks-ir-chn2vcr1.cloudservices.tatacommunications.com
    # do this BEFORE building --push
    
    docker build -t $IMAGE_REGISTRY/$REGISTRY_PROJECT/poweriq-ingest:latest . -f 07_build_app/Dockerfile.ingestion --push
    docker build -t $IMAGE_REGISTRY/$REGISTRY_PROJECT/poweriq-ingest-swap:latest . -f 07_build_app/Dockerfile.ingestion-swap --push
    docker build -t $IMAGE_REGISTRY/$REGISTRY_PROJECT/poweriq-dashboard:latest . -f 07_build_app/Dockerfile.dashboard-unified --push
    
    set -a && source .env && set +a
    export IMAGE=$IMAGE_REGISTRY/$REGISTRY_PROJECT/poweriq-ingest:latest && python 08_deploy/image-signing/sign_image.py sign
    export IMAGE=$IMAGE_REGISTRY/$REGISTRY_PROJECT/poweriq-ingest-swap:latest && python 08_deploy/image-signing/sign_image.py sign
    export IMAGE=$IMAGE_REGISTRY/$REGISTRY_PROJECT/poweriq-dashboard:latest && python 08_deploy/image-signing/sign_image.py sign
    Vayu ML Services reject unsigned images — don't skip the sign step.
  7. Realtime Inference → ML Service → Create Deployment for all three images. Set INGEST_API_BASE / SWAP_INGEST_API_BASE on the dashboard deployment to the other two deployments' URLs. Open port 443 on every endpoint's public IP (firewall rule SOP) so judges can reach them.

Model results

Numbers below are read straight from the metadata committed alongside each model in 05_model_registry/*_metadata.json — retrain and these files (and this section) should be regenerated together so they never drift apart.

  • Depot forecast: RandomForestRegressor, 300 trees, depth 12. Test MAE 37.1 kW, R² 0.78, overload-risk accuracy 97.7%, recall 42% on the rare overload class — the dataset's overload events look like sudden demand spikes, not perfectly predicted from rolling averages alone. Mitigation already built in: ingestion_api.py also flags risk the instant current load crosses the 85% margin, so a live overload is never missed just because the forecast under-predicted it.
  • Swap SoH model: R² 0.91, MAE 3.1 pts, retirement-flag accuracy 94.6%, retirement recall 0.83.
  • Swap demand model: R² 0.78, MAE 1.7 swaps/15min, shortfall-risk accuracy 95.7%, recall 0.42 — same honest limitation as depot: rare positive class, harder to catch than the common case.

Retrain: python 04_starter-kit/train_forecast_model.py (depot), train_health_model.py / train_swap_forecast_model.py (swap).

Dashboard performance fix (white-flash / laggy buttons)

Both app.py and app_unified.py used to end the script with time.sleep(DASHBOARD_REFRESH_SECONDS); st.rerun() at the top level. A bare st.rerun() tears down and rebuilds the entire Streamlit page — sidebar, charts, everything — and this was firing on a fixed timer, which is what produced the white-flash-every-few-seconds effect. It also meant every button click competed with that timer: a click triggers its own full-page rerun on top of whatever the auto-refresh was already doing, and each render pass made 5–6 sequential blocking HTTP calls to the ingestion API before it could draw anything, so clicks could feel like they'd done nothing for several seconds.

Fixed by:

  • Wrapping the live panel in @st.fragment(run_every=DASHBOARD_REFRESH_SECONDS), so the timer only re-runs that fragment, not the whole app — no more flash, and widgets outside it (like the mode toggle) stay responsive while it refreshes.
  • Firing all of a render pass's API calls concurrently via a small ThreadPoolExecutor (fetch_many) instead of one after another.
  • Switching every button/toggle's st.rerun() to st.rerun(scope="fragment"), so an action redraws only the fragment it's in, not the whole page.

What's intentionally simplified

  • In-memory aggregation, not a Kafka-consuming aggregator service — both ingestion APIs aggregate in-process for demo reliability, and publish to Kafka in parallel so the topic is populated for the platform-usage deliverable. A dedicated Kafka-consumer aggregator is the "more correct" architecture, called out here as a known simplification rather than an oversight.
  • Redistribution/throttling rules are threshold heuristics, not a full routing optimizer — intentional for the hackathon timeline, documented as the clearest near-term roadmap item in boo.md.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages