nakas/ecmwf_open_data_forcast
0
1#!/usr/bin/env python32"""3ECMWF Open Data Weather Forecast Application4Access real ECMWF operational forecast data with coordinate-based lookups.5"""6 7import gradio as gr8import numpy as np9import pandas as pd10import matplotlib.pyplot as plt11import xarray as xr12import requests13import tempfile14import os15import time16from datetime import datetime, timedelta17import warnings18import folium19import plotly.graph_objects as go20import plotly.express as px21from plotly.subplots import make_subplots22 23warnings.filterwarnings('ignore')24 25try:26 from ecmwf.opendata import Client as OpenDataClient27 OPENDATA_AVAILABLE = True28except ImportError:29 OPENDATA_AVAILABLE = False30 31 32class ECMWFDataManager:33 def __init__(self):34 self.temp_dir = tempfile.mkdtemp()35 self.client = None36 if OPENDATA_AVAILABLE:37 try:38 self.client = OpenDataClient()39 except:40 self.client = None41 42 # AWS S3 direct access URLs43 self.aws_base_url = "https://ecmwf-forecasts.s3.eu-central-1.amazonaws.com"44 45 # ECMWF Open Data parameters - verified available as of 2024/202546 self.parameters = {47 # Surface level parameters (single level)48 "2t": {"name": "Temperature (2m)", "units": "°C", "description": "2-meter temperature", "level_type": "sfc"},49 "msl": {"name": "Sea Level Pressure", "units": "hPa", "description": "Mean sea level pressure", "level_type": "sfc"},50 "sp": {"name": "Surface Pressure", "units": "hPa", "description": "Surface pressure", "level_type": "sfc"},51 "10u": {"name": "Wind U (10m)", "units": "m/s", "description": "10-meter U wind component", "level_type": "sfc"},52 "10v": {"name": "Wind V (10m)", "units": "m/s", "description": "10-meter V wind component", "level_type": "sfc"},53 "tp": {"name": "Precipitation", "units": "mm", "description": "Total precipitation", "level_type": "sfc"},54 "tcwv": {"name": "Water Vapor", "units": "kg/m²", "description": "Total column water vapor", "level_type": "sfc"},55 "skt": {"name": "Skin Temperature", "units": "°C", "description": "Skin temperature", "level_type": "sfc"},56 "ro": {"name": "Runoff", "units": "m", "description": "Runoff", "level_type": "sfc"},57 "st": {"name": "Soil Temperature", "units": "°C", "description": "Soil temperature", "level_type": "sfc"},58 # Severe weather and convective parameters59 "cape": {"name": "CAPE", "units": "J/kg", "description": "Convective Available Potential Energy", "level_type": "sfc"},60 "cin": {"name": "CIN", "units": "J/kg", "description": "Convective Inhibition", "level_type": "sfc"},61 "lftx": {"name": "Lifted Index", "units": "K", "description": "Surface Lifted Index", "level_type": "sfc"},62 "4lftx": {"name": "Best Lifted Index", "units": "K", "description": "Best (4-layer) Lifted Index", "level_type": "sfc"},63 "cp": {"name": "Convective Precipitation", "units": "mm", "description": "Convective precipitation", "level_type": "sfc"},64 "lsp": {"name": "Large Scale Precipitation", "units": "mm", "description": "Large-scale precipitation", "level_type": "sfc"},65 "sf": {"name": "Snowfall", "units": "m", "description": "Snowfall", "level_type": "sfc"},66 "10fg": {"name": "Wind Gust (10m)", "units": "m/s", "description": "10-meter wind gust", "level_type": "sfc"},67 # Pressure level parameters (add common levels)68 "t": {"name": "Temperature", "units": "°C", "description": "Temperature at pressure levels", "level_type": "pl", "levels": [850, 500, 200]},69 "gh": {"name": "Geopotential Height", "units": "m", "description": "Geopotential height", "level_type": "pl", "levels": [850, 500, 200]},70 "u": {"name": "Wind U", "units": "m/s", "description": "U wind component", "level_type": "pl", "levels": [850, 500, 200]},71 "v": {"name": "Wind V", "units": "m/s", "description": "V wind component", "level_type": "pl", "levels": [850, 500, 200]},72 "q": {"name": "Specific Humidity", "units": "g/kg", "description": "Specific humidity", "level_type": "pl", "levels": [850, 500]},73 "r": {"name": "Relative Humidity", "units": "%", "description": "Relative humidity", "level_type": "pl", "levels": [850, 500]},74 }75 76 self.forecast_cache = {}77 self.preloaded_data = {}78 79 def get_latest_forecast_info(self, max_retries=3):80 """Get the most recent available forecast run with retry logic"""81 now = datetime.utcnow()82 83 # Check recent 6-hour cycles84 for hours_back in range(4, 24, 6):85 test_time = now - timedelta(hours=hours_back)86 run_hour = (test_time.hour // 6) * 687 run_time = test_time.replace(hour=run_hour, minute=0, second=0, microsecond=0)88 89 date_str = run_time.strftime("%Y%m%d")90 time_str = f"{run_hour:02d}"91 92 # Test availability with retry logic93 test_url = f"{self.aws_base_url}/{date_str}/{time_str}z/0p25/oper/"94 95 for attempt in range(max_retries):96 try:97 response = requests.head(test_url, timeout=10)98 if response.status_code == 429:99 if attempt < max_retries - 1:100 print(f"Rate limit hit while checking forecast availability (attempt {attempt + 1}/{max_retries}). Waiting 10 seconds...")101 time.sleep(10)102 continue103 else:104 print(f"Max retries reached while checking forecast availability due to rate limiting")105 break106 elif response.status_code in [200, 403]:107 return date_str, time_str, run_time108 else:109 break110 except Exception:111 if attempt < max_retries - 1:112 time.sleep(2) # Short wait for general errors113 continue114 else:115 break116 117 # Fallback118 return now.strftime("%Y%m%d"), "12", now119 120 def download_forecast_data(self, parameter="2t", step=0, level=None, max_retries=3):121 """Download ECMWF forecast data using multiple methods with retry logic for rate limiting"""122 date_str, time_str, run_time = self.get_latest_forecast_info()123 124 # Get parameter info125 param_info = self.parameters.get(parameter, {})126 level_type = param_info.get('level_type', 'sfc')127 128 # Method 1: Official client with retry logic129 if OPENDATA_AVAILABLE and self.client:130 for attempt in range(max_retries):131 try:132 cache_suffix = f"_{level}" if level else ""133 filename = os.path.join(self.temp_dir, f'ecmwf_{parameter}{cache_suffix}_{step}h.grib')134 135 # Build retrieval request136 request = {137 "type": "fc",138 "param": parameter,139 "step": step,140 "target": filename141 }142 143 # Add pressure level if needed144 if level_type == 'pl' and level:145 request["levelist"] = level146 elif level_type == 'pl':147 # Use first available level if no specific level requested148 levels = param_info.get('levels', [850])149 request["levelist"] = levels[0]150 151 self.client.retrieve(**request)152 153 if os.path.exists(filename) and os.path.getsize(filename) > 1000:154 level_info = f" at {level}hPa" if level else ""155 return filename, f"Downloaded {parameter}{level_info} +{step}h via ECMWF client"156 157 except Exception as e:158 error_msg = str(e).lower()159 if "429" in error_msg or "rate limit" in error_msg or "too many requests" in error_msg:160 if attempt < max_retries - 1: # Don't wait on last attempt161 print(f"Rate limit hit for {parameter} (attempt {attempt + 1}/{max_retries}). Waiting 10 seconds...")162 time.sleep(10)163 continue164 else:165 print(f"Max retries reached for {parameter} due to rate limiting")166 else:167 print(f"Client method failed for {parameter}: {e}")168 break169 170 # Method 2: AWS S3 direct access (for surface parameters only) with retry logic171 if level_type == 'sfc':172 for attempt in range(max_retries):173 try:174 step_str = f"{step:03d}"175 filename = f"{date_str}{time_str}0000-{step_str}h-oper-fc.grib2"176 url = f"{self.aws_base_url}/{date_str}/{time_str}z/0p25/oper/{filename}"177 178 response = requests.get(url, timeout=120, stream=True)179 180 if response.status_code == 429:181 if attempt < max_retries - 1: # Don't wait on last attempt182 print(f"Rate limit hit for {parameter} AWS download (attempt {attempt + 1}/{max_retries}). Waiting 10 seconds...")183 time.sleep(10)184 continue185 else:186 print(f"Max retries reached for {parameter} AWS download due to rate limiting")187 break188 elif response.status_code == 200:189 local_file = os.path.join(self.temp_dir, f'ecmwf_{parameter}_{step}h.grib2')190 191 with open(local_file, 'wb') as f:192 for chunk in response.iter_content(chunk_size=8192):193 f.write(chunk)194 195 if os.path.getsize(local_file) > 1000:196 return local_file, f"Downloaded {parameter} +{step}h via AWS S3"197 else:198 print(f"AWS returned status {response.status_code} for {parameter}")199 break200 201 except Exception as e:202 print(f"AWS method failed for {parameter}: {e}")203 break204 205 return None, f"Failed to download {parameter} at +{step}h"206 207 def extract_point_data(self, filename, lat, lon, parameter):208 """Extract weather data at specific coordinates"""209 try:210 ds = xr.open_dataset(filename, engine='cfgrib', backend_kwargs={'indexpath': ''})211 212 data_vars = list(ds.data_vars.keys())213 if not data_vars:214 return None215 216 data = ds[data_vars[0]]217 218 # Handle coordinates219 if 'latitude' in ds.coords:220 lats, lons = ds.latitude, ds.longitude221 elif 'lat' in ds.coords:222 lats, lons = ds.lat, ds.longitude223 else:224 return None225 226 # Select first time if multiple227 if 'time' in data.dims and len(data.time) > 1:228 data = data.isel(time=0)229 elif 'valid_time' in data.dims:230 data = data.isel(valid_time=0)231 232 # Find nearest point233 try:234 point_data = data.sel(latitude=lat, longitude=lon, method='nearest')235 except:236 try:237 point_data = data.sel(lat=lat, lon=lon, method='nearest')238 except:239 return None240 241 value = float(point_data.values)242 ds.close()243 244 return self.convert_units(value, parameter)245 246 except Exception as e:247 print(f"Error extracting point data: {e}")248 return None249 250 def convert_units(self, value, parameter):251 """Convert values to standard meteorological units"""252 if parameter in ['2t', 'skt', 't', 'st'] and value > 100:253 return value - 273.15 # K to °C254 elif parameter in ['msl', 'sp']:255 return value / 100 # Pa to hPa256 elif parameter == 'tp':257 return value * 1000 # m to mm258 elif parameter == 'q':259 return value * 1000 # kg/kg to g/kg260 elif parameter == 'ro':261 return value * 1000 # m to mm262 elif parameter == 'gh':263 return value / 9.80665 # m²/s² to meters (geopotential to height)264 return value265 266 def preload_all_data(self):267 """Preload all forecast data for quick access"""268 # 3-hourly for first 24 hours (ECMWF operational availability), then longer intervals269 forecast_steps = [0, 3, 6, 9, 12, 15, 18, 21, 24, 30, 36, 42, 48, 60, 72, 96, 120]270 # Include basic surface parameters and severe weather indicators271 surface_params = ["2t", "msl", "sp", "10u", "10v", "tp", "tcwv", "skt", "cape", "10fg", "cp", "lsp"]272 273 total_files = len(surface_params) * len(forecast_steps)274 loaded_count = 0275 276 for param in surface_params:277 for step in forecast_steps:278 try:279 cache_key = f"{param}_{step}"280 filename, msg = self.download_forecast_data(param, step)281 282 if filename:283 self.preloaded_data[cache_key] = filename284 loaded_count += 1285 except Exception as e:286 print(f"Failed to preload {param} at step {step}: {e}")287 continue288 289 return loaded_count, total_files290 291 def get_point_forecast(self, latitude, longitude):292 """Get comprehensive forecast for a specific location"""293 forecast_data = []294 # 3-hourly for first 24 hours (ECMWF operational availability), then longer intervals295 forecast_steps = [0, 3, 6, 9, 12, 15, 18, 21, 24, 30, 36, 42, 48, 60, 72, 96, 120]296 297 # Focus on surface parameters including severe weather indicators298 surface_params = ["2t", "msl", "sp", "10u", "10v", "tp", "tcwv", "skt", "cape", "10fg", "cp", "lsp"]299 300 for param in surface_params:301 if param not in self.parameters:302 continue303 304 param_data = []305 for step in forecast_steps:306 try:307 cache_key = f"{param}_{step}"308 309 # Try to use preloaded data first310 if cache_key in self.preloaded_data:311 filename = self.preloaded_data[cache_key]312 else:313 filename, _ = self.download_forecast_data(param, step)314 315 if filename and os.path.exists(filename):316 value = self.extract_point_data(filename, latitude, longitude, param)317 if value is not None:318 param_data.append({319 'step': step,320 'value': value,321 'datetime': datetime.utcnow() + timedelta(hours=step)322 })323 except Exception as e:324 print(f"Error getting {param} at step {step}: {e}")325 continue326 327 if param_data:328 forecast_data.append({329 'parameter': param,330 'name': self.parameters[param]['name'],331 'units': self.parameters[param]['units'],332 'data': param_data333 })334 335 return forecast_data336 337 338class WeatherNarrativeGenerator:339 """Generate natural language weather descriptions"""340 341 def __init__(self):342 pass343 344 def get_temperature_descriptor(self, temp_f):345 """Convert temperature to descriptive terms (input in Fahrenheit)"""346 if temp_f < 14:347 return "extremely cold"348 elif temp_f < 32:349 return "very cold"350 elif temp_f < 50:351 return "cold"352 elif temp_f < 68:353 return "cool"354 elif temp_f < 77:355 return "mild"356 elif temp_f < 86:357 return "warm"358 elif temp_f < 95:359 return "hot"360 else:361 return "very hot"362 363 def get_wind_descriptor(self, wind_speed_ms):364 """Convert wind speed to descriptive terms"""365 wind_speed_mph = wind_speed_ms * 2.237 # Convert m/s to mph366 if wind_speed_mph < 1:367 return "calm"368 elif wind_speed_mph < 8:369 return "light winds"370 elif wind_speed_mph < 18:371 return "moderate winds"372 elif wind_speed_mph < 25:373 return "strong winds"374 elif wind_speed_mph < 39:375 return "very strong winds"376 else:377 return "extremely strong winds"378 379 def get_precipitation_descriptor(self, precip_mm):380 """Convert precipitation to descriptive terms"""381 if precip_mm < 0.1:382 return None383 elif precip_mm < 1:384 return "light showers"385 elif precip_mm < 5:386 return "moderate rain"387 elif precip_mm < 10:388 return "heavy rain"389 else:390 return "very heavy rain"391 392 def get_sky_condition(self, cloud_cover_percent=None, precip_mm=0):393 """Determine sky conditions based on available data"""394 if precip_mm > 0.1:395 if precip_mm < 1:396 return "mostly cloudy with light showers"397 elif precip_mm < 5:398 return "overcast with rain"399 else:400 return "stormy with heavy rain"401 402 # If no cloud data available, infer from other conditions403 return "partly cloudy"404 405 def get_wind_direction_text(self, u_wind, v_wind):406 """Convert wind components to direction description"""407 if abs(u_wind) < 0.5 and abs(v_wind) < 0.5:408 return ""409 410 # Calculate wind direction (meteorological convention)411 import math412 wind_dir = (270 - math.degrees(math.atan2(v_wind, u_wind))) % 360413 414 directions = [415 "north", "northeast", "east", "southeast",416 "south", "southwest", "west", "northwest"417 ]418 idx = int((wind_dir + 22.5) / 45) % 8419 return f"from the {directions[idx]}"420 421 422 423 def analyze_weather_trend(self, weather_data):424 """Analyze weather trends to create smart groupings"""425 trends = []426 427 # Sort by hour428 sorted_hours = sorted(weather_data.keys())429 430 current_trend = {431 'start_hour': sorted_hours[0] if sorted_hours else 0,432 'end_hour': sorted_hours[0] if sorted_hours else 0,433 'conditions': {},434 'temp_range': []435 }436 437 for hour in sorted_hours:438 data = weather_data[hour]439 440 # Calculate current conditions441 temp = data.get('temp', 0)442 wind_u = data.get('wind_u', 0)443 wind_v = data.get('wind_v', 0)444 wind_speed = (wind_u**2 + wind_v**2)**0.5445 precip = data.get('precip', 0)446 447 # Determine if conditions are similar to current trend448 current_wind_speed = current_trend['conditions'].get('wind_speed', wind_speed)449 current_precip = current_trend['conditions'].get('precip', precip)450 451 # Check for significant changes452 wind_change = abs(wind_speed - current_wind_speed) > 2 # 2 m/s change453 precip_change = abs(precip - current_precip) > 0.5 # 0.5mm change454 455 # If conditions changed significantly, start new trend456 if wind_change or precip_change:457 if current_trend['temp_range']: # Save previous trend458 trends.append(current_trend.copy())459 460 # Start new trend461 current_trend = {462 'start_hour': hour,463 'end_hour': hour,464 'conditions': {465 'wind_speed': wind_speed,466 'wind_u': wind_u,467 'wind_v': wind_v,468 'precip': precip469 },470 'temp_range': [temp]471 }472 else:473 # Continue current trend474 current_trend['end_hour'] = hour475 current_trend['temp_range'].append(temp)476 # Update average conditions477 current_trend['conditions']['wind_speed'] = wind_speed478 current_trend['conditions']['wind_u'] = wind_u479 current_trend['conditions']['wind_v'] = wind_v480 current_trend['conditions']['precip'] = precip481 482 # Add final trend483 if current_trend['temp_range']:484 trends.append(current_trend)485 486 return trends487 488 def hour_to_time_description(self, hour):489 """Convert hour to natural time description"""490 if hour == 0:491 return "midnight"492 elif hour < 6:493 return "early morning"494 elif hour < 12:495 return "morning"496 elif hour == 12:497 return "noon"498 elif hour < 18:499 return "afternoon"500 elif hour < 21:501 return "evening"502 else:503 return "night"504 505 def generate_time_range_text(self, start_hour, end_hour):506 """Generate natural time range description"""507 if start_hour == end_hour:508 if start_hour == 0:509 return "around midnight"510 elif start_hour == 12:511 return "around noon"512 else:513 period = self.hour_to_time_description(start_hour)514 return f"in the {period}"515 else:516 start_desc = self.hour_to_time_description(start_hour)517 end_desc = self.hour_to_time_description(end_hour)518 519 if start_desc == end_desc:520 return f"throughout the {start_desc}"521 else:522 return f"from {start_desc} through {end_desc}"523 524 def get_comfort_description(self, temp_f, wind_speed_ms, precip_mm):525 """Generate comfort and activity descriptions"""526 descriptions = []527 528 # Temperature comfort529 if temp_f < 32:530 descriptions.append("Bundle up in winter clothing and watch for icy conditions")531 elif temp_f < 50:532 descriptions.append("A warm jacket or coat will be needed for outdoor activities")533 elif temp_f < 68:534 descriptions.append("Light layers are recommended for comfortable outdoor time")535 elif temp_f < 80:536 descriptions.append("Perfect weather for outdoor activities and recreation")537 else:538 descriptions.append("Stay hydrated and seek shade during extended outdoor time")539 540 # Wind impact541 wind_mph = wind_speed_ms * 2.237542 if wind_mph > 15:543 descriptions.append("Strong winds may affect driving conditions and outdoor events")544 elif wind_mph > 8:545 descriptions.append("Moderate winds will be noticeable, especially in exposed areas")546 547 # Precipitation impact548 if precip_mm > 5:549 descriptions.append("Heavy rain expected - indoor activities recommended, roads may be slick")550 elif precip_mm > 1:551 descriptions.append("Keep an umbrella handy and allow extra time for travel")552 elif precip_mm > 0.1:553 descriptions.append("Light rain possible - consider bringing a light rain jacket")554 555 return descriptions556 557 def get_pressure_description(self, pressure_hpa):558 """Generate atmospheric pressure description"""559 if pressure_hpa > 1025:560 return "High pressure systems typically bring stable, clear conditions"561 elif pressure_hpa < 1005:562 return "Low pressure systems often bring unsettled weather and possible storms"563 else:564 return "Pressure conditions are typical for the season"565 566 def _max_risk_level(self, current, new):567 """Helper function to compare risk levels"""568 levels = {"none": 0, "low": 1, "moderate": 2, "high": 3, "extreme": 4}569 if levels.get(new, 0) > levels.get(current, 0):570 return new571 return current572 573 def assess_thunderstorm_risk(self, cape, cin=None, lifted_index=None, wind_gust=None, convective_precip=None):574 """Assess thunderstorm potential based on atmospheric parameters"""575 risks = []576 risk_level = "none"577 578 # CAPE analysis (Convective Available Potential Energy)579 if cape is not None and cape > 0:580 if cape > 4000:581 risks.append("Very high atmospheric instability with explosive thunderstorm potential")582 risk_level = "extreme"583 elif cape > 2500:584 risks.append("High atmospheric instability favoring severe thunderstorm development")585 risk_level = "high"586 elif cape > 1500:587 risks.append("Moderate atmospheric instability supporting thunderstorm development")588 risk_level = "moderate"589 elif cape > 500:590 risks.append("Low to moderate instability with isolated thunderstorm potential")591 risk_level = "low"592 593 # Lifted Index analysis (lower values = higher instability)594 if lifted_index is not None:595 if lifted_index < -6:596 risks.append("Extremely unstable atmosphere - severe thunderstorms likely")597 risk_level = self._max_risk_level(risk_level, "extreme")598 elif lifted_index < -3:599 risks.append("Very unstable conditions favoring strong thunderstorms")600 risk_level = self._max_risk_level(risk_level, "high")601 elif lifted_index < 0:602 risks.append("Unstable atmosphere conducive to thunderstorm development")603 risk_level = self._max_risk_level(risk_level, "moderate")604 605 # Wind gust analysis606 if wind_gust is not None:607 wind_gust_mph = wind_gust * 2.237 # Convert to mph608 if wind_gust_mph > 58: # Severe thunderstorm criteria609 risks.append(f"Damaging wind gusts up to {wind_gust_mph:.0f} mph possible - severe weather likely")610 risk_level = self._max_risk_level(risk_level, "high")611 elif wind_gust_mph > 39:612 risks.append(f"Strong wind gusts up to {wind_gust_mph:.0f} mph expected")613 risk_level = self._max_risk_level(risk_level, "moderate")614 615 # Convective precipitation analysis616 if convective_precip is not None and convective_precip > 0:617 if convective_precip > 25: # Heavy convective precipitation618 risks.append("Heavy convective rainfall with flash flood potential")619 risk_level = self._max_risk_level(risk_level, "high")620 elif convective_precip > 10:621 risks.append("Significant convective rainfall expected")622 risk_level = self._max_risk_level(risk_level, "moderate")623 624 return risks, risk_level625 626 def get_hazard_warnings(self, weather_data):627 """Generate weather hazard warnings based on forecast data"""628 warnings = []629 max_risk_level = "none"630 631 for hour, data in weather_data.items():632 cape = data.get('cape', 0)633 wind_gust = data.get('wind_gust', 0)634 conv_precip = data.get('conv_precip', 0)635 temp = data.get('temp', 0)636 637 # Thunderstorm risk assessment638 storm_risks, risk_level = self.assess_thunderstorm_risk(639 cape=cape, 640 wind_gust=wind_gust,641 convective_precip=conv_precip642 )643 644 if storm_risks:645 max_risk_level = self._max_risk_level(max_risk_level, risk_level)646 for risk in storm_risks[:2]: # Limit to top 2 risks per hour647 if risk not in warnings:648 warnings.append(risk)649 650 # Extreme temperature warnings651 if temp < -10: # Very cold conditions652 warning = "Extreme cold conditions - risk of frostbite and hypothermia"653 if warning not in warnings:654 warnings.append(warning)655 max_risk_level = self._max_risk_level(max_risk_level, "moderate")656 elif temp > 35: # Very hot conditions in Celsius657 warning = "Extreme heat conditions - risk of heat exhaustion and heat stroke"658 if warning not in warnings:659 warnings.append(warning)660 max_risk_level = self._max_risk_level(max_risk_level, "moderate")661 662 # High wind warnings663 if wind_gust > 15: # > 33 mph664 warning = f"High wind warning - gusts up to {wind_gust * 2.237:.0f} mph"665 if warning not in warnings:666 warnings.append(warning)667 max_risk_level = self._max_risk_level(max_risk_level, "moderate")668 669 return warnings, max_risk_level670 671 def get_snow_analysis(self, snowfall):672 """Analyze snowfall potential"""673 if snowfall > 0.3: # 30cm+674 return "Heavy snow expected - significant travel disruption likely"675 elif snowfall > 0.1: # 10cm+676 return "Moderate snowfall expected - travel may be impacted"677 elif snowfall > 0.02: # 2cm+678 return "Light snow possible - minor travel impacts"679 return None680 681 def generate_detailed_period_description(self, trend, trend_index, total_trends):682 """Generate a comprehensive description for a weather period"""683 start_hour = trend['start_hour']684 end_hour = trend['end_hour']685 temp_range = trend['temp_range']686 conditions = trend['conditions']687 688 if not temp_range:689 return ""690 691 # Time description with more context692 time_desc = self.generate_time_range_text(start_hour, end_hour)693 694 # Enhanced temperature analysis695 avg_temp = sum(temp_range) / len(temp_range)696 temp_desc = self.get_temperature_descriptor(avg_temp)697 698 if len(temp_range) > 1:699 min_temp = min(temp_range)700 max_temp = max(temp_range)701 temp_trend = "rising" if temp_range[-1] > temp_range[0] else "falling" if temp_range[-1] < temp_range[0] else "steady"702 703 if max_temp - min_temp > 5:704 temp_text = f"temperatures {temp_desc}, {temp_trend} from {min_temp:.0f}°F to {max_temp:.0f}°F"705 else:706 temp_text = f"temperatures {temp_desc} around {avg_temp:.0f}°F, remaining {temp_trend}"707 else:708 temp_text = f"temperatures {temp_desc} around {avg_temp:.0f}°F"709 710 # Weather conditions analysis711 precip = conditions.get('precip', 0)712 wind_speed = conditions.get('wind_speed', 0)713 wind_u = conditions.get('wind_u', 0)714 wind_v = conditions.get('wind_v', 0)715 pressure = conditions.get('pressure', 1013)716 717 # Sky conditions with more detail718 sky_desc = self.get_sky_condition(precip_mm=precip)719 720 # Enhanced wind analysis721 wind_desc = self.get_wind_descriptor(wind_speed)722 wind_dir = self.get_wind_direction_text(wind_u, wind_v)723 wind_mph = wind_speed * 2.237724 725 # Build comprehensive description726 description = f"{time_desc.capitalize()}, expect {sky_desc} with {temp_text}"727 728 # Add precipitation details729 precip_desc = self.get_precipitation_descriptor(precip)730 if precip_desc and "with rain" not in sky_desc and "with showers" not in sky_desc:731 description += f" and {precip_desc}"732 733 # Add detailed wind information734 if wind_speed > 2: # Lowered threshold for more wind reporting735 description += f". {wind_desc.capitalize()}"736 if wind_dir:737 description += f" {wind_dir}"738 if wind_mph > 10:739 description += f" at {wind_mph:.0f} mph"740 741 description += "."742 743 # Add comfort and activity guidance744 comfort_items = self.get_comfort_description(avg_temp, wind_speed, precip)745 if comfort_items:746 description += f" {comfort_items[0]}."747 if len(comfort_items) > 1:748 description += f" {comfort_items[1]}."749 750 return description751 752 def generate_24_hour_forecast(self, forecast_data):753 """Generate a comprehensive, detailed 24-hour narrative forecast"""754 if not forecast_data:755 return "Weather forecast data is not available at this time."756 757 # Convert forecast data to time-indexed format758 weather_by_hour = {}759 760 for param_info in forecast_data:761 param_name = param_info['parameter']762 for data_point in param_info['data']:763 hour = data_point['step']764 if hour <= 24: # Only use first 24 hours765 if hour not in weather_by_hour:766 weather_by_hour[hour] = {}767 768 # Map parameter names to our internal names769 if param_name == '2t':770 weather_by_hour[hour]['temp'] = data_point['value']771 elif param_name == 'tp':772 weather_by_hour[hour]['precip'] = data_point['value']773 elif param_name == '10u':774 weather_by_hour[hour]['wind_u'] = data_point['value']775 elif param_name == '10v':776 weather_by_hour[hour]['wind_v'] = data_point['value']777 elif param_name == 'msl':778 weather_by_hour[hour]['pressure'] = data_point['value']779 elif param_name == 'cape':780 weather_by_hour[hour]['cape'] = data_point['value']781 elif param_name == '10fg':782 weather_by_hour[hour]['wind_gust'] = data_point['value']783 elif param_name == 'cp':784 weather_by_hour[hour]['conv_precip'] = data_point['value']785 elif param_name == 'lsp':786 weather_by_hour[hour]['large_precip'] = data_point['value']787 elif param_name == 'sf':788 weather_by_hour[hour]['snowfall'] = data_point['value']789 790 if not weather_by_hour:791 return "Insufficient weather data to generate forecast."792 793 # Analyze trends for smart grouping794 trends = self.analyze_weather_trend(weather_by_hour)795 796 # Assess severe weather hazards first797 hazard_warnings, max_risk_level = self.get_hazard_warnings(weather_by_hour)798 799 # Generate comprehensive narrative with hazard alerts800 forecast_text = "📝 **24-Hour Detailed Weather Forecast**\n\n"801 802 # Add severe weather alerts if present803 if hazard_warnings:804 risk_emoji = {"extreme": "🚨", "high": "⚠️", "moderate": "⚡", "low": "🌩️"}.get(max_risk_level, "⚠️")805 forecast_text += f"{risk_emoji} **WEATHER HAZARDS ALERT - {max_risk_level.upper()} RISK**\n\n"806 for warning in hazard_warnings[:3]: # Limit to top 3 warnings807 forecast_text += f"• {warning}\n"808 forecast_text += "\n"809 810 # Enhanced overall summary811 all_temps = []812 all_pressures = []813 total_precip = 0814 total_snowfall = 0815 max_wind = 0816 max_wind_gust = 0817 max_cape = 0818 819 for hour_data in weather_by_hour.values():820 if 'temp' in hour_data:821 all_temps.append(hour_data['temp'])822 if 'precip' in hour_data:823 total_precip += hour_data['precip']824 if 'pressure' in hour_data:825 all_pressures.append(hour_data['pressure'])826 if 'snowfall' in hour_data:827 total_snowfall += hour_data['snowfall']828 if 'wind_gust' in hour_data:829 max_wind_gust = max(max_wind_gust, hour_data['wind_gust'])830 if 'cape' in hour_data:831 max_cape = max(max_cape, hour_data['cape'])832 # Calculate wind speed833 if 'wind_u' in hour_data and 'wind_v' in hour_data:834 wind_speed = (hour_data['wind_u']**2 + hour_data['wind_v']**2)**0.5835 max_wind = max(max_wind, wind_speed)836 837 if all_temps:838 high_temp = max(all_temps)839 low_temp = min(all_temps)840 temp_range_desc = self.get_temperature_descriptor((high_temp + low_temp) / 2)841 temp_swing = high_temp - low_temp842 843 # Enhanced summary with more details844 forecast_text += f"**Today's Overview:** Expect a {temp_range_desc} day with temperatures ranging from {low_temp:.0f}°F to {high_temp:.0f}°F"845 846 if temp_swing > 15:847 forecast_text += f" - a significant {temp_swing:.0f}-degree temperature range"848 elif temp_swing > 10:849 forecast_text += f" - a moderate {temp_swing:.0f}-degree temperature variation"850 else:851 forecast_text += " with relatively stable temperatures"852 853 # Add precipitation summary854 if total_precip > 0.1:855 precip_desc = self.get_precipitation_descriptor(total_precip)856 if precip_desc:857 forecast_text += f". {precip_desc.capitalize()} is expected"858 if total_precip > 5:859 forecast_text += " - plan for wet conditions and potential travel delays"860 elif total_precip > 1:861 forecast_text += " - keep rain gear accessible"862 else:863 forecast_text += ". No significant precipitation expected"864 865 # Add wind summary866 if max_wind_gust > 5:867 wind_desc = self.get_wind_descriptor(max_wind_gust)868 forecast_text += f". {wind_desc.capitalize()} with gusts up to {max_wind_gust * 2.237:.0f} mph possible"869 elif max_wind > 5:870 wind_desc = self.get_wind_descriptor(max_wind)871 forecast_text += f". {wind_desc.capitalize()}"872 873 # Add snowfall summary if present874 if total_snowfall > 0.01: # > 1cm875 snow_desc = self.get_snow_analysis(total_snowfall)876 if snow_desc:877 forecast_text += f". {snow_desc}"878 879 # Add thunderstorm potential if significant CAPE880 if max_cape > 500:881 cape_risks, _ = self.assess_thunderstorm_risk(cape=max_cape)882 if cape_risks:883 forecast_text += f". {cape_risks[0]}"884 885 # Add pressure context if available886 if all_pressures:887 avg_pressure = sum(all_pressures) / len(all_pressures)888 pressure_desc = self.get_pressure_description(avg_pressure)889 forecast_text += f". {pressure_desc}"890 891 forecast_text += ".\n\n"892 893 # Generate detailed trend-based narrative894 forecast_text += "**Detailed Period Forecast:**\n\n"895 896 for i, trend in enumerate(trends):897 period_desc = self.generate_detailed_period_description(trend, i, len(trends))898 if period_desc:899 forecast_text += period_desc + "\n\n"900 901 # Add comprehensive guidance section902 forecast_text += "**Planning Guidance:**\n\n"903 904 # Clothing recommendations905 if all_temps:906 avg_temp = sum(all_temps) / len(all_temps)907 if avg_temp < 32:908 forecast_text += "• **Clothing:** Heavy winter coat, insulated boots, gloves, and warm hat recommended.\n"909 elif avg_temp < 50:910 forecast_text += "• **Clothing:** Warm jacket or coat, long pants, and closed-toe shoes recommended.\n"911 elif avg_temp < 68:912 forecast_text += "• **Clothing:** Light jacket or sweater, comfortable layers for temperature changes.\n"913 elif avg_temp < 80:914 forecast_text += "• **Clothing:** Light, comfortable clothing perfect for most outdoor activities.\n"915 else:916 forecast_text += "• **Clothing:** Light, breathable fabrics and sun protection recommended.\n"917 918 # Activity recommendations919 if total_precip > 5:920 forecast_text += "• **Activities:** Indoor activities recommended due to heavy rain. Outdoor events should be postponed or moved indoors.\n"921 elif total_precip > 1:922 forecast_text += "• **Activities:** Outdoor activities possible with proper rain gear. Indoor backup plans advised.\n"923 elif max_wind > 8:924 forecast_text += "• **Activities:** Outdoor activities should account for windy conditions. Secure loose objects.\n"925 else:926 forecast_text += "• **Activities:** Good conditions for most outdoor activities and events.\n"927 928 # Travel considerations929 if total_precip > 1 or max_wind > 6:930 forecast_text += "• **Travel:** Allow extra time for travel, reduced visibility and wet roads possible.\n"931 else:932 forecast_text += "• **Travel:** Normal travel conditions expected.\n"933 934 forecast_text += "\n"935 936 # Add disclaimer937 forecast_text += "*This detailed forecast is based on ECMWF operational model data. Weather conditions can change rapidly - check for updates before making important outdoor plans.*"938 939 return forecast_text940 941 942class WeatherApp:943 def __init__(self):944 self.ecmwf = ECMWFDataManager()945 self.narrative_generator = WeatherNarrativeGenerator()946 self.preload_status = {"loaded": False, "count": 0, "total": 0}947 948 def create_map(self):949 """Create interactive map for location selection"""950 try:951 m = folium.Map(952 location=[45.0, 0.0],953 zoom_start=2,954 tiles='OpenStreetMap'955 )956 957 # Add click functionality958 m.add_child(folium.ClickForMarker(popup="Click for coordinates"))959 960 return m._repr_html_()961 except:962 return """963 <div style="padding: 20px; background: #f0f8ff; border-radius: 8px; text-align: center;">964 <h3>🗺️ World Map</h3>965 <p>Map unavailable - use coordinate inputs below</p>966 </div>967 """968 969 def preload_data(self):970 """Preload forecast data for faster access"""971 try:972 loaded_count, total_files = self.ecmwf.preload_all_data()973 self.preload_status = {"loaded": True, "count": loaded_count, "total": total_files}974 975 return f"""✅ Data Preloaded Successfully!976 977📊 Status: {loaded_count}/{total_files} files cached978🌍 Coverage: Global forecast data ready979⚡ Ready for instant weather lookups anywhere on Earth!980 981Now you can click on the map or enter coordinates for instant forecasts."""982 except Exception as e:983 return f"❌ Preload failed: {str(e)}"984 985 def get_weather_forecast(self, latitude, longitude):986 """Get weather forecast for specified coordinates"""987 try:988 if not (-90 <= latitude <= 90) or not (-180 <= longitude <= 180):989 return "Invalid coordinates", "", "", "Cannot generate forecast for invalid coordinates."990 991 forecast_data = self.ecmwf.get_point_forecast(latitude, longitude)992 993 if not forecast_data:994 return "No forecast data available", "", "", "No weather data available to generate narrative forecast."995 996 # Create visualization with calculated wind speed997 fig = go.Figure()998 999 colors = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#e67e22', '#34495e']1000 color_idx = 01001 1002 # Find wind components for speed calculation1003 wind_u_data = None1004 wind_v_data = None1005 1006 for param_info in forecast_data:1007 if param_info['parameter'] == '10u':1008 wind_u_data = param_info['data']1009 elif param_info['parameter'] == '10v':1010 wind_v_data = param_info['data']1011 1012 for param_info in forecast_data:1013 if param_info['data']:1014 steps = [d['step'] for d in param_info['data']]1015 values = [d['value'] for d in param_info['data']]1016 1017 fig.add_trace(go.Scatter(1018 x=steps,1019 y=values,1020 mode='lines+markers',1021 name=f"{param_info['name']} ({param_info['units']})",1022 line=dict(color=colors[color_idx % len(colors)], width=2),1023 marker=dict(size=4),1024 connectgaps=True1025 ))1026 color_idx += 11027 1028 # Add calculated wind speed if both components available1029 if wind_u_data and wind_v_data and len(wind_u_data) == len(wind_v_data):1030 wind_speeds = []1031 wind_steps = []1032 for i, u_data in enumerate(wind_u_data):1033 if i < len(wind_v_data):1034 v_data = wind_v_data[i]1035 if u_data['step'] == v_data['step']:1036 wind_speed = np.sqrt(u_data['value']**2 + v_data['value']**2)1037 wind_speeds.append(wind_speed)1038 wind_steps.append(u_data['step'])1039 1040 if wind_speeds:1041 fig.add_trace(go.Scatter(1042 x=wind_steps,1043 y=wind_speeds,1044 mode='lines+markers',1045 name="Wind Speed (m/s)",1046 line=dict(color=colors[color_idx % len(colors)], width=3, dash='dash'),1047 marker=dict(size=4),1048 connectgaps=True1049 ))1050 1051 fig.update_layout(1052 title=f"🌍 ECMWF 3-Hourly Forecast - {latitude:.3f}°N, {longitude:.3f}°E",1053 xaxis_title="Hours Ahead",1054 yaxis_title="Values",1055 height=700,1056 hovermode='x unified',1057 xaxis=dict(1058 tickmode='array',1059 tickvals=[0, 3, 6, 9, 12, 15, 18, 21, 24, 48, 72, 96, 120],1060 ticktext=['0h', '3h', '6h', '9h', '12h', '15h', '18h', '21h', '1d', '2d', '3d', '4d', '5d'],1061 gridcolor='lightgray',1062 gridwidth=11063 ),1064 legend=dict(1065 orientation="h",1066 yanchor="bottom",1067 y=1.02,1068 xanchor="right",1069 x=11070 ),1071 margin=dict(t=80)1072 )1073 1074 # Create enhanced data table with calculated parameters1075 table_data = []1076 for param_info in forecast_data:1077 for data_point in param_info['data']:1078 table_data.append({1079 'Parameter': param_info['name'],1080 'Hours': f"+{data_point['step']}h",1081 'Value': f"{data_point['value']:.2f} {param_info['units']}",1082 'Valid Time': data_point['datetime'].strftime('%Y-%m-%d %H:%M UTC')1083 })1084 1085 # Add calculated wind parameters1086 if wind_u_data and wind_v_data:1087 for i, u_data in enumerate(wind_u_data):1088 if i < len(wind_v_data):1089 v_data = wind_v_data[i]1090 if u_data['step'] == v_data['step']:1091 # Wind speed1092 wind_speed = np.sqrt(u_data['value']**2 + v_data['value']**2)1093 # Wind direction (meteorological convention)1094 wind_dir = (270 - np.degrees(np.arctan2(v_data['value'], u_data['value']))) % 3601095 1096 table_data.extend([{1097 'Parameter': 'Wind Speed (calculated)',1098 'Hours': f"+{u_data['step']}h",1099 'Value': f"{wind_speed:.2f} m/s",1100 'Valid Time': u_data['datetime'].strftime('%Y-%m-%d %H:%M UTC')1101 }, {1102 'Parameter': 'Wind Direction (calculated)',1103 'Hours': f"+{u_data['step']}h",1104 'Value': f"{wind_dir:.0f} degrees",1105 'Valid Time': u_data['datetime'].strftime('%Y-%m-%d %H:%M UTC')1106 }])1107 1108 df = pd.DataFrame(table_data)1109 table_html = df.to_html(index=False, classes="table table-striped")1110 1111 status = f"""✅ 3-Hourly Forecast Retrieved!1112📍 Location: {latitude:.4f}°N, {longitude:.4f}°E1113📊 Parameters: {len(forecast_data)} weather variables1114⏰ Forecast range: 3-hourly for first 24h, then extended to 120h1115🔄 Data points: {len(table_data)} measurements1116🕐 Resolution: 3-hour intervals for first day, then 6-hour+"""1117 1118 # Generate narrative forecast1119 narrative_forecast = self.narrative_generator.generate_24_hour_forecast(forecast_data)1120 1121 return status, fig, table_html, narrative_forecast1122 1123 except Exception as e:1124 return f"Error: {str(e)}", None, "", "Unable to generate narrative forecast due to data error."1125 1126 1127# Initialize the application1128weather_app = WeatherApp()1129 1130# Gradio interface1131with gr.Blocks(title="ECMWF Weather Forecast") as app:1132 gr.Markdown("""1133 # 🌍 ECMWF Global Weather Forecast1134 ## Real-time weather data from ECMWF operational forecasts1135 1136 **Features:**1137 - 🌐 Global coverage at 25km resolution1138 - 🕐 **3-hourly forecasts for first 24 hours**1139 - 📝 **Plain English narrative forecasts** (like weather.gov)1140 - 🔄 Updated every 6 hours1141 - 📊 Professional meteorological data1142 - 🆓 No API keys required1143 """)1144 1145 with gr.Row():1146 with gr.Column(scale=2):1147 gr.Markdown("### 🗺️ Interactive World Map")1148 map_display = gr.HTML(value=weather_app.create_map())1149 1150 with gr.Column(scale=1):1151 gr.Markdown("### ⚡ Quick Setup")1152 preload_btn = gr.Button("🚀 Preload Global Data", variant="primary", size="lg")1153 preload_status = gr.Textbox(label="Status", lines=8, interactive=False)1154 1155 gr.Markdown("### 📍 Enter Coordinates")1156 latitude = gr.Number(1157 label="Latitude (-90 to 90)",1158 value=40.7128,1159 minimum=-90,1160 maximum=90,1161 step=0.0011162 )1163 longitude = gr.Number(1164 label="Longitude (-180 to 180)", 1165 value=-74.0060,1166 minimum=-180,1167 maximum=180,1168 step=0.0011169 )1170 1171 get_forecast_btn = gr.Button("🌤️ Get Weather Forecast", variant="secondary", size="lg")1172 1173 with gr.Row():1174 with gr.Column():1175 forecast_status = gr.Textbox(label="Forecast Status", lines=6)1176 forecast_plot = gr.Plot(label="Weather Forecast Chart")1177 with gr.Column():1178 narrative_forecast = gr.Markdown(label="24-Hour Weather Forecast", value="Click 'Get Weather Forecast' to generate a narrative forecast...")1179 forecast_table = gr.HTML(label="Detailed Forecast Data")1180 1181 # Event handlers1182 preload_btn.click(1183 weather_app.preload_data,1184 outputs=[preload_status]1185 )1186 1187 get_forecast_btn.click(1188 weather_app.get_weather_forecast,1189 inputs=[latitude, longitude],1190 outputs=[forecast_status, forecast_plot, forecast_table, narrative_forecast]1191 )1192 1193if __name__ == "__main__":1194 app.launch(server_name="0.0.0.0", server_port=7860)