Band faces now use signed distance to the displaced solid intervals, continuous across bands, so MC interpolates the interface instead of terracing at density levels. The Gradient amplitude is restored to width/8 (about 4 m peak-to-peak at 16 m). Ring/terrace audit at LOD0-8 on the FI floor and SW ceiling passes; negative controls: L30 (rings) and L31 (flat) both fail. Hole/interface audits, 16 passage rows and 24/24 determinism pass. Evaluator 20. Cost vs L31 SW +1.9%, FI +1.5%. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX
119 lines
5.8 KiB
Python
119 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Audit the displaced floor across the passage-mouth fade edge."""
|
|
import argparse
|
|
import math
|
|
import resource
|
|
import sys
|
|
|
|
import AuditRingTerraceL32 as ring
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("scene")
|
|
parser.add_argument("--center", nargs=2, type=float, required=True)
|
|
parser.add_argument("--lod", type=int, default=0)
|
|
parser.add_argument("--inner-radius-m", type=float, default=5.5)
|
|
parser.add_argument("--outer-radius-m", type=float, default=9.5)
|
|
parser.add_argument("--step-m", type=float, default=0.125)
|
|
parser.add_argument("--height-min-m", type=float, default=-122.5)
|
|
parser.add_argument("--height-max-m", type=float, default=-117.5)
|
|
parser.add_argument("--preferred-height-m", type=float, default=-119.5)
|
|
parser.add_argument("--highest-surface", action="store_true",
|
|
help="choose the topmost upward face inside the height window")
|
|
parser.add_argument("--plateau-min-m", type=float, default=0.5)
|
|
parser.add_argument("--step-threshold-m", type=float, default=0.1)
|
|
parser.add_argument("--normal-jump-max-deg", type=float, default=20.0)
|
|
args = parser.parse_args()
|
|
if (args.inner_radius_m < 0.0 or args.outer_radius_m <= args.inner_radius_m
|
|
or args.step_m <= 0.0):
|
|
parser.error("invalid radial interval or sample spacing")
|
|
|
|
cx, cy = args.center
|
|
preferred_height = None if args.highest_surface else args.preferred_height_m
|
|
bounds = (cx - args.outer_radius_m, cx + args.outer_radius_m,
|
|
cy - args.outer_radius_m, cy + args.outer_radius_m)
|
|
voxel_um, triangles, selected_tiles = ring.read_scene(
|
|
args.scene, args.lod, args.height_min_m, args.height_max_m, bounds)
|
|
bucket_size = max(2.0, 16.0 * voxel_um / 1_000_000.0 * (1 << args.lod))
|
|
buckets = ring.build_buckets(triangles, True, bounds, bucket_size)
|
|
|
|
count = round((args.outer_radius_m - args.inner_radius_m) / args.step_m)
|
|
lines = []
|
|
for angle_degrees in (135.0, 180.0, 225.0, 270.0, 315.0):
|
|
angle = math.radians(angle_degrees)
|
|
points = [(cx + radius * math.cos(angle), cy + radius * math.sin(angle))
|
|
for radius in (args.inner_radius_m + i * args.step_m
|
|
for i in range(count + 1))]
|
|
lines.append((f"radial-{angle_degrees:.0f}deg", points))
|
|
|
|
# Tangential sections stay outside the open mouth while crossing the outer
|
|
# fade edge on both sides of the 6.5 m center radius.
|
|
tangent_count = round(8.0 / args.step_m)
|
|
tangent_values = [-4.0 + i * args.step_m for i in range(tangent_count + 1)]
|
|
for angle_degrees in (225.0, 315.0):
|
|
angle = math.radians(angle_degrees)
|
|
normal = (math.cos(angle), math.sin(angle))
|
|
tangent = (-normal[1], normal[0])
|
|
points = [(cx + 6.5 * normal[0] + t * tangent[0],
|
|
cy + 6.5 * normal[1] + t * tangent[1])
|
|
for t in tangent_values]
|
|
lines.append((f"straight-tangent-{angle_degrees:.0f}deg", points))
|
|
|
|
failed = False
|
|
max_plateau = 0.0
|
|
max_step = 0.0
|
|
max_normal = 0.0
|
|
min_coverage = 1.0
|
|
for name, points in lines:
|
|
profile = ring.audit_profile(points, buckets, True, args.plateau_min_m,
|
|
0.0005, args.step_threshold_m, bucket_size, preferred_height)
|
|
samples = [ring.sample_surface(buckets, x, y, True, bucket_size,
|
|
preferred_height)
|
|
for x, y in points]
|
|
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 > args.normal_jump_max_deg:
|
|
px, py = points[index - 1]
|
|
x, y = points[index]
|
|
print(f"[PassageFadeNormalJump] line={name} "
|
|
f"radius_m={math.hypot(x-cx,y-cy):.3f} "
|
|
f"from_m=({px:.3f},{py:.3f},{prior[0]:.3f}) "
|
|
f"to_m=({x:.3f},{y:.3f},{current[0]:.3f}) "
|
|
f"normal_delta_deg={delta:.3f}")
|
|
coverage = profile["coverage"]
|
|
min_coverage = min(min_coverage, coverage)
|
|
plateau = max((run[0] for run in profile["plateaus"]), default=0.0)
|
|
step = max((run[1] for run in profile["plateaus"]), default=0.0)
|
|
angle = profile["max_angle"]
|
|
max_plateau = max(max_plateau, plateau)
|
|
max_step = max(max_step, step)
|
|
max_normal = max(max_normal, angle)
|
|
ok = (profile["valid"] and plateau < args.plateau_min_m
|
|
and angle <= args.normal_jump_max_deg)
|
|
failed |= not ok
|
|
print(f"[PassageFadeProfile] line={name} coverage={coverage:.3f} "
|
|
f"height_range_m={profile.get('height_range', 0.0):.6f} "
|
|
f"flat_step_runs={len(profile['plateaus'])} "
|
|
f"max_normal_delta_deg={angle:.6f} status={'PASS' if ok else 'FAIL'}")
|
|
|
|
passed = not failed and min_coverage >= 0.95
|
|
print(f"[PassageFadeRingAudit] scene={args.scene} lod={args.lod} "
|
|
f"center_m=({cx:.6f},{cy:.6f}) mouth_radius_m=4.500 "
|
|
f"fade_m=5.500..7.500 radial_m={args.inner_radius_m:.3f}.."
|
|
f"{args.outer_radius_m:.3f} lines={len(lines)} selected_tiles={selected_tiles} "
|
|
f"coverage_min={min_coverage:.3f} longest_flat_run_m={max_plateau:.6f} "
|
|
f"following_step_m={max_step:.6f} max_normal_delta_deg={max_normal:.6f} "
|
|
f"thresholds=(coverage>=0.950,plateau<0.500,step>=0.100,normal<=20.000) "
|
|
f"status={'PASS' if passed else 'FAIL'}")
|
|
print(f"[PassageFadeRSS] peak_rss_kib={resource.getrusage(resource.RUSAGE_SELF).ru_maxrss}")
|
|
raise SystemExit(0 if passed else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|