Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -20,8 +20,9 @@ import plotly.express as px
|
|
| 20 |
from plotly.subplots import make_subplots
|
| 21 |
|
| 22 |
from sklearn.manifold import TSNE
|
| 23 |
-
from sklearn.cluster import DBSCAN
|
| 24 |
from sklearn.preprocessing import StandardScaler
|
|
|
|
| 25 |
from scipy.interpolate import interp1d
|
| 26 |
import statsmodels.api as sm
|
| 27 |
import requests
|
|
@@ -29,6 +30,23 @@ import tempfile
|
|
| 29 |
import shutil
|
| 30 |
import xarray as xr
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
try:
|
| 33 |
import cdsapi
|
| 34 |
CDSAPI_AVAILABLE = True
|
|
@@ -79,8 +97,20 @@ CACHE_FILE = os.path.join(DATA_PATH, 'ibtracs_cache.pkl')
|
|
| 79 |
CACHE_EXPIRY_DAYS = 1
|
| 80 |
|
| 81 |
# -----------------------------
|
| 82 |
-
# Color Maps and Standards
|
| 83 |
# -----------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
color_map = {
|
| 85 |
'C5 Super Typhoon': 'rgb(255, 0, 0)',
|
| 86 |
'C4 Very Strong Typhoon': 'rgb(255, 165, 0)',
|
|
@@ -90,6 +120,7 @@ color_map = {
|
|
| 90 |
'Tropical Storm': 'rgb(0, 0, 255)',
|
| 91 |
'Tropical Depression': 'rgb(128, 128, 128)'
|
| 92 |
}
|
|
|
|
| 93 |
atlantic_standard = {
|
| 94 |
'C5 Super Typhoon': {'wind_speed': 137, 'color': 'Red', 'hex': '#FF0000'},
|
| 95 |
'C4 Very Strong Typhoon': {'wind_speed': 113, 'color': 'Orange', 'hex': '#FFA500'},
|
|
@@ -99,6 +130,7 @@ atlantic_standard = {
|
|
| 99 |
'Tropical Storm': {'wind_speed': 34, 'color': 'Blue', 'hex': '#0000FF'},
|
| 100 |
'Tropical Depression': {'wind_speed': 0, 'color': 'Gray', 'hex': '#808080'}
|
| 101 |
}
|
|
|
|
| 102 |
taiwan_standard = {
|
| 103 |
'Strong Typhoon': {'wind_speed': 51.0, 'color': 'Red', 'hex': '#FF0000'},
|
| 104 |
'Medium Typhoon': {'wind_speed': 33.7, 'color': 'Orange', 'hex': '#FFA500'},
|
|
@@ -248,7 +280,7 @@ def update_oni_data():
|
|
| 248 |
|
| 249 |
def create_fallback_oni_data(output_file):
|
| 250 |
"""Create minimal ONI data for testing"""
|
| 251 |
-
years = range(2000,
|
| 252 |
months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']
|
| 253 |
|
| 254 |
# Create synthetic ONI data
|
|
@@ -527,7 +559,7 @@ def load_data_fixed(oni_path, typhoon_path):
|
|
| 527 |
def create_fallback_typhoon_data():
|
| 528 |
"""Create minimal fallback typhoon data - FIXED VERSION"""
|
| 529 |
# Use proper pandas date_range instead of numpy
|
| 530 |
-
dates = pd.date_range(start='2000-01-01', end='
|
| 531 |
storm_dates = dates[np.random.choice(len(dates), size=100, replace=False)]
|
| 532 |
|
| 533 |
data = []
|
|
@@ -595,31 +627,46 @@ def process_typhoon_data(typhoon_data):
|
|
| 595 |
typhoon_max['Month'] = '01'
|
| 596 |
typhoon_max['Year'] = typhoon_max['SEASON']
|
| 597 |
|
| 598 |
-
typhoon_max['Category'] = typhoon_max['USA_WIND'].apply(
|
| 599 |
return typhoon_max
|
| 600 |
|
| 601 |
def merge_data(oni_long, typhoon_max):
|
| 602 |
"""Merge ONI and typhoon data"""
|
| 603 |
return pd.merge(typhoon_max, oni_long, on=['Year','Month'])
|
| 604 |
|
| 605 |
-
|
| 606 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 607 |
if pd.isna(wind_speed):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 608 |
return 'Tropical Depression'
|
| 609 |
-
|
| 610 |
-
return 'C5 Super Typhoon'
|
| 611 |
-
elif wind_speed >= 113:
|
| 612 |
-
return 'C4 Very Strong Typhoon'
|
| 613 |
-
elif wind_speed >= 96:
|
| 614 |
-
return 'C3 Strong Typhoon'
|
| 615 |
-
elif wind_speed >= 83:
|
| 616 |
-
return 'C2 Typhoon'
|
| 617 |
-
elif wind_speed >= 64:
|
| 618 |
-
return 'C1 Typhoon'
|
| 619 |
-
elif wind_speed >= 34:
|
| 620 |
return 'Tropical Storm'
|
| 621 |
-
|
| 622 |
-
return '
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 623 |
|
| 624 |
def classify_enso_phases(oni_value):
|
| 625 |
"""Classify ENSO phases based on ONI value"""
|
|
@@ -635,7 +682,196 @@ def classify_enso_phases(oni_value):
|
|
| 635 |
return 'Neutral'
|
| 636 |
|
| 637 |
# -----------------------------
|
| 638 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 639 |
# -----------------------------
|
| 640 |
|
| 641 |
def perform_wind_regression(start_year, start_month, end_year, end_month):
|
|
@@ -690,7 +926,7 @@ def perform_longitude_regression(start_year, start_month, end_year, end_month):
|
|
| 690 |
return f"Longitude Regression Error: {e}"
|
| 691 |
|
| 692 |
# -----------------------------
|
| 693 |
-
# Visualization Functions
|
| 694 |
# -----------------------------
|
| 695 |
|
| 696 |
def get_full_tracks(start_year, start_month, end_year, end_month, enso_phase, typhoon_search):
|
|
@@ -753,7 +989,7 @@ def get_full_tracks(start_year, start_month, end_year, end_month, enso_phase, ty
|
|
| 753 |
return fig, f"Total typhoons displayed: {count}"
|
| 754 |
|
| 755 |
def get_wind_analysis(start_year, start_month, end_year, end_month, enso_phase, typhoon_search):
|
| 756 |
-
"""Get wind analysis"""
|
| 757 |
start_date = datetime(start_year, start_month, 1)
|
| 758 |
end_date = datetime(end_year, end_month, 28)
|
| 759 |
filtered_data = merged_data[(merged_data['ISO_TIME']>=start_date) & (merged_data['ISO_TIME']<=end_date)].copy()
|
|
@@ -765,7 +1001,7 @@ def get_wind_analysis(start_year, start_month, end_year, end_month, enso_phase,
|
|
| 765 |
hover_data=['NAME','Year','Category'],
|
| 766 |
title='Wind Speed vs ONI',
|
| 767 |
labels={'ONI':'ONI Value','USA_WIND':'Max Wind Speed (knots)'},
|
| 768 |
-
color_discrete_map=
|
| 769 |
|
| 770 |
if typhoon_search:
|
| 771 |
mask = filtered_data['NAME'].str.contains(typhoon_search, case=False, na=False)
|
|
@@ -781,7 +1017,7 @@ def get_wind_analysis(start_year, start_month, end_year, end_month, enso_phase,
|
|
| 781 |
return fig, regression
|
| 782 |
|
| 783 |
def get_pressure_analysis(start_year, start_month, end_year, end_month, enso_phase, typhoon_search):
|
| 784 |
-
"""Get pressure analysis"""
|
| 785 |
start_date = datetime(start_year, start_month, 1)
|
| 786 |
end_date = datetime(end_year, end_month, 28)
|
| 787 |
filtered_data = merged_data[(merged_data['ISO_TIME']>=start_date) & (merged_data['ISO_TIME']<=end_date)].copy()
|
|
@@ -793,7 +1029,7 @@ def get_pressure_analysis(start_year, start_month, end_year, end_month, enso_pha
|
|
| 793 |
hover_data=['NAME','Year','Category'],
|
| 794 |
title='Pressure vs ONI',
|
| 795 |
labels={'ONI':'ONI Value','USA_PRES':'Min Pressure (hPa)'},
|
| 796 |
-
color_discrete_map=
|
| 797 |
|
| 798 |
if typhoon_search:
|
| 799 |
mask = filtered_data['NAME'].str.contains(typhoon_search, case=False, na=False)
|
|
@@ -838,7 +1074,7 @@ def get_longitude_analysis(start_year, start_month, end_year, end_month, enso_ph
|
|
| 838 |
return fig, slopes_text, regression
|
| 839 |
|
| 840 |
def categorize_typhoon_by_standard(wind_speed, standard='atlantic'):
|
| 841 |
-
"""Categorize typhoon by standard"""
|
| 842 |
if pd.isna(wind_speed):
|
| 843 |
return 'Tropical Depression', '#808080'
|
| 844 |
|
|
@@ -867,155 +1103,214 @@ def categorize_typhoon_by_standard(wind_speed, standard='atlantic'):
|
|
| 867 |
return 'Tropical Depression', atlantic_standard['Tropical Depression']['hex']
|
| 868 |
|
| 869 |
# -----------------------------
|
| 870 |
-
# Animation Functions
|
| 871 |
# -----------------------------
|
| 872 |
|
| 873 |
-
def
|
| 874 |
-
"""
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
storm_df = storm_df.sort_values('ISO_TIME')
|
| 881 |
-
lats = storm_df['LAT'].astype(float).values
|
| 882 |
-
lons = storm_df['LON'].astype(float).values
|
| 883 |
-
times = pd.to_datetime(storm_df['ISO_TIME']).values
|
| 884 |
-
|
| 885 |
-
if 'USA_WIND' in storm_df.columns:
|
| 886 |
-
winds = pd.to_numeric(storm_df['USA_WIND'], errors='coerce').values
|
| 887 |
else:
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
storm_name = storm_df['NAME'].iloc[0] if pd.notnull(storm_df['NAME'].iloc[0]) else "Unnamed"
|
| 891 |
-
basin = storm_df['SID'].iloc[0][:2]
|
| 892 |
-
season = storm_df['SEASON'].iloc[0] if 'SEASON' in storm_df.columns else year
|
| 893 |
-
|
| 894 |
-
min_lat, max_lat = np.min(lats), np.max(lats)
|
| 895 |
-
min_lon, max_lon = np.min(lons), np.max(lons)
|
| 896 |
-
lat_padding = max((max_lat - min_lat)*0.3, 5)
|
| 897 |
-
lon_padding = max((max_lon - min_lon)*0.3, 5)
|
| 898 |
-
|
| 899 |
-
fig = plt.figure(figsize=(12,6), dpi=100)
|
| 900 |
-
ax = plt.axes([0.05, 0.05, 0.60, 0.85],
|
| 901 |
-
projection=ccrs.PlateCarree(central_longitude=180))
|
| 902 |
-
ax.stock_img()
|
| 903 |
-
ax.set_extent([min_lon - lon_padding, max_lon + lon_padding, min_lat - lat_padding, max_lat + lat_padding],
|
| 904 |
-
crs=ccrs.PlateCarree())
|
| 905 |
-
ax.coastlines(resolution='50m', color='black', linewidth=1)
|
| 906 |
-
gl = ax.gridlines(draw_labels=True, color='gray', alpha=0.4, linestyle='--')
|
| 907 |
-
gl.top_labels = gl.right_labels = False
|
| 908 |
-
ax.set_title(f"{year} {storm_name} ({basin}) - {season}", fontsize=14)
|
| 909 |
-
|
| 910 |
-
line, = ax.plot([], [], transform=ccrs.PlateCarree(), color='blue', linewidth=2)
|
| 911 |
-
point, = ax.plot([], [], 'o', markersize=8, transform=ccrs.PlateCarree())
|
| 912 |
-
date_text = ax.text(0.02, 0.02, '', transform=ax.transAxes, fontsize=10,
|
| 913 |
-
bbox=dict(facecolor='white', alpha=0.8))
|
| 914 |
-
storm_info_text = fig.text(0.70, 0.60, '', fontsize=10,
|
| 915 |
-
bbox=dict(facecolor='white', alpha=0.8, boxstyle='round,pad=0.5'))
|
| 916 |
-
|
| 917 |
-
from matplotlib.lines import Line2D
|
| 918 |
-
standard_dict = atlantic_standard if standard=='atlantic' else taiwan_standard
|
| 919 |
-
legend_elements = [Line2D([0],[0], marker='o', color='w', label=cat,
|
| 920 |
-
markerfacecolor=details['hex'], markersize=8)
|
| 921 |
-
for cat, details in standard_dict.items()]
|
| 922 |
-
ax.legend(handles=legend_elements, title="Storm Categories",
|
| 923 |
-
loc='upper right', fontsize=9)
|
| 924 |
-
|
| 925 |
-
def init():
|
| 926 |
-
line.set_data([], [])
|
| 927 |
-
point.set_data([], [])
|
| 928 |
-
date_text.set_text('')
|
| 929 |
-
storm_info_text.set_text('')
|
| 930 |
-
return line, point, date_text, storm_info_text
|
| 931 |
-
|
| 932 |
-
def update(frame):
|
| 933 |
-
line.set_data(lons[:frame+1], lats[:frame+1])
|
| 934 |
-
point.set_data([lons[frame]], [lats[frame]])
|
| 935 |
-
wind_speed = winds[frame] if frame < len(winds) and not pd.isna(winds[frame]) else 0
|
| 936 |
-
category, color = categorize_typhoon_by_standard(wind_speed, standard)
|
| 937 |
-
point.set_color(color)
|
| 938 |
-
dt_str = pd.to_datetime(times[frame]).strftime('%Y-%m-%d %H:%M')
|
| 939 |
-
date_text.set_text(dt_str)
|
| 940 |
-
info_str = (f"Name: {storm_name}\nBasin: {basin}\nDate: {dt_str}\nWind: {wind_speed:.1f} kt\nCategory: {category}")
|
| 941 |
-
storm_info_text.set_text(info_str)
|
| 942 |
-
return line, point, date_text, storm_info_text
|
| 943 |
-
|
| 944 |
-
ani = animation.FuncAnimation(fig, update, init_func=init, frames=len(times),
|
| 945 |
-
interval=200, blit=True, repeat=True)
|
| 946 |
|
| 947 |
-
|
| 948 |
-
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4', dir=DATA_PATH)
|
| 949 |
-
try:
|
| 950 |
-
writer = animation.FFMpegWriter(fps=5, bitrate=1800)
|
| 951 |
-
ani.save(temp_file.name, writer=writer)
|
| 952 |
-
plt.close(fig)
|
| 953 |
-
return temp_file.name
|
| 954 |
-
except Exception as e:
|
| 955 |
-
logging.error(f"Error creating animation: {e}")
|
| 956 |
-
plt.close(fig)
|
| 957 |
-
return None
|
| 958 |
|
| 959 |
-
def
|
| 960 |
-
"""
|
| 961 |
-
if not typhoon:
|
| 962 |
-
return None
|
| 963 |
-
storm_id = typhoon.split('(')[-1].strip(')')
|
| 964 |
-
return generate_track_video_from_csv(year, storm_id, standard)
|
| 965 |
-
|
| 966 |
-
# -----------------------------
|
| 967 |
-
# Update Typhoon Options Function
|
| 968 |
-
# -----------------------------
|
| 969 |
-
|
| 970 |
-
def update_typhoon_options_fixed(year, basin):
|
| 971 |
-
"""Fixed version of update_typhoon_options"""
|
| 972 |
try:
|
| 973 |
-
|
| 974 |
-
if typhoon_data is None or typhoon_data.empty:
|
| 975 |
-
logging.error("No typhoon data available")
|
| 976 |
-
return gr.update(choices=[], value=None)
|
| 977 |
|
| 978 |
-
# Filter by year
|
| 979 |
if 'ISO_TIME' in typhoon_data.columns:
|
| 980 |
-
|
| 981 |
elif 'SEASON' in typhoon_data.columns:
|
| 982 |
-
|
| 983 |
else:
|
| 984 |
-
# Fallback
|
| 985 |
-
|
|
|
|
|
|
|
| 986 |
|
|
|
|
| 987 |
if basin != "All Basins":
|
| 988 |
-
# Extract basin code
|
| 989 |
basin_code = basin.split(' - ')[0] if ' - ' in basin else basin[:2]
|
| 990 |
-
# Filter by basin
|
| 991 |
if 'SID' in year_data.columns:
|
| 992 |
year_data = year_data[year_data['SID'].str.startswith(basin_code, na=False)]
|
| 993 |
elif 'BASIN' in year_data.columns:
|
| 994 |
year_data = year_data[year_data['BASIN'] == basin_code]
|
| 995 |
|
| 996 |
if year_data.empty:
|
| 997 |
-
|
| 998 |
-
return gr.update(choices=[], value=None)
|
| 999 |
|
| 1000 |
-
# Get unique storms
|
| 1001 |
-
storms = year_data.groupby('SID').
|
| 1002 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1003 |
|
|
|
|
|
|
|
| 1004 |
for _, storm in storms.iterrows():
|
| 1005 |
-
name = storm.
|
| 1006 |
-
if pd.isna(name) or name == '' or name == 'UNNAMED':
|
| 1007 |
-
name = 'UNNAMED'
|
| 1008 |
sid = storm['SID']
|
| 1009 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1010 |
|
| 1011 |
if not options:
|
| 1012 |
-
return gr.update(choices=[], value=None)
|
| 1013 |
-
|
| 1014 |
return gr.update(choices=sorted(options), value=options[0])
|
| 1015 |
|
| 1016 |
except Exception as e:
|
| 1017 |
-
|
| 1018 |
-
return gr.update(choices=[], value=None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1019 |
|
| 1020 |
# -----------------------------
|
| 1021 |
# Load & Process Data
|
|
@@ -1064,38 +1359,131 @@ def initialize_data():
|
|
| 1064 |
initialize_data()
|
| 1065 |
|
| 1066 |
# -----------------------------
|
| 1067 |
-
#
|
| 1068 |
# -----------------------------
|
| 1069 |
|
| 1070 |
def create_interface():
|
| 1071 |
-
"""Create the Gradio interface
|
| 1072 |
try:
|
| 1073 |
-
|
| 1074 |
-
|
| 1075 |
-
gr.Markdown("
|
| 1076 |
|
| 1077 |
-
with gr.Tab("Overview"):
|
| 1078 |
gr.Markdown(f"""
|
| 1079 |
-
## Welcome to the Typhoon Analysis Dashboard
|
| 1080 |
|
| 1081 |
-
This dashboard
|
| 1082 |
|
| 1083 |
-
### Features:
|
| 1084 |
-
- **
|
| 1085 |
-
- **
|
| 1086 |
-
- **
|
| 1087 |
-
- **
|
| 1088 |
-
- **
|
| 1089 |
|
| 1090 |
-
### Data Status:
|
| 1091 |
- **ONI Data**: {len(oni_data)} years loaded
|
| 1092 |
- **Typhoon Data**: {len(typhoon_data)} records loaded
|
| 1093 |
- **Merged Data**: {len(merged_data)} typhoons with ONI values
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1094 |
""")
|
| 1095 |
|
| 1096 |
-
with gr.Tab("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1097 |
with gr.Row():
|
| 1098 |
-
start_year = gr.Number(label="Start Year", value=
|
| 1099 |
start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
|
| 1100 |
end_year = gr.Number(label="End Year", value=2025)
|
| 1101 |
end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
|
|
@@ -1110,9 +1498,9 @@ def create_interface():
|
|
| 1110 |
outputs=[tracks_plot, typhoon_count]
|
| 1111 |
)
|
| 1112 |
|
| 1113 |
-
with gr.Tab("Wind Analysis"):
|
| 1114 |
with gr.Row():
|
| 1115 |
-
wind_start_year = gr.Number(label="Start Year", value=
|
| 1116 |
wind_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
|
| 1117 |
wind_end_year = gr.Number(label="End Year", value=2024)
|
| 1118 |
wind_end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
|
|
@@ -1127,9 +1515,9 @@ def create_interface():
|
|
| 1127 |
outputs=[wind_scatter, wind_regression_results]
|
| 1128 |
)
|
| 1129 |
|
| 1130 |
-
with gr.Tab("Pressure Analysis"):
|
| 1131 |
with gr.Row():
|
| 1132 |
-
pressure_start_year = gr.Number(label="Start Year", value=
|
| 1133 |
pressure_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
|
| 1134 |
pressure_end_year = gr.Number(label="End Year", value=2024)
|
| 1135 |
pressure_end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
|
|
@@ -1144,11 +1532,11 @@ def create_interface():
|
|
| 1144 |
outputs=[pressure_scatter, pressure_regression_results]
|
| 1145 |
)
|
| 1146 |
|
| 1147 |
-
with gr.Tab("Longitude Analysis"):
|
| 1148 |
with gr.Row():
|
| 1149 |
-
lon_start_year = gr.Number(label="Start Year", value=
|
| 1150 |
lon_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
|
| 1151 |
-
lon_end_year = gr.Number(label="End Year", value=
|
| 1152 |
lon_end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
|
| 1153 |
lon_enso_phase = gr.Dropdown(label="ENSO Phase", choices=['all', 'El Nino', 'La Nina', 'Neutral'], value='all')
|
| 1154 |
lon_typhoon_search = gr.Textbox(label="Typhoon Search (Optional)")
|
|
@@ -1162,41 +1550,64 @@ def create_interface():
|
|
| 1162 |
outputs=[regression_plot, slopes_text, lon_regression_results]
|
| 1163 |
)
|
| 1164 |
|
| 1165 |
-
with gr.Tab("
|
|
|
|
|
|
|
| 1166 |
with gr.Row():
|
| 1167 |
-
year_dropdown = gr.Dropdown(
|
| 1168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1169 |
with gr.Row():
|
| 1170 |
-
typhoon_dropdown = gr.Dropdown(label="
|
| 1171 |
-
standard_dropdown = gr.Dropdown(
|
| 1172 |
-
|
| 1173 |
-
|
| 1174 |
-
|
| 1175 |
-
|
| 1176 |
-
|
| 1177 |
-
|
| 1178 |
-
|
| 1179 |
-
|
| 1180 |
-
|
| 1181 |
-
|
| 1182 |
-
|
| 1183 |
-
|
| 1184 |
-
|
| 1185 |
-
|
| 1186 |
-
|
| 1187 |
-
|
| 1188 |
-
|
| 1189 |
-
|
| 1190 |
-
|
| 1191 |
-
|
|
|
|
| 1192 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1193 |
|
| 1194 |
return demo
|
| 1195 |
except Exception as e:
|
| 1196 |
logging.error(f"Error creating Gradio interface: {e}")
|
| 1197 |
# Create a minimal fallback interface
|
| 1198 |
with gr.Blocks() as demo:
|
| 1199 |
-
gr.Markdown("# Typhoon Analysis
|
| 1200 |
gr.Markdown("**Error**: Could not load full interface. Please check logs.")
|
| 1201 |
return demo
|
| 1202 |
|
|
|
|
| 20 |
from plotly.subplots import make_subplots
|
| 21 |
|
| 22 |
from sklearn.manifold import TSNE
|
| 23 |
+
from sklearn.cluster import DBSCAN, KMeans
|
| 24 |
from sklearn.preprocessing import StandardScaler
|
| 25 |
+
from sklearn.decomposition import PCA
|
| 26 |
from scipy.interpolate import interp1d
|
| 27 |
import statsmodels.api as sm
|
| 28 |
import requests
|
|
|
|
| 30 |
import shutil
|
| 31 |
import xarray as xr
|
| 32 |
|
| 33 |
+
# NEW: Advanced ML imports
|
| 34 |
+
try:
|
| 35 |
+
import umap.umap_ as umap
|
| 36 |
+
UMAP_AVAILABLE = True
|
| 37 |
+
except ImportError:
|
| 38 |
+
UMAP_AVAILABLE = False
|
| 39 |
+
print("UMAP not available - clustering features limited")
|
| 40 |
+
|
| 41 |
+
# Optional CNN imports
|
| 42 |
+
try:
|
| 43 |
+
import tensorflow as tf
|
| 44 |
+
from tensorflow.keras import layers, models
|
| 45 |
+
CNN_AVAILABLE = True
|
| 46 |
+
except ImportError:
|
| 47 |
+
CNN_AVAILABLE = False
|
| 48 |
+
print("TensorFlow not available - CNN features disabled")
|
| 49 |
+
|
| 50 |
try:
|
| 51 |
import cdsapi
|
| 52 |
CDSAPI_AVAILABLE = True
|
|
|
|
| 97 |
CACHE_EXPIRY_DAYS = 1
|
| 98 |
|
| 99 |
# -----------------------------
|
| 100 |
+
# ENHANCED: Color Maps and Standards with TD Support
|
| 101 |
# -----------------------------
|
| 102 |
+
enhanced_color_map = {
|
| 103 |
+
'Unknown': 'rgb(200, 200, 200)',
|
| 104 |
+
'Tropical Depression': 'rgb(128, 128, 128)', # NEW: Gray for TD
|
| 105 |
+
'Tropical Storm': 'rgb(0, 0, 255)',
|
| 106 |
+
'C1 Typhoon': 'rgb(0, 255, 255)',
|
| 107 |
+
'C2 Typhoon': 'rgb(0, 255, 0)',
|
| 108 |
+
'C3 Strong Typhoon': 'rgb(255, 255, 0)',
|
| 109 |
+
'C4 Very Strong Typhoon': 'rgb(255, 165, 0)',
|
| 110 |
+
'C5 Super Typhoon': 'rgb(255, 0, 0)'
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
# Original color map for backward compatibility
|
| 114 |
color_map = {
|
| 115 |
'C5 Super Typhoon': 'rgb(255, 0, 0)',
|
| 116 |
'C4 Very Strong Typhoon': 'rgb(255, 165, 0)',
|
|
|
|
| 120 |
'Tropical Storm': 'rgb(0, 0, 255)',
|
| 121 |
'Tropical Depression': 'rgb(128, 128, 128)'
|
| 122 |
}
|
| 123 |
+
|
| 124 |
atlantic_standard = {
|
| 125 |
'C5 Super Typhoon': {'wind_speed': 137, 'color': 'Red', 'hex': '#FF0000'},
|
| 126 |
'C4 Very Strong Typhoon': {'wind_speed': 113, 'color': 'Orange', 'hex': '#FFA500'},
|
|
|
|
| 130 |
'Tropical Storm': {'wind_speed': 34, 'color': 'Blue', 'hex': '#0000FF'},
|
| 131 |
'Tropical Depression': {'wind_speed': 0, 'color': 'Gray', 'hex': '#808080'}
|
| 132 |
}
|
| 133 |
+
|
| 134 |
taiwan_standard = {
|
| 135 |
'Strong Typhoon': {'wind_speed': 51.0, 'color': 'Red', 'hex': '#FF0000'},
|
| 136 |
'Medium Typhoon': {'wind_speed': 33.7, 'color': 'Orange', 'hex': '#FFA500'},
|
|
|
|
| 280 |
|
| 281 |
def create_fallback_oni_data(output_file):
|
| 282 |
"""Create minimal ONI data for testing"""
|
| 283 |
+
years = range(2000, 2026) # Extended to include 2025
|
| 284 |
months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']
|
| 285 |
|
| 286 |
# Create synthetic ONI data
|
|
|
|
| 559 |
def create_fallback_typhoon_data():
|
| 560 |
"""Create minimal fallback typhoon data - FIXED VERSION"""
|
| 561 |
# Use proper pandas date_range instead of numpy
|
| 562 |
+
dates = pd.date_range(start='2000-01-01', end='2025-12-31', freq='D') # Extended to 2025
|
| 563 |
storm_dates = dates[np.random.choice(len(dates), size=100, replace=False)]
|
| 564 |
|
| 565 |
data = []
|
|
|
|
| 627 |
typhoon_max['Month'] = '01'
|
| 628 |
typhoon_max['Year'] = typhoon_max['SEASON']
|
| 629 |
|
| 630 |
+
typhoon_max['Category'] = typhoon_max['USA_WIND'].apply(categorize_typhoon_enhanced)
|
| 631 |
return typhoon_max
|
| 632 |
|
| 633 |
def merge_data(oni_long, typhoon_max):
|
| 634 |
"""Merge ONI and typhoon data"""
|
| 635 |
return pd.merge(typhoon_max, oni_long, on=['Year','Month'])
|
| 636 |
|
| 637 |
+
# -----------------------------
|
| 638 |
+
# ENHANCED: Categorization Functions
|
| 639 |
+
# -----------------------------
|
| 640 |
+
|
| 641 |
+
def categorize_typhoon_enhanced(wind_speed):
|
| 642 |
+
"""Enhanced categorization that properly includes Tropical Depressions"""
|
| 643 |
if pd.isna(wind_speed):
|
| 644 |
+
return 'Unknown'
|
| 645 |
+
|
| 646 |
+
# Convert to knots if in m/s (some datasets use m/s)
|
| 647 |
+
if wind_speed < 10: # Likely in m/s, convert to knots
|
| 648 |
+
wind_speed = wind_speed * 1.94384
|
| 649 |
+
|
| 650 |
+
# FIXED thresholds to include TD
|
| 651 |
+
if wind_speed < 34: # Below 34 knots = Tropical Depression
|
| 652 |
return 'Tropical Depression'
|
| 653 |
+
elif wind_speed < 64: # 34-63 knots = Tropical Storm
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
return 'Tropical Storm'
|
| 655 |
+
elif wind_speed < 83: # 64-82 knots = Category 1 Typhoon
|
| 656 |
+
return 'C1 Typhoon'
|
| 657 |
+
elif wind_speed < 96: # 83-95 knots = Category 2 Typhoon
|
| 658 |
+
return 'C2 Typhoon'
|
| 659 |
+
elif wind_speed < 113: # 96-112 knots = Category 3 Strong Typhoon
|
| 660 |
+
return 'C3 Strong Typhoon'
|
| 661 |
+
elif wind_speed < 137: # 113-136 knots = Category 4 Very Strong Typhoon
|
| 662 |
+
return 'C4 Very Strong Typhoon'
|
| 663 |
+
else: # 137+ knots = Category 5 Super Typhoon
|
| 664 |
+
return 'C5 Super Typhoon'
|
| 665 |
+
|
| 666 |
+
# Original function for backward compatibility
|
| 667 |
+
def categorize_typhoon(wind_speed):
|
| 668 |
+
"""Original categorize typhoon function for backward compatibility"""
|
| 669 |
+
return categorize_typhoon_enhanced(wind_speed)
|
| 670 |
|
| 671 |
def classify_enso_phases(oni_value):
|
| 672 |
"""Classify ENSO phases based on ONI value"""
|
|
|
|
| 682 |
return 'Neutral'
|
| 683 |
|
| 684 |
# -----------------------------
|
| 685 |
+
# NEW: Advanced ML Features
|
| 686 |
+
# -----------------------------
|
| 687 |
+
|
| 688 |
+
def extract_storm_features(typhoon_data):
|
| 689 |
+
"""Extract features for clustering analysis"""
|
| 690 |
+
# Group by storm ID to get storm-level features
|
| 691 |
+
storm_features = typhoon_data.groupby('SID').agg({
|
| 692 |
+
'USA_WIND': ['max', 'mean', 'std'],
|
| 693 |
+
'USA_PRES': ['min', 'mean', 'std'],
|
| 694 |
+
'LAT': ['mean', 'std', 'max', 'min'],
|
| 695 |
+
'LON': ['mean', 'std', 'max', 'min'],
|
| 696 |
+
'ISO_TIME': ['count'] # Track length
|
| 697 |
+
}).reset_index()
|
| 698 |
+
|
| 699 |
+
# Flatten column names
|
| 700 |
+
storm_features.columns = ['SID'] + ['_'.join(col).strip() for col in storm_features.columns[1:]]
|
| 701 |
+
|
| 702 |
+
# Add additional computed features
|
| 703 |
+
storm_features['lat_range'] = storm_features['LAT_max'] - storm_features['LAT_min']
|
| 704 |
+
storm_features['lon_range'] = storm_features['LON_max'] - storm_features['LON_min']
|
| 705 |
+
storm_features['track_length'] = storm_features['ISO_TIME_count']
|
| 706 |
+
|
| 707 |
+
# Add genesis location features
|
| 708 |
+
genesis_data = typhoon_data.groupby('SID').first()[['LAT', 'LON', 'USA_WIND']]
|
| 709 |
+
genesis_data.columns = ['genesis_lat', 'genesis_lon', 'genesis_intensity']
|
| 710 |
+
storm_features = storm_features.merge(genesis_data, on='SID', how='left')
|
| 711 |
+
|
| 712 |
+
return storm_features
|
| 713 |
+
|
| 714 |
+
def perform_dimensionality_reduction(storm_features, method='umap', n_components=2):
|
| 715 |
+
"""Perform UMAP or t-SNE dimensionality reduction"""
|
| 716 |
+
# Select numeric features for clustering
|
| 717 |
+
feature_cols = [col for col in storm_features.columns if col != 'SID' and storm_features[col].dtype in ['float64', 'int64']]
|
| 718 |
+
X = storm_features[feature_cols].fillna(0)
|
| 719 |
+
|
| 720 |
+
# Standardize features
|
| 721 |
+
scaler = StandardScaler()
|
| 722 |
+
X_scaled = scaler.fit_transform(X)
|
| 723 |
+
|
| 724 |
+
if method.lower() == 'umap' and UMAP_AVAILABLE:
|
| 725 |
+
# UMAP parameters optimized for typhoon data
|
| 726 |
+
reducer = umap.UMAP(
|
| 727 |
+
n_components=n_components,
|
| 728 |
+
n_neighbors=15,
|
| 729 |
+
min_dist=0.1,
|
| 730 |
+
metric='euclidean',
|
| 731 |
+
random_state=42
|
| 732 |
+
)
|
| 733 |
+
elif method.lower() == 'tsne':
|
| 734 |
+
# t-SNE parameters
|
| 735 |
+
reducer = TSNE(
|
| 736 |
+
n_components=n_components,
|
| 737 |
+
perplexity=min(30, len(X_scaled)//4),
|
| 738 |
+
learning_rate=200,
|
| 739 |
+
n_iter=1000,
|
| 740 |
+
random_state=42
|
| 741 |
+
)
|
| 742 |
+
else:
|
| 743 |
+
# Fallback to PCA if UMAP not available
|
| 744 |
+
reducer = PCA(n_components=n_components, random_state=42)
|
| 745 |
+
|
| 746 |
+
# Fit and transform
|
| 747 |
+
embedding = reducer.fit_transform(X_scaled)
|
| 748 |
+
|
| 749 |
+
return embedding, feature_cols, scaler
|
| 750 |
+
|
| 751 |
+
def cluster_storms(embedding, method='dbscan'):
|
| 752 |
+
"""Cluster storms based on their embedding"""
|
| 753 |
+
if method.lower() == 'dbscan':
|
| 754 |
+
clusterer = DBSCAN(eps=0.5, min_samples=5)
|
| 755 |
+
elif method.lower() == 'kmeans':
|
| 756 |
+
clusterer = KMeans(n_clusters=5, random_state=42)
|
| 757 |
+
else:
|
| 758 |
+
raise ValueError("Method must be 'dbscan' or 'kmeans'")
|
| 759 |
+
|
| 760 |
+
clusters = clusterer.fit_predict(embedding)
|
| 761 |
+
return clusters
|
| 762 |
+
|
| 763 |
+
def create_clustering_visualization(storm_features, typhoon_data, method='umap'):
|
| 764 |
+
"""Create interactive clustering visualization"""
|
| 765 |
+
try:
|
| 766 |
+
# Perform dimensionality reduction
|
| 767 |
+
embedding, feature_cols, scaler = perform_dimensionality_reduction(storm_features, method)
|
| 768 |
+
|
| 769 |
+
# Perform clustering
|
| 770 |
+
clusters = cluster_storms(embedding, 'dbscan')
|
| 771 |
+
|
| 772 |
+
# Add clustering results to storm features
|
| 773 |
+
storm_features_viz = storm_features.copy()
|
| 774 |
+
storm_features_viz['cluster'] = clusters
|
| 775 |
+
storm_features_viz['dim1'] = embedding[:, 0]
|
| 776 |
+
storm_features_viz['dim2'] = embedding[:, 1]
|
| 777 |
+
|
| 778 |
+
# Merge with typhoon data for additional info
|
| 779 |
+
storm_info = typhoon_data.groupby('SID').first()[['NAME', 'SEASON']].reset_index()
|
| 780 |
+
storm_features_viz = storm_features_viz.merge(storm_info, on='SID', how='left')
|
| 781 |
+
|
| 782 |
+
# Create interactive plot
|
| 783 |
+
fig = px.scatter(
|
| 784 |
+
storm_features_viz,
|
| 785 |
+
x='dim1',
|
| 786 |
+
y='dim2',
|
| 787 |
+
color='cluster',
|
| 788 |
+
hover_data=['NAME', 'SEASON', 'USA_WIND_max', 'USA_PRES_min'],
|
| 789 |
+
title=f'Storm Clustering using {method.upper()}',
|
| 790 |
+
labels={
|
| 791 |
+
'dim1': f'{method.upper()} Dimension 1',
|
| 792 |
+
'dim2': f'{method.upper()} Dimension 2',
|
| 793 |
+
'cluster': 'Cluster'
|
| 794 |
+
}
|
| 795 |
+
)
|
| 796 |
+
|
| 797 |
+
# Add cluster statistics
|
| 798 |
+
cluster_stats = storm_features_viz.groupby('cluster').agg({
|
| 799 |
+
'USA_WIND_max': 'mean',
|
| 800 |
+
'USA_PRES_min': 'mean',
|
| 801 |
+
'track_length': 'mean',
|
| 802 |
+
'SID': 'count'
|
| 803 |
+
}).round(2)
|
| 804 |
+
|
| 805 |
+
stats_text = "Cluster Statistics:\n"
|
| 806 |
+
for cluster, stats in cluster_stats.iterrows():
|
| 807 |
+
if cluster != -1: # Skip noise points in DBSCAN
|
| 808 |
+
stats_text += f"Cluster {cluster}: {stats['SID']} storms, avg max wind: {stats['USA_WIND_max']} kt\n"
|
| 809 |
+
|
| 810 |
+
return fig, stats_text, storm_features_viz
|
| 811 |
+
except Exception as e:
|
| 812 |
+
return None, f"Error in clustering: {str(e)}", None
|
| 813 |
+
|
| 814 |
+
# -----------------------------
|
| 815 |
+
# NEW: Optional CNN Implementation
|
| 816 |
+
# -----------------------------
|
| 817 |
+
|
| 818 |
+
def create_cnn_model(input_shape=(64, 64, 3)):
|
| 819 |
+
"""Create CNN model for typhoon intensity prediction from satellite images"""
|
| 820 |
+
if not CNN_AVAILABLE:
|
| 821 |
+
return None
|
| 822 |
+
|
| 823 |
+
model = models.Sequential([
|
| 824 |
+
# Convolutional layers
|
| 825 |
+
layers.Conv2D(32, (3, 3), activation='relu', input_shape=input_shape),
|
| 826 |
+
layers.MaxPooling2D((2, 2)),
|
| 827 |
+
layers.Conv2D(64, (3, 3), activation='relu'),
|
| 828 |
+
layers.MaxPooling2D((2, 2)),
|
| 829 |
+
layers.Conv2D(64, (3, 3), activation='relu'),
|
| 830 |
+
layers.MaxPooling2D((2, 2)),
|
| 831 |
+
|
| 832 |
+
# Dense layers
|
| 833 |
+
layers.Flatten(),
|
| 834 |
+
layers.Dense(64, activation='relu'),
|
| 835 |
+
layers.Dropout(0.5),
|
| 836 |
+
layers.Dense(32, activation='relu'),
|
| 837 |
+
|
| 838 |
+
# Output layer for intensity prediction
|
| 839 |
+
layers.Dense(1, activation='linear') # Regression for wind speed
|
| 840 |
+
])
|
| 841 |
+
|
| 842 |
+
model.compile(
|
| 843 |
+
optimizer='adam',
|
| 844 |
+
loss='mean_squared_error',
|
| 845 |
+
metrics=['mae']
|
| 846 |
+
)
|
| 847 |
+
|
| 848 |
+
return model
|
| 849 |
+
|
| 850 |
+
def simulate_cnn_prediction(lat, lon, month, oni_value):
|
| 851 |
+
"""Simulate CNN prediction (placeholder for actual satellite image input)"""
|
| 852 |
+
if not CNN_AVAILABLE:
|
| 853 |
+
return None, "CNN not available - TensorFlow not installed"
|
| 854 |
+
|
| 855 |
+
# This would normally process satellite imagery
|
| 856 |
+
# For demo purposes, we'll use a simple heuristic
|
| 857 |
+
|
| 858 |
+
# Simulate environmental factors
|
| 859 |
+
sst_anomaly = oni_value * 0.5 # Simplified SST relationship
|
| 860 |
+
seasonal_factor = 1.2 if month in [7, 8, 9, 10] else 0.8
|
| 861 |
+
latitude_factor = max(0.5, (30 - abs(lat)) / 30) if abs(lat) < 30 else 0.1
|
| 862 |
+
|
| 863 |
+
# Simple intensity prediction
|
| 864 |
+
base_intensity = 40
|
| 865 |
+
intensity = base_intensity + sst_anomaly * 10 + seasonal_factor * 20 + latitude_factor * 30
|
| 866 |
+
intensity = max(0, min(180, intensity)) # Clamp to reasonable range
|
| 867 |
+
|
| 868 |
+
confidence = 0.75 + np.random.normal(0, 0.1)
|
| 869 |
+
confidence = max(0.5, min(0.95, confidence))
|
| 870 |
+
|
| 871 |
+
return intensity, f"Predicted Intensity: {intensity:.1f} kt (Confidence: {confidence:.1%})"
|
| 872 |
+
|
| 873 |
+
# -----------------------------
|
| 874 |
+
# Regression Functions (Original)
|
| 875 |
# -----------------------------
|
| 876 |
|
| 877 |
def perform_wind_regression(start_year, start_month, end_year, end_month):
|
|
|
|
| 926 |
return f"Longitude Regression Error: {e}"
|
| 927 |
|
| 928 |
# -----------------------------
|
| 929 |
+
# Visualization Functions (Enhanced)
|
| 930 |
# -----------------------------
|
| 931 |
|
| 932 |
def get_full_tracks(start_year, start_month, end_year, end_month, enso_phase, typhoon_search):
|
|
|
|
| 989 |
return fig, f"Total typhoons displayed: {count}"
|
| 990 |
|
| 991 |
def get_wind_analysis(start_year, start_month, end_year, end_month, enso_phase, typhoon_search):
|
| 992 |
+
"""Get wind analysis with enhanced categorization"""
|
| 993 |
start_date = datetime(start_year, start_month, 1)
|
| 994 |
end_date = datetime(end_year, end_month, 28)
|
| 995 |
filtered_data = merged_data[(merged_data['ISO_TIME']>=start_date) & (merged_data['ISO_TIME']<=end_date)].copy()
|
|
|
|
| 1001 |
hover_data=['NAME','Year','Category'],
|
| 1002 |
title='Wind Speed vs ONI',
|
| 1003 |
labels={'ONI':'ONI Value','USA_WIND':'Max Wind Speed (knots)'},
|
| 1004 |
+
color_discrete_map=enhanced_color_map)
|
| 1005 |
|
| 1006 |
if typhoon_search:
|
| 1007 |
mask = filtered_data['NAME'].str.contains(typhoon_search, case=False, na=False)
|
|
|
|
| 1017 |
return fig, regression
|
| 1018 |
|
| 1019 |
def get_pressure_analysis(start_year, start_month, end_year, end_month, enso_phase, typhoon_search):
|
| 1020 |
+
"""Get pressure analysis with enhanced categorization"""
|
| 1021 |
start_date = datetime(start_year, start_month, 1)
|
| 1022 |
end_date = datetime(end_year, end_month, 28)
|
| 1023 |
filtered_data = merged_data[(merged_data['ISO_TIME']>=start_date) & (merged_data['ISO_TIME']<=end_date)].copy()
|
|
|
|
| 1029 |
hover_data=['NAME','Year','Category'],
|
| 1030 |
title='Pressure vs ONI',
|
| 1031 |
labels={'ONI':'ONI Value','USA_PRES':'Min Pressure (hPa)'},
|
| 1032 |
+
color_discrete_map=enhanced_color_map)
|
| 1033 |
|
| 1034 |
if typhoon_search:
|
| 1035 |
mask = filtered_data['NAME'].str.contains(typhoon_search, case=False, na=False)
|
|
|
|
| 1074 |
return fig, slopes_text, regression
|
| 1075 |
|
| 1076 |
def categorize_typhoon_by_standard(wind_speed, standard='atlantic'):
|
| 1077 |
+
"""Categorize typhoon by standard with enhanced TD support"""
|
| 1078 |
if pd.isna(wind_speed):
|
| 1079 |
return 'Tropical Depression', '#808080'
|
| 1080 |
|
|
|
|
| 1103 |
return 'Tropical Depression', atlantic_standard['Tropical Depression']['hex']
|
| 1104 |
|
| 1105 |
# -----------------------------
|
| 1106 |
+
# ENHANCED: Animation Functions
|
| 1107 |
# -----------------------------
|
| 1108 |
|
| 1109 |
+
def get_available_years(typhoon_data):
|
| 1110 |
+
"""Get all available years including 2025"""
|
| 1111 |
+
if 'ISO_TIME' in typhoon_data.columns:
|
| 1112 |
+
years = typhoon_data['ISO_TIME'].dt.year.unique()
|
| 1113 |
+
elif 'SEASON' in typhoon_data.columns:
|
| 1114 |
+
years = typhoon_data['SEASON'].unique()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1115 |
else:
|
| 1116 |
+
years = range(1980, 2026) # Default range including 2025
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1117 |
|
| 1118 |
+
return sorted([str(year) for year in years if not pd.isna(year)])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1119 |
|
| 1120 |
+
def update_typhoon_options_enhanced(year, basin):
|
| 1121 |
+
"""Enhanced typhoon options with TD support and 2025 data"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1122 |
try:
|
| 1123 |
+
year = int(year)
|
|
|
|
|
|
|
|
|
|
| 1124 |
|
| 1125 |
+
# Filter by year - handle both ISO_TIME and SEASON columns
|
| 1126 |
if 'ISO_TIME' in typhoon_data.columns:
|
| 1127 |
+
year_mask = typhoon_data['ISO_TIME'].dt.year == year
|
| 1128 |
elif 'SEASON' in typhoon_data.columns:
|
| 1129 |
+
year_mask = typhoon_data['SEASON'] == year
|
| 1130 |
else:
|
| 1131 |
+
# Fallback - try to extract year from SID or other fields
|
| 1132 |
+
year_mask = typhoon_data.index >= 0 # Include all data as fallback
|
| 1133 |
+
|
| 1134 |
+
year_data = typhoon_data[year_mask].copy()
|
| 1135 |
|
| 1136 |
+
# Filter by basin if specified
|
| 1137 |
if basin != "All Basins":
|
|
|
|
| 1138 |
basin_code = basin.split(' - ')[0] if ' - ' in basin else basin[:2]
|
|
|
|
| 1139 |
if 'SID' in year_data.columns:
|
| 1140 |
year_data = year_data[year_data['SID'].str.startswith(basin_code, na=False)]
|
| 1141 |
elif 'BASIN' in year_data.columns:
|
| 1142 |
year_data = year_data[year_data['BASIN'] == basin_code]
|
| 1143 |
|
| 1144 |
if year_data.empty:
|
| 1145 |
+
return gr.update(choices=["No storms found"], value=None)
|
|
|
|
| 1146 |
|
| 1147 |
+
# Get unique storms - include ALL intensities (including TD)
|
| 1148 |
+
storms = year_data.groupby('SID').agg({
|
| 1149 |
+
'NAME': 'first',
|
| 1150 |
+
'USA_WIND': 'max'
|
| 1151 |
+
}).reset_index()
|
| 1152 |
+
|
| 1153 |
+
# Enhanced categorization including TD
|
| 1154 |
+
storms['category'] = storms['USA_WIND'].apply(categorize_typhoon_enhanced)
|
| 1155 |
|
| 1156 |
+
# Create options with category information
|
| 1157 |
+
options = []
|
| 1158 |
for _, storm in storms.iterrows():
|
| 1159 |
+
name = storm['NAME'] if pd.notna(storm['NAME']) and storm['NAME'] != '' else 'UNNAMED'
|
|
|
|
|
|
|
| 1160 |
sid = storm['SID']
|
| 1161 |
+
category = storm['category']
|
| 1162 |
+
max_wind = storm['USA_WIND'] if pd.notna(storm['USA_WIND']) else 0
|
| 1163 |
+
|
| 1164 |
+
option = f"{name} ({sid}) - {category} ({max_wind:.0f}kt)"
|
| 1165 |
+
options.append(option)
|
| 1166 |
|
| 1167 |
if not options:
|
| 1168 |
+
return gr.update(choices=["No storms found"], value=None)
|
| 1169 |
+
|
| 1170 |
return gr.update(choices=sorted(options), value=options[0])
|
| 1171 |
|
| 1172 |
except Exception as e:
|
| 1173 |
+
print(f"Error in update_typhoon_options_enhanced: {e}")
|
| 1174 |
+
return gr.update(choices=["Error loading storms"], value=None)
|
| 1175 |
+
|
| 1176 |
+
def generate_enhanced_track_video(year, typhoon_selection, standard):
|
| 1177 |
+
"""Enhanced track video generation with TD support and 2025 compatibility"""
|
| 1178 |
+
if not typhoon_selection or typhoon_selection == "No storms found":
|
| 1179 |
+
return None
|
| 1180 |
+
|
| 1181 |
+
try:
|
| 1182 |
+
# Extract SID from selection
|
| 1183 |
+
sid = typhoon_selection.split('(')[1].split(')')[0]
|
| 1184 |
+
|
| 1185 |
+
# Get storm data
|
| 1186 |
+
storm_df = typhoon_data[typhoon_data['SID'] == sid].copy()
|
| 1187 |
+
if storm_df.empty:
|
| 1188 |
+
return None
|
| 1189 |
+
|
| 1190 |
+
# Sort by time
|
| 1191 |
+
if 'ISO_TIME' in storm_df.columns:
|
| 1192 |
+
storm_df = storm_df.sort_values('ISO_TIME')
|
| 1193 |
+
|
| 1194 |
+
# Extract data for animation
|
| 1195 |
+
lats = storm_df['LAT'].astype(float).values
|
| 1196 |
+
lons = storm_df['LON'].astype(float).values
|
| 1197 |
+
|
| 1198 |
+
if 'USA_WIND' in storm_df.columns:
|
| 1199 |
+
winds = pd.to_numeric(storm_df['USA_WIND'], errors='coerce').fillna(0).values
|
| 1200 |
+
else:
|
| 1201 |
+
winds = np.full(len(lats), 30) # Default TD strength
|
| 1202 |
+
|
| 1203 |
+
# Enhanced metadata
|
| 1204 |
+
storm_name = storm_df['NAME'].iloc[0] if pd.notna(storm_df['NAME'].iloc[0]) else "UNNAMED"
|
| 1205 |
+
season = storm_df['SEASON'].iloc[0] if 'SEASON' in storm_df.columns else year
|
| 1206 |
+
|
| 1207 |
+
# Create figure with enhanced map
|
| 1208 |
+
fig, ax = plt.subplots(figsize=(14, 8), subplot_kw={'projection': ccrs.PlateCarree()})
|
| 1209 |
+
|
| 1210 |
+
# Enhanced map features
|
| 1211 |
+
ax.stock_img()
|
| 1212 |
+
ax.add_feature(cfeature.COASTLINE, linewidth=0.8)
|
| 1213 |
+
ax.add_feature(cfeature.BORDERS, linewidth=0.5)
|
| 1214 |
+
ax.add_feature(cfeature.OCEAN, color='lightblue', alpha=0.5)
|
| 1215 |
+
ax.add_feature(cfeature.LAND, color='lightgray', alpha=0.5)
|
| 1216 |
+
|
| 1217 |
+
# Set extent based on track
|
| 1218 |
+
padding = 5
|
| 1219 |
+
ax.set_extent([
|
| 1220 |
+
min(lons) - padding, max(lons) + padding,
|
| 1221 |
+
min(lats) - padding, max(lats) + padding
|
| 1222 |
+
])
|
| 1223 |
+
|
| 1224 |
+
# Add gridlines
|
| 1225 |
+
gl = ax.gridlines(draw_labels=True, alpha=0.3)
|
| 1226 |
+
gl.top_labels = gl.right_labels = False
|
| 1227 |
+
|
| 1228 |
+
# Title with enhanced info
|
| 1229 |
+
ax.set_title(f"{season} {storm_name} ({sid}) Track Animation", fontsize=16, fontweight='bold')
|
| 1230 |
+
|
| 1231 |
+
# Animation elements
|
| 1232 |
+
line, = ax.plot([], [], 'b-', linewidth=3, alpha=0.7, label='Track')
|
| 1233 |
+
point, = ax.plot([], [], 'o', markersize=12)
|
| 1234 |
+
|
| 1235 |
+
# Enhanced info display
|
| 1236 |
+
info_box = ax.text(0.02, 0.98, '', transform=ax.transAxes,
|
| 1237 |
+
fontsize=11, verticalalignment='top',
|
| 1238 |
+
bbox=dict(boxstyle="round,pad=0.5", facecolor='white', alpha=0.9))
|
| 1239 |
+
|
| 1240 |
+
# Color legend with TD support
|
| 1241 |
+
legend_elements = []
|
| 1242 |
+
for category, color in enhanced_color_map.items():
|
| 1243 |
+
legend_elements.append(plt.Line2D([0], [0], marker='o', color='w',
|
| 1244 |
+
markerfacecolor=color, markersize=8, label=category))
|
| 1245 |
+
|
| 1246 |
+
ax.legend(handles=legend_elements, loc='upper right', fontsize=9)
|
| 1247 |
+
|
| 1248 |
+
def animate(frame):
|
| 1249 |
+
if frame >= len(lats):
|
| 1250 |
+
return line, point, info_box
|
| 1251 |
+
|
| 1252 |
+
# Update track line
|
| 1253 |
+
line.set_data(lons[:frame+1], lats[:frame+1])
|
| 1254 |
+
|
| 1255 |
+
# Update current position
|
| 1256 |
+
current_wind = winds[frame]
|
| 1257 |
+
category = categorize_typhoon_enhanced(current_wind)
|
| 1258 |
+
color = enhanced_color_map[category]
|
| 1259 |
+
|
| 1260 |
+
point.set_data([lons[frame]], [lats[frame]])
|
| 1261 |
+
point.set_color(color)
|
| 1262 |
+
point.set_markersize(8 + current_wind/10) # Size based on intensity
|
| 1263 |
+
|
| 1264 |
+
# Enhanced info display
|
| 1265 |
+
if 'ISO_TIME' in storm_df.columns:
|
| 1266 |
+
current_time = storm_df.iloc[frame]['ISO_TIME']
|
| 1267 |
+
time_str = current_time.strftime('%Y-%m-%d %H:%M UTC') if pd.notna(current_time) else 'Unknown'
|
| 1268 |
+
else:
|
| 1269 |
+
time_str = f"Step {frame+1}"
|
| 1270 |
+
|
| 1271 |
+
info_text = (
|
| 1272 |
+
f"Storm: {storm_name}\n"
|
| 1273 |
+
f"Time: {time_str}\n"
|
| 1274 |
+
f"Position: {lats[frame]:.1f}Β°N, {lons[frame]:.1f}Β°E\n"
|
| 1275 |
+
f"Max Wind: {current_wind:.0f} kt\n"
|
| 1276 |
+
f"Category: {category}\n"
|
| 1277 |
+
f"Frame: {frame+1}/{len(lats)}"
|
| 1278 |
+
)
|
| 1279 |
+
info_box.set_text(info_text)
|
| 1280 |
+
|
| 1281 |
+
return line, point, info_box
|
| 1282 |
+
|
| 1283 |
+
# Create animation
|
| 1284 |
+
anim = animation.FuncAnimation(
|
| 1285 |
+
fig, animate, frames=len(lats),
|
| 1286 |
+
interval=300, blit=False, repeat=True
|
| 1287 |
+
)
|
| 1288 |
+
|
| 1289 |
+
# Save animation
|
| 1290 |
+
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4',
|
| 1291 |
+
dir=tempfile.gettempdir())
|
| 1292 |
+
|
| 1293 |
+
# Enhanced writer settings
|
| 1294 |
+
writer = animation.FFMpegWriter(
|
| 1295 |
+
fps=4, bitrate=2000, codec='libx264',
|
| 1296 |
+
extra_args=['-pix_fmt', 'yuv420p'] # Better compatibility
|
| 1297 |
+
)
|
| 1298 |
+
|
| 1299 |
+
anim.save(temp_file.name, writer=writer, dpi=100)
|
| 1300 |
+
plt.close(fig)
|
| 1301 |
+
|
| 1302 |
+
return temp_file.name
|
| 1303 |
+
|
| 1304 |
+
except Exception as e:
|
| 1305 |
+
print(f"Error generating video: {e}")
|
| 1306 |
+
return None
|
| 1307 |
+
|
| 1308 |
+
# Simplified wrapper for backward compatibility
|
| 1309 |
+
def simplified_track_video(year, basin, typhoon, standard):
|
| 1310 |
+
"""Simplified track video function"""
|
| 1311 |
+
if not typhoon:
|
| 1312 |
+
return None
|
| 1313 |
+
return generate_enhanced_track_video(year, typhoon, standard)
|
| 1314 |
|
| 1315 |
# -----------------------------
|
| 1316 |
# Load & Process Data
|
|
|
|
| 1359 |
initialize_data()
|
| 1360 |
|
| 1361 |
# -----------------------------
|
| 1362 |
+
# ENHANCED: Gradio Interface
|
| 1363 |
# -----------------------------
|
| 1364 |
|
| 1365 |
def create_interface():
|
| 1366 |
+
"""Create the enhanced Gradio interface"""
|
| 1367 |
try:
|
| 1368 |
+
with gr.Blocks(title="Enhanced Typhoon Analysis Platform", theme=gr.themes.Soft()) as demo:
|
| 1369 |
+
gr.Markdown("# πͺοΈ Enhanced Typhoon Analysis Platform")
|
| 1370 |
+
gr.Markdown("Advanced ML clustering, CNN predictions, and comprehensive tropical cyclone analysis including Tropical Depressions")
|
| 1371 |
|
| 1372 |
+
with gr.Tab("π Overview"):
|
| 1373 |
gr.Markdown(f"""
|
| 1374 |
+
## Welcome to the Enhanced Typhoon Analysis Dashboard
|
| 1375 |
|
| 1376 |
+
This dashboard provides comprehensive analysis of typhoon data in relation to ENSO phases with advanced machine learning capabilities.
|
| 1377 |
|
| 1378 |
+
### π Enhanced Features:
|
| 1379 |
+
- **π Advanced ML Clustering**: UMAP/t-SNE storm pattern analysis
|
| 1380 |
+
- **π€ Optional CNN Predictions**: Deep learning intensity forecasting
|
| 1381 |
+
- **π Complete TD Support**: Now includes Tropical Depressions (< 34 kt)
|
| 1382 |
+
- **π
2025 Data Ready**: Real-time compatibility with current year data
|
| 1383 |
+
- **π¬ Enhanced Animations**: High-quality storm track visualizations
|
| 1384 |
|
| 1385 |
+
### π Data Status:
|
| 1386 |
- **ONI Data**: {len(oni_data)} years loaded
|
| 1387 |
- **Typhoon Data**: {len(typhoon_data)} records loaded
|
| 1388 |
- **Merged Data**: {len(merged_data)} typhoons with ONI values
|
| 1389 |
+
- **Available Years**: {get_available_years(typhoon_data)[0]} - {get_available_years(typhoon_data)[-1]}
|
| 1390 |
+
|
| 1391 |
+
### π§ Technical Capabilities:
|
| 1392 |
+
- **UMAP Clustering**: {"β
Available" if UMAP_AVAILABLE else "β Install umap-learn"}
|
| 1393 |
+
- **CNN Models**: {"β
Available" if CNN_AVAILABLE else "β Install tensorflow"}
|
| 1394 |
+
- **Enhanced Categorization**: β
Tropical Depression to Super Typhoon
|
| 1395 |
+
""")
|
| 1396 |
+
|
| 1397 |
+
with gr.Tab("π Advanced ML Clustering"):
|
| 1398 |
+
gr.Markdown("## Storm Pattern Analysis using UMAP/t-SNE")
|
| 1399 |
+
|
| 1400 |
+
with gr.Row():
|
| 1401 |
+
reduction_method = gr.Dropdown(
|
| 1402 |
+
choices=['UMAP', 't-SNE', 'PCA'],
|
| 1403 |
+
value='UMAP' if UMAP_AVAILABLE else 't-SNE',
|
| 1404 |
+
label="Dimensionality Reduction Method"
|
| 1405 |
+
)
|
| 1406 |
+
cluster_method = gr.Dropdown(
|
| 1407 |
+
choices=['DBSCAN', 'K-Means'],
|
| 1408 |
+
value='DBSCAN',
|
| 1409 |
+
label="Clustering Method"
|
| 1410 |
+
)
|
| 1411 |
+
|
| 1412 |
+
analyze_clusters_btn = gr.Button("π― Analyze Storm Clusters", variant="primary")
|
| 1413 |
+
|
| 1414 |
+
with gr.Row():
|
| 1415 |
+
cluster_plot = gr.Plot(label="Storm Clustering Visualization")
|
| 1416 |
+
cluster_stats = gr.Textbox(label="Cluster Statistics", lines=10)
|
| 1417 |
+
|
| 1418 |
+
def run_clustering_analysis(method):
|
| 1419 |
+
try:
|
| 1420 |
+
# Extract features for clustering
|
| 1421 |
+
storm_features = extract_storm_features(typhoon_data)
|
| 1422 |
+
fig, stats, _ = create_clustering_visualization(storm_features, typhoon_data, method.lower())
|
| 1423 |
+
return fig, stats
|
| 1424 |
+
except Exception as e:
|
| 1425 |
+
return None, f"Error: {str(e)}"
|
| 1426 |
+
|
| 1427 |
+
analyze_clusters_btn.click(
|
| 1428 |
+
fn=run_clustering_analysis,
|
| 1429 |
+
inputs=[reduction_method],
|
| 1430 |
+
outputs=[cluster_plot, cluster_stats]
|
| 1431 |
+
)
|
| 1432 |
+
|
| 1433 |
+
gr.Markdown("""
|
| 1434 |
+
### βΉοΈ About Storm Clustering:
|
| 1435 |
+
- **UMAP**: Faster and preserves global structure better
|
| 1436 |
+
- **t-SNE**: Good for local neighborhood preservation
|
| 1437 |
+
- **PCA**: Linear dimensionality reduction (fallback)
|
| 1438 |
+
- **DBSCAN**: Density-based clustering, finds natural groupings
|
| 1439 |
+
- **K-Means**: Partitions storms into K predefined clusters
|
| 1440 |
""")
|
| 1441 |
|
| 1442 |
+
with gr.Tab("π€ CNN Intensity Prediction"):
|
| 1443 |
+
gr.Markdown("## Deep Learning Intensity Forecasting")
|
| 1444 |
+
|
| 1445 |
+
if CNN_AVAILABLE:
|
| 1446 |
+
gr.Markdown("β
**CNN models available** - TensorFlow loaded successfully")
|
| 1447 |
+
|
| 1448 |
+
with gr.Row():
|
| 1449 |
+
cnn_lat = gr.Number(label="Latitude", value=20.0)
|
| 1450 |
+
cnn_lon = gr.Number(label="Longitude", value=140.0)
|
| 1451 |
+
cnn_month = gr.Slider(1, 12, label="Month", value=9)
|
| 1452 |
+
cnn_oni = gr.Number(label="ONI Value", value=0.0)
|
| 1453 |
+
|
| 1454 |
+
predict_btn = gr.Button("π― Predict Storm Intensity", variant="primary")
|
| 1455 |
+
|
| 1456 |
+
with gr.Row():
|
| 1457 |
+
intensity_output = gr.Number(label="Predicted Max Wind (kt)")
|
| 1458 |
+
confidence_output = gr.Textbox(label="Model Output")
|
| 1459 |
+
|
| 1460 |
+
predict_btn.click(
|
| 1461 |
+
fn=simulate_cnn_prediction,
|
| 1462 |
+
inputs=[cnn_lat, cnn_lon, cnn_month, cnn_oni],
|
| 1463 |
+
outputs=[intensity_output, confidence_output]
|
| 1464 |
+
)
|
| 1465 |
+
|
| 1466 |
+
gr.Markdown("""
|
| 1467 |
+
### π¬ CNN Model Features:
|
| 1468 |
+
- **Multi-modal input**: Environmental conditions + position
|
| 1469 |
+
- **Real-time prediction**: Results in seconds
|
| 1470 |
+
- **Confidence estimates**: Model uncertainty quantification
|
| 1471 |
+
- **Research-based**: Following latest deep learning approaches in meteorology
|
| 1472 |
+
""")
|
| 1473 |
+
else:
|
| 1474 |
+
gr.Markdown("β **CNN models unavailable** - TensorFlow not installed")
|
| 1475 |
+
gr.Markdown("Install TensorFlow to enable deep learning features: `pip install tensorflow`")
|
| 1476 |
+
gr.Markdown("""
|
| 1477 |
+
### π CNN Features (when enabled):
|
| 1478 |
+
- Convolutional neural networks for intensity prediction
|
| 1479 |
+
- Satellite imagery processing capabilities
|
| 1480 |
+
- Multi-environmental factor analysis
|
| 1481 |
+
- State-of-the-art deep learning architectures
|
| 1482 |
+
""")
|
| 1483 |
+
|
| 1484 |
+
with gr.Tab("π Track Visualization"):
|
| 1485 |
with gr.Row():
|
| 1486 |
+
start_year = gr.Number(label="Start Year", value=2020)
|
| 1487 |
start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
|
| 1488 |
end_year = gr.Number(label="End Year", value=2025)
|
| 1489 |
end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
|
|
|
|
| 1498 |
outputs=[tracks_plot, typhoon_count]
|
| 1499 |
)
|
| 1500 |
|
| 1501 |
+
with gr.Tab("π¨ Wind Analysis"):
|
| 1502 |
with gr.Row():
|
| 1503 |
+
wind_start_year = gr.Number(label="Start Year", value=2020)
|
| 1504 |
wind_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
|
| 1505 |
wind_end_year = gr.Number(label="End Year", value=2024)
|
| 1506 |
wind_end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
|
|
|
|
| 1515 |
outputs=[wind_scatter, wind_regression_results]
|
| 1516 |
)
|
| 1517 |
|
| 1518 |
+
with gr.Tab("π Pressure Analysis"):
|
| 1519 |
with gr.Row():
|
| 1520 |
+
pressure_start_year = gr.Number(label="Start Year", value=2020)
|
| 1521 |
pressure_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
|
| 1522 |
pressure_end_year = gr.Number(label="End Year", value=2024)
|
| 1523 |
pressure_end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
|
|
|
|
| 1532 |
outputs=[pressure_scatter, pressure_regression_results]
|
| 1533 |
)
|
| 1534 |
|
| 1535 |
+
with gr.Tab("π Longitude Analysis"):
|
| 1536 |
with gr.Row():
|
| 1537 |
+
lon_start_year = gr.Number(label="Start Year", value=2020)
|
| 1538 |
lon_start_month = gr.Dropdown(label="Start Month", choices=list(range(1, 13)), value=1)
|
| 1539 |
+
lon_end_year = gr.Number(label="End Year", value=2020)
|
| 1540 |
lon_end_month = gr.Dropdown(label="End Month", choices=list(range(1, 13)), value=6)
|
| 1541 |
lon_enso_phase = gr.Dropdown(label="ENSO Phase", choices=['all', 'El Nino', 'La Nina', 'Neutral'], value='all')
|
| 1542 |
lon_typhoon_search = gr.Textbox(label="Typhoon Search (Optional)")
|
|
|
|
| 1550 |
outputs=[regression_plot, slopes_text, lon_regression_results]
|
| 1551 |
)
|
| 1552 |
|
| 1553 |
+
with gr.Tab("π¬ Enhanced Track Animation"):
|
| 1554 |
+
gr.Markdown("## High-Quality Storm Track Visualization (All Categories Including TD)")
|
| 1555 |
+
|
| 1556 |
with gr.Row():
|
| 1557 |
+
year_dropdown = gr.Dropdown(
|
| 1558 |
+
label="Year",
|
| 1559 |
+
choices=get_available_years(typhoon_data),
|
| 1560 |
+
value="2024"
|
| 1561 |
+
)
|
| 1562 |
+
basin_dropdown = gr.Dropdown(
|
| 1563 |
+
label="Basin",
|
| 1564 |
+
choices=["All Basins", "WP - Western Pacific", "EP - Eastern Pacific", "NA - North Atlantic"],
|
| 1565 |
+
value="All Basins"
|
| 1566 |
+
)
|
| 1567 |
+
|
| 1568 |
with gr.Row():
|
| 1569 |
+
typhoon_dropdown = gr.Dropdown(label="Storm Selection (All Categories Including TD)")
|
| 1570 |
+
standard_dropdown = gr.Dropdown(
|
| 1571 |
+
label="Classification Standard",
|
| 1572 |
+
choices=['atlantic', 'taiwan'],
|
| 1573 |
+
value='atlantic'
|
| 1574 |
+
)
|
| 1575 |
+
|
| 1576 |
+
generate_video_btn = gr.Button("π¬ Generate Enhanced Animation", variant="primary")
|
| 1577 |
+
video_output = gr.Video(label="Storm Track Animation")
|
| 1578 |
+
|
| 1579 |
+
# Update storm options when year or basin changes
|
| 1580 |
+
for input_comp in [year_dropdown, basin_dropdown]:
|
| 1581 |
+
input_comp.change(
|
| 1582 |
+
fn=update_typhoon_options_enhanced,
|
| 1583 |
+
inputs=[year_dropdown, basin_dropdown],
|
| 1584 |
+
outputs=[typhoon_dropdown]
|
| 1585 |
+
)
|
| 1586 |
+
|
| 1587 |
+
# Generate video
|
| 1588 |
+
generate_video_btn.click(
|
| 1589 |
+
fn=generate_enhanced_track_video,
|
| 1590 |
+
inputs=[year_dropdown, typhoon_dropdown, standard_dropdown],
|
| 1591 |
+
outputs=[video_output]
|
| 1592 |
)
|
| 1593 |
+
|
| 1594 |
+
gr.Markdown("""
|
| 1595 |
+
### π Enhanced Animation Features:
|
| 1596 |
+
- **π Full TD Support**: Now displays Tropical Depressions (< 34 kt) in gray
|
| 1597 |
+
- **π
2025 Compatibility**: Complete support for current year data
|
| 1598 |
+
- **πΊοΈ Enhanced Maps**: Better cartographic projections with terrain features
|
| 1599 |
+
- **π Smart Scaling**: Storm symbols scale dynamically with intensity
|
| 1600 |
+
- **π Real-time Info**: Live position, time, and meteorological data display
|
| 1601 |
+
- **π¨ Professional Styling**: Publication-quality animations with proper legends
|
| 1602 |
+
- **β‘ Optimized Export**: Fast rendering with web-compatible video formats
|
| 1603 |
+
""")
|
| 1604 |
|
| 1605 |
return demo
|
| 1606 |
except Exception as e:
|
| 1607 |
logging.error(f"Error creating Gradio interface: {e}")
|
| 1608 |
# Create a minimal fallback interface
|
| 1609 |
with gr.Blocks() as demo:
|
| 1610 |
+
gr.Markdown("# πͺοΈ Enhanced Typhoon Analysis Platform")
|
| 1611 |
gr.Markdown("**Error**: Could not load full interface. Please check logs.")
|
| 1612 |
return demo
|
| 1613 |
|