| """ |
| Mock Data Server for Thermal Grid RL Agent Hackathon Demo |
| ========================================================= |
| Loads the 3 real-world CSV datasets and exposes REST endpoints |
| that the RL environment can call instead of using hardcoded values. |
| |
| Endpoints |
| --------- |
| GET /ambient-temp?hour=<0-23> |
| GET /carbon-intensity?month=<1-12> |
| GET /pue-benchmark?region=india |
| GET /energy-price?hour=<0-23> |
| GET /off-peak?hour=<0-23> |
| GET /signals?hour=<0-23>&month=<1-12> ← all-in-one |
| |
| Run: uvicorn mock_data_server:app --reload --port 8000 |
| """ |
|
|
| from __future__ import annotations |
|
|
| import os |
| import sys |
| import random |
| from datetime import datetime |
| from pathlib import Path |
| from typing import Optional |
|
|
| import numpy as np |
| import pandas as pd |
|
|
| |
| |
| |
| try: |
| from fastapi import FastAPI, Query |
| from fastapi.responses import JSONResponse |
| import uvicorn |
| _HAS_FASTAPI = True |
| except ImportError: |
| _HAS_FASTAPI = False |
| FastAPI = None |
|
|
| |
| |
| |
| DATA_DIR = Path(__file__).parent / "data" |
| WEATHER_CSV = DATA_DIR / "india_2000_2024_daily_weather.csv" |
| CARBON_CSV = DATA_DIR / "india_monthly_full_release.csv" |
| REGION_CSV = DATA_DIR / "region-metadata.csv" |
|
|
| |
| |
| |
| _DATA_LOADED = False |
| _monthly_temp: dict[int, float] = {} |
| _monthly_carbon: dict[int, float] = {} |
| INDIA_BENCHMARK_PUE: float = 1.58 |
| def load_data(): |
| """Load and pre-process CSV data (once).""" |
| global _DATA_LOADED, _monthly_temp, _monthly_carbon, INDIA_BENCHMARK_PUE |
| if _DATA_LOADED: |
| return |
| |
| try: |
| print("[MockServer] Loading weather data …") |
| if not WEATHER_CSV.exists(): |
| raise FileNotFoundError(f"Weather CSV not found: {WEATHER_CSV}") |
| _weather_df = pd.read_csv(WEATHER_CSV, parse_dates=["date"]) |
| |
| _delhi_weather = _weather_df[_weather_df["city"] == "Delhi"].copy() |
| _delhi_weather["month"] = _delhi_weather["date"].dt.month |
| |
| |
| for month in range(1, 13): |
| sub = _delhi_weather[_delhi_weather["month"] == month] |
| if len(sub): |
| _monthly_temp[month] = float( |
| (sub["temperature_2m_max"].mean() + sub["temperature_2m_min"].mean()) / 2 |
| ) |
| else: |
| _monthly_temp[month] = 25.0 |
|
|
| print("[MockServer] Loading carbon intensity data …") |
| if not CARBON_CSV.exists(): |
| raise FileNotFoundError(f"Carbon CSV not found: {CARBON_CSV}") |
| |
| _carbon_df = pd.read_csv(CARBON_CSV, usecols=["Country code", "Variable", "Unit", "Date", "Value"]) |
| _india_carbon = _carbon_df[ |
| (_carbon_df["Variable"] == "CO2 intensity") & |
| (_carbon_df["Unit"] == "gCO2/kWh") |
| ].copy() |
| _india_carbon["Date"] = pd.to_datetime(_india_carbon["Date"]) |
| _india_carbon["month"] = _india_carbon["Date"].dt.month |
|
|
| |
| for month in range(1, 13): |
| sub = _india_carbon[_india_carbon["month"] == month] |
| if len(sub) and sub["Value"].notna().any(): |
| _monthly_carbon[month] = float(sub["Value"].mean()) |
| else: |
| _monthly_carbon[month] = 720.0 |
|
|
| print("[MockServer] Loading region benchmarks …") |
| if not REGION_CSV.exists(): |
| raise FileNotFoundError(f"Region CSV not found: {REGION_CSV}") |
| _region_df = pd.read_csv(REGION_CSV) |
| |
| _india_regions = _region_df[_region_df["location"].str.contains("India|Mumbai|Delhi|Hyderabad", na=False)] |
| _india_pue_values = _india_regions["power-usage-efficiency"].dropna().astype(float) |
| INDIA_BENCHMARK_PUE = float(_india_pue_values.mean()) if len(_india_pue_values) else 1.58 |
| |
| print(f"[MockServer] India benchmark PUE = {INDIA_BENCHMARK_PUE:.3f}") |
| _DATA_LOADED = True |
| print("[MockServer] All data loaded successfully [DONE]") |
| except Exception as e: |
| print(f"[MockServer] ERROR: Failed to load data: {e}", file=sys.stderr) |
| raise |
|
|
|
|
| |
| |
| |
| _INR_TO_USD = 1.0 / 84.0 |
| _TOU_INR: list[float] = [ |
| 5.5, 5.5, 5.5, 5.5, 5.5, 6.0, |
| 7.0, 8.5, 9.5, 10.0, 10.0, 9.5, |
| 9.0, 8.5, 9.0, 10.0, 11.0, 12.0, |
| 12.0, 11.5, 10.0, 8.5, 7.0, 6.0, |
| ] |
| _TOU_USD = [p * _INR_TO_USD for p in _TOU_INR] |
|
|
| |
| _OFF_PEAK_HOURS = set(range(0, 6)) |
|
|
| |
| _HOUR_TEMP_DELTA: list[float] = [ |
| -4.0, -4.5, -5.0, -5.0, -4.5, -3.0, |
| -2.0, 0.0, 2.0, 4.0, 5.0, 5.5, |
| 5.5, 5.0, 4.5, 4.0, 3.0, 1.5, |
| 0.0, -1.0, -2.0, -2.5, -3.0, -3.5, |
| ] |
|
|
| |
| _SOLAR_FACTOR: list[float] = [ |
| 1.00, 1.00, 1.00, 1.00, 1.00, 0.98, |
| 0.95, 0.88, 0.80, 0.72, 0.68, 0.65, |
| 0.63, 0.65, 0.68, 0.75, 0.82, 0.90, |
| 0.95, 0.98, 1.00, 1.00, 1.00, 1.00, |
| ] |
|
|
| |
| |
| |
|
|
| def _current_month() -> int: |
| return datetime.now().month |
|
|
|
|
| def get_ambient_temp(hour: int, month: Optional[int] = None) -> float: |
| """Return ambient temperature in °C for given hour and month.""" |
| load_data() |
| month = month or _current_month() |
| month = max(1, min(12, month)) |
| hour = hour % 24 |
| base = _monthly_temp.get(month, 25.0) |
| noise = random.gauss(0, 0.5) |
| return round(base + _HOUR_TEMP_DELTA[hour] + noise, 2) |
|
|
|
|
| def get_carbon_intensity(month: Optional[int] = None, hour: int = 12) -> float: |
| """Return grid carbon intensity in gCO₂/kWh.""" |
| load_data() |
| month = month or _current_month() |
| month = max(1, min(12, month)) |
| hour = hour % 24 |
| base = _monthly_carbon.get(month, 720.0) |
| noise = random.gauss(0, base * 0.03) |
| return round(float(np.clip(base * _SOLAR_FACTOR[hour] + noise, 300, 900)), 2) |
|
|
|
|
| def get_energy_price(hour: int) -> float: |
| """Return energy price in $/kWh.""" |
| hour = hour % 24 |
| noise = random.gauss(0, 0.002) |
| return round(float(np.clip(_TOU_USD[hour] + noise, 0.04, 0.20)), 4) |
|
|
|
|
| def get_off_peak(hour: int) -> int: |
| """Return 1 if off-peak, else 0.""" |
| return int((hour % 24) in _OFF_PEAK_HOURS) |
|
|
|
|
| def get_pue_benchmark(region: str = "india") -> float: |
| """Return real-world PUE benchmark for specified region.""" |
| load_data() |
| return round(INDIA_BENCHMARK_PUE, 3) |
|
|
|
|
| def get_all_signals(hour: int, month: Optional[int] = None) -> dict: |
| """Return all signals in a single dictionary.""" |
| month = month or _current_month() |
| amb = get_ambient_temp(hour, month) |
| ci = get_carbon_intensity(month, hour) |
| price = get_energy_price(hour) |
| offpk = get_off_peak(hour) |
| dr = 1 if (hour in {17, 18, 19} and random.random() < 0.25) else 0 |
| pue_b = get_pue_benchmark() |
| return { |
| "hour": hour, |
| "month": month, |
| "ambient_temp_c": amb, |
| "energy_price_per_kwh": price, |
| "grid_carbon_intensity_g_per_kwh": ci, |
| "off_peak_window": offpk, |
| "demand_response_signal": dr, |
| "pue_benchmark": pue_b, |
| } |
|
|
|
|
| |
| |
| |
| if _HAS_FASTAPI: |
| app = FastAPI( |
| title="Thermal Grid RL – Mock Data Server", |
| description="Serves real India weather, carbon, and grid data for the RL environment.", |
| version="1.0.0", |
| ) |
|
|
| @app.get("/health") |
| def health(): |
| load_data() |
| return {"status": "ok", "india_benchmark_pue": INDIA_BENCHMARK_PUE} |
|
|
| @app.get("/ambient-temp") |
| def ambient_temp( |
| hour: int = Query(12, ge=0, le=23), |
| month: Optional[int] = Query(None, ge=1, le=12), |
| ): |
| return {"ambient_temp_c": get_ambient_temp(hour, month)} |
|
|
| @app.get("/carbon-intensity") |
| def carbon_intensity( |
| hour: int = Query(12, ge=0, le=23), |
| month: Optional[int] = Query(None, ge=1, le=12), |
| ): |
| return {"grid_carbon_intensity_g_per_kwh": get_carbon_intensity(month, hour)} |
|
|
| @app.get("/energy-price") |
| def energy_price(hour: int = Query(12, ge=0, le=23)): |
| return {"energy_price_per_kwh": get_energy_price(hour)} |
|
|
| @app.get("/off-peak") |
| def off_peak(hour: int = Query(12, ge=0, le=23)): |
| return {"off_peak_window": get_off_peak(hour), "is_off_peak": bool(get_off_peak(hour))} |
|
|
| @app.get("/pue-benchmark") |
| def pue_benchmark(region: str = Query("india")): |
| return {"pue_benchmark": get_pue_benchmark(region), "region": region} |
|
|
| @app.get("/signals") |
| def signals( |
| hour: int = Query(12, ge=0, le=23), |
| month: Optional[int] = Query(None, ge=1, le=12), |
| ): |
| return get_all_signals(hour, month) |
|
|
| else: |
| print("[MockServer] WARNING: FastAPI not installed. " |
| "Install with: pip install fastapi uvicorn") |
| print("[MockServer] The HybridSignalGenerator can still run in 'direct' mode.") |
| app = None |
|
|
|
|
| |
| |
| |
| if __name__ == "__main__": |
| uvicorn.run("mock_data_server:app", host="0.0.0.0", port=8001, reload=True) |
|
|