import gradio as gr
import xarray as xr
import pandas as pd
import numpy as np
import plotly.graph_objects as go
from datetime import datetime, timedelta
import warnings
import logging
import traceback
warnings.filterwarnings('ignore')
# Set up detailed logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Catalog configuration - using latest.zarr URLs as per dynamical-org/notebooks
CATALOG = {
"NOAA GFS Analysis (Hourly)": {
"url": "https://data.dynamical.org/noaa/gfs/analysis-hourly/latest.zarr?email=treesixtyweather@gmail.com",
"type": "analysis",
"variables": ["temperature_2m", "precipitation", "wind_u_10m", "wind_v_10m", "mean_sea_level_pressure"]
},
"NOAA GFS Forecast": {
"url": "https://data.dynamical.org/noaa/gfs/forecast/latest.zarr?email=treesixtyweather@gmail.com",
"type": "forecast",
"variables": ["temperature_2m", "precipitation", "wind_u_10m", "wind_v_10m", "mean_sea_level_pressure"]
},
"NOAA GEFS Analysis": {
"url": "https://data.dynamical.org/noaa/gefs/analysis/latest.zarr?email=treesixtyweather@gmail.com",
"type": "analysis",
"variables": ["temperature_2m", "precipitation", "wind_u_10m", "wind_v_10m"]
},
"NOAA GEFS Forecast (35-day)": {
"url": "https://data.dynamical.org/noaa/gefs/forecast-35-day/latest.zarr?email=treesixtyweather@gmail.com",
"type": "forecast",
"variables": ["temperature_2m", "precipitation", "wind_u_10m", "wind_v_10m"]
},
"NOAA HRRR Forecast (48-hour)": {
"url": "https://data.dynamical.org/noaa/hrrr/forecast-48-hour/latest.zarr?email=treesixtyweather@gmail.com",
"type": "forecast",
"variables": ["temperature_2m", "precipitation", "wind_u_10m", "wind_v_10m"]
}
}
# Cache for loaded datasets
dataset_cache = {}
def load_dataset(dataset_name, use_cache=True):
"""Load a dataset from the Dynamical catalog"""
logger.info(f"=== Loading dataset: {dataset_name} ===")
if use_cache and dataset_name in dataset_cache:
logger.info(f"Dataset found in cache: {dataset_name}")
return dataset_cache[dataset_name], None
try:
url = CATALOG[dataset_name]["url"]
logger.info(f"Opening zarr store at: {url}")
# Open zarr store - using approach from dynamical-org/notebooks
logger.info("Opening zarr store with chunks=None")
ds = xr.open_zarr(url, chunks=None)
logger.info(f"Successfully opened zarr store")
logger.info(f"Dataset dimensions: {dict(ds.dims)}")
logger.info(f"Dataset variables: {list(ds.data_vars)}")
logger.info(f"Dataset coordinates: {list(ds.coords)}")
if use_cache:
dataset_cache[dataset_name] = ds
logger.info(f"Dataset cached: {dataset_name}")
return ds, None
except Exception as e:
error_msg = f"Error loading dataset: {str(e)}"
logger.error(f"=== ERROR loading {dataset_name} ===")
logger.error(f"URL: {CATALOG[dataset_name]['url']}")
logger.error(f"Exception type: {type(e).__name__}")
logger.error(f"Exception message: {str(e)}")
logger.error(f"Traceback:\n{traceback.format_exc()}")
return None, error_msg
def create_map_visualization(dataset_name, variable, time_index=0):
"""Create an interactive map visualization of the selected variable"""
logger.info(f"=== Creating map visualization ===")
logger.info(f"Dataset: {dataset_name}, Variable: {variable}, Time index: {time_index}")
try:
ds, error = load_dataset(dataset_name)
if ds is None:
logger.error(f"Dataset loading returned None: {error}")
return None, f"Error loading dataset: {dataset_name}\n{error}"
logger.info(f"Dataset loaded successfully")
# Check if variable exists
if variable not in ds.variables:
available_vars = list(ds.data_vars)
logger.error(f"Variable '{variable}' not found. Available: {available_vars}")
return None, f"Variable '{variable}' not found. Available: {available_vars}"
logger.info(f"Variable '{variable}' found in dataset")
# Get the data
data_var = ds[variable]
logger.info(f"Variable shape: {data_var.shape}, dims: {data_var.dims}")
# Handle time dimension
if 'time' in data_var.dims:
logger.info(f"Time dimension found, length: {len(ds.time)}")
if time_index >= len(ds.time):
time_index = 0
data_var = data_var.isel(time=time_index)
logger.info(f"Selected time index: {time_index}")
# Handle ensemble dimension if present
if 'ensemble' in data_var.dims:
logger.info(f"Ensemble dimension found, selecting ensemble 0")
data_var = data_var.isel(ensemble=0)
logger.info(f"Data variable shape after slicing: {data_var.shape}")
# Load data into memory (subsample for performance)
step = max(1, len(ds.latitude) // 200) # Limit to ~200 points per dimension
logger.info(f"Subsampling with step: {step}")
data_var = data_var.isel(latitude=slice(None, None, step), longitude=slice(None, None, step))
logger.info(f"Computing data values...")
data_values = data_var.compute().values
logger.info(f"Data values shape: {data_values.shape}, min: {data_values.min()}, max: {data_values.max()}")
# Get coordinates
lats = ds.latitude.isel(latitude=slice(None, None, step)).values
lons = ds.longitude.isel(longitude=slice(None, None, step)).values
logger.info(f"Lat range: [{lats.min()}, {lats.max()}], Lon range: [{lons.min()}, {lons.max()}]")
# Create plotly figure
fig = go.Figure(data=go.Heatmap(
z=data_values,
x=lons,
y=lats,
colorscale='RdBu_r',
hovertemplate='Lat: %{y:.2f}
Lon: %{x:.2f}
Value: %{z:.2f}'
))
time_str = ""
if 'time' in ds[variable].dims:
time_val = pd.to_datetime(ds.time.isel(time=time_index).values)
time_str = f" - {time_val.strftime('%Y-%m-%d %H:%M UTC')}"
fig.update_layout(
title=f"{dataset_name}: {variable}{time_str}",
xaxis_title="Longitude",
yaxis_title="Latitude",
height=600,
hovermode='closest'
)
logger.info(f"Map visualization created successfully")
return fig, f"Successfully loaded {dataset_name}"
except Exception as e:
error_msg = f"Error creating visualization: {str(e)}"
logger.error(f"=== ERROR creating visualization ===")
logger.error(f"Exception type: {type(e).__name__}")
logger.error(f"Exception message: {str(e)}")
logger.error(f"Traceback:\n{traceback.format_exc()}")
return None, error_msg
def get_point_forecast(dataset_name, lat, lon, variable):
"""Get forecast data for a specific point"""
logger.info(f"=== Getting point forecast ===")
logger.info(f"Dataset: {dataset_name}, Lat: {lat}, Lon: {lon}, Variable: {variable}")
try:
ds, error = load_dataset(dataset_name)
if ds is None:
logger.error(f"Dataset loading failed: {error}")
return None, f"Error loading dataset: {error}"
if variable not in ds.variables:
logger.error(f"Variable '{variable}' not found in dataset")
return None, f"Variable '{variable}' not found in dataset"
logger.info(f"Selecting nearest point to ({lat}, {lon})")
# Find nearest point
data_var = ds[variable].sel(latitude=lat, longitude=lon, method='nearest')
# Handle ensemble dimension
if 'ensemble' in data_var.dims:
logger.info(f"Handling ensemble dimension")
data_var = data_var.isel(ensemble=0)
logger.info(f"Point data shape: {data_var.shape}, dims: {data_var.dims}")
# Load data
logger.info(f"Computing point data values...")
data_values = data_var.compute().values
logger.info(f"Point data computed, shape: {data_values.shape}")
# Create time series plot
if 'time' in ds[variable].dims:
times = pd.to_datetime(ds.time.values)
logger.info(f"Creating time series plot with {len(times)} time steps")
fig = go.Figure()
fig.add_trace(go.Scatter(
x=times,
y=data_values,
mode='lines+markers',
name=variable
))
fig.update_layout(
title=f"Point Forecast: {variable} at ({lat:.2f}, {lon:.2f})",
xaxis_title="Time (UTC)",
yaxis_title=variable,
height=400,
hovermode='x unified'
)
# Create data table
df = pd.DataFrame({
'Time (UTC)': times,
variable: data_values
})
logger.info(f"Point forecast created successfully")
return fig, df.to_html(index=False)
else:
logger.warning(f"No time dimension found for {variable}")
return None, f"No time dimension found for {variable}"
except Exception as e:
error_msg = f"Error getting point forecast: {str(e)}"
logger.error(f"=== ERROR getting point forecast ===")
logger.error(f"Exception type: {type(e).__name__}")
logger.error(f"Exception message: {str(e)}")
logger.error(f"Traceback:\n{traceback.format_exc()}")
return None, error_msg
def update_available_variables(dataset_name):
"""Update the variable dropdown based on selected dataset"""
logger.info(f"=== Updating available variables for {dataset_name} ===")
try:
ds, error = load_dataset(dataset_name, use_cache=False)
if ds is None:
logger.warning(f"Could not load dataset, using default variables: {error}")
return gr.Dropdown(choices=CATALOG[dataset_name]["variables"], value=CATALOG[dataset_name]["variables"][0])
available_vars = list(ds.data_vars)
logger.info(f"Available variables: {available_vars}")
return gr.Dropdown(choices=available_vars, value=available_vars[0] if available_vars else None)
except Exception as e:
logger.error(f"Error updating variables: {str(e)}")
logger.error(f"Traceback:\n{traceback.format_exc()}")
return gr.Dropdown(choices=CATALOG[dataset_name]["variables"], value=CATALOG[dataset_name]["variables"][0])
# Create Gradio interface
with gr.Blocks(title="Dynamical Weather Catalog Viewer") as app:
gr.Markdown("""
# 🌍 Dynamical Weather Catalog Viewer
Explore weather analysis and forecast data from the [Dynamical.org catalog](https://dynamical.org/catalog/).
**Features:**
- Visualize global weather data on interactive maps
- Click a location to get point forecasts
- Browse multiple datasets: NOAA GFS, GEFS, and HRRR
""")
with gr.Row():
with gr.Column(scale=1):
dataset_dropdown = gr.Dropdown(
choices=list(CATALOG.keys()),
value=list(CATALOG.keys())[0],
label="Select Dataset"
)
variable_dropdown = gr.Dropdown(
choices=CATALOG[list(CATALOG.keys())[0]]["variables"],
value=CATALOG[list(CATALOG.keys())[0]]["variables"][0],
label="Select Variable"
)
time_slider = gr.Slider(
minimum=0,
maximum=10,
step=1,
value=0,
label="Time Index"
)
load_btn = gr.Button("Load Map", variant="primary")
status_text = gr.Textbox(label="Status", interactive=False)
with gr.Column(scale=2):
map_plot = gr.Plot(label="Map Visualization")
gr.Markdown("## 📍 Point Forecast")
gr.Markdown("Enter coordinates to get a time series forecast for a specific location")
with gr.Row():
lat_input = gr.Number(value=40.7, label="Latitude", precision=2)
lon_input = gr.Number(value=-74.0, label="Longitude", precision=2)
forecast_btn = gr.Button("Get Point Forecast", variant="secondary")
with gr.Row():
forecast_plot = gr.Plot(label="Time Series Forecast")
forecast_table = gr.HTML(label="Forecast Data")
# Event handlers
dataset_dropdown.change(
fn=update_available_variables,
inputs=[dataset_dropdown],
outputs=[variable_dropdown]
)
load_btn.click(
fn=create_map_visualization,
inputs=[dataset_dropdown, variable_dropdown, time_slider],
outputs=[map_plot, status_text]
)
forecast_btn.click(
fn=get_point_forecast,
inputs=[dataset_dropdown, lat_input, lon_input, variable_dropdown],
outputs=[forecast_plot, forecast_table]
)
if __name__ == "__main__":
app.launch()