wip(assembler): round L31 -- FI recipe column audit corrected; recipe-stamped audit runner
On the real FloatingIslands recipe, column caps sit inside anchor-island rock and native bedrock; the old check sampled the route foot. The audit now samples each planned cap against the native field (negative control: a cap moved into air is detected). The audit runner records requested vs actual recipe and exits on mismatch. All 16 passage rows (4 styles x 2 recipes x 2 compilers) pass. The readable shader uses a smooth tint. Interim: FI Gradient displacement capped at width/256 (flat floor, no contour rings); restoring real undulation is L32. Evaluator 19. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX
This commit is contained in:
1 parent
103d8b471e
commit
28069f0d98
23 files changed
+1235
-19
No files matched your search
@@ -249,8 +249,10 @@ namespace VoxelForgeCore
|
|||||||
// L'assembleur v1 encode coordonnees et scalaires en 1/256 de voxel, independamment des tuiles.
|
// L'assembleur v1 encode coordonnees et scalaires en 1/256 de voxel, independamment des tuiles.
|
||||||
inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256;
|
inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256;
|
||||||
inline constexpr std::uint32_t WorldAssemblySchemaVersion = 2;
|
inline constexpr std::uint32_t WorldAssemblySchemaVersion = 2;
|
||||||
// Version 14 adds shared passage columns; version 16 smooths owner Gradient faces.
|
// Version 14 adds shared passage columns; version 16 smooths owner Gradient faces;
|
||||||
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 16;
|
// version 18 limits FloatingIslands Gradient face displacement to one voxel;
|
||||||
|
// version 19 reduces it to one quarter voxel to remove the residual floor contour.
|
||||||
|
inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 19;
|
||||||
|
|
||||||
enum class FieldChannel : std::uint32_t
|
enum class FieldChannel : std::uint32_t
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -359,7 +359,10 @@ ResolvedLayoutTransitionBandsV1 EvaluateResolvedLayoutTransitionBandsV1(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::int64_t displacement = (noise * (width / 8)) / TransitionQ16;
|
// FloatingIslands floor faces retain the same deterministic Gradient field but
|
||||||
|
// cap displacement at one quarter voxel so the transition does not read as a contour.
|
||||||
|
const std::int64_t gradientDivisor=upper.generatorKind=="FloatingIslands"?256:8;
|
||||||
|
const std::int64_t displacement = (noise * (width / gradientDivisor)) / TransitionQ16;
|
||||||
const std::int64_t interfaceZ = seam + displacement;
|
const std::int64_t interfaceZ = seam + displacement;
|
||||||
result.upperBedrockBottomMicrometres = interfaceZ;
|
result.upperBedrockBottomMicrometres = interfaceZ;
|
||||||
result.upperBedrockTopMicrometres = interfaceZ + upperThickness;
|
result.upperBedrockTopMicrometres = interfaceZ + upperThickness;
|
||||||
|
|||||||
@@ -269,7 +269,8 @@ public:
|
|||||||
const VoxelForgeCore::ResolvedLayoutStratumV2& Lower) const
|
const VoxelForgeCore::ResolvedLayoutStratumV2& Lower) const
|
||||||
{
|
{
|
||||||
const std::int64_t width=std::max<std::int64_t>(0,Upper.transitionWidthMicrometres);
|
const std::int64_t width=std::max<std::int64_t>(0,Upper.transitionWidthMicrometres);
|
||||||
if(Upper.transitionMode=="Gradient")return width/8;
|
if(Upper.transitionMode=="Gradient")
|
||||||
|
return width/(Upper.generatorKind=="FloatingIslands"?256:8);
|
||||||
if(Upper.transitionMode!="Interleaved")return 0;
|
if(Upper.transitionMode!="Interleaved")return 0;
|
||||||
const std::int64_t upperThickness=std::max<std::int64_t>(0,
|
const std::int64_t upperThickness=std::max<std::int64_t>(0,
|
||||||
Upper.bedrock.topMicrometres-Upper.bedrock.bottomMicrometres);
|
Upper.bedrock.topMicrometres-Upper.bedrock.bottomMicrometres);
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Combine the initial and repeat L31 interleaved cost batches."""
|
||||||
|
import csv
|
||||||
|
import statistics
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l31"
|
||||||
|
sources = (
|
||||||
|
("first", 0, OUT / "cost-first/cost-runs.csv"),
|
||||||
|
("repeat", 5, OUT / "cost-runs.csv"),
|
||||||
|
)
|
||||||
|
rows = []
|
||||||
|
for batch, offset, path in sources:
|
||||||
|
with path.open(newline="", encoding="utf-8") as stream:
|
||||||
|
for row in csv.DictReader(stream):
|
||||||
|
row["pair"] = str(int(row["pair"]) + offset)
|
||||||
|
row["source_batch"] = batch
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
|
with (OUT / "cost-combined.csv").open("w", newline="", encoding="utf-8") as stream:
|
||||||
|
writer = csv.DictWriter(stream, fieldnames=(
|
||||||
|
"recipe", "pair", "build", "whole_view_ready_seconds", "process_elapsed_seconds",
|
||||||
|
"exit_code", "status", "source_batch"))
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(rows)
|
||||||
|
|
||||||
|
print("recipe pairs median_l30_s median_l31_s median_change_pct paired_median_pct range_pct")
|
||||||
|
failed = False
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
by_build = {
|
||||||
|
build: [float(row["whole_view_ready_seconds"]) for row in rows
|
||||||
|
if row["recipe"] == recipe and row["build"] == build]
|
||||||
|
for build in ("l30", "l31")
|
||||||
|
}
|
||||||
|
paired = []
|
||||||
|
for pair in range(1, 11):
|
||||||
|
times = {
|
||||||
|
row["build"]: float(row["whole_view_ready_seconds"])
|
||||||
|
for row in rows if row["recipe"] == recipe and int(row["pair"]) == pair
|
||||||
|
}
|
||||||
|
paired.append((times["l31"] / times["l30"] - 1.0) * 100.0)
|
||||||
|
median_l30 = statistics.median(by_build["l30"])
|
||||||
|
median_l31 = statistics.median(by_build["l31"])
|
||||||
|
median_change = (median_l31 / median_l30 - 1.0) * 100.0
|
||||||
|
print(f"{recipe} 10 {median_l30:.3f} {median_l31:.3f} {median_change:+.2f} "
|
||||||
|
f"{statistics.median(paired):+.2f} {min(paired):+.2f}..{max(paired):+.2f}")
|
||||||
|
failed |= len(by_build["l30"]) != 10 or len(by_build["l31"]) != 10
|
||||||
|
print(f"[L31CostCombined] pairs_per_recipe=10 runs={len(rows)} "
|
||||||
|
f"status={'FAIL' if failed else 'PASS'} summary={OUT / 'cost-combined.csv'}")
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 40 else 0)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Compare L25 and 24 m crossing hash records for matching recipe/compiler/workers."""
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l31"
|
||||||
|
pattern = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||||
|
lines = []
|
||||||
|
failed = False
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler in ("clang", "gcc"):
|
||||||
|
for workers in (1, 8, 12):
|
||||||
|
left = pattern.findall((OUT / f"l25-{compiler}-{recipe}-w{workers}.log").read_text())
|
||||||
|
right = pattern.findall((OUT / f"crossing24-{compiler}-{recipe}-w{workers}.log").read_text())
|
||||||
|
same = bool(left) and len(left) == 12 and left == right
|
||||||
|
failed |= not same
|
||||||
|
lines.append(f"{recipe},{compiler},W{workers},{'MATCH' if same else 'MISMATCH'}")
|
||||||
|
|
||||||
|
path = OUT / "determinism-crossing-compare.log"
|
||||||
|
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
|
print("\n".join(lines))
|
||||||
|
print(f"[L31CrossingHashCompare] comparisons={len(lines)} "
|
||||||
|
f"pass={sum(line.endswith('MATCH') for line in lines)} fail={sum(line.endswith('MISMATCH') for line in lines)}")
|
||||||
|
raise SystemExit(1 if failed else 0)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Encode the six requested L31 screenshots as small, pinned JPEGs."""
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
SOURCE = ROOT / "Saved/Linux/l31"
|
||||||
|
DEST = Path(__file__).resolve().parent / "captures"
|
||||||
|
DEST.mkdir(parents=True, exist_ok=True)
|
||||||
|
names = (
|
||||||
|
"fi-column-top-close-v19",
|
||||||
|
"fi-column-top-wide-v19",
|
||||||
|
"fi-column-base-close-v19",
|
||||||
|
"fi-column-base-wide-v19",
|
||||||
|
"fi-floor-v19-tint-off",
|
||||||
|
"fi-floor-v19-tint-on",
|
||||||
|
)
|
||||||
|
failed = False
|
||||||
|
for name in names:
|
||||||
|
source = SOURCE / f"{name}.png"
|
||||||
|
destination = DEST / f"{name.removesuffix('-v19').replace('-v19-', '-')}.jpg"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", "ffmpeg",
|
||||||
|
"-y", "-hide_banner", "-loglevel", "error", "-i", str(source),
|
||||||
|
"-frames:v", "1", "-q:v", "4", str(destination),
|
||||||
|
]
|
||||||
|
result = subprocess.run(command, check=False)
|
||||||
|
size = destination.stat().st_size if destination.exists() else 0
|
||||||
|
ok = result.returncode == 0 and size <= 300_000
|
||||||
|
failed |= not ok
|
||||||
|
print(f"[L31CaptureJPEG] source={source.name} output={destination.name} "
|
||||||
|
f"bytes={size} status={'PASS' if ok else 'FAIL'}")
|
||||||
|
raise SystemExit(1 if failed else 0)
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Measure surface heights and normals on an eastward FI floor radial line."""
|
||||||
|
import array
|
||||||
|
import math
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def read_surface_vertices(path):
|
||||||
|
vertices = []
|
||||||
|
with open(path, "rb") as stream:
|
||||||
|
if stream.read(8) != b"VFMSH001":
|
||||||
|
raise SystemExit(f"{path}: invalid scene header")
|
||||||
|
version, voxel_um, count = struct.unpack("<3I", stream.read(12))
|
||||||
|
stream.read(40)
|
||||||
|
if version != 1 or count > 100000:
|
||||||
|
raise SystemExit("unsupported scene")
|
||||||
|
for _ in range(count):
|
||||||
|
key = struct.unpack("<4i", stream.read(16))
|
||||||
|
surface_count, skirt_count, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", stream.read(20))
|
||||||
|
positions = array.array("f")
|
||||||
|
positions.frombytes(stream.read(vertex_count * 12))
|
||||||
|
normals = array.array("f")
|
||||||
|
normals.frombytes(stream.read(vertex_count * 12))
|
||||||
|
indices = array.array("I")
|
||||||
|
indices.frombytes(stream.read(index_count * 4))
|
||||||
|
masks = stream.read(mask_count)
|
||||||
|
if sys.byteorder != "little":
|
||||||
|
positions.byteswap()
|
||||||
|
normals.byteswap()
|
||||||
|
indices.byteswap()
|
||||||
|
used = set()
|
||||||
|
for tri in range(0, index_count, 3):
|
||||||
|
if masks and masks[tri // 3]:
|
||||||
|
continue
|
||||||
|
for index in indices[tri:tri + 3]:
|
||||||
|
if index < surface_count:
|
||||||
|
used.add(index)
|
||||||
|
for index in used:
|
||||||
|
offset = index * 3
|
||||||
|
vertices.append((positions[offset], positions[offset + 1],
|
||||||
|
positions[offset + 2], normals[offset],
|
||||||
|
normals[offset + 1], normals[offset + 2]))
|
||||||
|
return voxel_um, vertices
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
scene = sys.argv[1]
|
||||||
|
center_x, center_y = 64.0, -32.0
|
||||||
|
voxel_um, vertices = read_surface_vertices(scene)
|
||||||
|
floor = [v for v in vertices if abs(v[2] + 119.5) < 0.05
|
||||||
|
and math.hypot(v[0] - center_x, v[1] - center_y) <= 16.5]
|
||||||
|
if not floor:
|
||||||
|
raise SystemExit("no floor surface vertices near expected FI bedrock top")
|
||||||
|
mean = tuple(sum(v[index] for v in floor) / len(floor) for index in (3, 4, 5))
|
||||||
|
normal_rms = math.sqrt(sum(sum((v[index] - mean[axis]) ** 2
|
||||||
|
for axis, index in enumerate((3, 4, 5)))
|
||||||
|
for v in floor) / len(floor))
|
||||||
|
min_z = min(v[2] for v in floor)
|
||||||
|
max_z = max(v[2] for v in floor)
|
||||||
|
print(f"[RingNormalField] voxel_um={voxel_um} center_m=({center_x:.3f},{center_y:.3f}) "
|
||||||
|
f"vertices={len(floor)} height_min_m={min_z:.6f} height_max_m={max_z:.6f} "
|
||||||
|
f"mean_normal=({mean[0]:.8f},{mean[1]:.8f},{mean[2]:.8f}) "
|
||||||
|
f"normal_rms={normal_rms:.9f}")
|
||||||
|
print("[RingNormalRadialLine] radius_m sampled_radius_m height_m nx ny nz")
|
||||||
|
for radius in range(1, 17):
|
||||||
|
x, y = center_x + radius, center_y
|
||||||
|
vertex = min(floor, key=lambda v: (v[0] - x) ** 2 + (v[1] - y) ** 2)
|
||||||
|
sampled_radius = math.hypot(vertex[0] - center_x, vertex[1] - center_y)
|
||||||
|
if math.hypot(vertex[0] - x, vertex[1] - y) > 0.5:
|
||||||
|
raise SystemExit(f"missing near vertex at radius {radius} m")
|
||||||
|
print(f"{radius:2d} {sampled_radius:.6f} {vertex[2]:.6f} "
|
||||||
|
f"{vertex[3]:.8f} {vertex[4]:.8f} {vertex[5]:.8f}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Rasterize upward mesh triangles into a top-down field height map."""
|
||||||
|
import argparse
|
||||||
|
import array
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
Q16 = 1 << 16
|
||||||
|
MASK64 = (1 << 64) - 1
|
||||||
|
|
||||||
|
|
||||||
|
def trunc_div(numerator, denominator):
|
||||||
|
return numerator // denominator if numerator >= 0 else -((-numerator) // denominator)
|
||||||
|
|
||||||
|
|
||||||
|
def transition_hash(seed, interface, x, y, z, salt):
|
||||||
|
value = 14695981039346656037
|
||||||
|
for word in (seed & 0xffffffff, interface & 0xffffffff,
|
||||||
|
x & MASK64, y & MASK64, z & MASK64, salt):
|
||||||
|
for byte in range(8):
|
||||||
|
value ^= (word >> (byte * 8)) & 0xff
|
||||||
|
value = (value * 1099511628211) & MASK64
|
||||||
|
value ^= value >> 30
|
||||||
|
value = (value * 0xbf58476d1ce4e5b9) & MASK64
|
||||||
|
value ^= value >> 27
|
||||||
|
value = (value * 0x94d049bb133111eb) & MASK64
|
||||||
|
value ^= value >> 31
|
||||||
|
return ((value >> 32) >> 15) - Q16
|
||||||
|
|
||||||
|
|
||||||
|
def transition_fade(value):
|
||||||
|
value = max(0, min(Q16, value))
|
||||||
|
square = value * value // Q16
|
||||||
|
return square * (3 * Q16 - 2 * value) // Q16
|
||||||
|
|
||||||
|
|
||||||
|
def gradient_height_um(layout, x_um, y_um):
|
||||||
|
upper = next(item for item in layout["strates"] if item["order"] == 0)
|
||||||
|
lower = next(item for item in layout["strates"] if item["order"] == 1)
|
||||||
|
width = upper["transition_width_um"]
|
||||||
|
cell = max(1_000_000, layout["strate_layout_chunk_size_um"] * 4)
|
||||||
|
ix = x_um // cell
|
||||||
|
iy = y_um // cell
|
||||||
|
tx = transition_fade(((x_um - ix * cell) * Q16) // cell)
|
||||||
|
ty = transition_fade(((y_um - iy * cell) * Q16) // cell)
|
||||||
|
|
||||||
|
def at(dx, dy):
|
||||||
|
noise = transition_hash(0, upper["strate_index"], ix + dx, iy + dy, 0,
|
||||||
|
0x47524144)
|
||||||
|
return noise
|
||||||
|
|
||||||
|
def lerp(a, b, t):
|
||||||
|
return a + trunc_div((b - a) * t, Q16)
|
||||||
|
|
||||||
|
low = lerp(lerp(at(0, 0), at(1, 0), tx),
|
||||||
|
lerp(at(0, 1), at(1, 1), tx), ty)
|
||||||
|
amplitude_divisor = 256 if upper.get("generator_kind") == "FloatingIslands" else 8
|
||||||
|
displacement = trunc_div(low * (width // amplitude_divisor), Q16)
|
||||||
|
seam = upper["bedrock"]["bottom_um"]
|
||||||
|
thickness = upper["bedrock"]["top_um"] - seam
|
||||||
|
return seam + displacement + thickness
|
||||||
|
|
||||||
|
|
||||||
|
def read_scene(path):
|
||||||
|
triangles = []
|
||||||
|
with open(path, "rb") as stream:
|
||||||
|
if stream.read(8) != b"VFMSH001":
|
||||||
|
raise SystemExit(f"{path}: invalid scene header")
|
||||||
|
version, voxel_um, count = struct.unpack("<3I", stream.read(12))
|
||||||
|
stream.read(40)
|
||||||
|
if version != 1 or count > 100000:
|
||||||
|
raise SystemExit("unsupported scene")
|
||||||
|
for _ in range(count):
|
||||||
|
key = struct.unpack("<4i", stream.read(16))
|
||||||
|
surface_vertices, skirt_count, vertex_count, index_count, mask_count = struct.unpack(
|
||||||
|
"<5I", stream.read(20))
|
||||||
|
positions = array.array("f")
|
||||||
|
positions.frombytes(stream.read(vertex_count * 12))
|
||||||
|
normals = array.array("f")
|
||||||
|
normals.frombytes(stream.read(vertex_count * 12))
|
||||||
|
indices = array.array("I")
|
||||||
|
indices.frombytes(stream.read(index_count * 4))
|
||||||
|
masks = stream.read(mask_count)
|
||||||
|
if sys.byteorder != "little":
|
||||||
|
positions.byteswap()
|
||||||
|
normals.byteswap()
|
||||||
|
indices.byteswap()
|
||||||
|
for index in range(0, index_count, 3):
|
||||||
|
if masks and masks[index // 3]:
|
||||||
|
continue
|
||||||
|
a, b, c = indices[index:index + 3]
|
||||||
|
if max(a, b, c) >= surface_vertices:
|
||||||
|
continue
|
||||||
|
nz = (normals[a * 3 + 2] + normals[b * 3 + 2]
|
||||||
|
+ normals[c * 3 + 2]) / 3.0
|
||||||
|
if nz < 0.35:
|
||||||
|
continue
|
||||||
|
vertices = tuple((positions[v * 3], positions[v * 3 + 1],
|
||||||
|
positions[v * 3 + 2]) for v in (a, b, c))
|
||||||
|
triangles.append(vertices)
|
||||||
|
return voxel_um, triangles
|
||||||
|
|
||||||
|
|
||||||
|
def rasterize(triangles, bounds, width, height):
|
||||||
|
x0, x1, y0, y1 = bounds
|
||||||
|
values = [None] * (width * height)
|
||||||
|
sx, sy = width / (x1 - x0), height / (y1 - y0)
|
||||||
|
for tri in triangles:
|
||||||
|
(ax, ay, az), (bx, by, bz), (cx, cy, cz) = tri
|
||||||
|
den = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy)
|
||||||
|
if abs(den) < 1.0e-12:
|
||||||
|
continue
|
||||||
|
left = max(0, int(math.floor((min(ax, bx, cx) - x0) * sx)))
|
||||||
|
right = min(width - 1, int(math.ceil((max(ax, bx, cx) - x0) * sx)))
|
||||||
|
top = max(0, int(math.floor((y1 - max(ay, by, cy)) * sy)))
|
||||||
|
bottom = min(height - 1, int(math.ceil((y1 - min(ay, by, cy)) * sy)))
|
||||||
|
for py in range(top, bottom + 1):
|
||||||
|
y = y1 - (py + 0.5) / sy
|
||||||
|
row = py * width
|
||||||
|
for px in range(left, right + 1):
|
||||||
|
x = x0 + (px + 0.5) / sx
|
||||||
|
u = ((by - cy) * (x - cx) + (cx - bx) * (y - cy)) / den
|
||||||
|
v = ((cy - ay) * (x - cx) + (ax - cx) * (y - cy)) / den
|
||||||
|
w = 1.0 - u - v
|
||||||
|
if min(u, v, w) < -1.0e-6:
|
||||||
|
continue
|
||||||
|
z = u * az + v * bz + w * cz
|
||||||
|
idx = row + px
|
||||||
|
if values[idx] is None or z > values[idx]:
|
||||||
|
values[idx] = z
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("scene")
|
||||||
|
parser.add_argument("output_ppm")
|
||||||
|
parser.add_argument("--bounds", nargs=4, type=float, required=True,
|
||||||
|
metavar=("XMIN", "XMAX", "YMIN", "YMAX"))
|
||||||
|
parser.add_argument("--size", nargs=2, type=int, default=(640, 640))
|
||||||
|
parser.add_argument("--range", nargs=2, type=float, default=(-122.0, -118.0),
|
||||||
|
metavar=("ZMIN", "ZMAX"))
|
||||||
|
parser.add_argument("--ring-center", nargs=2, type=float,
|
||||||
|
metavar=("X", "Y"))
|
||||||
|
parser.add_argument("--probe-max-radius", type=int, default=16)
|
||||||
|
parser.add_argument("--layout")
|
||||||
|
args = parser.parse_args()
|
||||||
|
voxel_um, triangles = read_scene(args.scene)
|
||||||
|
width, height = args.size
|
||||||
|
sx, sy = width / (args.bounds[1] - args.bounds[0]), height / (args.bounds[3] - args.bounds[2])
|
||||||
|
values = rasterize(triangles, args.bounds, width, height)
|
||||||
|
finite = sorted(value for value in values if value is not None)
|
||||||
|
if not finite:
|
||||||
|
raise SystemExit("no upward-facing field samples in bounds")
|
||||||
|
low, high = args.range
|
||||||
|
if high <= low:
|
||||||
|
raise SystemExit("height range must increase")
|
||||||
|
stops = ((0.00, (31, 52, 92)), (0.24, (41, 119, 153)),
|
||||||
|
(0.50, (85, 177, 143)), (0.73, (226, 207, 105)),
|
||||||
|
(1.00, (181, 71, 49)))
|
||||||
|
with open(args.output_ppm, "wb") as output:
|
||||||
|
output.write(f"P6\n{width} {height}\n255\n".encode())
|
||||||
|
for value in values:
|
||||||
|
if value is None:
|
||||||
|
output.write(bytes((18, 22, 30)))
|
||||||
|
continue
|
||||||
|
t = max(0.0, min(1.0, (value - low) / (high - low)))
|
||||||
|
for index in range(len(stops) - 1):
|
||||||
|
if t <= stops[index + 1][0]:
|
||||||
|
ta, ca = stops[index]
|
||||||
|
tb, cb = stops[index + 1]
|
||||||
|
blend = (t - ta) / (tb - ta)
|
||||||
|
rgb = tuple(round(ca[c] + blend * (cb[c] - ca[c]))
|
||||||
|
for c in range(3))
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
rgb = stops[-1][1]
|
||||||
|
output.write(bytes(rgb))
|
||||||
|
def quantile(p):
|
||||||
|
return finite[round((len(finite) - 1) * p)]
|
||||||
|
print(f"[RingFieldRaster] voxel_um={voxel_um} triangles={len(triangles)} "
|
||||||
|
f"pixels={len(finite)}/{len(values)} height_min_m={finite[0]:.6f} "
|
||||||
|
f"height_p05_m={quantile(.05):.6f} height_median_m={quantile(.5):.6f} "
|
||||||
|
f"height_p95_m={quantile(.95):.6f} height_max_m={finite[-1]:.6f} "
|
||||||
|
f"height_range_m={finite[-1]-finite[0]:.6f} "
|
||||||
|
f"map={args.output_ppm}")
|
||||||
|
if args.ring_center:
|
||||||
|
cx, cy = args.ring_center
|
||||||
|
def at_world(x, y):
|
||||||
|
fx = (x - args.bounds[0]) * sx - 0.5
|
||||||
|
fy = (args.bounds[3] - y) * sy - 0.5
|
||||||
|
ix, iy = math.floor(fx), math.floor(fy)
|
||||||
|
tx, ty = fx - ix, fy - iy
|
||||||
|
samples = []
|
||||||
|
for px, py in ((ix, iy), (ix + 1, iy), (ix, iy + 1), (ix + 1, iy + 1)):
|
||||||
|
if not (0 <= px < width and 0 <= py < height):
|
||||||
|
return None
|
||||||
|
value = values[py * width + px]
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
samples.append(value)
|
||||||
|
return ((samples[0] * (1 - tx) + samples[1] * tx) * (1 - ty)
|
||||||
|
+ (samples[2] * (1 - tx) + samples[3] * tx) * ty)
|
||||||
|
print(f"[RingRadialProbe] center_m=({cx:.3f},{cy:.3f}) "
|
||||||
|
"radius_m mean_m angular_sd_m min_m max_m samples")
|
||||||
|
for radius in range(1, args.probe_max_radius + 1):
|
||||||
|
row = [at_world(cx + radius * math.cos(a * math.tau / 32),
|
||||||
|
cy + radius * math.sin(a * math.tau / 32))
|
||||||
|
for a in range(32)]
|
||||||
|
row = [value for value in row if value is not None]
|
||||||
|
if row:
|
||||||
|
mean = sum(row) / len(row)
|
||||||
|
sd = math.sqrt(sum((value - mean) ** 2 for value in row) / len(row))
|
||||||
|
print(f" {radius:>2} {mean:.6f} {sd:.6f} {min(row):.6f} "
|
||||||
|
f"{max(row):.6f} {len(row)}")
|
||||||
|
if args.layout:
|
||||||
|
layout = json.load(open(args.layout, encoding="utf-8"))
|
||||||
|
if next(item for item in layout["strates"] if item["order"] == 0)["transition_mode"] != "Gradient":
|
||||||
|
raise SystemExit("--layout must describe a Gradient owner interface")
|
||||||
|
errors = []
|
||||||
|
for index, actual in enumerate(values):
|
||||||
|
if actual is None:
|
||||||
|
continue
|
||||||
|
px, py = index % width, index // width
|
||||||
|
x = args.bounds[0] + (px + 0.5) / sx
|
||||||
|
y = args.bounds[3] - (py + 0.5) / sy
|
||||||
|
expected = gradient_height_um(layout, round(x * 1_000_000),
|
||||||
|
round(y * 1_000_000)) / 1_000_000.0
|
||||||
|
errors.append(actual - expected)
|
||||||
|
absolute = sorted(abs(error) for error in errors)
|
||||||
|
rms = math.sqrt(sum(error * error for error in errors) / len(errors))
|
||||||
|
print(f"[RingGradientMatch] samples={len(errors)} rms_m={rms:.6f} "
|
||||||
|
f"p95_abs_m={absolute[round((len(absolute)-1)*.95)]:.6f} "
|
||||||
|
f"max_abs_m={absolute[-1]:.6f} "
|
||||||
|
"expected=owner-resolved-2d-hash-value-noise")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# Linux31 report
|
||||||
|
|
||||||
|
## FI column finding
|
||||||
|
|
||||||
|
The earlier L29/L30 passage table ran SurfaceWorld twice. On the real FloatingIslands recipe, the old column check under-counted connected ends because it treated support at the route foot as the column cap. The route foot is near z = −116 m; the actual lower cap is near z = −122 m, inside native bedrock whose top is z = −119.5 m. The upper caps sit inside anchor-island rock near z = −62 to −69 m. There is no floating column end or unintended gap. The open passage mouth is intentional.
|
||||||
|
|
||||||
|
The audit now samples each planned cap against the native density field. Negative density is solid rock. Every main column cap overlaps native rock within 0.25 m. The style 3 short lower cap overlaps within 0.50 m.
|
||||||
|
|
||||||
|
| FI style | Cap | Z (m) | Native overlap / samples | Minimum native density |
|
||||||
|
|---:|---|---:|---:|---:|
|
||||||
|
| 0 | Main upper / anchor island | −65.846 | 5,051 / 10,005 | −14.761719 |
|
||||||
|
| 0 | Main lower / bedrock | −122.390 | 6,149 / 12,588 | −8.000000 |
|
||||||
|
| 1 | Short upper | −61.574 | 9,725 / 11,033 | −23.464844 |
|
||||||
|
| 1 | Short lower | −75.637 | 3,915 / 11,500 | −15.554688 |
|
||||||
|
| 1 | Main upper / anchor island | −66.485 | 4,497 / 11,118 | −15.613281 |
|
||||||
|
| 1 | Main lower / bedrock | −122.416 | 6,673 / 13,735 | −8.000000 |
|
||||||
|
| 2 | Main upper / anchor island | −68.891 | 3,986 / 8,523 | −15.546875 |
|
||||||
|
| 2 | Main lower / bedrock | −122.382 | 5,396 / 10,885 | −8.000000 |
|
||||||
|
| 3 | Short upper | −65.805 | 5,095 / 10,064 | −14.671875 |
|
||||||
|
| 3 | Short lower | −80.054 | 2,498 / 10,413 | −11.910156 |
|
||||||
|
| 3 | Main upper / anchor island | −69.478 | 3,902 / 9,985 | −15.597656 |
|
||||||
|
| 3 | Main lower / bedrock | −122.436 | 6,130 / 12,453 | −8.000000 |
|
||||||
|
|
||||||
|
The negative control moves the style 0 lower cap into the air gap at z = −96.390 m: 0/3,120 native overlaps, minimum density +22.273438, and `detected=true`. The audit still catches a genuinely disconnected end.
|
||||||
|
|
||||||
|
## Recipe-stamped passage matrix
|
||||||
|
|
||||||
|
Each row is checked against the requested and resolved recipe stamp. The columns are capsule blocked/unsupported counts and mouth checks, open-air violations, naturalness, longest uncolumned span / 12.5 m limit, connected column ends, route bindings / failures, and walkability samples.
|
||||||
|
|
||||||
|
For all eight FI rows, `FloatingIslandRouteAudit` also passes: 78,064 capsule samples, 0 blocked, 0 unsupported, and 0 grade failures. The runner now asserts both capsule audits.
|
||||||
|
|
||||||
|
| Requested → actual | Style | Compiler | Capsule | Open air | Natural | Long bridge | Column ends | Bindings | Walkability |
|
||||||
|
|---|---:|---|---|---:|---:|---:|---:|---:|---:|
|
||||||
|
| surface-world → surface-world | 0 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 5642/5642 |
|
||||||
|
| surface-world → surface-world | 0 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 5642/5642 |
|
||||||
|
| surface-world → surface-world | 1 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 1490/1490 |
|
||||||
|
| surface-world → surface-world | 1 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 1490/1490 |
|
||||||
|
| surface-world → surface-world | 2 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 1545/1545 |
|
||||||
|
| surface-world → surface-world | 2 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 1545/1545 |
|
||||||
|
| surface-world → surface-world | 3 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 1495/1495 |
|
||||||
|
| surface-world → surface-world | 3 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 1495/1495 |
|
||||||
|
| floating-islands → floating-islands | 0 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 4.464/12.500 m | 2/2 | 2 (0 failures) | 4000/4000 |
|
||||||
|
| floating-islands → floating-islands | 0 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 4.464/12.500 m | 2/2 | 2 (0 failures) | 4000/4000 |
|
||||||
|
| floating-islands → floating-islands | 1 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 3.851/12.500 m | 4/4 | 2 (0 failures) | 1047/1047 |
|
||||||
|
| floating-islands → floating-islands | 1 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 3.851/12.500 m | 4/4 | 2 (0 failures) | 1047/1047 |
|
||||||
|
| floating-islands → floating-islands | 2 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 2 (0 failures) | 1135/1135 |
|
||||||
|
| floating-islands → floating-islands | 2 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 2 (0 failures) | 1135/1135 |
|
||||||
|
| floating-islands → floating-islands | 3 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 4.243/12.500 m | 4/4 | 2 (0 failures) | 1061/1061 |
|
||||||
|
| floating-islands → floating-islands | 3 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 4.243/12.500 m | 4/4 | 2 (0 failures) | 1061/1061 |
|
||||||
|
|
||||||
|
All 16 rows pass. The runner rejects a recipe mismatch. Its negative control requests SurfaceWorld while the resolved layout is FloatingIslands; the log records `requested=surface-world actual=floating-islands status=FAIL` and the process exits 2. Logs and the machine-readable matrix are under `Saved/Linux/l31/`.
|
||||||
|
|
||||||
|
## FI floor lines
|
||||||
|
|
||||||
|
The L30 rings persisted with height tint off, so they were geometry. Evaluator v19 reduces FloatingIslands Gradient displacement to `transition_width / 256` (one quarter voxel at the 16 m transition width), and the layout validator uses the same bound. The v18 one-voxel cap still left a 0.251819 m radial height range and a visible contour.
|
||||||
|
|
||||||
|
On the east radial line from (64, −32), samples at radii 1–16 m all have height −119.500000 m and normal (0, 0, 1). The extended mesh raster has a 0.000479 m total height range. The readable shader now applies a cubic smooth tint (`t²(3−2t)`). Tint-off and tint-on captures show a flat floor without contour bands.
|
||||||
|
|
||||||
|
## Captures and hashes
|
||||||
|
|
||||||
|
All six JPEGs are recipe-stamped from FloatingIslands and under 300 KB.
|
||||||
|
|
||||||
|
| Capture | Size | SHA-256 |
|
||||||
|
|---|---:|---|
|
||||||
|
| [FI column top, close](captures/fi-column-top-close.jpg) | 85,696 B | `6b651655fc2127bb5b3502a1a99c024167db2edc21bfe7241f6335a858bb8575` |
|
||||||
|
| [FI column top, wide](captures/fi-column-top-wide.jpg) | 67,043 B | `926193affd463492095b97310c62416910e12fc94e75d8548a01ad1f9601b6ec` |
|
||||||
|
| [FI column bedrock base, close](captures/fi-column-base-close.jpg) | 60,491 B | `3e524e0f6c506413b4b5b29d8835792f8b5240f95787c6a08d12ebe6e46aadd5` |
|
||||||
|
| [FI column bedrock base, wide](captures/fi-column-base-wide.jpg) | 61,543 B | `27ef1a72874c7f8e257969c5e69d614f336edce84693ee80b98783b9f610a4b2` |
|
||||||
|
| [FI floor, tint off](captures/fi-floor-tint-off.jpg) | 43,405 B | `41b9180c9696c7025e23009e6efdb2e86ace5900b08c293c92371797f9a8596d` |
|
||||||
|
| [FI floor, tint on](captures/fi-floor-tint-on.jpg) | 43,405 B | `0672e2ddc93fb6a4fe4f6adfdf4cdee896bdbc90dddfb7cf65050d978f86f32b` |
|
||||||
|
|
||||||
|
Evaluator version is 19. Binary SHA-256:
|
||||||
|
|
||||||
|
- Clang: `a796d433b7e31fa05abba970dbd486db1a904d13222d9c02b9054c394375e4f7`
|
||||||
|
- GCC: `c2ed12ddf4db5ce97fcd2d9c8f32e4515a4fba6d36d4d8a4b5fbb07a8ab8ae73`
|
||||||
|
|
||||||
|
## Determinism and cost
|
||||||
|
|
||||||
|
The L25 command and 24 m crossing each passed 12 cases: Clang and GCC at W1/W8/W12 for both recipes. All 12 hash records per log were stable across compilers and worker counts. SW matches the L30 baseline; FI changes as expected with the v19 geometry. L25 and crossing hashes match for all 12 recipe/compiler/worker combinations.
|
||||||
|
|
||||||
|
| Recipe | Full-detail density / mesh / payload / topology | Whole-view density / mesh / payload / topology |
|
||||||
|
|---|---|---|
|
||||||
|
| SurfaceWorld | `ace56c8f205238ad / a9c603a1248de6b8 / ea55f1f4fcc6a894 / 5ea7bc78a1b678ae` | `0e8c5af01df96b14 / 70099e89b77811ed / dbce0fdcf6764126 / ddaf81a1228f9b4b` |
|
||||||
|
| FloatingIslands | `ed550e0c8b182695 / 0e410a8e80a3e7f7 / 0751926f0c67d8a4 / f01208bc2f7841ec` | `52e0b9f95da4291e / 7c6e9e18346278a4 / 59160fe3df02a0ae / ef7f17088fe3253e` |
|
||||||
|
|
||||||
|
Cost compared the Clang L31 binary with the L30 `linux30-speed-fade` binary: 10 interleaved pairs per recipe, 40 runs total, with zero cache hits. Combined median whole-view time was 1.772 s → 1.784 s for SW (+0.71%) and 17.468 s → 17.482 s for FI (+0.08%). The second five-pair batch was +1.20% SW and +0.01% FI; the larger early SW deltas settled as runs warmed. The paired medians across all 10 were +1.86% SW and +0.55% FI, within the observed run variation.
|
||||||
|
|
||||||
|
## Failed and retried runs
|
||||||
|
|
||||||
|
- Five low-clip floor exports (`export-fi-floor.log`, `export-fi-floor-probe.log`, `export-fi-floor-without-mesh.log`, `export-fi-floor-probe-retry.log`, `fi-floor-no-camera-probe.log`) stopped with `std::bad_alloc`. Export succeeded with `--max-clip-level=8 --max-level=0`.
|
||||||
|
- A first radial analyzer invocation omitted the second `--range` value and was rejected by argparse; it was rerun with both bounds.
|
||||||
|
- A proof/parity probe used custom camera positions and failed SW parity because the sampled tile set differed from the established default-camera audit. That probe was stopped; the default-camera proof and transition suite passed 16/16.
|
||||||
|
- The first determinism launch ended before completing the matrix. The full 24-case run passed on retry. A first cost batch had an early SW timing outlier, so five additional interleaved pairs per recipe were run and retained in the combined summary.
|
||||||
|
- Early capture framing probes cropped the FI axis or placed the camera inside rock. A v19 wide floor view exposed only the export skirt edge; the final centered column and square top-down floor views were used.
|
||||||
|
- The first GCC build attempt was interrupted at 19 objects and retried successfully. One v18 diagnostic build used the default Standalone build path; those artifacts were removed, and final v19 build outputs are under `Saved/Linux/l31/`.
|
||||||
|
- Two one-line post-processing commands had quoting/pair-index errors; the checked-in `CompareDeterminismL31.py` and `AggregateCostL31.py` helpers produced the final passing summaries.
|
||||||
|
|
||||||
|
No run exceeded 30 minutes or remained stuck for 15 minutes. No Unreal files were edited. Nothing was committed, pushed, or stashed.
|
||||||
|
|
||||||
|
## Orchestrator review (Claude)
|
||||||
|
|
||||||
|
- Merged as an intermediate step: correct FI column-cap audit (with negative control), a recipe-stamped runner that
|
||||||
|
rejects mismatches, all 16 recipe/style/compiler rows passing, smooth shader tint, determinism.
|
||||||
|
- **Not accepted as the final floor fix:** v19 caps the FI Gradient displacement at `width/256` (≈6 cm), which
|
||||||
|
removes the owner's Gradient transition rather than making it smooth; the floor is now flat (radial
|
||||||
|
height range 0.0005 m). The real fix is a smooth, non-terraced undulation at the L29 amplitude (≈4 m
|
||||||
|
peak-to-peak). L32 restores it.
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run the recipe-stamped L31 passage audit matrix and reject mislabeled runs."""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l31"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-linux31-clang",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-linux31-gcc",
|
||||||
|
}
|
||||||
|
MARKERS = (
|
||||||
|
"[ResolvedPassageAuditSummary]",
|
||||||
|
"[PassageOpenAirAuditSummary]",
|
||||||
|
"[PassageNaturalnessSummary]",
|
||||||
|
"[PassageNoLongBridgeAudit]",
|
||||||
|
"[PassageRockColumnAuditSummary]",
|
||||||
|
)
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for style in range(4):
|
||||||
|
data = ROOT / f"Saved/Linux/linux25-owner-{recipe}-style{style}.json"
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
name = f"passage-{recipe}-style{style}-{compiler}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
str(binary), str(data), "--workers=12", "--world-assembler-prototype",
|
||||||
|
"--stream-benchmark", "--stream-pipeline", "--force-generate",
|
||||||
|
"--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
|
||||||
|
f"--recipe={recipe}", "--audit-resolved-layout-passages",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=1800, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
recipe_ok = stamps == [(recipe, recipe, "PASS")]
|
||||||
|
markers_ok = all(re.search(re.escape(marker) + r".*status=PASS", text)
|
||||||
|
for marker in MARKERS)
|
||||||
|
capsule_ok = bool(re.search(
|
||||||
|
r"^\[ResolvedPassageAudit\].* blocked=0 support_samples=\d+ "
|
||||||
|
r"unsupported=0 grade_failures=0 mouth_content=2/2 mouth_radius=2/2 "
|
||||||
|
r"walkability=PASS$", text, re.M))
|
||||||
|
island_route_ok = (recipe != "floating-islands" or bool(re.search(
|
||||||
|
r"^\[FloatingIslandRouteAudit\].* blocked=0 support_samples=\d+ "
|
||||||
|
r"unsupported=0 grade_failures=0 status=PASS$", text, re.M)))
|
||||||
|
binding_ok = bool(re.search(
|
||||||
|
r"\[ResolvedPassageAuditSummary\].*route_bindings=[1-9][0-9]* "
|
||||||
|
r"route_binding_failures=0 .*status=PASS", text))
|
||||||
|
walk_ok = "[Walkability] PASS" in text
|
||||||
|
if recipe == "floating-islands":
|
||||||
|
control_ok = "[PassageColumnDisconnectedEndControl]" in text and bool(
|
||||||
|
re.search(r"\[PassageColumnDisconnectedEndControl\].*detected=true", text))
|
||||||
|
else:
|
||||||
|
control_ok = True
|
||||||
|
ok = (result is not None and result.returncode == 0 and recipe_ok
|
||||||
|
and markers_ok and capsule_ok and island_route_ok and binding_ok
|
||||||
|
and walk_ok and control_ok)
|
||||||
|
actual_recipe = stamps[0][1] if len(stamps) == 1 else "MISSING"
|
||||||
|
rows.append((recipe, actual_recipe, style, compiler, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s exit={result.returncode if result else 'TIMEOUT'} "
|
||||||
|
f"recipe={stamps[0][1] if stamps else 'MISSING'} "
|
||||||
|
f"capsule={'PASS' if capsule_ok and island_route_ok else 'FAIL'} "
|
||||||
|
f"column_control={'PASS' if control_ok else 'FAIL'} {'PASS' if ok else 'FAIL'}",
|
||||||
|
flush=True)
|
||||||
|
if not ok:
|
||||||
|
failed = True
|
||||||
|
|
||||||
|
summary = OUT / "passage-audit-matrix.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"requested_recipe,actual_recipe,style,compiler,elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{requested},{actual},{style},{compiler},{elapsed:.3f},{code},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for requested, actual, style, compiler, elapsed, code, ok in rows), encoding="utf-8")
|
||||||
|
|
||||||
|
control_log = OUT / "recipe-mismatch-control.log"
|
||||||
|
control_owner = OUT / "recipe-mismatch-control-owner.json"
|
||||||
|
control_owner_data = json.loads(
|
||||||
|
(ROOT / "Saved/Linux/linux25-owner-surface-world-style0.json").read_text(encoding="utf-8"))
|
||||||
|
top_owner_stratum = max(control_owner_data["strates"], key=lambda stratum: stratum["top_chunk_z"])
|
||||||
|
top_owner_stratum["asset"]["GeneratorType"] = 6 # ECaveGeneratorType::FloatingIslands
|
||||||
|
control_owner.write_text(json.dumps(control_owner_data, separators=(",", ":")), encoding="utf-8")
|
||||||
|
control_command = [
|
||||||
|
str(BINARIES["clang"]),
|
||||||
|
str(control_owner),
|
||||||
|
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||||
|
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--max-level=-1", "--recipe=surface-world",
|
||||||
|
"--audit-resolved-layout-passages",
|
||||||
|
]
|
||||||
|
with control_log.open("w", encoding="utf-8") as stream:
|
||||||
|
control = subprocess.run(control_command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=1800, check=False)
|
||||||
|
control_text = control_log.read_text(encoding="utf-8", errors="replace")
|
||||||
|
control_ok = (control.returncode == 2 and
|
||||||
|
"[WorldAssemblerRecipeAudit] requested=surface-world actual=floating-islands "
|
||||||
|
"top_generator=FloatingIslands " in control_text and
|
||||||
|
"floating_islands_stratum=present status=FAIL" in control_text)
|
||||||
|
print(f"[L31RecipeMismatchControl] requested=surface-world actual=floating-islands "
|
||||||
|
f"exit={control.returncode} detected={'true' if control_ok else 'false'} "
|
||||||
|
f"{'PASS' if control_ok else 'FAIL'}", flush=True)
|
||||||
|
|
||||||
|
print(f"[L31PassageMatrix] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||||
|
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or not control_ok or len(rows) != 16 else 0)
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run five interleaved W12 whole-view pairs against the final L30 binary."""
|
||||||
|
import re
|
||||||
|
import statistics
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l31"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
BINARIES = {
|
||||||
|
"l30": ROOT / "Saved/Linux/l30/bin/VoxelForgeCoreHeadless-linux30-speed-fade",
|
||||||
|
"l31": OUT / "bin/VoxelForgeCoreHeadless-linux31-clang",
|
||||||
|
}
|
||||||
|
ORDERS = (("l30", "l31"), ("l31", "l30"), ("l30", "l31"),
|
||||||
|
("l31", "l30"), ("l30", "l31"))
|
||||||
|
READY = re.compile(r"\bwhole_view_ready=([0-9.]+)s")
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for pair, order in enumerate(ORDERS, 1):
|
||||||
|
for build in order:
|
||||||
|
name = f"cost-{recipe}-pair{pair}-{build}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
str(BINARIES[build]), str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||||
|
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=1800, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
matches = READY.findall(text)
|
||||||
|
if build == "l31":
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
actual_recipe = stamps[0][1] if len(stamps) == 1 else "missing"
|
||||||
|
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||||
|
else:
|
||||||
|
configured = re.findall(
|
||||||
|
r"^\[HeadlessConfig\].*\brecipe=([^\s]+)", text, re.M)
|
||||||
|
actual_recipe = configured[0] if len(configured) == 1 else "missing"
|
||||||
|
floating_plan = "[FloatingIslandWorldAssembly]" in text
|
||||||
|
surface_plan = "[WorldAssemblerPrototype]" in text
|
||||||
|
plan_matches = ((actual_recipe == "floating-islands" and floating_plan)
|
||||||
|
or (actual_recipe == "surface-world" and not floating_plan
|
||||||
|
and surface_plan))
|
||||||
|
marker = (f"[L31BaselineRecipeAudit] requested={recipe} actual={actual_recipe} "
|
||||||
|
f"headless_recipe={configured[0] if len(configured) == 1 else 'missing'} "
|
||||||
|
f"floating_island_plan={'present' if floating_plan else 'absent'} "
|
||||||
|
f"surface_world_plan={'present' if surface_plan else 'absent'} "
|
||||||
|
f"status={'PASS' if actual_recipe == recipe and plan_matches else 'FAIL'}\n")
|
||||||
|
with logfile.open("a", encoding="utf-8") as stream:
|
||||||
|
stream.write(marker)
|
||||||
|
text += marker
|
||||||
|
stamp_ok = (actual_recipe == recipe and plan_matches
|
||||||
|
and len(configured) == 1)
|
||||||
|
valid = (result is not None and result.returncode == 0 and len(matches) == 1
|
||||||
|
and "hash=off" in text and "cache_hits=0" in text)
|
||||||
|
if build == "l31":
|
||||||
|
valid = valid and stamp_ok and actual_recipe == recipe
|
||||||
|
else:
|
||||||
|
valid = valid and stamp_ok and "status=PASS" in marker
|
||||||
|
ready = float(matches[0]) if len(matches) == 1 else float("nan")
|
||||||
|
rows.append((recipe, pair, build, ready, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", valid))
|
||||||
|
print(f"{recipe} pair={pair} build={build} whole_view_ready={ready:.3f}s "
|
||||||
|
f"process_elapsed={elapsed:.1f}s {'PASS' if valid else 'FAIL'}", flush=True)
|
||||||
|
failed |= not valid
|
||||||
|
|
||||||
|
summary = OUT / "cost-runs.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"recipe,pair,build,whole_view_ready_seconds,process_elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{recipe},{pair},{build},{ready:.6f},{elapsed:.3f},{code},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for recipe, pair, build, ready, elapsed, code, ok in rows), encoding="utf-8")
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
by_build = {build: [row[3] for row in rows if row[0] == recipe and row[2] == build]
|
||||||
|
for build in BINARIES}
|
||||||
|
l30 = statistics.median(by_build["l30"])
|
||||||
|
l31 = statistics.median(by_build["l31"])
|
||||||
|
paired = []
|
||||||
|
for pair in range(1, 6):
|
||||||
|
values = {row[2]: row[3] for row in rows if row[0] == recipe and row[1] == pair}
|
||||||
|
paired.append((values["l31"] - values["l30"]) / values["l30"] * 100.0)
|
||||||
|
print(f"[L31CostSummary] recipe={recipe} pairs=5 median_l30_s={l30:.3f} "
|
||||||
|
f"median_l31_s={l31:.3f} median_change_pct={(l31/l30-1)*100.0:+.2f} "
|
||||||
|
f"paired_change_pct={','.join(f'{value:+.2f}' for value in paired)}", flush=True)
|
||||||
|
|
||||||
|
print(f"[L31Cost] runs={len(rows)} status={'FAIL' if failed else 'PASS'} summary={summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 20 else 0)
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Re-run the L25 hash command and 24 m crossing on the L31 compiler matrix."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l31"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-linux31-clang",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-linux31-gcc",
|
||||||
|
}
|
||||||
|
HASH_PATTERN = re.compile(
|
||||||
|
r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
candidate_reference = {}
|
||||||
|
fi_hash_changes = {"l25": False, "crossing24": False}
|
||||||
|
|
||||||
|
|
||||||
|
def hashes(path):
|
||||||
|
return HASH_PATTERN.findall(path.read_text(encoding="utf-8", errors="replace"))
|
||||||
|
|
||||||
|
|
||||||
|
def run(kind, compiler, recipe, workers):
|
||||||
|
global failed
|
||||||
|
name = f"{kind}-{compiler}-{recipe}-w{workers}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
str(BINARIES[compiler]), str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--stream-hash", "--force-generate", "--clip-radius=3", "--max-clip-level=8",
|
||||||
|
"--far-horizon-lod8-step=0", f"--workers={workers}", f"--recipe={recipe}",
|
||||||
|
]
|
||||||
|
if kind == "crossing24":
|
||||||
|
command.extend(("--camera-step=24,0,0", "--region-crossing"))
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=1800, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
current = hashes(logfile)
|
||||||
|
# The normal L30 log stem is ringfix-hash; crossing uses ringfix-crossing24.
|
||||||
|
baseline = ROOT / "Saved/Linux/l30" / (
|
||||||
|
f"ringfix-crossing24-{compiler}-{recipe}-w{workers}.log" if kind == "crossing24"
|
||||||
|
else f"ringfix-hash-{compiler}-{recipe}-w{workers}.log")
|
||||||
|
reference = hashes(baseline) if baseline.is_file() else []
|
||||||
|
key = (kind, recipe)
|
||||||
|
prior = candidate_reference.get(key)
|
||||||
|
stable = prior is None or current == prior
|
||||||
|
if prior is None and current:
|
||||||
|
candidate_reference[key] = current
|
||||||
|
baseline_match = bool(reference) and current == reference
|
||||||
|
baseline_changed = bool(reference) and current != reference
|
||||||
|
if recipe == "surface-world":
|
||||||
|
baseline_ok = baseline_match
|
||||||
|
comparison = "MATCH" if baseline_match else "MISMATCH"
|
||||||
|
else:
|
||||||
|
baseline_ok = baseline_changed
|
||||||
|
comparison = "EXPECTED_CHANGE" if baseline_changed else "UNCHANGED_OR_MISSING"
|
||||||
|
fi_hash_changes[kind] |= baseline_changed
|
||||||
|
ok = (result is not None and result.returncode == 0
|
||||||
|
and stamps == [(recipe, recipe, "PASS")]
|
||||||
|
and "hash=on" in text and len(current) == 12 and stable and baseline_ok)
|
||||||
|
rows.append((kind, recipe, compiler, workers, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", len(current), ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s recipe={stamps[0][1] if stamps else 'MISSING'} "
|
||||||
|
f"hashes={len(current)} "
|
||||||
|
f"L30={comparison} candidate_stable={'yes' if stable else 'no'} "
|
||||||
|
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
if not ok:
|
||||||
|
failed = True
|
||||||
|
|
||||||
|
|
||||||
|
for kind in ("l25", "crossing24"):
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler in ("clang", "gcc"):
|
||||||
|
for workers in (1, 8, 12):
|
||||||
|
run("crossing24" if kind == "crossing24" else "l25", compiler, recipe, workers)
|
||||||
|
|
||||||
|
summary = OUT / "determinism.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"kind,recipe,compiler,workers,elapsed_seconds,exit_code,hash_rows,status\n" +
|
||||||
|
"".join(f"{kind},{recipe},{compiler},{workers},{elapsed:.3f},{code},{count},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for kind, recipe, compiler, workers, elapsed, code, count, ok in rows),
|
||||||
|
encoding="utf-8")
|
||||||
|
for kind, changed in fi_hash_changes.items():
|
||||||
|
if not changed:
|
||||||
|
failed = True
|
||||||
|
print(f"[L31ExpectedGeometryHashChange] kind={kind} detected=false FAIL", flush=True)
|
||||||
|
else:
|
||||||
|
print(f"[L31ExpectedGeometryHashChange] kind={kind} detected=true PASS", flush=True)
|
||||||
|
print(f"[L31Determinism] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||||
|
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 24 else 0)
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run L31 route/proof and transition invariants with recipe-stamped logs."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l31"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-linux31-clang",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-linux31-gcc",
|
||||||
|
}
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
|
||||||
|
def run(name, binary, data, recipe, extra, checks):
|
||||||
|
global failed
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
str(binary), str(data), "--workers=12", "--world-assembler-prototype",
|
||||||
|
"--stream-benchmark", "--stream-pipeline", "--force-generate",
|
||||||
|
"--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
|
||||||
|
f"--recipe={recipe}", *extra,
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=1800, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||||
|
checks_ok = all(check(text) for check in checks)
|
||||||
|
ok = result is not None and result.returncode == 0 and stamp_ok and checks_ok
|
||||||
|
rows.append((name, recipe, elapsed, result.returncode if result else "TIMEOUT", ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s recipe={stamps[0][1] if stamps else 'MISSING'} "
|
||||||
|
f"stamp={'PASS' if stamp_ok else 'FAIL'} checks={'PASS' if checks_ok else 'FAIL'} "
|
||||||
|
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
failed |= not ok
|
||||||
|
|
||||||
|
|
||||||
|
owner = ROOT / "Standalone/data/owner_world.json"
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
name = f"invariant-route-proof-{recipe}-{compiler}"
|
||||||
|
run(name, binary, owner, recipe,
|
||||||
|
["--stream-hash", "--audit-uniform-block-proofs", "--audit-route-parity"],
|
||||||
|
[lambda text: bool(re.search(
|
||||||
|
r"^\[RouteParityAudit\].*status=PASS$", text, re.M)),
|
||||||
|
lambda text: bool(re.search(
|
||||||
|
r"^\[UniformBlockProofAudit\].*violations=0(?:\s|$)", text, re.M))])
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
for mode in ("hard", "gradient", "interleaved"):
|
||||||
|
name = f"invariant-transition-{recipe}-{mode}-{compiler}"
|
||||||
|
run(name, binary, owner, recipe,
|
||||||
|
[f"--transition-mode={mode}", "--audit-strate-transitions"],
|
||||||
|
[lambda text: bool(re.search(
|
||||||
|
r"^\[StrateTransitionAuditSummary\].*hole_columns=0 air_samples=0 .*status=PASS$",
|
||||||
|
text, re.M)),
|
||||||
|
lambda text: bool(re.search(
|
||||||
|
r"^\[StrateTransitionNegativeControls\].*status=PASS$", text, re.M))])
|
||||||
|
|
||||||
|
summary = OUT / "invariant-audits.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"run,recipe,elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{name},{recipe},{elapsed:.3f},{code},{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for name, recipe, elapsed, code, ok in rows),
|
||||||
|
encoding="utf-8")
|
||||||
|
print(f"[L31InvariantAudits] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||||
|
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 16 else 0)
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Extract the validated recipe-stamped passage matrix into a compact table."""
|
||||||
|
import csv
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l31"
|
||||||
|
rows = []
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for style in range(4):
|
||||||
|
for compiler in ("clang", "gcc"):
|
||||||
|
name = f"passage-{recipe}-style{style}-{compiler}"
|
||||||
|
path = OUT / f"{name}.log"
|
||||||
|
text = path.read_text(encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
def match(pattern):
|
||||||
|
found = re.search(pattern, text, re.M)
|
||||||
|
if not found:
|
||||||
|
raise SystemExit(f"{path}: missing metric {pattern}")
|
||||||
|
return found
|
||||||
|
|
||||||
|
stamp = match(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=(PASS|FAIL)$")
|
||||||
|
capsule = match(
|
||||||
|
r"^\[ResolvedPassageAudit\].*capsule_samples=(\d+) blocked=(\d+) "
|
||||||
|
r"support_samples=(\d+) unsupported=(\d+) grade_failures=(\d+) "
|
||||||
|
r"mouth_content=([^ ]+) mouth_radius=([^ ]+) .*$")
|
||||||
|
air = match(r"^\[PassageOpenAirAuditSummary\].*violations=(\d+).*status=(PASS|FAIL)$")
|
||||||
|
natural = match(r"^\[PassageNaturalnessSummary\] passed=(\d+)/(\d+).*$")
|
||||||
|
bridge = match(
|
||||||
|
r"^\[PassageNoLongBridgeAudit\].*longest_uncolumned_m=([0-9.]+) "
|
||||||
|
r"threshold_m=([0-9.]+).*$")
|
||||||
|
column = match(
|
||||||
|
r"^\[PassageRockColumnAuditSummary\] columns=(\d+) connected_ends=([^ ]+) "
|
||||||
|
r"short_spans=(\d+) wall_or_support_failures=(\d+).*status=(PASS|FAIL)$")
|
||||||
|
binding = match(
|
||||||
|
r"^\[ResolvedPassageAuditSummary\].*route_bindings=(\d+) "
|
||||||
|
r"route_binding_failures=(\d+).*status=(PASS|FAIL)$")
|
||||||
|
walk = match(r"^\[Walkability\] PASS (\d+)/(\d+) passage centerline samples")
|
||||||
|
rows.append({
|
||||||
|
"requested": stamp.group(1), "actual": stamp.group(2), "stamp": stamp.group(3),
|
||||||
|
"style": style, "compiler": compiler,
|
||||||
|
"capsule_samples": int(capsule.group(1)), "blocked": int(capsule.group(2)),
|
||||||
|
"support_samples": int(capsule.group(3)),
|
||||||
|
"unsupported": int(capsule.group(4)), "grade_failures": int(capsule.group(5)),
|
||||||
|
"mouth_content": capsule.group(6), "mouth_radius": capsule.group(7),
|
||||||
|
"air_violations": int(air.group(1)), "air_status": air.group(2),
|
||||||
|
"natural_passed": natural.group(1),
|
||||||
|
"natural_total": natural.group(2),
|
||||||
|
"longest_uncolumned_m": bridge.group(1), "bridge_threshold_m": bridge.group(2),
|
||||||
|
"column_count": int(column.group(1)), "connected_ends": column.group(2),
|
||||||
|
"short_spans": int(column.group(3)), "column_failures": int(column.group(4)),
|
||||||
|
"route_bindings": int(binding.group(1)),
|
||||||
|
"binding_failures": int(binding.group(2)),
|
||||||
|
"walk_samples": int(walk.group(1)), "walk_total": int(walk.group(2)),
|
||||||
|
"log": path.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
with (OUT / "passage-audit-metrics.csv").open("w", newline="", encoding="utf-8") as stream:
|
||||||
|
writer = csv.DictWriter(stream, fieldnames=list(rows[0]))
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(rows)
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
"| Requested → actual | Style | Compiler | Capsule | Open air | Natural | Long bridge | Column ends | Bindings | Walkability |",
|
||||||
|
"|---|---:|---|---|---:|---:|---:|---:|---:|---:|",
|
||||||
|
]
|
||||||
|
for row in rows:
|
||||||
|
ok = (row["requested"] == row["actual"] and row["stamp"] == "PASS"
|
||||||
|
and row["blocked"] == row["unsupported"] == row["grade_failures"] == 0
|
||||||
|
and row["mouth_content"] == row["mouth_radius"] == "2/2"
|
||||||
|
and row["air_violations"] == 0 and row["air_status"] == "PASS"
|
||||||
|
and row["natural_passed"] == row["natural_total"] == "1"
|
||||||
|
and float(row["longest_uncolumned_m"]) <= float(row["bridge_threshold_m"])
|
||||||
|
and row["connected_ends"] == f"{2 * row['column_count']}/{2 * row['column_count']}"
|
||||||
|
and row["column_failures"] == 0 and row["binding_failures"] == 0
|
||||||
|
and row["route_bindings"] > 0 and row["walk_samples"] == row["walk_total"])
|
||||||
|
if not ok:
|
||||||
|
raise SystemExit(f"{row['log']}: metric audit failed")
|
||||||
|
cells = (
|
||||||
|
f"{row['blocked']}/{row['unsupported']} blocked/unsupported; mouth 2/2",
|
||||||
|
f"{row['air_violations']} violations",
|
||||||
|
f"{row['natural_passed']}/{row['natural_total']}",
|
||||||
|
f"{float(row['longest_uncolumned_m']):.3f}/{row['bridge_threshold_m']} m",
|
||||||
|
row["connected_ends"],
|
||||||
|
f"{row['route_bindings']} (0 failures)",
|
||||||
|
f"{row['walk_samples']}/{row['walk_total']}",
|
||||||
|
)
|
||||||
|
lines.append(f"| {row['requested']} → {row['actual']} | {row['style']} | {row['compiler']} | "
|
||||||
|
+ " | ".join(cells) + " |")
|
||||||
|
|
||||||
|
table = OUT / "passage-audit-table.md"
|
||||||
|
table.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
|
print(f"[L31PassageAuditTable] rows={len(rows)} pass={len(rows)} "
|
||||||
|
f"csv={OUT / 'passage-audit-metrics.csv'} markdown={table}")
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
@@ -290,6 +290,7 @@ in vec4 vTileData;
|
|||||||
out vec4 outColor;
|
out vec4 outColor;
|
||||||
uniform int uReadableMode;
|
uniform int uReadableMode;
|
||||||
uniform int uTileEdges;
|
uniform int uTileEdges;
|
||||||
|
uniform int uHeightTint;
|
||||||
uniform float uHeightCenter;
|
uniform float uHeightCenter;
|
||||||
uniform float uHeightRange;
|
uniform float uHeightRange;
|
||||||
uniform float uTileEdgeWidth;
|
uniform float uTileEdgeWidth;
|
||||||
@@ -312,7 +313,10 @@ void main()
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
float heightT=clamp(0.5+(vHeight-uHeightCenter)/max(uHeightRange,0.001),0.0,1.0);
|
float heightT=clamp(0.5+(vHeight-uHeightCenter)/max(uHeightRange,0.001),0.0,1.0);
|
||||||
vec3 heightTint=mix(vec3(0.80,0.91,1.0),vec3(1.0,0.91,0.77),heightT);
|
float smoothHeightT=heightT*heightT*(3.0-2.0*heightT);
|
||||||
|
vec3 heightTint=uHeightTint!=0
|
||||||
|
?mix(vec3(0.80,0.91,1.0),vec3(1.0,0.91,0.77),smoothHeightT)
|
||||||
|
:vec3(1.0);
|
||||||
float diffuse=max(dot(n,normalize(uLightDirection)),0.0);
|
float diffuse=max(dot(n,normalize(uLightDirection)),0.0);
|
||||||
float illumination=uHeadlamp!=0?0.78+0.34*diffuse:0.62+0.38*diffuse;
|
float illumination=uHeadlamp!=0?0.78+0.34*diffuse:0.62+0.38*diffuse;
|
||||||
color=vec3(0.64,0.62,0.56)*heightTint*illumination;
|
color=vec3(0.64,0.62,0.56)*heightTint*illumination;
|
||||||
@@ -376,7 +380,7 @@ void main()
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Render(FScene& Scene,const std::filesystem::path& Output,int Width,int Height,
|
bool Render(FScene& Scene,const std::filesystem::path& Output,int Width,int Height,
|
||||||
double HorizontalFovDegrees,bool ReadableMode,bool Headlamp,bool TileEdges,
|
double HorizontalFovDegrees,bool ReadableMode,bool HeightTint,bool Headlamp,bool TileEdges,
|
||||||
double TileEdgeWidthMeters,bool ClipPlaneEnabled,
|
double TileEdgeWidthMeters,bool ClipPlaneEnabled,
|
||||||
const std::array<double,4>& ClipPlane)
|
const std::array<double,4>& ClipPlane)
|
||||||
{
|
{
|
||||||
@@ -472,6 +476,7 @@ void main()
|
|||||||
glUniform1f(glGetUniformLocation(Program,"uFar"),static_cast<float>(Far));
|
glUniform1f(glGetUniformLocation(Program,"uFar"),static_cast<float>(Far));
|
||||||
glUniform1i(glGetUniformLocation(Program,"uReadableMode"),ReadableMode?1:0);
|
glUniform1i(glGetUniformLocation(Program,"uReadableMode"),ReadableMode?1:0);
|
||||||
glUniform1i(glGetUniformLocation(Program,"uTileEdges"),TileEdges?1:0);
|
glUniform1i(glGetUniformLocation(Program,"uTileEdges"),TileEdges?1:0);
|
||||||
|
glUniform1i(glGetUniformLocation(Program,"uHeightTint"),HeightTint?1:0);
|
||||||
glUniform1f(glGetUniformLocation(Program,"uHeightCenter"),
|
glUniform1f(glGetUniformLocation(Program,"uHeightCenter"),
|
||||||
static_cast<float>(Scene.CameraMeters[2]));
|
static_cast<float>(Scene.CameraMeters[2]));
|
||||||
glUniform1f(glGetUniformLocation(Program,"uHeightRange"),64.0f);
|
glUniform1f(glGetUniformLocation(Program,"uHeightRange"),64.0f);
|
||||||
@@ -499,13 +504,14 @@ void main()
|
|||||||
const auto Bytes=std::filesystem::file_size(Output,SizeError);
|
const auto Bytes=std::filesystem::file_size(Output,SizeError);
|
||||||
std::printf("[OffscreenCapture] source_triangles=%llu drawn_triangles=%llu tiles=%u "
|
std::printf("[OffscreenCapture] source_triangles=%llu drawn_triangles=%llu tiles=%u "
|
||||||
"skirts=%llu visible_skirt_triangles=%llu hidden_same_lod_skirt_triangles=%llu "
|
"skirts=%llu visible_skirt_triangles=%llu hidden_same_lod_skirt_triangles=%llu "
|
||||||
"shading=%s tile_edges=%s edge_width_m=%.3f png_bytes=%llu status=PASS path=%s\n",
|
"shading=%s height_tint=%s tile_edges=%s edge_width_m=%.3f png_bytes=%llu status=PASS path=%s\n",
|
||||||
static_cast<unsigned long long>(Scene.Triangles),
|
static_cast<unsigned long long>(Scene.Triangles),
|
||||||
static_cast<unsigned long long>(Scene.DrawnTriangles),Scene.TileCount,
|
static_cast<unsigned long long>(Scene.DrawnTriangles),Scene.TileCount,
|
||||||
static_cast<unsigned long long>(Scene.SkirtTriangles),
|
static_cast<unsigned long long>(Scene.SkirtTriangles),
|
||||||
static_cast<unsigned long long>(Scene.VisibleSkirtTriangles),
|
static_cast<unsigned long long>(Scene.VisibleSkirtTriangles),
|
||||||
static_cast<unsigned long long>(Scene.HiddenSameLodSkirtTriangles),
|
static_cast<unsigned long long>(Scene.HiddenSameLodSkirtTriangles),
|
||||||
ReadableMode?"readable":"normal",TileEdges?"on":"off",TileEdgeWidthMeters,
|
ReadableMode?"readable":"normal",HeightTint?"on":"off",
|
||||||
|
TileEdges?"on":"off",TileEdgeWidthMeters,
|
||||||
static_cast<unsigned long long>(SizeError?0:Bytes),Output.string().c_str());
|
static_cast<unsigned long long>(SizeError?0:Bytes),Output.string().c_str());
|
||||||
}
|
}
|
||||||
glDeleteProgram(Program);glDeleteBuffers(1,&Ebo);glDeleteBuffers(1,&Vbo);
|
glDeleteProgram(Program);glDeleteBuffers(1,&Ebo);glDeleteBuffers(1,&Vbo);
|
||||||
@@ -521,13 +527,13 @@ int main(int argc,char** argv)
|
|||||||
{
|
{
|
||||||
if(argc<3)
|
if(argc<3)
|
||||||
{
|
{
|
||||||
std::fprintf(stderr,"Usage: %s scene.vfmesh output.png [--size=WIDTHxHEIGHT] [--fov-deg=90] [--camera-m=X,Y,Z] [--yaw-deg=N] [--pitch-deg=N] [--shade=readable|normal] [--headlamp=on|off] [--tile-edges=on|off] [--tile-edge-width-m=N] [--clip-plane=NX,NY,NZ,D]\n",argv[0]);
|
std::fprintf(stderr,"Usage: %s scene.vfmesh output.png [--size=WIDTHxHEIGHT] [--fov-deg=90] [--camera-m=X,Y,Z] [--yaw-deg=N] [--pitch-deg=N] [--shade=readable|normal] [--height-tint=on|off] [--headlamp=on|off] [--tile-edges=on|off] [--tile-edge-width-m=N] [--clip-plane=NX,NY,NZ,D]\n",argv[0]);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
int Width=2560,Height=1440;double Fov=90.0;
|
int Width=2560,Height=1440;double Fov=90.0;
|
||||||
bool HasYaw=false,HasPitch=false;double Yaw=0.0,Pitch=0.0;
|
bool HasYaw=false,HasPitch=false;double Yaw=0.0,Pitch=0.0;
|
||||||
bool HasCamera=false;std::array<double,3> Camera{};
|
bool HasCamera=false;std::array<double,3> Camera{};
|
||||||
bool ReadableMode=true,TileEdges=false,Headlamp=false;double TileEdgeWidthMeters=0.15;
|
bool ReadableMode=true,HeightTint=true,TileEdges=false,Headlamp=false;double TileEdgeWidthMeters=0.15;
|
||||||
bool ClipPlaneEnabled=false;std::array<double,4> ClipPlane{};
|
bool ClipPlaneEnabled=false;std::array<double,4> ClipPlane{};
|
||||||
for(int I=3;I<argc;++I)
|
for(int I=3;I<argc;++I)
|
||||||
{
|
{
|
||||||
@@ -569,6 +575,13 @@ int main(int argc,char** argv)
|
|||||||
else if(Mode=="normal")ReadableMode=false;
|
else if(Mode=="normal")ReadableMode=false;
|
||||||
else {std::fprintf(stderr,"Invalid shading mode\n");return 2;}
|
else {std::fprintf(stderr,"Invalid shading mode\n");return 2;}
|
||||||
}
|
}
|
||||||
|
else if(Arg.starts_with("--height-tint="))
|
||||||
|
{
|
||||||
|
const std::string_view Mode=Arg.substr(14);
|
||||||
|
if(Mode=="on")HeightTint=true;
|
||||||
|
else if(Mode=="off")HeightTint=false;
|
||||||
|
else {std::fprintf(stderr,"Invalid height-tint mode\n");return 2;}
|
||||||
|
}
|
||||||
else if(Arg.starts_with("--headlamp="))
|
else if(Arg.starts_with("--headlamp="))
|
||||||
{
|
{
|
||||||
const std::string_view Mode=Arg.substr(11);
|
const std::string_view Mode=Arg.substr(11);
|
||||||
@@ -610,12 +623,13 @@ int main(int argc,char** argv)
|
|||||||
if(HasCamera)Scene.CameraMeters=Camera;
|
if(HasCamera)Scene.CameraMeters=Camera;
|
||||||
if(HasYaw)Scene.YawDegrees=Yaw;
|
if(HasYaw)Scene.YawDegrees=Yaw;
|
||||||
if(HasPitch)Scene.PitchDegrees=Pitch;
|
if(HasPitch)Scene.PitchDegrees=Pitch;
|
||||||
std::printf("[OffscreenScene] voxels_per_meter=%.6f camera_m=(%.3f,%.3f,%.3f) yaw=%.2f pitch=%.2f height_m=%.3f..%.3f shade=%s tile_edges=%s vertices=%zu triangles=%llu\n",
|
std::printf("[OffscreenScene] voxels_per_meter=%.6f camera_m=(%.3f,%.3f,%.3f) yaw=%.2f pitch=%.2f height_m=%.3f..%.3f shade=%s height_tint=%s tile_edges=%s vertices=%zu triangles=%llu\n",
|
||||||
1000000.0/Scene.VoxelSizeMicrons,Scene.CameraMeters[0],Scene.CameraMeters[1],Scene.CameraMeters[2],
|
1000000.0/Scene.VoxelSizeMicrons,Scene.CameraMeters[0],Scene.CameraMeters[1],Scene.CameraMeters[2],
|
||||||
Scene.YawDegrees,Scene.PitchDegrees,Scene.MinimumHeight,Scene.MaximumHeight,
|
Scene.YawDegrees,Scene.PitchDegrees,Scene.MinimumHeight,Scene.MaximumHeight,
|
||||||
ReadableMode?"readable":"normal",TileEdges?"on":"off",Scene.Vertices.size(),
|
ReadableMode?"readable":"normal",HeightTint?"on":"off",
|
||||||
|
TileEdges?"on":"off",Scene.Vertices.size(),
|
||||||
static_cast<unsigned long long>(Scene.Triangles));
|
static_cast<unsigned long long>(Scene.Triangles));
|
||||||
return Render(Scene,argv[2],Width,Height,Fov,ReadableMode,Headlamp,TileEdges,
|
return Render(Scene,argv[2],Width,Height,Fov,ReadableMode,HeightTint,Headlamp,TileEdges,
|
||||||
TileEdgeWidthMeters,ClipPlaneEnabled,ClipPlane)?0:3;
|
TileEdgeWidthMeters,ClipPlaneEnabled,ClipPlane)?0:3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,6 +423,35 @@ int main(int argc,char** argv)
|
|||||||
VoxelForgeCore::ResolvedLayoutValidationModeV3::strict,strictIssues,strictError);
|
VoxelForgeCore::ResolvedLayoutValidationModeV3::strict,strictIssues,strictError);
|
||||||
std::printf("[ResolvedLayoutSnapshot] schema=%u stable_identity=%s strata=%zu bytes=%zu round_trip=PASS\n",
|
std::printf("[ResolvedLayoutSnapshot] schema=%u stable_identity=%s strata=%zu bytes=%zu round_trip=PASS\n",
|
||||||
resolvedLayout.schemaVersion,resolvedLayout.stableIdentity.c_str(),resolvedLayout.strates.size(),layoutJson.size());
|
resolvedLayout.schemaVersion,resolvedLayout.stableIdentity.c_str(),resolvedLayout.strates.size(),layoutJson.size());
|
||||||
|
const auto requestedRecipe=options.FloatingIslandsRecipe
|
||||||
|
?std::string_view("floating-islands"):std::string_view("surface-world");
|
||||||
|
const auto topStratum=std::max_element(resolvedLayout.strates.begin(),resolvedLayout.strates.end(),
|
||||||
|
[](const auto& a,const auto& b)
|
||||||
|
{return a.interiorTopMicrometres<b.interiorTopMicrometres;});
|
||||||
|
const bool hasFloatingIslands=std::any_of(resolvedLayout.strates.begin(),resolvedLayout.strates.end(),
|
||||||
|
[](const auto& stratum){return stratum.generatorKind=="FloatingIslands";});
|
||||||
|
const bool hasSurfaceWorldPlan=std::any_of(resolvedLayout.strates.begin(),resolvedLayout.strates.end(),
|
||||||
|
[&](const auto& stratum)
|
||||||
|
{
|
||||||
|
return stratum.generatorKind=="SurfaceWorld"
|
||||||
|
&&stratum.recipeId=="SurfaceWorld.Heightfield2D"
|
||||||
|
&&std::any_of(worldAssembly.plan.strates.begin(),worldAssembly.plan.strates.end(),
|
||||||
|
[&](const auto& plan){return plan.strateIndex==stratum.strateIndex;});
|
||||||
|
});
|
||||||
|
const auto actualRecipe=hasFloatingIslands?std::string_view("floating-islands")
|
||||||
|
:hasSurfaceWorldPlan?std::string_view("surface-world")
|
||||||
|
:std::string_view("missing");
|
||||||
|
const char* topGenerator=topStratum==resolvedLayout.strates.end()
|
||||||
|
?"missing":topStratum->generatorKind.c_str();
|
||||||
|
std::printf("[WorldAssemblerRecipeAudit] requested=%.*s actual=%.*s top_generator=%s surface_world_plan=%s floating_islands_stratum=%s status=%s\n",
|
||||||
|
static_cast<int>(requestedRecipe.size()),requestedRecipe.data(),
|
||||||
|
static_cast<int>(actualRecipe.size()),actualRecipe.data(),
|
||||||
|
topGenerator,hasSurfaceWorldPlan?"present":"absent",hasFloatingIslands?"present":"absent",
|
||||||
|
actualRecipe==requestedRecipe?"PASS":"FAIL");
|
||||||
|
if(actualRecipe!=requestedRecipe)
|
||||||
|
{std::fprintf(stderr,"Requested recipe %.*s resolved to %.*s\n",
|
||||||
|
static_cast<int>(requestedRecipe.size()),requestedRecipe.data(),
|
||||||
|
static_cast<int>(actualRecipe.size()),actualRecipe.data());return 2;}
|
||||||
std::printf("[ResolvedLayoutCompatibility] accepted=%s issues=%zu error=%s\n",
|
std::printf("[ResolvedLayoutCompatibility] accepted=%s issues=%zu error=%s\n",
|
||||||
compatibilityAccepted?"true":"false",compatibilityIssues.size(),compatibilityError.c_str());
|
compatibilityAccepted?"true":"false",compatibilityIssues.size(),compatibilityError.c_str());
|
||||||
for(const auto& issue:compatibilityIssues)
|
for(const auto& issue:compatibilityIssues)
|
||||||
|
|||||||
@@ -1084,6 +1084,8 @@ namespace StandaloneStreamBenchmark
|
|||||||
double surfaceRadiusReliefMetres=0.0;
|
double surfaceRadiusReliefMetres=0.0;
|
||||||
double longestShortSpanMetres=0.0;
|
double longestShortSpanMetres=0.0;
|
||||||
bool passed=true;
|
bool passed=true;
|
||||||
|
bool disconnectedEndControlApplicable=false;
|
||||||
|
bool disconnectedEndControlDetected=false;
|
||||||
};
|
};
|
||||||
const auto auditPassageColumns=[&](const VoxelForgeCore::World& Field,
|
const auto auditPassageColumns=[&](const VoxelForgeCore::World& Field,
|
||||||
const VoxelForgeCore::World& Native,
|
const VoxelForgeCore::World& Native,
|
||||||
@@ -1095,7 +1097,7 @@ namespace StandaloneStreamBenchmark
|
|||||||
/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||||
constexpr std::array<std::array<double,2>,5> supportOffsets{{
|
constexpr std::array<std::array<double,2>,5> supportOffsets{{
|
||||||
{{0.0,0.0}},{{0.45,0.0}},{{-0.45,0.0}},{{0.0,0.45}},{{0.0,-0.45}}}};
|
{{0.0,0.0}},{{0.45,0.0}},{{-0.45,0.0}},{{0.0,0.45}},{{0.0,-0.45}}}};
|
||||||
const auto endpointConnected=[&](const std::array<double,4>& point)
|
const auto endpointSupportCount=[&](const std::array<double,4>& point)
|
||||||
{
|
{
|
||||||
const double x=point[0]*qMetres,y=point[1]*qMetres;
|
const double x=point[0]*qMetres,y=point[1]*qMetres;
|
||||||
const double floor=point[2]*qMetres-point[3]*qMetres+0.13;
|
const double floor=point[2]*qMetres-point[3]*qMetres+0.13;
|
||||||
@@ -1109,7 +1111,58 @@ namespace StandaloneStreamBenchmark
|
|||||||
{solid=true;break;}
|
{solid=true;break;}
|
||||||
supported+=solid;
|
supported+=solid;
|
||||||
}
|
}
|
||||||
return supported>=3;
|
return supported;
|
||||||
|
};
|
||||||
|
const auto endpointConnected=[&](const std::array<double,4>& point)
|
||||||
|
{return endpointSupportCount(point)>=3;};
|
||||||
|
struct FColumnCapProbe
|
||||||
|
{
|
||||||
|
std::uint64_t columnSamples=0,nativeOverlapSamples=0;
|
||||||
|
double firstOverlapDepthMetres=std::numeric_limits<double>::infinity();
|
||||||
|
double minimumNativeDensity=std::numeric_limits<double>::infinity();
|
||||||
|
};
|
||||||
|
const auto probeColumnCap=[&](const VoxelForgeCore::PassageVoidCrossingAuditV1& crossing,
|
||||||
|
std::size_t segment,double capZMetres,bool upperCap,double depthLimitMetres)
|
||||||
|
{
|
||||||
|
FColumnCapProbe probe;
|
||||||
|
constexpr double DepthStepMetres=0.25;
|
||||||
|
constexpr double RadialStepMetres=0.50;
|
||||||
|
constexpr std::size_t DirectionCount=16;
|
||||||
|
const double axisX=qToMetres(crossing.columnAxisSubvoxels[0]);
|
||||||
|
const double axisY=qToMetres(crossing.columnAxisSubvoxels[1]);
|
||||||
|
for(double depth=DepthStepMetres;depth<=depthLimitMetres+1.0e-6;
|
||||||
|
depth+=DepthStepMetres)
|
||||||
|
{
|
||||||
|
const double z=capZMetres+(upperCap?-depth:depth);
|
||||||
|
const double maxRadius=qToMetres(crossing.columnMaximumRadiusSubvoxels);
|
||||||
|
const std::size_t radialSteps=static_cast<std::size_t>(
|
||||||
|
std::ceil(maxRadius/RadialStepMetres));
|
||||||
|
for(std::size_t radialStep=0;radialStep<=radialSteps;++radialStep)
|
||||||
|
{
|
||||||
|
const double radius=std::min(maxRadius,
|
||||||
|
radialStep*RadialStepMetres);
|
||||||
|
for(std::size_t direction=0;direction<DirectionCount;++direction)
|
||||||
|
{
|
||||||
|
const double angle=2.0*3.14159265358979323846
|
||||||
|
*direction/DirectionCount;
|
||||||
|
const VoxelForgeCore::Vec3d sample{
|
||||||
|
(axisX+radius*std::cos(angle))/coreMetres,
|
||||||
|
(axisY+radius*std::sin(angle))/coreMetres,z/coreMetres};
|
||||||
|
if(!Field.isPassageColumnSample(passageIndex,segment,sample))continue;
|
||||||
|
++probe.columnSamples;
|
||||||
|
const double nativeDensity=Native.densityAt(sample);
|
||||||
|
probe.minimumNativeDensity=std::min(probe.minimumNativeDensity,
|
||||||
|
nativeDensity);
|
||||||
|
if(nativeDensity<0.0)
|
||||||
|
{
|
||||||
|
++probe.nativeOverlapSamples;
|
||||||
|
probe.firstOverlapDepthMetres=std::min(
|
||||||
|
probe.firstOverlapDepthMetres,depth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return probe;
|
||||||
};
|
};
|
||||||
const auto columnSolidAt=[&](std::size_t segment,double x,double y,double z)
|
const auto columnSolidAt=[&](std::size_t segment,double x,double y,double z)
|
||||||
{
|
{
|
||||||
@@ -1122,6 +1175,9 @@ namespace StandaloneStreamBenchmark
|
|||||||
{x/coreMetres,y/coreMetres,z/coreMetres});
|
{x/coreMetres,y/coreMetres,z/coreMetres});
|
||||||
};
|
};
|
||||||
const auto crossings=Field.passageVoidCrossings();
|
const auto crossings=Field.passageVoidCrossings();
|
||||||
|
double controlLowestRouteZ=std::numeric_limits<double>::infinity();
|
||||||
|
bool disconnectedEndControlApplicable=false;
|
||||||
|
bool disconnectedEndControlDetected=false;
|
||||||
for(const auto& crossing:crossings)
|
for(const auto& crossing:crossings)
|
||||||
{
|
{
|
||||||
if(crossing.passageIndex!=passageIndex)continue;
|
if(crossing.passageIndex!=passageIndex)continue;
|
||||||
@@ -1157,8 +1213,63 @@ namespace StandaloneStreamBenchmark
|
|||||||
}
|
}
|
||||||
if(crossing.lastSegment+1>=passage.controlPoints.size())
|
if(crossing.lastSegment+1>=passage.controlPoints.size())
|
||||||
{audit.passed=false;continue;}
|
{audit.passed=false;continue;}
|
||||||
|
const double capTopMetres=qToMetres(crossing.columnTopZSubvoxels);
|
||||||
|
const double capBottomMetres=qToMetres(crossing.columnBottomZSubvoxels);
|
||||||
|
const auto topCap=probeColumnCap(crossing,crossing.firstSegment,
|
||||||
|
capTopMetres,true,6.0);
|
||||||
|
const auto bottomCap=probeColumnCap(crossing,crossing.lastSegment,
|
||||||
|
capBottomMetres,false,6.0);
|
||||||
|
const bool topCapConnected=topCap.nativeOverlapSamples>0;
|
||||||
|
const bool bottomCapConnected=bottomCap.nativeOverlapSamples>0;
|
||||||
|
const std::uint32_t routeStartSupport=endpointSupportCount(
|
||||||
|
crossing.startPointSubvoxels);
|
||||||
|
const std::uint32_t routeEndSupport=endpointSupportCount(
|
||||||
|
crossing.endPointSubvoxels);
|
||||||
|
std::printf("[PassageColumnRouteEndpointSupport] id=%s start=%u/5 start_z_m=%.3f end=%u/5 end_z_m=%.3f threshold=3/5\n",
|
||||||
|
passage.stableId.c_str(),routeStartSupport,
|
||||||
|
qToMetres(crossing.startPointSubvoxels[2]),routeEndSupport,
|
||||||
|
qToMetres(crossing.endPointSubvoxels[2]));
|
||||||
|
const auto printCap=[&](const char* endName,const char* side,double capZ,
|
||||||
|
const FColumnCapProbe& probe,bool connected)
|
||||||
|
{
|
||||||
|
std::printf("[PassageColumnCapAudit] id=%s end=%s side=%s cap_z_m=%.3f column_samples=%llu native_overlap_samples=%llu first_overlap_depth_m=%.3f minimum_native_density=%.6f search_depth_m=6.000 status=%s\n",
|
||||||
|
passage.stableId.c_str(),endName,side,capZ,
|
||||||
|
static_cast<unsigned long long>(probe.columnSamples),
|
||||||
|
static_cast<unsigned long long>(probe.nativeOverlapSamples),
|
||||||
|
probe.firstOverlapDepthMetres,probe.minimumNativeDensity,
|
||||||
|
connected?"PASS":"FAIL");
|
||||||
|
};
|
||||||
|
printCap("top","upper",capTopMetres,topCap,topCapConnected);
|
||||||
|
printCap("bottom","lower",capBottomMetres,bottomCap,bottomCapConnected);
|
||||||
|
const auto upper=std::find_if(ResolvedLayout->strates.begin(),
|
||||||
|
ResolvedLayout->strates.end(),[&](const auto& value)
|
||||||
|
{return value.strateIndex==passage.upperStrateIndex;});
|
||||||
|
const double startRouteZ=qToMetres(crossing.startPointSubvoxels[2]);
|
||||||
|
const double endRouteZ=qToMetres(crossing.endPointSubvoxels[2]);
|
||||||
|
const double lowerRouteZ=std::min(startRouteZ,endRouteZ);
|
||||||
|
if(upper!=ResolvedLayout->strates.end()
|
||||||
|
&&upper->generatorKind=="FloatingIslands"
|
||||||
|
&&lowerRouteZ<controlLowestRouteZ)
|
||||||
|
{
|
||||||
|
controlLowestRouteZ=lowerRouteZ;
|
||||||
|
// Move the hypothetical lower cap 20 m upward into the FI air gap.
|
||||||
|
// Its first 2 m are still inside the original column bounds, but a
|
||||||
|
// truncated column there has no native substrate to join.
|
||||||
|
const double hypotheticalBottomZ=lowerRouteZ+20.0;
|
||||||
|
const auto disconnectedControl=probeColumnCap(crossing,
|
||||||
|
crossing.lastSegment,hypotheticalBottomZ,false,2.0);
|
||||||
|
disconnectedEndControlApplicable=disconnectedControl.columnSamples>0;
|
||||||
|
disconnectedEndControlDetected=disconnectedEndControlApplicable
|
||||||
|
&&disconnectedControl.nativeOverlapSamples==0;
|
||||||
|
std::printf("[PassageColumnDisconnectedEndControl] source=air_gap_truncated_lower_cap cap_z_m=%.3f column_samples=%llu native_overlap_samples=%llu minimum_native_density=%.6f detected=%s\n",
|
||||||
|
hypotheticalBottomZ,
|
||||||
|
static_cast<unsigned long long>(disconnectedControl.columnSamples),
|
||||||
|
static_cast<unsigned long long>(disconnectedControl.nativeOverlapSamples),
|
||||||
|
disconnectedControl.minimumNativeDensity,
|
||||||
|
disconnectedEndControlDetected?"true":"false");
|
||||||
|
}
|
||||||
++audit.columns;
|
++audit.columns;
|
||||||
audit.connectedEnds+=connectedStart+connectedEnd;
|
audit.connectedEnds+=topCapConnected+bottomCapConnected;
|
||||||
const double axisX=qToMetres(crossing.columnAxisSubvoxels[0]);
|
const double axisX=qToMetres(crossing.columnAxisSubvoxels[0]);
|
||||||
const double axisY=qToMetres(crossing.columnAxisSubvoxels[1]);
|
const double axisY=qToMetres(crossing.columnAxisSubvoxels[1]);
|
||||||
const double minimumRadius=qToMetres(crossing.columnMinimumRadiusSubvoxels);
|
const double minimumRadius=qToMetres(crossing.columnMinimumRadiusSubvoxels);
|
||||||
@@ -1262,7 +1373,7 @@ namespace StandaloneStreamBenchmark
|
|||||||
crossingSurfaceRelief);
|
crossingSurfaceRelief);
|
||||||
const double measuredWall=crossingMinimumSurfaceRadius
|
const double measuredWall=crossingMinimumSurfaceRadius
|
||||||
-crossing.maximumRouteOffsetMetres-crossing.maximumBoreRadiusMetres;
|
-crossing.maximumRouteOffsetMetres-crossing.maximumBoreRadiusMetres;
|
||||||
const bool columnPass=connectedStart&&connectedEnd
|
const bool columnPass=topCapConnected&&bottomCapConnected
|
||||||
&&minimumWall>=1.50&&crossingRouteFailures==0
|
&&minimumWall>=1.50&&crossingRouteFailures==0
|
||||||
&&crossingWallFailures==0&&std::isfinite(measuredWall)
|
&&crossingWallFailures==0&&std::isfinite(measuredWall)
|
||||||
&&measuredWall>=1.50&&crossingSurfaceRelief>=0.20;
|
&&measuredWall>=1.50&&crossingSurfaceRelief>=0.20;
|
||||||
@@ -1270,13 +1381,17 @@ namespace StandaloneStreamBenchmark
|
|||||||
std::printf("[PassageRockColumnAudit] id=%s segments=%zu..%zu span_m=%.3f axis_m=(%.3f,%.3f) connected=%d/2 route_support=%llu/%llu wall_margin_m=%.3f threshold_m=1.500 radii_m=%.3f..%.3f relief_p2p_m=%.3f nominal_radius_m=%.3f status=%s\n",
|
std::printf("[PassageRockColumnAudit] id=%s segments=%zu..%zu span_m=%.3f axis_m=(%.3f,%.3f) connected=%d/2 route_support=%llu/%llu wall_margin_m=%.3f threshold_m=1.500 radii_m=%.3f..%.3f relief_p2p_m=%.3f nominal_radius_m=%.3f status=%s\n",
|
||||||
passage.stableId.c_str(),crossing.firstSegment,crossing.lastSegment,
|
passage.stableId.c_str(),crossing.firstSegment,crossing.lastSegment,
|
||||||
crossing.unsupportedLengthMetres,axisX,axisY,
|
crossing.unsupportedLengthMetres,axisX,axisY,
|
||||||
static_cast<int>(connectedStart)+static_cast<int>(connectedEnd),
|
static_cast<int>(topCapConnected)+static_cast<int>(bottomCapConnected),
|
||||||
static_cast<unsigned long long>(probeCount-crossingRouteFailures),
|
static_cast<unsigned long long>(probeCount-crossingRouteFailures),
|
||||||
static_cast<unsigned long long>(probeCount),measuredWall,
|
static_cast<unsigned long long>(probeCount),measuredWall,
|
||||||
crossingMinimumSurfaceRadius,crossingMaximumSurfaceRadius,
|
crossingMinimumSurfaceRadius,crossingMaximumSurfaceRadius,
|
||||||
crossingSurfaceRelief,qToMetres(crossing.columnNominalRadiusSubvoxels),
|
crossingSurfaceRelief,qToMetres(crossing.columnNominalRadiusSubvoxels),
|
||||||
columnPass?"PASS":"FAIL");
|
columnPass?"PASS":"FAIL");
|
||||||
}
|
}
|
||||||
|
audit.disconnectedEndControlApplicable|=disconnectedEndControlApplicable;
|
||||||
|
audit.disconnectedEndControlDetected|=disconnectedEndControlDetected;
|
||||||
|
if(disconnectedEndControlApplicable&&!disconnectedEndControlDetected)
|
||||||
|
audit.passed=false;
|
||||||
return audit;
|
return audit;
|
||||||
};
|
};
|
||||||
if(Assembly)
|
if(Assembly)
|
||||||
@@ -1384,6 +1499,8 @@ namespace StandaloneStreamBenchmark
|
|||||||
double longestUncolumnedSpanMetres=0.0,longestShortSpanMetres=0.0;
|
double longestUncolumnedSpanMetres=0.0,longestShortSpanMetres=0.0;
|
||||||
double l26LegacyBridgeSpanMetres=0.0;
|
double l26LegacyBridgeSpanMetres=0.0;
|
||||||
bool columnAuditsPassed=true,noLongBridgeAuditPassed=true;
|
bool columnAuditsPassed=true,noLongBridgeAuditPassed=true;
|
||||||
|
bool columnDisconnectedEndControlApplicable=false;
|
||||||
|
bool columnDisconnectedEndControlDetected=false;
|
||||||
bool l26NegativeControlApplicable=false;
|
bool l26NegativeControlApplicable=false;
|
||||||
const auto plannedVoidCrossings=Shape.passageVoidCrossings();
|
const auto plannedVoidCrossings=Shape.passageVoidCrossings();
|
||||||
for(const auto& crossing:plannedVoidCrossings)
|
for(const auto& crossing:plannedVoidCrossings)
|
||||||
@@ -1439,6 +1556,8 @@ namespace StandaloneStreamBenchmark
|
|||||||
longestShortSpanMetres=std::max(longestShortSpanMetres,
|
longestShortSpanMetres=std::max(longestShortSpanMetres,
|
||||||
columnAudit.longestShortSpanMetres);
|
columnAudit.longestShortSpanMetres);
|
||||||
columnAuditsPassed&=columnAudit.passed;
|
columnAuditsPassed&=columnAudit.passed;
|
||||||
|
columnDisconnectedEndControlApplicable|=columnAudit.disconnectedEndControlApplicable;
|
||||||
|
columnDisconnectedEndControlDetected|=columnAudit.disconnectedEndControlDetected;
|
||||||
++openAirAudits;
|
++openAirAudits;
|
||||||
const auto radialSquared=[](const std::array<std::int64_t,3>& p)
|
const auto radialSquared=[](const std::array<std::int64_t,3>& p)
|
||||||
{return p[0]*p[0]+p[1]*p[1];};
|
{return p[0]*p[0]+p[1]*p[1];};
|
||||||
@@ -1753,12 +1872,15 @@ namespace StandaloneStreamBenchmark
|
|||||||
longestShortSpanMetres,l26NegativeControlApplicable
|
longestShortSpanMetres,l26NegativeControlApplicable
|
||||||
?(l26NoLongBridgeControlDetected?"DETECTED":"MISSED"):"NOT_APPLICABLE",
|
?(l26NoLongBridgeControlDetected?"DETECTED":"MISSED"):"NOT_APPLICABLE",
|
||||||
noLongBridgeAuditPassed?"PASS":"FAIL");
|
noLongBridgeAuditPassed?"PASS":"FAIL");
|
||||||
std::printf("[PassageRockColumnAuditSummary] columns=%llu connected_ends=%llu/%llu short_spans=%llu wall_or_support_failures=%llu status=%s\n",
|
std::printf("[PassageRockColumnAuditSummary] columns=%llu connected_ends=%llu/%llu short_spans=%llu wall_or_support_failures=%llu disconnected_end_control=%s status=%s\n",
|
||||||
static_cast<unsigned long long>(columnCrossings),
|
static_cast<unsigned long long>(columnCrossings),
|
||||||
static_cast<unsigned long long>(columnConnectedEnds),
|
static_cast<unsigned long long>(columnConnectedEnds),
|
||||||
static_cast<unsigned long long>(columnCrossings*2),
|
static_cast<unsigned long long>(columnCrossings*2),
|
||||||
static_cast<unsigned long long>(shortGroundedSpans),
|
static_cast<unsigned long long>(shortGroundedSpans),
|
||||||
static_cast<unsigned long long>(columnRouteFailures+columnWallFailures),
|
static_cast<unsigned long long>(columnRouteFailures+columnWallFailures),
|
||||||
|
columnDisconnectedEndControlApplicable
|
||||||
|
?(columnDisconnectedEndControlDetected?"DETECTED":"MISSED")
|
||||||
|
:"NOT_APPLICABLE",
|
||||||
columnAuditsPassed?"PASS":"FAIL");
|
columnAuditsPassed?"PASS":"FAIL");
|
||||||
const bool l23SmoothWallDetected=!looksNatural(0.0,0.0);
|
const bool l23SmoothWallDetected=!looksNatural(0.0,0.0);
|
||||||
std::printf("[PassageNaturalnessNegativeControl] source=L23_smooth_unrelieved_wall relief_p2p_m=0.000 threshold_m=%.3f detected=%s\n",
|
std::printf("[PassageNaturalnessNegativeControl] source=L23_smooth_unrelieved_wall relief_p2p_m=0.000 threshold_m=%.3f detected=%s\n",
|
||||||
|
|||||||
Reference in new issue
Block a user