"""Run-up forecast endpoints""" from fastapi import APIRouter, HTTPException, Query, Path, Body from typing import Dict, List, Optional from datetime import datetime import uuid router = APIRouter() @router.post("/runup", response_model=Dict) async def forecast_runup( zone: str = Query(..., description="Coastal zone name"), source: Optional[str] = Query(None, description="Source region (e.g., 'sumatra', 'chile')"), parameters: Optional[Dict] = Body(None, description="Parameter values for forecast") ): """Generate run-up forecast for a coastal zone""" # Placeholder for different zones zone_data = { "hilo_bay_hawaii": { "becf": 4.8, "typical_beach_slope": 0.05, "shelf_width_km": 22, "population": 45000 }, "khao_lak_thailand": { "becf": 4.1, "typical_beach_slope": 0.03, "shelf_width_km": 65, "population": 20000 }, "banda_aceh_indonesia": { "becf": 7.3, "typical_beach_slope": 0.04, "shelf_width_km": 65, "population": 250000 } } if zone not in zone_data: # Return generic forecast runup = 5.0 # default else: data = zone_data[zone] # Simplified forecast based on BECF runup = 1.0 * (data['becf'] ** 0.5) * 2 return { "forecast_id": str(uuid.uuid4()), "zone": zone, "timestamp": datetime.utcnow().isoformat(), "runup_m": runup, "confidence_interval": [runup * 0.88, runup * 1.12], "lead_time_min": 67, "parameters_used": parameters or {}, "zone_data": zone_data.get(zone, {}), "method": "physical_scaling" } @router.post("/ensemble", response_model=Dict) async def ensemble_forecast( zone: str = Query(..., description="Coastal zone name"), n_samples: int = Query(100, ge=10, le=1000, description="Number of ensemble samples") ): """Generate ensemble forecast with uncertainty quantification""" import numpy as np # Generate ensemble base_runup = 5.0 uncertainties = np.random.normal(0, 0.5, n_samples) ensemble = base_runup + uncertainties return { "forecast_id": str(uuid.uuid4()), "zone": zone, "timestamp": datetime.utcnow().isoformat(), "ensemble_statistics": { "mean": float(np.mean(ensemble)), "median": float(np.median(ensemble)), "std": float(np.std(ensemble)), "p10": float(np.percentile(ensemble, 10)), "p90": float(np.percentile(ensemble, 90)), "min": float(np.min(ensemble)), "max": float(np.max(ensemble)), "n_samples": n_samples }, "ensemble": ensemble.tolist()[:10] # sample first 10 } @router.get("/zone/{zone_name}", response_model=Dict) async def get_zone_forecast_history( zone_name: str = Path(...), days: int = Query(7, ge=1, le=30, description="Number of days of history") ): """Get forecast history for a zone""" # Placeholder history import numpy as np from datetime import datetime, timedelta history = [] for i in range(days): date = datetime.utcnow() - timedelta(days=i) history.append({ "date": date.isoformat(), "forecast_runup": 5.0 + np.random.normal(0, 0.3), "observed_runup": None # would be filled if observed }) return { "zone": zone_name, "history": history } @router.post("/validate", response_model=Dict) async def validate_forecast( forecast_id: str = Query(..., description="Forecast ID"), observed_runup: float = Query(..., description="Observed run-up height [m]") ): """Validate a forecast against observation""" # Placeholder validation error = abs(5.0 - observed_runup) / observed_runup * 100 return { "forecast_id": forecast_id, "observed_runup": observed_runup, "forecast_runup": 5.0, "error_percent": error, "within_15_percent": error < 15, "within_30_percent": error < 30, "bias": 5.0 - observed_runup } @router.get("/scenarios", response_model=List[Dict]) async def get_forecast_scenarios(): """Get pre-computed forecast scenarios""" return [ { "id": "tohoku_2011", "name": "Tōhoku 2011", "description": "2011 Tōhoku earthquake and tsunami", "source": "Japan Trench", "magnitude": 9.0, "runup_m": 40.5 }, { "id": "indian_ocean_2004", "name": "Indian Ocean 2004", "description": "2004 Sumatra-Andaman earthquake and tsunami", "source": "Sumatra", "magnitude": 9.1, "runup_m": 30.0 }, { "id": "hokkaido_1993", "name": "Hokkaido 1993", "description": "1993 Hokkaido Nansei-Oki earthquake", "source": "Sea of Japan", "magnitude": 7.8, "runup_m": 31.0 } ]