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())
@@ -0,0 +1,410 @@
#!/usr/bin/env python3
"""Capture L48 cavern entrance views and audit the exported floor profile."""
from __future__ import annotations
import argparse
import importlib.util
import json
import math
import os
from pathlib import Path
import subprocess
import sys
import time
ROOT = Path(__file__).resolve().parents[4]
VERIFY = ROOT / "Standalone/Tools/verify"
LINUX37 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux37"
sys.path.insert(0, str(VERIFY))
from runner import bounded_run, current_stamp, first_stamp
def load_module(name: str, path: Path):
spec = importlib.util.spec_from_file_location(name, path)
if spec is None or spec.loader is None:
raise RuntimeError(f"cannot import {path}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
PROFILE = load_module("l45_ring_profile", ROOT /
"Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py")
SHAPE = load_module("l45_shape_audit", LINUX37 / "AuditMouthShapeL37.py")
PARSER = argparse.ArgumentParser(description=__doc__)
PARSER.add_argument("--layout", type=Path, required=True)
PARSER.add_argument("--artifact-dir", type=Path, required=True)
PARSER.add_argument("--capture-dir", type=Path, required=True)
PARSER.add_argument("--data", type=Path, default=ROOT / "Standalone/data/owner_world.json")
OPTIONS = PARSER.parse_args()
ARTIFACT_DIR = OPTIONS.artifact_dir.resolve()
CAPTURE_DIR = OPTIONS.capture_dir.resolve()
LAYOUT_PATH = OPTIONS.layout.resolve()
ARTIFACT_DIR.mkdir(parents=True, exist_ok=True)
CAPTURE_DIR.mkdir(parents=True, exist_ok=True)
PNG_DIR = ARTIFACT_DIR / "capture-png"
PNG_DIR.mkdir(parents=True, exist_ok=True)
HEADLESS = Path(os.environ["VOXELFORGE_L37_HEADLESS"])
RENDERER = Path(os.environ["VOXELFORGE_L37_RENDERER"])
EXPECTED_STAMP = current_stamp()
def run_logged(name: str, command: list[str], *, capture: bool = False) -> str:
log = ARTIFACT_DIR / f"{name}.log"
started = time.monotonic()
with log.open("w", encoding="utf-8") as stream:
result = bounded_run(command, timeout=1800, stdout=stream)
elapsed = time.monotonic() - started
output = log.read_text(encoding="utf-8", errors="replace")
if result is None:
raise RuntimeError(f"{name} timed out; see {log}")
print(f"[L48CaptureRun] step={name} elapsed={elapsed:.1f}s "
f"exit={result.returncode} log={log}", flush=True)
if result.returncode != 0:
raise RuntimeError(f"{name} failed; see {log}")
if capture and (first_stamp(output) != EXPECTED_STAMP
or first_stamp(bounded_run(
[str(command[0]), "--print-source-stamp"], timeout=15).stdout or "")
!= EXPECTED_STAMP):
raise RuntimeError(f"{name} executable source stamp mismatch")
return output
def stamp_by_id(layout: dict, stable_id: str) -> dict:
for stamp in layout.get("stamps", []):
if stamp.get("stable_id") == stable_id:
return stamp
raise RuntimeError(f"missing stamp {stable_id}")
def metres(stamp: dict, key: str = "position_um") -> tuple[float, float, float]:
return tuple(value / 1_000_000.0 for value in stamp[key])
def camera_angles(camera, target):
dx, dy, dz = (target[index] - camera[index] for index in range(3))
return (math.degrees(math.atan2(dx, dy)),
math.degrees(math.atan2(dz, math.hypot(dx, dy))))
def fmt(values) -> str:
return ",".join(f"{value:.6f}" for value in values)
def camera_spec(camera, target):
yaw, pitch = camera_angles(camera, target)
return (*camera, yaw, pitch)
def edge_point(edge: dict, t: float) -> tuple[float, float, float]:
points = edge["control_points_um"]
one = 1.0 - t
weights = (one ** 3, 3 * one ** 2 * t, 3 * one * t ** 2, t ** 3)
return tuple(sum(weights[index] * points[index][axis]
for index in range(4)) / 1_000_000.0
for axis in range(3))
def cavern_features(layout: dict):
if (layout.get("cavern_graph_vocabulary_version") != 1
or len(layout.get("cavern_rooms", [])) != 1
or len(layout.get("cavern_edges", [])) != 1):
raise RuntimeError("L48 capture needs one resolved cavern room and edge")
room = layout["cavern_rooms"][0]
edge = layout["cavern_edges"][0]
return room, edge
layout = json.loads(LAYOUT_PATH.read_text(encoding="utf-8"))
room, edge = cavern_features(layout)
portal_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-mouth-approach-v1")
floor_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-floor")
dais_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-dais")
entrance = edge_point(edge, 0.0)
next_ramp = edge_point(edge, 0.08)
portal = metres(portal_stamp)
chamber = metres(room, "center_um")
floor = metres(floor_stamp)
floor_top = floor[2] + floor_stamp["half_extents_um"][2] / 1_000_000.0
dais = metres(dais_stamp)
dais_top = dais[2] + dais_stamp["half_extents_um"][2] / 1_000_000.0
ground_z = entrance[2]
direction_length = math.hypot(next_ramp[0] - entrance[0], next_ramp[1] - entrance[1])
direction = ((next_ramp[0] - entrance[0]) / direction_length,
(next_ramp[1] - entrance[1]) / direction_length)
outward_length = math.hypot(entrance[0] - chamber[0], entrance[1] - chamber[1])
outward = ((entrance[0] - chamber[0]) / outward_length,
(entrance[1] - chamber[1]) / outward_length)
mouth_normal = (-direction[1], direction[0])
mesh = ARTIFACT_DIR / "landmark-l48.vfmesh"
native_mesh = ARTIFACT_DIR / "control-plain-native.vfmesh"
export_anchor = edge_point(edge, 0.25)
export_camera = (export_anchor[0], export_anchor[1], export_anchor[2] + 1.4)
export_target_floor = edge_point(edge, 0.42)
export_target = (export_target_floor[0], export_target_floor[1],
export_target_floor[2] + 1.2)
run_logged("landmark-export", [str(HEADLESS), str(OPTIONS.data), "--workers=12",
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
"--force-generate", "--clip-radius=4", "--max-clip-level=0", "--max-level=0",
"--render-distance-chunks=0", "--recipe=surface-world", "--landmark-chamber-demo",
f"--camera={fmt(camera_spec(export_camera, export_target))}",
f"--mesh-export={mesh}", f"--resolved-layout-output={LAYOUT_PATH}"], capture=True)
run_logged("plain-native-export", [str(HEADLESS), str(OPTIONS.data), "--workers=12",
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
"--force-generate", "--clip-radius=4", "--max-clip-level=0", "--max-level=0",
"--render-distance-chunks=0", "--recipe=surface-world",
f"--camera={fmt(camera_spec(export_camera, export_target))}",
f"--mesh-export={native_mesh}",
f"--resolved-layout-output={ARTIFACT_DIR / 'control-plain-native-layout.json'}"],
capture=True)
layout = json.loads(LAYOUT_PATH.read_text(encoding="utf-8"))
room, edge = cavern_features(layout)
portal_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-mouth-approach-v1")
floor_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-floor")
dais_stamp = stamp_by_id(layout, "landmark/sw-arrival-chamber-v1-dais")
entrance = edge_point(edge, 0.0)
next_ramp = edge_point(edge, 0.08)
portal = metres(portal_stamp)
chamber = metres(room, "center_um")
floor = metres(floor_stamp)
floor_top = floor[2] + floor_stamp["half_extents_um"][2] / 1_000_000.0
dais = metres(dais_stamp)
dais_top = dais[2] + dais_stamp["half_extents_um"][2] / 1_000_000.0
ground_z = entrance[2]
direction_length = math.hypot(next_ramp[0] - entrance[0], next_ramp[1] - entrance[1])
direction = ((next_ramp[0] - entrance[0]) / direction_length,
(next_ramp[1] - entrance[1]) / direction_length)
outward_length = math.hypot(entrance[0] - chamber[0], entrance[1] - chamber[1])
outward = ((entrance[0] - chamber[0]) / outward_length,
(entrance[1] - chamber[1]) / outward_length)
if not mesh.is_file() or mesh.stat().st_size == 0:
raise RuntimeError(f"missing mesh export {mesh}")
entrance_audit = ROOT / "Standalone/Research/WorldAssemblerM3/Linux47/MeasureEntranceL47.py"
run_logged("entrance-floor-control-l46", [sys.executable, str(entrance_audit),
"--mesh", str(ROOT / "Saved/Linux/l46/landmark-l46.vfmesh"),
"--layout", str(ROOT / "Saved/Linux/l46/layouts/landmark-reachability-clang.json"),
"--out", str(ARTIFACT_DIR / "entrance-profile-control-l46.json"), "--expect=fail"])
run_logged("entrance-floor-audit", [sys.executable, str(entrance_audit),
"--mesh", str(mesh), "--layout", str(LAYOUT_PATH),
"--out", str(ARTIFACT_DIR / "entrance-profile-l48.json"), "--expect=pass"])
voxel_um, triangles, tile_count = PROFILE.read_scene(str(mesh), 0,
floor_top - 8.0, entrance[2] + 9.0,
(min(entrance[0], chamber[0]) - 20.0, max(entrance[0], chamber[0]) + 20.0,
min(entrance[1], chamber[1]) - 20.0, max(entrance[1], chamber[1]) + 20.0))
bounds = (min(entrance[0], chamber[0]) - 20.0, max(entrance[0], chamber[0]) + 20.0,
min(entrance[1], chamber[1]) - 20.0, max(entrance[1], chamber[1]) + 20.0)
buckets = PROFILE.build_buckets(triangles, True, bounds, cell_size=1.0)
ground_candidates = []
for ix in range(-20, 21):
for iy in range(-20, 21):
x, y = portal[0] + ix * 0.5, portal[1] + iy * 0.5
dx, dy = x - portal[0], y - portal[1]
distance = math.hypot(dx, dy)
if not 6.0 <= distance <= 10.0:
continue
hit = PROFILE.sample_surface(buckets, x, y, True, 1.0,
preferred_height=ground_z)
if hit is None or abs(hit[0] - ground_z) > 3.0 or hit[1][2] < 0.85:
continue
alignment = max(-1.0, min(1.0,
(dx * mouth_normal[0] + dy * mouth_normal[1]) / distance))
angle = math.acos(alignment)
score = abs(distance - 7.0) + angle * 4.0
ground_candidates.append((score, x, y, hit[0]))
if not ground_candidates:
raise RuntimeError("no walkable mesh ground lies 6 to 10 m from the cave mouth")
_, ground_x, ground_y, outside_ground = min(ground_candidates)
inside_floor = PROFILE.sample_surface(buckets, chamber[0], chamber[1], True, 1.0,
preferred_height=floor_top)
if inside_floor is None or abs(inside_floor[0] - floor_top) > 1.5:
raise RuntimeError(f"chamber floor mesh hit {inside_floor} does not match designed floor {floor_top:.3f} m")
floor_top = inside_floor[0]
ground_camera = (ground_x, ground_y, outside_ground + 1.6)
entry_ground = PROFILE.sample_surface(buckets, entrance[0], entrance[1], True, 1.0,
preferred_height=ground_z)
if entry_ground is None:
raise RuntimeError("cave mouth has no walkable ground surface in the exported mesh")
entry_camera = (entrance[0] + mouth_normal[0] * 3.8,
entrance[1] + mouth_normal[1] * 3.8, ground_z + 1.6)
entry_target_floor = edge_point(edge, 0.30)
entry_target = (entry_target_floor[0], entry_target_floor[1], entry_target_floor[2] + 1.4)
low_camera = (entrance[0] + outward[0] * 1.0,
entrance[1] + outward[1] * 1.0, ground_z + 0.65)
low_target_floor = edge_point(edge, 0.24)
low_target = (low_target_floor[0], low_target_floor[1], low_target_floor[2] + 0.15)
to_dais = (dais[0] - chamber[0], dais[1] - chamber[1])
to_dais_length = math.hypot(*to_dais)
opposite_dais = (-to_dais[0] / to_dais_length, -to_dais[1] / to_dais_length)
inside_x = chamber[0]
inside_y = chamber[1]
inside_floor = PROFILE.sample_surface(buckets, inside_x, inside_y, True, 1.0,
preferred_height=floor_top)
inside_eye_z = inside_floor[0] + 1.6
chamber_camera = (chamber[0] + opposite_dais[0] * 5.5,
chamber[1] + opposite_dais[1] * 5.5, inside_eye_z)
chamber_target = (dais[0], dais[1], dais_top - 0.25)
overview_camera = (entrance[0] + outward[0] * 18.0,
entrance[1] + outward[1] * 18.0, outside_ground + 15.0)
overview_target = ((entrance[0] + chamber[0]) * 0.5,
(entrance[1] + chamber[1]) * 0.5,
(ground_z + floor_top) * 0.5)
inward = (-outward[0], -outward[1])
clip_point = (entrance[0] + outward[0] * 0.5,
entrance[1] + outward[1] * 0.5)
overview_clip = (inward[0], inward[1], 0.0,
-(inward[0] * clip_point[0] + inward[1] * clip_point[1]))
views = (
("ground-entrance", ground_camera, (portal[0], portal[1], portal[2]),
(portal[0], portal[1], ground_z, 20.0), -509.0),
("entrance-looking-in", entry_camera, entry_target, (entrance[0], entrance[1], entrance[2], 16.0), -507.0),
("entrance-floor-low", low_camera, low_target,
(entrance[0], entrance[1], entrance[2], 12.0), ground_z),
("inside-to-dais", chamber_camera, chamber_target,
(chamber[0], chamber[1], chamber[2], 15.0), chamber[2]),
("overview-entrance-and-chamber", overview_camera, overview_target,
((entrance[0] + chamber[0]) * 0.5,
(entrance[1] + chamber[1]) * 0.5,
(ground_z + floor_top) * 0.5, 34.0), -509.0),
)
for name, camera, target, focus, height_center in views:
png = PNG_DIR / f"{name}.png"
jpg = CAPTURE_DIR / f"{name}.jpg"
yaw, pitch = camera_angles(camera, target)
fov = (80.0 if name == "overview-entrance-and-chamber" else
70.0 if name in ("inside-to-dais", "entrance-looking-in") else
58.0 if name == "entrance-floor-low" else 65.0
if name == "ground-entrance" else 60.0)
if name == "ground-entrance":
fov = 55.0
print(f"[L48CaptureCamera] view={name} camera_m=({fmt(camera)}) "
f"target_m=({fmt(target)}) yaw_deg={yaw:.3f} pitch_deg={pitch:.3f} "
f"fov_deg={fov:.1f} entrance_distance_m={math.dist(camera, portal):.3f}", flush=True)
render = [str(RENDERER), str(mesh), str(png), "--size=1600x900", f"--fov-deg={fov:.1f}",
f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}",
"--shade=readable", "--headlamp=on", "--height-tint=on",
"--light-direction=0.3,-0.4,0.85",
f"--height-center-m={(floor_top if name == 'inside-to-dais' else height_center):.6f}",
f"--height-range-m={4 if name == 'inside-to-dais' else 6 if name == 'entrance-floor-low' else 16}",
"--tile-skirts=off", "--tile-edges=off",
f"--focus-region={fmt(focus)}"]
if name == "overview-entrance-and-chamber":
render.append(f"--clip-plane={fmt(overview_clip)}")
run_logged(f"{name}-render", render, capture=True)
for quality in (4, 6, 8):
run_logged(f"{name}-jpeg-q{quality}", ["ffmpeg", "-nostdin", "-hide_banner",
"-loglevel", "error", "-y", "-i", str(png), "-frames:v", "1",
"-q:v", str(quality), str(jpg)])
if jpg.is_file() and 0 < jpg.stat().st_size <= 300_000:
break
if not jpg.is_file() or not 0 < jpg.stat().st_size <= 300_000:
raise RuntimeError(f"{jpg.name} exceeds the 300 KB capture limit")
quality_log = run_logged(f"{name}-quality", [sys.executable,
str(VERIFY / "capture_quality.py"), str(jpg)])
print(f"[L48CaptureJPEG] file={jpg} bytes={jpg.stat().st_size} "
f"quality_log={quality_log.strip().replace(chr(10), ' ')} status=PASS", flush=True)
def region_triangles(center, radius_xy, z_min, z_max):
selected = []
for triangle in triangles:
points = tuple(vertex[0] for vertex in triangle[2])
centroid = tuple(sum(point[axis] for point in points) / 3.0 for axis in range(3))
if (math.hypot(centroid[0] - center[0], centroid[1] - center[1]) <= radius_xy
and z_min <= centroid[2] <= z_max):
selected.append(triangle)
return selected
def tunnel_wall_triangles(edge, t, height_above_floor):
center = edge_point(edge, t)
before = edge_point(edge, max(0.0, t - 0.01))
after = edge_point(edge, min(1.0, t + 0.01))
tx, ty = after[0] - before[0], after[1] - before[1]
tangent_length = math.hypot(tx, ty)
tx, ty = tx / tangent_length, ty / tangent_length
expected_radius = edge["bore_radius_um"] / 1_000_000.0 * math.sqrt(
1.0 - (height_above_floor / (edge["bore_height_um"] / 1_000_000.0)) ** 2)
selected = []
for triangle in triangles:
points = tuple(vertex[0] for vertex in triangle[2])
centroid = tuple(sum(point[axis] for point in points) / 3.0 for axis in range(3))
dx, dy = centroid[0] - center[0], centroid[1] - center[1]
longitudinal = dx * tx + dy * ty
lateral = abs(-dy * tx + dx * ty)
vertical = centroid[2] - center[2]
if (abs(longitudinal) <= 1.5
and abs(lateral - expected_radius) <= 0.7
and abs(vertical - height_above_floor) <= 0.75):
selected.append(triangle)
print(f"[L47TunnelWallWindow] t={t:.3f} center_m=({center[0]:.3f},"
f"{center[1]:.3f},{center[2]:.3f}) expected_radius_m={expected_radius:.3f} "
f"longitudinal_half_width_m=1.500 vertical_half_width_m=0.750 "
f"triangles={len(selected)}", flush=True)
return selected
def audit_shape(name, region, center):
metrics = SHAPE.analyze_triangles(region, center[:2], center[2],
crop_to_center=False, include_sides=True)
passed = (metrics["triangles"] > 0
and metrics["planar_area"] < SHAPE.PLANE_LIMIT_M2
and metrics["straight"] <= SHAPE.STRAIGHT_LIMIT_M
and metrics["corners"] == 0)
print(f"[LandmarkBuiltShape] region={name} triangles={metrics['triangles']} "
f"planar_patch_m2={metrics['planar_area']:.4f} "
f"straight_run_m={metrics['straight']:.4f} sharp_corners={metrics['corners']} "
f"corner_turn_max_deg={metrics['corner_turn_max']:.2f} "
f"thresholds=({SHAPE.PLANE_LIMIT_M2:.2f}m2,{SHAPE.STRAIGHT_LIMIT_M:.2f}m,"
f"{SHAPE.CORNER_TURN_LIMIT_DEG:.0f}deg) status={'PASS' if passed else 'FAIL'}",
flush=True)
if not passed:
raise RuntimeError(f"L37 built-shape thresholds failed in {name}")
return metrics
entrance_metrics = audit_shape("cave-mouth", region_triangles(
portal, 5.0, ground_z + 0.8, ground_z + 4.5),
(portal[0], portal[1], ground_z))
tunnel_mid = edge_point(edge, 0.64)
tunnel_metrics = audit_shape("tunnel-wall-edge", tunnel_wall_triangles(
edge, 0.64, 1.2),
(tunnel_mid[0], tunnel_mid[1], tunnel_mid[2] + 1.2))
dais_surface = PROFILE.sample_surface(buckets, dais[0], dais[1], True, 1.0,
preferred_height=dais_top - 0.4)
if dais_surface is None:
raise RuntimeError("dais stamp has no upward-facing surface in the exported mesh")
dais_rise = dais_surface[0] - floor_top
print(f"[LandmarkDaisSurface] center_m=({dais[0]:.3f},{dais[1]:.3f}) "
f"floor_z_m={floor_top:.3f} top_z_m={dais_surface[0]:.3f} "
f"rise_m={dais_rise:.3f} minimum_m=1.000 "
f"status={'PASS' if dais_rise >= 1.0 else 'FAIL'}", flush=True)
if dais_rise < 1.0:
raise RuntimeError(f"dais rises only {dais_rise:.3f} m above the chamber floor")
dais_metrics = audit_shape("dais-platform", region_triangles(
dais, 1.7, floor_top + 0.6, dais_top + 0.4),
(dais[0], dais[1], dais_surface[0]))
metrics_path = ARTIFACT_DIR / "landmark-shape-metrics.json"
metrics_path.write_text(json.dumps({
"graph_vocabulary_version": layout["cavern_graph_vocabulary_version"],
"entry_m": entrance, "portal_m": portal, "entry_ground_m": ground_z,
"chamber_center_m": chamber, "chamber_floor_m": floor_top,
"tunnel_midpoint_m": tunnel_mid,
"dais_center_m": dais, "dais_top_m": dais_top,
"dais_surface_m": dais_surface[0], "dais_rise_m": dais_rise,
"l37_thresholds": {"planar_patch_m2_lt": SHAPE.PLANE_LIMIT_M2,
"straight_run_m_lte": SHAPE.STRAIGHT_LIMIT_M,
"corner_turn_deg_lt": SHAPE.CORNER_TURN_LIMIT_DEG},
"entrance_arch": entrance_metrics, "tunnel_wall": tunnel_metrics,
"dais_platform": dais_metrics,
"mesh_tiles": tile_count, "mesh_voxel_um": voxel_um,
}, indent=2) + "\n", encoding="utf-8")
print(f"[L48CaptureShapeMetrics] file={metrics_path} status=PASS", flush=True)
@@ -0,0 +1,60 @@
# Linux48 report
## Whole-surface audit
`AuditWholeSurfaceL48.py` scans every LOD0 triangle in the route corridor plus 3 m and the chamber box plus 3 m. It welds shared edges, compares geometric face normals, and scores area-weighted normal variation, high-angle edge density, maximum jumps and sliver area in 1 m cells. Connected hot cells become located clusters. A separate floor pass bins the full floor patch at 0.25 m along the route and 0.5 m across it, removes a local moving-mean trend, and checks ripple and adjacent steps. Thresholds use ground-facing native patches from the same export. JSON results and red-triangle XY maps are emitted.
The detector has no general quadric residual or periodic-spectrum metric yet; terraces outside its floor patch rely on the normal-cluster measures. The control results are:
| Mesh | Result | Evidence |
|---|---|---|
| L45 ramp | Expected fail | One normal cluster, 0.669 m² |
| L46 entrance | Expected fail | 84.3 mm floor ripple; 159.1 mm largest step |
| L47 wall-foot | Expected fail | One 5-cell cluster, 5.243 m², 42 severe edges; floor profile 34.5/57.6 mm ripple/step |
| Plain native | Pass | No clusters; floor profile not present |
| L47 smooth floor | Pass | 0.59 mm ripple; 1.15 mm largest step |
| L48 candidate | **Fail** | Two clusters, 7.095 m² total, 65.5 severe edges, 303 flagged triangles; floor profile 34.5/57.6 mm |
The L47 cluster is at approximately `x=[-5,0], y=[22,25], z=[-505,-504] m`. At 0.188 m above the floor its wall-band p95 dihedral is 54.6° with a 160.4° maximum; the selected floor rails did not reach this band. The complete control output is in `Saved/Linux/l48/whole-surface-audit.json`.
## Teeth measurement and attempted fix
The L47 measurements localize the teeth to the floor-cap/bore-wall junction. The post-stamp cap has a hard radial cutoff there, and the floor-adjacent re-carve also uses a non-SDF weighted blend. A re-carve fade alone left the teeth in the capture. The final trial replaced both cavern cap gates with a rounded max of the sloped-floor distance and radial bore distance, restoring the L47 re-carve weight. This reduced the apparent tooth height in the capture but did not clear the whole-surface gate; the final mesh has more flagged area and a second cluster. The hard cap edge is implicated, but the cause is not proven and this is not a validated fix.
## Hashes and cost
Clang and GCC builds used source stamp `05ea28750b2ee89aa61655c386c6d74a6841c4d071374cda03c73a89b5efb578`.
Demo-off whole-view hashes match L47 exactly at W1 and W12:
- floating-islands: density `598e9daaa1744ccb`, mesh `3114e0f4f6dbf734`, payload `c49c4e48bff74089`, topology `cadd4edd10d4d8ef`.
- surface-world: density `7dc75e2c09302258`, mesh `0cb34c40ceec4ce3`, payload `9d23e8c46b515cf1`, topology `2962c9464991eedd`.
Fast took 202.7 s. Medium took 637.6 s; the whole-surface stage took 161.8 s and failed on L48. Full and its five-pair cost comparison were not run because medium did not pass. All build and verification processes used `TMPDIR=Saved/Linux/l48/tmp`; no command wrote to `/tmp`.
## Tiers and captures
| Tier | Result |
|---|---|
| Fast | Pass, 202.7 s |
| Medium | Fail, 637.6 s: L48 whole-surface gate |
| Full | Not run: medium prerequisite failed |
The five captures are `ground-entrance.jpg`, `entrance-looking-in.jpg`, `overview-entrance-and-chamber.jpg`, `inside-to-dais.jpg`, and `entrance-floor-low.jpg` in `captures/`. Flag maps: `flagged-regions-L47-wall-foot-control.svg` and `flagged-regions-L48-final.svg` (red flagged triangles, gray ROI, blue route).
## Failed runs
- The first fast invocation had no built binary; after the Clang/GCC builds, fast passed.
- Bypassing the post-stamp floor cap failed entrance rails. Broad SmoothMin/cap-mask trials either closed the mouth or failed the built-shape check; one planar mouth patch measured 5.575 m².
- Radial re-carve fades at 0.5 m and 1 m preserved the rails but left visible teeth and failed the whole-surface audit. Changing the support inset had no mesh effect.
- The final rounded-cap trial passed the legacy rails and shape checks, but failed medium with the clusters above. The 15-trial budget is exhausted. No full tier, commit, push or stash was performed.
## Orchestrator review (Claude)
Split. **The whole-surface audit is the result of this round,** and it's committed here as a tool, not yet wired into
the tiers. All three historical defects fail it (L45 ramp, L46 entrance, L47 wall foot) and plain native terrain
passes. It also shows that L47's floor near the walls carries 34.5 mm ripple and a 57.6 mm step, which the three
rails never sampled. **The Core change (rounded cap) did not pass** and is parked with the tier wiring on
`wip/l48-surface-audit`. Wiring the gate now would turn medium red on HEAD, because the flagged demo still has the
teeth. `assembler/world` stays on L47 Core. L49 builds the floor and bore as one SDF, with a filleted floor/wall
corner, before stamps, instead of patching with a post-pass cap.
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

File diff suppressed because it is too large. Load diff

After

Width:  |  Height:  |  Size: 3.1 MiB

File diff suppressed because it is too large. Load diff

After

Width:  |  Height:  |  Size: 2.6 MiB

File diff suppressed because it is too large. Load diff

After

Width:  |  Height:  |  Size: 2.9 MiB

File diff suppressed because it is too large. Load diff

After

Width:  |  Height:  |  Size: 2.9 MiB

File diff suppressed because it is too large. Load diff

After

Width:  |  Height:  |  Size: 2.9 MiB

File diff suppressed because it is too large. Load diff

After

Width:  |  Height:  |  Size: 2.9 MiB

File diff suppressed because it is too large. Load diff

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

@@ -0,0 +1,52 @@
# R15 — Whole-surface artifact audit
## Key recommendations
1. Scan every LOD0 triangle in the exported region using three area-weighted metrics over overlapping surface patches: **detrended geometric residual**, **smoothed normal variation**, and **periodic-band coherence**. Group adjacent anomalous patches into clusters before failing the mesh.
2. Calibrate each scale against plain native terrain exported by the same mesher at 25 cm. Choose thresholds on held-out clean regions, then lock them. The gate should pass clean references and reject **each of the five pre-fix defect meshes**; if one slips through, improve the detector rather than tune the threshold around that mesh.
3. Add the offline check after mesh export in `verify.py`’s medium capture-and-audit stage. Report cluster coordinates and scores, plus a false-color mesh or marked capture. Full tier already reuses passed medium evidence; fast would need a small LOD0 export to include this gate.
## Metrics
For triangle \(f=(v_0,v_1,v_2)\), compute area \(A_f=\frac12\|(v_1-v_0)\times(v_2-v_0)\|\) and **geometric** face normal \(n_f\) from its positions. Use these normals for shape checks; stored smooth MC normals can hide or exaggerate geometric changes.
- **Detrended residual \(D_r\):** Fit a robust local quadric \(q_r\) over a geodesic neighborhood of radius \(r\), and measure the area-weighted RMS signed normal distance:
\[
D_r(P)=\sqrt{\frac{\sum_{v\in P}A_v\,\delta_r(v)^2}{\sum_{v\in P}A_v}},
\qquad \delta_r(v)=(v-q_r(v))\cdot n_r(v).
\]
Also retain the 95th percentile and largest adjacent residual jump in the patch. This measures relief and steps without assuming the surface is a floor or wall.
- **Smoothed normal variation \(G_r\):** For adjacent faces \(f,g\), let \(\theta_{fg}=\cos^{-1}(n_f\cdot n_g)\) and \(d_{fg}\) be their geodesic center distance. Average normals over radius \(r\) first, then compute an area-weighted RMS of \(\theta_{fg}/d_{fg}\). This catches clustered creases and teeth while reducing single-triangle MC facet noise.
- **Periodic coherence \(C_\lambda\):** Take a spectrum or autocorrelation of \(\delta_r\) across overlapping surface patches, in all local tangent directions. Score the strongest narrow-band energy relative to neighboring frequencies, and require phase coherence across adjacent patches. Search wavelengths from roughly two to 32 voxels (0.5–8 m); use multiple window sizes so the longest periods have enough support. This detects repeated ribs, washboard, and seams without relying on a chosen rail.
Use all three as a small, interpretable suite. A raw dihedral histogram loses location and mixes ordinary MC facets with real creases. Discrete Gaussian curvature \(K_i=(2\pi-\sum_j\theta_{ij})/A_i^*\) and cotangent-Laplacian mean curvature are useful visual diagnostics, but raw one-ring estimates can be noisy on irregular MC triangles; prefer multi-scale smoothing or quadric fits. Triangle quality, such as \(2r/R\) or minimum altitude-to-edge ratio, is a useful secondary mesh-health check, not a primary artifact detector: ridges and staircases need not contain slivers.
MC linearly interpolates grid-edge crossings; the output can miss undersampled details, contain poor-shaped facets, and fail to preserve sharp features. That makes universal angle, curvature, or triangle-quality cutoffs unreliable. [CGAL’s isosurfacing manual](https://cgal.github.io/9465/doc/Isosurfacing_3/index.html) and [Dietrich et al. on MC triangle quality](https://doi.org/10.1109/TVCG.2008.60) document these limitations. [MeshLab](https://github.com/cnr-isti-vclab/meshlab/blob/main/docs/dox/filter.dox) and [libigl](https://libigl.github.io/tutorial/) provide useful curvature and quality measures; libigl also documents quadric curvature fitting. The specific three-metric combination above is a recommendation inferred from those methods and VoxelForge’s regressions, not a published universal detector.
## Reference and threshold method
Build a reference set from native-only LOD0 meshes using the same 25 cm mesher, recipes, and export path. Match patches by surface orientation and broad shape class; compare patch scores, not individual triangle scores. Use a held-out clean set to set per-scale thresholds from the distribution of **per-export maximum cluster scores** (with block bootstrap or equivalent uncertainty), so scanning more surface does not silently increase false alarms.
Threshold a connected cluster of adjacent anomalous patches, with a minimum footprint calibrated to the smallest visible defect. Ignore isolated single-triangle excursions unless a separate topology or fragment check catches them. Lock thresholds before scoring candidate fixes. Require every pre-fix defect mesh to fail individually and clean native references to pass. Keep all five bad meshes as fixtures; the native holdout guards against thresholds that simply reject rough terrain.
## Defect-to-metric map
| Known defect | Primary detector | Evidence and expected signal |
|---|---|---|
| **L35 wall ribs** | \(C_\lambda + G_r\) | The old wall has measured periods near 4.75 m in normals and 7.44 m in radius. A coherent band should cluster over the wall area. [L35 report](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md:5) |
| **L36 floor washboard** | \(C_\lambda + D_r\) | The old floor repeats at 1.438 m and 7.250 m, with 2.25–2.48 cm amplitude. Whole-surface patches include the tread that the wall-only scan missed. [L36 report](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux36/REPORT.md:5) |
| **L45 segmented ramp** | \(C_\lambda + D_r\) | Segment seams create repeated surface residuals along the ramp. The L45 control already fails its floor-ripple gate at 17.9 mm against a 15 mm limit. [L45 report](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux45/REPORT.md:42) [L46 report](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux46/REPORT.md:15) |
| **L46 entrance staircase** | \(D_r\), including adjacent residual jump; \(G_r\) | The first 7 m measures 39.9–51.4 mm ripple and 81.8–82.9 mm residual steps. Detrending against the sloped floor separates the steps from the intended grade. [L47 report](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux47/REPORT.md:5) |
| **L47 wall-foot sawteeth** | \(G_r + D_r\), clustered | Scan all connected faces, including the wall foot. The existing three floor rails do not reach it. [L47 report](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux47/REPORT.md:31) |
This three-metric set should catch all five while passing smooth native terrain **if** the matched reference and control gate confirm the separation. The reports establish the defects and measured signals; they do not establish final mesh-wide thresholds.
## Implementation and reporting
The existing `.vfmesh` readers expose tile LOD, voxel size, positions, normals, indices, and masks. Extend the tile-streaming approach used by [the ring audit](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py) and [the tile scanner](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux32/ScanMeshSurfaceTilesL32.py): analyze the whole target region at LOD0, include all face orientations, and weld duplicate vertices at same-LOD tile boundaries. Keep the audit offline on exported geometry.
The natural insertion point is after `CaptureLandmarkL47.py` and passage mesh exports in [`run_captures_and_mesh_audits()`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Tools/verify/verify.py:651). That stage runs in medium and is reused by full; fast currently has no equivalent capture export. A fast-tier version would need a small targeted LOD0 export. [Voxel Tools’ smooth-terrain documentation](https://voxel-tools.readthedocs.io/en/latest/smooth_terrain/) notes that voxel LOD stitches can still show sharp steps, while [SAGA’s Terrain Ruggedness Index](https://saga-gis.sourceforge.io/saga_tool_doc/7.6.1/ta_morphometry_16.html) illustrates the value of neighborhood-scale terrain measures; its DEM assumptions do not cover arbitrary tunnel surfaces.
For each cluster, report world-space centroid, bounds, geodesic area and extent, triangle IDs, strongest metric, scale or wavelength, value, threshold, LOD, voxel size, and mesh/layout identity. Emit JSON plus a compact CSV, and a false-color mesh or marked image with cluster IDs and a reproducible camera pose. MeshLab’s face-quality coloring is one available review path.