feat: separate S-52 mariner runtime presentation

This commit is contained in:
OpenAI Codex
2026-09-18 06:21:54 +08:00
parent f238f1c471
commit a7c9812bd8
12 changed files with 939 additions and 6 deletions

View File

@@ -0,0 +1,157 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
from collections import Counter
import json
from pathlib import Path
from typing import Any
import mapbox_vector_tile
DYNAMIC_FINAL_FIELDS = {
"s52_is_safety_contour",
"s52_sounding_prefix",
"s52_sounding_is_shallow",
"s52_isolated_danger",
"s52_display_base_promoted",
}
def decode_features(path: Path) -> list[tuple[str, dict[str, Any]]]:
decoded = mapbox_vector_tile.decode(path.read_bytes())
features: list[tuple[str, dict[str, Any]]] = []
for layer_name, layer in decoded.items():
for feature in layer.get("features", []):
features.append((layer_name, dict(feature.get("properties") or {})))
return features
def audit_runtime(features: list[tuple[str, dict[str, Any]]]) -> dict[str, Any]:
runtime = [
(layer, props)
for layer, props in features
if props.get("s52_mariner_dependent") == 1
]
failures: list[str] = []
if not runtime:
failures.append("没有发现 s52_mariner_dependent=1 Feature")
csp_counts = Counter(str(props.get("s52_runtime_csp") or "") for _, props in runtime)
leaked = Counter()
for _, props in runtime:
csp = str(props.get("s52_runtime_csp") or "")
if not csp:
failures.append("runtime Feature 缺少 s52_runtime_csp")
if csp and csp not in str(props.get("s52_csp") or "").split(","):
failures.append(f"{csp} 未保留在 s52_csp")
for key in DYNAMIC_FINAL_FIELDS:
if key in props:
leaked[key] += 1
if csp in {"DEPARE01", "DEPARE02"}:
if "depth_min_m" not in props:
failures.append(f"{csp} 缺少 depth_min_m")
if "s52_area_color" in props:
leaked["DEPARE:s52_area_color"] += 1
elif csp == "DEPCNT02" and "contour_depth_m" not in props:
failures.append("DEPCNT02 缺少 contour_depth_m")
elif csp == "SOUNDG02" and "s52_sounding_depth_m" not in props:
failures.append("SOUNDG02 缺少 s52_sounding_depth_m")
if leaked:
failures.append(f"发现 runtime 最终状态泄漏: {dict(leaked)}")
return {
"feature_count": len(features),
"mariner_dependent_count": len(runtime),
"runtime_csp_counts": dict(csp_counts),
"layer_counts": dict(Counter(layer for layer, _ in runtime)),
"fact_counts": {
key: sum(1 for _, props in features if key in props)
for key in (
"depth_min_m",
"depth_max_m",
"contour_depth_m",
"s52_sounding_depth_m",
"s52_hazard_depth_m",
"s52_surrounding_max_drval1_m",
"s52_surrounding_min_drval2_m",
)
},
"dynamic_final_leaks": dict(leaked),
"failures": failures,
"status": "PASS" if not failures else "FAIL",
}
def audit_reference(features: list[tuple[str, dict[str, Any]]]) -> dict[str, Any]:
failures: list[str] = []
runtime_markers = sum(
1 for _, props in features if props.get("s52_mariner_dependent") == 1
)
if runtime_markers:
failures.append(f"reference PBF 出现 {runtime_markers} 个 runtime marker")
return {
"feature_count": len(features),
"runtime_marker_count": runtime_markers,
"fixed_result_counts": {
"safety_contour": sum(
1 for _, props in features if props.get("s52_is_safety_contour") == 1
),
"isolated_danger": sum(
1 for _, props in features if props.get("s52_isolated_danger") == 1
),
"display_base_promoted": sum(
1 for _, props in features if props.get("s52_display_base_promoted") == 1
),
"sounding_prefix": sum(
1 for _, props in features if "s52_sounding_prefix" in props
),
},
"presentation_sequence_counts": {
key: sum(1 for _, props in features if key in props)
for key in (
"s52_symbol_count",
"s52_line_count",
"s52_symbol_rotation_deg",
"s52_symbol_rule_sequence",
"s52_priority_override",
)
},
"failures": failures,
"status": "PASS" if not failures else "FAIL",
}
def main() -> int:
parser = argparse.ArgumentParser(
description="检查 CM95 S-52 runtime/reference Delivery PBF 边界"
)
parser.add_argument("--runtime-pbf", type=Path, required=True)
parser.add_argument("--reference-pbf", type=Path, required=True)
parser.add_argument("--output", type=Path)
args = parser.parse_args()
runtime = audit_runtime(decode_features(args.runtime_pbf))
reference = audit_reference(decode_features(args.reference_pbf))
result = {
"schema": "cm95-s52-runtime-contract-check/v1",
"runtime_pbf": str(args.runtime_pbf),
"reference_pbf": str(args.reference_pbf),
"runtime": runtime,
"reference": reference,
"status": "PASS"
if runtime["status"] == reference["status"] == "PASS"
else "FAIL",
}
text = json.dumps(result, ensure_ascii=False, indent=2)
print(text)
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(text + "\n", encoding="utf-8")
return 0 if result["status"] == "PASS" else 2
if __name__ == "__main__":
raise SystemExit(main())