clarindasusan commited on
Commit
b0befeb
Β·
verified Β·
1 Parent(s): 51bcb4b

Update api.py

Browse files
Files changed (1) hide show
  1. api.py +416 -8
api.py CHANGED
@@ -50,6 +50,10 @@ from src.allocation import (
50
  get_allocation_summary, reset_all_allocations, initialize_default_teams
51
  )
52
  from src.live_data_fetcher import IMDCycloneDataFetcher, CycloneFeatureEngineer
 
 
 
 
53
 
54
 
55
  # ============================================================================
@@ -83,6 +87,7 @@ multi_hazard = MultiHazardPredictor(MODEL_DIR)
83
  lane_mapper = LaneFloodMapper(flood_predictor)
84
  imd_fetcher = IMDCycloneDataFetcher()
85
  cyclone_engineer = CycloneFeatureEngineer()
 
86
 
87
 
88
  # ── Replace the existing FloodFeatures block and add the rest ──────────────
@@ -373,35 +378,438 @@ import numpy as np # needed for explain endpoint
373
 
374
  @app.get("/predict/cyclone/live")
375
  def predict_cyclone_live():
 
 
 
 
 
 
376
  bulletin = imd_fetcher.fetch_hourly_bulletin()
377
- if bulletin["status"] != "success":
378
- raise HTTPException(502, f"IMD fetch failed: {bulletin.get('message')}")
 
 
 
 
 
 
 
 
 
 
379
 
380
- raw_params = imd_fetcher.parse_cyclone_parameters(bulletin["content"])
381
  if not raw_params:
382
- raise HTTPException(404, "No cyclone parameters detected in IMD bulletin")
 
 
 
 
 
 
383
 
 
384
  engineered = cyclone_engineer.engineer_features(raw_params)
385
- features = cyclone_engineer.to_model_features(engineered) # ← bridge
386
 
387
  errors = cyclone_predictor.validate_input(features)
388
  if errors:
389
  raise HTTPException(422, {"validation_errors": errors})
390
 
391
  result = cyclone_predictor.predict(features, 50)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
  return {
393
- "source": "IMD RSMC live bulletin",
 
394
  "raw_parameters": raw_params,
395
- "model_features_used": features, # helpful for debugging
 
396
  **prediction_result_to_dict(result),
397
  }
398
 
 
399
  @app.get("/live/cyclone/raw")
400
  def get_live_cyclone_raw():
401
- return imd_fetcher.fetch_hourly_bulletin()
 
 
 
 
 
 
 
 
 
 
402
 
403
 
 
 
 
 
 
 
 
 
 
404
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
  # ============================================================================
406
  # LANE-LEVEL FLOOD MAP ENDPOINTS
407
  # ============================================================================
 
50
  get_allocation_summary, reset_all_allocations, initialize_default_teams
51
  )
52
  from src.live_data_fetcher import IMDCycloneDataFetcher, CycloneFeatureEngineer
53
+ from src.live_data_fetcher import (
54
+ IMDCycloneDataFetcher, CycloneFeatureEngineer,
55
+ IMDFloodDataFetcher # ← add this
56
+ )
57
 
58
 
59
  # ============================================================================
 
87
  lane_mapper = LaneFloodMapper(flood_predictor)
88
  imd_fetcher = IMDCycloneDataFetcher()
89
  cyclone_engineer = CycloneFeatureEngineer()
90
+ flood_fetcher = IMDFloodDataFetcher()
91
 
92
 
93
  # ── Replace the existing FloodFeatures block and add the rest ──────────────
 
378
 
379
  @app.get("/predict/cyclone/live")
380
  def predict_cyclone_live():
381
+ """
382
+ Fetch live IMD data, predict cyclone risk, return prediction
383
+ + a heatmap-ready GeoJSON point for frontend rendering.
384
+ Falls back to RSMC page scrape if bulletin TXT is unavailable.
385
+ """
386
+ # ── Step 1: Try bulletin TXT ───────────────────────────────────────────
387
  bulletin = imd_fetcher.fetch_hourly_bulletin()
388
+ raw_params = {}
389
+
390
+ if bulletin["status"] == "success":
391
+ raw_params = imd_fetcher.parse_cyclone_parameters(bulletin["content"])
392
+
393
+ # ── Step 2: Fallback to page scrape ───────────────────────────────────
394
+ if not raw_params:
395
+ page_data = imd_fetcher.fetch_rsmc_page_alerts()
396
+ if page_data["alerts"]:
397
+ # Try to parse coords from alert text
398
+ combined_text = " ".join(page_data["alerts"])
399
+ raw_params = imd_fetcher.parse_cyclone_parameters(combined_text)
400
 
401
+ # ── Step 3: If still no params, return no-storm status ────────────────
402
  if not raw_params:
403
+ return {
404
+ "status": "no_active_storm",
405
+ "message": "No active cyclone detected in IMD feeds",
406
+ "source": bulletin.get("url_used", "IMD RSMC"),
407
+ "timestamp": datetime.now().isoformat(),
408
+ "heatmap_geojson": None,
409
+ }
410
 
411
+ # ── Step 4: Engineer features and predict ─────────────────────────────
412
  engineered = cyclone_engineer.engineer_features(raw_params)
413
+ features = cyclone_engineer.to_model_features(engineered)
414
 
415
  errors = cyclone_predictor.validate_input(features)
416
  if errors:
417
  raise HTTPException(422, {"validation_errors": errors})
418
 
419
  result = cyclone_predictor.predict(features, 50)
420
+
421
+ # ── Step 5: Build heatmap GeoJSON ─────────────────────────────────────
422
+ lat = raw_params.get("LAT")
423
+ lon = raw_params.get("LON")
424
+ heatmap_geojson = _build_cyclone_heatmap_geojson(
425
+ lat=lat,
426
+ lon=lon,
427
+ risk_score=result.risk_score,
428
+ risk_tier=result.risk_tier.value,
429
+ uncertainty=result.uncertainty,
430
+ raw_params=raw_params,
431
+ features=features,
432
+ ) if (lat and lon) else None
433
+
434
  return {
435
+ "status": "active_storm",
436
+ "source": bulletin.get("url_used", "IMD RSMC page"),
437
  "raw_parameters": raw_params,
438
+ "model_features_used": features,
439
+ "heatmap_geojson": heatmap_geojson,
440
  **prediction_result_to_dict(result),
441
  }
442
 
443
+
444
  @app.get("/live/cyclone/raw")
445
  def get_live_cyclone_raw():
446
+ """
447
+ Returns raw IMD bulletin content + RSMC page alerts.
448
+ Useful for debugging what IMD is currently publishing.
449
+ """
450
+ bulletin = imd_fetcher.fetch_hourly_bulletin()
451
+ page = imd_fetcher.fetch_rsmc_page_alerts()
452
+ return {
453
+ "bulletin": bulletin,
454
+ "rsmc_page": page,
455
+ "timestamp": datetime.now().isoformat(),
456
+ }
457
 
458
 
459
+ @app.get("/live/cyclone/heatmap")
460
+ def get_cyclone_heatmap():
461
+ """
462
+ Dedicated heatmap endpoint β€” returns GeoJSON FeatureCollection
463
+ with risk-annotated points ready for Leaflet / Mapbox heatmap layer.
464
+ Frontend can poll this every N minutes and re-render.
465
+ """
466
+ bulletin = imd_fetcher.fetch_hourly_bulletin()
467
+ raw_params = {}
468
 
469
+ if bulletin["status"] == "success":
470
+ raw_params = imd_fetcher.parse_cyclone_parameters(bulletin["content"])
471
+
472
+ if not raw_params:
473
+ page_data = imd_fetcher.fetch_rsmc_page_alerts()
474
+ combined = " ".join(page_data.get("alerts", []))
475
+ raw_params = imd_fetcher.parse_cyclone_parameters(combined)
476
+
477
+ if not raw_params or "LAT" not in raw_params or "LON" not in raw_params:
478
+ # Return empty FeatureCollection β€” frontend renders nothing
479
+ return {
480
+ "type": "FeatureCollection",
481
+ "features": [],
482
+ "metadata": {
483
+ "status": "no_active_storm",
484
+ "timestamp": datetime.now().isoformat(),
485
+ "message": "No active cyclone with known coordinates detected",
486
+ }
487
+ }
488
+
489
+ engineered = cyclone_engineer.engineer_features(raw_params)
490
+ features = cyclone_engineer.to_model_features(engineered)
491
+ result = cyclone_predictor.predict(features, 50)
492
+
493
+ geojson = _build_cyclone_heatmap_geojson(
494
+ lat=raw_params["LAT"],
495
+ lon=raw_params["LON"],
496
+ risk_score=result.risk_score,
497
+ risk_tier=result.risk_tier.value,
498
+ uncertainty=result.uncertainty,
499
+ raw_params=raw_params,
500
+ features=features,
501
+ )
502
+
503
+ return geojson
504
+
505
+
506
+ # ── Helper β€” builds heatmap GeoJSON ───────────────────────────────────────
507
+
508
+ def _build_cyclone_heatmap_geojson(
509
+ lat: float,
510
+ lon: float,
511
+ risk_score: float,
512
+ risk_tier: str,
513
+ uncertainty: float,
514
+ raw_params: dict,
515
+ features: dict,
516
+ ) -> dict:
517
+ """
518
+ Returns a GeoJSON FeatureCollection with:
519
+ - A Point at storm centre with full risk properties
520
+ - Radius rings at 50km, 150km, 300km for heatmap intensity falloff
521
+
522
+ Frontend usage (Leaflet example):
523
+ L.heatLayer(
524
+ geojson.features.map(f => [
525
+ f.geometry.coordinates[1],
526
+ f.geometry.coordinates[0],
527
+ f.properties.intensity
528
+ ]),
529
+ { radius: 60, blur: 40, maxZoom: 10 }
530
+ )
531
+ """
532
+ import math
533
+
534
+ color_map = {
535
+ "LOW": "#2ecc71",
536
+ "MODERATE": "#f39c12",
537
+ "HIGH": "#e74c3c",
538
+ "CRITICAL": "#8e44ad",
539
+ }
540
+
541
+ features_list = []
542
+
543
+ # Centre point β€” full intensity
544
+ features_list.append({
545
+ "type": "Feature",
546
+ "geometry": {"type": "Point", "coordinates": [lon, lat]},
547
+ "properties": {
548
+ "risk_score": risk_score,
549
+ "risk_tier": risk_tier,
550
+ "uncertainty": uncertainty,
551
+ "intensity": risk_score, # Leaflet heatmap weight
552
+ "color": color_map.get(risk_tier, "#95a5a6"),
553
+ "wind_kmh": raw_params.get("MAX_WIND", 0) * 1.852,
554
+ "pressure_hpa": raw_params.get("MIN_PRESSURE", 1000),
555
+ "point_type": "storm_centre",
556
+ "label": f"Cyclone Risk: {risk_tier} ({risk_score:.2f})",
557
+ }
558
+ })
559
+
560
+ # Falloff rings β€” intensity decreases with distance
561
+ for radius_km, falloff in [(50, 0.85), (150, 0.60), (300, 0.30)]:
562
+ # Generate 8 cardinal points on the ring
563
+ for bearing_deg in range(0, 360, 45):
564
+ bearing = math.radians(bearing_deg)
565
+ R = 6371 # Earth radius km
566
+ lat_r = math.radians(lat)
567
+ lon_r = math.radians(lon)
568
+ d_r = radius_km / R
569
+
570
+ ring_lat = math.degrees(math.asin(
571
+ math.sin(lat_r) * math.cos(d_r) +
572
+ math.cos(lat_r) * math.sin(d_r) * math.cos(bearing)
573
+ ))
574
+ ring_lon = math.degrees(lon_r + math.atan2(
575
+ math.sin(bearing) * math.sin(d_r) * math.cos(lat_r),
576
+ math.cos(d_r) - math.sin(lat_r) * math.sin(math.radians(ring_lat))
577
+ ))
578
+
579
+ features_list.append({
580
+ "type": "Feature",
581
+ "geometry": {
582
+ "type": "Point",
583
+ "coordinates": [ring_lon, ring_lat]
584
+ },
585
+ "properties": {
586
+ "risk_score": round(risk_score * falloff, 4),
587
+ "intensity": round(risk_score * falloff, 4),
588
+ "risk_tier": risk_tier,
589
+ "point_type": f"ring_{radius_km}km",
590
+ "radius_km": radius_km,
591
+ "color": color_map.get(risk_tier, "#95a5a6"),
592
+ }
593
+ })
594
+
595
+ return {
596
+ "type": "FeatureCollection",
597
+ "features": features_list,
598
+ "metadata": {
599
+ "storm_centre": {"lat": lat, "lon": lon},
600
+ "risk_score": risk_score,
601
+ "risk_tier": risk_tier,
602
+ "uncertainty": uncertainty,
603
+ "wind_kmh": round(raw_params.get("MAX_WIND", 0) * 1.852, 1),
604
+ "pressure_hpa": raw_params.get("MIN_PRESSURE", 1000),
605
+ "timestamp": datetime.now().isoformat(),
606
+ "source": "IMD RSMC + FNN Cyclone Predictor",
607
+ "total_points": len(features_list),
608
+ "rendering_hint": {
609
+ "leaflet_heatmap": "use intensity property as weight",
610
+ "mapbox_circle": "use risk_score for fill-opacity, color for fill-color",
611
+ "refresh_seconds": 1800,
612
+ }
613
+ }
614
+ }
615
+
616
+ @app.get("/live/flood/heatmap")
617
+ def get_flood_heatmap():
618
+ """
619
+ Fetches live rainfall from IMD city stations, runs FNN flood
620
+ prediction for each city, returns heatmap-ready GeoJSON.
621
+
622
+ Frontend usage (Leaflet heatmap):
623
+ fetch('/live/flood/heatmap')
624
+ .then(r => r.json())
625
+ .then(geojson => {
626
+ L.heatLayer(
627
+ geojson.features.map(f => [
628
+ f.geometry.coordinates[1],
629
+ f.geometry.coordinates[0],
630
+ f.properties.intensity
631
+ ]),
632
+ { radius: 80, blur: 50, maxZoom: 8, max: 1.0 }
633
+ ).addTo(map);
634
+ });
635
+
636
+ Each feature also has 'color' and 'risk_tier' for circle/marker layers.
637
+ """
638
+ if not flood_predictor.is_ready():
639
+ raise HTTPException(503, "Flood model not loaded. Run train_model.py first.")
640
+
641
+ city_data = flood_fetcher.fetch_all_cities()
642
+
643
+ color_map = {
644
+ "LOW": "#2ecc71",
645
+ "MODERATE": "#f39c12",
646
+ "HIGH": "#e74c3c",
647
+ "CRITICAL": "#8e44ad",
648
+ }
649
+
650
+ features_list = []
651
+ failed_cities = []
652
+ successful_cities = []
653
+
654
+ for city_obs in city_data:
655
+ city = city_obs["city"]
656
+
657
+ # Use observed rainfall or fallback to 0
658
+ rainfall_mm = city_obs.get("rainfall_mm")
659
+ if rainfall_mm is None:
660
+ # IMD fetch failed for this city β€” use 0 rainfall
661
+ # Still render city with baseline risk
662
+ rainfall_mm = 0.0
663
+ data_source = "baseline (IMD unavailable)"
664
+ else:
665
+ data_source = "IMD live"
666
+
667
+ lat = city_obs.get("lat", 0)
668
+ lon = city_obs.get("lon", 0)
669
+
670
+ if not lat or not lon:
671
+ failed_cities.append(city)
672
+ continue
673
+
674
+ # Build full flood feature vector
675
+ static = city_obs.get("static_features", {})
676
+ soil_sat = flood_fetcher.estimate_soil_saturation(rainfall_mm)
677
+
678
+ flood_features = {
679
+ "rainfall_mm": float(rainfall_mm),
680
+ "elevation_m": static.get("elevation_m", 50.0),
681
+ "soil_saturation_pct": soil_sat,
682
+ "dist_river": static.get("dist_river", 2.0),
683
+ "drainage_capacity_index": static.get("drainage_capacity_index", 0.5),
684
+ "flow_accumulation": static.get("flow_accumulation", 0.5),
685
+ "twi": static.get("twi", 8.0),
686
+ }
687
+
688
+ # Validate and predict
689
+ errors = flood_predictor.validate_input(flood_features)
690
+ if errors:
691
+ failed_cities.append(f"{city}: {errors}")
692
+ continue
693
+
694
+ try:
695
+ result = flood_predictor.predict(flood_features, n_mc_samples=30)
696
+ except Exception as e:
697
+ failed_cities.append(f"{city}: {str(e)}")
698
+ continue
699
+
700
+ successful_cities.append(city)
701
+
702
+ features_list.append({
703
+ "type": "Feature",
704
+ "geometry": {
705
+ "type": "Point",
706
+ "coordinates": [lon, lat]
707
+ },
708
+ "properties": {
709
+ # Heatmap rendering
710
+ "intensity": result.risk_score,
711
+ "color": color_map.get(result.risk_tier.value, "#95a5a6"),
712
+
713
+ # Risk info
714
+ "city": city,
715
+ "risk_score": result.risk_score,
716
+ "risk_tier": result.risk_tier.value,
717
+ "uncertainty": result.uncertainty,
718
+ "ci_lower": result.confidence_interval[0],
719
+ "ci_upper": result.confidence_interval[1],
720
+
721
+ # Input data (useful for tooltip display)
722
+ "rainfall_mm": round(rainfall_mm, 1),
723
+ "soil_saturation_pct": round(soil_sat, 1),
724
+ "elevation_m": static.get("elevation_m"),
725
+ "data_source": data_source,
726
+
727
+ # Tooltip-ready label
728
+ "label": (
729
+ f"{city}: {result.risk_tier.value} flood risk "
730
+ f"(score={result.risk_score:.2f}, "
731
+ f"rain={rainfall_mm:.1f}mm)"
732
+ ),
733
+ }
734
+ })
735
+
736
+ return {
737
+ "type": "FeatureCollection",
738
+ "features": features_list,
739
+ "metadata": {
740
+ "timestamp": datetime.now().isoformat(),
741
+ "source": "IMD City Weather + FNN Flood Model",
742
+ "cities_monitored": len(city_data),
743
+ "cities_successful": len(successful_cities),
744
+ "cities_failed": len(failed_cities),
745
+ "successful": successful_cities,
746
+ "failed": failed_cities,
747
+ "data_note": (
748
+ "Rainfall from IMD city stations where available. "
749
+ "Cities with unavailable data show baseline risk (0mm rainfall). "
750
+ "Static features (elevation, drainage, TWI) from training dataset."
751
+ ),
752
+ "rendering_hint": {
753
+ "leaflet_heatmap": "use 'intensity' property as weight",
754
+ "leaflet_circles": "use 'color' for fillColor, 'risk_score' for radius scaling",
755
+ "mapbox": "use 'risk_score' for fill-opacity, 'color' for fill-color",
756
+ "refresh_seconds": 3600,
757
+ }
758
+ }
759
+ }
760
+
761
+
762
+ @app.get("/live/flood/city/{city_name}")
763
+ def get_flood_risk_single_city(city_name: str):
764
+ """
765
+ Get live flood risk for a single city by name.
766
+ City names: Mumbai, Chennai, Kolkata, Delhi, Hyderabad,
767
+ Bangalore, Bhubaneswar, Patna, Guwahati, Kochi
768
+ """
769
+ if not flood_predictor.is_ready():
770
+ raise HTTPException(503, "Flood model not loaded.")
771
+
772
+ # Normalise city name
773
+ city_name = city_name.strip().title()
774
+ station_id = flood_fetcher.CITY_STATIONS.get(city_name)
775
+
776
+ if not station_id:
777
+ raise HTTPException(404, {
778
+ "error": f"City '{city_name}' not monitored.",
779
+ "available_cities": list(flood_fetcher.CITY_STATIONS.keys())
780
+ })
781
+
782
+ obs = flood_fetcher.fetch_city_rainfall(city_name, station_id)
783
+ rainfall_mm = obs.get("rainfall_mm") or 0.0
784
+ lat, lon = flood_fetcher.STATION_COORDS[city_name]
785
+ static = flood_fetcher.CITY_STATIC_FEATURES[city_name]
786
+ soil_sat = flood_fetcher.estimate_soil_saturation(rainfall_mm)
787
+
788
+ flood_features = {
789
+ "rainfall_mm": float(rainfall_mm),
790
+ "elevation_m": static["elevation_m"],
791
+ "soil_saturation_pct": soil_sat,
792
+ "dist_river": static["dist_river"],
793
+ "drainage_capacity_index": static["drainage_capacity_index"],
794
+ "flow_accumulation": static["flow_accumulation"],
795
+ "twi": static["twi"],
796
+ }
797
+
798
+ errors = flood_predictor.validate_input(flood_features)
799
+ if errors:
800
+ raise HTTPException(422, {"validation_errors": errors})
801
+
802
+ result = flood_predictor.predict(flood_features, n_mc_samples=50)
803
+
804
+ return {
805
+ "city": city_name,
806
+ "coordinates": {"lat": lat, "lon": lon},
807
+ "rainfall_mm": rainfall_mm,
808
+ "imd_status": obs["status"],
809
+ "flood_features": flood_features,
810
+ **prediction_result_to_dict(result),
811
+ "timestamp": datetime.now().isoformat(),
812
+ }
813
  # ============================================================================
814
  # LANE-LEVEL FLOOD MAP ENDPOINTS
815
  # ============================================================================