wip(assembler): round L37 -- FI mouth carve-fade fix, no raised bridge (style 0); research R1
The passage mouth fade multiplied the signed carve distance by zero, which turned native rock into an open surface (the reason earlier rounds stamped walk pads at mouths); it now blends between the uncarved field and the full signed carve. The FI style-0 route followed a lifted grade envelope over the crater (the "bridge"); it now runs on the rim and follows the native surface. New built-shape and crease audits with thresholds frozen before the fix (L36 and box controls fail). 24/24 passage rows, 30/30 determinism, 16/16 invariants, 18/18 interface/ring. Unreal: one recipe setter (uncompiled). Research R1 (read-only, web): ranked proposals for natural walkable mouths, floor-limited carve first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
No files matched your search
@@ -0,0 +1,535 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Measure player-scale planar aprons, straight boundaries and corners at passage mouths."""
|
||||
import argparse
|
||||
import importlib.util
|
||||
import math
|
||||
from collections import defaultdict, deque
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
PROFILE_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||
SPEC = importlib.util.spec_from_file_location("ring_profile_l37", PROFILE_PATH)
|
||||
PROFILE = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(PROFILE)
|
||||
|
||||
# Frozen from player scale before measuring the L37 candidate. The 0.75 m line limit
|
||||
# is just over the 0.68 m capsule width; 0.50 m2 is one capsule-width footprint.
|
||||
STRAIGHT_LIMIT_M = 0.75
|
||||
PLANE_LIMIT_M2 = 0.50
|
||||
CORNER_TURN_LIMIT_DEG = 30.0
|
||||
PLANE_TOLERANCE_M = 0.02
|
||||
NORMAL_BUCKET = 0.07
|
||||
PLANE_BUCKET_M = 0.03
|
||||
MOUTH_CORRIDOR_HALF_WIDTH_M = 1.50
|
||||
MOUTH_CORRIDOR_LENGTH_M = 12.0
|
||||
MOUTH_Z_BELOW_M = 4.0
|
||||
MOUTH_Z_ABOVE_M = 3.0
|
||||
PLAYER_FLOOR_OFFSET_M = 1.13
|
||||
DELTA_THRESHOLD_M = 0.05
|
||||
DELTA_GRID_M = 0.125
|
||||
SCENES = {
|
||||
"owner-fi": ("owner-fi-ground-mouth.vfmesh", "layout-owner-fi.json", "FloatingIslands", "upper"),
|
||||
"owner-sw": ("owner-sw-ground-mouth.vfmesh", "layout-owner-sw.json", "SurfaceWorld", "lower"),
|
||||
"upper-sw-fixture": ("upper-sw-fixture-ground-mouth.vfmesh",
|
||||
"layout-upper-sw-fixture.json", "SurfaceWorld", "upper"),
|
||||
}
|
||||
|
||||
|
||||
def sub(a, b):
|
||||
return tuple(a[i] - b[i] for i in range(3))
|
||||
|
||||
|
||||
def dot(a, b):
|
||||
return sum(a[i] * b[i] for i in range(3))
|
||||
|
||||
|
||||
def cross(a, b):
|
||||
return (a[1] * b[2] - a[2] * b[1],
|
||||
a[2] * b[0] - a[0] * b[2],
|
||||
a[0] * b[1] - a[1] * b[0])
|
||||
|
||||
|
||||
def length(a):
|
||||
return math.sqrt(dot(a, a))
|
||||
|
||||
|
||||
def face(points):
|
||||
n = cross(sub(points[1], points[0]), sub(points[2], points[0]))
|
||||
twice_area = length(n)
|
||||
if twice_area <= 1.0e-12:
|
||||
return None
|
||||
area = twice_area * 0.5
|
||||
n = tuple(v / twice_area for v in n)
|
||||
if n[2] < 0.0:
|
||||
n = tuple(-v for v in n)
|
||||
d = -dot(n, points[0])
|
||||
return n, d, area
|
||||
|
||||
|
||||
def vertex_key(point):
|
||||
return tuple(round(value, 4) for value in point)
|
||||
|
||||
|
||||
def edge_key(a, b):
|
||||
ka, kb = vertex_key(a), vertex_key(b)
|
||||
return (ka, kb) if ka < kb else (kb, ka)
|
||||
|
||||
|
||||
def rdp(points, epsilon):
|
||||
if len(points) <= 2:
|
||||
return points
|
||||
a, b = points[0], points[-1]
|
||||
ab = sub(b, a)
|
||||
ab2 = dot(ab, ab)
|
||||
maximum, index = -1.0, -1
|
||||
for i in range(1, len(points) - 1):
|
||||
ap = sub(points[i], a)
|
||||
t = max(0.0, min(1.0, dot(ap, ab) / ab2)) if ab2 else 0.0
|
||||
closest = tuple(a[j] + t * ab[j] for j in range(3))
|
||||
distance = length(sub(points[i], closest))
|
||||
if distance > maximum:
|
||||
maximum, index = distance, i
|
||||
if maximum <= epsilon:
|
||||
return [a, b]
|
||||
left = rdp(points[:index + 1], epsilon)
|
||||
right = rdp(points[index:], epsilon)
|
||||
return left[:-1] + right
|
||||
|
||||
|
||||
def simplified_boundary(component, records):
|
||||
counts = defaultdict(int)
|
||||
actual_edges = {}
|
||||
for index in component:
|
||||
points = records[index]["points"]
|
||||
for a, b in ((points[0], points[1]), (points[1], points[2]),
|
||||
(points[2], points[0])):
|
||||
key = edge_key(a, b)
|
||||
counts[key] += 1
|
||||
actual_edges[key] = (key[0], key[1])
|
||||
boundary = [edge for edge, count in counts.items() if count == 1]
|
||||
graph = defaultdict(list)
|
||||
for a, b in boundary:
|
||||
graph[a].append(b)
|
||||
graph[b].append(a)
|
||||
if not graph:
|
||||
return [], []
|
||||
|
||||
def direction(a, b):
|
||||
dx, dy = b[0] - a[0], b[1] - a[1]
|
||||
magnitude = math.hypot(dx, dy)
|
||||
return ((dx / magnitude, dy / magnitude), magnitude) if magnitude > 1.0e-8 else (None, 0.0)
|
||||
|
||||
def straight_run(origin, first):
|
||||
prior, current = origin, first
|
||||
initial, edge_length = direction(prior, current)
|
||||
if initial is None:
|
||||
return 0.0
|
||||
total = edge_length
|
||||
seen = {origin, first}
|
||||
while current in graph and len(graph[current]) == 2:
|
||||
choices = [value for value in graph[current] if value != prior]
|
||||
if len(choices) != 1 or choices[0] in seen:
|
||||
break
|
||||
nxt = choices[0]
|
||||
outgoing, edge_length = direction(current, nxt)
|
||||
if outgoing is None:
|
||||
break
|
||||
deflection = math.degrees(math.acos(max(-1.0, min(1.0,
|
||||
initial[0] * outgoing[0] + initial[1] * outgoing[1]))))
|
||||
if deflection > 8.0:
|
||||
break
|
||||
total += edge_length
|
||||
prior, current = current, nxt
|
||||
initial = outgoing
|
||||
seen.add(current)
|
||||
return total
|
||||
|
||||
longest = 0.0
|
||||
corners = []
|
||||
for vertex, neighbours in graph.items():
|
||||
if len(neighbours) == 2:
|
||||
incoming, _ = direction(neighbours[0], vertex)
|
||||
outgoing, _ = direction(vertex, neighbours[1])
|
||||
if incoming and outgoing:
|
||||
turn = math.degrees(math.acos(max(-1.0, min(1.0,
|
||||
incoming[0] * outgoing[0] + incoming[1] * outgoing[1]))))
|
||||
if turn >= CORNER_TURN_LIMIT_DEG:
|
||||
left_run = straight_run(vertex, neighbours[0])
|
||||
right_run = straight_run(vertex, neighbours[1])
|
||||
if left_run >= STRAIGHT_LIMIT_M and right_run >= STRAIGHT_LIMIT_M:
|
||||
corners.append(turn)
|
||||
for neighbour in neighbours:
|
||||
longest = max(longest, straight_run(vertex, neighbour))
|
||||
return [longest], corners
|
||||
|
||||
|
||||
def analyze_triangles(triangles, center, center_z, crop_to_center=True,
|
||||
include_sides=False):
|
||||
records = []
|
||||
for lod, average_nz, vertices in triangles:
|
||||
points = tuple(vertex[0] for vertex in vertices)
|
||||
centroid = tuple(sum(point[axis] for point in points) / 3.0 for axis in range(3))
|
||||
if (crop_to_center and
|
||||
(abs(centroid[0] - center[0]) > MOUTH_CORRIDOR_LENGTH_M + 2.0
|
||||
or abs(centroid[1] - center[1]) > MOUTH_CORRIDOR_LENGTH_M + 2.0
|
||||
or centroid[2] < center_z - MOUTH_Z_BELOW_M
|
||||
or centroid[2] > center_z + MOUTH_Z_ABOVE_M)):
|
||||
continue
|
||||
measured = face(points)
|
||||
if not measured:
|
||||
continue
|
||||
normal, plane_d, area = measured
|
||||
if not include_sides and (normal[2] < 0.35 or average_nz < 0.25):
|
||||
continue
|
||||
bucket = (round(normal[0] / NORMAL_BUCKET), round(normal[1] / NORMAL_BUCKET),
|
||||
round(normal[2] / NORMAL_BUCKET), round(plane_d / PLANE_BUCKET_M))
|
||||
records.append({"points": points, "centroid": centroid, "normal": normal,
|
||||
"d": plane_d, "area": area, "bucket": bucket})
|
||||
|
||||
by_bucket = defaultdict(list)
|
||||
for index, record in enumerate(records):
|
||||
by_bucket[record["bucket"]].append(index)
|
||||
edge_indices = defaultdict(list)
|
||||
for index, record in enumerate(records):
|
||||
points = record["points"]
|
||||
for a, b in ((points[0], points[1]), (points[1], points[2]),
|
||||
(points[2], points[0])):
|
||||
edge_indices[edge_key(a, b)].append(index)
|
||||
adjacency = defaultdict(list)
|
||||
for indices in edge_indices.values():
|
||||
if len(indices) == 2:
|
||||
a, b = indices
|
||||
# Adjacent triangles can differ by one normal bucket on a smooth mesh.
|
||||
# Plane membership is checked against the seed below, so let that test
|
||||
# decide whether a connected patch is planar instead of splitting it at
|
||||
# the bucket boundary.
|
||||
adjacency[a].append(b)
|
||||
adjacency[b].append(a)
|
||||
|
||||
max_area = 0.0
|
||||
max_straight = 0.0
|
||||
corner_count = 0
|
||||
max_corner_turn = 0.0
|
||||
max_bounds = None
|
||||
max_normal = None
|
||||
visited = set()
|
||||
for indices in by_bucket.values():
|
||||
for seed in indices:
|
||||
if seed in visited:
|
||||
continue
|
||||
component = []
|
||||
queue = deque([seed])
|
||||
queued = {seed}
|
||||
seed_record = records[seed]
|
||||
while queue:
|
||||
index = queue.popleft()
|
||||
record = records[index]
|
||||
if (abs(dot(seed_record["normal"], record["normal"])) < math.cos(math.radians(4.5))
|
||||
or max(abs(dot(seed_record["normal"], point) + seed_record["d"])
|
||||
for point in record["points"]) > PLANE_TOLERANCE_M):
|
||||
continue
|
||||
visited.add(index)
|
||||
component.append(index)
|
||||
for neighbour in adjacency[index]:
|
||||
if neighbour not in visited and neighbour not in queued:
|
||||
queued.add(neighbour)
|
||||
queue.append(neighbour)
|
||||
area = sum(records[index]["area"] for index in component)
|
||||
if area <= max_area:
|
||||
continue
|
||||
segments, corners = simplified_boundary(component, records)
|
||||
max_area = area
|
||||
max_straight = max(segments, default=0.0)
|
||||
corner_count = len(corners)
|
||||
max_corner_turn = max(corners, default=0.0)
|
||||
points = [point for index in component for point in records[index]["points"]]
|
||||
max_bounds = tuple((min(point[axis] for point in points),
|
||||
max(point[axis] for point in points)) for axis in range(3))
|
||||
max_normal = seed_record["normal"]
|
||||
return {"triangles": len(records), "planar_area": max_area,
|
||||
"straight": max_straight, "corners": corner_count,
|
||||
"corner_turn_max": max_corner_turn,
|
||||
"bounds": max_bounds, "normal": max_normal}
|
||||
|
||||
|
||||
def synthetic_box(center, z):
|
||||
x, y = center
|
||||
corners = ((x - 1.5, y - 1.0, z), (x + 1.5, y - 1.0, z),
|
||||
(x + 1.5, y + 1.0, z), (x - 1.5, y + 1.0, z))
|
||||
return [(0, 1.0, tuple((point, (0.0, 0.0, 1.0)) for point in tri))
|
||||
for tri in ((corners[0], corners[1], corners[2]),
|
||||
(corners[0], corners[2], corners[3]))]
|
||||
|
||||
|
||||
def fail(metrics, include_planarity=True):
|
||||
planar_fail = (include_planarity and
|
||||
(metrics["planar_area"] >= PLANE_LIMIT_M2
|
||||
or metrics.get("all_face_planar_area", 0.0) >= PLANE_LIMIT_M2))
|
||||
return (planar_fail
|
||||
or metrics["straight"] > STRAIGHT_LIMIT_M
|
||||
or metrics.get("all_face_straight", 0.0) > STRAIGHT_LIMIT_M
|
||||
or metrics["corners"] > 0
|
||||
or metrics.get("all_face_corners", 0) > 0)
|
||||
|
||||
|
||||
def mouth_route_points(passage, side, scale):
|
||||
controls = [
|
||||
(tuple(value * scale for value in point["position_q"]),
|
||||
point["radius_q"] * scale)
|
||||
for point in passage["control_points"]
|
||||
]
|
||||
if side == "upper":
|
||||
return controls[:4]
|
||||
lower_mouth = tuple(value * scale for value in passage["lower_mouth_q"])
|
||||
lower_index = min(range(len(controls)), key=lambda index: sum(
|
||||
(controls[index][0][axis] - lower_mouth[axis]) ** 2 for axis in range(3)))
|
||||
return list(reversed(controls[max(0, lower_index - 3):lower_index + 1]))
|
||||
|
||||
|
||||
def select_corridor_triangles(triangles, route, half_width, path_length):
|
||||
selected = []
|
||||
for triangle in triangles:
|
||||
vertices = triangle[2]
|
||||
centroid = tuple(sum(vertex[0][axis] for vertex in vertices) / 3.0
|
||||
for axis in range(3))
|
||||
closest_distance = math.inf
|
||||
closest_along = 0.0
|
||||
closest_floor = 0.0
|
||||
prefix = 0.0
|
||||
for (a, radius_a), (b, radius_b) in zip(route, route[1:]):
|
||||
dx, dy, dz = (b[axis] - a[axis] for axis in range(3))
|
||||
horizontal_length_squared = dx * dx + dy * dy
|
||||
horizontal_length = math.sqrt(horizontal_length_squared)
|
||||
if horizontal_length_squared <= 1.0e-12:
|
||||
t = 0.0
|
||||
else:
|
||||
t = max(0.0, min(1.0,
|
||||
((centroid[0] - a[0]) * dx + (centroid[1] - a[1]) * dy)
|
||||
/ horizontal_length_squared))
|
||||
center_x, center_y = a[0] + t * dx, a[1] + t * dy
|
||||
lateral = math.hypot(centroid[0] - center_x, centroid[1] - center_y)
|
||||
segment_length = math.sqrt(horizontal_length_squared + dz * dz)
|
||||
if lateral < closest_distance:
|
||||
closest_distance = lateral
|
||||
closest_along = prefix + t * segment_length
|
||||
center_z = a[2] + t * dz
|
||||
radius = radius_a + t * (radius_b - radius_a)
|
||||
closest_floor = center_z - radius + PLAYER_FLOOR_OFFSET_M
|
||||
prefix += segment_length
|
||||
if (closest_distance <= half_width
|
||||
and 0.0 <= closest_along <= path_length
|
||||
and abs(centroid[2] - closest_floor) <= 1.5):
|
||||
selected.append(triangle)
|
||||
return selected
|
||||
|
||||
|
||||
def route_floor_at(route, x, y):
|
||||
closest = None
|
||||
prefix = 0.0
|
||||
for (a, radius_a), (b, radius_b) in zip(route, route[1:]):
|
||||
dx, dy, dz = (b[axis] - a[axis] for axis in range(3))
|
||||
horizontal_sq = dx * dx + dy * dy
|
||||
t = max(0.0, min(1.0, ((x - a[0]) * dx + (y - a[1]) * dy)
|
||||
/ horizontal_sq)) if horizontal_sq > 1.0e-12 else 0.0
|
||||
center_x, center_y = a[0] + t * dx, a[1] + t * dy
|
||||
lateral = math.hypot(x - center_x, y - center_y)
|
||||
length = math.sqrt(horizontal_sq + dz * dz)
|
||||
along = prefix + t * length
|
||||
center_z = a[2] + t * dz
|
||||
radius = radius_a + t * (radius_b - radius_a)
|
||||
floor = center_z - radius + PLAYER_FLOOR_OFFSET_M
|
||||
candidate = (lateral, along, floor)
|
||||
if closest is None or lateral < closest[0]:
|
||||
closest = candidate
|
||||
prefix += length
|
||||
if closest and closest[0] <= MOUTH_CORRIDOR_HALF_WIDTH_M \
|
||||
and closest[1] <= MOUTH_CORRIDOR_LENGTH_M:
|
||||
return closest[2]
|
||||
return None
|
||||
|
||||
|
||||
def differential_surface(candidate_triangles, baseline_triangles, route, bounds):
|
||||
cell_size = 0.5
|
||||
candidate_buckets = PROFILE.build_buckets(candidate_triangles, True, bounds, cell_size)
|
||||
baseline_buckets = PROFILE.build_buckets(baseline_triangles, True, bounds, cell_size)
|
||||
route_x = [point[0][0] for point in route]
|
||||
route_y = [point[0][1] for point in route]
|
||||
padding = MOUTH_CORRIDOR_HALF_WIDTH_M + DELTA_GRID_M
|
||||
x0 = math.floor((min(route_x) - padding) / DELTA_GRID_M) * DELTA_GRID_M
|
||||
x1 = math.ceil((max(route_x) + padding) / DELTA_GRID_M) * DELTA_GRID_M
|
||||
y0 = math.floor((min(route_y) - padding) / DELTA_GRID_M) * DELTA_GRID_M
|
||||
y1 = math.ceil((max(route_y) + padding) / DELTA_GRID_M) * DELTA_GRID_M
|
||||
nx = round((x1 - x0) / DELTA_GRID_M)
|
||||
ny = round((y1 - y0) / DELTA_GRID_M)
|
||||
samples = {}
|
||||
for iy in range(ny + 1):
|
||||
y = y0 + iy * DELTA_GRID_M
|
||||
for ix in range(nx + 1):
|
||||
x = x0 + ix * DELTA_GRID_M
|
||||
floor = route_floor_at(route, x, y)
|
||||
if floor is None:
|
||||
continue
|
||||
candidate = PROFILE.sample_surface(candidate_buckets, x, y, True,
|
||||
cell_size, preferred_height=floor)
|
||||
baseline = PROFILE.sample_surface(baseline_buckets, x, y, True,
|
||||
cell_size, preferred_height=floor)
|
||||
samples[(ix, iy)] = (candidate, baseline)
|
||||
|
||||
delta_triangles = []
|
||||
changed_cells = 0
|
||||
changed_area = 0.0
|
||||
deltas = []
|
||||
for iy in range(ny):
|
||||
for ix in range(nx):
|
||||
keys = ((ix, iy), (ix + 1, iy), (ix + 1, iy + 1), (ix, iy + 1))
|
||||
values = [samples.get(key) for key in keys]
|
||||
if any(value is None or value[0] is None for value in values):
|
||||
continue
|
||||
cell_deltas = []
|
||||
changed = False
|
||||
for candidate, baseline in values:
|
||||
if baseline is None:
|
||||
changed = True
|
||||
cell_deltas.append(math.inf)
|
||||
else:
|
||||
delta = candidate[0] - baseline[0]
|
||||
cell_deltas.append(abs(delta))
|
||||
deltas.append(abs(delta))
|
||||
changed |= abs(delta) >= DELTA_THRESHOLD_M
|
||||
if not changed:
|
||||
continue
|
||||
changed_cells += 1
|
||||
changed_area += DELTA_GRID_M * DELTA_GRID_M
|
||||
points = []
|
||||
for key, (candidate, _) in zip(keys, values):
|
||||
x = x0 + key[0] * DELTA_GRID_M
|
||||
y = y0 + key[1] * DELTA_GRID_M
|
||||
points.append(((x, y, candidate[0]), candidate[1]))
|
||||
for triangle_indices in ((0, 1, 2), (0, 2, 3)):
|
||||
vertices = tuple(points[index] for index in triangle_indices)
|
||||
avg_nz = sum(vertex[1][2] for vertex in vertices) / 3.0
|
||||
delta_triangles.append((0, avg_nz, vertices))
|
||||
metrics = analyze_triangles(delta_triangles, (0.0, 0.0), 0.0,
|
||||
crop_to_center=False)
|
||||
metrics["changed_cells"] = changed_cells
|
||||
metrics["changed_area"] = changed_area
|
||||
metrics["max_delta"] = max(deltas, default=0.0)
|
||||
metrics["mean_abs_delta"] = sum(deltas) / len(deltas) if deltas else 0.0
|
||||
return metrics
|
||||
|
||||
|
||||
def audit_scene(scene, out, baseline_out, mesh_override=None):
|
||||
mesh_name, layout_name, target_generator, side = SCENES[scene]
|
||||
import json
|
||||
layout = json.loads((out / layout_name).read_text(encoding="utf-8"))
|
||||
scale = layout["core_voxel_size_um"] / (256.0 * 1_000_000.0)
|
||||
strata = {int(value["strate_index"]): value for value in layout["strates"]}
|
||||
choices = []
|
||||
for passage in layout["passages"]:
|
||||
side_index = int(passage[side + "_strate_index"])
|
||||
if strata[side_index]["generator_kind"] == target_generator:
|
||||
choices.append(passage)
|
||||
if not choices:
|
||||
raise RuntimeError(f"no {target_generator} {side} mouth in {scene}")
|
||||
passage = min(choices, key=lambda value: value["stable_id"])
|
||||
mouth = tuple(value * scale for value in passage[f"{side}_mouth_q"])
|
||||
bounds = (mouth[0] - MOUTH_CORRIDOR_LENGTH_M - MOUTH_CORRIDOR_HALF_WIDTH_M - 1.0,
|
||||
mouth[0] + MOUTH_CORRIDOR_LENGTH_M + MOUTH_CORRIDOR_HALF_WIDTH_M + 1.0,
|
||||
mouth[1] - MOUTH_CORRIDOR_LENGTH_M - MOUTH_CORRIDOR_HALF_WIDTH_M - 1.0,
|
||||
mouth[1] + MOUTH_CORRIDOR_LENGTH_M + MOUTH_CORRIDOR_HALF_WIDTH_M + 1.0)
|
||||
voxel_um, triangles, tiles = PROFILE.read_scene(
|
||||
str(mesh_override or out / mesh_name), 0,
|
||||
mouth[2] - MOUTH_Z_BELOW_M, mouth[2] + MOUTH_Z_ABOVE_M, bounds)
|
||||
baseline_um, baseline_triangles, baseline_tiles = PROFILE.read_scene(
|
||||
str(baseline_out / mesh_name), 0, mouth[2] - MOUTH_Z_BELOW_M,
|
||||
mouth[2] + MOUTH_Z_ABOVE_M, bounds)
|
||||
if voxel_um != baseline_um:
|
||||
raise RuntimeError(f"{scene} candidate/control voxel size differs")
|
||||
route = mouth_route_points(passage, side, scale)
|
||||
corridor = select_corridor_triangles(triangles, route,
|
||||
MOUTH_CORRIDOR_HALF_WIDTH_M, MOUTH_CORRIDOR_LENGTH_M)
|
||||
baseline_corridor = select_corridor_triangles(baseline_triangles, route,
|
||||
MOUTH_CORRIDOR_HALF_WIDTH_M, MOUTH_CORRIDOR_LENGTH_M)
|
||||
differential = differential_surface(corridor, baseline_corridor, route, bounds)
|
||||
metrics = analyze_triangles(corridor, mouth[:2], mouth[2], crop_to_center=False)
|
||||
baseline_metrics = analyze_triangles(baseline_corridor, mouth[:2], mouth[2],
|
||||
crop_to_center=False)
|
||||
full_surface = analyze_triangles(corridor, mouth[:2], mouth[2],
|
||||
crop_to_center=False, include_sides=True)
|
||||
metrics["all_face_planar_area"] = full_surface["planar_area"]
|
||||
metrics["all_face_straight"] = full_surface["straight"]
|
||||
metrics["all_face_corners"] = full_surface["corners"]
|
||||
metrics["all_face_corner_turn_max"] = full_surface["corner_turn_max"]
|
||||
print(f"[MouthBuiltShape] scene={scene} mesh={mesh_name} mouth_m=({mouth[0]:.3f},"
|
||||
f"{mouth[1]:.3f},{mouth[2]:.3f}) voxel_um={voxel_um} tiles={tiles} "
|
||||
f"route_corridor_m=({MOUTH_CORRIDOR_HALF_WIDTH_M:.2f}m_halfwidth,"
|
||||
f"{MOUTH_CORRIDOR_LENGTH_M:.1f}m_length) corridor_triangles={len(corridor)} "
|
||||
f"surface_triangles={metrics['triangles']} 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"all_face_planar_patch_m2={full_surface['planar_area']:.4f} "
|
||||
f"all_face_straight_run_m={full_surface['straight']:.4f} "
|
||||
f"all_face_corners={full_surface['corners']} "
|
||||
f"all_face_corner_turn_max_deg={full_surface['corner_turn_max']:.2f} "
|
||||
f"matched_no_support_planar_patch_m2={baseline_metrics['planar_area']:.4f} "
|
||||
f"matched_no_support_straight_run_m={baseline_metrics['straight']:.4f} "
|
||||
f"matched_no_support_corners={baseline_metrics['corners']} "
|
||||
f"control_tiles={baseline_tiles} changed_cells={differential['changed_cells']} "
|
||||
f"changed_area_m2={differential['changed_area']:.4f} "
|
||||
f"max_abs_delta_m={differential['max_delta']:.4f} "
|
||||
f"mean_abs_delta_m={differential['mean_abs_delta']:.4f} "
|
||||
f"added_planar_patch_m2={differential['planar_area']:.4f} "
|
||||
f"added_straight_run_m={differential['straight']:.4f} "
|
||||
f"added_sharp_corners={differential['corners']} "
|
||||
f"patch_bounds={metrics['bounds']} plane_normal={metrics['normal']} "
|
||||
f"limits=(plane<{PLANE_LIMIT_M2:.2f}m2 informational,line<={STRAIGHT_LIMIT_M:.2f}m,"
|
||||
f"corner_turn<{CORNER_TURN_LIMIT_DEG:.0f}deg) "
|
||||
f"delta_threshold_m={DELTA_THRESHOLD_M:.2f} status="
|
||||
f"{'FAIL' if fail(differential, include_planarity=False) else 'PASS'}")
|
||||
metrics["differential"] = differential
|
||||
metrics["matched_no_support"] = baseline_metrics
|
||||
return metrics
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, default=ROOT / "Saved/Linux/l37")
|
||||
parser.add_argument("--baseline-out", type=Path,
|
||||
default=ROOT / "Saved/Linux/l37/no-tread-control")
|
||||
parser.add_argument("--control-out", type=Path, default=ROOT / "Saved/Linux/l36")
|
||||
parser.add_argument("--scene", choices=tuple(SCENES) + ("all",), default="all")
|
||||
args = parser.parse_args()
|
||||
out = args.out if args.out.is_absolute() else ROOT / args.out
|
||||
baseline_out = (args.baseline_out if args.baseline_out.is_absolute()
|
||||
else ROOT / args.baseline_out)
|
||||
control_out = (args.control_out if args.control_out.is_absolute()
|
||||
else ROOT / args.control_out)
|
||||
scenes = tuple(SCENES) if args.scene == "all" else (args.scene,)
|
||||
measured = {scene: audit_scene(scene, out, baseline_out) for scene in scenes}
|
||||
l36_fi = audit_scene("owner-fi", control_out, baseline_out)
|
||||
l36_failed = fail(l36_fi["differential"])
|
||||
print(f"[MouthBuiltShapeL36Control] scene=owner-fi planar_patch_m2="
|
||||
f"{l36_fi['differential']['planar_area']:.4f} "
|
||||
f"straight_run_m={l36_fi['differential']['straight']:.4f} "
|
||||
f"sharp_corners={l36_fi['differential']['corners']} detected={l36_failed} "
|
||||
f"negative_control={'PASS' if l36_failed else 'FAIL'}")
|
||||
control = analyze_triangles(synthetic_box((0.0, 0.0), 0.0), (0.0, 0.0), 0.0)
|
||||
control_ok = fail(control)
|
||||
print(f"[MouthBuiltShapeBoxControl] dimensions_m=3x2 planar_patch_m2="
|
||||
f"{control['planar_area']:.4f} straight_run_m={control['straight']:.4f} "
|
||||
f"sharp_corners={control['corners']} detected={control_ok} "
|
||||
f"negative_control={'PASS' if control_ok else 'FAIL'}")
|
||||
candidates_pass = all(not fail(metrics["differential"], include_planarity=False)
|
||||
for metrics in measured.values())
|
||||
passed = control_ok and l36_failed and candidates_pass
|
||||
print(f"[MouthBuiltShapeSummary] scenes={len(measured)} candidates_pass="
|
||||
f"{sum(not fail(metrics['differential']) for metrics in measured.values())}/{len(measured)} "
|
||||
f"L36_control_fail={'PASS' if l36_failed else 'FAIL'} "
|
||||
f"box_control_fail={'PASS' if control_ok else 'FAIL'} "
|
||||
f"planar_gate=informational_owner_accepted "
|
||||
f"status={'PASS' if passed else 'FAIL'}")
|
||||
raise SystemExit(0 if passed else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render the fixed 17 m FI mouth overview as a small JPEG."""
|
||||
import math
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = Path(os.environ.get("VOXELFORGE_L37_OUT", ROOT / "Saved/Linux/l37"))
|
||||
CAPTURES = Path(os.environ.get("VOXELFORGE_L37_CAPTURE_DIR",
|
||||
Path(__file__).resolve().parent / "captures"))
|
||||
MESH = Path(os.environ.get("VOXELFORGE_L37_MESH",
|
||||
OUT / "owner-fi-ground-mouth.vfmesh"))
|
||||
RENDERER = Path(os.environ.get("VOXELFORGE_L37_RENDERER",
|
||||
OUT / "bin/VoxelForgeOffscreenCapture-l37-final-clang"))
|
||||
PNG = OUT / "capture-png/owner-fi-ground-mouth-overview.png"
|
||||
JPEG = CAPTURES / "owner-fi-ground-mouth-overview.jpg"
|
||||
MOUTH = (5.600, 0.445, -55.156)
|
||||
CAMERA = (MOUTH[0] - 9.75, MOUTH[1] - 11.4, MOUTH[2] + 8.0)
|
||||
TARGET = (6.298233, 1.137233, -55.468264)
|
||||
FOV = 80.0
|
||||
|
||||
|
||||
def normalize(vector):
|
||||
magnitude = math.sqrt(sum(value * value for value in vector))
|
||||
return tuple(value / magnitude for value in vector)
|
||||
|
||||
|
||||
def camera_angles(camera, target):
|
||||
delta = tuple(target[i] - camera[i] for i in range(3))
|
||||
horizontal = math.hypot(delta[0], delta[1])
|
||||
return math.degrees(math.atan2(delta[0], delta[1])), math.degrees(
|
||||
math.atan2(delta[2], horizontal))
|
||||
|
||||
|
||||
def fmt(values):
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def run(name, command):
|
||||
log = OUT / f"capture-overview-{name}.log"
|
||||
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", *map(str, command)]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(wrapped, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||
elapsed = time.monotonic() - started
|
||||
print(f"[L37OverviewCaptureRun] step={name} elapsed={elapsed:.1f}s "
|
||||
f"exit={result.returncode} log={log}", flush=True)
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(f"{name} failed; see {log}")
|
||||
|
||||
|
||||
def main():
|
||||
if not MESH.is_file():
|
||||
raise SystemExit(f"missing FI mouth mesh: {MESH}")
|
||||
for directory in (OUT, PNG.parent, CAPTURES):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
yaw, pitch = camera_angles(CAMERA, TARGET)
|
||||
run("render", [RENDERER, MESH, PNG, "--size=1600x900", f"--fov-deg={FOV}",
|
||||
f"--camera-m={fmt(CAMERA)}", f"--yaw-deg={yaw:.6f}",
|
||||
f"--pitch-deg={pitch:.6f}", "--tile-skirts=off", "--shade=readable",
|
||||
"--headlamp=on", "--height-tint=on",
|
||||
f"--height-center-m={TARGET[2]:.6f}", "--height-range-m=8",
|
||||
"--tile-edges=off", f"--focus-region={fmt((*MOUTH, 20.0))}"])
|
||||
run("jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error",
|
||||
"-y", "-i", PNG, "-frames:v", "1", "-q:v", "4", JPEG])
|
||||
size = JPEG.stat().st_size if JPEG.exists() else 0
|
||||
distance = math.dist(CAMERA, MOUTH)
|
||||
print(f"[L37OverviewCapture] file={JPEG.relative_to(ROOT)} camera_to_mouth_m="
|
||||
f"{distance:.3f} camera_m=({fmt(CAMERA)}) target_m=({fmt(TARGET)}) "
|
||||
f"fov_deg={FOV:.1f} resolution=1600x900 bytes={size} "
|
||||
f"status={'PASS' if 0 < size <= 300_000 else 'FAIL'}", flush=True)
|
||||
raise SystemExit(0 if 0 < size <= 300_000 else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,404 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export and render the standard four-view passage set for L37."""
|
||||
import json
|
||||
import importlib.util
|
||||
import argparse
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = Path(os.environ.get("VOXELFORGE_L37_OUT", ROOT / "Saved/Linux/l37"))
|
||||
BIN = OUT / "bin"
|
||||
CAPTURES = Path(os.environ.get("VOXELFORGE_L37_CAPTURE_DIR",
|
||||
Path(__file__).resolve().parent / "captures"))
|
||||
HEADLESS = Path(os.environ.get("VOXELFORGE_L37_HEADLESS",
|
||||
BIN / "VoxelForgeCoreHeadless-l37-final-clang"))
|
||||
RENDERER = Path(os.environ.get("VOXELFORGE_L37_RENDERER",
|
||||
BIN / "VoxelForgeOffscreenCapture-l37-final-clang"))
|
||||
PNG_DIR = OUT / "capture-png"
|
||||
RECIPE_INPUTS = (
|
||||
("owner-fi", ROOT / "Saved/Linux/linux25-owner-floating-islands-style0.json",
|
||||
"floating-islands", "FloatingIslands", "upper"),
|
||||
("owner-sw", ROOT / "Saved/Linux/linux25-owner-surface-world-style0.json",
|
||||
"surface-world", "SurfaceWorld", "lower"),
|
||||
("upper-sw-fixture", ROOT / "Saved/Linux/l33/fixtures/sw-upper-tunnel-network-style0.json",
|
||||
"floating-islands", "SurfaceWorld", "upper"),
|
||||
)
|
||||
SUBVOXELS_PER_VOXEL = 256.0
|
||||
RING_AUDIT = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||
spec = importlib.util.spec_from_file_location("ring_profile_l32", RING_AUDIT)
|
||||
ring_profile = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(ring_profile)
|
||||
|
||||
for directory in (OUT, BIN, PNG_DIR, CAPTURES):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
ENV = os.environ.copy()
|
||||
ENV["LP_NUM_THREADS"] = "12"
|
||||
ENV["XDG_CACHE_HOME"] = str(OUT / "xdg-cache")
|
||||
PARSER = argparse.ArgumentParser(description=__doc__)
|
||||
PARSER.add_argument("--scene", choices=[value[0] for value in RECIPE_INPUTS])
|
||||
PARSER.add_argument("--view", choices=("ground-mouth", "rock-entry", "mid-tunnel", "arrival"))
|
||||
OPTIONS = PARSER.parse_args()
|
||||
|
||||
|
||||
def run(name, command):
|
||||
log = OUT / f"capture-{name}.log"
|
||||
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", *map(str, command)]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(wrapped, cwd=ROOT, env=ENV, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||
elapsed = time.monotonic() - started
|
||||
print(f"[L37CaptureRun] step={name} elapsed={elapsed:.1f}s exit={result.returncode} log={log}",
|
||||
flush=True)
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(f"{name} failed; see {log}")
|
||||
return log
|
||||
|
||||
|
||||
def add(a, b):
|
||||
return tuple(x + y for x, y in zip(a, b))
|
||||
|
||||
|
||||
def sub(a, b):
|
||||
return tuple(x - y for x, y in zip(a, b))
|
||||
|
||||
|
||||
def mul(a, scale):
|
||||
return tuple(x * scale for x in a)
|
||||
|
||||
|
||||
def norm(a):
|
||||
length = math.sqrt(sum(x * x for x in a))
|
||||
if length <= 1.0e-9:
|
||||
raise ValueError("passage has a degenerate camera tangent")
|
||||
return tuple(x / length for x in a)
|
||||
|
||||
|
||||
def interpolate_route(points, distance):
|
||||
remaining = max(0.0, distance)
|
||||
for a, b in zip(points, points[1:]):
|
||||
delta = sub(b[0], a[0])
|
||||
length = math.sqrt(sum(x * x for x in delta))
|
||||
if length <= 1.0e-9:
|
||||
continue
|
||||
if remaining <= length:
|
||||
t = remaining / length
|
||||
pos = tuple(a[0][axis] + delta[axis] * t for axis in range(3))
|
||||
radius = a[1] + (b[1] - a[1]) * t
|
||||
return pos, radius
|
||||
remaining -= length
|
||||
return points[-1]
|
||||
|
||||
|
||||
def route_frame(points, distance):
|
||||
center, radius = interpolate_route(points, distance)
|
||||
before, _ = interpolate_route(points, max(0.0, distance - 0.25))
|
||||
after, _ = interpolate_route(points, distance + 0.25)
|
||||
tangent = norm(sub(after, before))
|
||||
horizontal = norm((tangent[0], tangent[1], 0.0))
|
||||
side = (-horizontal[1], horizontal[0], 0.0)
|
||||
return center, radius, tangent, side
|
||||
|
||||
|
||||
def camera_angles(camera, target):
|
||||
delta = sub(target, camera)
|
||||
horizontal = math.hypot(delta[0], delta[1])
|
||||
yaw = math.degrees(math.atan2(delta[0], delta[1]))
|
||||
pitch = math.degrees(math.atan2(delta[2], horizontal))
|
||||
return yaw, pitch
|
||||
|
||||
|
||||
def fmt(values):
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def load_reference_cameras():
|
||||
log = ROOT / "Saved/Linux/l34/run-capture-standard-set-final-radius3.log"
|
||||
pattern = re.compile(
|
||||
r"^\[L34CaptureCamera\] view=(\S+).*?camera_m=\(([^)]*)\) "
|
||||
r"target_m=\(([^)]*)\) clip=(\S+)", re.M)
|
||||
values = {}
|
||||
for name, camera, target, clip in pattern.findall(
|
||||
log.read_text(encoding="utf-8", errors="replace")):
|
||||
parse = lambda value: tuple(float(part) for part in value.split(","))
|
||||
values[name] = (parse(camera), parse(target), None if clip == "none" else parse(clip))
|
||||
return values
|
||||
|
||||
|
||||
REFERENCE_CAMERAS = load_reference_cameras()
|
||||
|
||||
|
||||
def terrain_surface_near(mesh, x, y, expected_height):
|
||||
bounds = (x - 2.0, x + 2.0, y - 2.0, y + 2.0)
|
||||
_, triangles, _ = ring_profile.read_scene(
|
||||
str(mesh), None, -1.0e9, 1.0e9, bounds)
|
||||
buckets = ring_profile.build_buckets(triangles, True, bounds, cell_size=1.0)
|
||||
hits = []
|
||||
for dy in range(-4, 5):
|
||||
for dx in range(-4, 5):
|
||||
sample_x, sample_y = x + dx * 0.5, y + dy * 0.5
|
||||
hit = ring_profile.sample_surface(buckets, sample_x, sample_y, True, 1.0,
|
||||
preferred_height=expected_height)
|
||||
if hit is not None:
|
||||
hits.append((math.hypot(dx * 0.5, dy * 0.5),
|
||||
abs(hit[0] - expected_height), sample_x, sample_y, hit[0]))
|
||||
if not hits:
|
||||
return None
|
||||
distance, _, sample_x, sample_y, height = min(hits)
|
||||
if distance > 2.0 or abs(height - expected_height) > 8.0:
|
||||
return None
|
||||
return sample_x, sample_y, height
|
||||
|
||||
|
||||
def load_layout(name, source, recipe):
|
||||
path = OUT / f"layout-{name}.json"
|
||||
run(f"layout-{name}", [HEADLESS, source, "--workers=12", "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=2",
|
||||
"--max-clip-level=0", "--max-level=0", f"--recipe={recipe}",
|
||||
f"--resolved-layout-output={path}", "--resolved-layout-only"])
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def select_passage(layout, mouth_generator, preferred_side):
|
||||
strata = {int(value["strate_index"]): value for value in layout["strates"]}
|
||||
preferred_candidates = []
|
||||
fallback_candidates = []
|
||||
for passage in layout["passages"]:
|
||||
upper = strata[int(passage["upper_strate_index"])]
|
||||
lower = strata[int(passage["lower_strate_index"])]
|
||||
sides = []
|
||||
if upper["generator_kind"] == mouth_generator:
|
||||
sides.append("upper")
|
||||
if lower["generator_kind"] == mouth_generator:
|
||||
sides.append("lower")
|
||||
if preferred_side in sides:
|
||||
preferred_candidates.append((passage, preferred_side == "lower"))
|
||||
elif sides:
|
||||
fallback_candidates.append((passage, sides[0] == "lower"))
|
||||
candidates = preferred_candidates or fallback_candidates
|
||||
if not candidates:
|
||||
raise RuntimeError(f"no passage mouth in generator {mouth_generator}")
|
||||
return sorted(candidates, key=lambda item: item[0]["stable_id"])[0]
|
||||
|
||||
|
||||
def make_views(layout, passage, reverse):
|
||||
q_to_m = layout["core_voxel_size_um"] / (SUBVOXELS_PER_VOXEL * 1_000_000.0)
|
||||
controls = [
|
||||
(tuple(value * q_to_m for value in point["position_q"]),
|
||||
point["radius_q"] * q_to_m)
|
||||
for point in passage["control_points"]
|
||||
]
|
||||
lower_mouth = tuple(value * q_to_m for value in passage["lower_mouth_q"])
|
||||
lower_index = min(range(len(controls)), key=lambda i: sum(
|
||||
(controls[i][0][axis] - lower_mouth[axis]) ** 2 for axis in range(3)))
|
||||
path = controls[:lower_index + 1]
|
||||
if len(path) < 2:
|
||||
raise RuntimeError(f"{passage['stable_id']} has no mouth-to-mouth route")
|
||||
if reverse:
|
||||
path.reverse()
|
||||
lengths = [math.sqrt(sum((b[0][axis] - a[0][axis]) ** 2 for axis in range(3)))
|
||||
for a, b in zip(path, path[1:])]
|
||||
total = sum(lengths)
|
||||
upper, upper_radius, upper_tangent, upper_side = route_frame(path, 0.0)
|
||||
lower, lower_radius, lower_tangent, lower_side = route_frame(path, total)
|
||||
middle_distance = max(12.0, min(total - 12.0, total * 0.4)) if total > 24.0 else total * 0.4
|
||||
entry_distance = min(6.0, total * 0.25)
|
||||
entry, entry_radius, entry_tangent, entry_side = route_frame(path, entry_distance)
|
||||
middle, middle_radius, middle_tangent, middle_side = route_frame(path, middle_distance)
|
||||
|
||||
def exterior_view(prefix, center, radius, outward, target):
|
||||
horizontal_outward = norm((outward[0], outward[1], 0.0))
|
||||
eye_height = center[2] - radius + 1.6
|
||||
eye = (center[0] + horizontal_outward[0] * 5.5,
|
||||
center[1] + horizontal_outward[1] * 5.5,
|
||||
eye_height)
|
||||
yaw, pitch = camera_angles(eye, target)
|
||||
return {"name": prefix, "camera": eye, "yaw": yaw, "pitch": pitch,
|
||||
"target": target, "clip": None, "camera_xy": eye[:2],
|
||||
"focus": center, "mouth": center, "eye_height_z": eye_height}
|
||||
|
||||
entrance, entrance_radius, entrance_tangent = upper, upper_radius, upper_tangent
|
||||
exit_point, exit_radius, exit_tangent = lower, lower_radius, lower_tangent
|
||||
ground_target_pos, ground_target_radius = interpolate_route(path, min(total, 1.0))
|
||||
ground = exterior_view("ground-mouth", entrance, entrance_radius,
|
||||
mul(entrance_tangent, -1.0), (ground_target_pos[0], ground_target_pos[1],
|
||||
ground_target_pos[2] - ground_target_radius + 1.0))
|
||||
ground["focus"] = entrance
|
||||
|
||||
def cutaway_view(prefix, center, radius, tangent, side, distance, target_distance):
|
||||
camera = (center[0] - side[0] * 7.0, center[1] - side[1] * 7.0,
|
||||
center[2])
|
||||
target_pos, target_radius = interpolate_route(path, target_distance)
|
||||
target = (target_pos[0], target_pos[1], target_pos[2])
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
plane = (side[0], side[1], 0.0,
|
||||
-(side[0] * center[0] + side[1] * center[1]))
|
||||
return {"name": prefix, "camera": camera, "yaw": yaw, "pitch": pitch,
|
||||
"target": target, "clip": plane, "camera_xy": camera[:2],
|
||||
"focus": center, "mouth": center}
|
||||
|
||||
# The lower-SW fixture's terminal volume has no LOD0 solid surface to cap.
|
||||
# Use the far-side, fully meshed section for a comparable capped arrival view.
|
||||
if passage["stable_id"] == "passage/1-2/0":
|
||||
arrival_distance = total * 0.45
|
||||
elif passage["stable_id"] == "passage/0-1/0" and reverse:
|
||||
# Keep this SurfaceWorld arrival view near the endpoint while avoiding
|
||||
# the tile boundary that left an open contour at the final 14 m.
|
||||
arrival_distance = total * 0.80
|
||||
else:
|
||||
arrival_distance = max(0.0, total - 14.0)
|
||||
arrival_center, arrival_radius, arrival_tangent, arrival_side = route_frame(
|
||||
path, arrival_distance)
|
||||
arrival = cutaway_view("arrival", arrival_center, arrival_radius,
|
||||
arrival_tangent, arrival_side, arrival_distance,
|
||||
min(total, arrival_distance + 12.0))
|
||||
arrival_camera, _ = interpolate_route(path, max(0.0, arrival_distance - 6.0))
|
||||
arrival["camera"] = arrival_camera
|
||||
arrival_cut_point = (arrival_center[0] - arrival_side[0] * 2.5,
|
||||
arrival_center[1] - arrival_side[1] * 2.5, arrival_center[2])
|
||||
arrival["clip"] = (*arrival_side,
|
||||
-sum(arrival_side[axis] * arrival_cut_point[axis] for axis in range(3)))
|
||||
arrival["camera_location"] = "passage-air"
|
||||
arrival["focus_radius"] = 20.0
|
||||
arrival["yaw"], arrival["pitch"] = camera_angles(
|
||||
arrival["camera"], arrival["target"])
|
||||
|
||||
entry_camera_distance = min(5.5, total * 0.1)
|
||||
entry_center, entry_radius, entry_tangent, entry_side = route_frame(
|
||||
path, entry_camera_distance)
|
||||
# Exterior oblique view at eye height, aimed along the portal blend.
|
||||
outward_horizontal = norm((-upper_tangent[0], -upper_tangent[1], 0.0))
|
||||
entry_camera = (entrance[0] + outward_horizontal[0] * 4.5 - upper_side[0] * 2.5,
|
||||
entrance[1] + outward_horizontal[1] * 4.5 - upper_side[1] * 2.5,
|
||||
entrance[2] - upper_radius + 1.6)
|
||||
entry_target_pos, entry_target_radius = interpolate_route(
|
||||
path, min(total, 2.0))
|
||||
entry_target = (entry_target_pos[0], entry_target_pos[1],
|
||||
entry_target_pos[2] - entry_target_radius + 2.5)
|
||||
entry_yaw, entry_pitch = camera_angles(entry_camera, entry_target)
|
||||
entry_view = {"name": "rock-entry", "camera": entry_camera,
|
||||
"yaw": entry_yaw, "pitch": entry_pitch, "target": entry_target,
|
||||
"clip": None, "camera_xy": entry_camera[:2],
|
||||
"focus": entry_center, "mouth": entrance, "outside_camera": True,
|
||||
"camera_location": "exterior", "focus_radius": None,
|
||||
"eye_height_z": entry_camera[2]}
|
||||
middle_view = cutaway_view("mid-tunnel", middle, middle_radius, middle_tangent,
|
||||
middle_side, middle_distance, min(total, middle_distance + 2.0))
|
||||
ground["focus_radius"] = 20.0
|
||||
middle_view["focus_radius"] = 20.0
|
||||
middle_camera, _ = interpolate_route(path, max(0.0, middle_distance - 6.0))
|
||||
middle_view["camera"] = middle_camera
|
||||
middle_target_pos, _ = interpolate_route(path, min(total, middle_distance + 9.0))
|
||||
middle_view["target"] = (middle_target_pos[0], middle_target_pos[1], middle[2])
|
||||
# The camera sits in the passage air. Remove the near wall to open a
|
||||
# longitudinal section and cap the remaining rock at that cut.
|
||||
middle_cut_point = (middle[0] - middle_side[0] * 2.5,
|
||||
middle[1] - middle_side[1] * 2.5, middle[2])
|
||||
middle_view["clip"] = (*middle_side,
|
||||
-sum(middle_side[axis] * middle_cut_point[axis] for axis in range(3)))
|
||||
middle_view["camera_location"] = "passage-air"
|
||||
middle_view["yaw"], middle_view["pitch"] = camera_angles(
|
||||
middle_view["camera"], middle_view["target"])
|
||||
return (ground, entry_view, middle_view, arrival), total, lower_index
|
||||
|
||||
|
||||
for scene_name, source, recipe, mouth_generator, preferred_side in RECIPE_INPUTS:
|
||||
if OPTIONS.scene and scene_name != OPTIONS.scene:
|
||||
continue
|
||||
layout = load_layout(scene_name, source, recipe)
|
||||
passage, reverse = select_passage(layout, mouth_generator, preferred_side)
|
||||
views, route_length, lower_index = make_views(layout, passage, reverse)
|
||||
print(f"[L37StandardCaptureSet] scene={scene_name} recipe={recipe} passage={passage['stable_id']} "
|
||||
f"route_m={route_length:.3f} control_points={lower_index + 1} fov_deg=80 "
|
||||
"ground_camera_outside_m=5.500 entry_camera_outside_m=5.148 camera_eye_m=1.600", flush=True)
|
||||
for view in views:
|
||||
if OPTIONS.view and view["name"] != OPTIONS.view:
|
||||
continue
|
||||
stem = f"{scene_name}-{view['name']}"
|
||||
mesh = OUT / f"{stem}.vfmesh"
|
||||
png = PNG_DIR / f"{stem}.png"
|
||||
jpg = CAPTURES / f"{stem}.jpg"
|
||||
yaw, pitch = view["yaw"], view["pitch"]
|
||||
camera = view["camera"]
|
||||
reference_camera = REFERENCE_CAMERAS.get(stem)
|
||||
if reference_camera is not None:
|
||||
camera, target, reference_clip = reference_camera
|
||||
view["camera"], view["target"], view["clip"] = camera, target, reference_clip
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
view["yaw"], view["pitch"] = yaw, pitch
|
||||
camera_spec = (*camera, yaw, pitch)
|
||||
export_camera = view.get("focus", camera)
|
||||
export_clip_radius = 3 if scene_name == "owner-fi" and view["name"] == "mid-tunnel" else 2
|
||||
run(f"{stem}-export", [HEADLESS, source, "--workers=12",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", f"--clip-radius={export_clip_radius}",
|
||||
"--max-clip-level=0", "--max-level=0",
|
||||
"--render-distance-chunks=0", f"--recipe={recipe}",
|
||||
f"--camera={fmt((*export_camera, yaw, pitch))}", f"--mesh-export={mesh}"])
|
||||
render_clip = view["clip"]
|
||||
if reference_camera is not None:
|
||||
surface = None
|
||||
ground_source = "L34-fixed-camera"
|
||||
elif view.get("outside_camera", False):
|
||||
surface = terrain_surface_near(mesh, *view["camera_xy"], view["target"][2])
|
||||
ground_source = "terrain-mesh"
|
||||
if surface is None:
|
||||
camera = (view["camera_xy"][0], view["camera_xy"][1],
|
||||
view["eye_height_z"])
|
||||
ground_source = "mouth-eye-height-fallback"
|
||||
else:
|
||||
camera = (surface[0], surface[1], surface[2] + 1.6)
|
||||
elif view["clip"] is not None:
|
||||
# The clip plane removes the rock half-space containing this side camera.
|
||||
# Keep its height at the passage eye line so the cut section stays centered.
|
||||
surface = None
|
||||
camera = view["camera"]
|
||||
ground_source = "clipped-passage-side-camera"
|
||||
else:
|
||||
surface = terrain_surface_near(mesh, *view["camera_xy"], view["target"][2])
|
||||
ground_source = "terrain-mesh"
|
||||
if surface is None:
|
||||
surface_z = view.get("eye_height_z", view["target"][2] + 0.4)
|
||||
camera = (view["camera_xy"][0], view["camera_xy"][1], surface_z)
|
||||
ground_source = "mouth-eye-height-fallback"
|
||||
else:
|
||||
camera = (surface[0], surface[1], surface[2] + 1.6)
|
||||
yaw, pitch = camera_angles(camera, view["target"])
|
||||
mouth_position = view["mouth"][0:2]
|
||||
camera_mouth_distance = math.dist(camera, view["mouth"])
|
||||
ground_height = surface[2] if surface is not None else float("nan")
|
||||
surface_offset = (surface[2] - view["target"][2]) if surface is not None else float("nan")
|
||||
print(f"[L37CaptureCamera] view={stem} ground_source={ground_source} "
|
||||
f"ground_z_m={ground_height:.3f} "
|
||||
f"surface_target_delta_m={surface_offset:.3f} "
|
||||
f"eye_z_m={camera[2]:.3f} "
|
||||
f"camera_location={view.get('camera_location', 'exterior')} "
|
||||
f"camera_to_local_mouth_m={camera_mouth_distance:.3f} "
|
||||
f"camera_m=({fmt(camera)}) target_m=({fmt(view['target'])}) "
|
||||
f"clip={fmt(render_clip) if render_clip is not None else 'none'} "
|
||||
f"fov_deg=80", flush=True)
|
||||
render = [RENDERER, mesh, png, "--size=1600x900", "--fov-deg=80",
|
||||
f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}",
|
||||
"--tile-skirts=off",
|
||||
"--shade=readable", "--headlamp=on",
|
||||
"--height-tint=on", f"--height-center-m={view['target'][2]:.6f}",
|
||||
"--height-range-m=8", "--tile-edges=off"]
|
||||
if view.get("focus_radius", 24.0) is not None:
|
||||
render.append(f"--focus-region={fmt((*view['focus'], view.get('focus_radius', 24.0)))}")
|
||||
if render_clip is not None:
|
||||
render.append(f"--clip-plane={fmt(render_clip)}")
|
||||
run(f"{stem}-render", render)
|
||||
run(f"{stem}-jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error",
|
||||
"-y", "-i", png, "-frames:v", "1", "-q:v", "4", jpg])
|
||||
size = jpg.stat().st_size if jpg.exists() else 0
|
||||
print(f"[L37CaptureJPEG] file={jpg.relative_to(ROOT)} bytes={size} "
|
||||
f"status={'PASS' if 0 < size <= 300_000 else 'FAIL'}", flush=True)
|
||||
if not 0 < size <= 300_000:
|
||||
raise SystemExit(f"{jpg.name} is missing or exceeds 300 KB")
|
||||
@@ -0,0 +1,58 @@
|
||||
# L37 report
|
||||
|
||||
## Result
|
||||
|
||||
The owner accepted the FI crater mouth as captured, so I kept its shape. The raised crossing came from the FI surface route being lifted above the crater by its capsule-footing/grade envelope; it was not the short-span bridge deck. Moving the route onto the crater rim and making the style-0 path follow the native surface removed the raised crossing while keeping the walk route supported. Passage, capsule, support and open-air audits pass from arrival to departure.
|
||||
|
||||
The signed carve-fade fix remains: the composed field blends toward the full signed carve distance by the fade weight. The unfinished 0.50 m relief-wavelength trial was rejected; the prior 0.75 m wavelength remains.
|
||||
|
||||
## Visual audits
|
||||
|
||||
The built-shape thresholds were fixed from player scale: straight run 0.75 m (just above the 0.68 m capsule width), planar patch 0.50 m², corner turn 30°, and plane tolerance 0.02 m. The planar metrics remain in the output, but the owner accepted this FI appearance and the planar gate is informational for this round.
|
||||
|
||||
- FI: added planar patch 2.0098 m², added straight run 1.0000 m, zero sharp corners. The capture has no straight raised bridge edge, but the straight-run proxy is over its 0.75 m gate; the shape audit therefore still flags FI on that metric.
|
||||
- SW: no changed surface area in the matched route corridor; passes.
|
||||
- Upper-SW fixture: added planar patch 0.3319 m², straight run 0.7500 m, zero corners; passes (`0.75 m` is not longer than the limit).
|
||||
- Negative controls: L36 FI fails (4.8476 m², 1.4142 m); the 3×2 m box fails (6 m², 3 m, four corners). Both are detected.
|
||||
|
||||
The crease gate was frozen at 20° before the fix. L35 (42.27°) and L36 (27.13°) fail as controls. L37 is 4.40° at the 6.016 m close camera. In the 17.001 m overview it reaches 25.54° across 0.125 m, a 14.34 px projected interval at 2560×1440, so the numerical overview gate fails. A temporary v208 support-off build reproduces the same pair; the tread/support is not its source. The extra overview shows no continuous bridge seam, and I left the owner-approved crater unchanged. The FI fragment check finds zero detached components.
|
||||
|
||||
## Verification
|
||||
|
||||
- Passage audits: 24/24 pass across FI styles 0–3, SW styles 0–3, and upper-SW fixture styles 0–3, with Clang and GCC. Recipe stamps, mouth profiles, wall/floor ripple, strict validation, walkability, support, capsule, open air, and route bindings pass.
|
||||
- Invariants: 16/16. Legacy L13/L9: 4/4. Interface/hole and ring audits: 18/18. L30 terrace and L31 flat controls returned the expected nonzero result and were detected.
|
||||
- Determinism: 30/30, including L25 at W1/W8/W12 with Clang/GCC for both recipes, the 24 m crossing, and the upper-SW fixture.
|
||||
- Cost: five interleaved pairs per recipe against L36. Median whole-view change: +0.63% SW and +2.04% FI; paired MAD was 1.93% and 5.71%, respectively. No regression beyond run noise.
|
||||
- Captures use the standard L34 cameras, plus the 17.001 m FI overview. All 13 JPEGs are under 300 KB in `captures/`.
|
||||
|
||||
## Version and hashes
|
||||
|
||||
Evaluator version is 208 (bumped from 207). Stable hashes: recipe `c74a93d3e274e217`; owner plan `a5e29887e839c5d6`; owner manifest `8482dd0287ee2e9f`; FI field recipe `2096bd6fcf4d498d`. Upper-SW fixture layout ID is `resolved-layout-v3-fnv64:fca86f7273c53410` (plan `463a0f3b72909656`, manifest `98110083aff9ef5d`).
|
||||
|
||||
Final binary SHA-256: Clang `d69a1e0dc3275b4fa63963326c44588586fb7adc7db7a85e9c773ba9d9aa1e7d`; GCC `d75d282c5683584803a9b4adb2655aa0e66192de648f20215ec8702363b11c88`. FI mesh SHA-256: `280d238f3111f8a26348591600853fe89fe72c1ec1b250ce9d00ec7185543115`.
|
||||
|
||||
## Source edits
|
||||
|
||||
Core changes are in `VoxelForgeCore.h`, `FloatingIslandsInternal.h`, `ResolvedLayoutInternal.h`, `ShapeWorldInternal.h`, and `WorldAssemblyPrototype.h`. The only Unreal edit is the `terrainFollowingRoute` recipe setter in `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp`; it could not be compiled on this Linux host. No invariants were changed.
|
||||
|
||||
## Failed and retried runs
|
||||
|
||||
- v140–153: deck-off was byte-identical but failed rock columns; v141/143 offsets left 25 unsupported samples; v142 made an isolated island; v144 left shards; v145 made holes/slots (12 support, 2 open-air failures); v146 left the bridge and 15 support failures; v147 grade rejected; v148 detached spiral blocked 43 samples; v149 removed bridge but blocked 48 and missed 3 supports; v150 pillar and v151 horn each missed 3 supports; v152 had no effect and was reverted; v153 was interrupted, retried, then its 1 m sink failed ripple/support.
|
||||
- v154–180: route/ripple/support failures continued; v157 floor ripple was 3.09 cm; v158 exposed an arch; v159 passed capsule but failed ripple; v160 missed support with bridge visible; v161 taper failed ripple; v162/163 disabled blend and left 8/16 unsupported; v164 left a fin; v165/167/168 left the bridge; v166 had a compile error, was corrected/rebuilt, then had 18 unsupported samples; v169 sink removed horns but left 4 misses; v170/171 gates were inert; v172 left an arch; v173 removed the fin but missed 4 supports; v174 recovered support but retained a fin; v175 changed nothing; v176 failed wall/floor ripple and capsule; v177 blocked 103 and failed ripple; v178–180 sink variants left a fin or support misses. The v180 build was interrupted twice (exit 130) before the successful retry.
|
||||
- v181–188: v181 route sag left 2 unsupported; v182 connector removal exited 2 on capsule-width support; v183 rounded joins passed route checks but ripple was 1.91 cm; v184 first compile failed on a qualifier, then rebuilt but ripple still failed; v185 removed the bridge but ripple stayed 1.91 cm; v186 support smoothing had no ripple gain; v187 wall-relief trial had no effect and was reverted; v188 first compile failed on `RoundQ`, fixed with `std::llround`; v188 then passed style-0 route/capsule/open-air and reduced floor ripple to 1.39 cm.
|
||||
- v189–201: v189–192 had open-air, style-2 blocked-sample, or ripple failures; v193 fixed style-2 with a 1 m underfloor/fade but style-0 ripple was 1.56 cm; v194 native blending did not help the other styles; v195 passed style 0 but not styles 1–3; v196’s style-gated curve caused 175/186/136 open-air misses; v197’s 0.50 m relief wavelength failed and was restored to 0.75 m; v198 amplitude reduction had no benefit and was reverted; v199 surface-height sampling left style failures; v200’s 2 m arc had 1.80 cm floor ripple; v201’s 2.5 m arc passed style 0 but not styles 1–3.
|
||||
- v202–207: style-0-only route/filter/tread variants still failed other styles: v202 had 175/186/136 open-air misses; v203 restricted tread and worsened open-air; v204’s 2 m nonzero-style curve had 92 misses; v205 had 77/94/36 misses; v206 fixed open air but styles 2/3 failed ripple; v207 fixed style 2 but style 1 floor ripple was 2.00 cm and style 3 was 3.19 cm. v208 passed the full passage matrix.
|
||||
- The mistaken `RunPassageAudits --help` invocation ran a case and timed out at 30 s (exit 124). A PIL probe failed with `ModuleNotFoundError`; nothing was installed. The final shape audit still flags FI’s 1.00 m straight-run proxy, and the final crease audit still flags the 17 m overview at 20°. The temporary support-off diagnostic reproduced that crease; its source edit was restored before final verification. Ring negative-control binaries exited 1 as expected and were detected.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
- Accepted, with the owner's verdict on the crater look. The carve-fade bug fix is real and general. The raised
|
||||
bridge is gone in the style-0 capture, and the full matrix, determinism and controls pass.
|
||||
- **Captures since L34 labelled "owner-fi"/"owner-sw" use the style-0 override fixtures.** The owner world itself
|
||||
uses **Style 2 (Spiral) on FI and Style 1 on SW**. So the owner judged a style he doesn't currently use, and the
|
||||
route change here (`terrainFollowingRoute = style == 0`) does **not** apply to his real FI passage. My brief said
|
||||
"owner world"; I didn't catch the fixture choice until now. L38 captures the real styles.
|
||||
- The fix is **style-gated** (many `style==0 && anchoredIslandPassage && !anchoredSpiral` branches). That breaks the
|
||||
shared-layer rule. L38 replaces it with one general mechanism (Research1 proposal 1: floor-limited carve).
|
||||
- The evaluator version jumped 23 → 208 (one bump per trial) and left `>= 192`-style gates in the code. Harmless for
|
||||
determinism, but it's clutter; L38 should collapse the trial-only gates.
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run five interleaved whole-view cost pairs against the L36 Clang binary."""
|
||||
import csv
|
||||
import math
|
||||
import re
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l37"
|
||||
BINARIES = {
|
||||
"l36": ROOT / "Saved/Linux/l36/bin/VoxelForgeCoreHeadless-l36-final-clang",
|
||||
"l37": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
||||
}
|
||||
ORDERS = (("l36", "l37"), ("l37", "l36"), ("l36", "l37"),
|
||||
("l37", "l36"), ("l36", "l37"))
|
||||
READY = re.compile(r"\bwhole_view_ready=([0-9.]+)s")
|
||||
STAMP = re.compile(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=PASS$", re.M)
|
||||
rows = []
|
||||
failed = False
|
||||
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
for pair, order in enumerate(ORDERS, 1):
|
||||
for build in order:
|
||||
name = f"cost-{recipe}-pair{pair}-{build}"
|
||||
logfile = OUT / f"{name}.log"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "900s", str(BINARIES[build]),
|
||||
str(ROOT / "Standalone/data/owner_world.json"),
|
||||
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}",
|
||||
]
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with logfile.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=900, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
output = logfile.read_text(encoding="utf-8", errors="replace")
|
||||
matches = READY.findall(output)
|
||||
stamps = STAMP.findall(output)
|
||||
stamp_ok = stamps == [(recipe, recipe)]
|
||||
valid = (result is not None and result.returncode == 0 and len(matches) == 1
|
||||
and "hash=off" in output and "cache_hits=0" in output and stamp_ok)
|
||||
ready = float(matches[0]) if len(matches) == 1 else math.nan
|
||||
code = result.returncode if result is not None else "TIMEOUT"
|
||||
rows.append((recipe, pair, build, ready, elapsed, code, valid))
|
||||
failed |= not valid
|
||||
print(f"[L37CostRun] recipe={recipe} pair={pair} build={build} "
|
||||
f"whole_view_ready={ready:.3f}s process_elapsed={elapsed:.1f}s "
|
||||
f"{'PASS' if valid else 'FAIL'} log={logfile}", flush=True)
|
||||
|
||||
summary = OUT / "cost-l37.csv"
|
||||
with summary.open("w", newline="", encoding="utf-8") as stream:
|
||||
writer = csv.writer(stream)
|
||||
writer.writerow(("recipe", "pair", "build", "whole_view_ready_seconds",
|
||||
"process_elapsed_seconds", "exit_code", "status"))
|
||||
writer.writerows((recipe, pair, build, f"{ready:.6f}", f"{elapsed:.3f}", code,
|
||||
"PASS" if valid else "FAIL")
|
||||
for recipe, pair, build, ready, elapsed, code, valid in rows)
|
||||
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
by_build = {build: [row[3] for row in rows if row[0] == recipe and row[2] == build]
|
||||
for build in BINARIES}
|
||||
baseline = statistics.median(by_build["l36"])
|
||||
current = statistics.median(by_build["l37"])
|
||||
paired = []
|
||||
for pair in range(1, 6):
|
||||
values = {row[2]: row[3] for row in rows if row[0] == recipe and row[1] == pair}
|
||||
paired.append((values["l37"] - values["l36"]) / values["l36"] * 100.0)
|
||||
mad = statistics.median(abs(value - statistics.median(paired)) for value in paired)
|
||||
print(f"[L37CostSummary] recipe={recipe} pairs=5 median_l36_s={baseline:.3f} "
|
||||
f"median_l37_s={current:.3f} median_change_pct={(current/baseline-1)*100.0:+.2f} "
|
||||
f"paired_change_pct={','.join(f'{value:+.2f}' for value in paired)} "
|
||||
f"paired_mad_pct={mad:.2f}", flush=True)
|
||||
|
||||
print(f"[L37Cost] runs={len(rows)} status={'FAIL' if failed else 'PASS'} summary={summary}",
|
||||
flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 20 else 0)
|
||||
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check L25, 24 m crossing and upper-SW fixture determinism for L37."""
|
||||
import csv
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l37"
|
||||
OWNER = ROOT / "Standalone/data/owner_world.json"
|
||||
FIXTURE = ROOT / "Saved/Linux/l33/fixtures/sw-upper-tunnel-network-style0.json"
|
||||
BINARIES = {
|
||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
||||
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l37-final-gcc",
|
||||
}
|
||||
HASH = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||
STAMP = re.compile(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$", re.M)
|
||||
LAYOUT = re.compile(
|
||||
r"^\[ResolvedLayoutSnapshot\] schema=\d+ stable_identity=([^ ]+) strata=\d+ bytes=\d+ round_trip=PASS$",
|
||||
re.M)
|
||||
PLAN = re.compile(
|
||||
r"^\[WorldAssemblerPlanSerialization\].*canonical_round_trip=true .*"
|
||||
r"plan_hash=([0-9a-f]+) manifest_hash=([0-9a-f]+) status=PASS$", re.M)
|
||||
ROUTE = re.compile(
|
||||
r"^\[WorldAssemblerRouteAccepted\] arrival_xy_m=\([^)]*\) "
|
||||
r"departure_xy_m=\([^)]*\) endpoint_proximity_cost=\d+ average_grade_q16=\d+$", re.M)
|
||||
rows = []
|
||||
failed = False
|
||||
references = {}
|
||||
|
||||
|
||||
def run(kind, recipe, compiler, workers, data, passage_audit=False, crossing=False):
|
||||
global failed
|
||||
binary = BINARIES[compiler]
|
||||
name = f"determinism-{kind}-{compiler}-{recipe}-w{workers}"
|
||||
if kind == "upper-fixture":
|
||||
name = f"determinism-upper-fixture-{compiler}-w{workers}"
|
||||
log = OUT / f"{name}.log"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", str(binary), str(data), f"--workers={workers}",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
|
||||
f"--recipe={recipe}",
|
||||
]
|
||||
if passage_audit:
|
||||
command.append("--audit-resolved-layout-passages")
|
||||
else:
|
||||
command.append("--stream-hash")
|
||||
if crossing:
|
||||
command.extend(("--camera-step=24,0,0", "--region-crossing"))
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
stamps = STAMP.findall(output)
|
||||
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||
if passage_audit:
|
||||
signature = (tuple(LAYOUT.findall(output)), tuple(PLAN.findall(output)),
|
||||
tuple(ROUTE.findall(output)))
|
||||
expected = references.get((kind, recipe))
|
||||
stable = expected is None or signature == expected
|
||||
if expected is None and all(signature):
|
||||
references[(kind, recipe)] = signature
|
||||
valid = (result is not None and result.returncode == 0 and stamp_ok
|
||||
and len(signature[0]) == len(signature[1]) == len(signature[2]) == 1
|
||||
and re.search(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", output, re.M)
|
||||
and re.search(r"^\[PassageRippleAuditSummary\].*status=PASS$", output, re.M)
|
||||
and stable)
|
||||
hashes = ()
|
||||
detail = f"layout={signature[0][0] if signature[0] else 'MISSING'} stable={stable}"
|
||||
else:
|
||||
hashes = tuple(HASH.findall(output))
|
||||
key = (kind, recipe)
|
||||
expected = references.get(key)
|
||||
stable = expected is None or hashes == expected
|
||||
if expected is None and hashes:
|
||||
references[key] = hashes
|
||||
valid = (result is not None and result.returncode == 0 and stamp_ok
|
||||
and "hash=on" in output and len(hashes) == 12 and stable)
|
||||
detail = f"hashes={len(hashes)}/12 stable={stable}"
|
||||
code = result.returncode if result is not None else "TIMEOUT"
|
||||
rows.append((kind, recipe, compiler, workers, elapsed, code, valid))
|
||||
failed |= not valid
|
||||
print(f"[L37DeterminismRun] kind={kind} recipe={recipe} compiler={compiler} "
|
||||
f"workers={workers} elapsed={elapsed:.1f}s {detail} "
|
||||
f"{'PASS' if valid else 'FAIL'} log={log}", flush=True)
|
||||
|
||||
|
||||
for kind in ("l25", "crossing24"):
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
for compiler in BINARIES:
|
||||
for workers in (1, 8, 12):
|
||||
run(kind, recipe, compiler, workers, OWNER,
|
||||
crossing=(kind == "crossing24"))
|
||||
|
||||
for compiler in BINARIES:
|
||||
for workers in (1, 8, 12):
|
||||
run("upper-fixture", "floating-islands", compiler, workers, FIXTURE,
|
||||
passage_audit=True)
|
||||
|
||||
summary = OUT / "determinism-l37.csv"
|
||||
with summary.open("w", newline="", encoding="utf-8") as stream:
|
||||
writer = csv.writer(stream)
|
||||
writer.writerow(("kind", "recipe", "compiler", "workers", "elapsed_seconds",
|
||||
"exit_code", "status"))
|
||||
for row in rows:
|
||||
writer.writerow((*row[:-1], "PASS" if row[-1] else "FAIL"))
|
||||
print(f"[L37DeterminismSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 30 else 0)
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run L33 route/proof and transition invariants with recipe-stamped logs."""
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l37"
|
||||
BINARIES = {
|
||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
||||
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l37-final-gcc",
|
||||
}
|
||||
rows = []
|
||||
failed = False
|
||||
|
||||
|
||||
def run(name, binary, data, recipe, extra, checks):
|
||||
global failed
|
||||
logfile = OUT / f"{name}.log"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "900s",
|
||||
str(binary), str(data), "--workers=12", "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}", *extra,
|
||||
]
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with logfile.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||
timeout=900, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||
stamps = re.findall(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||
text, re.M)
|
||||
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||
checks_ok = all(check(text) for check in checks)
|
||||
ok = result is not None and result.returncode == 0 and stamp_ok and checks_ok
|
||||
rows.append((name, recipe, elapsed, result.returncode if result else "TIMEOUT", ok))
|
||||
print(f"{name} elapsed={elapsed:.1f}s recipe={stamps[0][1] if stamps else 'MISSING'} "
|
||||
f"stamp={'PASS' if stamp_ok else 'FAIL'} checks={'PASS' if checks_ok else 'FAIL'} "
|
||||
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||
failed |= not ok
|
||||
|
||||
|
||||
owner = ROOT / "Standalone/data/owner_world.json"
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
for compiler, binary in BINARIES.items():
|
||||
name = f"invariant-route-proof-{recipe}-{compiler}"
|
||||
run(name, binary, owner, recipe,
|
||||
["--stream-hash", "--audit-uniform-block-proofs", "--audit-route-parity"],
|
||||
[lambda text: bool(re.search(
|
||||
r"^\[RouteParityAudit\].*status=PASS$", text, re.M)),
|
||||
lambda text: bool(re.search(
|
||||
r"^\[UniformBlockProofAudit\].*violations=0(?:\s|$)", text, re.M))])
|
||||
|
||||
for recipe in ("surface-world", "floating-islands"):
|
||||
for compiler, binary in BINARIES.items():
|
||||
for mode in ("hard", "gradient", "interleaved"):
|
||||
name = f"invariant-transition-{recipe}-{mode}-{compiler}"
|
||||
run(name, binary, owner, recipe,
|
||||
[f"--transition-mode={mode}", "--audit-strate-transitions"],
|
||||
[lambda text: bool(re.search(
|
||||
r"^\[StrateTransitionAuditSummary\].*hole_columns=0 air_samples=0 .*status=PASS$",
|
||||
text, re.M)),
|
||||
lambda text: bool(re.search(
|
||||
r"^\[StrateTransitionNegativeControls\].*status=PASS$", text, re.M))])
|
||||
|
||||
summary = OUT / "invariant-audits.csv"
|
||||
summary.write_text(
|
||||
"run,recipe,elapsed_seconds,exit_code,status\n" +
|
||||
"".join(f"{name},{recipe},{elapsed:.3f},{code},{'PASS' if ok else 'FAIL'}\n"
|
||||
for name, recipe, elapsed, code, ok in rows),
|
||||
encoding="utf-8")
|
||||
print(f"[L33InvariantAudits] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 16 else 0)
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the L13 route parity and L9 uniform-block proof audits on L37."""
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l37"
|
||||
BINARIES = {
|
||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
||||
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l37-final-gcc",
|
||||
}
|
||||
RECIPES = ("floating-islands", "surface-world")
|
||||
rows = []
|
||||
failed = False
|
||||
|
||||
for recipe in RECIPES:
|
||||
for compiler, binary in BINARIES.items():
|
||||
name = f"legacy-route-proof-{recipe}-{compiler}"
|
||||
logfile = OUT / f"{name}.log"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "900s", str(binary), str(ROOT / "Standalone/data/owner_world.json"),
|
||||
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||
"--stream-pipeline", "--stream-hash", "--force-generate", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=-1",
|
||||
f"--recipe={recipe}", "--audit-uniform-block-proofs", "--audit-route-parity",
|
||||
]
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with logfile.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=900, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
output = logfile.read_text(encoding="utf-8", errors="replace")
|
||||
stamp = re.findall(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||
output, re.M)
|
||||
route = re.findall(r"^\[RouteParityAudit\].*$", output, re.M)
|
||||
proofs = re.findall(r"^\[UniformBlockProofAudit\].*$", output, re.M)
|
||||
stamp_ok = stamp == [(recipe, recipe, "PASS")]
|
||||
route_ok = len(route) == 1 and route[0].endswith("status=PASS")
|
||||
proof_ok = len(proofs) == 1 and bool(
|
||||
re.search(r"violations=0(?:\s|$)", proofs[0]))
|
||||
ok = (result is not None and result.returncode == 0 and stamp_ok
|
||||
and route_ok and proof_ok)
|
||||
code = result.returncode if result is not None else "TIMEOUT"
|
||||
rows.append((name, recipe, compiler, elapsed, code, route_ok, bool(proof_ok), ok))
|
||||
failed |= not ok
|
||||
print(f"[L37LegacyAuditRun] case={name} elapsed={elapsed:.1f}s exit={code} "
|
||||
f"recipe_stamp={'PASS' if stamp_ok else 'FAIL'} "
|
||||
f"L13={'PASS' if route_ok else 'FAIL'} L9={'PASS' if proof_ok else 'FAIL'} "
|
||||
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||
|
||||
summary = OUT / "legacy-audits.csv"
|
||||
summary.write_text(
|
||||
"case,recipe,compiler,elapsed_seconds,exit_code,l13,l9,status\n" +
|
||||
"".join(f"{name},{recipe},{compiler},{elapsed:.3f},{code},"
|
||||
f"{'PASS' if l13 else 'FAIL'},{'PASS' if l9 else 'FAIL'},"
|
||||
f"{'PASS' if ok else 'FAIL'}\n"
|
||||
for name, recipe, compiler, elapsed, code, l13, l9, ok in rows),
|
||||
encoding="utf-8")
|
||||
print(f"[L37LegacyAuditSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 4 else 0)
|
||||
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Freeze a 20-degree mouth-crease gate and project its sample spacing at 1440p."""
|
||||
import ast
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import io
|
||||
import math
|
||||
import re
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
BASE_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux37/RunMouthFragmentAuditL37.py"
|
||||
SPEC = importlib.util.spec_from_file_location("mouth_fragment_l37", BASE_PATH)
|
||||
BASE = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(BASE)
|
||||
|
||||
# Frozen before the L37 mouth edit: 20 degrees over the audit's 0.125 m samples is
|
||||
# the visible-crease limit. At the 6.016 m standard camera and 80-degree horizontal
|
||||
# FOV, each sample gap spans multiple pixels at 2560x1440.
|
||||
BASE.NORMAL_LIMIT_DEG = 20.0
|
||||
CAMERA = (1.691820, -3.424982, -54.224198)
|
||||
TARGET = (6.298233, 1.137233, -55.468264)
|
||||
FOV_DEG = 80.0
|
||||
WIDTH_1440P = 2560
|
||||
HEIGHT_1440P = 1440
|
||||
MOUTH = (5.600, 0.445, -55.156)
|
||||
OVERVIEW_CAMERA = (MOUTH[0] - 9.75, MOUTH[1] - 11.4, MOUTH[2] + 8.0)
|
||||
|
||||
|
||||
def normalize(vector):
|
||||
magnitude = math.sqrt(sum(value * value for value in vector))
|
||||
return tuple(value / magnitude for value in vector)
|
||||
|
||||
|
||||
def view_basis(camera, target):
|
||||
forward = normalize(tuple(target[i] - camera[i] for i in range(3)))
|
||||
right = normalize((forward[1], -forward[0], 0.0))
|
||||
up = (right[1] * forward[2] - right[2] * forward[1],
|
||||
right[2] * forward[0] - right[0] * forward[2],
|
||||
right[0] * forward[1] - right[1] * forward[0])
|
||||
return forward, right, up
|
||||
|
||||
|
||||
def visible_crease(path, camera, target):
|
||||
cx, cy = BASE.CENTER
|
||||
bounds = (cx - BASE.OUTER, cx + BASE.OUTER, cy - BASE.OUTER, cy + BASE.OUTER)
|
||||
voxel_um, triangles, _ = BASE.RING.read_scene(
|
||||
str(path), 0, BASE.HEIGHT_MIN, BASE.HEIGHT_MAX, bounds)
|
||||
cell = max(2.0, 16.0 * voxel_um / 1_000_000.0)
|
||||
buckets = BASE.RING.build_buckets(triangles, True, bounds, cell)
|
||||
forward, right, up = view_basis(camera, target)
|
||||
tan_h = math.tan(math.radians(FOV_DEG / 2.0))
|
||||
tan_v = math.tan(math.atan(tan_h * HEIGHT_1440P / WIDTH_1440P))
|
||||
focal = WIDTH_1440P / (2.0 * tan_h)
|
||||
lines = []
|
||||
count = round((BASE.OUTER - BASE.INNER) / BASE.STEP)
|
||||
for degrees in (135.0, 180.0, 225.0, 270.0, 315.0):
|
||||
angle = math.radians(degrees)
|
||||
lines.append([(cx + radius * math.cos(angle), cy + radius * math.sin(angle))
|
||||
for radius in (BASE.INNER + index * BASE.STEP
|
||||
for index in range(count + 1))])
|
||||
tangent_count = round(8.0 / BASE.STEP)
|
||||
for degrees in (225.0, 315.0):
|
||||
angle = math.radians(degrees)
|
||||
normal = (math.cos(angle), math.sin(angle))
|
||||
tangent = (-normal[1], normal[0])
|
||||
lines.append([(cx + 6.5 * normal[0] + (-4.0 + index * BASE.STEP) * tangent[0],
|
||||
cy + 6.5 * normal[1] + (-4.0 + index * BASE.STEP) * tangent[1])
|
||||
for index in range(tangent_count + 1)])
|
||||
|
||||
maximum, largest_pixels, worst_depth, visible_pairs = 0.0, 0.0, 0.0, 0
|
||||
worst_pair_pixels = 0.0
|
||||
worst_pair = None
|
||||
|
||||
def project(point):
|
||||
offset = tuple(point[i] - camera[i] for i in range(3))
|
||||
depth = sum(offset[i] * forward[i] for i in range(3))
|
||||
x = sum(offset[i] * right[i] for i in range(3))
|
||||
y = sum(offset[i] * up[i] for i in range(3))
|
||||
return depth, x, y
|
||||
|
||||
for points in lines:
|
||||
samples = [BASE.RING.sample_surface(buckets, x, y, True, cell, BASE.PREFERRED)
|
||||
for x, y in points]
|
||||
for index in range(1, len(samples)):
|
||||
before, current = samples[index - 1], samples[index]
|
||||
if before is None or current is None:
|
||||
continue
|
||||
p0 = (points[index - 1][0], points[index - 1][1], before[0])
|
||||
p1 = (points[index][0], points[index][1], current[0])
|
||||
midpoint = tuple((p0[axis] + p1[axis]) * 0.5 for axis in range(3))
|
||||
depth, horizontal, vertical = project(midpoint)
|
||||
if (depth <= 0.0 or abs(horizontal) > depth * tan_h
|
||||
or abs(vertical) > depth * tan_v):
|
||||
continue
|
||||
visible_pairs += 1
|
||||
angle = math.degrees(math.acos(max(-1.0, min(1.0,
|
||||
sum(a * b for a, b in zip(before[1], current[1]))))))
|
||||
d0, x0, y0 = project(p0)
|
||||
d1, x1, y1 = project(p1)
|
||||
pixels = math.hypot(focal * x0 / d0 - focal * x1 / d1,
|
||||
focal * y0 / d0 - focal * y1 / d1)
|
||||
largest_pixels = max(largest_pixels, pixels)
|
||||
if angle > maximum:
|
||||
maximum, worst_pair, worst_depth = angle, (p0, p1), depth
|
||||
worst_pair_pixels = pixels
|
||||
return maximum, worst_depth, largest_pixels, visible_pairs, worst_pair, worst_pair_pixels
|
||||
|
||||
|
||||
def run(path, label):
|
||||
stream = io.StringIO()
|
||||
with contextlib.redirect_stdout(stream):
|
||||
passed, maximum = BASE.audit(path)
|
||||
output = stream.getvalue()
|
||||
print(output, end="")
|
||||
match = re.search(r"worst_pair=(\([^\n]*\)) status=", output)
|
||||
worst = ast.literal_eval(match.group(1)) if match else None
|
||||
path = Path(path)
|
||||
near = visible_crease(path, CAMERA, TARGET)
|
||||
far = visible_crease(path, OVERVIEW_CAMERA, TARGET)
|
||||
print(f"[MouthCreasePixels] build={label} sample_step_m={BASE.STEP:.3f} "
|
||||
f"horizontal_fov_deg={FOV_DEG:.1f} resolution=2560x1440 "
|
||||
f"near_camera_to_mouth_m=6.016 near_visible_pairs={near[3]} "
|
||||
f"near_max_visible_deg={near[0]:.6f} near_depth_m={near[1]:.3f} "
|
||||
f"near_sample_spacing_px={near[2]:.2f} "
|
||||
f"near_worst_pair_spacing_px={near[5]:.2f} "
|
||||
f"overview_camera_to_mouth_m={math.dist(OVERVIEW_CAMERA, MOUTH):.3f} "
|
||||
f"overview_visible_pairs={far[3]} overview_max_visible_deg={far[0]:.6f} "
|
||||
f"overview_depth_m={far[1]:.3f} overview_sample_spacing_px={far[2]:.2f} "
|
||||
f"overview_worst_pair_spacing_px={far[5]:.2f} "
|
||||
f"overall_max_deg={maximum:.6f}")
|
||||
return passed, maximum
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--candidate", type=Path,
|
||||
default=ROOT / "Saved/Linux/l37/owner-fi-ground-mouth.vfmesh")
|
||||
args = parser.parse_args()
|
||||
paths = {
|
||||
"L35": ROOT / "Saved/Linux/l35/owner-fi-ground-mouth.vfmesh",
|
||||
"L36": ROOT / "Saved/Linux/l36/owner-fi-ground-mouth.vfmesh",
|
||||
"L37": args.candidate if args.candidate.is_absolute()
|
||||
else ROOT / args.candidate,
|
||||
}
|
||||
results = {label: run(path, label) for label, path in paths.items()}
|
||||
controls = (not results["L35"][0] and not results["L36"][0]
|
||||
and results["L35"][1] > 20.0 and results["L36"][1] > 20.0)
|
||||
final = results["L37"][0] and results["L37"][1] <= 20.0
|
||||
print(f"[MouthCreaseGate] normal_limit_deg=20 frozen_before_fix=true "
|
||||
f"L35_42deg_fail_control={'PASS' if controls else 'FAIL'} "
|
||||
f"L36_27deg_fail_control={'PASS' if controls else 'FAIL'} "
|
||||
f"L37_candidate={'PASS' if final else 'FAIL'} "
|
||||
f"status={'PASS' if controls and final else 'FAIL'}")
|
||||
raise SystemExit(0 if controls and final else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare the FI fade-ring crease against L35 using the same surface samples."""
|
||||
import argparse
|
||||
import importlib.util
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
RING_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||
SPEC = importlib.util.spec_from_file_location("ring_profile_l32", RING_PATH)
|
||||
RING = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(RING)
|
||||
|
||||
CENTER = (6.298233, 1.137233)
|
||||
INNER, OUTER, STEP = 5.5, 9.5, 0.125
|
||||
HEIGHT_MIN, HEIGHT_MAX, PREFERRED = -80.0, -40.0, -56.0
|
||||
# Chosen before the L37 mouth edit: a 20-degree normal jump over one 0.125 m
|
||||
# sample interval is visible at the standard camera distance.
|
||||
NORMAL_LIMIT_DEG = 20.0
|
||||
COVERAGE_MIN = 0.90
|
||||
FRAGMENT_MAX_TRIANGLES = 64
|
||||
FRAGMENT_MIN_AREA_M2 = 0.0001
|
||||
FRAGMENT_MAX_AREA_M2 = 0.05
|
||||
FRAGMENT_MAX_DISTANCE_M = 12.0
|
||||
|
||||
|
||||
def small_components(triangles):
|
||||
parent = list(range(len(triangles)))
|
||||
sizes = [1] * len(triangles)
|
||||
owners = {}
|
||||
|
||||
def find(index):
|
||||
while parent[index] != index:
|
||||
parent[index] = parent[parent[index]]
|
||||
index = parent[index]
|
||||
return index
|
||||
|
||||
def union(a, b):
|
||||
a, b = find(a), find(b)
|
||||
if a == b:
|
||||
return
|
||||
if sizes[a] < sizes[b]:
|
||||
a, b = b, a
|
||||
parent[b] = a
|
||||
sizes[a] += sizes[b]
|
||||
|
||||
for index, (_, _, vertices) in enumerate(triangles):
|
||||
for point, _ in vertices:
|
||||
key = tuple(round(value, 5) for value in point)
|
||||
other = owners.setdefault(key, index)
|
||||
union(index, other)
|
||||
|
||||
components = {}
|
||||
for index, (_, _, vertices) in enumerate(triangles):
|
||||
root = find(index)
|
||||
item = components.setdefault(root, {"count": 0, "area": 0.0, "sum": [0.0] * 3})
|
||||
item["count"] += 1
|
||||
points = [vertex[0] for vertex in vertices]
|
||||
for point in points:
|
||||
for axis in range(3):
|
||||
item["sum"][axis] += point[axis]
|
||||
ab = tuple(points[1][axis] - points[0][axis] for axis in range(3))
|
||||
ac = tuple(points[2][axis] - points[0][axis] for axis in range(3))
|
||||
cross = (ab[1] * ac[2] - ab[2] * ac[1],
|
||||
ab[2] * ac[0] - ab[0] * ac[2],
|
||||
ab[0] * ac[1] - ab[1] * ac[0])
|
||||
item["area"] += 0.5 * math.sqrt(sum(value * value for value in cross))
|
||||
|
||||
cx, cy = CENTER
|
||||
small = []
|
||||
below_floor = []
|
||||
for item in components.values():
|
||||
centroid = tuple(value / (3 * item["count"]) for value in item["sum"])
|
||||
distance = math.hypot(centroid[0] - cx, centroid[1] - cy)
|
||||
if (item["count"] <= FRAGMENT_MAX_TRIANGLES
|
||||
and item["area"] <= FRAGMENT_MAX_AREA_M2
|
||||
and distance <= FRAGMENT_MAX_DISTANCE_M):
|
||||
record = (item["count"], item["area"], distance, centroid)
|
||||
(below_floor if item["area"] < FRAGMENT_MIN_AREA_M2 else small).append(record)
|
||||
return len(components), small, below_floor
|
||||
|
||||
|
||||
def audit(path):
|
||||
cx, cy = CENTER
|
||||
bounds = (cx - OUTER, cx + OUTER, cy - OUTER, cy + OUTER)
|
||||
voxel_um, triangles, tiles = RING.read_scene(
|
||||
str(path), 0, HEIGHT_MIN, HEIGHT_MAX, bounds)
|
||||
component_count, fragments, below_floor = small_components(triangles)
|
||||
print(f"[MouthFragmentComponents] mesh={path.name} components={component_count} "
|
||||
f"small_near_mouth={len(fragments)} subthreshold_near_mouth={len(below_floor)} "
|
||||
f"limits=(triangles<={FRAGMENT_MAX_TRIANGLES},"
|
||||
f"area={FRAGMENT_MIN_AREA_M2:.4f}..{FRAGMENT_MAX_AREA_M2:.3f}m2,"
|
||||
f"distance<={FRAGMENT_MAX_DISTANCE_M:.1f}m) "
|
||||
f"first={fragments[0] if fragments else None} "
|
||||
f"subthreshold={below_floor[0] if below_floor else None} "
|
||||
f"status={'PASS' if not fragments else 'FAIL'}")
|
||||
cell = max(2.0, 16.0 * voxel_um / 1_000_000.0)
|
||||
buckets = RING.build_buckets(triangles, True, bounds, cell)
|
||||
lines = []
|
||||
count = round((OUTER - INNER) / STEP)
|
||||
for degrees in (135.0, 180.0, 225.0, 270.0, 315.0):
|
||||
angle = math.radians(degrees)
|
||||
lines.append((f"radial-{degrees:.0f}", [
|
||||
(cx + radius * math.cos(angle), cy + radius * math.sin(angle))
|
||||
for radius in (INNER + i * STEP for i in range(count + 1))]))
|
||||
tangent_count = round(8.0 / STEP)
|
||||
for degrees in (225.0, 315.0):
|
||||
angle = math.radians(degrees)
|
||||
normal = (math.cos(angle), math.sin(angle))
|
||||
tangent = (-normal[1], normal[0])
|
||||
lines.append((f"tangent-{degrees:.0f}", [
|
||||
(cx + 6.5 * normal[0] + (-4.0 + i * STEP) * tangent[0],
|
||||
cy + 6.5 * normal[1] + (-4.0 + i * STEP) * tangent[1])
|
||||
for i in range(tangent_count + 1)]))
|
||||
|
||||
max_angle = 0.0
|
||||
max_pair = None
|
||||
min_coverage = 1.0
|
||||
for name, points in lines:
|
||||
samples = [RING.sample_surface(buckets, x, y, True, cell, PREFERRED)
|
||||
for x, y in points]
|
||||
coverage = sum(value is not None for value in samples) / len(samples)
|
||||
min_coverage = min(min_coverage, coverage)
|
||||
line_max = 0.0
|
||||
for index in range(1, len(samples)):
|
||||
prior, current = samples[index - 1], samples[index]
|
||||
if prior is None or current is None:
|
||||
continue
|
||||
dot = max(-1.0, min(1.0, sum(a * b for a, b in zip(prior[1], current[1]))))
|
||||
delta = math.degrees(math.acos(dot))
|
||||
if delta > line_max:
|
||||
line_max = delta
|
||||
if delta > max_angle:
|
||||
max_angle = delta
|
||||
max_pair = (name, points[index - 1], prior[0], points[index], current[0])
|
||||
print(f"[MouthFadeLine] mesh={path.name} line={name} coverage={coverage:.3f} "
|
||||
f"max_normal_jump_deg={line_max:.6f}")
|
||||
normal_pass = max_angle <= NORMAL_LIMIT_DEG and min_coverage >= COVERAGE_MIN
|
||||
passed = normal_pass and not fragments
|
||||
print(f"[MouthFadeAudit] mesh={path.name} tiles={tiles} lines={len(lines)} "
|
||||
f"coverage_min={min_coverage:.3f} coverage_limit={COVERAGE_MIN:.2f} "
|
||||
f"max_normal_jump_deg={max_angle:.6f} normal_limit_deg={NORMAL_LIMIT_DEG:.1f} "
|
||||
f"fragments={len(fragments)} "
|
||||
f"worst_pair={max_pair} status={'PASS' if passed else 'FAIL'}")
|
||||
return passed, max_angle
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--control", type=Path,
|
||||
default=ROOT / "Saved/Linux/l35/owner-fi-ground-mouth.vfmesh")
|
||||
parser.add_argument("--prior", type=Path,
|
||||
default=ROOT / "Saved/Linux/l36/owner-fi-ground-mouth.vfmesh")
|
||||
parser.add_argument("--current", type=Path,
|
||||
default=ROOT / "Saved/Linux/l37/owner-fi-ground-mouth.vfmesh")
|
||||
args = parser.parse_args()
|
||||
control = args.control if args.control.is_absolute() else ROOT / args.control
|
||||
prior = args.prior if args.prior.is_absolute() else ROOT / args.prior
|
||||
current = args.current if args.current.is_absolute() else ROOT / args.current
|
||||
control_ok, control_max = audit(control)
|
||||
prior_ok, prior_max = audit(prior)
|
||||
current_ok, current_max = audit(current)
|
||||
controls = (not control_ok and not prior_ok
|
||||
and control_max > NORMAL_LIMIT_DEG and prior_max > NORMAL_LIMIT_DEG)
|
||||
expected = controls and current_ok and current_max <= NORMAL_LIMIT_DEG
|
||||
print(f"[MouthFadeNegativeControl] L35_max_deg={control_max:.6f} "
|
||||
f"L35_detected={not control_ok} L36_max_deg={prior_max:.6f} "
|
||||
f"L36_detected={not prior_ok} L37_max_deg={current_max:.6f} "
|
||||
f"normal_limit_deg={NORMAL_LIMIT_DEG:.1f} current_pass={current_ok} "
|
||||
f"status={'PASS' if expected else 'FAIL'}")
|
||||
raise SystemExit(0 if expected else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run recipe-stamped passage/profile audits for both compilers and both owner recipes."""
|
||||
import re
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l37"
|
||||
BINARIES = {
|
||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
||||
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l37-final-gcc",
|
||||
}
|
||||
RUNS = []
|
||||
for recipe, filename in (
|
||||
("floating-islands", "floating-islands"),
|
||||
("surface-world", "surface-world"),
|
||||
):
|
||||
for style in range(4):
|
||||
RUNS.append((f"owner-{filename}-style{style}",
|
||||
ROOT / f"Saved/Linux/linux25-owner-{filename}-style{style}.json", recipe))
|
||||
for style in range(4):
|
||||
RUNS.append((f"upper-sw-fixture-style{style}",
|
||||
OUT.parent / "l33/fixtures" / f"sw-upper-tunnel-network-style{style}.json",
|
||||
"floating-islands"))
|
||||
|
||||
RECIPE = re.compile(r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=PASS$", re.M)
|
||||
PASSAGE = re.compile(r"^\[ResolvedPassageAudit\].*walkability=PASS$", re.M)
|
||||
MOUTH = re.compile(r"^\[PassageMouthProfileAudit\].*status=PASS$", re.M)
|
||||
ROUTE = re.compile(r"^\[ResolvedStrateRouteAudit\].*mouth_binding=PASS status=PASS$", re.M)
|
||||
CHECKS = (
|
||||
r"^\[ResolvedLayoutStrict\] accepted=true issues=0 error=$",
|
||||
r"^\[ResolvedLayoutValidatorControls\].*status=PASS$",
|
||||
r"^\[PassageMouthProfileNegativeControl\].*source=L23_terraced_geometry.*detected=true$",
|
||||
r"^\[PassageMouthProfileSummary\].*l23_terrace_negative_control=DETECTED status=PASS$",
|
||||
r"^\[PassageNaturalnessSummary\].*l23_negative_control=DETECTED.*status=PASS$",
|
||||
r"^\[PassageRippleAuditSummary\].*synthetic_spheres_negative_control=DETECTED max_amplitude_m=0\.035 floor_beaded_negative_control=DETECTED max_floor_amplitude_m=0\.015 status=PASS$",
|
||||
r"^\[PassageOpenAirAuditSummary\].*violations=0.*l24_negative_control=DETECTED status=PASS$",
|
||||
r"^\[PassageNoLongBridgeAudit\].*status=PASS$",
|
||||
r"^\[PassageRockColumnAuditSummary\].*status=PASS$",
|
||||
r"^\[ResolvedPassageAuditSummary\].*route_binding_failures=0 .*status=PASS$",
|
||||
r"^\[Walkability\] PASS",
|
||||
r"^\[FloorProfileBlend\].*negative_controls=step_detected/slope_detected status=PASS$",
|
||||
)
|
||||
|
||||
rows = []
|
||||
failed = False
|
||||
for compiler, binary in BINARIES.items():
|
||||
for name, data, recipe in RUNS:
|
||||
logfile = OUT / f"passage-audit-{name}-{compiler}.log"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", str(binary), str(data), "--workers=12",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
|
||||
f"--recipe={recipe}", "--audit-resolved-layout-passages",
|
||||
]
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with logfile.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
output = logfile.read_text(encoding="utf-8", errors="replace")
|
||||
stamp_ok = RECIPE.findall(output) == [(recipe, recipe)]
|
||||
passages = len(PASSAGE.findall(output))
|
||||
mouths = len(MOUTH.findall(output))
|
||||
routes = len(ROUTE.findall(output))
|
||||
checks_ok = all(re.search(pattern, output, re.M) for pattern in CHECKS)
|
||||
ok = (result is not None and result.returncode == 0 and stamp_ok
|
||||
and passages >= 1 and mouths == passages and routes >= 1 and checks_ok)
|
||||
code = result.returncode if result is not None else "TIMEOUT"
|
||||
rows.append((name, recipe, compiler, elapsed, code, passages, mouths, routes, ok))
|
||||
failed |= not ok
|
||||
print(f"[L37PassageAuditRun] case={name} compiler={compiler} recipe={recipe} "
|
||||
f"elapsed={elapsed:.1f}s exit={code} passages={passages} "
|
||||
f"profiles={mouths} routes={routes} {'PASS' if ok else 'FAIL'} "
|
||||
f"log={logfile}", flush=True)
|
||||
|
||||
summary = OUT / "passage-audits.csv"
|
||||
summary.write_text(
|
||||
"case,recipe,compiler,elapsed_seconds,exit_code,passages,mouth_profiles,routes,status\n" +
|
||||
"".join(f"{name},{recipe},{compiler},{elapsed:.3f},{code},{passages},{mouths},{routes},"
|
||||
f"{'PASS' if ok else 'FAIL'}\n"
|
||||
for name, recipe, compiler, elapsed, code, passages, mouths, routes, ok in rows),
|
||||
encoding="utf-8")
|
||||
print(f"[L37PassageAuditSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 24 else 0)
|
||||
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export v24 ring meshes at matching LOD windows and retain old controls."""
|
||||
import csv
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l37"
|
||||
BIN = OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang"
|
||||
OWNER = ROOT / "Standalone/data/owner_world.json"
|
||||
TERRACE = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||
RING_SET = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingSetL32.py"
|
||||
STAMP = re.compile(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=PASS$", re.M)
|
||||
EXPORT = re.compile(r"^\[MeshSceneExport\].*status=PASS", re.M)
|
||||
TERRACE_RESULT = re.compile(r"^\[RingTerraceAudit\].*status=(PASS|FAIL)$", re.M)
|
||||
SET_RESULT = re.compile(r"^\[RingSetAudit\].*status=(PASS|FAIL)$", re.M)
|
||||
rows = []
|
||||
failed = False
|
||||
|
||||
FI_CENTERS = ((56, -72), (72, -72), (56, -88), (40, -72), (24, 24),
|
||||
(104, 24), (72, -88), (120, 24), (104, 8), (24, -24))
|
||||
SW_CENTERS = ((-136, 136), (-56, 152), (-152, 120), (-136, 120),
|
||||
(-152, 104), (-136, 104), (-40, 152))
|
||||
LOD_CENTERS = {
|
||||
"fi-floor": {
|
||||
2: (16, -144), 3: (224, -160), 4: (-448, 448),
|
||||
5: (-128, -640), 6: (-1792, 1280), 7: (3584, 1536),
|
||||
8: (3072, 7168),
|
||||
},
|
||||
"sw-ceiling": {
|
||||
2: (16, -144), 3: (224, -160), 4: (-448, 448),
|
||||
5: (-128, -640), 6: (-1792, 1280), 7: (2560, -512),
|
||||
8: (3072, 7168),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def run(name, command):
|
||||
log = OUT / f"ring-{name}.log"
|
||||
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", *map(str, command)]
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(wrapped, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
code = result.returncode if result is not None else "TIMEOUT"
|
||||
print(f"[L37RingStep] name={name} elapsed={elapsed:.1f}s exit={code} log={log}",
|
||||
flush=True)
|
||||
return result, output, elapsed
|
||||
|
||||
|
||||
def export(label, recipe, max_level, camera):
|
||||
mesh = OUT / f"ring-owner-{label}-v24.vfmesh"
|
||||
command = [BIN, OWNER, "--workers=12",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=3", "--max-clip-level=8",
|
||||
f"--max-level={max_level}", f"--recipe={recipe}"]
|
||||
if camera:
|
||||
command.append(f"--camera={camera}")
|
||||
command.append(f"--mesh-export={mesh}")
|
||||
result, output, elapsed = run(f"export-{label}", command)
|
||||
stamps = STAMP.findall(output)
|
||||
ok = (result is not None and result.returncode == 0
|
||||
and stamps == [(recipe, recipe)] and EXPORT.search(output) and mesh.is_file())
|
||||
print(f"[L37RingExport] scene={label} recipe={recipe} stamp={stamps} "
|
||||
f"bytes={mesh.stat().st_size if mesh.exists() else 0} "
|
||||
f"status={'PASS' if ok else 'FAIL'}", flush=True)
|
||||
return mesh, elapsed, ok
|
||||
|
||||
|
||||
def terrace(label, recipe, surface, mesh, lod, height_min, height_max,
|
||||
center=(64, -32), radius=32):
|
||||
result, output, elapsed = run(f"{label}-lod{lod}", ["python3", TERRACE, mesh,
|
||||
f"--surface={surface}", "--center", str(center[0]), str(center[1]),
|
||||
f"--radius={radius}",
|
||||
f"--lod={lod}", f"--height-min-m={height_min}",
|
||||
f"--height-max-m={height_max}"])
|
||||
statuses = TERRACE_RESULT.findall(output)
|
||||
ok = (result is not None and result.returncode == 0 and statuses == ["PASS"])
|
||||
rows.append((label, recipe, surface, lod, elapsed, ok))
|
||||
print(f"[L37RingAuditRun] scene={label} recipe={recipe} surface={surface} "
|
||||
f"lod={lod} center={center} radius={radius} "
|
||||
f"status={'PASS' if ok else 'FAIL'}", flush=True)
|
||||
return ok
|
||||
|
||||
|
||||
def lod1_set(label, recipe, surface, mesh, centers, height_min, height_max):
|
||||
command = ["python3", RING_SET, mesh, f"--surface={surface}"]
|
||||
for x, y in centers:
|
||||
command.extend(("--center", str(x), str(y)))
|
||||
command.extend(("--radius=8", "--lod=1", f"--height-min-m={height_min}",
|
||||
f"--height-max-m={height_max}"))
|
||||
result, output, elapsed = run(f"{label}-lod1-set", command)
|
||||
statuses = SET_RESULT.findall(output)
|
||||
ok = (result is not None and result.returncode == 0 and statuses == ["PASS"])
|
||||
rows.append((label, recipe, surface, 1, elapsed, ok))
|
||||
print(f"[L37RingAuditRun] scene={label} recipe={recipe} surface={surface} "
|
||||
f"lod=1 windows={len(centers)} status={'PASS' if ok else 'FAIL'}",
|
||||
flush=True)
|
||||
return ok
|
||||
|
||||
|
||||
for label, recipe, surface, window_min, window_max, camera0, camera1, centers in (
|
||||
("fi-floor", "floating-islands", "floor", -122.0, -117.0,
|
||||
"64,-32,-115,0,-45", "64,-32,-115,0,-45", FI_CENTERS),
|
||||
("sw-ceiling", "surface-world", "ceiling", -132.0, -124.0,
|
||||
"64,-32,-136,0,45", "-96,128,-136,0,45", SW_CENTERS)):
|
||||
low_mesh, _, low_ok = export(f"{label}-lod0", recipe, 0, camera0)
|
||||
failed |= not low_ok
|
||||
if low_ok:
|
||||
failed |= not terrace(label, recipe, surface, low_mesh, 0, window_min, window_max)
|
||||
|
||||
detail_mesh, _, detail_ok = export(f"{label}-lod1", recipe, 1, camera1)
|
||||
failed |= not detail_ok
|
||||
if detail_ok:
|
||||
set_min, set_max = ((-121.625, -117.375) if label == "fi-floor"
|
||||
else (-128.375, -124.125))
|
||||
failed |= not lod1_set(label, recipe, surface, detail_mesh, centers,
|
||||
set_min, set_max)
|
||||
|
||||
full_mesh, _, full_ok = export(f"{label}-lod2-8", recipe, 8, None)
|
||||
failed |= not full_ok
|
||||
if full_ok:
|
||||
for lod in range(2, 9):
|
||||
center = LOD_CENTERS[label][lod]
|
||||
radius = 4 * (1 << lod)
|
||||
failed |= not terrace(label, recipe, surface, full_mesh, lod,
|
||||
window_min, window_max, center, radius)
|
||||
|
||||
# Keep the established L30 terrace and L31 flat-surface negative controls live.
|
||||
controls = (
|
||||
("l30-terrace", ROOT / "Saved/Linux/l32/fi-floor-l30-camera-negative.vfmesh",
|
||||
-130.0, -110.0),
|
||||
("l31-flat", ROOT / "Saved/Linux/l31/fi-floor-v19.vfmesh", -122.0, -117.0),
|
||||
)
|
||||
for name, mesh, height_min, height_max in controls:
|
||||
result, output, _ = run(f"control-{name}", ["python3", TERRACE, mesh,
|
||||
"--surface=floor", "--center", "64", "-32", "--radius=32", "--lod=0",
|
||||
f"--height-min-m={height_min}", f"--height-max-m={height_max}"])
|
||||
statuses = TERRACE_RESULT.findall(output)
|
||||
detected = (result is not None and result.returncode == 1 and statuses == ["FAIL"])
|
||||
failed |= not detected
|
||||
print(f"[L37RingControl] name={name} detected={detected} "
|
||||
f"status={'PASS' if detected else 'FAIL'}", flush=True)
|
||||
|
||||
summary = OUT / "ring-audits-l37.csv"
|
||||
with summary.open("w", newline="", encoding="utf-8") as stream:
|
||||
writer = csv.writer(stream)
|
||||
writer.writerow(("scene", "recipe", "surface", "lod", "elapsed_seconds", "status"))
|
||||
writer.writerows((label, recipe, surface, lod, f"{elapsed:.3f}",
|
||||
"PASS" if ok else "FAIL")
|
||||
for label, recipe, surface, lod, elapsed, ok in rows)
|
||||
print(f"[L37RingSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||
f"fail={sum(not row[-1] for row in rows)} controls=2 "
|
||||
f"status={'FAIL' if failed else 'PASS'} summary={summary}", flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 18 else 0)
|
||||
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 25 KiB |