Bands = end profiles of per-control-span shell capsules (~9.35 m pitch = span spacing). Shell now smooth-unions adjacent spans over 6 m and reuses the interior chord distance; a 0.60 m clearance fades in 5-20 m from each mouth. Close-ups: bands reduced but still visible on part of the exposed shell. Cost vs L109: near-ready +8..+12% (FI 0.88 -> 1.00, TN 0.92 -> 1.01 s @W12). Audits, Clang/GCC determinism, fast tier and Linux UE build pass. Kept on its own branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
89 lines
3.8 KiB
Python
89 lines
3.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Check the paired L109/L110 capture set and 4 m shell close-ups."""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
import struct
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[4]
|
|
CAPTURE_ROOT = Path(__file__).resolve().parent / "captures"
|
|
RESEARCH_ROOT = CAPTURE_ROOT.parent
|
|
INDEX = CAPTURE_ROOT / "capture-index.json"
|
|
OUTPUT = CAPTURE_ROOT / "capture-verification.json"
|
|
|
|
|
|
def sha256(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as stream:
|
|
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
|
digest.update(block)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def png_size(path: Path) -> tuple[int, int]:
|
|
with path.open("rb") as stream:
|
|
header = stream.read(24)
|
|
if len(header) != 24 or header[:8] != b"\x89PNG\r\n\x1a\n" or header[12:16] != b"IHDR":
|
|
raise RuntimeError(f"invalid PNG: {path}")
|
|
return struct.unpack(">II", header[16:24])
|
|
|
|
|
|
def main() -> int:
|
|
captures = json.loads(INDEX.read_text(encoding="utf-8"))["captures"]
|
|
counts = {variant: Counter(row["kind"] for row in captures
|
|
if row["variant"] == variant)
|
|
for variant in ("before", "after")}
|
|
by_variant = {variant: {row["name"]: row for row in captures
|
|
if row["variant"] == variant}
|
|
for variant in ("before", "after")}
|
|
errors: list[str] = []
|
|
for variant in ("before", "after"):
|
|
rows = [row for row in captures if row["variant"] == variant]
|
|
if len(rows) != 53:
|
|
errors.append(f"{variant}: expected 53 captures, got {len(rows)}")
|
|
for row in rows:
|
|
image = RESEARCH_ROOT / row["capture"]
|
|
if not row.get("verified") or row.get("render_tile_skirts") != "off":
|
|
errors.append(f"{variant}/{row['name']}: render verification flags failed")
|
|
if row.get("drawn_triangles", 0) <= 0 or not image.is_file():
|
|
errors.append(f"{variant}/{row['name']}: missing geometry or image")
|
|
elif png_size(image) != (1920, 1080):
|
|
errors.append(f"{variant}/{row['name']}: unexpected image size {png_size(image)}")
|
|
closeups = [row for row in rows if row["kind"] == "open-air-closeup"]
|
|
if len(closeups) != 11:
|
|
errors.append(f"{variant}: expected 11 close-ups, got {len(closeups)}")
|
|
for row in closeups:
|
|
distance = row.get("shell_standoff_actual_m")
|
|
if distance is None or not 3.0 <= distance <= 5.0:
|
|
errors.append(f"{variant}/{row['name']}: close-up standoff {distance}")
|
|
names_before, names_after = set(by_variant["before"]), set(by_variant["after"])
|
|
if names_before != names_after:
|
|
errors.append("before/after capture names do not match")
|
|
pair_counts = Counter()
|
|
for name in sorted(names_before & names_after):
|
|
left, right = by_variant["before"][name], by_variant["after"][name]
|
|
same = sha256(RESEARCH_ROOT / left["capture"]) == sha256(
|
|
RESEARCH_ROOT / right["capture"])
|
|
pair_counts[f"{left['kind']}_{'identical' if same else 'changed'}"] += 1
|
|
summary = {
|
|
"status": "PASS" if not errors else "FAIL",
|
|
"capture_count": len(captures),
|
|
"open_air_sections": len(json.loads(INDEX.read_text(encoding="utf-8"))[
|
|
"open_air_sections"]),
|
|
"per_variant_kind_counts": {variant: dict(sorted(counts[variant].items()))
|
|
for variant in counts},
|
|
"paired_image_bytes": dict(sorted(pair_counts.items())),
|
|
"errors": errors,
|
|
}
|
|
OUTPUT.write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8")
|
|
print(json.dumps(summary, sort_keys=True))
|
|
return 0 if not errors else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|