CoolFace
Apppublic

nakas/ecmwf_open_data_forcast

sourceHugging Facemitupdated 1y agoView on Hugging Face
0likes
app.py1194 linesDownload Raw Back to root
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)