diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -34,16 +34,12 @@ from sklearn.decomposition import PCA from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import train_test_split from sklearn.metrics import mean_absolute_error, r2_score -from scipy.interpolate import interp1d, RBFInterpolator, griddata -from scipy.ndimage import gaussian_filter +from scipy.interpolate import interp1d, RBFInterpolator import statsmodels.api as sm import requests import tempfile import shutil import xarray as xr -import urllib.request -from urllib.error import URLError -import ssl # NEW: Advanced ML imports try: @@ -76,9 +72,6 @@ except ImportError: import tropycal.tracks as tracks -# Suppress SSL warnings for oceanic data downloads -ssl._create_default_https_context = ssl._create_unverified_context - # ----------------------------- # Configuration and Setup # ----------------------------- @@ -121,7 +114,7 @@ CACHE_FILE = os.path.join(DATA_PATH, 'ibtracs_cache.pkl') CACHE_EXPIRY_DAYS = 1 # ----------------------------- -# ENHANCED: Color Maps and Standards with TD Support - FIXED TAIWAN CLASSIFICATION +# ENHANCED: Color Maps and Standards with TD Support (FIXED TAIWAN CLASSIFICATION) # ----------------------------- # Enhanced color mapping with TD support (for Plotly) enhanced_color_map = { @@ -147,14 +140,12 @@ matplotlib_color_map = { 'C5 Super Typhoon': '#FF0000' # Red } -# FIXED: Taiwan color mapping with correct CMA 2006 standards -taiwan_color_map_fixed = { - 'Tropical Depression': '#808080', # Gray - 'Tropical Storm': '#0000FF', # Blue - 'Severe Tropical Storm': '#00FFFF', # Cyan - 'Typhoon': '#FFFF00', # Yellow - 'Severe Typhoon': '#FFA500', # Orange - 'Super Typhoon': '#FF0000' # Red +# FIXED Taiwan color mapping with correct categories +taiwan_color_map = { + 'Tropical Depression': '#808080', # Gray + 'Tropical Storm': '#0000FF', # Blue + 'Moderate Typhoon': '#FFA500', # Orange + 'Intense Typhoon': '#FF0000' # Red } def rgb_string_to_hex(rgb_string): @@ -175,9 +166,9 @@ def get_matplotlib_color(category): """Get matplotlib-compatible color for a storm category""" return matplotlib_color_map.get(category, '#808080') -def get_taiwan_color_fixed(category): - """Get corrected Taiwan standard color""" - return taiwan_color_map_fixed.get(category, '#808080') +def get_taiwan_color(category): + """Get Taiwan standard color for a storm category""" + return taiwan_color_map.get(category, '#808080') # Cluster colors for route visualization CLUSTER_COLORS = [ @@ -213,269 +204,14 @@ atlantic_standard = { 'Tropical Depression': {'wind_speed': 0, 'color': 'Gray', 'hex': '#808080'} } -# FIXED: Taiwan standard with correct CMA 2006 thresholds -taiwan_standard_fixed = { - 'Super Typhoon': {'wind_speed_ms': 51.0, 'wind_speed_kt': 99.2, 'color': 'Red', 'hex': '#FF0000'}, - 'Severe Typhoon': {'wind_speed_ms': 41.5, 'wind_speed_kt': 80.7, 'color': 'Orange', 'hex': '#FFA500'}, - 'Typhoon': {'wind_speed_ms': 32.7, 'wind_speed_kt': 63.6, 'color': 'Yellow', 'hex': '#FFFF00'}, - 'Severe Tropical Storm': {'wind_speed_ms': 24.5, 'wind_speed_kt': 47.6, 'color': 'Cyan', 'hex': '#00FFFF'}, - 'Tropical Storm': {'wind_speed_ms': 17.2, 'wind_speed_kt': 33.4, 'color': 'Blue', 'hex': '#0000FF'}, - 'Tropical Depression': {'wind_speed_ms': 0, 'wind_speed_kt': 0, 'color': 'Gray', 'hex': '#808080'} +# FIXED Taiwan standard with correct official CWA thresholds +taiwan_standard = { + 'Intense Typhoon': {'wind_speed': 51.0, 'color': 'Red', 'hex': '#FF0000'}, # 100+ knots (51.0+ m/s) + 'Moderate Typhoon': {'wind_speed': 32.7, 'color': 'Orange', 'hex': '#FFA500'}, # 64-99 knots (32.7-50.9 m/s) + 'Tropical Storm': {'wind_speed': 17.2, 'color': 'Blue', 'hex': '#0000FF'}, # 34-63 knots (17.2-32.6 m/s) + 'Tropical Depression': {'wind_speed': 0, 'color': 'Gray', 'hex': '#808080'} # <34 knots (<17.2 m/s) } -# ----------------------------- -# ENHANCED: Oceanic Data Integration -# ----------------------------- - -class OceanicDataManager: - """Manages real-time oceanic data for enhanced typhoon prediction""" - - def __init__(self): - self.sst_base_url = "https://www.ncei.noaa.gov/erddap/griddap/NOAA_OISST_V2.nc" - self.slp_base_url = "https://psl.noaa.gov/thredds/dodsC/Datasets/ncep.reanalysis.dailyavgs/surface/slp.nc" - self.cache_dir = os.path.join(DATA_PATH, 'oceanic_cache') - self.create_cache_directory() - - def create_cache_directory(self): - """Create cache directory for oceanic data""" - try: - os.makedirs(self.cache_dir, exist_ok=True) - except Exception as e: - logging.warning(f"Could not create cache directory: {e}") - self.cache_dir = tempfile.mkdtemp() - - def get_sst_data(self, lat_min, lat_max, lon_min, lon_max, date_start, date_end=None): - """ - Fetch Sea Surface Temperature data from NOAA OISST v2 - - Parameters: - lat_min, lat_max: Latitude bounds - lon_min, lon_max: Longitude bounds - date_start: Start date (datetime or string) - date_end: End date (datetime or string, optional) - """ - try: - if date_end is None: - date_end = date_start - - # Convert dates to strings if needed - if isinstance(date_start, datetime): - date_start_str = date_start.strftime('%Y-%m-%d') - else: - date_start_str = str(date_start) - - if isinstance(date_end, datetime): - date_end_str = date_end.strftime('%Y-%m-%d') - else: - date_end_str = str(date_end) - - # Construct ERDDAP URL with parameters - url_params = ( - f"?sst[({date_start_str}):1:({date_end_str})]" - f"[({lat_min}):1:({lat_max})]" - f"[({lon_min}):1:({lon_max})]" - ) - full_url = self.sst_base_url + url_params - - logging.info(f"Fetching SST data from: {full_url}") - - # Use xarray to open the remote dataset - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - ds = xr.open_dataset(full_url) - - # Extract SST data - sst_data = ds['sst'].values - lats = ds['latitude'].values - lons = ds['longitude'].values - times = ds['time'].values - - ds.close() - - return { - 'sst': sst_data, - 'latitude': lats, - 'longitude': lons, - 'time': times, - 'success': True - } - - except Exception as e: - logging.error(f"Error fetching SST data: {e}") - return self._get_fallback_sst_data(lat_min, lat_max, lon_min, lon_max) - - def get_slp_data(self, lat_min, lat_max, lon_min, lon_max, date_start, date_end=None): - """ - Fetch Sea Level Pressure data from NCEP/NCAR Reanalysis - - Parameters similar to get_sst_data - """ - try: - if date_end is None: - date_end = date_start - - # Convert dates for OPeNDAP access - if isinstance(date_start, datetime): - # NCEP uses different time indexing, may need adjustment - date_start_str = date_start.strftime('%Y-%m-%d') - else: - date_start_str = str(date_start) - - if isinstance(date_end, datetime): - date_end_str = date_end.strftime('%Y-%m-%d') - else: - date_end_str = str(date_end) - - logging.info(f"Fetching SLP data for {date_start_str} to {date_end_str}") - - # Use xarray to open OPeNDAP dataset - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - # Open the full dataset (this might be large, so we'll subset) - ds = xr.open_dataset(self.slp_base_url) - - # Subset by time and location - # Note: Coordinate names might vary, adjust as needed - lat_coord = 'lat' if 'lat' in ds.dims else 'latitude' - lon_coord = 'lon' if 'lon' in ds.dims else 'longitude' - - # Subset the data - subset = ds.sel( - time=slice(date_start_str, date_end_str), - **{lat_coord: slice(lat_min, lat_max), - lon_coord: slice(lon_min, lon_max)} - ) - - # Extract SLP data - slp_data = subset['slp'].values - lats = subset[lat_coord].values - lons = subset[lon_coord].values - times = subset['time'].values - - ds.close() - - return { - 'slp': slp_data, - 'latitude': lats, - 'longitude': lons, - 'time': times, - 'success': True - } - - except Exception as e: - logging.error(f"Error fetching SLP data: {e}") - return self._get_fallback_slp_data(lat_min, lat_max, lon_min, lon_max) - - def _get_fallback_sst_data(self, lat_min, lat_max, lon_min, lon_max): - """Generate realistic fallback SST data based on climatology""" - # Create a reasonable grid - lats = np.linspace(lat_min, lat_max, 20) - lons = np.linspace(lon_min, lon_max, 20) - - # Generate climatological SST values for Western Pacific - sst_values = np.zeros((1, len(lats), len(lons))) - - for i, lat in enumerate(lats): - for j, lon in enumerate(lons): - # Climatological SST estimation for Western Pacific - if lat < 10: # Tropical - base_sst = 29.0 - elif lat < 20: # Subtropical - base_sst = 28.0 - (lat - 10) * 0.3 - elif lat < 30: # Temperate - base_sst = 25.0 - (lat - 20) * 0.5 - else: # Cool waters - base_sst = 20.0 - (lat - 30) * 0.3 - - # Add some realistic variation - sst_values[0, i, j] = base_sst + np.random.normal(0, 0.5) - - return { - 'sst': sst_values, - 'latitude': lats, - 'longitude': lons, - 'time': [datetime.now()], - 'success': False, - 'note': 'Using climatological fallback data' - } - - def _get_fallback_slp_data(self, lat_min, lat_max, lon_min, lon_max): - """Generate realistic fallback SLP data""" - lats = np.linspace(lat_min, lat_max, 20) - lons = np.linspace(lon_min, lon_max, 20) - - slp_values = np.zeros((1, len(lats), len(lons))) - - for i, lat in enumerate(lats): - for j, lon in enumerate(lons): - # Climatological SLP estimation - if lat < 30: # Subtropical high influence - base_slp = 1013 + 3 * np.cos(np.radians(lat * 6)) - else: # Mid-latitude - base_slp = 1010 - (lat - 30) * 0.2 - - slp_values[0, i, j] = base_slp + np.random.normal(0, 2) - - return { - 'slp': slp_values, - 'latitude': lats, - 'longitude': lons, - 'time': [datetime.now()], - 'success': False, - 'note': 'Using climatological fallback data' - } - - def interpolate_data_to_point(self, data_dict, target_lat, target_lon, variable='sst'): - """Interpolate gridded data to a specific point""" - try: - data = data_dict[variable] - lats = data_dict['latitude'] - lons = data_dict['longitude'] - - # Take most recent time if multiple times available - if len(data.shape) == 3: # time, lat, lon - data_2d = data[-1, :, :] - else: # lat, lon - data_2d = data - - # Create coordinate grids - lon_grid, lat_grid = np.meshgrid(lons, lats) - - # Flatten for interpolation - points = np.column_stack((lat_grid.flatten(), lon_grid.flatten())) - values = data_2d.flatten() - - # Remove NaN values - valid_mask = ~np.isnan(values) - points = points[valid_mask] - values = values[valid_mask] - - if len(values) == 0: - return np.nan - - # Interpolate to target point - interpolated_value = griddata( - points, values, (target_lat, target_lon), - method='linear', fill_value=np.nan - ) - - # If linear interpolation fails, try nearest neighbor - if np.isnan(interpolated_value): - interpolated_value = griddata( - points, values, (target_lat, target_lon), - method='nearest' - ) - - return interpolated_value - - except Exception as e: - logging.error(f"Error interpolating {variable} data: {e}") - return np.nan - -# Global oceanic data manager -oceanic_manager = None - # ----------------------------- # Utility Functions for HF Spaces # ----------------------------- @@ -973,7 +709,7 @@ def merge_data(oni_long, typhoon_max): return pd.merge(typhoon_max, oni_long, on=['Year','Month']) # ----------------------------- -# ENHANCED: Categorization Functions - FIXED TAIWAN CLASSIFICATION +# ENHANCED: Categorization Functions (FIXED TAIWAN) # ----------------------------- def categorize_typhoon_enhanced(wind_speed): @@ -1001,33 +737,26 @@ def categorize_typhoon_enhanced(wind_speed): else: # 137+ knots = Category 5 Super Typhoon return 'C5 Super Typhoon' -def categorize_typhoon_taiwan_fixed(wind_speed): - """ - FIXED Taiwan categorization system based on CMA 2006 standards - Reference: CMA Tropical Cyclone Data Center official classification - """ +def categorize_typhoon_taiwan(wind_speed): + """FIXED Taiwan categorization system according to official CWA standards""" if pd.isna(wind_speed): return 'Tropical Depression' - # Convert from knots to m/s if input appears to be in knots - if wind_speed > 50: # Likely in knots, convert to m/s - wind_speed_ms = wind_speed * 0.514444 - else: + # Convert from knots to m/s (official CWA uses m/s thresholds) + if wind_speed > 200: # Likely already in m/s wind_speed_ms = wind_speed + else: # Likely in knots, convert to m/s + wind_speed_ms = wind_speed * 0.514444 - # CMA 2006 Classification Standards (used by Taiwan CWA) - if wind_speed_ms >= 51.0: - return 'Super Typhoon' # ≥51.0 m/s (≥99.2 kt) - elif wind_speed_ms >= 41.5: - return 'Severe Typhoon' # 41.5–50.9 m/s (80.7–99.1 kt) - elif wind_speed_ms >= 32.7: - return 'Typhoon' # 32.7–41.4 m/s (63.6–80.6 kt) - elif wind_speed_ms >= 24.5: - return 'Severe Tropical Storm' # 24.5–32.6 m/s (47.6–63.5 kt) - elif wind_speed_ms >= 17.2: - return 'Tropical Storm' # 17.2–24.4 m/s (33.4–47.5 kt) - else: - return 'Tropical Depression' # < 17.2 m/s (< 33.4 kt) + # Official CWA Taiwan classification thresholds + if wind_speed_ms >= 51.0: # 100+ knots + return 'Intense Typhoon' + elif wind_speed_ms >= 32.7: # 64-99 knots + return 'Moderate Typhoon' + elif wind_speed_ms >= 17.2: # 34-63 knots + return 'Tropical Storm' + else: # <34 knots + return 'Tropical Depression' # Original function for backward compatibility def categorize_typhoon(wind_speed): @@ -1047,673 +776,912 @@ def classify_enso_phases(oni_value): else: return 'Neutral' -# FIXED: Combined categorization function -def categorize_typhoon_by_standard_fixed(wind_speed, standard='atlantic'): - """FIXED categorization function supporting both standards with correct Taiwan thresholds""" +def categorize_typhoon_by_standard(wind_speed, standard='atlantic'): + """FIXED categorization function with correct Taiwan standards""" if pd.isna(wind_speed): return 'Tropical Depression', '#808080' if standard == 'taiwan': - category = categorize_typhoon_taiwan_fixed(wind_speed) - color = taiwan_color_map_fixed.get(category, '#808080') + category = categorize_typhoon_taiwan(wind_speed) + color = taiwan_color_map.get(category, '#808080') return category, color - else: - # Atlantic/International standard (unchanged) + # Atlantic/International standard (existing logic is correct) if wind_speed >= 137: - return 'C5 Super Typhoon', '#FF0000' + return 'C5 Super Typhoon', '#FF0000' # Red elif wind_speed >= 113: - return 'C4 Very Strong Typhoon', '#FFA500' + return 'C4 Very Strong Typhoon', '#FFA500' # Orange elif wind_speed >= 96: - return 'C3 Strong Typhoon', '#FFFF00' + return 'C3 Strong Typhoon', '#FFFF00' # Yellow elif wind_speed >= 83: - return 'C2 Typhoon', '#00FF00' + return 'C2 Typhoon', '#00FF00' # Green elif wind_speed >= 64: - return 'C1 Typhoon', '#00FFFF' + return 'C1 Typhoon', '#00FFFF' # Cyan elif wind_speed >= 34: - return 'Tropical Storm', '#0000FF' - else: - return 'Tropical Depression', '#808080' + return 'Tropical Storm', '#0000FF' # Blue + return 'Tropical Depression', '#808080' # Gray # ----------------------------- -# ENHANCED: Historical Environmental Analysis +# FIXED: Genesis Potential Index (GPI) Based Prediction System # ----------------------------- -def analyze_historical_environment(typhoon_data, oni_data): - """Analyze historical environmental conditions for better predictions""" +def calculate_genesis_potential_index(sst, rh, vorticity, wind_shear, lat, lon, month, oni_value): + """ + Calculate Genesis Potential Index based on environmental parameters + Following Emanuel and Nolan (2004) formulation with modifications for monthly predictions + """ try: - logging.info("Analyzing historical environmental patterns...") - - # Get historical storm data with environmental conditions - historical_analysis = { - 'sst_patterns': {}, - 'slp_patterns': {}, - 'oni_relationships': {}, - 'seasonal_variations': {}, - 'intensity_predictors': {} + # Base environmental parameters + + # SST factor - optimal range 26-30°C + sst_factor = max(0, (sst - 26.5) / 4.0) if sst > 26.5 else 0 + + # Humidity factor - mid-level relative humidity (600 hPa) + rh_factor = max(0, (rh - 40) / 50.0) # Normalized 40-90% + + # Vorticity factor - low-level absolute vorticity (850 hPa) + vort_factor = max(0, min(vorticity / 5e-5, 3.0)) # Cap at reasonable max + + # Wind shear factor - vertical wind shear (inverse relationship) + shear_factor = max(0, (20 - wind_shear) / 15.0) if wind_shear < 20 else 0 + + # Coriolis factor - latitude dependency + coriolis_factor = max(0, min(abs(lat) / 20.0, 1.0)) if abs(lat) > 5 else 0 + + # Seasonal factor + seasonal_weights = { + 1: 0.3, 2: 0.2, 3: 0.4, 4: 0.6, 5: 0.8, 6: 1.0, + 7: 1.2, 8: 1.4, 9: 1.5, 10: 1.3, 11: 0.9, 12: 0.5 } + seasonal_factor = seasonal_weights.get(month, 1.0) - # Analyze by storm intensity categories - for category in ['Tropical Depression', 'Tropical Storm', 'C1 Typhoon', - 'C2 Typhoon', 'C3 Strong Typhoon', 'C4 Very Strong Typhoon', 'C5 Super Typhoon']: - - # Filter storms by category - if 'USA_WIND' in typhoon_data.columns: - category_storms = typhoon_data[ - typhoon_data['USA_WIND'].apply(categorize_typhoon_enhanced) == category - ] - - if len(category_storms) > 0: - historical_analysis['intensity_predictors'][category] = { - 'avg_genesis_lat': category_storms['LAT'].mean(), - 'avg_genesis_lon': category_storms['LON'].mean(), - 'count': len(category_storms['SID'].unique()), - 'seasonal_distribution': category_storms['ISO_TIME'].dt.month.value_counts().to_dict() if 'ISO_TIME' in category_storms.columns else {} - } - - # Analyze ENSO relationships - if len(oni_data) > 0: - for phase in ['El Nino', 'La Nina', 'Neutral']: - # This would be enhanced with actual storm-ENSO matching - historical_analysis['oni_relationships'][phase] = { - 'storm_frequency_modifier': 1.0, # Will be calculated from real data - 'intensity_modifier': 0.0, - 'track_shift': {'lat': 0.0, 'lon': 0.0} - } - - logging.info("Historical environmental analysis complete") - return historical_analysis + # ENSO modulation + if oni_value > 0.5: # El Niño + enso_factor = 0.6 if lon > 140 else 0.8 # Suppress in WP + elif oni_value < -0.5: # La Niña + enso_factor = 1.4 if lon > 140 else 1.1 # Enhance in WP + else: # Neutral + enso_factor = 1.0 + + # Regional modulation (Western Pacific specifics) + if 10 <= lat <= 25 and 120 <= lon <= 160: # Main Development Region + regional_factor = 1.3 + elif 5 <= lat <= 15 and 130 <= lon <= 150: # Prime genesis zone + regional_factor = 1.5 + else: + regional_factor = 0.8 + + # Calculate GPI + gpi = (sst_factor * rh_factor * vort_factor * shear_factor * + coriolis_factor * seasonal_factor * enso_factor * regional_factor) + + return max(0, min(gpi, 5.0)) # Cap at reasonable maximum except Exception as e: - logging.error(f"Error in historical environmental analysis: {e}") - return {} + logging.error(f"Error calculating GPI: {e}") + return 0.0 -# ----------------------------- -# ENHANCED: Environmental Intensity Prediction -# ----------------------------- +def get_environmental_conditions(lat, lon, month, oni_value): + """ + Get realistic environmental conditions for a given location and time + Based on climatological patterns and ENSO modulation + """ + try: + # Base SST calculation (climatological) + base_sst = 28.5 - 0.15 * abs(lat - 15) # Peak at 15°N + seasonal_sst_adj = 2.0 * np.cos(2 * np.pi * (month - 9) / 12) # Peak in Sep + enso_sst_adj = oni_value * 0.8 if lon > 140 else oni_value * 0.4 + sst = base_sst + seasonal_sst_adj + enso_sst_adj + + # Relative humidity (600 hPa) + base_rh = 75 - 0.5 * abs(lat - 12) # Peak around 12°N + seasonal_rh_adj = 10 * np.cos(2 * np.pi * (month - 8) / 12) # Peak in Aug + monsoon_effect = 5 if 100 <= lon <= 120 and month in [6,7,8,9] else 0 + rh = max(40, min(90, base_rh + seasonal_rh_adj + monsoon_effect)) + + # Low-level vorticity (850 hPa) + base_vort = 2e-5 * (1 + 0.1 * np.sin(2 * np.pi * lat / 30)) + seasonal_vort_adj = 1e-5 * np.cos(2 * np.pi * (month - 8) / 12) + itcz_effect = 1.5e-5 if 5 <= lat <= 15 else 0 + vorticity = max(0, base_vort + seasonal_vort_adj + itcz_effect) + + # Vertical wind shear (200-850 hPa) + base_shear = 8 + 0.3 * abs(lat - 20) # Lower near 20°N + seasonal_shear_adj = 4 * np.cos(2 * np.pi * (month - 2) / 12) # Low in Aug-Sep + enso_shear_adj = oni_value * 3 if lon > 140 else 0 # El Niño increases shear + wind_shear = max(2, base_shear + seasonal_shear_adj + enso_shear_adj) + + return { + 'sst': sst, + 'relative_humidity': rh, + 'vorticity': vorticity, + 'wind_shear': wind_shear + } + + except Exception as e: + logging.error(f"Error getting environmental conditions: {e}") + return { + 'sst': 28.0, + 'relative_humidity': 70.0, + 'vorticity': 2e-5, + 'wind_shear': 10.0 + } -def calculate_environmental_intensity_potential(lat, lon, month, oni_value, sst_data=None, slp_data=None): +def generate_genesis_prediction_monthly(month, oni_value, year=2025): """ - Calculate environmental intensity potential based on oceanic conditions - - This function integrates multiple environmental factors to estimate - the maximum potential intensity a storm could achieve in given conditions. + Generate realistic typhoon genesis prediction for a given month using GPI + Returns day-by-day genesis potential and storm development scenarios """ try: - # Base intensity potential from climatology - base_potential = 45 # kt - baseline for tropical storm formation + logging.info(f"Generating GPI-based prediction for month {month}, ONI {oni_value}") - # SST contribution (most important factor) - if sst_data and sst_data['success']: - try: - sst_value = oceanic_manager.interpolate_data_to_point( - sst_data, lat, lon, 'sst' - ) - - if not np.isnan(sst_value): - # Convert to Celsius if needed (OISST is in Celsius) - sst_celsius = sst_value if sst_value < 50 else sst_value - 273.15 + # Define the Western Pacific domain + lat_range = np.arange(5, 35, 2.5) # 5°N to 35°N + lon_range = np.arange(110, 180, 2.5) # 110°E to 180°E + + # Number of days in the month + days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1] + if month == 2 and year % 4 == 0: # Leap year + days_in_month = 29 + + # Daily GPI evolution + daily_gpi_maps = [] + genesis_events = [] + + for day in range(1, days_in_month + 1): + # Calculate GPI for each grid point + gpi_field = np.zeros((len(lat_range), len(lon_range))) + + for i, lat in enumerate(lat_range): + for j, lon in enumerate(lon_range): + # Get environmental conditions + env_conditions = get_environmental_conditions(lat, lon, month, oni_value) - # Enhanced SST-intensity relationship based on research - if sst_celsius >= 30.0: # Very warm - super typhoon potential - sst_contribution = 80 + (sst_celsius - 30) * 10 - elif sst_celsius >= 28.5: # Warm - typhoon potential - sst_contribution = 40 + (sst_celsius - 28.5) * 26.7 - elif sst_celsius >= 26.5: # Marginal - tropical storm potential - sst_contribution = 0 + (sst_celsius - 26.5) * 20 - else: # Too cool for significant development - sst_contribution = -30 + # Add daily variability + daily_variation = 0.1 * np.sin(2 * np.pi * day / 30) + np.random.normal(0, 0.05) - base_potential += sst_contribution - logging.debug(f"SST: {sst_celsius:.1f}°C, contribution: {sst_contribution:.1f}kt") - else: - # Use climatological SST - clim_sst = get_climatological_sst(lat, lon, month) - base_potential += max(0, (clim_sst - 26.5) * 15) + # Calculate GPI + gpi = calculate_genesis_potential_index( + sst=env_conditions['sst'] + daily_variation, + rh=env_conditions['relative_humidity'], + vorticity=env_conditions['vorticity'], + wind_shear=env_conditions['wind_shear'], + lat=lat, + lon=lon, + month=month, + oni_value=oni_value + ) - except Exception as e: - logging.warning(f"Error processing SST data: {e}") - clim_sst = get_climatological_sst(lat, lon, month) - base_potential += max(0, (clim_sst - 26.5) * 15) - else: - # Use climatological SST if real data unavailable - clim_sst = get_climatological_sst(lat, lon, month) - base_potential += max(0, (clim_sst - 26.5) * 15) - - # SLP contribution (atmospheric environment) - if slp_data and slp_data['success']: - try: - slp_value = oceanic_manager.interpolate_data_to_point( - slp_data, lat, lon, 'slp' - ) - - if not np.isnan(slp_value): - # Convert from Pa to hPa if needed - slp_hpa = slp_value if slp_value > 500 else slp_value / 100 + gpi_field[i, j] = gpi + + daily_gpi_maps.append({ + 'day': day, + 'gpi_field': gpi_field, + 'lat_range': lat_range, + 'lon_range': lon_range + }) + + # Check for genesis events (GPI > threshold) + genesis_threshold = 1.5 # Adjusted threshold + if np.max(gpi_field) > genesis_threshold: + # Find peak genesis locations + peak_indices = np.where(gpi_field > genesis_threshold) + if len(peak_indices[0]) > 0: + # Select strongest genesis point + max_idx = np.argmax(gpi_field) + max_i, max_j = np.unravel_index(max_idx, gpi_field.shape) - # Lower pressure = better environment for intensification - if slp_hpa < 1008: # Low pressure environment - slp_contribution = (1008 - slp_hpa) * 3 - elif slp_hpa > 1015: # High pressure - suppressed development - slp_contribution = (1015 - slp_hpa) * 2 - else: # Neutral - slp_contribution = 0 + genesis_lat = lat_range[max_i] + genesis_lon = lon_range[max_j] + genesis_gpi = gpi_field[max_i, max_j] - base_potential += slp_contribution - logging.debug(f"SLP: {slp_hpa:.1f}hPa, contribution: {slp_contribution:.1f}kt") + # Determine probability of actual genesis + genesis_prob = min(0.8, genesis_gpi / 3.0) - except Exception as e: - logging.warning(f"Error processing SLP data: {e}") - - # ENSO modulation - if oni_value > 1.0: # Strong El Niño - enso_modifier = -15 # Suppressed development - elif oni_value > 0.5: # Moderate El Niño - enso_modifier = -8 - elif oni_value < -1.0: # Strong La Niña - enso_modifier = +12 # Enhanced development - elif oni_value < -0.5: # Moderate La Niña - enso_modifier = +6 - else: # Neutral - enso_modifier = oni_value * 2 - - base_potential += enso_modifier - - # Seasonal modulation - seasonal_factors = { - 1: -12, 2: -10, 3: -8, 4: -5, 5: 0, 6: 5, - 7: 12, 8: 15, 9: 18, 10: 12, 11: 5, 12: -8 - } - seasonal_modifier = seasonal_factors.get(month, 0) - base_potential += seasonal_modifier - - # Latitude effects - if lat < 8: # Too close to equator - weak Coriolis - lat_modifier = -20 - elif lat < 12: # Good for development - lat_modifier = 5 - elif lat < 25: # Prime development zone - lat_modifier = 10 - elif lat < 35: # Marginal - lat_modifier = -5 - else: # Too far north - lat_modifier = -25 - - base_potential += lat_modifier - - # Wind shear estimation (simplified) - shear_factor = estimate_wind_shear(lat, lon, month, oni_value) - base_potential -= shear_factor - - # Apply realistic bounds - environmental_potential = max(25, min(185, base_potential)) + if np.random.random() < genesis_prob: + genesis_events.append({ + 'day': day, + 'lat': genesis_lat, + 'lon': genesis_lon, + 'gpi': genesis_gpi, + 'probability': genesis_prob, + 'date': f"{year}-{month:02d}-{day:02d}" + }) + + # Generate storm tracks for genesis events + storm_predictions = [] + for i, genesis in enumerate(genesis_events): + storm_track = generate_storm_track_from_genesis( + genesis['lat'], + genesis['lon'], + genesis['day'], + month, + oni_value, + storm_id=i+1 + ) + + if storm_track: + storm_predictions.append({ + 'storm_id': i + 1, + 'genesis_event': genesis, + 'track': storm_track, + 'uncertainty': calculate_track_uncertainty(storm_track) + }) return { - 'potential_intensity': environmental_potential, - 'sst_contribution': sst_contribution if 'sst_contribution' in locals() else 0, - 'slp_contribution': slp_contribution if 'slp_contribution' in locals() else 0, - 'enso_modifier': enso_modifier, - 'seasonal_modifier': seasonal_modifier, - 'latitude_modifier': lat_modifier, - 'shear_factor': shear_factor + 'month': month, + 'year': year, + 'oni_value': oni_value, + 'daily_gpi_maps': daily_gpi_maps, + 'genesis_events': genesis_events, + 'storm_predictions': storm_predictions, + 'summary': { + 'total_genesis_events': len(genesis_events), + 'total_storm_predictions': len(storm_predictions), + 'peak_gpi_day': max(daily_gpi_maps, key=lambda x: np.max(x['gpi_field']))['day'], + 'peak_gpi_value': max(np.max(day_data['gpi_field']) for day_data in daily_gpi_maps) + } } except Exception as e: - logging.error(f"Error calculating environmental potential: {e}") + logging.error(f"Error in genesis prediction: {e}") + import traceback + traceback.print_exc() return { - 'potential_intensity': 50, - 'error': str(e) + 'error': str(e), + 'month': month, + 'oni_value': oni_value, + 'storm_predictions': [] } -def get_climatological_sst(lat, lon, month): - """Get climatological SST for a location and month""" - # Simplified climatological SST model for Western Pacific - base_sst = 28.0 # Base warm pool temperature - - # Latitude effect - if lat < 5: - lat_effect = 0.5 # Warm near equator - elif lat < 15: - lat_effect = 1.0 # Peak warm pool - elif lat < 25: - lat_effect = 0.0 - (lat - 15) * 0.3 # Cooling northward - else: - lat_effect = -3.0 - (lat - 25) * 0.2 # Much cooler - - # Seasonal effect - seasonal_cycle = { - 1: -1.0, 2: -1.2, 3: -0.8, 4: 0.0, 5: 0.5, 6: 0.8, - 7: 1.0, 8: 1.2, 9: 1.0, 10: 0.5, 11: 0.0, 12: -0.5 - } - seasonal_effect = seasonal_cycle.get(month, 0) - - return base_sst + lat_effect + seasonal_effect - -def estimate_wind_shear(lat, lon, month, oni_value): - """Estimate wind shear based on location, season, and ENSO state""" - # Base shear climatology - if 5 <= lat <= 20 and 120 <= lon <= 160: # Low shear region - base_shear = 5 # kt equivalent intensity reduction - elif lat > 25: # Higher latitude - more shear - base_shear = 15 + (lat - 25) * 2 - else: # Marginal regions - base_shear = 10 +def generate_storm_track_from_genesis(genesis_lat, genesis_lon, genesis_day, month, oni_value, storm_id=1): + """ + Generate a realistic storm track from a genesis location + """ + try: + track_points = [] + current_lat = genesis_lat + current_lon = genesis_lon + current_intensity = 25 # Start as TD + + # Track duration (3-10 days typically) + track_duration_hours = np.random.randint(72, 240) + + for hour in range(0, track_duration_hours + 6, 6): + # Calculate storm motion + # Base motion patterns for Western Pacific + if current_lat < 20: # Low latitude - westward motion + lat_speed = 0.1 + np.random.normal(0, 0.05) # Slight poleward + lon_speed = -0.3 + np.random.normal(0, 0.1) # Westward + elif current_lat < 25: # Mid latitude - WNW motion + lat_speed = 0.15 + np.random.normal(0, 0.05) + lon_speed = -0.2 + np.random.normal(0, 0.1) + else: # High latitude - recurvature + lat_speed = 0.2 + np.random.normal(0, 0.05) + lon_speed = 0.1 + np.random.normal(0, 0.1) # Eastward + + # ENSO effects on motion + if oni_value > 0.5: # El Niño - more eastward + lon_speed += 0.05 + elif oni_value < -0.5: # La Niña - more westward + lon_speed -= 0.05 + + # Update position + current_lat += lat_speed + current_lon += lon_speed + + # Intensity evolution + # Get environmental conditions for intensity change + env_conditions = get_environmental_conditions(current_lat, current_lon, month, oni_value) + + # Intensity change factors + sst_factor = max(0, env_conditions['sst'] - 26.5) + shear_factor = max(0, (15 - env_conditions['wind_shear']) / 10) + + # Basic intensity change + if hour < 48: # Development phase + intensity_change = 3 + sst_factor + shear_factor + np.random.normal(0, 2) + elif hour < 120: # Mature phase + intensity_change = 1 + sst_factor * 0.5 + np.random.normal(0, 1.5) + else: # Weakening phase + intensity_change = -2 + sst_factor * 0.3 + np.random.normal(0, 1) + + # Environmental limits + if current_lat > 30: # Cool waters + intensity_change -= 5 + if current_lon < 120: # Land interaction + intensity_change -= 8 + + current_intensity += intensity_change + current_intensity = max(15, min(180, current_intensity)) # Realistic bounds + + # Calculate pressure + pressure = max(900, 1013 - (current_intensity - 25) * 0.9) + + # Add uncertainty + position_uncertainty = 0.5 + (hour / 120) * 1.5 # Growing uncertainty + intensity_uncertainty = 5 + (hour / 120) * 15 + + track_points.append({ + 'hour': hour, + 'day': genesis_day + hour / 24.0, + 'lat': current_lat, + 'lon': current_lon, + 'intensity': current_intensity, + 'pressure': pressure, + 'category': categorize_typhoon_enhanced(current_intensity), + 'position_uncertainty': position_uncertainty, + 'intensity_uncertainty': intensity_uncertainty + }) + + # Stop if storm moves too far or weakens significantly + if current_lat > 40 or current_lat < 0 or current_lon < 100 or current_intensity < 20: + break + + return track_points + + except Exception as e: + logging.error(f"Error generating storm track: {e}") + return None + +def calculate_track_uncertainty(track_points): + """Calculate uncertainty metrics for a storm track""" + if not track_points: + return {'position': 0, 'intensity': 0} - # Seasonal modulation - if month in [12, 1, 2, 3]: # Winter - high shear - seasonal_shear = 8 - elif month in [6, 7, 8, 9]: # Summer - low shear - seasonal_shear = -3 - else: # Transition seasons - seasonal_shear = 2 + # Position uncertainty grows with time + position_uncertainty = [point['position_uncertainty'] for point in track_points] - # ENSO modulation - if oni_value > 0.5: # El Niño - increased shear - enso_shear = 5 + oni_value * 3 - elif oni_value < -0.5: # La Niña - decreased shear - enso_shear = oni_value * 2 - else: - enso_shear = 0 + # Intensity uncertainty + intensity_uncertainty = [point['intensity_uncertainty'] for point in track_points] - total_shear = base_shear + seasonal_shear + enso_shear - return max(0, total_shear) - -# ----------------------------- -# ENHANCED: Realistic Storm Prediction with Oceanic Data -# ----------------------------- - -def get_realistic_genesis_locations(): - """Get realistic typhoon genesis regions based on climatology""" return { - "Western Pacific Main Development Region": {"lat": 12.5, "lon": 145.0, "description": "Peak activity zone (Guam area)"}, - "South China Sea": {"lat": 15.0, "lon": 115.0, "description": "Secondary development region"}, - "Philippine Sea": {"lat": 18.0, "lon": 135.0, "description": "Recurving storm region"}, - "Marshall Islands": {"lat": 8.0, "lon": 165.0, "description": "Eastern development zone"}, - "Monsoon Trough": {"lat": 10.0, "lon": 130.0, "description": "Monsoon-driven genesis"}, - "ITCZ Region": {"lat": 6.0, "lon": 140.0, "description": "Near-equatorial development"}, - "Subtropical Region": {"lat": 22.0, "lon": 125.0, "description": "Late season development"}, - "Bay of Bengal": {"lat": 15.0, "lon": 88.0, "description": "Indian Ocean cyclones"}, - "Eastern Pacific": {"lat": 12.0, "lon": -105.0, "description": "Hurricane development zone"}, - "Atlantic MDR": {"lat": 12.0, "lon": -45.0, "description": "Main Development Region"} + 'position_mean': np.mean(position_uncertainty), + 'position_max': np.max(position_uncertainty), + 'intensity_mean': np.mean(intensity_uncertainty), + 'intensity_max': np.max(intensity_uncertainty), + 'track_length': len(track_points) } - -def predict_storm_route_and_intensity_with_oceanic_data( - genesis_region, month, oni_value, - forecast_hours=72, use_real_data=True, - models=None, enable_animation=True -): +def create_predict_animation(prediction_data, enable_animation=True): """ - Enhanced prediction system integrating real-time oceanic data - - This function provides the most realistic storm development prediction - by incorporating current SST and SLP conditions from global datasets. + Typhoon genesis PREDICT tab animation: + shows monthly genesis-potential + progressive storm positions """ try: - genesis_locations = get_realistic_genesis_locations() - - if genesis_region not in genesis_locations: - genesis_region = "Western Pacific Main Development Region" - - genesis_info = genesis_locations[genesis_region] - start_lat = genesis_info["lat"] - start_lon = genesis_info["lon"] - - logging.info(f"Starting enhanced prediction for {genesis_region}") - - # Determine data bounds for oceanic data fetch - lat_buffer = 10 # degrees - lon_buffer = 15 # degrees - lat_min = start_lat - lat_buffer - lat_max = start_lat + lat_buffer - lon_min = start_lon - lon_buffer - lon_max = start_lon + lon_buffer - - # Fetch current oceanic conditions - current_date = datetime.now() - sst_data = None - slp_data = None - - if use_real_data: - try: - logging.info("Fetching real-time oceanic data...") - - # Fetch SST data - sst_data = oceanic_manager.get_sst_data( - lat_min, lat_max, lon_min, lon_max, - current_date - timedelta(days=1), # Yesterday's data (most recent available) - current_date - ) - - # Fetch SLP data - slp_data = oceanic_manager.get_slp_data( - lat_min, lat_max, lon_min, lon_max, - current_date - timedelta(days=1), - current_date + daily_maps = prediction_data.get('daily_gpi_maps', []) + if not daily_maps: + return create_error_plot("No GPI data for prediction") + + storms = prediction_data.get('storm_predictions', []) + month = prediction_data['month'] + oni = prediction_data['oni_value'] + year = prediction_data.get('year', 2025) + + # -- 1) static underlay: full storm routes (dashed gray lines) + static_routes = [] + for s in storms: + track = s.get('track', []) + if not track: continue + lats = [pt['lat'] for pt in track] + lons = [pt['lon'] for pt in track] + static_routes.append( + go.Scattergeo( + lat=lats, lon=lons, + mode='lines', + line=dict(width=2, dash='dash', color='gray'), + showlegend=False, hoverinfo='skip' ) - - logging.info(f"SST fetch: {'Success' if sst_data['success'] else 'Failed'}") - logging.info(f"SLP fetch: {'Success' if slp_data['success'] else 'Failed'}") - - except Exception as e: - logging.warning(f"Error fetching real-time data, using climatology: {e}") - use_real_data = False - - # Initialize results structure - results = { - 'current_prediction': {}, - 'route_forecast': [], - 'confidence_scores': {}, - 'environmental_data': { - 'sst_source': 'Real-time NOAA OISST' if (sst_data and sst_data['success']) else 'Climatological', - 'slp_source': 'Real-time NCEP/NCAR' if (slp_data and slp_data['success']) else 'Climatological', - 'use_real_data': use_real_data - }, - 'model_info': 'Enhanced Oceanic-Coupled Model', - 'genesis_info': genesis_info - } - - # Calculate initial environmental potential - env_potential = calculate_environmental_intensity_potential( - start_lat, start_lon, month, oni_value, sst_data, slp_data - ) - - # Realistic starting intensity (TD level) with environmental modulation - base_intensity = 30 # Base TD intensity - environmental_boost = min(8, max(-5, env_potential['potential_intensity'] - 50) * 0.15) - predicted_intensity = base_intensity + environmental_boost - predicted_intensity = max(25, min(45, predicted_intensity)) # Keep in TD-weak TS range - - # Enhanced genesis conditions - results['current_prediction'] = { - 'intensity_kt': predicted_intensity, - 'pressure_hpa': 1008 - (predicted_intensity - 25) * 0.8, - 'category': categorize_typhoon_enhanced(predicted_intensity), - 'genesis_region': genesis_region, - 'environmental_potential': env_potential['potential_intensity'], - 'sst_contribution': env_potential.get('sst_contribution', 0), - 'environmental_favorability': 'High' if env_potential['potential_intensity'] > 80 else - ('Moderate' if env_potential['potential_intensity'] > 50 else 'Low') + ) + + # figure out map bounds + all_lats = [pt['lat'] for s in storms for pt in s.get('track',[])] + all_lons = [pt['lon'] for s in storms for pt in s.get('track',[])] + mb = { + 'lat_min': min(5, min(all_lats)-5) if all_lats else 5, + 'lat_max': max(35, max(all_lats)+5) if all_lats else 35, + 'lon_min': min(110, min(all_lons)-10) if all_lons else 110, + 'lon_max': max(180, max(all_lons)+10) if all_lons else 180 } - - # Enhanced route prediction with environmental coupling - current_lat = start_lat - current_lon = start_lon - current_intensity = predicted_intensity - - route_points = [] - - # Historical environmental analysis for better predictions - historical_patterns = analyze_historical_environment(typhoon_data, oni_data) - - # Track storm development with oceanic data integration - for hour in range(0, forecast_hours + 6, 6): - - # Dynamic oceanic conditions along track - if use_real_data and sst_data and slp_data: - # Get current environmental conditions - current_env = calculate_environmental_intensity_potential( - current_lat, current_lon, month, oni_value, sst_data, slp_data + + # -- 2) build frames + frames = [] + for idx, day_data in enumerate(daily_maps): + day = day_data['day'] + gpi = day_data['gpi_field'] + lats = day_data['lat_range'] + lons = day_data['lon_range'] + + traces = [] + # genesis‐potential scatter + traces.append(go.Scattergeo( + lat=np.repeat(lats, len(lons)), + lon=np.tile(lons, len(lats)), + mode='markers', + marker=dict( + size=6, color=gpi.flatten(), + colorscale='Viridis', cmin=0, cmax=3, opacity=0.6, + showscale=(idx==0), + colorbar=(dict( + title=dict(text="Genesis
Potential
Index", side="right") + ) if idx==0 else None) + ), + name='GPI', + showlegend=(idx==0), + hovertemplate=( + 'GPI: %{marker.color:.2f}
' + 'Lat: %{lat:.1f}°N
' + 'Lon: %{lon:.1f}°E
' + f'Day {day} of {month:02d}/{year}' ) - environmental_limit = current_env['potential_intensity'] - else: - # Use climatological estimates - current_env = calculate_environmental_intensity_potential( - current_lat, current_lon, month, oni_value, None, None + )) + + # storm positions up to this day + for s in storms: + past = [pt for pt in s.get('track',[]) if pt['day'] <= day] + if not past: continue + lats_p = [pt['lat'] for pt in past] + lons_p = [pt['lon'] for pt in past] + intens = [pt['intensity'] for pt in past] + cats = [pt['category'] for pt in past] + + # line history + traces.append(go.Scattergeo( + lat=lats_p, lon=lons_p, mode='lines', + line=dict(width=2, color='gray'), + showlegend=(idx==0), hoverinfo='skip' + )) + # current position + traces.append(go.Scattergeo( + lat=[lats_p[-1]], lon=[lons_p[-1]], + mode='markers', + marker=dict(size=10, symbol='circle', color='red'), + showlegend=(idx==0), + hovertemplate=( + f"{s['storm_id']}
" + f"Intensity: {intens[-1]} kt
" + f"Category: {cats[-1]}" + ) + )) + + frames.append(go.Frame( + data=traces, + name=str(day), # ← name is REQUIRED as string :contentReference[oaicite:1]{index=1} + layout=go.Layout( + geo=dict( + projection_type="natural earth", + showland=True, landcolor="lightgray", + showocean=True, oceancolor="lightblue", + showcoastlines=True, coastlinecolor="darkgray", + center=dict(lat=(mb['lat_min']+mb['lat_max'])/2, + lon=(mb['lon_min']+mb['lon_max'])/2), + lonaxis_range=[mb['lon_min'], mb['lon_max']], + lataxis_range=[mb['lat_min'], mb['lat_max']], + resolution=50 + ), + title=f"Day {day} of {month:02d}/{year} | ONI: {oni:.2f}" ) - environmental_limit = current_env['potential_intensity'] - - # Enhanced storm motion with environmental steering - base_speed = calculate_environmental_steering_speed( - current_lat, current_lon, month, oni_value, slp_data - ) - - # Motion vectors with environmental influences - lat_tendency, lon_tendency = calculate_motion_tendency( - current_lat, current_lon, month, oni_value, hour, slp_data + )) + + # -- 3) initial Figure (static + first frame) + init_data = static_routes + list(frames[0].data) + fig = go.Figure(data=init_data, frames=frames) + + # -- 4) play/pause + slider (redraw=True!) + if enable_animation and len(frames)>1: + steps = [ + dict(method="animate", + args=[[fr.name], + {"mode":"immediate", + "frame":{"duration":600,"redraw":True}, + "transition":{"duration":0}}], + label=fr.name) + for fr in frames + ] + + fig.update_layout( + updatemenus=[dict( + type="buttons", showactive=False, + x=1.05, y=0.05, xanchor="right", yanchor="bottom", + buttons=[ + dict(label="▶ Play", + method="animate", + args=[None, # None=all frames + {"frame":{"duration":600,"redraw":True}, # ← redraw fixes dead ▶ + "fromcurrent":True,"transition":{"duration":0}}]), + dict(label="⏸ Pause", + method="animate", + args=[[None], + {"frame":{"duration":0,"redraw":False}, + "mode":"immediate"}]) + ] + )], + sliders=[dict(active=0, pad=dict(t=50), steps=steps)] ) - - # Update position - current_lat += lat_tendency - current_lon += lon_tendency - - # Enhanced intensity evolution with environmental limits - intensity_tendency = calculate_environmental_intensity_change( - current_intensity, environmental_limit, hour, current_lat, current_lon, - month, oni_value, sst_data + else: + # fallback: show only final day + static routes + final = static_routes + list(frames[-1].data) + fig = go.Figure(data=final) + + # -- 5) shared layout styling + fig.update_layout( + title={ + 'text': f"🌊 Typhoon Prediction — {month:02d}/{year} | ONI: {oni:.2f}", + 'x':0.5,'font':{'size':18} + }, + geo=dict( + projection_type="natural earth", + showland=True, landcolor="lightgray", + showocean=True, oceancolor="lightblue", + showcoastlines=True, coastlinecolor="darkgray", + showlakes=True, lakecolor="lightblue", + showcountries=True, countrycolor="gray", + resolution=50, + center=dict(lat=20, lon=140), + lonaxis_range=[110,180], lataxis_range=[5,35] + ), + width=1100, height=750, + showlegend=True, + legend=dict( + x=0.02,y=0.98, + bgcolor="rgba(255,255,255,0.7)", + bordercolor="gray",borderwidth=1 ) - - # Update intensity with environmental constraints - current_intensity += intensity_tendency - current_intensity = max(20, min(environmental_limit, current_intensity)) - - # Enhanced confidence calculation - confidence = calculate_dynamic_confidence( - hour, current_lat, current_lon, use_real_data, - sst_data['success'] if sst_data else False, - slp_data['success'] if slp_data else False + ) + + return fig + + except Exception as e: + logging.error(f"Error in predict animation: {e}") + import traceback; traceback.print_exc() + return create_error_plot(f"Animation error: {e}") +def create_genesis_animation(prediction_data, enable_animation=True): + """ + Create professional typhoon track animation showing daily genesis potential and storm development + Following NHC/JTWC visualization standards with proper geographic map and time controls + """ + try: + daily_maps = prediction_data.get('daily_gpi_maps', []) + if not daily_maps: + return create_error_plot("No GPI data available for animation") + + storm_predictions = prediction_data.get('storm_predictions', []) + month = prediction_data['month'] + oni_value = prediction_data['oni_value'] + year = prediction_data.get('year', 2025) + + # ---- 1) Prepare static full-track routes ---- + static_routes = [] + for storm in storm_predictions: + track = storm.get('track', []) + if not track: + continue + lats = [pt['lat'] for pt in track] + lons = [pt['lon'] for pt in track] + static_routes.append( + go.Scattergeo( + lat=lats, + lon=lons, + mode='lines', + line=dict(width=2, dash='dash', color='gray'), + showlegend=False, + hoverinfo='skip' + ) ) - - # Determine development stage with environmental context - stage = get_environmental_development_stage(hour, current_intensity, environmental_limit) - - # Environmental metadata - if sst_data and sst_data['success']: - current_sst = oceanic_manager.interpolate_data_to_point( - sst_data, current_lat, current_lon, 'sst' + + # ---- 2) Build animation frames ---- + frames = [] + # determine map bounds from all storm tracks + all_lats = [pt['lat'] for storm in storm_predictions for pt in storm.get('track', [])] + all_lons = [pt['lon'] for storm in storm_predictions for pt in storm.get('track', [])] + map_bounds = { + 'lat_min': min(5, min(all_lats) - 5) if all_lats else 5, + 'lat_max': max(35, max(all_lats) + 5) if all_lats else 35, + 'lon_min': min(110, min(all_lons) - 10) if all_lons else 110, + 'lon_max': max(180, max(all_lons) + 10) if all_lons else 180 + } + + for day_idx, day_data in enumerate(daily_maps): + day = day_data['day'] + gpi = day_data['gpi_field'] + lats = day_data['lat_range'] + lons = day_data['lon_range'] + + traces = [] + # Genesis potential dots + traces.append(go.Scattergeo( + lat=np.repeat(lats, len(lons)), + lon=np.tile(lons, len(lats)), + mode='markers', + marker=dict( + size=6, + color=gpi.flatten(), + colorscale='Viridis', + cmin=0, cmax=3, opacity=0.6, + showscale=(day_idx == 0), + colorbar=(dict( + title=dict(text="Genesis
Potential
Index", side="right") + ) if day_idx == 0 else None) + ), + name='Genesis Potential', + showlegend=(day_idx == 0), + hovertemplate=( + 'GPI: %{marker.color:.2f}
' + + 'Lat: %{lat:.1f}°N
' + + 'Lon: %{lon:.1f}°E
' + + f'Day {day} of {month:02d}/{year}' ) - else: - current_sst = get_climatological_sst(current_lat, current_lon, month) - - if slp_data and slp_data['success']: - current_slp = oceanic_manager.interpolate_data_to_point( - slp_data, current_lat, current_lon, 'slp' + )) + + # Storm positions up to this day + for storm in storm_predictions: + past = [pt for pt in storm.get('track', []) if pt['day'] <= day] + if not past: + continue + lats_p = [pt['lat'] for pt in past] + lons_p = [pt['lon'] for pt in past] + intens = [pt['intensity'] for pt in past] + cats = [pt['category'] for pt in past] + + # historical line + traces.append(go.Scattergeo( + lat=lats_p, lon=lons_p, mode='lines', + line=dict(width=2, color='gray'), + name=f"{storm['storm_id']} Track", + showlegend=(day_idx == 0), + hoverinfo='skip' + )) + # current position + traces.append(go.Scattergeo( + lat=[lats_p[-1]], lon=[lons_p[-1]], mode='markers', + marker=dict(size=10, symbol='circle', color='red'), + name=f"{storm['storm_id']} Position", + showlegend=(day_idx == 0), + hovertemplate=( + f"{storm['storm_id']}
" + f"Intensity: {intens[-1]} kt
" + f"Category: {cats[-1]}" + ) + )) + + frames.append(go.Frame( + data=traces, + name=str(day), + layout=go.Layout( + geo=dict( + projection_type="natural earth", + showland=True, landcolor="lightgray", + showocean=True, oceancolor="lightblue", + showcoastlines=True, coastlinecolor="darkgray", + center=dict( + lat=(map_bounds['lat_min'] + map_bounds['lat_max'])/2, + lon=(map_bounds['lon_min'] + map_bounds['lon_max'])/2 + ), + lonaxis_range=[map_bounds['lon_min'], map_bounds['lon_max']], + lataxis_range=[map_bounds['lat_min'], map_bounds['lat_max']], + resolution=50 + ), + title=f"Day {day} of {month:02d}/{year} ONI: {oni_value:.2f}" ) - current_slp = current_slp if current_slp > 500 else current_slp / 100 # Convert to hPa - else: - current_slp = 1013 # Standard atmosphere - - route_points.append({ - 'hour': hour, - 'lat': current_lat, - 'lon': current_lon, - 'intensity_kt': current_intensity, - 'category': categorize_typhoon_enhanced(current_intensity), - 'confidence': confidence, - 'development_stage': stage, - 'forward_speed_kmh': base_speed * 111, # Convert to km/h - 'pressure_hpa': max(900, 1013 - (current_intensity - 25) * 0.9) - }) + )) + + # ---- 3) Initialize figure with static routes + first frame ---- + initial_data = static_routes + list(frames[0].data) + fig = go.Figure(data=initial_data, frames=frames) + + # ---- 4) Add play/pause buttons with redraw=True ---- + if enable_animation and len(frames) > 1: + # slider steps + steps = [ + dict(method="animate", + args=[[fr.name], + {"mode": "immediate", + "frame": {"duration": 600, "redraw": True}, + "transition": {"duration": 0}}], + label=fr.name) + for fr in frames + ] + + fig.update_layout( + updatemenus=[dict( + type="buttons", showactive=False, + x=1.05, y=0.05, xanchor="right", yanchor="bottom", + buttons=[ + dict(label="▶ Play", + method="animate", + args=[None, # None means “all frames” + {"frame": {"duration": 600, "redraw": True}, + "fromcurrent": True, + "transition": {"duration": 0}} + ]), # redraw=True fixes the dead play button :contentReference[oaicite:1]{index=1} + dict(label="⏸ Pause", + method="animate", + args=[[None], + {"frame": {"duration": 0, "redraw": False}, + "mode": "immediate"}]) + ] + )], + sliders=[dict(active=0, pad=dict(t=50), steps=steps)] + ) + # No-animation fallback: just show final day + routes + else: + final = static_routes + list(frames[-1].data) + fig = go.Figure(data=final) + + # ---- 5) Common layout styling ---- + fig.update_layout( + title={ + 'text': f"🌊 Typhoon Genesis & Development Forecast
" + f"{month:02d}/{year} | ONI: {oni_value:.2f}", + 'x': 0.5, 'font': {'size': 18} + }, + geo=dict( + projection_type="natural earth", + showland=True, landcolor="lightgray", + showocean=True, oceancolor="lightblue", + showcoastlines=True, coastlinecolor="darkgray", + showlakes=True, lakecolor="lightblue", + showcountries=True, countrycolor="gray", + resolution=50, + center=dict(lat=20, lon=140), + lonaxis_range=[110, 180], lataxis_range=[5, 35] + ), + width=1100, height=750, + showlegend=True, + legend=dict(x=0.02, y=0.98, + bgcolor="rgba(255,255,255,0.7)", + bordercolor="gray", borderwidth=1) + ) + + return fig + + except Exception as e: + logging.error(f"Error creating professional genesis animation: {e}") + import traceback; traceback.print_exc() + return create_error_plot(f"Animation error: {e}") + + +def create_error_plot(error_message): + """Create a simple error plot""" + fig = go.Figure() + fig.add_annotation( + text=error_message, + xref="paper", yref="paper", + x=0.5, y=0.5, xanchor='center', yanchor='middle', + showarrow=False, font_size=16 + ) + fig.update_layout(title="Error in Visualization") + return fig + +def create_prediction_summary(prediction_data): + """Create a comprehensive summary of the prediction""" + try: + if 'error' in prediction_data: + return f"Error in prediction: {prediction_data['error']}" + + month = prediction_data['month'] + oni_value = prediction_data['oni_value'] + summary = prediction_data.get('summary', {}) + genesis_events = prediction_data.get('genesis_events', []) + storm_predictions = prediction_data.get('storm_predictions', []) + + month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + month_name = month_names[month - 1] + + summary_text = f""" +TYPHOON GENESIS PREDICTION SUMMARY - {month_name.upper()} 2025 +{'='*70} + +🌊 ENVIRONMENTAL CONDITIONS: +• Month: {month_name} (Month {month}) +• ONI Value: {oni_value:.2f} {'(El Niño)' if oni_value > 0.5 else '(La Niña)' if oni_value < -0.5 else '(Neutral)'} +• Season Phase: {'Peak Season' if month in [7,8,9,10] else 'Off Season' if month in [1,2,3,4,11,12] else 'Transition Season'} + +📊 GENESIS POTENTIAL ANALYSIS: +• Peak GPI Day: Day {summary.get('peak_gpi_day', 'Unknown')} +• Peak GPI Value: {summary.get('peak_gpi_value', 0):.2f} +• Total Genesis Events: {summary.get('total_genesis_events', 0)} +• Storm Development Success: {summary.get('total_storm_predictions', 0)}/{summary.get('total_genesis_events', 0)} events + +🎯 GENESIS EVENTS BREAKDOWN: +""" - results['route_forecast'] = route_points - - # Realistic confidence scores - results['confidence_scores'] = { - 'genesis': 0.88, - 'early_development': 0.82, - 'position_24h': 0.85, - 'position_48h': 0.78, - 'position_72h': 0.68, - 'intensity_24h': 0.75, - 'intensity_48h': 0.65, - 'intensity_72h': 0.55, - 'long_term': max(0.3, 0.8 - (forecast_hours / 240) * 0.5) - } + if genesis_events: + for i, event in enumerate(genesis_events, 1): + summary_text += f""" +Event {i}: +• Date: {event['date']} +• Location: {event['lat']:.1f}°N, {event['lon']:.1f}°E +• GPI Value: {event['gpi']:.2f} +• Genesis Probability: {event['probability']*100:.0f}% +""" + else: + summary_text += "\n• No significant genesis events predicted for this month\n" - # Model information - results['model_info'] = f"Enhanced Realistic Model - {genesis_region}" + summary_text += f""" + +🌪️ STORM TRACK PREDICTIONS: +""" + + if storm_predictions: + for storm in storm_predictions: + track = storm['track'] + if track: + genesis = storm['genesis_event'] + max_intensity = max(pt['intensity'] for pt in track) + max_category = categorize_typhoon_enhanced(max_intensity) + track_duration = len(track) * 6 # hours + final_pos = track[-1] + + summary_text += f""" +Storm {storm['storm_id']}: +• Genesis: Day {genesis['day']}, {genesis['lat']:.1f}°N {genesis['lon']:.1f}°E +• Peak Intensity: {max_intensity:.0f} kt ({max_category}) +• Track Duration: {track_duration} hours ({track_duration/24:.1f} days) +• Final Position: {final_pos['lat']:.1f}°N, {final_pos['lon']:.1f}°E +• Uncertainty: ±{storm['uncertainty']['position_mean']:.1f}° position, ±{storm['uncertainty']['intensity_mean']:.0f} kt intensity +""" + else: + summary_text += "\n• No storm development predicted from genesis events\n" + + # Add climatological context + summary_text += f""" + +📈 CLIMATOLOGICAL CONTEXT: +• {month_name} Typical Activity: {'Very High' if month in [8,9] else 'High' if month in [7,10] else 'Moderate' if month in [6,11] else 'Low'} +• ENSO Influence: {'Strong suppression expected' if oni_value > 1.0 else 'Moderate suppression' if oni_value > 0.5 else 'Strong enhancement likely' if oni_value < -1.0 else 'Moderate enhancement' if oni_value < -0.5 else 'Near-normal activity'} +• Regional Focus: Western Pacific Main Development Region (10-25°N, 120-160°E) + +🔧 METHODOLOGY DETAILS: +• Genesis Potential Index: Emanuel & Nolan (2004) formulation +• Environmental Factors: SST, humidity, vorticity, wind shear, Coriolis effect +• Temporal Resolution: Daily evolution throughout month +• Spatial Resolution: 2.5° grid spacing +• ENSO Modulation: Integrated ONI effects on environmental parameters +• Track Prediction: Physics-based storm motion and intensity evolution + +⚠️ UNCERTAINTY & LIMITATIONS: +• Genesis timing: ±2-3 days typical uncertainty +• Track position: Growing uncertainty with time (±0.5° to ±2°) +• Intensity prediction: ±5-15 kt uncertainty range +• Environmental assumptions: Based on climatological patterns +• Model limitations: Simplified compared to operational NWP systems + +🎯 FORECAST CONFIDENCE: +• Genesis Location: {'High' if summary.get('peak_gpi_value', 0) > 2 else 'Moderate' if summary.get('peak_gpi_value', 0) > 1 else 'Low'} +• Genesis Timing: {'High' if month in [7,8,9] else 'Moderate' if month in [6,10] else 'Low'} +• Track Prediction: Moderate (physics-based motion patterns) +• Intensity Evolution: Moderate (environmental constraints applied) + +📋 OPERATIONAL IMPLICATIONS: +• Monitor Days {', '.join([str(event['day']) for event in genesis_events[:3]])} for potential development +• Focus regions: {', '.join([f"{event['lat']:.0f}°N {event['lon']:.0f}°E" for event in genesis_events[:3]])} +• Preparedness level: {'High' if len(storm_predictions) > 2 else 'Moderate' if len(storm_predictions) > 0 else 'Routine'} + +🔬 RESEARCH APPLICATIONS: +• Suitable for seasonal planning and climate studies +• Genesis mechanism investigation +• ENSO-typhoon relationship analysis +• Environmental parameter sensitivity studies + +⚠️ IMPORTANT DISCLAIMERS: +• This is a research prediction system, not operational forecast +• Use official meteorological services for real-time warnings +• Actual conditions may differ from climatological assumptions +• Model simplified compared to operational prediction systems +• Uncertainty grows significantly beyond 5-7 day lead times +""" - return results + return summary_text except Exception as e: - logging.error(f"Realistic prediction error: {str(e)}") - return { - 'error': f"Prediction error: {str(e)}", - 'current_prediction': {'intensity_kt': 30, 'category': 'Tropical Depression'}, - 'route_forecast': [], - 'confidence_scores': {}, - 'model_info': 'Error in prediction' - } + logging.error(f"Error creating prediction summary: {e}") + return f"Error generating summary: {str(e)}" # ----------------------------- # FIXED: ADVANCED ML FEATURES WITH ROBUST ERROR HANDLING # ----------------------------- -def calculate_environmental_steering_speed(lat, lon, month, oni_value, slp_data): - """Calculate storm forward speed based on environmental steering""" - base_speed = 0.15 # Default speed in degrees/hour - - # Latitude effects - if lat < 20: - speed_factor = 0.8 # Slower in tropics - elif lat < 30: - speed_factor = 1.2 # Faster in subtropics - else: - speed_factor = 1.5 # Fast in mid-latitudes - - # Pressure gradient effects (if SLP data available) - if slp_data and slp_data['success']: - try: - # Calculate approximate pressure gradient (simplified) - slp_value = oceanic_manager.interpolate_data_to_point(slp_data, lat, lon, 'slp') - if not np.isnan(slp_value): - slp_hpa = slp_value if slp_value > 500 else slp_value / 100 - if slp_hpa < 1008: # Low pressure - faster motion - speed_factor *= 1.2 - elif slp_hpa > 1015: # High pressure - slower motion - speed_factor *= 0.8 - except: - pass - - return base_speed * speed_factor - -def calculate_motion_tendency(lat, lon, month, oni_value, hour, slp_data): - """Calculate motion tendency with environmental steering""" - # Base climatological motion - ridge_position = 32 + 4 * np.sin(2 * np.pi * (month - 6) / 4) - - if lat < ridge_position - 10: - base_lat_tendency = 0.05 # Poleward - base_lon_tendency = -0.12 # Westward - elif lat > ridge_position - 3: - base_lat_tendency = 0.15 # Strong poleward (recurvature) - base_lon_tendency = 0.08 # Eastward - else: - base_lat_tendency = 0.08 # Moderate poleward - base_lon_tendency = -0.06 # Moderate westward - - # ENSO steering effects - if oni_value > 0.5: # El Niño - base_lon_tendency += 0.03 # More eastward - base_lat_tendency += 0.01 # Slightly more poleward - elif oni_value < -0.5: # La Niña - base_lon_tendency -= 0.04 # More westward - - # Add realistic motion uncertainty - motion_uncertainty = 0.02 + (hour / 120) * 0.03 - lat_noise = np.random.normal(0, motion_uncertainty) - lon_noise = np.random.normal(0, motion_uncertainty) - - return base_lat_tendency + lat_noise, base_lon_tendency + lon_noise - -def calculate_environmental_intensity_change( - current_intensity, environmental_limit, hour, lat, lon, month, oni_value, sst_data -): - """Calculate intensity change based on environmental conditions""" - - # Base intensity tendency based on development stage - if hour <= 48: # Development phase - if current_intensity < environmental_limit * 0.6: - base_tendency = 3.5 # Rapid development possible - elif current_intensity < environmental_limit * 0.8: - base_tendency = 2.0 # Moderate development - else: - base_tendency = 0.5 # Near limit - elif hour <= 120: # Mature phase - if current_intensity < environmental_limit: - base_tendency = 1.0 # Slow intensification - else: - base_tendency = -0.5 # Slight weakening - else: # Extended phase - base_tendency = -2.0 # General weakening trend - - # Environmental limit constraint - if current_intensity >= environmental_limit: - base_tendency = min(base_tendency, -1.0) # Force weakening if over limit - - # SST effects on development rate - if sst_data and sst_data['success']: - try: - sst_value = oceanic_manager.interpolate_data_to_point(sst_data, lat, lon, 'sst') - if not np.isnan(sst_value): - sst_celsius = sst_value if sst_value < 50 else sst_value - 273.15 - if sst_celsius >= 29.5: # Very warm - enhanced development - base_tendency += 1.5 - elif sst_celsius >= 28.0: # Warm - normal development - base_tendency += 0.5 - elif sst_celsius < 26.5: # Cool - inhibited development - base_tendency -= 2.0 - except: - pass - - # Land interaction - if lon < 110 or (120 < lon < 125 and lat > 20): # Near land masses - base_tendency -= 8.0 - - # High latitude weakening - if lat > 35: - base_tendency -= 10.0 - elif lat > 30: - base_tendency -= 4.0 - - # Add realistic intensity uncertainty - intensity_noise = np.random.normal(0, 1.0) - - return base_tendency + intensity_noise - -def calculate_dynamic_confidence(hour, lat, lon, use_real_data, sst_success, slp_success): - """Calculate dynamic confidence based on data availability and conditions""" - base_confidence = 0.92 - - # Time penalty - time_penalty = (hour / 120) * 0.35 - - # Data quality bonus - data_bonus = 0.0 - if use_real_data: - if sst_success: - data_bonus += 0.08 - if slp_success: - data_bonus += 0.05 - - # Environmental uncertainty - environment_penalty = 0.0 - if lat > 30 or lon < 115: # Challenging forecast regions - environment_penalty = 0.12 - elif lat > 25: - environment_penalty = 0.06 - - final_confidence = base_confidence + data_bonus - time_penalty - environment_penalty - return max(0.25, min(0.95, final_confidence)) -def get_environmental_development_stage(hour, intensity, environmental_limit): - """Determine development stage based on time and environmental context""" - intensity_fraction = intensity / max(environmental_limit, 50) - - if hour <= 24: - return 'Genesis' - elif hour <= 72: - if intensity_fraction < 0.3: - return 'Early Development' - elif intensity_fraction < 0.6: - return 'Active Development' - else: - return 'Rapid Development' - elif hour <= 120: - if intensity_fraction > 0.8: - return 'Peak Intensity' - else: - return 'Mature Stage' - else: - return 'Extended Forecast' def extract_storm_features(typhoon_data): """Extract comprehensive features for clustering analysis - FIXED VERSION""" try: @@ -2417,594 +2385,6 @@ def create_separate_clustering_plots(storm_features, typhoon_data, method='umap' ) return error_fig, error_fig, error_fig, error_fig, f"Error in clustering: {str(e)}" -# ----------------------------- -# ENHANCED: Advanced Prediction System with Route Forecasting -# ----------------------------- - -def create_advanced_prediction_model(typhoon_data): - """Create advanced ML model for intensity and route prediction""" - try: - if typhoon_data is None or typhoon_data.empty: - return None, "No data available for model training" - - # Prepare training data - features = [] - targets = [] - - for sid in typhoon_data['SID'].unique(): - storm_data = typhoon_data[typhoon_data['SID'] == sid].sort_values('ISO_TIME') - - if len(storm_data) < 3: # Need at least 3 points for prediction - continue - - for i in range(len(storm_data) - 1): - current = storm_data.iloc[i] - next_point = storm_data.iloc[i + 1] - - # Extract features (current state) - feature_row = [] - - # Current position - feature_row.extend([ - current.get('LAT', 20), - current.get('LON', 140) - ]) - - # Current intensity - feature_row.extend([ - current.get('USA_WIND', 30), - current.get('USA_PRES', 1000) - ]) - - # Time features - if 'ISO_TIME' in current and pd.notna(current['ISO_TIME']): - month = current['ISO_TIME'].month - day_of_year = current['ISO_TIME'].dayofyear - else: - month = 9 # Peak season default - day_of_year = 250 - - feature_row.extend([month, day_of_year]) - - # Motion features (if previous point exists) - if i > 0: - prev = storm_data.iloc[i - 1] - dlat = current.get('LAT', 20) - prev.get('LAT', 20) - dlon = current.get('LON', 140) - prev.get('LON', 140) - speed = np.sqrt(dlat**2 + dlon**2) - bearing = np.arctan2(dlat, dlon) - else: - speed = 0 - bearing = 0 - - feature_row.extend([speed, bearing]) - - features.append(feature_row) - - # Target: next position and intensity - targets.append([ - next_point.get('LAT', 20), - next_point.get('LON', 140), - next_point.get('USA_WIND', 30) - ]) - - if len(features) < 10: # Need sufficient training data - return None, "Insufficient data for model training" - - # Train model - X = np.array(features) - y = np.array(targets) - - # Split data - X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) - - # Create separate models for position and intensity - models = {} - - # Position model (lat, lon) - pos_model = RandomForestRegressor(n_estimators=100, random_state=42) - pos_model.fit(X_train, y_train[:, :2]) - models['position'] = pos_model - - # Intensity model (wind speed) - int_model = RandomForestRegressor(n_estimators=100, random_state=42) - int_model.fit(X_train, y_train[:, 2]) - models['intensity'] = int_model - - # Calculate model performance - pos_pred = pos_model.predict(X_test) - int_pred = int_model.predict(X_test) - - pos_mae = mean_absolute_error(y_test[:, :2], pos_pred) - int_mae = mean_absolute_error(y_test[:, 2], int_pred) - - model_info = f"Position MAE: {pos_mae:.2f}°, Intensity MAE: {int_mae:.2f} kt" - - return models, model_info - - except Exception as e: - return None, f"Error creating prediction model: {str(e)}" - -def create_animated_route_visualization(prediction_results, show_uncertainty=True, enable_animation=True): - """Create comprehensive animated route visualization with intensity plots""" - try: - if 'route_forecast' not in prediction_results or not prediction_results['route_forecast']: - return None, "No route forecast data available" - - route_data = prediction_results['route_forecast'] - - # Extract data for plotting - hours = [point['hour'] for point in route_data] - lats = [point['lat'] for point in route_data] - lons = [point['lon'] for point in route_data] - intensities = [point['intensity_kt'] for point in route_data] - categories = [point['category'] for point in route_data] - confidences = [point.get('confidence', 0.8) for point in route_data] - stages = [point.get('development_stage', 'Unknown') for point in route_data] - speeds = [point.get('forward_speed_kmh', 15) for point in route_data] - pressures = [point.get('pressure_hpa', 1013) for point in route_data] - - # Create subplot layout with map and intensity plot - fig = make_subplots( - rows=2, cols=2, - subplot_titles=('Storm Track Animation', 'Wind Speed vs Time', 'Forward Speed vs Time', 'Pressure vs Time'), - specs=[[{"type": "geo", "colspan": 2}, None], - [{"type": "xy"}, {"type": "xy"}]], - vertical_spacing=0.15, - row_heights=[0.7, 0.3] - ) - - if enable_animation: - # Add frames for animation - frames = [] - - # Static background elements first - # Add complete track as background - fig.add_trace( - go.Scattergeo( - lon=lons, - lat=lats, - mode='lines', - line=dict(color='lightgray', width=2, dash='dot'), - name='Complete Track', - showlegend=True, - opacity=0.4 - ), - row=1, col=1 - ) - - # Genesis marker (always visible) - fig.add_trace( - go.Scattergeo( - lon=[lons[0]], - lat=[lats[0]], - mode='markers', - marker=dict( - size=25, - color='gold', - symbol='star', - line=dict(width=3, color='black') - ), - name='Genesis', - showlegend=True, - hovertemplate=( - f"GENESIS
" - f"Position: {lats[0]:.1f}°N, {lons[0]:.1f}°E
" - f"Initial: {intensities[0]:.0f} kt
" - f"Region: {prediction_results['genesis_info']['description']}
" - "" - ) - ), - row=1, col=1 - ) - - # Create animation frames - for i in range(len(route_data)): - frame_lons = lons[:i+1] - frame_lats = lats[:i+1] - frame_intensities = intensities[:i+1] - frame_categories = categories[:i+1] - frame_hours = hours[:i+1] - - # Current position marker - current_color = enhanced_color_map.get(frame_categories[-1], 'rgb(128,128,128)') - current_size = 15 + (frame_intensities[-1] / 10) - - frame_data = [ - # Animated track up to current point - go.Scattergeo( - lon=frame_lons, - lat=frame_lats, - mode='lines+markers', - line=dict(color='blue', width=4), - marker=dict( - size=[8 + (intensity/15) for intensity in frame_intensities], - color=[enhanced_color_map.get(cat, 'rgb(128,128,128)') for cat in frame_categories], - opacity=0.8, - line=dict(width=1, color='white') - ), - name='Current Track', - showlegend=False - ), - # Current position highlight - go.Scattergeo( - lon=[frame_lons[-1]], - lat=[frame_lats[-1]], - mode='markers', - marker=dict( - size=current_size, - color=current_color, - symbol='circle', - line=dict(width=3, color='white') - ), - name='Current Position', - showlegend=False, - hovertemplate=( - f"Hour {route_data[i]['hour']}
" - f"Position: {lats[i]:.1f}°N, {lons[i]:.1f}°E
" - f"Intensity: {intensities[i]:.0f} kt
" - f"Category: {categories[i]}
" - f"Stage: {stages[i]}
" - f"Speed: {speeds[i]:.1f} km/h
" - f"Confidence: {confidences[i]*100:.0f}%
" - "" - ) - ), - # Animated wind plot - go.Scatter( - x=frame_hours, - y=frame_intensities, - mode='lines+markers', - line=dict(color='red', width=3), - marker=dict(size=6, color='red'), - name='Wind Speed', - showlegend=False, - yaxis='y2' - ), - # Animated speed plot - go.Scatter( - x=frame_hours, - y=speeds[:i+1], - mode='lines+markers', - line=dict(color='green', width=2), - marker=dict(size=4, color='green'), - name='Forward Speed', - showlegend=False, - yaxis='y3' - ), - # Animated pressure plot - go.Scatter( - x=frame_hours, - y=pressures[:i+1], - mode='lines+markers', - line=dict(color='purple', width=2), - marker=dict(size=4, color='purple'), - name='Pressure', - showlegend=False, - yaxis='y4' - ) - ] - - frames.append(go.Frame( - data=frame_data, - name=str(i), - layout=go.Layout( - title=f"Storm Development Animation - Hour {route_data[i]['hour']}
" - f"Intensity: {intensities[i]:.0f} kt | Category: {categories[i]} | Stage: {stages[i]} | Speed: {speeds[i]:.1f} km/h" - ) - )) - - fig.frames = frames - - # Add play/pause controls - fig.update_layout( - updatemenus=[ - { - "buttons": [ - { - "args": [None, {"frame": {"duration": 1000, "redraw": True}, - "fromcurrent": True, "transition": {"duration": 300}}], - "label": "▶️ Play", - "method": "animate" - }, - { - "args": [[None], {"frame": {"duration": 0, "redraw": True}, - "mode": "immediate", "transition": {"duration": 0}}], - "label": "⏸️ Pause", - "method": "animate" - }, - { - "args": [None, {"frame": {"duration": 500, "redraw": True}, - "fromcurrent": True, "transition": {"duration": 300}}], - "label": "⏩ Fast", - "method": "animate" - } - ], - "direction": "left", - "pad": {"r": 10, "t": 87}, - "showactive": False, - "type": "buttons", - "x": 0.1, - "xanchor": "right", - "y": 0, - "yanchor": "top" - } - ], - sliders=[{ - "active": 0, - "yanchor": "top", - "xanchor": "left", - "currentvalue": { - "font": {"size": 16}, - "prefix": "Hour: ", - "visible": True, - "xanchor": "right" - }, - "transition": {"duration": 300, "easing": "cubic-in-out"}, - "pad": {"b": 10, "t": 50}, - "len": 0.9, - "x": 0.1, - "y": 0, - "steps": [ - { - "args": [[str(i)], {"frame": {"duration": 300, "redraw": True}, - "mode": "immediate", "transition": {"duration": 300}}], - "label": f"H{route_data[i]['hour']}", - "method": "animate" - } - for i in range(0, len(route_data), max(1, len(route_data)//20)) # Limit slider steps - ] - }] - ) - - else: - # Static view with all points - # Add genesis marker - fig.add_trace( - go.Scattergeo( - lon=[lons[0]], - lat=[lats[0]], - mode='markers', - marker=dict( - size=25, - color='gold', - symbol='star', - line=dict(width=3, color='black') - ), - name='Genesis', - showlegend=True, - hovertemplate=( - f"GENESIS
" - f"Position: {lats[0]:.1f}°N, {lons[0]:.1f}°E
" - f"Initial: {intensities[0]:.0f} kt
" - "" - ) - ), - row=1, col=1 - ) - - # Add full track with intensity coloring - for i in range(0, len(route_data), max(1, len(route_data)//50)): # Sample points for performance - point = route_data[i] - color = enhanced_color_map.get(point['category'], 'rgb(128,128,128)') - size = 8 + (point['intensity_kt'] / 12) - - fig.add_trace( - go.Scattergeo( - lon=[point['lon']], - lat=[point['lat']], - mode='markers', - marker=dict( - size=size, - color=color, - opacity=point.get('confidence', 0.8), - line=dict(width=1, color='white') - ), - name=f"Hour {point['hour']}" if i % 10 == 0 else None, - showlegend=(i % 10 == 0), - hovertemplate=( - f"Hour {point['hour']}
" - f"Position: {point['lat']:.1f}°N, {point['lon']:.1f}°E
" - f"Intensity: {point['intensity_kt']:.0f} kt
" - f"Category: {point['category']}
" - f"Stage: {point.get('development_stage', 'Unknown')}
" - f"Speed: {point.get('forward_speed_kmh', 15):.1f} km/h
" - "" - ) - ), - row=1, col=1 - ) - - # Connect points with track line - fig.add_trace( - go.Scattergeo( - lon=lons, - lat=lats, - mode='lines', - line=dict(color='black', width=3), - name='Forecast Track', - showlegend=True - ), - row=1, col=1 - ) - - # Add static intensity, speed, and pressure plots - # Wind speed plot - fig.add_trace( - go.Scatter( - x=hours, - y=intensities, - mode='lines+markers', - line=dict(color='red', width=3), - marker=dict(size=6, color='red'), - name='Wind Speed', - showlegend=False - ), - row=2, col=1 - ) - - # Add category threshold lines - thresholds = [34, 64, 83, 96, 113, 137] - threshold_names = ['TS', 'C1', 'C2', 'C3', 'C4', 'C5'] - - for thresh, name in zip(thresholds, threshold_names): - fig.add_trace( - go.Scatter( - x=[min(hours), max(hours)], - y=[thresh, thresh], - mode='lines', - line=dict(color='gray', width=1, dash='dash'), - name=name, - showlegend=False, - hovertemplate=f"{name} Threshold: {thresh} kt" - ), - row=2, col=1 - ) - - # Forward speed plot - fig.add_trace( - go.Scatter( - x=hours, - y=speeds, - mode='lines+markers', - line=dict(color='green', width=2), - marker=dict(size=4, color='green'), - name='Forward Speed', - showlegend=False - ), - row=2, col=2 - ) - - # Add uncertainty cone if requested - if show_uncertainty and len(route_data) > 1: - uncertainty_lats_upper = [] - uncertainty_lats_lower = [] - uncertainty_lons_upper = [] - uncertainty_lons_lower = [] - - for i, point in enumerate(route_data): - # Uncertainty grows with time and decreases with confidence - base_uncertainty = 0.4 + (i / len(route_data)) * 1.8 - confidence_factor = point.get('confidence', 0.8) - uncertainty = base_uncertainty / confidence_factor - - uncertainty_lats_upper.append(point['lat'] + uncertainty) - uncertainty_lats_lower.append(point['lat'] - uncertainty) - uncertainty_lons_upper.append(point['lon'] + uncertainty) - uncertainty_lons_lower.append(point['lon'] - uncertainty) - - uncertainty_lats = uncertainty_lats_upper + uncertainty_lats_lower[::-1] - uncertainty_lons = uncertainty_lons_upper + uncertainty_lons_lower[::-1] - - fig.add_trace( - go.Scattergeo( - lon=uncertainty_lons, - lat=uncertainty_lats, - mode='lines', - fill='toself', - fillcolor='rgba(128,128,128,0.15)', - line=dict(color='rgba(128,128,128,0.4)', width=1), - name='Uncertainty Cone', - showlegend=True - ), - row=1, col=1 - ) - - # Enhanced layout - fig.update_layout( - title=f"Comprehensive Storm Development Analysis
Starting from {prediction_results['genesis_info']['description']}", - height=1000, # Taller for better subplot visibility - width=1400, # Wider - showlegend=True - ) - - # Update geo layout - fig.update_geos( - projection_type="natural earth", - showland=True, - landcolor="LightGray", - showocean=True, - oceancolor="LightBlue", - showcoastlines=True, - coastlinecolor="DarkGray", - showlakes=True, - lakecolor="LightBlue", - center=dict(lat=np.mean(lats), lon=np.mean(lons)), - projection_scale=2.0, - row=1, col=1 - ) - - # Update subplot axes - fig.update_xaxes(title_text="Forecast Hour", row=2, col=1) - fig.update_yaxes(title_text="Wind Speed (kt)", row=2, col=1) - fig.update_xaxes(title_text="Forecast Hour", row=2, col=2) - fig.update_yaxes(title_text="Forward Speed (km/h)", row=2, col=2) - - # Generate enhanced forecast text - current = prediction_results['current_prediction'] - genesis_info = prediction_results['genesis_info'] - - # Calculate some statistics - max_intensity = max(intensities) - max_intensity_time = hours[intensities.index(max_intensity)] - avg_speed = np.mean(speeds) - - forecast_text = f""" -COMPREHENSIVE STORM DEVELOPMENT FORECAST -{'='*65} - -GENESIS CONDITIONS: -• Region: {current.get('genesis_region', 'Unknown')} -• Description: {genesis_info['description']} -• Starting Position: {lats[0]:.1f}°N, {lons[0]:.1f}°E -• Initial Intensity: {current['intensity_kt']:.0f} kt (Tropical Depression) -• Genesis Pressure: {current.get('pressure_hpa', 1008):.0f} hPa - -STORM CHARACTERISTICS: -• Peak Intensity: {max_intensity:.0f} kt at Hour {max_intensity_time} -• Average Forward Speed: {avg_speed:.1f} km/h -• Total Distance: {sum([speeds[i]/6 for i in range(len(speeds))]):.0f} km -• Final Position: {lats[-1]:.1f}°N, {lons[-1]:.1f}°E -• Forecast Duration: {hours[-1]} hours ({hours[-1]/24:.1f} days) - -DEVELOPMENT TIMELINE: -• Hour 0 (Genesis): {intensities[0]:.0f} kt - {categories[0]} -• Hour 24: {intensities[min(4, len(intensities)-1)]:.0f} kt - {categories[min(4, len(categories)-1)]} -• Hour 48: {intensities[min(8, len(intensities)-1)]:.0f} kt - {categories[min(8, len(categories)-1)]} -• Hour 72: {intensities[min(12, len(intensities)-1)]:.0f} kt - {categories[min(12, len(categories)-1)]} -• Final: {intensities[-1]:.0f} kt - {categories[-1]} - -MOTION ANALYSIS: -• Initial Motion: {speeds[0]:.1f} km/h -• Peak Speed: {max(speeds):.1f} km/h at Hour {hours[speeds.index(max(speeds))]} -• Final Motion: {speeds[-1]:.1f} km/h - -CONFIDENCE ASSESSMENT: -• Genesis Likelihood: {prediction_results['confidence_scores'].get('genesis', 0.85)*100:.0f}% -• 24-hour Track: {prediction_results['confidence_scores'].get('position_24h', 0.85)*100:.0f}% -• 48-hour Track: {prediction_results['confidence_scores'].get('position_48h', 0.75)*100:.0f}% -• 72-hour Track: {prediction_results['confidence_scores'].get('position_72h', 0.65)*100:.0f}% -• Long-term: {prediction_results['confidence_scores'].get('long_term', 0.50)*100:.0f}% - -FEATURES: -{"✅ Animation Enabled - Use controls to watch development" if enable_animation else "📊 Static Analysis - All time steps displayed"} -✅ Realistic Forward Speeds (15-25 km/h typical) -✅ Environmental Coupling (ENSO, SST, Shear) -✅ Multi-stage Development Cycle -✅ Uncertainty Quantification - -MODEL: {prediction_results['model_info']} - """ - - return fig, forecast_text.strip() - - except Exception as e: - error_msg = f"Error creating comprehensive visualization: {str(e)}" - logging.error(error_msg) - import traceback - traceback.print_exc() - return None, error_msg - # ----------------------------- # Regression Functions (Original) # ----------------------------- @@ -3209,7 +2589,7 @@ def get_longitude_analysis(start_year, start_month, end_year, end_month, enso_ph return fig, slopes_text, regression # ----------------------------- -# ENHANCED: Animation Functions with Taiwan Standard Support - FIXED VERSION +# ENHANCED: Animation Functions with Taiwan Standard Support # ----------------------------- def get_available_years(typhoon_data): @@ -3294,8 +2674,8 @@ def update_typhoon_options_enhanced(year, basin): print(f"Error in update_typhoon_options_enhanced: {e}") return gr.update(choices=["Error loading storms"], value=None) -def generate_enhanced_track_video_fixed(year, typhoon_selection, standard): - """FIXED: Enhanced track video generation with working animation display""" +def generate_enhanced_track_video(year, typhoon_selection, standard): + """Enhanced track video generation with TD support, Taiwan standard, and 2025 compatibility""" if not typhoon_selection or typhoon_selection == "No storms found": return None @@ -3320,17 +2700,16 @@ def generate_enhanced_track_video_fixed(year, typhoon_selection, standard): if 'USA_WIND' in storm_df.columns: winds = pd.to_numeric(storm_df['USA_WIND'], errors='coerce').fillna(0).values else: - winds = np.full(len(lats), 30) + winds = np.full(len(lats), 30) # Default TD strength # Enhanced metadata storm_name = storm_df['NAME'].iloc[0] if pd.notna(storm_df['NAME'].iloc[0]) else "UNNAMED" season = storm_df['SEASON'].iloc[0] if 'SEASON' in storm_df.columns else year - print(f"Generating FIXED video for {storm_name} ({sid}) with {len(lats)} track points using {standard} standard") + print(f"Generating video for {storm_name} ({sid}) with {len(lats)} track points using {standard} standard") - # FIXED: Create figure with proper cartopy setup - fig = plt.figure(figsize=(16, 10)) - ax = plt.axes(projection=ccrs.PlateCarree()) + # Create figure with enhanced map + fig, ax = plt.subplots(figsize=(16, 10), subplot_kw={'projection': ccrs.PlateCarree()}) # Enhanced map features ax.stock_img() @@ -3350,35 +2729,26 @@ def generate_enhanced_track_video_fixed(year, typhoon_selection, standard): gl = ax.gridlines(draw_labels=True, alpha=0.3) gl.top_labels = gl.right_labels = False - # Title + # Title with enhanced info and standard ax.set_title(f"{season} {storm_name} ({sid}) Track Animation - {standard.upper()} Standard", fontsize=18, fontweight='bold') - # FIXED: Animation elements - proper initialization with cartopy transforms - # Initialize empty line for track with correct transform - track_line, = ax.plot([], [], 'b-', linewidth=3, alpha=0.7, - label='Track', transform=ccrs.PlateCarree()) - - # Initialize current position marker - current_point, = ax.plot([], [], 'o', markersize=15, - transform=ccrs.PlateCarree()) - - # Historical track points (to show path traversed) - history_points, = ax.plot([], [], 'o', markersize=6, alpha=0.4, color='blue', - transform=ccrs.PlateCarree()) + # Animation elements + line, = ax.plot([], [], 'b-', linewidth=3, alpha=0.7, label='Track') + point, = ax.plot([], [], 'o', markersize=15) - # Info text box + # Enhanced info display info_box = ax.text(0.02, 0.98, '', transform=ax.transAxes, fontsize=12, verticalalignment='top', bbox=dict(boxstyle="round,pad=0.5", facecolor='white', alpha=0.9)) - # FIXED: Color legend with proper categories for both standards + # Color legend with both standards - ENHANCED legend_elements = [] + if standard == 'taiwan': - categories = ['Tropical Depression', 'Tropical Storm', 'Severe Tropical Storm', - 'Typhoon', 'Severe Typhoon', 'Super Typhoon'] + categories = ['Tropical Depression', 'Tropical Storm', 'Moderate Typhoon', 'Intense Typhoon'] for category in categories: - color = get_taiwan_color_fixed(category) + color = get_taiwan_color(category) legend_elements.append(plt.Line2D([0], [0], marker='o', color='w', markerfacecolor=color, markersize=10, label=category)) else: @@ -3391,51 +2761,40 @@ def generate_enhanced_track_video_fixed(year, typhoon_selection, standard): ax.legend(handles=legend_elements, loc='upper right', fontsize=10) - # FIXED: Animation function with proper artist updates and cartopy compatibility - def animate_fixed(frame): - """Fixed animation function that properly updates tracks with cartopy""" + def animate(frame): try: if frame >= len(lats): - return track_line, current_point, history_points, info_box - - # FIXED: Update track line up to current frame - current_lons = lons[:frame+1] - current_lats = lats[:frame+1] + return line, point, info_box - # Update the track line data (this is the key fix!) - track_line.set_data(current_lons, current_lats) + # Update track line + line.set_data(lons[:frame+1], lats[:frame+1]) - # FIXED: Update historical points (smaller markers showing traversed path) - if frame > 0: - history_points.set_data(current_lons[:-1], current_lats[:-1]) - - # FIXED: Update current position with correct categorization + # Update current position with appropriate categorization current_wind = winds[frame] if standard == 'taiwan': - category, color = categorize_typhoon_by_standard_fixed(current_wind, 'taiwan') + category, color = categorize_typhoon_by_standard(current_wind, 'taiwan') else: - category, color = categorize_typhoon_by_standard_fixed(current_wind, 'atlantic') + category, color = categorize_typhoon_by_standard(current_wind, 'atlantic') - # Debug for first few frames + # Debug print for first few frames if frame < 3: - print(f"FIXED Frame {frame}: Wind={current_wind:.1f}kt, Category={category}, Color={color}") + print(f"Frame {frame}: Wind={current_wind:.1f}kt, Category={category}, Color={color}, Standard={standard}") - # Update current position marker - current_point.set_data([lons[frame]], [lats[frame]]) - current_point.set_color(color) - current_point.set_markersize(12 + current_wind/8) + point.set_data([lons[frame]], [lats[frame]]) + point.set_color(color) + point.set_markersize(10 + current_wind/8) # Size based on intensity - # FIXED: Enhanced info display with correct Taiwan wind speed conversion + # Enhanced info display with standard information if 'ISO_TIME' in storm_df.columns and frame < len(storm_df): current_time = storm_df.iloc[frame]['ISO_TIME'] time_str = current_time.strftime('%Y-%m-%d %H:%M UTC') if pd.notna(current_time) else 'Unknown' else: time_str = f"Step {frame+1}" - # Corrected wind speed display for Taiwan standard + # Convert wind speed for Taiwan standard display if standard == 'taiwan': - wind_ms = current_wind * 0.514444 + wind_ms = current_wind * 0.514444 # Convert to m/s for display wind_display = f"{current_wind:.0f} kt ({wind_ms:.1f} m/s)" else: wind_display = f"{current_wind:.0f} kt" @@ -3451,116 +2810,47 @@ def generate_enhanced_track_video_fixed(year, typhoon_selection, standard): ) info_box.set_text(info_text) - # FIXED: Return all modified artists (crucial for proper display) - return track_line, current_point, history_points, info_box + return line, point, info_box except Exception as e: print(f"Error in animate frame {frame}: {e}") - return track_line, current_point, history_points, info_box + return line, point, info_box - # FIXED: Create animation with cartopy-compatible settings - # Key fixes: blit=False (crucial for cartopy), proper interval + # Create animation anim = animation.FuncAnimation( - fig, animate_fixed, frames=len(lats), - interval=600, blit=False, repeat=True # blit=False is essential for cartopy! + fig, animate, frames=len(lats), + interval=400, blit=False, repeat=True # Slightly slower for better viewing ) - # Save animation with optimized settings + # Save animation temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4', dir=tempfile.gettempdir()) - # FIXED: Writer settings optimized for track visibility + # Enhanced writer settings writer = animation.FFMpegWriter( - fps=2, bitrate=3000, codec='libx264', # Slower FPS for better track visibility - extra_args=['-pix_fmt', 'yuv420p'] + fps=3, bitrate=2000, codec='libx264', # Slower FPS for better visibility + extra_args=['-pix_fmt', 'yuv420p'] # Better compatibility ) - print(f"Saving FIXED animation to {temp_file.name}") - anim.save(temp_file.name, writer=writer, dpi=120) + print(f"Saving animation to {temp_file.name}") + anim.save(temp_file.name, writer=writer, dpi=120) # Higher DPI for better quality plt.close(fig) - print(f"FIXED video generated successfully: {temp_file.name}") + print(f"Video generated successfully: {temp_file.name}") return temp_file.name except Exception as e: - print(f"Error generating FIXED video: {e}") + print(f"Error generating video: {e}") import traceback traceback.print_exc() return None -# FIXED: Update the simplified wrapper function -def simplified_track_video_fixed(year, basin, typhoon, standard): - """Simplified track video function with FIXED animation and Taiwan classification""" +# Simplified wrapper for backward compatibility - FIXED +def simplified_track_video(year, basin, typhoon, standard): + """Simplified track video function with fixed color handling""" if not typhoon: return None - return generate_enhanced_track_video_fixed(year, typhoon, standard) - -# ----------------------------- -# Enhanced Gradio Interface with Oceanic Data Integration -# ----------------------------- - -def generate_enhanced_environmental_forecast_text(results, base_forecast_text): - """Generate enhanced forecast text with environmental details""" - try: - current = results['current_prediction'] - env_data = results['environmental_data'] - route_forecast = results['route_forecast'] - - # Environmental analysis - env_analysis_text = f""" - -ENHANCED ENVIRONMENTAL ANALYSIS -{'='*65} - -REAL-TIME OCEANIC CONDITIONS: -• SST Data Source: {env_data.get('sst_source', 'Unknown')} -• SLP Data Source: {env_data.get('slp_source', 'Unknown')} -• Real-time Integration: {'✅ Active' if env_data.get('use_real_data', False) else '❌ Climatological Fallback'} - -ENVIRONMENTAL POTENTIAL ANALYSIS: -• Genesis Potential: {current.get('environmental_potential', 'Unknown')} kt -• Environmental Favorability: {current.get('environmental_favorability', 'Unknown')} -• SST Contribution: {current.get('sst_contribution', 0):+.1f} kt -• Current Environmental Limit: {current.get('environmental_potential', 50):.0f} kt - -TRACK-POINT ENVIRONMENTAL CONDITIONS: -""" - - # Add sample of environmental conditions along track - if route_forecast and len(route_forecast) > 0: - sample_points = [0, len(route_forecast)//4, len(route_forecast)//2, - 3*len(route_forecast)//4, len(route_forecast)-1] - - for i in sample_points: - if i < len(route_forecast): - point = route_forecast[i] - env_analysis_text += f""" -• Hour {point['hour']}: - - Position: {point['lat']:.1f}°N, {point['lon']:.1f}°E - - Intensity: {point['intensity_kt']:.0f} kt (Limit: {point.get('environmental_limit', 'N/A')} kt) - - SST: {point.get('sst_celsius', 'N/A'):.1f}°C | SLP: {point.get('slp_hpa', 'N/A'):.0f} hPa - - Development Stage: {point['development_stage']} - - Tendency: {point.get('intensity_tendency', 0):+.1f} kt/6hr""" - - env_analysis_text += f""" - -OCEANIC DATA QUALITY ASSESSMENT: -• Position Confidence: {results['confidence_scores'].get('position_72h', 0.5)*100:.0f}% (72hr) -• Intensity Confidence: {results['confidence_scores'].get('intensity_72h', 0.5)*100:.0f}% (72hr) -• Environmental Coupling: {results['confidence_scores'].get('environmental_coupling', 0.5)*100:.0f}% - -TECHNICAL IMPLEMENTATION: -• Model: {results['model_info']} -• Data Protocols: ERDDAP (SST) + OPeNDAP (SLP) -• Spatial Interpolation: Linear with nearest-neighbor fallback -• Physics: Emanuel potential intensity + environmental coupling - """ - - return base_forecast_text + env_analysis_text - - except Exception as e: - logging.error(f"Error generating enhanced forecast text: {e}") - return base_forecast_text + f"\n\nError in environmental analysis: {str(e)}" + return generate_enhanced_track_video(year, typhoon, standard) # ----------------------------- # Load & Process Data @@ -3573,13 +2863,9 @@ merged_data = None def initialize_data(): """Initialize all data safely""" - global oni_data, typhoon_data, merged_data, oceanic_manager + global oni_data, typhoon_data, merged_data try: logging.info("Starting data loading process...") - - # Initialize oceanic manager - oceanic_manager = OceanicDataManager() - update_oni_data() oni_data, typhoon_data = load_data_fixed(ONI_DATA_PATH, TYPHOON_DATA_PATH) @@ -3609,8 +2895,15 @@ def initialize_data(): typhoon_max = process_typhoon_data(typhoon_data) merged_data = merge_data(oni_long, typhoon_max) +# Initialize data +initialize_data() + +# ----------------------------- +# ENHANCED: Gradio Interface with Fixed Route Visualization and Enhanced Features +# ----------------------------- + def create_interface(): - """Create the enhanced Gradio interface with oceanic data integration""" + """Create the enhanced Gradio interface with robust error handling""" try: # Ensure data is available if oni_data is None or typhoon_data is None or merged_data is None: @@ -3630,302 +2923,111 @@ def create_interface(): year_range_display = "Unknown" available_years = [str(year) for year in range(2000, 2026)] - with gr.Blocks(title="Enhanced Typhoon Analysis Platform with Oceanic Data", theme=gr.themes.Soft()) as demo: - gr.Markdown("# 🌊 Enhanced Typhoon Analysis Platform with Real-time Oceanic Data") - gr.Markdown("**Advanced ML clustering, real-time SST/SLP integration, route predictions, and comprehensive tropical cyclone analysis**") + with gr.Blocks(title="Enhanced Typhoon Analysis Platform", theme=gr.themes.Soft()) as demo: + gr.Markdown("# 🌪️ Enhanced Typhoon Analysis Platform") + gr.Markdown("**Advanced ML clustering, route predictions, and comprehensive tropical cyclone analysis including Tropical Depressions**") with gr.Tab("🏠 Overview"): overview_text = f""" - ## 🌊 Welcome to the Enhanced Typhoon Analysis Dashboard with Oceanic Coupling - - This dashboard provides comprehensive analysis of typhoon data with **real-time oceanic data integration** for unprecedented forecast accuracy. - - ### 🚀 NEW Oceanic Data Features: - - **🌊 Real-time SST Data**: NOAA OISST v2 Sea Surface Temperature via ERDDAP - - **🌡️ Real-time SLP Data**: NCEP/NCAR Sea Level Pressure via OPeNDAP - - **🔄 Dynamic Environmental Coupling**: Live oceanic conditions drive intensity predictions - - **📊 Historical Environmental Analysis**: Past storm-environment relationships inform predictions - - **🎯 Environmental Potential Index**: Real-time calculation of maximum possible intensity - - **🌍 Global Data Coverage**: Automatic fallback to climatology when real-time data unavailable - - ### 📊 Enhanced Capabilities: - - **Environmental Intensity Modeling**: SST-driven maximum potential intensity calculations - - **Dynamic Steering**: SLP-based atmospheric steering patterns - - **ENSO-Environment Coupling**: Combined ENSO and oceanic state influences - - **Uncertainty Quantification**: Data quality-based confidence scoring - - **Multi-source Integration**: Seamless blending of real-time and climatological data + ## Welcome to the Enhanced Typhoon Analysis Dashboard + + This dashboard provides comprehensive analysis of typhoon data in relation to ENSO phases with advanced machine learning capabilities. + + ### 🚀 Enhanced Features: + - **Advanced ML Clustering**: UMAP/t-SNE storm pattern analysis with separate visualizations + - **Predictive Routing**: Advanced storm track and intensity forecasting with uncertainty quantification + - **Complete TD Support**: Now includes Tropical Depressions (< 34 kt) + - **Taiwan Standard**: Full support for Taiwan meteorological classification system + - **2025 Data Ready**: Real-time compatibility with current year data + - **Enhanced Animations**: High-quality storm track visualizations with both standards ### 📊 Data Status: - **ONI Data**: {len(oni_data)} years loaded - - **Typhoon Data**: {total_records:,} records loaded - - **Oceanic Data Sources**: NOAA OISST v2 + NCEP/NCAR Reanalysis + - **Typhoon Data**: {total_records:,} records loaded + - **Merged Data**: {len(merged_data):,} typhoons with ONI values - **Available Years**: {year_range_display} - ### 🔧 Technical Infrastructure: - - **Real-time Data Access**: xarray + OPeNDAP + ERDDAP protocols - - **Environmental Interpolation**: Spatial interpolation to storm locations - - **Physics-based Modeling**: Emanuel potential intensity theory implementation - - **Fallback Systems**: Robust climatological backup when real-time data unavailable + ### 🔧 Technical Capabilities: + - **UMAP Clustering**: {"✅ Available" if UMAP_AVAILABLE else "⚠️ Limited to t-SNE/PCA"} + - **AI Predictions**: {"🧠 Deep Learning" if CNN_AVAILABLE else "🔬 Physics-based"} + - **Enhanced Categorization**: Tropical Depression to Super Typhoon + - **Platform**: Optimized for Hugging Face Spaces - ### 🔬 Scientific Accuracy: - - **SST-Intensity Relationship**: Based on latest tropical cyclone research - - **Shear Parameterization**: ENSO and seasonal wind shear modeling - - **Genesis Climatology**: Realistic development regions and frequencies - - **Track Forecasting**: Environmental steering with oceanic state dependencies + ### 📈 Research Applications: + - Climate change impact studies + - Seasonal forecasting research + - Storm pattern classification + - ENSO-typhoon relationship analysis + - Intensity prediction model development """ gr.Markdown(overview_text) - with gr.Tab("🌊 Real-time Oceanic Storm Prediction"): - gr.Markdown("## 🌊 Advanced Storm Development with Live Oceanic Data") - - gr.Markdown(""" - ### 🔥 Revolutionary Features: - - **🌊 Live SST Integration**: Current sea surface temperatures from NOAA satellites - - **🌡️ Real-time SLP Data**: Current atmospheric pressure from global reanalysis - - **🎯 Environmental Potential**: Real-time calculation of maximum storm intensity - - **📈 Historical Learning**: Past storm-environment relationships guide predictions - - **🌍 Global Coverage**: Automatic data fetching with intelligent fallbacks - """) + with gr.Tab("🌊 Monthly Typhoon Genesis Prediction"): + gr.Markdown("## 🌊 Monthly Typhoon Genesis Prediction") + gr.Markdown("**Enter month (1-12) and ONI value to see realistic typhoon development throughout the month using Genesis Potential Index**") with gr.Row(): - with gr.Column(scale=2): - gr.Markdown("### 🌊 Genesis & Environmental Configuration") - - genesis_options = list(get_realistic_genesis_locations().keys()) - genesis_region = gr.Dropdown( - choices=genesis_options, - value="Western Pacific Main Development Region", - label="🌊 Typhoon Genesis Region", - info="Climatologically realistic development regions" - ) - - # Enhanced environmental controls - with gr.Row(): - use_real_oceanic = gr.Checkbox( - label="🌊 Use Real-time Oceanic Data", - value=True, - info="Fetch live SST/SLP data (may take 10-30 seconds)" - ) - show_environmental_details = gr.Checkbox( - label="📊 Show Environmental Analysis", - value=True, - info="Display detailed environmental breakdown" - ) - - # Display selected region info with real-time data status - def update_genesis_info_enhanced(region): - locations = get_realistic_genesis_locations() - if region in locations: - info = locations[region] - base_info = f"📍 Location: {info['lat']:.1f}°N, {info['lon']:.1f}°E\n📝 {info['description']}" - - # Add climatological information - clim_sst = get_climatological_sst(info['lat'], info['lon'], 9) # September - env_potential = calculate_environmental_intensity_potential( - info['lat'], info['lon'], 9, 0.0, None, None - ) - - enhanced_info = ( - f"{base_info}\n" - f"🌡️ Climatological SST: {clim_sst:.1f}°C\n" - f"⚡ Environmental Potential: {env_potential['potential_intensity']:.0f} kt" - ) - return enhanced_info - return "Select a genesis region" - - genesis_info_display = gr.Textbox( - label="Selected Region Analysis", - lines=4, - interactive=False, - value=update_genesis_info_enhanced("Western Pacific Main Development Region") + with gr.Column(scale=1): + genesis_month = gr.Slider( + 1, 12, + label="Month", + value=9, + step=1, + info="1=Jan, 2=Feb, ..., 12=Dec" ) - - genesis_region.change( - fn=update_genesis_info_enhanced, - inputs=[genesis_region], - outputs=[genesis_info_display] + genesis_oni = gr.Number( + label="ONI Value", + value=0.0, + info="El Niño (+) / La Niña (-) / Neutral (0)" ) - - with gr.Row(): - pred_month = gr.Slider( - 1, 12, label="Month", value=9, - info="Peak season: Jul-Oct (affects SST/shear patterns)" - ) - pred_oni = gr.Number( - label="ONI Value", value=0.0, - info="Current ENSO state (-3 to 3, affects oceanic patterns)" - ) - - with gr.Row(): - forecast_hours = gr.Number( - label="Forecast Length (hours)", - value=72, - minimum=24, - maximum=240, - step=6, - info="Extended forecasting with environmental evolution" - ) - advanced_physics = gr.Checkbox( - label="Advanced Environmental Physics", - value=True, - info="Full SST-intensity coupling and wind shear modeling" - ) - - with gr.Row(): - show_uncertainty = gr.Checkbox( - label="Environmental Uncertainty Cone", - value=True, - info="Uncertainty based on data quality and environmental variability" - ) - enable_animation = gr.Checkbox( - label="Animated Development", - value=True, - info="Watch storm-environment interaction evolve" - ) - - with gr.Column(scale=1): - gr.Markdown("### ⚙️ Oceanic Prediction Controls") - predict_oceanic_btn = gr.Button( - "🌊 Generate Enhanced Oceanic Forecast", - variant="primary", - size="lg" + enable_genesis_animation = gr.Checkbox( + label="Enable Animation", + value=True, + info="Watch daily genesis potential evolution" ) - - gr.Markdown("### 📊 Environmental Conditions") - current_intensity = gr.Number(label="Genesis Intensity (kt)", interactive=False) - current_category = gr.Textbox(label="Initial Category", interactive=False) - environmental_potential = gr.Number(label="Environmental Potential (kt)", interactive=False) - environmental_favorability = gr.Textbox(label="Environmental Favorability", interactive=False) - - gr.Markdown("### 🔧 Data Sources") - sst_data_source = gr.Textbox(label="SST Data Source", interactive=False) - slp_data_source = gr.Textbox(label="SLP Data Source", interactive=False) - model_confidence = gr.Textbox(label="Model Info", interactive=False) + generate_genesis_btn = gr.Button("🌊 Generate Monthly Genesis Prediction", variant="primary", size="lg") + + with gr.Column(scale=2): + gr.Markdown("### 🌊 What You'll Get:") + gr.Markdown(""" + - **Daily GPI Evolution**: See genesis potential change day-by-day throughout the month + - **Genesis Event Detection**: Automatic identification of likely cyclogenesis times/locations + - **Storm Track Development**: Physics-based tracks from each genesis point + - **Real-time Animation**: Watch storms develop and move with uncertainty visualization + - **Environmental Analysis**: SST, humidity, wind shear, and vorticity effects + - **ENSO Modulation**: How El Niño/La Niña affects monthly patterns + """) with gr.Row(): - route_plot = gr.Plot(label="🗺️ Advanced Oceanic-Coupled Forecast") + genesis_animation = gr.Plot(label="🗺️ Daily Genesis Potential & Storm Development") with gr.Row(): - forecast_details = gr.Textbox( - label="📋 Comprehensive Environmental Forecast", - lines=25, - max_lines=30 - ) + genesis_summary = gr.Textbox(label="📋 Monthly Genesis Analysis Summary", lines=25) - def run_oceanic_prediction( - region, month, oni, hours, advanced_phys, uncertainty, - animation, use_real_data, show_env_details - ): + def run_genesis_prediction(month, oni, animation): try: - # Run enhanced oceanic prediction - results = predict_storm_route_and_intensity_with_oceanic_data( - region, month, oni, hours, - use_real_data=use_real_data, - models=None, - enable_animation=animation - ) - - # Extract enhanced conditions - current = results['current_prediction'] - env_data = results['environmental_data'] - - intensity = current['intensity_kt'] - category = current['category'] - env_potential = current.get('environmental_potential', 50) - env_favorability = current.get('environmental_favorability', 'Unknown') - - # Data source information - sst_source = env_data.get('sst_source', 'Unknown') - slp_source = env_data.get('slp_source', 'Unknown') - - # Create enhanced visualization - fig, forecast_text = create_animated_route_visualization( - results, uncertainty, animation - ) + # Generate monthly prediction using GPI + prediction_data = generate_genesis_prediction_monthly(month, oni, year=2025) - # Enhanced forecast text with environmental details - if show_env_details: - enhanced_forecast_text = generate_enhanced_environmental_forecast_text( - results, forecast_text - ) - else: - enhanced_forecast_text = forecast_text + # Create animation + genesis_fig = create_genesis_animation(prediction_data, animation) - model_info = f"{results['model_info']}\nReal-time Data: {'Yes' if use_real_data else 'No'}" + # Generate summary + summary_text = create_prediction_summary(prediction_data) - return ( - intensity, - category, - env_potential, - env_favorability, - sst_source, - slp_source, - model_info, - fig, - enhanced_forecast_text - ) + return genesis_fig, summary_text except Exception as e: - error_msg = f"Enhanced oceanic prediction failed: {str(e)}" - logging.error(error_msg) import traceback - traceback.print_exc() - - return ( - 30, "Tropical Depression", 50, "Unknown", - "Error", "Error", f"Prediction failed: {str(e)}", - None, f"Error generating enhanced forecast: {str(e)}" - ) + error_msg = f"Genesis prediction failed: {str(e)}\n\nDetails:\n{traceback.format_exc()}" + logging.error(error_msg) + return create_error_plot(error_msg), error_msg - predict_oceanic_btn.click( - fn=run_oceanic_prediction, - inputs=[ - genesis_region, pred_month, pred_oni, forecast_hours, - advanced_physics, show_uncertainty, enable_animation, - use_real_oceanic, show_environmental_details - ], - outputs=[ - current_intensity, current_category, environmental_potential, - environmental_favorability, sst_data_source, slp_data_source, - model_confidence, route_plot, forecast_details - ] + generate_genesis_btn.click( + fn=run_genesis_prediction, + inputs=[genesis_month, genesis_oni, enable_genesis_animation], + outputs=[genesis_animation, genesis_summary] ) - - # Enhanced information section - oceanic_info_text = """ - ### 🌊 Oceanic Data Integration Features: - - #### 🔥 Real-time Data Sources: - - **SST**: NOAA OISST v2 - Daily 0.25° resolution satellite-based sea surface temperatures - - **SLP**: NCEP/NCAR Reanalysis - 6-hourly 2.5° resolution atmospheric pressure fields - - **Coverage**: Global oceans with 1-2 day latency for most recent conditions - - **Protocols**: ERDDAP and OPeNDAP for standardized data access - - #### 🧠 Environmental Physics: - - **Emanuel Potential Intensity**: Theoretical maximum intensity based on thermodynamics - - **SST-Intensity Coupling**: Non-linear relationship between sea surface temperature and storm intensity - - **Atmospheric Steering**: Sea level pressure gradients drive storm motion patterns - - **Wind Shear Modeling**: Vertical wind shear estimation from pressure patterns and ENSO state - - #### 🎯 Enhanced Accuracy: - - **Real-time Environmental Limits**: Current oceanic conditions set maximum achievable intensity - - **Dynamic Development**: Storm intensification rate depends on real SST and atmospheric conditions - - **Track Steering**: Motion influenced by current pressure patterns rather than climatology alone - - **Confidence Scoring**: Higher confidence when real-time data successfully integrated - - #### 🔄 Fallback Systems: - - **Automatic Degradation**: Seamlessly switches to climatology if real-time data unavailable - - **Quality Assessment**: Evaluates data completeness and provides appropriate confidence levels - - **Hybrid Approach**: Combines real-time data with climatological patterns for optimal accuracy - - **Error Handling**: Robust system continues operation even with partial data failures - - #### 📊 Output Enhancements: - - **Environmental Metadata**: Track-point SST, SLP, and environmental limits - - **Data Source Tracking**: Clear indication of real-time vs climatological data usage - - **Uncertainty Quantification**: Confidence intervals based on data availability and environmental complexity - - **Detailed Analysis**: Comprehensive breakdown of environmental factors affecting development - """ - gr.Markdown(oceanic_info_text) with gr.Tab("🔬 Advanced ML Clustering"): gr.Markdown("## 🎯 Storm Pattern Analysis with Separate Visualizations") @@ -4084,9 +3186,9 @@ def create_interface(): outputs=[typhoon_dropdown] ) - # Generate video with fixed function + # Generate video generate_video_btn.click( - fn=generate_enhanced_track_video_fixed, + fn=generate_enhanced_track_video, inputs=[year_dropdown, typhoon_dropdown, standard_dropdown], outputs=[video_output] ) @@ -4153,7 +3255,7 @@ def create_interface(): except Exception as e: gr.Markdown(f"Visualization error: {str(e)}") - # Enhanced statistics + # Enhanced statistics - FIXED formatting total_storms = len(typhoon_data['SID'].unique()) if 'SID' in typhoon_data.columns else 0 total_records = len(typhoon_data) @@ -4214,7 +3316,7 @@ def create_interface(): - **Complete TD Analysis** - First platform to include comprehensive TD tracking - **Dual Classification Systems** - Both Atlantic and Taiwan standards supported - **Advanced ML Clustering** - DBSCAN pattern recognition with separate visualizations - - **Real-time Oceanic Predictions** - Physics-based with SST/SLP integration + - **Real-time Predictions** - Physics-based and optional CNN intensity forecasting - **2025 Data Ready** - Full compatibility with current season data - **Enhanced Animations** - Professional-quality storm track videos - **Multi-basin Analysis** - Comprehensive Pacific and Atlantic coverage @@ -4224,13 +3326,12 @@ def create_interface(): - Seasonal forecasting research - Storm pattern classification - ENSO-typhoon relationship analysis - - Oceanic-atmospheric coupling research + - Intensity prediction model development - Cross-regional classification comparisons """ gr.Markdown(stats_text) return demo - except Exception as e: logging.error(f"Error creating Gradio interface: {e}") import traceback @@ -4294,12 +3395,8 @@ def create_minimal_fallback_interface(): return demo -# Initialize data -initialize_data() - # Create and launch the interface demo = create_interface() if __name__ == "__main__": - demo.launch(share=True) - + demo.launch(share=True) # Enable sharing with public link \ No newline at end of file