diff --git a/DOCS.md b/DOCS.md index fe585ea..4b0b693 100644 --- a/DOCS.md +++ b/DOCS.md @@ -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. diff --git a/README.md b/README.md index 4aea8d0..6b9858d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/examples/02_congestion_resolution.py b/examples/02_congestion_resolution.py index 520c631..0cb4b0e 100644 --- a/examples/02_congestion_resolution.py +++ b/examples/02_congestion_resolution.py @@ -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(): diff --git a/examples/03_locked_paths.py b/examples/03_locked_paths.py index ace3bb6..d01aec1 100644 --- a/examples/03_locked_paths.py +++ b/examples/03_locked_paths.py @@ -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 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...") - results2 = pf.route_all(netlist_phase2, {"secondary_net": 2.0}) - - 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") diff --git a/examples/04_sbends_and_radii.py b/examples/04_sbends_and_radii.py index 2234878..44c3d46 100644 --- a/examples/04_sbends_and_radii.py +++ b/examples/04_sbends_and_radii.py @@ -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 diff --git a/examples/05_orientation_stress.py b/examples/05_orientation_stress.py new file mode 100644 index 0000000..1f48487 --- /dev/null +++ b/examples/05_orientation_stress.py @@ -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() diff --git a/examples/congestion.png b/examples/congestion.png index 775d78b..509e11a 100644 Binary files a/examples/congestion.png and b/examples/congestion.png differ diff --git a/examples/locked.png b/examples/locked.png index 8c45ebf..0fbcf32 100644 Binary files a/examples/locked.png and b/examples/locked.png differ diff --git a/examples/orientation_stress.png b/examples/orientation_stress.png new file mode 100644 index 0000000..20556c3 Binary files /dev/null and b/examples/orientation_stress.png differ diff --git a/examples/sbends_radii.png b/examples/sbends_radii.png index 1a8f9c5..a736212 100644 Binary files a/examples/sbends_radii.png and b/examples/sbends_radii.png differ diff --git a/examples/simple_route.png b/examples/simple_route.png index 400488d..98f7b4b 100644 Binary files a/examples/simple_route.png and b/examples/simple_route.png differ diff --git a/inire/router/danger_map.py b/inire/router/danger_map.py index 2588e80..1ebf617 100644 --- a/inire/router/danger_map.py +++ b/inire/router/danger_map.py @@ -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 diff --git a/inire/tests/test_components.py b/inire/tests/test_components.py index ffb43a4..37da884 100644 --- a/inire/tests/test_components.py +++ b/inire/tests/test_components.py @@ -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