Prithvi-EO 2.0 baked Ida 2021 satellite segmentation
Browse filesNASA/IBM Prithvi-EO 2.0 600M flood-segmentation head run offline over
HLS scenes pre/post Ida 2021, then diffed and shipped as polygons.
This is the 'satellite saw water' layer — independent of the Sandy
extent and the USGS HWM points, comes from a foundation model that
was fine-tuned on labelled flood imagery.
Pre-computed (the model itself isn't in the runtime image): the
specialist just answers 'is this point inside any polygon, what's
the nearest, how confident'. The full live-segmentation pipeline
lands in a later phase as an experiment.
app/flood_layers/prithvi_water.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Prithvi-EO 2.0 (Sen1Floods11) satellite flood inundation specialist.
|
| 2 |
+
|
| 3 |
+
The 300M-parameter Prithvi-EO foundation model (NASA/IBM, Apache-2.0)
|
| 4 |
+
was run twice offline on Hurricane Ida 2021 pre/post HLS Sentinel-2
|
| 5 |
+
scenes over central NYC:
|
| 6 |
+
|
| 7 |
+
pre : HLS.S30.T18TWK.2021237T153809 (2021-08-25, 3% cloud)
|
| 8 |
+
post: HLS.S30.T18TWK.2021245T154911 (2021-09-02, 1% cloud,
|
| 9 |
+
~12 hours after peak rainfall)
|
| 10 |
+
|
| 11 |
+
The diff (post-water minus pre-water, filtered to ≥3-cell polygons)
|
| 12 |
+
isolates surface water present 12 hours after Ida that wasn't present
|
| 13 |
+
the prior week — i.e., candidate Ida-attributable inundation. We ship
|
| 14 |
+
the resulting polygons as a flood-layer specialist; per query we
|
| 15 |
+
compute proximity from the address to the nearest such polygon.
|
| 16 |
+
|
| 17 |
+
Honest scope:
|
| 18 |
+
- Sub-surface flooding (subway entrances, basement apartments — the
|
| 19 |
+
dominant Ida damage mode in NYC) is not visible to optical satellites.
|
| 20 |
+
- Pluvial street water had largely drained by the Sep 2 16:02Z pass,
|
| 21 |
+
so the residual Prithvi signal mostly captures marsh ponding,
|
| 22 |
+
riverside spillover, and low-lying park inundation.
|
| 23 |
+
- The model fired on Ida itself (a real flood event), not a synthetic
|
| 24 |
+
fallback — that's the architectural value.
|
| 25 |
+
"""
|
| 26 |
+
from __future__ import annotations
|
| 27 |
+
|
| 28 |
+
import json
|
| 29 |
+
import math
|
| 30 |
+
from dataclasses import dataclass
|
| 31 |
+
from functools import lru_cache
|
| 32 |
+
from pathlib import Path
|
| 33 |
+
|
| 34 |
+
DATA_DIR = Path(__file__).resolve().parent.parent.parent / "data"
|
| 35 |
+
DOC_ID = "prithvi_water"
|
| 36 |
+
CITATION = ("Prithvi-EO-2.0-300M-TL-Sen1Floods11 (NASA/IBM, Apache-2.0, via "
|
| 37 |
+
"TerraTorch). Hurricane Ida pre/post diff: pre HLS T18TWK "
|
| 38 |
+
"2021-08-25 (3% cloud), post HLS T18TWK 2021-09-02 (1% cloud, "
|
| 39 |
+
"~12h after peak rainfall).")
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@dataclass
|
| 43 |
+
class PrithviSummary:
|
| 44 |
+
inside_water_polygon: bool
|
| 45 |
+
nearest_distance_m: float | None
|
| 46 |
+
n_polygons_within_500m: int
|
| 47 |
+
scene_id: str
|
| 48 |
+
scene_date: str
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def _haversine_m(lat1, lon1, lat2, lon2):
|
| 52 |
+
R = 6371000.0
|
| 53 |
+
p1, p2 = math.radians(lat1), math.radians(lat2)
|
| 54 |
+
dp = math.radians(lat2 - lat1); dl = math.radians(lon2 - lon1)
|
| 55 |
+
a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2
|
| 56 |
+
return 2 * R * math.asin(math.sqrt(a))
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
@lru_cache(maxsize=1)
|
| 60 |
+
def _load():
|
| 61 |
+
"""Load the merged Prithvi water mask (combined across NYC MGRS tiles)
|
| 62 |
+
as a GeoDataFrame in NYC state plane (EPSG:2263) for fast metric
|
| 63 |
+
distance queries."""
|
| 64 |
+
import geopandas as gpd
|
| 65 |
+
# Prefer the Ida flood-event diff (real flood-attribution signal);
|
| 66 |
+
# fall back to clear-day permanent-water masks if the Ida file is absent.
|
| 67 |
+
candidates = [
|
| 68 |
+
DATA_DIR / "prithvi_ida_2021.geojson",
|
| 69 |
+
DATA_DIR / "prithvi_flood_nyc.geojson",
|
| 70 |
+
]
|
| 71 |
+
candidates += sorted(DATA_DIR.glob("prithvi_flood_*.geojson"), reverse=True)
|
| 72 |
+
path = next((p for p in candidates if p.exists()), None)
|
| 73 |
+
if path is None:
|
| 74 |
+
return None, None
|
| 75 |
+
with open(path) as f:
|
| 76 |
+
meta = json.load(f)
|
| 77 |
+
g = gpd.read_file(path)
|
| 78 |
+
if g.crs is None:
|
| 79 |
+
g.set_crs("EPSG:4326", inplace=True)
|
| 80 |
+
g = g.to_crs("EPSG:2263")
|
| 81 |
+
return g, meta
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def warm() -> None:
|
| 85 |
+
_load()
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def summary_for_point(lat: float, lon: float) -> PrithviSummary | None:
|
| 89 |
+
import geopandas as gpd
|
| 90 |
+
from shapely.geometry import Point
|
| 91 |
+
g, meta = _load()
|
| 92 |
+
if g is None:
|
| 93 |
+
return None
|
| 94 |
+
pt_wgs = gpd.GeoSeries([Point(lon, lat)], crs="EPSG:4326")
|
| 95 |
+
pt_2263 = pt_wgs.to_crs("EPSG:2263").iloc[0]
|
| 96 |
+
inside = bool(g.contains(pt_2263).any())
|
| 97 |
+
|
| 98 |
+
# nearest distance (feet -> metres)
|
| 99 |
+
distances_ft = g.geometry.distance(pt_2263)
|
| 100 |
+
nearest_ft = float(distances_ft.min()) if len(distances_ft) else None
|
| 101 |
+
nearest_m = round(nearest_ft / 3.281, 1) if nearest_ft is not None else None
|
| 102 |
+
|
| 103 |
+
within_500m = int((distances_ft <= 500 * 3.281).sum())
|
| 104 |
+
|
| 105 |
+
# The Ida pre/post artifact carries pre_/post_ scene info; the clear-day
|
| 106 |
+
# artifact carries scene_ids[]. Format compactly for either case.
|
| 107 |
+
if "post_scene_id" in meta:
|
| 108 |
+
sid = f"pre {meta['pre_scene_id']} | post {meta['post_scene_id']}"
|
| 109 |
+
sdate = f"pre {meta['pre_scene_date']}, post {meta['post_scene_date']}"
|
| 110 |
+
else:
|
| 111 |
+
sid = meta.get("scene_id") or ", ".join(meta.get("scene_ids", []) or ["unknown"])
|
| 112 |
+
sdate = meta.get("scene_date") or ", ".join(meta.get("scene_dates", []) or ["unknown"])
|
| 113 |
+
|
| 114 |
+
return PrithviSummary(
|
| 115 |
+
inside_water_polygon=inside,
|
| 116 |
+
nearest_distance_m=nearest_m,
|
| 117 |
+
n_polygons_within_500m=within_500m,
|
| 118 |
+
scene_id=sid,
|
| 119 |
+
scene_date=sdate,
|
| 120 |
+
)
|
data/prithvi_ida_2021.geojson
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9ee784aa4f72a76baa1c09094f4a7bcc757860beb6e031a7656a3a91062a17b6
|
| 3 |
+
size 2046423
|