tools(audit): whole-surface mesh artifact audit with historical controls (L48, tool only); research R15

AuditWholeSurfaceL48.py scans every LOD0 triangle in the route corridor + 3 m and the chamber box + 3 m:
welded-edge normal variation, high-angle edge density, max jumps and sliver area in 1 m cells, clustered
and located, with thresholds from native patches in the same export, plus a detrended full-width floor
profile. L45 ramp, L46 entrance and L47 wall-foot meshes fail it; plain native terrain passes. Red-triangle
SVG maps included. Not wired into the tiers yet; the attempted Core fix is parked on wip/l48-surface-audit.
Research R15: mesh artifact metrics.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-24 14:53:11 +02:00
1 parent c091e4434f
commit 3073d1b2fd
16 files changed
+346656

No files matched your search

@@ -0,0 +1,709 @@
#!/usr/bin/env python3
"""Audit every LOD0 mesh triangle in a cavern corridor for clustered defects."""
from __future__ import annotations
import argparse
import array
import json
import math
from pathlib import Path
import struct
import sys
ROOT = Path(__file__).resolve().parents[4]
CELL_M = 1.0
CORRIDOR_MARGIN_M = 3.0
OPEN_GROUND_EXTENSION_M = 5.0
WELD_M = 0.0001
class Triangle:
__slots__ = ("verts", "center", "area", "normal", "smooth", "quality",
"route_distance", "route_lateral", "route_s", "floor_z",
"in_region", "reference")
def __init__(self, verts, area, normal, smooth, quality):
self.verts = verts
self.center = tuple(sum(v[i] for v in verts) / 3.0 for i in range(3))
self.area = area
self.normal = normal
self.smooth = smooth
self.quality = quality
self.route_distance = 0.0
self.route_lateral = 0.0
self.route_s = 0.0
self.floor_z = 0.0
self.in_region = False
self.reference = False
def normalize(value):
length = math.sqrt(sum(item * item for item in value))
return tuple(item / length for item in value) if length > 1.0e-12 else (0.0, 0.0, 0.0)
def dot(a, b):
return sum(a[i] * b[i] for i in range(3))
def angle_degrees(a, b):
return math.degrees(math.acos(max(-1.0, min(1.0, dot(a, b)))))
def quantile(values, fraction):
if not values:
return 0.0
ordered = sorted(values)
return ordered[min(len(ordered) - 1, max(0, int(math.ceil(fraction * len(ordered))) - 1))]
def read_mesh(path: Path):
triangles = []
selected_tiles = 0
lod_counts = {}
with path.open("rb") as stream:
prefix = stream.read(60)
if len(prefix) != 60 or prefix[:8] != b"VFMSH001":
raise ValueError(f"{path}: invalid VFMSH001 mesh header")
version, voxel_um, tile_count = struct.unpack_from("<3I", prefix, 8)
if version != 1 or not voxel_um or tile_count > 1_000_000:
raise ValueError(f"{path}: unsupported mesh version or tile count")
for _ in range(tile_count):
header = stream.read(36)
if len(header) != 36:
raise ValueError(f"{path}: truncated tile header")
key = struct.unpack_from("<4i", header, 0)
surface_count, _surface_aux, vertex_count, index_count, mask_count = \
struct.unpack_from("<5I", header, 16)
lod_counts[key[3]] = lod_counts.get(key[3], 0) + 1
position_bytes = stream.read(vertex_count * 12)
normal_bytes = stream.read(vertex_count * 12)
index_bytes = stream.read(index_count * 4)
masks = stream.read(mask_count)
if (len(position_bytes) != vertex_count * 12
or len(normal_bytes) != vertex_count * 12
or len(index_bytes) != index_count * 4
or len(masks) != mask_count):
raise ValueError(f"{path}: truncated tile payload")
if key[3] != 0:
continue
selected_tiles += 1
positions = array.array("f")
positions.frombytes(position_bytes)
normals = array.array("f")
normals.frombytes(normal_bytes)
indices = array.array("I")
indices.frombytes(index_bytes)
if sys.byteorder != "little":
positions.byteswap()
normals.byteswap()
indices.byteswap()
for offset in range(0, index_count - 2, 3):
if masks and masks[offset // 3]:
continue
ids = indices[offset:offset + 3]
if max(ids) >= surface_count:
continue
verts = tuple(tuple(float(positions[index * 3 + axis]) for axis in range(3))
for index in ids)
edge_a = tuple(verts[1][axis] - verts[0][axis] for axis in range(3))
edge_b = tuple(verts[2][axis] - verts[0][axis] for axis in range(3))
cross = (edge_a[1] * edge_b[2] - edge_a[2] * edge_b[1],
edge_a[2] * edge_b[0] - edge_a[0] * edge_b[2],
edge_a[0] * edge_b[1] - edge_a[1] * edge_b[0])
cross_length = math.sqrt(dot(cross, cross))
if cross_length <= 1.0e-12:
continue
face_normal = tuple(value / cross_length for value in cross)
area = cross_length * 0.5
smooth_normal = normalize(tuple(
sum(float(normals[index * 3 + axis]) for index in ids) for axis in range(3)))
if dot(face_normal, smooth_normal) < 0.0:
face_normal = tuple(-value for value in face_normal)
edge_squares = 0.0
for a, b in ((verts[0], verts[1]), (verts[1], verts[2]),
(verts[2], verts[0])):
edge_squares += sum((a[i] - b[i]) ** 2 for i in range(3))
quality = 4.0 * math.sqrt(3.0) * area / max(edge_squares, 1.0e-15)
triangles.append(Triangle(verts, area, face_normal, smooth_normal, quality))
if stream.read(1):
raise ValueError(f"{path}: trailing bytes after mesh tiles")
if not triangles:
raise ValueError(f"{path}: no LOD0 surface triangles")
return triangles, voxel_um, selected_tiles, lod_counts
def bezier(points, t):
one = 1.0 - t
weights = (one ** 3, 3.0 * one * one * t, 3.0 * one * t * t, t ** 3)
return tuple(sum(weights[i] * points[i][axis] for i in range(4)) for axis in range(3))
def make_route(layout):
edges = layout.get("cavern_edges", [])
rooms = layout.get("cavern_rooms", [])
if len(edges) != 1 or len(rooms) != 1:
raise ValueError("cavern audit expects one resolved room and one edge in its layout")
edge = edges[0]
points = [[value / 1_000_000.0 for value in row]
for row in edge["control_points_um"]]
bore_radius = edge["bore_radius_um"] / 1_000_000.0
bore_height = edge["bore_height_um"] / 1_000_000.0
samples = [bezier(points, index / 160.0) for index in range(161)]
horizontal_s = [0.0]
for a, b in zip(samples, samples[1:]):
horizontal_s.append(horizontal_s[-1] + math.hypot(b[0] - a[0], b[1] - a[1]))
first_dx = samples[1][0] - samples[0][0]
first_dy = samples[1][1] - samples[0][1]
first_len = max(1.0e-9, math.hypot(first_dx, first_dy))
extension = (samples[0][0] - first_dx / first_len * OPEN_GROUND_EXTENSION_M,
samples[0][1] - first_dy / first_len * OPEN_GROUND_EXTENSION_M,
samples[0][2] - (samples[1][2] - samples[0][2]) / first_len
* OPEN_GROUND_EXTENSION_M)
route_points = [extension, *samples]
route_s = [-OPEN_GROUND_EXTENSION_M, *horizontal_s]
segments = []
for i, (a, b) in enumerate(zip(route_points, route_points[1:])):
segments.append((a, b, route_s[i], route_s[i + 1]))
room = rooms[0]
room_center = tuple(value / 1_000_000.0 for value in room["center_um"])
room_half = tuple(value / 1_000_000.0 for value in room["half_extents_um"])
room_floor = room["floor_um"] / 1_000_000.0
return {
"segments": segments,
"edge": edge,
"bore_radius": bore_radius,
"bore_height": bore_height,
"room_center": room_center,
"room_half": room_half,
"room_floor": room_floor,
"route_points": samples,
"route_length": horizontal_s[-1],
}
def route_nearest(point, route):
best = None
for a, b, s0, s1 in route["segments"]:
dx, dy = b[0] - a[0], b[1] - a[1]
denominator = dx * dx + dy * dy
t = 0.0 if denominator <= 1.0e-12 else max(0.0, min(1.0,
((point[0] - a[0]) * dx + (point[1] - a[1]) * dy) / denominator))
x, y = a[0] + dx * t, a[1] + dy * t
distance = math.hypot(point[0] - x, point[1] - y)
if best is None or distance < best[0]:
segment_length = math.sqrt(max(denominator, 1.0e-12))
lateral = (dx * (point[1] - y) - dy * (point[0] - x)) / segment_length
best = (distance, s0 + (s1 - s0) * t,
a[2] + (b[2] - a[2]) * t, lateral)
return best
def mark_region(triangles, route):
room_c = route["room_center"]
room_h = route["room_half"]
radius = route["bore_radius"] + CORRIDOR_MARGIN_M
for tri in triangles:
x, y, z = tri.center
distance, distance_s, floor_z, lateral = route_nearest(tri.center, route)
tri.route_distance = distance
tri.route_lateral = lateral
tri.route_s = distance_s
tri.floor_z = floor_z
corridor = (-OPEN_GROUND_EXTENSION_M <= distance_s <= route["route_length"]
and distance <= radius
and floor_z - CORRIDOR_MARGIN_M <= z
<= floor_z + route["bore_height"] + CORRIDOR_MARGIN_M)
room_box = (abs(x - room_c[0]) <= room_h[0] + CORRIDOR_MARGIN_M
and abs(y - room_c[1]) <= room_h[1] + CORRIDOR_MARGIN_M
and route["room_floor"] - CORRIDOR_MARGIN_M <= z
<= route["room_floor"] + 2.0 * room_h[2] + CORRIDOR_MARGIN_M)
tri.in_region = corridor or room_box
outside_bore = (distance > route["bore_radius"] + 0.45
or abs(x - room_c[0]) > room_h[0] + 0.45
or abs(y - room_c[1]) > room_h[1] + 0.45)
# Keep the reference on native, ground-facing terrain. The outer mesh
# also contains steep cavern walls and room edges, which are features.
tri.reference = tri.in_region and outside_bore and tri.smooth[2] >= 0.45
def vertex_key(point):
return tuple(int(round(value / WELD_M)) for value in point)
def make_edges(triangles, indices):
edge_map = {}
for index in indices:
tri = triangles[index]
points = tri.verts
for a, b in ((0, 1), (1, 2), (2, 0)):
ka, kb = vertex_key(points[a]), vertex_key(points[b])
if ka == kb:
continue
key = (ka, kb) if ka < kb else (kb, ka)
delta = tuple(points[a][axis] - points[b][axis] for axis in range(3))
length = math.sqrt(dot(delta, delta))
midpoint = tuple((points[a][axis] + points[b][axis]) * 0.5 for axis in range(3))
edge_map.setdefault(key, []).append((index, length, midpoint))
pairs = []
for incident in edge_map.values():
unique = {}
for index, length, midpoint in incident:
unique[index] = (length, midpoint)
incident = list(unique.items())
if len(incident) < 2:
continue
if len(incident) > 4:
incident = incident[:4]
for left in range(len(incident)):
for right in range(left + 1, len(incident)):
i, (length, midpoint) = incident[left]
j, _ = incident[right]
angle = max(angle_degrees(triangles[i].normal, triangles[j].normal),
0.75 * angle_degrees(triangles[i].smooth, triangles[j].smooth))
pairs.append((i, j, angle, length, midpoint))
return pairs
def cell_of(point):
return tuple(math.floor(value / CELL_M) for value in point)
def score_surface(triangles, considered, pairs, edge_cutoff, rough_cutoff,
ref_cell_density):
considered_set = set(considered)
variation = {index: 0.0 for index in considered}
max_jump = {index: 0.0 for index in considered}
severe_count = {index: 0.0 for index in considered}
for i, j, angle, length, _midpoint in pairs:
if i not in considered_set or j not in considered_set:
continue
weighted = angle * length * 0.5
variation[i] += weighted
variation[j] += weighted
max_jump[i] = max(max_jump[i], angle)
max_jump[j] = max(max_jump[j], angle)
if angle >= edge_cutoff:
severe_count[i] += 0.5
severe_count[j] += 0.5
cells = {}
for index in considered:
tri = triangles[index]
key = cell_of(tri.center)
cell = cells.setdefault(key, {"area": 0.0, "variation": 0.0, "severe": 0.0,
"sliver_area": 0.0, "slivers": 0, "triangles": 0, "max_jump": 0.0,
"reference_area": 0.0, "reference_variation": 0.0,
"reference_severe": 0.0, "reference_sliver_area": 0.0,
"reference_slivers": 0})
cell["area"] += tri.area
cell["variation"] += variation[index]
cell["severe"] += severe_count[index]
cell["triangles"] += 1
cell["max_jump"] = max(cell["max_jump"], max_jump[index])
sliver = tri.quality < 0.07 and tri.area < 0.12
if sliver:
cell["sliver_area"] += tri.area
cell["slivers"] += 1
if tri.reference:
cell["reference_area"] += tri.area
cell["reference_variation"] += variation[index]
cell["reference_severe"] += severe_count[index]
if sliver:
cell["reference_sliver_area"] += tri.area
cell["reference_slivers"] += 1
ref_rough = []
ref_density = []
ref_sliver_fraction = []
for cell in cells.values():
if cell["reference_area"] >= max(0.15, cell["area"] * 0.65):
ref_rough.append(cell["reference_variation"] / max(cell["reference_area"], 1.0e-9))
ref_density.append(cell["reference_severe"] / max(cell["reference_area"], 1.0e-9))
ref_sliver_fraction.append(cell["reference_sliver_area"]
/ max(cell["reference_area"], 1.0e-9))
reference_median = quantile(ref_rough, 0.5)
reference_mad = quantile([abs(value - reference_median) for value in ref_rough], 0.5)
cell_rough_limit = max(10.0, quantile(ref_rough, 0.995) * 1.5,
reference_median + 8.0 * reference_mad)
edge_density_limit = max(1.5, quantile(ref_density, 0.995) * 1.7,
ref_cell_density * 1.7)
sliver_limit = max(0.22, quantile(ref_sliver_fraction, 0.995) + 0.18)
hot = set()
for key, cell in cells.items():
area = max(cell["area"], 1.0e-9)
roughness = cell["variation"] / area
edge_density = cell["severe"] / area
sliver_fraction = cell["sliver_area"] / area
rough_cluster = roughness > cell_rough_limit and cell["severe"] >= 2.0
edge_cluster = cell["severe"] >= 4.0 and edge_density > edge_density_limit
sliver_cluster = cell["slivers"] >= 5 and sliver_fraction > sliver_limit
if rough_cluster or edge_cluster or sliver_cluster:
hot.add(key)
cell["roughness_deg_per_m"] = roughness
cell["severe_edge_density_m2"] = edge_density
cell["sliver_area_fraction"] = sliver_fraction
cell["reasons"] = [name for name, active in (("normal-variation", rough_cluster),
("edge-density", edge_cluster), ("slivers", sliver_cluster)) if active]
clusters = []
flagged_cells = set()
remaining = set(hot)
while remaining:
stack = [remaining.pop()]
members = []
while stack:
current = stack.pop()
members.append(current)
x, y, z = current
neighbors = [(x + dx, y + dy, z + dz)
for dx in (-1, 0, 1) for dy in (-1, 0, 1) for dz in (-1, 0, 1)
if dx or dy or dz]
for neighbor in neighbors:
if neighbor in remaining:
remaining.remove(neighbor)
stack.append(neighbor)
cells_in_cluster = [cells[key] for key in members]
cluster_area = sum(cell["area"] for cell in cells_in_cluster)
cluster_severe = sum(cell["severe"] for cell in cells_in_cluster)
cluster_slivers = sum(cell["slivers"] for cell in cells_in_cluster)
accepted = len(members) >= 2 and cluster_area >= 0.25 and \
(cluster_severe >= 8.0 or cluster_slivers >= 12)
if accepted:
flagged_cells.update(members)
clusters.append({"cells": len(members), "area_m2": cluster_area,
"severe_edges": cluster_severe, "sliver_triangles": cluster_slivers,
"bounds_m": [[min(key[i] for key in members) * CELL_M,
(max(key[i] for key in members) + 1) * CELL_M]
for i in range(3)],
"reasons": sorted({reason for cell in cells_in_cluster
for reason in cell["reasons"]})})
clusters.sort(key=lambda cluster: (cluster["severe_edges"], cluster["area_m2"]), reverse=True)
flagged_triangles = [index for index in considered
if cell_of(triangles[index].center) in flagged_cells]
return {"cells": cells, "hot_cells": hot, "flagged_cells": flagged_cells,
"clusters": clusters, "hot_triangles": flagged_triangles,
"thresholds": {"edge_angle_deg": edge_cutoff,
"roughness_deg_per_m": cell_rough_limit,
"severe_edge_density_m2": edge_density_limit,
"sliver_area_fraction": sliver_limit,
"reference_cell_roughness_p995_deg_per_m": quantile(ref_rough, 0.995),
"reference_cell_roughness_median_deg_per_m": reference_median}}
def floor_profile_audit(triangles, considered, region_indices, route):
"""Measure the whole floor patch as area-weighted bins, not selected rails."""
if route is None:
return {"present": False, "detected": False, "triangles": 0, "bins": 0,
"lateral_bands": 0, "ripple_amplitude_m": 0.0,
"max_adjacent_step_m": 0.0}
along_step = 0.25
lateral_step = 0.5
bins = {}
floor_triangles = 0
wall_triangles = 0
wall_bins = set()
for index in region_indices:
tri = triangles[index]
height = tri.center[2] - tri.floor_z
if (0.0 <= tri.route_s <= 10.0
and route["bore_radius"] - 0.9 <= tri.route_distance
<= route["bore_radius"] + 1.0
and 0.45 <= height <= route["bore_height"] + 0.5
and abs(tri.smooth[2]) <= 0.45):
wall_triangles += 1
wall_bins.add(int(tri.route_s))
for index in considered:
tri = triangles[index]
if (tri.route_s < 0.0 or tri.route_s > min(10.0, route["route_length"])
or tri.route_distance > min(1.9, route["bore_radius"] - 0.1)
or abs(tri.center[2] - tri.floor_z) > 0.60
or tri.smooth[2] < 0.55):
continue
floor_triangles += 1
along_bin = int(math.floor(tri.route_s / along_step))
lateral_bin = int(math.floor((tri.route_lateral + 2.0) / lateral_step))
key = (lateral_bin, along_bin)
row = bins.setdefault(key, [0.0, 0.0, 0])
residual = tri.center[2] - tri.floor_z
row[0] += residual * tri.area
row[1] += tri.area
row[2] += 1
by_lateral = {}
for (lateral_bin, along_bin), (weighted_residual, area, count) in bins.items():
if area < 0.002:
continue
by_lateral.setdefault(lateral_bin, {})[along_bin] = weighted_residual / area
amplitudes = []
steps = []
valid_bins = 0
for values in by_lateral.values():
ordered = sorted(values.items())
run = []
runs = []
for item in ordered:
if run and item[0] != run[-1][0] + 1:
runs.append(run)
run = []
run.append(item)
if run:
runs.append(run)
for segment in runs:
if len(segment) < 8:
continue
valid_bins += len(segment)
raw = [value for _, value in segment]
highpass = []
for position, value in enumerate(raw):
nearby = raw[max(0, position - 4):min(len(raw), position + 5)]
highpass.append(value - sum(nearby) / len(nearby))
amplitudes.append(max(highpass) - min(highpass))
steps.extend(abs(raw[i] - raw[i - 1]) for i in range(1, len(raw)))
max_ripple = max(amplitudes, default=0.0) * 0.5
max_step = max(steps, default=0.0)
present = (floor_triangles >= 24 and valid_bins >= 8
and wall_triangles >= 24 and len(wall_bins) >= 3)
detected = present and (max_ripple > 0.015 or max_step > 0.050)
return {"present": present, "detected": detected, "triangles": floor_triangles,
"bins": valid_bins, "lateral_bands": len(by_lateral),
"wall_triangles": wall_triangles, "wall_bins": len(wall_bins),
"ripple_amplitude_m": max_ripple, "max_adjacent_step_m": max_step,
"thresholds": {"ripple_amplitude_m": 0.015, "adjacent_step_m": 0.050}}
def wall_band_measure(pairs, route, edge_cutoff):
"""Locate high dihedrals around the bore-foot and the L47 0.25 m band edge."""
if route is None:
return {}
bins = {}
radius = route["bore_radius"]
for _i, _j, angle, _length, midpoint in pairs:
distance, route_s, floor_z, _lateral = route_nearest(midpoint, route)
height = midpoint[2] - floor_z
if (0.0 <= route_s <= 10.0 and abs(distance - radius) <= 0.75
and -0.25 <= height <= 0.875):
index = int(math.floor((height + 0.25) / 0.125))
entry = bins.setdefault(index, [])
entry.append(angle)
result = {}
for index, angles in sorted(bins.items()):
center_height = (index + 0.5) * 0.125 - 0.25
result[f"{center_height:.3f}"] = {
"edges": len(angles),
"over_reference_cutoff": sum(value >= edge_cutoff for value in angles),
"p95_dihedral_deg": quantile(angles, 0.95),
"max_dihedral_deg": max(angles, default=0.0),
}
return result
def radial_wall_measure(pairs, route, edge_cutoff):
"""Measure dihedrals across the hard bore-radius gate at the entrance foot."""
if route is None:
return {}
bins = {}
radius = route["bore_radius"]
for _i, _j, angle, _length, midpoint in pairs:
distance, route_s, floor_z, _lateral = route_nearest(midpoint, route)
height = midpoint[2] - floor_z
if (-5.0 <= route_s <= 3.0 and -1.0 <= height <= 0.75
and abs(distance - radius) <= 1.0):
index = int(math.floor((distance - (radius - 1.0)) / 0.125))
bins.setdefault(index, []).append(angle)
return {f"{radius - 1.0 + (index + 0.5) * 0.125:.3f}": {
"edges": len(angles),
"over_reference_cutoff": sum(value >= edge_cutoff for value in angles),
"p95_dihedral_deg": quantile(angles, 0.95),
"max_dihedral_deg": max(angles, default=0.0),
} for index, angles in sorted(bins.items())}
def svg_for(path, triangles, route, hot_triangles, label):
selected = [triangles[index] for index in hot_triangles]
all_region = [tri for tri in triangles if tri.in_region]
points = [point for tri in all_region for point in tri.verts]
if not points:
return
xmin, xmax = min(p[0] for p in points), max(p[0] for p in points)
ymin, ymax = min(p[1] for p in points), max(p[1] for p in points)
width, height, margin = 1200.0, 900.0, 35.0
sx = (width - 2 * margin) / max(1.0e-6, xmax - xmin)
sy = (height - 2 * margin) / max(1.0e-6, ymax - ymin)
scale = min(sx, sy)
draw_w, draw_h = (xmax - xmin) * scale, (ymax - ymin) * scale
ox, oy = (width - draw_w) * 0.5, (height - draw_h) * 0.5
def project(point):
return ox + (point[0] - xmin) * scale, height - oy - (point[1] - ymin) * scale
lines = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{int(width)}" height="{int(height)}" viewBox="0 0 {int(width)} {int(height)}">',
'<rect width="100%" height="100%" fill="#fbfbfa"/>',
f'<text x="28" y="28" font-family="sans-serif" font-size="19" fill="#222">{label}: whole-surface defect clusters (XY plan)</text>',
'<g fill="#c8ced2" fill-opacity="0.24" stroke="#8b959b" stroke-opacity="0.12" stroke-width="0.3">']
for tri in all_region:
coords = [project(p) for p in tri.verts]
lines.append('<polygon points="' + " ".join(f"{x:.1f},{y:.1f}" for x, y in coords)
+ '"/>')
lines.append('</g><g fill="#d7191c" fill-opacity="0.78" stroke="#7f0000" stroke-width="0.4">')
for tri in selected:
coords = [project(p) for p in tri.verts]
lines.append('<polygon points="' + " ".join(f"{x:.1f},{y:.1f}" for x, y in coords)
+ '"/>')
route_coords = [project(p) for p in route["route_points"]]
lines.append('</g><polyline points="' + " ".join(f"{x:.1f},{y:.1f}" for x, y in route_coords)
+ '" fill="none" stroke="#1769aa" stroke-width="2"/>')
lines.append(f'<text x="28" y="{int(height - 24)}" font-family="sans-serif" font-size="15" fill="#333">Blue: intended route · Red: flagged triangle cells · Gray: remaining ROI triangles · flagged triangles: {len(selected)}</text>')
lines.append('</svg>')
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
def audit_case(label, mesh_path, layout_path, expect, focus, svg_dir):
triangles, voxel_um, tile_count, lod_counts = read_mesh(mesh_path)
route = None
if layout_path != Path("-"):
layout = json.loads(layout_path.read_text(encoding="utf-8"))
route = make_route(layout)
mark_region(triangles, route)
region_indices = [i for i, tri in enumerate(triangles) if tri.in_region]
else:
region_indices = list(range(len(triangles)))
for tri in triangles:
tri.in_region = True
tri.reference = True
if len(region_indices) < 24:
raise ValueError(f"{label}: only {len(region_indices)} triangles intersect the ROI")
reference_indices = [i for i in region_indices if triangles[i].reference]
if len(reference_indices) < 24:
reference_indices = region_indices
for index in region_indices:
triangles[index].reference = True
edge_pairs = make_edges(triangles, region_indices)
reference_edges = [edge[2] for edge in edge_pairs
if triangles[edge[0]].reference and triangles[edge[1]].reference]
edge_cutoff = max(35.0, quantile(reference_edges, 0.995))
reference_set = set(reference_indices)
ref_roughness = {index: 0.0 for index in reference_indices}
for i, j, angle, length, _midpoint in edge_pairs:
if i in reference_set and j in reference_set:
ref_roughness[i] += angle * length * 0.5
ref_roughness[j] += angle * length * 0.5
ref_values = [ref_roughness[index] / max(triangles[index].area, 1.0e-9)
for index in reference_indices]
rough_cutoff = 10.0
ref_density = 0.0
ref_area = sum(triangles[index].area for index in reference_indices)
ref_severe = 0.0
for i, j, angle, _length, _midpoint in edge_pairs:
if i in reference_set and j in reference_set and angle >= edge_cutoff:
ref_severe += 1.0
ref_density = ref_severe / max(ref_area, 1.0e-9)
if focus == "floor":
considered = [i for i in region_indices if route is not None
and triangles[i].route_s >= 1.5
and triangles[i].route_s <= 10.0
and triangles[i].route_distance <= 1.25
and abs(triangles[i].center[2] - triangles[i].floor_z) <= 0.24
and triangles[i].smooth[2] >= 0.55]
else:
considered = region_indices
if len(considered) < 24:
raise ValueError(f"{label}: focus={focus} has only {len(considered)} triangles")
scores = score_surface(triangles, considered, edge_pairs, edge_cutoff,
rough_cutoff, ref_density)
floor_profile = floor_profile_audit(triangles, considered, region_indices, route)
wall_band = wall_band_measure(edge_pairs, route, edge_cutoff)
radial_wall = radial_wall_measure(edge_pairs, route, edge_cutoff)
found = bool(scores["clusters"]) or floor_profile["detected"]
passed = expect == "measure" or (expect == "fail" and found) or (expect == "pass" and not found)
if svg_dir and route is not None:
svg_for(svg_dir / f"flagged-regions-{label}.svg", triangles, route,
scores["hot_triangles"], label)
cluster_summary = scores["clusters"][:8]
result = {
"label": label,
"mesh": str(mesh_path),
"layout": None if layout_path == Path("-") else str(layout_path),
"focus": focus,
"expected": expect,
"observed": "fail" if found else "pass",
"status": "PASS" if passed else "FAIL",
"voxel_um": voxel_um,
"lod0_tiles": tile_count,
"lod_tile_counts": lod_counts,
"triangles_loaded_lod0": len(triangles),
"triangles_in_roi": len(region_indices),
"triangles_audited": len(considered),
"adjacent_pairs": len(edge_pairs),
"reference_triangles": len(reference_indices),
"reference_edge_p995_deg": quantile(reference_edges, 0.995),
"reference_p995_normal_variation_deg_per_m": quantile(ref_values, 0.995),
"thresholds": scores["thresholds"],
"flagged_cells": len(scores["hot_cells"]),
"flagged_triangles": len(scores["hot_triangles"]),
"floor_profile": floor_profile,
"wall_band_height_histogram": wall_band,
"radial_wall_histogram": radial_wall,
"clusters": cluster_summary,
}
print(f"[WholeSurfaceAudit] label={label} focus={focus} lod0_tiles={tile_count} "
f"triangles_roi={len(region_indices)} audited={len(considered)} "
f"adjacent_pairs={len(edge_pairs)} reference={len(reference_indices)} "
f"edge_cutoff_deg={edge_cutoff:.2f} "
f"roughness_cutoff_deg_m={scores['thresholds']['roughness_deg_per_m']:.2f} "
f"hot_cells={len(scores['hot_cells'])} flagged_triangles={len(scores['hot_triangles'])} "
f"clusters={len(scores['clusters'])} observed={result['observed'].upper()} "
f"expected={expect.upper()} status={result['status']}")
print(f"[WholeSurfaceFloorProfile] label={label} triangles={floor_profile['triangles']} "
f"bins={floor_profile['bins']} lateral_bands={floor_profile['lateral_bands']} "
f"wall_triangles={floor_profile.get('wall_triangles', 0)} "
f"wall_bins={floor_profile.get('wall_bins', 0)} "
f"ripple_amplitude_m={floor_profile['ripple_amplitude_m']:.4f} "
f"max_adjacent_step_m={floor_profile['max_adjacent_step_m']:.4f} "
f"observed={'FAIL' if floor_profile['detected'] else ('PASS' if floor_profile['present'] else 'NOT_PRESENT')}")
if wall_band:
print(f"[WholeSurfaceWallBandMeasure] label={label} voxel_m={voxel_um / 1_000_000.0:.3f} "
f"recarve_edge_height_m=0.250 radial_band_m={route['bore_radius']:.3f} "
f"height_histogram={json.dumps(wall_band, sort_keys=True, separators=(',', ':'))}")
print(f"[WholeSurfaceRadialWallMeasure] label={label} route_s_m=-5..3 "
f"height_above_floor_m=-1..0.75 bore_radius_m={route['bore_radius']:.3f} "
f"radial_histogram={json.dumps(radial_wall, sort_keys=True, separators=(',', ':'))}")
for index, cluster in enumerate(cluster_summary):
print(f"[WholeSurfaceCluster] label={label} index={index} cells={cluster['cells']} "
f"area_m2={cluster['area_m2']:.3f} severe_edges={cluster['severe_edges']:.1f} "
f"slivers={cluster['sliver_triangles']} bounds_m={cluster['bounds_m']} "
f"reasons={','.join(cluster['reasons'])}")
hot_cells = scores["cells"]
for key in sorted(scores["hot_cells"], key=lambda item: (
hot_cells[item]["severe"], hot_cells[item]["variation"]), reverse=True)[:12]:
cell = hot_cells[key]
local = route_nearest(tuple((axis + 0.5) * CELL_M for axis in key), route) if route else None
print(f"[WholeSurfaceHotCell] label={label} cell_m=({key[0]},{key[1]},{key[2]}) "
f"route_s_m={local[1] if local else 0.0:.2f} radial_m={local[0] if local else 0.0:.2f} "
f"height_above_floor_m={(key[2] + 0.5 - local[2]) if local else 0.0:.2f} "
f"area_m2={cell['area']:.3f} roughness_deg_m={cell['roughness_deg_per_m']:.2f} "
f"severe_edges={cell['severe']:.1f} edge_density_m2={cell['severe_edge_density_m2']:.2f} "
f"slivers={cell['slivers']} max_jump_deg={cell['max_jump']:.2f} "
f"reasons={','.join(cell['reasons'])}")
return result
def main():
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)
args = parser.parse_args()
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")
results.append(audit_case(label, Path(mesh), Path(layout), expect, focus, args.svg_dir))
args.json.parent.mkdir(parents=True, exist_ok=True)
args.json.write_text(json.dumps({"audit": "whole-surface-l48-v1", "cases": results},
indent=2) + "\n", encoding="utf-8")
failures = [result for result in results if result["status"] != "PASS"]
print(f"[WholeSurfaceAuditSummary] 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())