feat: complete Wallonia land cover and terrain sources
This commit is contained in:
@@ -16,7 +16,10 @@ from app.core.config import Settings
|
||||
from app.db.session import get_db
|
||||
from app.main import app
|
||||
from app.models import Dataset, Job, Project
|
||||
from app.schemas.thematic_raster import ThematicRasterAcquireRequest, ThematicRasterSelectionRequest
|
||||
from app.schemas.thematic_raster import (
|
||||
ThematicRasterAcquireRequest,
|
||||
ThematicRasterSelectionRequest,
|
||||
)
|
||||
from app.schemas.temporal import TemporalComparisonRequest
|
||||
from app.services.dataset_service import DatasetService
|
||||
from app.services.temporal_analysis_service import TemporalAnalysisService
|
||||
@@ -24,8 +27,12 @@ from app.services.walous_land_cover_service import WalousLandCoverService
|
||||
|
||||
|
||||
def load_provisioner():
|
||||
path = Path(__file__).resolve().parents[2] / "scripts" / "provision_walous_sources.py"
|
||||
spec = importlib.util.spec_from_file_location("walous_source_provisioner_test", path)
|
||||
path = (
|
||||
Path(__file__).resolve().parents[2] / "scripts" / "provision_walous_sources.py"
|
||||
)
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"walous_source_provisioner_test", path
|
||||
)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
@@ -54,7 +61,14 @@ class FakeSession:
|
||||
return self.project
|
||||
if model is Dataset and self.dataset is not None and row_id == self.dataset.id:
|
||||
return self.dataset
|
||||
match = next((item for item in self.added if isinstance(item, model) and item.id == row_id), None)
|
||||
match = next(
|
||||
(
|
||||
item
|
||||
for item in self.added
|
||||
if isinstance(item, model) and item.id == row_id
|
||||
),
|
||||
None,
|
||||
)
|
||||
if match is not None:
|
||||
return match
|
||||
return None
|
||||
@@ -80,12 +94,13 @@ def make_source(
|
||||
*,
|
||||
dtype: str = "uint8",
|
||||
nodata: int = 255,
|
||||
class_codes: list[int] | None = None,
|
||||
) -> tuple[list[float], np.ndarray]:
|
||||
to_3812 = Transformer.from_crs("EPSG:4326", "EPSG:3812", always_xy=True)
|
||||
to_4326 = Transformer.from_crs("EPSG:3812", "EPSG:4326", always_xy=True)
|
||||
x, y = to_3812.transform(4.85, 50.45)
|
||||
transform = from_origin(x, y + 100, 1, 1)
|
||||
class_codes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 80, 90]
|
||||
class_codes = class_codes or [1, 2, 3, 4, 5, 6, 7, 8, 9, 80, 90]
|
||||
values = np.empty((100, len(class_codes) * 20), dtype=dtype)
|
||||
for index, class_code in enumerate(class_codes):
|
||||
values[:, index * 20 : (index + 1) * 20] = class_code
|
||||
@@ -118,20 +133,40 @@ def settings(source_dir: Path) -> Settings:
|
||||
|
||||
|
||||
def test_walous_registry_reports_real_provisioning_state(tmp_path: Path) -> None:
|
||||
before = {item["key"]: item for item in WalousLandCoverService.list_products(settings=settings(tmp_path))}
|
||||
before = {
|
||||
item["key"]: item
|
||||
for item in WalousLandCoverService.list_products(settings=settings(tmp_path))
|
||||
}
|
||||
assert before["walous_land_cover_2023"]["status"] == "source_not_provisioned"
|
||||
make_source(tmp_path / "walous_land_cover_2023_3812.tif")
|
||||
after = {item["key"]: item for item in WalousLandCoverService.list_products(settings=settings(tmp_path))}
|
||||
after = {
|
||||
item["key"]: item
|
||||
for item in WalousLandCoverService.list_products(settings=settings(tmp_path))
|
||||
}
|
||||
assert after["walous_land_cover_2023"]["configured"] is True
|
||||
assert after["walous_land_cover_2023"]["source_crs"] == "EPSG:3812"
|
||||
assert after["walous_land_cover_2023"]["native_resolution_m"] == 1.0
|
||||
assert after["walous_land_cover_2023"]["analysis_resolution_m"] == 10.0
|
||||
assert after["walous_land_cover_2023"]["coverage_zones"] == ["wallonia"]
|
||||
assert after["walous_land_cover_2023"]["included_source_values"] == [1, 2, 3, 4, 5, 6, 7, 8, 9, 80, 90]
|
||||
assert after["walous_land_cover_2023"]["included_source_values"] == [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
80,
|
||||
90,
|
||||
]
|
||||
assert after["walous_land_cover_2023"]["source_value_unit"] == "walous_class_code"
|
||||
|
||||
|
||||
def test_walous_provisioner_accepts_official_non_contiguous_class_codes(tmp_path: Path) -> None:
|
||||
def test_walous_provisioner_accepts_official_non_contiguous_class_codes(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
source_path = tmp_path / "walous_land_cover_2023_3812.tif"
|
||||
make_source(source_path)
|
||||
|
||||
@@ -140,7 +175,86 @@ def test_walous_provisioner_accepts_official_non_contiguous_class_codes(tmp_path
|
||||
assert validation["sample_classes"] == [1, 2, 3, 4, 5, 6, 7, 8, 9, 80, 90]
|
||||
|
||||
|
||||
def test_walous_acquisition_reads_real_classes_and_persists_provenance(tmp_path: Path, monkeypatch) -> None:
|
||||
def test_walous_2018_registry_and_provisioner_accept_official_stacked_classes(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
source_codes = sorted(WalousLandCoverService.WALOUS_2018_CLASS_CROSSWALK)
|
||||
source_path = tmp_path / "walous_land_cover_2018_3812.tif"
|
||||
make_source(source_path, class_codes=source_codes)
|
||||
|
||||
validation = load_provisioner().validate_raster(source_path)
|
||||
registry = {
|
||||
item["key"]: item
|
||||
for item in WalousLandCoverService.list_products(settings=settings(tmp_path))
|
||||
}
|
||||
|
||||
assert validation["sample_classes"] == source_codes
|
||||
assert validation["implicit_source_nodata_values"] == [0]
|
||||
assert registry["walous_land_cover_2018"]["configured"] is True
|
||||
assert registry["walous_land_cover_2018"]["observation_year"] == 2018
|
||||
assert "crosswalk" in registry["walous_land_cover_2018"]["limitation_message"]
|
||||
|
||||
|
||||
def test_walous_2018_acquisition_normalizes_stacked_classes_with_explicit_provenance(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
source_codes = sorted(WalousLandCoverService.WALOUS_2018_CLASS_CROSSWALK)
|
||||
bbox, _values = make_source(
|
||||
tmp_path / "walous_land_cover_2018_3812.tif",
|
||||
class_codes=source_codes,
|
||||
)
|
||||
project = Project(id=uuid4(), name="Belgium")
|
||||
captured = {}
|
||||
|
||||
def persist(_db, **kwargs):
|
||||
captured.update(kwargs)
|
||||
return SimpleNamespace(id=uuid4())
|
||||
|
||||
monkeypatch.setattr(DatasetService, "import_raster_bytes", persist)
|
||||
result = WalousLandCoverService.acquire(
|
||||
FakeSession(project),
|
||||
project.id,
|
||||
ThematicRasterAcquireRequest(
|
||||
bbox={
|
||||
"min_x": bbox[0],
|
||||
"min_y": bbox[1],
|
||||
"max_x": bbox[2],
|
||||
"max_y": bbox[3],
|
||||
"crs": "EPSG:4326",
|
||||
},
|
||||
product_key="walous_land_cover_2018",
|
||||
force_refresh=True,
|
||||
),
|
||||
settings=settings(tmp_path),
|
||||
)
|
||||
|
||||
assert result["observation_year"] == 2018
|
||||
assert captured["source_metadata"]["classes_present"] == [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
80,
|
||||
90,
|
||||
]
|
||||
assert captured["source_metadata"]["source_classes_present"] == source_codes
|
||||
assert captured["source_metadata"]["class_crosswalk"][62] == 2
|
||||
assert captured["source_metadata"]["class_crosswalk"][0] == 255
|
||||
assert (
|
||||
captured["source_metadata"]["attribution"]
|
||||
== "Service public de Wallonie (SPW), UCLouvain, ULB, ISSeP"
|
||||
)
|
||||
assert captured["observed_at"].date().isoformat() == "2018-12-31"
|
||||
|
||||
|
||||
def test_walous_acquisition_reads_real_classes_and_persists_provenance(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
bbox, _values = make_source(tmp_path / "walous_land_cover_2023_3812.tif")
|
||||
project = Project(id=uuid4(), name="Belgium")
|
||||
db = FakeSession(project)
|
||||
@@ -156,7 +270,13 @@ def test_walous_acquisition_reads_real_classes_and_persists_provenance(tmp_path:
|
||||
db,
|
||||
project.id,
|
||||
ThematicRasterAcquireRequest(
|
||||
bbox={"min_x": bbox[0], "min_y": bbox[1], "max_x": bbox[2], "max_y": bbox[3], "crs": "EPSG:4326"},
|
||||
bbox={
|
||||
"min_x": bbox[0],
|
||||
"min_y": bbox[1],
|
||||
"max_x": bbox[2],
|
||||
"max_y": bbox[3],
|
||||
"crs": "EPSG:4326",
|
||||
},
|
||||
product_key="walous_land_cover_2023",
|
||||
force_refresh=True,
|
||||
),
|
||||
@@ -166,7 +286,19 @@ def test_walous_acquisition_reads_real_classes_and_persists_provenance(tmp_path:
|
||||
assert result["output_dataset_id"] == str(output_id)
|
||||
assert result["resolution_m"] == 10
|
||||
assert captured["source_name"] == "spw_walous_land_cover"
|
||||
assert captured["source_metadata"]["classes_present"] == [1, 2, 3, 4, 5, 6, 7, 8, 9, 80, 90]
|
||||
assert captured["source_metadata"]["classes_present"] == [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
80,
|
||||
90,
|
||||
]
|
||||
assert captured["provenance_metadata"]["resampling"] == "nearest"
|
||||
assert captured["temporal_series_key"].startswith("spw:walous:land-cover:")
|
||||
assert captured["observed_at"].date().isoformat() == "2023-06-25"
|
||||
@@ -174,7 +306,9 @@ def test_walous_acquisition_reads_real_classes_and_persists_provenance(tmp_path:
|
||||
assert captured["valid_to"] == captured["observed_at"]
|
||||
|
||||
|
||||
def test_walous_acquisition_accepts_official_signed_int8_nodata(tmp_path: Path, monkeypatch) -> None:
|
||||
def test_walous_acquisition_accepts_official_signed_int8_nodata(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
bbox, _values = make_source(
|
||||
tmp_path / "walous_land_cover_2023_3812.tif",
|
||||
dtype="int8",
|
||||
@@ -193,7 +327,13 @@ def test_walous_acquisition_accepts_official_signed_int8_nodata(tmp_path: Path,
|
||||
FakeSession(project),
|
||||
project.id,
|
||||
ThematicRasterAcquireRequest(
|
||||
bbox={"min_x": bbox[0], "min_y": bbox[1], "max_x": bbox[2], "max_y": bbox[3], "crs": "EPSG:4326"},
|
||||
bbox={
|
||||
"min_x": bbox[0],
|
||||
"min_y": bbox[1],
|
||||
"max_x": bbox[2],
|
||||
"max_y": bbox[3],
|
||||
"crs": "EPSG:4326",
|
||||
},
|
||||
product_key="walous_land_cover_2023",
|
||||
force_refresh=True,
|
||||
),
|
||||
@@ -201,14 +341,28 @@ def test_walous_acquisition_accepts_official_signed_int8_nodata(tmp_path: Path,
|
||||
)
|
||||
|
||||
assert result["output_dataset_id"] == str(output_id)
|
||||
assert captured["source_metadata"]["classes_present"] == [1, 2, 3, 4, 5, 6, 7, 8, 9, 80, 90]
|
||||
assert captured["source_metadata"]["classes_present"] == [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
80,
|
||||
90,
|
||||
]
|
||||
with rasterio.MemoryFile(captured["content"]) as memory:
|
||||
with memory.open() as derived:
|
||||
assert derived.dtypes == ("uint8",)
|
||||
assert derived.nodata == 255
|
||||
|
||||
|
||||
def test_walous_analysis_returns_semantic_area_metrics(tmp_path: Path, monkeypatch) -> None:
|
||||
def test_walous_analysis_returns_semantic_area_metrics(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
bbox, _values = make_source(tmp_path / "walous_land_cover_2023_3812.tif")
|
||||
project = Project(id=uuid4(), name="Belgium")
|
||||
output_id = uuid4()
|
||||
@@ -221,7 +375,13 @@ def test_walous_analysis_returns_semantic_area_metrics(tmp_path: Path, monkeypat
|
||||
monkeypatch.setattr(DatasetService, "import_raster_bytes", persist)
|
||||
db = FakeSession(project)
|
||||
payload = ThematicRasterAcquireRequest(
|
||||
bbox={"min_x": bbox[0], "min_y": bbox[1], "max_x": bbox[2], "max_y": bbox[3], "crs": "EPSG:4326"},
|
||||
bbox={
|
||||
"min_x": bbox[0],
|
||||
"min_y": bbox[1],
|
||||
"max_x": bbox[2],
|
||||
"max_y": bbox[3],
|
||||
"crs": "EPSG:4326",
|
||||
},
|
||||
product_key="walous_land_cover_2023",
|
||||
force_refresh=True,
|
||||
)
|
||||
@@ -247,7 +407,10 @@ def test_walous_analysis_returns_semantic_area_metrics(tmp_path: Path, monkeypat
|
||||
output_id,
|
||||
ThematicRasterSelectionRequest(bbox=payload.bbox),
|
||||
)
|
||||
metrics = {item["metric_key"]: item["metric_value"] for item in result["summary"]["metrics"]}
|
||||
metrics = {
|
||||
item["metric_key"]: item["metric_value"]
|
||||
for item in result["summary"]["metrics"]
|
||||
}
|
||||
|
||||
assert result["metric_kind"] == "categorical_area"
|
||||
assert metrics["land_cover_observed_area_ha"] > 0
|
||||
@@ -270,7 +433,10 @@ def test_walous_render_png_uses_governed_class_colours(tmp_path: Path) -> None:
|
||||
dataset_type="raster",
|
||||
source="SPW WALOUS",
|
||||
source_name="spw_walous_land_cover",
|
||||
source_metadata={"product_key": "walous_land_cover_2023", "bbox_epsg4326": bbox},
|
||||
source_metadata={
|
||||
"product_key": "walous_land_cover_2023",
|
||||
"bbox_epsg4326": bbox,
|
||||
},
|
||||
storage_path=str(tmp_path / "walous_land_cover_2023_3812.tif"),
|
||||
status="ready",
|
||||
)
|
||||
@@ -281,7 +447,9 @@ def test_walous_render_png_uses_governed_class_colours(tmp_path: Path) -> None:
|
||||
assert rendered.startswith(b"\x89PNG\r\n\x1a\n")
|
||||
|
||||
|
||||
def test_walous_temporal_comparison_reuses_persisted_raster_metrics(monkeypatch) -> None:
|
||||
def test_walous_temporal_comparison_reuses_persisted_raster_metrics(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
project_id = uuid4()
|
||||
earlier = Dataset(
|
||||
id=uuid4(),
|
||||
@@ -320,14 +488,16 @@ def test_walous_temporal_comparison_reuses_persisted_raster_metrics(monkeypatch)
|
||||
"metric_unit": "ha",
|
||||
"aggregation_method": "nearest_resampled_cells_times_cell_area",
|
||||
"primary_metric_key": "land_cover_observed_area_ha",
|
||||
"metrics": [{
|
||||
"metric_key": "land_cover_observed_area_ha",
|
||||
"metric_label": "Gekarteerde landbedekking",
|
||||
"metric_value": value,
|
||||
"metric_unit": "ha",
|
||||
"aggregation_method": "nearest_resampled_cells_times_cell_area",
|
||||
"is_estimate": True,
|
||||
}],
|
||||
"metrics": [
|
||||
{
|
||||
"metric_key": "land_cover_observed_area_ha",
|
||||
"metric_label": "Gekarteerde landbedekking",
|
||||
"metric_value": value,
|
||||
"metric_unit": "ha",
|
||||
"aggregation_method": "nearest_resampled_cells_times_cell_area",
|
||||
"is_estimate": True,
|
||||
}
|
||||
],
|
||||
},
|
||||
"limitation_message": "Cell-based estimate.",
|
||||
}
|
||||
@@ -336,10 +506,18 @@ def test_walous_temporal_comparison_reuses_persisted_raster_metrics(monkeypatch)
|
||||
payload = TemporalComparisonRequest(
|
||||
earlier_dataset_id=earlier.id,
|
||||
later_dataset_id=later.id,
|
||||
bbox={"min_x": 4.8, "min_y": 50.4, "max_x": 4.9, "max_y": 50.5, "crs": "EPSG:4326"},
|
||||
bbox={
|
||||
"min_x": 4.8,
|
||||
"min_y": 50.4,
|
||||
"max_x": 4.9,
|
||||
"max_y": 50.5,
|
||||
"crs": "EPSG:4326",
|
||||
},
|
||||
)
|
||||
|
||||
result = TemporalAnalysisService.compare(TemporalSession(), project_id=project_id, payload=payload)
|
||||
result = TemporalAnalysisService.compare(
|
||||
TemporalSession(), project_id=project_id, payload=payload
|
||||
)
|
||||
|
||||
assert result.metric.earlier_value == 4.0
|
||||
assert result.metric.later_value == 5.5
|
||||
@@ -354,7 +532,10 @@ def test_walous_api_routes_use_canonical_envelopes(monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
WalousLandCoverService,
|
||||
"acquire",
|
||||
lambda *_args, **_kwargs: {"output_dataset_id": str(dataset_id), "provider": WalousLandCoverService.PROVIDER},
|
||||
lambda *_args, **_kwargs: {
|
||||
"output_dataset_id": str(dataset_id),
|
||||
"provider": WalousLandCoverService.PROVIDER,
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
WalousLandCoverService,
|
||||
@@ -364,7 +545,13 @@ def test_walous_api_routes_use_canonical_envelopes(monkeypatch) -> None:
|
||||
"product_key": "walous_land_cover_2023",
|
||||
"theme": "land_cover_use",
|
||||
"metric_kind": "categorical_area",
|
||||
"selection_bbox": {"min_x": 4.8, "min_y": 50.4, "max_x": 4.9, "max_y": 50.5, "crs": "EPSG:4326"},
|
||||
"selection_bbox": {
|
||||
"min_x": 4.8,
|
||||
"min_y": 50.4,
|
||||
"max_x": 4.9,
|
||||
"max_y": 50.5,
|
||||
"crs": "EPSG:4326",
|
||||
},
|
||||
"selected_cell_count": 100,
|
||||
"valid_cell_count": 100,
|
||||
"coverage_ratio": 1.0,
|
||||
@@ -390,20 +577,37 @@ def test_walous_api_routes_use_canonical_envelopes(monkeypatch) -> None:
|
||||
acquisition = client.post(
|
||||
f"/api/v1/projects/{project.id}/datasets/walous/acquire",
|
||||
json={
|
||||
"bbox": {"min_x": 4.8, "min_y": 50.4, "max_x": 4.9, "max_y": 50.5, "crs": "EPSG:4326"},
|
||||
"bbox": {
|
||||
"min_x": 4.8,
|
||||
"min_y": 50.4,
|
||||
"max_x": 4.9,
|
||||
"max_y": 50.5,
|
||||
"crs": "EPSG:4326",
|
||||
},
|
||||
"product_key": "walous_land_cover_2023",
|
||||
},
|
||||
)
|
||||
selection = client.post(
|
||||
f"/api/v1/projects/{project.id}/datasets/{dataset_id}/raster/walous/select",
|
||||
json={"bbox": {"min_x": 4.8, "min_y": 50.4, "max_x": 4.9, "max_y": 50.5, "crs": "EPSG:4326"}},
|
||||
json={
|
||||
"bbox": {
|
||||
"min_x": 4.8,
|
||||
"min_y": 50.4,
|
||||
"max_x": 4.9,
|
||||
"max_y": 50.5,
|
||||
"crs": "EPSG:4326",
|
||||
}
|
||||
},
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert products.status_code == 200 and set(products.json()) == {"data"}
|
||||
assert products.json()["data"]["total"] == 2
|
||||
assert products.json()["data"]["total"] == 3
|
||||
assert acquisition.status_code == 200 and set(acquisition.json()) == {"data"}
|
||||
assert acquisition.json()["data"]["job_type"] == "raster.walous.acquire"
|
||||
assert selection.status_code == 200 and selection.json()["data"]["theme"] == "land_cover_use"
|
||||
assert (
|
||||
selection.status_code == 200
|
||||
and selection.json()["data"]["theme"] == "land_cover_use"
|
||||
)
|
||||
assert any(isinstance(item, Job) for item in db.added)
|
||||
|
||||
Reference in New Issue
Block a user