Compare commits

..

No commits in common. "ba76589ffbe39e87438b0c2b74d859e853a97d37" and "43a9a6cb3afe9051da208315bcbbe1cc13fd43b4" have entirely different histories.

13 changed files with 57 additions and 204 deletions

25
DOCS.md
View file

@ -51,28 +51,3 @@ 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.

View file

@ -62,8 +62,6 @@ 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

View file

@ -1,3 +1,4 @@
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
@ -8,7 +9,7 @@ from inire.utils.visualization import plot_routing_results
def main() -> None: def main() -> None:
print("Running Example 02: Congestion Resolution (Triple Crossing)...") print("Running Example 02: Congestion Resolution (Crossing)...")
# 1. Setup Environment (Open space) # 1. Setup Environment (Open space)
bounds = (0, 0, 100, 100) bounds = (0, 0, 100, 100)
@ -21,24 +22,23 @@ def main() -> None:
pf = PathFinder(router, evaluator) pf = PathFinder(router, evaluator)
# 2. Define Netlist # 2. Define Netlist
# Three nets that all converge on the same central area. # Two nets that MUST cross.
# Negotiated Congestion must find non-overlapping paths for all of them. # Since crossings are illegal in single-layer routing, one net must detour around the other.
netlist = { netlist = {
"horizontal": (Port(10, 50, 0), Port(90, 50, 0)), "horizontal": (Port(10, 50, 0), Port(90, 50, 0)),
"vertical_up": (Port(45, 10, 90), Port(45, 90, 90)), "vertical": (Port(50, 10, 90), Port(50, 90, 90)),
"vertical_down": (Port(55, 90, 270), Port(55, 10, 270)),
} }
net_widths = {nid: 2.0 for nid in netlist} net_widths = {"horizontal": 2.0, "vertical": 2.0}
# 3. Route with Negotiated Congestion # 3. Route with Negotiated Congestion
# We increase the base penalty to encourage faster divergence # We increase the base penalty to encourage faster divergence
pf.base_congestion_penalty = 1000.0 pf.base_congestion_penalty = 500.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 for all nets.") print("Success! Congestion resolved (one net detoured).")
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():

View file

@ -1,3 +1,4 @@
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
@ -8,64 +9,68 @@ from inire.utils.visualization import plot_routing_results
def main() -> None: def main() -> None:
print("Running Example 03: Locked Paths (Incremental Routing - Bus Scenario)...") print("Running Example 03: Locked Paths (Incremental Routing)...")
# 1. Setup Environment # 1. Setup Environment
bounds = (0, 0, 120, 120) bounds = (0, 0, 100, 100)
engine = CollisionEngine(clearance=2.0) engine = CollisionEngine(clearance=2.0)
danger_map = DangerMap(bounds=bounds) danger_map = DangerMap(bounds=bounds)
danger_map.precompute([]) # Start with empty space danger_map.precompute([]) # No initial obstacles
evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.2) evaluator = CostEvaluator(engine, danger_map)
router = AStarRouter(evaluator, node_limit=200000) router = AStarRouter(evaluator)
pf = PathFinder(router, evaluator) pf = PathFinder(router, evaluator)
# 2. Phase 1: Route a "Bus" of 3 parallel nets # 2. Phase 1: Route a "Critical" Net
# We give them a small jog to make the locked geometry more interesting # This net gets priority and takes the best path.
netlist_p1 = { netlist_phase1 = {
"bus_0": (Port(10, 40, 0), Port(110, 45, 0)), "critical_net": (Port(10, 50, 0), Port(90, 50, 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 bus (3 nets)...") print("Phase 1: Routing critical_net...")
results_p1 = pf.route_all(netlist_p1, {nid: 2.0 for nid in netlist_p1}) results1 = pf.route_all(netlist_phase1, {"critical_net": 3.0}) # Wider trace
# Lock all Phase 1 nets if not results1["critical_net"].is_valid:
path_polys = [] print("Error: Phase 1 failed.")
for nid, res in results_p1.items(): return
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.")
# Update danger map with the newly locked geometry # 3. Lock the Critical Net
print("Updating DangerMap with locked paths...") # This converts the dynamic path into a static obstacle in the collision engine.
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)
# 3. Phase 2: Route secondary nets that must navigate around the locked bus # 4. Phase 2: Route a Secondary Net
# These nets cross the bus vertically. # This net must route *around* the locked critical_net.
netlist_p2 = { # Start and end points force a crossing path if it were straight.
"cross_left": (Port(30, 10, 90), Port(30, 110, 90)), netlist_phase2 = {
"cross_right": (Port(80, 110, 270), Port(80, 10, 270)), # Top to bottom "secondary_net": (Port(50, 10, 90), Port(50, 90, 90)),
} }
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})
# 4. Check Results print("Phase 2: Routing secondary_net around locked path...")
for nid, res in results_p2.items(): results2 = pf.route_all(netlist_phase2, {"secondary_net": 2.0})
status = "Success" if res.is_valid else "Failed"
print(f" {nid:12}: {status}, collisions={res.collisions}") if results2["secondary_net"].is_valid:
print("Success! Secondary net routed around locked path.")
else:
print("Failed to route secondary net.")
# 5. Visualize # 5. Visualize
all_results = {**results_p1, **results_p2} # Combine results and netlists for plotting
all_netlists = {**netlist_p1, **netlist_p2} all_results = {**results1, **results2}
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")

View file

@ -3,6 +3,7 @@ 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

View file

@ -1,56 +0,0 @@
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()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Before After
Before After

View file

@ -70,10 +70,6 @@ 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)
@ -81,4 +77,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 1e15 # Outside bounds is impossible return 1e6 # Outside bounds is expensive

View file

@ -1,9 +1,7 @@
import numpy as np
import pytest import pytest
from shapely.geometry import Point
from inire.geometry.components import Bend90, SBend, Straight from inire.geometry.components import Bend90, SBend, Straight
from inire.geometry.primitives import Port, rotate_port, translate_port from inire.geometry.primitives import Port
def test_straight_generation() -> None: def test_straight_generation() -> None:
@ -92,67 +90,3 @@ 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