diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 1cac800..c9185c4 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2265,32 +2265,36 @@ mouth and off the walk route. It stays disabled by default. Active records carry L43's report records the off/on hashes, walk and interface audits, outside-stamp timing, tier results, captures and any failed runs. -### 8.54 Opt-in native lattice field — World Assembler M3 R26 slice 1 (Linux L58) +### 8.54 Opt-in native lattice field — World Assembler M3 R26 slices 1–2 (Linux L58–L59) -`Core.Lattice.PrimitiveField` is a version-1 recipe selected only for one Maze or VerticalShafts +`Core.Lattice.PrimitiveField` is an opt-in recipe selected only for one Maze or VerticalShafts strate by `--recipe=lattice --lattice-strate=N`. The legacy generator assets and their default -dispatch stay unchanged. Planning first binds the actual incoming lower mouth and outgoing upper -mouth from the resolved passage records, then creates arrival/exit overlap corridors, a mandatory -four-turn helix and the maze spine; two deterministic dead ends are added after that route. The -resolved room and edge seeds use `(world seed, strate, recipe version, stable ID)`. An audit control -translates the helix 20 m away from both route junctions to make the shaft network orphaned. +dispatch stay unchanged. Version 2 binds the actual incoming lower mouth and outgoing upper mouth, +then creates arrival/exit overlap corridors, an open vertical shaft and the maze spine; two +deterministic dead ends are added after that route. Resolved room and edge seeds use +`(world seed, strate, recipe version, stable ID)`. The orphan control translates the shaft 20 m +away from both route junctions and must fail reachability. -The prototype helix uses a 6 m centreline radius, 3 m clear width, 5 m pitch, four turns and 2.6 m -finished floor-to-soffit height. Its centreline descends 20 m over roughly 151 m; the inner-edge -grade is about 10.0 degrees. The 2.4 m floor-wall smooth-max gives an isolated ideal fillet radius -of 0.8485 m. A separate 0.4 m feature-union blend prevents adjoining ramps from erasing the tread. -The swept-clear diameter is 15 m; the conservative padded feature envelope is about 20.6 m (the -approved approximately 21 m site envelope). +The version-2 shaft core is a vertical cylinder of 4.5 m radius. Its air continues 8 m above the +top tread and 2 m below the bottom tread. The ramp winds around the core at a 6 m centreline radius, +with 3 m clear width, 5 m pitch and four turns. The open annular shaft air joins the core air, while +one-metre support slabs follow the helix under each tread. This leaves a 4 m vertical gap between +one tread and the underside of the next. The ledge's inner edge is open to the well; the route +centreline stays 1.5 m from the drop and leaves 1.16 m beyond the 0.34 m capsule radius. Its inner +edge grade is about 10.0 degrees. The maze retains 2.6 m room headroom. The shaft's padded outer +design envelope is 21 m. The 1.9 m floor-wall smooth-max gives an isolated ideal fillet radius of +0.6718 m; the separate 0.05 m feature-union blend keeps route junctions connected. The layout reuses the resolved cavern graph vocabulary. A per-strate opt-in selects the native -negative-solid/positive-air field; canonical feature order is stable-ID order. Room/corridor bounds -are indexed by the graph BVH and polyline segments have a nested deterministic BVH for density and -interval queries. Box classification stays conservative, and `isTileShapeFree` refuses any tile -that intersects the graph. The L58 pipeline audits the composed field from actual mouth to mouth -with a 0.34 m by 1.76 m capsule, per-station support, 2.2 m clearance and a 15 degree grade limit; -it also checks the orphan control, brute-force interval samples and the composed wall-foot fillet. -The Linux58 report contains the measured reachability, surface, peer-determinism, timing and capture -results; the code path is not promoted to legacy Maze/VerticalShafts generation. +negative-solid/positive-air field; canonical feature order is stable-ID order. The ring-shaped shaft +air and its support layers use an analytic helix lookup instead of testing every polyline segment +per density sample. The core cylinder and annular wall have conservative interval bounds; false +all-air and all-solid controls remain rejected, and `isTileShapeFree` refuses occupied tiles. The +L59 pipeline checks the composed field from actual mouth to mouth with a 0.34 m by 1.76 m capsule, +support at every station, 2.2 m clearance and a 15 degree grade limit. It also checks the orphan +control, whole-surface ramp lip/wall-foot/underside, brute-force interval samples and the composed +wall-foot fillet. See the Linux58 and Linux59 reports for evidence. This code path is not promoted +to legacy Maze/VerticalShafts generation. ## 9. Multiplayer model (listen-server first, dedicated-friendly) diff --git a/CODEMAP.md b/CODEMAP.md index f061fd1..74850f8 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -170,8 +170,8 @@ remains `0`, and lateral regions remain gated off. | `../../VoxelForgeCore/Private/ModuleEntry/` | Unreal module entry and an isolated `CoreMinimal.h` shim for the actual engine header. | | `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` | Standard-library-only public Core API. `VOXELFORGECORE_API` exports the API from the Unreal module and is empty for standalone builds. | | `../../Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` / `../../Standalone/Core/src/ResolvedLayoutInternal.h` | `ResolvedLayoutSnapshotV3`: physical layout-chunk width, versioned micrometre intervals, owned ceiling/content/bedrock bands, honored transition settings, deterministic per-column transition bands, recipe anchors, director-planned passages, canonical JSON/FNV identity, compatibility/strict validators, and negative-control audit. Built once from the director-resolved plan and consumed by bounds planning, Core dispatch, composition, and validation. §8.40–8.42, §8.46. | -| `ResolvedLayoutStratePassageCapabilitiesV1::{hasNativeCaveField,hasNativeLatticeField}`, `IsNativeLatticeFieldStratum`, `HasNativeGraphField` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` / `Standalone/Core/src/ResolvedLayoutInternal.h` | Explicit per-strate native graph capability; `Core.Lattice.PrimitiveField` is restricted to Maze/VerticalShafts recipe version 1. Lattice seeds include world seed, strate, recipe version and stable feature ID; explicit polyline records keep their endpoints and receive conservative bounds. §8.54. | -| `FCavernRuntimeFeature::FSegmentNode`, `ApplyResolvedNativeCavesField`, `ClassifyNativeCaveFieldBox` | `Standalone/Core/src/ShapeWorldInternal.h` | A deterministic nested segment BVH accelerates native graph distance and conservative interval evaluation; the outer graph BVH still supplies tile candidates and protects `isTileShapeFree`. The 0.4 m feature-union width is separate from the floor fillet width. §8.54. | +| `NativeLatticeFieldRecipeVersion`, `IsNativeLatticeFieldStratum`, `LayoutLatticeFeatureSeed` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h` / `Standalone/Core/src/ResolvedLayoutInternal.h` | Explicit Maze/VerticalShafts opt-in at lattice recipe version 2; stable feature seeds include world seed, strate, recipe version and feature ID. §8.54. | +| `FCavernRuntimeFeature::openShaftRamp`, `ApplyResolvedNativeCavesField`, `ClassifyNativeCaveFieldBox` | `Standalone/Core/src/ShapeWorldInternal.h` | Analytic open shaft ring, helical support lookup, exact cylinder core bounds and conservative interval proofs; the outer graph BVH supplies tile candidates and protects `isTileShapeFree`. §8.54. | | `../../Standalone/Core/src/FloatingIslandsInternal.h` | Canonical standalone/Core floating-island planner, geometry-versioned field, manifest serializer, influence bounds, capsule-route checks, and thickness audit. | | `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|surface-fall|crossing|gate-stress|visual-edit|visual-lod|visual-gap-walk|visual-gap-inject|dig-motion|parity|horizon|horizon-render|determinism|region-eval|step2d|step2e|step2f|step2f-capture|step3-scale|step3-game`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, `-Label`, `-HorizonLevel`, `-HorizonRenderDistanceChunks`, `-Step2fProfile=0..7`, `-Step2fCaptureView=inside|wall-closeup`, `-ParityExportSize=32|64|128`, `-ParityExportTile=x,y,z`, `-SharedDDC`, `-SampleDensityStacks`, `-StackSampleIntervalUs`, and `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH`; stages the host/assets, isolates user/DDC/config state, serializes one Unreal launch at a time, enforces the 8 GiB/30-minute guards, waits for 6 GiB free RAM before launches, and emits compact `result.json`/`summary.txt` plus full logs. Parity can render its game leg with `-EnableRender` and compares the selected export region against the exact streamed tile hashes. `visual-gap-walk` records camera-frustum surface-point coverage at 6000 cm/s; `visual-gap-inject` proves an early component hide is detected; `dig-motion` logs per-frame vertical velocity, gate, support readiness, and movement mode. `-Cvars @{ 'voxel.TestGapAuditLegacyHandoff'=1 }` replays the old visibility handoff for A/B audit baselines. Horizon traces report desired/loaded tiles, resident stream geometry, worker seconds, steady wall time, and peak private memory; `horizon-render` captures the owner view; `step2f-capture` selects one of eight parameter sets, finds a positive-density camera point and aims at a sampled rock wall for an inside or wall-closeup view, then saves it under `Saved/Research/step2f/captures` (pass the persistent render DDC with `-SharedDDC`); `determinism` compares per-tile hashes across the default CRT path and `-voxel.CrtFma3=0`; `region-eval` compares the live path with the default-off editor chunk program on the 259³ lattice and five-seed probe set; `step2d` retains the earlier independent field; `step2e` retains its earlier benchmark; `step2f` compares frozen step 2e and the coherent field for all eight profiles on the same 259³ ring, applies geometry gates, and checks sound bounds, seams, and 1-vs-6-worker hashes. `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH` checks a full-ring hash at another partition count. `-SampleDensityStacks` captures profiles, and `-StackSampleIntervalUs` sets the sampling interval. | Step-3 scale and game validation | `step3-scale` measures the same 519³ and 259³ six-worker lattices at 12.5 cm. The cached medians are 7.347496 s and 1.169567 s respectively; determinism and brick sound-proof audits pass. `step3-game` routes in-game generation through the brick evaluator and records tile timings, spawn fill, moving frames, and profile-switch rebuilds. The visible D3D12 run measured tile generation p50/p95/max 3.326/31.045/169.867 ms, full-detail L0 fill 12.369 s for 2,765 tiles, and moving-frame p50/p95/p99/max 4.856/8.922/13.090/80.575 ms. Switching profiles rebuilt 5,815 loaded tiles. Both high-resolution screenshot requests were logged, but the harness found only one fresh PNG and it is a blank pale viewport; no lit comparison capture is accepted. Raw output: `Saved/TestHarness/step3_game_visible_final`; scale output: `Saved/Research/step3/scale/step3_scale_cacheperf_v1`. | @@ -1310,18 +1310,19 @@ See ARCHITECTURE §8.53 and [`Standalone/Research/WorldAssemblerM3/Linux43/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux43/REPORT.md) for layout identities, owner hashes, outside-box timing, audits and captures. -### 3.36 Native lattice field — World Assembler M3 R26 slice 1 (Linux L58) +### 3.36 Native lattice field — World Assembler M3 R26 slices 1–2 (Linux L58–L59) | Symbol | Canonical file | Role | |---|---|---| -| `IsNativeLatticeFieldStratum`, `LayoutLatticeFeatureSeed`, `ResolvedCavernRoomV1::unionWidthMicrometres`, `ResolvedCavernEdgeV1::unionWidthMicrometres` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, `Standalone/Core/src/ResolvedLayoutInternal.h` | Versioned Maze/VerticalShafts opt-in and deterministic seed from `(seed, strate, recipe version, stable ID)`; optional union width preserves prior cave JSON when unset. | -| `MakeNativeLatticeSpine`, `--recipe=lattice`, `--lattice-strate=N` | `Standalone/Source/StreamBenchmarkHeadless.cpp` | Resolves actual adjacent mouths, builds a mandatory four-turn helix and maze spine, then adds two seeded dead ends; `--lattice-orphan-control` moves the helix away from both junctions. | -| `AuditNativeLatticeField` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples the final field along the mouth-to-mouth capsule route, checking support, headroom, grade, orphan rejection, composed fillet, interval signs and feature-tile exclusion. | -| `BuildLatticeFixtureL58.py`, `CaptureNativeLatticeL58.py`, `AuditLatticeSurfaceL58.py` | `Standalone/Research/WorldAssemblerM3/Linux58/` | Isolated Maze strate fixture, four reviewed captures and the L54 whole-surface triangle audit adapted to explicit native polylines. | +| `NativeLatticeFieldRecipeVersion`, `IsNativeLatticeFieldStratum`, `LayoutLatticeFeatureSeed` | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, `Standalone/Core/src/ResolvedLayoutInternal.h` | Version-2 Maze/VerticalShafts opt-in and deterministic seed from `(seed, strate, recipe version, stable ID)`. | +| `MakeNativeLatticeSpine`, `--recipe=lattice`, `--lattice-strate=N` | `Standalone/Source/StreamBenchmarkHeadless.cpp` | Resolves actual adjacent mouths, builds the radius-4.5 m open shaft core, supported four-turn wall ledge and maze spine, then adds two seeded dead ends; the orphan control translates the shaft away from both junctions. | +| `AuditNativeLatticeField` | `Standalone/Source/StreamBenchmarkPipeline.h` | Samples the final field along the mouth-to-mouth capsule route, checking every-station support, headroom, grade, orphan rejection, composed fillet, shaft air/wall intervals and tile exclusion. | +| `BuildLatticeFixtureL59.py`, `CaptureNativeLatticeL59.py`, `AuditLatticeSurfaceL59.py` | `Standalone/Research/WorldAssemblerM3/Linux59/` | Isolated Maze fixture, four JPEG views of the open shaft and maze, and L54 whole-surface audit extended to include the ramp underside. | -See ARCHITECTURE §8.54 and -[`Standalone/Research/WorldAssemblerM3/Linux58/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux58/REPORT.md) -for resolved records, final-field reachability, controls, timing, hashes and captures. +See ARCHITECTURE §8.54 and the +[`Linux59 report`](Standalone/Research/WorldAssemblerM3/Linux59/REPORT.md) for the open shaft's +measured geometry, final-field reachability, controls, timing, hashes and captures. The L58 report +retains the first-slice evidence. ## 4. The density pipeline (most-edited hot path) diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 3ec913d..2fbaa20 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -1004,11 +1004,13 @@ namespace VoxelForgeCore &&stratum.recipeVersion==1; } + inline constexpr std::uint32_t NativeLatticeFieldRecipeVersion=2; + inline bool IsNativeLatticeFieldStratum(const ResolvedLayoutStratumV2& stratum) noexcept { return (stratum.generatorKind=="Maze"||stratum.generatorKind=="VerticalShafts") &&stratum.recipeId=="Core.Lattice.PrimitiveField" - &&stratum.recipeVersion==1; + &&stratum.recipeVersion==NativeLatticeFieldRecipeVersion; } inline bool HasNativeGraphField(const ResolvedLayoutStratumV2& stratum) noexcept diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h index 621b6ea..c7bc081 100644 --- a/Standalone/Core/src/ResolvedLayoutInternal.h +++ b/Standalone/Core/src/ResolvedLayoutInternal.h @@ -1901,7 +1901,8 @@ bool ResolveResolvedCavernGraphV1(ResolvedLayoutSnapshotV3& layout,std::int32_t // The shared JSON value type stores numbers as IEEE-754 doubles. Keep the // serialized seed in the exact integer range, as the stamp vocabulary does. room.irregularitySeed=(isNativeLatticeStrate(room.strateIndex) - ?LayoutLatticeFeatureSeed(seed,room.strateIndex,1,room.stableId) + ?LayoutLatticeFeatureSeed(seed,room.strateIndex, + NativeLatticeFieldRecipeVersion,room.stableId) :LayoutStampSeed(seed,room.strateIndex,room.stableId)) &0x001fffffffffffffull; const std::int64_t pad=room.smoothWidthMicrometres @@ -1918,7 +1919,8 @@ bool ResolveResolvedCavernGraphV1(ResolvedLayoutSnapshotV3& layout,std::int32_t { const bool nativeLattice=isNativeLatticeStrate(edge.strateIndex); edge.irregularitySeed=(nativeLattice - ?LayoutLatticeFeatureSeed(seed,edge.strateIndex,1,edge.stableId) + ?LayoutLatticeFeatureSeed(seed,edge.strateIndex, + NativeLatticeFieldRecipeVersion,edge.stableId) :LayoutStampSeed(seed,edge.strateIndex,edge.stableId)) &0x001fffffffffffffull; using FCurvePoint=std::array; diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 900baa8..b260f76 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -3332,6 +3332,14 @@ private: const VoxelForgeCore::ResolvedCavernRoomV1* room=nullptr; const VoxelForgeCore::ResolvedCavernEdgeV1* edge=nullptr; bool nativeGraph=false; + bool openShaftRamp=false; + std::array openShaftCenterMetres{}; + std::array openShaftStartDirection{1.0,0.0}; + double openShaftCenterlineRadiusMetres=0.0; + double openShaftStartFloorMetres=0.0; + double openShaftPitchPerTurnMetres=0.0; + double openShaftTurnCount=0.0; + double openShaftRotationSign=1.0; std::array boundsMinimumMicrometres{},boundsMaximumMicrometres{}; std::array centerMetres{},halfExtentsMetres{}; struct FSegment @@ -3792,6 +3800,15 @@ private: :found->passageCapabilities.hasNativeLatticeField); }; const float micronsPerVoxel=static_cast(Owner->CoreVoxelSizeMicrons); + const auto shaftCoreRoom=std::find_if(ResolvedLayout->cavernRooms.begin(), + ResolvedLayout->cavernRooms.end(),[](const auto& room) + {return room.stableId=="lattice/spine/shaft-core-v2";}); + std::array shaftCoreCenter{}; + if(shaftCoreRoom!=ResolvedLayout->cavernRooms.end()) + { + shaftCoreCenter={shaftCoreRoom->centerMicrometres[0]/1.0e6, + shaftCoreRoom->centerMicrometres[1]/1.0e6}; + } for(const auto& room:ResolvedLayout->cavernRooms) { FCavernRuntimeFeature runtime;runtime.room=&room; @@ -3822,6 +3839,11 @@ private: { FCavernRuntimeFeature runtime;runtime.edge=&edge; runtime.nativeGraph=isNativeGraphStrate(edge.strateIndex); + runtime.openShaftRamp=runtime.nativeGraph + &&edge.stableId=="lattice/spine/01-helix-ramp-v1" + &&shaftCoreRoom!=ResolvedLayout->cavernRooms.end(); + if(runtime.openShaftRamp) + runtime.openShaftCenterMetres=shaftCoreCenter; runtime.boundsMinimumMicrometres=edge.boundsMinimumMicrometres; runtime.boundsMaximumMicrometres=edge.boundsMaximumMicrometres; for(std::size_t point=0;point<4;++point) @@ -3865,6 +3887,37 @@ private: std::abs((segment.end[2]-segment.start[2])/horizontal)); runtime.segments.push_back(segment); } + if(runtime.openShaftRamp&&!runtime.segments.empty()) + { + const auto& first=runtime.segments.front().start; + const auto& last=runtime.segments.back().end; + const double startX=first[0]-shaftCoreCenter[0]; + const double startY=first[1]-shaftCoreCenter[1]; + runtime.openShaftCenterlineRadiusMetres=std::hypot(startX,startY); + if(runtime.openShaftCenterlineRadiusMetres>1.0e-9) + runtime.openShaftStartDirection={ + startX/runtime.openShaftCenterlineRadiusMetres, + startY/runtime.openShaftCenterlineRadiusMetres}; + runtime.openShaftStartFloorMetres=first[2]; + double totalAngle=0.0; + for(std::size_t segment=0;segment(index); @@ -5245,9 +5298,50 @@ public: const std::int32_t featureStrate=feature.room?feature.room->strateIndex :feature.edge?feature.edge->strateIndex:-1; if(featureStrate!=strateIndex)continue; + bool featureOverlapsBox=true,boxWithinFeatureBounds=true; + for(std::size_t axis=0;axis<3;++axis) + { + const double featureMinimum=feature.boundsMinimumMicrometres[axis]/1.0e6; + const double featureMaximum=feature.boundsMaximumMicrometres[axis]/1.0e6; + if(hi[axis]featureMaximum) + {featureOverlapsBox=false;break;} + boxWithinFeatureBounds=boxWithinFeatureBounds + &&lo[axis]>=featureMinimum&&hi[axis]<=featureMaximum; + } + if(!featureOverlapsBox)continue; + // The point field omits a record outside its resolved bounds. A partial + // overlap cannot reuse the record's unbounded SDF for an all-air proof. + if(!boxWithinFeatureBounds)return {0.0,0.0,false}; FMetricInterval featureVoid; + bool openHelixSupportCouldApply=true; + double openHelixSupportLowerBound=std::numeric_limits::infinity(); if(feature.room) { + if(nativeLattice + &&feature.room->stableId=="lattice/spine/shaft-core-v2") + { + const double radius=feature.halfExtentsMetres[0]; + if(!(radius>0.0))return {0.0,0.0,false}; + double radialMinimumSquared=0.0,radialMaximumSquared=0.0; + for(std::size_t axis=0;axis<2;++axis) + { + const double center=feature.centerMetres[axis]; + const double deltaMinimum=centerhi[axis]?center-hi[axis]:0.0; + const double deltaMaximum=std::max(std::abs(lo[axis]-center), + std::abs(hi[axis]-center)); + radialMinimumSquared+=deltaMinimum*deltaMinimum; + radialMaximumSquared+=deltaMaximum*deltaMaximum; + } + const double radialMinimum=std::sqrt(radialMinimumSquared)-radius; + const double radialMaximum=std::sqrt(radialMaximumSquared)-radius; + const double floorZ=feature.room->floorMicrometres/1000000.0; + const FMetricInterval bottom={floorZ-hi[2],floorZ-lo[2],true}; + featureVoid={std::max(radialMinimum,bottom.minimum), + std::max(radialMaximum,bottom.maximum),true}; + } + else + { double normalizedMinimumSquared=0.0,normalizedMaximumSquared=0.0; double meanRadius=0.0; for(std::size_t axis=0;axis<3;++axis) @@ -5271,14 +5365,135 @@ public: const double width=feature.room->smoothWidthMicrometres/1000000.0; featureVoid={SmoothMaxMetres(ellipsoid.minimum,floor.minimum,width), SmoothMaxMetres(ellipsoid.maximum,floor.maximum,width),true}; + } } else if(feature.edge&&!feature.segments.empty()) { + const bool openHelixRamp=nativeLattice&&feature.openShaftRamp; const double radius=feature.edge->boreRadiusMicrometres/1000000.0; const double height=feature.edge->boreHeightMicrometres/1000000.0; if(!(radius>0.0&&height>0.0))return {0.0,0.0,false}; double tubeMinimum=std::numeric_limits::infinity(); double tubeMaximum=std::numeric_limits::infinity(); + if(openHelixRamp) + { + double radialMinimumSquared=0.0,radialMaximumSquared=0.0; + for(std::size_t axis=0;axis<2;++axis) + { + const double center=feature.openShaftCenterMetres[axis]; + const double deltaMinimum=centerhi[axis]?center-hi[axis]:0.0; + const double deltaMaximum=std::max(std::abs(lo[axis]-center), + std::abs(hi[axis]-center)); + radialMinimumSquared+=deltaMinimum*deltaMinimum; + radialMaximumSquared+=deltaMaximum*deltaMaximum; + } + const double radialMinimum=std::sqrt(radialMinimumSquared); + const double radialMaximum=std::sqrt(radialMaximumSquared); + const double centerline=feature.openShaftCenterlineRadiusMetres; + const double nearestRingDistance=centerlineradialMaximum + ?centerline-radialMaximum:0.0; + tubeMinimum=nearestRingDistance-radius; + tubeMaximum=std::max(std::abs(radialMinimum-centerline), + std::abs(radialMaximum-centerline))-radius; + openHelixSupportCouldApply=tubeMinimum<0.0; + const double startFloor=feature.openShaftStartFloorMetres; + const double pitch=feature.openShaftPitchPerTurnMetres; + const double turnCount=feature.openShaftTurnCount; + if(!std::isfinite(tubeMinimum)||!std::isfinite(tubeMaximum) + ||!std::isfinite(startFloor)||!(pitch>0.0) + ||!(turnCount>=1.0))return {0.0,0.0,false}; + const FMetricInterval tube={tubeMinimum,tubeMaximum,true}; + const double grade=pitch/(6.283185307179586476925286766559 + *feature.openShaftCenterlineRadiusMetres); + const double verticalScale=1.0/std::sqrt(1.0+grade*grade); + double phaseMinimum=0.0,phaseMaximum=1.0; + const double boxCenterX=(lo[0]+hi[0])*0.5; + const double boxCenterY=(lo[1]+hi[1])*0.5; + const double centerDx=boxCenterX-feature.openShaftCenterMetres[0]; + const double centerDy=boxCenterY-feature.openShaftCenterMetres[1]; + const double centerDistance=std::hypot(centerDx,centerDy); + const double halfDiagonal=0.5*std::hypot(hi[0]-lo[0],hi[1]-lo[1]); + if(centerDistance>halfDiagonal) + { + const double angle=std::atan2(feature.openShaftRotationSign + *(feature.openShaftStartDirection[0]*centerDy + -feature.openShaftStartDirection[1]*centerDx), + feature.openShaftStartDirection[0]*centerDx + +feature.openShaftStartDirection[1]*centerDy); + constexpr double TwoPi=6.283185307179586476925286766559; + double centerPhase=angle/TwoPi; + const double angleRadius=std::asin(std::clamp( + halfDiagonal/centerDistance,0.0,1.0)); + const double phaseRadius=angleRadius/TwoPi; + // Point evaluation normalizes phase into [0,1). If this box + // crosses the start ray, retain the full phase range; otherwise + // unwrap its center into the same range as the point field. + if(std::abs(angle)>angleRadius) + { + if(centerPhase<0.0)centerPhase+=1.0; + phaseMinimum=centerPhase-phaseRadius; + phaseMaximum=centerPhase+phaseRadius; + } + } + double verticalMinimum=std::numeric_limits::infinity(); + double verticalMaximum=-std::numeric_limits::infinity(); + const double coordinateMinimum=(startFloor-hi[2])/pitch-phaseMaximum; + const double coordinateMaximum=(startFloor-lo[2])/pitch-phaseMinimum; + if(!std::isfinite(coordinateMinimum)||!std::isfinite(coordinateMaximum)) + return {0.0,0.0,false}; + const int lastTurn=std::max(0,static_cast(turnCount)-1); + const int firstPossibleTurn=std::clamp( + static_cast(std::floor(coordinateMinimum)),-1,lastTurn+1); + const int lastPossibleTurn=std::clamp( + static_cast(std::floor(coordinateMaximum)),-1,lastTurn+1); + const auto corridorInterval=[&](int turn) + { + const double floorMinimum=startFloor-pitch*(turn+phaseMaximum); + const double floorMaximum=startFloor-pitch*(turn+phaseMinimum); + const double floorToQueryMinimum=(floorMinimum-hi[2])*verticalScale; + const double floorToQueryMaximum=(floorMaximum-lo[2])*verticalScale; + const double queryToCeilingMinimum=(lo[2]-floorMaximum-height) + *verticalScale; + const double queryToCeilingMaximum=(hi[2]-floorMinimum-height) + *verticalScale; + const double turnMinimum=std::max(floorToQueryMinimum, + queryToCeilingMinimum); + const double turnMaximum=std::max(floorToQueryMaximum, + queryToCeilingMaximum); + return FMetricInterval{turnMinimum,turnMaximum,true}; + }; + for(int turn=firstPossibleTurn;turn<=lastPossibleTurn;++turn) + { + const int turnA=std::clamp(turn,0,lastTurn); + const int turnB=std::clamp(turn+1,0,lastTurn); + const FMetricInterval corridorA=corridorInterval(turnA); + const FMetricInterval corridorB=corridorInterval(turnB); + verticalMinimum=std::min(verticalMinimum, + std::min(corridorA.minimum,corridorB.minimum)); + verticalMaximum=std::max(verticalMaximum, + std::min(corridorA.maximum,corridorB.maximum)); + const double supportDepth=feature.edge->floorSupportDepthMicrometres + /1000000.0; + const double progressMinimum=turn+phaseMinimum; + const double progressMaximum=turn+phaseMaximum; + const double floorMinimum=startFloor-pitch*progressMaximum; + const double floorMaximum=startFloor-pitch*progressMinimum; + const double supportFloorMinimum=(floorMinimum-hi[2])*verticalScale; + const double supportUndersideMinimum=(lo[2]-floorMaximum+supportDepth) + *verticalScale; + openHelixSupportLowerBound=std::min(openHelixSupportLowerBound, + std::max({tubeMinimum,supportFloorMinimum, + supportUndersideMinimum})); + } + const FMetricInterval vertical={verticalMinimum,verticalMaximum,true}; + const double width=feature.edge->smoothWidthMicrometres/1000000.0; + featureVoid={SmoothMaxMetres(tube.minimum,vertical.minimum,width), + SmoothMaxMetres(tube.maximum,vertical.maximum,width),true}; + } + else + { const std::array inverseScale={1.0/radius,1.0/radius, 1.0/height}; const std::array boxMinimum={lo[0]*inverseScale[0], @@ -5387,6 +5602,7 @@ public: const double width=feature.edge->smoothWidthMicrometres/1000000.0; featureVoid={SmoothMaxMetres(tube.minimum,floor.minimum,width), SmoothMaxMetres(tube.maximum,floor.maximum,width),true}; + } } else return {0.0,0.0,false}; if(nativeLattice) @@ -5401,10 +5617,17 @@ public: &&lo[0]<=feature.boundsMaximumMicrometres[0]/1.0e6 &&hi[1]>=feature.boundsMinimumMicrometres[1]/1.0e6 &&lo[1]<=feature.boundsMaximumMicrometres[1]/1.0e6; - if(supportDepth>0.0&&overlapsXY + const bool supportCouldApply=overlapsXY + &&(!feature.openShaftRamp||openHelixSupportCouldApply); + if(supportDepth>0.0&&supportCouldApply &&hi[2]>=floorMinimum-supportDepth&&lo[2]<=floorMaximum) + { + double supportLower=std::max(-supportDepth,lo[2]-floorMaximum); + if(feature.openShaftRamp) + supportLower=openHelixSupportLowerBound; latticeFloorSupportLowerBound=std::min(latticeFloorSupportLowerBound, - std::max(-supportDepth,lo[2]-floorMaximum)); + supportLower); + } } if(!std::isfinite(featureVoid.minimum)||!std::isfinite(featureVoid.maximum)) return {0.0,0.0,false}; @@ -6962,6 +7185,10 @@ private: return; } if(!feature.edge)return; + // The open helix is already carved with its exact cylindrical/turn + // field above. Its four serialized control points are a compact record + // envelope, not a walkable Bezier centerline for this legacy cap. + if(feature.openShaftRamp)return; const auto bezier=[&](float t,std::size_t axis) { const float one=1.0f-t; @@ -7098,114 +7325,209 @@ private: const std::int32_t strateIndex=feature.room?feature.room->strateIndex :feature.edge?feature.edge->strateIndex:-1; if(strateIndex!=bound.StrateIndex)continue; + bool insideFeatureBounds=true; + for(std::size_t axis=0;axis<3;++axis) + insideFeatureBounds=insideFeatureBounds + &&point[axis]*1000000.0>=feature.boundsMinimumMicrometres[axis] + &&point[axis]*1000000.0<=feature.boundsMaximumMicrometres[axis]; + if(!insideFeatureBounds)continue; double featureVoid=std::numeric_limits::infinity(); if(feature.room) { - double normalizedSquared=0.0,meanRadius=0.0; - for(std::size_t axis=0;axis<3;++axis) + if(nativeLattice + &&feature.room->stableId=="lattice/spine/shaft-core-v2") { - const double radius=feature.halfExtentsMetres[axis]; - if(!(radius>0.0)){normalizedSquared=std::numeric_limits::infinity();break;} - const double delta=(point[axis]-feature.centerMetres[axis])/radius; - normalizedSquared+=delta*delta;meanRadius+=radius/3.0; + const double radius=feature.halfExtentsMetres[0]; + if(!(radius>0.0))continue; + const double radial=std::hypot( + point[0]-feature.centerMetres[0], + point[1]-feature.centerMetres[1])-radius; + const double bottom=feature.room->floorMicrometres/1000000.0-point[2]; + featureVoid=std::max(radial,bottom); + } + else + { + double normalizedSquared=0.0,meanRadius=0.0; + for(std::size_t axis=0;axis<3;++axis) + { + const double radius=feature.halfExtentsMetres[axis]; + if(!(radius>0.0)) + {normalizedSquared=std::numeric_limits::infinity();break;} + const double delta=(point[axis]-feature.centerMetres[axis])/radius; + normalizedSquared+=delta*delta;meanRadius+=radius/3.0; + } + const double ellipsoid=(std::sqrt(normalizedSquared)-1.0)*meanRadius; + const double floor=feature.room->floorMicrometres/1000000.0-point[2]; + const double width=feature.room->smoothWidthMicrometres/1000000.0; + featureVoid=SmoothMaxMetres(ellipsoid,floor,width); } - const double ellipsoid=(std::sqrt(normalizedSquared)-1.0)*meanRadius; - const double floor=feature.room->floorMicrometres/1000000.0-point[2]; - const double width=feature.room->smoothWidthMicrometres/1000000.0; - featureVoid=SmoothMaxMetres(ellipsoid,floor,width); } else if(feature.edge&&!feature.segments.empty()) { + const bool openHelixRamp=nativeLattice&&feature.openShaftRamp; double tube=std::numeric_limits::infinity(); double nearestFloor=0.0,nearestGrade=0.0,nearestRadial=0.0; + double openHelixVerticalSdf=std::numeric_limits::infinity(); + double openHelixPhase=0.0; + int openHelixTurnA=0,openHelixTurnB=0; + double openHelixVerticalScale=1.0; const double radius=feature.edge->boreRadiusMicrometres/1000000.0; const double height=feature.edge->boreHeightMicrometres/1000000.0; if(!(radius>0.0&&height>0.0))continue; const double minScale=std::min(radius,height); - std::uint32_t nearestSegment=std::numeric_limits::max(); - const auto evaluateSegment=[&](std::uint32_t segmentIndex) + if(openHelixRamp) { - if(segmentIndex>=feature.segments.size())return; - const auto& segment=feature.segments[segmentIndex]; - double lengthSquared=0.0,dot=0.0; - for(std::size_t axis=0;axis<3;++axis) + const double dx=point[0]-feature.openShaftCenterMetres[0]; + const double dy=point[1]-feature.openShaftCenterMetres[1]; + const double shaftRadial=std::hypot(dx,dy); + nearestRadial=std::abs(shaftRadial + -feature.openShaftCenterlineRadiusMetres); + tube=nearestRadial-radius; + const double phase=std::atan2(feature.openShaftRotationSign + *(feature.openShaftStartDirection[0]*dy + -feature.openShaftStartDirection[1]*dx), + feature.openShaftStartDirection[0]*dx + +feature.openShaftStartDirection[1]*dy); + constexpr double TwoPi=6.283185307179586476925286766559; + openHelixPhase=(phase<0.0?phase+TwoPi:phase)/TwoPi; + const double pitch=feature.openShaftPitchPerTurnMetres; + if(!(pitch>0.0)||!(feature.openShaftTurnCount>=1.0))continue; + const double grade=pitch/(TwoPi + *feature.openShaftCenterlineRadiusMetres); + openHelixVerticalScale=1.0/std::sqrt(1.0+grade*grade); + const double turnCoordinate=(feature.openShaftStartFloorMetres-point[2]) + /pitch-openHelixPhase; + const int turnFloor=static_cast(std::floor(turnCoordinate)); + const int lastTurn=std::max(0, + static_cast(feature.openShaftTurnCount)-1); + openHelixTurnA=std::clamp(turnFloor,0,lastTurn); + openHelixTurnB=std::clamp(turnFloor+1,0,lastTurn); + const auto floorForTurn=[&](int turn) { - const double delta=segment.end[axis]-segment.start[axis]; - lengthSquared+=delta*delta; - dot+=(point[axis]-segment.start[axis])*delta; - } - const double t=lengthSquared>0.0 - ?std::clamp(dot/lengthSquared,0.0,1.0):0.0; - double radialSquared=0.0,vertical=0.0; - for(std::size_t axis=0;axis<3;++axis) - { - const double delta=point[axis]-(segment.start[axis] - +t*(segment.end[axis]-segment.start[axis])); - if(axis<2)radialSquared+=delta*delta; - else vertical=delta; - } - const double normalized=std::sqrt(radialSquared/(radius*radius) - +vertical*vertical/(height*height)); - const double distance=(normalized-1.0)*std::min(radius,height); - if(distance1.0e-9 - ?(segment.end[2]-segment.start[2])/horizontal:0.0; - } - }; - const auto nodeLowerBound=[&](const FCavernRuntimeFeature::FSegmentNode& node) - { - double gapSquared=0.0; - for(std::size_t axis=0;axis<3;++axis) - { - const double scale=axis==2?height:radius; - const double q=point[axis]/scale; - const double low=node.minimum[axis]/scale; - const double high=node.maximum[axis]/scale; - const double gap=qhigh?q-high:0.0; - gapSquared+=gap*gap; - } - return (std::sqrt(gapSquared)-1.0)*minScale; - }; - if(!feature.segmentNodes.empty()) - { - const auto visitNode=[&](const auto& self,int nodeIndex)->void - { - if(nodeIndex<0||static_cast(nodeIndex)>=feature.segmentNodes.size())return; - const auto& node=feature.segmentNodes[static_cast(nodeIndex)]; - if(nodeLowerBound(node)>tube)return; - if(node.left<0) - { - for(std::size_t offset=node.first;offset(node.left)]); - const double rightBound=nodeLowerBound(feature.segmentNodes[ - static_cast(node.right)]); - if(leftBound<=rightBound) - {self(self,node.left);self(self,node.right);} - else {self(self,node.right);self(self,node.left);} + return feature.openShaftStartFloorMetres + -pitch*(turn+openHelixPhase); }; - visitNode(visitNode,0); + const auto corridorSdf=[&](int turn) + { + const double floorZ=floorForTurn(turn); + return std::max((floorZ-point[2])*openHelixVerticalScale, + (point[2]-(floorZ+height))*openHelixVerticalScale); + }; + openHelixVerticalSdf=std::min(corridorSdf(openHelixTurnA), + corridorSdf(openHelixTurnB)); + } + else + { + std::uint32_t nearestSegment=std::numeric_limits::max(); + const auto evaluateSegment=[&](std::uint32_t segmentIndex) + { + if(segmentIndex>=feature.segments.size())return; + const auto& segment=feature.segments[segmentIndex]; + double lengthSquared=0.0,dot=0.0; + for(std::size_t axis=0;axis<3;++axis) + { + const double delta=segment.end[axis]-segment.start[axis]; + lengthSquared+=delta*delta; + dot+=(point[axis]-segment.start[axis])*delta; + } + const double t=lengthSquared>0.0 + ?std::clamp(dot/lengthSquared,0.0,1.0):0.0; + double radialSquared=0.0,vertical=0.0; + for(std::size_t axis=0;axis<3;++axis) + { + const double delta=point[axis]-(segment.start[axis] + +t*(segment.end[axis]-segment.start[axis])); + if(axis<2)radialSquared+=delta*delta; + else vertical=delta; + } + const double normalized=std::sqrt(radialSquared/(radius*radius) + +vertical*vertical/(height*height)); + const double distance=(normalized-1.0)*minScale; + if(distance1.0e-9 + ?(segment.end[2]-segment.start[2])/horizontal:0.0; + } + }; + const auto nodeLowerBound=[&](const FCavernRuntimeFeature::FSegmentNode& node) + { + double gapSquared=0.0; + for(std::size_t axis=0;axis<3;++axis) + { + const double scale=axis==2?height:radius; + const double q=point[axis]/scale; + const double low=node.minimum[axis]/scale; + const double high=node.maximum[axis]/scale; + const double gap=qhigh?q-high:0.0; + gapSquared+=gap*gap; + } + return (std::sqrt(gapSquared)-1.0)*minScale; + }; + if(!feature.segmentNodes.empty()) + { + const auto visitNode=[&](const auto& self,int nodeIndex)->void + { + if(nodeIndex<0||static_cast(nodeIndex)>=feature.segmentNodes.size())return; + const auto& node=feature.segmentNodes[static_cast(nodeIndex)]; + if(nodeLowerBound(node)>tube)return; + if(node.left<0) + { + for(std::size_t offset=node.first;offset(node.left)]); + const double rightBound=nodeLowerBound(feature.segmentNodes[ + static_cast(node.right)]); + if(leftBound<=rightBound) + {self(self,node.left);self(self,node.right);} + else {self(self,node.right);self(self,node.left);} + }; + visitNode(visitNode,0); + } + else for(std::uint32_t segment=0;segmentfloorSupportDepthMicrometres/1000000.0; - if(nativeLattice&&supportDepth>0.0&&nearestRadial0.0) + { + if(openHelixRamp) + { + const auto supportSdf=[&](int turn) + { + const double floorZ=feature.openShaftStartFloorMetres + -feature.openShaftPitchPerTurnMetres + *(turn+openHelixPhase); + const double vertical=std::max( + (floorZ-point[2])*openHelixVerticalScale, + (point[2]-(floorZ-supportDepth)) + *openHelixVerticalScale); + return std::max(nearestRadial-radius,vertical); + }; + latticeFloorSupportMinimum=std::min(latticeFloorSupportMinimum, + std::min(supportSdf(openHelixTurnA), + supportSdf(openHelixTurnB))); + } + else if(nearestRadialsmoothWidthMicrometres/1000000.0; - featureVoid=SmoothMaxMetres(tube,floor,width); + if(openHelixRamp) + featureVoid=SmoothMaxMetres(tube,openHelixVerticalSdf,width); + else + { + const double floor=(nearestFloor-point[2]) + /std::sqrt(1.0+nearestGrade*nearestGrade); + featureVoid=SmoothMaxMetres(tube,floor,width); + } } if(!std::isfinite(featureVoid))continue; if(!std::isfinite(caveVoid))caveVoid=featureVoid; diff --git a/Standalone/Research/WorldAssemblerM3/Linux59/AuditLatticeSurfaceL59.py b/Standalone/Research/WorldAssemblerM3/Linux59/AuditLatticeSurfaceL59.py new file mode 100644 index 0000000..9743cf2 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux59/AuditLatticeSurfaceL59.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +"""Run L54's triangle audit over L59's open-shaft lattice and controls.""" +from __future__ import annotations + +import argparse +import importlib.util +import json +import math +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +AUDIT54 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux54/AuditWholeSurfaceL54.py" +spec = importlib.util.spec_from_file_location("whole_surface_l54", AUDIT54) +audit54 = importlib.util.module_from_spec(spec) +assert spec and spec.loader +spec.loader.exec_module(audit54) +original_make_route = audit54.make_route + + +def build_segment_bvh(segments: list[tuple]) -> dict: + def build(indices: list[int]) -> dict: + minimum = tuple(min(min(segments[index][0][axis], segments[index][1][axis]) + for index in indices) for axis in range(3)) + maximum = tuple(max(max(segments[index][0][axis], segments[index][1][axis]) + for index in indices) for axis in range(3)) + if len(indices) <= 8: + return {"minimum": minimum, "maximum": maximum, + "segments": tuple(sorted(indices)), "left": None, "right": None} + axis = max(range(3), key=lambda value: maximum[value] - minimum[value]) + indices.sort(key=lambda index: (segments[index][0][axis] + + segments[index][1][axis]) * 0.5) + middle = len(indices) // 2 + return {"minimum": minimum, "maximum": maximum, "segments": None, + "left": build(indices[:middle]), "right": build(indices[middle:])} + + if not segments: + raise ValueError("lattice audit route has no measurable segments") + return build(list(range(len(segments)))) + + +def make_lattice_route(layout: dict, phase_m: float = 0.0) -> dict: + edges = sorted((edge for edge in layout.get("cavern_edges", []) + if edge["stable_id"] in ("lattice/spine/00-arrival-portal-v1", + "lattice/spine/01-helix-ramp-v1", "lattice/spine/99-departure-portal-v1") + or edge["stable_id"].startswith("lattice/spine/maze-edge-")), + key=lambda edge: (0 if edge["stable_id"].startswith("lattice/spine/00-") else + 1 if edge["stable_id"].startswith("lattice/spine/01-") else + 2 if edge["stable_id"].startswith("lattice/spine/maze-edge-") else 3, + edge["stable_id"])) + if not edges: + raise ValueError("lattice audit needs its resolved mandatory spine edges") + segments = [] + routes = [] + for edge_index, edge in enumerate(edges): + points = [[value / 1_000_000.0 for value in row] + for row in edge.get("polyline_um", [])] + if len(points) < 2: + raise ValueError(f"{edge['stable_id']}: explicit path is missing") + radius = edge["bore_radius_um"] / 1_000_000.0 + height = edge["bore_height_um"] / 1_000_000.0 + support_depth = edge.get("floor_support_depth_um", 0) / 1_000_000.0 + horizontal_s = [0.0] + for a, b in zip(points, points[1:]): + horizontal_s.append(horizontal_s[-1] + ((b[0] - a[0]) ** 2 + + (b[1] - a[1]) ** 2) ** 0.5) + first = points[1] + first_dx, first_dy = first[0] - points[0][0], first[1] - points[0][1] + first_length = max(1.0e-9, (first_dx * first_dx + first_dy * first_dy) ** 0.5) + extension = (points[0][0] - first_dx / first_length * audit54.OPEN_GROUND_EXTENSION_M, + points[0][1] - first_dy / first_length * audit54.OPEN_GROUND_EXTENSION_M, + points[0][2] - (first[2] - points[0][2]) / first_length + * audit54.OPEN_GROUND_EXTENSION_M) + route_points = [extension, *points] + route_s = [-audit54.OPEN_GROUND_EXTENSION_M, *horizontal_s] + for index, (a, b) in enumerate(zip(route_points, route_points[1:])): + segments.append((a, b, route_s[index], route_s[index + 1], + radius, height, edge_index)) + routes.append({"edge": edge, "points": points, "radius": radius, + "height": height, "support_depth": support_depth, + "length": horizontal_s[-1], "index": edge_index}) + # The shaft core is an open cylindrical volume, not a walkable room. Its + # axis-aligned bounds include unrelated rock outside the cylinder and + # above the ramp headroom; the helix corridor already includes the inner + # wall, lip, foot, and underside where those surfaces meet the route. + rooms = [{"room": room, + "center": tuple(value / 1_000_000.0 for value in room["center_um"]), + "half": tuple(value / 1_000_000.0 for value in room["half_extents_um"]), + "floor": room["floor_um"] / 1_000_000.0} + for room in sorted(layout.get("cavern_rooms", []), key=lambda row: row["stable_id"]) + if room.get("stable_id") != "lattice/spine/shaft-core-v2"] + return {"segments": segments, "edges": routes, "rooms": rooms, "phase_m": phase_m, + "segment_bvh": build_segment_bvh(segments)} + + +def make_route(layout: dict, phase_m: float = 0.0) -> dict: + if any(edge.get("stable_id", "").startswith("lattice/") + for edge in layout.get("cavern_edges", [])): + return make_lattice_route(layout, phase_m) + return original_make_route(layout, phase_m) + + +def lattice_route_nearest(point: tuple[float, float, float], route: dict): + """Resolve stacked helix turns in 3D, then measure corridor radius in XY.""" + best = None + def consider(segment_index: int) -> None: + nonlocal best + a, b, s0, s1, radius, height, edge_index = route["segments"][segment_index] + dx, dy, dz = (b[axis] - a[axis] for axis in range(3)) + length_squared = dx * dx + dy * dy + dz * dz + t = 0.0 if length_squared <= 1.0e-12 else max(0.0, min(1.0, + ((point[0] - a[0]) * dx + (point[1] - a[1]) * dy + + (point[2] - a[2]) * dz) / length_squared)) + x, y, z = (a[axis] + (b[axis] - a[axis]) * t for axis in range(3)) + distance3d_squared = ((point[0] - x) ** 2 + (point[1] - y) ** 2 + + (point[2] - z) ** 2) + if (best is None or distance3d_squared < best[0] + or (distance3d_squared == best[0] and segment_index < best[1])): + horizontal_length = math.hypot(dx, dy) + lateral = ((dx * (point[1] - y) - dy * (point[0] - x)) + / horizontal_length if horizontal_length > 1.0e-9 else 0.0) + best = (distance3d_squared, segment_index, + math.hypot(point[0] - x, point[1] - y), + s0 + (s1 - s0) * t, z, lateral, radius, height, edge_index, + route["edges"][edge_index]["length"]) + + def lower_bound_squared(node: dict) -> float: + distance_squared = 0.0 + for axis in range(3): + gap = (node["minimum"][axis] - point[axis] + if point[axis] < node["minimum"][axis] + else point[axis] - node["maximum"][axis] + if point[axis] > node["maximum"][axis] else 0.0) + distance_squared += gap * gap + return distance_squared + + def visit(node: dict) -> None: + if best is not None and lower_bound_squared(node) > best[0]: + return + if node["segments"] is not None: + for segment_index in node["segments"]: + consider(segment_index) + return + left, right = node["left"], node["right"] + left_bound, right_bound = lower_bound_squared(left), lower_bound_squared(right) + if left_bound <= right_bound: + visit(left) + visit(right) + else: + visit(right) + visit(left) + + visit(route["segment_bvh"]) + if best is None: + raise ValueError("lattice audit route has no measurable segments") + return best[2:] + + +def mark_lattice_region(triangles: list, route: dict) -> None: + margin = audit54.CORRIDOR_MARGIN_M + extension = audit54.OPEN_GROUND_EXTENSION_M + bounds = [] + for edge in route["edges"]: + points = edge["points"] + radius, height = edge["radius"], edge["height"] + support_depth = edge["support_depth"] + bounds.append(( + min(point[0] for point in points) - radius - margin, + max(point[0] for point in points) + radius + margin, + min(point[1] for point in points) - radius - margin, + max(point[1] for point in points) + radius + margin, + min(point[2] for point in points) - support_depth - margin, + max(point[2] for point in points) + height + margin)) + for room in route["rooms"]: + center, half, floor = room["center"], room["half"], room["floor"] + bounds.append((center[0] - half[0] - margin, center[0] + half[0] + margin, + center[1] - half[1] - margin, center[1] + half[1] + margin, + floor - margin, floor + 2.0 * half[2] + margin)) + + for triangle in triangles: + x, y, z = triangle.center + if not any(low_x <= x <= high_x and low_y <= y <= high_y + and low_z <= z <= high_z + for low_x, high_x, low_y, high_y, low_z, high_z in bounds): + triangle.in_region = False + triangle.reference = False + continue + nearest = lattice_route_nearest(triangle.center, route) + distance, distance_s, floor_z, lateral, radius, height, edge_index, edge_length = nearest + support_depth = route["edges"][edge_index]["support_depth"] + corridor = (-extension <= distance_s <= edge_length + and distance <= radius + margin + and floor_z - support_depth - margin <= z <= floor_z + height + margin) + room_box = any(abs(x - room["center"][0]) <= room["half"][0] + margin + and abs(y - room["center"][1]) <= room["half"][1] + margin + and room["floor"] - margin <= z + <= room["floor"] + 2.0 * room["half"][2] + margin + for room in route["rooms"]) + triangle.in_region = corridor or room_box + inside_room = any(abs(x - room["center"][0]) <= room["half"][0] + 0.45 + and abs(y - room["center"][1]) <= room["half"][1] + 0.45 + for room in route["rooms"]) + outside_bore = distance > radius + 0.45 and not inside_room + triangle.reference = (triangle.in_region and outside_bore + and triangle.smooth[2] >= 0.45) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--case", nargs=5, action="append", required=True, + metavar=("LABEL", "MESH", "LAYOUT_OR_DASH", "EXPECT", "FOCUS")) + parser.add_argument("--json", type=Path, required=True) + parser.add_argument("--svg-dir", type=Path) + parser.add_argument("--phase", type=float, choices=(-0.5, 0.0, 0.5), default=0.0) + args = parser.parse_args() + audit54.make_route = make_route + results = [] + for label, mesh, layout, expect, focus in args.case: + if expect not in ("pass", "fail", "measure") or focus not in ("whole", "floor"): + parser.error("EXPECT must be pass/fail/measure and FOCUS must be whole/floor") + native_lattice = layout != "-" and any( + edge.get("stable_id", "").startswith("lattice/") + for edge in json.loads(Path(layout).read_text(encoding="utf-8")).get( + "cavern_edges", [])) + original_margin = audit54.CORRIDOR_MARGIN_M + original_extension = audit54.OPEN_GROUND_EXTENSION_M + original_nearest = audit54.route_nearest + original_mark_region = audit54.mark_region + if native_lattice: + # Keep the whole-surface test on native records: one core voxel of + # tolerance captures their meshed walls while excluding the separate + # legacy transition above the resolved mouth. + audit54.CORRIDOR_MARGIN_M = 0.25 + audit54.OPEN_GROUND_EXTENSION_M = 0.0 + audit54.route_nearest = lattice_route_nearest + audit54.mark_region = mark_lattice_region + try: + results.append(audit54.audit_case(label, Path(mesh), Path(layout), expect, + focus, args.svg_dir, args.phase)) + finally: + audit54.CORRIDOR_MARGIN_M = original_margin + audit54.OPEN_GROUND_EXTENSION_M = original_extension + audit54.route_nearest = original_nearest + audit54.mark_region = original_mark_region + if native_lattice: + print("[L59LatticeAuditScope] margin_m=0.250 ramp_underside_depth_m=1.000 " + "outside_mouth_extension_m=0.000 nearest_centerline=3D " + "radius_measure=horizontal status=PASS") + args.json.parent.mkdir(parents=True, exist_ok=True) + args.json.write_text(json.dumps({"audit": "whole-surface-open-shaft-l59-v1", + "cases": results}, indent=2) + "\n", encoding="utf-8") + failures = [result for result in results if result["status"] != "PASS"] + print(f"[L59WholeSurfaceAuditSummary] cases={len(results)} failed={len(failures)} " + f"json={args.json} status={'FAIL' if failures else 'PASS'}") + return 1 if failures else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux59/BuildLatticeFixtureL59.py b/Standalone/Research/WorldAssemblerM3/Linux59/BuildLatticeFixtureL59.py new file mode 100644 index 0000000..4a32d16 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux59/BuildLatticeFixtureL59.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Split the owner surface strate around the L59 native lattice fixture.""" +from __future__ import annotations + +import argparse +import hashlib +import json +from pathlib import Path + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for block in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--owner", type=Path, required=True) + parser.add_argument("--out", type=Path, required=True) + args = parser.parse_args() + + root = json.loads(args.owner.read_text(encoding="utf-8")) + strates = root.get("strates") + if not isinstance(strates, list): + raise ValueError("owner input has no strates array") + by_index = {int(row.get("index", -1)): row for row in strates if isinstance(row, dict)} + if set(by_index) != {0, 1} or len(strates) != 2: + raise ValueError("fixture requires the reviewed two-strate owner input") + upper, surface = by_index[0], by_index[1] + if (int(upper.get("asset", {}).get("GeneratorType", -1)) != 0 + or int(surface.get("asset", {}).get("GeneratorType", -1)) != 4): + raise ValueError("fixture requires TunnelNetwork over SurfaceWorld") + if int(upper.get("passage_config", {}).get("Connections", -1)) != 1: + raise ValueError("owner arrival passage count differs from the reviewed input") + + # Keep both resolved mouths in the small synthetic Maze layer. The authored + # owner reach ranges are intentionally broad and can place a synthetic mouth + # hundreds of metres beyond a four-chunk prototype layer. + upper["passage_config"].update({"ReachMin": 32, "ReachMax": 32}) + upper["asset"]["PassageConfig"].update({"ReachMin": 32, "ReachMax": 32}) + + maze = json.loads(json.dumps(surface)) + maze.update({"index": 1, "top_chunk_z": -16, "bottom_chunk_z": -19, + "height_chunks": 4}) + maze["asset"].update({"StrateName": "L59 Native Lattice Maze", + "GeneratorType": 3, "StrateHeightInChunks": 4}) + maze["asset"].setdefault("MazeParams", {})["BoundarySealThickness"] = 8 + maze["passage_config"]["Connections"] = 1 + maze["passage_config"].update({"ReachMin": 32, "ReachMax": 32}) + maze["asset"]["PassageConfig"].update({"Connections": 1, "ReachMin": 32, "ReachMax": 32}) + + lower = json.loads(json.dumps(surface)) + lower.update({"index": 2, "top_chunk_z": -20, "bottom_chunk_z": -23, + "height_chunks": 4}) + lower["asset"].update({"StrateName": "L59 Lower Surface", "GeneratorType": 4, + "StrateHeightInChunks": 4}) + lower["passage_config"]["Connections"] = 1 + lower["asset"]["PassageConfig"]["Connections"] = 1 + + root["total_strates"] = 3 + root["strates"] = [upper, maze, lower] + # Keep the legacy passage portals proportional to this compact native + # prototype; the owner world remains at its authored scale. + root.update({"owner_voxel_size_cm": 6.25, + "owner_voxel_size_microns": 62500, + "core_voxel_size_cm": 25, + "core_voxel_size_microns": 250000, + "owner_to_core_length_scale": 0.25}) + args.out.parent.mkdir(parents=True, exist_ok=True) + args.out.write_text(json.dumps(root, indent=2, sort_keys=True) + "\n", + encoding="utf-8") + print(f"[LatticeFixtureBuild] source={args.owner} source_sha256={sha256(args.owner)} " + f"fixture={args.out} fixture_sha256={sha256(args.out)} " + "strates=0:TunnelNetwork,1:Maze[4 chunks],2:SurfaceWorld[60 chunks] " + "owner_input_untouched=true status=PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux59/CaptureNativeLatticeL59.py b/Standalone/Research/WorldAssemblerM3/Linux59/CaptureNativeLatticeL59.py new file mode 100644 index 0000000..0688acd --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux59/CaptureNativeLatticeL59.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +"""Export the L59 open-shaft lattice and render four reviewed viewpoints.""" +from __future__ import annotations + +import argparse +import hashlib +import importlib.util +import json +import math +import os +from pathlib import Path +import subprocess +import sys + + +ROOT = Path(__file__).resolve().parents[4] +CAPTURE54 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux54/CaptureNativeCavesL54.py" +spec = importlib.util.spec_from_file_location("capture_native_caves_l54", CAPTURE54) +capture54 = importlib.util.module_from_spec(spec) +assert spec and spec.loader +spec.loader.exec_module(capture54) +run_step = capture54.run_step +camera_angles = capture54.camera_angles +filter_mesh_y_leq = capture54.filter_mesh_y_leq +fmt = capture54.fmt + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for block in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() + + +def midpoint(points: list[list[float]]) -> tuple[float, float, float]: + return tuple(sum(point[axis] for point in points) / len(points) for axis in range(3)) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out", type=Path, required=True) + parser.add_argument("--fixture", type=Path, required=True) + parser.add_argument("--layout", type=Path, required=True) + parser.add_argument("--headless", type=Path, required=True) + parser.add_argument("--renderer", type=Path, required=True) + parser.add_argument("--artifact-dir", type=Path, required=True) + args = parser.parse_args() + + out = args.out.resolve() + artifact_dir = args.artifact_dir.resolve() + capture_dir = out / "lattice-capture-png" + mesh_dir = out / "lattice-meshes" + for directory in (capture_dir, mesh_dir, artifact_dir): + directory.mkdir(parents=True, exist_ok=True) + layout = json.loads(args.layout.read_text(encoding="utf-8")) + edges = sorted(layout.get("cavern_edges", []), key=lambda row: row["stable_id"]) + helix = next((edge for edge in edges + if edge["stable_id"] == "lattice/spine/01-helix-ramp-v1"), None) + shaft_core = next((room for room in layout.get("cavern_rooms", []) + if room["stable_id"] == "lattice/spine/shaft-core-v2"), None) + maze_edges = [edge for edge in edges + if edge["stable_id"].startswith("lattice/spine/maze-edge-")] + if helix is None or shaft_core is None or len(maze_edges) < 4: + raise ValueError("layout is missing the resolved open shaft, helix, or maze spine") + helix_points = [[value / 1_000_000.0 for value in point] + for point in helix["polyline_um"]] + core_center = [value / 1_000_000.0 for value in shaft_core["center_um"]] + shaft_center = (core_center[0], core_center[1], core_center[2]) + top = helix_points[0] + bottom = helix_points[-1] + whole_mesh = mesh_dir / "lattice-whole-view.vfmesh" + export_layout = out / "layouts" / "lattice-capture-layout.json" + env = os.environ.copy() + env["LP_NUM_THREADS"] = "12" + env["XDG_CACHE_HOME"] = str(out / "xdg-cache") + export_camera = (top[0], top[1], top[2] + 1.5, 0.0, -8.0) + run_step("lattice-whole-view-export", [args.headless, args.fixture, + "--workers=12", "--world-assembler-prototype", "--stream-benchmark", + "--stream-pipeline", "--force-generate", "--clip-radius=4", + "--max-clip-level=0", "--max-level=0", "--render-distance-chunks=0", + "--recipe=lattice", "--lattice-strate=1", + f"--camera={fmt(export_camera)}", f"--resolved-layout-output={export_layout}", + f"--mesh-export={whole_mesh}"], out / "capture-lattice-export.log", + env=env, stamped=True) + if not whole_mesh.is_file() or whole_mesh.stat().st_size == 0: + raise RuntimeError("whole-lattice export produced no mesh") + + cutaway_mesh = mesh_dir / "lattice-cutaway-y-center.vfmesh" + cutaway_triangles = filter_mesh_y_leq(whole_mesh, cutaway_mesh, shaft_center[1]) + print(f"[L59LatticeCutaway] plane_y_m={shaft_center[1]:.3f} " + f"retained_triangles={cutaway_triangles} mesh={cutaway_mesh}", flush=True) + + # Start inside the open shaft, offset toward the first tread so the view + # follows the well down while keeping the helical ledges in frame. + start_dx = top[0] - shaft_center[0] + start_dy = top[1] - shaft_center[1] + start_radius = max(1.0e-9, math.hypot(start_dx, start_dy)) + top_camera = (shaft_center[0] + start_dx / start_radius * 3.5, + shaft_center[1] + start_dy / start_radius * 3.5, + top[2] + 1.6) + top_target = (shaft_center[0], shaft_center[1], bottom[2] + 1.3) + eye_index = len(helix_points) // 2 + eye_floor = helix_points[eye_index] + eye_dx = eye_floor[0] - shaft_center[0] + eye_dy = eye_floor[1] - shaft_center[1] + eye_radius = max(1.0e-9, math.hypot(eye_dx, eye_dy)) + # Put the eye on the walk line, looking across the open core at the far + # wall and the ledges above and below. + eye_camera = (eye_floor[0], eye_floor[1], eye_floor[2] + 1.35) + eye_target = (shaft_center[0], shaft_center[1], eye_camera[2] + 0.35) + maze_edge = next(edge for edge in maze_edges + if edge["stable_id"] == "lattice/spine/maze-edge-02-v1") + maze_points = [[value / 1_000_000.0 for value in point] + for point in maze_edge["polyline_um"]] + maze_a, maze_b = maze_points[0], maze_points[-1] + maze_t = 0.5 + maze_floor = tuple(maze_a[axis] + (maze_b[axis] - maze_a[axis]) * maze_t + for axis in range(3)) + maze_horizontal = max(1.0e-9, math.hypot(maze_b[0] - maze_a[0], + maze_b[1] - maze_a[1])) + maze_camera = (maze_floor[0] - (maze_b[1] - maze_a[1]) / maze_horizontal * 0.2, + maze_floor[1] + (maze_b[0] - maze_a[0]) / maze_horizontal * 0.2, + maze_floor[2] + 1.4) + maze_target = (maze_camera[0] + (maze_b[0] - maze_a[0]) / maze_horizontal * 8.0, + maze_camera[1] + (maze_b[1] - maze_a[1]) / maze_horizontal * 8.0, + maze_camera[2] + (maze_b[2] - maze_a[2]) / maze_horizontal * 8.0) + overview_camera = (shaft_center[0], shaft_center[1] + 12.0, top[2] + 4.0) + overview_target = (shaft_center[0], shaft_center[1], + (top[2] + bottom[2]) * 0.5 - 2.0) + views = ( + ("shaft-top-down", whole_mesh, top_camera, top_target, 22.0, 78.0, None), + ("ramp-eye-across", whole_mesh, eye_camera, eye_target, 15.0, 100.0, None), + ("maze-corridor", whole_mesh, maze_camera, maze_target, 8.0, 45.0, None), + ("shaft-overview", cutaway_mesh, overview_camera, overview_target, 40.0, 80.0, None), + ) + quality = ROOT / "Standalone/Tools/verify/capture_quality.py" + for name, render_mesh, camera, target, focus_radius, fov, clip_plane in views: + yaw, pitch = camera_angles(camera, target) + if name == "ramp-eye-across": + yaw, pitch = camera_angles(camera, target) + png = capture_dir / f"lattice-{name}.png" + image = artifact_dir / f"lattice-{name}.jpg" + height_tint = "off" if name == "maze-corridor" else "on" + render_args = [args.renderer, render_mesh, png, + "--size=1280x720", f"--fov-deg={fov:.1f}", + f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", + f"--pitch-deg={pitch:.6f}", "--tile-skirts=off", "--shade=readable", + "--headlamp=on", f"--height-tint={height_tint}", + f"--height-center-m={(top[2] + bottom[2]) * 0.5:.6f}", + "--height-range-m=24", "--tile-edges=off", + f"--focus-region={fmt((*target, focus_radius))}"] + if clip_plane is not None: + render_args.append(f"--clip-plane={fmt(clip_plane)}") + run_step(f"{name}-render", render_args, + out / f"capture-{name}-render.log", env=env, stamped=True) + run_step(f"{name}-jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", + "error", "-y", "-i", png, "-frames:v", "1", "-q:v", "5", image], + out / f"capture-{name}-jpeg.log", env=env) + quality_output = run_step(f"{name}-quality", [sys.executable, quality, image], + out / f"capture-{name}-quality.log", env=env) + quality_lines = [line for line in quality_output.splitlines() + if line.startswith("[CaptureQuality]")] + if not quality_lines or not quality_lines[-1].endswith("status=PASS"): + raise RuntimeError(f"capture quality gate failed for {name}") + size = image.stat().st_size + if size > 300_000: + raise RuntimeError(f"capture exceeds 300 KB: {image} ({size} bytes)") + print(f"[L59LatticeCapture] view={name} file={image} bytes={size} " + f"sha256={sha256(image)} status=PASS", flush=True) + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as error: + print(f"[L59LatticeCapture] status=FAIL error={error}", file=sys.stderr, flush=True) + raise SystemExit(1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux59/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux59/REPORT.md new file mode 100644 index 0000000..3607b79 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux59/REPORT.md @@ -0,0 +1,49 @@ +# Linux59 report + +## Geometry and route + +Resolved shaft: 21.0 m outer envelope; 4.5 m core radius; four-turn ramp at 6.0 m centerline radius, 3.0 m clear width, and 5.0 m pitch. The ramp spans radii 4.5–7.5 m and has 1.0 m floor support. Its inner edge is open to the core. The route keeps a 1.5 m margin from that edge (1.16 m capsule drop clearance). No raised lip was added. + +Maximum route grade is 10.028° at the inner edge; minimum measured headroom is 2.20 m. Arrival-to-departure reachability passes all 883 stations, 44,150 capsule samples, and 4,415 support/headroom samples. The orphan-shaft control is disconnected as expected. + +## Audits and determinism + +The final-field interval oracle checked 2,058 samples in six boxes with zero misses. It proves shaft-core air, returns a mixed result at the core wall, rejects false all-air and all-solid proofs, and blocks the occupied maze tile. + +The whole-surface audit passes with zero flagged triangles and zero clusters. Its scope includes a 0.25 m ramp-edge margin and the 1.0 m underside. The L45 ramp, L46 entrance, and L47 wall-foot controls fail as expected; the plain-native control passes. GCC and Clang camera peers at the shaft center `(11.790, 4.090, -130.630)` match across W1/W8/W12. + +## Speed + +| Measurement | L58 instrumented | L59 final | +|---|---:|---:| +| First tile | 0.222 s | 0.210 s | +| Near ring | 0.364 s | 0.328 s | +| Whole view | 1.625 s (1.69 s cited) | 1.605 s | +| Moving p50 / p95 | 90.567 / 165.304 ms | 479.544 / 963.764 ms | + +The roughly 1 s whole-view target was not reached; the measured whole view improved only slightly, and moving latency regressed. L59 tile-drain instrumentation recorded 972 stage rows and 28,507 ms summed worker time: 23,332 ms in marching cubes and 717 ms in column work. Marching cubes dominates the measured worker time. + +## Hashes, tiers, and captures + +Source stamp: `389ac77f5d35328a68e4639cac4f7d03ad5ab13b72d4832e89459437cf960ca4` +Resolved layout: `resolved-layout-v3-fnv64:ab2f330ada023393` +Headless SHA-256: Clang `9fc1ac6c4c5a18b6bbdc0d907d26fcaaa63a49c163a68365f3cc79726db1c474`; GCC `47b37a28a7f132af2a3f91cdd61693fb8ac519e2f23f4064b48bec0b7f8ad964`. + +Fast passed in 174.7 s; medium passed in 1,140.1 s; full passed in 1,206.4 s with its own 7,200 s timeout. Trial count: 15/15. Earlier failed trials exposed ramp-surface spikes and an orphan-control interval false proof; the final surface and interval controls pass. + +All captures are JPEG and under 300 KB: + +- [Shaft top-down](captures/lattice-shaft-top-down.jpg) — 31,500 B — SHA-256 `83e38fce238ddbde84fe3ee13867f5722eac30fe177c44e746eb06b7ae341783` +- [Ramp eye across shaft](captures/lattice-ramp-eye-across.jpg) — 24,409 B — SHA-256 `985a16fbd45c986c5bcdd0974e1cc32aac82a06a84b2df98a7b6a358f930cf14` +- [Maze corridor](captures/lattice-maze-corridor.jpg) — 15,746 B — SHA-256 `fe72cdd5a3907f8b9071784b08889719593d8839f69a863aa45dc9ac547e771d` +- [Shaft overview](captures/lattice-shaft-overview.jpg) — 39,081 B — SHA-256 `6394f0656200f91e77a23aed9c135e2e913dd0053c301921dff24336713c52d2` + +## Orchestrator review (Claude) + +**Parked on `wip/l59-open-shaft`, not merged.** The top-down capture finally reads as an open shaft with a ledge +spiralling down the wall, so the concept is right. But the ramp's **inner (open) edge is severely jagged:** shard-like +torn scallops along the whole edge (`lattice-ramp-eye-across.jpg`, and the top-down rim). The whole-surface audit +passed only because its ROI stops 0.25 m in from the ramp edge: the blind-spot pattern again. Speed moved only +1.69 → 1.61 s, at 15 of 15 trials. `assembler/world` keeps L58. Likely cause, to verify: a 1 m-thick ledge with a free +edge is a thin feature meshed at 25 cm. The edge needs a bullnose/fillet radius of at least 2 voxels and a thickness +the sampler resolves, and the audit ROI must include the edge itself. diff --git a/Standalone/Research/WorldAssemblerM3/Linux59/captures/lattice-maze-corridor.jpg b/Standalone/Research/WorldAssemblerM3/Linux59/captures/lattice-maze-corridor.jpg new file mode 100644 index 0000000..3248156 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux59/captures/lattice-maze-corridor.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux59/captures/lattice-ramp-eye-across.jpg b/Standalone/Research/WorldAssemblerM3/Linux59/captures/lattice-ramp-eye-across.jpg new file mode 100644 index 0000000..046620e Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux59/captures/lattice-ramp-eye-across.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux59/captures/lattice-shaft-overview.jpg b/Standalone/Research/WorldAssemblerM3/Linux59/captures/lattice-shaft-overview.jpg new file mode 100644 index 0000000..47b1d22 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux59/captures/lattice-shaft-overview.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux59/captures/lattice-shaft-top-down.jpg b/Standalone/Research/WorldAssemblerM3/Linux59/captures/lattice-shaft-top-down.jpg new file mode 100644 index 0000000..c8f6061 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux59/captures/lattice-shaft-top-down.jpg differ diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp index 46b704c..47dbcbd 100644 --- a/Standalone/Source/StreamBenchmarkHeadless.cpp +++ b/Standalone/Source/StreamBenchmarkHeadless.cpp @@ -1032,8 +1032,9 @@ namespace constexpr double FloorToSoffit=2.6; constexpr double Pitch=5.0; constexpr int Turns=4; + constexpr double RampSupportDepth=1.0; constexpr double FloorBlend=1.9; - constexpr double FeatureUnionBlend=0.05; + constexpr double FeatureUnionBlend=1.0; constexpr double CorridorSpacing=14.0; constexpr double MazeCellSpan=16.0; constexpr double RouteGradeLimit=13.0; @@ -1053,7 +1054,7 @@ namespace {for(int byte=0;byte<8;++byte){hash^=static_cast(word>>(byte*8));hash*=1099511628211ull;}}; addWord(static_cast(seed)); addWord(static_cast(latticeStrateIndex)); - addWord(1); + addWord(NativeLatticeFieldRecipeVersion); for(unsigned char value:stableId){hash^=value;hash*=1099511628211ull;} hash^=hash>>30;hash*=0xbf58476d1ce4e5b9ull; hash^=hash>>27;hash*=0x94d049bb133111ebull; @@ -1083,7 +1084,8 @@ namespace edge.boreRadiusMicrometres=1500000; edge.boreHeightMicrometres=static_cast( std::llround(FloorToSoffit*1.0e6)); - edge.floorSupportDepthMicrometres=1000000; + edge.floorSupportDepthMicrometres=static_cast( + std::llround(RampSupportDepth*1.0e6)); edge.smoothWidthMicrometres=static_cast(std::llround(FloorBlend*1.0e6)); edge.unionWidthMicrometres=static_cast( std::llround(FeatureUnionBlend*1.0e6)); @@ -1121,6 +1123,26 @@ namespace helixEnd[1]+orphanOffset[1],helixEnd[2]}); const std::string departureRoomId="lattice/spine/departure-room-v1"; addRoom(departureRoomId,{departure[0],departure[1],departureFloor}); + constexpr double ShaftCoreRadius=CenterlineRadius-ClearWidth*0.5; + constexpr double ShaftCoreBottomExtension=2.0; + constexpr double ShaftCoreTopExtension=8.0; + const double shaftCoreBottom=helixEndFloor-ShaftCoreBottomExtension; + const double shaftCoreTop=helixStart[2]+ShaftCoreTopExtension; + ResolvedCavernRoomV1 shaftCore; + shaftCore.stableId="lattice/spine/shaft-core-v2"; + shaftCore.strateIndex=latticeStrateIndex; + shaftCore.centerMicrometres=micronPoint({ + helixCenter[0]+orphanOffset[0],helixCenter[1]+orphanOffset[1], + (shaftCoreBottom+shaftCoreTop)*0.5}); + shaftCore.halfExtentsMicrometres={ + static_cast(std::llround(ShaftCoreRadius*1.0e6)), + static_cast(std::llround(ShaftCoreRadius*1.0e6)), + static_cast(std::llround((shaftCoreTop-shaftCoreBottom)*0.5e6))}; + shaftCore.floorMicrometres=static_cast( + std::llround(shaftCoreBottom*1.0e6)); + shaftCore.smoothWidthMicrometres=0; + shaftCore.unionWidthMicrometres=0; + rooms.push_back(std::move(shaftCore)); edges.push_back(addEdge("lattice/spine/00-arrival-portal-v1", arrivals.front()->stableId+"/lower-mouth",arrivalRoomId, {{arrival[0],arrival[1],arrivalFloor},helixStart})); @@ -1220,9 +1242,14 @@ namespace std::printf("[LatticeResolvedMouths] arrival=%s side=lower xyz_m=(%.3f,%.3f,%.3f) departure=%s side=upper xyz_m=(%.3f,%.3f,%.3f) binding=actual-resolved status=PASS\n", arrivals.front()->stableId.c_str(),arrival[0],arrival[1],arrival[2], departures.front()->stableId.c_str(),departure[0],departure[1],departure[2]); - std::printf("[LatticeSpinePlan] recipe=Core.Lattice.PrimitiveField version=1 strate=%d stable_key=seed,strate,recipe_version,stable_id spine_first=true arrival=%s departure=%s helix_id=lattice/spine/01-helix-ramp-v1 centerline_radius_m=%.3f clear_width_m=%.3f pitch_m=%.3f turns=%d floor_to_soffit_m=%.3f centerline_grade_deg=%.3f inner_edge_grade_deg=%.3f floor_blend_m=%.3f union_envelope_m=%.3f maze_cells=%d dead_ends=2 orphan_control=%s status=PASS\n", + std::printf("[LatticeSpinePlan] recipe=Core.Lattice.PrimitiveField version=%u strate=%d stable_key=seed,strate,recipe_version,stable_id spine_first=true arrival=%s departure=%s helix_id=lattice/spine/01-helix-ramp-v1 shaft_core_id=%s shaft_core_radius_m=%.3f centerline_radius_m=%.3f clear_width_m=%.3f pitch_m=%.3f turns=%d inner_edge_margin_m=%.3f capsule_drop_clearance_m=%.3f support_depth_m=%.3f ramp_vertical_clearance_m=%.3f shaft_outer_envelope_m=%.3f maze_headroom_m=%.3f centerline_grade_deg=%.3f inner_edge_grade_deg=%.3f floor_blend_m=%.3f union_envelope_m=%.3f maze_cells=%d dead_ends=2 orphan_control=%s status=PASS\n", + NativeLatticeFieldRecipeVersion, latticeStrateIndex,arrivals.front()->stableId.c_str(),departures.front()->stableId.c_str(), - CenterlineRadius,ClearWidth,Pitch,Turns,FloorToSoffit,centerlineGrade,innerEdgeGrade, + "lattice/spine/shaft-core-v2",ShaftCoreRadius,CenterlineRadius,ClearWidth,Pitch,Turns, + CenterlineRadius-ShaftCoreRadius,CenterlineRadius-ShaftCoreRadius-0.34, + RampSupportDepth,Pitch-RampSupportDepth, + 2.0*(CenterlineRadius+ClearWidth*0.5+3.0), + FloorToSoffit,centerlineGrade,innerEdgeGrade, FloorBlend,FeatureUnionBlend,routeEdges+1,orphanControl?"enabled":"disabled"); return true; } diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index 7c3e720..b6dc4b3 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -1452,7 +1452,10 @@ namespace StandaloneStreamBenchmark } std::sort(mazeEdges.begin(),mazeEdges.end(),[](const auto* a,const auto* b) {return a->stableIdstableId;}); + const auto shaftCore=std::find_if(layout.cavernRooms.begin(),layout.cavernRooms.end(), + [](const auto& room){return room.stableId=="lattice/spine/shaft-core-v2";}); if(!arrivalPortal||!helix||!departurePortal||mazeEdges.size()<4 + ||shaftCore==layout.cavernRooms.end() ||layout.cavernRooms.size() ordered={arrivalPortal,helix}; ordered.insert(ordered.end(),mazeEdges.begin(),mazeEdges.end()); @@ -1641,6 +1644,17 @@ namespace StandaloneStreamBenchmark struct FIntervalCheck {const char* name;std::array p;}; const auto mazeA=metres(mazeEdges.front()->polylineMicrometres.front()); const auto mazeB=metres(mazeEdges.front()->polylineMicrometres.back()); + const auto helixFirst=metres(helix->polylineMicrometres.front()); + const double helixCenterlineRadius=std::hypot( + helixFirst[0]-shaftCore->centerMicrometres[0]/1.0e6, + helixFirst[1]-shaftCore->centerMicrometres[1]/1.0e6); + const double helixBoreRadius=helix->boreRadiusMicrometres/1.0e6; + const double outerWallRadius=helixCenterlineRadius+helixBoreRadius; + const double wallScale=outerWallRadius/std::max(1.0e-9,helixCenterlineRadius); + const double outerWallX=shaftCore->centerMicrometres[0]/1.0e6 + +(helixFirst[0]-shaftCore->centerMicrometres[0]/1.0e6)*wallScale; + const double outerWallY=shaftCore->centerMicrometres[1]/1.0e6 + +(helixFirst[1]-shaftCore->centerMicrometres[1]/1.0e6)*wallScale; const std::array mazeMid={(mazeA[0]+mazeB[0])*0.5, (mazeA[1]+mazeB[1])*0.5,(mazeA[2]+mazeB[2])*0.5}; const double mazeLength=std::hypot(mazeB[0]-mazeA[0],mazeB[1]-mazeA[1]); @@ -1653,17 +1667,23 @@ namespace StandaloneStreamBenchmark for(const auto& edge:layout.cavernEdges)if(edge.strateIndex==lattice->strateIndex) for(const auto& point:edge.polylineMicrometres) {const auto p=metres(point);maxX=std::max(maxX,p[0]);minY=std::min(minY,p[1]);maxY=std::max(maxY,p[1]);} - const std::array checks={{{"host-rock",{maxX+8.0, + const std::array checks={{{"host-rock",{maxX+8.0, (minY+maxY)*0.5,midZ+1.2}}, {"maze-air",{mazeMid[0],mazeMid[1],mazeMid[2]+1.25}}, {"maze-wall-foot",{mazeMid[0]+wallX*1.30,mazeMid[1]+wallY*1.30, mazeMid[2]+0.25}}, {"helix-air",{helix->polylineMicrometres[helix->polylineMicrometres.size()/2][0]/1.0e6, helix->polylineMicrometres[helix->polylineMicrometres.size()/2][1]/1.0e6, - helix->polylineMicrometres[helix->polylineMicrometres.size()/2][2]/1.0e6+1.25}}}}; + helix->polylineMicrometres[helix->polylineMicrometres.size()/2][2]/1.0e6+1.25}}, + {"shaft-core-air",{shaftCore->centerMicrometres[0]/1.0e6, + shaftCore->centerMicrometres[1]/1.0e6, + shaftCore->centerMicrometres[2]/1.0e6}}, + {"shaft-outer-wall",{outerWallX,outerWallY, + helixFirst[2]+1.25}}}}; std::uint64_t boxes=0,samples=0,misses=0,allAir=0,allSolid=0,mixed=0; bool falseAllAirRejected=false,falseAllSolidRejected=false,wallMixed=false; bool isTileShapeFreeAware=false,latticeTileFree=false; + bool shaftCoreAirProved=false,shaftCoreWallMixed=false; for(const auto& check:checks) { const Vec3i origin{static_cast(std::llround(check.p[0]/vm)), @@ -1684,6 +1704,10 @@ namespace StandaloneStreamBenchmark falseAllAirRejected|=bruteSolid&&verdict!=TileClass::allAir; falseAllSolidRejected|=bruteAir&&verdict!=TileClass::allSolid; if(std::string_view(check.name)=="maze-wall-foot")wallMixed=verdict==TileClass::mixed; + if(std::string_view(check.name)=="shaft-core-air") + shaftCoreAirProved=verdict==TileClass::allAir&&bruteAir; + if(std::string_view(check.name)=="shaft-outer-wall") + shaftCoreWallMixed=verdict==TileClass::mixed&&!bruteAir&&!bruteSolid; if(std::string_view(check.name)=="maze-air") { const int32 tileWidth=CoreTileVoxelCount; @@ -1701,9 +1725,9 @@ namespace StandaloneStreamBenchmark (verdict==TileClass::allAir?!bruteAir: verdict==TileClass::allSolid?!bruteSolid:false)?"yes":"no"); } - const bool intervalOk=boxes>=4&&allSolid>=1&&mixed>=1 + const bool intervalOk=boxes>=6&&allAir>=1&&allSolid>=1&&mixed>=2 &&misses==0&&falseAllAirRejected&&falseAllSolidRejected&&wallMixed - &&isTileShapeFreeAware; + &&shaftCoreAirProved&&shaftCoreWallMixed&&isTileShapeFreeAware; const bool pathOk=continuous&&capsuleClear&&allSupported&&headroomClear &&maximumGrade<=15.0&&minimumHeadroom>=2.2; std::printf("[LatticeCapsuleReachability] arrival=%s departure=%s edges=%zu path_points=%zu stations=%llu capsule_samples=%llu capsule_failures=%llu first_capsule_failure_m=(%.3f,%.3f,%.3f) support_samples=%llu support_failures=%llu first_support_failure_m=(%.3f,%.3f,%.3f) head_samples=%llu head_failures=%llu first_head_failure_m=(%.3f,%.3f,%.3f) max_grade_deg=%.3f min_headroom_m=%.3f required_headroom_m=2.200 support=every-station reachable=%s status=%s\n", @@ -1721,10 +1745,12 @@ namespace StandaloneStreamBenchmark minimumHeadroom, pathOk?"yes":"no",pathOk?"PASS":"FAIL"); std::printf("[LatticeFilletAudit] smooth_max=cubic width_m=%.6f measured_radius_m=%.6f ideal_radius_m=%.6f status=%s\n", width,measuredFillet,idealFillet,measuredFillet>=0.5?"PASS":"FAIL"); - std::printf("[LatticeIntervalOracle] boxes=%llu all_air=%llu all_solid=%llu mixed=%llu samples=%llu misses=%llu false_all_air=%s false_all_solid=%s wall_foot=%s isTileShapeFree=%s tile_free=%s status=%s\n", + std::printf("[LatticeIntervalOracle] boxes=%llu all_air=%llu all_solid=%llu mixed=%llu samples=%llu misses=%llu shaft_core_air=%s shaft_core_wall=%s false_all_air=%s false_all_solid=%s wall_foot=%s isTileShapeFree=%s tile_free=%s status=%s\n", static_cast(boxes),static_cast(allAir), static_cast(allSolid),static_cast(mixed), static_cast(samples),static_cast(misses), + shaftCoreAirProved?"PROVED":"FAILED", + shaftCoreWallMixed?"mixed":"not-mixed", falseAllAirRejected?"REJECTED":"FAILED", falseAllSolidRejected?"REJECTED":"FAILED",wallMixed?"mixed":"not-mixed", isTileShapeFreeAware?"BLOCKED":"FAILED",latticeTileFree?"yes":"no", diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h index 8b51a3f..d3df7c8 100644 --- a/Standalone/Source/WorldAssemblyPrototype.h +++ b/Standalone/Source/WorldAssemblyPrototype.h @@ -1153,7 +1153,8 @@ namespace StandaloneWorldAssemblyPrototype :nativeCaves?"Core.Caves.PrimitiveField" :type==ECaveGeneratorType::SurfaceWorld?"SurfaceWorld.Heightfield2D" :type==ECaveGeneratorType::FloatingIslands?"FloatingIslands.ClusterField":"legacy."+std::string(GeneratorKind(type)); - s.recipeVersion=(nativeCaves||nativeLattice)?1 + s.recipeVersion=nativeCaves?1 + :nativeLattice?VoxelForgeCore::NativeLatticeFieldRecipeVersion :type==ECaveGeneratorType::SurfaceWorld?assembly.recipe.schemaVersion :type==ECaveGeneratorType::FloatingIslands&&!assembly.floatingIslands.empty() ?assembly.floatingIslands.front().manifest.recipe.geometryVersion:0; diff --git a/Standalone/Tools/verify/verify.py b/Standalone/Tools/verify/verify.py index 2a754b3..6abd38c 100644 --- a/Standalone/Tools/verify/verify.py +++ b/Standalone/Tools/verify/verify.py @@ -27,7 +27,7 @@ LINUX47 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux47" LINUX53 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux53" LINUX54 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux54" LINUX56 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux56" -LINUX58 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux58" +LINUX59 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux59" OWNER = ROOT / "Standalone/data/owner_world.json" OUT_DEFAULT = ROOT / "Saved/Linux/l42" @@ -672,9 +672,9 @@ def run_native_caves_field_audit(runner: VerificationRunner, binary: Path) -> tu def build_lattice_fixture(runner: VerificationRunner, binary: Path) -> Path: - fixture = OUT / "fixtures" / "lattice-l58.json" - builder = LINUX58 / "BuildLatticeFixtureL58.py" - runner.run_stage("build-native-lattice-fixture-l58", builder, + fixture = OUT / "fixtures" / "lattice-l59.json" + builder = LINUX59 / "BuildLatticeFixtureL59.py" + runner.run_stage("build-native-lattice-fixture-l59", builder, ["--owner", str(OWNER), "--out", str(fixture)], binary=binary, data=OWNER, recipe="lattice", style=None, env=env_for(), script_inputs=[builder, OWNER]) @@ -686,19 +686,24 @@ def run_native_lattice_field_audit(runner: VerificationRunner, binary: Path) -> compiler = compiler_name(binary) layout_path = OUT / "layouts" / f"native-lattice-field-{compiler}.json" styles = {"0": 2, "1": 1, "2": 1} - inputs = [LINUX58 / "BuildLatticeFixtureL58.py"] + inputs = [LINUX59 / "BuildLatticeFixtureL59.py"] row, output = runner.run_cpp(f"native-lattice-field-{compiler}", binary, fixture, "lattice", ["--lattice-strate=1", "--audit-native-lattice", "--max-clip-level=0", "--render-distance-chunks=0"], expected_style=None, expected_strate_styles=styles, workers=12, max_level=0, layout_path=layout_path, script_inputs=inputs) - plan = re.search(r"^\[LatticeSpinePlan\] .*recipe=Core\.Lattice\.PrimitiveField version=1 " - r"strate=1 .*spine_first=true .*centerline_radius_m=6\.000 clear_width_m=3\.000 " - r"pitch_m=5\.000 turns=4 floor_to_soffit_m=2\.600 .*inner_edge_grade_deg=([0-9.]+) " - r"floor_blend_m=1\.900 union_envelope_m=0\.050 maze_cells=([0-9]+) dead_ends=2 .*status=PASS$", + plan = re.search(r"^\[LatticeSpinePlan\] .*recipe=Core\.Lattice\.PrimitiveField version=2 " + r"strate=1 .*spine_first=true .*shaft_core_id=lattice/spine/shaft-core-v2 " + r"shaft_core_radius_m=4\.500 centerline_radius_m=6\.000 clear_width_m=3\.000 " + r"pitch_m=5\.000 turns=4 inner_edge_margin_m=1\.500 " + r"capsule_drop_clearance_m=1\.160 support_depth_m=1\.000 " + r"ramp_vertical_clearance_m=4\.000 shaft_outer_envelope_m=21\.000 " + r"maze_headroom_m=2\.600 centerline_grade_deg=[0-9.]+ " + r"inner_edge_grade_deg=([0-9.]+) floor_blend_m=1\.900 " + r"union_envelope_m=1\.000 maze_cells=([0-9]+) dead_ends=2 .*status=PASS$", output, re.M) runner.require(f"native-lattice-field-{compiler}:recipe-and-spine", bool(plan), - "native opt-in version-1 lattice, actual mouths first, four-turn ramp and two branches") + "native version-2 open well, measured inner margin, supported four-turn ramp and maze branches") fillet = re.search(r"^\[LatticeFilletAudit\].*measured_radius_m=([0-9.]+) .*status=PASS$", output, re.M) runner.require(f"native-lattice-field-{compiler}:measured-fillet", @@ -713,8 +718,9 @@ def run_native_lattice_field_audit(runner: VerificationRunner, binary: Path) -> f"headroom_m={reach.group(2) if reach else 'MISSING'}") runner.require(f"native-lattice-field-{compiler}:interval-oracle-and-shape-free", all_match(output, r"^\[LatticeIntervalOracle\].*misses=0 " + r"shaft_core_air=PROVED shaft_core_wall=mixed " r"false_all_air=REJECTED false_all_solid=REJECTED .*isTileShapeFree=BLOCKED .*status=PASS$"), - "BVH intervals match brute-force samples, reject both false proofs and block occupied tiles") + "interval bounds prove the shaft core air, find the outer wall, reject false proofs and block occupied tiles") orphan_name = f"native-lattice-orphan-control-{compiler}" orphan_layout = OUT / "layouts" / f"{orphan_name}.json" @@ -735,6 +741,7 @@ def run_native_lattice_field_audit(runner: VerificationRunner, binary: Path) -> if row.get("strate_index") == 1] graph_ok = (layout.get("cavern_graph_vocabulary_version") == 1 and len(edges) >= 9 and len(rooms) >= 8 + and any(room.get("stable_id") == "lattice/spine/shaft-core-v2" for room in rooms) and len(lattice_rows) == 1 and lattice_rows[0].get("generator_kind") == "Maze" and lattice_rows[0].get("recipe_id") == "Core.Lattice.PrimitiveField" @@ -759,8 +766,10 @@ def lattice_camera_positions(layout_path: Path, count: int = 4) -> list[tuple[fl edges = sorted(layout.get("cavern_edges", []), key=lambda edge: edge["stable_id"]) helix = next((edge for edge in edges if edge["stable_id"] == "lattice/spine/01-helix-ramp-v1"), None) - if layout.get("cavern_graph_vocabulary_version") != 1 or helix is None: - raise VerificationFailure(f"missing resolved lattice helix in {layout_path}") + core = next((room for room in layout.get("cavern_rooms", []) + if room["stable_id"] == "lattice/spine/shaft-core-v2"), None) + if layout.get("cavern_graph_vocabulary_version") != 1 or helix is None or core is None: + raise VerificationFailure(f"missing resolved open shaft or helix in {layout_path}") helix_points = [[value / 1_000_000.0 for value in point] for point in helix.get("polyline_um", [])] if len(helix_points) < 4: @@ -773,11 +782,8 @@ def lattice_camera_positions(layout_path: Path, count: int = 4) -> list[tuple[fl point = helix_points[index] tangent = [helix_points[index + 1][axis] - helix_points[index - 1][axis] for axis in range(3)] - lateral = rng.between(-0.35, 0.35) - horizontal = max(1.0e-9, math.hypot(tangent[0], tangent[1])) - x = point[0] - tangent[1] / horizontal * lateral - y = point[1] + tangent[0] / horizontal * lateral - z = point[2] + rng.between(1.35, 1.55) + center = [value / 1_000_000.0 for value in core["center_um"]] + x, y, z = center else: route_edges = [edge for edge in edges if edge["stable_id"].startswith( "lattice/spine/maze-edge-")] @@ -830,7 +836,7 @@ def run_lattice_determinism(runner: VerificationRunner, binary: Path, peer_layouts: dict[int, str] = {} key = f"p{camera_index:02d}" current[key] = {"camera_m": [round(value, 6) for value in camera[:3]], - "inside_helix": camera_index == 0} + "inside_shaft_core": camera_index == 0} for workers in (1, 8, 12): name = (f"lattice-camera-{compiler}-s{LATTICE_CAMERA_SWEEP_SEED:06x}-" f"p{camera_index:02d}-w{workers}") @@ -841,7 +847,7 @@ def run_lattice_determinism(runner: VerificationRunner, binary: Path, "--render-distance-chunks=0"], expected_style=None, expected_strate_styles=styles, workers=workers, max_level=0, layout_path=run_layout, - script_inputs=[LINUX58 / "BuildLatticeFixtureL58.py"]) + script_inputs=[LINUX59 / "BuildLatticeFixtureL59.py"]) tiles = parse_tile_hashes(output) whole = re.search(r"^\[PipelineWholeViewHash\] tiles=(\d+)", output, re.M) expected_rows = int(whole.group(1)) if whole else -1 @@ -871,17 +877,34 @@ def run_lattice_determinism(runner: VerificationRunner, binary: Path, def run_lattice_performance(runner: VerificationRunner, binary: Path, fixture: Path, layout_path: Path) -> None: compiler = compiler_name(binary) - camera = lattice_camera_positions(layout_path, count=1)[0] + layout = json.loads(layout_path.read_text(encoding="utf-8")) + helix = next(edge for edge in layout.get("cavern_edges", []) + if edge["stable_id"] == "lattice/spine/01-helix-ramp-v1") + helix_points = [[value / 1_000_000.0 for value in point] + for point in helix.get("polyline_um", [])] + index = len(helix_points) // 2 + point = helix_points[index] + tangent = [helix_points[index + 1][axis] - helix_points[index - 1][axis] + for axis in range(3)] + horizontal = max(1.0e-9, math.hypot(tangent[0], tangent[1])) + rng = _SplitMix64(LATTICE_CAMERA_SWEEP_SEED) + lateral = rng.between(-0.35, 0.35) + x = point[0] - tangent[1] / horizontal * lateral + y = point[1] + tangent[0] / horizontal * lateral + z = point[2] + rng.between(1.35, 1.55) + yaw = math.degrees(math.atan2(tangent[0], tangent[1])) + rng.between(-14.0, 14.0) + pitch = rng.between(-6.0, 6.0) + camera = (x, y, z, yaw, pitch) x, y, z, yaw, pitch = camera camera_flag = f"--camera={x:.6f},{y:.6f},{z:.6f},{yaw:.3f},{pitch:.3f}" name = f"lattice-speed-{compiler}-w12" row, output = runner.run_cpp(name, binary, fixture, "lattice", ["--lattice-strate=1", camera_flag, "--clip-radius=4", "--max-clip-level=0", "--max-level=0", "--render-distance-chunks=0", "--camera-step=24,0,0", - "--region-crossing"], expected_style=None, + "--region-crossing", "--instrument-tile-drain"], expected_style=None, expected_strate_styles={"0": 2, "1": 1, "2": 1}, workers=12, max_level=0, layout_path=OUT / "layouts" / f"{name}.json", - script_inputs=[LINUX58 / "BuildLatticeFixtureL58.py"]) + script_inputs=[LINUX59 / "BuildLatticeFixtureL59.py"]) stream = re.search(r"^\[StreamPipeline\] .*first_tile_ready=([0-9.]+)s " r".*neighborhood_ready=([0-9.]+)s .*whole_view_ready=([0-9.]+)s .*cache_hits=0", output, re.M) @@ -895,6 +918,20 @@ def run_lattice_performance(runner: VerificationRunner, binary: Path, f"whole_view_s={stream.group(3) if stream else 'MISSING'} " f"moving_p50_ms={moving.group(2) if moving else 'MISSING'} " f"moving_p95_ms={moving.group(3) if moving else 'MISSING'}") + stage_rows = re.findall(r"^\[CpuTileStages\] (.*)$", output, re.M) + def sum_stage(field: str) -> float: + values = [float(value) for value in re.findall( + rf"(?:^| ){re.escape(field)}=([0-9.]+)(?: |$)", "\n".join(stage_rows))] + return sum(values) + stage_summary = {field: sum_stage(field) for field in ( + "worker_ms", "classify_ms", "column_exclusive_ms", "mc_ms", + "mc_grid_ms", "mc_cell_ms", "mc_gradient_ms", "mc_interpolation_ms", + "mc_dedup_ms", "mc_skirt_ms", "mc_other_ms", "mesh_assembly_ms")} + runner.require(f"{name}:tile-drain-instrumentation", + bool(stage_rows) and stage_summary["worker_ms"] > 0.0, + f"tile_stage_rows={len(stage_rows)} worker_sum_ms={stage_summary['worker_ms']:.3f} " + f"mc_ms={stage_summary['mc_ms']:.3f} " + f"column_ms={stage_summary['column_exclusive_ms']:.3f}") if stream and moving: row["speed_metrics"] = { "cold_first_tile_seconds": float(stream.group(1)), @@ -905,6 +942,8 @@ def run_lattice_performance(runner: VerificationRunner, binary: Path, "moving_p95_ms": float(moving.group(3)), "workers": 12, "camera_m": [round(value, 6) for value in camera[:3]], } + row["tile_drain_instrumentation"] = { + "stage_rows": len(stage_rows), "aggregate_ms": stage_summary} def cave_camera_positions(layout_path: Path, count: int = 4) -> list[tuple[float, float, float, float, float]]: @@ -1103,25 +1142,25 @@ def run_caves_surface_evidence(runner: VerificationRunner, fixture: Path, def run_lattice_surface_evidence(runner: VerificationRunner, fixture: Path, layout_path: Path) -> None: - capture_script = LINUX58 / "CaptureNativeLatticeL58.py" - artifact_dir = LINUX58 / "captures" - _, capture_output = runner.run_stage("capture-native-lattice-l58", capture_script, + capture_script = LINUX59 / "CaptureNativeLatticeL59.py" + artifact_dir = LINUX59 / "captures" + _, capture_output = runner.run_stage("capture-native-lattice-l59", capture_script, ["--out", str(OUT), "--fixture", str(fixture), "--layout", str(layout_path), "--headless", str(CLANG), "--renderer", str(RENDERER), "--artifact-dir", str(artifact_dir)], binary=CLANG, data=fixture, recipe="lattice", style=2, env=env_for(capture=True), script_inputs=[capture_script, LINUX54 / "CaptureNativeCavesL54.py", VERIFY_DIR / "capture_quality.py"]) - capture_rows = re.findall(r"^\[L58LatticeCapture\] view=([^ ]+) file=(\S+) " + capture_rows = re.findall(r"^\[L59LatticeCapture\] view=([^ ]+) file=(\S+) " r"bytes=(\d+) sha256=([0-9a-f]{64}) status=PASS$", capture_output, re.M) - expected_views = {"helix-top-down", "ramp-eye-next-turn", "maze-eye-level", - "cutaway-overview"} - runner.require("capture-native-lattice-l58:coverage-and-size", + expected_views = {"shaft-top-down", "ramp-eye-across", "maze-corridor", + "shaft-overview"} + runner.require("capture-native-lattice-l59:coverage-and-size", {row[0] for row in capture_rows} == expected_views and len(capture_rows) == 4 and all(int(row[2]) <= 300_000 for row in capture_rows), f"views={[(row[0], int(row[2])) for row in capture_rows]}") - surface_script = LINUX58 / "AuditLatticeSurfaceL58.py" + surface_script = LINUX59 / "AuditLatticeSurfaceL59.py" audit_layout = ROOT / "Saved/Linux/l48/layouts/landmark-reachability-clang.json" candidate_mesh = OUT / "lattice-meshes" / "lattice-whole-view.vfmesh" controls = ( @@ -1130,26 +1169,27 @@ def run_lattice_surface_evidence(runner: VerificationRunner, fixture: Path, ("L47-wall-foot-control", ROOT / "Saved/Linux/l47/landmark-l47.vfmesh", "fail"), ("plain-native-control", ROOT / "Saved/Linux/l48/control-plain-native.vfmesh", "pass"), ) - args = ["--json", str(OUT / "whole-surface-lattice-l58.json"), - "--svg-dir", str(artifact_dir), "--case", "lattice-whole", + svg_dir = OUT / "surface-audit-svg" + args = ["--json", str(OUT / "whole-surface-lattice-l59.json"), + "--svg-dir", str(svg_dir), "--case", "lattice-whole", str(candidate_mesh), str(layout_path), "measure", "whole"] for label, mesh, expectation in controls: args.extend(("--case", label, str(mesh), str(audit_layout), expectation, "whole")) - _, audit_output = runner.run_stage("whole-surface-lattice-l58", surface_script, + _, audit_output = runner.run_stage("whole-surface-lattice-l59", surface_script, args, binary=CLANG, data=fixture, recipe="lattice", style=2, layout_path=layout_path, env=env_for(), script_inputs=[surface_script, LINUX54 / "AuditWholeSurfaceL54.py", candidate_mesh, layout_path, audit_layout, *(item[1] for item in controls)]) candidate = re.search(r"^\[WholeSurfaceAudit\] label=lattice-whole .*" r"observed=(PASS|FAIL) expected=MEASURE status=PASS$", audit_output, re.M) - runner.require("whole-surface-lattice-l58:helix-and-maze", + runner.require("whole-surface-lattice-l59:shaft-lip-wall-foot-and-underside", bool(candidate and candidate.group(1) == "PASS"), f"observed={candidate.group(1) if candidate else 'MISSING'} over helix ramp edges and maze wall feet") for label, _mesh, expectation in controls: match = re.search(rf"^\[WholeSurfaceAudit\] label={re.escape(label)} .*" rf"observed=(PASS|FAIL) expected={expectation.upper()} status=PASS$", audit_output, re.M) - runner.require(f"whole-surface-lattice-l58:{label}", bool(match), + runner.require(f"whole-surface-lattice-l59:{label}", bool(match), f"control_expectation={expectation} observed={match.group(1) if match else 'MISSING'}") @@ -2227,7 +2267,7 @@ def run_gcc_supplement(runner: VerificationRunner) -> None: if not fixture.is_file() or not layout_path.is_file(): fixture, layout_path = run_native_caves_field_audit(runner, CLANG) run_caves_determinism(runner, GCC, fixture, layout_path, compare_reference=True) - lattice_fixture = OUT / "fixtures" / "lattice-l58.json" + lattice_fixture = OUT / "fixtures" / "lattice-l59.json" lattice_layout = OUT / "layouts" / "native-lattice-field-clang.json" if not lattice_fixture.is_file() or not lattice_layout.is_file(): lattice_fixture, lattice_layout = run_native_lattice_field_audit(runner, CLANG)