Initial public release
GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s
GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s
This commit is contained in:
Vendored
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
# Test Fixtures
|
||||
|
||||
Fixtures should be tiny, deterministic and safe to commit.
|
||||
|
||||
Planned fixtures:
|
||||
- geospatial/project_area.geojson
|
||||
- vectors/reference_buildings.geojson
|
||||
- vectors/predicted_buildings.geojson
|
||||
- rasters/tiny_raster.tif or generated raster fixture in tests
|
||||
|
||||
See `docs/FIXTURE_STRATEGY.md`.
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"class_id": 2,
|
||||
"x_center": 0.95,
|
||||
"y_center": 0.5,
|
||||
"width": 0.2,
|
||||
"height": -0.1
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"class_id": 0,
|
||||
"x_center": 0.5,
|
||||
"y_center": 0.5,
|
||||
"width": 0.2,
|
||||
"height": 0.3
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"native_id": "GRB-GBG-001",
|
||||
"feature_status": "active"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[4.8470, 51.1550],
|
||||
[4.8480, 51.1550],
|
||||
[4.8480, 51.1560],
|
||||
[4.8470, 51.1560],
|
||||
[4.8470, 51.1550]
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"native_id": "GRB-GBG-002",
|
||||
"feature_status": "active"
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[193277.5, 205708.3],
|
||||
[193377.5, 205708.3],
|
||||
[193377.5, 205808.3],
|
||||
[193277.5, 205808.3],
|
||||
[193277.5, 205708.3]
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
# tests/fixtures/geojson
|
||||
|
||||
Reserved for GeoIntel development assets. Do not commit large real datasets unless explicitly approved.
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {"name": "Demo AOI"},
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [[[5.0,51.0],[5.01,51.0],[5.01,51.01],[5.0,51.01],[5.0,51.0]]]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{"type":"Feature","properties":{"id":"det_1","class":"building","confidence":0.92},"geometry":{"type":"Polygon","coordinates":[[[5.0011,51.0011],[5.0031,51.0011],[5.0031,51.0031],[5.0011,51.0031],[5.0011,51.0011]]]}},
|
||||
{"type":"Feature","properties":{"id":"det_fp","class":"building","confidence":0.61},"geometry":{"type":"Polygon","coordinates":[[[5.004,51.004],[5.005,51.004],[5.005,51.005],[5.004,51.005],[5.004,51.004]]]}}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{"type":"Feature","properties":{"id":"ref_1","class":"building"},"geometry":{"type":"Polygon","coordinates":[[[5.001,51.001],[5.003,51.001],[5.003,51.003],[5.001,51.003],[5.001,51.001]]]}},
|
||||
{"type":"Feature","properties":{"id":"ref_2","class":"building"},"geometry":{"type":"Polygon","coordinates":[[[5.006,51.006],[5.008,51.006],[5.008,51.008],[5.006,51.008],[5.006,51.006]]]}}
|
||||
]
|
||||
}
|
||||
Vendored
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
# tests/fixtures/rasters
|
||||
|
||||
Reserved for GeoIntel development assets. Do not commit large real datasets unless explicitly approved.
|
||||
Vendored
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
# tests/fixtures/vectors
|
||||
|
||||
Reserved for GeoIntel development assets. Do not commit large real datasets unless explicitly approved.
|
||||
@@ -0,0 +1,72 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = ROOT / "scripts" / "run_accuracy_phase1_baseline.py"
|
||||
SPEC = importlib.util.spec_from_file_location("accuracy_phase1_baseline", SCRIPT)
|
||||
assert SPEC and SPEC.loader
|
||||
MODULE = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(MODULE)
|
||||
|
||||
INFERENCE_SCRIPT = ROOT / "scripts" / "collect_accuracy_phase1_inference_smoke.py"
|
||||
INFERENCE_SPEC = importlib.util.spec_from_file_location("accuracy_phase1_inference", INFERENCE_SCRIPT)
|
||||
assert INFERENCE_SPEC and INFERENCE_SPEC.loader
|
||||
INFERENCE_MODULE = importlib.util.module_from_spec(INFERENCE_SPEC)
|
||||
INFERENCE_SPEC.loader.exec_module(INFERENCE_MODULE)
|
||||
|
||||
|
||||
def test_sha256_file_is_stable(tmp_path: Path) -> None:
|
||||
artifact = tmp_path / "manifest.json"
|
||||
artifact.write_bytes(b'{"version":1}\n')
|
||||
assert MODULE.sha256_file(artifact) == "50208d78350a7a160dec59a82df1499b6ca7da33e54c5eb11c97e342118e68bb"
|
||||
|
||||
|
||||
def test_mirror_inventory_distinguishes_identical_and_drifted_files(tmp_path: Path) -> None:
|
||||
(tmp_path / "geointel").mkdir()
|
||||
(tmp_path / "same.txt").write_text("same", encoding="utf-8")
|
||||
(tmp_path / "geointel" / "same.txt").write_text("same", encoding="utf-8")
|
||||
(tmp_path / "drift.txt").write_text("root", encoding="utf-8")
|
||||
(tmp_path / "geointel" / "drift.txt").write_text("mirror", encoding="utf-8")
|
||||
tracked = {"same.txt", "drift.txt", "geointel/same.txt", "geointel/drift.txt"}
|
||||
|
||||
report = MODULE.mirror_inventory(tmp_path, tracked)
|
||||
|
||||
assert report["tracked_mirror_file_count"] == 2
|
||||
assert report["paired_identical_file_count"] == 1
|
||||
assert report["paired_different_file_count"] == 1
|
||||
assert report["different_files"][0]["path"] == "drift.txt"
|
||||
|
||||
|
||||
def test_artifact_roles_do_not_claim_images_are_models() -> None:
|
||||
assert MODULE.artifact_role(Path("candidate.pt")) == "model_checkpoint"
|
||||
assert MODULE.artifact_role(Path("split-manifest.json")) == "manifest"
|
||||
assert MODULE.artifact_role(Path("contact_sheet_001.png")) == "visual_review"
|
||||
assert MODULE.artifact_role(Path("orthophoto.tif")) == "raster"
|
||||
|
||||
|
||||
def test_inference_summary_is_explicit_for_empty_and_non_empty_outputs() -> None:
|
||||
assert INFERENCE_MODULE._summarize_detections([]) == {
|
||||
"count": 0,
|
||||
"class_counts": {},
|
||||
"confidence": {"minimum": None, "maximum": None, "mean": None},
|
||||
"sample": [],
|
||||
}
|
||||
|
||||
detections = [
|
||||
{"class_name": "building", "confidence": 0.8, "bbox": [1, 2, 3, 4]},
|
||||
{"class_name": "building", "confidence": 0.4, "bbox": [5, 6, 7, 8]},
|
||||
{"class_name": "shed", "confidence": 0.6, "bbox": [9, 10, 11, 12]},
|
||||
]
|
||||
summary = INFERENCE_MODULE._summarize_detections(detections)
|
||||
|
||||
assert summary["count"] == 3
|
||||
assert summary["class_counts"] == {"building": 2, "shed": 1}
|
||||
assert summary["confidence"] == {
|
||||
"minimum": 0.4,
|
||||
"maximum": 0.8,
|
||||
"mean": 0.6,
|
||||
}
|
||||
assert summary["sample"] == detections
|
||||
@@ -0,0 +1,26 @@
|
||||
import pytest
|
||||
|
||||
from scripts.assess_belgium_building_training_iteration import (
|
||||
INFERENCE_CONFIG_FIELDS,
|
||||
assert_same_inference_config,
|
||||
)
|
||||
|
||||
|
||||
def test_regional_model_routes_are_frozen_in_inference_config() -> None:
|
||||
assert "regional_models" in INFERENCE_CONFIG_FIELDS
|
||||
calibration = {field: None for field in INFERENCE_CONFIG_FIELDS}
|
||||
calibration["regional_models"] = {"flanders": "/models/flanders.pt"}
|
||||
test = dict(calibration)
|
||||
test["regional_models"] = {"flanders": "/models/other.pt"}
|
||||
|
||||
with pytest.raises(ValueError, match="regional_models"):
|
||||
assert_same_inference_config(calibration, test, "test")
|
||||
|
||||
|
||||
def test_regional_tile_counts_may_differ_between_splits() -> None:
|
||||
calibration = {field: None for field in INFERENCE_CONFIG_FIELDS}
|
||||
test = dict(calibration)
|
||||
calibration["regional_model_tile_counts"] = {"flanders": 48}
|
||||
test["regional_model_tile_counts"] = {"flanders": 64}
|
||||
|
||||
assert_same_inference_config(calibration, test, "test")
|
||||
@@ -0,0 +1,50 @@
|
||||
import pytest
|
||||
|
||||
from scripts.audit_yolo_cross_tile_repetition import (
|
||||
interior_global_key,
|
||||
tile_offsets,
|
||||
)
|
||||
from scripts.audit_yolo_label_relationships import Box
|
||||
|
||||
|
||||
def test_tile_offsets_parse_exporter_filename() -> None:
|
||||
assert tile_offsets("/data/geel_0005_r256_c512.png") == (256, 512)
|
||||
with pytest.raises(ValueError, match="no row/column offsets"):
|
||||
tile_offsets("/data/not-a-tile.png")
|
||||
|
||||
|
||||
def test_interior_global_key_reconstructs_same_object_across_tiles() -> None:
|
||||
first = Box(0, 0.75, 0.5, 0.1, 0.1)
|
||||
second = Box(0, 0.25, 0.5, 0.1, 0.1)
|
||||
|
||||
first_key = interior_global_key(
|
||||
first,
|
||||
row_offset=0,
|
||||
column_offset=0,
|
||||
tile_size=512,
|
||||
edge_tolerance_pixels=0.5,
|
||||
)
|
||||
second_key = interior_global_key(
|
||||
second,
|
||||
row_offset=0,
|
||||
column_offset=256,
|
||||
tile_size=512,
|
||||
edge_tolerance_pixels=0.5,
|
||||
)
|
||||
|
||||
assert first_key == second_key == (358.4, 230.4, 409.6, 281.6)
|
||||
|
||||
|
||||
def test_interior_global_key_rejects_edge_clipped_box() -> None:
|
||||
edge_box = Box(0, 0.05, 0.5, 0.1, 0.1)
|
||||
|
||||
assert (
|
||||
interior_global_key(
|
||||
edge_box,
|
||||
row_offset=0,
|
||||
column_offset=256,
|
||||
tile_size=512,
|
||||
edge_tolerance_pixels=0.5,
|
||||
)
|
||||
is None
|
||||
)
|
||||
@@ -0,0 +1,45 @@
|
||||
from scripts.audit_yolo_label_outliers import classify_box
|
||||
from scripts.audit_yolo_label_relationships import Box
|
||||
|
||||
|
||||
DEFAULTS = {
|
||||
"tile_size": 640,
|
||||
"min_dimension_pixels": 4.0,
|
||||
"extreme_aspect_ratio": 8.0,
|
||||
"edge_tolerance_pixels": 0.5,
|
||||
}
|
||||
|
||||
|
||||
def test_classify_box_reports_independent_geometric_risks() -> None:
|
||||
box = Box(0, 0.5, 0.003, 0.2, 0.006)
|
||||
|
||||
assert classify_box(box, **DEFAULTS) == [
|
||||
{
|
||||
"category": "small_dimension",
|
||||
"width_px": 128.0,
|
||||
"height_px": 3.84,
|
||||
"area_px2": 491.52,
|
||||
"aspect_ratio": 33.333333,
|
||||
},
|
||||
{
|
||||
"category": "extreme_aspect_ratio",
|
||||
"width_px": 128.0,
|
||||
"height_px": 3.84,
|
||||
"area_px2": 491.52,
|
||||
"aspect_ratio": 33.333333,
|
||||
},
|
||||
{
|
||||
"category": "tile_edge",
|
||||
"edge_sides": ["top"],
|
||||
"width_px": 128.0,
|
||||
"height_px": 3.84,
|
||||
"area_px2": 491.52,
|
||||
"aspect_ratio": 33.333333,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_classify_box_ignores_ordinary_interior_box() -> None:
|
||||
box = Box(0, 0.5, 0.5, 0.1, 0.08)
|
||||
|
||||
assert classify_box(box, **DEFAULTS) == []
|
||||
@@ -0,0 +1,45 @@
|
||||
from scripts.audit_yolo_label_relationships import Box, audit_boxes, classify_pair
|
||||
|
||||
|
||||
DEFAULTS = {
|
||||
"near_duplicate_iou": 0.9,
|
||||
"containment_threshold": 0.98,
|
||||
"max_nested_area_ratio": 4.0,
|
||||
}
|
||||
|
||||
|
||||
def test_classifies_exact_near_and_possible_nested_relationships() -> None:
|
||||
exact = Box(0, 0.5, 0.5, 0.2, 0.2)
|
||||
near = Box(0, 0.501, 0.5, 0.2, 0.2)
|
||||
outer = Box(0, 0.5, 0.5, 0.18, 0.18)
|
||||
inner = Box(0, 0.5, 0.5, 0.1, 0.1)
|
||||
|
||||
assert classify_pair(exact, exact, **DEFAULTS) == ("exact_duplicate", 1.0)
|
||||
assert classify_pair(exact, near, **DEFAULTS)[0] == "near_duplicate"
|
||||
assert classify_pair(outer, inner, **DEFAULTS) == ("possible_nested", 1.0)
|
||||
|
||||
|
||||
def test_ignores_other_classes_and_non_overlapping_boxes() -> None:
|
||||
first = Box(0, 0.2, 0.2, 0.1, 0.1)
|
||||
other_class = Box(1, 0.2, 0.2, 0.1, 0.1)
|
||||
distant = Box(0, 0.8, 0.8, 0.1, 0.1)
|
||||
|
||||
assert classify_pair(first, other_class, **DEFAULTS) is None
|
||||
assert classify_pair(first, distant, **DEFAULTS) is None
|
||||
|
||||
|
||||
def test_audit_boxes_returns_pair_indices_and_coordinates() -> None:
|
||||
first = Box(0, 0.5, 0.5, 0.2, 0.2)
|
||||
second = Box(0, 0.5, 0.5, 0.2, 0.2)
|
||||
relationships = audit_boxes([first, second], **DEFAULTS)
|
||||
|
||||
assert relationships == [
|
||||
{
|
||||
"relationship": "exact_duplicate",
|
||||
"score": 1.0,
|
||||
"first_index": 0,
|
||||
"second_index": 1,
|
||||
"first_box": [0, 0.5, 0.5, 0.2, 0.2],
|
||||
"second_box": [0, 0.5, 0.5, 0.2, 0.2],
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,60 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "build_regional_yolo_dataset.py"
|
||||
SPEC = importlib.util.spec_from_file_location("build_regional_yolo_dataset", SCRIPT)
|
||||
assert SPEC and SPEC.loader
|
||||
module = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(module)
|
||||
|
||||
|
||||
def test_regional_dataset_balances_contexts_and_keeps_validation_closed() -> None:
|
||||
manifest = {"samples": [
|
||||
{"sample_slug": "f-train", "region": "flanders", "split": "train", "context": "ribbon"},
|
||||
{"sample_slug": "f-val", "region": "flanders", "split": "val", "context": "mixed"},
|
||||
{"sample_slug": "w-train", "region": "wallonia", "split": "train", "context": "ribbon"},
|
||||
]}
|
||||
summary = {"tiles": [
|
||||
{"sample_slug": "f-train", "split": "train", "image_path": "/f-pos.png", "kept": True},
|
||||
{"sample_slug": "f-train", "split": "train", "image_path": "/f-neg.png", "is_negative": True},
|
||||
{"sample_slug": "f-val", "split": "val", "image_path": "/f-val.png"},
|
||||
{"sample_slug": "w-train", "split": "train", "image_path": "/w.png"},
|
||||
]}
|
||||
train, val, evidence = module.build(
|
||||
summary=summary, manifest=manifest, region="flanders",
|
||||
priority_contexts={"ribbon"}, priority_repeat=3, negative_repeat=2,
|
||||
)
|
||||
assert train == ["/f-pos.png"] * 3 + ["/f-neg.png"] * 2
|
||||
assert val == ["/f-val.png"]
|
||||
assert evidence["negative_train_entry_count"] == 2
|
||||
assert evidence["protected_samples_in_training"] == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("protected_split", ["calibration", "test", "background-test", "challenge"])
|
||||
def test_regional_dataset_rejects_protected_tiles(protected_split: str) -> None:
|
||||
manifest = {"samples": [
|
||||
{"sample_slug": "f-cal", "region": "flanders", "split": protected_split, "context": "ribbon"},
|
||||
]}
|
||||
summary = {"tiles": [{"sample_slug": "f-cal", "split": protected_split, "image_path": "/cal.png"}]}
|
||||
with pytest.raises(ValueError, match="protected"):
|
||||
module.build(
|
||||
summary=summary, manifest=manifest, region="flanders",
|
||||
priority_contexts=set(), priority_repeat=1, negative_repeat=1,
|
||||
)
|
||||
|
||||
|
||||
def test_regional_dataset_rejects_manifest_protection_hidden_by_tile_split() -> None:
|
||||
manifest = {"samples": [
|
||||
{"sample_slug": "f-cal", "region": "flanders", "split": "calibration", "context": "ribbon"},
|
||||
]}
|
||||
summary = {"tiles": [{"sample_slug": "f-cal", "split": "train", "image_path": "/cal.png"}]}
|
||||
with pytest.raises(ValueError, match="protected"):
|
||||
module.build(
|
||||
summary=summary, manifest=manifest, region="flanders",
|
||||
priority_contexts=set(), priority_repeat=1, negative_repeat=1,
|
||||
)
|
||||
@@ -0,0 +1,290 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def load(name: str):
|
||||
path = ROOT / "scripts" / f"{name}.py"
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
miner = load("build_building_proposal_classifier_dataset")
|
||||
trainer = load("train_building_proposal_classifier")
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def _write_json(path: Path, payload: dict) -> None:
|
||||
path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
|
||||
|
||||
|
||||
def _governed_proposal_crop_fixture(tmp_path: Path) -> tuple[Path, Path]:
|
||||
"""Create a complete tiny crop release without importing Torch or YOLO."""
|
||||
|
||||
corpus_manifest = tmp_path / "corpus-manifest.json"
|
||||
_write_json(corpus_manifest, {"samples": []})
|
||||
corpus_sha256 = _sha256(corpus_manifest)
|
||||
corpus_freeze = tmp_path / "corpus-freeze.json"
|
||||
_write_json(corpus_freeze, {"immutable": True})
|
||||
summary = tmp_path / "source-summary.json"
|
||||
_write_json(summary, {"source_manifest_sha256": corpus_sha256, "tiles": []})
|
||||
|
||||
dataset_dir = tmp_path / "proposal-crops"
|
||||
entries: list[dict] = []
|
||||
for split in ("train", "val"):
|
||||
for label, colour in (("negative", (0, 0, 0)), ("positive", (255, 255, 255))):
|
||||
crop_path = dataset_dir / split / label / f"{split}-{label}.jpg"
|
||||
crop_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
Image.new("RGB", (2, 2), colour).save(crop_path)
|
||||
relative_path = crop_path.relative_to(dataset_dir).as_posix()
|
||||
entries.append(
|
||||
{
|
||||
"relative_path": relative_path,
|
||||
"sha256": _sha256(crop_path),
|
||||
"size_bytes": crop_path.stat().st_size,
|
||||
"split": split,
|
||||
"label": label,
|
||||
"sample_slug": f"{split}-{label}",
|
||||
"proposal_index": 0,
|
||||
"proposal_score": 0.8,
|
||||
"source_box_xyxy": [0.0, 0.0, 1.0, 1.0],
|
||||
"source_image_path": str(tmp_path / "source-image.tif"),
|
||||
"source_image_sha256": "a" * 64,
|
||||
"source_label_path": str(tmp_path / "source-label.txt"),
|
||||
"source_label_sha256": "b" * 64,
|
||||
}
|
||||
)
|
||||
entries.sort(key=lambda item: item["relative_path"])
|
||||
counts = {f"{split}/{label}": 1 for split in ("train", "val") for label in ("negative", "positive")}
|
||||
payload: dict = {
|
||||
"schema_version": 1,
|
||||
"status": "ok",
|
||||
"immutable": True,
|
||||
"dataset_kind": "building_proposal_classifier_crops",
|
||||
"fixture_mode": False,
|
||||
"governed_corpus_live_recheck": True,
|
||||
"source": {
|
||||
"corpus_manifest": {"path": str(corpus_manifest), "sha256": corpus_sha256},
|
||||
"corpus_freeze": {"path": str(corpus_freeze), "sha256": _sha256(corpus_freeze)},
|
||||
"summary": {
|
||||
"path": str(summary),
|
||||
"sha256": _sha256(summary),
|
||||
"source_manifest_sha256": corpus_sha256,
|
||||
},
|
||||
"training_release": {
|
||||
"dataset_yaml_path": "fixture-dataset.yaml",
|
||||
"dataset_yaml_sha256": "d" * 64,
|
||||
"corpus_manifest_sha256": corpus_sha256,
|
||||
},
|
||||
"proposal_model": {"path": str(tmp_path / "proposal.pt"), "sha256": "c" * 64},
|
||||
},
|
||||
"parameters": {"crop_scale": 1.4},
|
||||
"counts": counts,
|
||||
"sample_counts": {item["sample_slug"]: 1 for item in entries},
|
||||
"tile_count": 2,
|
||||
"crop_count": len(entries),
|
||||
"crops_sha256": hashlib.sha256(trainer._canonical_json_bytes({"crops": entries})).hexdigest(),
|
||||
"crops": entries,
|
||||
}
|
||||
payload["manifest_sha256"] = trainer._payload_sha256(payload)
|
||||
_write_json(dataset_dir / trainer.PROPOSAL_DATASET_PROVENANCE_NAME, payload)
|
||||
return dataset_dir, corpus_manifest
|
||||
|
||||
|
||||
def test_classify_proposals_consumes_reference_once() -> None:
|
||||
reference = [(0.0, 0.0, 10.0, 10.0)]
|
||||
proposals = [((0.0, 0.0, 10.0, 10.0), 0.9), ((0.0, 0.0, 10.0, 10.0), 0.8)]
|
||||
assert [item[0] for item in miner.classify_proposals(proposals, reference, 0.25)] == ["positive", "negative"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("protected_split", ["calibration", "test", "background-test", "challenge"])
|
||||
def test_eligible_tiles_rejects_protected_manifest_split(protected_split: str) -> None:
|
||||
manifest = {"samples": [{"sample_slug": "x", "region": "flanders", "split": protected_split}]}
|
||||
summary = {"tiles": [{"sample_slug": "x", "split": "train", "image_path": "x.png"}]}
|
||||
with pytest.raises(ValueError, match="protected"):
|
||||
miner.eligible_tiles(summary, manifest, "flanders")
|
||||
|
||||
|
||||
def test_binary_metrics() -> None:
|
||||
result = trainer.binary_metrics([0.9, 0.8, 0.2, 0.1], [1, 0, 1, 0])
|
||||
assert result == {"tp": 1, "fp": 1, "fn": 1, "precision": 0.5, "recall": 0.5, "f1": 0.5}
|
||||
|
||||
|
||||
def test_builder_rechecks_frozen_corpus_against_live_governed_state(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
observed: dict[str, object] = {}
|
||||
|
||||
def fake_assert(path: Path, **kwargs: object) -> dict:
|
||||
observed["path"] = path
|
||||
observed.update(kwargs)
|
||||
return {"samples": []}
|
||||
|
||||
monkeypatch.setattr(miner, "assert_frozen_manifest_training_eligible", fake_assert)
|
||||
manifest_path = tmp_path / "corpus.json"
|
||||
manifest_path.write_text("{}", encoding="utf-8")
|
||||
assert miner.load_governed_corpus_manifest(manifest_path, fixture_mode=False) == {"samples": []}
|
||||
assert observed == {"path": manifest_path, "fixture_mode": False, "verify_live": True}
|
||||
|
||||
|
||||
def test_builder_rejects_summary_not_bound_to_exact_corpus_manifest(tmp_path: Path) -> None:
|
||||
manifest_path = tmp_path / "corpus.json"
|
||||
manifest_path.write_text('{"samples": []}', encoding="utf-8")
|
||||
with pytest.raises(ValueError, match="not bound"):
|
||||
miner.assert_summary_source_manifest_binding({"source_manifest_sha256": "0" * 64}, manifest_path)
|
||||
|
||||
|
||||
def test_builder_emits_immutable_checksum_bound_crop_provenance(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
class FakeTensor:
|
||||
def __init__(self, values: list[list[float]] | list[float]) -> None:
|
||||
self._values = values
|
||||
|
||||
def cpu(self) -> "FakeTensor":
|
||||
return self
|
||||
|
||||
def tolist(self) -> list[list[float]] | list[float]:
|
||||
return self._values
|
||||
|
||||
class FakeBoxes:
|
||||
xyxy = FakeTensor([[40.0, 40.0, 60.0, 60.0], [0.0, 0.0, 10.0, 10.0]])
|
||||
conf = FakeTensor([0.9, 0.8])
|
||||
|
||||
class FakeResult:
|
||||
boxes = FakeBoxes()
|
||||
|
||||
class FakeYOLO:
|
||||
def __init__(self, model: str) -> None:
|
||||
self.model = model
|
||||
|
||||
def predict(self, sources: list[str], **_kwargs: object) -> list[FakeResult]:
|
||||
return [FakeResult() for _source in sources]
|
||||
|
||||
corpus_manifest = tmp_path / "corpus-manifest.json"
|
||||
manifest = {
|
||||
"samples": [
|
||||
{"sample_slug": "train-a", "split": "train", "region": "flanders"},
|
||||
{"sample_slug": "val-b", "split": "val", "region": "flanders"},
|
||||
]
|
||||
}
|
||||
_write_json(corpus_manifest, manifest)
|
||||
_write_json(tmp_path / "corpus-freeze.json", {"immutable": True})
|
||||
tiles: list[dict[str, object]] = []
|
||||
for sample_slug, split in (("train-a", "train"), ("val-b", "val")):
|
||||
image_path = tmp_path / f"{sample_slug}.png"
|
||||
Image.new("RGB", (100, 100), (50, 100, 150)).save(image_path)
|
||||
label_path = tmp_path / f"{sample_slug}.txt"
|
||||
label_path.write_text("0 0.5 0.5 0.2 0.2\n", encoding="utf-8")
|
||||
tiles.append(
|
||||
{
|
||||
"sample_slug": sample_slug,
|
||||
"split": split,
|
||||
"kept": True,
|
||||
"image_path": str(image_path),
|
||||
"label_path": str(label_path),
|
||||
}
|
||||
)
|
||||
summary_path = tmp_path / "source-summary.json"
|
||||
_write_json(
|
||||
summary_path,
|
||||
{
|
||||
"source_manifest_sha256": _sha256(corpus_manifest),
|
||||
"training_release_manifest": "fixture-training-release.json",
|
||||
"training_release_manifest_sha256": "a" * 64,
|
||||
"training_asset_manifest": "fixture-training-assets.json",
|
||||
"tiles": tiles,
|
||||
},
|
||||
)
|
||||
model_path = tmp_path / "proposal-model.pt"
|
||||
model_path.write_bytes(b"proposal-model")
|
||||
output_dir = tmp_path / "proposal-crops"
|
||||
|
||||
monkeypatch.setattr(miner, "load_governed_corpus_manifest", lambda *_args, **_kwargs: manifest)
|
||||
monkeypatch.setattr(
|
||||
miner,
|
||||
"assert_yolo_summary_bound_to_embedded_training_release",
|
||||
lambda **_kwargs: {
|
||||
"dataset_yaml": {"path": "fixture-dataset.yaml", "sha256": "d" * 64},
|
||||
"corpus": {"manifest_sha256": _sha256(corpus_manifest)},
|
||||
},
|
||||
)
|
||||
monkeypatch.setitem(sys.modules, "ultralytics", types.SimpleNamespace(YOLO=FakeYOLO))
|
||||
monkeypatch.setattr(
|
||||
miner.sys,
|
||||
"argv",
|
||||
[
|
||||
"build_building_proposal_classifier_dataset.py",
|
||||
"--model",
|
||||
str(model_path),
|
||||
"--summary",
|
||||
str(summary_path),
|
||||
"--corpus-manifest",
|
||||
str(corpus_manifest),
|
||||
"--output-dir",
|
||||
str(output_dir),
|
||||
],
|
||||
)
|
||||
|
||||
assert miner.main() == 0
|
||||
provenance_path = output_dir / miner.PROPOSAL_DATASET_PROVENANCE_NAME
|
||||
provenance = json.loads(provenance_path.read_text(encoding="utf-8"))
|
||||
assert provenance["immutable"] is True
|
||||
assert provenance["crop_count"] == 4
|
||||
assert provenance["manifest_sha256"] == miner._immutable_payload_sha256(provenance)
|
||||
assert all(_sha256(output_dir / item["relative_path"]) == item["sha256"] for item in provenance["crops"])
|
||||
with pytest.raises(RuntimeError, match="immutable provenance"):
|
||||
miner._write_immutable_json(provenance_path, {"different": True})
|
||||
|
||||
|
||||
def test_trainer_validates_every_crop_and_source_binding_before_torch(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
dataset_dir, corpus_manifest = _governed_proposal_crop_fixture(tmp_path)
|
||||
monkeypatch.setattr(
|
||||
trainer,
|
||||
"assert_yolo_summary_bound_to_embedded_training_release",
|
||||
lambda **_kwargs: {
|
||||
"dataset_yaml": {"path": "fixture-dataset.yaml", "sha256": "d" * 64},
|
||||
"corpus": {"manifest_sha256": _sha256(corpus_manifest)},
|
||||
},
|
||||
)
|
||||
payload = trainer.validate_proposal_dataset_provenance(dataset_dir, corpus_manifest, fixture_mode=False)
|
||||
assert payload["crop_count"] == 4
|
||||
|
||||
crop = dataset_dir / "train" / "positive" / "train-positive.jpg"
|
||||
crop.write_bytes(b"tampered")
|
||||
with pytest.raises(trainer.ProposalDatasetProvenanceError, match="checksum"):
|
||||
trainer.validate_proposal_dataset_provenance(dataset_dir, corpus_manifest, fixture_mode=False)
|
||||
|
||||
|
||||
def test_trainer_rechecks_live_governed_corpus_before_pytorch(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||
observed: dict[str, object] = {}
|
||||
|
||||
def fake_assert(path: Path, **kwargs: object) -> dict:
|
||||
observed["path"] = path
|
||||
observed.update(kwargs)
|
||||
return {"samples": []}
|
||||
|
||||
monkeypatch.setattr(trainer, "assert_frozen_manifest_training_eligible", fake_assert)
|
||||
manifest_path = tmp_path / "corpus.json"
|
||||
manifest_path.write_text("{}", encoding="utf-8")
|
||||
assert trainer.load_governed_corpus_manifest(manifest_path, fixture_mode=False) == {"samples": []}
|
||||
assert observed == {"path": manifest_path, "fixture_mode": False, "verify_live": True}
|
||||
@@ -0,0 +1,53 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.combine_belgium_building_portfolio_specs import combine_specs
|
||||
|
||||
|
||||
def write_spec(path: Path, slug: str, *, resolution: float = 0.25, status: str = "complete") -> Path:
|
||||
path.write_text(json.dumps({
|
||||
"status": status,
|
||||
"side_m": 256.0,
|
||||
"resolution_m": resolution,
|
||||
"samples": [{"sample_slug": slug}],
|
||||
}), encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
def test_combine_specs_records_provenance(tmp_path: Path) -> None:
|
||||
payload = combine_specs([write_spec(tmp_path / "one.json", "one"), write_spec(tmp_path / "two.json", "two")])
|
||||
assert payload["sample_count"] == 2
|
||||
assert [sample["sample_slug"] for sample in payload["samples"]] == ["one", "two"]
|
||||
assert all(len(source["sha256"]) == 64 for source in payload["source_specs"])
|
||||
|
||||
|
||||
def test_combine_specs_rejects_duplicate_slugs(tmp_path: Path) -> None:
|
||||
with pytest.raises(ValueError, match="Duplicate"):
|
||||
combine_specs([write_spec(tmp_path / "one.json", "same"), write_spec(tmp_path / "two.json", "same")])
|
||||
|
||||
|
||||
def test_combine_specs_rejects_mismatched_resolution(tmp_path: Path) -> None:
|
||||
with pytest.raises(ValueError, match="resolution"):
|
||||
combine_specs([write_spec(tmp_path / "one.json", "one"), write_spec(tmp_path / "two.json", "two", resolution=0.5)])
|
||||
|
||||
|
||||
def test_combine_specs_rejects_incomplete_source(tmp_path: Path) -> None:
|
||||
with pytest.raises(ValueError, match="not complete"):
|
||||
combine_specs([write_spec(tmp_path / "one.json", "one", status="in_progress")])
|
||||
|
||||
|
||||
def test_combine_specs_accepts_populated_legacy_v1_when_dimensions_come_from_complete_spec(tmp_path: Path) -> None:
|
||||
legacy = tmp_path / "legacy.json"
|
||||
legacy.write_text(json.dumps({"schema_version": 1, "samples": [{"sample_slug": "legacy"}]}), encoding="utf-8")
|
||||
payload = combine_specs([legacy, write_spec(tmp_path / "current.json", "current")])
|
||||
assert payload["sample_count"] == 2
|
||||
assert payload["source_specs"][0]["source_status"] == "legacy_unstated"
|
||||
|
||||
|
||||
def test_combine_specs_rejects_empty_legacy_v1(tmp_path: Path) -> None:
|
||||
legacy = tmp_path / "legacy.json"
|
||||
legacy.write_text(json.dumps({"schema_version": 1, "samples": []}), encoding="utf-8")
|
||||
with pytest.raises(ValueError, match="not complete"):
|
||||
combine_specs([legacy, write_spec(tmp_path / "current.json", "current")])
|
||||
@@ -0,0 +1,21 @@
|
||||
import pytest
|
||||
|
||||
from scripts.derive_yolo_min_dimension_corpus import filter_label_lines
|
||||
|
||||
|
||||
def test_filter_label_lines_removes_only_rows_below_pixel_floor() -> None:
|
||||
lines = [
|
||||
"0 0.5 0.5 0.005 0.25",
|
||||
"0 0.5 0.5 0.0078125 0.25",
|
||||
"0 0.5 0.5 0.1 0.1",
|
||||
]
|
||||
|
||||
kept, removed = filter_label_lines(lines, tile_size=512, min_dimension_pixels=4)
|
||||
|
||||
assert kept == lines[1:]
|
||||
assert removed == [0]
|
||||
|
||||
|
||||
def test_filter_label_lines_rejects_invalid_source_rows() -> None:
|
||||
with pytest.raises(ValueError, match="invalid YOLO row"):
|
||||
filter_label_lines(["0 0.5 0.5"], tile_size=512, min_dimension_pixels=4)
|
||||
@@ -0,0 +1,49 @@
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from scripts.derive_yolo_nonoverlap_evaluation_view import (
|
||||
image_has_low_visual_variance,
|
||||
select_nonoverlap_tiles,
|
||||
)
|
||||
|
||||
|
||||
def tile(name: str, index: int, split: str = "val") -> dict[str, object]:
|
||||
return {
|
||||
"image_path": f"/data/sample_{index:04d}_{name}.png",
|
||||
"tile_index": index,
|
||||
"sample_slug": "sample",
|
||||
"split": split,
|
||||
"kept": True,
|
||||
}
|
||||
|
||||
|
||||
def test_select_nonoverlap_tiles_keeps_complete_512_grid() -> None:
|
||||
tiles = [
|
||||
tile("r0_c0", 0),
|
||||
tile("r0_c256", 1),
|
||||
tile("r0_c512", 2),
|
||||
tile("r256_c0", 3),
|
||||
tile("r256_c256", 4),
|
||||
tile("r256_c512", 5),
|
||||
tile("r512_c0", 6),
|
||||
tile("r512_c256", 7),
|
||||
tile("r512_c512", 8),
|
||||
tile("r0_c0", 9, split="train"),
|
||||
]
|
||||
|
||||
selected = select_nonoverlap_tiles(tiles, 512)
|
||||
|
||||
assert [item["tile_index"] for item in selected] == [0, 2, 6, 8]
|
||||
|
||||
|
||||
def test_image_has_low_visual_variance_rejects_blank_no_data(tmp_path: Path) -> None:
|
||||
blank = tmp_path / "blank.png"
|
||||
real = tmp_path / "real.png"
|
||||
Image.new("L", (8, 8), color=255).save(blank)
|
||||
image = Image.new("L", (8, 8), color=100)
|
||||
image.putpixel((0, 0), 120)
|
||||
image.save(real)
|
||||
|
||||
assert image_has_low_visual_variance(blank, 3) is True
|
||||
assert image_has_low_visual_variance(real, 3) is False
|
||||
@@ -0,0 +1,23 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.evaluate_belgium_building_candidate import parse_regional_models
|
||||
|
||||
|
||||
def test_parse_regional_models_accepts_explicit_unique_routes() -> None:
|
||||
assert parse_regional_models(["flanders=/models/flanders.pt", "wallonia=/models/wallonia.pt"]) == {
|
||||
"flanders": Path("/models/flanders.pt"),
|
||||
"wallonia": Path("/models/wallonia.pt"),
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["flanders", "=/model.pt", "flanders="])
|
||||
def test_parse_regional_models_rejects_incomplete_routes(value: str) -> None:
|
||||
with pytest.raises(ValueError, match="expected REGION"):
|
||||
parse_regional_models([value])
|
||||
|
||||
|
||||
def test_parse_regional_models_rejects_duplicate_routes() -> None:
|
||||
with pytest.raises(ValueError, match="Duplicate regional model"):
|
||||
parse_regional_models(["flanders=/first.pt", "FLANDERS=/second.pt"])
|
||||
@@ -0,0 +1,228 @@
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from scripts.evaluate_yolo_checkpoint_matrix import (
|
||||
dataset_overlap_evidence,
|
||||
main,
|
||||
model_lineage_independence_evidence,
|
||||
validate_pure_background_prefixes,
|
||||
write_blocked_manifest,
|
||||
)
|
||||
|
||||
|
||||
def test_dataset_overlap_evidence_marks_repeated_validation_rows(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
dataset_yaml = tmp_path / "dataset.yaml"
|
||||
dataset_yaml.write_text("path: .\nval: images/val\n", encoding="utf-8")
|
||||
(tmp_path / "yolo_tile_dataset_summary.json").write_text(
|
||||
json.dumps({"tile_size": 512, "stride": 256}), encoding="utf-8"
|
||||
)
|
||||
|
||||
evidence = dataset_overlap_evidence(dataset_yaml)
|
||||
|
||||
assert evidence["status"] == "overlapping"
|
||||
assert evidence["overlap_pixels"] == 256
|
||||
assert evidence["validation_tiles_non_overlapping"] is False
|
||||
assert evidence["statistical_independence_established"] is False
|
||||
|
||||
|
||||
def test_dataset_overlap_evidence_is_explicit_when_summary_missing(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
evidence = dataset_overlap_evidence(tmp_path / "dataset.yaml")
|
||||
|
||||
assert evidence["status"] == "unavailable"
|
||||
assert evidence["validation_tiles_non_overlapping"] is None
|
||||
assert evidence["statistical_independence_established"] is False
|
||||
|
||||
|
||||
def test_nonoverlap_does_not_overclaim_statistical_independence(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
dataset_yaml = tmp_path / "dataset.yaml"
|
||||
dataset_yaml.write_text("path: .\nval: images/val\n", encoding="utf-8")
|
||||
(tmp_path / "yolo_tile_dataset_summary.json").write_text(
|
||||
json.dumps({"tile_size": 512, "stride": 512}), encoding="utf-8"
|
||||
)
|
||||
|
||||
evidence = dataset_overlap_evidence(dataset_yaml)
|
||||
|
||||
assert evidence["validation_tiles_non_overlapping"] is True
|
||||
assert evidence["statistical_independence_established"] is False
|
||||
assert "not established" in evidence["interpretation"]
|
||||
|
||||
|
||||
def _write_summary(path: Path, rows: list[tuple[str, str]]) -> None:
|
||||
path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"tile_size": 512,
|
||||
"stride": 512,
|
||||
"tiles": [
|
||||
{"sample_slug": sample_slug, "split": split}
|
||||
for sample_slug, split in rows
|
||||
],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def test_model_lineage_independence_detects_exposure_in_every_split(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
evaluation = tmp_path / "evaluation"
|
||||
evaluation.mkdir()
|
||||
dataset_yaml = evaluation / "dataset.yaml"
|
||||
dataset_yaml.write_text("path: .\nval: images/val\n", encoding="utf-8")
|
||||
_write_summary(
|
||||
evaluation / "yolo_tile_dataset_summary.json",
|
||||
[("turnhout", "val"), ("postel_bos", "val")],
|
||||
)
|
||||
active = tmp_path / "active.json"
|
||||
challenger = tmp_path / "challenger.json"
|
||||
_write_summary(active, [("postel_bos", "train"), ("turnhout", "val")])
|
||||
_write_summary(
|
||||
challenger, [("postel_bos", "train"), ("dessel", "train")]
|
||||
)
|
||||
|
||||
evidence = model_lineage_independence_evidence(
|
||||
dataset_yaml, [active, challenger]
|
||||
)
|
||||
|
||||
assert evidence["status"] == "overlap"
|
||||
assert evidence["overlapping_evaluation_samples"] == ["postel_bos", "turnhout"]
|
||||
assert evidence["independent_for_all_supplied_lineage_corpora"] is False
|
||||
assert [
|
||||
row["overlapping_evaluation_samples"] for row in evidence["lineage_corpora"]
|
||||
] == [["postel_bos", "turnhout"], ["postel_bos"]]
|
||||
assert evidence["lineage_corpora"][0]["exposure_roles"]["turnhout"] == ["val"]
|
||||
|
||||
|
||||
def test_model_lineage_independence_accepts_disjoint_samples(tmp_path: Path) -> None:
|
||||
evaluation = tmp_path / "evaluation"
|
||||
evaluation.mkdir()
|
||||
dataset_yaml = evaluation / "dataset.yaml"
|
||||
dataset_yaml.write_text("path: .\nval: images/val\n", encoding="utf-8")
|
||||
_write_summary(
|
||||
evaluation / "yolo_tile_dataset_summary.json", [("turnhout", "val")]
|
||||
)
|
||||
training = tmp_path / "training.json"
|
||||
_write_summary(training, [("mol", "train")])
|
||||
|
||||
evidence = model_lineage_independence_evidence(dataset_yaml, [training])
|
||||
|
||||
assert evidence["status"] == "independent"
|
||||
assert evidence["overlapping_evaluation_samples"] == []
|
||||
assert evidence["independent_for_all_supplied_lineage_corpora"] is True
|
||||
|
||||
|
||||
def test_model_lineage_independence_is_unavailable_without_ancestral_corpus(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
evaluation = tmp_path / "evaluation"
|
||||
evaluation.mkdir()
|
||||
dataset_yaml = evaluation / "dataset.yaml"
|
||||
dataset_yaml.write_text("path: .\nval: images/val\n", encoding="utf-8")
|
||||
_write_summary(
|
||||
evaluation / "yolo_tile_dataset_summary.json", [("turnhout", "val")]
|
||||
)
|
||||
|
||||
evidence = model_lineage_independence_evidence(dataset_yaml, [])
|
||||
|
||||
assert evidence["status"] == "unavailable"
|
||||
assert evidence["overlapping_evaluation_samples"] == []
|
||||
assert evidence["independent_for_all_supplied_lineage_corpora"] is False
|
||||
|
||||
|
||||
def test_blocked_manifest_records_that_models_and_gpu_were_not_used(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
dataset_yaml = tmp_path / "dataset.yaml"
|
||||
dataset_yaml.write_text("path: .\nval: images/val\n", encoding="utf-8")
|
||||
output = tmp_path / "evidence" / "matrix.json"
|
||||
|
||||
write_blocked_manifest(
|
||||
output,
|
||||
dataset_yaml,
|
||||
{
|
||||
"status": "overlap",
|
||||
"independent_for_all_supplied_lineage_corpora": False,
|
||||
},
|
||||
)
|
||||
|
||||
payload = json.loads(output.read_text(encoding="utf-8"))
|
||||
assert payload["status"] == "blocked_model_lineage_sample_exposure"
|
||||
assert payload["model_loading_attempted"] is False
|
||||
assert payload["gpu_inference_attempted"] is False
|
||||
|
||||
|
||||
def test_cli_blocks_lineage_validation_exposure_before_model_resolution(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
evaluation = tmp_path / "evaluation"
|
||||
images = evaluation / "images" / "val"
|
||||
images.mkdir(parents=True)
|
||||
(images / "turnhout_r0_c0.png").write_bytes(b"not opened before gate")
|
||||
dataset_yaml = evaluation / "dataset.yaml"
|
||||
dataset_yaml.write_text("path: .\nval: images/val\n", encoding="utf-8")
|
||||
_write_summary(
|
||||
evaluation / "yolo_tile_dataset_summary.json", [("turnhout", "val")]
|
||||
)
|
||||
ancestor = tmp_path / "ancestor.json"
|
||||
_write_summary(ancestor, [("turnhout", "val"), ("mol", "train")])
|
||||
output = tmp_path / "blocked.json"
|
||||
monkeypatch.setattr(
|
||||
sys,
|
||||
"argv",
|
||||
[
|
||||
"evaluate_yolo_checkpoint_matrix.py",
|
||||
"--dataset-yaml",
|
||||
str(dataset_yaml),
|
||||
"--model",
|
||||
str(tmp_path / "model-is-never-resolved.pt"),
|
||||
"--output",
|
||||
str(output),
|
||||
"--background-prefix",
|
||||
"background",
|
||||
"--lineage-summary",
|
||||
str(ancestor),
|
||||
"--require-lineage-sample-independence",
|
||||
],
|
||||
)
|
||||
|
||||
assert main() == 3
|
||||
payload = json.loads(output.read_text(encoding="utf-8"))
|
||||
assert payload["status"] == "blocked_model_lineage_sample_exposure"
|
||||
assert payload["model_loading_attempted"] is False
|
||||
|
||||
|
||||
def test_pure_background_prefix_rejects_nonempty_labels(tmp_path: Path) -> None:
|
||||
image_dir = tmp_path / "images" / "val"
|
||||
label_dir = tmp_path / "labels" / "val"
|
||||
image_dir.mkdir(parents=True)
|
||||
label_dir.mkdir(parents=True)
|
||||
image = image_dir / "sparse_bg_0001.png"
|
||||
image.write_bytes(b"image")
|
||||
(label_dir / "sparse_bg_0001.txt").write_text(
|
||||
"0 0.5 0.5 0.1 0.1\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
import pytest
|
||||
|
||||
with pytest.raises(ValueError, match="non-empty labels"):
|
||||
validate_pure_background_prefixes([image], ("sparse_bg",))
|
||||
|
||||
|
||||
def test_pure_background_prefix_accepts_empty_labels(tmp_path: Path) -> None:
|
||||
image_dir = tmp_path / "images" / "val"
|
||||
label_dir = tmp_path / "labels" / "val"
|
||||
image_dir.mkdir(parents=True)
|
||||
label_dir.mkdir(parents=True)
|
||||
image = image_dir / "pure_bg_0001.png"
|
||||
image.write_bytes(b"image")
|
||||
(label_dir / "pure_bg_0001.txt").write_text("", encoding="utf-8")
|
||||
|
||||
assert validate_pure_background_prefixes([image], ("pure_bg",)) == [image]
|
||||
@@ -0,0 +1,40 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.export_experimental_yolo_train_shard import (
|
||||
is_canonical_train_window,
|
||||
validate_experimental_request,
|
||||
)
|
||||
from scripts.train_experimental_yolo_candidate import validate_paths
|
||||
from scripts.train_experimental_building_proposal_filter import validate_inputs
|
||||
from scripts.compose_experimental_yolo_rehearsal_dataset import validate_sources
|
||||
|
||||
|
||||
def test_export_refuses_non_experimental_output(tmp_path: Path) -> None:
|
||||
manifest = tmp_path / "manifest.json"
|
||||
manifest.write_text("{}", encoding="utf-8")
|
||||
with pytest.raises(ValueError, match="output must remain"):
|
||||
validate_experimental_request(manifest, tmp_path / "output")
|
||||
|
||||
|
||||
def test_training_refuses_paths_outside_experimental_root(tmp_path: Path) -> None:
|
||||
with pytest.raises(ValueError, match="must remain"):
|
||||
validate_paths(tmp_path / "dataset", tmp_path / "output")
|
||||
|
||||
|
||||
def test_train_shard_rejects_near_duplicate_edge_cover_windows() -> None:
|
||||
canonical = {"row_off": 512, "col_off": 0, "height": 512, "width": 512}
|
||||
edge_cover = {"row_off": 513, "col_off": 0, "height": 512, "width": 512}
|
||||
assert is_canonical_train_window(canonical, 512)
|
||||
assert not is_canonical_train_window(edge_cover, 512)
|
||||
|
||||
|
||||
def test_proposal_filter_refuses_non_experimental_paths(tmp_path: Path) -> None:
|
||||
with pytest.raises(ValueError, match="must remain"):
|
||||
validate_inputs(tmp_path / "summary.json", tmp_path / "output")
|
||||
|
||||
|
||||
def test_rehearsal_composer_refuses_non_experimental_output(tmp_path: Path) -> None:
|
||||
with pytest.raises(ValueError, match="output must remain"):
|
||||
validate_sources(tmp_path / "old.json", tmp_path / "new.json", tmp_path / "out")
|
||||
@@ -0,0 +1,74 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.export_yolo_diagnostic_evaluation_tiles import (
|
||||
is_canonical_evaluation_window,
|
||||
validate_diagnostic_manifest,
|
||||
)
|
||||
|
||||
|
||||
def _write_manifest(tmp_path: Path, *, split: str = "calibration") -> Path:
|
||||
manifest = tmp_path / "operator_samples_manifest.json"
|
||||
manifest.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"purpose": "non_protected_diagnostic_evaluation",
|
||||
"training_eligibility": {
|
||||
"status": "not_eligible_evaluation_only"
|
||||
},
|
||||
"samples": [
|
||||
{
|
||||
"sample_slug": "fresh-aoi",
|
||||
"split": split,
|
||||
"sample_role": "positive",
|
||||
}
|
||||
],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
import hashlib
|
||||
|
||||
digest = hashlib.sha256(manifest.read_bytes()).hexdigest()
|
||||
(tmp_path / "NO_TRAINING.json").write_text(
|
||||
json.dumps({"training_allowed": False, "manifest_sha256": digest}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return manifest
|
||||
|
||||
|
||||
def test_validate_diagnostic_manifest_accepts_bound_calibration(tmp_path: Path) -> None:
|
||||
manifest = _write_manifest(tmp_path)
|
||||
|
||||
payload = validate_diagnostic_manifest(manifest)
|
||||
|
||||
assert payload["samples"][0]["sample_slug"] == "fresh-aoi"
|
||||
|
||||
|
||||
def test_validate_diagnostic_manifest_rejects_training_split(tmp_path: Path) -> None:
|
||||
manifest = _write_manifest(tmp_path, split="train")
|
||||
|
||||
with pytest.raises(ValueError, match="calibration-only"):
|
||||
validate_diagnostic_manifest(manifest)
|
||||
|
||||
|
||||
def test_validate_diagnostic_manifest_rejects_unbound_marker(tmp_path: Path) -> None:
|
||||
manifest = _write_manifest(tmp_path)
|
||||
(tmp_path / "NO_TRAINING.json").write_text(
|
||||
json.dumps({"training_allowed": False, "manifest_sha256": "0" * 64}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="not bound"):
|
||||
validate_diagnostic_manifest(manifest)
|
||||
|
||||
|
||||
def test_canonical_evaluation_window_rejects_overlapping_edge_cover() -> None:
|
||||
assert is_canonical_evaluation_window(
|
||||
{"row_off": 512, "col_off": 512, "height": 512, "width": 512}, 512
|
||||
)
|
||||
assert not is_canonical_evaluation_window(
|
||||
{"row_off": 521, "col_off": 512, "height": 512, "width": 512}, 512
|
||||
)
|
||||
@@ -0,0 +1,93 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.provision_belgium_building_training_portfolio import AOIS, load_custom_aois
|
||||
|
||||
|
||||
V63_CANDIDATES = {
|
||||
"aarschot-mixed-cal-candidate-v63",
|
||||
"beveren-ribbon-cal-candidate-v63",
|
||||
"oostkamp-suburban-cal-candidate-v63",
|
||||
}
|
||||
V66_CANDIDATES = {
|
||||
"zutendaal-lowrise-cal-candidate-v66",
|
||||
"zoersel-lowrise-cal-candidate-v66",
|
||||
"landen-rural-cal-candidate-v66",
|
||||
}
|
||||
|
||||
|
||||
def test_portfolio_slugs_are_unique() -> None:
|
||||
slugs = [aoi.slug for aoi in AOIS]
|
||||
assert len(slugs) == len(set(slugs))
|
||||
|
||||
|
||||
def test_v63_calibration_candidates_are_flemish_train_role_inputs() -> None:
|
||||
candidates = {aoi.slug: aoi for aoi in AOIS if aoi.slug in V63_CANDIDATES}
|
||||
assert set(candidates) == V63_CANDIDATES
|
||||
assert all(aoi.region == "flanders" for aoi in candidates.values())
|
||||
assert all(aoi.split == "train" for aoi in candidates.values())
|
||||
assert all(aoi.sample_role == "positive" for aoi in candidates.values())
|
||||
|
||||
|
||||
def test_v66_lowrise_candidates_are_flemish_train_role_inputs() -> None:
|
||||
candidates = {aoi.slug: aoi for aoi in AOIS if aoi.slug in V66_CANDIDATES}
|
||||
assert set(candidates) == V66_CANDIDATES
|
||||
assert all(aoi.region == "flanders" for aoi in candidates.values())
|
||||
assert all(aoi.split == "train" for aoi in candidates.values())
|
||||
assert all("lowrise" in aoi.context for aoi in candidates.values())
|
||||
|
||||
|
||||
def test_custom_aoi_spec_loads_non_protected_calibration(tmp_path: Path) -> None:
|
||||
spec = tmp_path / "aois.json"
|
||||
spec.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"aois": [
|
||||
{
|
||||
"slug": "fresh-rural-cal",
|
||||
"region": "wallonia",
|
||||
"context": "rural-lowrise",
|
||||
"split": "calibration",
|
||||
"lon": 5.25,
|
||||
"lat": 50.25,
|
||||
}
|
||||
]
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = load_custom_aois(spec)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].slug == "fresh-rural-cal"
|
||||
assert result[0].split == "calibration"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("split", ["test", "background-test"])
|
||||
def test_custom_aoi_spec_rejects_protected_splits(
|
||||
tmp_path: Path, split: str
|
||||
) -> None:
|
||||
spec = tmp_path / "aois.json"
|
||||
spec.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"aois": [
|
||||
{
|
||||
"slug": "forbidden",
|
||||
"region": "flanders",
|
||||
"context": "urban",
|
||||
"split": split,
|
||||
"lon": 4.5,
|
||||
"lat": 51.0,
|
||||
}
|
||||
]
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(SystemExit, match="protected/unsupported split"):
|
||||
load_custom_aois(spec)
|
||||
@@ -0,0 +1,97 @@
|
||||
import pytest
|
||||
|
||||
from scripts.render_operator_yolo_label_qa_contact_sheets import (
|
||||
CONTACT_SHEET_NAME_TEMPLATE,
|
||||
build_label_highlights,
|
||||
build_relationship_highlights,
|
||||
filter_tiles_by_samples,
|
||||
)
|
||||
|
||||
|
||||
TILES = [
|
||||
{"sample_slug": "genk-industry-train"},
|
||||
{"sample_slug": "lokeren-ribbon-train"},
|
||||
{"sample_slug": "ostend-coastal-train"},
|
||||
]
|
||||
|
||||
|
||||
def test_filter_tiles_by_samples_keeps_only_explicit_samples() -> None:
|
||||
assert filter_tiles_by_samples(
|
||||
TILES, ["genk-industry-train", "ostend-coastal-train"]
|
||||
) == [
|
||||
TILES[0],
|
||||
TILES[2],
|
||||
]
|
||||
|
||||
|
||||
def test_filter_tiles_by_samples_rejects_missing_sample() -> None:
|
||||
with pytest.raises(ValueError, match="absent from summary"):
|
||||
filter_tiles_by_samples(TILES, ["missing"])
|
||||
|
||||
|
||||
def test_filter_tiles_by_samples_without_filter_preserves_tiles() -> None:
|
||||
assert filter_tiles_by_samples(TILES, []) is TILES
|
||||
|
||||
|
||||
def test_contact_sheet_name_template_is_stable_and_one_indexed() -> None:
|
||||
assert CONTACT_SHEET_NAME_TEMPLATE.format(index=1) == "contact_sheet_001.png"
|
||||
assert CONTACT_SHEET_NAME_TEMPLATE.format(index=12) == "contact_sheet_012.png"
|
||||
|
||||
|
||||
def test_relationship_highlights_bind_indices_and_preserve_highest_priority() -> None:
|
||||
summary = {
|
||||
"flagged_tiles": [
|
||||
{
|
||||
"label_path": "/data/tile.txt",
|
||||
"relationships": [
|
||||
{
|
||||
"relationship": "possible_nested",
|
||||
"first_index": 2,
|
||||
"second_index": 4,
|
||||
},
|
||||
{
|
||||
"relationship": "exact_duplicate",
|
||||
"first_index": 2,
|
||||
"second_index": 5,
|
||||
},
|
||||
{
|
||||
"relationship": "near_duplicate",
|
||||
"first_index": 6,
|
||||
"second_index": 7,
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
assert build_relationship_highlights(summary) == {
|
||||
"/data/tile.txt": {
|
||||
2: "exact_duplicate",
|
||||
4: "possible_nested",
|
||||
5: "exact_duplicate",
|
||||
6: "near_duplicate",
|
||||
7: "near_duplicate",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_label_highlights_support_outliers_with_stable_priority() -> None:
|
||||
summary = {
|
||||
"flagged_tiles": [
|
||||
{
|
||||
"label_path": "/data/tile.txt",
|
||||
"outliers": [
|
||||
{"category": "tile_edge", "index": 3},
|
||||
{"category": "small_dimension", "index": 3},
|
||||
{"category": "extreme_aspect_ratio", "index": 4},
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
assert build_label_highlights(summary) == {
|
||||
"/data/tile.txt": {
|
||||
3: "small_dimension",
|
||||
4: "extreme_aspect_ratio",
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user