Spaces:
Sleeping
Sleeping
Remove all fallback data - fail fast if DWD ICON unavailable
Browse files- Remove fetch_fallback_data() and process_fallback_data() functions
- Replace all fallback calls with proper error exceptions
- Update error handling to fail cleanly with informative messages
- Remove synthetic data generation and fallback processing
- Simplify plot creation by removing fallback data handling
- Application now works only with real DWD ICON data or fails
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
app.py
CHANGED
|
@@ -226,8 +226,7 @@ def fetch_dwd_icon_data(lat, lon):
|
|
| 226 |
print(f"Fetching real DWD ICON data for {lat:.3f}°N, {lon:.3f}°E")
|
| 227 |
|
| 228 |
if not GRIB_AVAILABLE:
|
| 229 |
-
|
| 230 |
-
return fetch_fallback_data(lat, lon)
|
| 231 |
|
| 232 |
# Get latest model run
|
| 233 |
run_date = get_latest_dwd_run()
|
|
@@ -250,16 +249,14 @@ def fetch_dwd_icon_data(lat, lon):
|
|
| 250 |
clat_file, clon_file = download_dwd_coordinate_files(run_date)
|
| 251 |
|
| 252 |
if not clat_file or not clon_file:
|
| 253 |
-
|
| 254 |
-
return fetch_fallback_data(lat, lon)
|
| 255 |
|
| 256 |
# Parse coordinate files
|
| 257 |
clat_ds = parse_grib_file(clat_file)
|
| 258 |
clon_ds = parse_grib_file(clon_file)
|
| 259 |
|
| 260 |
if clat_ds is None or clon_ds is None:
|
| 261 |
-
|
| 262 |
-
return fetch_fallback_data(lat, lon)
|
| 263 |
|
| 264 |
# Get coordinate arrays
|
| 265 |
# DWD coordinate files may use different variable names
|
|
@@ -287,7 +284,7 @@ def fetch_dwd_icon_data(lat, lon):
|
|
| 287 |
print(f"Error extracting coordinate arrays: {e}")
|
| 288 |
print(f"CLAT dataset: {clat_ds}")
|
| 289 |
print(f"CLON dataset: {clon_ds}")
|
| 290 |
-
|
| 291 |
|
| 292 |
# Find nearest grid point
|
| 293 |
nearest_idx = find_nearest_grid_point(lat, lon, grid_lats, grid_lons)
|
|
@@ -353,76 +350,8 @@ def fetch_dwd_icon_data(lat, lon):
|
|
| 353 |
print(f"Error fetching real DWD ICON data: {e}")
|
| 354 |
import traceback
|
| 355 |
traceback.print_exc()
|
| 356 |
-
|
| 357 |
|
| 358 |
-
def fetch_fallback_data(lat, lon):
|
| 359 |
-
"""
|
| 360 |
-
Generate realistic fallback data when real DWD data is unavailable
|
| 361 |
-
"""
|
| 362 |
-
print("Using fallback synthetic data")
|
| 363 |
-
|
| 364 |
-
current_time = datetime.now(timezone.utc)
|
| 365 |
-
forecast_hours = []
|
| 366 |
-
|
| 367 |
-
# Generate forecast times - 4 days with 6-hour intervals
|
| 368 |
-
for i in range(0, 97, 6): # Every 6 hours for 4 days
|
| 369 |
-
forecast_time = current_time + timedelta(hours=i)
|
| 370 |
-
forecast_hours.append(forecast_time)
|
| 371 |
-
|
| 372 |
-
# Create realistic weather patterns based on location and season
|
| 373 |
-
import math
|
| 374 |
-
base_temp = 15 + 10 * math.sin((current_time.timetuple().tm_yday - 80) * 2 * math.pi / 365)
|
| 375 |
-
|
| 376 |
-
simulated_data = {
|
| 377 |
-
"location": {"lat": lat, "lon": lon, "name": f"Location {lat:.2f}°N, {lon:.2f}°E"},
|
| 378 |
-
"current": {
|
| 379 |
-
"temp_c": base_temp,
|
| 380 |
-
"humidity": 65,
|
| 381 |
-
"wind_kph": 15,
|
| 382 |
-
"pressure_mb": 1013,
|
| 383 |
-
"cloud": 40,
|
| 384 |
-
"vis_km": 10
|
| 385 |
-
},
|
| 386 |
-
"forecast": {
|
| 387 |
-
"forecastday": []
|
| 388 |
-
}
|
| 389 |
-
}
|
| 390 |
-
|
| 391 |
-
# Generate daily data
|
| 392 |
-
current_day = None
|
| 393 |
-
day_data = None
|
| 394 |
-
|
| 395 |
-
for i, forecast_time in enumerate(forecast_hours):
|
| 396 |
-
if forecast_time.date() != current_day:
|
| 397 |
-
if day_data:
|
| 398 |
-
simulated_data["forecast"]["forecastday"].append(day_data)
|
| 399 |
-
|
| 400 |
-
current_day = forecast_time.date()
|
| 401 |
-
day_data = {
|
| 402 |
-
"date": current_day.strftime("%Y-%m-%d"),
|
| 403 |
-
"hour": []
|
| 404 |
-
}
|
| 405 |
-
|
| 406 |
-
# Generate hourly data
|
| 407 |
-
hour_temp = base_temp + 5 * math.sin(forecast_time.hour * math.pi / 12) + 2 * math.sin(i * 0.1)
|
| 408 |
-
hour_data = {
|
| 409 |
-
"time": forecast_time.strftime("%Y-%m-%d %H:%M"),
|
| 410 |
-
"temp_c": hour_temp,
|
| 411 |
-
"humidity": int(60 + 20 * math.cos(forecast_time.hour * math.pi / 12 + i * 0.1)),
|
| 412 |
-
"wind_kph": 10 + 8 * math.sin(forecast_time.hour * math.pi / 8 + i * 0.05),
|
| 413 |
-
"wind_dir": int((forecast_time.hour * 15 + i * 5) % 360),
|
| 414 |
-
"pressure_mb": 1013 + 5 * math.sin(forecast_time.hour * math.pi / 12),
|
| 415 |
-
"precip_mm": max(0, math.sin(forecast_time.hour * math.pi / 6 + i * 0.2) * 0.5),
|
| 416 |
-
"cloud": int(30 + 40 * math.sin(forecast_time.hour * math.pi / 10 + i * 0.15)),
|
| 417 |
-
"vis_km": 10 + 5 * math.cos(forecast_time.hour * math.pi / 12),
|
| 418 |
-
"gust_kph": 15 + 10 * math.sin(forecast_time.hour * math.pi / 6 + i * 0.1)
|
| 419 |
-
}
|
| 420 |
-
day_data["hour"].append(hour_data)
|
| 421 |
-
|
| 422 |
-
if day_data:
|
| 423 |
-
simulated_data["forecast"]["forecastday"].append(day_data)
|
| 424 |
-
|
| 425 |
-
return simulated_data
|
| 426 |
|
| 427 |
def get_forecast_data(lat, lon, forecast_hour="00"):
|
| 428 |
"""
|
|
@@ -434,13 +363,11 @@ def get_forecast_data(lat, lon, forecast_hour="00"):
|
|
| 434 |
# Fetch data from DWD ICON model
|
| 435 |
weather_data = fetch_dwd_icon_data(lat, lon)
|
| 436 |
|
| 437 |
-
#
|
| 438 |
if 'weather_data' in weather_data:
|
| 439 |
-
# Real DWD GRIB2 data
|
| 440 |
return process_real_dwd_data(weather_data, lat, lon)
|
| 441 |
else:
|
| 442 |
-
|
| 443 |
-
return process_fallback_data(weather_data, lat, lon)
|
| 444 |
|
| 445 |
except Exception as e:
|
| 446 |
import traceback
|
|
@@ -449,27 +376,8 @@ def get_forecast_data(lat, lon, forecast_hour="00"):
|
|
| 449 |
print("Full error traceback:")
|
| 450 |
print(traceback.format_exc())
|
| 451 |
|
| 452 |
-
#
|
| 453 |
-
|
| 454 |
-
hours = np.arange(0, forecast_days * 24, 6)
|
| 455 |
-
np.random.seed(int(lat * 100 + lon * 100))
|
| 456 |
-
|
| 457 |
-
current_date = datetime.now()
|
| 458 |
-
timestamps = [current_date + timedelta(hours=int(h)) for h in hours]
|
| 459 |
-
temperature = 15 + 10 * np.sin(hours * np.pi / 12) + np.random.normal(0, 2, len(hours))
|
| 460 |
-
humidity = 60 + 20 * np.sin(hours * np.pi / 24 + np.pi/4) + np.random.normal(0, 5, len(hours))
|
| 461 |
-
wind_speed = 5 + 3 * np.sin(hours * np.pi / 18) + np.random.normal(0, 1, len(hours))
|
| 462 |
-
|
| 463 |
-
return {
|
| 464 |
-
'timestamps': timestamps,
|
| 465 |
-
'temperature': temperature,
|
| 466 |
-
'humidity': humidity,
|
| 467 |
-
'wind_speed': wind_speed,
|
| 468 |
-
'lat': lat,
|
| 469 |
-
'lon': lon,
|
| 470 |
-
'error': error_msg,
|
| 471 |
-
'forecast_date': 'Fallback synthetic data'
|
| 472 |
-
}
|
| 473 |
|
| 474 |
def process_real_dwd_data(dwd_data, lat, lon):
|
| 475 |
"""
|
|
@@ -651,63 +559,6 @@ def process_real_dwd_data(dwd_data, lat, lon):
|
|
| 651 |
print(f"Error processing real DWD data: {e}")
|
| 652 |
raise e
|
| 653 |
|
| 654 |
-
def process_fallback_data(weather_data, lat, lon):
|
| 655 |
-
"""
|
| 656 |
-
Process fallback simulated data into forecast format
|
| 657 |
-
"""
|
| 658 |
-
# Extract hourly forecast data
|
| 659 |
-
timestamps = []
|
| 660 |
-
temperature = []
|
| 661 |
-
humidity = []
|
| 662 |
-
wind_speed = []
|
| 663 |
-
wind_direction = []
|
| 664 |
-
wind_gust = []
|
| 665 |
-
pressure = []
|
| 666 |
-
precipitation = []
|
| 667 |
-
cloud_cover = []
|
| 668 |
-
visibility = []
|
| 669 |
-
|
| 670 |
-
# Process hourly data from all forecast days
|
| 671 |
-
for day_forecast in weather_data["forecast"]["forecastday"]:
|
| 672 |
-
for hour_data in day_forecast["hour"]:
|
| 673 |
-
# Parse timestamp
|
| 674 |
-
timestamp = datetime.strptime(hour_data["time"], "%Y-%m-%d %H:%M")
|
| 675 |
-
timestamps.append(timestamp)
|
| 676 |
-
|
| 677 |
-
# Extract weather variables
|
| 678 |
-
temperature.append(hour_data["temp_c"])
|
| 679 |
-
humidity.append(hour_data["humidity"])
|
| 680 |
-
wind_speed.append(hour_data["wind_kph"] * 0.277778) # Convert kph to m/s
|
| 681 |
-
wind_direction.append(hour_data["wind_dir"])
|
| 682 |
-
wind_gust.append(hour_data["gust_kph"] * 0.277778) # Convert kph to m/s
|
| 683 |
-
pressure.append(hour_data["pressure_mb"])
|
| 684 |
-
precipitation.append(hour_data["precip_mm"])
|
| 685 |
-
cloud_cover.append(hour_data["cloud"])
|
| 686 |
-
visibility.append(hour_data["vis_km"])
|
| 687 |
-
|
| 688 |
-
# Limit to reasonable forecast length (4 days = 96 hours)
|
| 689 |
-
max_hours = min(len(timestamps), 96)
|
| 690 |
-
|
| 691 |
-
result = {
|
| 692 |
-
'timestamps': timestamps[:max_hours],
|
| 693 |
-
'temperature': temperature[:max_hours],
|
| 694 |
-
'humidity': humidity[:max_hours],
|
| 695 |
-
'wind_speed': wind_speed[:max_hours],
|
| 696 |
-
'wind_direction': wind_direction[:max_hours],
|
| 697 |
-
'wind_gust': wind_gust[:max_hours],
|
| 698 |
-
'pressure': pressure[:max_hours],
|
| 699 |
-
'precipitation': precipitation[:max_hours],
|
| 700 |
-
'cloud_cover': cloud_cover[:max_hours],
|
| 701 |
-
'visibility': visibility[:max_hours],
|
| 702 |
-
'lat': lat,
|
| 703 |
-
'lon': lon,
|
| 704 |
-
'forecast_date': datetime.utcnow().strftime('%Y-%m-%d %H:%M UTC'),
|
| 705 |
-
'data_source': 'DWD ICON Model (Simulated)',
|
| 706 |
-
'location_name': weather_data["location"]["name"]
|
| 707 |
-
}
|
| 708 |
-
|
| 709 |
-
print(f"Successfully processed {len(timestamps)} hours of fallback forecast data")
|
| 710 |
-
return result
|
| 711 |
|
| 712 |
def analyze_weather_events(forecast_data):
|
| 713 |
"""
|
|
@@ -1593,8 +1444,8 @@ def create_forecast_plot(forecast_data):
|
|
| 1593 |
ax9 = fig.add_subplot(gs[2, 2])
|
| 1594 |
ax9.axis('off')
|
| 1595 |
|
| 1596 |
-
#
|
| 1597 |
-
data_source = "Real DWD ICON Data"
|
| 1598 |
forecast_info = forecast_data.get('forecast_date', 'Unknown')
|
| 1599 |
|
| 1600 |
# Grid point info
|
|
@@ -1638,11 +1489,7 @@ Wind: {forecast_data['wind_speed'][0]:.1f} m/s
|
|
| 1638 |
if 'pressure' in forecast_data:
|
| 1639 |
summary_text += f"Pressure: {forecast_data['pressure'][0]:.1f} hPa\n"
|
| 1640 |
|
| 1641 |
-
|
| 1642 |
-
if 'error' in forecast_data:
|
| 1643 |
-
summary_text += f"\nNote: Using fallback data\nReason: {forecast_data['error'][:80]}..."
|
| 1644 |
-
|
| 1645 |
-
color = 'lightgreen' if 'error' not in forecast_data else 'lightyellow'
|
| 1646 |
ax9.text(0.05, 0.95, summary_text, transform=ax9.transAxes, fontsize=8,
|
| 1647 |
verticalalignment='top', bbox=dict(boxstyle='round', facecolor=color, alpha=0.7))
|
| 1648 |
|
|
@@ -1655,30 +1502,27 @@ def process_map_click(lat, lon):
|
|
| 1655 |
if lat is None or lon is None:
|
| 1656 |
return "Please click on the map to select a location", None, ""
|
| 1657 |
|
| 1658 |
-
|
| 1659 |
-
|
| 1660 |
-
|
| 1661 |
-
|
| 1662 |
-
|
| 1663 |
-
|
| 1664 |
-
|
| 1665 |
-
|
| 1666 |
-
data_type = "Real DWD ICON Data"
|
| 1667 |
forecast_info = forecast_data.get('forecast_date', '')
|
| 1668 |
summary = f"Forecast for location: {lat:.3f}°N, {lon:.3f}°E\n\nUsing: {data_type}\nForecast: {forecast_info}"
|
| 1669 |
|
| 1670 |
-
if 'error' in forecast_data:
|
| 1671 |
-
summary += f"\n\nNote: Real data unavailable - {forecast_data['error'][:150]}..."
|
| 1672 |
-
|
| 1673 |
# Generate NOAA-style text forecast
|
| 1674 |
location_name = forecast_data.get('location_name', f"{lat:.2f}°N, {lon:.2f}°E")
|
| 1675 |
text_forecast = generate_forecast_text(forecast_data, location_name)
|
| 1676 |
|
| 1677 |
-
|
| 1678 |
-
|
| 1679 |
-
|
| 1680 |
-
|
| 1681 |
-
|
| 1682 |
|
| 1683 |
def create_attribution_text():
|
| 1684 |
"""Create proper attribution for the dataset"""
|
|
|
|
| 226 |
print(f"Fetching real DWD ICON data for {lat:.3f}°N, {lon:.3f}°E")
|
| 227 |
|
| 228 |
if not GRIB_AVAILABLE:
|
| 229 |
+
raise Exception("GRIB2 libraries not available. Install cfgrib and pygrib for DWD ICON data access.")
|
|
|
|
| 230 |
|
| 231 |
# Get latest model run
|
| 232 |
run_date = get_latest_dwd_run()
|
|
|
|
| 249 |
clat_file, clon_file = download_dwd_coordinate_files(run_date)
|
| 250 |
|
| 251 |
if not clat_file or not clon_file:
|
| 252 |
+
raise Exception("Failed to download coordinate files from DWD ICON server")
|
|
|
|
| 253 |
|
| 254 |
# Parse coordinate files
|
| 255 |
clat_ds = parse_grib_file(clat_file)
|
| 256 |
clon_ds = parse_grib_file(clon_file)
|
| 257 |
|
| 258 |
if clat_ds is None or clon_ds is None:
|
| 259 |
+
raise Exception("Failed to parse coordinate files from DWD ICON server")
|
|
|
|
| 260 |
|
| 261 |
# Get coordinate arrays
|
| 262 |
# DWD coordinate files may use different variable names
|
|
|
|
| 284 |
print(f"Error extracting coordinate arrays: {e}")
|
| 285 |
print(f"CLAT dataset: {clat_ds}")
|
| 286 |
print(f"CLON dataset: {clon_ds}")
|
| 287 |
+
raise Exception(f"Failed to extract coordinate arrays: {e}")
|
| 288 |
|
| 289 |
# Find nearest grid point
|
| 290 |
nearest_idx = find_nearest_grid_point(lat, lon, grid_lats, grid_lons)
|
|
|
|
| 350 |
print(f"Error fetching real DWD ICON data: {e}")
|
| 351 |
import traceback
|
| 352 |
traceback.print_exc()
|
| 353 |
+
raise Exception(f"Failed to fetch DWD ICON data: {e}")
|
| 354 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
|
| 356 |
def get_forecast_data(lat, lon, forecast_hour="00"):
|
| 357 |
"""
|
|
|
|
| 363 |
# Fetch data from DWD ICON model
|
| 364 |
weather_data = fetch_dwd_icon_data(lat, lon)
|
| 365 |
|
| 366 |
+
# Process real DWD GRIB2 data only
|
| 367 |
if 'weather_data' in weather_data:
|
|
|
|
| 368 |
return process_real_dwd_data(weather_data, lat, lon)
|
| 369 |
else:
|
| 370 |
+
raise Exception(\"Invalid weather data format received from DWD ICON server\")
|
|
|
|
| 371 |
|
| 372 |
except Exception as e:
|
| 373 |
import traceback
|
|
|
|
| 376 |
print("Full error traceback:")
|
| 377 |
print(traceback.format_exc())
|
| 378 |
|
| 379 |
+
# No fallback - raise the error
|
| 380 |
+
raise Exception(error_msg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
|
| 382 |
def process_real_dwd_data(dwd_data, lat, lon):
|
| 383 |
"""
|
|
|
|
| 559 |
print(f"Error processing real DWD data: {e}")
|
| 560 |
raise e
|
| 561 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 562 |
|
| 563 |
def analyze_weather_events(forecast_data):
|
| 564 |
"""
|
|
|
|
| 1444 |
ax9 = fig.add_subplot(gs[2, 2])
|
| 1445 |
ax9.axis('off')
|
| 1446 |
|
| 1447 |
+
# Only real DWD ICON data now
|
| 1448 |
+
data_source = "Real DWD ICON Data"
|
| 1449 |
forecast_info = forecast_data.get('forecast_date', 'Unknown')
|
| 1450 |
|
| 1451 |
# Grid point info
|
|
|
|
| 1489 |
if 'pressure' in forecast_data:
|
| 1490 |
summary_text += f"Pressure: {forecast_data['pressure'][0]:.1f} hPa\n"
|
| 1491 |
|
| 1492 |
+
color = 'lightgreen'
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1493 |
ax9.text(0.05, 0.95, summary_text, transform=ax9.transAxes, fontsize=8,
|
| 1494 |
verticalalignment='top', bbox=dict(boxstyle='round', facecolor=color, alpha=0.7))
|
| 1495 |
|
|
|
|
| 1502 |
if lat is None or lon is None:
|
| 1503 |
return "Please click on the map to select a location", None, ""
|
| 1504 |
|
| 1505 |
+
try:
|
| 1506 |
+
# Get forecast data - will raise exception if it fails
|
| 1507 |
+
forecast_data = get_forecast_data(lat, lon)
|
| 1508 |
+
|
| 1509 |
+
# Create plot
|
| 1510 |
+
plot = create_forecast_plot(forecast_data)
|
| 1511 |
+
|
| 1512 |
+
# Create summary text
|
| 1513 |
+
data_type = "Real DWD ICON Data"
|
| 1514 |
forecast_info = forecast_data.get('forecast_date', '')
|
| 1515 |
summary = f"Forecast for location: {lat:.3f}°N, {lon:.3f}°E\n\nUsing: {data_type}\nForecast: {forecast_info}"
|
| 1516 |
|
|
|
|
|
|
|
|
|
|
| 1517 |
# Generate NOAA-style text forecast
|
| 1518 |
location_name = forecast_data.get('location_name', f"{lat:.2f}°N, {lon:.2f}°E")
|
| 1519 |
text_forecast = generate_forecast_text(forecast_data, location_name)
|
| 1520 |
|
| 1521 |
+
return summary, plot, text_forecast
|
| 1522 |
+
|
| 1523 |
+
except Exception as e:
|
| 1524 |
+
error_msg = f"❌ Failed to retrieve DWD ICON data: {str(e)}"
|
| 1525 |
+
return error_msg, None, "Unable to generate forecast - DWD ICON data unavailable"
|
| 1526 |
|
| 1527 |
def create_attribution_text():
|
| 1528 |
"""Create proper attribution for the dataset"""
|