diff --git "a/app.py" "b/app.py"
--- "a/app.py"
+++ "b/app.py"
@@ -114,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 = {
@@ -140,12 +140,14 @@ matplotlib_color_map = {
'C5 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
+# 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
}
def rgb_string_to_hex(rgb_string):
@@ -166,9 +168,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(category):
- """Get Taiwan standard color for a storm category"""
- return taiwan_color_map.get(category, '#808080')
+def get_taiwan_color_fixed(category):
+ """Get corrected Taiwan standard color"""
+ return taiwan_color_map_fixed.get(category, '#808080')
# Cluster colors for route visualization
CLUSTER_COLORS = [
@@ -204,12 +206,14 @@ atlantic_standard = {
'Tropical Depression': {'wind_speed': 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)
+# 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'}
}
# -----------------------------
@@ -709,7 +713,7 @@ def merge_data(oni_long, typhoon_max):
return pd.merge(typhoon_max, oni_long, on=['Year','Month'])
# -----------------------------
-# ENHANCED: Categorization Functions (FIXED TAIWAN)
+# ENHANCED: Categorization Functions - FIXED TAIWAN CLASSIFICATION
# -----------------------------
def categorize_typhoon_enhanced(wind_speed):
@@ -737,26 +741,33 @@ def categorize_typhoon_enhanced(wind_speed):
else: # 137+ knots = Category 5 Super Typhoon
return 'C5 Super Typhoon'
-def categorize_typhoon_taiwan(wind_speed):
- """FIXED Taiwan categorization system according to official CWA standards"""
+def categorize_typhoon_taiwan_fixed(wind_speed):
+ """
+ FIXED Taiwan categorization system based on CMA 2006 standards
+ Reference: CMA Tropical Cyclone Data Center official classification
+ """
if pd.isna(wind_speed):
return 'Tropical Depression'
- # 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
+ # 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:
+ wind_speed_ms = wind_speed
- # 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'
+ # 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)
# Original function for backward compatibility
def categorize_typhoon(wind_speed):
@@ -776,907 +787,33 @@ def classify_enso_phases(oni_value):
else:
return 'Neutral'
-def categorize_typhoon_by_standard(wind_speed, standard='atlantic'):
- """FIXED categorization function with correct Taiwan standards"""
+# FIXED: Combined categorization function
+def categorize_typhoon_by_standard_fixed(wind_speed, standard='atlantic'):
+ """FIXED categorization function supporting both standards with correct Taiwan thresholds"""
if pd.isna(wind_speed):
return 'Tropical Depression', '#808080'
if standard == 'taiwan':
- category = categorize_typhoon_taiwan(wind_speed)
- color = taiwan_color_map.get(category, '#808080')
+ category = categorize_typhoon_taiwan_fixed(wind_speed)
+ color = taiwan_color_map_fixed.get(category, '#808080')
return category, color
+
else:
- # Atlantic/International standard (existing logic is correct)
+ # Atlantic/International standard (unchanged)
if wind_speed >= 137:
- return 'C5 Super Typhoon', '#FF0000' # Red
+ return 'C5 Super Typhoon', '#FF0000'
elif wind_speed >= 113:
- return 'C4 Very Strong Typhoon', '#FFA500' # Orange
+ return 'C4 Very Strong Typhoon', '#FFA500'
elif wind_speed >= 96:
- return 'C3 Strong Typhoon', '#FFFF00' # Yellow
+ return 'C3 Strong Typhoon', '#FFFF00'
elif wind_speed >= 83:
- return 'C2 Typhoon', '#00FF00' # Green
+ return 'C2 Typhoon', '#00FF00'
elif wind_speed >= 64:
- return 'C1 Typhoon', '#00FFFF' # Cyan
+ return 'C1 Typhoon', '#00FFFF'
elif wind_speed >= 34:
- return 'Tropical Storm', '#0000FF' # Blue
- return 'Tropical Depression', '#808080' # Gray
-
-# -----------------------------
-# FIXED: Genesis Potential Index (GPI) Based Prediction System
-# -----------------------------
-
-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:
- # 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)
-
- # 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 calculating GPI: {e}")
- return 0.0
-
-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 generate_genesis_prediction_monthly(month, oni_value, year=2025):
- """
- Generate realistic typhoon genesis prediction for a given month using GPI
- Returns day-by-day genesis potential and storm development scenarios
- """
- try:
- logging.info(f"Generating GPI-based prediction for month {month}, ONI {oni_value}")
-
- # 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)
-
- # Add daily variability
- daily_variation = 0.1 * np.sin(2 * np.pi * day / 30) + np.random.normal(0, 0.05)
-
- # 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
- )
-
- 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)
-
- genesis_lat = lat_range[max_i]
- genesis_lon = lon_range[max_j]
- genesis_gpi = gpi_field[max_i, max_j]
-
- # Determine probability of actual genesis
- genesis_prob = min(0.8, genesis_gpi / 3.0)
-
- 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 {
- '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 in genesis prediction: {e}")
- import traceback
- traceback.print_exc()
- return {
- 'error': str(e),
- 'month': month,
- 'oni_value': oni_value,
- 'storm_predictions': []
- }
-
-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}
-
- # Position uncertainty grows with time
- position_uncertainty = [point['position_uncertainty'] for point in track_points]
-
- # Intensity uncertainty
- intensity_uncertainty = [point['intensity_uncertainty'] for point in track_points]
-
- return {
- '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 create_predict_animation(prediction_data, enable_animation=True):
- """
- Typhoon genesis PREDICT tab animation:
- shows monthly genesis-potential + progressive storm positions
- """
- try:
- 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'
- )
- )
-
- # 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
- }
-
- # -- 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}'
- )
- ))
-
- # 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}"
- )
- ))
-
- # -- 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)]
- )
+ return 'Tropical Storm', '#0000FF'
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
- )
- )
-
- 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'
- )
- )
-
- # ---- 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}'
- )
- ))
-
- # 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}"
- )
- ))
-
- # ---- 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:
-"""
-
- 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"
-
- 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 summary_text
-
- except Exception as e:
- logging.error(f"Error creating prediction summary: {e}")
- return f"Error generating summary: {str(e)}"
+ return 'Tropical Depression', '#808080'
# -----------------------------
# FIXED: ADVANCED ML FEATURES WITH ROBUST ERROR HANDLING
@@ -2385,6 +1522,873 @@ 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 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"}
+ }
+
+def predict_storm_route_and_intensity_realistic(genesis_region, month, oni_value, models=None, forecast_hours=72, use_advanced_physics=True):
+ """Realistic prediction with proper typhoon speeds and development"""
+ try:
+ genesis_locations = get_realistic_genesis_locations()
+
+ if genesis_region not in genesis_locations:
+ genesis_region = "Western Pacific Main Development Region" # Default
+
+ genesis_info = genesis_locations[genesis_region]
+ lat = genesis_info["lat"]
+ lon = genesis_info["lon"]
+
+ results = {
+ 'current_prediction': {},
+ 'route_forecast': [],
+ 'confidence_scores': {},
+ 'model_info': 'Realistic Genesis Model',
+ 'genesis_info': genesis_info
+ }
+
+ # REALISTIC starting intensity - Tropical Depression level
+ base_intensity = 30 # Start at TD level (25-35 kt)
+
+ # Environmental factors for genesis
+ if oni_value > 1.0: # Strong El Niño - suppressed development
+ intensity_modifier = -6
+ elif oni_value > 0.5: # Moderate El Niño
+ intensity_modifier = -3
+ elif oni_value < -1.0: # Strong La Niña - enhanced development
+ intensity_modifier = +8
+ elif oni_value < -0.5: # Moderate La Niña
+ intensity_modifier = +5
+ else: # Neutral
+ intensity_modifier = oni_value * 2
+
+ # Seasonal genesis effects
+ seasonal_factors = {
+ 1: -8, 2: -6, 3: -4, 4: -2, 5: 2, 6: 6,
+ 7: 10, 8: 12, 9: 15, 10: 10, 11: 4, 12: -5
+ }
+ seasonal_modifier = seasonal_factors.get(month, 0)
+
+ # Genesis region favorability
+ region_factors = {
+ "Western Pacific Main Development Region": 8,
+ "South China Sea": 4,
+ "Philippine Sea": 5,
+ "Marshall Islands": 7,
+ "Monsoon Trough": 6,
+ "ITCZ Region": 3,
+ "Subtropical Region": 2,
+ "Bay of Bengal": 4,
+ "Eastern Pacific": 6,
+ "Atlantic MDR": 5
+ }
+ region_modifier = region_factors.get(genesis_region, 0)
+
+ # Calculate realistic starting intensity (TD level)
+ predicted_intensity = base_intensity + intensity_modifier + seasonal_modifier + region_modifier
+ predicted_intensity = max(25, min(40, predicted_intensity)) # Keep in TD-weak TS range
+
+ # Add realistic uncertainty for genesis
+ intensity_uncertainty = np.random.normal(0, 2)
+ predicted_intensity += intensity_uncertainty
+ predicted_intensity = max(25, min(38, predicted_intensity)) # TD range
+
+ results['current_prediction'] = {
+ 'intensity_kt': predicted_intensity,
+ 'pressure_hpa': 1008 - (predicted_intensity - 25) * 0.6, # Realistic TD pressure
+ 'category': categorize_typhoon_enhanced(predicted_intensity),
+ 'genesis_region': genesis_region
+ }
+
+ # REALISTIC route prediction with proper typhoon speeds
+ current_lat = lat
+ current_lon = lon
+ current_intensity = predicted_intensity
+
+ route_points = []
+
+ # Track storm development over time with REALISTIC SPEEDS
+ for hour in range(0, forecast_hours + 6, 6):
+
+ # REALISTIC typhoon motion - much faster speeds
+ # Typical typhoon forward speed: 15-25 km/h (0.14-0.23°/hour)
+
+ # Base forward speed depends on latitude and storm intensity
+ if current_lat < 20: # Low latitude - slower
+ base_speed = 0.12 # ~13 km/h
+ elif current_lat < 30: # Mid latitude - moderate
+ base_speed = 0.18 # ~20 km/h
+ else: # High latitude - faster
+ base_speed = 0.25 # ~28 km/h
+
+ # Intensity affects speed (stronger storms can move faster)
+ intensity_speed_factor = 1.0 + (current_intensity - 50) / 200
+ base_speed *= max(0.8, min(1.4, intensity_speed_factor))
+
+ # Beta drift (Coriolis effect) - realistic values
+ beta_drift_lat = 0.02 * np.sin(np.radians(current_lat))
+ beta_drift_lon = -0.05 * np.cos(np.radians(current_lat))
+
+ # Seasonal steering patterns with realistic speeds
+ if month in [6, 7, 8, 9]: # Peak season
+ ridge_strength = 1.2
+ ridge_position = 32 + 4 * np.sin(2 * np.pi * (month - 6) / 4)
+ else: # Off season
+ ridge_strength = 0.9
+ ridge_position = 28
+
+ # REALISTIC motion based on position relative to subtropical ridge
+ if current_lat < ridge_position - 10: # Well south of ridge - westward movement
+ lat_tendency = base_speed * 0.3 + beta_drift_lat # Slight poleward
+ lon_tendency = -base_speed * 0.9 + beta_drift_lon # Strong westward
+ elif current_lat > ridge_position - 3: # Near ridge - recurvature
+ lat_tendency = base_speed * 0.8 + beta_drift_lat # Strong poleward
+ lon_tendency = base_speed * 0.4 + beta_drift_lon # Eastward
+ else: # In between - normal WNW motion
+ lat_tendency = base_speed * 0.4 + beta_drift_lat # Moderate poleward
+ lon_tendency = -base_speed * 0.7 + beta_drift_lon # Moderate westward
+
+ # ENSO steering modulation (realistic effects)
+ if oni_value > 0.5: # El Niño - more eastward/poleward motion
+ lon_tendency += 0.05
+ lat_tendency += 0.02
+ elif oni_value < -0.5: # La Niña - more westward motion
+ lon_tendency -= 0.08
+ lat_tendency -= 0.01
+
+ # Add motion uncertainty that grows with time (realistic error growth)
+ motion_uncertainty = 0.02 + (hour / 120) * 0.04
+ lat_noise = np.random.normal(0, motion_uncertainty)
+ lon_noise = np.random.normal(0, motion_uncertainty)
+
+ # Update position with realistic speeds
+ current_lat += lat_tendency + lat_noise
+ current_lon += lon_tendency + lon_noise
+
+ # REALISTIC intensity evolution with proper development cycles
+
+ # Development phase (first 48-72 hours) - realistic intensification
+ if hour <= 48:
+ if current_intensity < 50: # Still weak - rapid development possible
+ if 10 <= current_lat <= 25 and 115 <= current_lon <= 165: # Favorable environment
+ intensity_tendency = 4.5 if current_intensity < 35 else 3.0
+ elif 120 <= current_lon <= 155 and 15 <= current_lat <= 20: # Best environment
+ intensity_tendency = 6.0 if current_intensity < 40 else 4.0
+ else:
+ intensity_tendency = 2.0
+ elif current_intensity < 80: # Moderate intensity
+ intensity_tendency = 2.5 if (120 <= current_lon <= 155 and 10 <= current_lat <= 25) else 1.0
+ else: # Already strong
+ intensity_tendency = 1.0
+
+ # Mature phase (48-120 hours) - peak intensity maintenance
+ elif hour <= 120:
+ if current_lat < 25 and current_lon > 120: # Still in favorable waters
+ if current_intensity < 120:
+ intensity_tendency = 1.5
+ else:
+ intensity_tendency = 0.0 # Maintain intensity
+ else:
+ intensity_tendency = -1.5
+
+ # Extended phase (120+ hours) - gradual weakening
+ else:
+ if current_lat < 30 and current_lon > 115:
+ intensity_tendency = -2.0 # Slow weakening
+ else:
+ intensity_tendency = -3.5 # Faster weakening
+
+ # Environmental modulation (realistic effects)
+ if current_lat > 35: # High latitude - rapid weakening
+ intensity_tendency -= 12
+ elif current_lat > 30: # Moderate latitude
+ intensity_tendency -= 5
+ elif current_lon < 110: # Land interaction
+ intensity_tendency -= 15
+ elif 125 <= current_lon <= 155 and 10 <= current_lat <= 25: # Warm pool
+ intensity_tendency += 2
+ elif 160 <= current_lon <= 180 and 15 <= current_lat <= 30: # Still warm
+ intensity_tendency += 1
+
+ # SST effects (realistic temperature impact)
+ if current_lat < 8: # Very warm but weak Coriolis
+ intensity_tendency += 0.5
+ elif 8 <= current_lat <= 20: # Sweet spot for development
+ intensity_tendency += 2.0
+ elif 20 < current_lat <= 30: # Marginal
+ intensity_tendency -= 1.0
+ elif current_lat > 30: # Cool waters
+ intensity_tendency -= 4.0
+
+ # Shear effects (simplified but realistic)
+ if month in [12, 1, 2, 3]: # High shear season
+ intensity_tendency -= 2.0
+ elif month in [7, 8, 9]: # Low shear season
+ intensity_tendency += 1.0
+
+ # Update intensity with realistic bounds and variability
+ intensity_noise = np.random.normal(0, 1.5) # Small random fluctuations
+ current_intensity += intensity_tendency + intensity_noise
+ current_intensity = max(20, min(185, current_intensity)) # Realistic range
+
+ # Calculate confidence based on forecast time and environment
+ base_confidence = 0.92
+ time_penalty = (hour / 120) * 0.45
+ environment_penalty = 0.15 if current_lat > 30 or current_lon < 115 else 0
+ confidence = max(0.25, base_confidence - time_penalty - environment_penalty)
+
+ # Determine development stage
+ if hour <= 24:
+ stage = 'Genesis'
+ elif hour <= 72:
+ stage = 'Development'
+ elif hour <= 120:
+ stage = 'Mature'
+ elif hour <= 240:
+ stage = 'Extended'
+ else:
+ stage = 'Long-term'
+
+ 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)
+ })
+
+ 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)
+ }
+
+ # Model information
+ results['model_info'] = f"Enhanced Realistic Model - {genesis_region}"
+
+ return results
+
+ 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'
+ }
+
+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)
# -----------------------------
@@ -2589,7 +2593,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
+# ENHANCED: Animation Functions with Taiwan Standard Support - FIXED VERSION
# -----------------------------
def get_available_years(typhoon_data):
@@ -2674,8 +2678,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(year, typhoon_selection, standard):
- """Enhanced track video generation with TD support, Taiwan standard, and 2025 compatibility"""
+def generate_enhanced_track_video_fixed(year, typhoon_selection, standard):
+ """FIXED: Enhanced track video generation with working animation display"""
if not typhoon_selection or typhoon_selection == "No storms found":
return None
@@ -2700,16 +2704,17 @@ def generate_enhanced_track_video(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) # Default TD strength
+ winds = np.full(len(lats), 30)
# 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 video for {storm_name} ({sid}) with {len(lats)} track points using {standard} standard")
+ print(f"Generating FIXED video for {storm_name} ({sid}) with {len(lats)} track points using {standard} standard")
- # Create figure with enhanced map
- fig, ax = plt.subplots(figsize=(16, 10), subplot_kw={'projection': ccrs.PlateCarree()})
+ # FIXED: Create figure with proper cartopy setup
+ fig = plt.figure(figsize=(16, 10))
+ ax = plt.axes(projection=ccrs.PlateCarree())
# Enhanced map features
ax.stock_img()
@@ -2729,26 +2734,35 @@ def generate_enhanced_track_video(year, typhoon_selection, standard):
gl = ax.gridlines(draw_labels=True, alpha=0.3)
gl.top_labels = gl.right_labels = False
- # Title with enhanced info and standard
+ # Title
ax.set_title(f"{season} {storm_name} ({sid}) Track Animation - {standard.upper()} Standard",
fontsize=18, fontweight='bold')
- # Animation elements
- line, = ax.plot([], [], 'b-', linewidth=3, alpha=0.7, label='Track')
- point, = ax.plot([], [], 'o', markersize=15)
+ # 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())
- # Enhanced info display
+ # Historical track points (to show path traversed)
+ history_points, = ax.plot([], [], 'o', markersize=6, alpha=0.4, color='blue',
+ transform=ccrs.PlateCarree())
+
+ # Info text box
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))
- # Color legend with both standards - ENHANCED
+ # FIXED: Color legend with proper categories for both standards
legend_elements = []
-
if standard == 'taiwan':
- categories = ['Tropical Depression', 'Tropical Storm', 'Moderate Typhoon', 'Intense Typhoon']
+ categories = ['Tropical Depression', 'Tropical Storm', 'Severe Tropical Storm',
+ 'Typhoon', 'Severe Typhoon', 'Super Typhoon']
for category in categories:
- color = get_taiwan_color(category)
+ color = get_taiwan_color_fixed(category)
legend_elements.append(plt.Line2D([0], [0], marker='o', color='w',
markerfacecolor=color, markersize=10, label=category))
else:
@@ -2761,40 +2775,51 @@ def generate_enhanced_track_video(year, typhoon_selection, standard):
ax.legend(handles=legend_elements, loc='upper right', fontsize=10)
- def animate(frame):
+ # FIXED: Animation function with proper artist updates and cartopy compatibility
+ def animate_fixed(frame):
+ """Fixed animation function that properly updates tracks with cartopy"""
try:
if frame >= len(lats):
- return line, point, info_box
+ return track_line, current_point, history_points, info_box
- # Update track line
- line.set_data(lons[:frame+1], lats[:frame+1])
+ # FIXED: Update track line up to current frame
+ current_lons = lons[:frame+1]
+ current_lats = lats[:frame+1]
- # Update current position with appropriate categorization
+ # Update the track line data (this is the key fix!)
+ track_line.set_data(current_lons, current_lats)
+
+ # 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
current_wind = winds[frame]
if standard == 'taiwan':
- category, color = categorize_typhoon_by_standard(current_wind, 'taiwan')
+ category, color = categorize_typhoon_by_standard_fixed(current_wind, 'taiwan')
else:
- category, color = categorize_typhoon_by_standard(current_wind, 'atlantic')
+ category, color = categorize_typhoon_by_standard_fixed(current_wind, 'atlantic')
- # Debug print for first few frames
+ # Debug for first few frames
if frame < 3:
- print(f"Frame {frame}: Wind={current_wind:.1f}kt, Category={category}, Color={color}, Standard={standard}")
+ print(f"FIXED Frame {frame}: Wind={current_wind:.1f}kt, Category={category}, Color={color}")
- point.set_data([lons[frame]], [lats[frame]])
- point.set_color(color)
- point.set_markersize(10 + current_wind/8) # Size based on intensity
+ # 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)
- # Enhanced info display with standard information
+ # FIXED: Enhanced info display with correct Taiwan wind speed conversion
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}"
- # Convert wind speed for Taiwan standard display
+ # Corrected wind speed display for Taiwan standard
if standard == 'taiwan':
- wind_ms = current_wind * 0.514444 # Convert to m/s for display
+ wind_ms = current_wind * 0.514444
wind_display = f"{current_wind:.0f} kt ({wind_ms:.1f} m/s)"
else:
wind_display = f"{current_wind:.0f} kt"
@@ -2810,47 +2835,49 @@ def generate_enhanced_track_video(year, typhoon_selection, standard):
)
info_box.set_text(info_text)
- return line, point, info_box
+ # FIXED: Return all modified artists (crucial for proper display)
+ return track_line, current_point, history_points, info_box
except Exception as e:
print(f"Error in animate frame {frame}: {e}")
- return line, point, info_box
+ return track_line, current_point, history_points, info_box
- # Create animation
+ # FIXED: Create animation with cartopy-compatible settings
+ # Key fixes: blit=False (crucial for cartopy), proper interval
anim = animation.FuncAnimation(
- fig, animate, frames=len(lats),
- interval=400, blit=False, repeat=True # Slightly slower for better viewing
+ fig, animate_fixed, frames=len(lats),
+ interval=600, blit=False, repeat=True # blit=False is essential for cartopy!
)
- # Save animation
+ # Save animation with optimized settings
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4',
dir=tempfile.gettempdir())
- # Enhanced writer settings
+ # FIXED: Writer settings optimized for track visibility
writer = animation.FFMpegWriter(
- fps=3, bitrate=2000, codec='libx264', # Slower FPS for better visibility
- extra_args=['-pix_fmt', 'yuv420p'] # Better compatibility
+ fps=2, bitrate=3000, codec='libx264', # Slower FPS for better track visibility
+ extra_args=['-pix_fmt', 'yuv420p']
)
- print(f"Saving animation to {temp_file.name}")
- anim.save(temp_file.name, writer=writer, dpi=120) # Higher DPI for better quality
+ print(f"Saving FIXED animation to {temp_file.name}")
+ anim.save(temp_file.name, writer=writer, dpi=120)
plt.close(fig)
- print(f"Video generated successfully: {temp_file.name}")
+ print(f"FIXED video generated successfully: {temp_file.name}")
return temp_file.name
except Exception as e:
- print(f"Error generating video: {e}")
+ print(f"Error generating FIXED video: {e}")
import traceback
traceback.print_exc()
return None
-# Simplified wrapper for backward compatibility - FIXED
-def simplified_track_video(year, basin, typhoon, standard):
- """Simplified track video function with fixed color handling"""
+# 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"""
if not typhoon:
return None
- return generate_enhanced_track_video(year, typhoon, standard)
+ return generate_enhanced_track_video_fixed(year, typhoon, standard)
# -----------------------------
# Load & Process Data
@@ -2962,73 +2989,6 @@ def create_interface():
"""
gr.Markdown(overview_text)
- 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=1):
- genesis_month = gr.Slider(
- 1, 12,
- label="Month",
- value=9,
- step=1,
- info="1=Jan, 2=Feb, ..., 12=Dec"
- )
- genesis_oni = gr.Number(
- label="ONI Value",
- value=0.0,
- info="El Niño (+) / La Niña (-) / Neutral (0)"
- )
- enable_genesis_animation = gr.Checkbox(
- label="Enable Animation",
- value=True,
- info="Watch daily genesis potential evolution"
- )
- 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():
- genesis_animation = gr.Plot(label="🗺️ Daily Genesis Potential & Storm Development")
-
- with gr.Row():
- genesis_summary = gr.Textbox(label="📋 Monthly Genesis Analysis Summary", lines=25)
-
- def run_genesis_prediction(month, oni, animation):
- try:
- # Generate monthly prediction using GPI
- prediction_data = generate_genesis_prediction_monthly(month, oni, year=2025)
-
- # Create animation
- genesis_fig = create_genesis_animation(prediction_data, animation)
-
- # Generate summary
- summary_text = create_prediction_summary(prediction_data)
-
- return genesis_fig, summary_text
-
- except Exception as e:
- import traceback
- 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
-
- generate_genesis_btn.click(
- fn=run_genesis_prediction,
- inputs=[genesis_month, genesis_oni, enable_genesis_animation],
- outputs=[genesis_animation, genesis_summary]
- )
-
with gr.Tab("🔬 Advanced ML Clustering"):
gr.Markdown("## 🎯 Storm Pattern Analysis with Separate Visualizations")
gr.Markdown("**Four separate plots: Clustering, Routes, Pressure Evolution, and Wind Evolution**")
@@ -3081,6 +3041,156 @@ def create_interface():
inputs=[reduction_method],
outputs=[cluster_plot, routes_plot, pressure_plot, wind_plot, cluster_stats]
)
+
+ cluster_info_text = """
+ ### 📊 Enhanced Clustering Features:
+ - **Separate Visualizations**: Four distinct plots for comprehensive analysis
+ - **Multi-dimensional Analysis**: Uses 15+ storm characteristics including intensity, track shape, genesis location
+ - **Route Visualization**: Geographic storm tracks colored by cluster membership
+ - **Temporal Analysis**: Pressure and wind evolution patterns by cluster
+ - **DBSCAN Clustering**: Automatic pattern discovery without predefined cluster count
+ - **Interactive**: Hover over points to see storm details, zoom and pan all plots
+
+ ### 🎯 How to Interpret:
+ - **Clustering Plot**: Each dot is a storm positioned by similarity (close = similar characteristics)
+ - **Routes Plot**: Actual geographic storm tracks, colored by which cluster they belong to
+ - **Pressure Plot**: Shows how pressure changes over time for storms in each cluster
+ - **Wind Plot**: Shows wind speed evolution patterns for each cluster
+ - **Cluster Colors**: Each cluster gets a unique color across all four visualizations
+ """
+ gr.Markdown(cluster_info_text)
+
+ with gr.Tab("🌊 Realistic Storm Genesis & Prediction"):
+ gr.Markdown("## 🌊 Realistic Typhoon Development from Genesis")
+
+ if CNN_AVAILABLE:
+ gr.Markdown("🧠 **Deep Learning models available** - TensorFlow loaded successfully")
+ method_description = "Hybrid CNN-Physics genesis modeling with realistic development cycles"
+ else:
+ gr.Markdown("🔬 **Physics-based models available** - Using climatological relationships")
+ method_description = "Advanced physics-based genesis modeling with environmental coupling"
+
+ gr.Markdown(f"**Current Method**: {method_description}")
+ gr.Markdown("**🌊 Realistic Genesis**: Select from climatologically accurate development regions")
+ gr.Markdown("**📈 TD Starting Point**: Storms begin at realistic Tropical Depression intensities (25-35 kt)")
+ gr.Markdown("**🎬 Animation Support**: Watch storm development unfold over time")
+
+ with gr.Row():
+ with gr.Column(scale=2):
+ gr.Markdown("### 🌊 Genesis 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="Select realistic development region based on climatology"
+ )
+
+ # Display selected region info
+ def update_genesis_info(region):
+ locations = get_realistic_genesis_locations()
+ if region in locations:
+ info = locations[region]
+ return f"📍 Location: {info['lat']:.1f}°N, {info['lon']:.1f}°E\n📝 {info['description']}"
+ return "Select a genesis region"
+
+ genesis_info_display = gr.Textbox(
+ label="Selected Region Info",
+ lines=2,
+ interactive=False,
+ value=update_genesis_info("Western Pacific Main Development Region")
+ )
+
+ genesis_region.change(
+ fn=update_genesis_info,
+ inputs=[genesis_region],
+ outputs=[genesis_info_display]
+ )
+
+ with gr.Row():
+ pred_month = gr.Slider(1, 12, label="Month", value=9, info="Peak season: Jul-Oct")
+ pred_oni = gr.Number(label="ONI Value", value=0.0, info="ENSO index (-3 to 3)")
+ with gr.Row():
+ forecast_hours = gr.Number(
+ label="Forecast Length (hours)",
+ value=72,
+ minimum=20,
+ maximum=100000,
+ step=6,
+ info="Extended forecasting: 20-1000hours (42 days max)"
+ )
+ advanced_physics = gr.Checkbox(
+ label="Advanced Physics",
+ value=True,
+ info="Enhanced environmental modeling"
+ )
+ with gr.Row():
+ show_uncertainty = gr.Checkbox(label="Show Uncertainty Cone", value=True)
+ enable_animation = gr.Checkbox(
+ label="Enable Animation",
+ value=True,
+ info="Animated storm development vs static view"
+ )
+
+ with gr.Column(scale=1):
+ gr.Markdown("### ⚙️ Prediction Controls")
+ predict_btn = gr.Button("🌊 Generate Realistic Storm Forecast", variant="primary", size="lg")
+
+ gr.Markdown("### 📊 Genesis Conditions")
+ current_intensity = gr.Number(label="Genesis Intensity (kt)", interactive=False)
+ current_category = gr.Textbox(label="Initial Category", interactive=False)
+ model_confidence = gr.Textbox(label="Model Info", interactive=False)
+
+ with gr.Row():
+ route_plot = gr.Plot(label="🗺️ Advanced Route & Intensity Forecast")
+
+ with gr.Row():
+ forecast_details = gr.Textbox(label="📋 Detailed Forecast Summary", lines=20, max_lines=25)
+
+ def run_realistic_prediction(region, month, oni, hours, advanced_phys, uncertainty, animation):
+ try:
+ # Run realistic prediction with genesis region
+ results = predict_storm_route_and_intensity_realistic(
+ region, month, oni,
+ forecast_hours=hours,
+ use_advanced_physics=advanced_phys
+ )
+
+ # Extract genesis conditions
+ current = results['current_prediction']
+ intensity = current['intensity_kt']
+ category = current['category']
+ genesis_info = results.get('genesis_info', {})
+
+ # Create enhanced visualization
+ fig, forecast_text = create_animated_route_visualization(
+ results, uncertainty, animation
+ )
+
+ model_info = f"{results['model_info']}\nGenesis: {genesis_info.get('description', 'Unknown')}"
+
+ return (
+ intensity,
+ category,
+ model_info,
+ fig,
+ forecast_text
+ )
+ except Exception as e:
+ error_msg = f"Realistic prediction failed: {str(e)}"
+ logging.error(error_msg)
+ import traceback
+ traceback.print_exc()
+ return (
+ 30, "Tropical Depression", f"Prediction failed: {str(e)}",
+ None, f"Error generating realistic forecast: {str(e)}"
+ )
+
+ predict_btn.click(
+ fn=run_realistic_prediction,
+ inputs=[genesis_region, pred_month, pred_oni, forecast_hours, advanced_physics, show_uncertainty, enable_animation],
+ outputs=[current_intensity, current_category, model_confidence, route_plot, forecast_details]
+ )
with gr.Tab("🗺️ Track Visualization"):
with gr.Row():
@@ -3186,12 +3296,41 @@ def create_interface():
outputs=[typhoon_dropdown]
)
- # Generate video
+ # FIXED: Generate video with fixed function
generate_video_btn.click(
- fn=generate_enhanced_track_video,
+ fn=generate_enhanced_track_video_fixed,
inputs=[year_dropdown, typhoon_dropdown, standard_dropdown],
outputs=[video_output]
)
+
+ # FIXED animation info text with corrected Taiwan standards
+ animation_info_text = """
+ ### 🎬 Enhanced Animation Features:
+ - **Dual Standards**: Full support for both Atlantic and Taiwan classification systems
+ - **Full TD Support**: Now displays Tropical Depressions (< 34 kt) in gray
+ - **2025 Compatibility**: Complete support for current year data
+ - **Enhanced Maps**: Better cartographic projections with terrain features
+ - **Smart Scaling**: Storm symbols scale dynamically with intensity
+ - **Real-time Info**: Live position, time, and meteorological data display
+ - **Professional Styling**: Publication-quality animations with proper legends
+ - **Optimized Export**: Fast rendering with web-compatible video formats
+ - **FIXED Animation**: Tracks now display properly with cartopy integration
+
+ ### 🎌 Taiwan Standard Features (CORRECTED):
+ - **CMA 2006 Standards**: Uses official China Meteorological Administration classification
+ - **Six Categories**: TD → TS → STS → TY → STY → Super TY
+ - **Correct Thresholds**:
+ * Tropical Depression: < 17.2 m/s (< 33.4 kt)
+ * Tropical Storm: 17.2-24.4 m/s (33.4-47.5 kt)
+ * Severe Tropical Storm: 24.5-32.6 m/s (47.6-63.5 kt)
+ * Typhoon: 32.7-41.4 m/s (63.6-80.6 kt)
+ * Severe Typhoon: 41.5-50.9 m/s (80.7-99.1 kt)
+ * Super Typhoon: ≥51.0 m/s (≥99.2 kt)
+ - **m/s Display**: Shows both knots and meters per second
+ - **CWB Compatible**: Matches Central Weather Bureau classifications
+ - **Fixed Color Coding**: Gray → Blue → Cyan → Yellow → Orange → Red
+ """
+ gr.Markdown(animation_info_text)
with gr.Tab("📊 Data Statistics & Insights"):
gr.Markdown("## 📈 Comprehensive Dataset Analysis")