CCockrum commited on
Commit
6e44663
·
verified ·
1 Parent(s): d7a2baf

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +489 -0
app.py ADDED
@@ -0,0 +1,489 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import pandas as pd
3
+ import numpy as np
4
+ import plotly.graph_objects as go
5
+ import plotly.express as px
6
+ from plotly.subplots import make_subplots
7
+ import requests
8
+ from datetime import datetime, timedelta
9
+ import warnings
10
+ import json
11
+ warnings.filterwarnings('ignore')
12
+
13
+ class EnhancedOceanClimateAgent:
14
+ def __init__(self):
15
+ self.anomaly_threshold = 2.0
16
+ self.critical_temp_change = 1.5
17
+
18
+ # API endpoints
19
+ self.noaa_base_url = "https://api.tidesandcurrents.noaa.gov/api/prod/datagetter"
20
+ self.noaa_stations_url = "https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/stations.json"
21
+
22
+ # Popular NOAA stations for different regions
23
+ self.default_stations = {
24
+ "San Francisco, CA": "9414290",
25
+ "New York, NY": "8518750",
26
+ "Miami, FL": "8723214",
27
+ "Seattle, WA": "9447130",
28
+ "Boston, MA": "8443970",
29
+ "Los Angeles, CA": "9410660",
30
+ "Galveston, TX": "8771450",
31
+ "Charleston, SC": "8665530"
32
+ }
33
+
34
+ def get_noaa_data(self, station_id, product, start_date, end_date, units="metric"):
35
+ """Fetch data from NOAA API"""
36
+ params = {
37
+ 'product': product,
38
+ 'application': 'OceanClimateAgent',
39
+ 'begin_date': start_date.strftime('%Y%m%d'),
40
+ 'end_date': end_date.strftime('%Y%m%d'),
41
+ 'station': station_id,
42
+ 'time_zone': 'gmt',
43
+ 'units': units,
44
+ 'format': 'json'
45
+ }
46
+
47
+ try:
48
+ response = requests.get(self.noaa_base_url, params=params, timeout=30)
49
+ if response.status_code == 200:
50
+ data = response.json()
51
+ if 'data' in data:
52
+ return pd.DataFrame(data['data'])
53
+ else:
54
+ print(f"No data returned for {product}: {data.get('error', {}).get('message', 'Unknown error')}")
55
+ return None
56
+ else:
57
+ print(f"API error {response.status_code} for {product}")
58
+ return None
59
+ except Exception as e:
60
+ print(f"Error fetching {product}: {str(e)}")
61
+ return None
62
+
63
+ def get_comprehensive_station_data(self, station_name, days_back=30):
64
+ """Get comprehensive data from a NOAA station"""
65
+ station_id = self.default_stations.get(station_name)
66
+ if not station_id:
67
+ return None, "Station not found"
68
+
69
+ end_date = datetime.now()
70
+ start_date = end_date - timedelta(days=days_back)
71
+
72
+ # Available NOAA products
73
+ products_to_fetch = {
74
+ 'water_level': 'water_level',
75
+ 'water_temperature': 'water_temperature',
76
+ 'air_temperature': 'air_temperature',
77
+ 'wind': 'wind',
78
+ 'air_pressure': 'air_pressure',
79
+ 'salinity': 'salinity',
80
+ 'currents': 'currents'
81
+ }
82
+
83
+ all_data = {}
84
+ success_count = 0
85
+
86
+ for product_name, product_code in products_to_fetch.items():
87
+ data = self.get_noaa_data(station_id, product_code, start_date, end_date)
88
+ if data is not None and not data.empty:
89
+ all_data[product_name] = data
90
+ success_count += 1
91
+
92
+ if success_count == 0:
93
+ return None, "No data available for this station and time period"
94
+
95
+ return all_data, f"Successfully retrieved {success_count}/{len(products_to_fetch)} data types"
96
+
97
+ def process_noaa_data(self, raw_data):
98
+ """Process and combine NOAA data for analysis"""
99
+ if not raw_data:
100
+ return None
101
+
102
+ # Process water level data (primary dataset)
103
+ if 'water_level' in raw_data:
104
+ df = raw_data['water_level'].copy()
105
+ df['datetime'] = pd.to_datetime(df['t'])
106
+ df['water_level'] = pd.to_numeric(df['v'], errors='coerce')
107
+
108
+ # Add other parameters when available
109
+ if 'water_temperature' in raw_data:
110
+ temp_df = raw_data['water_temperature'].copy()
111
+ temp_df['datetime'] = pd.to_datetime(temp_df['t'])
112
+ temp_df['water_temp'] = pd.to_numeric(temp_df['v'], errors='coerce')
113
+ df = df.merge(temp_df[['datetime', 'water_temp']], on='datetime', how='left')
114
+
115
+ if 'air_temperature' in raw_data:
116
+ air_temp_df = raw_data['air_temperature'].copy()
117
+ air_temp_df['datetime'] = pd.to_datetime(air_temp_df['t'])
118
+ air_temp_df['air_temp'] = pd.to_numeric(air_temp_df['v'], errors='coerce')
119
+ df = df.merge(air_temp_df[['datetime', 'air_temp']], on='datetime', how='left')
120
+
121
+ if 'wind' in raw_data:
122
+ wind_df = raw_data['wind'].copy()
123
+ wind_df['datetime'] = pd.to_datetime(wind_df['t'])
124
+ wind_df['wind_speed'] = pd.to_numeric(wind_df['s'], errors='coerce')
125
+ wind_df['wind_direction'] = pd.to_numeric(wind_df['d'], errors='coerce')
126
+ df = df.merge(wind_df[['datetime', 'wind_speed', 'wind_direction']], on='datetime', how='left')
127
+
128
+ if 'air_pressure' in raw_data:
129
+ pressure_df = raw_data['air_pressure'].copy()
130
+ pressure_df['datetime'] = pd.to_datetime(pressure_df['t'])
131
+ pressure_df['air_pressure'] = pd.to_numeric(pressure_df['v'], errors='coerce')
132
+ df = df.merge(pressure_df[['datetime', 'air_pressure']], on='datetime', how='left')
133
+
134
+ if 'salinity' in raw_data:
135
+ salinity_df = raw_data['salinity'].copy()
136
+ salinity_df['datetime'] = pd.to_datetime(salinity_df['t'])
137
+ salinity_df['salinity'] = pd.to_numeric(salinity_df['v'], errors='coerce')
138
+ df = df.merge(salinity_df[['datetime', 'salinity']], on='datetime', how='left')
139
+
140
+ return df
141
+
142
+ return None
143
+
144
+ def detect_anomalies(self, data, column, window=24): # 24 hours for hourly data
145
+ """Detect anomalies using rolling statistics"""
146
+ if column not in data.columns or data[column].isna().all():
147
+ return pd.Series([False] * len(data)), pd.Series([0] * len(data))
148
+
149
+ rolling_mean = data[column].rolling(window=window, center=True, min_periods=1).mean()
150
+ rolling_std = data[column].rolling(window=window, center=True, min_periods=1).std()
151
+
152
+ # Avoid division by zero
153
+ rolling_std = rolling_std.fillna(1)
154
+ rolling_std = rolling_std.replace(0, 1)
155
+
156
+ z_scores = np.abs((data[column] - rolling_mean) / rolling_std)
157
+ anomalies = z_scores > self.anomaly_threshold
158
+
159
+ return anomalies, z_scores
160
+
161
+ def calculate_trends(self, data, column, hours=168): # 7 days
162
+ """Calculate trend over specified period"""
163
+ if column not in data.columns or data[column].isna().all():
164
+ return 0
165
+
166
+ recent_data = data.tail(hours)
167
+ if len(recent_data) < 2:
168
+ return 0
169
+
170
+ x = np.arange(len(recent_data))
171
+ y = recent_data[column].dropna()
172
+
173
+ if len(y) < 2:
174
+ return 0
175
+
176
+ x = x[:len(y)]
177
+ slope = np.polyfit(x, y, 1)[0] if len(x) > 1 else 0
178
+ return slope
179
+
180
+ def generate_climate_analysis(self, data, station_name):
181
+ """Generate comprehensive climate analysis"""
182
+ if data is None or data.empty:
183
+ return {}, []
184
+
185
+ analysis = {}
186
+ alerts = []
187
+
188
+ # Water level analysis
189
+ if 'water_level' in data.columns:
190
+ wl_trend = self.calculate_trends(data, 'water_level')
191
+ analysis['water_level_trend'] = wl_trend * 24 # per day
192
+
193
+ if abs(wl_trend * 24) > 5: # >5cm per day change
194
+ alerts.append(f"🟡 Significant water level change: {wl_trend*24:.1f}cm/day at {station_name}")
195
+
196
+ # Temperature analysis
197
+ if 'water_temp' in data.columns:
198
+ temp_trend = self.calculate_trends(data, 'water_temp')
199
+ analysis['water_temp_trend'] = temp_trend * 24 # per day
200
+
201
+ if temp_trend * 24 > 0.5: # >0.5°C per day
202
+ alerts.append(f"🔴 Rapid water temperature rise: {temp_trend*24:.2f}°C/day at {station_name}")
203
+
204
+ # Anomaly detection
205
+ for col in ['water_level', 'water_temp', 'wind_speed']:
206
+ if col in data.columns:
207
+ anomalies, z_scores = self.detect_anomalies(data, col)
208
+ anomaly_pct = (anomalies.sum() / len(data)) * 100
209
+ analysis[f'{col}_anomaly_frequency'] = anomaly_pct
210
+
211
+ if anomaly_pct > 10:
212
+ alerts.append(f"🟡 High {col.replace('_', ' ')} anomaly frequency: {anomaly_pct:.1f}% at {station_name}")
213
+
214
+ if not alerts:
215
+ alerts.append(f"✅ No significant anomalies detected at {station_name}")
216
+
217
+ return analysis, alerts
218
+
219
+ # Initialize the enhanced agent
220
+ agent = EnhancedOceanClimateAgent()
221
+
222
+ def analyze_real_ocean_data(station_name, days_back, anomaly_sensitivity, use_real_data):
223
+ """Main analysis function with real NOAA data"""
224
+
225
+ agent.anomaly_threshold = anomaly_sensitivity
226
+
227
+ if use_real_data:
228
+ # Fetch real NOAA data
229
+ raw_data, status_msg = agent.get_comprehensive_station_data(station_name, days_back)
230
+
231
+ if raw_data is None:
232
+ return None, None, None, f"❌ Error: {status_msg}", "No alerts - data unavailable", None
233
+
234
+ # Process the data
235
+ data = agent.process_noaa_data(raw_data)
236
+
237
+ if data is None or data.empty:
238
+ return None, None, None, "❌ No processable data available", "No alerts - data unavailable", None
239
+
240
+ data_source = f"📡 Real NOAA data from {station_name} ({status_msg})"
241
+
242
+ else:
243
+ # Use synthetic data for demonstration
244
+ data = generate_synthetic_data(days_back)
245
+ data_source = f"🔬 Synthetic demonstration data ({days_back} days)"
246
+
247
+ # Generate analysis and alerts
248
+ analysis, alerts = agent.generate_climate_analysis(data, station_name)
249
+
250
+ # Create visualizations
251
+ fig1 = create_main_dashboard(data, agent)
252
+ fig2 = create_anomaly_plots(data, agent)
253
+ fig3 = create_correlation_plot(data)
254
+
255
+ # Format analysis text
256
+ analysis_text = format_analysis_results(analysis, data_source)
257
+ alerts_text = "\n".join([f"- {alert}" for alert in alerts])
258
+
259
+ # Create CSV for download
260
+ csv_data = data.to_csv(index=False) if data is not None else ""
261
+
262
+ return fig1, fig2, fig3, analysis_text, alerts_text, csv_data
263
+
264
+ def generate_synthetic_data(days):
265
+ """Generate synthetic data for demonstration"""
266
+ dates = pd.date_range(start=datetime.now() - timedelta(days=days), periods=days*24, freq='H')
267
+
268
+ # Synthetic water level with tidal patterns
269
+ tidal_pattern = 2 * np.sin(2 * np.pi * np.arange(len(dates)) / 12.42) # M2 tide
270
+ water_level = 100 + tidal_pattern + np.random.normal(0, 0.3, len(dates))
271
+
272
+ # Water temperature with daily cycle
273
+ daily_temp_cycle = 2 * np.sin(2 * np.pi * np.arange(len(dates)) / 24)
274
+ water_temp = 15 + daily_temp_cycle + np.random.normal(0, 0.5, len(dates))
275
+
276
+ # Wind patterns
277
+ wind_speed = 5 + 3 * np.sin(2 * np.pi * np.arange(len(dates)) / (24*3)) + np.random.normal(0, 1, len(dates))
278
+ wind_direction = 180 + 45 * np.sin(2 * np.pi * np.arange(len(dates)) / (24*2)) + np.random.normal(0, 20, len(dates))
279
+
280
+ return pd.DataFrame({
281
+ 'datetime': dates,
282
+ 'water_level': water_level,
283
+ 'water_temp': water_temp,
284
+ 'wind_speed': np.maximum(0, wind_speed),
285
+ 'wind_direction': wind_direction % 360,
286
+ 'air_pressure': 1013 + np.random.normal(0, 10, len(dates))
287
+ })
288
+
289
+ def create_main_dashboard(data, agent):
290
+ """Create main dashboard visualization"""
291
+ fig = make_subplots(
292
+ rows=2, cols=2,
293
+ subplot_titles=('Water Level', 'Water Temperature', 'Wind Speed', 'Air Pressure'),
294
+ vertical_spacing=0.1
295
+ )
296
+
297
+ # Water Level
298
+ if 'water_level' in data.columns:
299
+ fig.add_trace(
300
+ go.Scatter(x=data['datetime'], y=data['water_level'],
301
+ name='Water Level', line=dict(color='blue')),
302
+ row=1, col=1
303
+ )
304
+
305
+ # Add anomalies
306
+ anomalies, _ = agent.detect_anomalies(data, 'water_level')
307
+ if anomalies.any():
308
+ anomaly_data = data[anomalies]
309
+ fig.add_trace(
310
+ go.Scatter(x=anomaly_data['datetime'], y=anomaly_data['water_level'],
311
+ mode='markers', name='Anomalies',
312
+ marker=dict(color='red', size=6)),
313
+ row=1, col=1
314
+ )
315
+
316
+ # Water Temperature
317
+ if 'water_temp' in data.columns:
318
+ fig.add_trace(
319
+ go.Scatter(x=data['datetime'], y=data['water_temp'],
320
+ name='Water Temp', line=dict(color='red')),
321
+ row=1, col=2
322
+ )
323
+
324
+ # Wind Speed
325
+ if 'wind_speed' in data.columns:
326
+ fig.add_trace(
327
+ go.Scatter(x=data['datetime'], y=data['wind_speed'],
328
+ name='Wind Speed', line=dict(color='green')),
329
+ row=2, col=1
330
+ )
331
+
332
+ # Air Pressure
333
+ if 'air_pressure' in data.columns:
334
+ fig.add_trace(
335
+ go.Scatter(x=data['datetime'], y=data['air_pressure'],
336
+ name='Air Pressure', line=dict(color='purple')),
337
+ row=2, col=2
338
+ )
339
+
340
+ fig.update_layout(height=600, showlegend=False, title_text="Ocean and Atmospheric Data Dashboard")
341
+ return fig
342
+
343
+ def create_anomaly_plots(data, agent):
344
+ """Create anomaly detection plots"""
345
+ fig = make_subplots(
346
+ rows=1, cols=2,
347
+ subplot_titles=('Water Level Anomalies', 'Temperature Anomalies')
348
+ )
349
+
350
+ # Water level anomalies
351
+ if 'water_level' in data.columns:
352
+ _, z_scores = agent.detect_anomalies(data, 'water_level')
353
+ fig.add_trace(
354
+ go.Scatter(x=data['datetime'], y=z_scores,
355
+ mode='lines', name='Water Level Z-Score'),
356
+ row=1, col=1
357
+ )
358
+ fig.add_hline(y=agent.anomaly_threshold, line_dash="dash", line_color="red", row=1, col=1)
359
+
360
+ # Temperature anomalies
361
+ if 'water_temp' in data.columns:
362
+ _, z_scores = agent.detect_anomalies(data, 'water_temp')
363
+ fig.add_trace(
364
+ go.Scatter(x=data['datetime'], y=z_scores,
365
+ mode='lines', name='Temperature Z-Score', line=dict(color='red')),
366
+ row=1, col=2
367
+ )
368
+ fig.add_hline(y=agent.anomaly_threshold, line_dash="dash", line_color="red", row=1, col=2)
369
+
370
+ fig.update_layout(height=400, showlegend=False, title_text="Anomaly Detection Analysis")
371
+ return fig
372
+
373
+ def create_correlation_plot(data):
374
+ """Create correlation heatmap"""
375
+ numeric_cols = [col for col in ['water_level', 'water_temp', 'wind_speed', 'air_pressure']
376
+ if col in data.columns]
377
+
378
+ if len(numeric_cols) < 2:
379
+ # Return empty plot if insufficient data
380
+ fig = go.Figure()
381
+ fig.add_annotation(text="Insufficient data for correlation analysis",
382
+ xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False)
383
+ return fig
384
+
385
+ corr_matrix = data[numeric_cols].corr()
386
+
387
+ fig = px.imshow(corr_matrix,
388
+ labels=dict(color="Correlation"),
389
+ color_continuous_scale='RdBu_r',
390
+ aspect="auto",
391
+ title="Parameter Correlations")
392
+ return fig
393
+
394
+ def format_analysis_results(analysis, data_source):
395
+ """Format analysis results for display"""
396
+ result = f"### {data_source}\n\n**Key Trends:**\n"
397
+
398
+ for key, value in analysis.items():
399
+ if 'trend' in key:
400
+ param = key.replace('_trend', '').replace('_', ' ').title()
401
+ unit = 'cm/day' if 'water_level' in key else '°C/day' if 'temp' in key else 'units/day'
402
+ result += f"- {param}: {value:.3f} {unit}\n"
403
+ elif 'anomaly_frequency' in key:
404
+ param = key.replace('_anomaly_frequency', '').replace('_', ' ').title()
405
+ result += f"- {param} anomalies: {value:.1f}%\n"
406
+
407
+ return result
408
+
409
+ # Create Gradio interface
410
+ with gr.Blocks(title="🌊 Enhanced Ocean Climate Monitoring AI Agent", theme=gr.themes.Ocean()) as demo:
411
+ gr.Markdown("""
412
+ # 🌊 Enhanced Ocean Climate Monitoring AI Agent
413
+ ### Real-time Analysis with NOAA Data Integration
414
+
415
+ This enhanced AI agent can fetch real ocean data from NOAA stations or use synthetic data for demonstration.
416
+ Monitor water levels, temperature, currents, and detect climate anomalies at major coastal locations.
417
+ """)
418
+
419
+ with gr.Row():
420
+ with gr.Column(scale=1):
421
+ gr.Markdown("### Configuration")
422
+ station_name = gr.Dropdown(
423
+ choices=list(agent.default_stations.keys()),
424
+ value="San Francisco, CA",
425
+ label="NOAA Station Location"
426
+ )
427
+ days_back = gr.Slider(
428
+ minimum=7,
429
+ maximum=90,
430
+ value=30,
431
+ step=1,
432
+ label="Days of Historical Data"
433
+ )
434
+ anomaly_sensitivity = gr.Slider(
435
+ minimum=1.0,
436
+ maximum=3.0,
437
+ value=2.0,
438
+ step=0.1,
439
+ label="Anomaly Detection Sensitivity"
440
+ )
441
+ use_real_data = gr.Checkbox(
442
+ label="Use Real NOAA Data",
443
+ value=True,
444
+ info="Uncheck to use synthetic data"
445
+ )
446
+ analyze_btn = gr.Button("🔍 Analyze Ocean Data", variant="primary")
447
+
448
+ with gr.Column(scale=2):
449
+ gr.Markdown("### Climate Alerts")
450
+ alerts_output = gr.Markdown()
451
+
452
+ with gr.Row():
453
+ analysis_output = gr.Markdown()
454
+
455
+ with gr.Tab("Main Dashboard"):
456
+ dashboard_plot = gr.Plot()
457
+
458
+ with gr.Tab("Anomaly Detection"):
459
+ anomaly_plot = gr.Plot()
460
+
461
+ with gr.Tab("Correlations"):
462
+ correlation_plot = gr.Plot()
463
+
464
+ with gr.Tab("Data Export"):
465
+ gr.Markdown("### Download Analyzed Data")
466
+ csv_output = gr.File(label="Download CSV Data")
467
+ gr.Markdown("*Note: Real NOAA data usage is subject to their terms of service*")
468
+
469
+ # Set up the analysis function
470
+ analyze_btn.click(
471
+ fn=analyze_real_ocean_data,
472
+ inputs=[station_name, days_back, anomaly_sensitivity, use_real_data],
473
+ outputs=[dashboard_plot, anomaly_plot, correlation_plot, analysis_output, alerts_output, csv_output]
474
+ )
475
+
476
+ # Auto-run on startup with synthetic data
477
+ demo.load(
478
+ fn=analyze_real_ocean_data,
479
+ inputs=[
480
+ gr.Text(value="San Francisco, CA", visible=False),
481
+ gr.Number(value=30, visible=False),
482
+ gr.Number(value=2.0, visible=False),
483
+ gr.Checkbox(value=False, visible=False) # Start with synthetic data
484
+ ],
485
+ outputs=[dashboard_plot, anomaly_plot, correlation_plot, analysis_output, alerts_output, csv_output]
486
+ )
487
+
488
+ if __name__ == "__main__":
489
+ demo.launch()