Promote focused small-building detector
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-13 12:15:27 +02:00
parent b3f7c3ca63
commit 1689dce928
20 changed files with 546 additions and 61 deletions
@@ -119,6 +119,7 @@ def audit_feature_collection(payload: dict[str, Any], geod: Any, shape: Any) ->
reference_ids: set[str] = set()
false_negative_areas: list[float] = []
matched_reference_areas: list[float] = []
reference_records: dict[str, dict[str, Any]] = {}
bucket_counts = {
label: {"false_negative": 0, "matched_reference": 0, "total_reference": 0, "false_negative_rate": None}
for label, _, _ in AREA_BUCKETS
@@ -144,6 +145,11 @@ def audit_feature_collection(payload: dict[str, Any], geod: Any, shape: Any) ->
bucket = area_bucket(area_m2)
reference_id = stable_reference_id(feature, geometry)
reference_ids.add(reference_id)
reference_records[reference_id] = {
"area_m2": area_m2,
"area_bucket": bucket,
"feature": feature,
}
bucket_role = "false_negative" if role == "false_negative" else "matched_reference"
bucket_counts[bucket][bucket_role] += 1
bucket_counts[bucket]["total_reference"] += 1
@@ -161,6 +167,7 @@ def audit_feature_collection(payload: dict[str, Any], geod: Any, shape: Any) ->
return {
"false_negative_ids": false_negative_ids,
"reference_ids": reference_ids,
"reference_records": reference_records,
"false_negative_count": len(false_negative_areas),
"matched_reference_count": len(matched_reference_areas),
"total_reference_count": total_reference,
@@ -285,6 +292,7 @@ def run_audit(portfolio_args: list[str], output_dir: Path) -> tuple[Path, Path]:
)
sample_reports: list[dict[str, Any]] = []
persistent_evidence_features: list[dict[str, Any]] = []
for sample_slug in sorted(expected_slugs):
portfolio_rows = []
false_negative_sets = []
@@ -297,7 +305,7 @@ def run_audit(portfolio_args: list[str], output_dir: Path) -> tuple[Path, Path]:
{
key: value
for key, value in raw.items()
if key not in {"false_negative_ids", "reference_ids"}
if key not in {"false_negative_ids", "reference_ids", "reference_records"}
}
)
if any(reference_ids != reference_sets[0] for reference_ids in reference_sets[1:]):
@@ -309,16 +317,61 @@ def run_audit(portfolio_args: list[str], output_dir: Path) -> tuple[Path, Path]:
f"Sample {sample_slug} has different reference populations across portfolios ({counts})"
)
persistent_ids = sorted(set.intersection(*false_negative_sets))
reference_records = portfolio_samples[parsed_portfolios[0][0]][sample_slug]["reference_records"]
persistent_areas = [float(reference_records[reference_id]["area_m2"]) for reference_id in persistent_ids]
persistent_bucket_counts = {
label: {"count": 0, "share": 0.0}
for label, _, _ in AREA_BUCKETS
}
for reference_id in persistent_ids:
record = reference_records[reference_id]
persistent_bucket_counts[record["area_bucket"]]["count"] += 1
source_feature = record["feature"]
properties = dict(source_feature.get("properties") or {})
properties.update(
{
"qa_evidence_role": "persistent_false_negative",
"sample_slug": sample_slug,
"persistent_reference_id": reference_id,
"area_m2": record["area_m2"],
"area_bucket": record["area_bucket"],
"compared_portfolios": labels,
}
)
persistent_evidence_features.append(
{
"type": "Feature",
"id": f"{sample_slug}:{reference_id}",
"properties": properties,
"geometry": source_feature["geometry"],
}
)
if persistent_ids:
for values in persistent_bucket_counts.values():
values["share"] = values["count"] / len(persistent_ids)
sample_reports.append(
{
"sample_slug": sample_slug,
"reference_population_count": len(reference_sets[0]),
"persistent_false_negative_count": len(persistent_ids),
"persistent_reference_ids": persistent_ids,
"persistent_false_negative_area_m2": area_stats(persistent_areas),
"persistent_area_buckets": persistent_bucket_counts,
"portfolios": portfolio_rows,
}
)
output_dir = output_dir.expanduser().resolve()
output_dir.mkdir(parents=True, exist_ok=True)
persistent_geojson_path = output_dir / "persistent_false_negatives.geojson"
persistent_geojson_path.write_text(
json.dumps(
{"type": "FeatureCollection", "features": persistent_evidence_features},
indent=2,
sort_keys=True,
),
encoding="utf-8",
)
report = {
"generated_at": datetime.now(timezone.utc).isoformat(),
"schema_version": 1,
@@ -328,10 +381,9 @@ def run_audit(portfolio_args: list[str], output_dir: Path) -> tuple[Path, Path]:
"portfolios": portfolio_meta,
"sample_count": len(sample_reports),
"samples": sample_reports,
"persistent_evidence_geojson_path": str(persistent_geojson_path),
"recommendations": build_recommendations(sample_reports),
}
output_dir = output_dir.expanduser().resolve()
output_dir.mkdir(parents=True, exist_ok=True)
json_path = output_dir / "detection_false_negative_audit.json"
json_path.write_text(json.dumps(report, indent=2, sort_keys=True), encoding="utf-8")
@@ -345,18 +397,26 @@ def run_audit(portfolio_args: list[str], output_dir: Path) -> tuple[Path, Path]:
"",
"## AOI comparison",
"",
"| AOI | Persistent misses | "
"| AOI | Persistent misses | Persistent tiny/small | Persistent median m2 | "
+ " | ".join(f"{label} FN rate" for label, _ in parsed_portfolios)
+ " |",
"|---|---:|" + "---:|" * len(parsed_portfolios),
"|---|---:|---:|---:|" + "---:|" * len(parsed_portfolios),
]
for sample in sample_reports:
rates = [
f"{(row['false_negative_rate'] or 0.0):.3f}"
for row in sample["portfolios"]
]
persistent_buckets = sample["persistent_area_buckets"]
persistent_small_count = sum(
persistent_buckets[key]["count"]
for key in ("tiny_lt_25_m2", "small_25_100_m2")
)
persistent_median = sample["persistent_false_negative_area_m2"]["median"]
persistent_median_text = f"{persistent_median:.1f}" if persistent_median is not None else "n/a"
lines.append(
f"| {sample['sample_slug']} | {sample['persistent_false_negative_count']} | "
f"{persistent_small_count} | {persistent_median_text} | "
+ " | ".join(rates)
+ " |"
)