diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 0adffd3..1136ca3 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2110,6 +2110,34 @@ when SurfaceWorld is the upper strate. No evaluator or recipe version changed. [`Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md) records the four-style audits, captures, determinism, owner hashes, costs and retries. +### 8.48 Irregular passage-wall relief and grounded tread shoulders — World Assembler M3 L35 + +L34's owner-SW profile had a 7.438 m radius repeat with 0.0361 m (3.61 cm) amplitude; an independent +mesh-normal scan also found a 4.750 m period at 0.02682 (1.537°). The geometry came from piecewise +capsule joins, while broad value-noise bands added a second regular pattern. The shared carve now +uses Catmull–Rom centerline interpolation on eligible interior segments, excluding short spans and +terminal controls. Passage walls use deterministic, rotated 43/29/19 m relief with reduced terrain +band contribution; other `VF_FieldSurfaceRelief` users, including grounded bridge columns, are +unchanged. The shared evaluator is version 21. + +The residual radius scan on owner-SW style 0 measures a 7.062 m period at 0.0109 m (1.09 cm), +with 0.0044 m (0.44 cm) coherent amplitude. The independent L35 mesh-normal scan passes at 8 m +and 0.02496 axial amplitude, below its 0.0255 threshold. The ripple audit samples both side-wall +radii every 0.125 m in 32 m windows, scans 0.5–8 m periods, and rejects amplitudes of 0.035 m or +more. Its synthetic control uses 2 m radius spheres spaced every 1.5 m; it measures 0.0599 m and +must fail. + +At the FI mouth, the flat apron is the shared passage support tread, not `LandingExtensionQ`, a tile +edge or a separate support layer. The tread is required by capsule and support probes. L35 gives its +outer entry shoulder a shallow taper and seeded grain while preserving the supported walk core and +lower-mouth overlap width. The shallow quarter-weight support-height/radius spline keeps the tread +continuous without moving its XY footprint. + +Cascade routes also replace identical full-depth cosine lobes with a seeded shallow profile on each +turn. Passage, capsule, support, open-air, bridge, binding, strict layout, capture, determinism, ring, +and cost evidence are recorded in +[`Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md). + ## 9. Multiplayer model (listen-server first, dedicated-friendly) > **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a diff --git a/CODEMAP.md b/CODEMAP.md index 57a98d6..1d00b64 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -1243,6 +1243,18 @@ The owner SurfaceWorld hashes match L32 and evaluator/recipe versions are unchan ARCHITECTURE §8.47 and [`Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux33/REPORT.md). +### 3.31 Passage-wall ripple and FI tread-shoulder audit — World Assembler M3 L35 + +| Symbol | Canonical file | Role | +|---|---|---| +| `VF_FieldPassageWallRelief` | `Standalone/Core/src/VoxelForgeFieldShared.h` | Supplies passage-only irregular wall relief at rotated 43/29/19 m scales. Other `VF_FieldSurfaceRelief` users stay unchanged. | +| `ApplyResolvedStratePassageCarves` | `Standalone/Core/src/ShapeWorldInternal.h` | Catmull–Rom smooths eligible interior sweep joins, applies passage-only wall relief, and tapers/grains the entry support shoulder below the capsule/support probe footprint. | +| `PlanResolvedLayoutPassagesV1` | `Standalone/Core/src/ResolvedLayoutInternal.h` | Gives each Cascade turn a seeded shallow radial profile instead of identical repeated full-depth cosine lobes. | +| `auditPassageRipples` / `PassageRippleAudit` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples field wall radius at 0.125 m spacing, scans periods 0.5–8 m in 32 m windows, and rejects amplitudes ≥0.035 m. The negative control uses 2 m radius spheres at 1.5 m spacing. | + +`WorldAssemblyEvaluatorVersion=21`. See ARCHITECTURE §8.48 and +[`Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md). + ## 4. The density pipeline (most-edited hot path) ### 4.1 `GetDensityWithParams` (TunnelNetwork) — VoxelGenerator.cpp:3749 diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 508d7ad..8216b8b 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -251,7 +251,7 @@ namespace VoxelForgeCore inline constexpr std::uint32_t WorldAssemblySchemaVersion = 2; // Version 14 adds shared passage columns; version 16 smooths owner Gradient faces; // version 20 restores full Gradient amplitude and uses a continuous signed-distance band union. - inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 20; + inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 21; enum class FieldChannel : std::uint32_t { diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h index f136d08..e4c0aac 100644 --- a/Standalone/Core/src/ResolvedLayoutInternal.h +++ b/Standalone/Core/src/ResolvedLayoutInternal.h @@ -965,7 +965,32 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t const double ledge=std::min(ringRadius/4, static_cast(cfg?std::max(0, cfg->cascadeLedgeSubvoxels):0)); - radius-=ledge*(0.5-0.5*std::cos(angle*steps)); + // Give each turn a seeded, shallow angular profile so the same cosine + // scallop cannot repeat at full ledge depth on every revolution. + const std::uint32_t controlCount=std::clamp(steps*2u,8u, + segments/2u); + const double coordinate=angle*controlCount + /(2.0*3.14159265358979323846); + const std::uint32_t control0=static_cast(std::floor(coordinate)); + const std::uint32_t control1=(control0+1u)%controlCount; + const double blend=coordinate-std::floor(coordinate); + const auto controlValue=[&](std::uint32_t control) + { + const std::uint32_t salt=0x43415343u + ^(loopIndex*0x9e3779b9u)^(control*0x85ebca6bu); + const std::uint32_t bits=static_cast(PassageSeed(seed, + upper.strateIndex,lower.strateIndex,connection,salt)>>40); + return 2.0*static_cast(bits)/16777215.0-1.0; + }; + const double smooth=blend*blend*(3.0-2.0*blend); + const double irregular=std::lerp(controlValue(control0), + controlValue(control1),smooth); + const std::uint32_t loopHash=static_cast(PassageSeed(seed, + upper.strateIndex,lower.strateIndex,connection, + 0x4c454447u+loopIndex)>>40); + const double amplitude=0.60+0.40 + *static_cast(loopHash)/16777215.0; + radius-=ledge*amplitude*(0.24+0.10*irregular); } if(pathStyle!=1u&&anchoredIslandPassage&&passageAnchor) { diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 8db109d..d30f942 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -911,7 +911,34 @@ public: DeterministicSqrt(playerDx*playerDx+playerDy*playerDy +playerVertical*playerVertical)-0.34*qPerMetre); } - const double cx=ax+t*dx,cy=ay+t*dy,cz=az+t*dz; + double cx=ax+t*dx,cy=ay+t*dy,cz=az+t*dz; + const bool smoothCarveJoin=index>2&&index+12(index-1)]; + const auto& p3=passage.controlPoints[static_cast(index+2)]; + constexpr double SmoothJoinWeight=1.0; + const double t2=t*t,t3=t2*t; + for(std::size_t axis=0;axis<3;++axis) + { + const double v0=static_cast(p0.positionSubvoxels[axis]); + const double v1=static_cast(a.positionSubvoxels[axis]); + const double v2=static_cast(b.positionSubvoxels[axis]); + const double v3=static_cast(p3.positionSubvoxels[axis]); + const double curve=0.5*((2.0*v1)+(-v0+v2)*t + +(2.0*v0-5.0*v1+4.0*v2-v3)*t2 + +(-v0+3.0*v1-3.0*v2+v3)*t3); + const double line=axis==0?cx:axis==1?cy:cz; + const double blended=line+(curve-line)*SmoothJoinWeight; + if(axis==0)cx=blended; + else if(axis==1)cy=blended; + else cz=blended; + } + } const double ex=qx-cx,ey=qy-cy,ez=qz-cz; const double radius=static_cast(a.radiusSubvoxels) +t*(b.radiusSubvoxels-a.radiusSubvoxels); @@ -963,11 +990,11 @@ public: ?std::clamp(-(qz-(nearestA.positionSubvoxels[2]+bestT*( static_cast(nearestB.positionSubvoxels[2])-nearestA.positionSubvoxels[2]))) /distanceToCenter,0.0,1.0):0.0; - const float relief=VF_FieldSurfaceRelief(static_cast(X),static_cast(Y), + const float relief=VF_FieldPassageWallRelief(static_cast(X),static_cast(Y), static_cast(Z),static_cast(Owner?Owner->Seed:0)); const double wallReliefWeight=1.0-0.80*verticalWallAlignment; const double roughSignedDistance=bestSignedDistance - +static_cast(std::clamp(relief,-1.0f,1.0f))*0.24*qPerMetre*wallReliefWeight; + +static_cast(std::clamp(relief,-1.0f,1.0f))*0.21*qPerMetre*wallReliefWeight; if(IsPassageShortSpanSegment(passageIndex,static_cast(bestIndex))) { const double bridgeDistance=PassageBridgeSignedDistance(passageIndex, @@ -986,6 +1013,7 @@ public: const double dy=static_cast(segmentB.positionSubvoxels[1])-segmentA.positionSubvoxels[1]; const double radiusA=segmentA.radiusSubvoxels; const double radiusB=segmentB.radiusSubvoxels; + const bool entryMouthShoulder=index<=2; // Keep the tread within the probe band and below the capsule's // lowest sampled point. const double floorOffset=IsPassageShortSpanSegment(passageIndex, @@ -1000,10 +1028,33 @@ public: :bestT; const double centerX=segmentA.positionSubvoxels[0]+supportT*dx; const double centerY=segmentA.positionSubvoxels[1]+supportT*dy; - const double centerZ=segmentA.positionSubvoxels[2]+supportT*( + double centerZ=segmentA.positionSubvoxels[2]+supportT*( static_cast(segmentB.positionSubvoxels[2])-segmentA.positionSubvoxels[2]); - const double radius=segmentA.radiusSubvoxels+supportT*( + double radius=segmentA.radiusSubvoxels+supportT*( static_cast(segmentB.radiusSubvoxels)-segmentA.radiusSubvoxels); + const bool smoothSupportJoin=index>0&&index+12(index-1)) + &&!IsPassageShortSpanSegment(passageIndex,static_cast(index)) + &&!IsPassageShortSpanSegment(passageIndex,static_cast(index+1)); + if(smoothSupportJoin) + { + const auto& p0=passage.controlPoints[static_cast(index-1)]; + const auto& p3=passage.controlPoints[static_cast(index+2)]; + const double t2=supportT*supportT,t3=t2*supportT; + const auto interpolate=[&](double v0,double v1,double v2,double v3) + { + return 0.5*((2.0*v1)+(-v0+v2)*supportT + +(2.0*v0-5.0*v1+4.0*v2-v3)*t2 + +(-v0+3.0*v1-3.0*v2+v3)*t3); + }; + constexpr double SmoothSupportHeightWeight=0.25; + centerZ+=(interpolate(p0.positionSubvoxels[2],segmentA.positionSubvoxels[2], + segmentB.positionSubvoxels[2],p3.positionSubvoxels[2])-centerZ) + *SmoothSupportHeightWeight; + radius+=(interpolate(p0.radiusSubvoxels,segmentA.radiusSubvoxels, + segmentB.radiusSubvoxels,p3.radiusSubvoxels)-radius) + *SmoothSupportHeightWeight; + } const double horizontalX=qx-centerX,horizontalY=qy-centerY; const double horizontalDistance=DeterministicSqrt(horizontalX*horizontalX +horizontalY*horizontalY); @@ -1016,16 +1067,42 @@ public: const bool fallbackSupport=qz>=supportPlayerCenterZ-1.50*qPerMetre; const double supportThicknessVoxels=fallbackSupport ?qPerMetre/Q:0.5*qPerMetre/Q; - const double floorQ=centerZ-radius+floorOffset*qPerMetre; - const double floorVoxels=floorQ/Q; // Keep the supported ramp to a comfortable walking width. Extending a // full bore-radius shelf across overlapping helix coils can block the // capsule on the neighbouring turn. const bool lowerMouthBodyOverlap=index>=lowerMouthIndex-2 &&supportQueryDepth<0.98*qPerMetre; + const double supportEdgeNoise=entryMouthShoulder?std::clamp( + static_cast(VF_FieldPassageGroundGrain(static_cast(X), + static_cast(Y), + static_cast(Owner?Owner->Seed:0))),-1.0,1.0):0.0; + const double supportHalfWidthMetres=lowerMouthBodyOverlap?0.90 + :entryMouthShoulder?1.20+0.20*supportEdgeNoise:1.25; const double supportHalfWidth=std::min(radius, - (lowerMouthBodyOverlap?0.90:1.25)*qPerMetre); - if(ZfloorVoxels+1.0/Q)return; + supportHalfWidthMetres*qPerMetre); + // Keep the walk core within a 0.50 m span with shallow relief, then + // taper the outer shoulder; capsule and probe widths remain supported. + const double supportEdgeCoreMetres=entryMouthShoulder?0.60:0.90; + const double supportEdgeBlendMetres=std::min( + entryMouthShoulder?0.85:0.35, + std::max(0.0,supportHalfWidth/qPerMetre-supportEdgeCoreMetres)); + const double supportEdgeT=supportEdgeBlendMetres>1.0e-6 + ?std::clamp((horizontalDistance + -(supportHalfWidth-supportEdgeBlendMetres*qPerMetre)) + /(supportEdgeBlendMetres*qPerMetre),0.0,1.0):0.0; + const double supportEdgeSmooth=supportEdgeT*supportEdgeT + *(3.0-2.0*supportEdgeT); + const double localSupportThicknessVoxels=entryMouthShoulder + ?supportThicknessVoxels*(1.0-0.72*supportEdgeSmooth) + :supportThicknessVoxels; + const double supportEdgeDepth=entryMouthShoulder + ?0.35+0.15*supportEdgeNoise:0.35; + const double floorQ=centerZ-radius+floorOffset*qPerMetre + +supportEdgeNoise*(horizontalDistance>0.60*qPerMetre + ?0.35:0.14)*qPerMetre + -supportEdgeSmooth*supportEdgeDepth*qPerMetre; + const double floorVoxels=floorQ/Q; + if(ZfloorVoxels+1.0/Q)return; if(horizontalDistance>supportHalfWidth)return; const double supportDistanceSq=horizontalDistance*horizontalDistance +(Z-floorVoxels)*(Z-floorVoxels); @@ -1034,7 +1111,7 @@ public: hasSupportFloor=true; bestSupportSegmentIndex=index; bestSupportDensity=std::clamp(Z-floorVoxels, - -supportThicknessVoxels,0.0); + -localSupportThicknessVoxels,0.0); }; std::ptrdiff_t supportCenterIndex=bestIndex; if(bestIndex= PLATEAU and step_size >= STEP: + max_flat = max(max_flat, flat_length) + max_step = max(max_step, step_size) + step_runs.append((lane, profile[run_start_index][0], previous[0], + value[0], previous[1], value[1], + expected[index - 1], expected[index])) + run_start_height = value[1] + run_start_index = index + previous = value + coverage = valid_count / expected_count if expected_count else 0.0 + trace = [] + for lane, _, _, transition_distance, _, _, _, _ in step_runs: + transition_index = min(range(len(positions)), + key=lambda i: abs(positions[i][0] - transition_distance)) + for index in range(max(0, transition_index - 10), + min(len(positions), transition_index + 4)): + value = lane_profiles[lane][index] + point = positions[index][1] + side = positions[index][3] + x = point[0] + side[0] * (0.75 if lane == 1 else -0.75 if lane == 2 else 0.0) + y = point[1] + side[1] * (0.75 if lane == 1 else -0.75 if lane == 2 else 0.0) + trace.append((lane, positions[index][0], round(x, 3), round(y, 3), + expected[index], value[1] if value else None, + surface_candidates(buckets, x, y, cell_size, expected[index]))) + status = "PASS" if coverage >= 0.75 and max_flat == 0.0 else \ + "NO_COVERAGE" if coverage < 0.75 else "FAIL" + return { + "mesh": mesh_path.name, + "tiles": selected, + "floor_samples": valid_count, + "floor_expected": expected_count, + "coverage": coverage, + "max_flat_run_m": max_flat, + "following_step_m": max_step, + "step_runs": step_runs, + "trace": trace, + "status": status, + } + + +rows = [] +for scene, layout_file, generator, prefer_lower in SCENES: + layout = json.loads((OUT / layout_file).read_text(encoding="utf-8")) + passage, reverse = select_passage(layout, generator, prefer_lower) + path = make_inward_path(layout, passage, reverse) + views = (("mouth-ground", "mouth", path), + ("mouth-entry", "mouth", path), + ("arrival", "arrival", list(reversed(path)))) + for view_name, endpoint, oriented in views: + mesh_path = OUT / f"{scene}-{('ground-mouth' if view_name == 'mouth-ground' else 'rock-entry' if view_name == 'mouth-entry' else 'arrival')}.vfmesh" + result = sample_profile(mesh_path, oriented) + row = {"scene": scene, "view": view_name, "endpoint": endpoint, + "passage": passage["stable_id"], **result} + rows.append(row) + print(f"[L35MouthMeshProfile] scene={scene} view={view_name} endpoint={endpoint} " + f"passage={passage['stable_id']} mesh={result['mesh']} " + f"tiles={result['tiles']} floor_samples={result['floor_samples']}/" + f"{result['floor_expected']} coverage={result['coverage']:.3f} " + f"flat_run_m={result['max_flat_run_m']:.3f} " + f"following_step_m={result['following_step_m']:.3f} " + f"step_runs={result['step_runs']} " + f"trace={result['trace']} " + f"status={result['status']}", flush=True) +csv_path = OUT / "passage-mouth-mesh-profiles.csv" +with csv_path.open("w", newline="", encoding="utf-8") as stream: + writer = csv.DictWriter(stream, fieldnames=list(rows[0])) + writer.writeheader() + writer.writerows(rows) +print(f"[L35MouthMeshProfileSummary] profiles={len(rows)} " + f"pass={sum(row['status'] == 'PASS' for row in rows)} " + f"no_coverage={sum(row['status'] == 'NO_COVERAGE' for row in rows)} " + f"fail={sum(row['status'] == 'FAIL' for row in rows)} file={csv_path}", flush=True) diff --git a/Standalone/Research/WorldAssemblerM3/Linux35/AuditTunnelMeshNormalsL35.py b/Standalone/Research/WorldAssemblerM3/Linux35/AuditTunnelMeshNormalsL35.py new file mode 100644 index 0000000..2d872db --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux35/AuditTunnelMeshNormalsL35.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +"""Measure axial normal ripple in a camera-matched tunnel mesh window.""" +import importlib.util +import math +import re +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +RESEARCH = Path(__file__).resolve().parent +RING_AUDIT = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py" +SPEC = importlib.util.spec_from_file_location("ring_profile_l32", RING_AUDIT) +PROFILE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(PROFILE) +CAMERA_LOG = ROOT / "Saved/Linux/l34/run-capture-standard-set-final-radius3.log" +CAMERA_RE = re.compile( + r"^\[L34CaptureCamera\] view=(\S+).*?camera_m=\(([^)]*)\) " + r"target_m=\(([^)]*)\)", re.M) +MAXIMUM_AXIAL_NORMAL_AMPLITUDE = 0.0255 + + +def normalize(v): + length = math.sqrt(sum(x * x for x in v)) + return tuple(x / length for x in v) if length > 1.0e-12 else (0.0, 0.0, 0.0) + + +def fit(samples, spacing=0.25): + if len(samples) < 32: + return 0.0, 0.0 + best_period, best_amplitude = 0.0, 0.0 + for index in range(8, 129): + period = index * 0.0625 + if period > 8.0: + break + omega = 2.0 * math.pi / period + cc = ss = cs = yc = ys = 0.0 + for i, value in enumerate(samples): + phase = omega * (i * spacing) + c, s = math.cos(phase), math.sin(phase) + cc += c * c + ss += s * s + cs += c * s + yc += value * c + ys += value * s + determinant = cc * ss - cs * cs + if determinant <= 1.0e-9: + continue + cosine = (yc * ss - ys * cs) / determinant + sine = (ys * cc - yc * cs) / determinant + amplitude = math.hypot(cosine, sine) + if amplitude > best_amplitude: + best_period, best_amplitude = period, amplitude + return best_period, best_amplitude + + +def main(): + if len(sys.argv) != 3: + raise SystemExit("usage: AuditTunnelMeshNormalsL35.py MESH L34_VIEW_NAME") + mesh = Path(sys.argv[1]) + view_name = sys.argv[2] + cameras = {name: (tuple(map(float, camera.split(","))), + tuple(map(float, target.split(",")))) + for name, camera, target in CAMERA_RE.findall( + CAMERA_LOG.read_text(encoding="utf-8", errors="replace"))} + if view_name not in cameras: + raise SystemExit(f"missing L34 camera: {view_name}") + camera, target = cameras[view_name] + axis = normalize(tuple(target[i] - camera[i] for i in range(3))) + down = (0.0, 0.0, -1.0) + axis_down = sum(axis[i] * down[i] for i in range(3)) + floor_dir = normalize(tuple(down[i] - axis_down * axis[i] for i in range(3))) + x_min, x_max = min(camera[0], target[0]) - 8.0, max(camera[0], target[0]) + 8.0 + y_min, y_max = min(camera[1], target[1]) - 8.0, max(camera[1], target[1]) + 8.0 + z_min, z_max = min(camera[2], target[2]) - 8.0, max(camera[2], target[2]) + 8.0 + voxel_um, triangles, tiles = PROFILE.read_scene( + str(mesh), None, z_min, z_max, (x_min, x_max, y_min, y_max)) + spacing = 0.25 + bin_count = 48 + sums = [0.0] * bin_count + counts = [0] * bin_count + for _, _, verts in triangles: + centroid = tuple(sum(v[0][axis_index] for v in verts) / 3.0 + for axis_index in range(3)) + delta = tuple(centroid[i] - camera[i] for i in range(3)) + along = sum(delta[i] * axis[i] for i in range(3)) + if not 0.0 <= along < bin_count * spacing: + continue + radial = tuple(delta[i] - along * axis[i] for i in range(3)) + radius = math.sqrt(sum(value * value for value in radial)) + if not 0.5 <= radius <= 4.0: + continue + radial_unit = tuple(value / radius for value in radial) + if sum(radial_unit[i] * floor_dir[i] for i in range(3)) < 0.25: + continue + normal = normalize(tuple(sum(v[1][axis_index] for v in verts) / 3.0 + for axis_index in range(3))) + axial = abs(sum(normal[i] * axis[i] for i in range(3))) + bin_index = int(along / spacing) + sums[bin_index] += axial + counts[bin_index] += 1 + valid = [(i, sums[i] / counts[i]) for i in range(bin_count) if counts[i]] + if len(valid) < 32: + print(f"[TunnelMeshNormalAudit] mesh={mesh} view={view_name} tiles={tiles} " + f"bins={len(valid)}/{bin_count} status=INSUFFICIENT") + raise SystemExit(1) + values = [sums[i] / counts[i] if counts[i] else 0.0 for i in range(bin_count)] + mean = sum(value for _, value in valid) / len(valid) + values = [value - mean for value in values] + period, amplitude = fit(values, spacing) + passed = amplitude < MAXIMUM_AXIAL_NORMAL_AMPLITUDE + print(f"[TunnelMeshNormalAudit] mesh={mesh} view={view_name} voxel_um={voxel_um} " + f"tiles={tiles} bins={len(valid)}/{bin_count} floor_sector=0.25 " + f"period_m={period:.3f} axial_normal_amplitude={amplitude:.5f} " + f"axial_normal_amplitude_deg={math.degrees(math.asin(min(1.0, amplitude))):.3f} " + f"threshold={MAXIMUM_AXIAL_NORMAL_AMPLITUDE:.4f} " + f"status={'PASS' if passed else 'FAIL'}") + if not passed: + raise SystemExit(1) + + +if __name__ == "__main__": + main() diff --git a/Standalone/Research/WorldAssemblerM3/Linux35/CapturePassageStandardSetL35.py b/Standalone/Research/WorldAssemblerM3/Linux35/CapturePassageStandardSetL35.py new file mode 100644 index 0000000..7e3e6d5 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux35/CapturePassageStandardSetL35.py @@ -0,0 +1,401 @@ +#!/usr/bin/env python3 +"""Export and render the standard four-view passage set for L35.""" +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 = ROOT / "Saved/Linux/l35" +BIN = OUT / "bin" +CAPTURES = Path(__file__).resolve().parent / "captures" +HEADLESS = BIN / "VoxelForgeCoreHeadless-l35-final-clang" +RENDERER = BIN / "VoxelForgeOffscreenCapture-l35-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"[L35CaptureRun] 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"[L35StandardCaptureSet] 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"[L35CaptureCamera] 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"[L35CaptureJPEG] 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") diff --git a/Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md new file mode 100644 index 0000000..a3d1055 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux35/REPORT.md @@ -0,0 +1,124 @@ +# VoxelForge L35 report + +## Rib diagnosis and fix + +The ribs are in the geometry, not just the lighting. L34 owner-SW style 0 measured a 7.438 m +radius period at 0.0361 m (3.61 cm). Its LOD0 mesh also measured a 4.750 m normal period with +0.02682 axial amplitude (1.537°), above the 0.0255 limit. The large period tracks the old 32-voxel +noise octave (8 m at 25 cm per voxel); the 4.75 m normal period is near its 16-voxel octave (4 m). +The 8 m layout chunk is the same length by coincidence: the relief is sampled directly in world +coordinates. Neither the roughly 2 m route samples, 25 cm voxel size, nor 2 m portal fade matches +the measured periods. + +The shared passage carve used the minimum of piecewise-linear capsule segments, which added local +scallops at joins. The regular wall-relief octaves added the longer beat. L35 Catmull–Rom smooths +eligible interior sweep joins, excluding short spans and the route ends. It replaces the old relief +with irregular rotated 43/29/19 m scales and reduces the terrain-band contribution. The natural +wall wobble remains. The evaluator version is 21. + +On owner-SW style 0, L35 measures a 7.062 m period at 0.0109 m (1.09 cm); coherent amplitude is +0.0044 m (0.44 cm). Its mesh-normal scan passes at 8 m with 0.02496 axial amplitude (1.430°). +The periodic residual stays below both the 0.035 m ripple limit and the 0.0255 normal limit. + +The FI slab is the shared passage support tread under the bore. It is not `LandingExtensionQ`, a +tile-boundary seam, or a separate support layer. The flat projecting apron in L34 was too broad; the +support tread is required for capsule and probe walkability. L35 tapers and grains its entry shoulder +and keeps a shallow supported walk core. The capture now shows a narrow, slightly flatter tread at +the mouth joined to the crater by irregular ground. The support and capsule audits pass. + +## Ripple and world audits + +The axial audit samples side-wall radii every 0.125 m across 32 m windows and scans periods from +0.5 to 8 m. The synthetic control is 2 m radius spheres spaced 1.5 m apart; it measures 0.0599 m +and is detected above the 0.035 m threshold. The L34 real geometry measures 0.0361 m and fails. +After the fix, all 24 recipe-stamped passage rows pass: four styles for both owner recipes and the +upper-SW fixture, with Clang and GCC. Walkability, support, open-air, route bindings, mouth density +profiles, and strict layout checks pass in those rows. + +The branch-locked FI floor and SW ceiling ring audits pass at LOD0 through LOD8. Coverage is 1.000, +each profile has at least 3.16 m relief, and no flat run or following step was found. The largest +normal change is 5.34°. The L30 ring control still fails on its 1.875 m flat followed by a 0.241 m +step; the L31 flat control fails with only 0.000046 m height range. These are intended negative +controls. Ring logs and per-LOD windows are under `Saved/Linux/l35/ring-*`. + +The recipe-stamped interface/hole and transition suite passes 16/16; hole checks report no hole +columns or air samples, and strict validator controls reject 10/10 malformed layouts. L13 and L9 +pass 4/4. The separate mesh-mouth profile supplement reports 3 pass, 6 no-coverage, 0 fail; the +density mouth profiles are covered by the 24/24 passage suite. + +## Captures + +The 12 standard 1600×900 JPEGs are in [`captures/`](captures/). They use the L34 fixed cameras, +eye height and framing, and are all below 44 KB (limit 300 KB): four views each for `owner-fi`, +`owner-sw`, and `upper-sw-fixture` (`ground-mouth`, `rock-entry`, `mid-tunnel`, `arrival`). + +## Determinism and hashes + +The L25 command and 24 m crossing pass at W1/W8/W12 with Clang and GCC for both recipes. The +upper-SW fixture passes at the same worker counts with both compilers. Result: 30/30 stable runs. +Clang and GCC W12 hashes match. Both recipes report recipe hash +`a2985985e6985a2a`, plan hash `a5e29887e839c5d6`, and manifest hash `6c89e165364e56df`. + +| Recipe | L25 full detail: density / mesh / payload / topology | L25 whole view: density / mesh / payload / topology | +|---|---|---| +| SurfaceWorld | `ace56c8f205238ad / a9c603a1248de6b8 / ea55f1f4fcc6a894 / 5ea7bc78a1b678ae` | `b40597aa93148a45 / 8a42763cb936756d / 06c9171fdaa7875c / 9057043383ba37b1` | +| FloatingIslands | `ed550e0c8b182695 / 0e410a8e80a3e7f7 / 0751926f0c67d8a4 / f01208bc2f7841ec` | `7dd12412f1c036c6 / d074c75df2f14e7f / d4a2cf28c2873580 / 903ecae9e66ee20a` | + +Exact-query hashes (density / detail / collision) are SurfaceWorld +`c949c4df2ce6f9d9 / 114c501e2d830d94 / e6011d3a248d4849` and FloatingIslands +`7b7c50ac24ccadcc / efcba20c0c70c415 / e604823a249029bf`. The upper fixture layout identity is +`resolved-layout-v3-fnv64:fc4e73619037f88c`. + +Core binary SHA256: Clang `6a98169293a955f0119f1ad13b9ab949a9729948401c7f832446a6d980c9c6ae`; +GCC `8535726460807e198a0f05c7b1197ab4ca51c3fda18a87d45a1e49e754589542`. + +## Cost, edits, and run conditions + +Five interleaved L34/L35 pairs per recipe pass. Median whole-view time is SurfaceWorld 1.776 s → +1.807 s (+1.75%) and FloatingIslands 17.092 s → 17.251 s (+0.93%); paired changes are mixed and +within run noise. No Unreal files changed. Core builds and all runs used CPUs 0–11, a 6 GiB address +space limit and at most 12 workers. No run exceeded 30 minutes, no step was stopped, and the governor +was unchanged. + +Changed files: `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, +`Standalone/Core/src/ResolvedLayoutInternal.h`, `Standalone/Core/src/ShapeWorldInternal.h`, +`Standalone/Core/src/VoxelForgeFieldShared.h`, `Standalone/Source/StreamBenchmarkPipeline.h`, +`ARCHITECTURE.md`, and `CODEMAP.md`. No Unreal edits were made or compiled. + +## Failed and retried runs + +- The first ripple audit used a 1.25 m sphere spacing and a 0.045 m limit; its 0.0418 m control + was missed. The synthetic control is now 1.5 m spacing with 2 m spheres, and the limit is 0.035 m. +- Route resampling first failed support/open-air checks (12/24). Four-way resampling and raw routes + with the old relief failed mesh-normal scans at 5.688 m/0.03064 and 4.625 m/0.03263; old 13/9/5 + voxel relief also failed at 4.688 m/0.03546. SDF smoothing without terminal exclusion blocked 143 + samples; high route noise left 720/976 unsupported. Those variants were reverted. +- Removing support caused 30 unsupported samples; lowering its top caused 6; an early widened/noisy + form caused 1. Full and half support splines and full vertical smoothing each caused an unsupported + sample. Quarter-weight vertical/radius smoothing passed. The all-route 0.14 m grain trial caused + 132 unsupported FI samples and was reverted; mouth-only shoulder grain remains. +- Exact Cascade cosine lobes failed FI style 3 in both compilers (9.18 cm at 6.188 m). Full-depth + seeded profiles failed at 5.938 m/0.0537 m and 3.312 m/0.0792 m. The retained shallow seeded + profile passes the full matrix. +- Initial ring probes used the wrong data, camera, or 16 m radius; they returned no coverage or + only 1.857 m relief. The canonical owner input, 32 m LOD0 windows, and covered branch-locked + LOD1 windows pass. The first SW LOD1 set had three uncovered windows; retrying the seven covered + windows passed 77/77 profiles. +- A supplemental fade-ring scan across the rough owner-FI crater still exceeds the 20° normal + limit (42.27°); the same camera/window on L34 also measures 42.27°, so this is present in the + comparison build. An old fixed center has no coverage on the current layout. These probes are + recorded separately from the passing branch-locked FI/SW LOD0–8 ring suite. + +No commits, pushes, or stashes were made. + +## Orchestrator review (Claude) + +- Accepted: the wall ribs are gone in the captures (owner-SW mid-tunnel and upper-SW arrival walls now read smooth with + irregular wobble). Cause measured (capsule-join scallops plus regular 8 m/4 m relief octaves), fixed in the shared + carve, beaded control fails, L34 real geometry fails. Evaluator v21. +- Still open, for L36: + - **The floor still ripples.** In owner-SW mid-tunnel, the walk floor shows a regular washboard/step pattern. The + ripple audit samples only side-wall radii, so it can't see the floor (the support tread). + - **The FI mouth is not natural yet.** The slab is gone, but it was replaced by a jagged cluster of broken, faceted + pieces in the crater (owner-FI ground mouth). The 42° fade-ring normal jump that is also present in L34 belongs + to the same area. diff --git a/Standalone/Research/WorldAssemblerM3/Linux35/RunCostL35.py b/Standalone/Research/WorldAssemblerM3/Linux35/RunCostL35.py new file mode 100644 index 0000000..4677b2b --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux35/RunCostL35.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Run five interleaved whole-view cost pairs against the L34 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/l35" +BINARIES = { + "l34": ROOT / "Saved/Linux/l34/bin/VoxelForgeCoreHeadless-l34-clang", + "l35": OUT / "bin/VoxelForgeCoreHeadless-l35-final-clang", +} +ORDERS = (("l34", "l35"), ("l35", "l34"), ("l34", "l35"), + ("l35", "l34"), ("l34", "l35")) +READY = re.compile(r"\bwhole_view_ready=([0-9.]+)s") +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 = re.findall( + r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$", + output, re.M) + stamp_ok = stamps == [(recipe, recipe, "PASS")] + 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 + rows.append((recipe, pair, build, ready, elapsed, + result.returncode if result else "TIMEOUT", valid)) + failed |= not valid + print(f"[L35CostRun] 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-runs.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["l34"]) + current = statistics.median(by_build["l35"]) + 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["l35"] - values["l34"]) / values["l34"] * 100.0) + mad = statistics.median(abs(value - statistics.median(paired)) for value in paired) + print(f"[L35CostSummary] recipe={recipe} pairs=5 median_l34_s={baseline:.3f} " + f"median_l35_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"[L35Cost] runs={len(rows)} status={'FAIL' if failed else 'PASS'} summary={summary}", + flush=True) +raise SystemExit(1 if failed or len(rows) != 20 else 0) diff --git a/Standalone/Research/WorldAssemblerM3/Linux35/RunDeterminismL35.py b/Standalone/Research/WorldAssemblerM3/Linux35/RunDeterminismL35.py new file mode 100644 index 0000000..a8db497 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux35/RunDeterminismL35.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""Check L25, 24 m crossing and upper-SW fixture determinism for L35.""" +import csv +import re +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l35" +OWNER = ROOT / "Standalone/data/owner_world.json" +FIXTURE = ROOT / "Saved/Linux/l33/fixtures/sw-upper-tunnel-network-style0.json" +BINARIES = { + "clang": OUT / "bin/VoxelForgeCoreHeadless-l35-final-clang", + "gcc": OUT / "bin/VoxelForgeCoreHeadless-l35-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"[L35DeterminismRun] 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-l35.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"[L35DeterminismSummary] 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) diff --git a/Standalone/Research/WorldAssemblerM3/Linux35/RunInvariantAuditsL35.py b/Standalone/Research/WorldAssemblerM3/Linux35/RunInvariantAuditsL35.py new file mode 100644 index 0000000..b0468d8 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux35/RunInvariantAuditsL35.py @@ -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/l35" +BINARIES = { + "clang": OUT / "bin/VoxelForgeCoreHeadless-l35-final-clang", + "gcc": OUT / "bin/VoxelForgeCoreHeadless-l35-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) diff --git a/Standalone/Research/WorldAssemblerM3/Linux35/RunLegacyAuditsL35.py b/Standalone/Research/WorldAssemblerM3/Linux35/RunLegacyAuditsL35.py new file mode 100644 index 0000000..61f4858 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux35/RunLegacyAuditsL35.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Run the L13 route parity and L9 uniform-block proof audits on L35.""" +import re +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l35" +BINARIES = { + "clang": OUT / "bin/VoxelForgeCoreHeadless-l35-final-clang", + "gcc": OUT / "bin/VoxelForgeCoreHeadless-l35-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"[L35LegacyAuditRun] 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"[L35LegacyAuditSummary] 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) diff --git a/Standalone/Research/WorldAssemblerM3/Linux35/RunPassageAuditsL35.py b/Standalone/Research/WorldAssemblerM3/Linux35/RunPassageAuditsL35.py new file mode 100644 index 0000000..3ec613d --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux35/RunPassageAuditsL35.py @@ -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/l35" +BINARIES = { + "clang": OUT / "bin/VoxelForgeCoreHeadless-l35-final-clang", + "gcc": OUT / "bin/VoxelForgeCoreHeadless-l35-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 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"[L35PassageAuditRun] 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"[L35PassageAuditSummary] 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) diff --git a/Standalone/Research/WorldAssemblerM3/Linux35/captures/owner-fi-arrival.jpg b/Standalone/Research/WorldAssemblerM3/Linux35/captures/owner-fi-arrival.jpg new file mode 100644 index 0000000..53bc033 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux35/captures/owner-fi-arrival.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux35/captures/owner-fi-ground-mouth.jpg b/Standalone/Research/WorldAssemblerM3/Linux35/captures/owner-fi-ground-mouth.jpg new file mode 100644 index 0000000..b110bbd Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux35/captures/owner-fi-ground-mouth.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux35/captures/owner-fi-mid-tunnel.jpg b/Standalone/Research/WorldAssemblerM3/Linux35/captures/owner-fi-mid-tunnel.jpg new file mode 100644 index 0000000..0c5095c Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux35/captures/owner-fi-mid-tunnel.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux35/captures/owner-fi-rock-entry.jpg b/Standalone/Research/WorldAssemblerM3/Linux35/captures/owner-fi-rock-entry.jpg new file mode 100644 index 0000000..70fefde Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux35/captures/owner-fi-rock-entry.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux35/captures/owner-sw-arrival.jpg b/Standalone/Research/WorldAssemblerM3/Linux35/captures/owner-sw-arrival.jpg new file mode 100644 index 0000000..2b67450 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux35/captures/owner-sw-arrival.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux35/captures/owner-sw-ground-mouth.jpg b/Standalone/Research/WorldAssemblerM3/Linux35/captures/owner-sw-ground-mouth.jpg new file mode 100644 index 0000000..e24d5b4 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux35/captures/owner-sw-ground-mouth.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux35/captures/owner-sw-mid-tunnel.jpg b/Standalone/Research/WorldAssemblerM3/Linux35/captures/owner-sw-mid-tunnel.jpg new file mode 100644 index 0000000..8fc04d8 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux35/captures/owner-sw-mid-tunnel.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux35/captures/owner-sw-rock-entry.jpg b/Standalone/Research/WorldAssemblerM3/Linux35/captures/owner-sw-rock-entry.jpg new file mode 100644 index 0000000..59aec9a Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux35/captures/owner-sw-rock-entry.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux35/captures/upper-sw-fixture-arrival.jpg b/Standalone/Research/WorldAssemblerM3/Linux35/captures/upper-sw-fixture-arrival.jpg new file mode 100644 index 0000000..817fc5f Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux35/captures/upper-sw-fixture-arrival.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux35/captures/upper-sw-fixture-ground-mouth.jpg b/Standalone/Research/WorldAssemblerM3/Linux35/captures/upper-sw-fixture-ground-mouth.jpg new file mode 100644 index 0000000..7d9528d Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux35/captures/upper-sw-fixture-ground-mouth.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux35/captures/upper-sw-fixture-mid-tunnel.jpg b/Standalone/Research/WorldAssemblerM3/Linux35/captures/upper-sw-fixture-mid-tunnel.jpg new file mode 100644 index 0000000..d2fc91e Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux35/captures/upper-sw-fixture-mid-tunnel.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux35/captures/upper-sw-fixture-rock-entry.jpg b/Standalone/Research/WorldAssemblerM3/Linux35/captures/upper-sw-fixture-rock-entry.jpg new file mode 100644 index 0000000..0f16915 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux35/captures/upper-sw-fixture-rock-entry.jpg differ diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index 8afff40..4553b8b 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -812,6 +812,14 @@ namespace StandaloneStreamBenchmark std::uint64_t floorSamples=0,wallSamples=0,missingFloorSamples=0,missingWallSamples=0; bool passed=false; }; + struct FPassageRippleAudit + { + double periodMetres=0.0,amplitudeMetres=0.0,localPeakAmplitudeMetres=0.0; + double coherentPeriodMetres=0.0,coherentAmplitudeMetres=0.0; + double controlAmplitudeMetres=0.0; + std::uint64_t samples=0,expectedSamples=0; + bool controlDetected=false,passed=false; + }; constexpr double MaximumFlatRunMetres=8.0; constexpr double MinimumWallReliefMetres=0.20; constexpr double PassageProfileSpacingMetres=0.125; @@ -1113,6 +1121,174 @@ namespace StandaloneStreamBenchmark &&audit.wallSamples>=8; return audit; }; + const auto auditPassageRipples=[&](const VoxelForgeCore::World& Field, + const VoxelForgeCore::ResolvedLayoutPassageV1& passage) + { + FPassageRippleAudit audit; + constexpr double SampleSpacingMetres=0.125; + constexpr double WindowLengthMetres=32.0; + constexpr double MinimumPeriodMetres=0.5; + constexpr double MaximumPeriodMetres=8.0; + constexpr double PeriodStepMetres=0.0625; + constexpr double RippleAmplitudeLimitMetres=0.035; + struct FPathPoint + { + double x=0.0,y=0.0,z=0.0,radius=0.0,distance=0.0; + }; + const std::size_t end=std::min(passage.lowerMouthControlIndex, + passage.controlPoints.size()-1); + if(end<2)return audit; + std::vector path; + path.reserve(end+1); + for(std::size_t i=0;i<=end;++i) + { + const auto& point=passage.controlPoints[i]; + FPathPoint value{qToMetres(point.positionSubvoxels[0]), + qToMetres(point.positionSubvoxels[1]), + qToMetres(point.positionSubvoxels[2]), + qToMetres(point.radiusSubvoxels),0.0}; + if(!path.empty()) + { + const auto& previous=path.back(); + value.distance=previous.distance+std::sqrt( + (value.x-previous.x)*(value.x-previous.x) + +(value.y-previous.y)*(value.y-previous.y) + +(value.z-previous.z)*(value.z-previous.z)); + } + path.push_back(value); + } + const double total=path.back().distance; + if(total1.0e-9?(distance-a.distance)/length:0.0; + return FPathPoint{std::lerp(a.x,b.x,t),std::lerp(a.y,b.y,t), + std::lerp(a.z,b.z,t),std::lerp(a.radius,b.radius,t),distance}; + }; + const auto sinusoidAmplitude=[&](const std::vector>& samples, + double& bestPeriod) + { + if(samples.size()<64)return 0.0; + double best=0.0; + for(double period=MinimumPeriodMetres; + period<=MaximumPeriodMetres+1.0e-9;period+=PeriodStepMetres) + { + const double angular=6.2831853071795864769/period; + double cc=0.0,ss=0.0,cs=0.0,yc=0.0,ys=0.0; + for(const auto& sample:samples) + { + const double phase=angular*sample[0]; + const double c=std::cos(phase),s=std::sin(phase); + cc+=c*c;ss+=s*s;cs+=c*s; + yc+=sample[1]*c;ys+=sample[1]*s; + } + const double determinant=cc*ss-cs*cs; + if(determinant<=1.0e-9)continue; + const double cosine=(yc*ss-ys*cs)/determinant; + const double sine=(ys*cc-yc*cs)/determinant; + const double amplitude=std::hypot(cosine,sine); + if(amplitude>best){best=amplitude;bestPeriod=period;} + } + return best; + }; + const auto appendDetrendedProfile=[&](const std::vector& values, + double startDistance,std::vector>& output) + { + if(values.size()<64)return; + const double meanIndex=0.5*static_cast(values.size()-1); + double mean=0.0,xy=0.0,xx=0.0; + for(double value:values)mean+=value; + mean/=static_cast(values.size()); + for(std::size_t i=0;i(i)-meanIndex)*SampleSpacingMetres; + xy+=x*(values[i]-mean);xx+=x*x; + } + const double slope=xx>0.0?xy/xx:0.0; + for(std::size_t i=0;i(i)-meanIndex)*SampleSpacingMetres; + output.push_back({startDistance+static_cast(i)*SampleSpacingMetres, + values[i]-mean-slope*x}); + } + }; + std::vector beadControl; + beadControl.reserve(static_cast(WindowLengthMetres/SampleSpacingMetres)+1); + constexpr double BeadSpacingMetres=1.5; + constexpr double BeadSphereRadiusMetres=2.0; + for(std::size_t i=0;i<=static_cast(WindowLengthMetres/SampleSpacingMetres);++i) + { + const double x=static_cast(i)*SampleSpacingMetres; + const double nearest=std::abs(std::remainder(x,BeadSpacingMetres)); + beadControl.push_back(std::sqrt(std::max(0.0, + BeadSphereRadiusMetres*BeadSphereRadiusMetres-nearest*nearest))); + } + std::vector> beadSamples; + appendDetrendedProfile(beadControl,0.0,beadSamples); + double ignoredPeriod=0.0; + audit.controlAmplitudeMetres=sinusoidAmplitude(beadSamples,ignoredPeriod); + audit.controlDetected=audit.controlAmplitudeMetres>=RippleAmplitudeLimitMetres; + const std::array windowStarts{{0.03,0.31,0.61,0.91}}; + std::vector> coherentSamples; + for(const double fraction:windowStarts) + { + const double start=(total-WindowLengthMetres)*fraction; + std::vector radii; + radii.reserve(static_cast(WindowLengthMetres/SampleSpacingMetres)+1); + for(std::size_t sample=0; + sample<=static_cast(WindowLengthMetres/SampleSpacingMetres); + ++sample) + { + ++audit.expectedSamples; + const double distance=start+static_cast(sample)*SampleSpacingMetres; + const auto pose=interpolate(distance); + const auto before=interpolate(std::max(0.0,distance-0.25)); + const auto after=interpolate(std::min(total,distance+0.25)); + const double tangentX=after.x-before.x,tangentY=after.y-before.y; + const double tangentLength=std::hypot(tangentX,tangentY); + if(tangentLength<=1.0e-6)continue; + const double nx=-tangentY/tangentLength,ny=tangentX/tangentLength; + const double sampleRadius=std::max(0.20,pose.radius-0.18); + const double x0=pose.x/coreMetres,y0=pose.y/coreMetres,z0=pose.z/coreMetres; + const double offset=sampleRadius/coreMetres; + const double left=static_cast(Field.densityAt( + {x0+nx*offset,y0+ny*offset,z0}))*coreMetres; + const double right=static_cast(Field.densityAt( + {x0-nx*offset,y0-ny*offset,z0}))*coreMetres; + if(!std::isfinite(left)||!std::isfinite(right) + ||left< -0.75||left>0.75||right< -0.75||right>0.75)continue; + radii.push_back(sampleRadius+0.5*(left+right)); + ++audit.samples; + } + if(radii.size()(WindowLengthMetres/ + SampleSpacingMetres)*3/4)continue; + std::vector> windowSamples; + appendDetrendedProfile(radii,start,windowSamples); + double localPeriod=0.0; + const double localAmplitude=sinusoidAmplitude(windowSamples,localPeriod); + if(localAmplitude>audit.localPeakAmplitudeMetres) + { + audit.localPeakAmplitudeMetres=localAmplitude; + audit.periodMetres=localPeriod; + } + coherentSamples.insert(coherentSamples.end(),windowSamples.begin(), + windowSamples.end()); + } + audit.coherentAmplitudeMetres=sinusoidAmplitude(coherentSamples, + audit.coherentPeriodMetres); + audit.amplitudeMetres=audit.localPeakAmplitudeMetres; + const double coverage=audit.expectedSamples>0 + ?static_cast(audit.samples)/audit.expectedSamples:0.0; + audit.passed=audit.controlDetected&&coverage>=0.75 + &&audit.localPeakAmplitudeMetres=MinimumWallReliefMetres;}; @@ -1731,7 +1907,7 @@ namespace StandaloneStreamBenchmark return zUm>=stratum->content.bottomMicrometres&&zUmcontent.topMicrometres; }; std::uint64_t passageCount=0,passagePasses=0,blockedControls=0,naturalPasses=0; - std::uint64_t mouthProfilePasses=0; + std::uint64_t mouthProfilePasses=0,ripplePasses=0,rippleControlsDetected=0; std::uint64_t openAirViolations=0,openAirLegacyViolations=0,openAirSamples=0; std::uint64_t openAirAudits=0,openAirColumnSamples=0,openAirShortSpanSamples=0; std::uint64_t columnCrossings=0,shortGroundedSpans=0,columnConnectedEnds=0; @@ -1781,6 +1957,7 @@ namespace StandaloneStreamBenchmark const FPassageNaturalnessAudit naturalAudit=auditPassageNaturalness(Shape,passage); const FPassageMouthProfileAudit mouthProfileAudit= auditPassageMouthProfiles(Shape,passage); + const FPassageRippleAudit rippleAudit=auditPassageRipples(Shape,passage); const FPassageOpenAirAudit openAirAudit=auditPassageOpenAir(Shape,blockedWorld, passage,passageIndex); const FPassageColumnAudit columnAudit=auditPassageColumns(Shape,blockedWorld, @@ -1811,8 +1988,10 @@ namespace StandaloneStreamBenchmark &&mouthInContent(passage.lowerStrateIndex,passage.lowerMouthSubvoxels); ++passageCount;naturalPasses+=naturalAudit.passed; mouthProfilePasses+=mouthProfileAudit.passed; + ripplePasses+=rippleAudit.passed; + rippleControlsDetected+=rippleAudit.controlDetected; passagePasses+=passageAudit.passed&&mouthsInRadius&&mouthsInContent - &&naturalAudit.passed&&mouthProfileAudit.passed; + &&naturalAudit.passed&&mouthProfileAudit.passed&&rippleAudit.passed; std::printf("[ResolvedPassageAudit] id=%s interface=%d-%d control_points=%zu center_samples=%llu capsule_samples=%llu blocked=%llu support_samples=%llu unsupported=%llu grade_failures=%llu mouth_content=%d/2 mouth_radius=%d/2 walkability=%s\n", passage.stableId.c_str(),passage.upperStrateIndex,passage.lowerStrateIndex, passage.controlPoints.size(),static_cast(passageAudit.centers), @@ -1837,6 +2016,16 @@ namespace StandaloneStreamBenchmark mouthProfileAudit.wallFlatStepRunMetres,mouthProfileAudit.wallStepMetres, PassageProfileSpacingMetres,PassageProfilePlateauMinimumMetres, PassageProfileStepThresholdMetres,mouthProfileAudit.passed?"PASS":"FAIL"); + std::printf("[PassageRippleAudit] id=%s window_m=32.000 spacing_m=0.125 period_m=%.3f amplitude_m=%.4f amplitude_cm=%.2f coherent_period_m=%.3f coherent_amplitude_m=%.4f coherent_amplitude_cm=%.2f local_peak_amplitude_m=%.4f coverage=%llu/%llu threshold_m=0.035 synthetic_spheres_period_m=1.500 synthetic_spheres_amplitude_m=%.4f synthetic_spheres_detected=%s status=%s\n", + passage.stableId.c_str(),rippleAudit.periodMetres, + rippleAudit.amplitudeMetres,rippleAudit.amplitudeMetres*100.0, + rippleAudit.coherentPeriodMetres,rippleAudit.coherentAmplitudeMetres, + rippleAudit.coherentAmplitudeMetres*100.0, + rippleAudit.localPeakAmplitudeMetres, + static_cast(rippleAudit.samples), + static_cast(rippleAudit.expectedSamples), + rippleAudit.controlAmplitudeMetres,rippleAudit.controlDetected?"true":"false", + rippleAudit.passed?"PASS":"FAIL"); std::printf("[PassageOpenAirAudit] id=%s support_samples=%llu violations=%llu column_samples=%llu short_span_samples=%llu l24_ungated_violations=%llu route_support_samples=%llu route_support_violations=%llu spacing_m=%.3f status=%s\n", passage.stableId.c_str(),static_cast(openAirAudit.supportSamples), static_cast(openAirAudit.violations), @@ -2151,9 +2340,15 @@ namespace StandaloneStreamBenchmark PassageProfilePlateauMinimumMetres,PassageProfileStepThresholdMetres, l23TerraceControlDetected?"DETECTED":"MISSED", mouthProfilePasses==passageCount&&l23TerraceControlDetected?"PASS":"FAIL"); + std::printf("[PassageRippleAuditSummary] passed=%llu/%llu synthetic_spheres_negative_control=%s max_amplitude_m=0.035 status=%s\n", + static_cast(ripplePasses), + static_cast(passageCount), + rippleControlsDetected==passageCount?"DETECTED":"MISSED", + ripplePasses==passageCount?"PASS":"FAIL"); const bool auditPassed=passageCount>0&&passagePasses==passageCount &&naturalPasses==passageCount&&l23SmoothWallDetected &&mouthProfilePasses==passageCount&&l23TerraceControlDetected + &&ripplePasses==passageCount &&openAirAudits==passageCount&&openAirViolations==0&&l24OpenAirControlDetected &&columnAuditsPassed&&noLongBridgeAuditPassed &&routePasses>0&&routePasses==routeWalkPasses&&routePasses==routeBindings