Spaces:
Paused
Paused
| #!/usr/bin/env python3 | |
| """ | |
| ECMWF Open Data Explorer | |
| π OPEN DATA ACCESS - NO API KEYS REQUIRED! | |
| Access real ECMWF operational forecast data directly from their open data portal. | |
| Data is provided under CC BY 4.0 license and requires no authentication. | |
| Features: | |
| - Real ECMWF IFS operational forecasts | |
| - Latest weather data updated every 6 hours | |
| - Global coverage at 0.25Β° resolution | |
| - Multiple weather parameters | |
| - Interactive visualizations | |
| License: | |
| This code is licensed under the GNU General Public License v3.0 (GPL-3.0). | |
| You may copy, distribute and modify the software under the terms of the GPL-3.0 license. | |
| - License: https://www.gnu.org/licenses/gpl-3.0.html | |
| Data Attribution: | |
| Weather data provided by ECMWF (European Centre for Medium-Range Weather Forecasts) | |
| under their Open Data initiative. ECMWF data is made available under the | |
| Creative Commons Attribution 4.0 International (CC BY 4.0) license. | |
| You must provide appropriate attribution when using ECMWF data. | |
| - Data source: https://www.ecmwf.int/en/forecasts/datasets/open-data | |
| - Data license: https://creativecommons.org/licenses/by/4.0/ | |
| """ | |
| import gradio as gr | |
| import numpy as np | |
| import pandas as pd | |
| import matplotlib.pyplot as plt | |
| import xarray as xr | |
| import requests | |
| import tempfile | |
| import os | |
| from datetime import datetime, timedelta | |
| import warnings | |
| import folium | |
| import plotly.graph_objects as go | |
| import plotly.express as px | |
| from plotly.subplots import make_subplots | |
| warnings.filterwarnings('ignore') | |
| try: | |
| from ecmwf.opendata import Client as OpenDataClient | |
| OPENDATA_AVAILABLE = True | |
| except ImportError: | |
| OPENDATA_AVAILABLE = False | |
| class ECMWFOpenDataAccess: | |
| def __init__(self): | |
| self.temp_dir = tempfile.mkdtemp() | |
| self.client = None | |
| if OPENDATA_AVAILABLE: | |
| try: | |
| self.client = OpenDataClient() | |
| except: | |
| self.client = None | |
| # AWS S3 direct access URLs (completely free) | |
| self.aws_base_url = "https://ecmwf-forecasts.s3.eu-central-1.amazonaws.com" | |
| # Extended ECMWF open data parameters - much more available! | |
| self.parameters = { | |
| # Temperature & Humidity | |
| "2t": {"name": "2m Temperature", "units": "K", "description": "Temperature at 2 meters above surface", "group": "Temperature"}, | |
| "2d": {"name": "2m Dewpoint", "units": "K", "description": "Dewpoint temperature at 2m", "group": "Temperature"}, | |
| "skt": {"name": "Skin Temperature", "units": "K", "description": "Temperature of Earth's surface", "group": "Temperature"}, | |
| # Pressure Systems | |
| "msl": {"name": "Mean Sea Level Pressure", "units": "Pa", "description": "Pressure reduced to mean sea level", "group": "Pressure"}, | |
| "sp": {"name": "Surface Pressure", "units": "Pa", "description": "Pressure at surface", "group": "Pressure"}, | |
| # Wind (10m level) | |
| "10u": {"name": "10m U Wind", "units": "m/s", "description": "U-component of wind at 10m", "group": "Wind"}, | |
| "10v": {"name": "10m V Wind", "units": "m/s", "description": "V-component of wind at 10m", "group": "Wind"}, | |
| "10si": {"name": "10m Wind Speed", "units": "m/s", "description": "Wind speed at 10 meters", "group": "Wind"}, | |
| "10wdir": {"name": "10m Wind Direction", "units": "degrees", "description": "Wind direction at 10 meters", "group": "Wind"}, | |
| # Wind (100m level - for wind energy) | |
| "100u": {"name": "100m U Wind", "units": "m/s", "description": "U-component of wind at 100m", "group": "Wind"}, | |
| "100v": {"name": "100m V Wind", "units": "m/s", "description": "V-component of wind at 100m", "group": "Wind"}, | |
| "100si": {"name": "100m Wind Speed", "units": "m/s", "description": "Wind speed at 100 meters", "group": "Wind"}, | |
| "100wdir": {"name": "100m Wind Direction", "units": "degrees", "description": "Wind direction at 100 meters", "group": "Wind"}, | |
| # Precipitation & Water | |
| "tp": {"name": "Total Precipitation", "units": "m", "description": "Accumulated precipitation", "group": "Precipitation"}, | |
| "tcwv": {"name": "Total Column Water Vapour", "units": "kg/mΒ²", "description": "Water vapour in atmospheric column", "group": "Precipitation"}, | |
| # Radiation & Energy | |
| "ssrd": {"name": "Surface Solar Radiation", "units": "J/mΒ²", "description": "Solar radiation reaching surface", "group": "Radiation"}, | |
| "strd": {"name": "Surface Thermal Radiation", "units": "J/mΒ²", "description": "Thermal radiation from surface", "group": "Radiation"}, | |
| "ssr": {"name": "Surface Net Solar Radiation", "units": "J/mΒ²", "description": "Net solar radiation at surface", "group": "Radiation"}, | |
| "str": {"name": "Surface Net Thermal Radiation", "units": "J/mΒ²", "description": "Net thermal radiation at surface", "group": "Radiation"}, | |
| "tsr": {"name": "Top Net Solar Radiation", "units": "J/mΒ²", "description": "Net solar radiation at top of atmosphere", "group": "Radiation"}, | |
| "ttr": {"name": "Top Net Thermal Radiation", "units": "J/mΒ²", "description": "Net thermal radiation at top of atmosphere", "group": "Radiation"}, | |
| # Cloud Cover | |
| "tcc": {"name": "Total Cloud Cover", "units": "(0-1)", "description": "Fraction of sky covered by clouds", "group": "Clouds"}, | |
| "lcc": {"name": "Low Cloud Cover", "units": "(0-1)", "description": "Low level cloud cover", "group": "Clouds"}, | |
| "mcc": {"name": "Medium Cloud Cover", "units": "(0-1)", "description": "Medium level cloud cover", "group": "Clouds"}, | |
| "hcc": {"name": "High Cloud Cover", "units": "(0-1)", "description": "High level cloud cover", "group": "Clouds"}, | |
| # Additional Useful Parameters | |
| "cape": {"name": "CAPE", "units": "J/kg", "description": "Convective Available Potential Energy", "group": "Atmospheric"}, | |
| "gh": {"name": "Geopotential Height", "units": "mΒ²/sΒ²", "description": "Geopotential at various levels", "group": "Atmospheric"}, | |
| "vo": {"name": "Vorticity", "units": "sβ»ΒΉ", "description": "Relative vorticity", "group": "Atmospheric"} | |
| } | |
| def get_latest_forecast_info(self): | |
| """Get the latest available forecast run information""" | |
| try: | |
| # ECMWF runs at 00, 06, 12, 18 UTC | |
| now = datetime.utcnow() | |
| # Find the most recent model run (data available 7-9 hours after run time) | |
| for hours_back in range(4, 24, 6): # Check recent runs | |
| test_time = now - timedelta(hours=hours_back) | |
| # Round to nearest 6-hour cycle | |
| run_hour = (test_time.hour // 6) * 6 | |
| run_time = test_time.replace(hour=run_hour, minute=0, second=0, microsecond=0) | |
| date_str = run_time.strftime("%Y%m%d") | |
| time_str = f"{run_hour:02d}" | |
| # Test if this run is available | |
| test_url = f"{self.aws_base_url}/{date_str}/{time_str}z/0p25/oper/" | |
| try: | |
| response = requests.head(test_url, timeout=10) | |
| if response.status_code in [200, 403]: # 403 is OK, means it exists but we need specific file | |
| return date_str, time_str, run_time | |
| except: | |
| continue | |
| # Fallback | |
| return now.strftime("%Y%m%d"), "12", now | |
| except Exception as e: | |
| # Emergency fallback | |
| now = datetime.utcnow() | |
| return now.strftime("%Y%m%d"), "12", now | |
| def download_ecmwf_data(self, parameter="2t", step=0, max_retries=3): | |
| """Download real ECMWF data using multiple methods""" | |
| date_str, time_str, run_time = self.get_latest_forecast_info() | |
| # Method 1: Try ecmwf-opendata client (most reliable) | |
| if OPENDATA_AVAILABLE and self.client: | |
| try: | |
| filename = os.path.join(self.temp_dir, f'ecmwf_{parameter}_{step}h_{datetime.now().strftime("%Y%m%d_%H%M%S")}.grib') | |
| self.client.retrieve( | |
| type="fc", | |
| param=parameter, | |
| step=step, | |
| target=filename | |
| ) | |
| if os.path.exists(filename) and os.path.getsize(filename) > 1000: | |
| return filename, f"β ECMWF {parameter} data downloaded successfully via official client!\nRun: {date_str} {time_str}z, Step: +{step}h" | |
| except Exception as e: | |
| print(f"Client method failed: {str(e)}") | |
| # Method 2: Direct AWS S3 access (backup method) | |
| try: | |
| step_str = f"{step:03d}" | |
| filename = f"{date_str}{time_str}0000-{step_str}h-oper-fc.grib2" | |
| url = f"{self.aws_base_url}/{date_str}/{time_str}z/0p25/oper/{filename}" | |
| response = requests.get(url, timeout=120, stream=True) | |
| if response.status_code == 200: | |
| local_file = os.path.join(self.temp_dir, f'ecmwf_aws_{parameter}_{step}h.grib2') | |
| with open(local_file, 'wb') as f: | |
| for chunk in response.iter_content(chunk_size=8192): | |
| f.write(chunk) | |
| if os.path.getsize(local_file) > 1000: | |
| return local_file, f"β ECMWF data downloaded via AWS S3!\nForecast: {date_str} {time_str}z +{step}h\nParameter: {self.parameters.get(parameter, {}).get('name', parameter)}" | |
| except Exception as e: | |
| print(f"AWS method failed: {str(e)}") | |
| # Method 3: Try alternative forecast hours | |
| if step == 0: | |
| for alt_step in [6, 12, 24]: | |
| try: | |
| return self.download_ecmwf_data(parameter, alt_step, max_retries=1) | |
| except: | |
| continue | |
| return None, f"β Unable to download ECMWF data for {parameter} at +{step}h.\nThis could be due to:\n- Data not yet available for latest run\n- Network connectivity issues\n- Temporary ECMWF server issues\n\nTry a different forecast step or parameter." | |
| def create_weather_visualization(self, filename, parameter): | |
| """Create visualization from ECMWF GRIB data""" | |
| try: | |
| # Open the GRIB file with xarray | |
| try: | |
| ds = xr.open_dataset(filename, engine='cfgrib', backend_kwargs={'indexpath': ''}) | |
| except: | |
| # Try alternative method | |
| ds = xr.open_dataset(filename, engine='cfgrib') | |
| # Find the right variable | |
| param_info = self.parameters.get(parameter, {"name": parameter, "units": "units"}) | |
| # Get data variable (GRIB files may have different variable names) | |
| data_vars = list(ds.data_vars.keys()) | |
| if not data_vars: | |
| return None, "No data variables found in file" | |
| data_var = data_vars[0] # Use first available variable | |
| data = ds[data_var] | |
| # Handle coordinates | |
| if 'latitude' in ds.coords: | |
| lats = ds.latitude.values | |
| lons = ds.longitude.values | |
| elif 'lat' in ds.coords: | |
| lats = ds.lat.values | |
| lons = ds.lon.values | |
| else: | |
| return None, "Could not find latitude/longitude coordinates" | |
| # Get the data values (select first time step if multiple) | |
| if 'time' in data.dims and len(data.time) > 1: | |
| values = data.isel(time=0).values | |
| elif 'valid_time' in data.dims: | |
| values = data.isel(valid_time=0).values | |
| else: | |
| values = data.values | |
| # Handle 3D data (select first level if needed) | |
| if values.ndim > 2: | |
| values = values[0] | |
| # Convert temperature from Kelvin to Celsius if needed | |
| if parameter == "2t" and np.mean(values) > 100: | |
| values = values - 273.15 | |
| units = "Β°C" | |
| param_info["units"] = "Β°C" | |
| else: | |
| units = param_info["units"] | |
| # Create the plot | |
| fig, ax = plt.subplots(1, 1, figsize=(15, 10)) | |
| # Choose appropriate colormap | |
| if parameter == "2t": | |
| cmap = 'RdYlBu_r' | |
| levels = 30 | |
| elif parameter in ["msl", "sp"]: | |
| cmap = 'viridis' | |
| levels = 20 | |
| elif parameter == "tp": | |
| cmap = 'Blues' | |
| levels = 25 | |
| elif parameter in ["10u", "10v"]: | |
| cmap = 'RdBu_r' | |
| levels = 25 | |
| else: | |
| cmap = 'plasma' | |
| levels = 20 | |
| # Create contour plot | |
| X, Y = np.meshgrid(lons, lats) | |
| contour = ax.contourf(X, Y, values, levels=levels, cmap=cmap, extend='both') | |
| ax.contour(X, Y, values, levels=10, colors='black', alpha=0.3, linewidths=0.5) | |
| # Add colorbar | |
| cbar = plt.colorbar(contour, ax=ax, shrink=0.7, pad=0.02) | |
| cbar.set_label(f'{param_info["name"]} ({units})', fontsize=12) | |
| # Formatting | |
| ax.set_xlabel('Longitude (Β°)', fontsize=12) | |
| ax.set_ylabel('Latitude (Β°)', fontsize=12) | |
| ax.set_title(f'ECMWF Operational Forecast: {param_info["name"]}\n{datetime.now().strftime("%Y-%m-%d %H:%M UTC")}', | |
| fontsize=14, fontweight='bold') | |
| ax.grid(True, alpha=0.3) | |
| # Add geographical reference lines | |
| ax.axhline(y=0, color='red', linestyle='--', alpha=0.6, linewidth=1) # Equator | |
| ax.axvline(x=0, color='red', linestyle='--', alpha=0.6, linewidth=1) # Prime meridian | |
| plt.tight_layout() | |
| # Save plot | |
| plot_path = os.path.join(self.temp_dir, f'ecmwf_plot_{parameter}_{datetime.now().strftime("%Y%m%d_%H%M%S")}.png') | |
| plt.savefig(plot_path, dpi=150, bbox_inches='tight') | |
| plt.close() | |
| # Create data summary | |
| summary = f"""π ECMWF Real Forecast Data Summary | |
| Parameter: {param_info['name']} ({param_info['units']}) | |
| Description: {param_info.get('description', 'ECMWF operational forecast')} | |
| Data Statistics: | |
| β’ Min Value: {np.nanmin(values):.2f} {units} | |
| β’ Max Value: {np.nanmax(values):.2f} {units} | |
| β’ Mean Value: {np.nanmean(values):.2f} {units} | |
| β’ Std Dev: {np.nanstd(values):.2f} {units} | |
| Coverage: | |
| β’ Latitude: {np.min(lats):.1f}Β° to {np.max(lats):.1f}Β° | |
| β’ Longitude: {np.min(lons):.1f}Β° to {np.max(lons):.1f}Β° | |
| β’ Resolution: ~{abs(lats[1]-lats[0]):.2f}Β° (~25km) | |
| β’ Grid Points: {len(lats)} Γ {len(lons)} = {len(lats)*len(lons):,} | |
| Source: ECMWF IFS Operational Forecast | |
| Data: 100% FREE - No API keys required | |
| Updated: Every 6 hours (00, 06, 12, 18 UTC)""" | |
| ds.close() | |
| return plot_path, summary | |
| except Exception as e: | |
| return None, f"Error creating visualization: {str(e)}\n\nThis might be due to:\n- Corrupted download\n- Unsupported GRIB format\n- Missing cfgrib dependencies" | |
| class InteractiveECMWFMap: | |
| def __init__(self, ecmwf_data_access): | |
| self.ecmwf_data = ecmwf_data_access | |
| self.temp_dir = ecmwf_data_access.temp_dir | |
| self.forecast_cache = {} | |
| self.downloaded_files = {} # Cache for downloaded GRIB files | |
| self.data_preloaded = False # Track if data has been preloaded | |
| self.preload_progress = {} # Track preloading progress | |
| self.rapid_mode = False # Track if using rapid processing | |
| def create_interactive_map(self): | |
| """Create a simple, reliable map for point selection""" | |
| try: | |
| # Create a map centered on Bozeman, Montana | |
| bozeman_lat, bozeman_lon = 45.6796, -111.0447 | |
| m = folium.Map( | |
| location=[bozeman_lat, bozeman_lon], | |
| zoom_start=6, | |
| tiles='OpenStreetMap', | |
| width='100%', | |
| height='500px' | |
| ) | |
| # Add Bozeman marker | |
| folium.Marker( | |
| [bozeman_lat, bozeman_lon], | |
| popup="ποΈ Bozeman, Montana<br>Default forecast location<br>Click anywhere for forecasts!", | |
| icon=folium.Icon(color='blue', icon='home') | |
| ).add_to(m) | |
| # Return the map HTML directly without complex styling | |
| return m._repr_html_() | |
| except Exception as e: | |
| return f""" | |
| <div style="padding: 20px; background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 8px; text-align: center;"> | |
| <h4 style="color: #dc3545;">Map Loading Error</h4> | |
| <p style="color: #666;">The interactive map could not be loaded: {str(e)}</p> | |
| <p style="color: #666;">Please use the coordinate inputs below to enter your location manually.</p> | |
| <div style="margin-top: 20px; padding: 15px; background-color: #e3f2fd; border-radius: 5px;"> | |
| <strong>Manual Coordinates:</strong><br> | |
| Enter latitude and longitude values and click "π Get Point Forecast" | |
| </div> | |
| </div> | |
| """ | |
| def get_point_forecast_data(self, latitude, longitude, forecast_steps=None): | |
| """Get forecast data for a specific point - optimized with caching (supports rapid mode)""" | |
| if forecast_steps is None: | |
| if self.rapid_mode: | |
| forecast_steps = [0, 6, 12, 24, 48, 72] # Rapid mode: shorter range | |
| else: | |
| forecast_steps = [0, 3, 6, 12, 18, 24, 36, 48, 60, 72, 84, 96, 120] # Full mode | |
| try: | |
| results = {} | |
| all_data = [] | |
| # Use different parameter sets based on mode | |
| if self.rapid_mode: | |
| parameters_to_use = ["2t", "msl", "10u", "10v", "tp"] # Essential params for rapid mode | |
| else: | |
| parameters_to_use = ["2t", "2d", "msl", "sp", "10u", "10v", "tp", "tcwv", "ssrd", "tcc"] # Full set | |
| for param in parameters_to_use: | |
| param_data = [] | |
| for step in forecast_steps: | |
| try: | |
| # Create cache key for this parameter and step | |
| cache_key = f"{param}_{step}" | |
| # Check if we already have this file downloaded | |
| if cache_key in self.downloaded_files: | |
| filename = self.downloaded_files[cache_key] | |
| # Verify file still exists | |
| if not os.path.exists(filename): | |
| del self.downloaded_files[cache_key] | |
| filename = None | |
| else: | |
| filename = None | |
| # Download if not cached or file missing | |
| if filename is None: | |
| filename, download_msg = self.ecmwf_data.download_ecmwf_data(param, step) | |
| if filename: | |
| # Cache the downloaded file for reuse | |
| self.downloaded_files[cache_key] = filename | |
| if filename: | |
| # Extract point data from the cached/downloaded file | |
| point_value = self.extract_point_from_grib(filename, latitude, longitude, param) | |
| if point_value is not None: | |
| param_data.append({ | |
| 'step': step, | |
| 'value': point_value, | |
| 'datetime': datetime.utcnow() + timedelta(hours=step) | |
| }) | |
| all_data.append({ | |
| 'parameter': param, | |
| 'step': step, | |
| 'value': point_value, | |
| 'datetime': datetime.utcnow() + timedelta(hours=step), | |
| 'param_name': self.ecmwf_data.parameters[param]['name'], | |
| 'units': self.ecmwf_data.parameters[param]['units'] | |
| }) | |
| except Exception as e: | |
| print(f"Error processing {param} at step {step}: {str(e)}") | |
| continue | |
| if param_data: | |
| results[param] = param_data | |
| return results, all_data | |
| except Exception as e: | |
| return {}, [] | |
| def preload_all_forecast_data(self, progress_callback=None): | |
| """Preload all forecast data for instant point extraction""" | |
| try: | |
| # Extended forecast range up to 120 hours (5 days) | |
| forecast_steps = [0, 3, 6, 12, 18, 24, 36, 48, 60, 72, 84, 96, 120] | |
| # Focus on core parameters for faster loading | |
| core_parameters = ["2t", "2d", "msl", "sp", "10u", "10v", "tp", "tcwv", "ssrd", "tcc"] | |
| total_files = len(core_parameters) * len(forecast_steps) | |
| downloaded_count = 0 | |
| for param in core_parameters: | |
| for step in forecast_steps: | |
| try: | |
| cache_key = f"{param}_{step}" | |
| # Skip if already cached | |
| if cache_key in self.downloaded_files and os.path.exists(self.downloaded_files[cache_key]): | |
| downloaded_count += 1 | |
| continue | |
| # Download the data | |
| filename, download_msg = self.ecmwf_data.download_ecmwf_data(param, step) | |
| if filename: | |
| self.downloaded_files[cache_key] = filename | |
| downloaded_count += 1 | |
| # Update progress | |
| progress = (downloaded_count / total_files) * 100 | |
| self.preload_progress = { | |
| 'current': downloaded_count, | |
| 'total': total_files, | |
| 'percentage': progress, | |
| 'current_param': self.ecmwf_data.parameters[param]['name'], | |
| 'current_step': step | |
| } | |
| if progress_callback: | |
| progress_callback(self.preload_progress) | |
| except Exception as e: | |
| print(f"Error preloading {param} at step {step}: {str(e)}") | |
| continue | |
| self.data_preloaded = True | |
| return True, f"Successfully preloaded {downloaded_count} forecast files" | |
| except Exception as e: | |
| return False, f"Error during preloading: {str(e)}" | |
| def preload_rapid_forecast_data(self, progress_callback=None): | |
| """Preload rapid forecast data - fewer parameters, faster processing""" | |
| try: | |
| # Rapid mode: Essential parameters only, shorter forecast range | |
| rapid_forecast_steps = [0, 6, 12, 24, 48, 72] # 6 time steps vs 13 | |
| rapid_parameters = ["2t", "msl", "10u", "10v", "tp"] # 5 params vs 10 | |
| total_files = len(rapid_parameters) * len(rapid_forecast_steps) | |
| downloaded_count = 0 | |
| for param in rapid_parameters: | |
| for step in rapid_forecast_steps: | |
| try: | |
| cache_key = f"{param}_{step}" | |
| # Skip if already cached | |
| if cache_key in self.downloaded_files and os.path.exists(self.downloaded_files[cache_key]): | |
| downloaded_count += 1 | |
| continue | |
| # Download the data | |
| filename, download_msg = self.ecmwf_data.download_ecmwf_data(param, step) | |
| if filename: | |
| self.downloaded_files[cache_key] = filename | |
| downloaded_count += 1 | |
| # Update progress | |
| progress = (downloaded_count / total_files) * 100 | |
| self.preload_progress = { | |
| 'current': downloaded_count, | |
| 'total': total_files, | |
| 'percentage': progress, | |
| 'current_param': self.ecmwf_data.parameters[param]['name'], | |
| 'current_step': step | |
| } | |
| if progress_callback: | |
| progress_callback(self.preload_progress) | |
| except Exception as e: | |
| print(f"Error preloading {param} at step {step}: {str(e)}") | |
| continue | |
| self.data_preloaded = True | |
| self.rapid_mode = True | |
| return True, f"Successfully preloaded {downloaded_count} rapid forecast files" | |
| except Exception as e: | |
| return False, f"Error during rapid preloading: {str(e)}" | |
| def clear_cache(self): | |
| """Clear the downloaded files cache""" | |
| self.downloaded_files.clear() | |
| self.forecast_cache.clear() | |
| def get_cache_info(self): | |
| """Get information about cached files""" | |
| cached_files = len(self.downloaded_files) | |
| cache_size_mb = 0 | |
| for filename in self.downloaded_files.values(): | |
| try: | |
| if os.path.exists(filename): | |
| cache_size_mb += os.path.getsize(filename) / (1024 * 1024) | |
| except: | |
| continue | |
| return { | |
| 'cached_files': cached_files, | |
| 'cache_size_mb': round(cache_size_mb, 2) | |
| } | |
| def extract_point_from_grib(self, filename, lat, lon, parameter): | |
| """Extract data value at a specific lat/lon point from GRIB file""" | |
| try: | |
| # Open the GRIB file | |
| ds = xr.open_dataset(filename, engine='cfgrib', backend_kwargs={'indexpath': ''}) | |
| # Get the first data variable | |
| data_vars = list(ds.data_vars.keys()) | |
| if not data_vars: | |
| return None | |
| data = ds[data_vars[0]] | |
| # Handle coordinates | |
| if 'latitude' in ds.coords: | |
| lats = ds.latitude | |
| lons = ds.longitude | |
| elif 'lat' in ds.coords: | |
| lats = ds.lat | |
| lons = ds.longitude | |
| else: | |
| return None | |
| # Select first time if multiple times | |
| if 'time' in data.dims and len(data.time) > 1: | |
| data = data.isel(time=0) | |
| elif 'valid_time' in data.dims: | |
| data = data.isel(valid_time=0) | |
| # Find nearest point using xarray's selection | |
| try: | |
| point_data = data.sel(latitude=lat, longitude=lon, method='nearest') | |
| except: | |
| try: | |
| point_data = data.sel(lat=lat, lon=lon, method='nearest') | |
| except: | |
| return None | |
| value = float(point_data.values) | |
| # Convert temperature from Kelvin to Celsius if needed | |
| if parameter == "2t" and value > 100: | |
| value = value - 273.15 | |
| ds.close() | |
| return value | |
| except Exception as e: | |
| print(f"Error extracting point data: {str(e)}") | |
| return None | |
| def create_forecast_visualization(self, forecast_data, latitude, longitude): | |
| """Create clean time-series: X=forecast hours, Y=parameter values, all data organized""" | |
| try: | |
| if not forecast_data: | |
| return None, "No forecast data available" | |
| # Simple color palette for clear visualization | |
| colors = [ | |
| '#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', | |
| '#e67e22', '#34495e', '#95a5a6', '#f1c40f', '#8e44ad', '#27ae60', | |
| '#c0392b', '#2980b9', '#16a085', '#d35400', '#7f8c8d', '#2c3e50' | |
| ] | |
| color_index = 0 | |
| # Add each parameter as a separate line on the same chart | |
| for param_code, param_data in forecast_data.items(): | |
| if param_data: # Check if we have data for this parameter | |
| param_info = self.ecmwf_data.parameters.get(param_code, {}) | |
| param_name = param_info.get('name', param_code) | |
| param_units = param_info.get('units', 'units') | |
| # Extract forecast hours (X-axis) and values (Y-axis) | |
| forecast_hours = [d['step'] for d in param_data] | |
| values = [d['value'] for d in param_data] | |
| # Convert units for better readability | |
| if param_code == '2t' or param_code == '2d': # Temperature | |
| values = [v - 273.15 if v > 100 else v for v in values] # K to Β°C | |
| display_units = 'Β°C' | |
| elif param_code in ['msl', 'sp']: # Pressure | |
| values = [v/100 for v in values] # Pa to hPa | |
| display_units = 'hPa' | |
| elif param_code == 'tp': # Precipitation | |
| values = [v*1000 for v in values] # m to mm | |
| display_units = 'mm' | |
| elif param_code == 'tcc': # Cloud cover | |
| values = [v*100 for v in values] # fraction to percentage | |
| display_units = '%' | |
| elif param_code == 'ssrd': # Solar radiation | |
| values = [v/(3600*3) for v in values] # J/mΒ² to W/mΒ² (3-hour accumulation) | |
| display_units = 'W/mΒ²' | |
| else: | |
| display_units = param_units | |
| # Add the trace | |
| fig.add_trace( | |
| go.Scatter( | |
| x=forecast_hours, | |
| y=values, | |
| mode='lines+markers', | |
| name=f'{param_name} ({display_units})', | |
| line=dict( | |
| color=colors[color_index % len(colors)], | |
| width=3 | |
| ), | |
| marker=dict(size=6), | |
| hovertemplate=f'<b>{param_name}</b><br>' + | |
| 'Forecast Hour: %{x}<br>' + | |
| f'Value: %{{y}} {display_units}<br>' + | |
| '<extra></extra>' | |
| ) | |
| ) | |
| color_index += 1 | |
| # Clean, simple layout | |
| location_str = f"π Bozeman, Montana ({latitude:.4f}Β°N, {longitude:.4f}Β°W)" if abs(latitude - 45.6796) < 0.01 else f"π Custom Location ({latitude:.4f}Β°N, {longitude:.4f}Β°W/E)" | |
| fig.update_layout( | |
| title={ | |
| 'text': f'π ECMWF Forecast Data - All Parameters<br>{location_str}', | |
| 'x': 0.5, | |
| 'xanchor': 'center', | |
| 'font': {'size': 18, 'color': '#2c3e50'} | |
| }, | |
| xaxis_title="Forecast Hours Ahead", | |
| yaxis_title="Parameter Values (Various Units)", | |
| height=700, | |
| showlegend=True, | |
| legend=dict( | |
| orientation="v", | |
| yanchor="top", | |
| y=1, | |
| xanchor="left", | |
| x=1.02, | |
| font=dict(size=11) | |
| ), | |
| plot_bgcolor='white', | |
| paper_bgcolor='#f8f9fa', | |
| margin=dict(t=100, b=60, l=80, r=200), | |
| hovermode='x unified', | |
| xaxis=dict( | |
| gridcolor='lightgray', | |
| gridwidth=1, | |
| range=[-2, 125], | |
| dtick=12 | |
| ), | |
| yaxis=dict( | |
| gridcolor='lightgray', | |
| gridwidth=1 | |
| ) | |
| ) | |
| # Save plot | |
| plot_path = os.path.join(self.temp_dir, f'comprehensive_forecast_{datetime.now().strftime("%Y%m%d_%H%M%S")}.html') | |
| fig.write_html(plot_path) | |
| # Read HTML content | |
| with open(plot_path, 'r', encoding='utf-8') as f: | |
| plot_html = f.read() | |
| return plot_html, "Comprehensive forecast visualization created successfully" | |
| except Exception as e: | |
| return None, f"Error creating visualization: {str(e)}" | |
| def create_data_table(self, all_data, latitude, longitude): | |
| """Create organized data table: parameters grouped by units and sorted by valid time""" | |
| try: | |
| if not all_data: | |
| return "No data available" | |
| df = pd.DataFrame(all_data) | |
| # Group data by units for organized display | |
| unit_groups = {} | |
| for _, row in df.iterrows(): | |
| param_code = row['parameter'] | |
| # Standardize units for display | |
| if param_code in ['2t', '2d']: | |
| display_units = 'Β°C' | |
| display_value = row['value'] - 273.15 if row['value'] > 100 else row['value'] | |
| elif param_code in ['msl', 'sp']: | |
| display_units = 'hPa' | |
| display_value = row['value'] / 100 | |
| elif param_code == 'tp': | |
| display_units = 'mm' | |
| display_value = row['value'] * 1000 | |
| elif param_code == 'tcc': | |
| display_units = '%' | |
| display_value = row['value'] * 100 | |
| elif param_code == 'ssrd': | |
| display_units = 'W/mΒ²' | |
| display_value = row['value'] / (3600 * 3) # Convert J/mΒ² to W/mΒ² | |
| else: | |
| display_units = row['units'] | |
| display_value = row['value'] | |
| if display_units not in unit_groups: | |
| unit_groups[display_units] = [] | |
| unit_groups[display_units].append({ | |
| 'param_name': row['param_name'], | |
| 'step': row['step'], | |
| 'value': display_value, | |
| 'units': display_units, | |
| 'valid_time': row['datetime'] | |
| }) | |
| # Create organized table | |
| table_html = f""" | |
| <div style="background-color: #ffffff; border: 1px solid #ddd; border-radius: 8px; padding: 15px; margin: 10px 0;"> | |
| <h3 style="color: #333; margin-top: 0; border-bottom: 2px solid #007acc; padding-bottom: 10px;"> | |
| π Organized Forecast Data: {latitude:.4f}Β°N, {longitude:.4f}Β°W/E | |
| </h3> | |
| <p style="color: #666; margin-bottom: 20px;"> | |
| Data organized by units and sorted by forecast time. All values converted to standard meteorological units. | |
| </p> | |
| """ | |
| # Create a table for each unit group | |
| unit_order = ['Β°C', 'hPa', 'm/s', 'mm', '%', 'W/mΒ²', 'kg/mΒ²', 'J/kg', 'sβ»ΒΉ', 'degrees', 'J/mΒ²'] | |
| sorted_units = sorted(unit_groups.keys(), key=lambda x: unit_order.index(x) if x in unit_order else 999) | |
| for unit in sorted_units: | |
| data_for_unit = unit_groups[unit] | |
| # Sort by forecast step (time) | |
| data_for_unit.sort(key=lambda x: (x['step'], x['param_name'])) | |
| table_html += f""" | |
| <div style="margin-bottom: 25px;"> | |
| <h4 style="color: #007acc; margin-bottom: 10px; padding: 8px; background-color: #f0f8ff; border-left: 4px solid #007acc;"> | |
| π Parameters in {unit} | |
| </h4> | |
| <div style="overflow-x: auto;"> | |
| <table style="border-collapse: collapse; width: 100%; font-size: 13px;"> | |
| <thead> | |
| <tr style="background: linear-gradient(135deg, #007acc, #0056b3); color: white;"> | |
| <th style="padding: 10px 6px; border: 1px solid #0056b3; font-weight: bold;">Parameter</th> | |
| <th style="padding: 10px 6px; border: 1px solid #0056b3; font-weight: bold;">+Hours</th> | |
| <th style="padding: 10px 6px; border: 1px solid #0056b3; font-weight: bold;">Value ({unit})</th> | |
| <th style="padding: 10px 6px; border: 1px solid #0056b3; font-weight: bold;">Valid Time (UTC)</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| """ | |
| row_count = 0 | |
| for item in data_for_unit: | |
| bg_color = "#f8f9fa" if row_count % 2 == 0 else "#ffffff" | |
| row_count += 1 | |
| # Format value based on magnitude | |
| if abs(item['value']) >= 1000: | |
| value_display = f"{item['value']:,.0f}" | |
| elif abs(item['value']) >= 10: | |
| value_display = f"{item['value']:.1f}" | |
| else: | |
| value_display = f"{item['value']:.2f}" | |
| table_html += f""" | |
| <tr style="background-color: {bg_color}; border-bottom: 1px solid #dee2e6;" | |
| onmouseover="this.style.backgroundColor='#e3f2fd';" | |
| onmouseout="this.style.backgroundColor='{bg_color}';"> | |
| <td style="padding: 8px 6px; border: 1px solid #dee2e6; color: #333; font-weight: 500;">{item['param_name']}</td> | |
| <td style="padding: 8px 6px; border: 1px solid #dee2e6; color: #333; text-align: center;">+{item['step']}</td> | |
| <td style="padding: 8px 6px; border: 1px solid #dee2e6; color: #333; text-align: right; font-weight: bold;">{value_display}</td> | |
| <td style="padding: 8px 6px; border: 1px solid #dee2e6; color: #333; font-family: monospace; font-size: 12px;">{item['valid_time'].strftime('%Y-%m-%d %H:%M')}</td> | |
| </tr> | |
| """ | |
| table_html += """ | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| """ | |
| table_html += """ | |
| <div style="margin-top: 20px; padding: 12px; background-color: #e8f4fd; border-radius: 5px; font-size: 12px; color: #666;"> | |
| <strong>π Data Organization:</strong><br> | |
| β’ Parameters grouped by common units for easy comparison<br> | |
| β’ Values converted to standard meteorological units<br> | |
| β’ Sorted by forecast time within each unit group<br> | |
| β’ Updated every 6 hours from ECMWF operational forecasts | |
| </div> | |
| </div> | |
| """ | |
| return table_html | |
| except Exception as e: | |
| return f"Error creating organized data table: {str(e)}" | |
| # Initialize the data access and interactive map | |
| ecmwf_data = ECMWFOpenDataAccess() | |
| interactive_map = InteractiveECMWFMap(ecmwf_data) | |
| def get_real_weather_data(parameter, forecast_step): | |
| """Main function to get and visualize real ECMWF data""" | |
| try: | |
| # Download real ECMWF data | |
| filename, download_msg = ecmwf_data.download_ecmwf_data(parameter, forecast_step) | |
| if filename is None: | |
| return download_msg, None, "Download failed - no visualization available" | |
| # Create visualization | |
| plot_path, summary = ecmwf_data.create_weather_visualization(filename, parameter) | |
| if plot_path is None: | |
| return download_msg + "\n\n" + summary, None, "Visualization failed" | |
| return download_msg, plot_path, summary | |
| except Exception as e: | |
| return f"Error: {str(e)}", None, "Please try again or select different parameters" | |
| def check_ecmwf_status(): | |
| """Check ECMWF open data service status""" | |
| try: | |
| date_str, time_str, run_time = ecmwf_data.get_latest_forecast_info() | |
| status_msg = f"""π ECMWF Open Data Service Status | |
| β Service: Available | |
| β Authentication: Not required | |
| β API Keys: Not needed | |
| β Cost: Completely FREE | |
| Latest Available Forecast: | |
| β’ Date: {date_str} | |
| β’ Run: {time_str}z UTC | |
| β’ Model: IFS Operational | |
| β’ Resolution: 0.25Β° (~25km global) | |
| β’ Update Frequency: Every 6 hours | |
| Available Parameters: {len(ecmwf_data.parameters)} | |
| Forecast Range: 0-240 hours ahead | |
| Data Source: https://www.ecmwf.int/en/forecasts/datasets/open-data | |
| Access: Direct download from ECMWF's AWS S3 buckets""" | |
| return "β ECMWF Open Data is accessible!", status_msg | |
| except Exception as e: | |
| return f"β Service check failed: {str(e)}", "Please check your internet connection" | |
| def get_interactive_map(): | |
| """Generate the interactive Folium map""" | |
| try: | |
| map_html = interactive_map.create_interactive_map() | |
| # Add usage instructions below the map | |
| instructions_html = """ | |
| <div style="margin-top: 15px; padding: 15px; background-color: #e8f4fd; border-radius: 8px; border-left: 4px solid #007acc;"> | |
| <h4 style="color: #007acc; margin-top: 0;">πΊοΈ How to Use the Interactive Map:</h4> | |
| <ul style="color: #333; line-height: 1.6;"> | |
| <li><strong>Click anywhere</strong> on the map to select a location</li> | |
| <li><strong>Enter coordinates</strong> manually in the input fields on the right</li> | |
| <li><strong>Click "π Get Point Forecast"</strong> to retrieve detailed weather data</li> | |
| <li><strong>Use layer control</strong> (top-right) to switch between map styles</li> | |
| </ul> | |
| <p style="color: #666; font-size: 12px; margin-bottom: 0;"> | |
| <em>Note: If the map doesn't load, you can still use the coordinate inputs to get forecast data.</em> | |
| </p> | |
| </div> | |
| """ | |
| return map_html + instructions_html | |
| except Exception as e: | |
| # Return a user-friendly fallback with manual coordinate entry | |
| return f""" | |
| <div style="padding: 20px; background-color: #fff3cd; border: 1px solid #ffeaa7; border-radius: 8px; text-align: center;"> | |
| <h4 style="color: #856404;">β οΈ Map Loading Issue</h4> | |
| <p style="color: #856404;">The interactive map could not be loaded: {str(e)}</p> | |
| <p style="color: #856404;"><strong>Don't worry!</strong> You can still get weather forecasts by entering coordinates manually.</p> | |
| </div> | |
| <div style="margin-top: 15px; padding: 15px; background-color: #d4edda; border: 1px solid #c3e6cb; border-radius: 8px;"> | |
| <h4 style="color: #155724; margin-top: 0;">π Manual Coordinate Entry:</h4> | |
| <ol style="color: #155724; text-align: left; line-height: 1.6;"> | |
| <li>Enter <strong>Latitude</strong> (-90 to 90) in the input field</li> | |
| <li>Enter <strong>Longitude</strong> (-180 to 180) in the input field</li> | |
| <li>Click <strong>"π Get Point Forecast"</strong> to retrieve data</li> | |
| </ol> | |
| <p style="color: #155724; font-size: 12px; margin-bottom: 0;"> | |
| <em>Example: London = 51.5, -0.1 | New York = 40.7, -74.0 | Tokyo = 35.7, 139.7</em> | |
| </p> | |
| </div> | |
| """ | |
| def preload_forecast_data(): | |
| """Preload all forecast data and get Bozeman forecast""" | |
| try: | |
| # Preload all forecast data | |
| success, msg = interactive_map.preload_all_forecast_data() | |
| if success: | |
| # Auto-generate Bozeman forecast | |
| bozeman_lat, bozeman_lon = 45.6796, -111.0447 | |
| forecast_data, all_data = interactive_map.get_point_forecast_data(bozeman_lat, bozeman_lon) | |
| if forecast_data: | |
| # Create visualization for Bozeman | |
| plot_html, plot_msg = interactive_map.create_forecast_visualization(forecast_data, bozeman_lat, bozeman_lon) | |
| table_html = interactive_map.create_data_table(all_data, bozeman_lat, bozeman_lon) | |
| cache_info = interactive_map.get_cache_info() | |
| status_msg = f"""π ECMWF Data Successfully Preloaded! | |
| ποΈ Showing forecast for Bozeman, Montana ({bozeman_lat:.4f}Β°N, {bozeman_lon:.4f}Β°W) | |
| β ALL EXTENDED GLOBAL DATA DOWNLOADED: | |
| β’ {cache_info['cached_files']} GRIB files cached ({cache_info['cache_size_mb']} MB) | |
| β’ 10 core weather parameters Γ 13 time steps | |
| β’ Global coverage at 0.25Β° resolution (~25km) | |
| β’ Extended 120-hour forecast range (5 full days) | |
| π NOW READY FOR INSTANT FORECASTS: | |
| β’ Click anywhere on the map for instant results | |
| β’ Or enter any coordinates manually | |
| β’ All subsequent forecasts will be lightning fast! | |
| π Enhanced Forecast Display: | |
| β’ Parameters: {len(forecast_data)} weather variables | |
| β’ Extended time steps: 0, 3, 6, 12, 18, 24, 36, 48, 60, 72, 84, 96, 120 hours | |
| β’ Total data points: {len(all_data)} | |
| β’ Professional grouped time-series charts | |
| β’ Organized by weather parameter categories""" | |
| return status_msg, plot_html if plot_html else "", table_html | |
| else: | |
| return f"β Data preloaded successfully! {msg}\nClick on the map or enter coordinates to get forecasts.", "", "" | |
| else: | |
| return f"β Preloading failed: {msg}", "", "" | |
| except Exception as e: | |
| return f"β Error during preloading: {str(e)}", "", "" | |
| def rapid_preload_forecast_data(): | |
| """Rapid preload essential forecast data and get Bozeman forecast""" | |
| try: | |
| # Preload rapid forecast data | |
| success, msg = interactive_map.preload_rapid_forecast_data() | |
| if success: | |
| # Auto-generate Bozeman forecast | |
| bozeman_lat, bozeman_lon = 45.6796, -111.0447 | |
| forecast_data, all_data = interactive_map.get_point_forecast_data(bozeman_lat, bozeman_lon) | |
| if forecast_data: | |
| # Create visualization for Bozeman | |
| plot_html, plot_msg = interactive_map.create_forecast_visualization(forecast_data, bozeman_lat, bozeman_lon) | |
| table_html = interactive_map.create_data_table(all_data, bozeman_lat, bozeman_lon) | |
| cache_info = interactive_map.get_cache_info() | |
| status_msg = f"""β‘ ECMWF RAPID Data Successfully Preloaded! | |
| ποΈ Showing forecast for Bozeman, Montana ({bozeman_lat:.4f}Β°N, {bozeman_lon:.4f}Β°W) | |
| β ESSENTIAL GLOBAL DATA DOWNLOADED (RAPID MODE): | |
| β’ {cache_info['cached_files']} GRIB files cached ({cache_info['cache_size_mb']} MB) | |
| β’ 5 essential weather parameters Γ 6 time steps | |
| β’ Global coverage at 0.25Β° resolution (~25km) | |
| β’ Rapid 72-hour forecast range (3 days) | |
| β‘ PARAMETERS IN RAPID MODE: | |
| β’ Temperature (2t), Pressure (msl), Wind U/V (10u/10v), Precipitation (tp) | |
| β’ Optimized for quick processing and essential weather information | |
| π NOW READY FOR INSTANT FORECASTS: | |
| β’ Click anywhere on the map for instant results | |
| β’ Or enter any coordinates manually | |
| β’ All subsequent forecasts will be lightning fast! | |
| π Forecast Display: | |
| β’ Parameters: {len(forecast_data)} weather variables | |
| β’ Time steps: 0, 6, 12, 24, 48, 72 hours | |
| β’ Total data points: {len(all_data)} | |
| β’ Focused on essential meteorological data""" | |
| return status_msg, plot_html if plot_html else "", table_html | |
| else: | |
| return f"β Rapid data preloaded successfully! {msg}\nClick on the map or enter coordinates to get forecasts.", "", "" | |
| else: | |
| return f"β Rapid preloading failed: {msg}", "", "" | |
| except Exception as e: | |
| return f"β Error during rapid preloading: {str(e)}", "", "" | |
| def get_point_forecast(latitude, longitude): | |
| """Get comprehensive forecast data for a clicked point""" | |
| try: | |
| # Validate inputs | |
| lat = float(latitude) | |
| lon = float(longitude) | |
| if lat < -90 or lat > 90: | |
| return "Invalid latitude. Must be between -90 and 90.", "", "" | |
| if lon < -180 or lon > 180: | |
| return "Invalid longitude. Must be between -180 and 180.", "", "" | |
| # Get forecast data (will use cached data if available) | |
| forecast_data, all_data = interactive_map.get_point_forecast_data(lat, lon) | |
| if not forecast_data: | |
| return f"No forecast data available for location {lat:.3f}Β°N, {lon:.3f}Β°E", "", "" | |
| # Create visualization | |
| plot_html, plot_msg = interactive_map.create_forecast_visualization(forecast_data, lat, lon) | |
| # Create data table | |
| table_html = interactive_map.create_data_table(all_data, lat, lon) | |
| # Get cache information | |
| cache_info = interactive_map.get_cache_info() | |
| # Determine location name | |
| location_name = "" | |
| if abs(lat - 45.6796) < 0.01 and abs(lon + 111.0447) < 0.01: | |
| location_name = "ποΈ Bozeman, Montana" | |
| elif abs(lat - 51.5) < 0.1 and abs(lon + 0.1) < 0.1: | |
| location_name = "π¬π§ London, UK" | |
| elif abs(lat - 40.7) < 0.1 and abs(lon + 74.0) < 0.1: | |
| location_name = "π½ New York, USA" | |
| elif abs(lat - 35.7) < 0.1 and abs(lon - 139.7) < 0.1: | |
| location_name = "πΌ Tokyo, Japan" | |
| status_msg = f"""β β‘ INSTANT Extended Forecast Retrieved! | |
| π Location: {location_name} ({lat:.4f}Β°N, {lon:.4f}Β°W/E) | |
| π Parameters: {len(forecast_data)} weather variables | |
| β° Extended forecast: 0 to 120 hours (5 full days ahead) | |
| π Total data points: {len(all_data)} with 13 time steps | |
| π― ENHANCED Weather Analysis: | |
| β’ Temperature & humidity trends (Β°C) | |
| β’ Pressure systems analysis (hPa) | |
| β’ Complete wind analysis (10m & 100m levels) | |
| β’ Precipitation & cloud cover patterns | |
| β’ Solar radiation & energy balance | |
| β’ Atmospheric water vapor dynamics | |
| β’ Advanced meteorological parameters | |
| π Professional Time-Series Charts: | |
| β’ Organized by parameter groups | |
| β’ Extended 120-hour range | |
| β’ Time on X-axis, values on Y-axis | |
| β’ Professional color coding | |
| β’ Interactive plotly visualization | |
| π¦ Data System Status: | |
| β’ Cached files: {cache_info['cached_files']} GRIB files | |
| β’ Cache size: {cache_info['cache_size_mb']} MB | |
| β’ β‘ Lightning-fast extraction from global data | |
| β’ π Ready for ANY location worldwide!""" | |
| return status_msg, plot_html if plot_html else "", table_html | |
| except ValueError: | |
| return "Please enter valid latitude and longitude values.", "", "" | |
| except Exception as e: | |
| return f"Error retrieving forecast data: {str(e)}", "", "" | |
| # Create the Gradio interface | |
| def create_ecmwf_app(): | |
| with gr.Blocks(title="ECMWF Open Data Explorer") as app: | |
| gr.Markdown(""" | |
| # π ECMWF Open Data Explorer | |
| ## π REAL WEATHER DATA - NO API KEYS REQUIRED! π | |
| **Access professional ECMWF operational forecasts under CC BY 4.0 license** | |
| β¨ Real ECMWF IFS Data β’ π Global Coverage β’ π‘ Direct Access β’ π Updated Every 6 Hours | |
| """) | |
| with gr.Tabs(): | |
| # Tab 1: Real ECMWF Data | |
| with gr.TabItem("π Real ECMWF Forecasts"): | |
| gr.Markdown("### Download and Visualize Real ECMWF Operational Forecast Data") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| param_choice = gr.Radio( | |
| choices=list(ecmwf_data.parameters.keys()), | |
| value="2t", | |
| label="Weather Parameter" | |
| ) | |
| step_choice = gr.Radio( | |
| choices=["0", "6", "12", "24", "48", "72", "120"], | |
| value="0", | |
| label="Forecast Hours Ahead" | |
| ) | |
| # Show parameter info | |
| def update_param_info(param): | |
| info = ecmwf_data.parameters.get(param, {}) | |
| return f"**{info.get('name', param)}**\nUnits: {info.get('units', 'N/A')}\n{info.get('description', 'No description')}" | |
| param_info = gr.Textbox( | |
| label="Parameter Information", | |
| value=update_param_info("2t"), | |
| lines=3, | |
| interactive=False | |
| ) | |
| param_choice.change(update_param_info, param_choice, param_info) | |
| download_btn = gr.Button("π Get Real ECMWF Data", variant="primary", size="lg") | |
| with gr.Column(scale=2): | |
| status_output = gr.Textbox(label="Download Status", lines=4) | |
| weather_plot = gr.Image(label="ECMWF Weather Map") | |
| data_summary = gr.Textbox(label="Data Information", lines=15) | |
| download_btn.click( | |
| get_real_weather_data, | |
| inputs=[param_choice, step_choice], | |
| outputs=[status_output, weather_plot, data_summary] | |
| ) | |
| # Tab 2: Service Status | |
| with gr.TabItem("π‘ Service Status"): | |
| gr.Markdown("### ECMWF Open Data Service Information") | |
| status_btn = gr.Button("π Check ECMWF Service Status", variant="secondary") | |
| service_status = gr.Textbox(label="Service Status", lines=2) | |
| service_info = gr.Textbox(label="Detailed Information", lines=15) | |
| status_btn.click( | |
| check_ecmwf_status, | |
| outputs=[service_status, service_info] | |
| ) | |
| # Tab 3: Interactive Point Forecasts | |
| with gr.TabItem("πΊοΈ Interactive Map Forecasts"): | |
| gr.Markdown("### Get Detailed Forecast Data for Any Location") | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| # Interactive map display with initial placeholder | |
| map_display = gr.HTML( | |
| value=""" | |
| <div style="padding: 20px; background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 8px; text-align: center; height: 500px; display: flex; align-items: center; justify-content: center;"> | |
| <div> | |
| <h4 style="color: #007acc;">πΊοΈ Interactive Weather Map</h4> | |
| <p style="color: #666;">Click "π Load Map" to display the interactive map</p> | |
| <p style="color: #666;">Or use the coordinate inputs to enter your location manually</p> | |
| </div> | |
| </div> | |
| """, | |
| label="Interactive Map" | |
| ) | |
| refresh_map_btn = gr.Button("π Load Map", variant="secondary") | |
| with gr.Column(scale=1): | |
| gr.Markdown("### π Quick Start") | |
| preload_btn = gr.Button("π PRELOAD ALL DATA & Show Bozeman Forecast", variant="primary", size="lg") | |
| rapid_preload_btn = gr.Button("β‘ RAPID PRELOAD (Essential Data) - Faster", variant="secondary", size="lg") | |
| gr.Markdown(""" | |
| **Data Processing Modes:** | |
| - **Full Mode**: 10 parameters Γ 13 time steps (130 files, ~5+ min download) | |
| - **Rapid Mode**: 5 essential parameters Γ 6 time steps (30 files, ~1-2 min download) | |
| """) | |
| gr.Markdown("### π Custom Location") | |
| gr.Markdown("Click map or enter coordinates:") | |
| lat_input = gr.Number( | |
| label="Latitude (Bozeman, Montana)", | |
| value=45.6796, | |
| minimum=-90, | |
| maximum=90, | |
| step=0.001, | |
| precision=4 | |
| ) | |
| lon_input = gr.Number( | |
| label="Longitude (Bozeman, Montana)", | |
| value=-111.0447, | |
| minimum=-180, | |
| maximum=180, | |
| step=0.001, | |
| precision=4 | |
| ) | |
| get_forecast_btn = gr.Button("β‘ Get Instant Forecast", variant="secondary", size="lg") | |
| point_status = gr.Textbox(label="Status", lines=12) | |
| with gr.Row(): | |
| with gr.Column(): | |
| forecast_charts = gr.HTML(label="Forecast Charts") | |
| with gr.Column(): | |
| forecast_table = gr.HTML(label="Complete Data Table") | |
| # Event handlers | |
| refresh_map_btn.click( | |
| get_interactive_map, | |
| outputs=[map_display] | |
| ) | |
| preload_btn.click( | |
| preload_forecast_data, | |
| outputs=[point_status, forecast_charts, forecast_table] | |
| ) | |
| rapid_preload_btn.click( | |
| rapid_preload_forecast_data, | |
| outputs=[point_status, forecast_charts, forecast_table] | |
| ) | |
| get_forecast_btn.click( | |
| get_point_forecast, | |
| inputs=[lat_input, lon_input], | |
| outputs=[point_status, forecast_charts, forecast_table] | |
| ) | |
| # Tab 4: Information | |
| with gr.TabItem("π About ECMWF Open Data"): | |
| gr.Markdown(""" | |
| # π About ECMWF Open Data | |
| ## π **Open Access to Professional Weather Data (CC BY 4.0)** | |
| ### What is ECMWF Open Data? | |
| The **European Centre for Medium-Range Weather Forecasts (ECMWF)** provides free access to their operational forecast data through their Open Data initiative. This includes: | |
| β **IFS Operational Forecasts** - The same data used by meteorologists worldwide | |
| β **Global Coverage** - Complete Earth coverage at 0.25Β° resolution (~25km) | |
| β **Real-time Updates** - New forecasts every 6 hours (00, 06, 12, 18 UTC) | |
| β **Professional Quality** - Industry-standard numerical weather prediction | |
| β **No Authentication** - Direct access without API keys or registration | |
| ### Available Parameters | |
| | Code | Parameter | Units | Description | | |
| |------|-----------|-------|-------------| | |
| | **2t** | 2m Temperature | K (Β°C) | Air temperature at 2 meters height | | |
| | **msl** | Mean Sea Level Pressure | Pa | Atmospheric pressure at sea level | | |
| | **10u** | 10m U Wind Component | m/s | Eastward wind component | | |
| | **10v** | 10m V Wind Component | m/s | Northward wind component | | |
| | **tp** | Total Precipitation | m | Accumulated precipitation | | |
| | **2d** | 2m Dewpoint Temperature | K | Dewpoint at 2 meters | | |
| | **sp** | Surface Pressure | Pa | Pressure at surface level | | |
| | **tcwv** | Total Column Water Vapour | kg/mΒ² | Atmospheric water content | | |
| ### Forecast Steps Available | |
| - **0 hours**: Current analysis/nowcast | |
| - **6-72 hours**: Short-range forecasts (high accuracy) | |
| - **120+ hours**: Medium-range forecasts (5+ days ahead) | |
| ### Technical Details | |
| **Model**: IFS (Integrated Forecast System) | |
| **Resolution**: 0.25Β° latitude/longitude (~25km spacing) | |
| **Domain**: Global (90Β°N to 90Β°S, 180Β°W to 180Β°E) | |
| **Format**: GRIB2 (industry standard) | |
| **Update Frequency**: 4 times daily (00, 06, 12, 18 UTC) | |
| **Availability**: 7-9 hours after model run time | |
| ### Data Access Methods | |
| This application uses multiple access methods for reliability: | |
| 1. **Official ECMWF OpenData Client** - Primary method using ecmwf-opendata package | |
| 2. **Direct AWS S3 Access** - Backup method via Amazon S3 buckets | |
| 3. **Automatic Fallback** - Tries alternative forecast times if latest unavailable | |
| ### Why This Data is Special | |
| π **World-Leading Accuracy** - ECMWF consistently ranks #1 in forecast skill | |
| π **Global Standard** - Used by meteorological services worldwide | |
| π¬ **Scientific Quality** - Suitable for research and commercial applications | |
| π± **Accessible Format** - Easy to process and visualize | |
| π **Real-time** - Same data feed used for operational weather forecasting | |
| ### Perfect For | |
| - **Students** learning meteorology and atmospheric science | |
| - **Researchers** needing high-quality weather data | |
| - **Developers** building weather applications | |
| - **Educators** teaching weather and climate concepts | |
| - **Hobbyists** interested in weather analysis | |
| ### Data Usage and Licensing | |
| β **ECMWF Data License** - CC BY 4.0 (Attribution Required) | |
| β **No Registration Required** - Anonymous access | |
| β **No API Limits** - Reasonable use policy | |
| β **Commercial Use Allowed** - With proper attribution | |
| **Attribution Requirements for ECMWF Data:** | |
| - Must credit ECMWF as data source | |
| - Include link to ECMWF Open Data portal | |
| - Mention CC BY 4.0 license when redistributing | |
| --- | |
| **Data Source**: [ECMWF Open Data](https://www.ecmwf.int/en/forecasts/datasets/open-data) | |
| **Technical Documentation**: [ECMWF Data Portal](https://data.ecmwf.int/) | |
| **Model Information**: [IFS Documentation](https://www.ecmwf.int/en/forecasts/documentation-and-support) | |
| """) | |
| gr.Markdown(""" | |
| --- | |
| **π Real ECMWF Data - Professional Weather Forecasts Made Accessible** | |
| *Powered by ECMWF's Open Data initiative - Licensed under CC BY 4.0* | |
| """) | |
| return app | |
| if __name__ == "__main__": | |
| app = create_ecmwf_app() | |
| app.launch(server_name="0.0.0.0", server_port=7860) |