performance bottleneck

This commit is contained in:
Jan Petykiewicz 2026-03-12 23:50:25 -07:00
commit 3810e64a5c
18 changed files with 298 additions and 412 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 303 KiB

Before After
Before After

View file

@ -1,11 +1,12 @@
import numpy as np import numpy as np
import time
from inire.geometry.collision import CollisionEngine from inire.geometry.collision import CollisionEngine
from inire.geometry.primitives import Port from inire.geometry.primitives import Port
from inire.router.astar import AStarRouter from inire.router.astar import AStarRouter
from inire.router.cost import CostEvaluator from inire.router.cost import CostEvaluator
from inire.router.danger_map import DangerMap from inire.router.danger_map import DangerMap
from inire.router.pathfinder import PathFinder from inire.router.pathfinder import PathFinder
from inire.utils.visualization import plot_routing_results from inire.utils.visualization import plot_routing_results, plot_danger_map, plot_expanded_nodes
from shapely.geometry import box from shapely.geometry import box
def main() -> None: def main() -> None:
@ -28,8 +29,8 @@ def main() -> None:
evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.5) evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.5)
router = AStarRouter(evaluator, node_limit=50000, snap_size=5.0) router = AStarRouter(evaluator, node_limit=5000, snap_size=10.0)
pf = PathFinder(router, evaluator, max_iterations=10) pf = PathFinder(router, evaluator, max_iterations=20, base_congestion_penalty=500.0)
# 2. Define Netlist # 2. Define Netlist
netlist = {} netlist = {}
@ -48,16 +49,51 @@ def main() -> None:
net_widths = {nid: 2.0 for nid in netlist} net_widths = {nid: 2.0 for nid in netlist}
def iteration_callback(idx, current_results):
print(f" Iteration {idx} finished. Successes: {sum(1 for r in current_results.values() if r.is_valid)}/{len(netlist)}")
# fig, ax = plot_routing_results(current_results, obstacles, bounds, netlist=netlist)
# plot_danger_map(danger_map, ax=ax)
# fig.savefig(f"examples/07_iteration_{idx:02d}.png")
# import matplotlib.pyplot as plt
# plt.close(fig)
# 3. Route # 3. Route
print(f"Routing {len(netlist)} nets through 200um bottleneck...") print(f"Routing {len(netlist)} nets through 200um bottleneck...")
results = pf.route_all(netlist, net_widths) import cProfile, pstats
profiler = cProfile.Profile()
profiler.enable()
t0 = time.perf_counter()
results = pf.route_all(netlist, net_widths, store_expanded=True, iteration_callback=iteration_callback)
t1 = time.perf_counter()
profiler.disable()
stats = pstats.Stats(profiler).sort_stats('tottime')
stats.print_stats(20)
print(f"Routing took {t1-t0:.4f}s")
# 4. Check Results # 4. Check Results
success_count = sum(1 for res in results.values() if res.is_valid) success_count = sum(1 for res in results.values() if res.is_valid)
print(f"Routed {success_count}/{len(netlist)} nets successfully.") print(f"Routed {success_count}/{len(netlist)} nets successfully.")
for nid, res in results.items():
if not res.is_valid:
print(f" FAILED: {nid}")
else:
types = [move.move_type for move in res.path]
from collections import Counter
counts = Counter(types)
print(f" {nid}: {len(res.path)} segments, {dict(counts)}")
# 5. Visualize # 5. Visualize
fig, ax = plot_routing_results(results, obstacles, bounds, netlist=netlist) fig, ax = plot_routing_results(results, obstacles, bounds, netlist=netlist)
# Overlay Danger Map
plot_danger_map(danger_map, ax=ax)
# Overlay Expanded Nodes from last routed net (as an example)
if pf.router.last_expanded_nodes:
print(f"Plotting {len(pf.router.last_expanded_nodes)} expanded nodes for the last net...")
plot_expanded_nodes(pf.router.last_expanded_nodes, ax=ax, color='blue', alpha=0.1)
fig.savefig("examples/07_large_scale_routing.png") fig.savefig("examples/07_large_scale_routing.png")
print("Saved plot to examples/07_large_scale_routing.png") print("Saved plot to examples/07_large_scale_routing.png")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Before After
Before After

View file

@ -175,42 +175,56 @@ class CollisionEngine:
Returns: Returns:
Boolean if static, integer count if congestion. Boolean if static, integer count if congestion.
""" """
# Optimization: Pre-fetch some members
sz = self.safety_zone_radius
if buffer_mode == 'static': if buffer_mode == 'static':
# Use raw query against pre-dilated obstacles # Use raw query against pre-dilated obstacles
candidates = self.static_index.intersection(geometry.bounds) bounds = geometry.bounds
candidates = self.static_index.intersection(bounds)
static_prepared = self.static_prepared
static_dilated = self.static_dilated
static_geometries = self.static_geometries
for obj_id in candidates: for obj_id in candidates:
if self.static_prepared[obj_id].intersects(geometry): if static_prepared[obj_id].intersects(geometry):
if start_port or end_port: if start_port or end_port:
# Optimization: Skip expensive intersection if neither port is near the obstacle's bounds # Optimization: Skip expensive intersection if neither port is near the obstacle's bounds
# (Plus a small margin for safety zone)
sz = self.safety_zone_radius
is_near_port = False is_near_port = False
for p in [start_port, end_port]: b = static_dilated[obj_id].bounds
if p: if start_port:
# Quick bounds check if (b[0] - sz <= start_port.x <= b[2] + sz and
b = self.static_dilated[obj_id].bounds b[1] - sz <= start_port.y <= b[3] + sz):
if (b[0] - sz <= p.x <= b[2] + sz and is_near_port = True
b[1] - sz <= p.y <= b[3] + sz): if not is_near_port and end_port:
is_near_port = True if (b[0] - sz <= end_port.x <= b[2] + sz and
break b[1] - sz <= end_port.y <= b[3] + sz):
is_near_port = True
if not is_near_port: if not is_near_port:
return True # Collision, and not near any port safety zone return True # Collision, and not near any port safety zone
# Only if near port, do the expensive check # Only if near port, do the expensive check
raw_obstacle = self.static_geometries[obj_id] raw_obstacle = static_geometries[obj_id]
intersection = geometry.intersection(raw_obstacle) intersection = geometry.intersection(raw_obstacle)
if not intersection.is_empty: if not intersection.is_empty:
ix_minx, ix_miny, ix_maxx, ix_maxy = intersection.bounds ix_bounds = intersection.bounds
is_safe = False is_safe = False
for p in [start_port, end_port]: # Check start port
if p and (abs(ix_minx - p.x) < sz and if start_port:
abs(ix_maxx - p.x) < sz and if (abs(ix_bounds[0] - start_port.x) < sz and
abs(ix_miny - p.y) < sz and abs(ix_bounds[2] - start_port.x) < sz and
abs(ix_maxy - p.y) < sz): abs(ix_bounds[1] - start_port.y) < sz and
abs(ix_bounds[3] - start_port.y) < sz):
is_safe = True
# Check end port
if not is_safe and end_port:
if (abs(ix_bounds[0] - end_port.x) < sz and
abs(ix_bounds[2] - end_port.x) < sz and
abs(ix_bounds[1] - end_port.y) < sz and
abs(ix_bounds[3] - end_port.y) < sz):
is_safe = True is_safe = True
break
if is_safe: if is_safe:
continue continue
@ -222,9 +236,12 @@ class CollisionEngine:
test_poly = dilated_geometry if dilated_geometry else 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)
dynamic_geometries = self.dynamic_geometries
dynamic_prepared = self.dynamic_prepared
count = 0 count = 0
for obj_id in candidates: for obj_id in candidates:
other_net_id, _ = self.dynamic_geometries[obj_id] other_net_id, _ = dynamic_geometries[obj_id]
if other_net_id != net_id and self.dynamic_prepared[obj_id].intersects(test_poly): if other_net_id != net_id and dynamic_prepared[obj_id].intersects(test_poly):
count += 1 count += 1
return count return count

View file

@ -1,9 +1,10 @@
from __future__ import annotations from __future__ import annotations
from typing import Literal, cast import math
from typing import Literal, cast, Any
import numpy import numpy
import shapely import shapely
from shapely.geometry import Polygon, box from shapely.geometry import Polygon, box, MultiPolygon
from shapely.ops import unary_union from shapely.ops import unary_union
from .primitives import Port from .primitives import Port
@ -17,54 +18,18 @@ SEARCH_GRID_SNAP_UM = 5.0
def snap_search_grid(value: float, snap_size: float = SEARCH_GRID_SNAP_UM) -> float: def snap_search_grid(value: float, snap_size: float = SEARCH_GRID_SNAP_UM) -> float:
""" """
Snap a coordinate to the nearest search grid unit. Snap a coordinate to the nearest search grid unit.
Args:
value: Value to snap.
snap_size: The grid size to snap to.
Returns:
Snapped value.
""" """
if snap_size <= 0:
return value
return round(value / snap_size) * snap_size return round(value / snap_size) * snap_size
class ComponentResult: class ComponentResult:
""" """
The result of a component generation: geometry, final port, and physical length. Standard container for generated move geometry and state.
""" """
__slots__ = ('geometry', 'dilated_geometry', 'proxy_geometry', 'actual_geometry', 'end_port', 'length', 'bounds', 'dilated_bounds', 'move_type', '_t_cache') __slots__ = (
'geometry', 'dilated_geometry', 'proxy_geometry', 'actual_geometry',
geometry: list[Polygon] 'end_port', 'length', 'move_type', 'bounds', 'dilated_bounds', '_t_cache'
""" List of polygons representing the component geometry (could be proxy or arc) """ )
dilated_geometry: list[Polygon] | None
""" Optional list of pre-dilated polygons for collision optimization """
proxy_geometry: list[Polygon] | None
""" Simplified conservative proxy for tiered collision checks """
actual_geometry: list[Polygon] | None
""" High-fidelity 'actual' geometry for visualization (always the arc) """
end_port: Port
""" The final port after the component """
length: float
""" Physical length of the component path """
bounds: numpy.ndarray
""" Pre-calculated bounds for each polygon in geometry """
dilated_bounds: numpy.ndarray | None
""" Pre-calculated bounds for each polygon in dilated_geometry """
move_type: str | None
""" Identifier for the type of move (e.g. 'Straight', 'Bend90', 'SBend') """
_t_cache: dict[tuple[float, float], ComponentResult]
""" Cache for translated versions of this result """
def __init__( def __init__(
self, self,
@ -75,7 +40,7 @@ class ComponentResult:
proxy_geometry: list[Polygon] | None = None, proxy_geometry: list[Polygon] | None = None,
actual_geometry: list[Polygon] | None = None, actual_geometry: list[Polygon] | None = None,
skip_bounds: bool = False, skip_bounds: bool = False,
move_type: str | None = None, move_type: str = 'Unknown'
) -> None: ) -> None:
self.geometry = geometry self.geometry = geometry
self.dilated_geometry = dilated_geometry self.dilated_geometry = dilated_geometry
@ -100,7 +65,7 @@ class ComponentResult:
if (dxr, dyr) in self._t_cache: if (dxr, dyr) in self._t_cache:
return self._t_cache[(dxr, dyr)] return self._t_cache[(dxr, dyr)]
# Vectorized translation if possible, else list comp # Vectorized translation
geoms = list(self.geometry) geoms = list(self.geometry)
num_geom = len(self.geometry) num_geom = len(self.geometry)
@ -117,13 +82,14 @@ class ComponentResult:
geoms.extend(self.actual_geometry) geoms.extend(self.actual_geometry)
offsets.append(len(geoms)) offsets.append(len(geoms))
from shapely.affinity import translate import shapely
translated = [translate(p, dx, dy) for p in geoms] coords = shapely.get_coordinates(geoms)
translated = shapely.set_coordinates(geoms, coords + [dx, dy])
new_geom = translated[:offsets[0]] new_geom = list(translated[:offsets[0]])
new_dil = translated[offsets[0]:offsets[1]] if self.dilated_geometry is not None else None new_dil = list(translated[offsets[0]:offsets[1]]) if self.dilated_geometry is not None else None
new_proxy = translated[offsets[1]:offsets[2]] if self.proxy_geometry is not None else None new_proxy = list(translated[offsets[1]:offsets[2]]) if self.proxy_geometry is not None else None
new_actual = translated[offsets[2]:offsets[3]] if self.actual_geometry is not None else None new_actual = list(translated[offsets[2]:offsets[3]]) if self.actual_geometry is not None else None
new_port = Port(self.end_port.x + dx, self.end_port.y + dy, self.end_port.orientation) new_port = Port(self.end_port.x + dx, self.end_port.y + dy, self.end_port.orientation)
res = ComponentResult(new_geom, new_port, self.length, new_dil, new_proxy, new_actual, skip_bounds=True, move_type=self.move_type) res = ComponentResult(new_geom, new_port, self.length, new_dil, new_proxy, new_actual, skip_bounds=True, move_type=self.move_type)
@ -156,17 +122,6 @@ class Straight:
) -> ComponentResult: ) -> ComponentResult:
""" """
Generate a straight waveguide segment. Generate a straight waveguide segment.
Args:
start_port: Port to start from.
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.
snap_size: Grid size for snapping.
Returns:
A ComponentResult containing the straight segment.
""" """
rad = numpy.radians(start_port.orientation) rad = numpy.radians(start_port.orientation)
cos_val = numpy.cos(rad) cos_val = numpy.cos(rad)
@ -218,14 +173,6 @@ class Straight:
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:
""" """
Calculate number of segments for an arc to maintain a maximum sagitta. Calculate number of segments for an arc to maintain a maximum sagitta.
Args:
radius: Arc radius.
angle_deg: Total angle turned.
sagitta: Maximum allowed deviation.
Returns:
Minimum number of segments needed.
""" """
if radius <= 0: if radius <= 0:
return 1 return 1
@ -249,17 +196,6 @@ def _get_arc_polygons(
) -> list[Polygon]: ) -> list[Polygon]:
""" """
Helper to generate arc-shaped polygons using vectorized NumPy operations. Helper to generate arc-shaped polygons using vectorized NumPy operations.
Args:
cx, cy: Center coordinates.
radius: Arc radius.
width: Waveguide width.
t_start, t_end: Start and end angles (radians).
sagitta: Geometric fidelity.
dilation: Optional dilation to apply directly to the arc.
Returns:
List containing the arc polygon.
""" """
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)
@ -324,8 +260,6 @@ def _clip_bbox(
dx, dy = p[0] - cx, p[1] - cy dx, dy = p[0] - cx, p[1] - cy
dist = numpy.sqrt(dx**2 + dy**2) dist = numpy.sqrt(dx**2 + dy**2)
angle = numpy.arctan2(dy, dx) angle = numpy.arctan2(dy, dx)
# Check if corner angle is within the arc's angular sweep
angle_rel = (angle - ts_norm) % (2 * numpy.pi) angle_rel = (angle - ts_norm) % (2 * numpy.pi)
is_in_sweep = angle_rel <= sweep + 1e-6 is_in_sweep = angle_rel <= sweep + 1e-6
@ -379,18 +313,6 @@ def _apply_collision_model(
) -> list[Polygon]: ) -> list[Polygon]:
""" """
Applies the specified collision model to an arc geometry. Applies the specified collision model to an arc geometry.
Args:
arc_poly: High-fidelity arc.
collision_type: Model type or custom polygon.
radius: Arc radius.
width: Waveguide width.
cx, cy: Arc center.
clip_margin: Safety margin for clipping.
t_start, t_end: Arc angles.
Returns:
List of polygons representing the collision model.
""" """
if isinstance(collision_type, Polygon): if isinstance(collision_type, Polygon):
return [collision_type] return [collision_type]
@ -398,29 +320,29 @@ def _apply_collision_model(
if collision_type == "arc": if collision_type == "arc":
return [arc_poly] return [arc_poly]
# Get bounding box # Bounding box of the high-fidelity arc
minx, miny, maxx, maxy = arc_poly.bounds minx, miny, maxx, maxy = arc_poly.bounds
bbox = box(minx, miny, maxx, maxy) bbox_poly = box(minx, miny, maxx, maxy)
if collision_type == "bbox": if collision_type == "bbox":
return [bbox] return [bbox_poly]
if collision_type == "clipped_bbox": if collision_type == "clipped_bbox":
return [_clip_bbox(bbox, cx, cy, radius, width, clip_margin, arc_poly, t_start, t_end)] return [_clip_bbox(bbox_poly, cx, cy, radius, width, clip_margin, arc_poly, t_start, t_end)]
return [arc_poly] return [arc_poly]
class Bend90: class Bend90:
""" """
Move generator for 90-degree bends. Move generator for 90-degree waveguide bends.
""" """
@staticmethod @staticmethod
def generate( def generate(
start_port: Port, start_port: Port,
radius: float, radius: float,
width: float, width: float,
direction: str = "CW", direction: Literal["CW", "CCW"],
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,
@ -430,46 +352,31 @@ class Bend90:
""" """
Generate a 90-degree bend. Generate a 90-degree bend.
""" """
turn_angle = -90 if direction == "CW" else 90
rad_start = numpy.radians(start_port.orientation) rad_start = numpy.radians(start_port.orientation)
c_angle = rad_start + (numpy.pi / 2 if direction == "CCW" else -numpy.pi / 2)
# Initial guess for center # Center of the arc
cx_init = start_port.x + radius * numpy.cos(c_angle)
cy_init = start_port.y + radius * numpy.sin(c_angle)
t_start_init = c_angle + numpy.pi
t_end_init = t_start_init + (numpy.pi / 2 if direction == "CCW" else -numpy.pi / 2)
# Snap the target point
ex = snap_search_grid(cx_init + radius * numpy.cos(t_end_init), snap_size)
ey = snap_search_grid(cy_init + radius * numpy.sin(t_end_init), snap_size)
end_port = Port(ex, ey, float((start_port.orientation + turn_angle) % 360))
# Adjust geometry to perfectly hit snapped port
dx = ex - start_port.x
dy = ey - start_port.y
dist = numpy.sqrt(dx**2 + dy**2)
# New radius for the right triangle connecting start to end with 90 deg
actual_radius = dist / numpy.sqrt(2)
# Vector from start to end
mid_x, mid_y = (start_port.x + ex)/2, (start_port.y + ey)/2
# Normal vector (orthogonal to start->end)
# Flip direction based on CW/CCW
dir_sign = 1 if direction == "CCW" else -1
cx = mid_x - dir_sign * (ey - start_port.y) / 2
cy = mid_y + dir_sign * (ex - start_port.x) / 2
# Update angles based on new center
t_start = numpy.arctan2(start_port.y - cy, start_port.x - cx)
t_end = numpy.arctan2(ey - cy, ex - cx)
# Maintain directionality and angular span near pi/2
if direction == "CCW": if direction == "CCW":
while t_end < t_start: t_end += 2 * numpy.pi cx = start_port.x + radius * numpy.cos(rad_start + numpy.pi / 2)
cy = start_port.y + radius * numpy.sin(rad_start + numpy.pi / 2)
t_start = rad_start - numpy.pi / 2
t_end = t_start + numpy.pi / 2
new_ori = (start_port.orientation + 90) % 360
else: else:
while t_end > t_start: t_end -= 2 * numpy.pi cx = start_port.x + radius * numpy.cos(rad_start - numpy.pi / 2)
cy = start_port.y + radius * numpy.sin(rad_start - numpy.pi / 2)
t_start = rad_start + numpy.pi / 2
t_end = t_start - numpy.pi / 2
new_ori = (start_port.orientation - 90) % 360
# Snap the end point to the grid
ex_raw = cx + radius * numpy.cos(t_end)
ey_raw = cy + radius * numpy.sin(t_end)
ex = snap_search_grid(ex_raw, snap_size)
ey = snap_search_grid(ey_raw, snap_size)
# Slightly adjust radius to hit snapped point exactly
actual_radius = numpy.sqrt((ex - cx)**2 + (ey - cy)**2)
end_port = Port(ex, ey, new_ori)
arc_polys = _get_arc_polygons(cx, cy, actual_radius, width, t_start, t_end, sagitta) arc_polys = _get_arc_polygons(cx, cy, actual_radius, width, t_start, t_end, sagitta)
collision_polys = _apply_collision_model( collision_polys = _apply_collision_model(
@ -500,8 +407,6 @@ class Bend90:
move_type='Bend90' move_type='Bend90'
) )
)
class SBend: class SBend:
""" """

View file

@ -10,14 +10,8 @@ GRID_SNAP_UM = 0.001
def snap_nm(value: float) -> float: def snap_nm(value: float) -> float:
""" """
Snap a coordinate to the nearest 1nm (0.001 um). Snap a coordinate to the nearest 1nm (0.001 um).
Args:
value: Coordinate value to snap.
Returns:
Snapped coordinate value.
""" """
return round(value / GRID_SNAP_UM) * GRID_SNAP_UM return round(value * 1000) / 1000
class Port: class Port:
@ -26,39 +20,15 @@ class Port:
""" """
__slots__ = ('x', 'y', 'orientation') __slots__ = ('x', 'y', 'orientation')
x: float
""" x-coordinate in micrometers """
y: float
""" y-coordinate in micrometers """
orientation: float
""" Orientation in degrees: 0, 90, 180, 270 """
def __init__( def __init__(
self, self,
x: float, x: float,
y: float, y: float,
orientation: float, orientation: float,
) -> None: ) -> None:
""" self.x = x
Initialize and snap a Port. self.y = y
self.orientation = float(orientation % 360)
Args:
x: Initial x-coordinate.
y: Initial y-coordinate.
orientation: Initial orientation in degrees.
"""
# Snap x, y to 1nm
self.x = snap_nm(x)
self.y = snap_nm(y)
# Ensure orientation is one of {0, 90, 180, 270}
norm_orientation = int(round(orientation)) % 360
if norm_orientation not in {0, 90, 180, 270}:
norm_orientation = (round(norm_orientation / 90) * 90) % 360
self.orientation = float(norm_orientation)
def __repr__(self) -> str: def __repr__(self) -> str:
return f'Port(x={self.x}, y={self.y}, orientation={self.orientation})' return f'Port(x={self.x}, y={self.y}, orientation={self.orientation})'
@ -77,14 +47,6 @@ class Port:
def translate_port(port: Port, dx: float, dy: float) -> Port: def translate_port(port: Port, dx: float, dy: float) -> Port:
""" """
Translate a port by (dx, dy). Translate a port by (dx, dy).
Args:
port: Port to translate.
dx: x-offset.
dy: y-offset.
Returns:
A new translated Port.
""" """
return Port(port.x + dx, port.y + dy, port.orientation) return Port(port.x + dx, port.y + dy, port.orientation)
@ -92,14 +54,6 @@ def translate_port(port: Port, dx: float, dy: float) -> Port:
def rotate_port(port: Port, angle: float, origin: tuple[float, float] = (0, 0)) -> Port: def rotate_port(port: Port, angle: float, origin: tuple[float, float] = (0, 0)) -> Port:
""" """
Rotate a port by a multiple of 90 degrees around an origin. Rotate a port by a multiple of 90 degrees around an origin.
Args:
port: Port to rotate.
angle: Angle to rotate by (degrees).
origin: (x, y) origin to rotate around.
Returns:
A new rotated Port.
""" """
ox, oy = origin ox, oy = origin
px, py = port.x, port.y px, py = port.x, port.y

View file

@ -45,20 +45,29 @@ class AStarNode:
else: else:
# Union of parent's bbox and current move's bbox # Union of parent's bbox and current move's bbox
if component_result: if component_result:
# Merge all polygon bounds in the result # Use pre-calculated bounds if available, avoiding numpy overhead
b = component_result.dilated_bounds if component_result.dilated_bounds is not None else component_result.bounds # component_result.bounds is (N, 4)
minx = numpy.min(b[:, 0]) if component_result.dilated_bounds is not None:
miny = numpy.min(b[:, 1]) b = component_result.dilated_bounds
maxx = numpy.max(b[:, 2]) else:
maxy = numpy.max(b[:, 3]) b = component_result.bounds
# Fast min/max for typically 1 polygon
if len(b) == 1:
minx, miny, maxx, maxy = b[0]
else:
minx = min(row[0] for row in b)
miny = min(row[1] for row in b)
maxx = max(row[2] for row in b)
maxy = max(row[3] for row in b)
if parent.path_bbox: if parent.path_bbox:
pb = parent.path_bbox pb = parent.path_bbox
self.path_bbox = ( self.path_bbox = (
min(minx, pb[0]), minx if minx < pb[0] else pb[0],
min(miny, pb[1]), miny if miny < pb[1] else pb[1],
max(maxx, pb[2]), maxx if maxx > pb[2] else pb[2],
max(maxy, pb[3]) maxy if maxy > pb[3] else pb[3]
) )
else: else:
self.path_bbox = (minx, miny, maxx, maxy) self.path_bbox = (minx, miny, maxx, maxy)
@ -88,8 +97,11 @@ class AStarRouter:
self.node_limit = self.config.node_limit self.node_limit = self.config.node_limit
# Performance cache for collision checks # Performance cache for collision checks
# Key: (start_x, start_y, start_ori, move_type, width, net_id) -> bool # Key: (start_x_grid, start_y_grid, start_ori, move_type, width) -> bool
self._collision_cache: dict[tuple, bool] = {} self._collision_cache: dict[tuple, bool] = {}
# FAST CACHE: set of keys that are known to collide (hard collisions)
self._hard_collision_set: set[tuple] = set()
# New: cache for congestion overlaps within a single route session # New: cache for congestion overlaps within a single route session
self._congestion_cache: dict[tuple, int] = {} self._congestion_cache: dict[tuple, int] = {}
@ -117,38 +129,19 @@ class AStarRouter:
) -> list[ComponentResult] | None: ) -> list[ComponentResult] | None:
""" """
Route a single net using A*. Route a single net using A*.
Args:
start: Starting port.
target: Target port.
net_width: Waveguide width (um).
net_id: Optional net identifier.
bend_collision_type: Override collision model for this route.
return_partial: If True, return the best partial path on failure.
store_expanded: If True, keep track of all expanded nodes for visualization.
Returns:
List of moves forming the path, or None if failed.
""" """
# Clear congestion cache for each new net/iteration
self._congestion_cache.clear() self._congestion_cache.clear()
if store_expanded: if store_expanded:
self.last_expanded_nodes = [] self.last_expanded_nodes = []
if bend_collision_type is not None: if bend_collision_type is not None:
self.config.bend_collision_type = bend_collision_type self.config.bend_collision_type = bend_collision_type
# Do NOT clear _collision_cache here to allow sharing static collision results across nets
# self._collision_cache.clear()
open_set: list[AStarNode] = [] open_set: list[AStarNode] = []
# Calculate rounding precision based on search grid snap = self.config.snap_size
# e.g. 1.0 -> 0, 0.1 -> 1, 0.001 -> 3
state_precision = int(numpy.ceil(-numpy.log10(SEARCH_GRID_SNAP_UM))) if SEARCH_GRID_SNAP_UM < 1.0 else 0
# Key: (x, y, orientation) rounded to search grid # Key: (x_grid, y_grid, orientation_grid) -> min_g_cost
closed_set: set[tuple[float, float, float]] = set() closed_set: dict[tuple[int, int, int], float] = {}
start_node = AStarNode(start, 0.0, self.cost_evaluator.h_manhattan(start, target)) start_node = AStarNode(start, 0.0, self.cost_evaluator.h_manhattan(start, target))
heapq.heappush(open_set, start_node) heapq.heappush(open_set, start_node)
@ -156,10 +149,12 @@ class AStarRouter:
best_node = start_node best_node = start_node
nodes_expanded = 0 nodes_expanded = 0
node_limit = self.node_limit
reconstruct_path = self._reconstruct_path
while open_set: while open_set:
if nodes_expanded >= self.node_limit: if nodes_expanded >= node_limit:
logger.warning(f' AStar failed: node limit {self.node_limit} reached.') return reconstruct_path(best_node) if return_partial else None
return self._reconstruct_path(best_node) if return_partial else None
current = heapq.heappop(open_set) current = heapq.heappop(open_set)
@ -167,11 +162,11 @@ class AStarRouter:
if current.h_cost < best_node.h_cost: if current.h_cost < best_node.h_cost:
best_node = current best_node = current
# Prune if already visited # Prune if already visited with a better path
state = (round(current.port.x, state_precision), round(current.port.y, state_precision), round(current.port.orientation, 2)) state = (int(current.port.x / snap), int(current.port.y / snap), int(current.port.orientation / 1.0))
if state in closed_set: if state in closed_set and closed_set[state] <= current.g_cost + 1e-6:
continue continue
closed_set.add(state) closed_set[state] = current.g_cost
if store_expanded: if store_expanded:
self.last_expanded_nodes.append((current.port.x, current.port.y, current.port.orientation)) self.last_expanded_nodes.append((current.port.x, current.port.y, current.port.orientation))
@ -179,19 +174,16 @@ class AStarRouter:
nodes_expanded += 1 nodes_expanded += 1
self.total_nodes_expanded += 1 self.total_nodes_expanded += 1
if nodes_expanded % 10000 == 0:
logger.info(f'Nodes expanded: {nodes_expanded}, current: {current.port}, g: {current.g_cost:.1f}')
# Check if we reached the target exactly # Check if we reached the target exactly
if (abs(current.port.x - target.x) < 1e-6 and if (abs(current.port.x - target.x) < 1e-6 and
abs(current.port.y - target.y) < 1e-6 and abs(current.port.y - target.y) < 1e-6 and
abs(current.port.orientation - target.orientation) < 0.1): abs(current.port.orientation - target.orientation) < 0.1):
return self._reconstruct_path(current) return reconstruct_path(current)
# Expansion # Expansion
self._expand_moves(current, target, net_width, net_id, open_set, closed_set, state_precision, nodes_expanded) self._expand_moves(current, target, net_width, net_id, open_set, closed_set, snap, nodes_expanded)
return self._reconstruct_path(best_node) if return_partial else None return reconstruct_path(best_node) if return_partial else None
def _expand_moves( def _expand_moves(
self, self,
@ -200,32 +192,36 @@ class AStarRouter:
net_width: float, net_width: float,
net_id: str, net_id: str,
open_set: list[AStarNode], open_set: list[AStarNode],
closed_set: set[tuple[float, float, float]], closed_set: dict[tuple[int, int, int], float],
state_precision: int = 0, snap: float = 1.0,
nodes_expanded: int = 0, nodes_expanded: int = 0,
) -> None: ) -> None:
# 1. Snap-to-Target Look-ahead # 1. Snap-to-Target Look-ahead
dist = numpy.sqrt((current.port.x - target.x)**2 + (current.port.y - target.y)**2) dx_t = target.x - current.port.x
if dist < self.config.snap_to_target_dist: dy_t = target.y - current.port.y
dist_sq = dx_t*dx_t + dy_t*dy_t
snap_dist = self.config.snap_to_target_dist
if dist_sq < snap_dist * snap_dist:
# A. Try straight exact reach # A. Try straight exact reach
if abs(current.port.orientation - target.orientation) < 0.1: if abs(current.port.orientation - target.orientation) < 0.1:
rad = numpy.radians(current.port.orientation) rad = numpy.radians(current.port.orientation)
dx = target.x - current.port.x cos_r = numpy.cos(rad)
dy = target.y - current.port.y sin_r = numpy.sin(rad)
proj = dx * numpy.cos(rad) + dy * numpy.sin(rad) proj = dx_t * cos_r + dy_t * sin_r
perp = -dx * numpy.sin(rad) + dy * numpy.cos(rad) perp = -dx_t * sin_r + dy_t * cos_r
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, dilation=self._self_dilation, snap_size=self.config.snap_size) res = Straight.generate(current.port, proj, net_width, snap_to_grid=False, dilation=self._self_dilation, snap_size=self.config.snap_size)
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, 'SnapStraight', state_precision=state_precision) self._add_node(current, res, target, net_width, net_id, open_set, closed_set, 'SnapStraight', snap=snap)
# B. Try SBend exact reach # B. Try SBend exact reach
if abs(current.port.orientation - target.orientation) < 0.1: if abs(current.port.orientation - target.orientation) < 0.1:
rad = numpy.radians(current.port.orientation) rad = numpy.radians(current.port.orientation)
dx = target.x - current.port.x cos_r = numpy.cos(rad)
dy = target.y - current.port.y sin_r = numpy.sin(rad)
proj = dx * numpy.cos(rad) + dy * numpy.sin(rad) proj = dx_t * cos_r + dy_t * sin_r
perp = -dx * numpy.sin(rad) + dy * numpy.cos(rad) perp = -dx_t * sin_r + dy_t * cos_r
if proj > 0 and 0.5 <= abs(perp) < 100.0: # Match snap_to_target_dist if proj > 0 and 0.5 <= abs(perp) < snap_dist:
for radius in self.config.sbend_radii: for radius in self.config.sbend_radii:
try: try:
res = SBend.generate( res = SBend.generate(
@ -238,42 +234,40 @@ class AStarRouter:
dilation=self._self_dilation, dilation=self._self_dilation,
snap_size=self.config.snap_size snap_size=self.config.snap_size
) )
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, 'SnapSBend', move_radius=radius, state_precision=state_precision) self._add_node(current, res, target, net_width, net_id, open_set, closed_set, 'SnapSBend', move_radius=radius, snap=snap)
except ValueError: except ValueError:
pass pass
# 2. Lattice Straights # 2. Lattice Straights
cp = current.port cp = current.port
base_ori = round(cp.orientation, 2) base_ori = round(cp.orientation, 2)
state_key = (round(cp.x, state_precision), round(cp.y, state_precision), base_ori) state_key = (int(cp.x / snap), int(cp.y / snap), int(base_ori / 1.0))
lengths = self.config.straight_lengths # Backwards pruning
if dist < 5.0: allow_backwards = (dist_sq < 200*200)
fine_steps = [0.1, 0.5]
lengths = sorted(set(lengths + fine_steps))
for length in lengths: for length in self.config.straight_lengths:
# Level 1: Absolute cache (exact location) # Level 1: Absolute cache (exact location)
abs_key = (state_key, 'S', length, net_width) abs_key = (state_key, 'S', length, net_width)
if abs_key in self._move_cache: if abs_key in self._move_cache:
res = self._move_cache[abs_key] res = self._move_cache[abs_key]
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f'S{length}', state_precision=state_precision) self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f'S{length}', snap=snap)
else: else:
# Level 2: Relative cache (orientation only) # Level 2: Relative cache (orientation only)
rel_key = (base_ori, 'S', length, net_width, self._self_dilation) rel_key = (base_ori, 'S', length, net_width, self._self_dilation)
# OPTIMIZATION: Check static collision cache BEFORE translating # OPTIMIZATION: Check hard collision set
move_type = f'S{length}' move_type = f'S{length}'
cache_key = (state_key[0], state_key[1], base_ori, move_type, net_width) cache_key = (state_key[0], state_key[1], base_ori, move_type, net_width)
if cache_key in self._collision_cache and self._collision_cache[cache_key]: if cache_key in self._hard_collision_set:
continue # Hard collision cached continue
if rel_key in self._move_cache: if rel_key in self._move_cache:
res_rel = self._move_cache[rel_key] res_rel = self._move_cache[rel_key]
# Fast check: would translated end port be in closed set? # Fast check: would translated end port be in closed set?
ex = res_rel.end_port.x + cp.x ex = res_rel.end_port.x + cp.x
ey = res_rel.end_port.y + cp.y ey = res_rel.end_port.y + cp.y
end_state = (round(ex, state_precision), round(ey, state_precision), round(res_rel.end_port.orientation, 2)) end_state = (int(ex / snap), int(ey / snap), int(res_rel.end_port.orientation / 1.0))
if end_state in closed_set: if end_state in closed_set:
continue continue
res = res_rel.translate(cp.x, cp.y) res = res_rel.translate(cp.x, cp.y)
@ -282,29 +276,36 @@ class AStarRouter:
self._move_cache[rel_key] = res_rel self._move_cache[rel_key] = res_rel
res = res_rel.translate(cp.x, cp.y) res = res_rel.translate(cp.x, cp.y)
self._move_cache[abs_key] = res self._move_cache[abs_key] = res
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, move_type, state_precision=state_precision) self._add_node(current, res, target, net_width, net_id, open_set, closed_set, move_type, snap=snap)
# 3. Lattice Bends # 3. Lattice Bends
angle_to_target = numpy.degrees(numpy.arctan2(dy_t, dx_t))
for radius in self.config.bend_radii: for radius in self.config.bend_radii:
for direction in ['CW', 'CCW']: for direction in ['CW', 'CCW']:
if not allow_backwards:
turn = 90 if direction == 'CCW' else -90
new_ori = (cp.orientation + turn) % 360
new_diff = (angle_to_target - new_ori + 180) % 360 - 180
if abs(new_diff) > 135:
continue
move_type = f'B{radius}{direction}'
abs_key = (state_key, 'B', radius, direction, net_width, self.config.bend_collision_type) abs_key = (state_key, 'B', radius, direction, net_width, self.config.bend_collision_type)
if abs_key in self._move_cache: if abs_key in self._move_cache:
res = self._move_cache[abs_key] res = self._move_cache[abs_key]
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f'B{radius}{direction}', move_radius=radius, state_precision=state_precision) self._add_node(current, res, target, net_width, net_id, open_set, closed_set, move_type, move_radius=radius, snap=snap)
else: else:
rel_key = (base_ori, 'B', radius, direction, net_width, self.config.bend_collision_type, self._self_dilation) rel_key = (base_ori, 'B', radius, direction, net_width, self.config.bend_collision_type, self._self_dilation)
# OPTIMIZATION: Check static collision cache BEFORE translating
move_type = f'B{radius}{direction}'
cache_key = (state_key[0], state_key[1], base_ori, move_type, net_width) cache_key = (state_key[0], state_key[1], base_ori, move_type, net_width)
if cache_key in self._collision_cache and self._collision_cache[cache_key]: if cache_key in self._hard_collision_set:
continue continue
if rel_key in self._move_cache: if rel_key in self._move_cache:
res_rel = self._move_cache[rel_key] res_rel = self._move_cache[rel_key]
ex = res_rel.end_port.x + cp.x ex = res_rel.end_port.x + cp.x
ey = res_rel.end_port.y + cp.y ey = res_rel.end_port.y + cp.y
end_state = (round(ex, state_precision), round(ey, state_precision), round(res_rel.end_port.orientation, 2)) end_state = (int(ex / snap), int(ey / snap), int(res_rel.end_port.orientation / 1.0))
if end_state in closed_set: if end_state in closed_set:
continue continue
res = res_rel.translate(cp.x, cp.y) res = res_rel.translate(cp.x, cp.y)
@ -322,29 +323,28 @@ class AStarRouter:
self._move_cache[rel_key] = res_rel self._move_cache[rel_key] = res_rel
res = res_rel.translate(cp.x, cp.y) res = res_rel.translate(cp.x, cp.y)
self._move_cache[abs_key] = res self._move_cache[abs_key] = res
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, move_type, move_radius=radius, state_precision=state_precision) self._add_node(current, res, target, net_width, net_id, open_set, closed_set, move_type, move_radius=radius, snap=snap)
# 4. Discrete SBends # 4. Discrete SBends
for offset in self.config.sbend_offsets: for offset in self.config.sbend_offsets:
for radius in self.config.sbend_radii: for radius in self.config.sbend_radii:
move_type = f'SB{offset}R{radius}'
abs_key = (state_key, 'SB', offset, radius, net_width, self.config.bend_collision_type) abs_key = (state_key, 'SB', offset, radius, net_width, self.config.bend_collision_type)
if abs_key in self._move_cache: if abs_key in self._move_cache:
res = self._move_cache[abs_key] res = self._move_cache[abs_key]
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f'SB{offset}R{radius}', move_radius=radius, state_precision=state_precision) self._add_node(current, res, target, net_width, net_id, open_set, closed_set, move_type, move_radius=radius, snap=snap)
else: else:
rel_key = (base_ori, 'SB', offset, radius, net_width, self.config.bend_collision_type, self._self_dilation) rel_key = (base_ori, 'SB', offset, radius, net_width, self.config.bend_collision_type, self._self_dilation)
# OPTIMIZATION: Check static collision cache BEFORE translating
move_type = f'SB{offset}R{radius}'
cache_key = (state_key[0], state_key[1], base_ori, move_type, net_width) cache_key = (state_key[0], state_key[1], base_ori, move_type, net_width)
if cache_key in self._collision_cache and self._collision_cache[cache_key]: if cache_key in self._hard_collision_set:
continue continue
if rel_key in self._move_cache: if rel_key in self._move_cache:
res_rel = self._move_cache[rel_key] res_rel = self._move_cache[rel_key]
ex = res_rel.end_port.x + cp.x ex = res_rel.end_port.x + cp.x
ey = res_rel.end_port.y + cp.y ey = res_rel.end_port.y + cp.y
end_state = (round(ex, state_precision), round(ey, state_precision), round(res_rel.end_port.orientation, 2)) end_state = (int(ex / snap), int(ey / snap), int(res_rel.end_port.orientation / 1.0))
if end_state in closed_set: if end_state in closed_set:
continue continue
res = res_rel.translate(cp.x, cp.y) res = res_rel.translate(cp.x, cp.y)
@ -365,7 +365,7 @@ class AStarRouter:
except ValueError: except ValueError:
continue continue
self._move_cache[abs_key] = res self._move_cache[abs_key] = res
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, move_type, move_radius=radius, state_precision=state_precision) self._add_node(current, res, target, net_width, net_id, open_set, closed_set, move_type, move_radius=radius, snap=snap)
def _add_node( def _add_node(
self, self,
@ -375,99 +375,60 @@ class AStarRouter:
net_width: float, net_width: float,
net_id: str, net_id: str,
open_set: list[AStarNode], open_set: list[AStarNode],
closed_set: set[tuple[float, float, float]], closed_set: dict[tuple[int, int, int], float],
move_type: str, move_type: str,
move_radius: float | None = None, move_radius: float | None = None,
state_precision: int = 0, snap: float = 1.0,
) -> None: ) -> None:
# Check closed set before adding to open set end_p = result.end_port
state = (round(result.end_port.x, state_precision), round(result.end_port.y, state_precision), round(result.end_port.orientation, 2)) state = (int(end_p.x / snap), int(end_p.y / snap), int(end_p.orientation / 1.0))
if state in closed_set: # No need to check closed_set here as pop checks it, but it helps avoid push
if state in closed_set and closed_set[state] <= parent.g_cost: # Conservative
return return
parent_p = parent.port
cache_key = ( cache_key = (
round(parent.port.x, state_precision), int(parent_p.x / snap),
round(parent.port.y, state_precision), int(parent_p.y / snap),
round(parent.port.orientation, 2), int(parent_p.orientation / 1.0),
move_type, move_type,
net_width, net_width,
) )
if cache_key in self._collision_cache:
if self._collision_cache[cache_key]:
return
else:
# Ensure dilated geometry is present for collision check
if result.dilated_geometry is None:
dilation = self._self_dilation
result.dilated_geometry = [p.buffer(dilation) for p in result.geometry]
import shapely
result.dilated_bounds = shapely.bounds(result.dilated_geometry)
if cache_key in self._hard_collision_set:
return
# Safe area check
is_safe_area = False
danger_map = self.cost_evaluator.danger_map
if danger_map.get_cost(parent_p.x, parent_p.y) == 0 and danger_map.get_cost(end_p.x, end_p.y) == 0:
if result.length < (danger_map.safety_threshold - self.cost_evaluator.collision_engine.clearance):
is_safe_area = True
if not is_safe_area:
hard_coll = False hard_coll = False
collision_engine = self.cost_evaluator.collision_engine
for i, poly in enumerate(result.geometry): for i, poly in enumerate(result.geometry):
dil_poly = result.dilated_geometry[i] dil_poly = result.dilated_geometry[i] if result.dilated_geometry else None
if self.cost_evaluator.collision_engine.check_collision( if 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_p, end_port=end_p,
dilated_geometry=dil_poly dilated_geometry=dil_poly
): ):
hard_coll = True hard_coll = True
break break
self._collision_cache[cache_key] = hard_coll
if hard_coll: if hard_coll:
self._hard_collision_set.add(cache_key)
return return
# Ensure dilated geometry is present for self-intersection (if enabled) and cost evaluation # Congestion Check
if result.dilated_geometry is None:
dilation = self._self_dilation
result.dilated_geometry = [p.buffer(dilation) for p in result.geometry]
import shapely
result.dilated_bounds = shapely.bounds(result.dilated_geometry)
# 3. Check for Self-Intersection (Limited to last 50 segments for performance)
if result.dilated_geometry is not None:
# Union of current move's bounds for fast path-wide pruning
b = result.dilated_bounds if result.dilated_bounds is not None else result.bounds
m_minx = numpy.min(b[:, 0])
m_miny = numpy.min(b[:, 1])
m_maxx = numpy.max(b[:, 2])
m_maxy = numpy.max(b[:, 3])
# If current move doesn't overlap the entire parent path bbox, we can skip individual checks
if parent.path_bbox and not (m_minx > parent.path_bbox[2] or
m_maxx < parent.path_bbox[0] or
m_miny > parent.path_bbox[3] or
m_maxy < parent.path_bbox[1]):
for m_idx, move_poly in enumerate(result.geometry):
m_bounds = result.bounds[m_idx]
curr_p: AStarNode | None = parent
seg_idx = 0
while curr_p and curr_p.component_result and seg_idx < 50:
# Skip immediate parent AND grandparent to avoid tangent/port-safety issues
if seg_idx > 1:
res_p = curr_p.component_result
for p_idx, prev_poly in enumerate(res_p.geometry):
p_bounds = res_p.bounds[p_idx]
# Quick bounds overlap check
if not (m_bounds[0] > p_bounds[2] or
m_bounds[2] < p_bounds[0] or
m_bounds[1] > p_bounds[3] or
m_bounds[3] < p_bounds[1]):
# Raw geometry intersection is sufficient for self-collision
if move_poly.intersects(prev_poly):
return
curr_p = curr_p.parent
seg_idx += 1
# 2. Congestion Check (with per-session cache)
total_overlaps = 0 total_overlaps = 0
if cache_key in self._congestion_cache: if cache_key in self._congestion_cache:
total_overlaps = self._congestion_cache[cache_key] total_overlaps = self._congestion_cache[cache_key]
else: else:
collision_engine = self.cost_evaluator.collision_engine
for i, poly in enumerate(result.geometry): for i, poly in enumerate(result.geometry):
dil_poly = result.dilated_geometry[i] dil_poly = result.dilated_geometry[i] if result.dilated_geometry else None
overlaps = self.cost_evaluator.collision_engine.check_collision( overlaps = collision_engine.check_collision(
poly, net_id, buffer_mode='congestion', dilated_geometry=dil_poly poly, net_id, buffer_mode='congestion', dilated_geometry=dil_poly
) )
if isinstance(overlaps, int): if isinstance(overlaps, int):
@ -479,16 +440,13 @@ class AStarRouter:
penalty = self.config.sbend_penalty penalty = self.config.sbend_penalty
elif 'B' in move_type: elif 'B' in move_type:
penalty = self.config.bend_penalty penalty = self.config.bend_penalty
elif 'ZRoute' in move_type:
# ZRoute is like 2 bends
penalty = 2 * self.config.bend_penalty
move_cost = self.cost_evaluator.evaluate_move( move_cost = self.cost_evaluator.evaluate_move(
result.geometry, result.geometry,
result.end_port, result.end_port,
net_width, net_width,
net_id, net_id,
start_port=parent.port, start_port=parent_p,
length=result.length, length=result.length,
dilated_geometry=result.dilated_geometry, dilated_geometry=result.dilated_geometry,
penalty=penalty, penalty=penalty,
@ -500,11 +458,8 @@ class AStarRouter:
if move_cost > 1e12: if move_cost > 1e12:
return return
# Turn penalties scaled by radius to favor larger turns
ref_radius = 10.0
if 'B' in move_type and move_radius is not None: if 'B' in move_type and move_radius is not None:
# Scale cost to favor larger radius bends if they fit move_cost *= (10.0 / move_radius)**0.5
move_cost *= (ref_radius / move_radius)**0.5
g_cost = parent.g_cost + move_cost g_cost = parent.g_cost + move_cost
h_cost = self.cost_evaluator.h_manhattan(result.end_port, target) h_cost = self.cost_evaluator.h_manhattan(result.end_port, target)

View file

@ -11,11 +11,11 @@ class RouterConfig:
node_limit: int = 1000000 node_limit: int = 1000000
snap_size: float = 5.0 snap_size: float = 5.0
straight_lengths: list[float] = field(default_factory=lambda: [5.0, 10.0, 100.0]) straight_lengths: list[float] = field(default_factory=lambda: [10.0, 50.0, 100.0, 500.0, 1000.0])
bend_radii: list[float] = field(default_factory=lambda: [50.0]) bend_radii: list[float] = field(default_factory=lambda: [50.0, 100.0])
sbend_offsets: list[float] = field(default_factory=lambda: [-10.0, -5.0, 5.0, 10.0]) sbend_offsets: list[float] = field(default_factory=lambda: [-100.0, -50.0, -10.0, 10.0, 50.0, 100.0])
sbend_radii: list[float] = field(default_factory=lambda: [50.0]) sbend_radii: list[float] = field(default_factory=lambda: [50.0, 100.0, 500.0])
snap_to_target_dist: float = 100.0 snap_to_target_dist: float = 1000.0
bend_penalty: float = 250.0 bend_penalty: float = 250.0
sbend_penalty: float = 500.0 sbend_penalty: float = 500.0
bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | Any = "arc" bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | Any = "arc"

View file

@ -147,31 +147,36 @@ class CostEvaluator:
Total cost of the move, or 1e15 if invalid. Total cost of the move, or 1e15 if invalid.
""" """
_ = net_width # Unused _ = net_width # Unused
total_cost = length * self.unit_length_cost + penalty
# 1. Boundary Check # 1. Boundary Check
if not self.danger_map.is_within_bounds(end_port.x, end_port.y): danger_map = self.danger_map
if not danger_map.is_within_bounds(end_port.x, end_port.y):
return 1e15 return 1e15
# 2. Collision Check total_cost = length * self.unit_length_cost + penalty
for i, poly in enumerate(geometry):
dil_poly = dilated_geometry[i] if dilated_geometry else None
# Hard Collision (Static obstacles)
if not skip_static:
if self.collision_engine.check_collision(
poly, net_id, buffer_mode='static', start_port=start_port, end_port=end_port,
dilated_geometry=dil_poly
):
return 1e15
# Soft Collision (Negotiated Congestion) # 2. Collision Check
if not skip_congestion: # FAST PATH: skip_static and skip_congestion are often True when called from optimized AStar
overlaps = self.collision_engine.check_collision( if not skip_static or not skip_congestion:
poly, net_id, buffer_mode='congestion', dilated_geometry=dil_poly collision_engine = self.collision_engine
) for i, poly in enumerate(geometry):
if isinstance(overlaps, int) and overlaps > 0: dil_poly = dilated_geometry[i] if dilated_geometry else None
total_cost += overlaps * self.congestion_penalty # Hard Collision (Static obstacles)
if not skip_static:
if collision_engine.check_collision(
poly, net_id, buffer_mode='static', start_port=start_port, end_port=end_port,
dilated_geometry=dil_poly
):
return 1e15
# Soft Collision (Negotiated Congestion)
if not skip_congestion:
overlaps = 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
# 3. Proximity cost from Danger Map # 3. Proximity cost from Danger Map
total_cost += self.g_proximity(end_port.x, end_port.y) total_cost += danger_map.get_cost(end_port.x, end_port.y)
return total_cost return total_cost

View file

@ -3,7 +3,7 @@ from __future__ import annotations
import logging import logging
import time import time
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Callable
if TYPE_CHECKING: if TYPE_CHECKING:
from inire.geometry.components import ComponentResult from inire.geometry.components import ComponentResult
@ -85,6 +85,7 @@ class PathFinder:
netlist: dict[str, tuple[Port, Port]], netlist: dict[str, tuple[Port, Port]],
net_widths: dict[str, float], net_widths: dict[str, float],
store_expanded: bool = False, store_expanded: bool = False,
iteration_callback: Callable[[int, dict[str, RoutingResult]], None] | None = None,
) -> dict[str, RoutingResult]: ) -> dict[str, RoutingResult]:
""" """
Route all nets in the netlist using Negotiated Congestion. Route all nets in the netlist using Negotiated Congestion.
@ -93,6 +94,7 @@ class PathFinder:
netlist: Mapping of net_id to (start_port, target_port). netlist: Mapping of net_id to (start_port, target_port).
net_widths: Mapping of net_id to waveguide width. net_widths: Mapping of net_id to waveguide width.
store_expanded: Whether to store expanded nodes for the last iteration. store_expanded: Whether to store expanded nodes for the last iteration.
iteration_callback: Optional callback(iteration_idx, current_results).
Returns: Returns:
Mapping of net_id to RoutingResult. Mapping of net_id to RoutingResult.
@ -174,6 +176,9 @@ class PathFinder:
results[net_id] = RoutingResult(net_id, [], False, 0, reached_target=False) results[net_id] = RoutingResult(net_id, [], False, 0, reached_target=False)
any_congestion = True any_congestion = True
if iteration_callback:
iteration_callback(iteration, results)
if not any_congestion: if not any_congestion:
break break

View file

@ -58,8 +58,13 @@ def plot_routing_results(
geoms = [poly] geoms = [poly]
for g in geoms: for g in geoms:
x, y = g.exterior.xy if hasattr(g, "exterior"):
ax.fill(x, y, alpha=0.15, fc=color, ec=color, linestyle='--', lw=0.5, zorder=2) x, y = g.exterior.xy
ax.fill(x, y, alpha=0.15, fc=color, ec=color, linestyle='--', lw=0.5, zorder=2)
else:
# Fallback for LineString or other geometries
x, y = g.xy
ax.plot(x, y, color=color, alpha=0.15, linestyle='--', lw=0.5, zorder=2)
# 2. Plot "Actual" Geometry (The high-fidelity shape used for fabrication) # 2. Plot "Actual" Geometry (The high-fidelity shape used for fabrication)
# Use comp.actual_geometry if it exists (should be the arc) # Use comp.actual_geometry if it exists (should be the arc)
@ -71,8 +76,12 @@ def plot_routing_results(
else: else:
geoms = [poly] geoms = [poly]
for g in geoms: for g in geoms:
x, y = g.exterior.xy if hasattr(g, "exterior"):
ax.plot(x, y, color=color, lw=1.5, alpha=0.9, zorder=3, label=net_id if not label_added else "") x, y = g.exterior.xy
ax.plot(x, y, color=color, lw=1.5, alpha=0.9, zorder=3, label=net_id if not label_added else "")
else:
x, y = g.xy
ax.plot(x, y, color=color, lw=1.5, alpha=0.9, zorder=3, label=net_id if not label_added else "")
label_added = True label_added = True
# 3. Plot subtle port orientation arrow # 3. Plot subtle port orientation arrow