Compare commits

..

No commits in common. "c9bb8d64697e0f76a961be36a7aa32d6008d1dca" and "4714bed9a8df38a4ffc01211b79b8207c7965747" have entirely different histories.

17 changed files with 255 additions and 367 deletions

53
DOCS.md
View file

@ -6,23 +6,23 @@ This document describes the user-tunable parameters for the `inire` auto-router.
The `AStarRouter` is the core pathfinding engine. It can be configured directly through its constructor. The `AStarRouter` is the core pathfinding engine. It can be configured directly through its constructor.
| Parameter | Type | Default | Description | | Parameter | Type | Default | Description |
| :-------------------- | :------------ | :----------------- | :------------------------------------------------------------------------------------ | | :--- | :--- | :--- | :--- |
| `node_limit` | `int` | 1,000,000 | Maximum number of states to explore per net. Increase for very complex paths. | | `node_limit` | `int` | 1,000,000 | Maximum number of states to explore per net. Increase for very complex paths. |
| `straight_lengths` | `list[float]` | `[1.0, 5.0, 25.0]` | Discrete step sizes for straight waveguides (µm). Larger steps speed up search. | | `straight_lengths` | `list[float]` | `[1.0, 5.0, 25.0]` | Discrete step sizes for straight waveguides (µm). Larger steps speed up search in open space. |
| `bend_radii` | `list[float]` | `[10.0]` | Available radii for 90-degree turns (µm). Multiple values allow best-fit selection. | | `bend_radii` | `list[float]` | `[10.0]` | Available radii for 90-degree turns (µm). Multiple values allow the router to pick the best fit. |
| `sbend_offsets` | `list[float]` | `[-5, -2, 2, 5]` | Lateral offsets for parametric S-bends (µm). | | `sbend_offsets` | `list[float]` | `[-5, -2, 2, 5]` | Lateral offsets for parametric S-bends (µm). |
| `sbend_radii` | `list[float]` | `[10.0]` | Available radii for S-bends (µm). | | `sbend_radii` | `list[float]` | `[10.0]` | Available radii for S-bends (µm). |
| `snap_to_target_dist` | `float` | 20.0 | Distance (µm) at which the router attempts an exact bridge to the target port. | | `snap_to_target_dist`| `float` | 20.0 | Distance (µm) at which the router attempts an exact bridge to the target port. |
| `bend_penalty` | `float` | 50.0 | Flat cost added for every 90-degree bend. Higher values favor straight lines. | | `bend_penalty` | `float` | 50.0 | Flat cost added for every 90-degree bend. Higher values favor straight lines. |
| `sbend_penalty` | `float` | 100.0 | Flat cost added for every S-bend. Usually higher than `bend_penalty`. | | `sbend_penalty` | `float` | 100.0 | Flat cost added for every S-bend. Usually higher than `bend_penalty`. |
| `bend_collision_type` | `str` | `"arc"` | Collision model for bends: `"arc"`, `"bbox"`, or `"clipped_bbox"`. | | `bend_collision_type`| `str` | `"arc"` | Collision model for bends: `"arc"`, `"bbox"`, or `"clipped_bbox"`. |
| `bend_clip_margin` | `float` | 10.0 | Extra space (µm) around the waveguide before the bounding box corners are clipped. | | `bend_clip_margin` | `float` | 10.0 | Margin (µm) for the `"clipped_bbox"` collision model. |
### Bend Collision Models ### Bend Collision Models
* `"arc"`: High-fidelity model following the exact curved waveguide geometry. * `"arc"`: High-fidelity model following the exact curved waveguide geometry.
* `"bbox"`: Conservative model using the axis-aligned bounding box of the bend. Fast but blocks more space. * `"bbox"`: Conservative model using the axis-aligned bounding box of the bend. Fast but blocks more space.
* `"clipped_bbox"`: A middle ground that starts with the bounding box but applies 45-degree linear cuts to the inner and outer corners. The `bend_clip_margin` defines the extra safety distance from the waveguide edge to the cut line. * `"clipped_bbox"`: A middle ground that uses the bounding box but clips corners that are far from the waveguide.
--- ---
@ -30,11 +30,11 @@ The `AStarRouter` is the core pathfinding engine. It can be configured directly
The `CostEvaluator` defines the "goodness" of a path. The `CostEvaluator` defines the "goodness" of a path.
| Parameter | Type | Default | Description | | Parameter | Type | Default | Description |
| :------------------- | :------ | :--------- | :--------------------------------------------------------------------------------------- | | :--- | :--- | :--- | :--- |
| `unit_length_cost` | `float` | 1.0 | Cost per µm of wire length. | | `unit_length_cost` | `float` | 1.0 | Cost per µm of wire length. |
| `greedy_h_weight` | `float` | 1.1 | Heuristic weight. `1.0` is optimal; higher values (e.g. `1.5`) speed up search. | | `greedy_h_weight` | `float` | 1.1 | Heuristic weight. `1.0` is optimal; higher values (e.g., `1.5`) are faster but may produce longer paths. |
| `congestion_penalty` | `float` | 10,000.0 | Multiplier for overlaps in the multi-net Negotiated Congestion loop. | | `congestion_penalty`| `float` | 10,000.0 | Multiplier for overlaps in the multi-net Negotiated Congestion loop. |
--- ---
@ -42,19 +42,19 @@ The `CostEvaluator` defines the "goodness" of a path.
The `PathFinder` orchestrates multi-net routing using the Negotiated Congestion algorithm. The `PathFinder` orchestrates multi-net routing using the Negotiated Congestion algorithm.
| Parameter | Type | Default | Description | | Parameter | Type | Default | Description |
| :------------------------ | :------ | :------ | :-------------------------------------------------------------------------------------- | | :--- | :--- | :--- | :--- |
| `max_iterations` | `int` | 10 | Maximum number of rip-up and reroute iterations to resolve congestion. | | `max_iterations` | `int` | 10 | Maximum number of rip-up and reroute iterations to resolve congestion. |
| `base_congestion_penalty` | `float` | 100.0 | Starting penalty for overlaps. Multiplied by `1.5` each iteration if congestion remains.| | `base_congestion_penalty` | `float` | 100.0 | Starting penalty for overlaps. This value is multiplied by `1.5` each iteration if congestion persists. |
--- ---
## 4. CollisionEngine Parameters ## 4. CollisionEngine Parameters
| Parameter | Type | Default | Description | | Parameter | Type | Default | Description |
| :------------------- | :------ | :--------- | :------------------------------------------------------------------------------------ | | :--- | :--- | :--- | :--- |
| `clearance` | `float` | (Required) | Minimum required distance between any two waveguides or obstacles (µm). | | `clearance` | `float` | (Required) | Minimum required distance between any two waveguides or obstacles (µm). |
| `safety_zone_radius` | `float` | 0.0021 | Radius (µm) around ports where collisions are ignored for PDK boundary incidence. | | `safety_zone_radius`| `float` | 0.0021 | Radius (µm) around ports where collisions are ignored to allow PDK boundary incidence. |
--- ---
@ -62,7 +62,6 @@ The `PathFinder` orchestrates multi-net routing using the Negotiated Congestion
- **Coordinates**: Micrometers (µm). - **Coordinates**: Micrometers (µm).
- **Grid Snapping**: The router internally operates on a **1nm** grid for final ports and a **1µm** lattice for expansion moves. - **Grid Snapping**: The router internally operates on a **1nm** grid for final ports and a **1µm** lattice for expansion moves.
- **Search Space**: Assumptions are optimized for design areas up to **20mm x 20mm**. - **Search Space**: Assumptions are optimized for design areas up to **20mm x 20mm**.
- **Design Bounds**: The boundary limits defined in `DangerMap` strictly constrain the **physical edges** (dilated geometry) of the waveguide. Any move that would cause the waveguide or its required clearance to extend beyond these bounds is rejected with an infinite cost.
--- ---

View file

@ -74,17 +74,12 @@ Check the `examples/` directory for ready-to-run scripts demonstrating core feat
* **`examples/03_locked_paths.py`**: Incremental workflow using `lock_net()` to route around previously fixed paths. Generates `03_locked_paths.png`. * **`examples/03_locked_paths.py`**: Incremental workflow using `lock_net()` to route around previously fixed paths. Generates `03_locked_paths.png`.
* **`examples/04_sbends_and_radii.py`**: Complex paths using parametric S-bends and multiple bend radii. Generates `04_sbends_and_radii.png`. * **`examples/04_sbends_and_radii.py`**: Complex paths using parametric S-bends and multiple bend radii. Generates `04_sbends_and_radii.png`.
* **`examples/05_orientation_stress.py`**: Stress test for various port orientation combinations (U-turns, opposite directions). Generates `05_orientation_stress.png`. * **`examples/05_orientation_stress.py`**: Stress test for various port orientation combinations (U-turns, opposite directions). Generates `05_orientation_stress.png`.
* **`examples/06_bend_collision_models.py`**: Comparison of different collision models for bends (Arc vs. BBox vs. Clipped BBox). Generates `06_bend_collision_models.png`.
Run an example: Run an example:
```bash ```bash
python3 examples/01_simple_route.py python3 examples/01_simple_route.py
``` ```
## Documentation
Full documentation for all user-tunable parameters, cost functions, and collision models can be found in **[DOCS.md](DOCS.md)**.
## Architecture ## Architecture
`inire` operates on a **State-Lattice** defined by $(x, y, \theta)$. From any state, the router expands via three primary "Move" types: `inire` operates on a **State-Lattice** defined by $(x, y, \theta)$. From any state, the router expands via three primary "Move" types:

View file

@ -28,7 +28,7 @@ def main() -> None:
"vertical_up": (Port(45, 10, 90), Port(45, 90, 90)), "vertical_up": (Port(45, 10, 90), Port(45, 90, 90)),
"vertical_down": (Port(55, 90, 270), Port(55, 10, 270)), "vertical_down": (Port(55, 90, 270), Port(55, 10, 270)),
} }
net_widths = dict.fromkeys(netlist, 2.0) net_widths = {nid: 2.0 for nid in netlist}
# 3. Route with Negotiated Congestion # 3. Route with Negotiated Congestion
# We increase the base penalty to encourage faster divergence # We increase the base penalty to encourage faster divergence

View file

@ -28,7 +28,7 @@ def main() -> None:
"bus_2": (Port(10, 60, 0), Port(110, 65, 0)), "bus_2": (Port(10, 60, 0), Port(110, 65, 0)),
} }
print("Phase 1: Routing bus (3 nets)...") print("Phase 1: Routing bus (3 nets)...")
results_p1 = pf.route_all(netlist_p1, dict.fromkeys(netlist_p1, 2.0)) results_p1 = pf.route_all(netlist_p1, {nid: 2.0 for nid in netlist_p1})
# Lock all Phase 1 nets # Lock all Phase 1 nets
path_polys = [] path_polys = []
@ -53,7 +53,7 @@ def main() -> None:
print("Phase 2: Routing crossing nets around locked bus...") print("Phase 2: Routing crossing nets around locked bus...")
# We use a slightly different width for variety # We use a slightly different width for variety
results_p2 = pf.route_all(netlist_p2, dict.fromkeys(netlist_p2, 1.5)) results_p2 = pf.route_all(netlist_p2, {nid: 1.5 for nid in netlist_p2})
# 4. Check Results # 4. Check Results
for nid, res in results_p2.items(): for nid, res in results_p2.items():

View file

@ -1,3 +1,4 @@
from shapely.geometry import Polygon
from inire.geometry.collision import CollisionEngine from inire.geometry.collision import CollisionEngine
from inire.geometry.primitives import Port from inire.geometry.primitives import Port

View file

@ -35,7 +35,7 @@ def main() -> None:
# Sharp return: output is behind and oriented towards the input # Sharp return: output is behind and oriented towards the input
"return_loop": (Port(80, 20, 0), Port(40, 10, 180)), "return_loop": (Port(80, 20, 0), Port(40, 10, 180)),
} }
net_widths = dict.fromkeys(netlist, 2.0) net_widths = {nid: 2.0 for nid in netlist}
# 3. Route # 3. Route
results = pf.route_all(netlist, net_widths) results = pf.route_all(netlist, net_widths)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

View file

@ -1,70 +0,0 @@
from shapely.geometry import Polygon
from inire.geometry.collision import CollisionEngine
from inire.geometry.primitives import Port
from inire.router.astar import AStarRouter
from inire.router.cost import CostEvaluator
from inire.router.danger_map import DangerMap
from inire.router.pathfinder import PathFinder
from inire.utils.visualization import plot_routing_results
def main() -> None:
print("Running Example 06: Bend Collision Models...")
# 1. Setup Environment
# Give room for 10um bends near the edges
bounds = (-20, -20, 170, 170)
engine = CollisionEngine(clearance=2.0)
danger_map = DangerMap(bounds=bounds)
# Create three scenarios with identical obstacles
# We'll space them out vertically
obs_arc = Polygon([(40, 110), (60, 110), (60, 130), (40, 130)])
obs_bbox = Polygon([(40, 60), (60, 60), (60, 80), (40, 80)])
obs_clipped = Polygon([(40, 10), (60, 10), (60, 30), (40, 30)])
obstacles = [obs_arc, obs_bbox, obs_clipped]
for obs in obstacles:
engine.add_static_obstacle(obs)
danger_map.precompute(obstacles)
# We'll run three separate routers since collision_type is a router-level config
evaluator = CostEvaluator(engine, danger_map)
# Scenario 1: Standard 'arc' model (High fidelity)
router_arc = AStarRouter(evaluator, bend_collision_type="arc")
netlist_arc = {"arc_model": (Port(10, 120, 0), Port(90, 140, 90))}
# Scenario 2: 'bbox' model (Conservative axis-aligned box)
router_bbox = AStarRouter(evaluator, bend_collision_type="bbox")
netlist_bbox = {"bbox_model": (Port(10, 70, 0), Port(90, 90, 90))}
# Scenario 3: 'clipped_bbox' model (Balanced)
router_clipped = AStarRouter(evaluator, bend_collision_type="clipped_bbox", bend_clip_margin=1.0)
netlist_clipped = {"clipped_model": (Port(10, 20, 0), Port(90, 40, 90))}
# 2. Route each scenario
print("Routing Scenario 1 (Arc)...")
res_arc = PathFinder(router_arc, evaluator).route_all(netlist_arc, {"arc_model": 2.0})
print("Routing Scenario 2 (BBox)...")
res_bbox = PathFinder(router_bbox, evaluator).route_all(netlist_bbox, {"bbox_model": 2.0})
print("Routing Scenario 3 (Clipped BBox)...")
res_clipped = PathFinder(router_clipped, evaluator).route_all(netlist_clipped, {"clipped_model": 2.0})
# 3. Combine results for visualization
all_results = {**res_arc, **res_bbox, **res_clipped}
all_netlists = {**netlist_arc, **netlist_bbox, **netlist_clipped}
# 4. Visualize
# Note: plot_routing_results will show the 'collision geometry' used by the router
# since that's what's stored in res.path[i].geometry
fig, ax = plot_routing_results(all_results, obstacles, bounds, netlist=all_netlists)
fig.savefig("examples/06_bend_collision_models.png")
print("Saved plot to examples/06_bend_collision_models.png")
if __name__ == "__main__":
main()

View file

@ -1,141 +1,149 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Literal from typing import TYPE_CHECKING
import rtree import rtree
from shapely.geometry import Point, Polygon from shapely.geometry import Point, Polygon
from shapely.ops import unary_union
from shapely.prepared import prep from shapely.prepared import prep
if TYPE_CHECKING: if TYPE_CHECKING:
from shapely.prepared import PreparedGeometry from shapely.prepared import PreparedGeometry
from inire.geometry.primitives import Port from inire.geometry.primitives import Port
class CollisionEngine: class CollisionEngine:
"""Manages spatial queries for collision detection with unified dilation logic.""" """Manages spatial queries for collision detection."""
def __init__(self, clearance: float, max_net_width: float = 2.0, safety_zone_radius: float = 0.0021) -> None: def __init__(self, clearance: float, max_net_width: float = 2.0, safety_zone_radius: float = 0.0021) -> None:
self.clearance = clearance self.clearance = clearance
self.max_net_width = max_net_width self.max_net_width = max_net_width
self.safety_zone_radius = safety_zone_radius self.safety_zone_radius = safety_zone_radius
self.static_obstacles = rtree.index.Index()
# Static obstacles: store raw geometries to avoid double-dilation # To store geometries for precise checks
self.static_index = rtree.index.Index() self.obstacle_geometries: dict[int, Polygon] = {} # ID -> Polygon
self.static_geometries: dict[int, Polygon] = {} # ID -> Polygon self.prepared_obstacles: dict[int, PreparedGeometry] = {} # ID -> PreparedGeometry
self.static_prepared: dict[int, PreparedGeometry] = {} # ID -> PreparedGeometry self._id_counter = 0
self._static_id_counter = 0
# Dynamic paths for multi-net congestion # Dynamic paths for multi-net congestion
self.dynamic_index = rtree.index.Index() self.dynamic_paths = rtree.index.Index()
# obj_id -> (net_id, raw_geometry) # obj_id -> (net_id, geometry)
self.dynamic_geometries: dict[int, tuple[str, Polygon]] = {} self.path_geometries: dict[int, tuple[str, Polygon]] = {}
self._dynamic_id_counter = 0 self._dynamic_id_counter = 0
def add_static_obstacle(self, polygon: Polygon) -> None: def add_static_obstacle(self, polygon: Polygon, pre_dilate: bool = True) -> None:
"""Add a static obstacle (raw geometry) to the engine.""" """Add a static obstacle to the engine."""
obj_id = self._static_id_counter _ = pre_dilate # Keep for API compatibility
self._static_id_counter += 1 obj_id = self._id_counter
self._id_counter += 1
self.static_geometries[obj_id] = polygon self.obstacle_geometries[obj_id] = polygon
self.static_prepared[obj_id] = prep(polygon) self.prepared_obstacles[obj_id] = prep(polygon)
self.static_index.insert(obj_id, polygon.bounds)
# Index the bounding box of the original polygon
# We query with dilated moves, so original bounds are enough
self.static_obstacles.insert(obj_id, polygon.bounds)
def add_path(self, net_id: str, geometry: list[Polygon]) -> None: def add_path(self, net_id: str, geometry: list[Polygon]) -> None:
"""Add a net's routed path (raw geometry) to the dynamic index.""" """Add a net's routed path to the dynamic R-Tree."""
# Dilate by clearance/2 for congestion
dilation = self.clearance / 2.0
for poly in geometry: for poly in geometry:
dilated = poly.buffer(dilation)
obj_id = self._dynamic_id_counter obj_id = self._dynamic_id_counter
self._dynamic_id_counter += 1 self._dynamic_id_counter += 1
self.dynamic_geometries[obj_id] = (net_id, poly) self.path_geometries[obj_id] = (net_id, dilated)
self.dynamic_index.insert(obj_id, poly.bounds) self.dynamic_paths.insert(obj_id, dilated.bounds)
def remove_path(self, net_id: str) -> None: def remove_path(self, net_id: str) -> None:
"""Remove a net's path from the dynamic index.""" """Remove a net's path from the dynamic R-Tree."""
to_remove = [obj_id for obj_id, (nid, _) in self.dynamic_geometries.items() if nid == net_id] to_remove = [obj_id for obj_id, (nid, _) in self.path_geometries.items() if nid == net_id]
for obj_id in to_remove: for obj_id in to_remove:
nid, poly = self.dynamic_geometries.pop(obj_id) nid, dilated = self.path_geometries.pop(obj_id)
self.dynamic_index.delete(obj_id, poly.bounds) self.dynamic_paths.delete(obj_id, dilated.bounds)
def lock_net(self, net_id: str) -> None: def lock_net(self, net_id: str) -> None:
"""Move a net's dynamic path to static obstacles permanently.""" """Move a net's dynamic path to static obstacles permanently."""
to_move = [obj_id for obj_id, (nid, _) in self.dynamic_geometries.items() if nid == net_id] to_move = [obj_id for obj_id, (nid, _) in self.path_geometries.items() if nid == net_id]
for obj_id in to_move: for obj_id in to_move:
nid, poly = self.dynamic_geometries.pop(obj_id) nid, dilated = self.path_geometries.pop(obj_id)
self.dynamic_index.delete(obj_id, poly.bounds) self.dynamic_paths.delete(obj_id, dilated.bounds)
self.add_static_obstacle(poly)
# Add to static (already dilated for clearance)
new_static_id = self._id_counter
self._id_counter += 1
self.obstacle_geometries[new_static_id] = dilated
self.prepared_obstacles[new_static_id] = prep(dilated)
self.static_obstacles.insert(new_static_id, dilated.bounds)
def count_congestion(self, geometry: Polygon, net_id: str) -> int:
"""Count how many other nets collide with this geometry."""
dilation = self.clearance / 2.0
test_poly = geometry.buffer(dilation)
return self.count_congestion_prebuffered(test_poly, net_id)
def count_congestion_prebuffered(self, dilated_geometry: Polygon, net_id: str) -> int:
"""Count how many other nets collide with this pre-dilated geometry."""
candidates = self.dynamic_paths.intersection(dilated_geometry.bounds)
count = 0
for obj_id in candidates:
other_net_id, other_poly = self.path_geometries[obj_id]
if other_net_id != net_id and dilated_geometry.intersects(other_poly):
count += 1
return count
def is_collision( def is_collision(
self, self,
geometry: Polygon, geometry: Polygon,
net_width: float = 2.0, net_width: float,
start_port: Port | None = None, start_port: Port | None = None,
end_port: Port | None = None end_port: Port | None = None,
) -> bool: ) -> bool:
"""Alias for check_collision(buffer_mode='static') for backward compatibility.""" """Check if a geometry (e.g. a Move) collides with static obstacles."""
_ = net_width _ = net_width # Width is already integrated into engine dilation settings
res = self.check_collision(geometry, "default", buffer_mode="static", start_port=start_port, end_port=end_port) dilation = self.clearance / 2.0
return bool(res) test_poly = geometry.buffer(dilation)
return self.is_collision_prebuffered(test_poly, start_port=start_port, end_port=end_port)
def count_congestion(self, geometry: Polygon, net_id: str) -> int: def is_collision_prebuffered(
"""Alias for check_collision(buffer_mode='congestion') for backward compatibility."""
res = self.check_collision(geometry, net_id, buffer_mode="congestion")
return int(res)
def check_collision(
self, self,
geometry: Polygon, dilated_geometry: Polygon,
net_id: str,
buffer_mode: Literal["static", "congestion"] = "static",
start_port: Port | None = None, start_port: Port | None = None,
end_port: Port | None = None end_port: Port | None = None,
) -> bool | int: ) -> bool:
""" """Check if a pre-dilated geometry collides with static obstacles."""
Check for collisions using unified dilation logic. # Query R-Tree using the bounds of the dilated move
candidates = self.static_obstacles.intersection(dilated_geometry.bounds)
If buffer_mode == "static": for obj_id in candidates:
Returns True if geometry collides with static obstacles (buffered by full clearance). # Use prepared geometry for fast intersection
If buffer_mode == "congestion": if self.prepared_obstacles[obj_id].intersects(dilated_geometry):
Returns count of other nets colliding with geometry (both buffered by clearance/2). # Check safety zone (2nm radius)
""" if start_port or end_port:
if buffer_mode == "static": obstacle = self.obstacle_geometries[obj_id]
# Buffered move vs raw static obstacle intersection = dilated_geometry.intersection(obstacle)
# Distance must be >= clearance
test_poly = geometry.buffer(self.clearance)
candidates = self.static_index.intersection(test_poly.bounds)
for obj_id in candidates: if intersection.is_empty:
if self.static_prepared[obj_id].intersects(test_poly): continue
# Safety zone check (using exact intersection area/bounds)
if start_port or end_port:
intersection = test_poly.intersection(self.static_geometries[obj_id])
if intersection.is_empty:
continue
ix_minx, ix_miny, ix_maxx, ix_maxy = intersection.bounds # Precise check: is every point in the intersection close to either port?
ix_minx, ix_miny, ix_maxx, ix_maxy = intersection.bounds
is_safe = False is_near_start = False
for p in [start_port, end_port]: if start_port:
if p and (abs(ix_minx - p.x) < self.safety_zone_radius and if (abs(ix_minx - start_port.x) < self.safety_zone_radius and abs(ix_maxx - start_port.x) < self.safety_zone_radius and
abs(ix_maxx - p.x) < self.safety_zone_radius and abs(ix_miny - start_port.y) < self.safety_zone_radius and abs(ix_maxy - start_port.y) < self.safety_zone_radius):
abs(ix_miny - p.y) < self.safety_zone_radius and is_near_start = True
abs(ix_maxy - p.y) < self.safety_zone_radius):
is_safe = True
break
if is_safe:
continue
return True
return False
else: # buffer_mode == "congestion" is_near_end = False
# Both paths buffered by clearance/2 => Total separation = clearance if end_port:
dilation = self.clearance / 2.0 if (abs(ix_minx - end_port.x) < self.safety_zone_radius and abs(ix_maxx - end_port.x) < self.safety_zone_radius and
test_poly = geometry.buffer(dilation) abs(ix_miny - end_port.y) < self.safety_zone_radius and abs(ix_maxy - end_port.y) < self.safety_zone_radius):
candidates = self.dynamic_index.intersection(test_poly.bounds) is_near_end = True
count = 0 if is_near_start or is_near_end:
for obj_id in candidates: continue
other_net_id, other_poly = self.dynamic_geometries[obj_id]
if other_net_id != net_id: return True
# Buffer the other path segment too return False
if test_poly.intersects(other_poly.buffer(dilation)):
count += 1
return count

View file

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from typing import NamedTuple, Literal, Any from typing import NamedTuple, Literal, Union
import numpy as np import numpy as np
from shapely.geometry import Polygon, box from shapely.geometry import Polygon, box
@ -87,9 +87,6 @@ def _apply_collision_model(
arc_poly: Polygon, arc_poly: Polygon,
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon, collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon,
radius: float, radius: float,
width: float,
cx: float = 0.0,
cy: float = 0.0,
clip_margin: float = 10.0 clip_margin: float = 10.0
) -> list[Polygon]: ) -> list[Polygon]:
"""Applies the specified collision model to an arc geometry.""" """Applies the specified collision model to an arc geometry."""
@ -107,49 +104,8 @@ def _apply_collision_model(
return [bbox] return [bbox]
if collision_type == "clipped_bbox": if collision_type == "clipped_bbox":
res_poly = bbox safe_zone = arc_poly.buffer(clip_margin)
return [bbox.intersection(safe_zone)]
# Determine quadrant signs from arc centroid relative to center
# This ensures we always cut 'into' the box correctly
ac = arc_poly.centroid
sx = 1.0 if ac.x >= cx else -1.0
sy = 1.0 if ac.y >= cy else -1.0
r_out_cut = radius + width / 2.0 + clip_margin
r_in_cut = radius - width / 2.0 - clip_margin
corners = [(minx, miny), (minx, maxy), (maxx, miny), (maxx, maxy)]
for px, py in corners:
dx, dy = px - cx, py - cy
dist = np.sqrt(dx**2 + dy**2)
if dist > r_out_cut:
# Outer corner: remove part furthest from center
# We want minimum distance to line to be r_out_cut
d_cut = r_out_cut * np.sqrt(2)
elif r_in_cut > 0 and dist < r_in_cut:
# Inner corner: remove part closest to center
# We want maximum distance to line to be r_in_cut
d_cut = r_in_cut
else:
continue
# The cut line is sx*(x-cx) + sy*(y-cy) = d_cut
# sx*x + sy*y = sx*cx + sy*cy + d_cut
val = cx * sx + cy * sy + d_cut
try:
p1 = (px, py)
p2 = (px, (val - sx * px) / sy)
p3 = ((val - sy * py) / sx, py)
triangle = Polygon([p1, p2, p3])
if triangle.is_valid and triangle.area > 1e-9:
res_poly = res_poly.difference(triangle)
except ZeroDivisionError:
continue
return [res_poly]
return [arc_poly] return [arc_poly]
@ -179,9 +135,7 @@ class Bend90:
end_port = Port(ex, ey, float((start_port.orientation + turn_angle) % 360)) end_port = Port(ex, ey, float((start_port.orientation + turn_angle) % 360))
arc_polys = _get_arc_polygons(cx, cy, radius, width, t_start, t_end, sagitta) arc_polys = _get_arc_polygons(cx, cy, radius, width, t_start, t_end, sagitta)
collision_polys = _apply_collision_model( collision_polys = _apply_collision_model(arc_polys[0], collision_type, radius, clip_margin)
arc_polys[0], collision_type, radius, width, cx, cy, clip_margin
)
return ComponentResult(geometry=collision_polys, end_port=end_port, length=radius * np.pi / 2.0) return ComponentResult(geometry=collision_polys, end_port=end_port, length=radius * np.pi / 2.0)
@ -227,13 +181,5 @@ class SBend:
arc2 = _get_arc_polygons(cx2, cy2, radius, width, ts2, te2, sagitta)[0] arc2 = _get_arc_polygons(cx2, cy2, radius, width, ts2, te2, sagitta)[0]
combined_arc = unary_union([arc1, arc2]) combined_arc = unary_union([arc1, arc2])
if collision_type == "clipped_bbox": collision_polys = _apply_collision_model(combined_arc, collision_type, radius, clip_margin)
col1 = _apply_collision_model(arc1, collision_type, radius, width, cx1, cy1, clip_margin)
col2 = _apply_collision_model(arc2, collision_type, radius, width, cx2, cy2, clip_margin)
collision_polys = [unary_union(col1 + col2)]
else:
collision_polys = _apply_collision_model(
combined_arc, collision_type, radius, width, 0, 0, clip_margin
)
return ComponentResult(geometry=collision_polys, end_port=end_port, length=2 * radius * theta) return ComponentResult(geometry=collision_polys, end_port=end_port, length=2 * radius * theta)

View file

@ -189,7 +189,7 @@ class AStarRouter:
lengths = self.config.straight_lengths lengths = self.config.straight_lengths
if dist < 5.0: if dist < 5.0:
fine_steps = [0.1, 0.5] fine_steps = [0.1, 0.5]
lengths = sorted(set(lengths + fine_steps)) lengths = sorted(list(set(lengths + fine_steps)))
for length in lengths: for length in lengths:
res = Straight.generate(current.port, length, net_width) res = Straight.generate(current.port, length, net_width)
@ -255,9 +255,7 @@ class AStarRouter:
else: else:
hard_coll = False hard_coll = False
for poly in result.geometry: for poly in result.geometry:
if self.cost_evaluator.collision_engine.check_collision( if self.cost_evaluator.collision_engine.is_collision(poly, net_width, start_port=parent.port, end_port=result.end_port):
poly, net_id, buffer_mode="static", start_port=parent.port, end_port=result.end_port
):
hard_coll = True hard_coll = True
break break
self._collision_cache[cache_key] = hard_coll self._collision_cache[cache_key] = hard_coll
@ -302,6 +300,8 @@ class AStarRouter:
# Turn penalties scaled by radius to favor larger turns # Turn penalties scaled by radius to favor larger turns
ref_radius = 10.0 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 penalty: larger radius -> smaller penalty
# e.g. radius 10 -> factor 1.0, radius 30 -> factor 0.33
penalty_factor = ref_radius / move_radius penalty_factor = ref_radius / move_radius
move_cost += self.config.bend_penalty * penalty_factor move_cost += self.config.bend_penalty * penalty_factor
elif "SB" in move_type and move_radius is not None: elif "SB" in move_type and move_radius is not None:

View file

@ -1,8 +1,10 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Literal, Any from typing import Literal, TYPE_CHECKING, Any
if TYPE_CHECKING:
from shapely.geometry import Polygon
@dataclass @dataclass

View file

@ -74,23 +74,24 @@ class CostEvaluator:
_ = net_width # Unused _ = net_width # Unused
total_cost = length * self.unit_length_cost total_cost = length * self.unit_length_cost
# 1. Boundary Check (Centerline based for compatibility) # 1. Hard Collision check (Static obstacles)
if not self.danger_map.is_within_bounds(end_port.x, end_port.y): # We buffer by the full clearance to ensure distance >= clearance
return 1e15 hard_dilation = self.collision_engine.clearance
# 2. Collision Check
for poly in geometry: for poly in geometry:
# Hard Collision (Static obstacles) dilated_poly = poly.buffer(hard_dilation)
if self.collision_engine.check_collision( if self.collision_engine.is_collision_prebuffered(dilated_poly, start_port=start_port, end_port=end_port):
poly, net_id, buffer_mode="static", start_port=start_port, end_port=end_port return 1e15 # Impossible cost for hard collisions
):
return 1e15
# Soft Collision (Negotiated Congestion) # 2. Soft Collision check (Negotiated Congestion)
overlaps = self.collision_engine.check_collision(poly, net_id, buffer_mode="congestion") # We buffer by clearance/2 because both paths are buffered by clearance/2
if isinstance(overlaps, int) and overlaps > 0: soft_dilation = self.collision_engine.clearance / 2.0
for poly in geometry:
dilated_poly = poly.buffer(soft_dilation)
overlaps = self.collision_engine.count_congestion_prebuffered(dilated_poly, net_id)
if overlaps > 0:
total_cost += overlaps * self.congestion_penalty 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 += self.g_proximity(end_port.x, end_port.y)
return total_cost return total_cost

View file

@ -3,14 +3,13 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import numpy as np import numpy as np
import shapely
if TYPE_CHECKING: if TYPE_CHECKING:
from shapely.geometry import Polygon from shapely.geometry import Polygon
class DangerMap: class DangerMap:
"""A pre-computed grid for heuristic proximity costs, vectorized for performance.""" """A pre-computed grid for heuristic proximity costs."""
def __init__( def __init__(
self, self,
@ -29,36 +28,47 @@ class DangerMap:
self.width_cells = int(np.ceil((self.maxx - self.minx) / self.resolution)) self.width_cells = int(np.ceil((self.maxx - self.minx) / self.resolution))
self.height_cells = int(np.ceil((self.maxy - self.miny) / self.resolution)) self.height_cells = int(np.ceil((self.maxy - self.miny) / self.resolution))
# Use uint8 for memory efficiency if normalized, or float16/float32.
# Let's use float32 for simplicity and precision in the prototype.
# For a 1000x1000 grid, this is only 4MB.
# For 20000x20000, it's 1.6GB.
self.grid = np.zeros((self.width_cells, self.height_cells), dtype=np.float32) self.grid = np.zeros((self.width_cells, self.height_cells), dtype=np.float32)
def precompute(self, obstacles: list[Polygon]) -> None: def precompute(self, obstacles: list[Polygon]) -> None:
"""Pre-compute the proximity costs for the entire grid using vectorized operations.""" """Pre-compute the proximity costs for the entire grid."""
# For each cell, find distance to nearest obstacle.
# This is a distance transform problem.
# For the prototype, we can use a simpler approach or scipy.ndimage.distance_transform_edt.
from scipy.ndimage import distance_transform_edt from scipy.ndimage import distance_transform_edt
# 1. Create a binary mask of obstacles # Create a binary mask of obstacles
mask = np.ones((self.width_cells, self.height_cells), dtype=bool) mask = np.ones((self.width_cells, self.height_cells), dtype=bool)
# Rasterize obstacles (simplified: mark cells whose center is inside an obstacle)
# Create coordinate grids # This is slow for many obstacles; in a real engine, we'd use a faster rasterizer.
x_coords = np.linspace(self.minx + self.resolution/2, self.maxx - self.resolution/2, self.width_cells) from shapely.geometry import Point
y_coords = np.linspace(self.miny + self.resolution/2, self.maxy - self.resolution/2, self.height_cells)
xv, yv = np.meshgrid(x_coords, y_coords, indexing='ij')
for poly in obstacles: for poly in obstacles:
# Use shapely.contains_xy for fast vectorized point-in-polygon check # Get bounding box in grid coordinates
in_poly = shapely.contains_xy(poly, xv, yv) p_minx, p_miny, p_maxx, p_maxy = poly.bounds
mask[in_poly] = False x_start = max(0, int((p_minx - self.minx) / self.resolution))
x_end = min(self.width_cells, int((p_maxx - self.minx) / self.resolution) + 1)
y_start = max(0, int((p_miny - self.miny) / self.resolution))
y_end = min(self.height_cells, int((p_maxy - self.miny) / self.resolution) + 1)
# 2. Distance transform (mask=True for empty space) for ix in range(x_start, x_end):
cx = self.minx + (ix + 0.5) * self.resolution
for iy in range(y_start, y_end):
cy = self.miny + (iy + 0.5) * self.resolution
if poly.contains(Point(cx, cy)):
mask[ix, iy] = False
# Distance transform (mask=True for empty space)
distances = distance_transform_edt(mask) * self.resolution distances = distance_transform_edt(mask) * self.resolution
# 3. Proximity cost: k / d^2 if d < threshold, else 0 # Proximity cost: k / d^2 if d < threshold, else 0
# Cap distances at a small epsilon (e.g. 0.1um) to avoid division by zero # To avoid division by zero, we cap distances at a small epsilon (e.g. 0.1um)
safe_distances = np.maximum(distances, 0.1) safe_distances = np.maximum(distances, 0.1)
self.grid = np.where( self.grid = np.where(distances < self.safety_threshold, self.k / (safe_distances**2), 0.0).astype(np.float32)
distances < self.safety_threshold,
self.k / (safe_distances**2),
0.0
).astype(np.float32)
def is_within_bounds(self, x: float, y: float) -> bool: def is_within_bounds(self, x: float, y: float) -> bool:
"""Check if a coordinate is within the design bounds.""" """Check if a coordinate is within the design bounds."""

View file

@ -79,7 +79,7 @@ class PathFinder:
logger.debug(f" Net {net_id} routed in {time.monotonic() - net_start:.4f}s") logger.debug(f" Net {net_id} routed in {time.monotonic() - net_start:.4f}s")
if path: if path:
# 3. Add to index # 3. Add to R-Tree
all_geoms = [] all_geoms = []
for res in path: for res in path:
all_geoms.extend(res.geometry) all_geoms.extend(res.geometry)
@ -88,11 +88,7 @@ class PathFinder:
# Check if this new path has any congestion # Check if this new path has any congestion
collision_count = 0 collision_count = 0
for poly in all_geoms: for poly in all_geoms:
overlaps = self.cost_evaluator.collision_engine.check_collision( collision_count += self.cost_evaluator.collision_engine.count_congestion(poly, net_id)
poly, net_id, buffer_mode="congestion"
)
if isinstance(overlaps, int):
collision_count += overlaps
if collision_count > 0: if collision_count > 0:
any_congestion = True any_congestion = True
@ -124,12 +120,9 @@ class PathFinder:
collision_count = 0 collision_count = 0
for comp in res.path: for comp in res.path:
for poly in comp.geometry: for poly in comp.geometry:
overlaps = self.cost_evaluator.collision_engine.check_collision( collision_count += self.cost_evaluator.collision_engine.count_congestion(poly, net_id)
poly, net_id, buffer_mode="congestion"
)
if isinstance(overlaps, int):
collision_count += overlaps
final_results[net_id] = RoutingResult(net_id, res.path, collision_count == 0, collision_count) final_results[net_id] = RoutingResult(net_id, res.path, collision_count == 0, collision_count)
return final_results return final_results

View file

@ -1,4 +1,6 @@
import numpy as np
import pytest import pytest
from shapely.geometry import Point
from inire.geometry.components import Bend90, SBend, Straight from inire.geometry.components import Bend90, SBend, Straight
from inire.geometry.primitives import Port, rotate_port, translate_port from inire.geometry.primitives import Port, rotate_port, translate_port

View file

@ -3,7 +3,8 @@ from __future__ import annotations
import numpy as np import numpy as np
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from shapely.geometry import Polygon from shapely.geometry import Point, Polygon
from shapely.ops import unary_union
if TYPE_CHECKING: if TYPE_CHECKING:
from inire.geometry.primitives import Port from inire.geometry.primitives import Port