performance bottleneck
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 303 KiB |
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 52 KiB |
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||