diff --git a/DOCS.md b/DOCS.md index ea6292c..b6c8a1b 100644 --- a/DOCS.md +++ b/DOCS.md @@ -6,23 +6,23 @@ This document describes the user-tunable parameters for the `inire` auto-router. The `AStarRouter` is the core pathfinding engine. It can be configured directly through its constructor. -| Parameter | Type | Default | Description | -| :--- | :--- | :--- | :--- | -| `node_limit` | `int` | 1,000,000 | Maximum number of states to explore per net. Increase for very complex paths. | -| `straight_lengths` | `list[float]` | `[1.0, 5.0, 25.0]` | Discrete step sizes for straight waveguides (µm). Larger steps speed up search in open space. | -| `bend_radii` | `list[float]` | `[10.0]` | Available radii for 90-degree turns (µm). Multiple values allow the router to pick the best fit. | -| `sbend_offsets` | `list[float]` | `[-5, -2, 2, 5]` | Lateral offsets for parametric S-bends (µm). | -| `sbend_radii` | `list[float]` | `[10.0]` | Available radii for S-bends (µm). | -| `snap_to_target_dist`| `float` | 20.0 | Distance (µm) at which the router attempts an exact bridge to the target port. | -| `bend_penalty` | `float` | 50.0 | Flat cost added for every 90-degree bend. Higher values favor straight lines. | -| `sbend_penalty` | `float` | 100.0 | Flat cost added for every S-bend. Usually higher than `bend_penalty`. | -| `bend_collision_type`| `str` | `"arc"` | Collision model for bends: `"arc"`, `"bbox"`, or `"clipped_bbox"`. | -| `bend_clip_margin` | `float` | 10.0 | Margin (µm) for the `"clipped_bbox"` collision model. | +| Parameter | Type | Default | Description | +| :-------------------- | :------------ | :----------------- | :------------------------------------------------------------------------------------ | +| `node_limit` | `int` | 1,000,000 | Maximum number of states to explore per net. Increase for very complex paths. | +| `straight_lengths` | `list[float]` | `[1.0, 5.0, 25.0]` | Discrete step sizes for straight waveguides (µm). Larger steps speed up search. | +| `bend_radii` | `list[float]` | `[10.0]` | Available radii for 90-degree turns (µm). Multiple values allow best-fit selection. | +| `sbend_offsets` | `list[float]` | `[-5, -2, 2, 5]` | Lateral offsets for parametric S-bends (µm). | +| `sbend_radii` | `list[float]` | `[10.0]` | Available radii for S-bends (µm). | +| `snap_to_target_dist` | `float` | 20.0 | Distance (µm) at which the router attempts an exact bridge to the target port. | +| `bend_penalty` | `float` | 50.0 | Flat cost added for every 90-degree bend. Higher values favor straight lines. | +| `sbend_penalty` | `float` | 100.0 | Flat cost added for every S-bend. Usually higher than `bend_penalty`. | +| `bend_collision_type` | `str` | `"arc"` | Collision model for bends: `"arc"`, `"bbox"`, or `"clipped_bbox"`. | +| `bend_clip_margin` | `float` | 10.0 | Extra space (µm) around the waveguide before the bounding box corners are clipped. | ### Bend Collision Models * `"arc"`: High-fidelity model following the exact curved waveguide geometry. * `"bbox"`: Conservative model using the axis-aligned bounding box of the bend. Fast but blocks more space. -* `"clipped_bbox"`: A middle ground that uses the bounding box but clips corners that are far from the waveguide. +* `"clipped_bbox"`: A middle ground that starts with the bounding box but applies 45-degree linear cuts to the inner and outer corners. The `bend_clip_margin` defines the extra safety distance from the waveguide edge to the cut line. --- @@ -30,11 +30,11 @@ The `AStarRouter` is the core pathfinding engine. It can be configured directly The `CostEvaluator` defines the "goodness" of a path. -| Parameter | Type | Default | Description | -| :--- | :--- | :--- | :--- | -| `unit_length_cost` | `float` | 1.0 | Cost per µm of wire length. | -| `greedy_h_weight` | `float` | 1.1 | Heuristic weight. `1.0` is optimal; higher values (e.g., `1.5`) are faster but may produce longer paths. | -| `congestion_penalty`| `float` | 10,000.0 | Multiplier for overlaps in the multi-net Negotiated Congestion loop. | +| Parameter | Type | Default | Description | +| :------------------- | :------ | :--------- | :--------------------------------------------------------------------------------------- | +| `unit_length_cost` | `float` | 1.0 | Cost per µm of wire length. | +| `greedy_h_weight` | `float` | 1.1 | Heuristic weight. `1.0` is optimal; higher values (e.g. `1.5`) speed up search. | +| `congestion_penalty` | `float` | 10,000.0 | Multiplier for overlaps in the multi-net Negotiated Congestion loop. | --- @@ -42,19 +42,19 @@ The `CostEvaluator` defines the "goodness" of a path. The `PathFinder` orchestrates multi-net routing using the Negotiated Congestion algorithm. -| Parameter | Type | Default | Description | -| :--- | :--- | :--- | :--- | -| `max_iterations` | `int` | 10 | Maximum number of rip-up and reroute iterations to resolve congestion. | -| `base_congestion_penalty` | `float` | 100.0 | Starting penalty for overlaps. This value is multiplied by `1.5` each iteration if congestion persists. | +| Parameter | Type | Default | Description | +| :------------------------ | :------ | :------ | :-------------------------------------------------------------------------------------- | +| `max_iterations` | `int` | 10 | Maximum number of rip-up and reroute iterations to resolve congestion. | +| `base_congestion_penalty` | `float` | 100.0 | Starting penalty for overlaps. Multiplied by `1.5` each iteration if congestion remains.| --- ## 4. CollisionEngine Parameters -| Parameter | Type | Default | Description | -| :--- | :--- | :--- | :--- | -| `clearance` | `float` | (Required) | Minimum required distance between any two waveguides or obstacles (µm). | -| `safety_zone_radius`| `float` | 0.0021 | Radius (µm) around ports where collisions are ignored to allow PDK boundary incidence. | +| Parameter | Type | Default | Description | +| :------------------- | :------ | :--------- | :------------------------------------------------------------------------------------ | +| `clearance` | `float` | (Required) | Minimum required distance between any two waveguides or obstacles (µm). | +| `safety_zone_radius` | `float` | 0.0021 | Radius (µm) around ports where collisions are ignored for PDK boundary incidence. | --- @@ -62,6 +62,7 @@ The `PathFinder` orchestrates multi-net routing using the Negotiated Congestion - **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**. +- **Design Bounds**: The boundary limits defined in `DangerMap` strictly constrain the **physical edges** (dilated geometry) of the waveguide. Any move that would cause the waveguide or its required clearance to extend beyond these bounds is rejected with an infinite cost. --- diff --git a/README.md b/README.md index af1915f..dffee6d 100644 --- a/README.md +++ b/README.md @@ -74,12 +74,17 @@ Check the `examples/` directory for ready-to-run scripts demonstrating core feat * **`examples/03_locked_paths.py`**: Incremental workflow using `lock_net()` to route around previously fixed paths. Generates `03_locked_paths.png`. * **`examples/04_sbends_and_radii.py`**: Complex paths using parametric S-bends and multiple bend radii. Generates `04_sbends_and_radii.png`. * **`examples/05_orientation_stress.py`**: Stress test for various port orientation combinations (U-turns, opposite directions). Generates `05_orientation_stress.png`. +* **`examples/06_bend_collision_models.py`**: Comparison of different collision models for bends (Arc vs. BBox vs. Clipped BBox). Generates `06_bend_collision_models.png`. Run an example: ```bash python3 examples/01_simple_route.py ``` +## Documentation + +Full documentation for all user-tunable parameters, cost functions, and collision models can be found in **[DOCS.md](DOCS.md)**. + ## Architecture `inire` operates on a **State-Lattice** defined by $(x, y, \theta)$. From any state, the router expands via three primary "Move" types: diff --git a/examples/02_congestion_resolution.py b/examples/02_congestion_resolution.py index 34e39f5..3485eb9 100644 --- a/examples/02_congestion_resolution.py +++ b/examples/02_congestion_resolution.py @@ -28,7 +28,7 @@ def main() -> None: "vertical_up": (Port(45, 10, 90), Port(45, 90, 90)), "vertical_down": (Port(55, 90, 270), Port(55, 10, 270)), } - net_widths = {nid: 2.0 for nid in netlist} + net_widths = dict.fromkeys(netlist, 2.0) # 3. Route with Negotiated Congestion # We increase the base penalty to encourage faster divergence diff --git a/examples/03_locked_paths.py b/examples/03_locked_paths.py index ad630ed..4937783 100644 --- a/examples/03_locked_paths.py +++ b/examples/03_locked_paths.py @@ -28,7 +28,7 @@ def main() -> None: "bus_2": (Port(10, 60, 0), Port(110, 65, 0)), } print("Phase 1: Routing bus (3 nets)...") - results_p1 = pf.route_all(netlist_p1, {nid: 2.0 for nid in netlist_p1}) + results_p1 = pf.route_all(netlist_p1, dict.fromkeys(netlist_p1, 2.0)) # Lock all Phase 1 nets path_polys = [] @@ -50,10 +50,10 @@ def main() -> None: "cross_left": (Port(30, 10, 90), Port(30, 110, 90)), "cross_right": (Port(80, 110, 270), Port(80, 10, 270)), # Top to bottom } - + print("Phase 2: Routing crossing nets around locked bus...") # We use a slightly different width for variety - results_p2 = pf.route_all(netlist_p2, {nid: 1.5 for nid in netlist_p2}) + results_p2 = pf.route_all(netlist_p2, dict.fromkeys(netlist_p2, 1.5)) # 4. Check Results for nid, res in results_p2.items(): diff --git a/examples/04_sbends_and_radii.py b/examples/04_sbends_and_radii.py index 3b8fa28..8a7402e 100644 --- a/examples/04_sbends_and_radii.py +++ b/examples/04_sbends_and_radii.py @@ -1,4 +1,3 @@ -from shapely.geometry import Polygon from inire.geometry.collision import CollisionEngine from inire.geometry.primitives import Port diff --git a/examples/05_orientation_stress.py b/examples/05_orientation_stress.py index a84306d..ddff8b9 100644 --- a/examples/05_orientation_stress.py +++ b/examples/05_orientation_stress.py @@ -16,26 +16,26 @@ def main() -> None: 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} + net_widths = dict.fromkeys(netlist, 2.0) # 3. Route results = pf.route_all(netlist, net_widths) diff --git a/examples/06_bend_collision_models.png b/examples/06_bend_collision_models.png new file mode 100644 index 0000000..9088508 Binary files /dev/null and b/examples/06_bend_collision_models.png differ diff --git a/examples/06_bend_collision_models.py b/examples/06_bend_collision_models.py new file mode 100644 index 0000000..30fc4b5 --- /dev/null +++ b/examples/06_bend_collision_models.py @@ -0,0 +1,70 @@ +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.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 06: Bend Collision Models...") + + # 1. Setup Environment + # Give room for 10um bends near the edges + bounds = (-20, -20, 170, 170) + engine = CollisionEngine(clearance=2.0) + danger_map = DangerMap(bounds=bounds) + + # Create three scenarios with identical obstacles + # We'll space them out vertically + obs_arc = Polygon([(40, 110), (60, 110), (60, 130), (40, 130)]) + obs_bbox = Polygon([(40, 60), (60, 60), (60, 80), (40, 80)]) + obs_clipped = Polygon([(40, 10), (60, 10), (60, 30), (40, 30)]) + + obstacles = [obs_arc, obs_bbox, obs_clipped] + for obs in obstacles: + engine.add_static_obstacle(obs) + danger_map.precompute(obstacles) + + # We'll run three separate routers since collision_type is a router-level config + evaluator = CostEvaluator(engine, danger_map) + + # Scenario 1: Standard 'arc' model (High fidelity) + router_arc = AStarRouter(evaluator, bend_collision_type="arc") + netlist_arc = {"arc_model": (Port(10, 120, 0), Port(90, 140, 90))} + + # Scenario 2: 'bbox' model (Conservative axis-aligned box) + router_bbox = AStarRouter(evaluator, bend_collision_type="bbox") + netlist_bbox = {"bbox_model": (Port(10, 70, 0), Port(90, 90, 90))} + + # Scenario 3: 'clipped_bbox' model (Balanced) + router_clipped = AStarRouter(evaluator, bend_collision_type="clipped_bbox", bend_clip_margin=1.0) + netlist_clipped = {"clipped_model": (Port(10, 20, 0), Port(90, 40, 90))} + + # 2. Route each scenario + print("Routing Scenario 1 (Arc)...") + res_arc = PathFinder(router_arc, evaluator).route_all(netlist_arc, {"arc_model": 2.0}) + + print("Routing Scenario 2 (BBox)...") + res_bbox = PathFinder(router_bbox, evaluator).route_all(netlist_bbox, {"bbox_model": 2.0}) + + print("Routing Scenario 3 (Clipped BBox)...") + res_clipped = PathFinder(router_clipped, evaluator).route_all(netlist_clipped, {"clipped_model": 2.0}) + + # 3. Combine results for visualization + all_results = {**res_arc, **res_bbox, **res_clipped} + 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") + + +if __name__ == "__main__": + main() diff --git a/inire/geometry/collision.py b/inire/geometry/collision.py index 1faf6e2..a85f75e 100644 --- a/inire/geometry/collision.py +++ b/inire/geometry/collision.py @@ -3,8 +3,7 @@ from __future__ import annotations from typing import TYPE_CHECKING import rtree -from shapely.geometry import Point, Polygon -from shapely.ops import unary_union +from shapely.geometry import Polygon from shapely.prepared import prep if TYPE_CHECKING: @@ -129,19 +128,16 @@ class CollisionEngine: # Precise check: is every point in the intersection close to either port? ix_minx, ix_miny, ix_maxx, ix_maxy = intersection.bounds - + is_near_start = False - if start_port: - if (abs(ix_minx - start_port.x) < self.safety_zone_radius and abs(ix_maxx - start_port.x) < self.safety_zone_radius and - abs(ix_miny - start_port.y) < self.safety_zone_radius and abs(ix_maxy - start_port.y) < self.safety_zone_radius): - is_near_start = True - + if start_port and (abs(ix_minx - start_port.x) < self.safety_zone_radius and abs(ix_maxx - start_port.x) < self.safety_zone_radius and + abs(ix_miny - start_port.y) < self.safety_zone_radius and abs(ix_maxy - start_port.y) < self.safety_zone_radius): + is_near_start = True + is_near_end = False - if end_port: - if (abs(ix_minx - end_port.x) < self.safety_zone_radius and abs(ix_maxx - end_port.x) < self.safety_zone_radius and - abs(ix_miny - end_port.y) < self.safety_zone_radius and abs(ix_maxy - end_port.y) < self.safety_zone_radius): - is_near_end = True - + if end_port and (abs(ix_minx - end_port.x) < self.safety_zone_radius and abs(ix_maxx - end_port.x) < self.safety_zone_radius and + abs(ix_miny - end_port.y) < self.safety_zone_radius and abs(ix_maxy - end_port.y) < self.safety_zone_radius): + is_near_end = True if is_near_start or is_near_end: continue diff --git a/inire/geometry/components.py b/inire/geometry/components.py index da238be..82df9b5 100644 --- a/inire/geometry/components.py +++ b/inire/geometry/components.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import NamedTuple, Literal, Union +from typing import NamedTuple, Literal, Any import numpy as np from shapely.geometry import Polygon, box @@ -35,7 +35,7 @@ class Straight: ex = start_port.x + dx ey = start_port.y + dy - + if snap_to_grid: ex = snap_search_grid(ex) ey = snap_search_grid(ey) @@ -84,39 +84,83 @@ def _get_arc_polygons(cx: float, cy: float, radius: float, width: float, t_start def _apply_collision_model( - arc_poly: Polygon, + arc_poly: Polygon, collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon, radius: float, + width: float, + cx: float = 0.0, + cy: float = 0.0, clip_margin: float = 10.0 ) -> list[Polygon]: """Applies the specified collision model to an arc geometry.""" if isinstance(collision_type, Polygon): return [collision_type] - + if collision_type == "arc": return [arc_poly] - + # Get bounding box minx, miny, maxx, maxy = arc_poly.bounds bbox = box(minx, miny, maxx, maxy) - + if collision_type == "bbox": return [bbox] - + if collision_type == "clipped_bbox": - safe_zone = arc_poly.buffer(clip_margin) - return [bbox.intersection(safe_zone)] - + res_poly = bbox + + # Determine quadrant signs from arc centroid relative to center + # This ensures we always cut 'into' the box correctly + ac = arc_poly.centroid + sx = 1.0 if ac.x >= cx else -1.0 + sy = 1.0 if ac.y >= cy else -1.0 + + r_out_cut = radius + width / 2.0 + clip_margin + r_in_cut = radius - width / 2.0 - clip_margin + + corners = [(minx, miny), (minx, maxy), (maxx, miny), (maxx, maxy)] + for px, py in corners: + dx, dy = px - cx, py - cy + dist = np.sqrt(dx**2 + dy**2) + + if dist > r_out_cut: + # Outer corner: remove part furthest from center + # We want minimum distance to line to be r_out_cut + d_cut = r_out_cut * np.sqrt(2) + elif r_in_cut > 0 and dist < r_in_cut: + # Inner corner: remove part closest to center + # We want maximum distance to line to be r_in_cut + d_cut = r_in_cut + else: + continue + + # The cut line is sx*(x-cx) + sy*(y-cy) = d_cut + # sx*x + sy*y = sx*cx + sy*cy + d_cut + val = cx * sx + cy * sy + d_cut + + try: + p1 = (px, py) + p2 = (px, (val - sx * px) / sy) + p3 = ((val - sy * py) / sx, py) + + triangle = Polygon([p1, p2, p3]) + if triangle.is_valid and triangle.area > 1e-9: + res_poly = res_poly.difference(triangle) + except ZeroDivisionError: + continue + + return [res_poly] + return [arc_poly] class Bend90: @staticmethod def generate( - start_port: Port, - radius: float, - width: float, - direction: str = "CW", + start_port: Port, + radius: float, + width: float, + direction: str = "CW", sagitta: float = 0.01, collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc", clip_margin: float = 10.0 @@ -133,9 +177,11 @@ class Bend90: ex = snap_search_grid(cx + radius * np.cos(t_end)) ey = snap_search_grid(cy + radius * np.sin(t_end)) end_port = Port(ex, ey, float((start_port.orientation + turn_angle) % 360)) - + arc_polys = _get_arc_polygons(cx, cy, radius, width, t_start, t_end, sagitta) - collision_polys = _apply_collision_model(arc_polys[0], collision_type, radius, clip_margin) + collision_polys = _apply_collision_model( + arc_polys[0], collision_type, radius, width, cx, cy, clip_margin + ) return ComponentResult(geometry=collision_polys, end_port=end_port, length=radius * np.pi / 2.0) @@ -143,10 +189,10 @@ class Bend90: class SBend: @staticmethod def generate( - start_port: Port, - offset: float, - radius: float, - width: float, + start_port: Port, + offset: float, + radius: float, + width: float, sagitta: float = 0.01, collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc", clip_margin: float = 10.0 @@ -162,7 +208,7 @@ class SBend: ex = snap_search_grid(start_port.x + dx * np.cos(rad_start) - dy * np.sin(rad_start)) ey = snap_search_grid(start_port.y + dx * np.sin(rad_start) + dy * np.cos(rad_start)) end_port = Port(ex, ey, start_port.orientation) - + direction = 1 if offset > 0 else -1 c1_angle = rad_start + direction * np.pi / 2 cx1 = start_port.x + radius * np.cos(c1_angle) @@ -180,6 +226,14 @@ class SBend: arc1 = _get_arc_polygons(cx1, cy1, radius, width, ts1, te1, sagitta)[0] arc2 = _get_arc_polygons(cx2, cy2, radius, width, ts2, te2, sagitta)[0] combined_arc = unary_union([arc1, arc2]) - - collision_polys = _apply_collision_model(combined_arc, collision_type, radius, clip_margin) + + if collision_type == "clipped_bbox": + col1 = _apply_collision_model(arc1, collision_type, radius, width, cx1, cy1, clip_margin) + col2 = _apply_collision_model(arc2, collision_type, radius, width, cx2, cy2, clip_margin) + collision_polys = [unary_union(col1 + col2)] + else: + collision_polys = _apply_collision_model( + combined_arc, collision_type, radius, width, 0, 0, clip_margin + ) + return ComponentResult(geometry=collision_polys, end_port=end_port, length=2 * radius * theta) diff --git a/inire/router/astar.py b/inire/router/astar.py index f77c044..4d0e295 100644 --- a/inire/router/astar.py +++ b/inire/router/astar.py @@ -120,7 +120,7 @@ class AStarRouter: if state in closed_set: continue closed_set.add(state) - + nodes_expanded += 1 self.total_nodes_expanded += 1 @@ -162,7 +162,7 @@ class AStarRouter: if proj > 0 and abs(perp) < 1e-6: res = Straight.generate(current.port, proj, net_width, snap_to_grid=False) self._add_node(current, res, target, net_width, net_id, open_set, closed_set, "SnapStraight") - + # B. Try SBend exact reach if abs(current.port.orientation - target.orientation) < 0.1: rad = np.radians(current.port.orientation) @@ -174,9 +174,9 @@ class AStarRouter: for radius in self.config.sbend_radii: try: res = SBend.generate( - current.port, - perp, - radius, + current.port, + perp, + radius, net_width, collision_type=self.config.bend_collision_type, clip_margin=self.config.bend_clip_margin @@ -189,8 +189,8 @@ class AStarRouter: lengths = self.config.straight_lengths if dist < 5.0: fine_steps = [0.1, 0.5] - lengths = sorted(list(set(lengths + fine_steps))) - + lengths = sorted(set(lengths + fine_steps)) + for length in lengths: res = Straight.generate(current.port, length, net_width) self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f"S{length}") @@ -199,9 +199,9 @@ class AStarRouter: for radius in self.config.bend_radii: for direction in ["CW", "CCW"]: res = Bend90.generate( - current.port, - radius, - net_width, + current.port, + radius, + net_width, direction, collision_type=self.config.bend_collision_type, clip_margin=self.config.bend_clip_margin @@ -213,9 +213,9 @@ class AStarRouter: for radius in self.config.sbend_radii: try: res = SBend.generate( - current.port, - offset, - radius, + current.port, + offset, + radius, net_width, collision_type=self.config.bend_collision_type, clip_margin=self.config.bend_clip_margin @@ -276,7 +276,7 @@ class AStarRouter: dilated_move.bounds[1] > prev_poly.bounds[3] + dilation or \ dilated_move.bounds[3] < prev_poly.bounds[1] - dilation: continue - + dilated_prev = prev_poly.buffer(dilation) if dilated_move.intersects(dilated_prev): overlap = dilated_move.intersection(dilated_prev) @@ -286,10 +286,10 @@ class AStarRouter: seg_idx += 1 move_cost = self.cost_evaluator.evaluate_move( - result.geometry, - result.end_port, - net_width, - net_id, + result.geometry, + result.end_port, + net_width, + net_id, start_port=parent.port, length=result.length ) diff --git a/inire/router/config.py b/inire/router/config.py index b9af31f..0a9e115 100644 --- a/inire/router/config.py +++ b/inire/router/config.py @@ -1,10 +1,8 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Literal, TYPE_CHECKING, Any +from typing import Literal, Any -if TYPE_CHECKING: - from shapely.geometry import Polygon @dataclass diff --git a/inire/router/cost.py b/inire/router/cost.py index 17a18a3..354a98e 100644 --- a/inire/router/cost.py +++ b/inire/router/cost.py @@ -40,7 +40,7 @@ class CostEvaluator: greedy_h_weight=greedy_h_weight, congestion_penalty=congestion_penalty, ) - + # Use config values self.unit_length_cost = self.config.unit_length_cost self.greedy_h_weight = self.config.greedy_h_weight @@ -73,12 +73,19 @@ class CostEvaluator: """Calculate the cost of a single move (Straight, Bend, SBend).""" _ = net_width # Unused total_cost = length * self.unit_length_cost - - # 1. Hard Collision check (Static obstacles) + + # 1. Hard Collision & Boundary Check # We buffer by the full clearance to ensure distance >= clearance hard_dilation = self.collision_engine.clearance for poly in geometry: dilated_poly = poly.buffer(hard_dilation) + + # Boundary Check: Physical edges must stay within design bounds + minx, miny, maxx, maxy = dilated_poly.bounds + if not (self.danger_map.is_within_bounds(minx, miny) and + self.danger_map.is_within_bounds(maxx, maxy)): + return 1e15 # Out of bounds is impossible + if self.collision_engine.is_collision_prebuffered(dilated_poly, start_port=start_port, end_port=end_port): return 1e15 # Impossible cost for hard collisions diff --git a/inire/tests/test_components.py b/inire/tests/test_components.py index 37da884..3ce6b18 100644 --- a/inire/tests/test_components.py +++ b/inire/tests/test_components.py @@ -1,6 +1,4 @@ -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 @@ -66,7 +64,7 @@ def test_bend_collision_models() -> None: # 1. BBox model res_bbox = Bend90.generate(start, radius, width, direction="CCW", collision_type="bbox") - # Arc CCW R=10 from (0,0,0) ends at (10,10,90). + # Arc CCW R=10 from (0,0,0) ends at (10,10,90). # Waveguide width is 2.0, so bbox will be slightly larger than (0,0,10,10) minx, miny, maxx, maxy = res_bbox.geometry[0].bounds assert minx <= 0.0 + 1e-6 @@ -89,7 +87,7 @@ def test_sbend_collision_models() -> None: res_bbox = SBend.generate(start, offset, radius, width, collision_type="bbox") # Geometry should be a single bounding box polygon assert len(res_bbox.geometry) == 1 - + res_arc = SBend.generate(start, offset, radius, width, collision_type="arc") assert res_bbox.geometry[0].area > res_arc.geometry[0].area @@ -100,15 +98,15 @@ def test_sbend_continuity() -> None: 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 @@ -119,17 +117,17 @@ def test_arc_sagitta_precision() -> None: 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 @@ -139,20 +137,20 @@ def test_component_transform_invariance() -> None: 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 diff --git a/inire/utils/validation.py b/inire/utils/validation.py index 6dd8986..662fd81 100644 --- a/inire/utils/validation.py +++ b/inire/utils/validation.py @@ -3,8 +3,7 @@ from __future__ import annotations import numpy as np from typing import TYPE_CHECKING, Any -from shapely.geometry import Point, Polygon -from shapely.ops import unary_union +from shapely.geometry import Polygon if TYPE_CHECKING: from inire.geometry.primitives import Port @@ -28,7 +27,7 @@ def validate_routing_result( obstacle_collision_geoms = [] self_intersection_geoms = [] connectivity_errors = [] - + # 1. Connectivity Check total_length = 0.0 for i, comp in enumerate(result.path): @@ -38,7 +37,7 @@ def validate_routing_result( if expected_end: last_port = result.path[-1].end_port dist_to_end = np.sqrt((last_port.x - expected_end.x)**2 + (last_port.y - expected_end.y)**2) - if dist_to_end > 0.005: + if dist_to_end > 0.005: connectivity_errors.append(f"Final port position mismatch: {dist_to_end*1000:.2f}nm") if abs(last_port.orientation - expected_end.orientation) > 0.1: connectivity_errors.append(f"Final port orientation mismatch: {last_port.orientation} vs {expected_end.orientation}") @@ -46,9 +45,9 @@ def validate_routing_result( # 2. Geometry Buffering dilation_half = clearance / 2.0 dilation_full = clearance - + dilated_for_self = [] - + for i, comp in enumerate(result.path): for poly in comp.geometry: # Check against obstacles @@ -58,7 +57,7 @@ def validate_routing_result( intersection = d_full.intersection(obs) if intersection.area > 1e-9: obstacle_collision_geoms.append(intersection) - + # Save for self-intersection check dilated_for_self.append(poly.buffer(dilation_half)) @@ -68,13 +67,13 @@ def validate_routing_result( if j > i + 1: # Non-adjacent if seg_i.intersects(seg_j): overlap = seg_i.intersection(seg_j) - if overlap.area > 1e-6: + if overlap.area > 1e-6: self_intersection_geoms.append((i, j, overlap)) - is_valid = (len(obstacle_collision_geoms) == 0 and - len(self_intersection_geoms) == 0 and + is_valid = (len(obstacle_collision_geoms) == 0 and + len(self_intersection_geoms) == 0 and len(connectivity_errors) == 0) - + reasons = [] if obstacle_collision_geoms: reasons.append(f"Found {len(obstacle_collision_geoms)} obstacle collisions.")