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
This commit is contained in:
1 parent
e016f6a20f
commit
c091e4434f
13 files changed
+839
-27
No files matched your search
@@ -0,0 +1,401 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Capture L47 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"[L47CaptureRun] 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("L47 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-l47.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)
|
||||
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-l47.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"[L47CaptureCamera] 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"[L47CaptureJPEG] 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"[L47CaptureShapeMetrics] file={metrics_path} status=PASS", flush=True)
|
||||
@@ -0,0 +1,208 @@
|
||||
#!/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)
|
||||
@@ -0,0 +1,38 @@
|
||||
# Linux47 report
|
||||
|
||||
## Entrance floor
|
||||
|
||||
The L46 mesh has a repeating staircase through the first 7 m: 39.9–51.4 mm ripple and 81.8–82.9 mm largest residual step. The cause was the final hard floor cap, applied after the mouth and terrain stamps, using vertical `Z-floorZ` on a sloped Bezier floor. That field is not a signed distance to the ramp, so Marching Cubes interpolated a distorted gradient; stamp fill beside the cap made the ridges visible. Voxel height quantisation was not the main cause.
|
||||
|
||||
The cap now uses the grade-normalised signed distance. The tunnel SDF is reapplied in a floor-adjacent band, with the wider correction fading over the first 10% of the entrance; the narrow floor correction continues along the ramp. This removes the ridges without changing the tunnel wall profile. Mesh tile Z bounds now include resolved cavern bounds too; without that, the exported ramp lost triangles past about 6.6 m.
|
||||
|
||||
The mesh audit samples 0–10 m at 0.125 m on three rails (−1.5, 0, +1.5 m), requiring ≥85% coverage, ≤15 mm ripple and ≤50 mm adjacent residual step. L46 fails all rails: coverage 0.63–0.68, ripple 39.9–51.4 mm, step 81.8–82.9 mm. L47 passes all rails: 100% coverage, ripple 0.4–1.2 mm, no measured residual step. The Core audit also passes 243 samples with 0.3 mm maximum step and 0.0 mm reported ripple. Buried L43 and segmented-ramp L45 controls still fail as expected.
|
||||
|
||||
## Verifier, hashes and cost
|
||||
|
||||
The cost baseline now exports only Core, Source, Compat, and the build/helper scripts to `Saved/Linux/l47/baseline-e016f6a20fcfac785c9870b3d1133fd5df47a6d4`; an exit handler removes the export. It does not copy the repository to `/tmp`. `/tmp` was **28K before** and **28K after** the full tier; the saved top-level-entry comparison is identical and the baseline export was removed.
|
||||
|
||||
Source stamp / source manifest: `68328b0c2a154e261eba94a067eb7d879f37ef987a54758275b4d14e3e9746d3`. Demo-off owner hashes match L46 exactly for both recipes, with W1=W12. Whole-view hashes (density/mesh/payload/topology): floating-islands `598e9daaa1744ccb / 3114e0f4f6dbf734 / c49c4e48bff74089 / cadd4edd10d4d8ef`; surface-world `7dc75e2c09302258 / 0cb34c40ceec4ce3 / 9d23e8c46b515cf1 / 2962c9464991eedd`. Demo-on determinism passed 30/30 cells.
|
||||
|
||||
Five-pair median cost: floating-islands 16.390→16.435 s (+0.27%); surface-world 1.722→1.717 s (−0.29%). The baseline build took 40.2 s.
|
||||
|
||||
## Tiers, captures and failed runs
|
||||
|
||||
Fast **PASS** (195.9 s), medium **PASS** (567.8 s), full **PASS** (1606.5 s); zero failed checks. All used source stamp above. Final captures: [ground entrance](captures/ground-entrance.jpg), [looking in](captures/entrance-looking-in.jpg), [overview](captures/overview-entrance-and-chamber.jpg), [inside to dais](captures/inside-to-dais.jpg), and [low entrance floor](captures/entrance-floor-low.jpg).
|
||||
|
||||
Exploration failures: the first entrance gate caught a 171 mm step; stamp-only re-carving restored about 40 mm ripple and a 100 mm step; full-width re-carving failed the wall-shape check at t=0.58. The final mouth-local fade passed the wall check at t=0.64. The first fast invocation also stopped because the executable had not yet been built; the rebuilt fast tier passed. L46/L43/L45 negative controls are intentional failures and passed their expected-fail checks. Work stayed within the 15-trial budget.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
Accepted, with one new defect logged. The stamp matches the final Clang and GCC builds (`68328b0c…`); demo-off hashes
|
||||
are identical to L46; `/tmp` is unchanged after the full tier (1.8 MB used). The entrance floor is smooth now, and
|
||||
the cause, a vertical cap distance on a sloped floor, was measured before the fix.
|
||||
|
||||
**New defect:** a row of **jagged sawtooth teeth along the wall foot,** where the floor-adjacent band correction meets
|
||||
the bore wall. It shows in `entrance-looking-in.jpg` and along the edge in the overview. The three floor rails (−1.5,
|
||||
0, +1.5 m) don't reach the 2.6 m wall foot. **This is the third time** a fix has moved an artifact to where the
|
||||
audit isn't looking (L46 inside-only, L47 rails only). L48 adds a whole-surface artifact audit.
|
||||
|
||||
R14: no likely MSVC or UHT blocker. The Unreal adapter does not resolve stamps or cavern graphs, so the L43–L47 demos
|
||||
are standalone-only until the adapter supplies those records. The legacy chunk callers agree with the micrometre
|
||||
layout only at the default 8 m width.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
Reference in new issue
Block a user