#!/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)