Add Mol multi-zone operational validation
This commit is contained in:
@@ -460,6 +460,13 @@ manifests generated for AI handoff include source CRS metadata so pixel-space
|
||||
model outputs can be transformed to WGS84 GeoJSON coordinates. Current V1 upload
|
||||
support is limited to GeoTIFF-style rasters and GeoJSON/JSON reference vectors.
|
||||
|
||||
When `REAL_AREA_BBOX=minx,miny,maxx,maxy` is supplied, the same workflow also
|
||||
persists an EPSG:4326 project Area before uploading data. `REAL_AREA_NAME` and
|
||||
`REAL_PROJECT_REGION` retain operator context. The multi-sample runner fills
|
||||
these values from manifest `wgs84_bbox` and municipality metadata, so generated
|
||||
projects are immediately usable in the map without an alternate persistence
|
||||
path or API contract.
|
||||
|
||||
To prepare the documented operator sample corpus inside the all-in-one runtime
|
||||
container, run:
|
||||
|
||||
@@ -477,6 +484,12 @@ empty GRB FeatureCollections for negative-tile training; normal reference AOIs
|
||||
still fail when GRB returns no buildings. These are runtime artifacts only and
|
||||
are not committed to Git.
|
||||
|
||||
Mol additionally has operational holdouts for Achterbos, Gompel, Donk and
|
||||
Postel, with Mol center as the historical baseline and Postel-bos as a separate
|
||||
background control. Prepare and execute that pack with the documented
|
||||
`prepare_operator_real_data_samples.py` and
|
||||
`run_mol_operational_validation.sh` commands in `scripts/README.md`.
|
||||
|
||||
The current recommended local building model is
|
||||
`geointel-building-yolov8s-smallbld-minpx3-img640-ft30-pt` with tile size
|
||||
`512`, overlap `64` and confidence threshold `0.15`. Its SHA256 is
|
||||
|
||||
@@ -75,6 +75,7 @@ def test_all_in_one_dockerfile_copies_operator_scripts_for_runtime_use() -> None
|
||||
"verify_real_data_detection_qa_workflow.sh",
|
||||
"run_detection_quality_matrix.sh",
|
||||
"run_multi_sample_detection_quality_matrix.sh",
|
||||
"run_mol_operational_validation.sh",
|
||||
"export_detection_calibration_evidence.sh",
|
||||
"assemble_detection_calibration_evidence_portfolio.sh",
|
||||
"build_fixed_threshold_evidence_portfolio_inputs.py",
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import math
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
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("mol_operational_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 distance_m(left, right) -> float:
|
||||
radius_m = 6_371_008.8
|
||||
left_lat = math.radians(left.center_lat)
|
||||
right_lat = math.radians(right.center_lat)
|
||||
delta_lat = right_lat - left_lat
|
||||
delta_lon = math.radians(right.center_lon - left.center_lon)
|
||||
haversine = (
|
||||
math.sin(delta_lat / 2) ** 2
|
||||
+ math.cos(left_lat) * math.cos(right_lat) * math.sin(delta_lon / 2) ** 2
|
||||
)
|
||||
return 2 * radius_m * math.asin(math.sqrt(haversine))
|
||||
|
||||
|
||||
def test_mol_operational_registry_has_distinct_real_world_zones_and_holdouts() -> None:
|
||||
module = load_sample_preparer()
|
||||
|
||||
expected = {
|
||||
"mol": "center",
|
||||
"mol_achterbos": "residential",
|
||||
"mol_gompel": "mixed_settlement",
|
||||
"mol_donk": "canal_industrial",
|
||||
"mol_postel": "rural_village",
|
||||
}
|
||||
assert tuple(expected) == module.MOL_OPERATIONAL_SAMPLE_SLUGS
|
||||
assert module.MOL_BACKGROUND_CONTROL_SAMPLE_SLUGS == ("postel_bos",)
|
||||
assert module.MOL_OPERATIONAL_VALIDATION_SAMPLE_SLUGS == frozenset(expected) - {"mol"}
|
||||
|
||||
samples = [module.SAMPLES[slug] for slug in expected]
|
||||
assert all(sample.municipality == "Mol" for sample in samples)
|
||||
assert {sample.operational_zone for sample in samples} == set(expected.values())
|
||||
assert all(5.09 < sample.center_lon < 5.20 for sample in samples)
|
||||
assert all(51.18 < sample.center_lat < 51.30 for sample in samples)
|
||||
assert all(
|
||||
module.recommended_split_for_sample(module.SAMPLES[slug]) == "val"
|
||||
for slug in module.MOL_OPERATIONAL_VALIDATION_SAMPLE_SLUGS
|
||||
)
|
||||
|
||||
new_holdouts = [module.SAMPLES[slug] for slug in module.MOL_OPERATIONAL_VALIDATION_SAMPLE_SLUGS]
|
||||
assert min(
|
||||
distance_m(left, right)
|
||||
for index, left in enumerate(new_holdouts)
|
||||
for right in new_holdouts[index + 1 :]
|
||||
) >= 1_500
|
||||
|
||||
|
||||
def test_mol_sample_metadata_is_persisted_in_operator_manifest_records(tmp_path: Path, monkeypatch) -> None:
|
||||
module = load_sample_preparer()
|
||||
sample = module.SAMPLES["mol_achterbos"]
|
||||
monkeypatch.setattr(module, "sample_bounds", lambda _sample: ((1.0, 2.0, 3.0, 4.0), [5.0, 51.0, 5.1, 51.1]))
|
||||
monkeypatch.setattr(module, "sample_artifact_paths", lambda _sample, _output: (tmp_path / "ortho.tif", tmp_path / "reference.geojson"))
|
||||
monkeypatch.setattr(module, "fetch_orthophoto", lambda *_args: "https://example.test/ortho")
|
||||
monkeypatch.setattr(module, "fetch_reference", lambda *_args, **_kwargs: ("https://example.test/grb", 42))
|
||||
monkeypatch.setattr(module, "raster_summary", lambda _path: {"crs": "EPSG:31370"})
|
||||
|
||||
prepared = module.prepare_sample(sample, tmp_path, force=True)
|
||||
|
||||
assert prepared["municipality"] == "Mol"
|
||||
assert prepared["operational_zone"] == "residential"
|
||||
assert prepared["recommended_split"] == "val"
|
||||
assert prepared["wgs84_bbox"] == [5.0, 51.0, 5.1, 51.1]
|
||||
|
||||
|
||||
def test_real_data_matrix_propagates_project_region_and_persisted_area() -> None:
|
||||
workflow = (ROOT / "scripts" / "verify_real_data_detection_qa_workflow.sh").read_text(encoding="utf-8")
|
||||
matrix = (ROOT / "scripts" / "run_detection_quality_matrix.sh").read_text(encoding="utf-8")
|
||||
multi = (ROOT / "scripts" / "run_multi_sample_detection_quality_matrix.sh").read_text(encoding="utf-8")
|
||||
|
||||
assert 'REAL_PROJECT_REGION="${REAL_PROJECT_REGION:-Kempen}"' in workflow
|
||||
assert 'REAL_AREA_BBOX="${REAL_AREA_BBOX:-}"' in workflow
|
||||
assert '/api/v1/projects/${project_id}/areas' in workflow
|
||||
assert 'echo "Area: ${area_id}"' in workflow
|
||||
assert 'REAL_PROJECT_REGION="${REAL_PROJECT_REGION}"' in matrix
|
||||
assert 'REAL_AREA_BBOX="${REAL_AREA_BBOX}"' in matrix
|
||||
assert 'REAL_AREA_BBOX="${wgs84_bbox}"' in multi
|
||||
assert 'REAL_AREA_NAME="${sample_slug} AOI"' in multi
|
||||
assert 'project_region="Mol, Kempen"' in multi
|
||||
assert 'REAL_PROJECT_REGION="${project_region}"' in multi
|
||||
|
||||
|
||||
def test_mol_operational_runner_uses_real_positive_qa_and_background_paths() -> None:
|
||||
runner = (ROOT / "scripts" / "run_mol_operational_validation.sh").read_text(encoding="utf-8")
|
||||
readiness = (ROOT / "scripts" / "run_readiness_check.sh").read_text(encoding="utf-8")
|
||||
dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(encoding="utf-8")
|
||||
|
||||
assert "mol_achterbos mol_gompel mol_donk mol_postel" in runner
|
||||
assert "run_multi_sample_detection_quality_matrix.sh" in runner
|
||||
assert "run_operator_hard_negative_detection_matrix.sh" in runner
|
||||
assert "mol_operational_validation_summary.json" in runner
|
||||
assert "fixture_mode" not in runner
|
||||
assert "manual-fixture-detector" not in runner
|
||||
assert "bash -n scripts/run_mol_operational_validation.sh" in readiness
|
||||
assert "COPY scripts/run_mol_operational_validation.sh" in dockerfile
|
||||
Reference in New Issue
Block a user