passages: swept tube evaluates the exact route on in-bound cache misses (L100, v216)
L99 treated a missing exact-grid sweep sample as "no route here", so off-grid queries exposed the FI/TN boundary band inside passage 1-2 (flat ceiling/floor). In-bound misses now evaluate the exact route; a conservative route-feature bound still skips out-of-extent misses. Composition order unchanged (graph -> bands -> passage -> stamps -> floor caps). Comparison now counts added/removed crossings and triangles: 0/0 crossings on all four strates (FI +56 triangles). 1-2 mid-tunnel: L98->L100 changes 32k px (L99 changed 1.14M). Near-ready @W12 (L98/L99/L100): FI 2.62/1.78/2.05, TN 2.36/1.55/1.80, SW 1.17/0.95/1.04, VS 1.16/1.03/1.08 s. All walk audits pass; 24/24 Clang/GCC W1/W8/W12 determinism; fast tier PASS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
No files matched your search
@@ -0,0 +1,64 @@
|
||||
# L100 — swept-tube boundary regression
|
||||
|
||||
Branch: `wip/l99-swept-tube` (`fa6363b`, unchanged HEAD). L100 source stamp: `7224bf6e668d06ed71a92f0857820e3ae2b0eca40574a491331ab5979a88f698`.
|
||||
|
||||
## Cause and fix
|
||||
|
||||
**Measured:** the 1–2 midpoint query at `(-8.5, 12.5, -741.989990 m)` has band density `−0.499936938`; stamps and caverns are absent, and the cap leaves that value unchanged. The direct route query finds segment 8 at SDF `−1.83728031 m`, with reach, floor, and combined weights all 1, and returns `+7.35348845`. The bounded sweep leaves `−0.499936938` at this point. At the nearby exact `z=−742 m` lattice point both paths carve; at `z=−741.989990 m` the sweep has no exact cache entry. The strate field itself is `−1032.04004` and is masked by the boundary band. This identifies the band as the flat sheet; the 0.5 m blend, stamps, and caps are not the cause.
|
||||
|
||||
**Code-path conclusion:** L99 treated a missing exact-grid sweep sample as proof that no route affected the query and skipped route evaluation. Off-grid seam/field queries therefore exposed the band inside the tube. L100 evaluates the exact route on an in-bound cache miss. A conservative route-feature bound still skips misses outside candidate extents; mouth and column supports keep their separate paths. Composition stays `graph → bands → passage → stamps → floor caps`, so the tunnel cuts bands while authored stamps and landing caps retain precedence. This adds the missing ownership without restoring the old full route scan.
|
||||
|
||||
## Comparison metric
|
||||
|
||||
The comparison now counts added and removed edge crossings per tile, alongside moved shared crossings, and reports added/removed non-skirt triangles. The old mean/max displacement summarized only crossings shared by both meshes; a missing or new crossing had no displacement to average. `changed_mesh` could flag the tile but did not say whether surface appeared or disappeared.
|
||||
|
||||
Final direct-route versus bounded-sweep audit (LOD 0–2 near sets):
|
||||
|
||||
| Strate | Tiles | Added / removed crossings | Added / removed triangles | Moved crossings | Mean / max displacement (cm) | Pairing failures | Audit |
|
||||
|---|---:|---:|---:|---:|---:|---:|---|
|
||||
| SW | 1,408/1,408 | 0 / 0 | 0 / 0 | 9,214 | 0.0180 / 20.1114 | 5 | exit 14 |
|
||||
| FI | 1,216/1,216 | 0 / 0 | 56 / 0 | 10,238 | 0.0185 / 19.7997 | 37 | exit 14 |
|
||||
| TN | 1,344/1,344 | 0 / 0 | 0 / 0 | 11,370 | 0.0387 / 21.7442 | 41 | exit 14 |
|
||||
| VS | 1,344/1,344 | 0 / 0 | 0 / 0 | 5,087 | 0.0178 / 25.8798 | 4 | exit 14 |
|
||||
|
||||
All four comparison commands hit the legacy strict displacement/normal acceptance gate (1 cm maximum displacement, 2° maximum normal change, and no normal-pairing failures); they completed and found no added/removed crossing edges. Exit 14 is that diagnostic result, not a build or timeout failure. FI has 56 net added triangles.
|
||||
|
||||
## Readiness and timing
|
||||
|
||||
Fast tier: **PASS**, 274 blocking checks across 80 runs, including all resolved-passage and adjacent-interface walkability audits. Evaluator version 216 hashes are re-pinned. Determinism: 24/24 Clang/GCC runs at W1/W8/W12 and 20/20 cross-worker/compiler comparisons match exactly for all four strates.
|
||||
|
||||
L80 timings are medians of three measured repetitions in rotating L98/L99/L100 order, after a warmup for each version, stratum, and worker count. Values are seconds (`L98 / L99 / L100`). All 72 measured invocations completed; each L100 run reported source stamp `7224bf6e…`.
|
||||
|
||||
| Workers | Strate | Near ready, LOD 0–2 | Whole view |
|
||||
|---:|---|---:|---:|
|
||||
| 8 | SW | 1.364 / 0.940 / 1.425 | 3.035 / 2.413 / 3.429 |
|
||||
| 8 | FI | 2.792 / 1.719 / 2.157 | 4.385 / 2.931 / 3.409 |
|
||||
| 8 | TN | 2.746 / 1.723 / 2.175 | 4.022 / 2.790 / 3.294 |
|
||||
| 8 | VS | 1.631 / 1.352 / 1.522 | 2.779 / 2.553 / 2.745 |
|
||||
| 12 | SW | 1.174 / 0.949 / 1.039 | 2.509 / 2.148 / 2.280 |
|
||||
| 12 | FI | 2.617 / 1.783 / 2.054 | 3.919 / 2.902 / 3.197 |
|
||||
| 12 | TN | 2.358 / 1.547 / 1.802 | 3.458 / 2.539 / 2.813 |
|
||||
| 12 | VS | 1.158 / 1.032 / 1.083 | 2.181 / 2.027 / 2.097 |
|
||||
|
||||
On FI/TN, L100 remains 18–24% faster than L98 for near-ready and whole-view medians. It is 10–26% slower than L99, reflecting the exact fallback needed for off-grid misses.
|
||||
|
||||
## Captures
|
||||
|
||||
[`captures/INDEX.md`](captures/INDEX.md) lists all 15 paired views: both mouths and 25/50/75 route stations for each passage. All 30 PNGs pass the programmatic geometry gate (over 3,000 non-dominant-background pixels and at least two RGB colors); the index reports each pair’s changed-pixel count. It also records the 1–2 same-camera three-version check: visible geometry is 1,407,415 px in L98, 1,206,362 in L99, and 1,407,415 in L100; changed pixels are 1,135,959 L98→L99 and 1,134,150 L99→L100. L98→L100 changes 32,217 px. The 0–1 mid-25 and 2–3 mid-25/mid-50 cameras use documented nearest-surface targets where the forward route view had no visible L98 surface.
|
||||
|
||||
## Failed runs
|
||||
|
||||
- The four density-comparison audits exit 14 on their legacy strict geometry gate; the new added/removed crossing counts are zero in all four.
|
||||
- An intermediate composition-order experiment put passage carving after floor caps and failed the landmark walk audit. Restoring the L99 stamp/cap precedence made the final fast tier green.
|
||||
- The first Unreal build attempt exited before compilation because UBT could not rotate its log under read-only `/home/jahni/.config`. The retry redirected UBT config and logging under `Saved/Linux/l100` and succeeded (27/27 actions; three RealtimeMesh deprecation warnings).
|
||||
|
||||
No benchmark or capture process timed out.
|
||||
|
||||
## Evidence files
|
||||
|
||||
- Source point trace: `Saved/Linux/l100/logs/column-sweep.log`, `column-direct.log`.
|
||||
- Density comparison: `Saved/Linux/l100/density-compare/summary.json`.
|
||||
- Hash matrix: `Saved/Linux/l100/hashes/summary.json`.
|
||||
- Fast tier: `Saved/Linux/l100/fast-summary.json`.
|
||||
- L80 triple: `Saved/Linux/l100/readiness/l98-l99-l100/summary.json`.
|
||||
- Build, Clang, GCC, captures, and per-run logs: `Saved/Linux/l100/logs/` and `Saved/Linux/l100/`.
|
||||
@@ -0,0 +1,602 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Capture matched L98/L100 passage mouth and mid-route views."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import struct
|
||||
import subprocess
|
||||
import time
|
||||
import zlib
|
||||
from array import array
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l100/captures"
|
||||
IMAGE_DIR = Path(__file__).resolve().parent / "captures"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
LAYOUT_PATH = ROOT / "Saved/Linux/l99/resolved-sw.json"
|
||||
BINARIES = {
|
||||
"before-l98": ROOT / "Saved/Linux/l98/bin/VoxelForgeCoreHeadless-l98-clang19",
|
||||
"after-l100": ROOT / "Saved/Linux/l100/bin/VoxelForgeCoreHeadless-l100-clang",
|
||||
}
|
||||
RENDERER = ROOT / "Saved/Linux/l100/bin/VoxelForgeOffscreenCapture-l100-clang"
|
||||
VOXEL_Q = 256.0
|
||||
CORE_UM_PER_VOXEL = 250_000.0
|
||||
ROUTE_POINTS = {}
|
||||
ROUTE_LENGTHS = {}
|
||||
|
||||
|
||||
def add(a, b):
|
||||
return tuple(x + y for x, y in zip(a, b))
|
||||
|
||||
|
||||
def sub(a, b):
|
||||
return tuple(x - y for x, y in zip(a, b))
|
||||
|
||||
|
||||
def mul(a, s):
|
||||
return tuple(x * s for x in a)
|
||||
|
||||
|
||||
def dot(a, b):
|
||||
return sum(x * y for x, y in zip(a, b))
|
||||
|
||||
|
||||
def norm(a):
|
||||
length = math.sqrt(dot(a, a))
|
||||
if length < 1.0e-8:
|
||||
raise ValueError("degenerate capture direction")
|
||||
return tuple(x / length for x in a)
|
||||
|
||||
|
||||
def fmt(values):
|
||||
return ",".join(f"{value:.9f}" for value in values)
|
||||
|
||||
|
||||
def angles(camera, target):
|
||||
delta = sub(target, camera)
|
||||
return (math.degrees(math.atan2(delta[0], delta[1])),
|
||||
math.degrees(math.atan2(delta[2], math.hypot(delta[0], delta[1]))))
|
||||
|
||||
|
||||
def interpolate(points, distance):
|
||||
remaining = max(0.0, distance)
|
||||
for a, b in zip(points, points[1:]):
|
||||
delta = sub(b[0], a[0])
|
||||
length = math.sqrt(dot(delta, delta))
|
||||
if length < 1.0e-8:
|
||||
continue
|
||||
if remaining <= length:
|
||||
t = remaining / length
|
||||
return (tuple(a[0][i] + delta[i] * t for i in range(3)),
|
||||
a[1] + (b[1] - a[1]) * t)
|
||||
remaining -= length
|
||||
return points[-1]
|
||||
|
||||
|
||||
def route_frame(points, distance):
|
||||
center, radius = interpolate(points, distance)
|
||||
before, _ = interpolate(points, max(0.0, distance - 0.25))
|
||||
after, _ = interpolate(points, min(sum(
|
||||
math.sqrt(dot(sub(b[0], a[0]), sub(b[0], a[0])))
|
||||
for a, b in zip(points, points[1:])), distance + 0.25))
|
||||
tangent = norm(sub(after, before))
|
||||
horizontal = norm((tangent[0], tangent[1], 0.0))
|
||||
side = (-horizontal[1], horizontal[0], 0.0)
|
||||
return center, radius, tangent, side
|
||||
|
||||
|
||||
def route_length(points):
|
||||
return sum(math.sqrt(dot(sub(b[0], a[0]), sub(b[0], a[0])))
|
||||
for a, b in zip(points, points[1:]))
|
||||
|
||||
|
||||
def nearest_route(points, point):
|
||||
best = None
|
||||
distance_along = 0.0
|
||||
for a, b in zip(points, points[1:]):
|
||||
delta = sub(b[0], a[0])
|
||||
length_sq = dot(delta, delta)
|
||||
if length_sq < 1.0e-10:
|
||||
continue
|
||||
segment_length = math.sqrt(length_sq)
|
||||
t = max(0.0, min(1.0, dot(sub(point, a[0]), delta) / length_sq))
|
||||
center = add(a[0], mul(delta, t))
|
||||
tangent = norm(delta)
|
||||
radial = sub(point, center)
|
||||
radial = sub(radial, mul(tangent, dot(radial, tangent)))
|
||||
distance = math.sqrt(dot(sub(point, center), sub(point, center)))
|
||||
if best is None or distance < best[0]:
|
||||
radius = a[1] + (b[1] - a[1]) * t
|
||||
best = (distance, center, tangent, radial, radius,
|
||||
distance_along + t * segment_length)
|
||||
distance_along += segment_length
|
||||
if best is None:
|
||||
raise ValueError("passage route has no non-degenerate segment")
|
||||
return best
|
||||
|
||||
|
||||
def run(name, command, headless):
|
||||
log = OUT / f"{name}.log"
|
||||
wrapped = ["taskset", "-c", "0-11"]
|
||||
if headless:
|
||||
wrapped += ["prlimit", "--as=6442450944", "--"]
|
||||
wrapped += ["timeout", "1740s", *map(str, command)]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(wrapped, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1745)
|
||||
row = {"name": name, "exit_code": result.returncode,
|
||||
"wall_s": round(time.monotonic() - started, 3),
|
||||
"log": str(log.relative_to(ROOT))}
|
||||
print("[L100CaptureRun] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"capture step failed: {name}; see {log}")
|
||||
return row
|
||||
|
||||
|
||||
def nearest_surface_anchor(mesh, point, tile_search_margin_m=20.0):
|
||||
"""Return the nearest surface vertex and outward mesh normal from a VFMSH scene."""
|
||||
best_distance_sq = float("inf")
|
||||
best_position = best_normal = None
|
||||
with mesh.open("rb") as stream:
|
||||
magic = stream.read(8)
|
||||
has_materials = magic == b"VFMSH002"
|
||||
if magic not in (b"VFMSH001", b"VFMSH002"):
|
||||
raise RuntimeError(f"unsupported mesh scene header: {mesh}")
|
||||
version, voxel_um, tile_count = struct.unpack("<3I", stream.read(12))
|
||||
if version != (2 if has_materials else 1):
|
||||
raise RuntimeError(f"mesh scene version mismatch: {mesh}")
|
||||
stream.seek(40, 1) # stored camera position, yaw, and pitch
|
||||
for _ in range(tile_count):
|
||||
key = struct.unpack("<4i", stream.read(16))
|
||||
surface_count, _, vertex_count, index_count, mask_count = struct.unpack(
|
||||
"<5I", stream.read(20))
|
||||
extent = (32 << key[3]) * voxel_um / 1_000_000.0
|
||||
origin = (key[0] * extent, key[1] * extent, key[2] * extent)
|
||||
if any(point[axis] < origin[axis] - tile_search_margin_m or
|
||||
point[axis] > origin[axis] + extent + tile_search_margin_m
|
||||
for axis in range(3)):
|
||||
stream.seek(vertex_count * 24 + (10 * vertex_count if has_materials else 0)
|
||||
+ index_count * 4 + mask_count, 1)
|
||||
continue
|
||||
positions = array("f")
|
||||
positions.frombytes(stream.read(vertex_count * 12))
|
||||
normals = array("f")
|
||||
normals.frombytes(stream.read(vertex_count * 12))
|
||||
if has_materials:
|
||||
stream.seek(vertex_count * 10, 1)
|
||||
stream.seek(index_count * 4 + mask_count, 1)
|
||||
if struct.pack("=I", 1) != struct.pack("<I", 1):
|
||||
positions.byteswap()
|
||||
normals.byteswap()
|
||||
for index in range(min(surface_count, vertex_count)):
|
||||
offset = index * 3
|
||||
# VFMSH stores vertex coordinates in world space; tile origin
|
||||
# is metadata for edge rendering and must not be added again.
|
||||
position = tuple(float(positions[offset + axis]) for axis in range(3))
|
||||
delta = sub(position, point)
|
||||
distance_sq = dot(delta, delta)
|
||||
if distance_sq >= best_distance_sq:
|
||||
continue
|
||||
raw_normal = tuple(float(normals[offset + axis]) for axis in range(3))
|
||||
if dot(raw_normal, raw_normal) < 1.0e-10:
|
||||
continue
|
||||
best_distance_sq = distance_sq
|
||||
best_position = position
|
||||
best_normal = norm(raw_normal)
|
||||
if best_position is None:
|
||||
raise RuntimeError(f"no nearby surface vertices in mesh scene: {mesh}")
|
||||
return {"position_m": best_position, "normal": best_normal,
|
||||
"distance_m": math.sqrt(best_distance_sq)}
|
||||
|
||||
|
||||
def make_passages(layout):
|
||||
q_to_m = layout["core_voxel_size_um"] / (VOXEL_Q * 1_000_000.0)
|
||||
passages = []
|
||||
all_routes = []
|
||||
for passage in layout["passages"]:
|
||||
controls = [(tuple(float(q) * q_to_m for q in p["position_q"]),
|
||||
float(p["radius_q"]) * q_to_m)
|
||||
for p in passage["control_points"]]
|
||||
upper = tuple(float(q) * q_to_m for q in passage["upper_mouth_q"])
|
||||
lower = tuple(float(q) * q_to_m for q in passage["lower_mouth_q"])
|
||||
ui = min(range(len(controls)), key=lambda i: dot(sub(controls[i][0], upper),
|
||||
sub(controls[i][0], upper)))
|
||||
li = min(range(len(controls)), key=lambda i: dot(sub(controls[i][0], lower),
|
||||
sub(controls[i][0], lower)))
|
||||
points = controls[ui:li + 1]
|
||||
if len(points) < 2:
|
||||
raise RuntimeError(f"invalid mouth path: {passage['stable_id']}")
|
||||
upper_offset = math.sqrt(dot(sub(points[0][0], upper), sub(points[0][0], upper)))
|
||||
lower_offset = math.sqrt(dot(sub(points[-1][0], lower), sub(points[-1][0], lower)))
|
||||
length = route_length(points)
|
||||
passage_views = []
|
||||
for endpoint, distance, camera_distance, outward_sign in (
|
||||
("upper-mouth", 0.0, min(length, 7.4), -1.0),
|
||||
("lower-mouth", length, max(0.0, length - 5.5), 1.0)):
|
||||
center, _, tangent, side = route_frame(points, distance)
|
||||
camera_center, _, _, camera_side = route_frame(points, camera_distance)
|
||||
camera = (camera_center[0] + camera_side[0] * 2.5,
|
||||
camera_center[1] + camera_side[1] * 2.5, camera_center[2])
|
||||
outward = mul(tangent, outward_sign)
|
||||
target = (center[0] + outward[0] * 8.0 + side[0] * 2.5,
|
||||
center[1] + outward[1] * 8.0 + side[1] * 2.5, center[2])
|
||||
yaw, pitch = angles(camera, target)
|
||||
passage_views.append({"name": f"{passage['stable_id']}-{endpoint}",
|
||||
"kind": "mouth", "focus": center, "camera": camera, "target": target,
|
||||
"yaw": yaw, "pitch": pitch, "clip": None, "fov": 80.0,
|
||||
"focus_radius": 20.0, "passage": passage["stable_id"],
|
||||
"camera_location": "passage-air"})
|
||||
for fraction in (0.25, 0.50, 0.75):
|
||||
distance = length * fraction
|
||||
center, radius, _, side = route_frame(points, distance)
|
||||
camera = (center[0] + side[0] * 0.75,
|
||||
center[1] + side[1] * 0.75,
|
||||
center[2] - radius + 1.6)
|
||||
target_pos, target_radius = interpolate(
|
||||
points, min(length, distance + 14.0))
|
||||
target = (target_pos[0] + side[0] * 2.35,
|
||||
target_pos[1] + side[1] * 2.35,
|
||||
target_pos[2] - target_radius + 1.8)
|
||||
yaw, pitch = angles(camera, target)
|
||||
pct = int(round(fraction * 100))
|
||||
passage_views.append({"name": f"{passage['stable_id']}-mid-{pct:02d}",
|
||||
"kind": "mid-tunnel", "route_fraction": fraction,
|
||||
"focus": center, "camera": camera, "target": target,
|
||||
"yaw": yaw, "pitch": pitch, "clip": None,
|
||||
"fov": 78.0, "focus_radius": 24.0,
|
||||
"passage": passage["stable_id"]})
|
||||
passages.append({"stable_id": passage["stable_id"], "route_length_m": length,
|
||||
"control_points": len(points), "upper_mouth_anchor_offset_m": upper_offset,
|
||||
"lower_mouth_anchor_offset_m": lower_offset, "views": passage_views})
|
||||
all_routes.append((passage["stable_id"], points))
|
||||
return passages, all_routes
|
||||
|
||||
|
||||
def make_hotspot_views(routes, reach_summary):
|
||||
views = []
|
||||
for series, rows, value_key, location_key, region_key in (
|
||||
("displacement", reach_summary["top_displacement_tiles"],
|
||||
"max_displacement_cm", "displacement_location_metres", "displacement_region"),
|
||||
("normal", reach_summary["top_normal_tiles"],
|
||||
"max_normal_degrees", "normal_location_metres", "normal_region"),
|
||||
):
|
||||
for index, row in enumerate(rows, 1):
|
||||
point = tuple(row[location_key])
|
||||
candidates = [(nearest_route(route, point), name, route)
|
||||
for name, route in routes]
|
||||
(distance, center, tangent, radial, radius, along), nearest_id, route = min(
|
||||
candidates, key=lambda item: item[0][0])
|
||||
if math.sqrt(dot(radial, radial)) < 0.25:
|
||||
horizontal = norm((tangent[0], tangent[1], 0.0))
|
||||
radial = (-horizontal[1], horizontal[0], 0.0)
|
||||
outward = norm(radial)
|
||||
camera = add(point, mul(outward, 20.0))
|
||||
target = point
|
||||
plane = None
|
||||
yaw, pitch = angles(camera, target)
|
||||
views.append({"name": f"top-{series}-{index:02d}", "kind": f"top-{series}",
|
||||
"focus": point, "camera": camera, "target": target,
|
||||
"yaw": yaw, "pitch": pitch, "clip": plane, "fov": 68.0,
|
||||
"focus_radius": 18.0, "strate": row["strate"],
|
||||
"value": row[value_key], "region": row[region_key],
|
||||
"camera_anchor": "nearest-surface-l98-v5", "nearest_passage": nearest_id,
|
||||
"route_distance_m": distance})
|
||||
return views
|
||||
|
||||
|
||||
def decode_png(path):
|
||||
"""Decode 8-bit RGB/RGBA PNG pixels using only the Python standard library."""
|
||||
data = path.read_bytes()
|
||||
if data[:8] != b"\x89PNG\r\n\x1a\n":
|
||||
raise RuntimeError(f"not a PNG: {path}")
|
||||
width = height = bit_depth = color_type = interlace = None
|
||||
compressed = bytearray()
|
||||
offset = 8
|
||||
while offset < len(data):
|
||||
length = struct.unpack_from(">I", data, offset)[0]
|
||||
kind = data[offset + 4:offset + 8]
|
||||
chunk = data[offset + 8:offset + 8 + length]
|
||||
if kind == b"IHDR":
|
||||
width, height, bit_depth, color_type, compression, filtering, interlace = \
|
||||
struct.unpack(">IIBBBBB", chunk)
|
||||
if compression or filtering or interlace:
|
||||
raise RuntimeError(f"unsupported PNG coding in {path}")
|
||||
elif kind == b"IDAT":
|
||||
compressed.extend(chunk)
|
||||
elif kind == b"IEND":
|
||||
break
|
||||
offset += 12 + length
|
||||
if bit_depth != 8 or color_type not in (2, 6):
|
||||
raise RuntimeError(f"expected 8-bit RGB/RGBA PNG: {path}")
|
||||
channels = 3 if color_type == 2 else 4
|
||||
stride = width * channels
|
||||
encoded = zlib.decompress(compressed)
|
||||
pixels = bytearray(height * stride)
|
||||
source_offset = 0
|
||||
for y in range(height):
|
||||
filter_type = encoded[source_offset]
|
||||
source_offset += 1
|
||||
row = bytearray(encoded[source_offset:source_offset + stride])
|
||||
source_offset += stride
|
||||
prior_offset = (y - 1) * stride
|
||||
for index in range(stride):
|
||||
left = row[index - channels] if index >= channels else 0
|
||||
above = pixels[prior_offset + index] if y else 0
|
||||
upper_left = pixels[prior_offset + index - channels] \
|
||||
if y and index >= channels else 0
|
||||
if filter_type == 1:
|
||||
row[index] = (row[index] + left) & 255
|
||||
elif filter_type == 2:
|
||||
row[index] = (row[index] + above) & 255
|
||||
elif filter_type == 3:
|
||||
row[index] = (row[index] + ((left + above) >> 1)) & 255
|
||||
elif filter_type == 4:
|
||||
estimate = left + above - upper_left
|
||||
left_distance = abs(estimate - left)
|
||||
above_distance = abs(estimate - above)
|
||||
upper_left_distance = abs(estimate - upper_left)
|
||||
if left_distance <= above_distance and left_distance <= upper_left_distance:
|
||||
predictor = left
|
||||
elif above_distance <= upper_left_distance:
|
||||
predictor = above
|
||||
else:
|
||||
predictor = upper_left
|
||||
row[index] = (row[index] + predictor) & 255
|
||||
elif filter_type != 0:
|
||||
raise RuntimeError(f"unsupported PNG filter {filter_type}: {path}")
|
||||
start = y * stride
|
||||
pixels[start:start + stride] = row
|
||||
if channels == 3:
|
||||
return width, height, pixels
|
||||
rgb = bytearray(width * height * 3)
|
||||
rgb[0::3] = pixels[0::4]
|
||||
rgb[1::3] = pixels[1::4]
|
||||
rgb[2::3] = pixels[2::4]
|
||||
return width, height, rgb
|
||||
|
||||
|
||||
def image_geometry_metrics(path):
|
||||
width, height, rgb = decode_png(path)
|
||||
color_counts = {}
|
||||
for offset in range(0, len(rgb), 3):
|
||||
color = (rgb[offset], rgb[offset + 1], rgb[offset + 2])
|
||||
color_counts[color] = color_counts.get(color, 0) + 1
|
||||
total = width * height
|
||||
visible = total - max(color_counts.values())
|
||||
return {"width": width, "height": height, "unique_rgb": len(color_counts),
|
||||
"visible_geometry_pixels": visible,
|
||||
"visible_geometry_fraction": visible / total}
|
||||
|
||||
|
||||
def changed_pixel_count(before, after):
|
||||
before_width, before_height, before_rgb = decode_png(before)
|
||||
after_width, after_height, after_rgb = decode_png(after)
|
||||
if (before_width, before_height) != (after_width, after_height):
|
||||
raise RuntimeError(f"capture dimensions differ: {before} / {after}")
|
||||
return sum(before_rgb[offset] != after_rgb[offset]
|
||||
or before_rgb[offset + 1] != after_rgb[offset + 1]
|
||||
or before_rgb[offset + 2] != after_rgb[offset + 2]
|
||||
for offset in range(0, len(before_rgb), 3))
|
||||
|
||||
|
||||
def mesh_triangle_count(mesh):
|
||||
with mesh.open("rb") as stream:
|
||||
magic = stream.read(8)
|
||||
has_materials = magic == b"VFMSH002"
|
||||
if magic not in (b"VFMSH001", b"VFMSH002"):
|
||||
raise RuntimeError(f"unsupported mesh scene header: {mesh}")
|
||||
version, _, tile_count = struct.unpack("<3I", stream.read(12))
|
||||
if version != (2 if has_materials else 1):
|
||||
raise RuntimeError(f"mesh scene version mismatch: {mesh}")
|
||||
stream.seek(40, 1)
|
||||
triangles = 0
|
||||
for _ in range(tile_count):
|
||||
stream.seek(16, 1)
|
||||
_, _, vertex_count, index_count, mask_count = struct.unpack(
|
||||
"<5I", stream.read(20))
|
||||
triangles += index_count // 3
|
||||
stream.seek(vertex_count * 24 + (10 * vertex_count if has_materials else 0)
|
||||
+ index_count * 4 + mask_count, 1)
|
||||
return triangles
|
||||
|
||||
|
||||
def set_route_station(view, distance):
|
||||
points = ROUTE_POINTS[view["passage"]]
|
||||
length = ROUTE_LENGTHS[view["passage"]]
|
||||
center, radius, _, side = route_frame(points, distance)
|
||||
camera = (center[0] + side[0] * 0.75,
|
||||
center[1] + side[1] * 0.75,
|
||||
center[2] - radius + 1.6)
|
||||
target_pos, target_radius = interpolate(points, min(length, distance + 14.0))
|
||||
target = (target_pos[0] + side[0] * 2.35,
|
||||
target_pos[1] + side[1] * 2.35,
|
||||
target_pos[2] - target_radius + 1.8)
|
||||
yaw, pitch = angles(camera, target)
|
||||
if "route_fraction_requested" not in view:
|
||||
view["route_fraction_requested"] = view["route_fraction"]
|
||||
view["route_requested_focus"] = view["focus"]
|
||||
view["route_fraction"] = distance / length
|
||||
view["route_station_adjustment_m"] = distance - length * view["route_fraction_requested"]
|
||||
view["focus"] = center
|
||||
view["camera"] = camera
|
||||
view["target"] = target
|
||||
view["yaw"], view["pitch"] = yaw, pitch
|
||||
view["clip"] = None
|
||||
|
||||
|
||||
def render_command(view, mesh, image, camera, yaw, pitch):
|
||||
args = [RENDERER, mesh, image,
|
||||
"--size=1600x900", f"--fov-deg={view['fov']:.1f}",
|
||||
f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}",
|
||||
f"--pitch-deg={pitch:.6f}", "--shade=readable", "--headlamp=on",
|
||||
"--height-tint=on", "--tile-edges=off", "--tile-skirts=off",
|
||||
f"--height-center-m={view['focus'][2]:.6f}", "--height-range-m=10",
|
||||
f"--focus-region={fmt((*view['focus'], view['focus_radius']))}"]
|
||||
if view["clip"] is not None:
|
||||
args.append(f"--clip-plane={fmt(view['clip'])}")
|
||||
return args
|
||||
|
||||
|
||||
def export_scene(view, binary, mesh, stem):
|
||||
camera = view["camera"]
|
||||
yaw, pitch = angles(camera, view["target"])
|
||||
export_args = [binary, DATA, "--world-assembler-prototype", "--stream-benchmark",
|
||||
"--stream-pipeline", "--workers=8", "--force-generate", "--clip-radius=3",
|
||||
"--max-clip-level=0", "--max-level=0", "--render-distance-chunks=0",
|
||||
f"--camera={fmt((*camera, yaw, pitch))}", f"--mesh-export={mesh}"]
|
||||
row = run(f"{stem}-export", export_args, headless=True)
|
||||
row["scene_bytes"] = mesh.stat().st_size if mesh.exists() else 0
|
||||
row["scene_triangles"] = mesh_triangle_count(mesh) if row["scene_bytes"] else 0
|
||||
return row
|
||||
|
||||
|
||||
def export_and_render(view, label, binary):
|
||||
safe = view["name"].replace("/", "-")
|
||||
stem = f"{safe}-{label}"
|
||||
mesh = OUT / f"{stem}.vfmesh"
|
||||
image = IMAGE_DIR / f"{safe}-{label}.png"
|
||||
camera = view["camera"]
|
||||
yaw, pitch = angles(camera, view["target"])
|
||||
export_row = export_scene(view, binary, mesh, stem)
|
||||
if export_row["scene_bytes"] <= 0:
|
||||
raise RuntimeError(f"empty mesh scene: {mesh}")
|
||||
if export_row["scene_triangles"] == 0:
|
||||
if label != "before-l98" or view.get("kind") != "mid-tunnel":
|
||||
raise RuntimeError(f"route station exports no surface triangles: {mesh}")
|
||||
points = ROUTE_POINTS[view["passage"]]
|
||||
length = ROUTE_LENGTHS[view["passage"]]
|
||||
requested_fraction = view["route_fraction"]
|
||||
requested_distance = length * requested_fraction
|
||||
found_surface = False
|
||||
for offset in (5.0, -5.0, 10.0, -10.0, 20.0, -20.0, 35.0, -35.0,
|
||||
50.0, -50.0, 75.0, -75.0, 100.0, -100.0, 150.0,
|
||||
-150.0, 200.0, -200.0):
|
||||
distance = max(0.0, min(length, requested_distance + offset))
|
||||
if distance == requested_distance:
|
||||
continue
|
||||
set_route_station(view, distance)
|
||||
adjusted_stem = f"{stem}-station-{view['route_fraction']:.5f}"
|
||||
export_row = export_scene(view, binary, mesh, adjusted_stem)
|
||||
if export_row["scene_triangles"] > 0:
|
||||
view["camera_anchor"] = "route-station-shifted-to-nearby-geometry"
|
||||
found_surface = True
|
||||
break
|
||||
if not found_surface:
|
||||
raise RuntimeError(f"no geometry along nearby route stations: {mesh}")
|
||||
camera = view["camera"]
|
||||
yaw, pitch = angles(camera, view["target"])
|
||||
if label == "before-l98" and view.get("camera_anchor") == "nearest-surface-l98-v5":
|
||||
anchor = nearest_surface_anchor(mesh, view["focus"], tile_search_margin_m=256.0)
|
||||
camera = add(anchor["position_m"], mul(anchor["normal"], 8.0))
|
||||
view["camera"] = camera
|
||||
view["target"] = anchor["position_m"]
|
||||
view["yaw"], view["pitch"] = angles(camera, view["target"])
|
||||
view["clip"] = None
|
||||
view["surface_anchor"] = anchor
|
||||
yaw, pitch = view["yaw"], view["pitch"]
|
||||
render_args = render_command(view, mesh, image, camera, yaw, pitch)
|
||||
try:
|
||||
render_row = run(f"{stem}-render", render_args, headless=True)
|
||||
except RuntimeError:
|
||||
if view["clip"] is None:
|
||||
raise
|
||||
retry_args = [arg for arg in render_args
|
||||
if not (isinstance(arg, str) and arg.startswith("--clip-plane="))]
|
||||
render_row = run(f"{stem}-render-no-clip-retry", retry_args, headless=True)
|
||||
render_row["clip_fallback"] = True
|
||||
render_row["image"] = str(image.relative_to(ROOT))
|
||||
render_row["image_bytes"] = image.stat().st_size if image.exists() else 0
|
||||
if render_row["image_bytes"] <= 0:
|
||||
raise RuntimeError(f"empty image: {image}")
|
||||
geometry = image_geometry_metrics(image)
|
||||
if label == "before-l98" and (
|
||||
geometry["visible_geometry_pixels"] < 3000 or geometry["unique_rgb"] < 2):
|
||||
# At a few deep route stations the default forward view looks into empty
|
||||
# sky. Keep the camera on the route station and aim at the nearest L98
|
||||
# surface in the exported scene; retain that matched target for L100.
|
||||
anchor = nearest_surface_anchor(mesh, view["focus"], tile_search_margin_m=256.0)
|
||||
if anchor["distance_m"] > 60.0:
|
||||
raise RuntimeError(f"no nearby surface for fallback camera: {image} {anchor}")
|
||||
camera = view["camera"]
|
||||
target = anchor["position_m"]
|
||||
yaw, pitch = angles(camera, target)
|
||||
view["target"] = target
|
||||
view["yaw"], view["pitch"] = yaw, pitch
|
||||
view["clip"] = None
|
||||
view["camera_anchor"] = "route-station-nearest-surface-target"
|
||||
view["surface_anchor"] = anchor
|
||||
retry_args = render_command(view, mesh, image, camera, yaw, pitch)
|
||||
render_row = run(f"{stem}-render-surface-fallback", retry_args, headless=True)
|
||||
render_row["image"] = str(image.relative_to(ROOT))
|
||||
render_row["image_bytes"] = image.stat().st_size if image.exists() else 0
|
||||
geometry = image_geometry_metrics(image)
|
||||
render_row["geometry"] = geometry
|
||||
if geometry["visible_geometry_pixels"] < 3000 or geometry["unique_rgb"] < 2:
|
||||
raise RuntimeError(f"camera sees sky only or too little geometry: {image} {geometry}")
|
||||
mesh.unlink()
|
||||
return export_row, render_row
|
||||
|
||||
|
||||
def main():
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
IMAGE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
for path in (*BINARIES.values(), RENDERER, DATA, LAYOUT_PATH):
|
||||
if not path.is_file():
|
||||
raise SystemExit(f"missing input: {path}")
|
||||
layout = json.loads(LAYOUT_PATH.read_text(encoding="utf-8"))
|
||||
passages, routes = make_passages(layout)
|
||||
ROUTE_POINTS.clear()
|
||||
ROUTE_LENGTHS.clear()
|
||||
for stable_id, points in routes:
|
||||
ROUTE_POINTS[stable_id] = points
|
||||
ROUTE_LENGTHS[stable_id] = route_length(points)
|
||||
views = [view for passage in passages for view in passage["views"]]
|
||||
partial_path = OUT / "runs-partial.json"
|
||||
existing_records = {}
|
||||
if partial_path.is_file():
|
||||
for item in json.loads(partial_path.read_text(encoding="utf-8")):
|
||||
existing_records[item["view"]["name"]] = item
|
||||
records = []
|
||||
for view in views:
|
||||
prior = existing_records.get(view["name"])
|
||||
camera_matches = (prior is not None and
|
||||
prior["view"].get("camera_anchor") == view.get("camera_anchor"))
|
||||
if camera_matches:
|
||||
for key in ("camera", "target", "yaw", "pitch", "clip", "surface_anchor"):
|
||||
if key in prior["view"]:
|
||||
view[key] = prior["view"][key]
|
||||
if camera_matches and all(version in prior and
|
||||
(ROOT / prior[version]["render"]["image"]).is_file()
|
||||
for version in BINARIES):
|
||||
records.append(prior)
|
||||
print(f"[L100CaptureSkip] complete_pair={view['name']}", flush=True)
|
||||
continue
|
||||
pair = {"view": view}
|
||||
for label, binary in BINARIES.items():
|
||||
export_row, render_row = export_and_render(view, label, binary)
|
||||
pair[label] = {"export": export_row, "render": render_row}
|
||||
(OUT / "runs-partial.json").write_text(json.dumps(records + [pair], indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
pair["comparison"] = {"changed_pixels": changed_pixel_count(
|
||||
ROOT / pair["before-l98"]["render"]["image"],
|
||||
ROOT / pair["after-l100"]["render"]["image"])}
|
||||
(OUT / "runs-partial.json").write_text(json.dumps(records + [pair], indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
records.append(pair)
|
||||
summary = {"source_layout_identity": layout["stable_identity"],
|
||||
"view_count": len(views), "paired_png_count": len(views) * 2,
|
||||
"passages": passages, "views": records}
|
||||
(OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
print("[L100CaptureSummary] " + json.dumps({
|
||||
"views": len(views), "paired_png_count": len(views) * 2,
|
||||
"images": [row[version]["render"]["image"] for row in records
|
||||
for version in BINARIES]}, sort_keys=True), flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare the direct passage evaluator with the bounded L100 swept tube."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l100/density-compare"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
HEADLESS = ROOT / "Saved/Linux/l100/bin/VoxelForgeCoreHeadless-l100-clang"
|
||||
CAMERAS = {
|
||||
"SW": (8.0, 16.0, -372.2626953125),
|
||||
"FI": (25.8369140625, 3.19921875, -737.0),
|
||||
"TN": (19.166015625, -14.2001953125, -753.3740234375),
|
||||
"VS": (-20.0625, -5.068359375, -900.353515625),
|
||||
}
|
||||
SUMMARY = re.compile(r"^\[L93PassageReachSummary\] reach_m=4\.00 (.*)$", re.M)
|
||||
COVERAGE = re.compile(r"^\[L93PassageReachCoverage\] (.*)$", re.M)
|
||||
TILE = re.compile(
|
||||
r"^\[L93PassageReachTile\] reach_m=4\.00 key=\(([-\d]+),([-\d]+),([-\d]+),lod(\d+)\) "
|
||||
r"samples=(\d+) changed=(\d+) sign_flips=(\d+) moved_edges=(\d+) "
|
||||
r"added_crossings=(\d+) removed_crossings=(\d+) "
|
||||
r"added_surface_triangles=(\d+) removed_surface_triangles=(\d+) "
|
||||
r"max_displacement_cm=([\deE+.-]+) max_displacement_xyz_vox=\(([^)]*)\) "
|
||||
r"displacement_region=(\d+) normal_vertices=(\d+) max_normal_degrees=([\deE+.-]+) "
|
||||
r"max_normal_xyz_vox=\(([^)]*)\) normal_region=(\d+) changed_mesh=(\d+)$", re.M)
|
||||
VOXEL_SIZE_M = 0.25
|
||||
|
||||
|
||||
def xyz(text: str) -> list[float]:
|
||||
return [float(part) for part in text.split(",")]
|
||||
|
||||
|
||||
def parse_locations(output: str, strate: str) -> tuple[list[dict], list[dict]]:
|
||||
rows = []
|
||||
for match in TILE.finditer(output):
|
||||
(x, y, z, lod, samples, changed, flips, moved, added, removed,
|
||||
added_triangles, removed_triangles, displacement, displacement_xyz,
|
||||
displacement_region, normal_vertices, angle, normal_xyz, normal_region,
|
||||
changed_mesh) = match.groups()
|
||||
disp_vox = xyz(displacement_xyz)
|
||||
normal_vox = xyz(normal_xyz)
|
||||
rows.append({
|
||||
"strate": strate, "tile": [int(x), int(y), int(z), int(lod)],
|
||||
"samples": int(samples), "changed_samples": int(changed),
|
||||
"sign_flips": int(flips), "moved_edges": int(moved),
|
||||
"surface_added_crossings": int(added),
|
||||
"surface_removed_crossings": int(removed),
|
||||
"surface_added_triangles": int(added_triangles),
|
||||
"surface_removed_triangles": int(removed_triangles),
|
||||
"max_displacement_cm": float(displacement),
|
||||
"displacement_location_voxels": disp_vox,
|
||||
"displacement_location_metres": [value * VOXEL_SIZE_M for value in disp_vox],
|
||||
"displacement_region": int(displacement_region),
|
||||
"normal_vertices": int(normal_vertices), "max_normal_degrees": float(angle),
|
||||
"normal_location_voxels": normal_vox,
|
||||
"normal_location_metres": [value * VOXEL_SIZE_M for value in normal_vox],
|
||||
"normal_region": int(normal_region), "changed_mesh": bool(int(changed_mesh)),
|
||||
})
|
||||
top_displacement = sorted(rows, key=lambda row: row["max_displacement_cm"], reverse=True)[:5]
|
||||
top_normals = sorted(rows, key=lambda row: row["max_normal_degrees"], reverse=True)[:5]
|
||||
return top_displacement, top_normals
|
||||
|
||||
|
||||
def top_unique(rows: list[dict], value_key: str, location_key: str) -> list[dict]:
|
||||
chosen, seen = [], set()
|
||||
for row in sorted(rows, key=lambda item: item[value_key], reverse=True):
|
||||
if row[value_key] <= 0.0:
|
||||
continue
|
||||
location = tuple(round(value, 4) for value in row[location_key])
|
||||
if location in seen:
|
||||
continue
|
||||
seen.add(location)
|
||||
chosen.append(row)
|
||||
if len(chosen) == 5:
|
||||
break
|
||||
return chosen
|
||||
|
||||
|
||||
def run(strate: str) -> dict:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
log = OUT / f"reach-{strate.lower()}.log"
|
||||
camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", str(HEADLESS), str(DATA), "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", "--workers=8", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=8", f"--camera={camera}",
|
||||
"--audit-passage-carve-reach",
|
||||
]
|
||||
start = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
summary_match, coverage_match = SUMMARY.search(output), COVERAGE.search(output)
|
||||
if summary_match is None or coverage_match is None or result.returncode in (-9, 124):
|
||||
raise RuntimeError(f"incomplete carve-reach audit: exit={result.returncode} {log}")
|
||||
reach = dict(re.findall(r"([a-zA-Z0-9_]+)=([^ ]+)", summary_match.group(1)))
|
||||
coverage = dict(re.findall(r"([a-zA-Z0-9_]+)=([^ ]+)", coverage_match.group(1)))
|
||||
top_displacement, top_normals = parse_locations(output, strate)
|
||||
row = {
|
||||
"strate": strate, "exit_code": result.returncode,
|
||||
"wall_s": round(time.monotonic() - start, 3), "coverage": coverage,
|
||||
"bounded_sweep_summary": reach,
|
||||
"bounded_sweep_tile_rows": len(TILE.findall(output)),
|
||||
"top_displacement_tiles": top_displacement,
|
||||
"top_normal_tiles": top_normals,
|
||||
"log": str(log.relative_to(ROOT)),
|
||||
}
|
||||
print("[L100DensityCompareRun] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not HEADLESS.is_file() or not DATA.is_file():
|
||||
raise SystemExit(f"missing input: {HEADLESS if not HEADLESS.is_file() else DATA}")
|
||||
if "--summarize-only" in sys.argv:
|
||||
rows = json.loads((OUT / "runs-partial.json").read_text(encoding="utf-8"))
|
||||
else:
|
||||
rows = []
|
||||
for strate in CAMERAS:
|
||||
rows.append(run(strate))
|
||||
(OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
all_displacements = [row for result in rows for row in result["top_displacement_tiles"]]
|
||||
all_normals = [row for result in rows for row in result["top_normal_tiles"]]
|
||||
top_displacement = top_unique(all_displacements, "max_displacement_cm",
|
||||
"displacement_location_metres")
|
||||
top_normals = top_unique(all_normals, "max_normal_degrees", "normal_location_metres")
|
||||
summary = {"runs": rows, "top_displacement_tiles": top_displacement,
|
||||
"top_normal_tiles": top_normals}
|
||||
(OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check L100 whole-view tile hashes across W1/W8/W12 and Clang/GCC."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l100/hashes"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
BINARIES = {
|
||||
"Clang": ROOT / "Saved/Linux/l100/bin/VoxelForgeCoreHeadless-l100-clang",
|
||||
"GCC": ROOT / "Saved/Linux/l100/bin/VoxelForgeCoreHeadless-l100-gcc",
|
||||
}
|
||||
CAMERAS = {
|
||||
"SW": (8.0, 16.0, -372.2626953125),
|
||||
"FI": (25.8369140625, 3.19921875, -737.0),
|
||||
"TN": (19.166015625, -14.2001953125, -753.3740234375),
|
||||
"VS": (-20.0625, -5.068359375, -900.353515625),
|
||||
}
|
||||
TILE_HASH = re.compile(
|
||||
r"^\[PipelineTileHash\] lod=(-?\d+) x=(-?\d+) y=(-?\d+) z=(-?\d+) "
|
||||
r"density=(0x[0-9a-f]+) mesh=(0x[0-9a-f]+) payload=(0x[0-9a-f]+) "
|
||||
r"topology=(0x[0-9a-f]+)$", re.M)
|
||||
WHOLE_HASH = re.compile(
|
||||
r"^\[PipelineWholeViewHash\] tiles=(\d+) density=(0x[0-9a-f]+) "
|
||||
r"mesh=(0x[0-9a-f]+) payload=(0x[0-9a-f]+) topology=(0x[0-9a-f]+)$", re.M)
|
||||
FIELDS = ("density", "mesh", "payload", "topology")
|
||||
|
||||
|
||||
def run(strate: str, compiler: str, workers: int) -> dict:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
log = OUT / f"{strate.lower()}-{compiler.lower()}-w{workers}.log"
|
||||
camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", str(BINARIES[compiler]), str(DATA),
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
f"--workers={workers}", "--clip-radius=3", "--max-clip-level=8",
|
||||
"--max-level=8", f"--camera={camera}", "--stream-hash-tiles",
|
||||
]
|
||||
start = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
hashes = {
|
||||
(int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))):
|
||||
m.group(5, 6, 7, 8) for m in TILE_HASH.finditer(output)
|
||||
}
|
||||
whole = WHOLE_HASH.search(output)
|
||||
stamp = re.search(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]{64})$", output, re.M)
|
||||
if (result.returncode or whole is None or stamp is None
|
||||
or len(hashes) != int(whole.group(1))):
|
||||
raise RuntimeError(f"hash run failed or incomplete: exit={result.returncode} {log}")
|
||||
row = {
|
||||
"strate": strate, "compiler": compiler, "workers": workers,
|
||||
"source_stamp": stamp.group(1), "exit_code": result.returncode,
|
||||
"wall_s": round(time.monotonic() - start, 3), "tile_count": len(hashes),
|
||||
"tiles_by_lod": {str(lod): sum(1 for key in hashes if key[0] == lod)
|
||||
for lod in sorted({key[0] for key in hashes})},
|
||||
"whole_hash": list(whole.groups()), "tile_hashes": hashes,
|
||||
"log": str(log.relative_to(ROOT)),
|
||||
}
|
||||
print("[L100HashRun] " + json.dumps(
|
||||
{key: value for key, value in row.items() if key != "tile_hashes"},
|
||||
sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def compare(strate: str, reference: dict, candidate: dict) -> dict:
|
||||
keys = set(reference["tile_hashes"]) | set(candidate["tile_hashes"])
|
||||
mismatches = {
|
||||
field: sum(reference["tile_hashes"].get(key, (None,) * 4)[index]
|
||||
!= candidate["tile_hashes"].get(key, (None,) * 4)[index]
|
||||
for key in keys)
|
||||
for index, field in enumerate(FIELDS)
|
||||
}
|
||||
row = {
|
||||
"strate": strate, "reference": f"{reference['compiler']}-w{reference['workers']}",
|
||||
"candidate": f"{candidate['compiler']}-w{candidate['workers']}",
|
||||
"tile_counts_match": reference["tile_count"] == candidate["tile_count"],
|
||||
"whole_hashes_match": reference["whole_hash"] == candidate["whole_hash"],
|
||||
"tile_hashes_match": all(value == 0 for value in mismatches.values())
|
||||
and reference["tile_hashes"].keys() == candidate["tile_hashes"].keys(),
|
||||
"component_mismatch_tiles": mismatches,
|
||||
"tiles_reference": reference["tile_count"], "tiles_candidate": candidate["tile_count"],
|
||||
"reference_whole_hash": reference["whole_hash"],
|
||||
"candidate_whole_hash": candidate["whole_hash"],
|
||||
}
|
||||
print("[L100HashComparison] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def main() -> int:
|
||||
for binary in (*BINARIES.values(), DATA):
|
||||
if not binary.is_file():
|
||||
raise SystemExit(f"missing input: {binary}")
|
||||
detail, comparisons = [], []
|
||||
for strate in CAMERAS:
|
||||
group = {(compiler, workers): run(strate, compiler, workers)
|
||||
for compiler in BINARIES for workers in (1, 8, 12)}
|
||||
reference = group[("Clang", 1)]
|
||||
for key, candidate in group.items():
|
||||
if key == ("Clang", 1):
|
||||
continue
|
||||
comparisons.append(compare(strate, reference, candidate))
|
||||
detail.extend({key: value for key, value in row.items() if key != "tile_hashes"}
|
||||
for row in group.values())
|
||||
(OUT / "runs-partial.json").write_text(json.dumps(
|
||||
{"runs": detail, "comparisons": comparisons}, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
summary = {"runs": detail, "comparisons": comparisons}
|
||||
(OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
return 0 if all(row["tile_counts_match"] and row["whole_hashes_match"]
|
||||
and row["tile_hashes_match"] for row in comparisons) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
"""L80 cold-readiness protocol: three interleaved L98/L99 pairs at W8 and W12."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import statistics
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT_ROOT = ROOT / "Saved/Linux/l100/readiness"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
ALL_BINARIES = {
|
||||
"L98": ROOT / "Saved/Linux/l98/bin/VoxelForgeCoreHeadless-l98-clang19",
|
||||
"L99": ROOT / "Saved/Linux/l99/bin/VoxelForgeCoreHeadless-l99-clang",
|
||||
"L100": ROOT / "Saved/Linux/l100/bin/VoxelForgeCoreHeadless-l100-clang",
|
||||
}
|
||||
OUT = OUT_ROOT
|
||||
BINARIES = {}
|
||||
# Resolved owner camera index -> stratum. Keep the labels tied to the canonical layout.
|
||||
CAMERAS = {
|
||||
"SW": (8.0, 16.0, -372.2626953125),
|
||||
"FI": (25.8369140625, 3.19921875, -737.0),
|
||||
"TN": (19.166015625, -14.2001953125, -753.3740234375),
|
||||
"VS": (-20.0625, -5.068359375, -900.353515625),
|
||||
}
|
||||
PIPELINE = re.compile(r"^\[StreamPipeline\] (.*)$", re.M)
|
||||
FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)")
|
||||
NEAR = re.compile(r"^\[WorldAssemblerNearLODReady\] lods=0-2 tiles=(\d+)/(\d+) "
|
||||
r"cold_ready_s=([0-9.]+)$", re.M)
|
||||
|
||||
|
||||
def run(strate: str, workers: int, label: str, phase: str, rep: int) -> dict:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
log = OUT / f"w{workers}-{strate.lower()}-{label.lower()}-{phase}{rep}.log"
|
||||
camera = ",".join(f"{v:.12f}" for v in CAMERAS[strate]) + ",0,0"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"env", "timeout", "1740s", str(BINARIES[label]), str(DATA),
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
f"--workers={workers}", "--clip-radius=3", "--max-clip-level=8",
|
||||
"--max-level=8", f"--camera={camera}", "--profile-lod-work",
|
||||
"--stream-scheduler-policy=3",
|
||||
]
|
||||
start = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
pipeline_match, near_match = PIPELINE.search(output), NEAR.search(output)
|
||||
if result.returncode or pipeline_match is None or near_match is None:
|
||||
raise RuntimeError(f"readiness run failed: exit={result.returncode} {log}")
|
||||
pipeline = dict(FIELDS.findall(pipeline_match.group(1)))
|
||||
row = {
|
||||
"strate": strate, "workers": workers, "binary": label, "phase": phase,
|
||||
"replicate": rep, "exit_code": result.returncode,
|
||||
"wall_s": round(time.monotonic() - start, 3),
|
||||
"lod02_tiles": f"{near_match.group(1)}/{near_match.group(2)}",
|
||||
"lod02_ready_s": float(near_match.group(3)),
|
||||
"whole_view_ready_s": float(pipeline["whole_view_ready"].removesuffix("s")),
|
||||
"whole_tiles": pipeline["whole_tiles"].split("/")[0],
|
||||
"log": str(log.relative_to(ROOT)),
|
||||
}
|
||||
print("[L100ReadinessRun] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def main() -> int:
|
||||
global BINARIES, OUT
|
||||
pair = sys.argv[1:] or ["L98", "L100"]
|
||||
if len(pair) != 2 or len(set(pair)) != 2 or any(item not in ALL_BINARIES for item in pair):
|
||||
raise SystemExit("usage: RunL100Readiness.py L98 L100 | L99 L100")
|
||||
BINARIES = {label: ALL_BINARIES[label] for label in pair}
|
||||
OUT = OUT_ROOT / f"{pair[0].lower()}-{pair[1].lower()}"
|
||||
for binary in (*BINARIES.values(), DATA):
|
||||
if not binary.is_file():
|
||||
raise SystemExit(f"missing input: {binary}")
|
||||
rows = []
|
||||
first, second = tuple(BINARIES)
|
||||
orders = ((first, second), (second, first), (first, second))
|
||||
for workers in (8, 12):
|
||||
for strate in CAMERAS:
|
||||
for label in BINARIES:
|
||||
run(strate, workers, label, "warmup", 0)
|
||||
for strate in CAMERAS:
|
||||
for rep, order in enumerate(orders, 1):
|
||||
for label in order:
|
||||
rows.append(run(strate, workers, label, "measured", rep))
|
||||
(OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
summary = []
|
||||
for workers in (8, 12):
|
||||
for strate in CAMERAS:
|
||||
group = [row for row in rows
|
||||
if row["workers"] == workers and row["strate"] == strate]
|
||||
item = {"workers": workers, "strate": strate, "measured_pairs": 3}
|
||||
for label in BINARIES:
|
||||
subset = [row for row in group if row["binary"] == label]
|
||||
for metric in ("lod02_ready_s", "whole_view_ready_s"):
|
||||
values = [row[metric] for row in subset]
|
||||
item[f"{label}_{metric}_runs"] = values
|
||||
item[f"{label}_{metric}_median"] = statistics.median(values)
|
||||
item[f"{label}_process_wall_s_runs"] = [row["wall_s"] for row in subset]
|
||||
summary.append(item)
|
||||
(OUT / "summary.json").write_text(json.dumps(
|
||||
{"comparison_pair": pair, "runs": rows, "summary": summary}, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
"""L80 protocol with balanced, rotating L98/L99/L100 order."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l100/readiness/l98-l99-l100"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
BINARIES = {
|
||||
"L98": ROOT / "Saved/Linux/l98/bin/VoxelForgeCoreHeadless-l98-clang19",
|
||||
"L99": ROOT / "Saved/Linux/l99/bin/VoxelForgeCoreHeadless-l99-clang",
|
||||
"L100": ROOT / "Saved/Linux/l100/bin/VoxelForgeCoreHeadless-l100-clang",
|
||||
}
|
||||
CAMERAS = {
|
||||
"SW": (8.0, 16.0, -372.2626953125),
|
||||
"FI": (25.8369140625, 3.19921875, -737.0),
|
||||
"TN": (19.166015625, -14.2001953125, -753.3740234375),
|
||||
"VS": (-20.0625, -5.068359375, -900.353515625),
|
||||
}
|
||||
PIPELINE = re.compile(r"^\[StreamPipeline\] (.*)$", re.M)
|
||||
FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)")
|
||||
NEAR = re.compile(r"^\[WorldAssemblerNearLODReady\] lods=0-2 tiles=(\d+)/(\d+) "
|
||||
r"cold_ready_s=([0-9.]+)$", re.M)
|
||||
STAMP = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]{64})$", re.M)
|
||||
|
||||
|
||||
def run(strate: str, workers: int, label: str, phase: str, rep: int) -> dict:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
log = OUT / f"w{workers}-{strate.lower()}-{label.lower()}-{phase}{rep}.log"
|
||||
camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"env", "timeout", "1740s", str(BINARIES[label]), str(DATA),
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
f"--workers={workers}", "--clip-radius=3", "--max-clip-level=8",
|
||||
"--max-level=8", f"--camera={camera}", "--profile-lod-work",
|
||||
"--stream-scheduler-policy=3",
|
||||
]
|
||||
start = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
pipeline_match, near_match, stamp_match = (
|
||||
PIPELINE.search(output), NEAR.search(output), STAMP.search(output))
|
||||
if result.returncode or pipeline_match is None or near_match is None or stamp_match is None:
|
||||
raise RuntimeError(f"readiness run failed: exit={result.returncode} {log}")
|
||||
pipeline = dict(FIELDS.findall(pipeline_match.group(1)))
|
||||
row = {
|
||||
"strate": strate, "workers": workers, "binary": label, "phase": phase,
|
||||
"replicate": rep, "exit_code": result.returncode,
|
||||
"source_stamp": stamp_match.group(1), "wall_s": round(time.monotonic() - start, 3),
|
||||
"lod02_tiles": f"{near_match.group(1)}/{near_match.group(2)}",
|
||||
"lod02_ready_s": float(near_match.group(3)),
|
||||
"whole_view_ready_s": float(pipeline["whole_view_ready"].removesuffix("s")),
|
||||
"whole_tiles": pipeline["whole_tiles"].split("/")[0],
|
||||
"log": str(log.relative_to(ROOT)),
|
||||
}
|
||||
print("[L100ReadinessTripleRun] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def main() -> int:
|
||||
for binary in (*BINARIES.values(), DATA):
|
||||
if not binary.is_file():
|
||||
raise SystemExit(f"missing input: {binary}")
|
||||
rows = []
|
||||
labels = tuple(BINARIES)
|
||||
orders = (labels, labels[1:] + labels[:1], labels[2:] + labels[:2])
|
||||
for workers in (8, 12):
|
||||
for strate in CAMERAS:
|
||||
for label in labels:
|
||||
run(strate, workers, label, "warmup", 0)
|
||||
for strate in CAMERAS:
|
||||
for rep, order in enumerate(orders, 1):
|
||||
for label in order:
|
||||
rows.append(run(strate, workers, label, "measured", rep))
|
||||
(OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
summary = []
|
||||
for workers in (8, 12):
|
||||
for strate in CAMERAS:
|
||||
group = [row for row in rows if row["workers"] == workers
|
||||
and row["strate"] == strate]
|
||||
item = {"workers": workers, "strate": strate, "measured_runs": 3}
|
||||
for label in labels:
|
||||
subset = [row for row in group if row["binary"] == label]
|
||||
for metric in ("lod02_ready_s", "whole_view_ready_s"):
|
||||
values = [row[metric] for row in subset]
|
||||
item[f"{label}_{metric}_runs"] = values
|
||||
item[f"{label}_{metric}_median"] = statistics.median(values)
|
||||
item[f"{label}_process_wall_s_runs"] = [row["wall_s"] for row in subset]
|
||||
summary.append(item)
|
||||
(OUT / "summary.json").write_text(json.dumps(
|
||||
{"comparison_triple": labels, "runs": rows, "summary": summary}, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Profile L100 swept-tube lookups and direct route fallback work."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l100/sweep-counts"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
HEADLESS = ROOT / "Saved/Linux/l100/bin/VoxelForgeCoreHeadless-l100-clang"
|
||||
CAMERAS = {
|
||||
"SW": (8.0, 16.0, -372.2626953125),
|
||||
"FI": (25.8369140625, 3.19921875, -737.0),
|
||||
"TN": (19.166015625, -14.2001953125, -753.3740234375),
|
||||
"VS": (-20.0625, -5.068359375, -900.353515625),
|
||||
}
|
||||
LINE = re.compile(r"^\[PassageComposerTile\] key=\([^,]+,[^,]+,[^,]+,lod(\d+)\) (.*)$", re.M)
|
||||
FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)")
|
||||
COUNT_KEYS = (
|
||||
"route_candidates", "sweep_segments", "sweep_pairs", "sweep_outside_pairs",
|
||||
"sweep_min_updates", "sweep_base_samples", "sweep_detail_samples",
|
||||
"sweep_grid_samples", "sweep_no_influence_samples", "sweep_lookups",
|
||||
"sweep_cache_hits", "sweep_base_hits", "sweep_outside_hits", "sweep_missing",
|
||||
"carve_segment_visits", "support_segment_visits", "mouth_route_segment_checks",
|
||||
)
|
||||
|
||||
|
||||
def run(strate: str) -> dict:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
log = OUT / f"profile-sweep-{strate.lower()}.log"
|
||||
camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", str(HEADLESS), str(DATA), "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", "--workers=8", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=8", f"--camera={camera}",
|
||||
"--profile-passage-composer",
|
||||
]
|
||||
start = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"sweep count run failed: exit={result.returncode} {log}")
|
||||
rows = []
|
||||
for level_text, tail in LINE.findall(output):
|
||||
level = int(level_text)
|
||||
if level > 2:
|
||||
continue
|
||||
fields = dict(FIELDS.findall(tail))
|
||||
rows.append({"lod": level, **{key: int(fields.get(key, 0)) for key in COUNT_KEYS}})
|
||||
if not rows:
|
||||
raise RuntimeError(f"no profiled near-set passage tiles: {log}")
|
||||
totals = {key: sum(row[key] for row in rows) for key in COUNT_KEYS}
|
||||
row = {
|
||||
"strate": strate, "workers": 8, "near_profiled_tiles": len(rows),
|
||||
"near_tiles_by_lod": {str(level): sum(1 for item in rows if item["lod"] == level)
|
||||
for level in range(3)},
|
||||
"counters": totals, "wall_s": round(time.monotonic() - start, 3),
|
||||
"log": str(log.relative_to(ROOT)),
|
||||
}
|
||||
print("[L100SweepCounts] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def main() -> int:
|
||||
rows = [run(strate) for strate in CAMERAS]
|
||||
(OUT / "summary.json").write_text(json.dumps({"runs": rows}, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,37 @@
|
||||
# L100 passage captures
|
||||
|
||||
Each route has paired L98 and L100 views at both mouths and at 25%, 50%, and 75% stations. The camera gate decodes each PNG and requires visible geometry (>3,000 pixels after excluding the dominant background color, with at least two RGB colors). Changed pixels count RGB pixels with any channel difference between a matched pair.
|
||||
|
||||
L100 source stamp: `7224bf6e668d06ed71a92f0857820e3ae2b0eca40574a491331ab5979a88f698`.
|
||||
|
||||
| View | L98 geometry px | L100 geometry px | Changed px | Images |
|
||||
|---|---:|---:|---:|---|
|
||||
| `0-1 · upper-mouth` | 1,063,522 | 1,063,522 | 41,441 | [L98](passage-0-1-0-upper-mouth-before-l98.png) / [L100](passage-0-1-0-upper-mouth-after-l100.png) |
|
||||
| `0-1 · lower-mouth` | 1,420,316 | 1,420,316 | 23,644 | [L98](passage-0-1-0-lower-mouth-before-l98.png) / [L100](passage-0-1-0-lower-mouth-after-l100.png) |
|
||||
| `0-1 · mid-25 (27.5%); station +50 m to surface; surface-target fallback` | 422,920 | 422,935 | 706 | [L98](passage-0-1-0-mid-25-before-l98.png) / [L100](passage-0-1-0-mid-25-after-l100.png) |
|
||||
| `0-1 · mid-50 (50.0%)` | 1,375,241 | 1,375,118 | 20,082 | [L98](passage-0-1-0-mid-50-before-l98.png) / [L100](passage-0-1-0-mid-50-after-l100.png) |
|
||||
| `0-1 · mid-75 (75.0%)` | 1,368,802 | 1,368,801 | 12,919 | [L98](passage-0-1-0-mid-75-before-l98.png) / [L100](passage-0-1-0-mid-75-after-l100.png) |
|
||||
| `1-2 · upper-mouth` | 1,425,620 | 1,426,619 | 292,188 | [L98](passage-1-2-0-upper-mouth-before-l98.png) / [L100](passage-1-2-0-upper-mouth-after-l100.png) |
|
||||
| `1-2 · lower-mouth` | 223,508 | 223,508 | 57,795 | [L98](passage-1-2-0-lower-mouth-before-l98.png) / [L100](passage-1-2-0-lower-mouth-after-l100.png) |
|
||||
| `1-2 · mid-25 (25.0%)` | 1,409,914 | 1,409,907 | 15,713 | [L98](passage-1-2-0-mid-25-before-l98.png) / [L100](passage-1-2-0-mid-25-after-l100.png) |
|
||||
| `1-2 · mid-50 (50.0%)` | 1,407,415 | 1,407,415 | 32,217 | [L98](passage-1-2-0-mid-50-before-l98.png) / [L100](passage-1-2-0-mid-50-after-l100.png) |
|
||||
| `1-2 · mid-75 (75.0%)` | 1,261,589 | 1,264,705 | 263,048 | [L98](passage-1-2-0-mid-75-before-l98.png) / [L100](passage-1-2-0-mid-75-after-l100.png) |
|
||||
| `2-3 · upper-mouth` | 60,506 | 60,506 | 0 | [L98](passage-2-3-0-upper-mouth-before-l98.png) / [L100](passage-2-3-0-upper-mouth-after-l100.png) |
|
||||
| `2-3 · lower-mouth` | 60,257 | 60,257 | 0 | [L98](passage-2-3-0-lower-mouth-before-l98.png) / [L100](passage-2-3-0-lower-mouth-after-l100.png) |
|
||||
| `2-3 · mid-25 (25.0%); surface-target fallback` | 631,345 | 631,345 | 0 | [L98](passage-2-3-0-mid-25-before-l98.png) / [L100](passage-2-3-0-mid-25-after-l100.png) |
|
||||
| `2-3 · mid-50 (50.0%); surface-target fallback` | 629,698 | 629,698 | 4,208 | [L98](passage-2-3-0-mid-50-before-l98.png) / [L100](passage-2-3-0-mid-50-after-l100.png) |
|
||||
| `2-3 · mid-75 (75.0%)` | 1,163,903 | 1,163,903 | 0 | [L98](passage-2-3-0-mid-75-before-l98.png) / [L100](passage-2-3-0-mid-75-after-l100.png) |
|
||||
|
||||
## Same-camera 1–2 regression check
|
||||
|
||||
The L99 and L100 midpoint records have identical focus, camera, target, yaw, and pitch. This three-version check decodes every image and passes the same geometry gate.
|
||||
|
||||
| Version | Image | Visible geometry px | Changed px from L98 |
|
||||
|---|---|---:|---:|
|
||||
| L98 | [before](../../Linux99/captures/passage-1-2-0-mid-tunnel-before-l98.png) | 1,407,415 | — |
|
||||
| L99 | [flat-tunnel capture](../../Linux99/captures/passage-1-2-0-mid-tunnel-after-l99.png) | 1,206,362 | 1,135,959 |
|
||||
| L100 | [fixed capture](passage-1-2-0-mid-50-after-l100.png) | 1,407,415 | 32,217 |
|
||||
|
||||
L99→L100 changed 1,134,150 pixels. The L100 view returns to the L98 image at this camera (L98→L100: 32,217).
|
||||
|
||||
All 30 L98/L100 PNGs passed the geometry gate. Capture logs, camera coordinates, exports, and raw metadata are under `Saved/Linux/l100/captures/`.
|
||||
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 74 KiB |