104 lines
3.6 KiB
Python
104 lines
3.6 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Any
|
|
import numpy
|
|
|
|
from inire.constants import TOLERANCE_LINEAR
|
|
|
|
if TYPE_CHECKING:
|
|
from shapely.geometry import Polygon
|
|
from inire.geometry.primitives import Port
|
|
from inire.router.pathfinder import RoutingResult
|
|
|
|
|
|
def validate_routing_result(
|
|
result: RoutingResult,
|
|
static_obstacles: list[Polygon],
|
|
clearance: float,
|
|
expected_start: Port | None = None,
|
|
expected_end: Port | None = None,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Perform a high-precision validation of a routed path.
|
|
|
|
Args:
|
|
result: The routing result to validate.
|
|
static_obstacles: List of static obstacle geometries.
|
|
clearance: Required minimum distance.
|
|
expected_start: Optional expected start port.
|
|
expected_end: Optional expected end port.
|
|
|
|
Returns:
|
|
A dictionary with validation results.
|
|
"""
|
|
_ = expected_start
|
|
if not result.path:
|
|
return {"is_valid": False, "reason": "No path found"}
|
|
|
|
obstacle_collision_geoms = []
|
|
self_intersection_geoms = []
|
|
connectivity_errors = []
|
|
|
|
# 1. Connectivity Check
|
|
total_length = 0.0
|
|
for comp in result.path:
|
|
total_length += comp.length
|
|
|
|
# Boundary check
|
|
if expected_end:
|
|
last_port = result.path[-1].end_port
|
|
dist_to_end = numpy.sqrt((last_port.x - expected_end.x)**2 + (last_port.y - expected_end.y)**2)
|
|
if dist_to_end > 0.005:
|
|
connectivity_errors.append(f"Final port position mismatch: {dist_to_end*1000:.2f}nm")
|
|
if abs(last_port.orientation - expected_end.orientation) > 0.1:
|
|
connectivity_errors.append(f"Final port orientation mismatch: {last_port.orientation} vs {expected_end.orientation}")
|
|
|
|
# 2. Geometry Buffering
|
|
dilation_half = clearance / 2.0
|
|
dilation_full = clearance
|
|
|
|
dilated_for_self = []
|
|
|
|
for comp in result.path:
|
|
for poly in comp.geometry:
|
|
# Check against obstacles
|
|
d_full = poly.buffer(dilation_full)
|
|
for obs in static_obstacles:
|
|
if d_full.intersects(obs):
|
|
intersection = d_full.intersection(obs)
|
|
if intersection.area > 1e-9:
|
|
obstacle_collision_geoms.append(intersection)
|
|
|
|
# Save for self-intersection check
|
|
dilated_for_self.append(poly.buffer(dilation_half))
|
|
|
|
# 3. Self-intersection
|
|
for i, seg_i in enumerate(dilated_for_self):
|
|
for j, seg_j in enumerate(dilated_for_self):
|
|
if j > i + 1 and seg_i.intersects(seg_j): # Non-adjacent
|
|
overlap = seg_i.intersection(seg_j)
|
|
if overlap.area > TOLERANCE_LINEAR:
|
|
self_intersection_geoms.append((i, j, overlap))
|
|
|
|
is_valid = (len(obstacle_collision_geoms) == 0 and
|
|
len(self_intersection_geoms) == 0 and
|
|
len(connectivity_errors) == 0)
|
|
|
|
reasons = []
|
|
if obstacle_collision_geoms:
|
|
reasons.append(f"Found {len(obstacle_collision_geoms)} obstacle collisions.")
|
|
if self_intersection_geoms:
|
|
# report which indices
|
|
idx_str = ", ".join([f"{i}-{j}" for i, j, _ in self_intersection_geoms[:5]])
|
|
reasons.append(f"Found {len(self_intersection_geoms)} self-intersections (e.g. {idx_str}).")
|
|
if connectivity_errors:
|
|
reasons.extend(connectivity_errors)
|
|
|
|
return {
|
|
"is_valid": is_valid,
|
|
"reason": " ".join(reasons),
|
|
"obstacle_collisions": obstacle_collision_geoms,
|
|
"self_intersections": self_intersection_geoms,
|
|
"total_length": total_length,
|
|
"connectivity_ok": len(connectivity_errors) == 0,
|
|
}
|