research: one continuous tube through the mouth (L149, 0/16; kept as patch)
Mouth edges matched passage position, radius, tangent (dot 0.99999) and floor slope, but the cubic departure edge still curls ~1 m back alongside the passage within 5 m; shell floor violations remain; composed sweeps 0/16. Sixth mouth-join round (L144-L149): stop and take the design question to the owner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
1 parent
f21aab775f
commit
b8382eed84
14 files changed
+1374
No files matched your search
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Structural mouth continuity and forward-extension checks for L149 layouts."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _q_to_um(value: int, core_voxel_size_um: int) -> int:
|
||||
return int(value * core_voxel_size_um / 256)
|
||||
|
||||
|
||||
def _mouth(layout: dict[str, Any], edge: dict[str, Any]) -> tuple[dict[str, Any], int,
|
||||
list[int], int] | None:
|
||||
anchor = edge.get("start_anchor_stable_id", "")
|
||||
if not anchor.endswith(("/lower-mouth", "/upper-mouth")):
|
||||
return None
|
||||
side = anchor.rsplit("/", 1)[1]
|
||||
passage_id = anchor.rsplit("/", 1)[0]
|
||||
passage = next((row for row in layout.get("passages", [])
|
||||
if row.get("stable_id") == passage_id), None)
|
||||
if passage is None:
|
||||
return None
|
||||
key = "lower_mouth_q" if side == "lower-mouth" else "upper_mouth_q"
|
||||
mouth_q = passage.get(key)
|
||||
controls = passage.get("control_points", [])
|
||||
if not mouth_q or not controls:
|
||||
return None
|
||||
if side == "upper-mouth":
|
||||
index = 0
|
||||
else:
|
||||
center_height_q = round(1.13 * 256_000_000 / layout["core_voxel_size_um"])
|
||||
matches = [i for i, point in enumerate(controls)
|
||||
if point["position_q"][0] == mouth_q[0]
|
||||
and point["position_q"][1] == mouth_q[1]
|
||||
and point["position_q"][2] - point["radius_q"] + center_height_q
|
||||
== mouth_q[2]]
|
||||
if not matches:
|
||||
return None
|
||||
index = matches[-1]
|
||||
return passage, index, mouth_q, _q_to_um(controls[index]["radius_q"],
|
||||
layout["core_voxel_size_um"])
|
||||
|
||||
|
||||
def _dot_direction(a: list[float], b: list[float]) -> float:
|
||||
al, bl = math.sqrt(sum(x * x for x in a)), math.sqrt(sum(x * x for x in b))
|
||||
return sum(x * y for x, y in zip(a, b)) / (al * bl) if al > 0 and bl > 0 else -1.0
|
||||
|
||||
|
||||
def _bezier(points: list[list[int]], t: float) -> list[float]:
|
||||
u = 1.0 - t
|
||||
return [u**3 * points[0][axis] + 3*u*u*t * points[1][axis]
|
||||
+ 3*u*t*t * points[2][axis] + t**3 * points[3][axis]
|
||||
for axis in range(3)]
|
||||
|
||||
|
||||
def _metrics(layout: dict[str, Any], edge: dict[str, Any]) -> dict[str, Any]:
|
||||
found = _mouth(layout, edge)
|
||||
if found is None:
|
||||
return {"bound": False, "tangent_dot": -1.0, "radius_match": False,
|
||||
"position_match": False, "floor_slope_delta": math.inf,
|
||||
"forward_extension": False}
|
||||
passage, index, _, expected_radius = found
|
||||
controls = passage["control_points"]
|
||||
route = edge.get("control_points_um", [])
|
||||
if len(route) != 4 or len(controls) < 2:
|
||||
return {"bound": False, "tangent_dot": -1.0, "radius_match": False,
|
||||
"position_match": False, "floor_slope_delta": math.inf,
|
||||
"forward_extension": False}
|
||||
side = edge["start_anchor_stable_id"].rsplit("/", 1)[1]
|
||||
neighbor = 1 if side == "upper-mouth" else index - 1
|
||||
if neighbor < 0 or neighbor >= len(controls):
|
||||
return {"bound": False, "tangent_dot": -1.0, "radius_match": False,
|
||||
"position_match": False, "floor_slope_delta": math.inf,
|
||||
"forward_extension": False}
|
||||
mouth_point = controls[index]
|
||||
neighbor_point = controls[neighbor]
|
||||
core = layout["core_voxel_size_um"]
|
||||
expected = [_q_to_um(v, core) for v in mouth_point["position_q"]]
|
||||
expected[2] -= expected_radius
|
||||
expected_dir = [_q_to_um(mouth_point["position_q"][axis]
|
||||
- neighbor_point["position_q"][axis], core)
|
||||
for axis in range(2)]
|
||||
edge_dir = [route[1][0] - route[0][0], route[1][1] - route[0][1]]
|
||||
position_match = all(abs(route[0][axis] - expected[axis]) <= 2 for axis in range(3))
|
||||
radius_match = edge.get("bore_radius_um") == expected_radius
|
||||
tangent_dot = _dot_direction([*edge_dir, 0.0], [*expected_dir, 0.0])
|
||||
|
||||
floor_mouth = mouth_point["position_q"][2] - mouth_point["radius_q"]
|
||||
floor_neighbor = neighbor_point["position_q"][2] - neighbor_point["radius_q"]
|
||||
expected_slope = _q_to_um(floor_mouth - floor_neighbor, core) / max(
|
||||
1.0, math.hypot(*expected_dir))
|
||||
if side == "upper-mouth":
|
||||
expected_slope = -expected_slope
|
||||
route_slope = (route[1][2] - route[0][2]) / max(1.0, math.hypot(*edge_dir))
|
||||
floor_slope_delta = abs(route_slope - expected_slope)
|
||||
|
||||
tangent_length = math.hypot(*expected_dir)
|
||||
tangent = [v / tangent_length for v in expected_dir] if tangent_length else [0.0, 0.0]
|
||||
points = [list(p) for p in route]
|
||||
previous = _bezier(points, 0.0)
|
||||
distance = 0.0
|
||||
prior_progress = 0.0
|
||||
forward = True
|
||||
for step in range(1, 1025):
|
||||
current = _bezier(points, step / 1024.0)
|
||||
dx, dy = current[0] - previous[0], current[1] - previous[1]
|
||||
segment = math.hypot(dx, dy)
|
||||
next_distance = distance + segment
|
||||
if distance < 12_000_000:
|
||||
progress = ((current[0] - route[0][0]) * tangent[0]
|
||||
+ (current[1] - route[0][1]) * tangent[1])
|
||||
if progress < -100_000 or progress < prior_progress - 100_000:
|
||||
forward = False
|
||||
prior_progress = progress
|
||||
previous, distance = current, next_distance
|
||||
if distance >= 12_000_000:
|
||||
break
|
||||
return {"bound": True, "tangent_dot": tangent_dot,
|
||||
"radius_match": radius_match, "position_match": position_match,
|
||||
"floor_slope_delta": floor_slope_delta, "forward_extension": forward}
|
||||
|
||||
|
||||
def audit_layout(layout_path: Path) -> dict[str, Any]:
|
||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||
mouth_edges = [edge for edge in layout.get("cavern_edges", [])
|
||||
if edge.get("strate_index") in (2, 3)
|
||||
and edge.get("stable_id", "").endswith(("route-edge-0", "departure-edge"))
|
||||
and edge.get("start_anchor_stable_id", "").endswith(
|
||||
("/lower-mouth", "/upper-mouth"))]
|
||||
rows = [_metrics(layout, edge) for edge in mouth_edges]
|
||||
negative_controls = 0
|
||||
for edge in mouth_edges:
|
||||
found = _mouth(layout, edge)
|
||||
if found is None:
|
||||
continue
|
||||
passage, index, _, _ = found
|
||||
side = edge["start_anchor_stable_id"].rsplit("/", 1)[1]
|
||||
neighbor = 1 if side == "upper-mouth" else index - 1
|
||||
mouth_point = passage["control_points"][index]
|
||||
neighbor_point = passage["control_points"][neighbor]
|
||||
core = layout["core_voxel_size_um"]
|
||||
expected_dir = [_q_to_um(mouth_point["position_q"][axis]
|
||||
- neighbor_point["position_q"][axis], core)
|
||||
for axis in range(2)]
|
||||
altered = dict(edge)
|
||||
points = [list(p) for p in edge["control_points_um"]]
|
||||
length = max(1.0, math.hypot(*expected_dir))
|
||||
points[1][0] = points[0][0] - round(expected_dir[0] / length * 1_000_000)
|
||||
points[1][1] = points[0][1] - round(expected_dir[1] / length * 1_000_000)
|
||||
altered["control_points_um"] = points
|
||||
control = _metrics(layout, altered)
|
||||
if control["tangent_dot"] < 0.9:
|
||||
negative_controls += 1
|
||||
continuity = bool(rows) and all(
|
||||
row["bound"] and row["position_match"] and row["radius_match"]
|
||||
and row["tangent_dot"] >= 0.9 and row["floor_slope_delta"] <= 0.15
|
||||
for row in rows)
|
||||
no_backtrack = bool(rows) and all(row["forward_extension"] for row in rows)
|
||||
controls_ok = bool(mouth_edges) and negative_controls == len(mouth_edges)
|
||||
return {"edges": len(mouth_edges), "continuity": continuity,
|
||||
"no_backtrack_12m": no_backtrack, "negative_controls": negative_controls,
|
||||
"negative_controls_expected": len(mouth_edges), "negative_controls_pass": controls_ok,
|
||||
"minimum_tangent_dot": min((row["tangent_dot"] for row in rows), default=-1.0),
|
||||
"maximum_floor_slope_delta": max(
|
||||
(row["floor_slope_delta"] for row in rows), default=math.inf),
|
||||
"rows": rows}
|
||||
Reference in new issue
Block a user