Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
260 lines
13 KiB
Python
260 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""Run L54's triangle audit over L59's open-shaft lattice and controls."""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import importlib.util
|
|
import json
|
|
import math
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[4]
|
|
AUDIT54 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux54/AuditWholeSurfaceL54.py"
|
|
spec = importlib.util.spec_from_file_location("whole_surface_l54", AUDIT54)
|
|
audit54 = importlib.util.module_from_spec(spec)
|
|
assert spec and spec.loader
|
|
spec.loader.exec_module(audit54)
|
|
original_make_route = audit54.make_route
|
|
|
|
|
|
def build_segment_bvh(segments: list[tuple]) -> dict:
|
|
def build(indices: list[int]) -> dict:
|
|
minimum = tuple(min(min(segments[index][0][axis], segments[index][1][axis])
|
|
for index in indices) for axis in range(3))
|
|
maximum = tuple(max(max(segments[index][0][axis], segments[index][1][axis])
|
|
for index in indices) for axis in range(3))
|
|
if len(indices) <= 8:
|
|
return {"minimum": minimum, "maximum": maximum,
|
|
"segments": tuple(sorted(indices)), "left": None, "right": None}
|
|
axis = max(range(3), key=lambda value: maximum[value] - minimum[value])
|
|
indices.sort(key=lambda index: (segments[index][0][axis]
|
|
+ segments[index][1][axis]) * 0.5)
|
|
middle = len(indices) // 2
|
|
return {"minimum": minimum, "maximum": maximum, "segments": None,
|
|
"left": build(indices[:middle]), "right": build(indices[middle:])}
|
|
|
|
if not segments:
|
|
raise ValueError("lattice audit route has no measurable segments")
|
|
return build(list(range(len(segments))))
|
|
|
|
|
|
def make_lattice_route(layout: dict, phase_m: float = 0.0) -> dict:
|
|
edges = sorted((edge for edge in layout.get("cavern_edges", [])
|
|
if edge["stable_id"] in ("lattice/spine/00-arrival-portal-v1",
|
|
"lattice/spine/01-helix-ramp-v1", "lattice/spine/99-departure-portal-v1")
|
|
or edge["stable_id"].startswith("lattice/spine/maze-edge-")),
|
|
key=lambda edge: (0 if edge["stable_id"].startswith("lattice/spine/00-") else
|
|
1 if edge["stable_id"].startswith("lattice/spine/01-") else
|
|
2 if edge["stable_id"].startswith("lattice/spine/maze-edge-") else 3,
|
|
edge["stable_id"]))
|
|
if not edges:
|
|
raise ValueError("lattice audit needs its resolved mandatory spine edges")
|
|
segments = []
|
|
routes = []
|
|
for edge_index, edge in enumerate(edges):
|
|
points = [[value / 1_000_000.0 for value in row]
|
|
for row in edge.get("polyline_um", [])]
|
|
if len(points) < 2:
|
|
raise ValueError(f"{edge['stable_id']}: explicit path is missing")
|
|
radius = edge["bore_radius_um"] / 1_000_000.0
|
|
height = edge["bore_height_um"] / 1_000_000.0
|
|
support_depth = edge.get("floor_support_depth_um", 0) / 1_000_000.0
|
|
horizontal_s = [0.0]
|
|
for a, b in zip(points, points[1:]):
|
|
horizontal_s.append(horizontal_s[-1] + ((b[0] - a[0]) ** 2
|
|
+ (b[1] - a[1]) ** 2) ** 0.5)
|
|
first = points[1]
|
|
first_dx, first_dy = first[0] - points[0][0], first[1] - points[0][1]
|
|
first_length = max(1.0e-9, (first_dx * first_dx + first_dy * first_dy) ** 0.5)
|
|
extension = (points[0][0] - first_dx / first_length * audit54.OPEN_GROUND_EXTENSION_M,
|
|
points[0][1] - first_dy / first_length * audit54.OPEN_GROUND_EXTENSION_M,
|
|
points[0][2] - (first[2] - points[0][2]) / first_length
|
|
* audit54.OPEN_GROUND_EXTENSION_M)
|
|
route_points = [extension, *points]
|
|
route_s = [-audit54.OPEN_GROUND_EXTENSION_M, *horizontal_s]
|
|
for index, (a, b) in enumerate(zip(route_points, route_points[1:])):
|
|
segments.append((a, b, route_s[index], route_s[index + 1],
|
|
radius, height, edge_index))
|
|
routes.append({"edge": edge, "points": points, "radius": radius,
|
|
"height": height, "support_depth": support_depth,
|
|
"length": horizontal_s[-1], "index": edge_index})
|
|
# The shaft core is an open cylindrical volume, not a walkable room. Its
|
|
# axis-aligned bounds include unrelated rock outside the cylinder and
|
|
# above the ramp headroom; the helix corridor already includes the inner
|
|
# wall, lip, foot, and underside where those surfaces meet the route.
|
|
rooms = [{"room": room,
|
|
"center": tuple(value / 1_000_000.0 for value in room["center_um"]),
|
|
"half": tuple(value / 1_000_000.0 for value in room["half_extents_um"]),
|
|
"floor": room["floor_um"] / 1_000_000.0}
|
|
for room in sorted(layout.get("cavern_rooms", []), key=lambda row: row["stable_id"])
|
|
if room.get("stable_id") != "lattice/spine/shaft-core-v2"]
|
|
return {"segments": segments, "edges": routes, "rooms": rooms, "phase_m": phase_m,
|
|
"segment_bvh": build_segment_bvh(segments)}
|
|
|
|
|
|
def make_route(layout: dict, phase_m: float = 0.0) -> dict:
|
|
if any(edge.get("stable_id", "").startswith("lattice/")
|
|
for edge in layout.get("cavern_edges", [])):
|
|
return make_lattice_route(layout, phase_m)
|
|
return original_make_route(layout, phase_m)
|
|
|
|
|
|
def lattice_route_nearest(point: tuple[float, float, float], route: dict):
|
|
"""Resolve stacked helix turns in 3D, then measure corridor radius in XY."""
|
|
best = None
|
|
def consider(segment_index: int) -> None:
|
|
nonlocal best
|
|
a, b, s0, s1, radius, height, edge_index = route["segments"][segment_index]
|
|
dx, dy, dz = (b[axis] - a[axis] for axis in range(3))
|
|
length_squared = dx * dx + dy * dy + dz * dz
|
|
t = 0.0 if length_squared <= 1.0e-12 else max(0.0, min(1.0,
|
|
((point[0] - a[0]) * dx + (point[1] - a[1]) * dy
|
|
+ (point[2] - a[2]) * dz) / length_squared))
|
|
x, y, z = (a[axis] + (b[axis] - a[axis]) * t for axis in range(3))
|
|
distance3d_squared = ((point[0] - x) ** 2 + (point[1] - y) ** 2
|
|
+ (point[2] - z) ** 2)
|
|
if (best is None or distance3d_squared < best[0]
|
|
or (distance3d_squared == best[0] and segment_index < best[1])):
|
|
horizontal_length = math.hypot(dx, dy)
|
|
lateral = ((dx * (point[1] - y) - dy * (point[0] - x))
|
|
/ horizontal_length if horizontal_length > 1.0e-9 else 0.0)
|
|
best = (distance3d_squared, segment_index,
|
|
math.hypot(point[0] - x, point[1] - y),
|
|
s0 + (s1 - s0) * t, z, lateral, radius, height, edge_index,
|
|
route["edges"][edge_index]["length"])
|
|
|
|
def lower_bound_squared(node: dict) -> float:
|
|
distance_squared = 0.0
|
|
for axis in range(3):
|
|
gap = (node["minimum"][axis] - point[axis]
|
|
if point[axis] < node["minimum"][axis]
|
|
else point[axis] - node["maximum"][axis]
|
|
if point[axis] > node["maximum"][axis] else 0.0)
|
|
distance_squared += gap * gap
|
|
return distance_squared
|
|
|
|
def visit(node: dict) -> None:
|
|
if best is not None and lower_bound_squared(node) > best[0]:
|
|
return
|
|
if node["segments"] is not None:
|
|
for segment_index in node["segments"]:
|
|
consider(segment_index)
|
|
return
|
|
left, right = node["left"], node["right"]
|
|
left_bound, right_bound = lower_bound_squared(left), lower_bound_squared(right)
|
|
if left_bound <= right_bound:
|
|
visit(left)
|
|
visit(right)
|
|
else:
|
|
visit(right)
|
|
visit(left)
|
|
|
|
visit(route["segment_bvh"])
|
|
if best is None:
|
|
raise ValueError("lattice audit route has no measurable segments")
|
|
return best[2:]
|
|
|
|
|
|
def mark_lattice_region(triangles: list, route: dict) -> None:
|
|
margin = audit54.CORRIDOR_MARGIN_M
|
|
extension = audit54.OPEN_GROUND_EXTENSION_M
|
|
bounds = []
|
|
for edge in route["edges"]:
|
|
points = edge["points"]
|
|
radius, height = edge["radius"], edge["height"]
|
|
support_depth = edge["support_depth"]
|
|
bounds.append((
|
|
min(point[0] for point in points) - radius - margin,
|
|
max(point[0] for point in points) + radius + margin,
|
|
min(point[1] for point in points) - radius - margin,
|
|
max(point[1] for point in points) + radius + margin,
|
|
min(point[2] for point in points) - support_depth - margin,
|
|
max(point[2] for point in points) + height + margin))
|
|
for room in route["rooms"]:
|
|
center, half, floor = room["center"], room["half"], room["floor"]
|
|
bounds.append((center[0] - half[0] - margin, center[0] + half[0] + margin,
|
|
center[1] - half[1] - margin, center[1] + half[1] + margin,
|
|
floor - margin, floor + 2.0 * half[2] + margin))
|
|
|
|
for triangle in triangles:
|
|
x, y, z = triangle.center
|
|
if not any(low_x <= x <= high_x and low_y <= y <= high_y
|
|
and low_z <= z <= high_z
|
|
for low_x, high_x, low_y, high_y, low_z, high_z in bounds):
|
|
triangle.in_region = False
|
|
triangle.reference = False
|
|
continue
|
|
nearest = lattice_route_nearest(triangle.center, route)
|
|
distance, distance_s, floor_z, lateral, radius, height, edge_index, edge_length = nearest
|
|
support_depth = route["edges"][edge_index]["support_depth"]
|
|
corridor = (-extension <= distance_s <= edge_length
|
|
and distance <= radius + margin
|
|
and floor_z - support_depth - margin <= z <= floor_z + height + margin)
|
|
room_box = any(abs(x - room["center"][0]) <= room["half"][0] + margin
|
|
and abs(y - room["center"][1]) <= room["half"][1] + margin
|
|
and room["floor"] - margin <= z
|
|
<= room["floor"] + 2.0 * room["half"][2] + margin
|
|
for room in route["rooms"])
|
|
triangle.in_region = corridor or room_box
|
|
inside_room = any(abs(x - room["center"][0]) <= room["half"][0] + 0.45
|
|
and abs(y - room["center"][1]) <= room["half"][1] + 0.45
|
|
for room in route["rooms"])
|
|
outside_bore = distance > radius + 0.45 and not inside_room
|
|
triangle.reference = (triangle.in_region and outside_bore
|
|
and triangle.smooth[2] >= 0.45)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--case", nargs=5, action="append", required=True,
|
|
metavar=("LABEL", "MESH", "LAYOUT_OR_DASH", "EXPECT", "FOCUS"))
|
|
parser.add_argument("--json", type=Path, required=True)
|
|
parser.add_argument("--svg-dir", type=Path)
|
|
parser.add_argument("--phase", type=float, choices=(-0.5, 0.0, 0.5), default=0.0)
|
|
args = parser.parse_args()
|
|
audit54.make_route = make_route
|
|
results = []
|
|
for label, mesh, layout, expect, focus in args.case:
|
|
if expect not in ("pass", "fail", "measure") or focus not in ("whole", "floor"):
|
|
parser.error("EXPECT must be pass/fail/measure and FOCUS must be whole/floor")
|
|
native_lattice = layout != "-" and any(
|
|
edge.get("stable_id", "").startswith("lattice/")
|
|
for edge in json.loads(Path(layout).read_text(encoding="utf-8")).get(
|
|
"cavern_edges", []))
|
|
original_margin = audit54.CORRIDOR_MARGIN_M
|
|
original_extension = audit54.OPEN_GROUND_EXTENSION_M
|
|
original_nearest = audit54.route_nearest
|
|
original_mark_region = audit54.mark_region
|
|
if native_lattice:
|
|
# Keep the whole-surface test on native records: one core voxel of
|
|
# tolerance captures their meshed walls while excluding the separate
|
|
# legacy transition above the resolved mouth.
|
|
audit54.CORRIDOR_MARGIN_M = 0.25
|
|
audit54.OPEN_GROUND_EXTENSION_M = 0.0
|
|
audit54.route_nearest = lattice_route_nearest
|
|
audit54.mark_region = mark_lattice_region
|
|
try:
|
|
results.append(audit54.audit_case(label, Path(mesh), Path(layout), expect,
|
|
focus, args.svg_dir, args.phase))
|
|
finally:
|
|
audit54.CORRIDOR_MARGIN_M = original_margin
|
|
audit54.OPEN_GROUND_EXTENSION_M = original_extension
|
|
audit54.route_nearest = original_nearest
|
|
audit54.mark_region = original_mark_region
|
|
if native_lattice:
|
|
print("[L59LatticeAuditScope] margin_m=0.250 ramp_underside_depth_m=1.000 "
|
|
"outside_mouth_extension_m=0.000 nearest_centerline=3D "
|
|
"radius_measure=horizontal status=PASS")
|
|
args.json.parent.mkdir(parents=True, exist_ok=True)
|
|
args.json.write_text(json.dumps({"audit": "whole-surface-open-shaft-l59-v1",
|
|
"cases": results}, indent=2) + "\n", encoding="utf-8")
|
|
failures = [result for result in results if result["status"] != "PASS"]
|
|
print(f"[L59WholeSurfaceAuditSummary] cases={len(results)} failed={len(failures)} "
|
|
f"json={args.json} status={'FAIL' if failures else 'PASS'}")
|
|
return 1 if failures else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|