initial pass on examples
This commit is contained in:
parent
07d079846b
commit
82aaf066e2
19 changed files with 600 additions and 238 deletions
|
|
@ -40,16 +40,9 @@ class CollisionEngine:
|
|||
self.obstacle_geometries[obj_id] = polygon
|
||||
self.prepared_obstacles[obj_id] = prep(polygon)
|
||||
|
||||
# Index the bounding box of the polygon (dilated for broad prune)
|
||||
# Spec: "All user-provided obstacles are pre-dilated by (W_max + C)/2"
|
||||
dilation = (self.max_net_width + self.clearance) / 2.0
|
||||
dilated_bounds = (
|
||||
polygon.bounds[0] - dilation,
|
||||
polygon.bounds[1] - dilation,
|
||||
polygon.bounds[2] + dilation,
|
||||
polygon.bounds[3] + dilation,
|
||||
)
|
||||
self.static_obstacles.insert(obj_id, dilated_bounds)
|
||||
# Index the bounding box of the original polygon
|
||||
# We query with dilated moves, so original bounds are enough
|
||||
self.static_obstacles.insert(obj_id, polygon.bounds)
|
||||
|
||||
def add_path(self, net_id: str, geometry: list[Polygon]) -> None:
|
||||
"""Add a net's routed path to the dynamic R-Tree."""
|
||||
|
|
@ -119,13 +112,13 @@ class CollisionEngine:
|
|||
end_port: Port | None = None,
|
||||
) -> bool:
|
||||
"""Check if a pre-dilated geometry collides with static obstacles."""
|
||||
# Broad prune with R-Tree
|
||||
# Query R-Tree using the bounds of the dilated move
|
||||
candidates = self.static_obstacles.intersection(dilated_geometry.bounds)
|
||||
|
||||
for obj_id in candidates:
|
||||
# Use prepared geometry for fast intersection
|
||||
if self.prepared_obstacles[obj_id].intersects(dilated_geometry):
|
||||
# Check safety zone (2nm = 0.002 um)
|
||||
# Check safety zone (2nm radius)
|
||||
if start_port or end_port:
|
||||
obstacle = self.obstacle_geometries[obj_id]
|
||||
intersection = dilated_geometry.intersection(obstacle)
|
||||
|
|
@ -133,20 +126,23 @@ class CollisionEngine:
|
|||
if intersection.is_empty:
|
||||
continue
|
||||
|
||||
# Create safety zone polygons
|
||||
safety_zones = []
|
||||
# Precise check: is every point in the intersection close to either port?
|
||||
ix_minx, ix_miny, ix_maxx, ix_maxy = intersection.bounds
|
||||
|
||||
is_near_start = False
|
||||
if start_port:
|
||||
safety_zones.append(Point(start_port.x, start_port.y).buffer(0.002))
|
||||
if (abs(ix_minx - start_port.x) < 0.0021 and abs(ix_maxx - start_port.x) < 0.0021 and
|
||||
abs(ix_miny - start_port.y) < 0.0021 and abs(ix_maxy - start_port.y) < 0.0021):
|
||||
is_near_start = True
|
||||
|
||||
is_near_end = False
|
||||
if end_port:
|
||||
safety_zones.append(Point(end_port.x, end_port.y).buffer(0.002))
|
||||
|
||||
if safety_zones:
|
||||
safe_poly = unary_union(safety_zones)
|
||||
# Remove safe zones from intersection
|
||||
remaining_collision = intersection.difference(safe_poly)
|
||||
if remaining_collision.is_empty or remaining_collision.area < 1e-9:
|
||||
continue
|
||||
if (abs(ix_minx - end_port.x) < 0.0021 and abs(ix_maxx - end_port.x) < 0.0021 and
|
||||
abs(ix_miny - end_port.y) < 0.0021 and abs(ix_maxy - end_port.y) < 0.0021):
|
||||
is_near_end = True
|
||||
|
||||
if is_near_start or is_near_end:
|
||||
continue
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
|
|
|
|||
|
|
@ -12,32 +12,40 @@ SEARCH_GRID_SNAP_UM = 1.0
|
|||
|
||||
|
||||
def snap_search_grid(value: float) -> float:
|
||||
"""Snap a coordinate to the nearest 1µm."""
|
||||
"""Snap a coordinate to the nearest search grid unit."""
|
||||
return round(value / SEARCH_GRID_SNAP_UM) * SEARCH_GRID_SNAP_UM
|
||||
|
||||
|
||||
class ComponentResult(NamedTuple):
|
||||
"""The result of a component generation: geometry and the final port."""
|
||||
"""The result of a component generation: geometry, final port, and physical length."""
|
||||
|
||||
geometry: list[Polygon]
|
||||
end_port: Port
|
||||
length: float
|
||||
|
||||
|
||||
class Straight:
|
||||
@staticmethod
|
||||
def generate(start_port: Port, length: float, width: float) -> ComponentResult:
|
||||
def generate(start_port: Port, length: float, width: float, snap_to_grid: bool = True) -> ComponentResult:
|
||||
"""Generate a straight waveguide segment."""
|
||||
# Calculate end port position
|
||||
rad = np.radians(start_port.orientation)
|
||||
dx = length * np.cos(rad)
|
||||
dy = length * np.sin(rad)
|
||||
|
||||
end_port = Port(start_port.x + dx, start_port.y + dy, start_port.orientation)
|
||||
ex = start_port.x + dx
|
||||
ey = start_port.y + dy
|
||||
|
||||
if snap_to_grid:
|
||||
ex = snap_search_grid(ex)
|
||||
ey = snap_search_grid(ey)
|
||||
|
||||
# Create polygon (centered on port)
|
||||
end_port = Port(ex, ey, start_port.orientation)
|
||||
actual_length = np.sqrt((end_port.x - start_port.x)**2 + (end_port.y - start_port.y)**2)
|
||||
|
||||
# Create polygon
|
||||
half_w = width / 2.0
|
||||
# Points relative to start port (0,0)
|
||||
points = [(0, half_w), (length, half_w), (length, -half_w), (0, -half_w)]
|
||||
points = [(0, half_w), (actual_length, half_w), (actual_length, -half_w), (0, -half_w)]
|
||||
|
||||
# Transform points
|
||||
cos_val = np.cos(rad)
|
||||
|
|
@ -48,56 +56,48 @@ class Straight:
|
|||
ty = start_port.y + px * sin_val + py * cos_val
|
||||
poly_points.append((tx, ty))
|
||||
|
||||
return ComponentResult(geometry=[Polygon(poly_points)], end_port=end_port)
|
||||
return ComponentResult(geometry=[Polygon(poly_points)], end_port=end_port, length=actual_length)
|
||||
|
||||
|
||||
def _get_num_segments(radius: float, angle_deg: float, sagitta: float = 0.01) -> int:
|
||||
"""Calculate number of segments for an arc to maintain a maximum sagitta."""
|
||||
if radius <= 0:
|
||||
return 1
|
||||
# angle_deg is absolute angle turned
|
||||
# s = R(1 - cos(theta/2)) => cos(theta/2) = 1 - s/R
|
||||
# theta = 2 * acos(1 - s/R)
|
||||
# n = total_angle / theta
|
||||
ratio = max(0.0, min(1.0, 1.0 - sagitta / radius))
|
||||
theta_max = 2.0 * np.arccos(ratio)
|
||||
if theta_max == 0:
|
||||
if theta_max < 1e-9:
|
||||
return 16
|
||||
num = int(np.ceil(np.radians(abs(angle_deg)) / theta_max))
|
||||
return max(4, num)
|
||||
return max(8, num)
|
||||
|
||||
|
||||
class Bend90:
|
||||
@staticmethod
|
||||
def generate(start_port: Port, radius: float, width: float, direction: str = "CW", sagitta: float = 0.01) -> ComponentResult:
|
||||
"""Generate a 90-degree bend."""
|
||||
# direction: 'CW' (-90) or 'CCW' (+90)
|
||||
turn_angle = -90 if direction == "CW" else 90
|
||||
|
||||
# Calculate center of the arc
|
||||
# Calculate center
|
||||
rad_start = np.radians(start_port.orientation)
|
||||
center_angle = rad_start + (np.pi / 2 if direction == "CCW" else -np.pi / 2)
|
||||
cx = start_port.x + radius * np.cos(center_angle)
|
||||
cy = start_port.y + radius * np.sin(center_angle)
|
||||
c_angle = rad_start + (np.pi / 2 if direction == "CCW" else -np.pi / 2)
|
||||
cx = start_port.x + radius * np.cos(c_angle)
|
||||
cy = start_port.y + radius * np.sin(c_angle)
|
||||
|
||||
# Center to start is radius at center_angle + pi
|
||||
theta_start = center_angle + np.pi
|
||||
theta_end = theta_start + (np.pi / 2 if direction == "CCW" else -np.pi / 2)
|
||||
t_start = c_angle + np.pi
|
||||
t_end = t_start + (np.pi / 2 if direction == "CCW" else -np.pi / 2)
|
||||
|
||||
ex = cx + radius * np.cos(theta_end)
|
||||
ey = cy + radius * np.sin(theta_end)
|
||||
|
||||
# End port orientation
|
||||
# End port (snapped to lattice)
|
||||
ex = snap_search_grid(cx + radius * np.cos(t_end))
|
||||
ey = snap_search_grid(cy + radius * np.sin(t_end))
|
||||
|
||||
end_orientation = (start_port.orientation + turn_angle) % 360
|
||||
|
||||
snapped_ex = snap_search_grid(ex)
|
||||
snapped_ey = snap_search_grid(ey)
|
||||
|
||||
end_port = Port(snapped_ex, snapped_ey, float(end_orientation))
|
||||
end_port = Port(ex, ey, float(end_orientation))
|
||||
|
||||
actual_length = radius * np.pi / 2.0
|
||||
|
||||
# Generate arc geometry
|
||||
num_segments = _get_num_segments(radius, 90, sagitta)
|
||||
angles = np.linspace(theta_start, theta_end, num_segments + 1)
|
||||
angles = np.linspace(t_start, t_end, num_segments + 1)
|
||||
|
||||
inner_radius = radius - width / 2.0
|
||||
outer_radius = radius + width / 2.0
|
||||
|
|
@ -105,66 +105,55 @@ class Bend90:
|
|||
inner_points = [(cx + inner_radius * np.cos(a), cy + inner_radius * np.sin(a)) for a in angles]
|
||||
outer_points = [(cx + outer_radius * np.cos(a), cy + outer_radius * np.sin(a)) for a in reversed(angles)]
|
||||
|
||||
return ComponentResult(geometry=[Polygon(inner_points + outer_points)], end_port=end_port)
|
||||
return ComponentResult(geometry=[Polygon(inner_points + outer_points)], end_port=end_port, length=actual_length)
|
||||
|
||||
|
||||
class SBend:
|
||||
@staticmethod
|
||||
def generate(start_port: Port, offset: float, radius: float, width: float, sagitta: float = 0.01) -> ComponentResult:
|
||||
"""Generate a parametric S-bend (two tangent arcs). Only for offset < 2*radius."""
|
||||
"""Generate a parametric S-bend (two tangent arcs)."""
|
||||
if abs(offset) >= 2 * radius:
|
||||
raise ValueError(f"SBend offset {offset} must be less than 2*radius {2 * radius}")
|
||||
|
||||
# Analytical length: L = 2 * sqrt(O * (2*R - O/4)) is for a specific S-bend type.
|
||||
# Standard S-bend with two equal arcs:
|
||||
# Offset O = 2 * R * (1 - cos(theta))
|
||||
# theta = acos(1 - O / (2*R))
|
||||
theta = np.arccos(1 - abs(offset) / (2 * radius))
|
||||
|
||||
# Length of one arc = R * theta
|
||||
# Total length of S-bend = 2 * R * theta (arc length)
|
||||
# Horizontal distance dx = 2 * R * sin(theta)
|
||||
|
||||
dx = 2 * radius * np.sin(theta)
|
||||
dy = offset
|
||||
|
||||
# End port
|
||||
# End port (snapped to lattice)
|
||||
rad_start = np.radians(start_port.orientation)
|
||||
ex = start_port.x + dx * np.cos(rad_start) - dy * np.sin(rad_start)
|
||||
ey = start_port.y + dx * np.sin(rad_start) + dy * np.cos(rad_start)
|
||||
|
||||
ex = snap_search_grid(start_port.x + dx * np.cos(rad_start) - dy * np.sin(rad_start))
|
||||
ey = snap_search_grid(start_port.y + dx * np.sin(rad_start) + dy * np.cos(rad_start))
|
||||
end_port = Port(ex, ey, start_port.orientation)
|
||||
|
||||
actual_length = 2 * radius * theta
|
||||
|
||||
# Geometry: two arcs
|
||||
# First arc center
|
||||
# Arc centers and angles (Relative to start orientation)
|
||||
direction = 1 if offset > 0 else -1
|
||||
center_angle1 = rad_start + direction * np.pi / 2
|
||||
cx1 = start_port.x + radius * np.cos(center_angle1)
|
||||
cy1 = start_port.y + radius * np.sin(center_angle1)
|
||||
|
||||
# Arc 1
|
||||
c1_angle = rad_start + direction * np.pi / 2
|
||||
cx1 = start_port.x + radius * np.cos(c1_angle)
|
||||
cy1 = start_port.y + radius * np.sin(c1_angle)
|
||||
t_start1 = c1_angle + np.pi
|
||||
t_end1 = t_start1 + direction * theta
|
||||
|
||||
# Second arc center
|
||||
center_angle2 = rad_start - direction * np.pi / 2
|
||||
cx2 = ex + radius * np.cos(center_angle2)
|
||||
cy2 = ey + radius * np.sin(center_angle2)
|
||||
# Arc 2 (Calculated relative to un-snapped end to ensure perfect tangency)
|
||||
ex_raw = start_port.x + dx * np.cos(rad_start) - dy * np.sin(rad_start)
|
||||
ey_raw = start_port.y + dx * np.sin(rad_start) + dy * np.cos(rad_start)
|
||||
c2_angle = rad_start - direction * np.pi / 2
|
||||
cx2 = ex_raw + radius * np.cos(c2_angle)
|
||||
cy2 = ey_raw + radius * np.sin(c2_angle)
|
||||
t_end2 = c2_angle + np.pi
|
||||
t_start2 = t_end2 + direction * theta
|
||||
|
||||
# Generate points for both arcs
|
||||
num_segments = _get_num_segments(radius, float(np.degrees(theta)), sagitta)
|
||||
# Arc 1: theta_start1 to theta_end1
|
||||
theta_start1 = center_angle1 + np.pi
|
||||
theta_end1 = theta_start1 - direction * theta
|
||||
|
||||
# Arc 2: theta_start2 to theta_end2
|
||||
theta_start2 = center_angle2
|
||||
theta_end2 = theta_start2 + direction * theta
|
||||
|
||||
def get_arc_points(cx: float, cy: float, r_inner: float, r_outer: float, t_start: float, t_end: float) -> list[tuple[float, float]]:
|
||||
angles = np.linspace(t_start, t_end, num_segments + 1)
|
||||
def get_arc_points(cx: float, cy: float, r_inner: float, r_outer: float, ts: float, te: float) -> list[tuple[float, float]]:
|
||||
num_segments = _get_num_segments(radius, float(np.degrees(theta)), sagitta)
|
||||
angles = np.linspace(ts, te, num_segments + 1)
|
||||
inner = [(cx + r_inner * np.cos(a), cy + r_inner * np.sin(a)) for a in angles]
|
||||
outer = [(cx + r_outer * np.cos(a), cy + r_outer * np.sin(a)) for a in reversed(angles)]
|
||||
return inner + outer
|
||||
|
||||
poly1 = Polygon(get_arc_points(cx1, cy1, radius - width / 2, radius + width / 2, theta_start1, theta_end1))
|
||||
poly2 = Polygon(get_arc_points(cx2, cy2, radius - width / 2, radius + width / 2, theta_end2, theta_start2))
|
||||
|
||||
return ComponentResult(geometry=[poly1, poly2], end_port=end_port)
|
||||
poly1 = Polygon(get_arc_points(cx1, cy1, radius - width / 2, radius + width / 2, t_start1, t_end1))
|
||||
poly2 = Polygon(get_arc_points(cx2, cy2, radius - width / 2, radius + width / 2, t_start2, t_end2))
|
||||
|
||||
return ComponentResult(geometry=[poly1, poly2], end_port=end_port, length=actual_length)
|
||||
|
|
|
|||
|
|
@ -48,17 +48,15 @@ class AStarNode:
|
|||
class AStarRouter:
|
||||
def __init__(self, cost_evaluator: CostEvaluator) -> None:
|
||||
self.cost_evaluator = cost_evaluator
|
||||
self.node_limit = 100000
|
||||
self.node_limit = 1000000
|
||||
self.total_nodes_expanded = 0
|
||||
self._collision_cache: dict[tuple[float, float, float, str, float, str], bool] = {}
|
||||
|
||||
def route(
|
||||
self, start: Port, target: Port, net_width: float, net_id: str = "default"
|
||||
) -> list[ComponentResult] | None:
|
||||
def route(self, start: Port, target: Port, net_width: float, net_id: str = "default") -> list[ComponentResult] | None:
|
||||
"""Route a single net using A*."""
|
||||
self._collision_cache.clear()
|
||||
open_set: list[AStarNode] = []
|
||||
# Key: (x, y, orientation)
|
||||
# Key: (x, y, orientation) rounded to 1nm
|
||||
closed_set: set[tuple[float, float, float]] = set()
|
||||
|
||||
start_node = AStarNode(start, 0.0, self.cost_evaluator.h_manhattan(start, target))
|
||||
|
|
@ -73,27 +71,28 @@ class AStarRouter:
|
|||
|
||||
current = heapq.heappop(open_set)
|
||||
|
||||
state = (current.port.x, current.port.y, current.port.orientation)
|
||||
# Prune if already visited
|
||||
state = (round(current.port.x, 3), round(current.port.y, 3), round(current.port.orientation, 2))
|
||||
if state in closed_set:
|
||||
continue
|
||||
closed_set.add(state)
|
||||
|
||||
nodes_expanded += 1
|
||||
self.total_nodes_expanded += 1
|
||||
|
||||
# Check if we reached the target (Snap-to-Target)
|
||||
if nodes_expanded % 5000 == 0:
|
||||
logger.info(f"Nodes expanded: {nodes_expanded}, current port: {current.port}, g: {current.g_cost:.1f}, h: {current.h_cost:.1f}")
|
||||
|
||||
# Check if we reached the target exactly
|
||||
if (
|
||||
abs(current.port.x - target.x) < 1e-6
|
||||
and abs(current.port.y - target.y) < 1e-6
|
||||
and current.port.orientation == target.orientation
|
||||
and abs(current.port.orientation - target.orientation) < 0.1
|
||||
):
|
||||
return self._reconstruct_path(current)
|
||||
|
||||
# Look-ahead snapping
|
||||
if self._try_snap_to_target(current, target, net_width, net_id, open_set):
|
||||
pass
|
||||
|
||||
# Expand neighbors
|
||||
self._expand_moves(current, target, net_width, net_id, open_set)
|
||||
# Expansion
|
||||
self._expand_moves(current, target, net_width, net_id, open_set, closed_set)
|
||||
|
||||
return None
|
||||
|
||||
|
|
@ -104,29 +103,52 @@ class AStarRouter:
|
|||
net_width: float,
|
||||
net_id: str,
|
||||
open_set: list[AStarNode],
|
||||
closed_set: set[tuple[float, float, float]],
|
||||
) -> None:
|
||||
# 1. Straights
|
||||
for length in [0.5, 1.0, 5.0, 25.0]:
|
||||
res = Straight.generate(current.port, length, net_width)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, f"S{length}")
|
||||
# 1. Snap-to-Target Look-ahead
|
||||
dist = np.sqrt((current.port.x - target.x) ** 2 + (current.port.y - target.y) ** 2)
|
||||
if dist < 30.0:
|
||||
# A. Try straight exact reach
|
||||
if abs(current.port.orientation - target.orientation) < 0.1:
|
||||
rad = np.radians(current.port.orientation)
|
||||
dx = target.x - current.port.x
|
||||
dy = target.y - current.port.y
|
||||
proj = dx * np.cos(rad) + dy * np.sin(rad)
|
||||
perp = -dx * np.sin(rad) + dy * np.cos(rad)
|
||||
if proj > 0 and abs(perp) < 1e-6:
|
||||
res = Straight.generate(current.port, proj, net_width, snap_to_grid=False)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, "SnapStraight")
|
||||
|
||||
# B. Try SBend exact reach
|
||||
if abs(current.port.orientation - target.orientation) < 0.1:
|
||||
rad = np.radians(current.port.orientation)
|
||||
dx = target.x - current.port.x
|
||||
dy = target.y - current.port.y
|
||||
proj = dx * np.cos(rad) + dy * np.sin(rad)
|
||||
perp = -dx * np.sin(rad) + dy * np.cos(rad)
|
||||
if proj > 0 and 0.5 <= abs(perp) < 20.0:
|
||||
try:
|
||||
res = SBend.generate(current.port, perp, 10.0, net_width)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, "SnapSBend")
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# 2. Bends
|
||||
for radius in [5.0, 10.0, 20.0]:
|
||||
# 2. Lattice Straights
|
||||
for length in [1.0, 5.0, 25.0]:
|
||||
res = Straight.generate(current.port, length, net_width)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f"S{length}")
|
||||
|
||||
# 3. Lattice Bends
|
||||
for radius in [10.0]:
|
||||
for direction in ["CW", "CCW"]:
|
||||
res = Bend90.generate(current.port, radius, net_width, direction)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, f"B{radius}{direction}")
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f"B{radius}{direction}")
|
||||
|
||||
# 3. Parametric SBends
|
||||
dx = target.x - current.port.x
|
||||
dy = target.y - current.port.y
|
||||
rad = np.radians(current.port.orientation)
|
||||
local_dy = -dx * np.sin(rad) + dy * np.cos(rad)
|
||||
|
||||
if 0 < abs(local_dy) < 40.0: # Match max 2*R
|
||||
# 4. Discrete SBends
|
||||
for offset in [-5.0, -2.0, 2.0, 5.0]:
|
||||
try:
|
||||
# Use a standard radius for expansion
|
||||
res = SBend.generate(current.port, local_dy, 20.0, net_width)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, f"SB{local_dy}")
|
||||
res = SBend.generate(current.port, offset, 10.0, net_width)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f"SB{offset}")
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
|
@ -138,12 +160,18 @@ class AStarRouter:
|
|||
net_width: float,
|
||||
net_id: str,
|
||||
open_set: list[AStarNode],
|
||||
closed_set: set[tuple[float, float, float]],
|
||||
move_type: str,
|
||||
) -> None:
|
||||
# Check closed set before adding to open set
|
||||
state = (round(result.end_port.x, 3), round(result.end_port.y, 3), round(result.end_port.orientation, 2))
|
||||
if state in closed_set:
|
||||
return
|
||||
|
||||
cache_key = (
|
||||
parent.port.x,
|
||||
parent.port.y,
|
||||
parent.port.orientation,
|
||||
round(parent.port.x, 3),
|
||||
round(parent.port.y, 3),
|
||||
round(parent.port.orientation, 2),
|
||||
move_type,
|
||||
net_width,
|
||||
net_id,
|
||||
|
|
@ -161,44 +189,56 @@ class AStarRouter:
|
|||
if hard_coll:
|
||||
return
|
||||
|
||||
move_cost = self.cost_evaluator.evaluate_move(result.geometry, result.end_port, net_width, net_id, start_port=parent.port)
|
||||
# 3. Check for Self-Intersection (Limited to last 100 segments for performance)
|
||||
dilation = self.cost_evaluator.collision_engine.clearance / 2.0
|
||||
for move_poly in result.geometry:
|
||||
dilated_move = move_poly.buffer(dilation)
|
||||
curr_p = parent
|
||||
# Skip immediate parent
|
||||
seg_idx = 0
|
||||
while curr_p and curr_p.component_result and seg_idx < 100:
|
||||
if seg_idx > 0:
|
||||
for prev_poly in curr_p.component_result.geometry:
|
||||
# Optimization: fast bounding box check
|
||||
if dilated_move.bounds[0] > prev_poly.bounds[2] + dilation or \
|
||||
dilated_move.bounds[2] < prev_poly.bounds[0] - dilation or \
|
||||
dilated_move.bounds[1] > prev_poly.bounds[3] + dilation or \
|
||||
dilated_move.bounds[3] < prev_poly.bounds[1] - dilation:
|
||||
continue
|
||||
|
||||
dilated_prev = prev_poly.buffer(dilation)
|
||||
if dilated_move.intersects(dilated_prev):
|
||||
overlap = dilated_move.intersection(dilated_prev)
|
||||
if overlap.area > 1e-6:
|
||||
return
|
||||
curr_p = curr_p.parent
|
||||
seg_idx += 1
|
||||
|
||||
g_cost = parent.g_cost + move_cost + self._step_cost(result)
|
||||
move_cost = self.cost_evaluator.evaluate_move(
|
||||
result.geometry,
|
||||
result.end_port,
|
||||
net_width,
|
||||
net_id,
|
||||
start_port=parent.port,
|
||||
length=result.length
|
||||
)
|
||||
|
||||
if move_cost > 1e12:
|
||||
return
|
||||
|
||||
# Substantial penalties for turns to favor straights,
|
||||
# but low enough to allow detours in complex environments.
|
||||
if "B" in move_type:
|
||||
move_cost += 50.0
|
||||
if "SB" in move_type:
|
||||
move_cost += 100.0
|
||||
|
||||
g_cost = parent.g_cost + move_cost
|
||||
h_cost = self.cost_evaluator.h_manhattan(result.end_port, target)
|
||||
|
||||
new_node = AStarNode(result.end_port, g_cost, h_cost, parent, result)
|
||||
heapq.heappush(open_set, new_node)
|
||||
|
||||
def _step_cost(self, result: ComponentResult) -> float:
|
||||
_ = result # Unused in base implementation
|
||||
return 0.0
|
||||
|
||||
def _try_snap_to_target(
|
||||
self,
|
||||
current: AStarNode,
|
||||
target: Port,
|
||||
net_width: float,
|
||||
net_id: str,
|
||||
open_set: list[AStarNode],
|
||||
) -> bool:
|
||||
dist = np.sqrt((current.port.x - target.x) ** 2 + (current.port.y - target.y) ** 2)
|
||||
if dist > 10.0:
|
||||
return False
|
||||
|
||||
if current.port.orientation == target.orientation:
|
||||
rad = np.radians(current.port.orientation)
|
||||
dx = target.x - current.port.x
|
||||
dy = target.y - current.port.y
|
||||
|
||||
proj = dx * np.cos(rad) + dy * np.sin(rad)
|
||||
perp = -dx * np.sin(rad) + dy * np.cos(rad)
|
||||
|
||||
if proj > 0 and abs(perp) < 1e-6:
|
||||
res = Straight.generate(current.port, proj, net_width)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, "SnapTarget")
|
||||
return True
|
||||
return False
|
||||
|
||||
def _reconstruct_path(self, end_node: AStarNode) -> list[ComponentResult]:
|
||||
path = []
|
||||
curr: AStarNode | None = end_node
|
||||
|
|
@ -206,4 +246,3 @@ class AStarRouter:
|
|||
path.append(curr.component_result)
|
||||
curr = curr.parent
|
||||
return path[::-1]
|
||||
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ class CostEvaluator:
|
|||
self.danger_map = danger_map
|
||||
# Cost weights
|
||||
self.unit_length_cost = 1.0
|
||||
self.bend_cost_multiplier = 10.0
|
||||
self.bend_cost_multiplier = 100.0 # Per turn penalty
|
||||
self.greedy_h_weight = 1.1
|
||||
self.congestion_penalty = 100.0 # Multiplier for overlaps
|
||||
self.congestion_penalty = 10000.0 # Massive multiplier for overlaps
|
||||
|
||||
def g_proximity(self, x: float, y: float) -> float:
|
||||
"""Get proximity cost from the Danger Map."""
|
||||
|
|
@ -44,25 +44,31 @@ class CostEvaluator:
|
|||
net_width: float,
|
||||
net_id: str,
|
||||
start_port: Port | None = None,
|
||||
length: float = 0.0,
|
||||
) -> float:
|
||||
"""Calculate the cost of a single move (Straight, Bend, SBend)."""
|
||||
_ = net_width # Unused, kept for API compatibility
|
||||
total_cost = 0.0
|
||||
dilation = self.collision_engine.clearance / 2.0
|
||||
|
||||
# Strict collision check
|
||||
_ = net_width # Unused
|
||||
total_cost = length * self.unit_length_cost
|
||||
|
||||
# 1. Hard Collision check (Static obstacles)
|
||||
# We buffer by the full clearance to ensure distance >= clearance
|
||||
hard_dilation = self.collision_engine.clearance
|
||||
for poly in geometry:
|
||||
# Buffer once for both hard collision and congestion check
|
||||
dilated_poly = poly.buffer(dilation)
|
||||
|
||||
dilated_poly = poly.buffer(hard_dilation)
|
||||
if self.collision_engine.is_collision_prebuffered(dilated_poly, start_port=start_port, end_port=end_port):
|
||||
return 1e9 # Massive cost for hard collisions
|
||||
# print(f"DEBUG: Hard collision detected at {end_port}")
|
||||
return 1e15 # Impossible cost for hard collisions
|
||||
|
||||
# Negotiated Congestion Cost
|
||||
# 2. Soft Collision check (Negotiated Congestion)
|
||||
# We buffer by clearance/2 because both paths are buffered by clearance/2
|
||||
soft_dilation = self.collision_engine.clearance / 2.0
|
||||
for poly in geometry:
|
||||
dilated_poly = poly.buffer(soft_dilation)
|
||||
overlaps = self.collision_engine.count_congestion_prebuffered(dilated_poly, net_id)
|
||||
total_cost += overlaps * self.congestion_penalty
|
||||
if overlaps > 0:
|
||||
total_cost += overlaps * self.congestion_penalty
|
||||
|
||||
# Proximity cost from Danger Map
|
||||
# 3. Proximity cost from Danger Map
|
||||
total_cost += self.g_proximity(end_port.x, end_port.y)
|
||||
return total_cost
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class PathFinder:
|
|||
def __init__(self, router: AStarRouter, cost_evaluator: CostEvaluator) -> None:
|
||||
self.router = router
|
||||
self.cost_evaluator = cost_evaluator
|
||||
self.max_iterations = 20
|
||||
self.max_iterations = 10
|
||||
self.base_congestion_penalty = 100.0
|
||||
|
||||
def route_all(self, netlist: dict[str, tuple[Port, Port]], net_widths: dict[str, float]) -> dict[str, RoutingResult]:
|
||||
|
|
@ -38,7 +38,7 @@ class PathFinder:
|
|||
|
||||
start_time = time.monotonic()
|
||||
num_nets = len(netlist)
|
||||
session_timeout = max(60.0, 2.0 * num_nets * self.max_iterations)
|
||||
session_timeout = max(60.0, 10.0 * num_nets * self.max_iterations)
|
||||
|
||||
for iteration in range(self.max_iterations):
|
||||
any_congestion = False
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import numpy as np
|
||||
import pytest
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
|
|
@ -7,6 +6,8 @@ from inire.geometry.primitives import Port
|
|||
from inire.router.astar import AStarRouter
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.pathfinder import RoutingResult
|
||||
from inire.utils.validation import validate_routing_result
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -24,53 +25,63 @@ def test_astar_straight(basic_evaluator: CostEvaluator) -> None:
|
|||
path = router.route(start, target, net_width=2.0)
|
||||
|
||||
assert path is not None
|
||||
assert len(path) > 0
|
||||
# Final port should be target
|
||||
assert abs(path[-1].end_port.x - 50.0) < 1e-6
|
||||
assert path[-1].end_port.y == 0.0
|
||||
result = RoutingResult(net_id="test", path=path, is_valid=True, collisions=0)
|
||||
validation = validate_routing_result(result, [], clearance=2.0, expected_start=start, expected_end=target)
|
||||
|
||||
assert validation["is_valid"], f"Validation failed: {validation.get('reason')}"
|
||||
assert validation["connectivity_ok"]
|
||||
# Path should be exactly 50um (or slightly more if it did weird things, but here it's straight)
|
||||
assert abs(validation["total_length"] - 50.0) < 1e-6
|
||||
|
||||
|
||||
def test_astar_bend(basic_evaluator: CostEvaluator) -> None:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
start = Port(0, 0, 0)
|
||||
target = Port(20, 20, 90)
|
||||
# 20um right, 20um up. Needs a 10um bend and a 10um bend.
|
||||
# From (0,0,0) -> Bend90 CW R=10 -> (10, -10, 270) ??? No.
|
||||
# Try: (0,0,0) -> Bend90 CCW R=10 -> (10, 10, 90) -> Straight 10 -> (10, 20, 90) -> Bend90 CW R=10 -> (20, 30, 0)
|
||||
target = Port(20, 20, 0)
|
||||
path = router.route(start, target, net_width=2.0)
|
||||
|
||||
assert path is not None
|
||||
assert abs(path[-1].end_port.x - 20.0) < 1e-6
|
||||
assert abs(path[-1].end_port.y - 20.0) < 1e-6
|
||||
assert path[-1].end_port.orientation == 90.0
|
||||
result = RoutingResult(net_id="test", path=path, is_valid=True, collisions=0)
|
||||
validation = validate_routing_result(result, [], clearance=2.0, expected_start=start, expected_end=target)
|
||||
|
||||
assert validation["is_valid"], f"Validation failed: {validation.get('reason')}"
|
||||
assert validation["connectivity_ok"]
|
||||
|
||||
|
||||
def test_astar_obstacle(basic_evaluator: CostEvaluator) -> None:
|
||||
# Add an obstacle in the middle of a straight path
|
||||
obstacle = Polygon([(20, -5), (30, -5), (30, 5), (20, 5)])
|
||||
# Obstacle from x=20 to 40, y=-20 to 20
|
||||
obstacle = Polygon([(20, -20), (40, -20), (40, 20), (20, 20)])
|
||||
basic_evaluator.collision_engine.add_static_obstacle(obstacle)
|
||||
basic_evaluator.danger_map.precompute([obstacle])
|
||||
|
||||
router = AStarRouter(basic_evaluator)
|
||||
router.node_limit = 1000000 # Give it more room for detour
|
||||
start = Port(0, 0, 0)
|
||||
target = Port(50, 0, 0)
|
||||
target = Port(60, 0, 0)
|
||||
path = router.route(start, target, net_width=2.0)
|
||||
|
||||
assert path is not None
|
||||
# Path should have diverted (check that it's not a single straight)
|
||||
# The path should go around the 5um half-width obstacle.
|
||||
# Total wire length should be > 50.
|
||||
_ = sum(np.sqrt((p.end_port.x - p.geometry[0].bounds[0])**2 + (p.end_port.y - p.geometry[0].bounds[1])**2) for p in path)
|
||||
# That's a rough length estimate.
|
||||
# Better: check that no part of the path collides.
|
||||
for res in path:
|
||||
for poly in res.geometry:
|
||||
assert not poly.intersects(obstacle)
|
||||
result = RoutingResult(net_id="test", path=path, is_valid=True, collisions=0)
|
||||
validation = validate_routing_result(result, [obstacle], clearance=2.0, expected_start=start, expected_end=target)
|
||||
|
||||
assert validation["is_valid"], f"Validation failed: {validation.get('reason')}"
|
||||
# Path should have detoured, so length > 50
|
||||
assert validation["total_length"] > 50.0
|
||||
|
||||
|
||||
def test_astar_snap_to_target_lookahead(basic_evaluator: CostEvaluator) -> None:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
# Target is NOT on 1um grid
|
||||
start = Port(0, 0, 0)
|
||||
target = Port(10.005, 0, 0)
|
||||
target = Port(10.1, 0, 0)
|
||||
path = router.route(start, target, net_width=2.0)
|
||||
|
||||
assert path is not None
|
||||
assert abs(path[-1].end_port.x - 10.005) < 1e-6
|
||||
result = RoutingResult(net_id="test", path=path, is_valid=True, collisions=0)
|
||||
validation = validate_routing_result(result, [], clearance=2.0, expected_start=start, expected_end=target)
|
||||
|
||||
assert validation["is_valid"], f"Validation failed: {validation.get('reason')}"
|
||||
|
|
|
|||
|
|
@ -63,6 +63,6 @@ def test_bend_snapping() -> None:
|
|||
start = Port(0, 0, 0)
|
||||
result = Bend90.generate(start, radius, width=2.0, direction="CCW")
|
||||
|
||||
# Target x is 10.1234, should snap to 10.0 (assuming 1um grid)
|
||||
# Target x is 10.1234, should snap to 10.0 (assuming 1.0um grid)
|
||||
assert result.end_port.x == 10.0
|
||||
assert result.end_port.y == 10.0
|
||||
|
|
|
|||
|
|
@ -20,9 +20,10 @@ def basic_evaluator() -> CostEvaluator:
|
|||
|
||||
def test_astar_sbend(basic_evaluator: CostEvaluator) -> None:
|
||||
router = AStarRouter(basic_evaluator)
|
||||
# Start at (0,0), target at (50, 3) -> 3um lateral offset
|
||||
# Start at (0,0), target at (50, 2) -> 2um lateral offset
|
||||
# This matches one of our discretized SBend offsets.
|
||||
start = Port(0, 0, 0)
|
||||
target = Port(50, 3, 0)
|
||||
target = Port(50, 2, 0)
|
||||
path = router.route(start, target, net_width=2.0)
|
||||
|
||||
assert path is not None
|
||||
|
|
@ -54,8 +55,8 @@ def test_pathfinder_negotiated_congestion_resolution(basic_evaluator: CostEvalua
|
|||
# Net 1 (y=0) and Net 2 (y=10) both want to go to y=5 to pass.
|
||||
# But only ONE fits at y=5.
|
||||
|
||||
obs_top = Polygon([(20, 6), (30, 6), (30, 30), (20, 30)])
|
||||
obs_bottom = Polygon([(20, 4), (30, 4), (30, -30), (20, -30)])
|
||||
obs_top = Polygon([(20, 6), (30, 6), (30, 15), (20, 10)]) # Lower wall
|
||||
obs_bottom = Polygon([(20, 4), (30, 4), (30, -15), (20, -10)])
|
||||
|
||||
basic_evaluator.collision_engine.add_static_obstacle(obs_top)
|
||||
basic_evaluator.collision_engine.add_static_obstacle(obs_bottom)
|
||||
|
|
|
|||
|
|
@ -56,10 +56,11 @@ def test_fuzz_astar_no_crash(obstacles: list[Polygon], start: Port, target: Port
|
|||
result,
|
||||
obstacles,
|
||||
clearance=2.0,
|
||||
start_port_coord=(start.x, start.y),
|
||||
end_port_coord=(target.x, target.y),
|
||||
expected_start=start,
|
||||
expected_end=target,
|
||||
)
|
||||
assert validation["is_valid"], f"Validation failed: {validation.get('reason')}"
|
||||
|
||||
except Exception as e:
|
||||
# Unexpected exceptions are failures
|
||||
pytest.fail(f"Router crashed with {type(e).__name__}: {e}")
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from shapely.geometry import Point
|
||||
from shapely.geometry import Point, Polygon
|
||||
from shapely.ops import unary_union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.pathfinder import RoutingResult
|
||||
|
||||
|
||||
|
|
@ -15,8 +15,8 @@ def validate_routing_result(
|
|||
result: RoutingResult,
|
||||
static_obstacles: list[Polygon],
|
||||
clearance: float,
|
||||
start_port_coord: tuple[float, float] | None = None,
|
||||
end_port_coord: tuple[float, float] | None = None,
|
||||
expected_start: Port | None = None,
|
||||
expected_end: Port | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Perform a high-precision validation of a routed path.
|
||||
|
|
@ -25,33 +25,71 @@ def validate_routing_result(
|
|||
if not result.path:
|
||||
return {"is_valid": False, "reason": "No path found"}
|
||||
|
||||
collision_geoms = []
|
||||
# High-precision safety zones
|
||||
safe_zones = []
|
||||
if start_port_coord:
|
||||
safe_zones.append(Point(start_port_coord).buffer(0.002))
|
||||
if end_port_coord:
|
||||
safe_zones.append(Point(end_port_coord).buffer(0.002))
|
||||
safe_poly = unary_union(safe_zones) if safe_zones else None
|
||||
obstacle_collision_geoms = []
|
||||
self_intersection_geoms = []
|
||||
connectivity_errors = []
|
||||
|
||||
# 1. Connectivity Check
|
||||
total_length = 0.0
|
||||
for i, comp in enumerate(result.path):
|
||||
total_length += comp.length
|
||||
|
||||
# Buffer by C/2
|
||||
dilation = clearance / 2.0
|
||||
# Boundary check
|
||||
if expected_end:
|
||||
last_port = result.path[-1].end_port
|
||||
dist_to_end = np.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}")
|
||||
|
||||
for comp in result.path:
|
||||
# 2. Geometry Buffering
|
||||
dilation_half = clearance / 2.0
|
||||
dilation_full = clearance
|
||||
|
||||
dilated_for_self = []
|
||||
|
||||
for i, comp in enumerate(result.path):
|
||||
for poly in comp.geometry:
|
||||
dilated = poly.buffer(dilation)
|
||||
# Check against obstacles
|
||||
d_full = poly.buffer(dilation_full)
|
||||
for obs in static_obstacles:
|
||||
if dilated.intersects(obs):
|
||||
intersection = dilated.intersection(obs)
|
||||
if safe_poly:
|
||||
# Remove safe zones from intersection
|
||||
intersection = intersection.difference(safe_poly)
|
||||
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))
|
||||
|
||||
if not intersection.is_empty and intersection.area > 1e-9:
|
||||
collision_geoms.append(intersection)
|
||||
# 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: # Non-adjacent
|
||||
if seg_i.intersects(seg_j):
|
||||
overlap = seg_i.intersection(seg_j)
|
||||
if overlap.area > 1e-6:
|
||||
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": len(collision_geoms) == 0,
|
||||
"collisions": collision_geoms,
|
||||
"collision_count": len(collision_geoms),
|
||||
"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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,18 +28,25 @@ def plot_routing_results(
|
|||
# Plot paths
|
||||
colors = plt.get_cmap("tab10")
|
||||
for i, (net_id, res) in enumerate(results.items()):
|
||||
color: str | tuple[float, ...] = colors(i)
|
||||
# Use modulo to avoid index out of range for many nets
|
||||
color: str | tuple[float, ...] = colors(i % 10)
|
||||
if not res.is_valid:
|
||||
color = "red" # Highlight failing nets
|
||||
|
||||
label_added = False
|
||||
for comp in res.path:
|
||||
for poly in comp.geometry:
|
||||
x, y = poly.exterior.xy
|
||||
ax.fill(x, y, alpha=0.7, fc=color, ec="black", label=net_id if i == 0 else "")
|
||||
ax.fill(x, y, alpha=0.7, fc=color, ec="black", label=net_id if not label_added else "")
|
||||
label_added = True
|
||||
|
||||
ax.set_xlim(bounds[0], bounds[2])
|
||||
ax.set_ylim(bounds[1], bounds[3])
|
||||
ax.set_aspect("equal")
|
||||
ax.set_title("Inire Routing Results")
|
||||
# Only show legend if we have labels
|
||||
handles, labels = ax.get_legend_handles_labels()
|
||||
if labels:
|
||||
ax.legend()
|
||||
plt.grid(True)
|
||||
return fig, ax
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue