Spaces:
Sleeping
Sleeping
Update api.py
Browse files
api.py
CHANGED
|
@@ -1,392 +1,506 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
-
from fastapi import FastAPI, HTTPException
|
| 7 |
-
from fastapi.
|
| 8 |
from pydantic import BaseModel, Field
|
|
|
|
| 9 |
import os
|
| 10 |
-
import sys
|
| 11 |
-
from typing import Optional, Dict, List
|
| 12 |
-
from datetime import datetime
|
| 13 |
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
# Add src to path
|
| 26 |
-
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
| 27 |
|
| 28 |
# ============================================================================
|
| 29 |
-
#
|
| 30 |
# ============================================================================
|
| 31 |
|
| 32 |
app = FastAPI(
|
| 33 |
-
title="
|
| 34 |
-
description="
|
| 35 |
-
version="
|
| 36 |
-
docs_url="/docs",
|
| 37 |
-
redoc_url="/redoc"
|
| 38 |
)
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
-
|
| 45 |
-
districts_df = None
|
| 46 |
-
imd_fetcher = None
|
| 47 |
-
feature_engineer = None
|
| 48 |
-
|
| 49 |
-
FEATURE_NAMES = [
|
| 50 |
-
'LAT', 'LON', 'MAX_WIND', 'MIN_PRESSURE', 'RAD_NE', 'RAD_SE', 'RAD_SW', 'RAD_NW',
|
| 51 |
-
'RAD50_NE', 'RAD50_SE', 'RAD50_SW', 'RAD50_NW', 'RAD64_NE', 'RAD64_SE', 'RAD64_SW',
|
| 52 |
-
'RAD64_NW', 'MONTH', 'HOUR', 'DAY_OF_YEAR', 'SIN_DOY', 'COS_DOY', 'SIN_HOUR',
|
| 53 |
-
'COS_HOUR', 'STORM_AGE_HOURS', 'STORM_DURATION_HOURS', 'DIST_TO_ODISHA_KM',
|
| 54 |
-
'MOVEMENT_SPEED_KPH', 'WIND_t-6', 'WIND_t-12', 'WIND_t-18', 'WIND_t-24',
|
| 55 |
-
'PRESSURE_t-6', 'PRESSURE_t-12', 'PRESSURE_t-18', 'PRESSURE_t-24', 'WIND_CHANGE_6H',
|
| 56 |
-
'PRESSURE_CHANGE_6H', 'INTENSIFICATION_RATE', 'WIND_CHANGE_12H', 'PRESSURE_CHANGE_12H',
|
| 57 |
-
'STORM_DB', 'STORM_EX', 'STORM_MD', 'STORM_TC', 'STORM_TD', 'STORM_TS', 'STORM_TY',
|
| 58 |
-
'STORM_WV', 'AVG_RAD34', 'AVG_RAD50', 'AVG_RAD64', 'STORM_SIZE'
|
| 59 |
-
]
|
| 60 |
|
| 61 |
# ============================================================================
|
| 62 |
-
#
|
| 63 |
# ============================================================================
|
| 64 |
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
print("🚀 Initializing Cyclone Prediction API")
|
| 72 |
-
print("=" * 60)
|
| 73 |
-
|
| 74 |
-
# Load model
|
| 75 |
-
model_path = os.path.join("models", "xgboost_cyclone_model.pkl")
|
| 76 |
-
if os.path.exists(model_path):
|
| 77 |
-
try:
|
| 78 |
-
with open(model_path, 'rb') as f:
|
| 79 |
-
model = pickle.load(f)
|
| 80 |
-
print(f"✅ Model loaded from {model_path}")
|
| 81 |
-
except Exception as e:
|
| 82 |
-
print(f"❌ Error loading model: {e}")
|
| 83 |
-
model = None
|
| 84 |
-
else:
|
| 85 |
-
print(f"⚠️ Model file not found at {model_path}")
|
| 86 |
-
model = None
|
| 87 |
-
|
| 88 |
-
# Load districts data
|
| 89 |
-
districts_path = os.path.join("src", "data", "districts_master.csv")
|
| 90 |
-
if os.path.exists(districts_path):
|
| 91 |
-
try:
|
| 92 |
-
districts_df = pd.read_csv(districts_path)
|
| 93 |
-
print(f"✅ Loaded {len(districts_df)} districts")
|
| 94 |
-
except Exception as e:
|
| 95 |
-
print(f"❌ Error loading districts: {e}")
|
| 96 |
-
districts_df = None
|
| 97 |
-
else:
|
| 98 |
-
print(f"⚠️ Districts file not found at {districts_path}")
|
| 99 |
-
districts_df = None
|
| 100 |
-
|
| 101 |
-
# Initialize fetchers
|
| 102 |
-
try:
|
| 103 |
-
from live_data_fetcher import IMDCycloneDataFetcher, CycloneFeatureEngineer
|
| 104 |
-
imd_fetcher = IMDCycloneDataFetcher()
|
| 105 |
-
feature_engineer = CycloneFeatureEngineer()
|
| 106 |
-
print("✅ IMD fetchers initialized")
|
| 107 |
-
except ImportError as e:
|
| 108 |
-
print(f"⚠️ Could not import fetchers: {e}")
|
| 109 |
-
imd_fetcher = None
|
| 110 |
-
feature_engineer = None
|
| 111 |
-
|
| 112 |
-
print("=" * 60)
|
| 113 |
-
print(f"Model loaded: {model is not None}")
|
| 114 |
-
print(f"Districts loaded: {districts_df is not None}")
|
| 115 |
-
print(f"Fetchers ready: {imd_fetcher is not None}")
|
| 116 |
-
print("=" * 60)
|
| 117 |
|
| 118 |
# ============================================================================
|
| 119 |
-
#
|
| 120 |
# ============================================================================
|
| 121 |
|
| 122 |
-
class
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
# ============================================================================
|
| 139 |
-
#
|
| 140 |
# ============================================================================
|
| 141 |
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
elif wind_speed_knots < 137:
|
| 168 |
-
return "Category 4 Cyclone (Major)"
|
| 169 |
-
else:
|
| 170 |
-
return "Category 5 Cyclone (Catastrophic)"
|
| 171 |
|
| 172 |
# ============================================================================
|
| 173 |
-
#
|
| 174 |
# ============================================================================
|
| 175 |
|
| 176 |
-
@app.
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
|
| 181 |
-
@app.get("/health")
|
| 182 |
-
async def health_check():
|
| 183 |
-
"""Health check endpoint"""
|
| 184 |
return {
|
| 185 |
-
"
|
| 186 |
-
"
|
| 187 |
-
"
|
| 188 |
-
"
|
| 189 |
-
"timestamp": datetime.now().isoformat(),
|
| 190 |
-
"environment": "HuggingFace Spaces" if IS_HUGGINGFACE else "Local"
|
| 191 |
}
|
| 192 |
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
"
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
"
|
| 216 |
-
"LON": 85.3,
|
| 217 |
-
"MAX_WIND": 65,
|
| 218 |
-
"MIN_PRESSURE": 990
|
| 219 |
}
|
| 220 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
}
|
| 222 |
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
|
| 229 |
-
|
| 230 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
try:
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
input_df = build_model_input(engineered_features)
|
| 237 |
-
prediction = model.predict(input_df)
|
| 238 |
-
predicted_wind = float(prediction[0])
|
| 239 |
-
|
| 240 |
-
return PredictionResponse(
|
| 241 |
-
predicted_intensity=round(predicted_wind, 2),
|
| 242 |
-
predicted_intensity_kmh=round(predicted_wind * 1.852, 2),
|
| 243 |
-
intensity_category=get_intensity_category(predicted_wind),
|
| 244 |
-
status="success",
|
| 245 |
-
data_source="Manual Input",
|
| 246 |
-
current_parameters={
|
| 247 |
-
"latitude": engineered_features['LAT'],
|
| 248 |
-
"longitude": engineered_features['LON'],
|
| 249 |
-
"current_wind": engineered_features['MAX_WIND'],
|
| 250 |
-
"current_pressure": engineered_features['MIN_PRESSURE']
|
| 251 |
-
},
|
| 252 |
-
distance_to_odisha_km=round(engineered_features['DIST_TO_ODISHA_KM'], 2)
|
| 253 |
)
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
@app.
|
| 259 |
-
|
| 260 |
-
"""
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
if imd_fetcher is None:
|
| 274 |
-
raise HTTPException(status_code=503, detail="IMD fetcher not initialized")
|
| 275 |
-
|
| 276 |
-
try:
|
| 277 |
-
result = imd_fetcher.fetch_hourly_bulletin()
|
| 278 |
-
if not result or result.get("status") != "success":
|
| 279 |
-
return {
|
| 280 |
-
"status": "no_active_cyclone",
|
| 281 |
-
"message": "No active cyclone bulletin available",
|
| 282 |
-
"timestamp": datetime.now().isoformat()
|
| 283 |
-
}
|
| 284 |
-
return result
|
| 285 |
-
except Exception as e:
|
| 286 |
-
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
|
| 287 |
-
|
| 288 |
-
@app.get("/live/predict")
|
| 289 |
-
async def predict_from_live_imd():
|
| 290 |
-
"""Predict using live IMD bulletin data"""
|
| 291 |
-
if model is None:
|
| 292 |
-
raise HTTPException(status_code=503, detail="Model not loaded")
|
| 293 |
-
|
| 294 |
-
if imd_fetcher is None or feature_engineer is None:
|
| 295 |
-
raise HTTPException(status_code=503, detail="Fetchers not initialized")
|
| 296 |
-
|
| 297 |
-
try:
|
| 298 |
-
bulletin_result = imd_fetcher.fetch_hourly_bulletin()
|
| 299 |
-
|
| 300 |
-
if bulletin_result.get('status') != 'success':
|
| 301 |
-
return JSONResponse(
|
| 302 |
-
status_code=200,
|
| 303 |
-
content={
|
| 304 |
-
"status": "no_active_cyclone",
|
| 305 |
-
"message": "No active cyclone bulletin available",
|
| 306 |
-
"timestamp": datetime.now().isoformat()
|
| 307 |
-
}
|
| 308 |
-
)
|
| 309 |
-
|
| 310 |
-
params = imd_fetcher.parse_cyclone_parameters(bulletin_result['content'])
|
| 311 |
-
|
| 312 |
-
if not params or 'LAT' not in params or 'LON' not in params:
|
| 313 |
-
return JSONResponse(
|
| 314 |
-
status_code=200,
|
| 315 |
-
content={
|
| 316 |
-
"status": "no_active_cyclone",
|
| 317 |
-
"message": "No cyclone detected in bulletin",
|
| 318 |
-
"timestamp": datetime.now().isoformat()
|
| 319 |
-
}
|
| 320 |
-
)
|
| 321 |
-
|
| 322 |
-
engineered_features = feature_engineer.engineer_features(params)
|
| 323 |
-
input_df = build_model_input(engineered_features)
|
| 324 |
-
prediction = model.predict(input_df)
|
| 325 |
-
predicted_wind = float(prediction[0])
|
| 326 |
-
|
| 327 |
-
return {
|
| 328 |
-
"status": "success",
|
| 329 |
-
"data_source": "IMD RSMC Live Bulletin",
|
| 330 |
-
"predicted_intensity_knots": round(predicted_wind, 2),
|
| 331 |
-
"predicted_intensity_kmh": round(predicted_wind * 1.852, 2),
|
| 332 |
-
"intensity_category": get_intensity_category(predicted_wind),
|
| 333 |
-
"current_parameters": {
|
| 334 |
-
"latitude": params.get('LAT'),
|
| 335 |
-
"longitude": params.get('LON'),
|
| 336 |
-
"current_wind_knots": params.get('MAX_WIND'),
|
| 337 |
-
"current_pressure_hpa": params.get('MIN_PRESSURE')
|
| 338 |
-
},
|
| 339 |
-
"distance_to_odisha_km": round(engineered_features['DIST_TO_ODISHA_KM'], 2),
|
| 340 |
-
"timestamp": datetime.now().isoformat()
|
| 341 |
-
}
|
| 342 |
-
|
| 343 |
-
except Exception as e:
|
| 344 |
-
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
|
| 345 |
-
|
| 346 |
-
@app.get("/data/districts")
|
| 347 |
-
async def get_districts():
|
| 348 |
-
"""Get list of all districts"""
|
| 349 |
-
if districts_df is None:
|
| 350 |
-
raise HTTPException(status_code=503, detail="Districts data not loaded")
|
| 351 |
-
|
| 352 |
-
try:
|
| 353 |
-
districts_data = districts_df[[
|
| 354 |
-
'District', 'Latitude', 'Longitude', 'Flood_Zone',
|
| 355 |
-
'Vulnerability_Index', 'Infrastructure_Score', 'Is_Coastal'
|
| 356 |
-
]].to_dict('records')
|
| 357 |
-
|
| 358 |
-
return {
|
| 359 |
-
"total_districts": len(districts_data),
|
| 360 |
-
"districts": districts_data
|
| 361 |
-
}
|
| 362 |
-
except Exception as e:
|
| 363 |
-
raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
|
| 364 |
-
|
| 365 |
-
# Error handlers
|
| 366 |
-
@app.exception_handler(404)
|
| 367 |
-
async def not_found_handler(request, exc):
|
| 368 |
-
return JSONResponse(
|
| 369 |
-
status_code=404,
|
| 370 |
-
content={
|
| 371 |
-
"error": "Endpoint not found",
|
| 372 |
-
"available_endpoints": [
|
| 373 |
-
"/docs", "/health", "/info",
|
| 374 |
-
"/predict/manual", "/data/districts",
|
| 375 |
-
"/live/cyclones", "/live/predict"
|
| 376 |
-
]
|
| 377 |
-
}
|
| 378 |
-
)
|
| 379 |
|
| 380 |
-
@app.
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
)
|
| 389 |
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
Disaster Risk Prediction & Resource Allocation API
|
| 3 |
+
===================================================
|
| 4 |
+
FastAPI backend exposing:
|
| 5 |
+
|
| 6 |
+
Prediction Endpoints:
|
| 7 |
+
POST /predict/flood → lane-level or zone-level flood risk
|
| 8 |
+
POST /predict/cyclone → cyclone impact risk
|
| 9 |
+
POST /predict/landslide → landslide susceptibility
|
| 10 |
+
POST /predict/earthquake → earthquake structural risk
|
| 11 |
+
POST /predict/all → multi-hazard composite score
|
| 12 |
+
|
| 13 |
+
Flood Map Endpoints:
|
| 14 |
+
POST /map/flood/features → GeoJSON risk map from explicit feature input
|
| 15 |
+
POST /map/flood/osm → GeoJSON risk map auto-fetched from OpenStreetMap
|
| 16 |
+
POST /map/flood/geojson → GeoJSON risk map from uploaded road GeoJSON
|
| 17 |
+
|
| 18 |
+
Allocation Endpoints:
|
| 19 |
+
POST /allocate/auto → Hungarian-optimal auto allocation
|
| 20 |
+
POST /allocate/manual → Manual team → task assignment
|
| 21 |
+
POST /allocate/reset → Reset all allocations
|
| 22 |
+
GET /allocate/summary → Current allocation state
|
| 23 |
+
|
| 24 |
+
Team & Task Management:
|
| 25 |
+
POST /teams → Register a team
|
| 26 |
+
GET /teams → List all teams
|
| 27 |
+
POST /tasks → Register a task
|
| 28 |
+
GET /tasks → List all tasks
|
| 29 |
+
|
| 30 |
+
Utilities:
|
| 31 |
+
GET /health → Health check + model status
|
| 32 |
+
GET /features/{disaster} → Feature schema for a disaster type
|
| 33 |
+
POST /predict/flood/explain → Fuzzy membership interpretation
|
| 34 |
"""
|
| 35 |
|
| 36 |
+
from fastapi import FastAPI, HTTPException, Query
|
| 37 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 38 |
from pydantic import BaseModel, Field
|
| 39 |
+
from typing import Dict, List, Optional, Tuple, Any
|
| 40 |
import os
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
+
from src.disaster_predictors import (
|
| 43 |
+
FloodPredictor, CyclonePredictor, LandslidePredictor, EarthquakePredictor,
|
| 44 |
+
MultiHazardPredictor, FEATURE_SCHEMAS, PredictionResult, RiskTier
|
| 45 |
+
)
|
| 46 |
+
from src.lane_flood_mapper import LaneFloodMapper
|
| 47 |
+
from src.allocation import (
|
| 48 |
+
AllocationEngine, FieldTeam, Task, TaskStatus,
|
| 49 |
+
TEAMS, TASKS, ALLOCATIONS,
|
| 50 |
+
get_allocation_summary, reset_all_allocations, initialize_default_teams
|
| 51 |
+
)
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
# ============================================================================
|
| 54 |
+
# APP SETUP
|
| 55 |
# ============================================================================
|
| 56 |
|
| 57 |
app = FastAPI(
|
| 58 |
+
title="Disaster Risk Prediction & Resource Allocation API",
|
| 59 |
+
description="FNN-based multi-hazard risk prediction with lane-level flood mapping and optimal resource allocation",
|
| 60 |
+
version="2.0.0"
|
|
|
|
|
|
|
| 61 |
)
|
| 62 |
|
| 63 |
+
app.add_middleware(
|
| 64 |
+
CORSMiddleware,
|
| 65 |
+
allow_origins=["*"],
|
| 66 |
+
allow_methods=["*"],
|
| 67 |
+
allow_headers=["*"],
|
| 68 |
+
)
|
| 69 |
|
| 70 |
+
MODEL_DIR = os.getenv("MODEL_DIR", "models")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
# ============================================================================
|
| 73 |
+
# MODEL SINGLETONS (loaded once at startup)
|
| 74 |
# ============================================================================
|
| 75 |
|
| 76 |
+
flood_predictor = FloodPredictor(MODEL_DIR)
|
| 77 |
+
cyclone_predictor = CyclonePredictor(MODEL_DIR)
|
| 78 |
+
landslide_predictor = LandslidePredictor(MODEL_DIR)
|
| 79 |
+
earthquake_predictor = EarthquakePredictor(MODEL_DIR)
|
| 80 |
+
multi_hazard = MultiHazardPredictor(MODEL_DIR)
|
| 81 |
+
lane_mapper = LaneFloodMapper(flood_predictor)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
# ============================================================================
|
| 84 |
+
# REQUEST / RESPONSE SCHEMAS
|
| 85 |
# ============================================================================
|
| 86 |
|
| 87 |
+
class PredictionRequest(BaseModel):
|
| 88 |
+
features: Dict[str, float] = Field(
|
| 89 |
+
...,
|
| 90 |
+
description="Feature values. Get schema from GET /features/{disaster_type}"
|
| 91 |
+
)
|
| 92 |
+
n_mc_samples: int = Field(
|
| 93 |
+
default=50,
|
| 94 |
+
ge=10, le=200,
|
| 95 |
+
description="Monte Carlo dropout samples for uncertainty estimation"
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
class MultiHazardRequest(BaseModel):
|
| 100 |
+
features_by_type: Dict[str, Dict[str, float]] = Field(
|
| 101 |
+
...,
|
| 102 |
+
description='{"flood": {...}, "cyclone": {...}, ...}'
|
| 103 |
+
)
|
| 104 |
+
weights: Optional[Dict[str, float]] = Field(
|
| 105 |
+
default=None,
|
| 106 |
+
description="Custom disaster weights, e.g. {'flood': 0.5, 'cyclone': 0.3, ...}"
|
| 107 |
+
)
|
| 108 |
+
n_mc_samples: int = 30
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
class LaneFeaturesRequest(BaseModel):
|
| 112 |
+
segments: List[Dict[str, Any]] = Field(
|
| 113 |
+
...,
|
| 114 |
+
description="""List of segments, each containing:
|
| 115 |
+
segment_id (str), road_name (str, optional),
|
| 116 |
+
road_type (str, optional), coordinates [[lat,lon],...],
|
| 117 |
+
features: {flood feature dict}"""
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
class OSMMapRequest(BaseModel):
|
| 122 |
+
bbox: Tuple[float, float, float, float] = Field(
|
| 123 |
+
...,
|
| 124 |
+
description="Bounding box (south, west, north, east)"
|
| 125 |
+
)
|
| 126 |
+
base_features: Dict[str, float] = Field(
|
| 127 |
+
...,
|
| 128 |
+
description="Zone-level flood features applied to all road segments"
|
| 129 |
+
)
|
| 130 |
+
segment_overrides: Optional[List[Dict]] = Field(
|
| 131 |
+
default=None,
|
| 132 |
+
description="Per-segment feature overrides: [{segment_id, features}]"
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
class GeoJSONMapRequest(BaseModel):
|
| 137 |
+
geojson: Dict = Field(..., description="GeoJSON FeatureCollection of road segments")
|
| 138 |
+
feature_mapping: Dict[str, str] = Field(
|
| 139 |
+
...,
|
| 140 |
+
description='{"flood_feature_name": "geojson_property_name"}'
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
class AutoAllocateRequest(BaseModel):
|
| 145 |
+
strategy: str = Field(
|
| 146 |
+
default="balanced",
|
| 147 |
+
description="priority_based | proximity_based | balanced"
|
| 148 |
+
)
|
| 149 |
+
optimize_routes: bool = True
|
| 150 |
+
priority_weight: float = Field(
|
| 151 |
+
default=0.5, ge=0.0, le=1.0,
|
| 152 |
+
description="Weight of priority vs proximity in 'balanced' strategy"
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
class ManualAllocateRequest(BaseModel):
|
| 157 |
+
team_assignments: Dict[str, List[str]] = Field(
|
| 158 |
+
...,
|
| 159 |
+
description="{team_id: [task_id, ...]}"
|
| 160 |
+
)
|
| 161 |
+
optimize_routes: bool = True
|
| 162 |
+
respect_capacity: bool = True
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def prediction_result_to_dict(result: PredictionResult) -> dict:
|
| 166 |
+
return {
|
| 167 |
+
"risk_score": result.risk_score,
|
| 168 |
+
"risk_tier": result.risk_tier.value,
|
| 169 |
+
"uncertainty": result.uncertainty,
|
| 170 |
+
"confidence_interval": {
|
| 171 |
+
"lower": result.confidence_interval[0],
|
| 172 |
+
"upper": result.confidence_interval[1]
|
| 173 |
+
},
|
| 174 |
+
"feature_memberships": result.feature_memberships,
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
|
| 178 |
# ============================================================================
|
| 179 |
+
# HEALTH & METADATA
|
| 180 |
# ============================================================================
|
| 181 |
|
| 182 |
+
@app.get("/health")
|
| 183 |
+
def health():
|
| 184 |
+
return {
|
| 185 |
+
"status": "ok",
|
| 186 |
+
"models": {
|
| 187 |
+
"flood": flood_predictor.is_ready(),
|
| 188 |
+
"cyclone": cyclone_predictor.is_ready(),
|
| 189 |
+
"landslide": landslide_predictor.is_ready(),
|
| 190 |
+
"earthquake": earthquake_predictor.is_ready(),
|
| 191 |
+
},
|
| 192 |
+
"model_architecture": "Fuzzy Neural Network (ANFIS-style)",
|
| 193 |
+
"allocation_algorithm": "Hungarian + 2-opt route optimization",
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
@app.get("/features/{disaster_type}")
|
| 198 |
+
def get_feature_schema(disaster_type: str):
|
| 199 |
+
if disaster_type not in FEATURE_SCHEMAS:
|
| 200 |
+
raise HTTPException(404, f"Unknown disaster type: {disaster_type}. Valid: {list(FEATURE_SCHEMAS)}")
|
| 201 |
+
return {
|
| 202 |
+
"disaster_type": disaster_type,
|
| 203 |
+
"features": FEATURE_SCHEMAS[disaster_type],
|
| 204 |
+
"count": len(FEATURE_SCHEMAS[disaster_type]),
|
| 205 |
+
}
|
| 206 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
|
| 208 |
# ============================================================================
|
| 209 |
+
# PREDICTION ENDPOINTS
|
| 210 |
# ============================================================================
|
| 211 |
|
| 212 |
+
@app.post("/predict/flood")
|
| 213 |
+
def predict_flood(req: PredictionRequest):
|
| 214 |
+
errors = flood_predictor.validate_input(req.features)
|
| 215 |
+
if errors:
|
| 216 |
+
raise HTTPException(422, {"validation_errors": errors})
|
| 217 |
+
result = flood_predictor.predict(req.features, req.n_mc_samples)
|
| 218 |
+
return {"disaster_type": "flood", **prediction_result_to_dict(result)}
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
@app.post("/predict/cyclone")
|
| 222 |
+
def predict_cyclone(req: PredictionRequest):
|
| 223 |
+
errors = cyclone_predictor.validate_input(req.features)
|
| 224 |
+
if errors:
|
| 225 |
+
raise HTTPException(422, {"validation_errors": errors})
|
| 226 |
+
result = cyclone_predictor.predict(req.features, req.n_mc_samples)
|
| 227 |
+
return {"disaster_type": "cyclone", **prediction_result_to_dict(result)}
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
@app.post("/predict/landslide")
|
| 231 |
+
def predict_landslide(req: PredictionRequest):
|
| 232 |
+
errors = landslide_predictor.validate_input(req.features)
|
| 233 |
+
if errors:
|
| 234 |
+
raise HTTPException(422, {"validation_errors": errors})
|
| 235 |
+
result = landslide_predictor.predict(req.features, req.n_mc_samples)
|
| 236 |
+
return {"disaster_type": "landslide", **prediction_result_to_dict(result)}
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
@app.post("/predict/earthquake")
|
| 240 |
+
def predict_earthquake(req: PredictionRequest):
|
| 241 |
+
errors = earthquake_predictor.validate_input(req.features)
|
| 242 |
+
if errors:
|
| 243 |
+
raise HTTPException(422, {"validation_errors": errors})
|
| 244 |
+
result = earthquake_predictor.predict(req.features, req.n_mc_samples)
|
| 245 |
+
return {"disaster_type": "earthquake", **prediction_result_to_dict(result)}
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
@app.post("/predict/all")
|
| 249 |
+
def predict_all(req: MultiHazardRequest):
|
| 250 |
+
result = multi_hazard.predict_all(req.features_by_type, req.weights)
|
| 251 |
+
|
| 252 |
+
# Serialize PredictionResult objects
|
| 253 |
+
by_disaster_serialized = {}
|
| 254 |
+
for dt, pred_result in result["by_disaster"].items():
|
| 255 |
+
by_disaster_serialized[dt] = prediction_result_to_dict(pred_result)
|
| 256 |
|
|
|
|
|
|
|
|
|
|
| 257 |
return {
|
| 258 |
+
"composite_risk_score": result["composite_risk_score"],
|
| 259 |
+
"composite_risk_tier": result["composite_risk_tier"].value,
|
| 260 |
+
"active_predictors": result["active_predictors"],
|
| 261 |
+
"by_disaster": by_disaster_serialized,
|
|
|
|
|
|
|
| 262 |
}
|
| 263 |
|
| 264 |
+
|
| 265 |
+
@app.post("/predict/flood/explain")
|
| 266 |
+
def explain_flood(req: PredictionRequest):
|
| 267 |
+
"""
|
| 268 |
+
Returns fuzzy membership degrees per feature.
|
| 269 |
+
Shows how much each input feature falls into LOW / MEDIUM / HIGH fuzzy sets.
|
| 270 |
+
Useful for interpretability and debugging model behavior.
|
| 271 |
+
"""
|
| 272 |
+
errors = flood_predictor.validate_input(req.features)
|
| 273 |
+
if errors:
|
| 274 |
+
raise HTTPException(422, {"validation_errors": errors})
|
| 275 |
+
|
| 276 |
+
result = flood_predictor.predict(req.features, req.n_mc_samples)
|
| 277 |
+
memberships = result.feature_memberships
|
| 278 |
+
|
| 279 |
+
explanation = {}
|
| 280 |
+
if memberships:
|
| 281 |
+
for feat, degrees in memberships.items():
|
| 282 |
+
explanation[feat] = {
|
| 283 |
+
"LOW": round(degrees[0], 4),
|
| 284 |
+
"MEDIUM": round(degrees[1], 4),
|
| 285 |
+
"HIGH": round(degrees[2], 4),
|
| 286 |
+
"dominant_set": ["LOW", "MEDIUM", "HIGH"][int(np.argmax(degrees))]
|
|
|
|
|
|
|
|
|
|
| 287 |
}
|
| 288 |
+
|
| 289 |
+
return {
|
| 290 |
+
"risk_score": result.risk_score,
|
| 291 |
+
"risk_tier": result.risk_tier.value,
|
| 292 |
+
"fuzzy_explanation": explanation,
|
| 293 |
}
|
| 294 |
|
| 295 |
+
|
| 296 |
+
import numpy as np # needed for explain endpoint
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
# ============================================================================
|
| 300 |
+
# LANE-LEVEL FLOOD MAP ENDPOINTS
|
| 301 |
+
# ============================================================================
|
| 302 |
+
|
| 303 |
+
@app.post("/map/flood/features")
|
| 304 |
+
def flood_map_from_features(req: LaneFeaturesRequest):
|
| 305 |
+
"""
|
| 306 |
+
Primary endpoint: generate lane-level flood risk GeoJSON
|
| 307 |
+
from explicit per-segment feature values.
|
| 308 |
|
| 309 |
+
Returns a GeoJSON FeatureCollection where each LineString feature
|
| 310 |
+
has risk_score, risk_tier, color, and uncertainty properties.
|
| 311 |
+
"""
|
| 312 |
+
if not flood_predictor.is_ready():
|
| 313 |
+
raise HTTPException(503, "Flood model not loaded. Run train_model.py first.")
|
| 314 |
+
return lane_mapper.map_from_features(req.segments)
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
@app.post("/map/flood/osm")
|
| 318 |
+
def flood_map_from_osm(req: OSMMapRequest):
|
| 319 |
+
"""
|
| 320 |
+
Fetch road network from OpenStreetMap for a bounding box
|
| 321 |
+
and generate flood risk GeoJSON using zone-level features.
|
| 322 |
|
| 323 |
+
Requires osmnx: pip install osmnx
|
| 324 |
+
"""
|
| 325 |
+
if not flood_predictor.is_ready():
|
| 326 |
+
raise HTTPException(503, "Flood model not loaded. Run train_model.py first.")
|
| 327 |
try:
|
| 328 |
+
return lane_mapper.map_from_osm(
|
| 329 |
+
req.bbox, req.base_features, req.segment_overrides
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
)
|
| 331 |
+
except RuntimeError as e:
|
| 332 |
+
raise HTTPException(400, str(e))
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
@app.post("/map/flood/geojson")
|
| 336 |
+
def flood_map_from_geojson(req: GeoJSONMapRequest):
|
| 337 |
+
"""
|
| 338 |
+
Generate flood risk map from your own road GeoJSON.
|
| 339 |
+
Provide a feature_mapping to tell the API which GeoJSON property
|
| 340 |
+
corresponds to which flood input feature.
|
| 341 |
+
"""
|
| 342 |
+
if not flood_predictor.is_ready():
|
| 343 |
+
raise HTTPException(503, "Flood model not loaded. Run train_model.py first.")
|
| 344 |
+
return lane_mapper.map_from_geojson(req.geojson, req.feature_mapping)
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
# ============================================================================
|
| 348 |
+
# ALLOCATION ENDPOINTS
|
| 349 |
+
# ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
|
| 351 |
+
@app.post("/allocate/auto")
|
| 352 |
+
def auto_allocate(req: AutoAllocateRequest):
|
| 353 |
+
"""
|
| 354 |
+
Automatically allocate unassigned tasks to available teams.
|
| 355 |
+
Uses Hungarian algorithm for optimal bipartite matching,
|
| 356 |
+
then 2-opt for route optimization.
|
| 357 |
+
"""
|
| 358 |
+
if req.strategy not in ("priority_based", "proximity_based", "balanced"):
|
| 359 |
+
raise HTTPException(400, "strategy must be priority_based | proximity_based | balanced")
|
| 360 |
+
|
| 361 |
+
allocations = AllocationEngine.auto_allocation(
|
| 362 |
+
strategy=req.strategy,
|
| 363 |
+
optimize_routes=req.optimize_routes,
|
| 364 |
+
priority_weight=req.priority_weight
|
| 365 |
)
|
| 366 |
|
| 367 |
+
return {
|
| 368 |
+
"allocations_created": len(allocations),
|
| 369 |
+
"strategy": req.strategy,
|
| 370 |
+
"assignment_algorithm": "Hungarian (scipy.optimize.linear_sum_assignment)",
|
| 371 |
+
"route_algorithm": "Priority-weighted nearest neighbor + 2-opt",
|
| 372 |
+
"allocations": [a.dict() for a in allocations],
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
|
| 376 |
+
@app.post("/allocate/manual")
|
| 377 |
+
def manual_allocate(req: ManualAllocateRequest):
|
| 378 |
+
"""Manually specify team → task assignments."""
|
| 379 |
+
results = []
|
| 380 |
+
errors = []
|
| 381 |
+
|
| 382 |
+
for team_id, task_ids in req.team_assignments.items():
|
| 383 |
+
try:
|
| 384 |
+
allocation = AllocationEngine.manual_allocation(
|
| 385 |
+
team_id, task_ids,
|
| 386 |
+
optimize_route=req.optimize_routes,
|
| 387 |
+
respect_capacity=req.respect_capacity
|
| 388 |
+
)
|
| 389 |
+
results.append(allocation.dict())
|
| 390 |
+
except HTTPException as e:
|
| 391 |
+
errors.append({"team_id": team_id, "error": e.detail})
|
| 392 |
+
|
| 393 |
+
return {
|
| 394 |
+
"successful_allocations": len(results),
|
| 395 |
+
"failed_allocations": len(errors),
|
| 396 |
+
"allocations": results,
|
| 397 |
+
"errors": errors,
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
@app.post("/allocate/reset")
|
| 402 |
+
def reset_allocations():
|
| 403 |
+
"""Reset all allocations and task statuses to unassigned."""
|
| 404 |
+
reset_all_allocations()
|
| 405 |
+
return {"status": "reset", "message": "All allocations cleared, tasks reset to UNASSIGNED"}
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
@app.get("/allocate/summary")
|
| 409 |
+
def allocation_summary():
|
| 410 |
+
return get_allocation_summary()
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
# ============================================================================
|
| 414 |
+
# TEAM MANAGEMENT
|
| 415 |
+
# ============================================================================
|
| 416 |
+
|
| 417 |
+
@app.post("/teams", status_code=201)
|
| 418 |
+
def create_team(team: FieldTeam):
|
| 419 |
+
if team.id in TEAMS:
|
| 420 |
+
raise HTTPException(409, f"Team {team.id} already exists")
|
| 421 |
+
TEAMS[team.id] = team
|
| 422 |
+
return team
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
@app.get("/teams")
|
| 426 |
+
def list_teams():
|
| 427 |
+
return list(TEAMS.values())
|
| 428 |
+
|
| 429 |
+
|
| 430 |
+
@app.get("/teams/{team_id}")
|
| 431 |
+
def get_team(team_id: str):
|
| 432 |
+
if team_id not in TEAMS:
|
| 433 |
+
raise HTTPException(404, f"Team {team_id} not found")
|
| 434 |
+
return TEAMS[team_id]
|
| 435 |
+
|
| 436 |
+
|
| 437 |
+
@app.delete("/teams/{team_id}")
|
| 438 |
+
def delete_team(team_id: str):
|
| 439 |
+
if team_id not in TEAMS:
|
| 440 |
+
raise HTTPException(404)
|
| 441 |
+
del TEAMS[team_id]
|
| 442 |
+
return {"deleted": team_id}
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
# ============================================================================
|
| 446 |
+
# TASK MANAGEMENT
|
| 447 |
+
# ============================================================================
|
| 448 |
+
|
| 449 |
+
@app.post("/tasks", status_code=201)
|
| 450 |
+
def create_task(task: Task):
|
| 451 |
+
if task.id in TASKS:
|
| 452 |
+
raise HTTPException(409, f"Task {task.id} already exists")
|
| 453 |
+
TASKS[task.id] = task
|
| 454 |
+
return task
|
| 455 |
+
|
| 456 |
+
|
| 457 |
+
@app.get("/tasks")
|
| 458 |
+
def list_tasks(
|
| 459 |
+
status: Optional[str] = Query(None, description="Filter by status"),
|
| 460 |
+
disaster_type: Optional[str] = Query(None)
|
| 461 |
+
):
|
| 462 |
+
tasks = list(TASKS.values())
|
| 463 |
+
if status:
|
| 464 |
+
tasks = [t for t in tasks if t.status.value == status]
|
| 465 |
+
if disaster_type:
|
| 466 |
+
tasks = [t for t in tasks if t.disaster_type == disaster_type]
|
| 467 |
+
return tasks
|
| 468 |
+
|
| 469 |
+
|
| 470 |
+
@app.get("/tasks/{task_id}")
|
| 471 |
+
def get_task(task_id: str):
|
| 472 |
+
if task_id not in TASKS:
|
| 473 |
+
raise HTTPException(404)
|
| 474 |
+
return TASKS[task_id]
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
@app.patch("/tasks/{task_id}/status")
|
| 478 |
+
def update_task_status(task_id: str, status: TaskStatus):
|
| 479 |
+
if task_id not in TASKS:
|
| 480 |
+
raise HTTPException(404)
|
| 481 |
+
TASKS[task_id].status = status
|
| 482 |
+
return TASKS[task_id]
|
| 483 |
+
|
| 484 |
+
|
| 485 |
+
@app.delete("/tasks/{task_id}")
|
| 486 |
+
def delete_task(task_id: str):
|
| 487 |
+
if task_id not in TASKS:
|
| 488 |
+
raise HTTPException(404)
|
| 489 |
+
del TASKS[task_id]
|
| 490 |
+
return {"deleted": task_id}
|
| 491 |
+
|
| 492 |
+
|
| 493 |
+
# ============================================================================
|
| 494 |
+
# STARTUP
|
| 495 |
+
# ============================================================================
|
| 496 |
+
|
| 497 |
+
@app.on_event("startup")
|
| 498 |
+
def startup():
|
| 499 |
+
initialize_default_teams()
|
| 500 |
+
ready = [k for k, p in {
|
| 501 |
+
"flood": flood_predictor, "cyclone": cyclone_predictor,
|
| 502 |
+
"landslide": landslide_predictor, "earthquake": earthquake_predictor
|
| 503 |
+
}.items() if p.is_ready()]
|
| 504 |
+
|
| 505 |
+
print(f"[API] Models ready: {ready or 'None — run train_model.py'}")
|
| 506 |
+
print(f"[API] Default teams initialized: {list(TEAMS.keys())}")
|