caspian123 commited on
Commit
3a8794d
·
verified ·
1 Parent(s): 2ae8bc4
Files changed (1) hide show
  1. app.py +505 -0
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()