GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s
106 lines
3.6 KiB
Python
106 lines
3.6 KiB
Python
"""Flood risk must be a share of what was modelled, not of what was drawn.
|
|
|
|
The share and fraction divided the inundated cells by every cell whose centre
|
|
fell inside the selection, including cells where the VMM raster holds nodata
|
|
because the area lies outside the modelled extent. An operator drawing a
|
|
rectangle that reaches past the model coverage read "3% at risk" where the
|
|
honest answer is "of the 40% we have a model for, 7.5% is at risk, and for the
|
|
rest there is no model at all".
|
|
|
|
Terrain, bathymetry and thematic raster analysis already divide by valid cells
|
|
and report a coverage ratio; this brings flood hazard in line.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
np = pytest.importorskip("numpy")
|
|
|
|
from app.services.flood_hazard_analysis_service import ( # noqa: E402 - optional NumPy gate precedes service import
|
|
FloodHazardCellStatistics,
|
|
)
|
|
|
|
|
|
NODATA = -9999.0
|
|
|
|
|
|
def _stats(values, selected) -> FloodHazardCellStatistics:
|
|
return FloodHazardCellStatistics.from_cells(
|
|
np.asarray(values, dtype="float64"),
|
|
np.asarray(selected, dtype=bool),
|
|
nodata=NODATA,
|
|
)
|
|
|
|
|
|
def test_share_ignores_cells_the_model_does_not_cover() -> None:
|
|
# Ten selected cells: four modelled (one of them wet), six nodata.
|
|
values = [1.5, 0.0, 0.0, 0.0] + [NODATA] * 6
|
|
selected = [True] * 10
|
|
|
|
stats = _stats(values, selected)
|
|
|
|
assert stats.selected_cell_count == 10
|
|
assert stats.valid_cell_count == 4
|
|
assert stats.no_data_cell_count == 6
|
|
assert stats.inundated_cell_count == 1
|
|
# 1 of 4 modelled cells, not 1 of 10 drawn cells.
|
|
assert stats.inundated_fraction == pytest.approx(0.25)
|
|
assert stats.data_coverage_ratio == pytest.approx(0.4)
|
|
|
|
|
|
def test_cells_outside_the_drawn_selection_are_not_counted() -> None:
|
|
values = [1.5, 1.5, 0.0, 0.0]
|
|
selected = [True, False, True, False]
|
|
|
|
stats = _stats(values, selected)
|
|
|
|
assert stats.selected_cell_count == 2
|
|
assert stats.valid_cell_count == 2
|
|
assert stats.inundated_cell_count == 1
|
|
assert stats.inundated_fraction == pytest.approx(0.5)
|
|
|
|
|
|
def test_a_selection_without_any_model_data_reports_zero_coverage() -> None:
|
|
stats = _stats([NODATA] * 4, [True] * 4)
|
|
|
|
assert stats.valid_cell_count == 0
|
|
assert stats.no_data_cell_count == 4
|
|
assert stats.data_coverage_ratio == 0.0
|
|
# No model, so no risk figure may be invented.
|
|
assert stats.inundated_fraction is None
|
|
|
|
|
|
def test_nan_is_treated_as_missing_model_data() -> None:
|
|
stats = _stats([float("nan"), 2.0], [True, True])
|
|
|
|
assert stats.valid_cell_count == 1
|
|
assert stats.no_data_cell_count == 1
|
|
assert stats.inundated_cell_count == 1
|
|
|
|
|
|
def test_negative_depths_are_data_but_not_inundation() -> None:
|
|
"""A modelled zero or negative depth means dry, not unknown."""
|
|
|
|
stats = _stats([0.0, 0.0, 3.0], [True, True, True])
|
|
|
|
assert stats.valid_cell_count == 3
|
|
assert stats.inundated_cell_count == 1
|
|
assert stats.inundated_fraction == pytest.approx(1 / 3)
|
|
|
|
|
|
def test_depth_statistics_use_only_inundated_cells() -> None:
|
|
stats = _stats([0.0, 2.0, 4.0, NODATA], [True] * 4)
|
|
|
|
assert stats.depth_values.tolist() == [2.0, 4.0]
|
|
assert stats.depth_values.mean() == pytest.approx(3.0)
|
|
|
|
|
|
def test_areas_are_derived_from_the_matching_cell_populations() -> None:
|
|
stats = _stats([1.0, 1.0, 0.0, NODATA], [True] * 4)
|
|
|
|
# 100 m2 cells: 2 inundated, 3 modelled, 4 drawn.
|
|
assert stats.inundated_area_ha(100.0) == pytest.approx(2 * 100.0 / 10_000.0)
|
|
assert stats.analysed_area_ha(100.0) == pytest.approx(3 * 100.0 / 10_000.0)
|
|
assert stats.selected_area_ha(100.0) == pytest.approx(4 * 100.0 / 10_000.0)
|