clarindasusan commited on
Commit
3068f1a
·
verified ·
1 Parent(s): 560ffc8

Update api.py

Browse files
Files changed (1) hide show
  1. api.py +462 -348
api.py CHANGED
@@ -1,392 +1,506 @@
1
  """
2
- Cyclone Prediction & Resource Optimization API
3
- Optimized for HuggingFace Spaces Deployment
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  """
5
 
6
- from fastapi import FastAPI, HTTPException
7
- from fastapi.responses import RedirectResponse, JSONResponse
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
- # Error handling for imports
15
- try:
16
- import pickle
17
- import numpy as np
18
- import pandas as pd
19
- except ImportError as e:
20
- print(f"Warning: Failed to import dependencies: {e}")
21
-
22
- # Check if we're in HuggingFace Spaces
23
- IS_HUGGINGFACE = os.environ.get('SPACE_ID') is not None
24
-
25
- # Add src to path
26
- sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
27
 
28
  # ============================================================================
29
- # INITIALIZE APP
30
  # ============================================================================
31
 
32
  app = FastAPI(
33
- title="Cyclone Prediction & Resource Optimization API",
34
- description="ML-based cyclone prediction with real-time IMD data",
35
- version="4.0.0",
36
- docs_url="/docs",
37
- redoc_url="/redoc"
38
  )
39
 
40
- # ============================================================================
41
- # GLOBAL VARIABLES
42
- # ============================================================================
 
 
 
43
 
44
- model = None
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
- # INITIALIZATION
63
  # ============================================================================
64
 
65
- @app.on_event("startup")
66
- async def startup_event():
67
- """Initialize the application on startup"""
68
- global model, districts_df, imd_fetcher, feature_engineer
69
-
70
- print("=" * 60)
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
- # PYDANTIC MODELS
120
  # ============================================================================
121
 
122
- class SimpleCycloneInput(BaseModel):
123
- LAT: float = Field(..., description="Latitude", example=15.5)
124
- LON: float = Field(..., description="Longitude", example=85.3)
125
- MAX_WIND: Optional[float] = Field(65, description="Current max wind speed (knots)")
126
- MIN_PRESSURE: Optional[float] = Field(990, description="Current min pressure (hPa)")
127
- STORM_TYPE: Optional[str] = Field("TS", description="Storm type")
128
-
129
- class PredictionResponse(BaseModel):
130
- predicted_intensity: float
131
- predicted_intensity_kmh: float
132
- intensity_category: str
133
- status: str
134
- data_source: str
135
- current_parameters: Optional[Dict] = None
136
- distance_to_odisha_km: Optional[float] = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
  # ============================================================================
139
- # HELPER FUNCTIONS
140
  # ============================================================================
141
 
142
- def build_model_input(engineered_features: dict):
143
- """Build properly formatted DataFrame for model prediction"""
144
- safe_features = {}
145
- for feat in FEATURE_NAMES:
146
- value = engineered_features.get(feat)
147
- if value is None or (isinstance(value, float) and np.isnan(value)):
148
- safe_features[feat] = 0.0
149
- else:
150
- safe_features[feat] = value
151
-
152
- return pd.DataFrame([[safe_features[f] for f in FEATURE_NAMES]],
153
- columns=FEATURE_NAMES)
154
-
155
- def get_intensity_category(wind_speed_knots):
156
- """Categorize cyclone intensity"""
157
- if wind_speed_knots < 34:
158
- return "Tropical Depression"
159
- elif wind_speed_knots < 64:
160
- return "Tropical Storm"
161
- elif wind_speed_knots < 83:
162
- return "Category 1 Cyclone"
163
- elif wind_speed_knots < 96:
164
- return "Category 2 Cyclone"
165
- elif wind_speed_knots < 113:
166
- return "Category 3 Cyclone (Major)"
167
- elif wind_speed_knots < 137:
168
- return "Category 4 Cyclone (Major)"
169
- else:
170
- return "Category 5 Cyclone (Catastrophic)"
171
 
172
  # ============================================================================
173
- # API ENDPOINTS
174
  # ============================================================================
175
 
176
- @app.get("/")
177
- async def root():
178
- """Redirect to docs"""
179
- return RedirectResponse(url="/docs")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
- @app.get("/health")
182
- async def health_check():
183
- """Health check endpoint"""
184
  return {
185
- "status": "healthy",
186
- "model_loaded": model is not None,
187
- "districts_loaded": districts_df is not None,
188
- "fetchers_ready": imd_fetcher is not None and feature_engineer is not None,
189
- "timestamp": datetime.now().isoformat(),
190
- "environment": "HuggingFace Spaces" if IS_HUGGINGFACE else "Local"
191
  }
192
 
193
- @app.get("/info")
194
- async def get_api_info():
195
- """Get API information"""
196
- return {
197
- "title": "Cyclone Prediction & Resource Optimization API",
198
- "version": "4.0.0",
199
- "status": "operational",
200
- "model_loaded": model is not None,
201
- "districts_loaded": districts_df is not None,
202
- "total_districts": len(districts_df) if districts_df is not None else 0,
203
- "endpoints": {
204
- "docs": "/docs",
205
- "health": "/health",
206
- "predict": "/predict/manual (POST)",
207
- "districts": "/data/districts",
208
- "live_cyclones": "/live/cyclones",
209
- "live_predict": "/live/predict"
210
- },
211
- "example_request": {
212
- "url": "/predict/manual",
213
- "method": "POST",
214
- "body": {
215
- "LAT": 15.5,
216
- "LON": 85.3,
217
- "MAX_WIND": 65,
218
- "MIN_PRESSURE": 990
219
  }
220
- }
 
 
 
 
221
  }
222
 
223
- @app.post("/predict/manual", response_model=PredictionResponse)
224
- async def predict_from_manual_input(cyclone_data: SimpleCycloneInput):
225
- """Predict cyclone intensity from manual input"""
226
- if model is None:
227
- raise HTTPException(status_code=503, detail="Model not loaded. Check /health endpoint.")
 
 
 
 
 
 
 
 
228
 
229
- if feature_engineer is None:
230
- raise HTTPException(status_code=503, detail="Feature engineer not initialized.")
 
 
 
 
 
 
 
 
 
 
 
231
 
 
 
 
 
232
  try:
233
- raw_data = cyclone_data.model_dump()
234
- engineered_features = feature_engineer.engineer_features(raw_data)
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
- except Exception as e:
256
- raise HTTPException(status_code=500, detail=f"Prediction error: {str(e)}")
257
-
258
- @app.get("/live/cyclones")
259
- async def get_active_cyclones():
260
- """Fetch active cyclone alerts from IMD"""
261
- if imd_fetcher is None:
262
- raise HTTPException(status_code=503, detail="IMD fetcher not initialized")
263
-
264
- try:
265
- result = imd_fetcher.fetch_active_cyclones()
266
- return result
267
- except Exception as e:
268
- raise HTTPException(status_code=500, detail=f"Error fetching IMD data: {str(e)}")
269
-
270
- @app.get("/live/bulletin")
271
- async def get_live_bulletin():
272
- """Fetch hourly bulletin from IMD"""
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.exception_handler(500)
381
- async def internal_error_handler(request, exc):
382
- return JSONResponse(
383
- status_code=500,
384
- content={
385
- "error": "Internal server error",
386
- "message": str(exc)
387
- }
 
 
 
 
 
 
388
  )
389
 
390
- if __name__ == "__main__":
391
- import uvicorn
392
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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())}")