performance improvements
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 43 KiB |
|
|
@ -16,8 +16,8 @@ class CollisionEngine:
|
|||
"""
|
||||
__slots__ = (
|
||||
'clearance', 'max_net_width', 'safety_zone_radius',
|
||||
'static_index', 'static_geometries', 'static_prepared', '_static_id_counter',
|
||||
'dynamic_index', 'dynamic_geometries', '_dynamic_id_counter'
|
||||
'static_index', 'static_geometries', 'static_dilated', 'static_prepared', '_static_id_counter',
|
||||
'dynamic_index', 'dynamic_geometries', 'dynamic_dilated', '_dynamic_id_counter'
|
||||
)
|
||||
|
||||
clearance: float
|
||||
|
|
@ -47,21 +47,24 @@ class CollisionEngine:
|
|||
self.max_net_width = max_net_width
|
||||
self.safety_zone_radius = safety_zone_radius
|
||||
|
||||
# Static obstacles: store raw geometries to avoid double-dilation
|
||||
# Static obstacles
|
||||
self.static_index = rtree.index.Index()
|
||||
self.static_geometries: dict[int, Polygon] = {} # ID -> Polygon
|
||||
self.static_prepared: dict[int, PreparedGeometry] = {} # ID -> PreparedGeometry
|
||||
self.static_geometries: dict[int, Polygon] = {} # ID -> Raw Polygon
|
||||
self.static_dilated: dict[int, Polygon] = {} # ID -> Dilated Polygon (by clearance)
|
||||
self.static_prepared: dict[int, PreparedGeometry] = {} # ID -> Prepared Dilated
|
||||
self._static_id_counter = 0
|
||||
|
||||
# Dynamic paths for multi-net congestion
|
||||
self.dynamic_index = rtree.index.Index()
|
||||
# obj_id -> (net_id, raw_geometry)
|
||||
self.dynamic_geometries: dict[int, tuple[str, Polygon]] = {}
|
||||
# obj_id -> dilated_geometry (by clearance/2)
|
||||
self.dynamic_dilated: dict[int, Polygon] = {}
|
||||
self._dynamic_id_counter = 0
|
||||
|
||||
def add_static_obstacle(self, polygon: Polygon) -> None:
|
||||
"""
|
||||
Add a static obstacle (raw geometry) to the engine.
|
||||
Add a static obstacle to the engine.
|
||||
|
||||
Args:
|
||||
polygon: Raw obstacle geometry.
|
||||
|
|
@ -69,23 +72,31 @@ class CollisionEngine:
|
|||
obj_id = self._static_id_counter
|
||||
self._static_id_counter += 1
|
||||
|
||||
dilated = polygon.buffer(self.clearance)
|
||||
self.static_geometries[obj_id] = polygon
|
||||
self.static_prepared[obj_id] = prep(polygon)
|
||||
self.static_index.insert(obj_id, polygon.bounds)
|
||||
self.static_dilated[obj_id] = dilated
|
||||
self.static_prepared[obj_id] = prep(dilated)
|
||||
self.static_index.insert(obj_id, dilated.bounds)
|
||||
|
||||
def add_path(self, net_id: str, geometry: list[Polygon]) -> None:
|
||||
def add_path(self, net_id: str, geometry: list[Polygon], dilated_geometry: list[Polygon] | None = None) -> None:
|
||||
"""
|
||||
Add a net's routed path (raw geometry) to the dynamic index.
|
||||
Add a net's routed path to the dynamic index.
|
||||
|
||||
Args:
|
||||
net_id: Identifier for the net.
|
||||
geometry: List of raw polygons in the path.
|
||||
dilated_geometry: Optional list of pre-dilated polygons (by clearance/2).
|
||||
"""
|
||||
for poly in geometry:
|
||||
dilation = self.clearance / 2.0
|
||||
for i, poly in enumerate(geometry):
|
||||
obj_id = self._dynamic_id_counter
|
||||
self._dynamic_id_counter += 1
|
||||
|
||||
dil = dilated_geometry[i] if dilated_geometry else poly.buffer(dilation)
|
||||
|
||||
self.dynamic_geometries[obj_id] = (net_id, poly)
|
||||
self.dynamic_index.insert(obj_id, poly.bounds)
|
||||
self.dynamic_dilated[obj_id] = dil
|
||||
self.dynamic_index.insert(obj_id, dil.bounds)
|
||||
|
||||
def remove_path(self, net_id: str) -> None:
|
||||
"""
|
||||
|
|
@ -97,7 +108,8 @@ class CollisionEngine:
|
|||
to_remove = [obj_id for obj_id, (nid, _) in self.dynamic_geometries.items() if nid == net_id]
|
||||
for obj_id in to_remove:
|
||||
nid, poly = self.dynamic_geometries.pop(obj_id)
|
||||
self.dynamic_index.delete(obj_id, poly.bounds)
|
||||
dilated = self.dynamic_dilated.pop(obj_id)
|
||||
self.dynamic_index.delete(obj_id, dilated.bounds)
|
||||
|
||||
def lock_net(self, net_id: str) -> None:
|
||||
"""
|
||||
|
|
@ -109,7 +121,10 @@ class CollisionEngine:
|
|||
to_move = [obj_id for obj_id, (nid, _) in self.dynamic_geometries.items() if nid == net_id]
|
||||
for obj_id in to_move:
|
||||
nid, poly = self.dynamic_geometries.pop(obj_id)
|
||||
self.dynamic_index.delete(obj_id, poly.bounds)
|
||||
dilated = self.dynamic_dilated.pop(obj_id)
|
||||
self.dynamic_index.delete(obj_id, dilated.bounds)
|
||||
# Re-buffer for static clearance if necessary.
|
||||
# Note: dynamic is clearance/2, static is clearance.
|
||||
self.add_static_obstacle(poly)
|
||||
|
||||
def is_collision(
|
||||
|
|
@ -121,15 +136,6 @@ class CollisionEngine:
|
|||
) -> bool:
|
||||
"""
|
||||
Alias for check_collision(buffer_mode='static') for backward compatibility.
|
||||
|
||||
Args:
|
||||
geometry: Move geometry to check.
|
||||
net_width: Width of the net (unused).
|
||||
start_port: Starting port for safety check.
|
||||
end_port: Ending port for safety check.
|
||||
|
||||
Returns:
|
||||
True if collision detected.
|
||||
"""
|
||||
_ = net_width
|
||||
res = self.check_collision(geometry, 'default', buffer_mode='static', start_port=start_port, end_port=end_port)
|
||||
|
|
@ -138,13 +144,6 @@ class CollisionEngine:
|
|||
def count_congestion(self, geometry: Polygon, net_id: str) -> int:
|
||||
"""
|
||||
Alias for check_collision(buffer_mode='congestion') for backward compatibility.
|
||||
|
||||
Args:
|
||||
geometry: Move geometry to check.
|
||||
net_id: Identifier for the net.
|
||||
|
||||
Returns:
|
||||
Number of overlapping nets.
|
||||
"""
|
||||
res = self.check_collision(geometry, net_id, buffer_mode='congestion')
|
||||
return int(res)
|
||||
|
|
@ -156,6 +155,7 @@ class CollisionEngine:
|
|||
buffer_mode: Literal['static', 'congestion'] = 'static',
|
||||
start_port: Port | None = None,
|
||||
end_port: Port | None = None,
|
||||
dilated_geometry: Polygon | None = None,
|
||||
) -> bool | int:
|
||||
"""
|
||||
Check for collisions using unified dilation logic.
|
||||
|
|
@ -166,17 +166,24 @@ class CollisionEngine:
|
|||
buffer_mode: 'static' (full clearance) or 'congestion' (shared).
|
||||
start_port: Optional start port for safety zone.
|
||||
end_port: Optional end port for safety zone.
|
||||
dilated_geometry: Optional pre-buffered geometry (clearance/2).
|
||||
|
||||
Returns:
|
||||
Boolean if static, integer count if congestion.
|
||||
"""
|
||||
if buffer_mode == 'static':
|
||||
test_poly = geometry.buffer(self.clearance)
|
||||
candidates = self.static_index.intersection(test_poly.bounds)
|
||||
# Use raw query against pre-dilated obstacles
|
||||
candidates = self.static_index.intersection(geometry.bounds)
|
||||
|
||||
for obj_id in candidates:
|
||||
if self.static_prepared[obj_id].intersects(test_poly):
|
||||
if self.static_prepared[obj_id].intersects(geometry):
|
||||
if start_port or end_port:
|
||||
# Safety zone check: requires intersection of DILATED query and RAW obstacle.
|
||||
# Always re-buffer here because static check needs full clearance dilation,
|
||||
# whereas the provided dilated_geometry is usually clearance/2.
|
||||
dilation = self.clearance
|
||||
test_poly = geometry.buffer(dilation)
|
||||
|
||||
intersection = test_poly.intersection(self.static_geometries[obj_id])
|
||||
if intersection.is_empty:
|
||||
continue
|
||||
|
|
@ -198,12 +205,12 @@ class CollisionEngine:
|
|||
|
||||
# buffer_mode == 'congestion'
|
||||
dilation = self.clearance / 2.0
|
||||
test_poly = geometry.buffer(dilation)
|
||||
test_poly = dilated_geometry if dilated_geometry else geometry.buffer(dilation)
|
||||
candidates = self.dynamic_index.intersection(test_poly.bounds)
|
||||
|
||||
count = 0
|
||||
for obj_id in candidates:
|
||||
other_net_id, other_poly = self.dynamic_geometries[obj_id]
|
||||
if other_net_id != net_id and test_poly.intersects(other_poly.buffer(dilation)):
|
||||
other_net_id, _ = self.dynamic_geometries[obj_id]
|
||||
if other_net_id != net_id and test_poly.intersects(self.dynamic_dilated[obj_id]):
|
||||
count += 1
|
||||
return count
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Literal, cast, TYPE_CHECKING, Union, Any
|
||||
from typing import Literal, cast
|
||||
import numpy
|
||||
from shapely.geometry import Polygon, box
|
||||
from shapely.ops import unary_union
|
||||
|
||||
from .primitives import Port
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
|
||||
# Search Grid Snap (1.0 µm)
|
||||
|
|
@ -32,11 +30,14 @@ class ComponentResult:
|
|||
"""
|
||||
The result of a component generation: geometry, final port, and physical length.
|
||||
"""
|
||||
__slots__ = ('geometry', 'end_port', 'length')
|
||||
__slots__ = ('geometry', 'dilated_geometry', 'end_port', 'length')
|
||||
|
||||
geometry: list[Polygon]
|
||||
""" List of polygons representing the component geometry """
|
||||
|
||||
dilated_geometry: list[Polygon] | None
|
||||
""" Optional list of pre-dilated polygons for collision optimization """
|
||||
|
||||
end_port: Port
|
||||
""" The final port after the component """
|
||||
|
||||
|
|
@ -48,8 +49,10 @@ class ComponentResult:
|
|||
geometry: list[Polygon],
|
||||
end_port: Port,
|
||||
length: float,
|
||||
dilated_geometry: list[Polygon] | None = None,
|
||||
) -> None:
|
||||
self.geometry = geometry
|
||||
self.dilated_geometry = dilated_geometry
|
||||
self.end_port = end_port
|
||||
self.length = length
|
||||
|
||||
|
|
@ -64,6 +67,7 @@ class Straight:
|
|||
length: float,
|
||||
width: float,
|
||||
snap_to_grid: bool = True,
|
||||
dilation: float = 0.0,
|
||||
) -> ComponentResult:
|
||||
"""
|
||||
Generate a straight waveguide segment.
|
||||
|
|
@ -73,16 +77,17 @@ class Straight:
|
|||
length: Requested length.
|
||||
width: Waveguide width.
|
||||
snap_to_grid: Whether to snap the end port to the search grid.
|
||||
dilation: Optional dilation distance for pre-calculating collision geometry.
|
||||
|
||||
Returns:
|
||||
A ComponentResult containing the straight segment.
|
||||
"""
|
||||
rad = numpy.radians(start_port.orientation)
|
||||
dx = length * numpy.cos(rad)
|
||||
dy = length * numpy.sin(rad)
|
||||
cos_val = numpy.cos(rad)
|
||||
sin_val = numpy.sin(rad)
|
||||
|
||||
ex = start_port.x + dx
|
||||
ey = start_port.y + dy
|
||||
ex = start_port.x + length * cos_val
|
||||
ey = start_port.y + length * sin_val
|
||||
|
||||
if snap_to_grid:
|
||||
ex = snap_search_grid(ex)
|
||||
|
|
@ -91,21 +96,36 @@ class Straight:
|
|||
end_port = Port(ex, ey, start_port.orientation)
|
||||
actual_length = numpy.sqrt((end_port.x - start_port.x)**2 + (end_port.y - start_port.y)**2)
|
||||
|
||||
# Create polygon
|
||||
# Create polygons using vectorized points
|
||||
half_w = width / 2.0
|
||||
# Points relative to start port (0,0)
|
||||
points = [(0, half_w), (actual_length, half_w), (actual_length, -half_w), (0, -half_w)]
|
||||
pts_raw = numpy.array([
|
||||
[0, half_w],
|
||||
[actual_length, half_w],
|
||||
[actual_length, -half_w],
|
||||
[0, -half_w]
|
||||
])
|
||||
|
||||
# Rotation matrix (standard 2D rotation)
|
||||
rot_matrix = numpy.array([[cos_val, -sin_val], [sin_val, cos_val]])
|
||||
|
||||
# Transform points: P' = R * P + T
|
||||
poly_points = (pts_raw @ rot_matrix.T) + [start_port.x, start_port.y]
|
||||
geom = [Polygon(poly_points)]
|
||||
|
||||
# Transform points
|
||||
cos_val = numpy.cos(rad)
|
||||
sin_val = numpy.sin(rad)
|
||||
poly_points = []
|
||||
for px, py in points:
|
||||
tx = start_port.x + px * cos_val - py * sin_val
|
||||
ty = start_port.y + px * sin_val + py * cos_val
|
||||
poly_points.append((tx, ty))
|
||||
dilated_geom = None
|
||||
if dilation > 0:
|
||||
# Direct calculation of dilated rectangle instead of expensive buffer()
|
||||
half_w_dil = half_w + dilation
|
||||
pts_dil = numpy.array([
|
||||
[-dilation, half_w_dil],
|
||||
[actual_length + dilation, half_w_dil],
|
||||
[actual_length + dilation, -half_w_dil],
|
||||
[-dilation, -half_w_dil]
|
||||
])
|
||||
poly_points_dil = (pts_dil @ rot_matrix.T) + [start_port.x, start_port.y]
|
||||
dilated_geom = [Polygon(poly_points_dil)]
|
||||
|
||||
return ComponentResult(geometry=[Polygon(poly_points)], end_port=end_port, length=actual_length)
|
||||
return ComponentResult(geometry=geom, end_port=end_port, length=actual_length, dilated_geometry=dilated_geom)
|
||||
|
||||
|
||||
def _get_num_segments(radius: float, angle_deg: float, sagitta: float = 0.01) -> int:
|
||||
|
|
@ -140,7 +160,7 @@ def _get_arc_polygons(
|
|||
sagitta: float = 0.01,
|
||||
) -> list[Polygon]:
|
||||
"""
|
||||
Helper to generate arc-shaped polygons.
|
||||
Helper to generate arc-shaped polygons using vectorized NumPy operations.
|
||||
|
||||
Args:
|
||||
cx, cy: Center coordinates.
|
||||
|
|
@ -154,11 +174,20 @@ def _get_arc_polygons(
|
|||
"""
|
||||
num_segments = _get_num_segments(radius, float(numpy.degrees(abs(t_end - t_start))), sagitta)
|
||||
angles = numpy.linspace(t_start, t_end, num_segments + 1)
|
||||
|
||||
cos_a = numpy.cos(angles)
|
||||
sin_a = numpy.sin(angles)
|
||||
|
||||
inner_radius = radius - width / 2.0
|
||||
outer_radius = radius + width / 2.0
|
||||
inner_points = [(cx + inner_radius * numpy.cos(a), cy + inner_radius * numpy.sin(a)) for a in angles]
|
||||
outer_points = [(cx + outer_radius * numpy.cos(a), cy + outer_radius * numpy.sin(a)) for a in reversed(angles)]
|
||||
return [Polygon(inner_points + outer_points)]
|
||||
|
||||
inner_points = numpy.stack([cx + inner_radius * cos_a, cy + inner_radius * sin_a], axis=1)
|
||||
outer_points = numpy.stack([cx + outer_radius * cos_a[::-1], cy + outer_radius * sin_a[::-1]], axis=1)
|
||||
|
||||
# Concatenate inner and outer points to form the polygon ring
|
||||
poly_points = numpy.concatenate([inner_points, outer_points])
|
||||
|
||||
return [Polygon(poly_points)]
|
||||
|
||||
|
||||
def _clip_bbox(
|
||||
|
|
@ -291,6 +320,7 @@ class Bend90:
|
|||
sagitta: float = 0.01,
|
||||
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc",
|
||||
clip_margin: float = 10.0,
|
||||
dilation: float = 0.0,
|
||||
) -> ComponentResult:
|
||||
"""
|
||||
Generate a 90-degree bend.
|
||||
|
|
@ -303,6 +333,7 @@ class Bend90:
|
|||
sagitta: Geometric fidelity.
|
||||
collision_type: Collision model.
|
||||
clip_margin: Margin for clipped_bbox.
|
||||
dilation: Optional dilation distance for pre-calculating collision geometry.
|
||||
|
||||
Returns:
|
||||
A ComponentResult containing the bend.
|
||||
|
|
@ -323,8 +354,15 @@ class Bend90:
|
|||
collision_polys = _apply_collision_model(
|
||||
arc_polys[0], collision_type, radius, width, cx, cy, clip_margin
|
||||
)
|
||||
|
||||
dilated_geom = [p.buffer(dilation) for p in collision_polys] if dilation > 0 else None
|
||||
|
||||
return ComponentResult(geometry=collision_polys, end_port=end_port, length=radius * numpy.pi / 2.0)
|
||||
return ComponentResult(
|
||||
geometry=collision_polys,
|
||||
end_port=end_port,
|
||||
length=radius * numpy.pi / 2.0,
|
||||
dilated_geometry=dilated_geom
|
||||
)
|
||||
|
||||
|
||||
class SBend:
|
||||
|
|
@ -340,6 +378,7 @@ class SBend:
|
|||
sagitta: float = 0.01,
|
||||
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc",
|
||||
clip_margin: float = 10.0,
|
||||
dilation: float = 0.0,
|
||||
) -> ComponentResult:
|
||||
"""
|
||||
Generate a parametric S-bend (two tangent arcs).
|
||||
|
|
@ -352,6 +391,7 @@ class SBend:
|
|||
sagitta: Geometric fidelity.
|
||||
collision_type: Collision model.
|
||||
clip_margin: Margin for clipped_bbox.
|
||||
dilation: Optional dilation distance for pre-calculating collision geometry.
|
||||
|
||||
Returns:
|
||||
A ComponentResult containing the S-bend.
|
||||
|
|
@ -385,13 +425,22 @@ class SBend:
|
|||
arc2 = _get_arc_polygons(cx2, cy2, radius, width, ts2, te2, sagitta)[0]
|
||||
|
||||
if collision_type == "clipped_bbox":
|
||||
col1 = _apply_collision_model(arc1, collision_type, radius, width, cx1, cy1, clip_margin)
|
||||
col2 = _apply_collision_model(arc2, collision_type, radius, width, cx2, cy2, clip_margin)
|
||||
collision_polys = [cast('Polygon', unary_union(col1 + col2))]
|
||||
col1 = _apply_collision_model(arc1, collision_type, radius, width, cx1, cy1, clip_margin)[0]
|
||||
col2 = _apply_collision_model(arc2, collision_type, radius, width, cx2, cy2, clip_margin)[0]
|
||||
# Optimization: keep as list instead of unary_union for search efficiency
|
||||
collision_polys = [col1, col2]
|
||||
else:
|
||||
combined_arc = cast('Polygon', unary_union([arc1, arc2]))
|
||||
collision_polys = _apply_collision_model(
|
||||
combined_arc, collision_type, radius, width, 0, 0, clip_margin
|
||||
)
|
||||
# For other models, we can either combine or keep separate.
|
||||
# Keeping separate is generally better for CollisionEngine.
|
||||
col1 = _apply_collision_model(arc1, collision_type, radius, width, cx1, cy1, clip_margin)[0]
|
||||
col2 = _apply_collision_model(arc2, collision_type, radius, width, cx2, cy2, clip_margin)[0]
|
||||
collision_polys = [col1, col2]
|
||||
|
||||
return ComponentResult(geometry=collision_polys, end_port=end_port, length=2 * radius * theta)
|
||||
dilated_geom = [p.buffer(dilation) for p in collision_polys] if dilation > 0 else None
|
||||
|
||||
return ComponentResult(
|
||||
geometry=collision_polys,
|
||||
end_port=end_port,
|
||||
length=2 * radius * theta,
|
||||
dilated_geometry=dilated_geom
|
||||
)
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ class AStarRouter:
|
|||
"""
|
||||
Hybrid State-Lattice A* Router.
|
||||
"""
|
||||
__slots__ = ('cost_evaluator', 'config', 'node_limit', 'total_nodes_expanded', '_collision_cache')
|
||||
__slots__ = ('cost_evaluator', 'config', 'node_limit', 'total_nodes_expanded', '_collision_cache', '_self_dilation')
|
||||
|
||||
cost_evaluator: CostEvaluator
|
||||
""" The evaluator for path and proximity costs """
|
||||
|
|
@ -100,6 +100,9 @@ class AStarRouter:
|
|||
_collision_cache: dict[tuple[float, float, float, str, float, str], bool]
|
||||
""" Internal cache for move collision checks """
|
||||
|
||||
_self_dilation: float
|
||||
""" Cached dilation value for collision checks (clearance / 2.0) """
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
cost_evaluator: CostEvaluator,
|
||||
|
|
@ -146,6 +149,7 @@ class AStarRouter:
|
|||
self.node_limit = self.config.node_limit
|
||||
self.total_nodes_expanded = 0
|
||||
self._collision_cache = {}
|
||||
self._self_dilation = self.cost_evaluator.collision_engine.clearance / 2.0
|
||||
|
||||
def route(
|
||||
self,
|
||||
|
|
@ -226,7 +230,7 @@ class AStarRouter:
|
|||
proj = dx * numpy.cos(rad) + dy * numpy.sin(rad)
|
||||
perp = -dx * numpy.sin(rad) + dy * numpy.cos(rad)
|
||||
if proj > 0 and abs(perp) < 1e-6:
|
||||
res = Straight.generate(current.port, proj, net_width, snap_to_grid=False)
|
||||
res = Straight.generate(current.port, proj, net_width, snap_to_grid=False, dilation=self._self_dilation)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, 'SnapStraight')
|
||||
|
||||
# B. Try SBend exact reach
|
||||
|
|
@ -245,7 +249,8 @@ class AStarRouter:
|
|||
radius,
|
||||
net_width,
|
||||
collision_type=self.config.bend_collision_type,
|
||||
clip_margin=self.config.bend_clip_margin
|
||||
clip_margin=self.config.bend_clip_margin,
|
||||
dilation=self._self_dilation
|
||||
)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, 'SnapSBend', move_radius=radius)
|
||||
except ValueError:
|
||||
|
|
@ -258,7 +263,7 @@ class AStarRouter:
|
|||
lengths = sorted(set(lengths + fine_steps))
|
||||
|
||||
for length in lengths:
|
||||
res = Straight.generate(current.port, length, net_width)
|
||||
res = Straight.generate(current.port, length, net_width, dilation=self._self_dilation)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f'S{length}')
|
||||
|
||||
# 3. Lattice Bends
|
||||
|
|
@ -270,7 +275,8 @@ class AStarRouter:
|
|||
net_width,
|
||||
direction,
|
||||
collision_type=self.config.bend_collision_type,
|
||||
clip_margin=self.config.bend_clip_margin
|
||||
clip_margin=self.config.bend_clip_margin,
|
||||
dilation=self._self_dilation
|
||||
)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f'B{radius}{direction}', move_radius=radius)
|
||||
|
||||
|
|
@ -284,7 +290,8 @@ class AStarRouter:
|
|||
radius,
|
||||
net_width,
|
||||
collision_type=self.config.bend_collision_type,
|
||||
clip_margin=self.config.bend_clip_margin
|
||||
clip_margin=self.config.bend_clip_margin,
|
||||
dilation=self._self_dilation
|
||||
)
|
||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f'SB{offset}R{radius}', move_radius=radius)
|
||||
except ValueError:
|
||||
|
|
@ -320,9 +327,11 @@ class AStarRouter:
|
|||
return
|
||||
else:
|
||||
hard_coll = False
|
||||
for poly in result.geometry:
|
||||
for i, poly in enumerate(result.geometry):
|
||||
dil_poly = result.dilated_geometry[i] if result.dilated_geometry else None
|
||||
if self.cost_evaluator.collision_engine.check_collision(
|
||||
poly, net_id, buffer_mode='static', start_port=parent.port, end_port=result.end_port
|
||||
poly, net_id, buffer_mode='static', start_port=parent.port, end_port=result.end_port,
|
||||
dilated_geometry=dil_poly
|
||||
):
|
||||
hard_coll = True
|
||||
break
|
||||
|
|
@ -331,27 +340,37 @@ class AStarRouter:
|
|||
return
|
||||
|
||||
# 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: AStarNode | None = 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:
|
||||
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
|
||||
# Optimization: use pre-dilated geometries
|
||||
if result.dilated_geometry:
|
||||
for dilated_move in result.dilated_geometry:
|
||||
curr_p: AStarNode | None = parent
|
||||
seg_idx = 0
|
||||
while curr_p and curr_p.component_result and seg_idx < 100:
|
||||
if seg_idx > 0:
|
||||
res_p = curr_p.component_result
|
||||
if res_p.dilated_geometry:
|
||||
for dilated_prev in res_p.dilated_geometry:
|
||||
if (dilated_move.bounds[0] > dilated_prev.bounds[2] or
|
||||
dilated_move.bounds[2] < dilated_prev.bounds[0] or
|
||||
dilated_move.bounds[1] > dilated_prev.bounds[3] or
|
||||
dilated_move.bounds[3] < dilated_prev.bounds[1]):
|
||||
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
|
||||
if dilated_move.intersects(dilated_prev):
|
||||
overlap = dilated_move.intersection(dilated_prev)
|
||||
if overlap.area > 1e-6:
|
||||
return
|
||||
else:
|
||||
# Fallback if no pre-dilation (should not happen with new logic)
|
||||
dilation = self._self_dilation
|
||||
for prev_poly in res_p.geometry:
|
||||
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
|
||||
|
||||
move_cost = self.cost_evaluator.evaluate_move(
|
||||
result.geometry,
|
||||
|
|
@ -359,7 +378,8 @@ class AStarRouter:
|
|||
net_width,
|
||||
net_id,
|
||||
start_port=parent.port,
|
||||
length=result.length
|
||||
length=result.length,
|
||||
dilated_geometry=result.dilated_geometry
|
||||
)
|
||||
|
||||
if move_cost > 1e12:
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ class CostEvaluator:
|
|||
net_id: str,
|
||||
start_port: Port | None = None,
|
||||
length: float = 0.0,
|
||||
dilated_geometry: list[Polygon] | None = None,
|
||||
) -> float:
|
||||
"""
|
||||
Calculate the cost of a single move (Straight, Bend, SBend).
|
||||
|
|
@ -114,6 +115,7 @@ class CostEvaluator:
|
|||
net_id: Identifier for the net.
|
||||
start_port: Port at the start of the move.
|
||||
length: Physical path length of the move.
|
||||
dilated_geometry: Pre-calculated dilated polygons.
|
||||
|
||||
Returns:
|
||||
Total cost of the move, or 1e15 if invalid.
|
||||
|
|
@ -126,15 +128,19 @@ class CostEvaluator:
|
|||
return 1e15
|
||||
|
||||
# 2. Collision Check
|
||||
for poly in geometry:
|
||||
for i, poly in enumerate(geometry):
|
||||
dil_poly = dilated_geometry[i] if dilated_geometry else None
|
||||
# Hard Collision (Static obstacles)
|
||||
if self.collision_engine.check_collision(
|
||||
poly, net_id, buffer_mode='static', start_port=start_port, end_port=end_port
|
||||
poly, net_id, buffer_mode='static', start_port=start_port, end_port=end_port,
|
||||
dilated_geometry=dil_poly
|
||||
):
|
||||
return 1e15
|
||||
|
||||
# Soft Collision (Negotiated Congestion)
|
||||
overlaps = self.collision_engine.check_collision(poly, net_id, buffer_mode='congestion')
|
||||
overlaps = self.collision_engine.check_collision(
|
||||
poly, net_id, buffer_mode='congestion', dilated_geometry=dil_poly
|
||||
)
|
||||
if isinstance(overlaps, int) and overlaps > 0:
|
||||
total_cost += overlaps * self.congestion_penalty
|
||||
|
||||
|
|
|
|||
|
|
@ -118,15 +118,23 @@ class PathFinder:
|
|||
if path:
|
||||
# 3. Add to index
|
||||
all_geoms = []
|
||||
all_dilated = []
|
||||
for res in path:
|
||||
all_geoms.extend(res.geometry)
|
||||
self.cost_evaluator.collision_engine.add_path(net_id, all_geoms)
|
||||
if res.dilated_geometry:
|
||||
all_dilated.extend(res.dilated_geometry)
|
||||
else:
|
||||
# Fallback dilation
|
||||
dilation = self.cost_evaluator.collision_engine.clearance / 2.0
|
||||
all_dilated.extend([p.buffer(dilation) for p in res.geometry])
|
||||
|
||||
self.cost_evaluator.collision_engine.add_path(net_id, all_geoms, dilated_geometry=all_dilated)
|
||||
|
||||
# Check if this new path has any congestion
|
||||
collision_count = 0
|
||||
for poly in all_geoms:
|
||||
for i, poly in enumerate(all_geoms):
|
||||
overlaps = self.cost_evaluator.collision_engine.check_collision(
|
||||
poly, net_id, buffer_mode='congestion'
|
||||
poly, net_id, buffer_mode='congestion', dilated_geometry=all_dilated[i]
|
||||
)
|
||||
if isinstance(overlaps, int):
|
||||
collision_count += overlaps
|
||||
|
|
@ -172,9 +180,10 @@ class PathFinder:
|
|||
|
||||
collision_count = 0
|
||||
for comp in res.path:
|
||||
for poly in comp.geometry:
|
||||
for i, poly in enumerate(comp.geometry):
|
||||
dil_poly = comp.dilated_geometry[i] if comp.dilated_geometry else None
|
||||
overlaps = self.cost_evaluator.collision_engine.check_collision(
|
||||
poly, net_id, buffer_mode='congestion'
|
||||
poly, net_id, buffer_mode='congestion', dilated_geometry=dil_poly
|
||||
)
|
||||
if isinstance(overlaps, int):
|
||||
collision_count += overlaps
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ 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) == 1 # Now uses unary_union
|
||||
assert len(result.geometry) == 2 # Optimization: returns individual arcs
|
||||
|
||||
# Verify failure for large offset
|
||||
with pytest.raises(ValueError, match=r"SBend offset .* must be less than 2\*radius"):
|
||||
|
|
@ -85,11 +85,13 @@ def test_sbend_collision_models() -> None:
|
|||
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
|
||||
# Geometry should be a list of individual bbox polygons for each arc
|
||||
assert len(res_bbox.geometry) == 2
|
||||
|
||||
res_arc = SBend.generate(start, offset, radius, width, collision_type="arc")
|
||||
assert res_bbox.geometry[0].area > res_arc.geometry[0].area
|
||||
area_bbox = sum(p.area for p in res_bbox.geometry)
|
||||
area_arc = sum(p.area for p in res_arc.geometry)
|
||||
assert area_bbox > area_arc
|
||||
|
||||
|
||||
def test_sbend_continuity() -> None:
|
||||
|
|
@ -107,9 +109,10 @@ def test_sbend_continuity() -> None:
|
|||
# For a port at 90 deg, +offset is a shift in -x direction
|
||||
assert abs(res.end_port.x - (10.0 - offset)) < 1e-6
|
||||
|
||||
# Geometry should be connected (unary_union results in 1 polygon)
|
||||
assert len(res.geometry) == 1
|
||||
assert res.geometry[0].is_valid
|
||||
# Geometry should be a list of valid polygons
|
||||
assert len(res.geometry) == 2
|
||||
for p in res.geometry:
|
||||
assert p.is_valid
|
||||
|
||||
|
||||
def test_arc_sagitta_precision() -> None:
|
||||
|
|
|
|||