Compare commits
3 commits
43a9a6cb3a
...
ba76589ffb
| Author | SHA1 | Date | |
|---|---|---|---|
| ba76589ffb | |||
| 4cbd15bc0d | |||
| 556241bae3 |
25
DOCS.md
|
|
@ -51,3 +51,28 @@ The `CostEvaluator` defines the "goodness" of a path.
|
||||||
- **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**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Best Practices & Tuning Advice
|
||||||
|
|
||||||
|
### Speed vs. Optimality
|
||||||
|
The `greedy_h_weight` is your primary lever for search performance.
|
||||||
|
- **`1.0`**: Dijkstra-like behavior. Guarantees the shortest path but is very slow.
|
||||||
|
- **`1.1` to `1.2`**: Recommended range. Balances wire length with fast convergence.
|
||||||
|
- **`> 1.5`**: Extremely fast "greedy" search. May produce zig-zags or suboptimal detours.
|
||||||
|
|
||||||
|
### Avoiding "Zig-Zags"
|
||||||
|
If the router produces many small bends instead of a long straight line:
|
||||||
|
1. Increase `bend_penalty` (e.g., set to `100.0` or higher).
|
||||||
|
2. Ensure `straight_lengths` includes larger values like `25.0` or `100.0`.
|
||||||
|
3. Decrease `greedy_h_weight` closer to `1.0`.
|
||||||
|
|
||||||
|
### Handling Congestion
|
||||||
|
In multi-net designs, if nets are overlapping:
|
||||||
|
1. Increase `congestion_penalty` in `CostEvaluator`.
|
||||||
|
2. Increase `max_iterations` in `PathFinder`.
|
||||||
|
3. If a solution is still not found, check if the `clearance` is physically possible given the design's narrowest bottlenecks.
|
||||||
|
|
||||||
|
### S-Bend Usage
|
||||||
|
Parametric S-bends are triggered by the `sbend_offsets` list. If you need a specific lateral shift (e.g., 5.86µm for a 45° switchover), add it to `sbend_offsets`. The router will only use an S-bend if it can reach a state that is exactly on the lattice or the target.
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,8 @@ Check the `examples/` directory for ready-to-run scripts demonstrating core feat
|
||||||
* **`examples/01_simple_route.py`**: Basic single-net routing with visualization.
|
* **`examples/01_simple_route.py`**: Basic single-net routing with visualization.
|
||||||
* **`examples/02_congestion_resolution.py`**: Multi-net routing resolving bottlenecks using Negotiated Congestion.
|
* **`examples/02_congestion_resolution.py`**: Multi-net routing resolving bottlenecks using Negotiated Congestion.
|
||||||
* **`examples/03_locked_paths.py`**: Incremental workflow using `lock_net()` to route around previously fixed paths.
|
* **`examples/03_locked_paths.py`**: Incremental workflow using `lock_net()` to route around previously fixed paths.
|
||||||
|
* **`examples/04_sbends_and_radii.py`**: Complex paths using parametric S-bends and multiple bend radii.
|
||||||
|
* **`examples/05_orientation_stress.py`**: Stress test for various port orientation combinations (U-turns, opposite directions).
|
||||||
|
|
||||||
Run an example:
|
Run an example:
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
from inire.geometry.collision import CollisionEngine
|
from inire.geometry.collision import CollisionEngine
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
from inire.router.astar import AStarRouter
|
from inire.router.astar import AStarRouter
|
||||||
|
|
@ -9,7 +8,7 @@ from inire.utils.visualization import plot_routing_results
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
print("Running Example 02: Congestion Resolution (Crossing)...")
|
print("Running Example 02: Congestion Resolution (Triple Crossing)...")
|
||||||
|
|
||||||
# 1. Setup Environment (Open space)
|
# 1. Setup Environment (Open space)
|
||||||
bounds = (0, 0, 100, 100)
|
bounds = (0, 0, 100, 100)
|
||||||
|
|
@ -22,23 +21,24 @@ def main() -> None:
|
||||||
pf = PathFinder(router, evaluator)
|
pf = PathFinder(router, evaluator)
|
||||||
|
|
||||||
# 2. Define Netlist
|
# 2. Define Netlist
|
||||||
# Two nets that MUST cross.
|
# Three nets that all converge on the same central area.
|
||||||
# Since crossings are illegal in single-layer routing, one net must detour around the other.
|
# Negotiated Congestion must find non-overlapping paths for all of them.
|
||||||
netlist = {
|
netlist = {
|
||||||
"horizontal": (Port(10, 50, 0), Port(90, 50, 0)),
|
"horizontal": (Port(10, 50, 0), Port(90, 50, 0)),
|
||||||
"vertical": (Port(50, 10, 90), Port(50, 90, 90)),
|
"vertical_up": (Port(45, 10, 90), Port(45, 90, 90)),
|
||||||
|
"vertical_down": (Port(55, 90, 270), Port(55, 10, 270)),
|
||||||
}
|
}
|
||||||
net_widths = {"horizontal": 2.0, "vertical": 2.0}
|
net_widths = {nid: 2.0 for nid in netlist}
|
||||||
|
|
||||||
# 3. Route with Negotiated Congestion
|
# 3. Route with Negotiated Congestion
|
||||||
# We increase the base penalty to encourage faster divergence
|
# We increase the base penalty to encourage faster divergence
|
||||||
pf.base_congestion_penalty = 500.0
|
pf.base_congestion_penalty = 1000.0
|
||||||
results = pf.route_all(netlist, net_widths)
|
results = pf.route_all(netlist, net_widths)
|
||||||
|
|
||||||
# 4. Check Results
|
# 4. Check Results
|
||||||
all_valid = all(r.is_valid for r in results.values())
|
all_valid = all(r.is_valid for r in results.values())
|
||||||
if all_valid:
|
if all_valid:
|
||||||
print("Success! Congestion resolved (one net detoured).")
|
print("Success! Congestion resolved for all nets.")
|
||||||
else:
|
else:
|
||||||
print("Some nets failed or have collisions.")
|
print("Some nets failed or have collisions.")
|
||||||
for nid, res in results.items():
|
for nid, res in results.items():
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
from inire.geometry.collision import CollisionEngine
|
from inire.geometry.collision import CollisionEngine
|
||||||
from inire.geometry.primitives import Port
|
from inire.geometry.primitives import Port
|
||||||
from inire.router.astar import AStarRouter
|
from inire.router.astar import AStarRouter
|
||||||
|
|
@ -9,68 +8,64 @@ from inire.utils.visualization import plot_routing_results
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
print("Running Example 03: Locked Paths (Incremental Routing)...")
|
print("Running Example 03: Locked Paths (Incremental Routing - Bus Scenario)...")
|
||||||
|
|
||||||
# 1. Setup Environment
|
# 1. Setup Environment
|
||||||
bounds = (0, 0, 100, 100)
|
bounds = (0, 0, 120, 120)
|
||||||
engine = CollisionEngine(clearance=2.0)
|
engine = CollisionEngine(clearance=2.0)
|
||||||
danger_map = DangerMap(bounds=bounds)
|
danger_map = DangerMap(bounds=bounds)
|
||||||
danger_map.precompute([]) # No initial obstacles
|
danger_map.precompute([]) # Start with empty space
|
||||||
|
|
||||||
evaluator = CostEvaluator(engine, danger_map)
|
evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.2)
|
||||||
router = AStarRouter(evaluator)
|
router = AStarRouter(evaluator, node_limit=200000)
|
||||||
pf = PathFinder(router, evaluator)
|
pf = PathFinder(router, evaluator)
|
||||||
|
|
||||||
# 2. Phase 1: Route a "Critical" Net
|
# 2. Phase 1: Route a "Bus" of 3 parallel nets
|
||||||
# This net gets priority and takes the best path.
|
# We give them a small jog to make the locked geometry more interesting
|
||||||
netlist_phase1 = {
|
netlist_p1 = {
|
||||||
"critical_net": (Port(10, 50, 0), Port(90, 50, 0)),
|
"bus_0": (Port(10, 40, 0), Port(110, 45, 0)),
|
||||||
|
"bus_1": (Port(10, 50, 0), Port(110, 55, 0)),
|
||||||
|
"bus_2": (Port(10, 60, 0), Port(110, 65, 0)),
|
||||||
}
|
}
|
||||||
print("Phase 1: Routing critical_net...")
|
print("Phase 1: Routing bus (3 nets)...")
|
||||||
results1 = pf.route_all(netlist_phase1, {"critical_net": 3.0}) # Wider trace
|
results_p1 = pf.route_all(netlist_p1, {nid: 2.0 for nid in netlist_p1})
|
||||||
|
|
||||||
if not results1["critical_net"].is_valid:
|
# Lock all Phase 1 nets
|
||||||
print("Error: Phase 1 failed.")
|
path_polys = []
|
||||||
return
|
for nid, res in results_p1.items():
|
||||||
|
if res.is_valid:
|
||||||
|
print(f" Locking {nid}...")
|
||||||
|
engine.lock_net(nid)
|
||||||
|
path_polys.extend([p for comp in res.path for p in comp.geometry])
|
||||||
|
else:
|
||||||
|
print(f" Warning: {nid} failed to route correctly.")
|
||||||
|
|
||||||
# 3. Lock the Critical Net
|
# Update danger map with the newly locked geometry
|
||||||
# This converts the dynamic path into a static obstacle in the collision engine.
|
print("Updating DangerMap with locked paths...")
|
||||||
print("Locking critical_net...")
|
|
||||||
engine.lock_net("critical_net")
|
|
||||||
|
|
||||||
# Update danger map to reflect the new obstacle (optional but recommended for heuristics)
|
|
||||||
# Extract polygons from result
|
|
||||||
path_polys = [p for comp in results1["critical_net"].path for p in comp.geometry]
|
|
||||||
danger_map.precompute(path_polys)
|
danger_map.precompute(path_polys)
|
||||||
|
|
||||||
# 4. Phase 2: Route a Secondary Net
|
# 3. Phase 2: Route secondary nets that must navigate around the locked bus
|
||||||
# This net must route *around* the locked critical_net.
|
# These nets cross the bus vertically.
|
||||||
# Start and end points force a crossing path if it were straight.
|
netlist_p2 = {
|
||||||
netlist_phase2 = {
|
"cross_left": (Port(30, 10, 90), Port(30, 110, 90)),
|
||||||
"secondary_net": (Port(50, 10, 90), Port(50, 90, 90)),
|
"cross_right": (Port(80, 110, 270), Port(80, 10, 270)), # Top to bottom
|
||||||
}
|
}
|
||||||
|
|
||||||
|
print("Phase 2: Routing crossing nets around locked bus...")
|
||||||
|
# We use a slightly different width for variety
|
||||||
|
results_p2 = pf.route_all(netlist_p2, {nid: 1.5 for nid in netlist_p2})
|
||||||
|
|
||||||
print("Phase 2: Routing secondary_net around locked path...")
|
# 4. Check Results
|
||||||
results2 = pf.route_all(netlist_phase2, {"secondary_net": 2.0})
|
for nid, res in results_p2.items():
|
||||||
|
status = "Success" if res.is_valid else "Failed"
|
||||||
if results2["secondary_net"].is_valid:
|
print(f" {nid:12}: {status}, collisions={res.collisions}")
|
||||||
print("Success! Secondary net routed around locked path.")
|
|
||||||
else:
|
|
||||||
print("Failed to route secondary net.")
|
|
||||||
|
|
||||||
# 5. Visualize
|
# 5. Visualize
|
||||||
# Combine results and netlists for plotting
|
all_results = {**results_p1, **results_p2}
|
||||||
all_results = {**results1, **results2}
|
all_netlists = {**netlist_p1, **netlist_p2}
|
||||||
all_netlists = {**netlist_phase1, **netlist_phase2}
|
|
||||||
|
|
||||||
# Note: 'critical_net' is now in engine.static_obstacles internally,
|
|
||||||
# but for visualization we plot it from the result object to see it clearly.
|
|
||||||
# We pass an empty list for 'static_obstacles' to plot_routing_results
|
|
||||||
# because we want to see the path colored, not grayed out as an obstacle.
|
|
||||||
|
|
||||||
fig, ax = plot_routing_results(all_results, [], bounds, netlist=all_netlists)
|
fig, ax = plot_routing_results(all_results, [], bounds, netlist=all_netlists)
|
||||||
fig.savefig("examples/locked.png")
|
fig.savefig("examples/locked.png")
|
||||||
|
|
||||||
print("Saved plot to examples/locked.png")
|
print("Saved plot to examples/locked.png")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ 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
|
||||||
from inire.router.astar import AStarRouter
|
from inire.router.astar import AStarRouter
|
||||||
from inire.router.config import CostConfig, RouterConfig
|
|
||||||
from inire.router.cost import CostEvaluator
|
from inire.router.cost import CostEvaluator
|
||||||
from inire.router.danger_map import DangerMap
|
from inire.router.danger_map import DangerMap
|
||||||
from inire.router.pathfinder import PathFinder
|
from inire.router.pathfinder import PathFinder
|
||||||
|
|
|
||||||
56
examples/05_orientation_stress.py
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
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 05: Orientation Stress Test...")
|
||||||
|
|
||||||
|
# 1. Setup Environment
|
||||||
|
# Give some breathing room (-20 to 120) for U-turns and flips (R=10)
|
||||||
|
bounds = (-20, -20, 120, 120)
|
||||||
|
engine = CollisionEngine(clearance=2.0)
|
||||||
|
danger_map = DangerMap(bounds=bounds)
|
||||||
|
danger_map.precompute([])
|
||||||
|
|
||||||
|
evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.1)
|
||||||
|
router = AStarRouter(evaluator, node_limit=100000)
|
||||||
|
pf = PathFinder(router, evaluator)
|
||||||
|
|
||||||
|
# 2. Define Netlist with various orientation challenges
|
||||||
|
netlist = {
|
||||||
|
# Opposite directions: requires two 90-degree bends to flip orientation
|
||||||
|
"opposite": (Port(10, 80, 0), Port(90, 80, 180)),
|
||||||
|
|
||||||
|
# 90-degree turn: standard L-shape
|
||||||
|
"turn_90": (Port(10, 60, 0), Port(40, 90, 90)),
|
||||||
|
|
||||||
|
# Output behind input: requires a full U-turn
|
||||||
|
"behind": (Port(80, 40, 0), Port(20, 40, 0)),
|
||||||
|
|
||||||
|
# Sharp return: output is behind and oriented towards the input
|
||||||
|
"return_loop": (Port(80, 20, 0), Port(40, 10, 180)),
|
||||||
|
}
|
||||||
|
net_widths = {nid: 2.0 for nid in netlist}
|
||||||
|
|
||||||
|
# 3. Route
|
||||||
|
results = pf.route_all(netlist, net_widths)
|
||||||
|
|
||||||
|
# 4. Check Results
|
||||||
|
for nid, res in results.items():
|
||||||
|
status = "Success" if res.is_valid else "Failed"
|
||||||
|
total_len = sum(comp.length for comp in res.path) if res.path else 0
|
||||||
|
print(f" {nid:12}: {status}, total_length={total_len:.1f}")
|
||||||
|
|
||||||
|
# 5. Visualize
|
||||||
|
fig, ax = plot_routing_results(results, [], bounds, netlist=netlist)
|
||||||
|
fig.savefig("examples/orientation_stress.png")
|
||||||
|
print("Saved plot to examples/orientation_stress.png")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 66 KiB |
BIN
examples/orientation_stress.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 27 KiB |
|
|
@ -70,6 +70,10 @@ class DangerMap:
|
||||||
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:
|
||||||
|
"""Check if a coordinate is within the design bounds."""
|
||||||
|
return self.minx <= x <= self.maxx and self.miny <= y <= self.maxy
|
||||||
|
|
||||||
def get_cost(self, x: float, y: float) -> float:
|
def get_cost(self, x: float, y: float) -> float:
|
||||||
"""Get the proximity cost at a specific coordinate."""
|
"""Get the proximity cost at a specific coordinate."""
|
||||||
ix = int((x - self.minx) / self.resolution)
|
ix = int((x - self.minx) / self.resolution)
|
||||||
|
|
@ -77,4 +81,4 @@ class DangerMap:
|
||||||
|
|
||||||
if 0 <= ix < self.width_cells and 0 <= iy < self.height_cells:
|
if 0 <= ix < self.width_cells and 0 <= iy < self.height_cells:
|
||||||
return float(self.grid[ix, iy])
|
return float(self.grid[ix, iy])
|
||||||
return 1e6 # Outside bounds is expensive
|
return 1e15 # Outside bounds is impossible
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
|
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
|
from inire.geometry.primitives import Port, rotate_port, translate_port
|
||||||
|
|
||||||
|
|
||||||
def test_straight_generation() -> None:
|
def test_straight_generation() -> None:
|
||||||
|
|
@ -90,3 +92,67 @@ def test_sbend_collision_models() -> None:
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
def test_sbend_continuity() -> None:
|
||||||
|
# Verify SBend endpoints and continuity math
|
||||||
|
start = Port(10, 20, 90) # Starting facing up
|
||||||
|
offset = 4.0
|
||||||
|
radius = 20.0
|
||||||
|
width = 1.0
|
||||||
|
|
||||||
|
res = SBend.generate(start, offset, radius, width)
|
||||||
|
|
||||||
|
# Target orientation should be same as start
|
||||||
|
assert abs(res.end_port.orientation - 90.0) < 1e-6
|
||||||
|
|
||||||
|
# For a port at 90 deg, +offset is a shift in -x direction
|
||||||
|
assert abs(res.end_port.x - (10.0 - offset)) < 1e-6
|
||||||
|
|
||||||
|
# Geometry should be connected (unary_union results in 1 polygon)
|
||||||
|
assert len(res.geometry) == 1
|
||||||
|
assert res.geometry[0].is_valid
|
||||||
|
|
||||||
|
|
||||||
|
def test_arc_sagitta_precision() -> None:
|
||||||
|
# Verify that requested sagitta actually controls segment count
|
||||||
|
start = Port(0, 0, 0)
|
||||||
|
radius = 100.0 # Large radius to make sagitta significant
|
||||||
|
width = 2.0
|
||||||
|
|
||||||
|
# Coarse: 1um sagitta
|
||||||
|
res_coarse = Bend90.generate(start, radius, width, sagitta=1.0)
|
||||||
|
# Fine: 0.01um (10nm) sagitta
|
||||||
|
res_fine = Bend90.generate(start, radius, width, sagitta=0.01)
|
||||||
|
|
||||||
|
# Number of segments should be significantly higher for fine
|
||||||
|
# Exterior points = (segments + 1) * 2
|
||||||
|
pts_coarse = len(res_coarse.geometry[0].exterior.coords)
|
||||||
|
pts_fine = len(res_fine.geometry[0].exterior.coords)
|
||||||
|
|
||||||
|
assert pts_fine > pts_coarse * 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_component_transform_invariance() -> None:
|
||||||
|
# Verify that generating at (0,0) then transforming
|
||||||
|
# is same as generating at the transformed port.
|
||||||
|
start0 = Port(0, 0, 0)
|
||||||
|
radius = 10.0
|
||||||
|
width = 2.0
|
||||||
|
|
||||||
|
res0 = Bend90.generate(start0, radius, width, direction="CCW")
|
||||||
|
|
||||||
|
# Transform: Translate (10, 10) then Rotate 90
|
||||||
|
dx, dy = 10.0, 5.0
|
||||||
|
angle = 90.0
|
||||||
|
|
||||||
|
# 1. Transform the generated geometry
|
||||||
|
p_end_transformed = rotate_port(translate_port(res0.end_port, dx, dy), angle)
|
||||||
|
|
||||||
|
# 2. Generate at transformed start
|
||||||
|
start_transformed = rotate_port(translate_port(start0, dx, dy), angle)
|
||||||
|
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.y - p_end_transformed.y) < 1e-6
|
||||||
|
assert abs(res_transformed.end_port.orientation - p_end_transformed.orientation) < 1e-6
|
||||||
|
|
|
||||||