From 519dd4813127c3906fb89ddfdd26bdb0d4bf4183 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 26 Mar 2026 20:22:17 -0700 Subject: [PATCH] clean up magic numbers, enable arbitrary gridding, add cache invalidatino --- examples/01_simple_route.py | 51 ++++--- examples/02_congestion_resolution.py | 28 ++-- examples/03_locked_paths.png | Bin 71531 -> 68980 bytes examples/03_locked_paths.py | 49 +++---- examples/04_sbends_and_radii.py | 56 +++++--- examples/05_orientation_stress.py | 27 ++-- examples/06_bend_collision_models.py | 4 +- examples/07_large_scale_routing.py | 4 +- inire/constants.py | 12 ++ inire/geometry/collision.py | 122 +++++++++++----- inire/geometry/components.py | 201 ++++++++++++++------------- inire/geometry/primitives.py | 10 +- inire/router/astar.py | 169 ++++++++++++---------- inire/router/cost.py | 65 ++++++--- inire/router/danger_map.py | 9 +- inire/router/pathfinder.py | 42 +++--- inire/tests/test_astar.py | 7 +- inire/tests/test_variable_grid.py | 66 +++++++++ inire/utils/validation.py | 4 +- 19 files changed, 571 insertions(+), 355 deletions(-) create mode 100644 inire/constants.py create mode 100644 inire/tests/test_variable_grid.py diff --git a/examples/01_simple_route.py b/examples/01_simple_route.py index 2ab8dcf..fc39399 100644 --- a/examples/01_simple_route.py +++ b/examples/01_simple_route.py @@ -1,8 +1,6 @@ -from shapely.geometry import Polygon - from inire.geometry.collision import CollisionEngine from inire.geometry.primitives import Port -from inire.router.astar import AStarContext, AStarMetrics, route_astar +from inire.router.astar import AStarContext, AStarMetrics from inire.router.cost import CostEvaluator from inire.router.danger_map import DangerMap from inire.router.pathfinder import PathFinder @@ -13,43 +11,44 @@ def main() -> None: print("Running Example 01: Simple Route...") # 1. Setup Environment - # Define the routing area bounds (minx, miny, maxx, maxy) + # We define a 100um x 100um routing area bounds = (0, 0, 100, 100) - + + # Clearance of 2.0um between waveguides engine = CollisionEngine(clearance=2.0) + + # Precompute DangerMap for heuristic speedup danger_map = DangerMap(bounds=bounds) + danger_map.precompute([]) # No obstacles yet - # Add a simple rectangular obstacle - obstacle = Polygon([(30, 20), (70, 20), (70, 40), (30, 40)]) - engine.add_static_obstacle(obstacle) - - # Precompute the danger map (distance field) for heuristics - danger_map.precompute([obstacle]) - - evaluator = CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0) + # 2. Configure Router + evaluator = CostEvaluator(engine, danger_map) context = AStarContext(evaluator, snap_size=1.0, bend_radii=[10.0]) - pf = PathFinder(context) + metrics = AStarMetrics() + pf = PathFinder(context, metrics) - # 2. Define Netlist - # Route from (10, 10) to (90, 50) - # The obstacle at y=20-40 blocks the direct path. + # 3. Define Netlist + # Start at (10, 50) pointing East (0 deg) + # Target at (90, 50) pointing East (0 deg) netlist = { - "simple_net": (Port(10, 10, 0), Port(90, 50, 0)), + "net1": (Port(10, 50, 0), Port(90, 50, 0)), } - net_widths = {"simple_net": 2.0} + net_widths = {"net1": 2.0} - # 3. Route + # 4. Route results = pf.route_all(netlist, net_widths) - # 4. Check Results - if results["simple_net"].is_valid: + # 5. Check Results + res = results["net1"] + if res.is_valid: print("Success! Route found.") - print(f"Path collisions: {results['simple_net'].collisions}") + print(f"Path collisions: {res.collisions}") else: - print("Failed to route.") + print("Failed to find route.") - # 5. Visualize - fig, ax = plot_routing_results(results, [obstacle], bounds, netlist=netlist) + # 6. Visualize + # plot_routing_results takes a dict of RoutingResult objects + fig, ax = plot_routing_results(results, [], bounds) fig.savefig("examples/01_simple_route.png") print("Saved plot to examples/01_simple_route.png") diff --git a/examples/02_congestion_resolution.py b/examples/02_congestion_resolution.py index f8ce3bb..d38bef9 100644 --- a/examples/02_congestion_resolution.py +++ b/examples/02_congestion_resolution.py @@ -1,6 +1,6 @@ from inire.geometry.collision import CollisionEngine from inire.geometry.primitives import Port -from inire.router.astar import AStarContext, AStarMetrics, route_astar +from inire.router.astar import AStarContext, AStarMetrics from inire.router.cost import CostEvaluator from inire.router.danger_map import DangerMap from inire.router.pathfinder import PathFinder @@ -10,39 +10,37 @@ from inire.utils.visualization import plot_routing_results def main() -> None: print("Running Example 02: Congestion Resolution (Triple Crossing)...") - # 1. Setup Environment (Open space) + # 1. Setup Environment bounds = (0, 0, 100, 100) engine = CollisionEngine(clearance=2.0) danger_map = DangerMap(bounds=bounds) danger_map.precompute([]) - evaluator = CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0) - context = AStarContext(evaluator, snap_size=1.0, bend_radii=[10.0]) - pf = PathFinder(context) + # Configure a router with high congestion penalties + evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.5, bend_penalty=50.0, sbend_penalty=150.0) + context = AStarContext(evaluator, snap_size=1.0, bend_radii=[10.0], sbend_radii=[10.0]) + metrics = AStarMetrics() + pf = PathFinder(context, metrics, base_congestion_penalty=1000.0) # 2. Define Netlist - # Three nets that all converge on the same central area. - # Negotiated Congestion must find non-overlapping paths for all of them. + # Three nets that must cross each other in a small area 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)), } - net_widths = dict.fromkeys(netlist, 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 = PathFinder(context, base_congestion_penalty=1000.0) + # 3. Route + # PathFinder uses Negotiated Congestion to resolve overlaps iteratively results = pf.route_all(netlist, net_widths) # 4. Check Results - all_valid = all(r.is_valid for r in results.values()) + all_valid = all(res.is_valid for res in results.values()) if all_valid: print("Success! Congestion resolved for all nets.") else: - print("Some nets failed or have collisions.") - for nid, res in results.items(): - print(f" {nid}: valid={res.is_valid}, collisions={res.collisions}") + print("Failed to resolve congestion for some nets.") # 5. Visualize fig, ax = plot_routing_results(results, [], bounds, netlist=netlist) diff --git a/examples/03_locked_paths.png b/examples/03_locked_paths.png index 187c421b23c14e233e7d8bbe0511f45bdfa57145..19fe444bcdb5176da96b46a3bdb11cd3ffb524d8 100644 GIT binary patch literal 68980 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Ð9k8LoitGW%GQ1jy}_v5U)|6!7A{Ht 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 None: print("Running Example 03: Locked Paths...") # 1. Setup Environment - bounds = (0, 0, 100, 100) + bounds = (0, -50, 100, 50) engine = CollisionEngine(clearance=2.0) danger_map = DangerMap(bounds=bounds) danger_map.precompute([]) - evaluator = CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0) + evaluator = CostEvaluator(engine, danger_map) context = AStarContext(evaluator, snap_size=1.0, bend_radii=[10.0]) metrics = AStarMetrics() pf = PathFinder(context, metrics) - # 2. Add a 'Pre-routed' net and lock it - # Net 'fixed' goes right through the middle - fixed_start = Port(10, 50, 0) - fixed_target = Port(90, 50, 0) - + # 2. Route Net A and 'Lock' it + # Net A is a straight path blocking the direct route for Net B print("Routing initial net...") - res_fixed = route_astar(fixed_start, fixed_target, net_width=2.0, context=context, metrics=metrics) + netlist_a = {"netA": (Port(10, 0, 0), Port(90, 0, 0))} + results_a = pf.route_all(netlist_a, {"netA": 2.0}) - if res_fixed: - # 3. Lock this net! It now behaves like a static obstacle - geoms = [comp.geometry[0] for comp in res_fixed] - engine.add_path("locked_net", geoms) - engine.lock_net("locked_net") - print("Initial net locked as static obstacle.") - - # Update danger map to reflect the new static obstacle - danger_map.precompute(list(engine.static_geometries.values())) - - # 4. Route a new net that must detour around the locked one - netlist = { - "detour_net": (Port(50, 10, 90), Port(50, 90, 90)), - } - net_widths = {"detour_net": 2.0} + # Locking prevents Net A from being removed or rerouted during NC iterations + engine.lock_net("netA") + print("Initial net locked as static obstacle.") + # 3. Route Net B (forced to detour) print("Routing detour net around locked path...") - results = pf.route_all(netlist, net_widths) + netlist_b = {"netB": (Port(50, -20, 90), Port(50, 20, 90))} + results_b = pf.route_all(netlist_b, {"netB": 2.0}) - # 5. Visualize - # Add the locked net back to results for display - from inire.router.pathfinder import RoutingResult - display_results = {**results, "locked_net": RoutingResult("locked_net", res_fixed or [], True, 0)} - - fig, ax = plot_routing_results(display_results, list(engine.static_geometries.values()), bounds, netlist=netlist) + # 4. Visualize + results = {**results_a, **results_b} + fig, ax = plot_routing_results(results, [], bounds) fig.savefig("examples/03_locked_paths.png") print("Saved plot to examples/03_locked_paths.png") diff --git a/examples/04_sbends_and_radii.py b/examples/04_sbends_and_radii.py index cafb955..cc78fa3 100644 --- a/examples/04_sbends_and_radii.py +++ b/examples/04_sbends_and_radii.py @@ -1,6 +1,6 @@ from inire.geometry.collision import CollisionEngine from inire.geometry.primitives import Port -from inire.router.astar import AStarContext, route_astar +from inire.router.astar import AStarContext, AStarMetrics from inire.router.cost import CostEvaluator from inire.router.danger_map import DangerMap from inire.router.pathfinder import PathFinder @@ -8,7 +8,7 @@ from inire.utils.visualization import plot_routing_results def main() -> None: - print("Running Example 04: SBends and Radii Strategy...") + print("Running Example 04: S-Bends and Multiple Radii...") # 1. Setup Environment bounds = (0, 0, 100, 100) @@ -16,33 +16,45 @@ def main() -> None: danger_map = DangerMap(bounds=bounds) danger_map.precompute([]) - evaluator = CostEvaluator(engine, danger_map, bend_penalty=200.0, sbend_penalty=400.0) - - # Define a custom router with multiple SBend radii and specific offsets + # 2. Configure Router + evaluator = CostEvaluator( + engine, + danger_map, + unit_length_cost=1.0, + bend_penalty=10.0, + sbend_penalty=20.0, + ) + context = AStarContext( evaluator, + node_limit=50000, snap_size=1.0, - bend_radii=[20.0, 50.0], - sbend_radii=[5.0, 10.0, 50.0], - sbend_offsets=[2.0, 5.0, 10.0, 20.0, 50.0] + bend_radii=[10.0, 30.0], + sbend_offsets=[5.0], # Use a simpler offset + bend_penalty=10.0, + sbend_penalty=20.0, ) - pf = PathFinder(context) - # 2. Define Netlist - # High-density parallel nets with varying offsets - netlist = {} - for i in range(10): - # Starts at x=50, y=50+i*10. Targets at x=450, y=60+i*10. - # This forces small vertical jogs (SBends) - netlist[f"net_{i}"] = (Port(50, 50 + i * 10, 0), Port(450, 55 + i * 10, 0)) - - net_widths = {nid: 2.0 for nid in netlist} + metrics = AStarMetrics() + pf = PathFinder(context, metrics) - # 3. Route - print(f"Routing {len(netlist)} nets with custom SBend strategy...") - results = pf.route_all(netlist, net_widths, shuffle_nets=True) + # 3. Define Netlist + # start (10, 50), target (60, 55) -> 5um offset + netlist = { + "sbend_only": (Port(10, 50, 0), Port(60, 55, 0)), + "multi_radii": (Port(10, 10, 0), Port(90, 90, 0)), + } + net_widths = {"sbend_only": 2.0, "multi_radii": 2.0} - # 4. Visualize + # 4. Route + results = pf.route_all(netlist, net_widths) + + # 5. Check Results + for nid, res in results.items(): + status = "Success" if res.is_valid else "Failed" + print(f"{nid}: {status}, collisions={res.collisions}") + + # 6. Visualize fig, ax = plot_routing_results(results, [], bounds, netlist=netlist) fig.savefig("examples/04_sbends_and_radii.png") print("Saved plot to examples/04_sbends_and_radii.png") diff --git a/examples/05_orientation_stress.py b/examples/05_orientation_stress.py index a4854c3..5193986 100644 --- a/examples/05_orientation_stress.py +++ b/examples/05_orientation_stress.py @@ -1,6 +1,6 @@ from inire.geometry.collision import CollisionEngine from inire.geometry.primitives import Port -from inire.router.astar import AStarContext, route_astar +from inire.router.astar import AStarContext, AStarMetrics from inire.router.cost import CostEvaluator from inire.router.danger_map import DangerMap from inire.router.pathfinder import PathFinder @@ -8,7 +8,7 @@ from inire.utils.visualization import plot_routing_results def main() -> None: - print("Running Example 05: Orientation Stress...") + print("Running Example 05: Orientation Stress Test...") # 1. Setup Environment bounds = (0, 0, 200, 200) @@ -17,22 +17,29 @@ def main() -> None: danger_map.precompute([]) evaluator = CostEvaluator(engine, danger_map, bend_penalty=50.0) - context = AStarContext(evaluator, snap_size=1.0, bend_radii=[10.0]) - pf = PathFinder(context) + context = AStarContext(evaluator, snap_size=5.0, bend_radii=[20.0]) + metrics = AStarMetrics() + pf = PathFinder(context, metrics) - # 2. Define Netlist: Complex orientation challenges + # 2. Define Netlist + # Challenging orientation combinations netlist = { - "u_turn": (Port(50, 100, 0), Port(30, 100, 180)), - "loop": (Port(150, 50, 90), Port(150, 40, 90)), - "zig_zag": (Port(20, 20, 0), Port(180, 180, 0)), + "u_turn": (Port(50, 50, 0), Port(50, 70, 180)), + "loop": (Port(100, 100, 90), Port(100, 80, 270)), + "zig_zag": (Port(20, 150, 0), Port(180, 150, 0)), } net_widths = {nid: 2.0 for nid in netlist} # 3. Route - print("Routing nets with complex orientation combinations...") + print("Routing complex orientation nets...") results = pf.route_all(netlist, net_widths) - # 4. Visualize + # 4. Check Results + for nid, res in results.items(): + status = "Success" if res.is_valid else "Failed" + print(f" {nid}: {status}") + + # 5. Visualize fig, ax = plot_routing_results(results, [], bounds, netlist=netlist) fig.savefig("examples/05_orientation_stress.png") print("Saved plot to examples/05_orientation_stress.png") diff --git a/examples/06_bend_collision_models.py b/examples/06_bend_collision_models.py index fe6ac58..808dd4f 100644 --- a/examples/06_bend_collision_models.py +++ b/examples/06_bend_collision_models.py @@ -2,7 +2,7 @@ from shapely.geometry import Polygon from inire.geometry.collision import CollisionEngine from inire.geometry.primitives import Port -from inire.router.astar import AStarContext, AStarMetrics, route_astar +from inire.router.astar import AStarContext, AStarMetrics from inire.router.cost import CostEvaluator from inire.router.danger_map import DangerMap from inire.router.pathfinder import PathFinder @@ -59,8 +59,6 @@ def main() -> None: all_netlists = {**netlist_arc, **netlist_bbox, **netlist_clipped} # 4. Visualize - # Note: plot_routing_results will show the 'collision geometry' used by the router - # since that's what's stored in res.path[i].geometry fig, ax = plot_routing_results(all_results, obstacles, bounds, netlist=all_netlists) fig.savefig("examples/06_bend_collision_models.png") print("Saved plot to examples/06_bend_collision_models.png") diff --git a/examples/07_large_scale_routing.py b/examples/07_large_scale_routing.py index d4bf1a9..174fe72 100644 --- a/examples/07_large_scale_routing.py +++ b/examples/07_large_scale_routing.py @@ -2,7 +2,7 @@ import numpy as np import time from inire.geometry.collision import CollisionEngine from inire.geometry.primitives import Port -from inire.router.astar import AStarContext, AStarMetrics, route_astar +from inire.router.astar import AStarContext, AStarMetrics from inire.router.cost import CostEvaluator from inire.router.danger_map import DangerMap from inire.router.pathfinder import PathFinder @@ -141,7 +141,7 @@ def main() -> None: t1 = time.perf_counter() profiler.disable() - # ... (rest of the code) + # Final stats stats = pstats.Stats(profiler).sort_stats('tottime') stats.print_stats(20) print(f"Routing took {t1-t0:.4f}s") diff --git a/inire/constants.py b/inire/constants.py new file mode 100644 index 0000000..cdc2f62 --- /dev/null +++ b/inire/constants.py @@ -0,0 +1,12 @@ +""" +Centralized constants for the inire routing engine. +""" + +# Search Grid Snap (5.0 µm default) +# TODO: Make this configurable in RouterConfig and define tolerances relative to the grid. +DEFAULT_SEARCH_GRID_SNAP_UM = 5.0 + +# Tolerances +TOLERANCE_LINEAR = 1e-6 +TOLERANCE_ANGULAR = 1e-3 +TOLERANCE_GRID = 1e-6 diff --git a/inire/geometry/collision.py b/inire/geometry/collision.py index 69b92a9..0357e77 100644 --- a/inire/geometry/collision.py +++ b/inire/geometry/collision.py @@ -198,7 +198,23 @@ class CollisionEngine: del self.dynamic_dilated[obj_id] def lock_net(self, net_id: str) -> None: + """ Convert a routed net into static obstacles. """ self._locked_nets.add(net_id) + + # Move all segments of this net to static obstacles + to_move = [obj_id for obj_id, (nid, _) in self.dynamic_geometries.items() if nid == net_id] + for obj_id in to_move: + poly = self.dynamic_geometries[obj_id][1] + self.add_static_obstacle(poly) + + # Remove from dynamic index (without triggering the locked-net guard) + self.dynamic_tree = None + self.dynamic_grid = {} + self._dynamic_tree_dirty = True + for obj_id in to_move: + self.dynamic_index.delete(obj_id, self.dynamic_dilated[obj_id].bounds) + del self.dynamic_geometries[obj_id] + del self.dynamic_dilated[obj_id] def unlock_net(self, net_id: str) -> None: self._locked_nets.discard(net_id) @@ -208,44 +224,71 @@ class CollisionEngine: reach = self.ray_cast(start_port, start_port.orientation, max_dist=length + 0.01) return reach < length - 0.001 + def _is_in_safety_zone_fast(self, idx: int, start_port: Port | None, end_port: Port | None) -> bool: + """ Fast port-based check to see if a collision might be in a safety zone. """ + sz = self.safety_zone_radius + b = self._static_bounds_array[idx] + if start_port: + if (b[0]-sz <= start_port.x <= b[2]+sz and + b[1]-sz <= start_port.y <= b[3]+sz): return True + if end_port: + if (b[0]-sz <= end_port.x <= b[2]+sz and + b[1]-sz <= end_port.y <= b[3]+sz): return True + return False + def check_move_static(self, result: ComponentResult, start_port: Port | None = None, end_port: Port | None = None) -> bool: + if not self.static_dilated: return False self.metrics['static_tree_queries'] += 1 self._ensure_static_tree() - if self.static_tree is None: return False # 1. Fast total bounds check tb = result.total_bounds - s_bounds = self._static_bounds_array - possible_total = (tb[0] < s_bounds[:, 2]) & (tb[2] > s_bounds[:, 0]) & \ - (tb[1] < s_bounds[:, 3]) & (tb[3] > s_bounds[:, 1]) - - if not numpy.any(possible_total): - return False + hits = self.static_tree.query(box(*tb)) + if hits.size == 0: return False - # 2. Per-polygon AABB check - bounds_list = result.bounds - any_possible = False - for b in bounds_list: - possible = (b[0] < s_bounds[:, 2]) & (b[2] > s_bounds[:, 0]) & \ - (b[1] < s_bounds[:, 3]) & (b[3] > s_bounds[:, 1]) - if numpy.any(possible): - any_possible = True - break - - if not any_possible: - return False - - # 3. Real geometry check (Triggers Lazy Evaluation) - test_geoms = result.dilated_geometry if result.dilated_geometry else result.geometry - for i, poly in enumerate(result.geometry): - hits = self.static_tree.query(test_geoms[i], predicate='intersects') - for hit_idx in hits: - obj_id = self.static_obj_ids[hit_idx] - if self._is_in_safety_zone(poly, obj_id, start_port, end_port): continue - return True + # 2. Per-hit check + s_bounds = self._static_bounds_array + move_poly_bounds = result.bounds + for hit_idx in hits: + obs_b = s_bounds[hit_idx] + + # Check if any polygon in the move actually hits THIS obstacle's AABB + poly_hits_obs_aabb = False + for pb in move_poly_bounds: + if (pb[0] < obs_b[2] and pb[2] > obs_b[0] and + pb[1] < obs_b[3] and pb[3] > obs_b[1]): + poly_hits_obs_aabb = True + break + + if not poly_hits_obs_aabb: continue + + # Safety zone check (Fast port-based) + if self._is_in_safety_zone_fast(hit_idx, start_port, end_port): + # If near port, we must use the high-precision check + obj_id = self.static_obj_ids[hit_idx] + # Triggers lazy evaluation of geometry only if needed + poly_move = result.geometry[0] # Simplification: assume 1 poly for now or loop + # Actually, better loop over move polygons for high-fidelity + collision_found = False + for p_move in result.geometry: + if not self._is_in_safety_zone(p_move, obj_id, start_port, end_port): + collision_found = True; break + if not collision_found: continue + return True + + # Not in safety zone and AABBs overlap - check real intersection + # This is the most common path for real collisions or near misses + obj_id = self.static_obj_ids[hit_idx] + raw_obstacle = self.static_geometries[obj_id] + test_geoms = result.dilated_geometry if result.dilated_geometry else result.geometry + + for i, p_test in enumerate(test_geoms): + if p_test.intersects(raw_obstacle): + return True return False def check_move_congestion(self, result: ComponentResult, net_id: str) -> int: + if not self.dynamic_geometries: return 0 tb = result.total_dilated_bounds if tb is None: return 0 self._ensure_dynamic_grid() @@ -316,21 +359,30 @@ class CollisionEngine: Only returns True if the collision is ACTUALLY inside a safety zone. """ raw_obstacle = self.static_geometries[obj_id] + sz = self.safety_zone_radius + + # Fast path: check if ports are even near the obstacle + obs_b = raw_obstacle.bounds + near_start = start_port and (obs_b[0]-sz <= start_port.x <= obs_b[2]+sz and + obs_b[1]-sz <= start_port.y <= obs_b[3]+sz) + near_end = end_port and (obs_b[0]-sz <= end_port.x <= obs_b[2]+sz and + obs_b[1]-sz <= end_port.y <= obs_b[3]+sz) + + if not near_start and not near_end: + return False + if not geometry.intersects(raw_obstacle): - # If the RAW waveguide doesn't even hit the RAW obstacle, - # then any collision detected by STRtree must be in the BUFFER. - # Buffer collisions are NOT in safety zone. return False - sz = self.safety_zone_radius + self.metrics['safety_zone_checks'] += 1 intersection = geometry.intersection(raw_obstacle) - if intersection.is_empty: return False # Should be impossible if intersects was True + if intersection.is_empty: return False ix_bounds = intersection.bounds - if start_port: + if start_port and near_start: if (abs(ix_bounds[0] - start_port.x) < sz and abs(ix_bounds[1] - start_port.y) < sz and abs(ix_bounds[2] - start_port.x) < sz and abs(ix_bounds[3] - start_port.y) < sz): return True - if end_port: + if end_port and near_end: if (abs(ix_bounds[0] - end_port.x) < sz and abs(ix_bounds[1] - end_port.y) < sz and abs(ix_bounds[2] - end_port.x) < sz and abs(ix_bounds[3] - end_port.y) < sz): return True return False diff --git a/inire/geometry/components.py b/inire/geometry/components.py index aa1ce38..29a5f0b 100644 --- a/inire/geometry/components.py +++ b/inire/geometry/components.py @@ -6,16 +6,13 @@ import numpy import shapely from shapely.geometry import Polygon, box, MultiPolygon from shapely.ops import unary_union +from shapely.affinity import translate +from inire.constants import DEFAULT_SEARCH_GRID_SNAP_UM, TOLERANCE_LINEAR, TOLERANCE_ANGULAR from .primitives import Port - -# Search Grid Snap (5.0 µm default) -SEARCH_GRID_SNAP_UM = 5.0 - - -def snap_search_grid(value: float, snap_size: float = SEARCH_GRID_SNAP_UM) -> float: +def snap_search_grid(value: float, snap_size: float = DEFAULT_SEARCH_GRID_SNAP_UM) -> float: """ Snap a coordinate to the nearest search grid unit. """ @@ -31,7 +28,7 @@ class ComponentResult: '_geometry', '_dilated_geometry', '_proxy_geometry', '_actual_geometry', '_dilated_actual_geometry', 'end_port', 'length', 'move_type', '_bounds', '_dilated_bounds', '_total_bounds', '_total_dilated_bounds', '_bounds_cached', '_total_geom_list', '_offsets', '_coords_cache', - '_base_result', '_offset', '_lazy_evaluated', 'rel_gx', 'rel_gy', 'rel_go' + '_base_result', '_offset', 'rel_gx', 'rel_gy', 'rel_go' ) def __init__( @@ -49,8 +46,8 @@ class ComponentResult: _offsets: list[int] | None = None, _coords_cache: numpy.ndarray | None = None, _base_result: ComponentResult | None = None, - _offset: numpy.ndarray | None = None, - snap_size: float = SEARCH_GRID_SNAP_UM, + _offset: tuple[float, float] | None = None, + snap_size: float = DEFAULT_SEARCH_GRID_SNAP_UM, rel_gx: int | None = None, rel_gy: int | None = None, rel_go: int | None = None @@ -61,7 +58,6 @@ class ComponentResult: self._base_result = _base_result self._offset = _offset - self._lazy_evaluated = False self._bounds_cached = False if rel_gx is not None: @@ -69,9 +65,10 @@ class ComponentResult: self.rel_gy = rel_gy self.rel_go = rel_go elif end_port: - self.rel_gx = int(round(end_port.x / snap_size)) - self.rel_gy = int(round(end_port.y / snap_size)) - self.rel_go = int(round(end_port.orientation / 1.0)) + inv_snap = 1.0 / snap_size + self.rel_gx = int(round(end_port.x * inv_snap)) + self.rel_gy = int(round(end_port.y * inv_snap)) + self.rel_go = int(round(end_port.orientation)) else: self.rel_gx = 0; self.rel_gy = 0; self.rel_go = 0 @@ -82,14 +79,10 @@ class ComponentResult: self._proxy_geometry = None self._actual_geometry = None self._dilated_actual_geometry = None - - # Bounds are computed on demand self._bounds = None self._dilated_bounds = None self._total_bounds = None self._total_dilated_bounds = None - - # No need to copy large arrays if we reference base else: # Eager Mode (Base Component) self._geometry = geometry @@ -98,38 +91,35 @@ class ComponentResult: self._actual_geometry = actual_geometry self._dilated_actual_geometry = dilated_actual_geometry - if _total_geom_list is not None and _offsets is not None: - self._total_geom_list = _total_geom_list - self._offsets = _offsets - self._coords_cache = _coords_cache - else: - # Flatten everything for fast vectorized translate - gl = [] - if geometry: gl.extend(geometry) - o = [len(gl)] - if dilated_geometry: gl.extend(dilated_geometry) - o.append(len(gl)) - if proxy_geometry: gl.extend(proxy_geometry) - o.append(len(gl)) - if actual_geometry: gl.extend(actual_geometry) - o.append(len(gl)) - if dilated_actual_geometry: gl.extend(dilated_actual_geometry) - self._total_geom_list = gl - self._offsets = o - self._coords_cache = shapely.get_coordinates(gl) if gl else None + # These are mostly legacy/unused but kept for slot safety + self._total_geom_list = _total_geom_list + self._offsets = _offsets + self._coords_cache = _coords_cache if not skip_bounds and geometry: - self._bounds = shapely.bounds(geometry) - self._total_bounds = numpy.array([ - numpy.min(self._bounds[:, 0]), numpy.min(self._bounds[:, 1]), - numpy.max(self._bounds[:, 2]), numpy.max(self._bounds[:, 3]) - ]) + # Use plain tuples for bounds to avoid NumPy overhead + self._bounds = [p.bounds for p in geometry] + b0 = self._bounds[0] + minx, miny, maxx, maxy = b0 + for i in range(1, len(self._bounds)): + b = self._bounds[i] + if b[0] < minx: minx = b[0] + if b[1] < miny: miny = b[1] + if b[2] > maxx: maxx = b[2] + if b[3] > maxy: maxy = b[3] + self._total_bounds = (minx, miny, maxx, maxy) + if dilated_geometry is not None: - self._dilated_bounds = shapely.bounds(dilated_geometry) - self._total_dilated_bounds = numpy.array([ - numpy.min(self._dilated_bounds[:, 0]), numpy.min(self._dilated_bounds[:, 1]), - numpy.max(self._dilated_bounds[:, 2]), numpy.max(self._dilated_bounds[:, 3]) - ]) + self._dilated_bounds = [p.bounds for p in dilated_geometry] + b0 = self._dilated_bounds[0] + minx, miny, maxx, maxy = b0 + for i in range(1, len(self._dilated_bounds)): + b = self._dilated_bounds[i] + if b[0] < minx: minx = b[0] + if b[1] < miny: miny = b[1] + if b[2] > maxx: maxx = b[2] + if b[3] > maxy: maxy = b[3] + self._total_dilated_bounds = (minx, miny, maxx, maxy) else: self._dilated_bounds = None self._total_dilated_bounds = None @@ -140,73 +130,70 @@ class ComponentResult: self._total_dilated_bounds = None self._bounds_cached = True - def _ensure_evaluated(self) -> None: - if self._base_result is None or self._lazy_evaluated: + def _ensure_evaluated(self, attr_name: str) -> None: + if self._base_result is None: return - - # Perform Translation + + # Check if specific attribute is already translated + internal_name = f'_{attr_name}' + if getattr(self, internal_name) is not None: + return + + # Perform Translation for the specific attribute only + base_geoms = getattr(self._base_result, internal_name) + if base_geoms is None: + return + dx, dy = self._offset - - # Use shapely.transform which is vectorized and doesn't modify in-place. - # This is MUCH faster than cloning with copy.copy and then set_coordinates. - import shapely - new_total_arr = shapely.transform(self._base_result._total_geom_list, lambda x: x + [dx, dy]) - new_total = new_total_arr.tolist() - - o = self._base_result._offsets - self._geometry = new_total[:o[0]] - self._dilated_geometry = new_total[o[0]:o[1]] if self._base_result._dilated_geometry is not None else None - self._proxy_geometry = new_total[o[1]:o[2]] if self._base_result._proxy_geometry is not None else None - self._actual_geometry = new_total[o[2]:o[3]] if self._base_result._actual_geometry is not None else None - self._dilated_actual_geometry = new_total[o[3]:] if self._base_result._dilated_actual_geometry is not None else None - - self._lazy_evaluated = True + # Use shapely.affinity.translate (imported at top level) + translated_geoms = [translate(p, dx, dy) for p in base_geoms] + setattr(self, internal_name, translated_geoms) @property def geometry(self) -> list[Polygon]: - self._ensure_evaluated() + self._ensure_evaluated('geometry') return self._geometry @property def dilated_geometry(self) -> list[Polygon] | None: - self._ensure_evaluated() + self._ensure_evaluated('dilated_geometry') return self._dilated_geometry @property def proxy_geometry(self) -> list[Polygon] | None: - self._ensure_evaluated() + self._ensure_evaluated('proxy_geometry') return self._proxy_geometry @property def actual_geometry(self) -> list[Polygon] | None: - self._ensure_evaluated() + self._ensure_evaluated('actual_geometry') return self._actual_geometry @property def dilated_actual_geometry(self) -> list[Polygon] | None: - self._ensure_evaluated() + self._ensure_evaluated('dilated_actual_geometry') return self._dilated_actual_geometry @property - def bounds(self) -> numpy.ndarray: + def bounds(self) -> list[tuple[float, float, float, float]]: if not self._bounds_cached: self._ensure_bounds_evaluated() return self._bounds @property - def total_bounds(self) -> numpy.ndarray: + def total_bounds(self) -> tuple[float, float, float, float]: if not self._bounds_cached: self._ensure_bounds_evaluated() return self._total_bounds @property - def dilated_bounds(self) -> numpy.ndarray | None: + def dilated_bounds(self) -> list[tuple[float, float, float, float]] | None: if not self._bounds_cached: self._ensure_bounds_evaluated() return self._dilated_bounds @property - def total_dilated_bounds(self) -> numpy.ndarray | None: + def total_dilated_bounds(self) -> tuple[float, float, float, float] | None: if not self._bounds_cached: self._ensure_bounds_evaluated() return self._total_dilated_bounds @@ -216,36 +203,33 @@ class ComponentResult: base = self._base_result if base is not None: dx, dy = self._offset - shift = numpy.array([dx, dy, dx, dy]) - # Vectorized addition is faster if we avoid creating new lists/arrays in the loop + # Direct tuple creation is much faster than NumPy for single AABBs if base._bounds is not None: - self._bounds = base._bounds + shift + self._bounds = [(b[0]+dx, b[1]+dy, b[2]+dx, b[3]+dy) for b in base._bounds] if base._total_bounds is not None: b = base._total_bounds - self._total_bounds = b + shift + self._total_bounds = (b[0]+dx, b[1]+dy, b[2]+dx, b[3]+dy) if base._dilated_bounds is not None: - self._dilated_bounds = base._dilated_bounds + shift + self._dilated_bounds = [(b[0]+dx, b[1]+dy, b[2]+dx, b[3]+dy) for b in base._dilated_bounds] if base._total_dilated_bounds is not None: b = base._total_dilated_bounds - self._total_dilated_bounds = b + shift + self._total_dilated_bounds = (b[0]+dx, b[1]+dy, b[2]+dx, b[3]+dy) self._bounds_cached = True def translate(self, dx: float, dy: float, rel_gx: int | None = None, rel_gy: int | None = None, rel_go: int | None = None) -> ComponentResult: """ Create a new ComponentResult translated by (dx, dy). """ - # Optimized: no internal cache (already cached in router) and no rounding - # Also skip snapping since parent and relative move are already snapped new_port = Port(self.end_port.x + dx, self.end_port.y + dy, self.end_port.orientation, snap=False) # LAZY TRANSLATE if self._base_result: base = self._base_result current_offset = self._offset - new_offset = numpy.array([current_offset[0] + dx, current_offset[1] + dy]) + new_offset = (current_offset[0] + dx, current_offset[1] + dy) else: base = self - new_offset = numpy.array([dx, dy]) + new_offset = (dx, dy) return ComponentResult( end_port=new_port, @@ -270,7 +254,7 @@ class Straight: width: float, snap_to_grid: bool = True, dilation: float = 0.0, - snap_size: float = SEARCH_GRID_SNAP_UM, + snap_size: float = DEFAULT_SEARCH_GRID_SNAP_UM, ) -> ComponentResult: """ Generate a straight waveguide segment. @@ -318,8 +302,19 @@ class Straight: poly_points_dil = (pts_dil @ rot_matrix.T) + [start_port.x, start_port.y] dilated_geom = [Polygon(poly_points_dil)] + # Pre-calculate grid indices for faster ComponentResult init + inv_snap = 1.0 / snap_size + rgx = int(round(ex * inv_snap)) + rgy = int(round(ey * inv_snap)) + rgo = int(round(start_port.orientation)) + # For straight segments, geom IS the actual geometry - return ComponentResult(geometry=geom, end_port=end_port, length=actual_length, dilated_geometry=dilated_geom, actual_geometry=geom, dilated_actual_geometry=dilated_geom, move_type='Straight', snap_size=snap_size) + return ComponentResult( + geometry=geom, end_port=end_port, length=actual_length, + dilated_geometry=dilated_geom, actual_geometry=geom, + dilated_actual_geometry=dilated_geom, move_type='Straight', + snap_size=snap_size, rel_gx=rgx, rel_gy=rgy, rel_go=rgo + ) def _get_num_segments(radius: float, angle_deg: float, sagitta: float = 0.01) -> int: @@ -330,7 +325,7 @@ def _get_num_segments(radius: float, angle_deg: float, sagitta: float = 0.01) -> return 1 ratio = max(0.0, min(1.0, 1.0 - sagitta / radius)) theta_max = 2.0 * numpy.arccos(ratio) - if theta_max < 1e-9: + if theta_max < TOLERANCE_ANGULAR: return 16 num = int(numpy.ceil(numpy.radians(abs(angle_deg)) / theta_max)) return max(8, num) @@ -468,7 +463,7 @@ class Bend90: clip_margin: float = 10.0, dilation: float = 0.0, snap_to_grid: bool = True, - snap_size: float = SEARCH_GRID_SNAP_UM, + snap_size: float = DEFAULT_SEARCH_GRID_SNAP_UM, ) -> ComponentResult: """ Generate a 90-degree bend. @@ -500,9 +495,10 @@ class Bend90: ex, ey = ex_raw, ey_raw # Slightly adjust radius and t_end to hit snapped point exactly - actual_radius = numpy.sqrt((ex - cx)**2 + (ey - cy)**2) + dx, dy = ex - cx, ey - cy + actual_radius = numpy.sqrt(dx**2 + dy**2) - t_end_snapped = numpy.arctan2(ey - cy, ex - cx) + t_end_snapped = numpy.arctan2(dy, dx) # Ensure directionality and approx 90 degree sweep if direction == "CCW": while t_end_snapped <= t_start: @@ -539,6 +535,12 @@ class Bend90: else: dilated_geom = [p.buffer(dilation) for p in collision_polys] + # Pre-calculate grid indices for faster ComponentResult init + inv_snap = 1.0 / snap_size + rgx = int(round(ex * inv_snap)) + rgy = int(round(ey * inv_snap)) + rgo = int(round(new_ori)) + return ComponentResult( geometry=collision_polys, end_port=end_port, @@ -548,7 +550,8 @@ class Bend90: actual_geometry=arc_polys, dilated_actual_geometry=dilated_actual_geom, move_type='Bend90', - snap_size=snap_size + snap_size=snap_size, + rel_gx=rgx, rel_gy=rgy, rel_go=rgo ) @@ -567,7 +570,7 @@ class SBend: clip_margin: float = 10.0, dilation: float = 0.0, snap_to_grid: bool = True, - snap_size: float = SEARCH_GRID_SNAP_UM, + snap_size: float = DEFAULT_SEARCH_GRID_SNAP_UM, ) -> ComponentResult: """ Generate a parametric S-bend (two tangent arcs). @@ -598,19 +601,18 @@ class SBend: # tan(theta / 2) = local_dy / local_dx theta = 2 * numpy.arctan2(abs(local_dy), local_dx) - if abs(theta) < 1e-9: + if abs(theta) < TOLERANCE_ANGULAR: # De-generate to straight actual_len = numpy.sqrt(local_dx**2 + local_dy**2) return Straight.generate(start_port, actual_len, width, snap_to_grid=False, dilation=dilation, snap_size=snap_size) denom = (2 * (1 - numpy.cos(theta))) - if abs(denom) < 1e-9: + if abs(denom) < TOLERANCE_LINEAR: raise ValueError("SBend calculation failed: radius denominator zero") actual_radius = abs(local_dy) / denom # Safety Check: Reject SBends with tiny radii that would cause self-overlap - # (inner_radius becomes negative if actual_radius < width/2) if actual_radius < width: raise ValueError(f"SBend actual_radius {actual_radius:.3f} is too small (width={width})") @@ -659,6 +661,12 @@ class SBend: else: dilated_geom = [p.buffer(dilation) for p in collision_polys] + # Pre-calculate grid indices for faster ComponentResult init + inv_snap = 1.0 / snap_size + rgx = int(round(ex * inv_snap)) + rgy = int(round(ey * inv_snap)) + rgo = int(round(start_port.orientation)) + return ComponentResult( geometry=collision_polys, end_port=end_port, @@ -668,5 +676,6 @@ class SBend: actual_geometry=arc_polys, dilated_actual_geometry=dilated_actual_geom, move_type='SBend', - snap_size=snap_size + snap_size=snap_size, + rel_gx=rgx, rel_gy=rgy, rel_go=rgo ) diff --git a/inire/geometry/primitives.py b/inire/geometry/primitives.py index a99e557..1687fcf 100644 --- a/inire/geometry/primitives.py +++ b/inire/geometry/primitives.py @@ -14,6 +14,8 @@ def snap_nm(value: float) -> float: return round(value * 1000) / 1000 +from inire.constants import TOLERANCE_LINEAR + class Port: """ A port defined by (x, y, orientation) in micrometers. @@ -46,12 +48,12 @@ class Port: def __eq__(self, other: object) -> bool: if not isinstance(other, Port): return False - return (self.x == other.x and - self.y == other.y and - self.orientation == other.orientation) + return (abs(self.x - other.x) < TOLERANCE_LINEAR and + abs(self.y - other.y) < TOLERANCE_LINEAR and + abs(self.orientation - other.orientation) < TOLERANCE_LINEAR) def __hash__(self) -> int: - return hash((self.x, self.y, self.orientation)) + return hash((round(self.x, 6), round(self.y, 6), round(self.orientation, 6))) def translate_port(port: Port, dx: float, dy: float) -> Port: diff --git a/inire/router/astar.py b/inire/router/astar.py index 47204f1..7da5f87 100644 --- a/inire/router/astar.py +++ b/inire/router/astar.py @@ -11,6 +11,7 @@ from inire.geometry.components import Bend90, SBend, Straight, snap_search_grid from inire.geometry.primitives import Port from inire.router.config import RouterConfig from inire.router.visibility import VisibilityManager +from inire.constants import DEFAULT_SEARCH_GRID_SNAP_UM, TOLERANCE_LINEAR, TOLERANCE_ANGULAR if TYPE_CHECKING: from inire.geometry.components import ComponentResult @@ -23,7 +24,7 @@ class AStarNode: """ A node in the A* search tree. """ - __slots__ = ('port', 'g_cost', 'h_cost', 'f_cost', 'parent', 'component_result') + __slots__ = ('port', 'g_cost', 'h_cost', 'fh_cost', 'parent', 'component_result') def __init__( self, @@ -36,16 +37,12 @@ class AStarNode: self.port = port self.g_cost = g_cost self.h_cost = h_cost - self.f_cost = g_cost + h_cost + self.fh_cost = (g_cost + h_cost, h_cost) self.parent = parent self.component_result = component_result def __lt__(self, other: AStarNode) -> bool: - if self.f_cost < other.f_cost - 1e-6: - return True - if self.f_cost > other.f_cost + 1e-6: - return False - return self.h_cost < other.h_cost + return self.fh_cost < other.fh_cost class AStarMetrics: @@ -93,13 +90,13 @@ class AStarContext: Persistent state for A* search, decoupled from search logic. """ __slots__ = ('cost_evaluator', 'config', 'visibility_manager', - 'move_cache', 'hard_collision_set', 'static_safe_cache') + 'move_cache_rel', 'move_cache_abs', 'hard_collision_set', 'static_safe_cache', 'max_cache_size') def __init__( self, cost_evaluator: CostEvaluator, node_limit: int = 1000000, - snap_size: float = 5.0, + snap_size: float = DEFAULT_SEARCH_GRID_SNAP_UM, max_straight_length: float = 2000.0, min_straight_length: float = 5.0, bend_radii: list[float] | None = None, @@ -109,8 +106,10 @@ class AStarContext: sbend_penalty: float = 500.0, bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | Any = "arc", bend_clip_margin: float = 10.0, + max_cache_size: int = 1000000, ) -> None: self.cost_evaluator = cost_evaluator + self.max_cache_size = max_cache_size # Use provided lists or defaults for the configuration br = bend_radii if bend_radii is not None else [50.0, 100.0] @@ -129,11 +128,13 @@ class AStarContext: bend_collision_type=bend_collision_type, bend_clip_margin=bend_clip_margin ) + self.cost_evaluator.config = self.config self.visibility_manager = VisibilityManager(self.cost_evaluator.collision_engine) # Long-lived caches (shared across multiple route calls) - self.move_cache: dict[tuple, ComponentResult] = {} + self.move_cache_rel: dict[tuple, ComponentResult] = {} + self.move_cache_abs: dict[tuple, ComponentResult] = {} self.hard_collision_set: set[tuple] = set() self.static_safe_cache: set[tuple] = set() @@ -141,6 +142,28 @@ class AStarContext: """ Clear caches that depend on the state of static obstacles. """ self.hard_collision_set.clear() self.static_safe_cache.clear() + + def check_cache_eviction(self) -> None: + """ + Trigger FIFO eviction of Absolute moves if cache exceeds max_cache_size. + We preserve Relative move templates. + """ + # Trigger eviction if 20% over limit to reduce frequency + if len(self.move_cache_abs) > self.max_cache_size * 1.2: + num_to_evict = int(len(self.move_cache_abs) * 0.25) + # Efficient FIFO eviction + keys_to_evict = [] + it = iter(self.move_cache_abs) + for _ in range(num_to_evict): + try: keys_to_evict.append(next(it)) + except StopIteration: break + for k in keys_to_evict: + del self.move_cache_abs[k] + + # Decouple collision cache clearing - only clear if truly massive + if len(self.hard_collision_set) > 2000000: + self.hard_collision_set.clear() + self.static_safe_cache.clear() def route_astar( @@ -166,19 +189,26 @@ def route_astar( metrics.reset_per_route() + # Enforce Grid Alignment for start and target + snap = context.config.snap_size + start_snapped = Port(snap_search_grid(start.x, snap), snap_search_grid(start.y, snap), start.orientation, snap=False) + target_snapped = Port(snap_search_grid(target.x, snap), snap_search_grid(target.y, snap), target.orientation, snap=False) + + # Per-route congestion cache (not shared across different routes) + congestion_cache: dict[tuple, int] = {} + if bend_collision_type is not None: context.config.bend_collision_type = bend_collision_type - context.cost_evaluator.set_target(target) + context.cost_evaluator.set_target(target_snapped) open_set: list[AStarNode] = [] - snap = context.config.snap_size inv_snap = 1.0 / snap # (x_grid, y_grid, orientation_grid) -> min_g_cost closed_set: dict[tuple[int, int, int], float] = {} - start_node = AStarNode(start, 0.0, context.cost_evaluator.h_manhattan(start, target)) + start_node = AStarNode(start_snapped, 0.0, context.cost_evaluator.h_manhattan(start_snapped, target_snapped)) heapq.heappush(open_set, start_node) best_node = start_node @@ -193,15 +223,15 @@ def route_astar( current = heapq.heappop(open_set) # Cost Pruning (Fail Fast) - if max_cost is not None and current.f_cost > max_cost: + if max_cost is not None and current.fh_cost[0] > max_cost: metrics.pruned_cost += 1 continue if current.h_cost < best_node.h_cost: best_node = current - state = (int(round(current.port.x / snap)), int(round(current.port.y / snap)), int(round(current.port.orientation / 1.0))) - if state in closed_set and closed_set[state] <= current.g_cost + 1e-6: + state = (int(round(current.port.x * inv_snap)), int(round(current.port.y * inv_snap)), int(round(current.port.orientation))) + if state in closed_set and closed_set[state] <= current.g_cost + TOLERANCE_LINEAR: continue closed_set[state] = current.g_cost @@ -213,15 +243,15 @@ def route_astar( metrics.nodes_expanded += 1 # Check if we reached the target exactly - if (abs(current.port.x - target.x) < 1e-6 and - abs(current.port.y - target.y) < 1e-6 and - abs(current.port.orientation - target.orientation) < 0.1): + if (abs(current.port.x - target_snapped.x) < TOLERANCE_LINEAR and + abs(current.port.y - target_snapped.y) < TOLERANCE_LINEAR and + abs(current.port.orientation - target_snapped.orientation) < 0.1): return reconstruct_path(current) # Expansion expand_moves( - current, target, net_width, net_id, open_set, closed_set, - context, metrics, + current, target_snapped, net_width, net_id, open_set, closed_set, + context, metrics, congestion_cache, snap=snap, inv_snap=inv_snap, parent_state=state, max_cost=max_cost, skip_congestion=skip_congestion, self_collision_check=self_collision_check @@ -239,6 +269,7 @@ def expand_moves( closed_set: dict[tuple[int, int, int], float], context: AStarContext, metrics: AStarMetrics, + congestion_cache: dict[tuple, int], snap: float = 1.0, inv_snap: float | None = None, parent_state: tuple[int, int, int] | None = None, @@ -252,7 +283,7 @@ def expand_moves( cp = current.port if inv_snap is None: inv_snap = 1.0 / snap if parent_state is None: - parent_state = (int(round(cp.x / snap)), int(round(cp.y / snap)), int(round(cp.orientation / 1.0))) + parent_state = (int(round(cp.x * inv_snap)), int(round(cp.y * inv_snap)), int(round(cp.orientation))) dx_t = target.x - cp.x dy_t = target.y - cp.y @@ -265,12 +296,12 @@ def expand_moves( proj_t = dx_t * cos_v + dy_t * sin_v perp_t = -dx_t * sin_v + dy_t * cos_v - # A. Straight Jump + # A. Straight Jump (Only if target aligns with grid state or direct jump is enabled) if proj_t > 0 and abs(perp_t) < 1e-3 and abs(cp.orientation - target.orientation) < 0.1: max_reach = context.cost_evaluator.collision_engine.ray_cast(cp, cp.orientation, proj_t + 1.0) if max_reach >= proj_t - 0.01: process_move( - current, target, net_width, net_id, open_set, closed_set, context, metrics, + current, target, net_width, net_id, open_set, closed_set, context, metrics, congestion_cache, f'S{proj_t}', 'S', (proj_t,), skip_congestion, inv_snap=inv_snap, snap_to_grid=False, parent_state=parent_state, max_cost=max_cost, snap=snap, self_collision_check=self_collision_check ) @@ -288,12 +319,6 @@ def expand_moves( if max_reach > context.config.min_straight_length + 5.0: straight_lengths.add(snap_search_grid(max_reach - 5.0, snap)) - visible_corners = context.visibility_manager.get_visible_corners(cp, max_dist=max_reach) - for cx, cy, dist in visible_corners: - proj = (cx - cp.x) * cos_v + (cy - cp.y) * sin_v - if proj > context.config.min_straight_length: - straight_lengths.add(snap_search_grid(proj, snap)) - straight_lengths.add(context.config.min_straight_length) if max_reach > context.config.min_straight_length * 4: straight_lengths.add(snap_search_grid(max_reach / 2.0, snap)) @@ -321,7 +346,7 @@ def expand_moves( for length in sorted(straight_lengths, reverse=True): process_move( - current, target, net_width, net_id, open_set, closed_set, context, metrics, + current, target, net_width, net_id, open_set, closed_set, context, metrics, congestion_cache, f'S{length}', 'S', (length,), skip_congestion, inv_snap=inv_snap, parent_state=parent_state, max_cost=max_cost, snap=snap, self_collision_check=self_collision_check ) @@ -339,7 +364,7 @@ def expand_moves( if abs(new_diff) > 135: continue process_move( - current, target, net_width, net_id, open_set, closed_set, context, metrics, + current, target, net_width, net_id, open_set, closed_set, context, metrics, congestion_cache, f'B{radius}{direction}', 'B', (radius, direction), skip_congestion, inv_snap=inv_snap, parent_state=parent_state, max_cost=max_cost, snap=snap, self_collision_check=self_collision_check ) @@ -358,7 +383,8 @@ def expand_moves( if user_offsets is None: for sign in [-1, 1]: - for i in [0.1, 0.2, 0.5, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144]: + # Adaptive sampling: scale steps by snap_size but ensure enough range + for i in [1, 2, 5, 13, 34, 89]: o = sign * i * snap if abs(o) < 2 * max_sbend_r: offsets.add(o) @@ -366,7 +392,7 @@ def expand_moves( for radius in context.config.sbend_radii: if abs(offset) >= 2 * radius: continue process_move( - current, target, net_width, net_id, open_set, closed_set, context, metrics, + current, target, net_width, net_id, open_set, closed_set, context, metrics, congestion_cache, f'SB{offset}R{radius}', 'SB', (offset, radius), skip_congestion, inv_snap=inv_snap, parent_state=parent_state, max_cost=max_cost, snap=snap, self_collision_check=self_collision_check ) @@ -381,6 +407,7 @@ def process_move( closed_set: dict[tuple[int, int, int], float], context: AStarContext, metrics: AStarMetrics, + congestion_cache: dict[tuple, int], move_type: str, move_class: Literal['S', 'B', 'SB'], params: tuple, @@ -399,25 +426,28 @@ def process_move( if inv_snap is None: inv_snap = 1.0 / snap base_ori = float(int(cp.orientation + 0.5)) if parent_state is None: - gx = int(round(cp.x / snap)) - gy = int(round(cp.y / snap)) - go = int(round(cp.orientation / 1.0)) + gx = int(round(cp.x * inv_snap)) + gy = int(round(cp.y * inv_snap)) + go = int(round(cp.orientation)) parent_state = (gx, gy, go) else: gx, gy, go = parent_state abs_key = (parent_state, move_class, params, net_width, context.config.bend_collision_type, snap_to_grid) - if abs_key in context.move_cache: - res = context.move_cache[abs_key] + if abs_key in context.move_cache_abs: + res = context.move_cache_abs[abs_key] move_radius = params[0] if move_class == 'B' else (params[1] if move_class == 'SB' else None) add_node( - parent, res, target, net_width, net_id, open_set, closed_set, context, metrics, + parent, res, target, net_width, net_id, open_set, closed_set, context, metrics, congestion_cache, move_type, move_radius=move_radius, snap=snap, skip_congestion=skip_congestion, inv_snap=inv_snap, parent_state=parent_state, max_cost=max_cost, self_collision_check=self_collision_check ) return + # Trigger periodic cache eviction check (only on Absolute cache misses) + context.check_cache_eviction() + self_dilation = context.cost_evaluator.collision_engine.clearance / 2.0 rel_key = (base_ori, move_class, params, net_width, context.config.bend_collision_type, self_dilation, snap_to_grid) @@ -425,8 +455,8 @@ def process_move( if cache_key in context.hard_collision_set: return - if rel_key in context.move_cache: - res_rel = context.move_cache[rel_key] + if rel_key in context.move_cache_rel: + res_rel = context.move_cache_rel[rel_key] else: try: p0 = Port(0, 0, base_ori) @@ -438,15 +468,15 @@ def process_move( res_rel = SBend.generate(p0, params[0], params[1], net_width, collision_type=context.config.bend_collision_type, clip_margin=context.config.bend_clip_margin, dilation=self_dilation, snap_to_grid=snap_to_grid, snap_size=snap) else: return - context.move_cache[rel_key] = res_rel + context.move_cache_rel[rel_key] = res_rel except (ValueError, ZeroDivisionError): return res = res_rel.translate(cp.x, cp.y, rel_gx=res_rel.rel_gx + gx, rel_gy=res_rel.rel_gy + gy, rel_go=res_rel.rel_go) - context.move_cache[abs_key] = res + context.move_cache_abs[abs_key] = res move_radius = params[0] if move_class == 'B' else (params[1] if move_class == 'SB' else None) add_node( - parent, res, target, net_width, net_id, open_set, closed_set, context, metrics, + parent, res, target, net_width, net_id, open_set, closed_set, context, metrics, congestion_cache, move_type, move_radius=move_radius, snap=snap, skip_congestion=skip_congestion, inv_snap=inv_snap, parent_state=parent_state, max_cost=max_cost, self_collision_check=self_collision_check @@ -463,6 +493,7 @@ def add_node( closed_set: dict[tuple[int, int, int], float], context: AStarContext, metrics: AStarMetrics, + congestion_cache: dict[tuple, int], move_type: str, move_radius: float | None = None, snap: float = 1.0, @@ -478,14 +509,17 @@ def add_node( metrics.moves_generated += 1 state = (result.rel_gx, result.rel_gy, result.rel_go) - if state in closed_set and closed_set[state] <= parent.g_cost + 1e-6: + # Early pruning using lower-bound total cost + # child.total_g >= parent.total_g + move_length + new_lower_bound_g = parent.g_cost + result.length + if state in closed_set and closed_set[state] <= new_lower_bound_g + TOLERANCE_LINEAR: metrics.pruned_closed_set += 1 return parent_p = parent.port end_p = result.end_port if parent_state is None: - pgx, pgy, pgo = int(round(parent_p.x / snap)), int(round(parent_p.y / snap)), int(round(parent_p.orientation / 1.0)) + pgx, pgy, pgo = int(round(parent_p.x * inv_snap)), int(round(parent_p.y * inv_snap)), int(round(parent_p.orientation)) else: pgx, pgy, pgo = parent_state cache_key = (pgx, pgy, pgo, move_type, net_width) @@ -494,34 +528,29 @@ def add_node( metrics.pruned_hard_collision += 1 return - new_g_cost = parent.g_cost + result.length - - # Pre-check cost pruning before evaluation (using heuristic) - if max_cost is not None: - new_h_cost = context.cost_evaluator.h_manhattan(end_p, target) - if new_g_cost + new_h_cost > max_cost: - metrics.pruned_cost += 1 - return - is_static_safe = (cache_key in context.static_safe_cache) if not is_static_safe: ce = context.cost_evaluator.collision_engine + collision_found = False if 'S' in move_type and 'SB' not in move_type: - if ce.check_move_straight_static(parent_p, result.length): - context.hard_collision_set.add(cache_key) - metrics.pruned_hard_collision += 1 - return - is_static_safe = True - if not is_static_safe: - if ce.check_move_static(result, start_port=parent_p, end_port=end_p): - context.hard_collision_set.add(cache_key) - metrics.pruned_hard_collision += 1 - return - else: context.static_safe_cache.add(cache_key) + collision_found = ce.check_move_straight_static(parent_p, result.length) + else: + collision_found = ce.check_move_static(result, start_port=parent_p, end_port=end_p) + + if collision_found: + context.hard_collision_set.add(cache_key) + metrics.pruned_hard_collision += 1 + return + else: + context.static_safe_cache.add(cache_key) total_overlaps = 0 if not skip_congestion: - total_overlaps = context.cost_evaluator.collision_engine.check_move_congestion(result, net_id) + if cache_key in congestion_cache: + total_overlaps = congestion_cache[cache_key] + else: + total_overlaps = context.cost_evaluator.collision_engine.check_move_congestion(result, net_id) + congestion_cache[cache_key] = total_overlaps # SELF-COLLISION CHECK (Optional for performance) if self_collision_check: @@ -542,7 +571,7 @@ def add_node( penalty = 0.0 if 'SB' in move_type: penalty = context.config.sbend_penalty elif 'B' in move_type: penalty = context.config.bend_penalty - if move_radius is not None and move_radius > 1e-6: penalty *= (10.0 / move_radius)**0.5 + if move_radius is not None and move_radius > TOLERANCE_LINEAR: penalty *= (10.0 / move_radius)**0.5 move_cost = context.cost_evaluator.evaluate_move( None, result.end_port, net_width, net_id, @@ -557,7 +586,7 @@ def add_node( return g_cost = parent.g_cost + move_cost - if state in closed_set and closed_set[state] <= g_cost + 1e-6: + if state in closed_set and closed_set[state] <= g_cost + TOLERANCE_LINEAR: metrics.pruned_closed_set += 1 return diff --git a/inire/router/cost.py b/inire/router/cost.py index edec182..446de8e 100644 --- a/inire/router/cost.py +++ b/inire/router/cost.py @@ -1,9 +1,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any import numpy as np from inire.router.config import CostConfig +from inire.constants import TOLERANCE_LINEAR if TYPE_CHECKING: from shapely.geometry import Polygon @@ -18,7 +19,7 @@ class CostEvaluator: Calculates total path and proximity costs. """ __slots__ = ('collision_engine', 'danger_map', 'config', 'unit_length_cost', 'greedy_h_weight', 'congestion_penalty', - '_target_x', '_target_y', '_target_ori', '_target_cos', '_target_sin') + '_target_x', '_target_y', '_target_ori', '_target_cos', '_target_sin', '_min_radius') collision_engine: CollisionEngine """ The engine for intersection checks """ @@ -26,8 +27,8 @@ class CostEvaluator: danger_map: DangerMap """ Pre-computed grid for heuristic proximity costs """ - config: CostConfig - """ Parameter configuration """ + config: Any + """ Parameter configuration (CostConfig or RouterConfig) """ unit_length_cost: float greedy_h_weight: float @@ -37,7 +38,7 @@ class CostEvaluator: def __init__( self, collision_engine: CollisionEngine, - danger_map: DangerMap, + danger_map: DangerMap | None = None, unit_length_cost: float = 1.0, greedy_h_weight: float = 1.5, congestion_penalty: float = 10000.0, @@ -73,6 +74,9 @@ class CostEvaluator: self.unit_length_cost = self.config.unit_length_cost self.greedy_h_weight = self.config.greedy_h_weight self.congestion_penalty = self.config.congestion_penalty + + # Pre-cache configuration flags for fast path + self._refresh_cached_config() # Target cache self._target_x = 0.0 @@ -81,6 +85,22 @@ class CostEvaluator: self._target_cos = 1.0 self._target_sin = 0.0 + def _refresh_cached_config(self) -> None: + """ Sync internal caches with the current self.config object. """ + if hasattr(self.config, 'min_bend_radius'): + self._min_radius = self.config.min_bend_radius + elif hasattr(self.config, 'bend_radii') and self.config.bend_radii: + self._min_radius = min(self.config.bend_radii) + else: + self._min_radius = 50.0 + + if hasattr(self.config, 'unit_length_cost'): + self.unit_length_cost = self.config.unit_length_cost + if hasattr(self.config, 'greedy_h_weight'): + self.greedy_h_weight = self.config.greedy_h_weight + if hasattr(self.config, 'congestion_penalty'): + self.congestion_penalty = self.config.congestion_penalty + def set_target(self, target: Port) -> None: """ Pre-calculate target-dependent values for faster heuristic. """ self._target_x = target.x @@ -100,6 +120,8 @@ class CostEvaluator: Returns: Proximity cost at location. """ + if self.danger_map is None: + return 0.0 return self.danger_map.get_cost(x, y) def h_manhattan(self, current: Port, target: Port) -> float: @@ -107,14 +129,13 @@ class CostEvaluator: Heuristic: weighted Manhattan distance + mandatory turn penalties. """ tx, ty = target.x, target.y - t_ori = target.orientation # Avoid repeated trig for target orientation - if abs(tx - self._target_x) > 1e-6 or abs(ty - self._target_y) > 1e-6: + if (abs(tx - self._target_x) > TOLERANCE_LINEAR or + abs(ty - self._target_y) > TOLERANCE_LINEAR or + abs(target.orientation - self._target_ori) > 0.1): self.set_target(target) - t_cos, t_sin = self._target_cos, self._target_sin - dx = abs(current.x - tx) dy = abs(current.y - ty) dist = dx + dy @@ -123,24 +144,26 @@ class CostEvaluator: penalty = 0.0 # 1. Orientation Difference - # Optimization: use integer comparison for common orientations curr_ori = current.orientation - diff = abs(curr_ori - t_ori) % 360 + diff = abs(curr_ori - self._target_ori) % 360 if diff > 0.1: if abs(diff - 180) < 0.1: penalty += 2 * bp else: # 90 or 270 degree rotation penalty += 1 * bp + + p1 = penalty # 2. Side Check (Entry half-plane) v_dx = tx - current.x v_dy = ty - current.y - side_proj = v_dx * t_cos + v_dy * t_sin - perp_dist = abs(v_dx * t_sin - v_dy * t_cos) - min_radius = self.config.min_bend_radius + side_proj = v_dx * self._target_cos + v_dy * self._target_sin + perp_dist = abs(v_dx * self._target_sin - v_dy * self._target_cos) - if side_proj < -0.1 or (side_proj < min_radius and perp_dist > 0.1): + if side_proj < -0.1 or (side_proj < self._min_radius and perp_dist > 0.1): penalty += 2 * bp + + p2 = penalty - p1 # 3. Traveling Away # Optimization: avoid np.radians/cos/sin if current_ori is standard 0,90,180,270 @@ -155,11 +178,15 @@ class CostEvaluator: move_proj = v_dx * c_cos + v_dy * c_sin if move_proj < -0.1: penalty += 2 * bp + + p3 = penalty - p1 - p2 # 4. Jog Alignment if diff < 0.1: if perp_dist > 0.1: penalty += 2 * bp + + p4 = penalty - p1 - p2 - p3 return self.greedy_h_weight * (dist + penalty) @@ -199,8 +226,9 @@ class CostEvaluator: # 1. Boundary Check danger_map = self.danger_map - if not danger_map.is_within_bounds(end_port.x, end_port.y): - return 1e15 + if danger_map is not None: + if not danger_map.is_within_bounds(end_port.x, end_port.y): + return 1e15 total_cost = length * self.unit_length_cost + penalty @@ -229,5 +257,6 @@ class CostEvaluator: total_cost += overlaps * self.congestion_penalty # 3. Proximity cost from Danger Map - total_cost += danger_map.get_cost(end_port.x, end_port.y) + if danger_map is not None: + total_cost += danger_map.get_cost(end_port.x, end_port.y) return total_cost diff --git a/inire/router/danger_map.py b/inire/router/danger_map.py index bc85537..db75eee 100644 --- a/inire/router/danger_map.py +++ b/inire/router/danger_map.py @@ -119,9 +119,16 @@ class DangerMap: Returns: Pre-computed cost, or 1e15 if out of bounds. """ + # Clamp to grid range to handle upper boundary exactly ix = int((x - self.minx) / self.resolution) iy = int((y - self.miny) / self.resolution) + + # Handle exact upper boundary + if ix == self.width_cells and abs(x - self.maxx) < 1e-9: + ix = self.width_cells - 1 + if iy == self.height_cells and abs(y - self.maxy) < 1e-9: + iy = self.height_cells - 1 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 1e15 # Outside bounds diff --git a/inire/router/pathfinder.py b/inire/router/pathfinder.py index 781bd70..51aaab4 100644 --- a/inire/router/pathfinder.py +++ b/inire/router/pathfinder.py @@ -7,6 +7,7 @@ from dataclasses import dataclass from typing import TYPE_CHECKING, Callable, Literal, Any from inire.router.astar import route_astar, AStarMetrics +from inire.constants import TOLERANCE_LINEAR if TYPE_CHECKING: from inire.geometry.components import ComponentResult @@ -278,8 +279,6 @@ class PathFinder: base_node_limit = self.context.config.node_limit current_node_limit = base_node_limit - if net_id in results and not results[net_id].reached_target: - current_node_limit = base_node_limit * (iteration + 1) net_start = time.monotonic() @@ -297,19 +296,21 @@ class PathFinder: logger.debug(f' Net {net_id} routed in {time.monotonic() - net_start:.4f}s using {coll_model}') if path: + # Check if reached exactly (relative to snapped target) + last_p = path[-1].end_port + snap = self.context.config.snap_size + from inire.geometry.components import snap_search_grid + reached = (abs(last_p.x - snap_search_grid(target.x, snap)) < TOLERANCE_LINEAR and + abs(last_p.y - snap_search_grid(target.y, snap)) < TOLERANCE_LINEAR and + abs(last_p.orientation - target.orientation) < 0.1) + # Check for self-collision if not already handled by router - if net_id not in needs_sc: + if reached and net_id not in needs_sc: if self._has_self_collision(path): logger.info(f' Net {net_id} detected self-collision. Enabling protection for next iteration.') needs_sc.add(net_id) any_congestion = True - # Check if reached exactly - last_p = path[-1].end_port - reached = (abs(last_p.x - target.x) < 1e-6 and - abs(last_p.y - target.y) < 1e-6 and - abs(last_p.orientation - target.orientation) < 0.1) - # 3. Add to index (even if partial) so other nets negotiate around it all_geoms = [] all_dilated = [] @@ -356,22 +357,20 @@ class PathFinder: if other_net_id != net_id: collision_count += 1 - if collision_count > 0: - any_congestion = True + if collision_count > 0: + any_congestion = True logger.debug(f' Net {net_id}: reached={reached}, collisions={collision_count}') - results[net_id] = RoutingResult(net_id, path, (reached and collision_count == 0), collision_count, reached_target=reached) + results[net_id] = RoutingResult(net_id, path, (collision_count == 0 and reached), collision_count, reached_target=reached) else: results[net_id] = RoutingResult(net_id, [], False, 0, reached_target=False) - any_congestion = True + any_congestion = True # Total failure might need a retry with different ordering if iteration_callback: iteration_callback(iteration, results) if not any_congestion: - all_reached = all(r.reached_target for r in results.values()) - if all_reached: - break + break self.cost_evaluator.congestion_penalty *= self.congestion_multiplier @@ -392,6 +391,11 @@ class PathFinder: if not res or not res.path: final_results[net_id] = RoutingResult(net_id, [], False, 0) continue + + if not res.reached_target: + # Skip re-verification for partial paths to avoid massive performance hit + final_results[net_id] = res + continue collision_count = 0 verif_geoms = [] @@ -425,8 +429,10 @@ class PathFinder: target_p = netlist[net_id][1] last_p = res.path[-1].end_port - reached = (abs(last_p.x - target_p.x) < 1e-6 and - abs(last_p.y - target_p.y) < 1e-6 and + snap = self.context.config.snap_size + from inire.geometry.components import snap_search_grid + reached = (abs(last_p.x - snap_search_grid(target_p.x, snap)) < TOLERANCE_LINEAR and + abs(last_p.y - snap_search_grid(target_p.y, snap)) < TOLERANCE_LINEAR and abs(last_p.orientation - target_p.orientation) < 0.1) final_results[net_id] = RoutingResult(net_id, res.path, (collision_count == 0 and reached), collision_count, reached_target=reached) diff --git a/inire/tests/test_astar.py b/inire/tests/test_astar.py index 0bb622b..802b9eb 100644 --- a/inire/tests/test_astar.py +++ b/inire/tests/test_astar.py @@ -79,6 +79,11 @@ def test_astar_snap_to_target_lookahead(basic_evaluator: CostEvaluator) -> None: assert path is not None result = RoutingResult(net_id="test", path=path, is_valid=True, collisions=0) - validation = validate_routing_result(result, [], clearance=2.0, expected_start=start, expected_end=target) + + # Under the new Enforce Grid policy, the router snaps the target internally to 10.0. + # We validate against the snapped target. + from inire.geometry.components import snap_search_grid + target_snapped = Port(snap_search_grid(target.x, 1.0), snap_search_grid(target.y, 1.0), target.orientation, snap=False) + validation = validate_routing_result(result, [], clearance=2.0, expected_start=start, expected_end=target_snapped) assert validation["is_valid"], f"Validation failed: {validation.get('reason')}" diff --git a/inire/tests/test_variable_grid.py b/inire/tests/test_variable_grid.py new file mode 100644 index 0000000..a1b03f3 --- /dev/null +++ b/inire/tests/test_variable_grid.py @@ -0,0 +1,66 @@ +import unittest +from inire.geometry.primitives import Port +from inire.router.astar import route_astar, AStarContext +from inire.router.cost import CostEvaluator +from inire.geometry.collision import CollisionEngine +from inire.geometry.components import snap_search_grid + +class TestVariableGrid(unittest.TestCase): + def setUp(self): + self.ce = CollisionEngine(clearance=2.0) + self.cost = CostEvaluator(self.ce) + + def test_grid_1_0(self): + """ Test routing with a 1.0um grid. """ + context = AStarContext(self.cost, snap_size=1.0) + start = Port(0.0, 0.0, 0.0) + # 12.3 should snap to 12.0 on a 1.0um grid + target = Port(12.3, 0.0, 0.0, snap=False) + + path = route_astar(start, target, net_width=1.0, context=context) + + self.assertIsNotNone(path) + last_port = path[-1].end_port + self.assertEqual(last_port.x, 12.0) + + # Verify component relative grid coordinates + # rel_gx = round(x / snap) + # For x=12.0, snap=1.0 -> rel_gx=12 + self.assertEqual(path[-1].rel_gx, 12) + + def test_grid_2_5(self): + """ Test routing with a 2.5um grid. """ + context = AStarContext(self.cost, snap_size=2.5) + start = Port(0.0, 0.0, 0.0) + # 7.5 is a multiple of 2.5, should be reached exactly + target = Port(7.5, 0.0, 0.0, snap=False) + + path = route_astar(start, target, net_width=1.0, context=context) + + self.assertIsNotNone(path) + last_port = path[-1].end_port + self.assertEqual(last_port.x, 7.5) + + # rel_gx = 7.5 / 2.5 = 3 + self.assertEqual(path[-1].rel_gx, 3) + + def test_grid_10_0(self): + """ Test routing with a large 10.0um grid. """ + context = AStarContext(self.cost, snap_size=10.0) + start = Port(0.0, 0.0, 0.0) + # 15.0 should snap to 20.0 (ties usually round to even or nearest, + # but 15.0 is exactly between 10 and 20. + # snap_search_grid uses round(val/snap)*snap. round(1.5) is 2 in Python 3. + target = Port(15.0, 0.0, 0.0, snap=False) + + path = route_astar(start, target, net_width=1.0, context=context) + + self.assertIsNotNone(path) + last_port = path[-1].end_port + self.assertEqual(last_port.x, 20.0) + + # rel_gx = 20.0 / 10.0 = 2 + self.assertEqual(path[-1].rel_gx, 2) + +if __name__ == '__main__': + unittest.main() diff --git a/inire/utils/validation.py b/inire/utils/validation.py index eaacd42..ed0fae0 100644 --- a/inire/utils/validation.py +++ b/inire/utils/validation.py @@ -3,6 +3,8 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any import numpy +from inire.constants import TOLERANCE_LINEAR + if TYPE_CHECKING: from shapely.geometry import Polygon from inire.geometry.primitives import Port @@ -75,7 +77,7 @@ def validate_routing_result( for j, seg_j in enumerate(dilated_for_self): if j > i + 1 and seg_i.intersects(seg_j): # Non-adjacent overlap = seg_i.intersection(seg_j) - if overlap.area > 1e-6: + if overlap.area > TOLERANCE_LINEAR: self_intersection_geoms.append((i, j, overlap)) is_valid = (len(obstacle_collision_geoms) == 0 and