122 lines
4.4 KiB
Python
122 lines
4.4 KiB
Python
"""A selection finer than the source raster must answer, not return zero.
|
|
|
|
End-to-end counterpart to ``test_raster_cell_selection``: the analysis reads a
|
|
real GeoTIFF, so it proves the fallback survives the clip/mask path the service
|
|
actually uses rather than only the helper in isolation.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
|
|
np = pytest.importorskip("numpy")
|
|
rasterio = pytest.importorskip("rasterio")
|
|
|
|
from pyproj import Transformer # noqa: E402 - optional rasterio gate precedes geospatial imports
|
|
from rasterio.transform import from_origin # noqa: E402 - optional rasterio gate precedes geospatial imports
|
|
|
|
from app.core.config import Settings # noqa: E402 - optional rasterio gate precedes app imports
|
|
from app.models import Dataset # noqa: E402 - optional rasterio gate precedes app imports
|
|
from app.schemas.flood_hazard import FloodHazardSelectionRequest # noqa: E402 - optional rasterio gate precedes app imports
|
|
from app.services.flood_hazard_acquisition_service import ( # noqa: E402 - optional rasterio gate precedes app imports
|
|
FloodHazardAcquisitionService,
|
|
)
|
|
from app.services.flood_hazard_analysis_service import ( # noqa: E402 - optional rasterio gate precedes app imports
|
|
FloodHazardAnalysisService,
|
|
)
|
|
|
|
|
|
TO_4326 = Transformer.from_crs("EPSG:31370", "EPSG:4326", always_xy=True)
|
|
PRODUCT_KEY = "fluviaal_current_t100"
|
|
|
|
|
|
class FakeSession:
|
|
def __init__(self, objects):
|
|
self.objects = objects
|
|
|
|
def get(self, model, item_id):
|
|
return self.objects.get((model, item_id))
|
|
|
|
|
|
def _write_raster(path: Path, *, resolution: float, depth: float) -> None:
|
|
values = np.full((4, 4), depth, dtype="float32")
|
|
with rasterio.open(
|
|
path,
|
|
"w",
|
|
driver="GTiff",
|
|
width=4,
|
|
height=4,
|
|
count=1,
|
|
dtype="float32",
|
|
crs="EPSG:31370",
|
|
transform=from_origin(200_000, 210_000, resolution, resolution),
|
|
nodata=-9999.0,
|
|
) as output:
|
|
output.write(values, 1)
|
|
|
|
|
|
def _dataset(project_id, dataset_id, path: Path) -> Dataset:
|
|
return Dataset(
|
|
id=dataset_id,
|
|
project_id=project_id,
|
|
name="vmm-flood.tif",
|
|
dataset_type="raster",
|
|
source="vmm",
|
|
source_name=FloodHazardAcquisitionService.PROVIDER,
|
|
status="ready",
|
|
storage_path=str(path),
|
|
source_metadata={"product_key": PRODUCT_KEY, "normalized_value_unit": "m"},
|
|
)
|
|
|
|
|
|
def _bbox_for(min_x: float, min_y: float, max_x: float, max_y: float) -> dict:
|
|
left, bottom = TO_4326.transform(min_x, min_y)
|
|
right, top = TO_4326.transform(max_x, max_y)
|
|
return {"min_x": left, "min_y": bottom, "max_x": right, "max_y": top, "crs": "EPSG:4326"}
|
|
|
|
|
|
def _analyze(tmp_path: Path, bbox: dict, *, resolution: float = 100.0) -> dict:
|
|
project_id = uuid4()
|
|
dataset_id = uuid4()
|
|
path = tmp_path / "flood.tif"
|
|
_write_raster(path, resolution=resolution, depth=2.0)
|
|
dataset = _dataset(project_id, dataset_id, path)
|
|
db = FakeSession({(Dataset, dataset_id): dataset})
|
|
|
|
return FloodHazardAnalysisService.analyze(
|
|
db,
|
|
project_id,
|
|
dataset_id,
|
|
FloodHazardSelectionRequest(bbox=bbox),
|
|
settings=Settings(_env_file=None),
|
|
)
|
|
|
|
|
|
def test_a_selection_smaller_than_one_cell_reports_the_cell_it_touches(tmp_path: Path) -> None:
|
|
# A 40 x 30 m rectangle wholly inside one 100 m cell: no cell centre falls
|
|
# inside it, so the centre rule alone would report an empty selection.
|
|
result = _analyze(tmp_path, _bbox_for(200_010, 209_960, 200_050, 209_990))
|
|
|
|
assert result["inundated_cell_count"] == 1
|
|
assert result["inundated_fraction"] == pytest.approx(1.0)
|
|
assert "kleiner dan één rastercel" in result["coverage_warning"]
|
|
|
|
|
|
def test_a_normal_selection_is_unaffected(tmp_path: Path) -> None:
|
|
result = _analyze(tmp_path, _bbox_for(200_000, 209_700, 200_300, 210_000))
|
|
|
|
assert result["inundated_cell_count"] >= 9
|
|
assert result["coverage_warning"] is None
|
|
|
|
|
|
def test_the_reported_area_matches_the_cells_that_were_analysed(tmp_path: Path) -> None:
|
|
result = _analyze(tmp_path, _bbox_for(200_010, 209_960, 200_050, 209_990))
|
|
metrics = {item["metric_key"]: item["metric_value"] for item in result["summary"]["metrics"]}
|
|
|
|
# One 100 x 100 m cell, not the 0.12 ha that was drawn.
|
|
assert metrics["modelled_inundated_area_ha"] == pytest.approx(1.0)
|
|
assert metrics["selection_area_ha"] == pytest.approx(1.0)
|