Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,11 +1,3 @@
|
|
| 1 |
-
# app.py
|
| 2 |
-
# This is a self-contained Python script implementing a simplified version of PowerGrid Guardian.
|
| 3 |
-
# It includes data ingestion (with mocks), data fusion, PyPSA simulation wrapper, LLM orchestration (with mock),
|
| 4 |
-
# FastAPI backend (embedded), Gradio frontend, and basic tests (runnable via pytest if saved separately).
|
| 5 |
-
# For Hugging Face Spaces deployment, this file can be used directly as the entrypoint.
|
| 6 |
-
# Note: This is a demo system for decision-support only. Not for automatic grid control.
|
| 7 |
-
# Updated to use Qwen model from Hugging Face, and incorporated provided API keys for services.
|
| 8 |
-
|
| 9 |
import os
|
| 10 |
import json
|
| 11 |
import datetime
|
|
@@ -13,8 +5,6 @@ import random
|
|
| 13 |
import asyncio
|
| 14 |
import logging
|
| 15 |
from typing import List, Dict, Any
|
| 16 |
-
|
| 17 |
-
# Dependencies (ensure these are in requirements.txt for HF Spaces)
|
| 18 |
import pandas as pd
|
| 19 |
import numpy as np
|
| 20 |
import pypsa
|
|
@@ -24,6 +14,7 @@ from pydantic import BaseModel
|
|
| 24 |
import uvicorn
|
| 25 |
import httpx
|
| 26 |
from dotenv import load_dotenv
|
|
|
|
| 27 |
|
| 28 |
# Load environment variables
|
| 29 |
load_dotenv()
|
|
@@ -35,7 +26,7 @@ logger = logging.getLogger(__name__)
|
|
| 35 |
# === CONFIGURATION ===
|
| 36 |
DEMO_MODE = True # Use mock data if True
|
| 37 |
HF_API_KEY = os.getenv("HF_API_KEY")
|
| 38 |
-
#
|
| 39 |
GRIDSTATUS_API_KEY = os.getenv("GRIDSTATUS_API_KEY", "e9f1146bc3d242d19f7d64cf00f9fdd3")
|
| 40 |
WEATHERAPI_KEY = os.getenv("WEATHERAPI_KEY", "6325087c017744b0b0d13950252307")
|
| 41 |
EIA_API_KEY = os.getenv("EIA_API_KEY", "l0LsSOevbx1XqUMSEdEP7qVwDQXoeC3bFw8LPdGZ")
|
|
@@ -225,10 +216,13 @@ async def fetch_events(params: Dict) -> List[Event]:
|
|
| 225 |
))
|
| 226 |
return events
|
| 227 |
|
| 228 |
-
|
| 229 |
-
# Load from examples/toy_topology.json or mock
|
| 230 |
topology = {
|
| 231 |
-
"nodes": [
|
|
|
|
|
|
|
|
|
|
| 232 |
"edges": [Edge(edge_id="edge1", from_node="node1", to_node="node2", r_ohm_per_km=0.1, x_ohm_per_km=0.2, length_km=12.3, thermal_limit_mw=300).dict()]
|
| 233 |
}
|
| 234 |
return topology
|
|
@@ -292,7 +286,7 @@ def run_n_minus_1(net: pypsa.Network) -> List[Dict]:
|
|
| 292 |
# === LLM ORCHESTRATOR ===
|
| 293 |
class LLMOrchestrator:
|
| 294 |
async def build_context(self, node_id: str, timestamp: str) -> Dict:
|
| 295 |
-
topology =
|
| 296 |
timeseries = await fetch_grid_data({"respondent": "CISO", "start": (datetime.datetime.fromisoformat(timestamp) - datetime.timedelta(hours=24)).strftime("%Y-%m-%dT%H")})
|
| 297 |
weather = await fetch_weather({"latitude": topology["nodes"][0]["lat"], "longitude": topology["nodes"][0]["lon"]})
|
| 298 |
events = await fetch_events({"location": "Houston"})
|
|
@@ -347,6 +341,30 @@ class LLMOrchestrator:
|
|
| 347 |
except:
|
| 348 |
return False
|
| 349 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
# === FASTAPI BACKEND ===
|
| 351 |
app = FastAPI()
|
| 352 |
|
|
@@ -380,9 +398,8 @@ with gr.Blocks() as demo:
|
|
| 380 |
gr.Markdown("# PowerGrid Guardian Demo")
|
| 381 |
with gr.Row():
|
| 382 |
with gr.Column(scale=2):
|
| 383 |
-
gr.Markdown("## Map Panel
|
| 384 |
-
|
| 385 |
-
map_output = gr.Textbox(value="Node1 (lat:29.76, lon:-95.37) - Risk: Red", label="Map")
|
| 386 |
with gr.Column(scale=1):
|
| 387 |
gr.Markdown("## Alert Timeline")
|
| 388 |
alerts = gr.JSON(value=get_alerts(), label="Alerts")
|
|
@@ -394,6 +411,7 @@ with gr.Blocks() as demo:
|
|
| 394 |
|
| 395 |
rec_button.click(fn=lambda node: asyncio.run(get_recommendation(node)), inputs=node_input, outputs=rec_output)
|
| 396 |
sim_button.click(fn=run_simulation, outputs=sim_output)
|
|
|
|
| 397 |
|
| 398 |
# === TESTS (Pytest compatible - can be extracted to tests/ files) ===
|
| 399 |
def test_fusion():
|
|
@@ -424,20 +442,4 @@ if __name__ == "__main__":
|
|
| 424 |
uvicorn.run(app, host="0.0.0.0", port=8000)
|
| 425 |
threading.Thread(target=run_api, daemon=True).start()
|
| 426 |
# Launch Gradio
|
| 427 |
-
demo.launch(server_name="0.0.0.0", server_port=7860)
|
| 428 |
-
|
| 429 |
-
# For HF Spaces, the demo.launch() will be the entrypoint.
|
| 430 |
-
# To run tests: python -m pytest app.py (if pytest installed)
|
| 431 |
-
# requirements.txt example:
|
| 432 |
-
# fastapi==0.104.1
|
| 433 |
-
# uvicorn==0.24.0
|
| 434 |
-
# gradio==4.1.2
|
| 435 |
-
# pypsa==0.26.0
|
| 436 |
-
# pandas==2.1.3
|
| 437 |
-
# numpy==1.26.2
|
| 438 |
-
# httpx==0.25.2
|
| 439 |
-
# python-dotenv==1.0.0
|
| 440 |
-
# To create toy files (if needed):
|
| 441 |
-
# toy_topology.json: {"nodes": [{"node_id": "node1", "name": "A", "type": "substation", "lat": 29.76, "lon": -95.37, "voltage_kv": 138, "capacity_mw": 500, "connected_edges": ["edge1"]}], "edges": [{"edge_id": "edge1", "from_node": "node1", "to_node": "node2", "r_ohm_per_km": 0.1, "x_ohm_per_km": 0.2, "length_km": 12.3, "thermal_limit_mw": 300}]}
|
| 442 |
-
# toy_timeseries.csv: node_id,timestamp,measured_load_mw,measured_generation_mw,forecast_load_mw,reserve_margin_percent
|
| 443 |
-
# node1,2023-01-01T00:00:00,100,120,110,10
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
import json
|
| 3 |
import datetime
|
|
|
|
| 5 |
import asyncio
|
| 6 |
import logging
|
| 7 |
from typing import List, Dict, Any
|
|
|
|
|
|
|
| 8 |
import pandas as pd
|
| 9 |
import numpy as np
|
| 10 |
import pypsa
|
|
|
|
| 14 |
import uvicorn
|
| 15 |
import httpx
|
| 16 |
from dotenv import load_dotenv
|
| 17 |
+
import plotly.express as px
|
| 18 |
|
| 19 |
# Load environment variables
|
| 20 |
load_dotenv()
|
|
|
|
| 26 |
# === CONFIGURATION ===
|
| 27 |
DEMO_MODE = True # Use mock data if True
|
| 28 |
HF_API_KEY = os.getenv("HF_API_KEY")
|
| 29 |
+
# API keys
|
| 30 |
GRIDSTATUS_API_KEY = os.getenv("GRIDSTATUS_API_KEY", "e9f1146bc3d242d19f7d64cf00f9fdd3")
|
| 31 |
WEATHERAPI_KEY = os.getenv("WEATHERAPI_KEY", "6325087c017744b0b0d13950252307")
|
| 32 |
EIA_API_KEY = os.getenv("EIA_API_KEY", "l0LsSOevbx1XqUMSEdEP7qVwDQXoeC3bFw8LPdGZ")
|
|
|
|
| 216 |
))
|
| 217 |
return events
|
| 218 |
|
| 219 |
+
def load_topology() -> Dict:
|
| 220 |
+
# Load from examples/toy_topology.json or mock (made sync for simplicity in map generation)
|
| 221 |
topology = {
|
| 222 |
+
"nodes": [
|
| 223 |
+
Node(node_id="node1", name="Substation A", type="substation", lat=29.7604, lon=-95.3698, voltage_kv=138, capacity_mw=500, connected_edges=["edge1"]).dict(),
|
| 224 |
+
Node(node_id="node2", name="Substation B", type="substation", lat=29.75, lon=-95.36, voltage_kv=138, capacity_mw=400, connected_edges=["edge1"]).dict()
|
| 225 |
+
],
|
| 226 |
"edges": [Edge(edge_id="edge1", from_node="node1", to_node="node2", r_ohm_per_km=0.1, x_ohm_per_km=0.2, length_km=12.3, thermal_limit_mw=300).dict()]
|
| 227 |
}
|
| 228 |
return topology
|
|
|
|
| 286 |
# === LLM ORCHESTRATOR ===
|
| 287 |
class LLMOrchestrator:
|
| 288 |
async def build_context(self, node_id: str, timestamp: str) -> Dict:
|
| 289 |
+
topology = load_topology()
|
| 290 |
timeseries = await fetch_grid_data({"respondent": "CISO", "start": (datetime.datetime.fromisoformat(timestamp) - datetime.timedelta(hours=24)).strftime("%Y-%m-%dT%H")})
|
| 291 |
weather = await fetch_weather({"latitude": topology["nodes"][0]["lat"], "longitude": topology["nodes"][0]["lon"]})
|
| 292 |
events = await fetch_events({"location": "Houston"})
|
|
|
|
| 341 |
except:
|
| 342 |
return False
|
| 343 |
|
| 344 |
+
# === MAP GENERATION ===
|
| 345 |
+
def generate_map():
|
| 346 |
+
topology = load_topology()
|
| 347 |
+
nodes = topology['nodes']
|
| 348 |
+
df = pd.DataFrame(nodes)
|
| 349 |
+
# Mock risks for each node
|
| 350 |
+
df['risk'] = np.random.uniform(0, 1, len(df))
|
| 351 |
+
df['color'] = df['risk'] # For continuous color
|
| 352 |
+
df['size'] = 10 # Fixed size
|
| 353 |
+
fig = px.scatter_mapbox(
|
| 354 |
+
df,
|
| 355 |
+
lat="lat",
|
| 356 |
+
lon="lon",
|
| 357 |
+
color="risk",
|
| 358 |
+
size="size",
|
| 359 |
+
hover_name="name",
|
| 360 |
+
color_continuous_scale="RdYlGn_r", # Red for high risk, green for low
|
| 361 |
+
zoom=10,
|
| 362 |
+
height=500,
|
| 363 |
+
mapbox_style="open-street-map"
|
| 364 |
+
)
|
| 365 |
+
fig.update_layout(margin={"r":0,"t":0,"l":0,"b":0})
|
| 366 |
+
return fig
|
| 367 |
+
|
| 368 |
# === FASTAPI BACKEND ===
|
| 369 |
app = FastAPI()
|
| 370 |
|
|
|
|
| 398 |
gr.Markdown("# PowerGrid Guardian Demo")
|
| 399 |
with gr.Row():
|
| 400 |
with gr.Column(scale=2):
|
| 401 |
+
gr.Markdown("## Map Panel")
|
| 402 |
+
map_plot = gr.Plot(label="Grid Map")
|
|
|
|
| 403 |
with gr.Column(scale=1):
|
| 404 |
gr.Markdown("## Alert Timeline")
|
| 405 |
alerts = gr.JSON(value=get_alerts(), label="Alerts")
|
|
|
|
| 411 |
|
| 412 |
rec_button.click(fn=lambda node: asyncio.run(get_recommendation(node)), inputs=node_input, outputs=rec_output)
|
| 413 |
sim_button.click(fn=run_simulation, outputs=sim_output)
|
| 414 |
+
demo.load(generate_map, outputs=map_plot)
|
| 415 |
|
| 416 |
# === TESTS (Pytest compatible - can be extracted to tests/ files) ===
|
| 417 |
def test_fusion():
|
|
|
|
| 442 |
uvicorn.run(app, host="0.0.0.0", port=8000)
|
| 443 |
threading.Thread(target=run_api, daemon=True).start()
|
| 444 |
# Launch Gradio
|
| 445 |
+
demo.launch(server_name="0.0.0.0", server_port=7860)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|