diff --git a/DOCS.md b/DOCS.md index 4b0b693..fe585ea 100644 --- a/DOCS.md +++ b/DOCS.md @@ -51,28 +51,3 @@ 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 6b9858d..4aea8d0 100644 --- a/README.md +++ b/README.md @@ -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/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 0cb4b0e..520c631 100644 --- a/examples/02_congestion_resolution.py +++ b/examples/02_congestion_resolution.py @@ -1,3 +1,4 @@ + from inire.geometry.collision import CollisionEngine from inire.geometry.primitives import Port from inire.router.astar import AStarRouter @@ -8,7 +9,7 @@ from inire.utils.visualization import plot_routing_results def main() -> None: - print("Running Example 02: Congestion Resolution (Triple Crossing)...") + print("Running Example 02: Congestion Resolution (Crossing)...") # 1. Setup Environment (Open space) bounds = (0, 0, 100, 100) @@ -21,24 +22,23 @@ def main() -> None: pf = PathFinder(router, evaluator) # 2. Define Netlist - # Three nets that all converge on the same central area. - # Negotiated Congestion must find non-overlapping paths for all of them. + # Two nets that MUST cross. + # Since crossings are illegal in single-layer routing, one net must detour around the other. netlist = { "horizontal": (Port(10, 50, 0), Port(90, 50, 0)), - "vertical_up": (Port(45, 10, 90), Port(45, 90, 90)), - "vertical_down": (Port(55, 90, 270), Port(55, 10, 270)), + "vertical": (Port(50, 10, 90), Port(50, 90, 90)), } - net_widths = {nid: 2.0 for nid in netlist} + net_widths = {"horizontal": 2.0, "vertical": 2.0} # 3. Route with Negotiated Congestion # 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) # 4. Check Results all_valid = all(r.is_valid for r in results.values()) if all_valid: - print("Success! Congestion resolved for all nets.") + print("Success! Congestion resolved (one net detoured).") 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 d01aec1..ace3bb6 100644 --- a/examples/03_locked_paths.py +++ b/examples/03_locked_paths.py @@ -1,3 +1,4 @@ + from inire.geometry.collision import CollisionEngine from inire.geometry.primitives import Port from inire.router.astar import AStarRouter @@ -8,64 +9,68 @@ from inire.utils.visualization import plot_routing_results def main() -> None: - print("Running Example 03: Locked Paths (Incremental Routing - Bus Scenario)...") + print("Running Example 03: Locked Paths (Incremental Routing)...") # 1. Setup Environment - bounds = (0, 0, 120, 120) + bounds = (0, 0, 100, 100) engine = CollisionEngine(clearance=2.0) 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) - router = AStarRouter(evaluator, node_limit=200000) + evaluator = CostEvaluator(engine, danger_map) + router = AStarRouter(evaluator) pf = PathFinder(router, evaluator) - # 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)), + # 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)), } - print("Phase 1: Routing bus (3 nets)...") - results_p1 = pf.route_all(netlist_p1, {nid: 2.0 for nid in netlist_p1}) + print("Phase 1: Routing critical_net...") + results1 = pf.route_all(netlist_phase1, {"critical_net": 3.0}) # Wider trace - # 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.") + if not results1["critical_net"].is_valid: + print("Error: Phase 1 failed.") + return - # Update danger map with the newly locked geometry - print("Updating DangerMap with locked paths...") + # 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] danger_map.precompute(path_polys) - # 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 + # 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)), } - - 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 - for nid, res in results_p2.items(): - status = "Success" if res.is_valid else "Failed" - print(f" {nid:12}: {status}, collisions={res.collisions}") + 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.") # 5. Visualize - all_results = {**results_p1, **results_p2} - all_netlists = {**netlist_p1, **netlist_p2} + # 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. 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 44c3d46..2234878 100644 --- a/examples/04_sbends_and_radii.py +++ b/examples/04_sbends_and_radii.py @@ -3,6 +3,7 @@ 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 deleted file mode 100644 index 1f48487..0000000 --- a/examples/05_orientation_stress.py +++ /dev/null @@ -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() diff --git a/examples/congestion.png b/examples/congestion.png index 509e11a..775d78b 100644 Binary files a/examples/congestion.png and b/examples/congestion.png differ diff --git a/examples/locked.png b/examples/locked.png index 0fbcf32..8c45ebf 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 deleted file mode 100644 index 20556c3..0000000 Binary files a/examples/orientation_stress.png and /dev/null differ diff --git a/examples/sbends_radii.png b/examples/sbends_radii.png index a736212..1a8f9c5 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 98f7b4b..400488d 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 1ebf617..2588e80 100644 --- a/inire/router/danger_map.py +++ b/inire/router/danger_map.py @@ -70,10 +70,6 @@ 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) @@ -81,4 +77,4 @@ class DangerMap: if 0 <= ix < self.width_cells and 0 <= iy < self.height_cells: return float(self.grid[ix, iy]) - return 1e15 # Outside bounds is impossible + return 1e6 # Outside bounds is expensive diff --git a/inire/tests/test_components.py b/inire/tests/test_components.py index 37da884..ffb43a4 100644 --- a/inire/tests/test_components.py +++ b/inire/tests/test_components.py @@ -1,9 +1,7 @@ -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, rotate_port, translate_port +from inire.geometry.primitives import Port 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") 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