Compare commits

..

3 commits

Author SHA1 Message Date
jan
ba76589ffb improve exmples 2026-03-08 23:03:07 -07:00
4cbd15bc0d add various direction examples 2026-03-08 22:57:42 -07:00
556241bae3 more bend-related work 2026-03-08 22:18:33 -07:00
13 changed files with 204 additions and 57 deletions

25
DOCS.md
View file

@ -51,3 +51,28 @@ The `CostEvaluator` defines the "goodness" of a path.
- **Coordinates**: Micrometers (µm).
- **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**.
---
## 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,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/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/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:
```bash

View file

@ -1,4 +1,3 @@
from inire.geometry.collision import CollisionEngine
from inire.geometry.primitives import Port
from inire.router.astar import AStarRouter
@ -9,7 +8,7 @@ from inire.utils.visualization import plot_routing_results
def main() -> None:
print("Running Example 02: Congestion Resolution (Crossing)...")
print("Running Example 02: Congestion Resolution (Triple Crossing)...")
# 1. Setup Environment (Open space)
bounds = (0, 0, 100, 100)
@ -22,23 +21,24 @@ def main() -> None:
pf = PathFinder(router, evaluator)
# 2. Define Netlist
# Two nets that MUST cross.
# Since crossings are illegal in single-layer routing, one net must detour around the other.
# Three nets that all converge on the same central area.
# Negotiated Congestion must find non-overlapping paths for all of them.
netlist = {
"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
# 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)
# 4. Check Results
all_valid = all(r.is_valid for r in results.values())
if all_valid:
print("Success! Congestion resolved (one net detoured).")
print("Success! Congestion resolved for all nets.")
else:
print("Some nets failed or have collisions.")
for nid, res in results.items():

View file

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

View file

@ -3,7 +3,6 @@ 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.config import CostConfig, RouterConfig
from inire.router.cost import CostEvaluator
from inire.router.danger_map import DangerMap
from inire.router.pathfinder import PathFinder

View 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()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Before After
Before After

Binary file not shown.

After

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: 28 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Before After
Before After

View file

@ -70,6 +70,10 @@ class DangerMap:
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)
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:
"""Get the proximity cost at a specific coordinate."""
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:
return float(self.grid[ix, iy])
return 1e6 # Outside bounds is expensive
return 1e15 # Outside bounds is impossible

View file

@ -1,7 +1,9 @@
import numpy as np
import pytest
from shapely.geometry import Point
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:
@ -90,3 +92,67 @@ def test_sbend_collision_models() -> None:
res_arc = SBend.generate(start, offset, radius, width, collision_type="arc")
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