From 54ba1b49abcbfebe00defcd599c4df14179d2cdd Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Sun, 25 Oct 2026 20:27:04 +0100 Subject: [PATCH] wip(l59): open helix shaft (ledge open to the core) -- PARKED: jagged inner ramp edge, audit ROI missed it Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1 --- ARCHITECTURE.md | 46 +- CODEMAP.md | 21 +- .../include/VoxelForgeCore/VoxelForgeCore.h | 4 +- Standalone/Core/src/ResolvedLayoutInternal.h | 6 +- Standalone/Core/src/ShapeWorldInternal.h | 502 ++++++++++++++---- .../Linux59/AuditLatticeSurfaceL59.py | 259 +++++++++ .../Linux59/BuildLatticeFixtureL59.py | 83 +++ .../Linux59/CaptureNativeLatticeL59.py | 179 +++++++ .../WorldAssemblerM3/Linux59/REPORT.md | 49 ++ .../captures/lattice-maze-corridor.jpg | Bin 0 -> 15746 bytes .../captures/lattice-ramp-eye-across.jpg | Bin 0 -> 24409 bytes .../captures/lattice-shaft-overview.jpg | Bin 0 -> 39081 bytes .../captures/lattice-shaft-top-down.jpg | Bin 0 -> 31500 bytes Standalone/Source/StreamBenchmarkHeadless.cpp | 37 +- Standalone/Source/StreamBenchmarkPipeline.h | 36 +- Standalone/Source/WorldAssemblyPrototype.h | 3 +- Standalone/Tools/verify/verify.py | 114 ++-- 17 files changed, 1167 insertions(+), 172 deletions(-) create mode 100644 Standalone/Research/WorldAssemblerM3/Linux59/AuditLatticeSurfaceL59.py create mode 100644 Standalone/Research/WorldAssemblerM3/Linux59/BuildLatticeFixtureL59.py create mode 100644 Standalone/Research/WorldAssemblerM3/Linux59/CaptureNativeLatticeL59.py create mode 100644 Standalone/Research/WorldAssemblerM3/Linux59/REPORT.md create mode 100644 Standalone/Research/WorldAssemblerM3/Linux59/captures/lattice-maze-corridor.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux59/captures/lattice-ramp-eye-across.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux59/captures/lattice-shaft-overview.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux59/captures/lattice-shaft-top-down.jpg 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 0000000000000000000000000000000000000000..32481566bade2a54255a013e3234973ac3fdcc47 GIT binary patch literal 15746 zcmd5@30xFMy04ytAq0d`h>8NU_%=>Z5)tGQWmp;ABt&N-YDD9Ocm;_Eq995?bPbG% z@fStoa!erLh#El=jjT!l(d8DSqM)o0L(q5|Mbz-Vs_LE@j`!Yf#OzjeS66*?bk)E9 z^?lVf&{5a%E3yih8xid8^0tfD+b$zqP)9u)hfK}PUN?K)f;cNnD}(FXx37(j4TM%! zefwHl+t~E)-ya0<7&vgCz5U?9Lx%j}wnk)d-!@NC*VO;7O$Bd8k}r?j{&nBQJiBEq^a=zne}|{+?q_jjUJ!nGj5dYmTMQ z^&jq_2cc0592NG{-Yq`MRJ^JI_IkT-c(6lGdas|;XHB!$m~hmR=+Z}Po{!4^ByiK} zYT|U+u|pJ202I=oEy$4_q9AYqU<5ymq26Qsc-+cZM;5?z;sG=(Cg^Q&mJhQKn7g4e z)~m|pZPs%5b(brTSf5E#Kk{dN4pj9+=Dj|`-feKWk{xnBF=c7vJ+$J?YvsAO&;pl) z2zR6rXk>9UW*S7Xq9IPn5i$etGzbX*lkT2_Oh<4;7+rG`r`YLC%M}?Gf?-Y^@f;6n zHzc%{JRXgFCMPsKRtbylCZ&1aLCbn~l)cuPBw$(cDP-Q9a3lA2TwEQh@+=*DNnvBH zuu=yskzocxUCcjY z#sSN4j?r?}l(8-@{f^`)*Q_jB{(MZHPV@BO_^2tT>~B~_7D7Y+8O}as%;?6^UJAkD z)@x7ieU{^=R!Fs&20VX&QIu0yfLUaq7vU7;80MM5Ln+Fyt_-t6NKY|qhQ#&hB$kjZ z@^I~m`$wA|#SXhMc2m^ywE0i79Z#;E9_(59UfjrkKW3J*Qn&gPoJbdXM@>5!v0|%7 zt_lzXPjTR|qVRE8Q3Me!03#r2jA{=avMG!lg!}?k=*!ZT$uC(RW`M4G_z?Nt&lHl1 z>WRyqN1jY52?zw_-p|iUzCI>7(|<6u_-}K}&Dz>iYn_ubDwqS#PEQWim-(dTiSbA& zI7IPKf^jh6L|3knQbf7w2ZGtddC+qdQOM*lRyd$)#$J_Z2a&Gqm0%2zG;D86io$64V=!Cp6{4$s$(8RJ=O z+fv~-ZE&!uF5E~-amx?uGrC+2tC$#1LsHLE{UYpf=^9u@Oeij+*Kj@aPR~K2adCag zXS&*B&51MCooXC_+|ALdjB# zw+A$pc|7=e>n9guV}TI{&oN8DiOdL9I9o~+MmsjdI%yrV1rX@EnAmhI%odoj8ZF

s*@kb4O^ZNY$H`$AUt#qP&!l| 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..046620e8e8a641a5e6470bf0efa48958f09b078a GIT binary patch literal 24409 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..47b1d222f42fe751bfcec4141c71760b30f506db GIT binary patch literal 39081 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 HzV-YcE!WW& literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c8f60619cf5f9509bcb8649fac4563c8eb594fc6 GIT binary patch literal 31500 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 zeF>zt_;I-yCh(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)