The shared passage carve took the minimum of piecewise-linear capsules (scallops at joins) and added wall relief on regular 8 m / 4 m octaves, which read as evenly spaced ribs. Interior sweep joins are now Catmull-Rom smoothed and the relief uses irregular rotated 43/29/19 m scales; the natural wobble remains. New axial ripple audit (0.125 m radius samples, 0.5-8 m periods): beaded control and L34 geometry fail, all 24 passage rows pass. FI mouth tread apron tapered. 30/30 determinism, 16/16 invariants, ring audits LOD0-8 pass; cost within noise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX
124 lines
5.1 KiB
Python
124 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Measure axial normal ripple in a camera-matched tunnel mesh window."""
|
|
import importlib.util
|
|
import math
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[4]
|
|
RESEARCH = Path(__file__).resolve().parent
|
|
RING_AUDIT = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
|
SPEC = importlib.util.spec_from_file_location("ring_profile_l32", RING_AUDIT)
|
|
PROFILE = importlib.util.module_from_spec(SPEC)
|
|
SPEC.loader.exec_module(PROFILE)
|
|
CAMERA_LOG = ROOT / "Saved/Linux/l34/run-capture-standard-set-final-radius3.log"
|
|
CAMERA_RE = re.compile(
|
|
r"^\[L34CaptureCamera\] view=(\S+).*?camera_m=\(([^)]*)\) "
|
|
r"target_m=\(([^)]*)\)", re.M)
|
|
MAXIMUM_AXIAL_NORMAL_AMPLITUDE = 0.0255
|
|
|
|
|
|
def normalize(v):
|
|
length = math.sqrt(sum(x * x for x in v))
|
|
return tuple(x / length for x in v) if length > 1.0e-12 else (0.0, 0.0, 0.0)
|
|
|
|
|
|
def fit(samples, spacing=0.25):
|
|
if len(samples) < 32:
|
|
return 0.0, 0.0
|
|
best_period, best_amplitude = 0.0, 0.0
|
|
for index in range(8, 129):
|
|
period = index * 0.0625
|
|
if period > 8.0:
|
|
break
|
|
omega = 2.0 * math.pi / period
|
|
cc = ss = cs = yc = ys = 0.0
|
|
for i, value in enumerate(samples):
|
|
phase = omega * (i * spacing)
|
|
c, s = math.cos(phase), math.sin(phase)
|
|
cc += c * c
|
|
ss += s * s
|
|
cs += c * s
|
|
yc += value * c
|
|
ys += value * s
|
|
determinant = cc * ss - cs * cs
|
|
if determinant <= 1.0e-9:
|
|
continue
|
|
cosine = (yc * ss - ys * cs) / determinant
|
|
sine = (ys * cc - yc * cs) / determinant
|
|
amplitude = math.hypot(cosine, sine)
|
|
if amplitude > best_amplitude:
|
|
best_period, best_amplitude = period, amplitude
|
|
return best_period, best_amplitude
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) != 3:
|
|
raise SystemExit("usage: AuditTunnelMeshNormalsL35.py MESH L34_VIEW_NAME")
|
|
mesh = Path(sys.argv[1])
|
|
view_name = sys.argv[2]
|
|
cameras = {name: (tuple(map(float, camera.split(","))),
|
|
tuple(map(float, target.split(","))))
|
|
for name, camera, target in CAMERA_RE.findall(
|
|
CAMERA_LOG.read_text(encoding="utf-8", errors="replace"))}
|
|
if view_name not in cameras:
|
|
raise SystemExit(f"missing L34 camera: {view_name}")
|
|
camera, target = cameras[view_name]
|
|
axis = normalize(tuple(target[i] - camera[i] for i in range(3)))
|
|
down = (0.0, 0.0, -1.0)
|
|
axis_down = sum(axis[i] * down[i] for i in range(3))
|
|
floor_dir = normalize(tuple(down[i] - axis_down * axis[i] for i in range(3)))
|
|
x_min, x_max = min(camera[0], target[0]) - 8.0, max(camera[0], target[0]) + 8.0
|
|
y_min, y_max = min(camera[1], target[1]) - 8.0, max(camera[1], target[1]) + 8.0
|
|
z_min, z_max = min(camera[2], target[2]) - 8.0, max(camera[2], target[2]) + 8.0
|
|
voxel_um, triangles, tiles = PROFILE.read_scene(
|
|
str(mesh), None, z_min, z_max, (x_min, x_max, y_min, y_max))
|
|
spacing = 0.25
|
|
bin_count = 48
|
|
sums = [0.0] * bin_count
|
|
counts = [0] * bin_count
|
|
for _, _, verts in triangles:
|
|
centroid = tuple(sum(v[0][axis_index] for v in verts) / 3.0
|
|
for axis_index in range(3))
|
|
delta = tuple(centroid[i] - camera[i] for i in range(3))
|
|
along = sum(delta[i] * axis[i] for i in range(3))
|
|
if not 0.0 <= along < bin_count * spacing:
|
|
continue
|
|
radial = tuple(delta[i] - along * axis[i] for i in range(3))
|
|
radius = math.sqrt(sum(value * value for value in radial))
|
|
if not 0.5 <= radius <= 4.0:
|
|
continue
|
|
radial_unit = tuple(value / radius for value in radial)
|
|
if sum(radial_unit[i] * floor_dir[i] for i in range(3)) < 0.25:
|
|
continue
|
|
normal = normalize(tuple(sum(v[1][axis_index] for v in verts) / 3.0
|
|
for axis_index in range(3)))
|
|
axial = abs(sum(normal[i] * axis[i] for i in range(3)))
|
|
bin_index = int(along / spacing)
|
|
sums[bin_index] += axial
|
|
counts[bin_index] += 1
|
|
valid = [(i, sums[i] / counts[i]) for i in range(bin_count) if counts[i]]
|
|
if len(valid) < 32:
|
|
print(f"[TunnelMeshNormalAudit] mesh={mesh} view={view_name} tiles={tiles} "
|
|
f"bins={len(valid)}/{bin_count} status=INSUFFICIENT")
|
|
raise SystemExit(1)
|
|
values = [sums[i] / counts[i] if counts[i] else 0.0 for i in range(bin_count)]
|
|
mean = sum(value for _, value in valid) / len(valid)
|
|
values = [value - mean for value in values]
|
|
period, amplitude = fit(values, spacing)
|
|
passed = amplitude < MAXIMUM_AXIAL_NORMAL_AMPLITUDE
|
|
print(f"[TunnelMeshNormalAudit] mesh={mesh} view={view_name} voxel_um={voxel_um} "
|
|
f"tiles={tiles} bins={len(valid)}/{bin_count} floor_sector=0.25 "
|
|
f"period_m={period:.3f} axial_normal_amplitude={amplitude:.5f} "
|
|
f"axial_normal_amplitude_deg={math.degrees(math.asin(min(1.0, amplitude))):.3f} "
|
|
f"threshold={MAXIMUM_AXIAL_NORMAL_AMPLITUDE:.4f} "
|
|
f"status={'PASS' if passed else 'FAIL'}")
|
|
if not passed:
|
|
raise SystemExit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|