Files
VoxelForge/Standalone/Research/WorldAssemblerM3/Linux47/MeasureEntranceL47.py
T
Fr0zkaandClaude Opus 5.5 c091e4434f fix(core): smooth tunnel entrance floor; verifier keeps baselines out of /tmp (L47); research R14
The demo tunnel's hard floor cap used a vertical Z - floorZ on a sloped Bezier floor, which is not a signed
distance, so marching cubes produced a staircase through the first 7 m (40-51 mm ripple, 82 mm steps). The
cap now uses the grade-normalised signed distance with a floor-adjacent bore re-application fading over the
entrance; mesh tile Z bounds include cavern bounds. New three-rail entrance audit: L46 fails, L47 passes.
verify: the cost baseline exports only build inputs into Saved/Linux/<out>/baseline-<commit> and removes it;
/tmp unchanged after a full tier. Demo off: owner hashes identical to L46. fast/medium/full PASS.
Known: sawtooth at the tunnel wall foot (next round). Research R14: Windows/MSVC readiness review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-24 12:54:39 +02:00

209 lines
9.5 KiB
Python

#!/usr/bin/env python3
"""Measure the L46 entrance floor from open terrain through its first ten metres."""
from __future__ import annotations
import importlib.util
import argparse
import json
import math
from pathlib import Path
import sys
ROOT = Path(__file__).resolve().parents[4]
PROFILE_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
spec = importlib.util.spec_from_file_location("ring_profile", PROFILE_PATH)
if spec is None or spec.loader is None:
raise RuntimeError(f"cannot load {PROFILE_PATH}")
PROFILE = importlib.util.module_from_spec(spec)
spec.loader.exec_module(PROFILE)
ARTIFACTS = ROOT / "Saved/Linux/l47"
PARSER = argparse.ArgumentParser(description=__doc__)
PARSER.add_argument("--layout", type=Path,
default=ROOT / "Saved/Linux/l46/layouts/landmark-reachability-clang.json")
PARSER.add_argument("--mesh", type=Path,
default=ROOT / "Saved/Linux/l46/landmark-l46.vfmesh")
PARSER.add_argument("--out", type=Path, default=ARTIFACTS / "entrance-profile-before.json")
PARSER.add_argument("--expect", choices=("pass", "fail", "measure"), default="measure")
PARSER.add_argument("--lod", default="0", help="LOD level, or 'all' to include every exported level")
OPTIONS = PARSER.parse_args()
LAYOUT = OPTIONS.layout.resolve()
MESH = OPTIONS.mesh.resolve()
OUT = OPTIONS.out.resolve()
layout = json.loads(LAYOUT.read_text(encoding="utf-8"))
edge = layout["cavern_edges"][0]
controls = [[value / 1_000_000.0 for value in point]
for point in edge["control_points_um"]]
def bezier(t: float) -> tuple[float, float, float]:
u = 1.0 - t
weights = (u ** 3, 3.0 * u * u * t, 3.0 * u * t * t, t ** 3)
return tuple(sum(weights[i] * controls[i][axis] for i in range(4))
for axis in range(3))
route = [bezier(i / 4096.0) for i in range(4097)]
arc = [0.0]
for a, b in zip(route, route[1:]):
arc.append(arc[-1] + math.hypot(b[0] - a[0], b[1] - a[1]))
def route_at(distance: float) -> tuple[float, float, float]:
if distance < 0.0:
start = route[0]
dx = route[1][0] - start[0]
dy = route[1][1] - start[1]
dz = route[1][2] - start[2]
horizontal = math.hypot(dx, dy)
scale = distance / (arc[1] - arc[0])
return start[0] + dx * scale, start[1] + dy * scale, start[2] + dz * scale
index = min(len(arc) - 2, max(0, next(
(i for i, value in enumerate(arc) if value >= distance), len(arc) - 1) - 1))
span = arc[index + 1] - arc[index]
t = 0.0 if span <= 0.0 else (distance - arc[index]) / span
return tuple(route[index][axis] + (route[index + 1][axis] - route[index][axis]) * t
for axis in range(3))
entry = route[0]
tangent = route[1]
tx, ty = tangent[0] - entry[0], tangent[1] - entry[1]
length = math.hypot(tx, ty)
tx, ty = tx / length, ty / length
nx, ny = -ty, tx
requested_lod = None if OPTIONS.lod == "all" else int(OPTIONS.lod)
voxel_um, triangles, tile_count = PROFILE.read_scene(
str(MESH), requested_lod, entry[2] - 7.0, entry[2] + 5.0,
(entry[0] - 14.0, entry[0] + 14.0, entry[1] - 14.0, entry[1] + 14.0))
buckets = PROFILE.build_buckets(
triangles, True,
(entry[0] - 14.0, entry[0] + 14.0, entry[1] - 14.0, entry[1] + 14.0),
cell_size=1.0)
down_buckets = PROFILE.build_buckets(
triangles, False,
(entry[0] - 14.0, entry[0] + 14.0, entry[1] - 14.0, entry[1] + 14.0),
cell_size=1.0)
mesh_points = [vertex[0] for triangle in triangles for vertex in triangle[2]]
print(f"[EntranceProfileGeometry] entry_m=({entry[0]:.3f},{entry[1]:.3f},{entry[2]:.3f}) "
f"point10_m=({route_at(10.0)[0]:.3f},{route_at(10.0)[1]:.3f},"
f"{route_at(10.0)[2]:.3f}) mesh_xy=({min(p[0] for p in mesh_points):.3f},"
f"{max(p[0] for p in mesh_points):.3f},{min(p[1] for p in mesh_points):.3f},"
f"{max(p[1] for p in mesh_points):.3f}) mesh_z=({min(p[2] for p in mesh_points):.3f},"
f"{max(p[2] for p in mesh_points):.3f})")
for sample_distance in range(11):
sx, sy, expected = route_at(float(sample_distance))
bucket = buckets.get((math.floor(sx), math.floor(sy)), ())
hit = PROFILE.sample_surface(buckets, sx, sy, True, 1.0,
preferred_height=expected)
down_hit = PROFILE.sample_surface(down_buckets, sx, sy, False, 1.0,
preferred_height=expected)
print(f"[EntranceProfileBucket] distance_m={sample_distance:.1f} "
f"xy_m=({sx:.3f},{sy:.3f}) triangles={len(bucket)} "
f"hit_z_m={None if hit is None else round(hit[0], 4)} "
f"down_hit_z_m={None if down_hit is None else round(down_hit[0], 4)} "
f"expected_z_m={expected:.4f}")
rows = []
for rail, lateral in (("left", -1.5), ("center", 0.0), ("right", 1.5)):
distance = -5.0
while distance <= 10.000001:
x, y, expected = route_at(distance)
x += nx * lateral
y += ny * lateral
hit = PROFILE.sample_surface(buckets, x, y, True, 1.0,
preferred_height=expected)
rows.append({
"rail": rail,
"distance_m": round(distance, 3),
"x_m": x,
"y_m": y,
"expected_floor_m": expected,
"surface_z_m": None if hit is None else hit[0],
"normal_z": None if hit is None else hit[1][2],
"residual_m": None if hit is None else hit[0] - expected,
})
distance += 0.125
results = {}
for rail in ("left", "center", "right"):
profile = [row for row in rows if row["rail"] == rail and row["surface_z_m"] is not None
and 0.0 <= row["distance_m"] <= 10.0]
jumps = []
for previous, current in zip(profile, profile[1:]):
expected_delta = current["expected_floor_m"] - previous["expected_floor_m"]
residual_delta = current["residual_m"] - previous["residual_m"]
if abs(residual_delta) >= 0.025:
jumps.append({"from_m": previous["distance_m"], "to_m": current["distance_m"],
"delta_m": current["surface_z_m"] - previous["surface_z_m"],
"residual_delta_m": residual_delta,
"expected_delta_m": expected_delta})
residuals = [row["residual_m"] for row in profile]
ripple_residuals = []
for row in profile:
nearby = [other["residual_m"] for other in profile
if abs(other["distance_m"] - row["distance_m"]) <= 1.0]
ripple_residuals.append((row["distance_m"], row["residual_m"]
- sum(nearby) / max(1, len(nearby))))
amplitude, period = 0.0, 0.0
for candidate in (0.375 + index * 0.125 for index in range(30)):
cosine = sum(value * math.cos(2.0 * math.pi * distance / candidate)
for distance, value in ripple_residuals)
sine = sum(value * math.sin(2.0 * math.pi * distance / candidate)
for distance, value in ripple_residuals)
current_amplitude = 2.0 * math.hypot(cosine, sine) / max(1, len(ripple_residuals))
if current_amplitude > amplitude:
amplitude, period = current_amplitude, candidate
step_error = max((abs(item["residual_delta_m"]) for item in jumps), default=0.0)
coverage = len(profile) / 81.0
rail_passed = (coverage >= 0.85 and amplitude <= 0.015 and step_error <= 0.05)
results[rail] = {
"coverage": coverage,
"max_adjacent_step_m": max((abs(item["delta_m"]) for item in jumps), default=0.0),
"max_step_residual_m": step_error,
"ripple_amplitude_m": amplitude,
"ripple_period_m": period,
"passed": rail_passed,
"jumps_over_0_04m": jumps,
"residual_peak_to_peak_m": max(residuals) - min(residuals) if residuals else None,
}
payload = {
"layout": str(LAYOUT.relative_to(ROOT)),
"mesh": str(MESH.relative_to(ROOT)),
"voxel_um": voxel_um,
"selected_tiles": tile_count,
"sample_spacing_m": 0.125,
"distance_range_m": [-5.0, 10.0],
"lateral_offsets_m": {"left": -1.5, "center": 0.0, "right": 1.5},
"rails": results,
"samples": rows,
}
all_passed = all(result["passed"] for result in results.values())
expected_ok = (OPTIONS.expect == "measure"
or (OPTIONS.expect == "pass" and all_passed)
or (OPTIONS.expect == "fail" and not all_passed))
OUT.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
print(f"[EntranceProfileBefore] voxel_um={voxel_um} tiles={tile_count} "
f"lod={OPTIONS.lod} spacing_m=0.125 distance_m=-5.000..10.000 status=MEASURED")
for rail, summary in results.items():
print(f"[EntranceFloorRailAudit] rail={rail} coverage={summary['coverage']:.3f} "
f"ripple_amplitude_m={summary['ripple_amplitude_m']:.4f} "
f"ripple_period_m={summary['ripple_period_m']:.3f} "
f"max_step_residual_m={summary['max_step_residual_m']:.4f} "
f"status={'PASS' if summary['passed'] else 'FAIL'}")
for jump in summary["jumps_over_0_04m"]:
print(f"[EntranceProfileJump] rail={rail} from_m={jump['from_m']:.3f} "
f"to_m={jump['to_m']:.3f} delta_m={jump['delta_m']:+.4f} "
f"expected_delta_m={jump['expected_delta_m']:+.4f} "
f"residual_delta_m={jump['residual_delta_m']:+.4f}")
failed_rails = sum(not result["passed"] for result in results.values())
print(f"[EntranceFloorMeshAudit] samples_per_rail=81 distance_m=0.000..10.000 "
f"rails=3 failed_rails={failed_rails} observed={'PASS' if all_passed else 'FAIL'} "
f"expected={OPTIONS.expect.upper()} status={'PASS' if expected_ok else 'FAIL'}")
print(f"[EntranceProfileJSON] file={OUT} status={'PASS' if expected_ok else 'FAIL'}")
if not expected_ok:
raise SystemExit(1)