summaryrefslogtreecommitdiffstats
path: root/code/main.py
blob: d1807119ed891147811610526d6c20e28e0ba2f7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import argparse, geopandas as gpd, pandas as pd, rasterio, numpy as np
from rasterio.mask import mask
from pathlib import Path

from paths import DATA

DEPTH = DATA / "cems-efas/Europe_RP100_filled_depth.tif"
POP = DATA / "ghs_pop/GHS_POP_E2020_GLOBE_R2023A_4326_30ss_V1_0/GHS_POP_E2020_GLOBE_R2023A_4326_30ss_V1_0.tif"


def flood_file(path):
    """A file is used as-is; a CEMS folder resolves to its observedEventA layer."""
    if path.is_file():
        return path
    for ext in ("shp", "json"):
        if hits := sorted(path.glob(f"*observedEventA*.{ext}")):
            return hits[0]
    raise SystemExit(f"{path}: no observedEventA layer found")


def guess_buildings(path):
    """ems_de/... -> obat_de/**/*.gpkg, if that folder exists."""
    for parent in path.resolve().parents:
        if parent.name.startswith("ems_"):
            return next((DATA / ("obat_" + parent.name[4:])).glob("**/*.gpkg"), None)
    return None


def general(flood, path):
    print(f"File:      {path}")
    print(f"CRS:       {flood.crs}")
    print(f"Features:  {len(flood)} ({', '.join(flood.geom_type.unique())})")
    print(f"Area:      {flood.to_crs(3035).area.sum() / 1e6:.3f} km²")
    print("Bounds:    lon {:.4f}..{:.4f}, lat {:.4f}..{:.4f}".format(*flood.to_crs(4326).total_bounds[[0, 2, 1, 3]]))
    for col in flood.columns.drop("geometry"):
        if not pd.api.types.is_numeric_dtype(flood[col]) and flood[col].nunique() <= 10:
            counts = ", ".join(f"{k} ({v})" for k, v in flood[col].value_counts().items())
            print(f"{col + ':':<10} {counts}")


def buildings(flood, path):
    # Only read buildings inside the AOI's bounding box (the DE file is 8 GB, so never load it all)
    bld = gpd.read_file(path, bbox=tuple(flood.total_bounds), engine="pyogrio")
    hit = gpd.sjoin(bld, flood[["geometry"]], predicate="within").drop_duplicates("id")
    print(f"\n{len(hit)} flooded buildings (of {len(bld)} in the bounding box)")
    if hit.empty:
        return hit
    print(hit.groupby(["use", "epoch"]).size().to_string())
    print(f"Footprint: {hit['area'].sum():.0f} m², "
          f"mean height {hit.height.mean():.1f} m")
    return hit


def depth(hit, path):
    with rasterio.open(path) as r:
        d = np.array([v[0] for v in r.sample(zip(hit.geometry.x, hit.geometry.y))], dtype=float)
    d[d < 0] = np.nan
    if np.isnan(d).all():
        print("RP100 depth: no modelled depth at these buildings")
    else:
        print(f"RP100 depth at flooded buildings: mean {np.nanmean(d):.2f} m, max {np.nanmax(d):.2f} m, "
              f"{np.isnan(d).sum()} outside the modelled extent")


def population(flood, path):
    with rasterio.open(path) as r:
        arr, _ = mask(r, flood.to_crs(r.crs).geometry, crop=True, nodata=0)
    print(f"{arr.sum():.0f} people (approx.)")


if __name__ == "__main__":
    p = argparse.ArgumentParser(description="Summarise CEMS flood extents, with building impact where available.")
    p.add_argument("flood", nargs="+", type=Path, help="observedEventA file, or a CEMS vector folder")
    p.add_argument("--buildings", type=Path, help="GHS-OBAT .gpkg (default: guessed from the ems_XX folder)")
    p.add_argument("--no-buildings", action="store_true", help="skip the building analysis")
    p.add_argument("--depth", type=Path, default=DEPTH, help="RP100 depth raster")
    p.add_argument("--pop", type=Path, default=POP, help="population raster")
    args = p.parse_args()

    for i, arg in enumerate(args.flood):
        if i:
            print("\n" + "-" * 60 + "\n")
        path = flood_file(arg)
        flood = gpd.read_file(path).to_crs(4326)
        general(flood, path)

        bld_path = None if args.no_buildings else args.buildings or guess_buildings(path)
        if bld_path and bld_path.exists():
            hit = buildings(flood, bld_path)
            if len(hit) and args.depth.exists():
                depth(hit, args.depth)
        else:
            print("\nNo building data available; skipping building analysis")

        if args.pop.exists():
            population(flood, args.pop)