Compare commits
2 commits
4714bed9a8
...
c9bb8d6469
| Author | SHA1 | Date | |
|---|---|---|---|
| c9bb8d6469 | |||
| 58873692d6 |
17 changed files with 367 additions and 255 deletions
53
DOCS.md
53
DOCS.md
|
|
@ -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 in open space. |
|
| `straight_lengths` | `list[float]` | `[1.0, 5.0, 25.0]` | Discrete step sizes for straight waveguides (µm). Larger steps speed up search. |
|
||||||
| `bend_radii` | `list[float]` | `[10.0]` | Available radii for 90-degree turns (µm). Multiple values allow the router to pick the best fit. |
|
| `bend_radii` | `list[float]` | `[10.0]` | Available radii for 90-degree turns (µm). Multiple values allow best-fit selection. |
|
||||||
| `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 | Margin (µm) for the `"clipped_bbox"` collision model. |
|
| `bend_clip_margin` | `float` | 10.0 | Extra space (µm) around the waveguide before the bounding box corners are clipped. |
|
||||||
|
|
||||||
### 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 uses the bounding box but clips corners that are far from the waveguide.
|
* `"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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -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`) are faster but may produce longer paths. |
|
| `greedy_h_weight` | `float` | 1.1 | Heuristic weight. `1.0` is optimal; higher values (e.g. `1.5`) speed up search. |
|
||||||
| `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. This value is multiplied by `1.5` each iteration if congestion persists. |
|
| `base_congestion_penalty` | `float` | 100.0 | Starting penalty for overlaps. Multiplied by `1.5` each iteration if congestion remains.|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 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 to allow PDK boundary incidence. |
|
| `safety_zone_radius` | `float` | 0.0021 | Radius (µm) around ports where collisions are ignored for PDK boundary incidence. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -62,6 +62,7 @@ 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,12 +74,17 @@ 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:
|
||||||
|
|
|
||||||
|
|
@ -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 = {nid: 2.0 for nid in netlist}
|
net_widths = dict.fromkeys(netlist, 2.0)
|
||||||
|
|
||||||
# 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
|
||||||
|
|
|
||||||
|
|
@ -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, {nid: 2.0 for nid in netlist_p1})
|
results_p1 = pf.route_all(netlist_p1, dict.fromkeys(netlist_p1, 2.0))
|
||||||
|
|
||||||
# Lock all Phase 1 nets
|
# Lock all Phase 1 nets
|
||||||
path_polys = []
|
path_polys = []
|
||||||
|
|
@ -50,10 +50,10 @@ def main() -> None:
|
||||||
"cross_left": (Port(30, 10, 90), Port(30, 110, 90)),
|
"cross_left": (Port(30, 10, 90), Port(30, 110, 90)),
|
||||||
"cross_right": (Port(80, 110, 270), Port(80, 10, 270)), # Top to bottom
|
"cross_right": (Port(80, 110, 270), Port(80, 10, 270)), # Top to bottom
|
||||||
}
|
}
|
||||||
|
|
||||||
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, {nid: 1.5 for nid in netlist_p2})
|
results_p2 = pf.route_all(netlist_p2, dict.fromkeys(netlist_p2, 1.5))
|
||||||
|
|
||||||
# 4. Check Results
|
# 4. Check Results
|
||||||
for nid, res in results_p2.items():
|
for nid, res in results_p2.items():
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -16,26 +16,26 @@ def main() -> None:
|
||||||
engine = CollisionEngine(clearance=2.0)
|
engine = CollisionEngine(clearance=2.0)
|
||||||
danger_map = DangerMap(bounds=bounds)
|
danger_map = DangerMap(bounds=bounds)
|
||||||
danger_map.precompute([])
|
danger_map.precompute([])
|
||||||
|
|
||||||
evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.1)
|
evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.1)
|
||||||
router = AStarRouter(evaluator, node_limit=100000)
|
router = AStarRouter(evaluator, node_limit=100000)
|
||||||
pf = PathFinder(router, evaluator)
|
pf = PathFinder(router, evaluator)
|
||||||
|
|
||||||
# 2. Define Netlist with various orientation challenges
|
# 2. Define Netlist with various orientation challenges
|
||||||
netlist = {
|
netlist = {
|
||||||
# Opposite directions: requires two 90-degree bends to flip orientation
|
# Opposite directions: requires two 90-degree bends to flip orientation
|
||||||
"opposite": (Port(10, 80, 0), Port(90, 80, 180)),
|
"opposite": (Port(10, 80, 0), Port(90, 80, 180)),
|
||||||
|
|
||||||
# 90-degree turn: standard L-shape
|
# 90-degree turn: standard L-shape
|
||||||
"turn_90": (Port(10, 60, 0), Port(40, 90, 90)),
|
"turn_90": (Port(10, 60, 0), Port(40, 90, 90)),
|
||||||
|
|
||||||
# Output behind input: requires a full U-turn
|
# Output behind input: requires a full U-turn
|
||||||
"behind": (Port(80, 40, 0), Port(20, 40, 0)),
|
"behind": (Port(80, 40, 0), Port(20, 40, 0)),
|
||||||
|
|
||||||
# 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 = {nid: 2.0 for nid in netlist}
|
net_widths = dict.fromkeys(netlist, 2.0)
|
||||||
|
|
||||||
# 3. Route
|
# 3. Route
|
||||||
results = pf.route_all(netlist, net_widths)
|
results = pf.route_all(netlist, net_widths)
|
||||||
|
|
|
||||||
BIN
examples/06_bend_collision_models.png
Normal file
BIN
examples/06_bend_collision_models.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
70
examples/06_bend_collision_models.py
Normal file
70
examples/06_bend_collision_models.py
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
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()
|
||||||
|
|
@ -1,149 +1,141 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Literal
|
||||||
|
|
||||||
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."""
|
"""Manages spatial queries for collision detection with unified dilation logic."""
|
||||||
|
|
||||||
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()
|
|
||||||
# To store geometries for precise checks
|
# Static obstacles: store raw geometries to avoid double-dilation
|
||||||
self.obstacle_geometries: dict[int, Polygon] = {} # ID -> Polygon
|
self.static_index = rtree.index.Index()
|
||||||
self.prepared_obstacles: dict[int, PreparedGeometry] = {} # ID -> PreparedGeometry
|
self.static_geometries: dict[int, Polygon] = {} # ID -> Polygon
|
||||||
self._id_counter = 0
|
self.static_prepared: dict[int, PreparedGeometry] = {} # ID -> PreparedGeometry
|
||||||
|
self._static_id_counter = 0
|
||||||
|
|
||||||
# Dynamic paths for multi-net congestion
|
# Dynamic paths for multi-net congestion
|
||||||
self.dynamic_paths = rtree.index.Index()
|
self.dynamic_index = rtree.index.Index()
|
||||||
# obj_id -> (net_id, geometry)
|
# obj_id -> (net_id, raw_geometry)
|
||||||
self.path_geometries: dict[int, tuple[str, Polygon]] = {}
|
self.dynamic_geometries: dict[int, tuple[str, Polygon]] = {}
|
||||||
self._dynamic_id_counter = 0
|
self._dynamic_id_counter = 0
|
||||||
|
|
||||||
def add_static_obstacle(self, polygon: Polygon, pre_dilate: bool = True) -> None:
|
def add_static_obstacle(self, polygon: Polygon) -> None:
|
||||||
"""Add a static obstacle to the engine."""
|
"""Add a static obstacle (raw geometry) to the engine."""
|
||||||
_ = pre_dilate # Keep for API compatibility
|
obj_id = self._static_id_counter
|
||||||
obj_id = self._id_counter
|
self._static_id_counter += 1
|
||||||
self._id_counter += 1
|
|
||||||
|
|
||||||
self.obstacle_geometries[obj_id] = polygon
|
self.static_geometries[obj_id] = polygon
|
||||||
self.prepared_obstacles[obj_id] = prep(polygon)
|
self.static_prepared[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 to the dynamic R-Tree."""
|
"""Add a net's routed path (raw geometry) to the dynamic index."""
|
||||||
# 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.path_geometries[obj_id] = (net_id, dilated)
|
self.dynamic_geometries[obj_id] = (net_id, poly)
|
||||||
self.dynamic_paths.insert(obj_id, dilated.bounds)
|
self.dynamic_index.insert(obj_id, poly.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 R-Tree."""
|
"""Remove a net's path from the dynamic index."""
|
||||||
to_remove = [obj_id for obj_id, (nid, _) in self.path_geometries.items() if nid == net_id]
|
to_remove = [obj_id for obj_id, (nid, _) in self.dynamic_geometries.items() if nid == net_id]
|
||||||
for obj_id in to_remove:
|
for obj_id in to_remove:
|
||||||
nid, dilated = self.path_geometries.pop(obj_id)
|
nid, poly = self.dynamic_geometries.pop(obj_id)
|
||||||
self.dynamic_paths.delete(obj_id, dilated.bounds)
|
self.dynamic_index.delete(obj_id, poly.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.path_geometries.items() if nid == net_id]
|
to_move = [obj_id for obj_id, (nid, _) in self.dynamic_geometries.items() if nid == net_id]
|
||||||
for obj_id in to_move:
|
for obj_id in to_move:
|
||||||
nid, dilated = self.path_geometries.pop(obj_id)
|
nid, poly = self.dynamic_geometries.pop(obj_id)
|
||||||
self.dynamic_paths.delete(obj_id, dilated.bounds)
|
self.dynamic_index.delete(obj_id, poly.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,
|
net_width: float = 2.0,
|
||||||
start_port: Port | None = None,
|
start_port: Port | None = None,
|
||||||
end_port: Port | None = None,
|
end_port: Port | None = None
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Check if a geometry (e.g. a Move) collides with static obstacles."""
|
"""Alias for check_collision(buffer_mode='static') for backward compatibility."""
|
||||||
_ = net_width # Width is already integrated into engine dilation settings
|
_ = net_width
|
||||||
dilation = self.clearance / 2.0
|
res = self.check_collision(geometry, "default", buffer_mode="static", start_port=start_port, end_port=end_port)
|
||||||
test_poly = geometry.buffer(dilation)
|
return bool(res)
|
||||||
return self.is_collision_prebuffered(test_poly, start_port=start_port, end_port=end_port)
|
|
||||||
|
|
||||||
def is_collision_prebuffered(
|
def count_congestion(self, geometry: Polygon, net_id: str) -> int:
|
||||||
self,
|
"""Alias for check_collision(buffer_mode='congestion') for backward compatibility."""
|
||||||
dilated_geometry: Polygon,
|
res = self.check_collision(geometry, net_id, buffer_mode="congestion")
|
||||||
|
return int(res)
|
||||||
|
|
||||||
|
def check_collision(
|
||||||
|
self,
|
||||||
|
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:
|
) -> bool | int:
|
||||||
"""Check if a pre-dilated geometry collides with static obstacles."""
|
"""
|
||||||
# Query R-Tree using the bounds of the dilated move
|
Check for collisions using unified dilation logic.
|
||||||
candidates = self.static_obstacles.intersection(dilated_geometry.bounds)
|
|
||||||
|
If buffer_mode == "static":
|
||||||
|
Returns True if geometry collides with static obstacles (buffered by full clearance).
|
||||||
|
If buffer_mode == "congestion":
|
||||||
|
Returns count of other nets colliding with geometry (both buffered by clearance/2).
|
||||||
|
"""
|
||||||
|
if buffer_mode == "static":
|
||||||
|
# Buffered move vs raw static 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 self.static_prepared[obj_id].intersects(test_poly):
|
||||||
|
# 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
|
||||||
|
|
||||||
|
is_safe = False
|
||||||
|
for p in [start_port, end_port]:
|
||||||
|
if p and (abs(ix_minx - p.x) < self.safety_zone_radius and
|
||||||
|
abs(ix_maxx - p.x) < self.safety_zone_radius and
|
||||||
|
abs(ix_miny - p.y) < self.safety_zone_radius and
|
||||||
|
abs(ix_maxy - p.y) < self.safety_zone_radius):
|
||||||
|
is_safe = True
|
||||||
|
break
|
||||||
|
if is_safe:
|
||||||
|
continue
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
for obj_id in candidates:
|
else: # buffer_mode == "congestion"
|
||||||
# Use prepared geometry for fast intersection
|
# Both paths buffered by clearance/2 => Total separation = clearance
|
||||||
if self.prepared_obstacles[obj_id].intersects(dilated_geometry):
|
dilation = self.clearance / 2.0
|
||||||
# Check safety zone (2nm radius)
|
test_poly = geometry.buffer(dilation)
|
||||||
if start_port or end_port:
|
candidates = self.dynamic_index.intersection(test_poly.bounds)
|
||||||
obstacle = self.obstacle_geometries[obj_id]
|
|
||||||
intersection = dilated_geometry.intersection(obstacle)
|
count = 0
|
||||||
|
for obj_id in candidates:
|
||||||
if intersection.is_empty:
|
other_net_id, other_poly = self.dynamic_geometries[obj_id]
|
||||||
continue
|
if other_net_id != net_id:
|
||||||
|
# Buffer the other path segment too
|
||||||
# Precise check: is every point in the intersection close to either port?
|
if test_poly.intersects(other_poly.buffer(dilation)):
|
||||||
ix_minx, ix_miny, ix_maxx, ix_maxy = intersection.bounds
|
count += 1
|
||||||
|
return count
|
||||||
is_near_start = False
|
|
||||||
if start_port:
|
|
||||||
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_miny - start_port.y) < self.safety_zone_radius and abs(ix_maxy - start_port.y) < self.safety_zone_radius):
|
|
||||||
is_near_start = True
|
|
||||||
|
|
||||||
is_near_end = False
|
|
||||||
if end_port:
|
|
||||||
if (abs(ix_minx - end_port.x) < self.safety_zone_radius and abs(ix_maxx - end_port.x) < self.safety_zone_radius and
|
|
||||||
abs(ix_miny - end_port.y) < self.safety_zone_radius and abs(ix_maxy - end_port.y) < self.safety_zone_radius):
|
|
||||||
is_near_end = True
|
|
||||||
|
|
||||||
if is_near_start or is_near_end:
|
|
||||||
continue
|
|
||||||
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import NamedTuple, Literal, Union
|
from typing import NamedTuple, Literal, Any
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from shapely.geometry import Polygon, box
|
from shapely.geometry import Polygon, box
|
||||||
|
|
@ -35,7 +35,7 @@ class Straight:
|
||||||
|
|
||||||
ex = start_port.x + dx
|
ex = start_port.x + dx
|
||||||
ey = start_port.y + dy
|
ey = start_port.y + dy
|
||||||
|
|
||||||
if snap_to_grid:
|
if snap_to_grid:
|
||||||
ex = snap_search_grid(ex)
|
ex = snap_search_grid(ex)
|
||||||
ey = snap_search_grid(ey)
|
ey = snap_search_grid(ey)
|
||||||
|
|
@ -84,39 +84,83 @@ def _get_arc_polygons(cx: float, cy: float, radius: float, width: float, t_start
|
||||||
|
|
||||||
|
|
||||||
def _apply_collision_model(
|
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."""
|
||||||
if isinstance(collision_type, Polygon):
|
if isinstance(collision_type, Polygon):
|
||||||
return [collision_type]
|
return [collision_type]
|
||||||
|
|
||||||
if collision_type == "arc":
|
if collision_type == "arc":
|
||||||
return [arc_poly]
|
return [arc_poly]
|
||||||
|
|
||||||
# Get bounding box
|
# Get bounding box
|
||||||
minx, miny, maxx, maxy = arc_poly.bounds
|
minx, miny, maxx, maxy = arc_poly.bounds
|
||||||
bbox = box(minx, miny, maxx, maxy)
|
bbox = box(minx, miny, maxx, maxy)
|
||||||
|
|
||||||
if collision_type == "bbox":
|
if collision_type == "bbox":
|
||||||
return [bbox]
|
return [bbox]
|
||||||
|
|
||||||
if collision_type == "clipped_bbox":
|
if collision_type == "clipped_bbox":
|
||||||
safe_zone = arc_poly.buffer(clip_margin)
|
res_poly = bbox
|
||||||
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]
|
||||||
|
|
||||||
|
|
||||||
class Bend90:
|
class Bend90:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate(
|
def generate(
|
||||||
start_port: Port,
|
start_port: Port,
|
||||||
radius: float,
|
radius: float,
|
||||||
width: float,
|
width: float,
|
||||||
direction: str = "CW",
|
direction: str = "CW",
|
||||||
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
|
||||||
|
|
@ -133,9 +177,11 @@ class Bend90:
|
||||||
ex = snap_search_grid(cx + radius * np.cos(t_end))
|
ex = snap_search_grid(cx + radius * np.cos(t_end))
|
||||||
ey = snap_search_grid(cy + radius * np.sin(t_end))
|
ey = snap_search_grid(cy + radius * np.sin(t_end))
|
||||||
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(arc_polys[0], collision_type, radius, clip_margin)
|
collision_polys = _apply_collision_model(
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
@ -143,10 +189,10 @@ class Bend90:
|
||||||
class SBend:
|
class SBend:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate(
|
def generate(
|
||||||
start_port: Port,
|
start_port: Port,
|
||||||
offset: float,
|
offset: float,
|
||||||
radius: float,
|
radius: float,
|
||||||
width: float,
|
width: float,
|
||||||
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
|
||||||
|
|
@ -162,7 +208,7 @@ class SBend:
|
||||||
ex = snap_search_grid(start_port.x + dx * np.cos(rad_start) - dy * np.sin(rad_start))
|
ex = snap_search_grid(start_port.x + dx * np.cos(rad_start) - dy * np.sin(rad_start))
|
||||||
ey = snap_search_grid(start_port.y + dx * np.sin(rad_start) + dy * np.cos(rad_start))
|
ey = snap_search_grid(start_port.y + dx * np.sin(rad_start) + dy * np.cos(rad_start))
|
||||||
end_port = Port(ex, ey, start_port.orientation)
|
end_port = Port(ex, ey, start_port.orientation)
|
||||||
|
|
||||||
direction = 1 if offset > 0 else -1
|
direction = 1 if offset > 0 else -1
|
||||||
c1_angle = rad_start + direction * np.pi / 2
|
c1_angle = rad_start + direction * np.pi / 2
|
||||||
cx1 = start_port.x + radius * np.cos(c1_angle)
|
cx1 = start_port.x + radius * np.cos(c1_angle)
|
||||||
|
|
@ -180,6 +226,14 @@ class SBend:
|
||||||
arc1 = _get_arc_polygons(cx1, cy1, radius, width, ts1, te1, sagitta)[0]
|
arc1 = _get_arc_polygons(cx1, cy1, radius, width, ts1, te1, sagitta)[0]
|
||||||
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])
|
||||||
|
|
||||||
collision_polys = _apply_collision_model(combined_arc, collision_type, radius, clip_margin)
|
if collision_type == "clipped_bbox":
|
||||||
|
col1 = _apply_collision_model(arc1, collision_type, radius, width, cx1, cy1, clip_margin)
|
||||||
|
col2 = _apply_collision_model(arc2, collision_type, radius, width, cx2, cy2, clip_margin)
|
||||||
|
collision_polys = [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)
|
||||||
|
|
|
||||||
|
|
@ -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(list(set(lengths + fine_steps)))
|
lengths = sorted(set(lengths + fine_steps))
|
||||||
|
|
||||||
for length in lengths:
|
for length in lengths:
|
||||||
res = Straight.generate(current.port, length, net_width)
|
res = Straight.generate(current.port, length, net_width)
|
||||||
|
|
@ -255,7 +255,9 @@ 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.is_collision(poly, net_width, start_port=parent.port, end_port=result.end_port):
|
if self.cost_evaluator.collision_engine.check_collision(
|
||||||
|
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
|
||||||
|
|
@ -300,8 +302,6 @@ 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:
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Literal, TYPE_CHECKING, Any
|
from typing import Literal, Any
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from shapely.geometry import Polygon
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ class CostEvaluator:
|
||||||
greedy_h_weight=greedy_h_weight,
|
greedy_h_weight=greedy_h_weight,
|
||||||
congestion_penalty=congestion_penalty,
|
congestion_penalty=congestion_penalty,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use config values
|
# Use config values
|
||||||
self.unit_length_cost = self.config.unit_length_cost
|
self.unit_length_cost = self.config.unit_length_cost
|
||||||
self.greedy_h_weight = self.config.greedy_h_weight
|
self.greedy_h_weight = self.config.greedy_h_weight
|
||||||
|
|
@ -73,25 +73,24 @@ class CostEvaluator:
|
||||||
"""Calculate the cost of a single move (Straight, Bend, SBend)."""
|
"""Calculate the cost of a single move (Straight, Bend, SBend)."""
|
||||||
_ = net_width # Unused
|
_ = net_width # Unused
|
||||||
total_cost = length * self.unit_length_cost
|
total_cost = length * self.unit_length_cost
|
||||||
|
|
||||||
# 1. Hard Collision check (Static obstacles)
|
|
||||||
# We buffer by the full clearance to ensure distance >= clearance
|
|
||||||
hard_dilation = self.collision_engine.clearance
|
|
||||||
for poly in geometry:
|
|
||||||
dilated_poly = poly.buffer(hard_dilation)
|
|
||||||
if self.collision_engine.is_collision_prebuffered(dilated_poly, start_port=start_port, end_port=end_port):
|
|
||||||
return 1e15 # Impossible cost for hard collisions
|
|
||||||
|
|
||||||
# 2. Soft Collision check (Negotiated Congestion)
|
# 1. Boundary Check (Centerline based for compatibility)
|
||||||
# We buffer by clearance/2 because both paths are buffered by clearance/2
|
if not self.danger_map.is_within_bounds(end_port.x, end_port.y):
|
||||||
soft_dilation = self.collision_engine.clearance / 2.0
|
return 1e15
|
||||||
|
|
||||||
|
# 2. Collision Check
|
||||||
for poly in geometry:
|
for poly in geometry:
|
||||||
dilated_poly = poly.buffer(soft_dilation)
|
# Hard Collision (Static obstacles)
|
||||||
overlaps = self.collision_engine.count_congestion_prebuffered(dilated_poly, net_id)
|
if self.collision_engine.check_collision(
|
||||||
if overlaps > 0:
|
poly, net_id, buffer_mode="static", start_port=start_port, end_port=end_port
|
||||||
|
):
|
||||||
|
return 1e15
|
||||||
|
|
||||||
|
# Soft Collision (Negotiated Congestion)
|
||||||
|
overlaps = self.collision_engine.check_collision(poly, net_id, buffer_mode="congestion")
|
||||||
|
if isinstance(overlaps, int) and 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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,14 @@ 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."""
|
"""A pre-computed grid for heuristic proximity costs, vectorized for performance."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|
@ -28,47 +29,36 @@ 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."""
|
"""Pre-compute the proximity costs for the entire grid using vectorized operations."""
|
||||||
# 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
|
||||||
|
|
||||||
# Create a binary mask of obstacles
|
# 1. 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)
|
|
||||||
# This is slow for many obstacles; in a real engine, we'd use a faster rasterizer.
|
# Create coordinate grids
|
||||||
from shapely.geometry import Point
|
x_coords = np.linspace(self.minx + self.resolution/2, self.maxx - self.resolution/2, self.width_cells)
|
||||||
|
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:
|
||||||
# Get bounding box in grid coordinates
|
# Use shapely.contains_xy for fast vectorized point-in-polygon check
|
||||||
p_minx, p_miny, p_maxx, p_maxy = poly.bounds
|
in_poly = shapely.contains_xy(poly, xv, yv)
|
||||||
x_start = max(0, int((p_minx - self.minx) / self.resolution))
|
mask[in_poly] = False
|
||||||
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)
|
|
||||||
|
|
||||||
for ix in range(x_start, x_end):
|
# 2. Distance transform (mask=True for empty space)
|
||||||
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
|
||||||
|
|
||||||
# Proximity cost: k / d^2 if d < threshold, else 0
|
# 3. Proximity cost: k / d^2 if d < threshold, else 0
|
||||||
# To avoid division by zero, we cap distances at a small epsilon (e.g. 0.1um)
|
# Cap distances at a small epsilon (e.g. 0.1um) to avoid division by zero
|
||||||
safe_distances = np.maximum(distances, 0.1)
|
safe_distances = np.maximum(distances, 0.1)
|
||||||
self.grid = np.where(distances < self.safety_threshold, self.k / (safe_distances**2), 0.0).astype(np.float32)
|
self.grid = np.where(
|
||||||
|
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."""
|
||||||
|
|
|
||||||
|
|
@ -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 R-Tree
|
# 3. Add to index
|
||||||
all_geoms = []
|
all_geoms = []
|
||||||
for res in path:
|
for res in path:
|
||||||
all_geoms.extend(res.geometry)
|
all_geoms.extend(res.geometry)
|
||||||
|
|
@ -88,7 +88,11 @@ 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:
|
||||||
collision_count += self.cost_evaluator.collision_engine.count_congestion(poly, net_id)
|
overlaps = self.cost_evaluator.collision_engine.check_collision(
|
||||||
|
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
|
||||||
|
|
@ -120,9 +124,12 @@ 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:
|
||||||
collision_count += self.cost_evaluator.collision_engine.count_congestion(poly, net_id)
|
overlaps = self.cost_evaluator.collision_engine.check_collision(
|
||||||
|
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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
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
|
||||||
|
|
@ -66,7 +64,7 @@ def test_bend_collision_models() -> None:
|
||||||
|
|
||||||
# 1. BBox model
|
# 1. BBox model
|
||||||
res_bbox = Bend90.generate(start, radius, width, direction="CCW", collision_type="bbox")
|
res_bbox = Bend90.generate(start, radius, width, direction="CCW", collision_type="bbox")
|
||||||
# Arc CCW R=10 from (0,0,0) ends at (10,10,90).
|
# Arc CCW R=10 from (0,0,0) ends at (10,10,90).
|
||||||
# Waveguide width is 2.0, so bbox will be slightly larger than (0,0,10,10)
|
# Waveguide width is 2.0, so bbox will be slightly larger than (0,0,10,10)
|
||||||
minx, miny, maxx, maxy = res_bbox.geometry[0].bounds
|
minx, miny, maxx, maxy = res_bbox.geometry[0].bounds
|
||||||
assert minx <= 0.0 + 1e-6
|
assert minx <= 0.0 + 1e-6
|
||||||
|
|
@ -89,7 +87,7 @@ def test_sbend_collision_models() -> None:
|
||||||
res_bbox = SBend.generate(start, offset, radius, width, collision_type="bbox")
|
res_bbox = SBend.generate(start, offset, radius, width, collision_type="bbox")
|
||||||
# Geometry should be a single bounding box polygon
|
# Geometry should be a single bounding box polygon
|
||||||
assert len(res_bbox.geometry) == 1
|
assert len(res_bbox.geometry) == 1
|
||||||
|
|
||||||
res_arc = SBend.generate(start, offset, radius, width, collision_type="arc")
|
res_arc = SBend.generate(start, offset, radius, width, collision_type="arc")
|
||||||
assert res_bbox.geometry[0].area > res_arc.geometry[0].area
|
assert res_bbox.geometry[0].area > res_arc.geometry[0].area
|
||||||
|
|
||||||
|
|
@ -100,15 +98,15 @@ def test_sbend_continuity() -> None:
|
||||||
offset = 4.0
|
offset = 4.0
|
||||||
radius = 20.0
|
radius = 20.0
|
||||||
width = 1.0
|
width = 1.0
|
||||||
|
|
||||||
res = SBend.generate(start, offset, radius, width)
|
res = SBend.generate(start, offset, radius, width)
|
||||||
|
|
||||||
# Target orientation should be same as start
|
# Target orientation should be same as start
|
||||||
assert abs(res.end_port.orientation - 90.0) < 1e-6
|
assert abs(res.end_port.orientation - 90.0) < 1e-6
|
||||||
|
|
||||||
# For a port at 90 deg, +offset is a shift in -x direction
|
# For a port at 90 deg, +offset is a shift in -x direction
|
||||||
assert abs(res.end_port.x - (10.0 - offset)) < 1e-6
|
assert abs(res.end_port.x - (10.0 - offset)) < 1e-6
|
||||||
|
|
||||||
# Geometry should be connected (unary_union results in 1 polygon)
|
# Geometry should be connected (unary_union results in 1 polygon)
|
||||||
assert len(res.geometry) == 1
|
assert len(res.geometry) == 1
|
||||||
assert res.geometry[0].is_valid
|
assert res.geometry[0].is_valid
|
||||||
|
|
@ -119,17 +117,17 @@ def test_arc_sagitta_precision() -> None:
|
||||||
start = Port(0, 0, 0)
|
start = Port(0, 0, 0)
|
||||||
radius = 100.0 # Large radius to make sagitta significant
|
radius = 100.0 # Large radius to make sagitta significant
|
||||||
width = 2.0
|
width = 2.0
|
||||||
|
|
||||||
# Coarse: 1um sagitta
|
# Coarse: 1um sagitta
|
||||||
res_coarse = Bend90.generate(start, radius, width, sagitta=1.0)
|
res_coarse = Bend90.generate(start, radius, width, sagitta=1.0)
|
||||||
# Fine: 0.01um (10nm) sagitta
|
# Fine: 0.01um (10nm) sagitta
|
||||||
res_fine = Bend90.generate(start, radius, width, sagitta=0.01)
|
res_fine = Bend90.generate(start, radius, width, sagitta=0.01)
|
||||||
|
|
||||||
# Number of segments should be significantly higher for fine
|
# Number of segments should be significantly higher for fine
|
||||||
# Exterior points = (segments + 1) * 2
|
# Exterior points = (segments + 1) * 2
|
||||||
pts_coarse = len(res_coarse.geometry[0].exterior.coords)
|
pts_coarse = len(res_coarse.geometry[0].exterior.coords)
|
||||||
pts_fine = len(res_fine.geometry[0].exterior.coords)
|
pts_fine = len(res_fine.geometry[0].exterior.coords)
|
||||||
|
|
||||||
assert pts_fine > pts_coarse * 2
|
assert pts_fine > pts_coarse * 2
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -139,20 +137,20 @@ def test_component_transform_invariance() -> None:
|
||||||
start0 = Port(0, 0, 0)
|
start0 = Port(0, 0, 0)
|
||||||
radius = 10.0
|
radius = 10.0
|
||||||
width = 2.0
|
width = 2.0
|
||||||
|
|
||||||
res0 = Bend90.generate(start0, radius, width, direction="CCW")
|
res0 = Bend90.generate(start0, radius, width, direction="CCW")
|
||||||
|
|
||||||
# Transform: Translate (10, 10) then Rotate 90
|
# Transform: Translate (10, 10) then Rotate 90
|
||||||
dx, dy = 10.0, 5.0
|
dx, dy = 10.0, 5.0
|
||||||
angle = 90.0
|
angle = 90.0
|
||||||
|
|
||||||
# 1. Transform the generated geometry
|
# 1. Transform the generated geometry
|
||||||
p_end_transformed = rotate_port(translate_port(res0.end_port, dx, dy), angle)
|
p_end_transformed = rotate_port(translate_port(res0.end_port, dx, dy), angle)
|
||||||
|
|
||||||
# 2. Generate at transformed start
|
# 2. Generate at transformed start
|
||||||
start_transformed = rotate_port(translate_port(start0, dx, dy), angle)
|
start_transformed = rotate_port(translate_port(start0, dx, dy), angle)
|
||||||
res_transformed = Bend90.generate(start_transformed, radius, width, direction="CCW")
|
res_transformed = Bend90.generate(start_transformed, radius, width, direction="CCW")
|
||||||
|
|
||||||
assert abs(res_transformed.end_port.x - p_end_transformed.x) < 1e-6
|
assert abs(res_transformed.end_port.x - p_end_transformed.x) < 1e-6
|
||||||
assert abs(res_transformed.end_port.y - p_end_transformed.y) < 1e-6
|
assert abs(res_transformed.end_port.y - p_end_transformed.y) < 1e-6
|
||||||
assert abs(res_transformed.end_port.orientation - p_end_transformed.orientation) < 1e-6
|
assert abs(res_transformed.end_port.orientation - p_end_transformed.orientation) < 1e-6
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@ 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 Point, Polygon
|
from shapely.geometry import 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
|
||||||
|
|
@ -28,7 +27,7 @@ def validate_routing_result(
|
||||||
obstacle_collision_geoms = []
|
obstacle_collision_geoms = []
|
||||||
self_intersection_geoms = []
|
self_intersection_geoms = []
|
||||||
connectivity_errors = []
|
connectivity_errors = []
|
||||||
|
|
||||||
# 1. Connectivity Check
|
# 1. Connectivity Check
|
||||||
total_length = 0.0
|
total_length = 0.0
|
||||||
for i, comp in enumerate(result.path):
|
for i, comp in enumerate(result.path):
|
||||||
|
|
@ -38,7 +37,7 @@ def validate_routing_result(
|
||||||
if expected_end:
|
if expected_end:
|
||||||
last_port = result.path[-1].end_port
|
last_port = result.path[-1].end_port
|
||||||
dist_to_end = np.sqrt((last_port.x - expected_end.x)**2 + (last_port.y - expected_end.y)**2)
|
dist_to_end = np.sqrt((last_port.x - expected_end.x)**2 + (last_port.y - expected_end.y)**2)
|
||||||
if dist_to_end > 0.005:
|
if dist_to_end > 0.005:
|
||||||
connectivity_errors.append(f"Final port position mismatch: {dist_to_end*1000:.2f}nm")
|
connectivity_errors.append(f"Final port position mismatch: {dist_to_end*1000:.2f}nm")
|
||||||
if abs(last_port.orientation - expected_end.orientation) > 0.1:
|
if abs(last_port.orientation - expected_end.orientation) > 0.1:
|
||||||
connectivity_errors.append(f"Final port orientation mismatch: {last_port.orientation} vs {expected_end.orientation}")
|
connectivity_errors.append(f"Final port orientation mismatch: {last_port.orientation} vs {expected_end.orientation}")
|
||||||
|
|
@ -46,9 +45,9 @@ def validate_routing_result(
|
||||||
# 2. Geometry Buffering
|
# 2. Geometry Buffering
|
||||||
dilation_half = clearance / 2.0
|
dilation_half = clearance / 2.0
|
||||||
dilation_full = clearance
|
dilation_full = clearance
|
||||||
|
|
||||||
dilated_for_self = []
|
dilated_for_self = []
|
||||||
|
|
||||||
for i, comp in enumerate(result.path):
|
for i, comp in enumerate(result.path):
|
||||||
for poly in comp.geometry:
|
for poly in comp.geometry:
|
||||||
# Check against obstacles
|
# Check against obstacles
|
||||||
|
|
@ -58,7 +57,7 @@ def validate_routing_result(
|
||||||
intersection = d_full.intersection(obs)
|
intersection = d_full.intersection(obs)
|
||||||
if intersection.area > 1e-9:
|
if intersection.area > 1e-9:
|
||||||
obstacle_collision_geoms.append(intersection)
|
obstacle_collision_geoms.append(intersection)
|
||||||
|
|
||||||
# Save for self-intersection check
|
# Save for self-intersection check
|
||||||
dilated_for_self.append(poly.buffer(dilation_half))
|
dilated_for_self.append(poly.buffer(dilation_half))
|
||||||
|
|
||||||
|
|
@ -68,13 +67,13 @@ def validate_routing_result(
|
||||||
if j > i + 1: # Non-adjacent
|
if j > i + 1: # Non-adjacent
|
||||||
if seg_i.intersects(seg_j):
|
if seg_i.intersects(seg_j):
|
||||||
overlap = seg_i.intersection(seg_j)
|
overlap = seg_i.intersection(seg_j)
|
||||||
if overlap.area > 1e-6:
|
if overlap.area > 1e-6:
|
||||||
self_intersection_geoms.append((i, j, overlap))
|
self_intersection_geoms.append((i, j, overlap))
|
||||||
|
|
||||||
is_valid = (len(obstacle_collision_geoms) == 0 and
|
is_valid = (len(obstacle_collision_geoms) == 0 and
|
||||||
len(self_intersection_geoms) == 0 and
|
len(self_intersection_geoms) == 0 and
|
||||||
len(connectivity_errors) == 0)
|
len(connectivity_errors) == 0)
|
||||||
|
|
||||||
reasons = []
|
reasons = []
|
||||||
if obstacle_collision_geoms:
|
if obstacle_collision_geoms:
|
||||||
reasons.append(f"Found {len(obstacle_collision_geoms)} obstacle collisions.")
|
reasons.append(f"Found {len(obstacle_collision_geoms)} obstacle collisions.")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue