performance improvements

This commit is contained in:
Jan Petykiewicz 2026-03-09 12:23:17 -07:00
commit 8424171946
12 changed files with 212 additions and 118 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Before After
Before After

View file

@ -16,8 +16,8 @@ class CollisionEngine:
""" """
__slots__ = ( __slots__ = (
'clearance', 'max_net_width', 'safety_zone_radius', 'clearance', 'max_net_width', 'safety_zone_radius',
'static_index', 'static_geometries', 'static_prepared', '_static_id_counter', 'static_index', 'static_geometries', 'static_dilated', 'static_prepared', '_static_id_counter',
'dynamic_index', 'dynamic_geometries', '_dynamic_id_counter' 'dynamic_index', 'dynamic_geometries', 'dynamic_dilated', '_dynamic_id_counter'
) )
clearance: float clearance: float
@ -47,21 +47,24 @@ class CollisionEngine:
self.max_net_width = max_net_width self.max_net_width = max_net_width
self.safety_zone_radius = safety_zone_radius 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_index = rtree.index.Index()
self.static_geometries: dict[int, Polygon] = {} # ID -> Polygon self.static_geometries: dict[int, Polygon] = {} # ID -> Raw Polygon
self.static_prepared: dict[int, PreparedGeometry] = {} # ID -> PreparedGeometry 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 self._static_id_counter = 0
# Dynamic paths for multi-net congestion # Dynamic paths for multi-net congestion
self.dynamic_index = rtree.index.Index() self.dynamic_index = rtree.index.Index()
# obj_id -> (net_id, raw_geometry) # obj_id -> (net_id, raw_geometry)
self.dynamic_geometries: dict[int, tuple[str, Polygon]] = {} 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 self._dynamic_id_counter = 0
def add_static_obstacle(self, polygon: Polygon) -> None: 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: Args:
polygon: Raw obstacle geometry. polygon: Raw obstacle geometry.
@ -69,23 +72,31 @@ class CollisionEngine:
obj_id = self._static_id_counter obj_id = self._static_id_counter
self._static_id_counter += 1 self._static_id_counter += 1
dilated = polygon.buffer(self.clearance)
self.static_geometries[obj_id] = polygon self.static_geometries[obj_id] = polygon
self.static_prepared[obj_id] = prep(polygon) self.static_dilated[obj_id] = dilated
self.static_index.insert(obj_id, polygon.bounds) 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: Args:
net_id: Identifier for the net. net_id: Identifier for the net.
geometry: List of raw polygons in the path. 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 obj_id = self._dynamic_id_counter
self._dynamic_id_counter += 1 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_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: 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] to_remove = [obj_id for obj_id, (nid, _) in self.dynamic_geometries.items() if nid == net_id]
for obj_id in to_remove: for obj_id in to_remove:
nid, poly = self.dynamic_geometries.pop(obj_id) 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: 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] to_move = [obj_id for obj_id, (nid, _) in self.dynamic_geometries.items() if nid == net_id]
for obj_id in to_move: for obj_id in to_move:
nid, poly = self.dynamic_geometries.pop(obj_id) 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) self.add_static_obstacle(poly)
def is_collision( def is_collision(
@ -121,15 +136,6 @@ class CollisionEngine:
) -> bool: ) -> bool:
""" """
Alias for check_collision(buffer_mode='static') for backward compatibility. 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 _ = net_width
res = self.check_collision(geometry, 'default', buffer_mode='static', start_port=start_port, end_port=end_port) 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: def count_congestion(self, geometry: Polygon, net_id: str) -> int:
""" """
Alias for check_collision(buffer_mode='congestion') for backward compatibility. 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') res = self.check_collision(geometry, net_id, buffer_mode='congestion')
return int(res) return int(res)
@ -156,6 +155,7 @@ class CollisionEngine:
buffer_mode: Literal['static', 'congestion'] = 'static', buffer_mode: Literal['static', 'congestion'] = 'static',
start_port: Port | None = None, start_port: Port | None = None,
end_port: Port | None = None, end_port: Port | None = None,
dilated_geometry: Polygon | None = None,
) -> bool | int: ) -> bool | int:
""" """
Check for collisions using unified dilation logic. Check for collisions using unified dilation logic.
@ -166,17 +166,24 @@ class CollisionEngine:
buffer_mode: 'static' (full clearance) or 'congestion' (shared). buffer_mode: 'static' (full clearance) or 'congestion' (shared).
start_port: Optional start port for safety zone. start_port: Optional start port for safety zone.
end_port: Optional end port for safety zone. end_port: Optional end port for safety zone.
dilated_geometry: Optional pre-buffered geometry (clearance/2).
Returns: Returns:
Boolean if static, integer count if congestion. Boolean if static, integer count if congestion.
""" """
if buffer_mode == 'static': if buffer_mode == 'static':
test_poly = geometry.buffer(self.clearance) # Use raw query against pre-dilated obstacles
candidates = self.static_index.intersection(test_poly.bounds) candidates = self.static_index.intersection(geometry.bounds)
for obj_id in candidates: 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: 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]) intersection = test_poly.intersection(self.static_geometries[obj_id])
if intersection.is_empty: if intersection.is_empty:
continue continue
@ -198,12 +205,12 @@ class CollisionEngine:
# buffer_mode == 'congestion' # buffer_mode == 'congestion'
dilation = self.clearance / 2.0 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) candidates = self.dynamic_index.intersection(test_poly.bounds)
count = 0 count = 0
for obj_id in candidates: for obj_id in candidates:
other_net_id, other_poly = self.dynamic_geometries[obj_id] other_net_id, _ = self.dynamic_geometries[obj_id]
if other_net_id != net_id and test_poly.intersects(other_poly.buffer(dilation)): if other_net_id != net_id and test_poly.intersects(self.dynamic_dilated[obj_id]):
count += 1 count += 1
return count return count

View file

@ -1,14 +1,12 @@
from __future__ import annotations from __future__ import annotations
from typing import Literal, cast, TYPE_CHECKING, Union, Any from typing import Literal, cast
import numpy import numpy
from shapely.geometry import Polygon, box from shapely.geometry import Polygon, box
from shapely.ops import unary_union from shapely.ops import unary_union
from .primitives import Port from .primitives import Port
if TYPE_CHECKING:
from collections.abc import Sequence
# Search Grid Snap (1.0 µm) # Search Grid Snap (1.0 µm)
@ -32,11 +30,14 @@ class ComponentResult:
""" """
The result of a component generation: geometry, final port, and physical length. 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] geometry: list[Polygon]
""" List of polygons representing the component geometry """ """ List of polygons representing the component geometry """
dilated_geometry: list[Polygon] | None
""" Optional list of pre-dilated polygons for collision optimization """
end_port: Port end_port: Port
""" The final port after the component """ """ The final port after the component """
@ -48,8 +49,10 @@ class ComponentResult:
geometry: list[Polygon], geometry: list[Polygon],
end_port: Port, end_port: Port,
length: float, length: float,
dilated_geometry: list[Polygon] | None = None,
) -> None: ) -> None:
self.geometry = geometry self.geometry = geometry
self.dilated_geometry = dilated_geometry
self.end_port = end_port self.end_port = end_port
self.length = length self.length = length
@ -64,6 +67,7 @@ class Straight:
length: float, length: float,
width: float, width: float,
snap_to_grid: bool = True, snap_to_grid: bool = True,
dilation: float = 0.0,
) -> ComponentResult: ) -> ComponentResult:
""" """
Generate a straight waveguide segment. Generate a straight waveguide segment.
@ -73,16 +77,17 @@ class Straight:
length: Requested length. length: Requested length.
width: Waveguide width. width: Waveguide width.
snap_to_grid: Whether to snap the end port to the search grid. snap_to_grid: Whether to snap the end port to the search grid.
dilation: Optional dilation distance for pre-calculating collision geometry.
Returns: Returns:
A ComponentResult containing the straight segment. A ComponentResult containing the straight segment.
""" """
rad = numpy.radians(start_port.orientation) rad = numpy.radians(start_port.orientation)
dx = length * numpy.cos(rad) cos_val = numpy.cos(rad)
dy = length * numpy.sin(rad) sin_val = numpy.sin(rad)
ex = start_port.x + dx ex = start_port.x + length * cos_val
ey = start_port.y + dy ey = start_port.y + length * sin_val
if snap_to_grid: if snap_to_grid:
ex = snap_search_grid(ex) ex = snap_search_grid(ex)
@ -91,21 +96,36 @@ class Straight:
end_port = Port(ex, ey, start_port.orientation) 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) 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 half_w = width / 2.0
# Points relative to start port (0,0) pts_raw = numpy.array([
points = [(0, half_w), (actual_length, half_w), (actual_length, -half_w), (0, -half_w)] [0, half_w],
[actual_length, half_w],
[actual_length, -half_w],
[0, -half_w]
])
# Transform points # Rotation matrix (standard 2D rotation)
cos_val = numpy.cos(rad) rot_matrix = numpy.array([[cos_val, -sin_val], [sin_val, cos_val]])
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))
return ComponentResult(geometry=[Polygon(poly_points)], end_port=end_port, length=actual_length) # Transform points: P' = R * P + T
poly_points = (pts_raw @ rot_matrix.T) + [start_port.x, start_port.y]
geom = [Polygon(poly_points)]
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=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: 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, sagitta: float = 0.01,
) -> list[Polygon]: ) -> list[Polygon]:
""" """
Helper to generate arc-shaped polygons. Helper to generate arc-shaped polygons using vectorized NumPy operations.
Args: Args:
cx, cy: Center coordinates. 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) 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) 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 inner_radius = radius - width / 2.0
outer_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)] inner_points = numpy.stack([cx + inner_radius * cos_a, cy + inner_radius * sin_a], axis=1)
return [Polygon(inner_points + outer_points)] 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( def _clip_bbox(
@ -291,6 +320,7 @@ class Bend90:
sagitta: float = 0.01, sagitta: float = 0.01,
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc", collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc",
clip_margin: float = 10.0, clip_margin: float = 10.0,
dilation: float = 0.0,
) -> ComponentResult: ) -> ComponentResult:
""" """
Generate a 90-degree bend. Generate a 90-degree bend.
@ -303,6 +333,7 @@ class Bend90:
sagitta: Geometric fidelity. sagitta: Geometric fidelity.
collision_type: Collision model. collision_type: Collision model.
clip_margin: Margin for clipped_bbox. clip_margin: Margin for clipped_bbox.
dilation: Optional dilation distance for pre-calculating collision geometry.
Returns: Returns:
A ComponentResult containing the bend. A ComponentResult containing the bend.
@ -324,7 +355,14 @@ class Bend90:
arc_polys[0], collision_type, radius, width, cx, cy, clip_margin arc_polys[0], collision_type, radius, width, cx, cy, clip_margin
) )
return ComponentResult(geometry=collision_polys, end_port=end_port, length=radius * numpy.pi / 2.0) 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,
dilated_geometry=dilated_geom
)
class SBend: class SBend:
@ -340,6 +378,7 @@ class SBend:
sagitta: float = 0.01, sagitta: float = 0.01,
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc", collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc",
clip_margin: float = 10.0, clip_margin: float = 10.0,
dilation: float = 0.0,
) -> ComponentResult: ) -> ComponentResult:
""" """
Generate a parametric S-bend (two tangent arcs). Generate a parametric S-bend (two tangent arcs).
@ -352,6 +391,7 @@ class SBend:
sagitta: Geometric fidelity. sagitta: Geometric fidelity.
collision_type: Collision model. collision_type: Collision model.
clip_margin: Margin for clipped_bbox. clip_margin: Margin for clipped_bbox.
dilation: Optional dilation distance for pre-calculating collision geometry.
Returns: Returns:
A ComponentResult containing the S-bend. A ComponentResult containing the S-bend.
@ -385,13 +425,22 @@ class SBend:
arc2 = _get_arc_polygons(cx2, cy2, radius, width, ts2, te2, sagitta)[0] arc2 = _get_arc_polygons(cx2, cy2, radius, width, ts2, te2, sagitta)[0]
if collision_type == "clipped_bbox": if collision_type == "clipped_bbox":
col1 = _apply_collision_model(arc1, collision_type, radius, width, cx1, cy1, clip_margin) 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) col2 = _apply_collision_model(arc2, collision_type, radius, width, cx2, cy2, clip_margin)[0]
collision_polys = [cast('Polygon', unary_union(col1 + col2))] # Optimization: keep as list instead of unary_union for search efficiency
collision_polys = [col1, col2]
else: else:
combined_arc = cast('Polygon', unary_union([arc1, arc2])) # For other models, we can either combine or keep separate.
collision_polys = _apply_collision_model( # Keeping separate is generally better for CollisionEngine.
combined_arc, collision_type, radius, width, 0, 0, clip_margin 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
)

View file

@ -83,7 +83,7 @@ class AStarRouter:
""" """
Hybrid State-Lattice A* Router. 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 cost_evaluator: CostEvaluator
""" The evaluator for path and proximity costs """ """ The evaluator for path and proximity costs """
@ -100,6 +100,9 @@ class AStarRouter:
_collision_cache: dict[tuple[float, float, float, str, float, str], bool] _collision_cache: dict[tuple[float, float, float, str, float, str], bool]
""" Internal cache for move collision checks """ """ Internal cache for move collision checks """
_self_dilation: float
""" Cached dilation value for collision checks (clearance / 2.0) """
def __init__( def __init__(
self, self,
cost_evaluator: CostEvaluator, cost_evaluator: CostEvaluator,
@ -146,6 +149,7 @@ class AStarRouter:
self.node_limit = self.config.node_limit self.node_limit = self.config.node_limit
self.total_nodes_expanded = 0 self.total_nodes_expanded = 0
self._collision_cache = {} self._collision_cache = {}
self._self_dilation = self.cost_evaluator.collision_engine.clearance / 2.0
def route( def route(
self, self,
@ -226,7 +230,7 @@ class AStarRouter:
proj = dx * numpy.cos(rad) + dy * numpy.sin(rad) proj = dx * numpy.cos(rad) + dy * numpy.sin(rad)
perp = -dx * numpy.sin(rad) + dy * numpy.cos(rad) perp = -dx * numpy.sin(rad) + dy * numpy.cos(rad)
if proj > 0 and abs(perp) < 1e-6: 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') self._add_node(current, res, target, net_width, net_id, open_set, closed_set, 'SnapStraight')
# B. Try SBend exact reach # B. Try SBend exact reach
@ -245,7 +249,8 @@ class AStarRouter:
radius, radius,
net_width, net_width,
collision_type=self.config.bend_collision_type, 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) self._add_node(current, res, target, net_width, net_id, open_set, closed_set, 'SnapSBend', move_radius=radius)
except ValueError: except ValueError:
@ -258,7 +263,7 @@ class AStarRouter:
lengths = sorted(set(lengths + fine_steps)) lengths = sorted(set(lengths + fine_steps))
for length in lengths: 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}') self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f'S{length}')
# 3. Lattice Bends # 3. Lattice Bends
@ -270,7 +275,8 @@ class AStarRouter:
net_width, net_width,
direction, direction,
collision_type=self.config.bend_collision_type, 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) 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, radius,
net_width, net_width,
collision_type=self.config.bend_collision_type, 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) self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f'SB{offset}R{radius}', move_radius=radius)
except ValueError: except ValueError:
@ -320,9 +327,11 @@ class AStarRouter:
return return
else: else:
hard_coll = False 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( 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 hard_coll = True
break break
@ -331,20 +340,30 @@ class AStarRouter:
return return
# 3. Check for Self-Intersection (Limited to last 100 segments for performance) # 3. Check for Self-Intersection (Limited to last 100 segments for performance)
dilation = self.cost_evaluator.collision_engine.clearance / 2.0 # Optimization: use pre-dilated geometries
for move_poly in result.geometry: if result.dilated_geometry:
dilated_move = move_poly.buffer(dilation) for dilated_move in result.dilated_geometry:
curr_p: AStarNode | None = parent curr_p: AStarNode | None = parent
seg_idx = 0 seg_idx = 0
while curr_p and curr_p.component_result and seg_idx < 100: while curr_p and curr_p.component_result and seg_idx < 100:
if seg_idx > 0: if seg_idx > 0:
for prev_poly in curr_p.component_result.geometry: res_p = curr_p.component_result
if dilated_move.bounds[0] > prev_poly.bounds[2] + dilation or \ if res_p.dilated_geometry:
dilated_move.bounds[2] < prev_poly.bounds[0] - dilation or \ for dilated_prev in res_p.dilated_geometry:
dilated_move.bounds[1] > prev_poly.bounds[3] + dilation or \ if (dilated_move.bounds[0] > dilated_prev.bounds[2] or
dilated_move.bounds[3] < prev_poly.bounds[1] - dilation: 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 continue
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) dilated_prev = prev_poly.buffer(dilation)
if dilated_move.intersects(dilated_prev): if dilated_move.intersects(dilated_prev):
overlap = dilated_move.intersection(dilated_prev) overlap = dilated_move.intersection(dilated_prev)
@ -359,7 +378,8 @@ class AStarRouter:
net_width, net_width,
net_id, net_id,
start_port=parent.port, start_port=parent.port,
length=result.length length=result.length,
dilated_geometry=result.dilated_geometry
) )
if move_cost > 1e12: if move_cost > 1e12:

View file

@ -103,6 +103,7 @@ class CostEvaluator:
net_id: str, net_id: str,
start_port: Port | None = None, start_port: Port | None = None,
length: float = 0.0, length: float = 0.0,
dilated_geometry: list[Polygon] | None = None,
) -> float: ) -> float:
""" """
Calculate the cost of a single move (Straight, Bend, SBend). Calculate the cost of a single move (Straight, Bend, SBend).
@ -114,6 +115,7 @@ class CostEvaluator:
net_id: Identifier for the net. net_id: Identifier for the net.
start_port: Port at the start of the move. start_port: Port at the start of the move.
length: Physical path length of the move. length: Physical path length of the move.
dilated_geometry: Pre-calculated dilated polygons.
Returns: Returns:
Total cost of the move, or 1e15 if invalid. Total cost of the move, or 1e15 if invalid.
@ -126,15 +128,19 @@ class CostEvaluator:
return 1e15 return 1e15
# 2. Collision Check # 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) # Hard Collision (Static obstacles)
if self.collision_engine.check_collision( 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 return 1e15
# Soft Collision (Negotiated Congestion) # 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: if isinstance(overlaps, int) and overlaps > 0:
total_cost += overlaps * self.congestion_penalty total_cost += overlaps * self.congestion_penalty

View file

@ -118,15 +118,23 @@ class PathFinder:
if path: if path:
# 3. Add to index # 3. Add to index
all_geoms = [] all_geoms = []
all_dilated = []
for res in path: for res in path:
all_geoms.extend(res.geometry) 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 # Check if this new path has any congestion
collision_count = 0 collision_count = 0
for poly in all_geoms: for i, poly in enumerate(all_geoms):
overlaps = self.cost_evaluator.collision_engine.check_collision( 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): if isinstance(overlaps, int):
collision_count += overlaps collision_count += overlaps
@ -172,9 +180,10 @@ class PathFinder:
collision_count = 0 collision_count = 0
for comp in res.path: 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( 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): if isinstance(overlaps, int):
collision_count += overlaps collision_count += overlaps

View file

@ -50,7 +50,7 @@ def test_sbend_generation() -> None:
result = SBend.generate(start, offset, radius, width) result = SBend.generate(start, offset, radius, width)
assert result.end_port.y == 5.0 assert result.end_port.y == 5.0
assert result.end_port.orientation == 0.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 # Verify failure for large offset
with pytest.raises(ValueError, match=r"SBend offset .* must be less than 2\*radius"): 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 width = 2.0
res_bbox = SBend.generate(start, offset, radius, width, collision_type="bbox") res_bbox = SBend.generate(start, offset, radius, width, collision_type="bbox")
# Geometry should be a single bounding box polygon # Geometry should be a list of individual bbox polygons for each arc
assert len(res_bbox.geometry) == 1 assert len(res_bbox.geometry) == 2
res_arc = SBend.generate(start, offset, radius, width, collision_type="arc") 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: 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 # For a port at 90 deg, +offset is a shift in -x direction
assert abs(res.end_port.x - (10.0 - offset)) < 1e-6 assert abs(res.end_port.x - (10.0 - offset)) < 1e-6
# Geometry should be connected (unary_union results in 1 polygon) # Geometry should be a list of valid polygons
assert len(res.geometry) == 1 assert len(res.geometry) == 2
assert res.geometry[0].is_valid for p in res.geometry:
assert p.is_valid
def test_arc_sagitta_precision() -> None: def test_arc_sagitta_precision() -> None: