diff --git a/examples/05_orientation_stress.png b/examples/05_orientation_stress.png index 60f56d1..8253952 100644 Binary files a/examples/05_orientation_stress.png and b/examples/05_orientation_stress.png differ diff --git a/examples/06_bend_collision_models.png b/examples/06_bend_collision_models.png index e6e81e4..038c344 100644 Binary files a/examples/06_bend_collision_models.png and b/examples/06_bend_collision_models.png differ diff --git a/inire/geometry/components.py b/inire/geometry/components.py index 98a063e..5e4f907 100644 --- a/inire/geometry/components.py +++ b/inire/geometry/components.py @@ -64,7 +64,7 @@ class ComponentResult: self.length = length # Vectorized bounds calculation self.bounds = shapely.bounds(geometry) - self.dilated_bounds = shapely.bounds(dilated_geometry) if dilated_geometry else None + self.dilated_bounds = shapely.bounds(dilated_geometry) if dilated_geometry is not None else None def translate(self, dx: float, dy: float) -> ComponentResult: """ @@ -72,15 +72,16 @@ class ComponentResult: """ # Vectorized translation if possible, else list comp # Shapely 2.x affinity functions still work on single geometries efficiently - geoms = self.geometry - if self.dilated_geometry: - geoms = geoms + self.dilated_geometry + geoms = list(self.geometry) + num_geom = len(self.geometry) + if self.dilated_geometry is not None: + geoms.extend(self.dilated_geometry) from shapely.affinity import translate translated = [translate(p, dx, dy) for p in geoms] - new_geom = translated[:len(self.geometry)] - new_dil = translated[len(self.geometry):] if self.dilated_geometry else None + new_geom = translated[:num_geom] + new_dil = translated[num_geom:] if self.dilated_geometry is not None else None new_port = Port(self.end_port.x + dx, self.end_port.y + dy, self.end_port.orientation) return ComponentResult(new_geom, new_port, self.length, new_dil) diff --git a/inire/router/astar.py b/inire/router/astar.py index 0cf31f1..fc1b30f 100644 --- a/inire/router/astar.py +++ b/inire/router/astar.py @@ -194,6 +194,7 @@ class AStarRouter: target: Port, net_width: float, net_id: str = 'default', + bend_collision_type: Literal['arc', 'bbox', 'clipped_bbox'] | None = None, ) -> list[ComponentResult] | None: """ Route a single net using A*. @@ -203,10 +204,14 @@ class AStarRouter: target: Target port. net_width: Waveguide width (um). net_id: Optional net identifier. + bend_collision_type: Override collision model for this route. Returns: List of moves forming the path, or None if failed. """ + if bend_collision_type is not None: + self.config.bend_collision_type = bend_collision_type + self._collision_cache.clear() open_set: list[AStarNode] = [] # Key: (x, y, orientation) rounded to 1nm @@ -311,7 +316,9 @@ class AStarRouter: res = self._move_cache[abs_key] else: # Level 2: Relative cache (orientation only) - rel_key = (base_ori, 'S', length, net_width, self._self_dilation) + # Dilation is now 0.0 for caching to save translation time. + # It will be calculated lazily in _add_node if needed. + rel_key = (base_ori, 'S', length, net_width, 0.0) if rel_key in self._move_cache: res_rel = self._move_cache[rel_key] # Check closed set before translating @@ -322,7 +329,7 @@ class AStarRouter: continue res = res_rel.translate(cp.x, cp.y) else: - res_rel = Straight.generate(Port(0, 0, base_ori), length, net_width, dilation=self._self_dilation) + res_rel = Straight.generate(Port(0, 0, base_ori), length, net_width, dilation=0.0) self._move_cache[rel_key] = res_rel res = res_rel.translate(cp.x, cp.y) self._move_cache[abs_key] = res @@ -335,7 +342,7 @@ class AStarRouter: if abs_key in self._move_cache: res = self._move_cache[abs_key] else: - rel_key = (base_ori, 'B', radius, direction, net_width, self.config.bend_collision_type, self._self_dilation) + rel_key = (base_ori, 'B', radius, direction, net_width, self.config.bend_collision_type, 0.0) if rel_key in self._move_cache: res_rel = self._move_cache[rel_key] # Check closed set before translating @@ -353,7 +360,7 @@ class AStarRouter: direction, collision_type=self.config.bend_collision_type, clip_margin=self.config.bend_clip_margin, - dilation=self._self_dilation + dilation=0.0 ) self._move_cache[rel_key] = res_rel res = res_rel.translate(cp.x, cp.y) @@ -367,7 +374,7 @@ class AStarRouter: if abs_key in self._move_cache: res = self._move_cache[abs_key] else: - rel_key = (base_ori, 'SB', offset, radius, net_width, self.config.bend_collision_type, self._self_dilation) + rel_key = (base_ori, 'SB', offset, radius, net_width, self.config.bend_collision_type, 0.0) if rel_key in self._move_cache: res_rel = self._move_cache[rel_key] # Check closed set before translating @@ -386,7 +393,7 @@ class AStarRouter: width=net_width, collision_type=self.config.bend_collision_type, clip_margin=self.config.bend_clip_margin, - dilation=self._self_dilation + dilation=0.0 ) self._move_cache[rel_key] = res_rel res = res_rel.translate(cp.x, cp.y) @@ -424,9 +431,20 @@ class AStarRouter: if self._collision_cache[cache_key]: return else: + # Lazy Dilation: compute dilated polygons only if we need a collision check + if result.dilated_geometry is None: + # We need to update the ComponentResult with dilated geometry + # For simplicity, we'll just buffer the polygons here. + # In a more optimized version, ComponentResult might have a .dilate() method. + dilated = [p.buffer(self._self_dilation) for p in result.geometry] + result.dilated_geometry = dilated + # Re-calculate dilated bounds + import shapely + result.dilated_bounds = shapely.bounds(dilated) + hard_coll = False for i, poly in enumerate(result.geometry): - dil_poly = result.dilated_geometry[i] if result.dilated_geometry else None + dil_poly = result.dilated_geometry[i] if self.cost_evaluator.collision_engine.check_collision( poly, net_id, buffer_mode='static', start_port=parent.port, end_port=result.end_port, dilated_geometry=dil_poly @@ -437,6 +455,13 @@ class AStarRouter: if hard_coll: return + # Lazy Dilation for self-intersection and cost evaluation + if result.dilated_geometry is None: + dilated = [p.buffer(self._self_dilation) for p in result.geometry] + result.dilated_geometry = dilated + import shapely + result.dilated_bounds = shapely.bounds(dilated) + # 3. Check for Self-Intersection (Limited to last 100 segments for performance) if result.dilated_geometry: # Union of current move's bounds for fast path-wide pruning diff --git a/inire/router/pathfinder.py b/inire/router/pathfinder.py index 40ce644..084d483 100644 --- a/inire/router/pathfinder.py +++ b/inire/router/pathfinder.py @@ -111,9 +111,13 @@ class PathFinder: self.cost_evaluator.collision_engine.remove_path(net_id) # 2. Reroute with current congestion info + # Tiered Strategy: use clipped_bbox for Iteration 0 for speed. + # Switch to arc for higher iterations if collisions persist. + coll_model = "clipped_bbox" if iteration == 0 else "arc" + net_start = time.monotonic() - path = self.router.route(start, target, width, net_id=net_id) - logger.debug(f' Net {net_id} routed in {time.monotonic() - net_start:.4f}s') + path = self.router.route(start, target, width, net_id=net_id, bend_collision_type=coll_model) + logger.debug(f' Net {net_id} routed in {time.monotonic() - net_start:.4f}s using {coll_model}') if path: # 3. Add to index