diff --git a/Standalone/Research/WorldAssemblerM3/Linux147/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux147/REPORT.md new file mode 100644 index 0000000..5902769 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux147/REPORT.md @@ -0,0 +1,52 @@ +# Linux147 — passage audits on the Core cave planner + +**Result:** The L145 ripple-distance audit and L146 passage-interior air gate pass across all 16 seeds in both requested matrices. The two matrices each measured 5,171,796 capsule samples inside passage interiors with **zero solid samples**. Their no-passage controls detected solid in 48/48 passage interiors, and their masked-gap ripple controls passed 48/48 times. No generation code changed. + +The only non-green result is seed 2's known interface 2–3 shell/seam failure. Vertical Shafts remains 15/16, matching L139. The Tunnel Network comparison is 15/16 against L135's earlier 16/16; seed 2 is already known to fail after the L139 shaft change. It is not a failure introduced by either new audit gate. + +## Audit changes + +- `StreamBenchmarkPipeline.h` now requires zero solid capsule samples inside each passage interior. It runs the same interior classifier over the passage-stripped field and requires positive solid detections for every passage. +- The ripple fitter keeps each valid wall sample's actual route distance instead of compressing distances across missing probes. Its synthetic 5 m masked-gap control measures a 0.0581 m sine ripple and rejects a smooth cubic at 0.0000 m. +- No Core planner, evaluator, Unreal `Source/`, or generation code changed. `git diff --check` passed. + +## Per-seed verdicts + +Tunnel Network compares with the L135 16/16 composed sweep; Vertical Shafts compares with the L139 15/16 sweep. `P` means pass and `F` means fail. + +| Seed | Tunnel Network baseline → L147 | Vertical Shafts baseline → L147 | Verdict change | +|---:|:---:|:---:|---| +| 0 | P → P | P → P | None | +| 1 | P → P | P → P | None | +| 2 | P → F | F → F | Tunnel comparison: L135 predates the L139 shaft change. Current seed 2 has the existing interface 2–3 shell/seam failure; the new interior and ripple gates pass. Vertical Shafts remains the same known failure. | +| 3 | P → P | P → P | None | +| 4 | P → P | P → P | None | +| 5 | P → P | P → P | None | +| 6 | P → P | P → P | None | +| 7 | P → P | P → P | None | +| 8 | P → P | P → P | None | +| 9 | P → P | P → P | None | +| 10 | P → P | P → P | None | +| 11 | P → P | P → P | None | +| 12 | P → P | P → P | None | +| 13 | P → P | P → P | None | +| 14 | P → P | P → P | None | +| 15 | P → P | P → P | None | + +There were no newly passing seeds and no verdict changes caused by the added gates. For seed 2, all three passages reported `interior_air=PASS`; its interface 2–3 shell had eight `wall_missing` samples at route stations 489.627–490.321 m. The samples were outside the passage interior. The first was at `(-21.678, -1.714, -888.614) m`, with composed and native density `+2.341949`, expected wall classification, and wall-shell margin `−0.297164 m`. The passage and seam summaries fail as they did in L139. No interior-solid root cause trace was needed because the measured interior-solid count is zero. + +## Measurements and checks + +- Clang 19 standalone build (`-O2`, `-ffp-contract=off`, four jobs) passed. L147 source stamp: `c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f`. +- Tunnel Network: seeds 0–15, one composed run per seed, 430.715 s summed process time (13.542–51.457 s per seed); 15/16 passed. Logs and resolved layouts are under `Saved/Linux/l147/seeds/tunnel-network/`. +- Vertical Shafts: seeds 0–15, one composed run per seed, 392.185 s summed process time (13.069–39.769 s per seed); 15/16 passed. Logs and resolved layouts are under `Saved/Linux/l147/seeds/vertical-shafts/`. +- Across each matrix: 5,171,796 passage-interior capsule samples, 0 solid; 48/48 no-passage controls detected interior solid; 48/48 masked-gap ripple controls passed. Passage walkability, ripple, and all other existing gates passed for seeds other than the known seed-2 shell/seam failure. +- Fast tier passed in 357.3 s: `Saved/Linux/l147/fast/fast-summary.json`. The first invocation stopped before running checks because the verifier's expected binary path was empty; it passed after using the completed L147 build there. +- Owner identity comparison against the HEAD-stamped L140 binary passed: all pipeline hash lines, plan hash `538ee11456eb0dab`, resolved-layout identity `resolved-layout-v3-fnv64:d310515572977ebf`, and resolved-layout SHA-256 `50a0342902b6b897c5746fd86fac559244151894155cab6db0822229bfb877e4` matched. Details: `Saved/Linux/l147/identity/summary.json`. +- `git diff --check` passed. No L80 timing, Unreal build, Windows build, or cross-compiler determinism run was requested or performed. + +Runners: [`RunL147SeedSweep.py`](RunL147SeedSweep.py) and [`RunL147OwnerIdentity.py`](RunL147OwnerIdentity.py). No Unreal `Source/` files changed. + +## Remaining risk + +The seed-2 interface 2–3 shell/seam failure remains unresolved. It does not put solid samples inside the passage interior and is unchanged from the known L139 Vertical Shafts result. This round made no generation fix. diff --git a/Standalone/Research/WorldAssemblerM3/Linux147/RunL147OwnerIdentity.py b/Standalone/Research/WorldAssemblerM3/Linux147/RunL147OwnerIdentity.py new file mode 100644 index 0000000..0333fbc --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux147/RunL147OwnerIdentity.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +"""Compare owner and resolved-planner hashes against the exact L147 parent build.""" +from __future__ import annotations + +import hashlib +import json +import re +import subprocess +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l147/identity" +DATA = ROOT / "Standalone/data/owner_world.json" +HEAD_BIN = ROOT / "Saved/Linux/l140/bin/VoxelForgeCoreHeadless-l140-clang" +L147_BIN = ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l147-clang" +HEAD_STAMP = "6205e6c180ef32c41792dc3e0d8bf443a96df41974f40334af4bf3a157ab88da" +L147_STAMP = "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f" +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) +PIPELINE_HASH_RE = re.compile( + r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M) +PLAN_RE = re.compile(r"^\[WorldAssemblerPlanSerialization\].*plan_hash=([0-9a-f]+).*$", re.M) + + +def run(label: str, binary: Path, expected_stamp: str) -> dict[str, object]: + if not binary.is_file(): + raise SystemExit(f"missing {label} binary: {binary}") + log = OUT / f"{label}.log" + layout = OUT / f"{label}-layout.json" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=8", "--clip-radius=3", + "--max-clip-level=8", "--max-level=0", "--force-generate", "--seed=0", + "--recipe=surface-world", "--stream-hash", + f"--resolved-layout-output={layout}"] + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + stamp = STAMP_RE.search(content) + pipeline_hashes = PIPELINE_HASH_RE.findall(content) + plan = PLAN_RE.search(content) + layout_bytes = layout.read_bytes() if layout.is_file() else b"" + result = { + "label": label, + "exit": proc.returncode, + "stamp": stamp.group(1) if stamp else None, + "stamp_matches_expected": stamp is not None and stamp.group(1) == expected_stamp, + "pipeline_hashes": pipeline_hashes, + "plan_hash": plan.group(1) if plan else None, + "resolved_layout_sha256": hashlib.sha256(layout_bytes).hexdigest() if layout_bytes else None, + "resolved_layout_identity": None, + "log": str(log.relative_to(ROOT)), + "layout": str(layout.relative_to(ROOT)), + } + if layout_bytes: + result["resolved_layout_identity"] = json.loads(layout_bytes).get("stable_identity") + return result + + +def main() -> int: + OUT.mkdir(parents=True, exist_ok=True) + head = run("head", HEAD_BIN, HEAD_STAMP) + l147 = run("l147", L147_BIN, L147_STAMP) + passed = (head["exit"] == 0 and l147["exit"] == 0 + and head["stamp_matches_expected"] and l147["stamp_matches_expected"] + and bool(head["pipeline_hashes"]) + and head["pipeline_hashes"] == l147["pipeline_hashes"] + and head["plan_hash"] is not None and head["plan_hash"] == l147["plan_hash"] + and head["resolved_layout_sha256"] is not None + and head["resolved_layout_sha256"] == l147["resolved_layout_sha256"] + and head["resolved_layout_identity"] == l147["resolved_layout_identity"]) + summary = {"status": "PASS" if passed else "FAIL", "head": head, "l147": l147, + "owner_pipeline_hashes_identical": head["pipeline_hashes"] == l147["pipeline_hashes"], + "plan_hash_identical": head["plan_hash"] == l147["plan_hash"], + "resolved_layout_identical": + head["resolved_layout_sha256"] == l147["resolved_layout_sha256"]} + path = OUT / "summary.json" + path.write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8") + print(f"[L147OwnerIdentity] status={summary['status']} " + f"owner_hashes={summary['owner_pipeline_hashes_identical']} " + f"planner_hash={summary['plan_hash_identical']} " + f"resolved_layout={summary['resolved_layout_identical']} " + f"head_stamp={head['stamp']} l147_stamp={l147['stamp']} " + f"summary={path.relative_to(ROOT)}") + return 0 if passed else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux147/RunL147SeedSweep.py b/Standalone/Research/WorldAssemblerM3/Linux147/RunL147SeedSweep.py new file mode 100644 index 0000000..b2b539d --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux147/RunL147SeedSweep.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +"""Run the L147 composed passage audit for all 16 owner seeds.""" +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l147/seeds" +BIN = ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-l147-clang" +DATA = ROOT / "Standalone/data/owner_world.json" +STAMP = "c6b8a100355e271ddc40eb5973569d2ff4e2ba8d9c628c72f8e0af2493f0609f" +STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]+)$", re.M) +BASELINES = { + "tunnel-network": ROOT / "Saved/Linux/l135/optimized-seed-sweep.json", + "vertical-shafts": ROOT / "Saved/Linux/l139/seeds/summary.json", +} +DISPATCH = { + "tunnel-network": re.compile( + r"^\[DensityDispatch\] strate=2 generator=TunnelNetwork " + r"recipe=Core\.Caves\.PrimitiveField version=1 " + r"bound_path=Caves-native adapter=FCavesFieldAdapter$", re.M), + "vertical-shafts": re.compile( + r"^\[DensityDispatch\] strate=3 generator=VerticalShafts " + r"recipe=Core\.Caves\.PrimitiveField version=1 " + r"bound_path=Caves-native adapter=FCavesFieldAdapter$", re.M), +} +ROUTES = { + "tunnel-network": re.compile( + r"^\[ResolvedStrateRouteAudit\] strate=2 .*unsupported=0 " + r"grade_failures=0 .*status=PASS$", re.M), + "vertical-shafts": re.compile( + r"^\[ResolvedStrateRouteAudit\] strate=3 .*unsupported=0 " + r"grade_failures=0 .*status=PASS$", re.M), +} +CONTROLS = { + "tunnel-network": re.compile( + r"^\[TunnelNetworkBlockedRouteControl\].*expected=FAIL status=PASS$", re.M), + "vertical-shafts": re.compile( + r"^\[VerticalShaftsBlockedRouteControl\].*expected=FAIL status=PASS$", re.M), +} +REQUIRED = { + "passages": re.compile(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", re.M), + "seams": re.compile(r"^\[PassageTileSeamAudit\].*status=PASS$", re.M), + "shell": re.compile(r"^\[PassageShellAuditSummary\].*status=PASS$", re.M), + "naturalness": re.compile(r"^\[PassageNaturalnessSummary\].*status=PASS$", re.M), + "mouth_profile": re.compile(r"^\[PassageMouthProfileSummary\].*status=PASS$", re.M), + "ripple": re.compile(r"^\[PassageRippleAuditSummary\].*status=PASS$", re.M), + "passage_interior": re.compile( + r"^\[PassageInteriorCapsuleAuditSummary\].*status=PASS$", re.M), +} + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--kind", choices=tuple(BASELINES), required=True) + args = parser.parse_args() + if not BIN.is_file() or not DATA.is_file(): + raise SystemExit("missing L147 Clang binary or composed owner fixture") + baseline_path = BASELINES[args.kind] + baseline = json.loads(baseline_path.read_text(encoding="utf-8")) + baseline_results = {int(row["seed"]): row for row in baseline["results"]} + OUT.mkdir(parents=True, exist_ok=True) + mode_out = OUT / args.kind + mode_out.mkdir(parents=True, exist_ok=True) + results = [] + partial = mode_out / "runs-partial.json" + for seed in range(16): + log = mode_out / f"seed{seed}.log" + layout = mode_out / f"seed{seed}.json" + command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BIN), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=8", "--clip-radius=3", + "--max-clip-level=8", "--max-level=0", "--force-generate", f"--seed={seed}", + "--recipe=surface-world", "--density-dispatch-report", + "--audit-resolved-layout-passages", "--audit-passage-tile-seams", + f"--resolved-layout-output={layout}"] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + proc = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + stamp = STAMP_RE.search(content) + checks = {name: bool(pattern.search(content)) for name, pattern in REQUIRED.items()} + checks["source_stamp"] = stamp is not None and stamp.group(1) == STAMP + checks["dispatch"] = bool(DISPATCH[args.kind].search(content)) + checks["route"] = bool(ROUTES[args.kind].search(content)) + checks["blocked_route_control"] = bool(CONTROLS[args.kind].search(content)) + checks["three_passage_interiors"] = len(re.findall( + r"^\[ResolvedPassageAudit\].*interior_air=PASS", content, re.M)) == 3 + checks["three_interior_negative_controls"] = len(re.findall( + r"^\[ResolvedPassageInteriorNegativeControl\].*detected=true status=PASS$", + content, re.M)) == 3 + checks["three_ripple_gap_controls"] = len(re.findall( + r"^\[PassageRippleSamplingControl\].*status=PASS$", content, re.M)) == 3 + checks["three_blocked_passage_controls"] = len(re.findall( + r"^\[ResolvedPassageBlockedControl\].*detected=true$", content, re.M)) == 3 + checks["passage_count"] = len(re.findall( + r"^\[ResolvedPassageAudit\].*walkability=PASS$", content, re.M)) == 3 + checks["layout_output"] = layout.is_file() + passed = proc.returncode == 0 and all(checks.values()) + prior = baseline_results.get(seed, {}) + prior_passed = bool(prior.get("passed", False)) + change = ("newly failing" if prior_passed and not passed else + "newly passing" if not prior_passed and passed else + "still passing" if prior_passed else "still failing") + failed_checks = [name for name, ok in checks.items() if not ok] + reason = (", ".join(failed_checks) if failed_checks else + "all previous and new gates pass" if passed else + f"process exit {proc.returncode}") + row = {"kind": args.kind, "seed": seed, "prior_verdict": "PASS" if prior_passed else "FAIL", + "verdict": "PASS" if passed else "FAIL", "change": change, + "reason": reason, "exit": proc.returncode, "checks": checks, + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT)), "layout": str(layout.relative_to(ROOT))} + results.append(row) + print("[L147SeedSweep] " + json.dumps(row, sort_keys=True), flush=True) + partial.write_text(json.dumps(results, indent=2) + "\n", encoding="utf-8") + passed_count = sum(row["verdict"] == "PASS" for row in results) + summary = {"kind": args.kind, "source_stamp": STAMP, + "baseline": str(baseline_path.relative_to(ROOT)), "seeds": list(range(16)), + "passed": passed_count, "failed": 16 - passed_count, "results": results} + summary_path = mode_out / "summary.json" + summary_path.write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8") + print(f"[L147SeedSweepSummary] kind={args.kind} passed={passed_count}/16 " + f"failed={16-passed_count} status={'PASS' if passed_count == 16 else 'FAIL'}", + flush=True) + return 0 if passed_count == 16 else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index d2d867e..48b5a30 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -3036,6 +3036,7 @@ namespace StandaloneStreamBenchmark struct FWalkAudit { std::uint64_t segments=0,centers=0,capsuleSamples=0,blocked=0; + std::uint64_t passageInteriorCapsuleSamples=0,passageInteriorSolidSamples=0; std::uint64_t supportSamples=0,unsupported=0,gradeFailures=0; std::uint64_t supportOverlapSkips=0; std::array,8> firstBlocked{}; @@ -3049,7 +3050,8 @@ namespace StandaloneStreamBenchmark double supportStartMetres,double supportStepMetres, double supportMaximumMetres,std::uint32_t minimumSupportProbes, double supportProbeRadiusMetres, - const std::function& supportExemption={}) + const std::function& supportExemption={}, + const std::function& passageInteriorPredicate={}) { FWalkAudit audit; if(path.size()<2)return audit; @@ -3062,10 +3064,14 @@ namespace StandaloneStreamBenchmark const auto solidAt=[&](double xMetres,double yMetres,double zMetres) { ++audit.capsuleSamples; + const bool inPassageInterior=passageInteriorPredicate + &&passageInteriorPredicate(xMetres,yMetres,zMetres); + if(inPassageInterior)++audit.passageInteriorCapsuleSamples; const VoxelForgeCore::Vec3d position{xMetres/coreMetres,yMetres/coreMetres,zMetres/coreMetres}; const float density=Field.densityAt(position); if(density<0.0f) { + if(inPassageInterior)++audit.passageInteriorSolidSamples; if(audit.firstBlockedCount::max(), - std::size_t ignoredSegment=std::numeric_limits::max()) + std::size_t ignoredSegment=std::numeric_limits::max(), + std::size_t selectedPassage=std::numeric_limits::max()) { constexpr double CornerBlendMetres=0.50; constexpr double WallReliefMetres=0.21; @@ -3616,6 +3623,8 @@ namespace StandaloneStreamBenchmark for(std::size_t passageIndex=0; passageIndexpassages.size();++passageIndex) { + if(selectedPassage!=std::numeric_limits::max() + &&passageIndex!=selectedPassage)continue; const auto& passage=ResolvedLayout->passages[passageIndex]; if(passage.controlPoints.size()<2)continue; const auto& route=allPassageSmoothRoutes[passageIndex]; @@ -4183,8 +4192,9 @@ namespace StandaloneStreamBenchmark } return best; }; - const auto appendDetrendedProfile=[&](const std::vector& values, - double startDistance,std::vector>& output) + const auto appendDetrendedProfile=[&]( + const std::vector>& values, + std::vector>& output) { if(values.size()<64)return; // Remove a smooth mouth taper across each 32 m window while @@ -4193,11 +4203,12 @@ namespace StandaloneStreamBenchmark // 0.5–8 m periods that this audit is meant to detect. constexpr std::size_t Degree=3; std::array,Degree+1> normal{}; - const double meanIndex=0.5*static_cast(values.size()-1); - const double halfSpan=std::max(1.0,meanIndex); - for(std::size_t i=0;i(i)-meanIndex)/halfSpan; + const double x=(value[0]-meanDistance)/halfSpan; std::array powers{}; powers[0]=1.0; for(std::size_t power=1;power(i)-meanIndex)/halfSpan; + const double x=(value[0]-meanDistance)/halfSpan; double trend=normal[Degree][Degree+1]; for(std::size_t coefficient=Degree;coefficient-->0;) trend=trend*x+normal[coefficient][Degree+1]; - output.push_back({startDistance+static_cast(i)*SampleSpacingMetres, - values[i]-trend}); + output.push_back({value[0],value[1]-trend}); } }; const auto appendDetrendedPairs=[]( @@ -4258,7 +4268,7 @@ namespace StandaloneStreamBenchmark output.push_back({value[0],value[1]-meanY -slope*(value[0]-meanX)}); }; - std::vector beadControl; + std::vector> beadControl; beadControl.reserve(static_cast(WindowLengthMetres/SampleSpacingMetres)+1); constexpr double BeadSpacingMetres=1.5; constexpr double BeadSphereRadiusMetres=2.0; @@ -4266,14 +4276,50 @@ namespace StandaloneStreamBenchmark { const double x=static_cast(i)*SampleSpacingMetres; const double nearest=std::abs(std::remainder(x,BeadSpacingMetres)); - beadControl.push_back(std::sqrt(std::max(0.0, - BeadSphereRadiusMetres*BeadSphereRadiusMetres-nearest*nearest))); + beadControl.push_back({x,std::sqrt(std::max(0.0, + BeadSphereRadiusMetres*BeadSphereRadiusMetres-nearest*nearest))}); } std::vector> beadSamples; - appendDetrendedProfile(beadControl,0.0,beadSamples); + appendDetrendedProfile(beadControl,beadSamples); double ignoredPeriod=0.0; audit.controlAmplitudeMetres=sinusoidAmplitude(beadSamples,ignoredPeriod); audit.controlDetected=audit.controlAmplitudeMetres>=RippleAmplitudeLimitMetres; + std::vector> maskedSineControl,maskedSmoothControl; + constexpr double SamplingControlGapStartMetres=10.0; + constexpr double SamplingControlGapEndMetres=15.0; + for(std::size_t sample=0; + sample<=static_cast(WindowLengthMetres/SampleSpacingMetres); + ++sample) + { + const double distance=static_cast(sample)*SampleSpacingMetres; + if(distance>=SamplingControlGapStartMetres + &&distance> maskedSineResiduals,maskedSmoothResiduals; + appendDetrendedProfile(maskedSineControl,maskedSineResiduals); + appendDetrendedProfile(maskedSmoothControl,maskedSmoothResiduals); + double maskedSinePeriod=0.0,maskedSmoothPeriod=0.0; + const double maskedSineAmplitude=sinusoidAmplitude( + maskedSineResiduals,maskedSinePeriod); + const double maskedSmoothAmplitude=sinusoidAmplitude( + maskedSmoothResiduals,maskedSmoothPeriod); + const bool maskedSamplingControlPassed= + maskedSineAmplitude>=RippleAmplitudeLimitMetres + &&maskedSmoothAmplitude((SamplingControlGapEndMetres + -SamplingControlGapStartMetres)/SampleSpacingMetres), + maskedSineAmplitude,maskedSineAmplitude>=RippleAmplitudeLimitMetres + ?"true":"false",maskedSmoothAmplitude, + maskedSmoothAmplitude=FloorRippleAmplitudeLimitMetres; @@ -4327,7 +4373,7 @@ namespace StandaloneStreamBenchmark for(const double fraction:windowStarts) { const double start=(total-WindowLengthMetres)*fraction; - std::vector radii; + std::vector> radii; radii.reserve(static_cast(WindowLengthMetres/SampleSpacingMetres)+1); std::size_t wallProbeNonfinite=0,wallProbeOutOfBand=0; double wallProbeMinimum=std::numeric_limits::infinity(); @@ -4428,7 +4474,7 @@ namespace StandaloneStreamBenchmark // radius back to the signed wall distance so radius-carried ribs // remain visible; the cubic detrend removes the smooth mouth flare. const double measuredRadius=sampleRadius+0.5*(left+right); - radii.push_back(measuredRadius); + radii.push_back({distance,measuredRadius}); ++audit.samples; } if(radii.size()(WindowLengthMetres/ @@ -4449,7 +4495,7 @@ namespace StandaloneStreamBenchmark audit.wallWindowExpectedSamples+=static_cast( WindowLengthMetres/SampleSpacingMetres)+1; std::vector> windowSamples; - appendDetrendedProfile(radii,start,windowSamples); + appendDetrendedProfile(radii,windowSamples); double localPeriod=0.0; const double localAmplitude=sinusoidAmplitude(windowSamples,localPeriod); std::printf("[PassageWallRippleWindow] id=%s start_m=%.3f " @@ -4516,7 +4562,8 @@ namespace StandaloneStreamBenchmark // probe has no zero crossing. Judge ripple only in windows with // enough measured wall samples; naturalness separately requires // visible relief on the route as a whole. - audit.passed=audit.controlDetected&&audit.wallQualifiedWindows>0 + audit.passed=audit.controlDetected&&maskedSamplingControlPassed + &&audit.wallQualifiedWindows>0 &&wallWindowCoverage>=0.75 &&audit.localPeakAmplitudeMetrescontent.topMicrometres; }; std::uint64_t passageCount=0,passagePasses=0,blockedControls=0,naturalPasses=0; + std::uint64_t passageInteriorCapsuleSamples=0,passageInteriorSolidSamples=0; + std::uint64_t passageInteriorControls=0; std::uint64_t mouthProfilePasses=0,ripplePasses=0,rippleControlsDetected=0; std::uint64_t floorRippleControlsDetected=0,floorStepRippleControlsDetected=0; std::uint64_t shellAudits=0,shellFloorSamples=0,shellFloorViolations=0; @@ -4924,8 +4973,18 @@ namespace StandaloneStreamBenchmark const std::function passageSupportExemption= [&](double x,double y,double z) {return passageInteriorWinsAt(x,y,z-0.25);}; + const std::function passageInteriorPredicate= + [&](double x,double y,double z) + {return passageInteriorWinsAt(x,y,z, + std::numeric_limits::max(), + std::numeric_limits::max(),passageIndex);}; const FWalkAudit passageAudit=auditPlayerPath(Shape,path,0.375,0.125, - 1.125,3,0.45,passageSupportExemption); + 1.125,3,0.45,passageSupportExemption,passageInteriorPredicate); + const bool passageInteriorAirPassed= + passageAudit.passageInteriorCapsuleSamples>0 + &&passageAudit.passageInteriorSolidSamples==0; + passageInteriorCapsuleSamples+=passageAudit.passageInteriorCapsuleSamples; + passageInteriorSolidSamples+=passageAudit.passageInteriorSolidSamples; const FPassageNaturalnessAudit naturalAudit= auditPassageNaturalness(Shape,passageIndex); const auto lowerProfileStratum=std::find_if(ResolvedLayout->strates.begin(), @@ -4984,13 +5043,17 @@ namespace StandaloneStreamBenchmark rippleControlsDetected+=rippleAudit.controlDetected; floorRippleControlsDetected+=rippleAudit.floorControlDetected; floorStepRippleControlsDetected+=rippleAudit.floorStepControlDetected; - passagePasses+=passageAudit.passed&&mouthsInRadius&&mouthsInContent + passagePasses+=passageAudit.passed&&passageInteriorAirPassed + &&mouthsInRadius&&mouthsInContent &&naturalAudit.passed&&mouthProfileAudit.passed&&rippleAudit.passed; - std::printf("[ResolvedPassageAudit] id=%s interface=%d-%d control_points=%zu center_samples=%llu capsule_samples=%llu blocked=%llu support_samples=%llu support_overlap_skips=%llu unsupported=%llu grade_failures=%llu mouth_content=%d/2 mouth_radius=%d/2 walkability=%s\n", + std::printf("[ResolvedPassageAudit] id=%s interface=%d-%d control_points=%zu center_samples=%llu capsule_samples=%llu blocked=%llu passage_interior_capsule_samples=%llu passage_interior_solid_samples=%llu interior_air=%s support_samples=%llu support_overlap_skips=%llu unsupported=%llu grade_failures=%llu mouth_content=%d/2 mouth_radius=%d/2 walkability=%s\n", passage.stableId.c_str(),passage.upperStrateIndex,passage.lowerStrateIndex, passage.controlPoints.size(),static_cast(passageAudit.centers), static_cast(passageAudit.capsuleSamples), static_cast(passageAudit.blocked), + static_cast(passageAudit.passageInteriorCapsuleSamples), + static_cast(passageAudit.passageInteriorSolidSamples), + passageInteriorAirPassed?"PASS":"FAIL", static_cast(passageAudit.supportSamples), static_cast(passageAudit.supportOverlapSkips), static_cast(passageAudit.unsupported), @@ -5355,13 +5418,38 @@ namespace StandaloneStreamBenchmark path.push_back({point.x/coreMetres,point.y/coreMetres, playerZ/coreMetres}); } + const std::function passageInteriorPredicate= + [&](double x,double y,double z) + {return passageInteriorWinsAt(x,y,z, + std::numeric_limits::max(), + std::numeric_limits::max(),passageIndex);}; const FWalkAudit blockedAudit=auditPlayerPath(blockedWorld,path, - 0.125,0.125,0.25,3,0.45); + 0.125,0.125,0.25,3,0.45,{},passageInteriorPredicate); blockedControls+=blockedAudit.blocked>0&&!blockedAudit.passed; + const bool passageInteriorControlDetected= + blockedAudit.passageInteriorCapsuleSamples>0 + &&blockedAudit.passageInteriorSolidSamples>0; + passageInteriorControls+=passageInteriorControlDetected; std::printf("[ResolvedPassageBlockedControl] id=%s blocked_capsule_samples=%llu detected=%s\n", passage.stableId.c_str(),static_cast(blockedAudit.blocked), blockedAudit.blocked>0&&!blockedAudit.passed?"true":"false"); + std::printf("[ResolvedPassageInteriorNegativeControl] id=%s passage_interior_capsule_samples=%llu interior_solid_samples=%llu detected=%s status=%s\n", + passage.stableId.c_str(), + static_cast(blockedAudit.passageInteriorCapsuleSamples), + static_cast(blockedAudit.passageInteriorSolidSamples), + passageInteriorControlDetected?"true":"false", + passageInteriorControlDetected?"PASS":"FAIL"); } + const bool passageInteriorAuditPassed=passageCount>0 + &&passageInteriorCapsuleSamples>0&&passageInteriorSolidSamples==0 + &&passageInteriorControls==passageCount; + std::printf("[PassageInteriorCapsuleAuditSummary] interfaces=%llu capsule_samples=%llu solid_samples=%llu negative_controls=%llu/%llu status=%s\n", + static_cast(passageCount), + static_cast(passageInteriorCapsuleSamples), + static_cast(passageInteriorSolidSamples), + static_cast(passageInteriorControls), + static_cast(passageCount), + passageInteriorAuditPassed?"PASS":"FAIL"); const bool shellAuditPassed=shellAudits==passageCount &&shellFloorViolations==0&&shellWallViolations==0 &&shellInteriorViolations==0&&shellOpenAirSamples>0; @@ -5401,6 +5489,7 @@ namespace StandaloneStreamBenchmark floorStepRippleControlsDetected==passageCount?"DETECTED":"MISSED", ripplePasses==passageCount?"PASS":"FAIL"); const bool auditPassed=passageCount>0&&passagePasses==passageCount + &&passageInteriorAuditPassed &&naturalPasses==passageCount&&l23SmoothWallDetected &&mouthProfilePasses==passageCount&&l23TerraceControlDetected &&ripplePasses==passageCount