Spaces:
Sleeping
Sleeping
App.py
Browse files
app.py
ADDED
@@ -0,0 +1,505 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import requests
|
2 |
+
import subprocess
|
3 |
+
import time
|
4 |
+
import shutil
|
5 |
+
import sys
|
6 |
+
import gradio as gr
|
7 |
+
from datetime import datetime, timedelta
|
8 |
+
import json
|
9 |
+
|
10 |
+
# --- Setup Ollama and Model ---
|
11 |
+
def setup_ollama_once():
|
12 |
+
if shutil.which("ollama") is None:
|
13 |
+
subprocess.run(
|
14 |
+
[sys.executable, "-m", "pip", "install", "--quiet",
|
15 |
+
"langchain", "langchain_community", "gradio", "requests"],
|
16 |
+
check=True
|
17 |
+
)
|
18 |
+
subprocess.run("curl -fsSL https://ollama.com/install.sh | sh", shell=True, check=True)
|
19 |
+
subprocess.Popen("ollama serve", shell=True)
|
20 |
+
time.sleep(8)
|
21 |
+
subprocess.run(["ollama", "pull", "llama3.1"], check=True)
|
22 |
+
|
23 |
+
setup_ollama_once()
|
24 |
+
|
25 |
+
from langchain_community.llms import Ollama
|
26 |
+
_llama_client = Ollama(model="llama3.1")
|
27 |
+
|
28 |
+
# --- API Keys ---
|
29 |
+
GRIDSTATUS_API_KEY = "e9f1146bc3d242d19f7d64cf00f9fdd3"
|
30 |
+
WEATHERAPI_KEY = "6325087c017744b0b0d13950252307"
|
31 |
+
EIA_API_KEY = "l0LsSOevbx1XqUMSEdEP7qVwDQXoeC3bFw8LPdGZ"
|
32 |
+
SERPAPI_KEY = "83fc2175c280b9187692d652ee4bb8bbcdfc652b0b8ea8539d7b494ac08280f3"
|
33 |
+
|
34 |
+
# --- SerpAPI Integration for Situational Awareness ---
|
35 |
+
def search_grid_emergencies():
|
36 |
+
"""
|
37 |
+
Search for current emergencies that could impact grid operations
|
38 |
+
"""
|
39 |
+
emergency_queries = [
|
40 |
+
"New York power grid emergency outage blackout today",
|
41 |
+
"Northeast wildfire power line damage 2025",
|
42 |
+
"New York electrical grid failure infrastructure",
|
43 |
+
"NYISO emergency alert grid operations"
|
44 |
+
]
|
45 |
+
|
46 |
+
all_results = []
|
47 |
+
|
48 |
+
for query in emergency_queries:
|
49 |
+
try:
|
50 |
+
url = "https://serpapi.com/search"
|
51 |
+
params = {
|
52 |
+
"engine": "google",
|
53 |
+
"q": query,
|
54 |
+
"api_key": SERPAPI_KEY,
|
55 |
+
"num": 3, # Limit results per query
|
56 |
+
"tbm": "nws", # News search
|
57 |
+
"tbs": "qdr:d" # Last 24 hours
|
58 |
+
}
|
59 |
+
|
60 |
+
response = requests.get(url, params=params, timeout=10)
|
61 |
+
response.raise_for_status()
|
62 |
+
data = response.json()
|
63 |
+
|
64 |
+
if "news_results" in data:
|
65 |
+
for result in data["news_results"]:
|
66 |
+
all_results.append({
|
67 |
+
"type": "emergency",
|
68 |
+
"title": result.get("title", ""),
|
69 |
+
"snippet": result.get("snippet", ""),
|
70 |
+
"source": result.get("source", ""),
|
71 |
+
"date": result.get("date", "")
|
72 |
+
})
|
73 |
+
|
74 |
+
time.sleep(1) # Rate limiting
|
75 |
+
|
76 |
+
except Exception as e:
|
77 |
+
print(f"[ERROR] Emergency search failed for '{query}': {e}")
|
78 |
+
continue
|
79 |
+
|
80 |
+
return all_results
|
81 |
+
|
82 |
+
def search_high_demand_events():
|
83 |
+
"""
|
84 |
+
Search for events that could increase electricity demand
|
85 |
+
"""
|
86 |
+
# Get today's date for relevant searches
|
87 |
+
today = datetime.now()
|
88 |
+
tomorrow = today + timedelta(days=1)
|
89 |
+
date_str = today.strftime("%Y-%m-%d")
|
90 |
+
|
91 |
+
demand_queries = [
|
92 |
+
f"New York City major events concerts sports {date_str}",
|
93 |
+
f"NYC heat wave extreme weather {today.strftime('%B %Y')}",
|
94 |
+
f"Super Bowl NYC watch parties high electricity demand",
|
95 |
+
f"New York massive concert Madison Square Garden today",
|
96 |
+
f"NYC extreme cold weather heating demand {today.strftime('%B %Y')}"
|
97 |
+
]
|
98 |
+
|
99 |
+
all_results = []
|
100 |
+
|
101 |
+
for query in demand_queries:
|
102 |
+
try:
|
103 |
+
url = "https://serpapi.com/search"
|
104 |
+
params = {
|
105 |
+
"engine": "google",
|
106 |
+
"q": query,
|
107 |
+
"api_key": SERPAPI_KEY,
|
108 |
+
"num": 3,
|
109 |
+
"tbm": "nws",
|
110 |
+
"tbs": "qdr:w" # Last week
|
111 |
+
}
|
112 |
+
|
113 |
+
response = requests.get(url, params=params, timeout=10)
|
114 |
+
response.raise_for_status()
|
115 |
+
data = response.json()
|
116 |
+
|
117 |
+
if "news_results" in data:
|
118 |
+
for result in data["news_results"]:
|
119 |
+
all_results.append({
|
120 |
+
"type": "high_demand",
|
121 |
+
"title": result.get("title", ""),
|
122 |
+
"snippet": result.get("snippet", ""),
|
123 |
+
"source": result.get("source", ""),
|
124 |
+
"date": result.get("date", "")
|
125 |
+
})
|
126 |
+
|
127 |
+
time.sleep(1) # Rate limiting
|
128 |
+
|
129 |
+
except Exception as e:
|
130 |
+
print(f"[ERROR] High demand search failed for '{query}': {e}")
|
131 |
+
continue
|
132 |
+
|
133 |
+
return all_results
|
134 |
+
|
135 |
+
def get_situational_awareness():
|
136 |
+
"""
|
137 |
+
Combine emergency and high-demand event searches for comprehensive situational awareness
|
138 |
+
"""
|
139 |
+
print("[DEBUG] Fetching situational awareness data...")
|
140 |
+
|
141 |
+
emergencies = search_grid_emergencies()
|
142 |
+
high_demand_events = search_high_demand_events()
|
143 |
+
|
144 |
+
# Combine and deduplicate results
|
145 |
+
all_events = emergencies + high_demand_events
|
146 |
+
|
147 |
+
# Remove duplicates based on title similarity
|
148 |
+
unique_events = []
|
149 |
+
seen_titles = set()
|
150 |
+
|
151 |
+
for event in all_events:
|
152 |
+
title_words = set(event["title"].lower().split())
|
153 |
+
is_duplicate = False
|
154 |
+
|
155 |
+
for seen_title in seen_titles:
|
156 |
+
seen_words = set(seen_title.lower().split())
|
157 |
+
# If more than 50% of words overlap, consider it duplicate
|
158 |
+
overlap = len(title_words.intersection(seen_words))
|
159 |
+
if overlap > len(title_words) * 0.5:
|
160 |
+
is_duplicate = True
|
161 |
+
break
|
162 |
+
|
163 |
+
if not is_duplicate:
|
164 |
+
unique_events.append(event)
|
165 |
+
seen_titles.add(event["title"])
|
166 |
+
|
167 |
+
print(f"[DEBUG] Found {len(unique_events)} unique situational awareness events")
|
168 |
+
return unique_events
|
169 |
+
|
170 |
+
def format_situational_summary(events):
|
171 |
+
"""
|
172 |
+
Format situational awareness events for LLM consumption
|
173 |
+
"""
|
174 |
+
if not events:
|
175 |
+
return "No immediate emergencies or high-demand events detected."
|
176 |
+
|
177 |
+
emergency_events = [e for e in events if e["type"] == "emergency"]
|
178 |
+
demand_events = [e for e in events if e["type"] == "high_demand"]
|
179 |
+
|
180 |
+
summary = []
|
181 |
+
|
182 |
+
if emergency_events:
|
183 |
+
summary.append("CRITICAL ALERTS:")
|
184 |
+
for event in emergency_events[:3]: # Limit to top 3
|
185 |
+
summary.append(f"- {event['title']}")
|
186 |
+
if event['snippet']:
|
187 |
+
summary.append(f" {event['snippet'][:150]}...")
|
188 |
+
|
189 |
+
if demand_events:
|
190 |
+
summary.append("\nHIGH DEMAND EVENTS:")
|
191 |
+
for event in demand_events[:3]: # Limit to top 3
|
192 |
+
summary.append(f"- {event['title']}")
|
193 |
+
if event['snippet']:
|
194 |
+
summary.append(f" {event['snippet'][:150]}...")
|
195 |
+
|
196 |
+
return "\n".join(summary)
|
197 |
+
|
198 |
+
# --- Fetch Load Data using EIA API (Fixed) ---
|
199 |
+
def fetch_eia_load_direct():
|
200 |
+
"""
|
201 |
+
Use EIA API to get NYISO load data directly
|
202 |
+
This replaces the broken GridStatus API call
|
203 |
+
"""
|
204 |
+
try:
|
205 |
+
url = "https://api.eia.gov/v2/electricity/rto/region-data/data/"
|
206 |
+
params = {
|
207 |
+
"api_key": EIA_API_KEY,
|
208 |
+
"facets[respondent]": "NYIS", # NYISO code in EIA
|
209 |
+
"facets[type-name]": "D", # Demand/Load
|
210 |
+
"data[0]": "value",
|
211 |
+
"sort[0][column]": "period",
|
212 |
+
"sort[0][direction]": "desc",
|
213 |
+
"offset": 0,
|
214 |
+
"length": 1 # Just get the latest
|
215 |
+
}
|
216 |
+
|
217 |
+
response = requests.get(url, params=params, timeout=10)
|
218 |
+
response.raise_for_status()
|
219 |
+
data = response.json()
|
220 |
+
|
221 |
+
if not data["response"]["data"]:
|
222 |
+
raise ValueError("No load data returned from EIA")
|
223 |
+
|
224 |
+
latest_record = data["response"]["data"][0]
|
225 |
+
current_load = latest_record["value"]
|
226 |
+
|
227 |
+
print(f"[DEBUG] EIA Load for NYISO: {current_load} MW")
|
228 |
+
return float(current_load)
|
229 |
+
|
230 |
+
except Exception as e:
|
231 |
+
print(f"[ERROR] EIA load fetch failed: {e}")
|
232 |
+
return f"Error fetching EIA load data: {e}"
|
233 |
+
|
234 |
+
# --- Alternative: Try NYISO Direct API as Backup ---
|
235 |
+
def fetch_nyiso_direct_load():
|
236 |
+
"""
|
237 |
+
Backup method: Use NYISO's direct API for load data
|
238 |
+
"""
|
239 |
+
try:
|
240 |
+
from datetime import datetime
|
241 |
+
today = datetime.now().strftime("%Y%m%d")
|
242 |
+
url = f"http://mis.nyiso.com/public/csv/pal/{today}pal.csv"
|
243 |
+
|
244 |
+
response = requests.get(url, timeout=10)
|
245 |
+
response.raise_for_status()
|
246 |
+
|
247 |
+
# Parse CSV data to get latest load
|
248 |
+
lines = response.text.strip().split('\n')
|
249 |
+
if len(lines) < 2:
|
250 |
+
raise ValueError("Invalid CSV response from NYISO")
|
251 |
+
|
252 |
+
# Get the last row (most recent data)
|
253 |
+
latest_data = lines[-1].split(',')
|
254 |
+
# NYISO load is typically in the second column after timestamp
|
255 |
+
current_load = float(latest_data[1])
|
256 |
+
|
257 |
+
print(f"[DEBUG] NYISO Direct Load: {current_load} MW")
|
258 |
+
return current_load
|
259 |
+
|
260 |
+
except Exception as e:
|
261 |
+
print(f"[ERROR] NYISO direct fetch failed: {e}")
|
262 |
+
return f"Error fetching NYISO direct data: {e}"
|
263 |
+
|
264 |
+
# --- Main Load Fetch Function with Fallbacks ---
|
265 |
+
def fetch_load_with_fallbacks():
|
266 |
+
"""
|
267 |
+
Try multiple sources for load data with fallbacks
|
268 |
+
"""
|
269 |
+
# Try EIA first
|
270 |
+
load = fetch_eia_load_direct()
|
271 |
+
if isinstance(load, float):
|
272 |
+
return load
|
273 |
+
|
274 |
+
print("[DEBUG] EIA failed, trying NYISO direct...")
|
275 |
+
# Try NYISO direct as backup
|
276 |
+
load = fetch_nyiso_direct_load()
|
277 |
+
if isinstance(load, float):
|
278 |
+
return load
|
279 |
+
|
280 |
+
print("[DEBUG] All load sources failed, using estimated value")
|
281 |
+
# If all else fails, return a reasonable estimate based on typical NYISO load
|
282 |
+
return 18000.0 # MW - typical NYISO load
|
283 |
+
|
284 |
+
# --- Fetch Weather ---
|
285 |
+
def fetch_weather_forecast(city="New York"):
|
286 |
+
url = f"http://api.weatherapi.com/v1/current.json?key={WEATHERAPI_KEY}&q={city}&aqi=no"
|
287 |
+
try:
|
288 |
+
response = requests.get(url, timeout=5)
|
289 |
+
response.raise_for_status()
|
290 |
+
data = response.json()
|
291 |
+
temp = data["current"]["temp_f"]
|
292 |
+
condition = data["current"]["condition"]["text"]
|
293 |
+
return temp, condition
|
294 |
+
except Exception as e:
|
295 |
+
return None, f"Weather fetch error: {e}"
|
296 |
+
|
297 |
+
# --- Fetch NY Generator Profiles (from EIA) ---
|
298 |
+
def get_ny_generator_profiles():
|
299 |
+
url = "https://api.eia.gov/v2/electricity/generator/data/"
|
300 |
+
params = {
|
301 |
+
"api_key": EIA_API_KEY,
|
302 |
+
"facets[state]": "NY",
|
303 |
+
"facets[operational_status_code]": "OP",
|
304 |
+
"data[0]": "nameplate_capacity",
|
305 |
+
"data[1]": "energy_source",
|
306 |
+
"data[2]": "prime_mover",
|
307 |
+
"data[3]": "plant_name",
|
308 |
+
"offset": 0,
|
309 |
+
"length": 100
|
310 |
+
}
|
311 |
+
|
312 |
+
try:
|
313 |
+
response = requests.get(url, params=params, timeout=10)
|
314 |
+
response.raise_for_status()
|
315 |
+
raw = response.json()
|
316 |
+
plants = raw["response"]["data"]
|
317 |
+
print(f"[DEBUG] Retrieved {len(plants)} plants")
|
318 |
+
|
319 |
+
profiles = []
|
320 |
+
for p in plants:
|
321 |
+
capacity = p.get("nameplate_capacity", 0)
|
322 |
+
if capacity < 10:
|
323 |
+
continue
|
324 |
+
fuel = p.get("energy_source", "UNK")
|
325 |
+
mover = p.get("prime_mover", "UNK")
|
326 |
+
plant = p.get("plant_name", "Unknown")
|
327 |
+
|
328 |
+
if fuel in ["WND", "SUN"]:
|
329 |
+
category = "renewable"
|
330 |
+
elif fuel == "NUC":
|
331 |
+
category = "baseload"
|
332 |
+
elif mover in ["CT", "GT"]:
|
333 |
+
category = "peaker"
|
334 |
+
elif mover in ["CC", "ST"]:
|
335 |
+
category = "midload"
|
336 |
+
else:
|
337 |
+
category = "misc"
|
338 |
+
|
339 |
+
profiles.append({
|
340 |
+
"name": plant,
|
341 |
+
"fuel": fuel,
|
342 |
+
"mover": mover,
|
343 |
+
"capacity": capacity,
|
344 |
+
"category": category
|
345 |
+
})
|
346 |
+
|
347 |
+
return profiles
|
348 |
+
except Exception as e:
|
349 |
+
print(f"[DEBUG] Generator profile fetch error: {e}")
|
350 |
+
return []
|
351 |
+
|
352 |
+
# --- Core Logic: Enhanced Grid AI with Situational Awareness ---
|
353 |
+
def real_time_decision_with_situational_awareness():
|
354 |
+
# Fetch all data sources
|
355 |
+
load = fetch_load_with_fallbacks()
|
356 |
+
temp, weather_desc = fetch_weather_forecast("New York")
|
357 |
+
|
358 |
+
# NEW: Get situational awareness data
|
359 |
+
situational_events = get_situational_awareness()
|
360 |
+
situational_summary = format_situational_summary(situational_events)
|
361 |
+
|
362 |
+
# Handle weather fetch failure
|
363 |
+
if temp is None:
|
364 |
+
temp = 70 # Default temperature if weather fetch fails
|
365 |
+
weather_desc = "Weather data unavailable"
|
366 |
+
print(f"[DEBUG] Weather fetch failed, using defaults: {temp}°F, {weather_desc}")
|
367 |
+
|
368 |
+
plant_data = get_ny_generator_profiles()
|
369 |
+
if not plant_data:
|
370 |
+
print("[DEBUG] Generator profiles unavailable, using generic recommendations")
|
371 |
+
plant_summary = "Generator profile data temporarily unavailable"
|
372 |
+
else:
|
373 |
+
plant_summary = "\n".join([
|
374 |
+
f"- {p['name']} ({p['category']}, {p['fuel']}, {p['capacity']} MW)"
|
375 |
+
for p in plant_data[:10]
|
376 |
+
])
|
377 |
+
|
378 |
+
# Enhanced prompt with situational awareness
|
379 |
+
prompt = f"""
|
380 |
+
You are a senior electric grid operator managing the New York Independent System Operator (NYISO) regional power grid. Your role is to ensure real-time reliability, stability, and cost-efficiency while balancing generation resources, system constraints, and grid demands.
|
381 |
+
|
382 |
+
CURRENT GRID CONDITIONS:
|
383 |
+
- Current Load: {load} MW
|
384 |
+
- Weather: {temp}°F, {weather_desc}
|
385 |
+
- Situational Events: {situational_summary}
|
386 |
+
|
387 |
+
You understand the following facts about the grid and must incorporate this knowledge when analyzing live data and making dispatch decisions:
|
388 |
+
|
389 |
+
---
|
390 |
+
|
391 |
+
GRID FACTS:
|
392 |
+
|
393 |
+
- The grid demand (load) fluctuates minute-to-minute but typically ranges between 15,000 MW at night and peaks above 30,000 MW during extreme weather.
|
394 |
+
- Generation resources include:
|
395 |
+
• Nuclear plants: Must-run base load, slow ramp rates, typically 3,000+ MW total.
|
396 |
+
• Combined-Cycle Gas (CC): Flexible mid-merit plants, ramp times ~15-30 minutes.
|
397 |
+
• Combustion Turbines (CT)/Peakers: Fast-start plants for peak demand or emergencies, expensive to run.
|
398 |
+
• Wind and Solar: Variable renewable energy, non-dispatchable, may need curtailment during congestion or oversupply.
|
399 |
+
• Hydroelectric: Dispatchable with water flow and environmental limits.
|
400 |
+
• Battery Storage: Short duration, fast response for peak shaving and frequency support.
|
401 |
+
- Transmission constraints exist, especially in and out of NYC (Zone J), which has import limits around 5,000 MW causing congestion pricing.
|
402 |
+
- Spinning reserve requirement is at least 1,000 MW at all times to handle sudden outages or demand spikes.
|
403 |
+
- Fuel supply constraints occasionally limit gas-fired generation during cold snaps.
|
404 |
+
- Renewable curtailment is the last resort and only occurs when transmission congestion or excess generation threatens system stability.
|
405 |
+
- The system must always obey N-1 contingency standards — able to handle the loss of any single major generator or transmission line.
|
406 |
+
|
407 |
+
---
|
408 |
+
|
409 |
+
OPERATIONAL GOALS:
|
410 |
+
|
411 |
+
1. Always meet or exceed real-time electric demand with generation plus imports.
|
412 |
+
2. Maintain spinning reserves above minimum thresholds.
|
413 |
+
3. Avoid transmission congestion by adjusting dispatch in constrained zones.
|
414 |
+
4. Minimize the use of expensive peaker plants unless absolutely necessary.
|
415 |
+
5. Only curtail renewables if no other options exist.
|
416 |
+
6. Factor in weather conditions influencing load (heat waves increase demand, cloud cover reduces solar, wind speed affects wind output).
|
417 |
+
7. Prepare for forecasted demand changes within the next 30 to 60 minutes.
|
418 |
+
8. Anticipate generator outages or fuel supply issues.
|
419 |
+
|
420 |
+
---
|
421 |
+
|
422 |
+
ANALYSIS AND DECISION INSTRUCTIONS:
|
423 |
+
|
424 |
+
Given the live grid conditions you receive:
|
425 |
+
|
426 |
+
- Assess current load against total available capacity.
|
427 |
+
- Evaluate spinning reserve margin adequacy.
|
428 |
+
- Check transmission congestion zones and import/export flows.
|
429 |
+
- Consider generation mix and ramping capabilities.
|
430 |
+
- Assess renewable output and potential curtailment needs.
|
431 |
+
- Factor in weather impact on both load and renewable generation.
|
432 |
+
- Make recommendations about dispatch adjustments, including:
|
433 |
+
• Increasing/decreasing base load plants.
|
434 |
+
• Ramping combined-cycle gas plants.
|
435 |
+
• Starting/stopping peaker plants.
|
436 |
+
• Charging/discharging battery storage.
|
437 |
+
• Curtailing wind or solar generation.
|
438 |
+
• Import/export adjustments.
|
439 |
+
|
440 |
+
Always prioritize grid reliability and N-1 compliance. Justify your decisions with clear operational reasoning.
|
441 |
+
|
442 |
+
---
|
443 |
+
|
444 |
+
OUTPUT FORMAT:
|
445 |
+
|
446 |
+
Respond ONLY with the following structured summary:
|
447 |
+
|
448 |
+
Live Load: [number] MW
|
449 |
+
Forecast Load (Next 60 min): [number] MW
|
450 |
+
Total Available Capacity: [number] MW
|
451 |
+
Current Spinning Reserve: [number] MW
|
452 |
+
|
453 |
+
Decision: [Clear, concise statement of dispatch actions]
|
454 |
+
Reasoning: [Detailed explanation citing grid constraints, generation capabilities, reserve status, weather impacts, and congestion]
|
455 |
+
Risks and Recommendations: [Identify any risks, contingencies, or required monitoring]
|
456 |
+
|
457 |
+
---
|
458 |
+
|
459 |
+
EXAMPLE RESPONSE:
|
460 |
+
|
461 |
+
Live Load: 28,500 MW
|
462 |
+
Forecast Load (Next 60 min): 29,000 MW
|
463 |
+
Total Available Capacity: 31,000 MW
|
464 |
+
Current Spinning Reserve: 900 MW
|
465 |
+
|
466 |
+
Decision: Dispatch 2 combustion turbine peaker plants (Zone F) to add 500 MW, ramp up combined-cycle plants by 300 MW, and curtail 100 MW of wind generation in Zone D due to transmission congestion.
|
467 |
+
Reasoning: Load is approaching forecasted peak and spinning reserves are below the 1,000 MW requirement. Peakers provide quick ramping capacity while combined-cycle plants offer economic mid-merit generation. Wind curtailment is necessary to relieve congestion on the northern interface.
|
468 |
+
Risks and Recommendations: Monitor Zone J for import congestion; if reserves drop further, battery storage should be dispatched to maintain reserve margin.
|
469 |
+
|
470 |
+
---
|
471 |
+
|
472 |
+
Be concise but thorough. Act as a professional grid operator communicating operational status and decisions clearly to engineering and management teams.
|
473 |
+
"""
|
474 |
+
|
475 |
+
try:
|
476 |
+
decision = _llama_client.invoke(prompt).strip()
|
477 |
+
# FIXED: Removed the overly strict validation that was overriding the LLM response
|
478 |
+
# The LLM response is now used directly without being filtered out
|
479 |
+
except Exception as e:
|
480 |
+
print(f"[ERROR] LLM invocation failed: {e}")
|
481 |
+
decision = "Maintain normal operations.\n(Note: AI system temporarily unavailable, defaulting to safe option.)"
|
482 |
+
|
483 |
+
return (
|
484 |
+
f"=== ENHANCED GRID OPERATOR ASSESSMENT ===\n\n"
|
485 |
+
f"Live Load: {load} MW\n"
|
486 |
+
f"Weather: {temp}°F, {weather_desc}\n\n"
|
487 |
+
f"Situational Awareness:\n{situational_summary}\n\n"
|
488 |
+
f"=== OPERATIONAL DECISION ===\n{decision}\n\n"
|
489 |
+
f"=== EVENT DETAILS ===\n"
|
490 |
+
f"Emergency Events: {len([e for e in situational_events if e['type'] == 'emergency'])}\n"
|
491 |
+
f"High Demand Events: {len([e for e in situational_events if e['type'] == 'high_demand'])}"
|
492 |
+
)
|
493 |
+
|
494 |
+
# --- Gradio UI ---
|
495 |
+
with gr.Blocks() as demo:
|
496 |
+
gr.Markdown("## Auto Grid - Enhanced with Situational Awareness")
|
497 |
+
gr.Markdown("*Now using EIA API + SerpAPI for real-time emergency and high-demand event detection*")
|
498 |
+
|
499 |
+
output_text = gr.Textbox(label="Enhanced Grid Decision Output", lines=15)
|
500 |
+
|
501 |
+
fetch_btn = gr.Button("Fetch Live Data + Situational Awareness & Evaluate")
|
502 |
+
|
503 |
+
fetch_btn.click(fn=real_time_decision_with_situational_awareness, inputs=[], outputs=output_text)
|
504 |
+
|
505 |
+
demo.launch()
|