Audit Belgian corpus splits and record CUDA evidence
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s

This commit is contained in:
Jens
2026-07-26 22:29:02 +02:00
parent 68d3fa34e3
commit 7a83df1e7f
5 changed files with 116 additions and 12 deletions
@@ -15,6 +15,13 @@ assert SPEC and SPEC.loader
module = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(module)
ASSEMBLER_SPEC = importlib.util.spec_from_file_location(
"assemble_building_corpus", ROOT / "scripts" / "assemble_belgium_building_corpus.py"
)
assert ASSEMBLER_SPEC and ASSEMBLER_SPEC.loader
assembler = importlib.util.module_from_spec(ASSEMBLER_SPEC)
ASSEMBLER_SPEC.loader.exec_module(assembler)
def test_normalizer_retains_native_identity_and_records_rejections(tmp_path: Path) -> None:
raster_path = tmp_path / "image.tif"
@@ -69,3 +76,15 @@ def test_normalizer_retains_native_identity_and_records_rejections(tmp_path: Pat
"excluded_canopy": 1,
}
assert audit["temporal_mismatch_days"] == 31
def test_spatial_leakage_audit_fails_cross_split_neighbors() -> None:
samples = [
{"sample_slug": "train-a", "split": "train", "bbox_epsg4326": [4.0, 50.0, 4.01, 50.01]},
{"sample_slug": "val-a", "split": "val", "bbox_epsg4326": [4.005, 50.005, 4.02, 50.02]},
{"sample_slug": "test-far", "split": "test", "bbox_epsg4326": [5.0, 51.0, 5.01, 51.01]},
]
audit = assembler.audit_spatial_leakage(samples)
assert audit["status"] == "failed"
assert audit["findings"][0]["left"] == "train-a"
assert audit["findings"][0]["right"] == "val-a"
+31
View File
@@ -11467,3 +11467,34 @@ Next gate:
- Verification: frontend build and 78 focused contracts passed. The full
backend run reported 1124 passed plus five unrelated local Windows/WSL
`bash` path failures.
## 2026-07-26 - Belgian building corpus candidate and CUDA matrix
- Added provider-aware canonical label normalization for GRB, PICC and UrbIS.
Every source feature retains its native ID/class and receives an explicit
accept/reject reason covering geometry repair, duplicates, semantic
exclusions and resolvable pixel size.
- Added an immutable corpus assembler that resolves only persisted governed
Dataset IDs, validates regional provider pairing, copies checksum-bound
artifacts and refuses a non-empty output directory.
- Frozen experimental corpus `building-be-v1-candidate-20260726` contains 19
geographically separated AOIs spanning all three land regions and explicit
train, validation, calibration, test and background-test roles. Manifest
SHA-256 is `0450ce782c35c5955e519fae489ffdbef6075d871adb0d196cc6a244571788fa`.
- The training export contains 36 tiles and 7,219 tile-level labels; its
automated label-size, variance and split audit passed. A 12-tile regional
pilot contact sheet was rendered for human review.
- CUDA training on the RTX 4080 SUPER completed a generic YOLOv8s candidate
(`mAP50=0.0883`, `mAP50-95=0.0248`) and an incumbent fine-tune
(`mAP50=0.187`, `mAP50-95=0.0617`) on the held-out regional validation
samples. Both are immutable candidates and neither is promoted.
- A 60-epoch incumbent fine-tune completed with artifact SHA-256
`594f9fef356940e7f7839da36561387a9a2f436c1e498e5733f99e1170c22fa6`.
On the unopened regional test AOIs (Leuven, Mons and Brussels rail) it
achieved precision `0.315`, recall `0.229`, mAP50 `0.120` and mAP50-95
`0.0340`. The incumbent scored `0.227`, `0.233`, `0.0930` and `0.0262`
respectively. The challenger improves precision/AP but slightly lowers
recall and remains far below a credible national acceptance floor.
- The scores prove that the current small candidate corpus is insufficient for
a national production claim. Human review, broader negative coverage,
leakage audit and independent calibration/test evaluation remain blocking
gates; the active production asset was left unchanged.
+9 -9
View File
@@ -121,21 +121,21 @@ manual URL editing and without crossing provider coverage zones.
### WP2 — Reference-label normalization
- [ ] normalize GRB, PICC and UrbIS building semantics to canonical `building`;
- [ ] retain original feature IDs and source classes;
- [ ] quantify invalid geometry repairs, duplicates and temporal mismatches;
- [ ] define exclusion rules for ruins, canopies, underground structures and
- [x] normalize GRB, PICC and UrbIS building semantics to canonical `building`;
- [x] retain original feature IDs and source classes;
- [x] quantify invalid geometry repairs, duplicates and temporal mismatches;
- [x] define exclusion rules for ruins, canopies, underground structures and
objects below the resolvable pixel size;
- [ ] generate overlay/contact-sheet review packs.
- [x] generate overlay/contact-sheet review packs.
Exit: every accepted/rejected label has a machine-readable reason and provenance.
### WP3 — Frozen Belgium building corpus
- [ ] select and persist the geographic strata and split assignments;
- [ ] acquire imagery and references through governed services;
- [ ] export tiles at candidate resolutions with deterministic negative sampling;
- [ ] run leakage, checksum, class, label-size and visual-variance audits;
- [x] select and persist the initial geographic strata and split assignments;
- [x] acquire the candidate imagery and references through governed services;
- [x] export tiles at candidate resolutions with deterministic negative sampling;
- [x] run checksum, class, label-size and visual-variance audits;
- [ ] complete representative human label review;
- [ ] freeze dataset version `building-be-v1` and make it immutable.
+10
View File
@@ -940,3 +940,13 @@ This file now starts with the current implementation status. Older preparation/b
- [ ] Resolve governed North Sea bathymetry access without bypassing TLS.
- [x] Enforce the active building-only model classes, CUDA readiness and persisted Mol/Kempen Area scope.
- [ ] Add national/regional holdout evidence before expanding model classes or validated Areas.
## Belgian PyTorch training programme (2026-07-26)
- [x] Add governed SPW and UrbIS orthophoto acquisition.
- [x] Normalize GRB/PICC/UrbIS building labels with decision provenance.
- [x] Assemble and checksum an initial 19-AOI Belgian candidate corpus.
- [x] Run generic and incumbent-based CUDA candidate training without promotion.
- [ ] Complete representative human label/contact-sheet review.
- [ ] Expand each region/context/split until the national minimum-composition gate passes.
- [ ] Run explicit spatial leakage and independent calibration/test evaluation.
- [ ] Promote only if every regional and pure-background gate passes.
+47 -3
View File
@@ -7,14 +7,25 @@ import argparse
import hashlib
import json
import shutil
import sys
from pathlib import Path
from typing import Any
from uuid import UUID
from app.db.session import SessionLocal
from app.models import Dataset
from pyproj import Transformer
from shapely.geometry import box
from shapely.ops import transform as shapely_transform
from normalize_belgium_building_labels import normalize
REPO_ROOT = Path(__file__).resolve().parents[1]
APP_ROOT = REPO_ROOT if (REPO_ROOT / "app").is_dir() else REPO_ROOT / "backend"
for import_root in (Path(__file__).resolve().parent, APP_ROOT):
if str(import_root) not in sys.path:
sys.path.insert(0, str(import_root))
from app.db.session import SessionLocal # noqa: E402
from app.models import Dataset # noqa: E402
from normalize_belgium_building_labels import normalize # noqa: E402
REGION_SOURCES = {
"flanders": ("digitaal_vlaanderen_orthophoto", "grb"),
@@ -58,6 +69,33 @@ def _validate_pair(sample: dict[str, Any], raster: Dataset, reference: Dataset)
return region, expected_reference
def audit_spatial_leakage(samples: list[dict[str, Any]], buffer_m: float = 64.0) -> dict[str, Any]:
transformer = Transformer.from_crs("EPSG:4326", "EPSG:31370", always_xy=True)
findings: list[dict[str, Any]] = []
metric_boxes: list[tuple[dict[str, Any], Any]] = []
for sample in samples:
bounds = sample.get("bbox_epsg4326")
if not isinstance(bounds, list) or len(bounds) != 4:
raise SystemExit(f"Missing governed bbox for leakage audit: {sample['sample_slug']}")
metric_boxes.append((sample, shapely_transform(transformer.transform, box(*map(float, bounds)))))
for index, (left, left_geometry) in enumerate(metric_boxes):
for right, right_geometry in metric_boxes[index + 1 :]:
if left["split"] == right["split"]:
continue
distance_m = left_geometry.distance(right_geometry)
if distance_m < buffer_m:
findings.append(
{
"left": left["sample_slug"],
"left_split": left["split"],
"right": right["sample_slug"],
"right_split": right["split"],
"distance_m": distance_m,
}
)
return {"status": "ok" if not findings else "failed", "buffer_m": buffer_m, "findings": findings}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--spec", type=Path, required=True)
@@ -137,6 +175,12 @@ def main() -> int:
}
manifest_path = output_dir / "operator_samples_manifest.json"
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
leakage_audit = audit_spatial_leakage(manifest_samples)
(output_dir / "spatial-leakage-audit.json").write_text(
json.dumps(leakage_audit, ensure_ascii=False, indent=2), encoding="utf-8"
)
if leakage_audit["status"] != "ok":
raise SystemExit("Spatial split leakage audit failed")
freeze = {
"dataset_version": args.version,
"manifest_sha256": sha256(manifest_path),