more s-bend fixes and add docs
This commit is contained in:
parent
18b2f83a7b
commit
41a2d9f058
10 changed files with 249 additions and 116 deletions
|
|
@ -1,9 +1,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import NamedTuple
|
||||
from typing import NamedTuple, Literal, Union
|
||||
|
||||
import numpy as np
|
||||
from shapely.geometry import Polygon
|
||||
from shapely.geometry import Polygon, box
|
||||
from shapely.ops import unary_union
|
||||
|
||||
from .primitives import Port
|
||||
|
||||
|
|
@ -34,7 +35,7 @@ class Straight:
|
|||
|
||||
ex = start_port.x + dx
|
||||
ey = start_port.y + dy
|
||||
|
||||
|
||||
if snap_to_grid:
|
||||
ex = snap_search_grid(ex)
|
||||
ey = snap_search_grid(ey)
|
||||
|
|
@ -71,46 +72,85 @@ def _get_num_segments(radius: float, angle_deg: float, sagitta: float = 0.01) ->
|
|||
return max(8, num)
|
||||
|
||||
|
||||
def _get_arc_polygons(cx: float, cy: float, radius: float, width: float, t_start: float, t_end: float, sagitta: float = 0.01) -> list[Polygon]:
|
||||
"""Helper to generate arc-shaped polygons."""
|
||||
num_segments = _get_num_segments(radius, float(np.degrees(abs(t_end - t_start))), sagitta)
|
||||
angles = np.linspace(t_start, t_end, num_segments + 1)
|
||||
inner_radius = radius - width / 2.0
|
||||
outer_radius = radius + width / 2.0
|
||||
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 [Polygon(inner_points + outer_points)]
|
||||
|
||||
|
||||
def _apply_collision_model(
|
||||
arc_poly: Polygon,
|
||||
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon,
|
||||
radius: float,
|
||||
clip_margin: float = 10.0
|
||||
) -> list[Polygon]:
|
||||
"""Applies the specified collision model to an arc geometry."""
|
||||
if isinstance(collision_type, Polygon):
|
||||
return [collision_type]
|
||||
|
||||
if collision_type == "arc":
|
||||
return [arc_poly]
|
||||
|
||||
# Get bounding box
|
||||
minx, miny, maxx, maxy = arc_poly.bounds
|
||||
bbox = box(minx, miny, maxx, maxy)
|
||||
|
||||
if collision_type == "bbox":
|
||||
return [bbox]
|
||||
|
||||
if collision_type == "clipped_bbox":
|
||||
safe_zone = arc_poly.buffer(clip_margin)
|
||||
return [bbox.intersection(safe_zone)]
|
||||
|
||||
return [arc_poly]
|
||||
|
||||
|
||||
class Bend90:
|
||||
@staticmethod
|
||||
def generate(start_port: Port, radius: float, width: float, direction: str = "CW", sagitta: float = 0.01) -> ComponentResult:
|
||||
def generate(
|
||||
start_port: Port,
|
||||
radius: float,
|
||||
width: float,
|
||||
direction: str = "CW",
|
||||
sagitta: float = 0.01,
|
||||
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc",
|
||||
clip_margin: float = 10.0
|
||||
) -> ComponentResult:
|
||||
"""Generate a 90-degree bend."""
|
||||
turn_angle = -90 if direction == "CW" else 90
|
||||
|
||||
# Calculate center
|
||||
rad_start = np.radians(start_port.orientation)
|
||||
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)
|
||||
|
||||
t_start = c_angle + np.pi
|
||||
t_end = t_start + (np.pi / 2 if direction == "CCW" else -np.pi / 2)
|
||||
|
||||
# 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_port = Port(ex, ey, float((start_port.orientation + turn_angle) % 360))
|
||||
|
||||
arc_polys = _get_arc_polygons(cx, cy, radius, width, t_start, t_end, sagitta)
|
||||
collision_polys = _apply_collision_model(arc_polys[0], collision_type, radius, clip_margin)
|
||||
|
||||
end_orientation = (start_port.orientation + turn_angle) % 360
|
||||
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(t_start, t_end, num_segments + 1)
|
||||
|
||||
inner_radius = radius - width / 2.0
|
||||
outer_radius = radius + width / 2.0
|
||||
|
||||
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, length=actual_length)
|
||||
return ComponentResult(geometry=collision_polys, end_port=end_port, length=radius * np.pi / 2.0)
|
||||
|
||||
|
||||
class SBend:
|
||||
@staticmethod
|
||||
def generate(start_port: Port, offset: float, radius: float, width: float, sagitta: float = 0.01) -> ComponentResult:
|
||||
def generate(
|
||||
start_port: Port,
|
||||
offset: float,
|
||||
radius: float,
|
||||
width: float,
|
||||
sagitta: float = 0.01,
|
||||
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc",
|
||||
clip_margin: float = 10.0
|
||||
) -> ComponentResult:
|
||||
"""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}")
|
||||
|
|
@ -118,42 +158,28 @@ class SBend:
|
|||
theta = np.arccos(1 - abs(offset) / (2 * radius))
|
||||
dx = 2 * radius * np.sin(theta)
|
||||
dy = offset
|
||||
|
||||
# End port (snapped to lattice)
|
||||
rad_start = np.radians(start_port.orientation)
|
||||
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
|
||||
|
||||
# Arc centers and angles (Relative to start orientation)
|
||||
|
||||
direction = 1 if offset > 0 else -1
|
||||
|
||||
# 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
|
||||
ts1, te1 = c1_angle + np.pi, c1_angle + np.pi + direction * theta
|
||||
|
||||
# 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
|
||||
te2 = c2_angle + np.pi
|
||||
ts2 = te2 + direction * theta
|
||||
|
||||
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, 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)
|
||||
arc1 = _get_arc_polygons(cx1, cy1, radius, width, ts1, te1, sagitta)[0]
|
||||
arc2 = _get_arc_polygons(cx2, cy2, radius, width, ts2, te2, sagitta)[0]
|
||||
combined_arc = unary_union([arc1, arc2])
|
||||
|
||||
collision_polys = _apply_collision_model(combined_arc, collision_type, radius, clip_margin)
|
||||
return ComponentResult(geometry=collision_polys, end_port=end_port, length=2 * radius * theta)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||
|
||||
import heapq
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
|
@ -60,6 +60,8 @@ class AStarRouter:
|
|||
snap_to_target_dist: float = 20.0,
|
||||
bend_penalty: float = 50.0,
|
||||
sbend_penalty: float = 100.0,
|
||||
bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] = "arc",
|
||||
bend_clip_margin: float = 10.0,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the A* Router.
|
||||
|
|
@ -74,6 +76,8 @@ class AStarRouter:
|
|||
snap_to_target_dist: Distance threshold for lookahead snapping.
|
||||
bend_penalty: Flat cost penalty for each 90-degree bend.
|
||||
sbend_penalty: Flat cost penalty for each S-bend.
|
||||
bend_collision_type: Type of collision model for bends ('arc', 'bbox', 'clipped_bbox').
|
||||
bend_clip_margin: Margin for 'clipped_bbox' collision model.
|
||||
"""
|
||||
self.cost_evaluator = cost_evaluator
|
||||
self.config = RouterConfig(
|
||||
|
|
@ -85,6 +89,8 @@ class AStarRouter:
|
|||
snap_to_target_dist=snap_to_target_dist,
|
||||
bend_penalty=bend_penalty,
|
||||
sbend_penalty=sbend_penalty,
|
||||
bend_collision_type=bend_collision_type,
|
||||
bend_clip_margin=bend_clip_margin,
|
||||
)
|
||||
self.node_limit = self.config.node_limit
|
||||
self.total_nodes_expanded = 0
|
||||
|
|
@ -167,8 +173,15 @@ class AStarRouter:
|
|||
if proj > 0 and 0.5 <= abs(perp) < 20.0:
|
||||
for radius in self.config.sbend_radii:
|
||||
try:
|
||||
res = SBend.generate(current.port, perp, radius, net_width)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, "SnapSBend")
|
||||
res = SBend.generate(
|
||||
current.port,
|
||||
perp,
|
||||
radius,
|
||||
net_width,
|
||||
collision_type=self.config.bend_collision_type,
|
||||
clip_margin=self.config.bend_clip_margin
|
||||
)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, "SnapSBend", move_radius=radius)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
|
@ -185,15 +198,29 @@ class AStarRouter:
|
|||
# 3. Lattice Bends
|
||||
for radius in self.config.bend_radii:
|
||||
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, closed_set, f"B{radius}{direction}")
|
||||
res = Bend90.generate(
|
||||
current.port,
|
||||
radius,
|
||||
net_width,
|
||||
direction,
|
||||
collision_type=self.config.bend_collision_type,
|
||||
clip_margin=self.config.bend_clip_margin
|
||||
)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f"B{radius}{direction}", move_radius=radius)
|
||||
|
||||
# 4. Discrete SBends
|
||||
for offset in self.config.sbend_offsets:
|
||||
for radius in self.config.sbend_radii:
|
||||
try:
|
||||
res = SBend.generate(current.port, offset, radius, net_width)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f"SB{offset}R{radius}")
|
||||
res = SBend.generate(
|
||||
current.port,
|
||||
offset,
|
||||
radius,
|
||||
net_width,
|
||||
collision_type=self.config.bend_collision_type,
|
||||
clip_margin=self.config.bend_clip_margin
|
||||
)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f"SB{offset}R{radius}", move_radius=radius)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
|
@ -207,6 +234,7 @@ class AStarRouter:
|
|||
open_set: list[AStarNode],
|
||||
closed_set: set[tuple[float, float, float]],
|
||||
move_type: str,
|
||||
move_radius: float | None = None,
|
||||
) -> 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))
|
||||
|
|
@ -269,9 +297,19 @@ class AStarRouter:
|
|||
if move_cost > 1e12:
|
||||
return
|
||||
|
||||
if "B" in move_type:
|
||||
# Turn penalties scaled by radius to favor larger turns
|
||||
ref_radius = 10.0
|
||||
if "B" in move_type and move_radius is not None:
|
||||
# Scale penalty: larger radius -> smaller penalty
|
||||
# e.g. radius 10 -> factor 1.0, radius 30 -> factor 0.33
|
||||
penalty_factor = ref_radius / move_radius
|
||||
move_cost += self.config.bend_penalty * penalty_factor
|
||||
elif "SB" in move_type and move_radius is not None:
|
||||
penalty_factor = ref_radius / move_radius
|
||||
move_cost += self.config.sbend_penalty * penalty_factor
|
||||
elif "B" in move_type:
|
||||
move_cost += self.config.bend_penalty
|
||||
if "SB" in move_type:
|
||||
elif "SB" in move_type:
|
||||
move_cost += self.config.sbend_penalty
|
||||
|
||||
g_cost = parent.g_cost + move_cost
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Literal, TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from shapely.geometry import Polygon
|
||||
|
||||
|
||||
@dataclass
|
||||
class RouterConfig:
|
||||
"""Configuration parameters for the A* Router."""
|
||||
|
||||
node_limit: int = 500000
|
||||
node_limit: int = 1000000
|
||||
straight_lengths: list[float] = field(default_factory=lambda: [1.0, 5.0, 25.0])
|
||||
bend_radii: list[float] = field(default_factory=lambda: [10.0])
|
||||
sbend_offsets: list[float] = field(default_factory=lambda: [-5.0, -2.0, 2.0, 5.0])
|
||||
|
|
@ -15,6 +19,8 @@ class RouterConfig:
|
|||
snap_to_target_dist: float = 20.0
|
||||
bend_penalty: float = 50.0
|
||||
sbend_penalty: float = 100.0
|
||||
bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | Any = "arc"
|
||||
bend_clip_margin: float = 10.0
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
|||
|
|
@ -80,7 +80,6 @@ class CostEvaluator:
|
|||
for poly in geometry:
|
||||
dilated_poly = poly.buffer(hard_dilation)
|
||||
if self.collision_engine.is_collision_prebuffered(dilated_poly, start_port=start_port, end_port=end_port):
|
||||
# print(f"DEBUG: Hard collision detected at {end_port}")
|
||||
return 1e15 # Impossible cost for hard collisions
|
||||
|
||||
# 2. Soft Collision check (Negotiated Congestion)
|
||||
|
|
|
|||
|
|
@ -50,19 +50,43 @@ def test_sbend_generation() -> None:
|
|||
result = SBend.generate(start, offset, radius, width)
|
||||
assert result.end_port.y == 5.0
|
||||
assert result.end_port.orientation == 0.0
|
||||
assert len(result.geometry) == 2
|
||||
assert len(result.geometry) == 1 # Now uses unary_union
|
||||
|
||||
# Verify failure for large offset
|
||||
with pytest.raises(ValueError, match=r"SBend offset .* must be less than 2\*radius"):
|
||||
SBend.generate(start, 25.0, 10.0, 2.0)
|
||||
|
||||
|
||||
def test_bend_snapping() -> None:
|
||||
# Radius that results in non-integer coords
|
||||
radius = 10.1234
|
||||
def test_bend_collision_models() -> None:
|
||||
start = Port(0, 0, 0)
|
||||
result = Bend90.generate(start, radius, width=2.0, direction="CCW")
|
||||
radius = 10.0
|
||||
width = 2.0
|
||||
|
||||
# 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
|
||||
# 1. BBox model
|
||||
res_bbox = Bend90.generate(start, radius, width, direction="CCW", collision_type="bbox")
|
||||
# Arc CCW R=10 from (0,0,0) ends at (10,10,90).
|
||||
# Waveguide width is 2.0, so bbox will be slightly larger than (0,0,10,10)
|
||||
minx, miny, maxx, maxy = res_bbox.geometry[0].bounds
|
||||
assert minx <= 0.0 + 1e-6
|
||||
assert maxx >= 10.0 - 1e-6
|
||||
assert miny <= 0.0 + 1e-6
|
||||
assert maxy >= 10.0 - 1e-6
|
||||
|
||||
# 2. Clipped BBox model
|
||||
res_clipped = Bend90.generate(start, radius, width, direction="CCW", collision_type="clipped_bbox", clip_margin=1.0)
|
||||
# Area should be less than full bbox
|
||||
assert res_clipped.geometry[0].area < res_bbox.geometry[0].area
|
||||
|
||||
|
||||
def test_sbend_collision_models() -> None:
|
||||
start = Port(0, 0, 0)
|
||||
offset = 5.0
|
||||
radius = 10.0
|
||||
width = 2.0
|
||||
|
||||
res_bbox = SBend.generate(start, offset, radius, width, collision_type="bbox")
|
||||
# Geometry should be a single bounding box polygon
|
||||
assert len(res_bbox.geometry) == 1
|
||||
|
||||
res_arc = SBend.generate(start, offset, radius, width, collision_type="arc")
|
||||
assert res_bbox.geometry[0].area > res_arc.geometry[0].area
|
||||
|
|
|
|||
|
|
@ -30,9 +30,9 @@ def test_astar_sbend(basic_evaluator: CostEvaluator) -> None:
|
|||
# Check if any component in the path is an SBend
|
||||
found_sbend = False
|
||||
for res in path:
|
||||
# Check if it has 2 polygons (characteristic of our SBend implementation)
|
||||
# and end port orientation is same as start
|
||||
if len(res.geometry) == 2:
|
||||
# Check if the end port orientation is same as start
|
||||
# and it's not a single straight (which would have y=0)
|
||||
if abs(res.end_port.y - start.y) > 0.1 and abs(res.end_port.orientation - start.orientation) < 0.1:
|
||||
found_sbend = True
|
||||
break
|
||||
assert found_sbend
|
||||
|
|
@ -50,11 +50,6 @@ def test_pathfinder_negotiated_congestion_resolution(basic_evaluator: CostEvalua
|
|||
net_widths = {"net1": 2.0, "net2": 2.0}
|
||||
|
||||
# Force them into a narrow corridor that only fits ONE.
|
||||
# Obstacles creating a wide wall with a narrow 2um gap at y=5.
|
||||
# Gap y: 4 to 6. Center y=5.
|
||||
# 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, 15), (20, 10)]) # Lower wall
|
||||
obs_bottom = Polygon([(20, 4), (30, 4), (30, -15), (20, -10)])
|
||||
|
||||
|
|
|
|||
|
|
@ -36,9 +36,12 @@ def plot_routing_results(
|
|||
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 not label_added else "")
|
||||
label_added = True
|
||||
# Handle both Polygon and MultiPolygon (e.g. from SBend)
|
||||
geoms = [poly] if hasattr(poly, "exterior") else poly.geoms
|
||||
for g in geoms:
|
||||
x, y = g.exterior.xy
|
||||
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])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue