Files
geointel/backend/tests/test_sprint131_operator_sample_expansion.py
T
Codex a63d4eaadc
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled
Page GRB references for operator samples
2026-07-09 15:02:12 +02:00

228 lines
7.7 KiB
Python

from __future__ import annotations
import importlib.util
from pathlib import Path
import sys
import pytest
ROOT = Path(__file__).resolve().parents[2]
def load_sample_preparer():
script_path = ROOT / "scripts" / "prepare_operator_real_data_samples.py"
spec = importlib.util.spec_from_file_location("operator_sample_preparer", script_path)
assert spec is not None
assert spec.loader is not None
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def test_operator_sample_registry_includes_kempen_reference_and_background_candidates() -> None:
module = load_sample_preparer()
expected_reference_slugs = {"geel", "mol", "turnhout", "herentals", "balen", "retie", "westerlo"}
expected_background_slugs = {
"postel_bos",
"lommel_heide",
"kasterlee_bos",
"dessel_heide",
"ravels_bos",
"meerhout_bos",
"geel_bel",
"arendonk_heide",
}
assert expected_reference_slugs.issubset(module.SAMPLES)
assert expected_background_slugs.issubset(module.SAMPLES)
assert all(not module.SAMPLES[slug].allow_empty_reference for slug in expected_reference_slugs)
assert all(module.SAMPLES[slug].allow_empty_reference for slug in expected_background_slugs)
assert all(module.SAMPLES[slug].sample_role == "background_candidate" for slug in expected_background_slugs)
def test_operator_background_candidates_are_unique_enough_for_hard_negative_training() -> None:
module = load_sample_preparer()
background_samples = [
sample
for sample in module.SAMPLES.values()
if sample.sample_role == "background_candidate"
]
centers = {(round(sample.center_lon, 4), round(sample.center_lat, 4)) for sample in background_samples}
half_sizes = {sample.half_size_m for sample in background_samples}
assert len(background_samples) >= 8
assert len(centers) == len(background_samples)
assert min(sample.center_lon for sample in background_samples) < 4.85
assert max(sample.center_lon for sample in background_samples) > 5.25
assert min(sample.center_lat for sample in background_samples) < 51.18
assert max(sample.center_lat for sample in background_samples) > 51.33
assert half_sizes == {260.0}
def test_operator_sample_can_be_scaled_for_larger_training_aoi(tmp_path: Path) -> None:
module = load_sample_preparer()
sample = module.OperatorSample(
slug="geel",
display_name="Geel",
center_lon=5.0,
center_lat=51.0,
half_size_m=250.0,
)
configured = module.apply_sample_overrides(sample, width=1024, height=1024, half_size_scale=2.0)
ortho_path, reference_path = module.sample_artifact_paths(configured, tmp_path)
assert configured.width == 1024
assert configured.height == 1024
assert configured.half_size_m == 500.0
assert ortho_path.name == "geel_orthophoto_wms_1024.tif"
assert reference_path.name == "geel_grb_gbg_buildings.geojson"
def test_background_candidate_can_write_empty_reference_geojson(tmp_path: Path, monkeypatch) -> None:
module = load_sample_preparer()
class EmptyFeatureResponse:
headers = {"content-type": "application/geo+json"}
def raise_for_status(self) -> None:
return None
def json(self) -> dict:
return {"type": "FeatureCollection", "features": []}
class FakeRequests:
@staticmethod
def get(*args, **kwargs):
return EmptyFeatureResponse()
monkeypatch.setattr(module, "requests", FakeRequests)
monkeypatch.setattr(module, "prepared_url", lambda url, params: f"{url}?prepared=true")
sample = module.OperatorSample(
slug="background",
display_name="Background",
center_lon=5.0,
center_lat=51.0,
allow_empty_reference=True,
sample_role="background_candidate",
)
reference_path = tmp_path / "background.geojson"
source_url, feature_count = module.fetch_reference(sample, reference_path, [4.9, 50.9, 5.1, 51.1])
assert source_url.endswith("?prepared=true")
assert feature_count == 0
payload = reference_path.read_text(encoding="utf-8")
assert '"features": []' in payload
assert '"sample_role": "background_candidate"' in payload
def test_fetch_reference_follows_grb_next_links_until_complete(tmp_path: Path, monkeypatch) -> None:
module = load_sample_preparer()
requested: list[tuple[str, dict | None]] = []
def feature(feature_id: str) -> dict:
return {
"type": "Feature",
"id": feature_id,
"geometry": {"type": "Polygon", "coordinates": []},
"properties": {},
}
class FeatureResponse:
def __init__(self, payload: dict) -> None:
self.payload = payload
def raise_for_status(self) -> None:
return None
def json(self) -> dict:
return self.payload
class FakeRequests:
Request = module.requests.Request if module.requests else object
@staticmethod
def get(url, params=None, timeout=120):
requested.append((url, params))
if len(requested) == 1:
return FeatureResponse(
{
"type": "FeatureCollection",
"features": [feature("GBG.1")],
"numberReturned": 1,
"links": [
{
"rel": "next",
"type": "application/geo+json",
"href": "https://example.test/grb?page=2",
}
],
}
)
return FeatureResponse(
{
"type": "FeatureCollection",
"features": [feature("GBG.2")],
"numberReturned": 1,
"links": [],
}
)
monkeypatch.setattr(module, "requests", FakeRequests)
monkeypatch.setattr(module, "prepared_url", lambda url, params: f"{url}?prepared=true")
sample = module.OperatorSample(
slug="urban",
display_name="Urban",
center_lon=5.0,
center_lat=51.0,
)
source_url, feature_count = module.fetch_reference(sample, tmp_path / "urban.geojson", [4.9, 50.9, 5.1, 51.1])
payload = (tmp_path / "urban.geojson").read_text(encoding="utf-8")
assert source_url.endswith("?prepared=true")
assert feature_count == 2
assert requested == [
(module.GRB_GBG_URL, {"f": "application/geo+json", "limit": "1000", "bbox": "4.90000000,50.90000000,5.10000000,51.10000000"}),
("https://example.test/grb?page=2", None),
]
assert '"id": "GBG.1"' in payload
assert '"id": "GBG.2"' in payload
def test_reference_sample_still_rejects_empty_grb_response(tmp_path: Path, monkeypatch) -> None:
module = load_sample_preparer()
class EmptyFeatureResponse:
def raise_for_status(self) -> None:
return None
def json(self) -> dict:
return {"type": "FeatureCollection", "features": []}
class FakeRequests:
@staticmethod
def get(*args, **kwargs):
return EmptyFeatureResponse()
monkeypatch.setattr(module, "requests", FakeRequests)
monkeypatch.setattr(module, "prepared_url", lambda url, params: f"{url}?prepared=true")
sample = module.OperatorSample(
slug="urban",
display_name="Urban",
center_lon=5.0,
center_lat=51.0,
)
with pytest.raises(SystemExit, match="returned no building features"):
module.fetch_reference(sample, tmp_path / "urban.geojson", [4.9, 50.9, 5.1, 51.1])