"""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 from rasterio.transform import from_origin from app.core.config import Settings from app.models import Dataset from app.schemas.flood_hazard import FloodHazardSelectionRequest from app.services.flood_hazard_acquisition_service import FloodHazardAcquisitionService from app.services.flood_hazard_analysis_service import 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)