examples work
11
README.md
|
|
@ -67,16 +67,9 @@ if results["net1"].is_valid:
|
||||||
|
|
||||||
## Usage Examples
|
## Usage Examples
|
||||||
|
|
||||||
Check the `examples/` directory for ready-to-run scripts demonstrating core features:
|
For detailed visual demonstrations and architectural deep-dives, see the **[Examples README](examples/README.md)**.
|
||||||
|
|
||||||
* **`examples/01_simple_route.py`**: Basic single-net routing with visualization. Generates `01_simple_route.png`.
|
Check the `examples/` directory for ready-to-run scripts. To run an example:
|
||||||
* **`examples/02_congestion_resolution.py`**: Multi-net routing resolving bottlenecks using Negotiated Congestion. Generates `02_congestion_resolution.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/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:
|
|
||||||
```bash
|
```bash
|
||||||
python3 examples/01_simple_route.py
|
python3 examples/01_simple_route.py
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 116 KiB |
|
|
@ -18,8 +18,9 @@ def main() -> None:
|
||||||
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)
|
||||||
router = AStarRouter(evaluator, node_limit=100000, bend_collision_type="clipped_bbox", bend_clip_margin=1.0)
|
router.config.bend_collision_type = "clipped_bbox"
|
||||||
|
router.config.bend_clip_margin = 1.0
|
||||||
pf = PathFinder(router, evaluator)
|
pf = PathFinder(router, evaluator)
|
||||||
|
|
||||||
# 2. Define Netlist with various orientation challenges
|
# 2. Define Netlist with various orientation challenges
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 88 KiB |
|
|
@ -46,13 +46,13 @@ def main() -> None:
|
||||||
|
|
||||||
# 2. Route each scenario
|
# 2. Route each scenario
|
||||||
print("Routing Scenario 1 (Arc)...")
|
print("Routing Scenario 1 (Arc)...")
|
||||||
res_arc = PathFinder(router_arc, evaluator).route_all(netlist_arc, {"arc_model": 2.0})
|
res_arc = PathFinder(router_arc, evaluator, use_tiered_strategy=False).route_all(netlist_arc, {"arc_model": 2.0})
|
||||||
|
|
||||||
print("Routing Scenario 2 (BBox)...")
|
print("Routing Scenario 2 (BBox)...")
|
||||||
res_bbox = PathFinder(router_bbox, evaluator).route_all(netlist_bbox, {"bbox_model": 2.0})
|
res_bbox = PathFinder(router_bbox, evaluator, use_tiered_strategy=False).route_all(netlist_bbox, {"bbox_model": 2.0})
|
||||||
|
|
||||||
print("Routing Scenario 3 (Clipped BBox)...")
|
print("Routing Scenario 3 (Clipped BBox)...")
|
||||||
res_clipped = PathFinder(router_clipped, evaluator).route_all(netlist_clipped, {"clipped_model": 2.0})
|
res_clipped = PathFinder(router_clipped, evaluator, use_tiered_strategy=False).route_all(netlist_clipped, {"clipped_model": 2.0})
|
||||||
|
|
||||||
# 3. Combine results for visualization
|
# 3. Combine results for visualization
|
||||||
all_results = {**res_arc, **res_bbox, **res_clipped}
|
all_results = {**res_arc, **res_bbox, **res_clipped}
|
||||||
|
|
|
||||||
BIN
examples/07_large_scale_routing.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
70
examples/07_large_scale_routing.py
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import numpy as np
|
||||||
|
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
|
||||||
|
from shapely.geometry import box
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
print("Running Example 07: Fan-Out (5 Nets)...")
|
||||||
|
|
||||||
|
# 1. Setup Environment
|
||||||
|
# Small area for fast and reliable demonstration
|
||||||
|
bounds = (0, 0, 100, 100)
|
||||||
|
engine = CollisionEngine(clearance=2.0)
|
||||||
|
|
||||||
|
# Wide bottleneck at x=50, 60um gap (from y=20 to y=80)
|
||||||
|
obstacles = [
|
||||||
|
box(50, 0, 55, 20),
|
||||||
|
box(50, 80, 55, 100),
|
||||||
|
]
|
||||||
|
for obs in obstacles:
|
||||||
|
engine.add_static_obstacle(obs)
|
||||||
|
|
||||||
|
danger_map = DangerMap(bounds=bounds)
|
||||||
|
danger_map.precompute(obstacles)
|
||||||
|
|
||||||
|
evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.5)
|
||||||
|
|
||||||
|
# Increase node_limit for more complex search
|
||||||
|
router = AStarRouter(evaluator, node_limit=50000)
|
||||||
|
pf = PathFinder(router, evaluator, max_iterations=2)
|
||||||
|
|
||||||
|
# 2. Define Netlist: Fan-Out Configuration
|
||||||
|
netlist = {}
|
||||||
|
num_nets = 10
|
||||||
|
start_x = 10
|
||||||
|
# Bundle centered at y=50, 4um pitch
|
||||||
|
start_y_base = 50 - (num_nets * 4.0) / 2.0
|
||||||
|
|
||||||
|
end_x = 90
|
||||||
|
end_y_base = 10
|
||||||
|
end_y_pitch = 80.0 / (num_nets - 1)
|
||||||
|
|
||||||
|
for i in range(num_nets):
|
||||||
|
sy = start_y_base + i * 4.0
|
||||||
|
ey = end_y_base + i * end_y_pitch
|
||||||
|
|
||||||
|
net_id = f"net_{i:02d}"
|
||||||
|
netlist[net_id] = (Port(start_x, sy, 0), Port(end_x, ey, 0))
|
||||||
|
|
||||||
|
net_widths = {nid: 2.0 for nid in netlist}
|
||||||
|
|
||||||
|
# 3. Route
|
||||||
|
print(f"Routing {len(netlist)} nets through 60um bottleneck...")
|
||||||
|
results = pf.route_all(netlist, net_widths)
|
||||||
|
|
||||||
|
# 4. Check Results
|
||||||
|
success_count = sum(1 for res in results.values() if res.is_valid)
|
||||||
|
print(f"Routed {success_count}/{len(netlist)} nets successfully.")
|
||||||
|
|
||||||
|
# 5. Visualize
|
||||||
|
fig, ax = plot_routing_results(results, obstacles, bounds, netlist=netlist)
|
||||||
|
fig.savefig("examples/07_large_scale_routing.png")
|
||||||
|
print("Saved plot to examples/07_large_scale_routing.png")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
BIN
examples/08_custom_bend_geometry.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
66
examples/08_custom_bend_geometry.py
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
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 08: Custom Bend Geometry Models...")
|
||||||
|
|
||||||
|
bounds = (0, 0, 150, 150)
|
||||||
|
engine = CollisionEngine(clearance=2.0)
|
||||||
|
|
||||||
|
# Static obstacle to force specific bend paths
|
||||||
|
obstacle = Polygon([(60, 40), (90, 40), (90, 110), (60, 110)])
|
||||||
|
engine.add_static_obstacle(obstacle)
|
||||||
|
|
||||||
|
danger_map = DangerMap(bounds=bounds)
|
||||||
|
danger_map.precompute([obstacle])
|
||||||
|
evaluator = CostEvaluator(engine, danger_map)
|
||||||
|
|
||||||
|
# We will route three nets, each with a DIFFERENT collision model
|
||||||
|
# To do this cleanly with the current architecture, we'll use one router
|
||||||
|
# but change its config per route call (or use tiered escalation in PathFinder).
|
||||||
|
# Since AStarRouter.route now accepts bend_collision_type, we can do it directly.
|
||||||
|
|
||||||
|
router = AStarRouter(evaluator)
|
||||||
|
pf = PathFinder(router, evaluator)
|
||||||
|
|
||||||
|
netlist = {
|
||||||
|
"model_arc": (Port(10, 130, 0), Port(130, 100, -90)),
|
||||||
|
"model_bbox": (Port(10, 80, 0), Port(130, 50, -90)),
|
||||||
|
"model_clipped": (Port(10, 30, 0), Port(130, 10, -90)),
|
||||||
|
}
|
||||||
|
net_widths = {nid: 2.0 for nid in netlist}
|
||||||
|
|
||||||
|
# Manual routing to specify different models per net
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
print("Routing with 'arc' model...")
|
||||||
|
results["model_arc"] = pf.router.route(netlist["model_arc"][0], netlist["model_arc"][1], 2.0,
|
||||||
|
net_id="model_arc", bend_collision_type="arc")
|
||||||
|
|
||||||
|
print("Routing with 'bbox' model...")
|
||||||
|
results["model_bbox"] = pf.router.route(netlist["model_bbox"][0], netlist["model_bbox"][1], 2.0,
|
||||||
|
net_id="model_bbox", bend_collision_type="bbox")
|
||||||
|
|
||||||
|
print("Routing with 'clipped_bbox' model...")
|
||||||
|
results["model_clipped"] = pf.router.route(netlist["model_clipped"][0], netlist["model_clipped"][1], 2.0,
|
||||||
|
net_id="model_clipped", bend_collision_type="clipped_bbox")
|
||||||
|
|
||||||
|
# Wrap in RoutingResult for visualization
|
||||||
|
from inire.router.pathfinder import RoutingResult
|
||||||
|
final_results = {
|
||||||
|
nid: RoutingResult(nid, path if path else [], path is not None, 0)
|
||||||
|
for nid, path in results.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
fig, ax = plot_routing_results(final_results, [obstacle], bounds, netlist=netlist)
|
||||||
|
fig.savefig("examples/08_custom_bend_geometry.png")
|
||||||
|
print("Saved plot to examples/08_custom_bend_geometry.png")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
BIN
examples/09_unroutable_best_effort.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
46
examples/09_unroutable_best_effort.py
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
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, RoutingResult
|
||||||
|
from inire.utils.visualization import plot_routing_results
|
||||||
|
from shapely.geometry import box
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
print("Running Example 09: Unroutable Nets & Best Effort Display...")
|
||||||
|
|
||||||
|
bounds = (0, 0, 100, 100)
|
||||||
|
engine = CollisionEngine(clearance=2.0)
|
||||||
|
|
||||||
|
# A large obstacle that completely blocks the target port
|
||||||
|
blocking_obs = box(40, 0, 60, 100)
|
||||||
|
engine.add_static_obstacle(blocking_obs)
|
||||||
|
|
||||||
|
danger_map = DangerMap(bounds=bounds)
|
||||||
|
danger_map.precompute([blocking_obs])
|
||||||
|
evaluator = CostEvaluator(engine, danger_map)
|
||||||
|
|
||||||
|
# Use a low node limit to fail quickly
|
||||||
|
router = AStarRouter(evaluator, node_limit=5000)
|
||||||
|
|
||||||
|
netlist = {
|
||||||
|
"blocked_net": (Port(10, 50, 0), Port(90, 50, 180))
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Routing blocked net (expecting failure)...")
|
||||||
|
# Manually call route with return_partial=True
|
||||||
|
path = router.route(netlist["blocked_net"][0], netlist["blocked_net"][1], 2.0,
|
||||||
|
net_id="blocked_net", return_partial=True)
|
||||||
|
|
||||||
|
# Wrap in RoutingResult. Even if path is returned, is_valid=False
|
||||||
|
results = {
|
||||||
|
"blocked_net": RoutingResult("blocked_net", path if path else [], False, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fig, ax = plot_routing_results(results, [blocking_obs], bounds, netlist=netlist)
|
||||||
|
fig.savefig("examples/09_unroutable_best_effort.png")
|
||||||
|
print("Saved plot to examples/09_unroutable_best_effort.png")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
38
examples/README.md
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Inire Routing Examples
|
||||||
|
|
||||||
|
This directory contains examples demonstrating the features and architectural capabilities of the `inire` router.
|
||||||
|
|
||||||
|
## Architectural Visualization
|
||||||
|
In all plots generated by `inire`, we distinguish between the search-time geometry and the final "actual" geometry:
|
||||||
|
* **Dashed Lines & Translucent Fill**: The **Collision Proxy** used during the A* search (e.g., `clipped_bbox` or `bbox`). This represents the conservative envelope the router used to guarantee clearance.
|
||||||
|
* **Solid Lines**: The **Actual Geometry** (high-fidelity arcs). This is the exact shape that will be used for PDK generation and fabrication.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Fan-Out (Negotiated Congestion)
|
||||||
|
Demonstrates the Negotiated Congestion algorithm handling multiple intersecting nets. The router iteratively increases penalties for overlaps until a collision-free solution is found. This example shows a bundle of nets fanning out through a narrow bottleneck.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 2. Custom Bend Geometry Models
|
||||||
|
`inire` supports multiple collision models for bends, allowing a trade-off between search speed and geometric accuracy:
|
||||||
|
* **Arc**: High-fidelity geometry (Highest accuracy).
|
||||||
|
* **BBox**: Simple axis-aligned bounding box (Fastest search).
|
||||||
|
* **Clipped BBox**: A balanced model that clips the corners of the AABB to better fit the arc (Optimal performance).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 3. Unroutable Nets & Best-Effort Display
|
||||||
|
When a net is physically blocked or exceeds the node limit, the router returns the "best-effort" partial path—the path that reached the point closest to the target according to the heuristic. This is critical for debugging design constraints.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 4. Orientation Stress Test
|
||||||
|
Demonstrates the router's ability to handle complex orientation requirements, including U-turns, 90-degree flips, and loops.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 5. Tiered Fidelity & Lazy Dilation
|
||||||
|
Our architecture leverages two key optimizations for high-performance routing:
|
||||||
|
1. **Tiered Fidelity**: Initial routing passes use fast `clipped_bbox` proxies. If collisions are found, the system automatically escalates to high-fidelity `arc` geometry for the affected regions.
|
||||||
|
2. **Lazy Dilation**: Geometric buffering (dilation) is deferred until a collision check is strictly necessary, avoiding thousands of redundant `buffer()` and `translate()` calls.
|
||||||
|
|
@ -31,14 +31,20 @@ class ComponentResult:
|
||||||
"""
|
"""
|
||||||
The result of a component generation: geometry, final port, and physical length.
|
The result of a component generation: geometry, final port, and physical length.
|
||||||
"""
|
"""
|
||||||
__slots__ = ('geometry', 'dilated_geometry', 'end_port', 'length', 'bounds', 'dilated_bounds')
|
__slots__ = ('geometry', 'dilated_geometry', 'proxy_geometry', 'actual_geometry', 'end_port', 'length', 'bounds', 'dilated_bounds', '_t_cache')
|
||||||
|
|
||||||
geometry: list[Polygon]
|
geometry: list[Polygon]
|
||||||
""" List of polygons representing the component geometry """
|
""" List of polygons representing the component geometry (could be proxy or arc) """
|
||||||
|
|
||||||
dilated_geometry: list[Polygon] | None
|
dilated_geometry: list[Polygon] | None
|
||||||
""" Optional list of pre-dilated polygons for collision optimization """
|
""" Optional list of pre-dilated polygons for collision optimization """
|
||||||
|
|
||||||
|
proxy_geometry: list[Polygon] | None
|
||||||
|
""" Simplified conservative proxy for tiered collision checks """
|
||||||
|
|
||||||
|
actual_geometry: list[Polygon] | None
|
||||||
|
""" High-fidelity 'actual' geometry for visualization (always the arc) """
|
||||||
|
|
||||||
end_port: Port
|
end_port: Port
|
||||||
""" The final port after the component """
|
""" The final port after the component """
|
||||||
|
|
||||||
|
|
@ -51,40 +57,76 @@ class ComponentResult:
|
||||||
dilated_bounds: numpy.ndarray | None
|
dilated_bounds: numpy.ndarray | None
|
||||||
""" Pre-calculated bounds for each polygon in dilated_geometry """
|
""" Pre-calculated bounds for each polygon in dilated_geometry """
|
||||||
|
|
||||||
|
_t_cache: dict[tuple[float, float], ComponentResult]
|
||||||
|
""" Cache for translated versions of this result """
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
geometry: list[Polygon],
|
geometry: list[Polygon],
|
||||||
end_port: Port,
|
end_port: Port,
|
||||||
length: float,
|
length: float,
|
||||||
dilated_geometry: list[Polygon] | None = None,
|
dilated_geometry: list[Polygon] | None = None,
|
||||||
|
proxy_geometry: list[Polygon] | None = None,
|
||||||
|
actual_geometry: list[Polygon] | None = None,
|
||||||
|
skip_bounds: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.geometry = geometry
|
self.geometry = geometry
|
||||||
self.dilated_geometry = dilated_geometry
|
self.dilated_geometry = dilated_geometry
|
||||||
|
self.proxy_geometry = proxy_geometry
|
||||||
|
self.actual_geometry = actual_geometry
|
||||||
self.end_port = end_port
|
self.end_port = end_port
|
||||||
self.length = length
|
self.length = length
|
||||||
# Vectorized bounds calculation
|
self._t_cache = {}
|
||||||
self.bounds = shapely.bounds(geometry)
|
if not skip_bounds:
|
||||||
self.dilated_bounds = shapely.bounds(dilated_geometry) if dilated_geometry is not None else None
|
# Vectorized bounds calculation
|
||||||
|
self.bounds = shapely.bounds(geometry)
|
||||||
|
self.dilated_bounds = shapely.bounds(dilated_geometry) if dilated_geometry is not None else None
|
||||||
|
|
||||||
def translate(self, dx: float, dy: float) -> ComponentResult:
|
def translate(self, dx: float, dy: float) -> ComponentResult:
|
||||||
"""
|
"""
|
||||||
Create a new ComponentResult translated by (dx, dy).
|
Create a new ComponentResult translated by (dx, dy).
|
||||||
"""
|
"""
|
||||||
|
dxr, dyr = round(dx, 3), round(dy, 3)
|
||||||
|
if (dxr, dyr) == (0.0, 0.0):
|
||||||
|
return self
|
||||||
|
if (dxr, dyr) in self._t_cache:
|
||||||
|
return self._t_cache[(dxr, dyr)]
|
||||||
|
|
||||||
# Vectorized translation if possible, else list comp
|
# Vectorized translation if possible, else list comp
|
||||||
# Shapely 2.x affinity functions still work on single geometries efficiently
|
|
||||||
geoms = list(self.geometry)
|
geoms = list(self.geometry)
|
||||||
num_geom = len(self.geometry)
|
num_geom = len(self.geometry)
|
||||||
|
|
||||||
|
offsets = [num_geom]
|
||||||
if self.dilated_geometry is not None:
|
if self.dilated_geometry is not None:
|
||||||
geoms.extend(self.dilated_geometry)
|
geoms.extend(self.dilated_geometry)
|
||||||
|
offsets.append(len(geoms))
|
||||||
|
|
||||||
|
if self.proxy_geometry is not None:
|
||||||
|
geoms.extend(self.proxy_geometry)
|
||||||
|
offsets.append(len(geoms))
|
||||||
|
|
||||||
|
if self.actual_geometry is not None:
|
||||||
|
geoms.extend(self.actual_geometry)
|
||||||
|
offsets.append(len(geoms))
|
||||||
|
|
||||||
from shapely.affinity import translate
|
from shapely.affinity import translate
|
||||||
translated = [translate(p, dx, dy) for p in geoms]
|
translated = [translate(p, dx, dy) for p in geoms]
|
||||||
|
|
||||||
new_geom = translated[:num_geom]
|
new_geom = translated[:offsets[0]]
|
||||||
new_dil = translated[num_geom:] if self.dilated_geometry is not None else None
|
new_dil = translated[offsets[0]:offsets[1]] if self.dilated_geometry is not None else None
|
||||||
|
new_proxy = translated[offsets[1]:offsets[2]] if self.proxy_geometry is not None else None
|
||||||
|
new_actual = translated[offsets[2]:offsets[3]] if self.actual_geometry is not None else None
|
||||||
|
|
||||||
new_port = Port(self.end_port.x + dx, self.end_port.y + dy, self.end_port.orientation)
|
new_port = Port(self.end_port.x + dx, self.end_port.y + dy, self.end_port.orientation)
|
||||||
return ComponentResult(new_geom, new_port, self.length, new_dil)
|
res = ComponentResult(new_geom, new_port, self.length, new_dil, new_proxy, new_actual, skip_bounds=True)
|
||||||
|
|
||||||
|
# Optimize: reuse and translate bounds
|
||||||
|
res.bounds = self.bounds + [dx, dy, dx, dy]
|
||||||
|
if self.dilated_bounds is not None:
|
||||||
|
res.dilated_bounds = self.dilated_bounds + [dx, dy, dx, dy]
|
||||||
|
|
||||||
|
self._t_cache[(dxr, dyr)] = res
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -159,7 +201,8 @@ class Straight:
|
||||||
poly_points_dil = (pts_dil @ rot_matrix.T) + [start_port.x, start_port.y]
|
poly_points_dil = (pts_dil @ rot_matrix.T) + [start_port.x, start_port.y]
|
||||||
dilated_geom = [Polygon(poly_points_dil)]
|
dilated_geom = [Polygon(poly_points_dil)]
|
||||||
|
|
||||||
return ComponentResult(geometry=geom, end_port=end_port, length=actual_length, dilated_geometry=dilated_geom)
|
# For straight segments, geom IS the actual geometry
|
||||||
|
return ComponentResult(geometry=geom, end_port=end_port, length=actual_length, dilated_geometry=dilated_geom, actual_geometry=geom)
|
||||||
|
|
||||||
|
|
||||||
def _get_num_segments(radius: float, angle_deg: float, sagitta: float = 0.01) -> int:
|
def _get_num_segments(radius: float, angle_deg: float, sagitta: float = 0.01) -> int:
|
||||||
|
|
@ -234,20 +277,31 @@ def _clip_bbox(
|
||||||
width: float,
|
width: float,
|
||||||
clip_margin: float,
|
clip_margin: float,
|
||||||
arc_poly: Polygon,
|
arc_poly: Polygon,
|
||||||
|
t_start: float | None = None,
|
||||||
|
t_end: float | None = None,
|
||||||
) -> Polygon:
|
) -> Polygon:
|
||||||
"""
|
"""
|
||||||
Clips corners of a bounding box for better collision modeling using direct vertex manipulation.
|
Clips corners of a bounding box for better collision modeling.
|
||||||
"""
|
"""
|
||||||
# Determination of which corners to clip
|
|
||||||
ac = arc_poly.centroid
|
|
||||||
qsx = 1.0 if ac.x >= cx else -1.0
|
|
||||||
qsy = 1.0 if ac.y >= cy else -1.0
|
|
||||||
|
|
||||||
r_out_cut = radius + width / 2.0 + clip_margin
|
r_out_cut = radius + width / 2.0 + clip_margin
|
||||||
r_in_cut = radius - width / 2.0 - clip_margin
|
r_in_cut = radius - width / 2.0 - clip_margin
|
||||||
|
|
||||||
|
# Angular range of the arc
|
||||||
|
if t_start is not None and t_end is not None:
|
||||||
|
ts, te = t_start, t_end
|
||||||
|
if ts > te:
|
||||||
|
ts, te = te, ts
|
||||||
|
# Sweep could cross 2pi boundary
|
||||||
|
sweep = (te - ts) % (2 * numpy.pi)
|
||||||
|
ts_norm = ts % (2 * numpy.pi)
|
||||||
|
else:
|
||||||
|
# Fallback: assume 90 deg based on centroid quadrant
|
||||||
|
ac = arc_poly.centroid
|
||||||
|
mid_angle = numpy.arctan2(ac.y - cy, ac.x - cx)
|
||||||
|
ts_norm = (mid_angle - numpy.pi/4) % (2 * numpy.pi)
|
||||||
|
sweep = numpy.pi/2
|
||||||
|
|
||||||
minx, miny, maxx, maxy = bbox.bounds
|
minx, miny, maxx, maxy = bbox.bounds
|
||||||
# Initial vertices: [minx,miny], [maxx,miny], [maxx,maxy], [minx,maxy]
|
|
||||||
verts = [
|
verts = [
|
||||||
numpy.array([minx, miny]),
|
numpy.array([minx, miny]),
|
||||||
numpy.array([maxx, miny]),
|
numpy.array([maxx, miny]),
|
||||||
|
|
@ -259,36 +313,41 @@ def _clip_bbox(
|
||||||
for p in verts:
|
for p in verts:
|
||||||
dx, dy = p[0] - cx, p[1] - cy
|
dx, dy = p[0] - cx, p[1] - cy
|
||||||
dist = numpy.sqrt(dx**2 + dy**2)
|
dist = numpy.sqrt(dx**2 + dy**2)
|
||||||
|
angle = numpy.arctan2(dy, dx)
|
||||||
|
|
||||||
# Normal vector components from center to corner
|
# Check if corner angle is within the arc's angular sweep
|
||||||
sx = 1.0 if dx > 1e-6 else (-1.0 if dx < -1e-6 else qsx)
|
angle_rel = (angle - ts_norm) % (2 * numpy.pi)
|
||||||
sy = 1.0 if dy > 1e-6 else (-1.0 if dy < -1e-6 else qsy)
|
is_in_sweep = angle_rel <= sweep + 1e-6
|
||||||
|
|
||||||
d_line = -1.0
|
d_line = -1.0
|
||||||
if dist > r_out_cut:
|
if is_in_sweep:
|
||||||
d_line = r_out_cut * numpy.sqrt(2)
|
# We can clip if outside R_out or inside R_in
|
||||||
elif r_in_cut > 0 and dist < r_in_cut:
|
if dist > radius + width/2.0 - 1e-6:
|
||||||
d_line = r_in_cut
|
d_line = r_out_cut * numpy.sqrt(2)
|
||||||
|
elif r_in_cut > 1e-3 and dist < radius - width/2.0 + 1e-6:
|
||||||
|
d_line = r_in_cut
|
||||||
|
else:
|
||||||
|
# Corner is outside angular sweep.
|
||||||
|
if dist > radius + width/2.0 - 1e-6:
|
||||||
|
d_line = r_out_cut * numpy.sqrt(2)
|
||||||
|
elif r_in_cut > 1e-3 and dist < radius - width/2.0 + 1e-6:
|
||||||
|
d_line = r_in_cut
|
||||||
|
|
||||||
if d_line > 0:
|
if d_line > 0:
|
||||||
# This corner needs clipping. Replace one vertex with two at intersection of line and edges.
|
sx = 1.0 if dx > 0 else -1.0
|
||||||
# Line: sx*(x-cx) + sy*(y-cy) = d_line
|
sy = 1.0 if dy > 0 else -1.0
|
||||||
# Edge x=px: y = cy + (d_line - sx*(px-cx))/sy
|
|
||||||
# Edge y=py: x = cx + (d_line - sy*(py-cy))/sx
|
|
||||||
try:
|
try:
|
||||||
|
# Intersection of line sx*(x-cx) + sy*(y-cy) = d_line with box edges
|
||||||
p_edge_x = numpy.array([p[0], cy + (d_line - sx * (p[0] - cx)) / sy])
|
p_edge_x = numpy.array([p[0], cy + (d_line - sx * (p[0] - cx)) / sy])
|
||||||
p_edge_y = numpy.array([cx + (d_line - sy * (p[1] - cy)) / sx, p[1]])
|
p_edge_y = numpy.array([cx + (d_line - sy * (p[1] - cy)) / sx, p[1]])
|
||||||
# Order matters for polygon winding.
|
|
||||||
# If we are at [minx, miny] and moving CCW towards [maxx, miny]:
|
# Check if intersection points are on the box boundary
|
||||||
# If we clip this corner, we should add p_edge_y then p_edge_x (or vice versa depending on orientation)
|
if (minx - 1e-6 <= p_edge_y[0] <= maxx + 1e-6 and
|
||||||
# For simplicity, we can just add both and let Polygon sort it out if it's convex,
|
miny - 1e-6 <= p_edge_x[1] <= maxy + 1e-6):
|
||||||
# but better to be precise.
|
new_verts.append(p_edge_x)
|
||||||
# Since we know the bounding box orientation, we can determine order.
|
new_verts.append(p_edge_y)
|
||||||
# BUT: Difference was safer. Let's try a simpler approach:
|
else:
|
||||||
# Just collect all possible vertices and use convex_hull if it's guaranteed convex.
|
new_verts.append(p)
|
||||||
# A clipped bbox is always convex.
|
|
||||||
new_verts.append(p_edge_x)
|
|
||||||
new_verts.append(p_edge_y)
|
|
||||||
except ZeroDivisionError:
|
except ZeroDivisionError:
|
||||||
new_verts.append(p)
|
new_verts.append(p)
|
||||||
else:
|
else:
|
||||||
|
|
@ -305,6 +364,8 @@ def _apply_collision_model(
|
||||||
cx: float = 0.0,
|
cx: float = 0.0,
|
||||||
cy: float = 0.0,
|
cy: float = 0.0,
|
||||||
clip_margin: float = 10.0,
|
clip_margin: float = 10.0,
|
||||||
|
t_start: float | None = None,
|
||||||
|
t_end: float | None = None,
|
||||||
) -> list[Polygon]:
|
) -> list[Polygon]:
|
||||||
"""
|
"""
|
||||||
Applies the specified collision model to an arc geometry.
|
Applies the specified collision model to an arc geometry.
|
||||||
|
|
@ -316,6 +377,7 @@ def _apply_collision_model(
|
||||||
width: Waveguide width.
|
width: Waveguide width.
|
||||||
cx, cy: Arc center.
|
cx, cy: Arc center.
|
||||||
clip_margin: Safety margin for clipping.
|
clip_margin: Safety margin for clipping.
|
||||||
|
t_start, t_end: Arc angles.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of polygons representing the collision model.
|
List of polygons representing the collision model.
|
||||||
|
|
@ -334,7 +396,7 @@ def _apply_collision_model(
|
||||||
return [bbox]
|
return [bbox]
|
||||||
|
|
||||||
if collision_type == "clipped_bbox":
|
if collision_type == "clipped_bbox":
|
||||||
return [_clip_bbox(bbox, cx, cy, radius, width, clip_margin, arc_poly)]
|
return [_clip_bbox(bbox, cx, cy, radius, width, clip_margin, arc_poly, t_start, t_end)]
|
||||||
|
|
||||||
return [arc_poly]
|
return [arc_poly]
|
||||||
|
|
||||||
|
|
@ -356,50 +418,74 @@ class Bend90:
|
||||||
) -> ComponentResult:
|
) -> ComponentResult:
|
||||||
"""
|
"""
|
||||||
Generate a 90-degree bend.
|
Generate a 90-degree bend.
|
||||||
|
|
||||||
Args:
|
|
||||||
start_port: Port to start from.
|
|
||||||
radius: Bend radius.
|
|
||||||
width: Waveguide width.
|
|
||||||
direction: "CW" or "CCW".
|
|
||||||
sagitta: Geometric fidelity.
|
|
||||||
collision_type: Collision model.
|
|
||||||
clip_margin: Margin for clipped_bbox.
|
|
||||||
dilation: Optional dilation distance for pre-calculating collision geometry.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A ComponentResult containing the bend.
|
|
||||||
"""
|
"""
|
||||||
turn_angle = -90 if direction == "CW" else 90
|
turn_angle = -90 if direction == "CW" else 90
|
||||||
rad_start = numpy.radians(start_port.orientation)
|
rad_start = numpy.radians(start_port.orientation)
|
||||||
c_angle = rad_start + (numpy.pi / 2 if direction == "CCW" else -numpy.pi / 2)
|
c_angle = rad_start + (numpy.pi / 2 if direction == "CCW" else -numpy.pi / 2)
|
||||||
cx = start_port.x + radius * numpy.cos(c_angle)
|
|
||||||
cy = start_port.y + radius * numpy.sin(c_angle)
|
|
||||||
t_start = c_angle + numpy.pi
|
|
||||||
t_end = t_start + (numpy.pi / 2 if direction == "CCW" else -numpy.pi / 2)
|
|
||||||
|
|
||||||
ex = snap_search_grid(cx + radius * numpy.cos(t_end))
|
# Initial guess for center
|
||||||
ey = snap_search_grid(cy + radius * numpy.sin(t_end))
|
cx_init = start_port.x + radius * numpy.cos(c_angle)
|
||||||
|
cy_init = start_port.y + radius * numpy.sin(c_angle)
|
||||||
|
t_start_init = c_angle + numpy.pi
|
||||||
|
t_end_init = t_start_init + (numpy.pi / 2 if direction == "CCW" else -numpy.pi / 2)
|
||||||
|
|
||||||
|
# Snap the target point
|
||||||
|
ex = snap_search_grid(cx_init + radius * numpy.cos(t_end_init))
|
||||||
|
ey = snap_search_grid(cy_init + radius * numpy.sin(t_end_init))
|
||||||
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)
|
# Adjust geometry to perfectly hit snapped port
|
||||||
|
dx = ex - start_port.x
|
||||||
|
dy = ey - start_port.y
|
||||||
|
dist = numpy.sqrt(dx**2 + dy**2)
|
||||||
|
|
||||||
|
# New radius for the right triangle connecting start to end with 90 deg
|
||||||
|
actual_radius = dist / numpy.sqrt(2)
|
||||||
|
|
||||||
|
# Vector from start to end
|
||||||
|
mid_x, mid_y = (start_port.x + ex)/2, (start_port.y + ey)/2
|
||||||
|
# Normal vector (orthogonal to start->end)
|
||||||
|
# Flip direction based on CW/CCW
|
||||||
|
dir_sign = 1 if direction == "CCW" else -1
|
||||||
|
cx = mid_x - dir_sign * (ey - start_port.y) / 2
|
||||||
|
cy = mid_y + dir_sign * (ex - start_port.x) / 2
|
||||||
|
|
||||||
|
# Update angles based on new center
|
||||||
|
t_start = numpy.arctan2(start_port.y - cy, start_port.x - cx)
|
||||||
|
t_end = numpy.arctan2(ey - cy, ex - cx)
|
||||||
|
|
||||||
|
# Maintain directionality and angular span near pi/2
|
||||||
|
if direction == "CCW":
|
||||||
|
while t_end < t_start: t_end += 2 * numpy.pi
|
||||||
|
else:
|
||||||
|
while t_end > t_start: t_end -= 2 * numpy.pi
|
||||||
|
|
||||||
|
arc_polys = _get_arc_polygons(cx, cy, actual_radius, width, t_start, t_end, sagitta)
|
||||||
collision_polys = _apply_collision_model(
|
collision_polys = _apply_collision_model(
|
||||||
arc_polys[0], collision_type, radius, width, cx, cy, clip_margin
|
arc_polys[0], collision_type, actual_radius, width, cx, cy, clip_margin, t_start, t_end
|
||||||
)
|
)
|
||||||
|
|
||||||
|
proxy_geom = None
|
||||||
|
if collision_type == "arc":
|
||||||
|
# Auto-generate a clipped_bbox proxy for tiered collision checks
|
||||||
|
proxy_geom = _apply_collision_model(
|
||||||
|
arc_polys[0], "clipped_bbox", actual_radius, width, cx, cy, clip_margin, t_start, t_end
|
||||||
|
)
|
||||||
|
|
||||||
dilated_geom = None
|
dilated_geom = None
|
||||||
if dilation > 0:
|
if dilation > 0:
|
||||||
if collision_type == "arc":
|
if collision_type == "arc":
|
||||||
dilated_geom = _get_arc_polygons(cx, cy, radius, width, t_start, t_end, sagitta, dilation=dilation)
|
dilated_geom = _get_arc_polygons(cx, cy, actual_radius, width, t_start, t_end, sagitta, dilation=dilation)
|
||||||
else:
|
else:
|
||||||
# For bbox or clipped_bbox, buffer the model itself (which is simpler than buffering the high-fidelity arc)
|
|
||||||
dilated_geom = [p.buffer(dilation) for p in collision_polys]
|
dilated_geom = [p.buffer(dilation) for p in collision_polys]
|
||||||
|
|
||||||
return ComponentResult(
|
return ComponentResult(
|
||||||
geometry=collision_polys,
|
geometry=collision_polys,
|
||||||
end_port=end_port,
|
end_port=end_port,
|
||||||
length=radius * numpy.pi / 2.0,
|
length=actual_radius * numpy.abs(t_end - t_start),
|
||||||
dilated_geometry=dilated_geom
|
dilated_geometry=dilated_geom,
|
||||||
|
proxy_geometry=proxy_geom,
|
||||||
|
actual_geometry=arc_polys
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -420,65 +506,61 @@ class SBend:
|
||||||
) -> ComponentResult:
|
) -> ComponentResult:
|
||||||
"""
|
"""
|
||||||
Generate a parametric S-bend (two tangent arcs).
|
Generate a parametric S-bend (two tangent arcs).
|
||||||
|
|
||||||
Args:
|
|
||||||
start_port: Port to start from.
|
|
||||||
offset: Lateral offset.
|
|
||||||
radius: Arc radii.
|
|
||||||
width: Waveguide width.
|
|
||||||
sagitta: Geometric fidelity.
|
|
||||||
collision_type: Collision model.
|
|
||||||
clip_margin: Margin for clipped_bbox.
|
|
||||||
dilation: Optional dilation distance for pre-calculating collision geometry.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A ComponentResult containing the S-bend.
|
|
||||||
"""
|
"""
|
||||||
if abs(offset) >= 2 * radius:
|
if abs(offset) >= 2 * radius:
|
||||||
raise ValueError(f"SBend offset {offset} must be less than 2*radius {2 * radius}")
|
raise ValueError(f"SBend offset {offset} must be less than 2*radius {2 * radius}")
|
||||||
|
|
||||||
theta = numpy.arccos(1 - abs(offset) / (2 * radius))
|
theta_init = numpy.arccos(1 - abs(offset) / (2 * radius))
|
||||||
dx = 2 * radius * numpy.sin(theta)
|
dx_init = 2 * radius * numpy.sin(theta_init)
|
||||||
dy = offset
|
|
||||||
rad_start = numpy.radians(start_port.orientation)
|
rad_start = numpy.radians(start_port.orientation)
|
||||||
ex = snap_search_grid(start_port.x + dx * numpy.cos(rad_start) - dy * numpy.sin(rad_start))
|
|
||||||
ey = snap_search_grid(start_port.y + dx * numpy.sin(rad_start) + dy * numpy.cos(rad_start))
|
# Snap the target point
|
||||||
|
ex = snap_search_grid(start_port.x + dx_init * numpy.cos(rad_start) - offset * numpy.sin(rad_start))
|
||||||
|
ey = snap_search_grid(start_port.y + dx_init * numpy.sin(rad_start) + offset * numpy.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
|
# Solve for theta and radius that hit (ex, ey) exactly
|
||||||
|
local_dx = (ex - start_port.x) * numpy.cos(rad_start) + (ey - start_port.y) * numpy.sin(rad_start)
|
||||||
|
local_dy = -(ex - start_port.x) * numpy.sin(rad_start) + (ey - start_port.y) * numpy.cos(rad_start)
|
||||||
|
|
||||||
|
# tan(theta / 2) = local_dy / local_dx
|
||||||
|
theta = 2 * numpy.arctan2(abs(local_dy), local_dx)
|
||||||
|
# Avoid division by zero if theta is 0 (though unlikely due to offset check)
|
||||||
|
actual_radius = abs(local_dy) / (2 * (1 - numpy.cos(theta))) if theta > 1e-9 else radius
|
||||||
|
|
||||||
|
direction = 1 if local_dy > 0 else -1
|
||||||
c1_angle = rad_start + direction * numpy.pi / 2
|
c1_angle = rad_start + direction * numpy.pi / 2
|
||||||
cx1 = start_port.x + radius * numpy.cos(c1_angle)
|
cx1 = start_port.x + actual_radius * numpy.cos(c1_angle)
|
||||||
cy1 = start_port.y + radius * numpy.sin(c1_angle)
|
cy1 = start_port.y + actual_radius * numpy.sin(c1_angle)
|
||||||
ts1, te1 = c1_angle + numpy.pi, c1_angle + numpy.pi + direction * theta
|
ts1, te1 = c1_angle + numpy.pi, c1_angle + numpy.pi + direction * theta
|
||||||
|
|
||||||
ex_raw = start_port.x + dx * numpy.cos(rad_start) - dy * numpy.sin(rad_start)
|
|
||||||
ey_raw = start_port.y + dx * numpy.sin(rad_start) + dy * numpy.cos(rad_start)
|
|
||||||
c2_angle = rad_start - direction * numpy.pi / 2
|
c2_angle = rad_start - direction * numpy.pi / 2
|
||||||
cx2 = ex_raw + radius * numpy.cos(c2_angle)
|
cx2 = ex + actual_radius * numpy.cos(c2_angle)
|
||||||
cy2 = ey_raw + radius * numpy.sin(c2_angle)
|
cy2 = ey + actual_radius * numpy.sin(c2_angle)
|
||||||
te2 = c2_angle + numpy.pi
|
te2 = c2_angle + numpy.pi
|
||||||
ts2 = te2 + direction * theta
|
ts2 = te2 + direction * theta
|
||||||
|
|
||||||
arc1 = _get_arc_polygons(cx1, cy1, radius, width, ts1, te1, sagitta)[0]
|
arc1 = _get_arc_polygons(cx1, cy1, actual_radius, width, ts1, te1, sagitta)[0]
|
||||||
arc2 = _get_arc_polygons(cx2, cy2, radius, width, ts2, te2, sagitta)[0]
|
arc2 = _get_arc_polygons(cx2, cy2, actual_radius, width, ts2, te2, sagitta)[0]
|
||||||
|
arc_polys = [arc1, arc2]
|
||||||
|
|
||||||
if collision_type == "clipped_bbox":
|
# Use the provided collision model for primary geometry
|
||||||
col1 = _apply_collision_model(arc1, collision_type, radius, width, cx1, cy1, clip_margin)[0]
|
col1 = _apply_collision_model(arc1, collision_type, actual_radius, width, cx1, cy1, clip_margin, ts1, te1)[0]
|
||||||
col2 = _apply_collision_model(arc2, collision_type, radius, width, cx2, cy2, clip_margin)[0]
|
col2 = _apply_collision_model(arc2, collision_type, actual_radius, width, cx2, cy2, clip_margin, ts2, te2)[0]
|
||||||
# Optimization: keep as list instead of unary_union for search efficiency
|
collision_polys = [col1, col2]
|
||||||
collision_polys = [col1, col2]
|
|
||||||
else:
|
proxy_geom = None
|
||||||
# For other models, we can either combine or keep separate.
|
if collision_type == "arc":
|
||||||
# Keeping separate is generally better for CollisionEngine.
|
# Auto-generate proxies
|
||||||
col1 = _apply_collision_model(arc1, collision_type, radius, width, cx1, cy1, clip_margin)[0]
|
p1 = _apply_collision_model(arc1, "clipped_bbox", actual_radius, width, cx1, cy1, clip_margin, ts1, te1)[0]
|
||||||
col2 = _apply_collision_model(arc2, collision_type, radius, width, cx2, cy2, clip_margin)[0]
|
p2 = _apply_collision_model(arc2, "clipped_bbox", actual_radius, width, cx2, cy2, clip_margin, ts2, te2)[0]
|
||||||
collision_polys = [col1, col2]
|
proxy_geom = [p1, p2]
|
||||||
|
|
||||||
dilated_geom = None
|
dilated_geom = None
|
||||||
if dilation > 0:
|
if dilation > 0:
|
||||||
if collision_type == "arc":
|
if collision_type == "arc":
|
||||||
d1 = _get_arc_polygons(cx1, cy1, radius, width, ts1, te1, sagitta, dilation=dilation)[0]
|
d1 = _get_arc_polygons(cx1, cy1, actual_radius, width, ts1, te1, sagitta, dilation=dilation)[0]
|
||||||
d2 = _get_arc_polygons(cx2, cy2, radius, width, ts2, te2, sagitta, dilation=dilation)[0]
|
d2 = _get_arc_polygons(cx2, cy2, actual_radius, width, ts2, te2, sagitta, dilation=dilation)[0]
|
||||||
dilated_geom = [d1, d2]
|
dilated_geom = [d1, d2]
|
||||||
else:
|
else:
|
||||||
dilated_geom = [p.buffer(dilation) for p in collision_polys]
|
dilated_geom = [p.buffer(dilation) for p in collision_polys]
|
||||||
|
|
@ -486,6 +568,8 @@ class SBend:
|
||||||
return ComponentResult(
|
return ComponentResult(
|
||||||
geometry=collision_polys,
|
geometry=collision_polys,
|
||||||
end_port=end_port,
|
end_port=end_port,
|
||||||
length=2 * radius * theta,
|
length=2 * actual_radius * theta,
|
||||||
dilated_geometry=dilated_geom
|
dilated_geometry=dilated_geom,
|
||||||
|
proxy_geometry=proxy_geom,
|
||||||
|
actual_geometry=arc_polys
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -19,38 +19,11 @@ if TYPE_CHECKING:
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@functools.total_ordering
|
|
||||||
class AStarNode:
|
class AStarNode:
|
||||||
"""
|
"""
|
||||||
A node in the A* search graph.
|
A node in the A* search tree.
|
||||||
"""
|
"""
|
||||||
__slots__ = ('port', 'g_cost', 'h_cost', 'f_cost', 'parent', 'component_result', 'count', 'path_bbox')
|
__slots__ = ('port', 'g_cost', 'h_cost', 'f_cost', 'parent', 'component_result', 'path_bbox')
|
||||||
|
|
||||||
port: Port
|
|
||||||
""" Port representing the state at this node """
|
|
||||||
|
|
||||||
g_cost: float
|
|
||||||
""" Actual cost from start to this node """
|
|
||||||
|
|
||||||
h_cost: float
|
|
||||||
""" Heuristic cost from this node to target """
|
|
||||||
|
|
||||||
f_cost: float
|
|
||||||
""" Total estimated cost (g + h) """
|
|
||||||
|
|
||||||
parent: AStarNode | None
|
|
||||||
""" Parent node in the search tree """
|
|
||||||
|
|
||||||
component_result: ComponentResult | None
|
|
||||||
""" The component move that led to this node """
|
|
||||||
|
|
||||||
count: int
|
|
||||||
""" Unique insertion order for tie-breaking """
|
|
||||||
|
|
||||||
path_bbox: tuple[float, float, float, float] | None
|
|
||||||
""" Bounding box of the entire path up to this node """
|
|
||||||
|
|
||||||
_count = 0
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|
@ -66,127 +39,68 @@ class AStarNode:
|
||||||
self.f_cost = g_cost + h_cost
|
self.f_cost = g_cost + h_cost
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.component_result = component_result
|
self.component_result = component_result
|
||||||
self.count = AStarNode._count
|
|
||||||
AStarNode._count += 1
|
|
||||||
|
|
||||||
# Calculate path_bbox
|
|
||||||
if parent is None:
|
if parent is None:
|
||||||
self.path_bbox = None
|
self.path_bbox = None
|
||||||
else:
|
else:
|
||||||
# Union of parent's bbox and current move's bbox
|
# Union of parent's bbox and current move's bbox
|
||||||
if component_result:
|
if component_result:
|
||||||
# Merge all polygon bounds in the result
|
# Merge all polygon bounds in the result
|
||||||
minx, miny, maxx, maxy = 1e15, 1e15, -1e15, -1e15
|
b = component_result.dilated_bounds if component_result.dilated_bounds is not None else component_result.bounds
|
||||||
for b in component_result.dilated_bounds if component_result.dilated_bounds is not None else component_result.bounds:
|
minx = numpy.min(b[:, 0])
|
||||||
minx = min(minx, b[0])
|
miny = numpy.min(b[:, 1])
|
||||||
miny = min(miny, b[1])
|
maxx = numpy.max(b[:, 2])
|
||||||
maxx = max(maxx, b[2])
|
maxy = numpy.max(b[:, 3])
|
||||||
maxy = max(maxy, b[3])
|
|
||||||
|
|
||||||
if parent.path_bbox:
|
if parent.path_bbox:
|
||||||
|
pb = parent.path_bbox
|
||||||
self.path_bbox = (
|
self.path_bbox = (
|
||||||
min(minx, parent.path_bbox[0]),
|
min(minx, pb[0]),
|
||||||
min(miny, parent.path_bbox[1]),
|
min(miny, pb[1]),
|
||||||
max(maxx, parent.path_bbox[2]),
|
max(maxx, pb[2]),
|
||||||
max(maxy, parent.path_bbox[3])
|
max(maxy, pb[3])
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.path_bbox = (minx, miny, maxx, maxy)
|
self.path_bbox = (minx, miny, maxx, maxy)
|
||||||
else:
|
|
||||||
self.path_bbox = parent.path_bbox
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def __lt__(self, other: AStarNode) -> bool:
|
def __lt__(self, other: AStarNode) -> bool:
|
||||||
# Tie-breaking: lower f first, then lower h, then order
|
# Tie-break with h_cost (favour nodes closer to target)
|
||||||
if abs(self.f_cost - other.f_cost) > 1e-9:
|
if abs(self.f_cost - other.f_cost) < 1e-6:
|
||||||
return self.f_cost < other.f_cost
|
|
||||||
if abs(self.h_cost - other.h_cost) > 1e-9:
|
|
||||||
return self.h_cost < other.h_cost
|
return self.h_cost < other.h_cost
|
||||||
return self.count < other.count
|
return self.f_cost < other.f_cost
|
||||||
|
|
||||||
def __eq__(self, other: object) -> bool:
|
|
||||||
if not isinstance(other, AStarNode):
|
|
||||||
return False
|
|
||||||
return self.count == other.count
|
|
||||||
|
|
||||||
|
|
||||||
class AStarRouter:
|
class AStarRouter:
|
||||||
"""
|
"""
|
||||||
Hybrid State-Lattice A* Router.
|
Waveguide router based on A* search on a continuous-state lattice.
|
||||||
"""
|
"""
|
||||||
__slots__ = ('cost_evaluator', 'config', 'node_limit', 'total_nodes_expanded', '_collision_cache', '_move_cache', '_self_dilation')
|
def __init__(self, cost_evaluator: CostEvaluator, node_limit: int | None = None, **kwargs) -> None:
|
||||||
|
|
||||||
cost_evaluator: CostEvaluator
|
|
||||||
""" The evaluator for path and proximity costs """
|
|
||||||
|
|
||||||
config: RouterConfig
|
|
||||||
""" Search configuration parameters """
|
|
||||||
|
|
||||||
node_limit: int
|
|
||||||
""" Maximum nodes to expand before failure """
|
|
||||||
|
|
||||||
total_nodes_expanded: int
|
|
||||||
""" Counter for debugging/profiling """
|
|
||||||
|
|
||||||
_collision_cache: dict[tuple[float, float, float, str, float, str], bool]
|
|
||||||
""" Internal cache for move collision checks """
|
|
||||||
|
|
||||||
_move_cache: dict[tuple[Any, ...], ComponentResult]
|
|
||||||
""" Internal cache for component generation """
|
|
||||||
|
|
||||||
_self_dilation: float
|
|
||||||
""" Cached dilation value for collision checks (clearance / 2.0) """
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
cost_evaluator: CostEvaluator,
|
|
||||||
node_limit: int = 1000000,
|
|
||||||
straight_lengths: list[float] | None = None,
|
|
||||||
bend_radii: list[float] | None = None,
|
|
||||||
sbend_offsets: list[float] | None = None,
|
|
||||||
sbend_radii: list[float] | None = None,
|
|
||||||
snap_to_target_dist: float = 20.0,
|
|
||||||
bend_penalty: float = 50.0,
|
|
||||||
sbend_penalty: float = 100.0,
|
|
||||||
bend_collision_type: Literal['arc', 'bbox', 'clipped_bbox'] = 'arc',
|
|
||||||
bend_clip_margin: float = 10.0,
|
|
||||||
) -> None:
|
|
||||||
"""
|
|
||||||
Initialize the A* Router.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
cost_evaluator: Path cost evaluator.
|
|
||||||
node_limit: Node expansion limit.
|
|
||||||
straight_lengths: Allowed straight lengths (um).
|
|
||||||
bend_radii: Allowed 90-deg radii (um).
|
|
||||||
sbend_offsets: Allowed S-bend lateral offsets (um).
|
|
||||||
sbend_radii: Allowed S-bend radii (um).
|
|
||||||
snap_to_target_dist: Radius for target lookahead (um).
|
|
||||||
bend_penalty: Penalty for 90-degree turns.
|
|
||||||
sbend_penalty: Penalty for S-bends.
|
|
||||||
bend_collision_type: Collision model for bends.
|
|
||||||
bend_clip_margin: Margin for clipped_bbox model.
|
|
||||||
"""
|
|
||||||
self.cost_evaluator = cost_evaluator
|
self.cost_evaluator = cost_evaluator
|
||||||
self.config = RouterConfig(
|
self.config = RouterConfig()
|
||||||
node_limit=node_limit,
|
|
||||||
straight_lengths=straight_lengths if straight_lengths is not None else [1.0, 5.0, 25.0, 100.0],
|
if node_limit is not None:
|
||||||
bend_radii=bend_radii if bend_radii is not None else [10.0],
|
self.config.node_limit = node_limit
|
||||||
sbend_offsets=sbend_offsets if sbend_offsets is not None else [-5.0, -2.0, 2.0, 5.0],
|
|
||||||
sbend_radii=sbend_radii if sbend_radii is not None else [10.0],
|
for k, v in kwargs.items():
|
||||||
snap_to_target_dist=snap_to_target_dist,
|
if hasattr(self.config, k):
|
||||||
bend_penalty=bend_penalty,
|
setattr(self.config, k, v)
|
||||||
sbend_penalty=sbend_penalty,
|
|
||||||
bend_collision_type=bend_collision_type,
|
|
||||||
bend_clip_margin=bend_clip_margin,
|
|
||||||
)
|
|
||||||
self.node_limit = self.config.node_limit
|
self.node_limit = self.config.node_limit
|
||||||
|
|
||||||
|
# Performance cache for collision checks
|
||||||
|
# Key: (start_x, start_y, start_ori, move_type, width, net_id) -> bool
|
||||||
|
self._collision_cache: dict[tuple, bool] = {}
|
||||||
|
|
||||||
|
# Cache for generated moves (relative to origin)
|
||||||
|
# Key: (orientation, type, params...) -> ComponentResult
|
||||||
|
self._move_cache: dict[tuple, ComponentResult] = {}
|
||||||
|
|
||||||
self.total_nodes_expanded = 0
|
self.total_nodes_expanded = 0
|
||||||
self._collision_cache = {}
|
|
||||||
self._move_cache = {}
|
@property
|
||||||
self._self_dilation = self.cost_evaluator.collision_engine.clearance / 2.0
|
def _self_dilation(self) -> float:
|
||||||
|
""" Clearance from other paths (negotiated congestion) """
|
||||||
|
return self.cost_evaluator.collision_engine.clearance / 2.0
|
||||||
|
|
||||||
def route(
|
def route(
|
||||||
self,
|
self,
|
||||||
|
|
@ -195,6 +109,7 @@ class AStarRouter:
|
||||||
net_width: float,
|
net_width: float,
|
||||||
net_id: str = 'default',
|
net_id: str = 'default',
|
||||||
bend_collision_type: Literal['arc', 'bbox', 'clipped_bbox'] | None = None,
|
bend_collision_type: Literal['arc', 'bbox', 'clipped_bbox'] | None = None,
|
||||||
|
return_partial: bool = False,
|
||||||
) -> list[ComponentResult] | None:
|
) -> list[ComponentResult] | None:
|
||||||
"""
|
"""
|
||||||
Route a single net using A*.
|
Route a single net using A*.
|
||||||
|
|
@ -205,6 +120,7 @@ class AStarRouter:
|
||||||
net_width: Waveguide width (um).
|
net_width: Waveguide width (um).
|
||||||
net_id: Optional net identifier.
|
net_id: Optional net identifier.
|
||||||
bend_collision_type: Override collision model for this route.
|
bend_collision_type: Override collision model for this route.
|
||||||
|
return_partial: If True, return the best partial path on failure.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of moves forming the path, or None if failed.
|
List of moves forming the path, or None if failed.
|
||||||
|
|
@ -212,7 +128,9 @@ class AStarRouter:
|
||||||
if bend_collision_type is not None:
|
if bend_collision_type is not None:
|
||||||
self.config.bend_collision_type = bend_collision_type
|
self.config.bend_collision_type = bend_collision_type
|
||||||
|
|
||||||
self._collision_cache.clear()
|
# Do NOT clear _collision_cache here to allow sharing static collision results across nets
|
||||||
|
# self._collision_cache.clear()
|
||||||
|
|
||||||
open_set: list[AStarNode] = []
|
open_set: list[AStarNode] = []
|
||||||
# Key: (x, y, orientation) rounded to 1nm
|
# Key: (x, y, orientation) rounded to 1nm
|
||||||
closed_set: set[tuple[float, float, float]] = set()
|
closed_set: set[tuple[float, float, float]] = set()
|
||||||
|
|
@ -220,15 +138,20 @@ class AStarRouter:
|
||||||
start_node = AStarNode(start, 0.0, self.cost_evaluator.h_manhattan(start, target))
|
start_node = AStarNode(start, 0.0, self.cost_evaluator.h_manhattan(start, target))
|
||||||
heapq.heappush(open_set, start_node)
|
heapq.heappush(open_set, start_node)
|
||||||
|
|
||||||
|
best_node = start_node
|
||||||
nodes_expanded = 0
|
nodes_expanded = 0
|
||||||
|
|
||||||
while open_set:
|
while open_set:
|
||||||
if nodes_expanded >= self.node_limit:
|
if nodes_expanded >= self.node_limit:
|
||||||
logger.warning(f' AStar failed: node limit {self.node_limit} reached.')
|
logger.warning(f' AStar failed: node limit {self.node_limit} reached.')
|
||||||
return None
|
return self._reconstruct_path(best_node) if return_partial else None
|
||||||
|
|
||||||
current = heapq.heappop(open_set)
|
current = heapq.heappop(open_set)
|
||||||
|
|
||||||
|
# Best effort tracking
|
||||||
|
if current.h_cost < best_node.h_cost:
|
||||||
|
best_node = current
|
||||||
|
|
||||||
# Prune if already visited
|
# Prune if already visited
|
||||||
state = (round(current.port.x, 3), round(current.port.y, 3), round(current.port.orientation, 2))
|
state = (round(current.port.x, 3), round(current.port.y, 3), round(current.port.orientation, 2))
|
||||||
if state in closed_set:
|
if state in closed_set:
|
||||||
|
|
@ -238,7 +161,7 @@ class AStarRouter:
|
||||||
nodes_expanded += 1
|
nodes_expanded += 1
|
||||||
self.total_nodes_expanded += 1
|
self.total_nodes_expanded += 1
|
||||||
|
|
||||||
if nodes_expanded % 5000 == 0:
|
if nodes_expanded % 10000 == 0:
|
||||||
logger.info(f'Nodes expanded: {nodes_expanded}, current: {current.port}, g: {current.g_cost:.1f}')
|
logger.info(f'Nodes expanded: {nodes_expanded}, current: {current.port}, g: {current.g_cost:.1f}')
|
||||||
|
|
||||||
# Check if we reached the target exactly
|
# Check if we reached the target exactly
|
||||||
|
|
@ -250,7 +173,7 @@ class AStarRouter:
|
||||||
# Expansion
|
# Expansion
|
||||||
self._expand_moves(current, target, net_width, net_id, open_set, closed_set)
|
self._expand_moves(current, target, net_width, net_id, open_set, closed_set)
|
||||||
|
|
||||||
return None
|
return self._reconstruct_path(best_node) if return_partial else None
|
||||||
|
|
||||||
def _expand_moves(
|
def _expand_moves(
|
||||||
self,
|
self,
|
||||||
|
|
@ -272,7 +195,7 @@ class AStarRouter:
|
||||||
proj = dx * numpy.cos(rad) + dy * numpy.sin(rad)
|
proj = dx * numpy.cos(rad) + dy * numpy.sin(rad)
|
||||||
perp = -dx * numpy.sin(rad) + dy * numpy.cos(rad)
|
perp = -dx * numpy.sin(rad) + dy * numpy.cos(rad)
|
||||||
if proj > 0 and abs(perp) < 1e-6:
|
if proj > 0 and abs(perp) < 1e-6:
|
||||||
res = Straight.generate(current.port, proj, net_width, snap_to_grid=False, dilation=self._self_dilation)
|
res = Straight.generate(current.port, proj, net_width, snap_to_grid=False, dilation=0.0)
|
||||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, 'SnapStraight')
|
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, 'SnapStraight')
|
||||||
|
|
||||||
# B. Try SBend exact reach
|
# B. Try SBend exact reach
|
||||||
|
|
@ -292,18 +215,17 @@ class AStarRouter:
|
||||||
net_width,
|
net_width,
|
||||||
collision_type=self.config.bend_collision_type,
|
collision_type=self.config.bend_collision_type,
|
||||||
clip_margin=self.config.bend_clip_margin,
|
clip_margin=self.config.bend_clip_margin,
|
||||||
dilation=self._self_dilation
|
dilation=0.0
|
||||||
)
|
)
|
||||||
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, 'SnapSBend', move_radius=radius)
|
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, 'SnapSBend', move_radius=radius)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Move Cache
|
# 2. Lattice Straights
|
||||||
cp = current.port
|
cp = current.port
|
||||||
base_ori = round(cp.orientation % 360, 2)
|
base_ori = round(cp.orientation, 2)
|
||||||
state_key = (round(cp.x, 3), round(cp.y, 3), base_ori)
|
state_key = (round(cp.x, 3), round(cp.y, 3), base_ori)
|
||||||
|
|
||||||
# 2. Lattice Straights
|
|
||||||
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]
|
||||||
|
|
@ -316,9 +238,7 @@ class AStarRouter:
|
||||||
res = self._move_cache[abs_key]
|
res = self._move_cache[abs_key]
|
||||||
else:
|
else:
|
||||||
# Level 2: Relative cache (orientation only)
|
# Level 2: Relative cache (orientation only)
|
||||||
# Dilation is now 0.0 for caching to save translation time.
|
rel_key = (base_ori, 'S', length, net_width, self._self_dilation)
|
||||||
# It will be calculated lazily in _add_node if needed.
|
|
||||||
rel_key = (base_ori, 'S', length, net_width, 0.0)
|
|
||||||
if rel_key in self._move_cache:
|
if rel_key in self._move_cache:
|
||||||
res_rel = self._move_cache[rel_key]
|
res_rel = self._move_cache[rel_key]
|
||||||
# Check closed set before translating
|
# Check closed set before translating
|
||||||
|
|
@ -329,7 +249,7 @@ class AStarRouter:
|
||||||
continue
|
continue
|
||||||
res = res_rel.translate(cp.x, cp.y)
|
res = res_rel.translate(cp.x, cp.y)
|
||||||
else:
|
else:
|
||||||
res_rel = Straight.generate(Port(0, 0, base_ori), length, net_width, dilation=0.0)
|
res_rel = Straight.generate(Port(0, 0, base_ori), length, net_width, dilation=self._self_dilation)
|
||||||
self._move_cache[rel_key] = res_rel
|
self._move_cache[rel_key] = res_rel
|
||||||
res = res_rel.translate(cp.x, cp.y)
|
res = res_rel.translate(cp.x, cp.y)
|
||||||
self._move_cache[abs_key] = res
|
self._move_cache[abs_key] = res
|
||||||
|
|
@ -342,7 +262,7 @@ class AStarRouter:
|
||||||
if abs_key in self._move_cache:
|
if abs_key in self._move_cache:
|
||||||
res = self._move_cache[abs_key]
|
res = self._move_cache[abs_key]
|
||||||
else:
|
else:
|
||||||
rel_key = (base_ori, 'B', radius, direction, net_width, self.config.bend_collision_type, 0.0)
|
rel_key = (base_ori, 'B', radius, direction, net_width, self.config.bend_collision_type, self._self_dilation)
|
||||||
if rel_key in self._move_cache:
|
if rel_key in self._move_cache:
|
||||||
res_rel = self._move_cache[rel_key]
|
res_rel = self._move_cache[rel_key]
|
||||||
# Check closed set before translating
|
# Check closed set before translating
|
||||||
|
|
@ -360,7 +280,7 @@ class AStarRouter:
|
||||||
direction,
|
direction,
|
||||||
collision_type=self.config.bend_collision_type,
|
collision_type=self.config.bend_collision_type,
|
||||||
clip_margin=self.config.bend_clip_margin,
|
clip_margin=self.config.bend_clip_margin,
|
||||||
dilation=0.0
|
dilation=self._self_dilation
|
||||||
)
|
)
|
||||||
self._move_cache[rel_key] = res_rel
|
self._move_cache[rel_key] = res_rel
|
||||||
res = res_rel.translate(cp.x, cp.y)
|
res = res_rel.translate(cp.x, cp.y)
|
||||||
|
|
@ -374,7 +294,7 @@ class AStarRouter:
|
||||||
if abs_key in self._move_cache:
|
if abs_key in self._move_cache:
|
||||||
res = self._move_cache[abs_key]
|
res = self._move_cache[abs_key]
|
||||||
else:
|
else:
|
||||||
rel_key = (base_ori, 'SB', offset, radius, net_width, self.config.bend_collision_type, 0.0)
|
rel_key = (base_ori, 'SB', offset, radius, net_width, self.config.bend_collision_type, self._self_dilation)
|
||||||
if rel_key in self._move_cache:
|
if rel_key in self._move_cache:
|
||||||
res_rel = self._move_cache[rel_key]
|
res_rel = self._move_cache[rel_key]
|
||||||
# Check closed set before translating
|
# Check closed set before translating
|
||||||
|
|
@ -393,7 +313,7 @@ class AStarRouter:
|
||||||
width=net_width,
|
width=net_width,
|
||||||
collision_type=self.config.bend_collision_type,
|
collision_type=self.config.bend_collision_type,
|
||||||
clip_margin=self.config.bend_clip_margin,
|
clip_margin=self.config.bend_clip_margin,
|
||||||
dilation=0.0
|
dilation=self._self_dilation
|
||||||
)
|
)
|
||||||
self._move_cache[rel_key] = res_rel
|
self._move_cache[rel_key] = res_rel
|
||||||
res = res_rel.translate(cp.x, cp.y)
|
res = res_rel.translate(cp.x, cp.y)
|
||||||
|
|
@ -425,22 +345,17 @@ class AStarRouter:
|
||||||
round(parent.port.orientation, 2),
|
round(parent.port.orientation, 2),
|
||||||
move_type,
|
move_type,
|
||||||
net_width,
|
net_width,
|
||||||
net_id,
|
|
||||||
)
|
)
|
||||||
if cache_key in self._collision_cache:
|
if cache_key in self._collision_cache:
|
||||||
if self._collision_cache[cache_key]:
|
if self._collision_cache[cache_key]:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
# Lazy Dilation: compute dilated polygons only if we need a collision check
|
# Ensure dilated geometry is present for collision check
|
||||||
if result.dilated_geometry is None:
|
if result.dilated_geometry is None:
|
||||||
# We need to update the ComponentResult with dilated geometry
|
dilation = self._self_dilation
|
||||||
# For simplicity, we'll just buffer the polygons here.
|
result.dilated_geometry = [p.buffer(dilation) for p in result.geometry]
|
||||||
# In a more optimized version, ComponentResult might have a .dilate() method.
|
|
||||||
dilated = [p.buffer(self._self_dilation) for p in result.geometry]
|
|
||||||
result.dilated_geometry = dilated
|
|
||||||
# Re-calculate dilated bounds
|
|
||||||
import shapely
|
import shapely
|
||||||
result.dilated_bounds = shapely.bounds(dilated)
|
result.dilated_bounds = shapely.bounds(result.dilated_geometry)
|
||||||
|
|
||||||
hard_coll = False
|
hard_coll = False
|
||||||
for i, poly in enumerate(result.geometry):
|
for i, poly in enumerate(result.geometry):
|
||||||
|
|
@ -455,23 +370,21 @@ class AStarRouter:
|
||||||
if hard_coll:
|
if hard_coll:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Lazy Dilation for self-intersection and cost evaluation
|
# Ensure dilated geometry is present for self-intersection (if enabled) and cost evaluation
|
||||||
if result.dilated_geometry is None:
|
if result.dilated_geometry is None:
|
||||||
dilated = [p.buffer(self._self_dilation) for p in result.geometry]
|
dilation = self._self_dilation
|
||||||
result.dilated_geometry = dilated
|
result.dilated_geometry = [p.buffer(dilation) for p in result.geometry]
|
||||||
import shapely
|
import shapely
|
||||||
result.dilated_bounds = shapely.bounds(dilated)
|
result.dilated_bounds = shapely.bounds(result.dilated_geometry)
|
||||||
|
|
||||||
# 3. Check for Self-Intersection (Limited to last 100 segments for performance)
|
# 3. Check for Self-Intersection (Limited to last 50 segments for performance)
|
||||||
if result.dilated_geometry:
|
if result.dilated_geometry is not None:
|
||||||
# Union of current move's bounds for fast path-wide pruning
|
# Union of current move's bounds for fast path-wide pruning
|
||||||
m_minx, m_miny, m_maxx, m_maxy = 1e15, 1e15, -1e15, -1e15
|
b = result.dilated_bounds if result.dilated_bounds is not None else result.bounds
|
||||||
for b in result.dilated_bounds if result.dilated_bounds is not None else result.bounds:
|
m_minx = numpy.min(b[:, 0])
|
||||||
|
m_miny = numpy.min(b[:, 1])
|
||||||
m_minx = min(m_minx, b[0])
|
m_maxx = numpy.max(b[:, 2])
|
||||||
m_miny = min(m_miny, b[1])
|
m_maxy = numpy.max(b[:, 3])
|
||||||
m_maxx = max(m_maxx, b[2])
|
|
||||||
m_maxy = max(m_maxy, b[3])
|
|
||||||
|
|
||||||
# If current move doesn't overlap the entire parent path bbox, we can skip individual checks
|
# If current move doesn't overlap the entire parent path bbox, we can skip individual checks
|
||||||
# (Except the immediate parent which we usually skip anyway)
|
# (Except the immediate parent which we usually skip anyway)
|
||||||
|
|
@ -484,7 +397,7 @@ class AStarRouter:
|
||||||
dm_bounds = result.dilated_bounds[dm_idx]
|
dm_bounds = result.dilated_bounds[dm_idx]
|
||||||
curr_p: AStarNode | None = parent
|
curr_p: AStarNode | None = parent
|
||||||
seg_idx = 0
|
seg_idx = 0
|
||||||
while curr_p and curr_p.component_result and seg_idx < 100:
|
while curr_p and curr_p.component_result and seg_idx < 50:
|
||||||
# Skip immediate parent to avoid tangent/port-safety issues
|
# Skip immediate parent to avoid tangent/port-safety issues
|
||||||
if seg_idx > 0:
|
if seg_idx > 0:
|
||||||
res_p = curr_p.component_result
|
res_p = curr_p.component_result
|
||||||
|
|
@ -506,6 +419,12 @@ class AStarRouter:
|
||||||
seg_idx += 1
|
seg_idx += 1
|
||||||
|
|
||||||
|
|
||||||
|
penalty = 0.0
|
||||||
|
if 'SB' in move_type:
|
||||||
|
penalty = self.config.sbend_penalty
|
||||||
|
elif 'B' in move_type:
|
||||||
|
penalty = self.config.bend_penalty
|
||||||
|
|
||||||
move_cost = self.cost_evaluator.evaluate_move(
|
move_cost = self.cost_evaluator.evaluate_move(
|
||||||
result.geometry,
|
result.geometry,
|
||||||
result.end_port,
|
result.end_port,
|
||||||
|
|
@ -514,7 +433,7 @@ class AStarRouter:
|
||||||
start_port=parent.port,
|
start_port=parent.port,
|
||||||
length=result.length,
|
length=result.length,
|
||||||
dilated_geometry=result.dilated_geometry,
|
dilated_geometry=result.dilated_geometry,
|
||||||
skip_static=True
|
penalty=penalty
|
||||||
)
|
)
|
||||||
|
|
||||||
if move_cost > 1e12:
|
if move_cost > 1e12:
|
||||||
|
|
@ -523,15 +442,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:
|
||||||
penalty_factor = ref_radius / move_radius
|
# Scale cost to favor larger radius bends if they fit
|
||||||
move_cost += self.config.bend_penalty * penalty_factor
|
move_cost *= (ref_radius / move_radius)**0.5
|
||||||
elif 'SB' in move_type and move_radius is not None:
|
|
||||||
penalty_factor = ref_radius / move_radius
|
|
||||||
move_cost += self.config.sbend_penalty * penalty_factor
|
|
||||||
elif 'B' in move_type:
|
|
||||||
move_cost += self.config.bend_penalty
|
|
||||||
elif 'SB' in move_type:
|
|
||||||
move_cost += self.config.sbend_penalty
|
|
||||||
|
|
||||||
g_cost = parent.g_cost + move_cost
|
g_cost = parent.g_cost + move_cost
|
||||||
h_cost = self.cost_evaluator.h_manhattan(result.end_port, target)
|
h_cost = self.cost_evaluator.h_manhattan(result.end_port, target)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ class RouterConfig:
|
||||||
sbend_radii: list[float] = field(default_factory=lambda: [10.0])
|
sbend_radii: list[float] = field(default_factory=lambda: [10.0])
|
||||||
snap_to_target_dist: float = 20.0
|
snap_to_target_dist: float = 20.0
|
||||||
bend_penalty: float = 50.0
|
bend_penalty: float = 50.0
|
||||||
sbend_penalty: float = 100.0
|
sbend_penalty: float = 150.0
|
||||||
bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | Any = "arc"
|
bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | Any = "arc"
|
||||||
bend_clip_margin: float = 10.0
|
bend_clip_margin: float = 10.0
|
||||||
|
|
||||||
|
|
@ -29,3 +29,4 @@ class CostConfig:
|
||||||
greedy_h_weight: float = 1.1
|
greedy_h_weight: float = 1.1
|
||||||
congestion_penalty: float = 10000.0
|
congestion_penalty: float = 10000.0
|
||||||
bend_penalty: float = 50.0
|
bend_penalty: float = 50.0
|
||||||
|
sbend_penalty: float = 150.0
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ class CostEvaluator:
|
||||||
greedy_h_weight: float = 1.1,
|
greedy_h_weight: float = 1.1,
|
||||||
congestion_penalty: float = 10000.0,
|
congestion_penalty: float = 10000.0,
|
||||||
bend_penalty: float = 50.0,
|
bend_penalty: float = 50.0,
|
||||||
|
sbend_penalty: float = 150.0,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Initialize the Cost Evaluator.
|
Initialize the Cost Evaluator.
|
||||||
|
|
@ -51,6 +52,7 @@ class CostEvaluator:
|
||||||
greedy_h_weight: Heuristic weighting (A* greedy factor).
|
greedy_h_weight: Heuristic weighting (A* greedy factor).
|
||||||
congestion_penalty: Multiplier for path overlaps in negotiated congestion.
|
congestion_penalty: Multiplier for path overlaps in negotiated congestion.
|
||||||
bend_penalty: Base cost for 90-degree bends.
|
bend_penalty: Base cost for 90-degree bends.
|
||||||
|
sbend_penalty: Base cost for parametric S-bends.
|
||||||
"""
|
"""
|
||||||
self.collision_engine = collision_engine
|
self.collision_engine = collision_engine
|
||||||
self.danger_map = danger_map
|
self.danger_map = danger_map
|
||||||
|
|
@ -59,6 +61,7 @@ class CostEvaluator:
|
||||||
greedy_h_weight=greedy_h_weight,
|
greedy_h_weight=greedy_h_weight,
|
||||||
congestion_penalty=congestion_penalty,
|
congestion_penalty=congestion_penalty,
|
||||||
bend_penalty=bend_penalty,
|
bend_penalty=bend_penalty,
|
||||||
|
sbend_penalty=sbend_penalty,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use config values
|
# Use config values
|
||||||
|
|
@ -102,8 +105,7 @@ class CostEvaluator:
|
||||||
# 90-degree turn cost: radius 10 -> ~15.7 um + penalty
|
# 90-degree turn cost: radius 10 -> ~15.7 um + penalty
|
||||||
penalty += 15.7 + self.config.bend_penalty
|
penalty += 15.7 + self.config.bend_penalty
|
||||||
|
|
||||||
# Add 1.5 multiplier for greediness (faster search)
|
return self.greedy_h_weight * (dist + penalty)
|
||||||
return 1.5 * (dist + penalty)
|
|
||||||
|
|
||||||
|
|
||||||
def evaluate_move(
|
def evaluate_move(
|
||||||
|
|
@ -116,6 +118,7 @@ class CostEvaluator:
|
||||||
length: float = 0.0,
|
length: float = 0.0,
|
||||||
dilated_geometry: list[Polygon] | None = None,
|
dilated_geometry: list[Polygon] | None = None,
|
||||||
skip_static: bool = False,
|
skip_static: bool = False,
|
||||||
|
penalty: float = 0.0,
|
||||||
) -> float:
|
) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate the cost of a single move (Straight, Bend, SBend).
|
Calculate the cost of a single move (Straight, Bend, SBend).
|
||||||
|
|
@ -129,12 +132,13 @@ class CostEvaluator:
|
||||||
length: Physical path length of the move.
|
length: Physical path length of the move.
|
||||||
dilated_geometry: Pre-calculated dilated polygons.
|
dilated_geometry: Pre-calculated dilated polygons.
|
||||||
skip_static: If True, bypass static collision checks (e.g. if already done).
|
skip_static: If True, bypass static collision checks (e.g. if already done).
|
||||||
|
penalty: Fixed cost penalty for the move type (bend, sbend).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Total cost of the move, or 1e15 if invalid.
|
Total cost of the move, or 1e15 if invalid.
|
||||||
"""
|
"""
|
||||||
_ = net_width # Unused
|
_ = net_width # Unused
|
||||||
total_cost = length * self.unit_length_cost
|
total_cost = length * self.unit_length_cost + penalty
|
||||||
|
|
||||||
# 1. Boundary Check
|
# 1. Boundary Check
|
||||||
if not self.danger_map.is_within_bounds(end_port.x, end_port.y):
|
if not self.danger_map.is_within_bounds(end_port.x, end_port.y):
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ class PathFinder:
|
||||||
"""
|
"""
|
||||||
Multi-net router using Negotiated Congestion.
|
Multi-net router using Negotiated Congestion.
|
||||||
"""
|
"""
|
||||||
__slots__ = ('router', 'cost_evaluator', 'max_iterations', 'base_congestion_penalty')
|
__slots__ = ('router', 'cost_evaluator', 'max_iterations', 'base_congestion_penalty', 'use_tiered_strategy')
|
||||||
|
|
||||||
router: AStarRouter
|
router: AStarRouter
|
||||||
""" The A* search engine """
|
""" The A* search engine """
|
||||||
|
|
@ -50,12 +50,16 @@ class PathFinder:
|
||||||
base_congestion_penalty: float
|
base_congestion_penalty: float
|
||||||
""" Starting penalty for overlaps """
|
""" Starting penalty for overlaps """
|
||||||
|
|
||||||
|
use_tiered_strategy: bool
|
||||||
|
""" If True, use simpler collision models in early iterations for speed """
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
router: AStarRouter,
|
router: AStarRouter,
|
||||||
cost_evaluator: CostEvaluator,
|
cost_evaluator: CostEvaluator,
|
||||||
max_iterations: int = 10,
|
max_iterations: int = 10,
|
||||||
base_congestion_penalty: float = 100.0,
|
base_congestion_penalty: float = 100.0,
|
||||||
|
use_tiered_strategy: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Initialize the PathFinder.
|
Initialize the PathFinder.
|
||||||
|
|
@ -65,11 +69,13 @@ class PathFinder:
|
||||||
cost_evaluator: The evaluator for path costs.
|
cost_evaluator: The evaluator for path costs.
|
||||||
max_iterations: Maximum number of rip-up and reroute iterations.
|
max_iterations: Maximum number of rip-up and reroute iterations.
|
||||||
base_congestion_penalty: Starting penalty for overlaps.
|
base_congestion_penalty: Starting penalty for overlaps.
|
||||||
|
use_tiered_strategy: Whether to use simplified collision models in early iterations.
|
||||||
"""
|
"""
|
||||||
self.router = router
|
self.router = router
|
||||||
self.cost_evaluator = cost_evaluator
|
self.cost_evaluator = cost_evaluator
|
||||||
self.max_iterations = max_iterations
|
self.max_iterations = max_iterations
|
||||||
self.base_congestion_penalty = base_congestion_penalty
|
self.base_congestion_penalty = base_congestion_penalty
|
||||||
|
self.use_tiered_strategy = use_tiered_strategy
|
||||||
|
|
||||||
def route_all(
|
def route_all(
|
||||||
self,
|
self,
|
||||||
|
|
@ -111,9 +117,11 @@ class PathFinder:
|
||||||
self.cost_evaluator.collision_engine.remove_path(net_id)
|
self.cost_evaluator.collision_engine.remove_path(net_id)
|
||||||
|
|
||||||
# 2. Reroute with current congestion info
|
# 2. Reroute with current congestion info
|
||||||
# Tiered Strategy: use clipped_bbox for Iteration 0 for speed.
|
# Tiered Strategy: use clipped_bbox for Iteration 0 for speed if target is arc.
|
||||||
# Switch to arc for higher iterations if collisions persist.
|
target_coll_model = self.router.config.bend_collision_type
|
||||||
coll_model = "clipped_bbox" if iteration == 0 else "arc"
|
coll_model = target_coll_model
|
||||||
|
if self.use_tiered_strategy and iteration == 0 and target_coll_model == "arc":
|
||||||
|
coll_model = "clipped_bbox"
|
||||||
|
|
||||||
net_start = time.monotonic()
|
net_start = time.monotonic()
|
||||||
path = self.router.route(start, target, width, net_id=net_id, bend_collision_type=coll_model)
|
path = self.router.route(start, target, width, net_id=net_id, bend_collision_type=coll_model)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ def plot_routing_results(
|
||||||
static_obstacles: list[Polygon],
|
static_obstacles: list[Polygon],
|
||||||
bounds: tuple[float, float, float, float],
|
bounds: tuple[float, float, float, float],
|
||||||
netlist: dict[str, tuple[Port, Port]] | None = None,
|
netlist: dict[str, tuple[Port, Port]] | None = None,
|
||||||
|
show_actual: bool = True,
|
||||||
) -> tuple[Figure, Axes]:
|
) -> tuple[Figure, Axes]:
|
||||||
"""
|
"""
|
||||||
Plot obstacles and routed paths using matplotlib.
|
Plot obstacles and routed paths using matplotlib.
|
||||||
|
|
@ -27,30 +28,30 @@ def plot_routing_results(
|
||||||
static_obstacles: List of static obstacle polygons.
|
static_obstacles: List of static obstacle polygons.
|
||||||
bounds: Plot limits (minx, miny, maxx, maxy).
|
bounds: Plot limits (minx, miny, maxx, maxy).
|
||||||
netlist: Optional original netlist for port visualization.
|
netlist: Optional original netlist for port visualization.
|
||||||
|
show_actual: If True, overlay high-fidelity geometry if available.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The matplotlib Figure and Axes objects.
|
The matplotlib Figure and Axes objects.
|
||||||
"""
|
"""
|
||||||
fig, ax = plt.subplots(figsize=(10, 10))
|
fig, ax = plt.subplots(figsize=(12, 12))
|
||||||
|
|
||||||
# Plot static obstacles (gray)
|
# Plot static obstacles (gray)
|
||||||
for poly in static_obstacles:
|
for poly in static_obstacles:
|
||||||
x, y = poly.exterior.xy
|
x, y = poly.exterior.xy
|
||||||
ax.fill(x, y, alpha=0.5, fc="gray", ec="black")
|
ax.fill(x, y, alpha=0.3, fc="gray", ec="black", zorder=1)
|
||||||
|
|
||||||
# Plot paths
|
# Plot paths
|
||||||
colors = plt.get_cmap("tab10")
|
colors = plt.get_cmap("tab20")
|
||||||
for i, (net_id, res) in enumerate(results.items()):
|
for i, (net_id, res) in enumerate(results.items()):
|
||||||
# Use modulo to avoid index out of range for many nets
|
color: str | tuple[float, ...] = colors(i % 20)
|
||||||
color: str | tuple[float, ...] = colors(i % 10)
|
|
||||||
if not res.is_valid:
|
if not res.is_valid:
|
||||||
color = "red" # Highlight failing nets
|
color = "red"
|
||||||
|
|
||||||
label_added = False
|
label_added = False
|
||||||
for _j, comp in enumerate(res.path):
|
for comp in res.path:
|
||||||
# 1. Plot geometry
|
# 1. Plot Collision Geometry (Translucent fill)
|
||||||
|
# This is the geometry used during search (e.g. proxy or arc)
|
||||||
for poly in comp.geometry:
|
for poly in comp.geometry:
|
||||||
# Handle both Polygon and MultiPolygon (e.g. from SBend)
|
|
||||||
if isinstance(poly, MultiPolygon):
|
if isinstance(poly, MultiPolygon):
|
||||||
geoms = list(poly.geoms)
|
geoms = list(poly.geoms)
|
||||||
else:
|
else:
|
||||||
|
|
@ -58,31 +59,52 @@ def plot_routing_results(
|
||||||
|
|
||||||
for g in geoms:
|
for g in geoms:
|
||||||
x, y = g.exterior.xy
|
x, y = g.exterior.xy
|
||||||
ax.fill(x, y, alpha=0.7, fc=color, ec="black", label=net_id if not label_added else "")
|
ax.fill(x, y, alpha=0.15, fc=color, ec=color, linestyle='--', lw=0.5, zorder=2)
|
||||||
|
|
||||||
|
# 2. Plot "Actual" Geometry (The high-fidelity shape used for fabrication)
|
||||||
|
# Use comp.actual_geometry if it exists (should be the arc)
|
||||||
|
actual_geoms_to_plot = comp.actual_geometry if comp.actual_geometry is not None else comp.geometry
|
||||||
|
|
||||||
|
for poly in actual_geoms_to_plot:
|
||||||
|
if isinstance(poly, MultiPolygon):
|
||||||
|
geoms = list(poly.geoms)
|
||||||
|
else:
|
||||||
|
geoms = [poly]
|
||||||
|
for g in geoms:
|
||||||
|
x, y = g.exterior.xy
|
||||||
|
ax.plot(x, y, color=color, lw=1.5, alpha=0.9, zorder=3, label=net_id if not label_added else "")
|
||||||
label_added = True
|
label_added = True
|
||||||
|
|
||||||
# 2. Plot subtle port orientation arrow for internal ports
|
# 3. Plot subtle port orientation arrow
|
||||||
p = comp.end_port
|
p = comp.end_port
|
||||||
rad = numpy.radians(p.orientation)
|
rad = numpy.radians(p.orientation)
|
||||||
u = numpy.cos(rad)
|
ax.quiver(p.x, p.y, numpy.cos(rad), numpy.sin(rad), color="black",
|
||||||
v = numpy.sin(rad)
|
scale=40, width=0.002, alpha=0.2, pivot="tail", zorder=4)
|
||||||
ax.quiver(p.x, p.y, u, v, color="black", scale=40, width=0.003, alpha=0.3, pivot="tail", zorder=4)
|
|
||||||
|
|
||||||
# 3. Plot main arrows for netlist ports (if provided)
|
if not res.path and not res.is_valid:
|
||||||
if netlist and net_id in netlist:
|
# Best-effort display: If the path is empty but failed, it might be unroutable.
|
||||||
start_p, target_p = netlist[net_id]
|
# We don't have a partial path in RoutingResult currently.
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 4. Plot main arrows for netlist ports
|
||||||
|
if netlist:
|
||||||
|
for net_id, (start_p, target_p) in netlist.items():
|
||||||
for p in [start_p, target_p]:
|
for p in [start_p, target_p]:
|
||||||
rad = numpy.radians(p.orientation)
|
rad = numpy.radians(p.orientation)
|
||||||
u = numpy.cos(rad)
|
ax.quiver(p.x, p.y, numpy.cos(rad), numpy.sin(rad), color="black",
|
||||||
v = numpy.sin(rad)
|
scale=25, width=0.004, pivot="tail", zorder=6)
|
||||||
ax.quiver(p.x, p.y, u, v, color="black", scale=25, width=0.005, pivot="tail", zorder=6)
|
|
||||||
|
|
||||||
ax.set_xlim(bounds[0], bounds[2])
|
ax.set_xlim(bounds[0], bounds[2])
|
||||||
ax.set_ylim(bounds[1], bounds[3])
|
ax.set_ylim(bounds[1], bounds[3])
|
||||||
ax.set_aspect("equal")
|
ax.set_aspect("equal")
|
||||||
ax.set_title("Inire Routing Results")
|
ax.set_title("Inire Routing Results (Dashed: Collision Proxy, Solid: Actual Geometry)")
|
||||||
handles, labels = ax.get_legend_handles_labels()
|
|
||||||
if labels:
|
# Legend handling for many nets
|
||||||
ax.legend()
|
if len(results) < 25:
|
||||||
plt.grid(True)
|
handles, labels = ax.get_legend_handles_labels()
|
||||||
|
if labels:
|
||||||
|
ax.legend(loc='upper left', bbox_to_anchor=(1, 1), fontsize='small', ncol=2)
|
||||||
|
fig.tight_layout()
|
||||||
|
|
||||||
|
plt.grid(True, which='both', linestyle=':', alpha=0.5)
|
||||||
return fig, ax
|
return fig, ax
|
||||||
|
|
|
||||||