import gradio as gr import pandas as pd import folium import requests from datetime import datetime, timedelta import io import os import xarray as xr import cfgrib # For reading GRIB2 files import numpy as np # For numerical operations import s3fs # For accessing S3 import fsspec # Needed for advanced caching # --- Configuration --- # NOAA_API_TOKEN is not needed for public GFS S3 access, but keep for consistency if using other NOAA APIs NOAA_API_TOKEN = os.getenv("NOAA_API_TOKEN") # Base URL/Path for GFS data on AWS S3 GFS_S3_BASE = "s3://noaa-gfs-bdp-pds/" # --- Helper Functions for GFS Data Retrieval --- def fetch_gfs_wind_data(forecast_date: datetime, forecast_hour_utc: int = 0): """ Fetches 10m wind data (u and v components) from GFS on AWS S3 for a given date and UTC hour. Returns a pandas DataFrame. """ date_str = forecast_date.strftime("%Y%m%d") hour_str = f"{forecast_hour_utc:02d}" # e.g., "00", "06" # We'll fetch the analysis file (f000) for simplicity. # For forecasts, change fFFF accordingly. grib_file_path = f"{GFS_S3_BASE}{date_str}/{hour_str}/atmos/gfs.t{hour_str}z.pgrb2.0p25.f000.grib2" print(f"Attempting to open GFS file: {grib_file_path}") try: # **Crucial Change: Use fsspec's simplecache to specify a local cache directory** # This tells cfgrib to download the file (and create its index) in /tmp # /tmp is typically writable in containerized environments like Hugging Face Spaces cached_grib_file_path = f"simplecache::{grib_file_path}" # Configure the cache for s3fs (anon=True for public buckets) # and specify the cache directory for simplecache storage_options = { "simplecache": { "cache_storage": "/tmp/gfs_cache" # Use /tmp or another suitable writable path }, "s3": { "anon": True # Anonymous access for public NOAA S3 bucket } } ds = xr.open_dataset( cached_grib_file_path, engine="cfgrib", backend_kwargs={'filter_by_keys': {'typeOfLevel': 'heightAboveGround', 'level': 10, 'shortName': ['u', 'v']}}, # Pass storage_options to open_dataset chunks='auto', # Enable dask for out-of-core processing if files are large storage_options=storage_options ) # Ensure the variable names are correct. GFS 0.25 typically uses 'u' and 'v' # if the filter_by_keys works correctly. # If not, you might see 'ugrd10m' and 'vgrd10m' or similar. u_var = [v for v in ds.data_vars if 'u' in v.lower() and '10m' in v.lower() or 'u' == v][0] v_var = [v for v in ds.data_vars if 'v' in v.lower() and '10m' in v.lower() or 'v' == v][0] ds['wind_speed'] = np.sqrt(ds[u_var]**2 + ds[v_var]**2) # Wind direction is typically measured clockwise from North. # atan2(U, V) gives math angle (counter-clockwise from East). # To convert to meteorological direction (from North, clockwise): # Angle = (270 - np.degrees(np.arctan2(U, V))) % 360 ds['wind_direction'] = (270 - np.degrees(np.arctan2(ds[u_var], ds[v_var]))) % 360 # Convert to pandas DataFrame for easier plotting with Folium df = ds[['wind_speed', 'wind_direction']].to_dataframe().reset_index() # Ensure latitude/longitude columns are named consistently for Folium # xarray might use 'latitude' and 'longitude' or 'lat' and 'lon' if 'latitude' not in df.columns and 'lat' in df.columns: df = df.rename(columns={'lat': 'latitude'}) if 'longitude' not in df.columns and 'lon' in df.columns: df = df.rename(columns={'lon': 'longitude'}) # Filter out NaN values that might result from calculations df = df.dropna(subset=['wind_speed', 'wind_direction', 'latitude', 'longitude']) return df except Exception as e: print(f"Error fetching GFS data: {e}") # Print current working directory and /tmp contents for debugging on HF Spaces print(f"Current directory: {os.getcwd()}") if os.path.exists('/tmp'): print(f"/tmp contents: {os.listdir('/tmp')}") return pd.DataFrame() # --- Gradio Interface Function (remains mostly the same) --- def visualize_global_wind(gfs_run_hour: str, days_ago: int): """ Fetches global GFS wind data and generates a Folium map. """ # No need for NOAA_API_TOKEN check here as GFS S3 access is anonymous # Get current time in Bozeman, MT (MDT is -6 UTC in summer, -7 UTC in winter) # The current time is 3:06:25 PM MDT on July 2, 2025. # MDT is UTC-6. So 3:06 PM MDT is 21:06 UTC. # GFS runs are 00Z, 06Z, 12Z, 18Z. The closest previous run to 21:06 UTC is 18Z. current_utc_time = datetime.utcnow() # Calculate the exact run date based on days_ago and selected hour # This logic ensures we pick the correct GFS run file path target_utc_date = current_utc_time - timedelta(days=days_ago) # GFS data is usually available for the 00, 06, 12, 18 UTC runs. # We want the *latest available* run for `days_ago`. # Let's say `gfs_run_hour` is '12'. We need the GFS file for `target_utc_date` at `12Z`. # The `fetch_gfs_wind_data` function is designed to fetch the analysis (f000) for a specific run. # So if you select "0 days ago" and "12" hour, it will attempt to get today's 12Z run. # This is correct. df_wind = fetch_gfs_wind_data(target_utc_date, forecast_hour_utc=int(gfs_run_hour)) if df_wind.empty: return "