more bend work; bounds constrain edges

This commit is contained in:
Jan Petykiewicz 2026-03-09 01:48:18 -07:00
commit 58873692d6
15 changed files with 251 additions and 124 deletions

53
DOCS.md
View file

@ -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. The `AStarRouter` is the core pathfinding engine. It can be configured directly through its constructor.
| Parameter | Type | Default | Description | | Parameter | Type | Default | Description |
| :--- | :--- | :--- | :--- | | :-------------------- | :------------ | :----------------- | :------------------------------------------------------------------------------------ |
| `node_limit` | `int` | 1,000,000 | Maximum number of states to explore per net. Increase for very complex paths. | | `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. | | `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 the router to pick the best fit. | | `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_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). | | `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. | | `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. | | `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`. | | `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_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. | | `bend_clip_margin` | `float` | 10.0 | Extra space (µm) around the waveguide before the bounding box corners are clipped. |
### Bend Collision Models ### Bend Collision Models
* `"arc"`: High-fidelity model following the exact curved waveguide geometry. * `"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. * `"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. The `CostEvaluator` defines the "goodness" of a path.
| Parameter | Type | Default | Description | | Parameter | Type | Default | Description |
| :--- | :--- | :--- | :--- | | :------------------- | :------ | :--------- | :--------------------------------------------------------------------------------------- |
| `unit_length_cost` | `float` | 1.0 | Cost per µm of wire length. | | `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. | | `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. | | `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. The `PathFinder` orchestrates multi-net routing using the Negotiated Congestion algorithm.
| Parameter | Type | Default | Description | | Parameter | Type | Default | Description |
| :--- | :--- | :--- | :--- | | :------------------------ | :------ | :------ | :-------------------------------------------------------------------------------------- |
| `max_iterations` | `int` | 10 | Maximum number of rip-up and reroute iterations to resolve congestion. | | `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. | | `base_congestion_penalty` | `float` | 100.0 | Starting penalty for overlaps. Multiplied by `1.5` each iteration if congestion remains.|
--- ---
## 4. CollisionEngine Parameters ## 4. CollisionEngine Parameters
| Parameter | Type | Default | Description | | Parameter | Type | Default | Description |
| :--- | :--- | :--- | :--- | | :------------------- | :------ | :--------- | :------------------------------------------------------------------------------------ |
| `clearance` | `float` | (Required) | Minimum required distance between any two waveguides or obstacles (µm). | | `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. | | `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). - **Coordinates**: Micrometers (µm).
- **Grid Snapping**: The router internally operates on a **1nm** grid for final ports and a **1µm** lattice for expansion moves. - **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**. - **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.
--- ---

View file

@ -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/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/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/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: Run an example:
```bash ```bash
python3 examples/01_simple_route.py 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 ## Architecture
`inire` operates on a **State-Lattice** defined by $(x, y, \theta)$. From any state, the router expands via three primary "Move" types: `inire` operates on a **State-Lattice** defined by $(x, y, \theta)$. From any state, the router expands via three primary "Move" types:

View file

@ -28,7 +28,7 @@ def main() -> None:
"vertical_up": (Port(45, 10, 90), Port(45, 90, 90)), "vertical_up": (Port(45, 10, 90), Port(45, 90, 90)),
"vertical_down": (Port(55, 90, 270), Port(55, 10, 270)), "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 # 3. Route with Negotiated Congestion
# We increase the base penalty to encourage faster divergence # We increase the base penalty to encourage faster divergence

View file

@ -28,7 +28,7 @@ def main() -> None:
"bus_2": (Port(10, 60, 0), Port(110, 65, 0)), "bus_2": (Port(10, 60, 0), Port(110, 65, 0)),
} }
print("Phase 1: Routing bus (3 nets)...") 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 # Lock all Phase 1 nets
path_polys = [] path_polys = []
@ -50,10 +50,10 @@ def main() -> None:
"cross_left": (Port(30, 10, 90), Port(30, 110, 90)), "cross_left": (Port(30, 10, 90), Port(30, 110, 90)),
"cross_right": (Port(80, 110, 270), Port(80, 10, 270)), # Top to bottom "cross_right": (Port(80, 110, 270), Port(80, 10, 270)), # Top to bottom
} }
print("Phase 2: Routing crossing nets around locked bus...") print("Phase 2: Routing crossing nets around locked bus...")
# We use a slightly different width for variety # 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 # 4. Check Results
for nid, res in results_p2.items(): for nid, res in results_p2.items():

View file

@ -1,4 +1,3 @@
from shapely.geometry import Polygon
from inire.geometry.collision import CollisionEngine from inire.geometry.collision import CollisionEngine
from inire.geometry.primitives import Port from inire.geometry.primitives import Port

View file

@ -16,26 +16,26 @@ def main() -> None:
engine = CollisionEngine(clearance=2.0) engine = CollisionEngine(clearance=2.0)
danger_map = DangerMap(bounds=bounds) danger_map = DangerMap(bounds=bounds)
danger_map.precompute([]) danger_map.precompute([])
evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.1) evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.1)
router = AStarRouter(evaluator, node_limit=100000) router = AStarRouter(evaluator, node_limit=100000)
pf = PathFinder(router, evaluator) pf = PathFinder(router, evaluator)
# 2. Define Netlist with various orientation challenges # 2. Define Netlist with various orientation challenges
netlist = { netlist = {
# Opposite directions: requires two 90-degree bends to flip orientation # Opposite directions: requires two 90-degree bends to flip orientation
"opposite": (Port(10, 80, 0), Port(90, 80, 180)), "opposite": (Port(10, 80, 0), Port(90, 80, 180)),
# 90-degree turn: standard L-shape # 90-degree turn: standard L-shape
"turn_90": (Port(10, 60, 0), Port(40, 90, 90)), "turn_90": (Port(10, 60, 0), Port(40, 90, 90)),
# Output behind input: requires a full U-turn # Output behind input: requires a full U-turn
"behind": (Port(80, 40, 0), Port(20, 40, 0)), "behind": (Port(80, 40, 0), Port(20, 40, 0)),
# Sharp return: output is behind and oriented towards the input # Sharp return: output is behind and oriented towards the input
"return_loop": (Port(80, 20, 0), Port(40, 10, 180)), "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 # 3. Route
results = pf.route_all(netlist, net_widths) results = pf.route_all(netlist, net_widths)

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View file

@ -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()

View file

@ -3,8 +3,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import rtree import rtree
from shapely.geometry import Point, Polygon from shapely.geometry import Polygon
from shapely.ops import unary_union
from shapely.prepared import prep from shapely.prepared import prep
if TYPE_CHECKING: if TYPE_CHECKING:
@ -129,19 +128,16 @@ class CollisionEngine:
# Precise check: is every point in the intersection close to either port? # Precise check: is every point in the intersection close to either port?
ix_minx, ix_miny, ix_maxx, ix_maxy = intersection.bounds ix_minx, ix_miny, ix_maxx, ix_maxy = intersection.bounds
is_near_start = False is_near_start = False
if start_port: 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
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):
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_start = True
is_near_end = False is_near_end = False
if end_port: 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
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):
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
is_near_end = True
if is_near_start or is_near_end: if is_near_start or is_near_end:
continue continue

View file

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from typing import NamedTuple, Literal, Union from typing import NamedTuple, Literal, Any
import numpy as np import numpy as np
from shapely.geometry import Polygon, box from shapely.geometry import Polygon, box
@ -35,7 +35,7 @@ class Straight:
ex = start_port.x + dx ex = start_port.x + dx
ey = start_port.y + dy ey = start_port.y + dy
if snap_to_grid: if snap_to_grid:
ex = snap_search_grid(ex) ex = snap_search_grid(ex)
ey = snap_search_grid(ey) 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( def _apply_collision_model(
arc_poly: Polygon, arc_poly: Polygon,
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon, collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon,
radius: float, radius: float,
width: float,
cx: float = 0.0,
cy: float = 0.0,
clip_margin: float = 10.0 clip_margin: float = 10.0
) -> list[Polygon]: ) -> list[Polygon]:
"""Applies the specified collision model to an arc geometry.""" """Applies the specified collision model to an arc geometry."""
if isinstance(collision_type, Polygon): if isinstance(collision_type, Polygon):
return [collision_type] return [collision_type]
if collision_type == "arc": if collision_type == "arc":
return [arc_poly] return [arc_poly]
# Get bounding box # Get bounding box
minx, miny, maxx, maxy = arc_poly.bounds minx, miny, maxx, maxy = arc_poly.bounds
bbox = box(minx, miny, maxx, maxy) bbox = box(minx, miny, maxx, maxy)
if collision_type == "bbox": if collision_type == "bbox":
return [bbox] return [bbox]
if collision_type == "clipped_bbox": if collision_type == "clipped_bbox":
safe_zone = arc_poly.buffer(clip_margin) res_poly = bbox
return [bbox.intersection(safe_zone)]
# 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] return [arc_poly]
class Bend90: class Bend90:
@staticmethod @staticmethod
def generate( def generate(
start_port: Port, start_port: Port,
radius: float, radius: float,
width: float, width: float,
direction: str = "CW", direction: str = "CW",
sagitta: float = 0.01, sagitta: float = 0.01,
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc", collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc",
clip_margin: float = 10.0 clip_margin: float = 10.0
@ -133,9 +177,11 @@ class Bend90:
ex = snap_search_grid(cx + radius * np.cos(t_end)) ex = snap_search_grid(cx + radius * np.cos(t_end))
ey = snap_search_grid(cy + radius * np.sin(t_end)) ey = snap_search_grid(cy + radius * np.sin(t_end))
end_port = Port(ex, ey, float((start_port.orientation + turn_angle) % 360)) 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) 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) return ComponentResult(geometry=collision_polys, end_port=end_port, length=radius * np.pi / 2.0)
@ -143,10 +189,10 @@ class Bend90:
class SBend: class SBend:
@staticmethod @staticmethod
def generate( def generate(
start_port: Port, start_port: Port,
offset: float, offset: float,
radius: float, radius: float,
width: float, width: float,
sagitta: float = 0.01, sagitta: float = 0.01,
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc", collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon = "arc",
clip_margin: float = 10.0 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)) 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)) 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) end_port = Port(ex, ey, start_port.orientation)
direction = 1 if offset > 0 else -1 direction = 1 if offset > 0 else -1
c1_angle = rad_start + direction * np.pi / 2 c1_angle = rad_start + direction * np.pi / 2
cx1 = start_port.x + radius * np.cos(c1_angle) 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] arc1 = _get_arc_polygons(cx1, cy1, radius, width, ts1, te1, sagitta)[0]
arc2 = _get_arc_polygons(cx2, cy2, radius, width, ts2, te2, sagitta)[0] arc2 = _get_arc_polygons(cx2, cy2, radius, width, ts2, te2, sagitta)[0]
combined_arc = unary_union([arc1, arc2]) 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) return ComponentResult(geometry=collision_polys, end_port=end_port, length=2 * radius * theta)

View file

@ -120,7 +120,7 @@ class AStarRouter:
if state in closed_set: if state in closed_set:
continue continue
closed_set.add(state) closed_set.add(state)
nodes_expanded += 1 nodes_expanded += 1
self.total_nodes_expanded += 1 self.total_nodes_expanded += 1
@ -162,7 +162,7 @@ class AStarRouter:
if proj > 0 and abs(perp) < 1e-6: if proj > 0 and abs(perp) < 1e-6:
res = Straight.generate(current.port, proj, net_width, snap_to_grid=False) 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") self._add_node(current, res, target, net_width, net_id, open_set, closed_set, "SnapStraight")
# B. Try SBend exact reach # B. Try SBend exact reach
if abs(current.port.orientation - target.orientation) < 0.1: if abs(current.port.orientation - target.orientation) < 0.1:
rad = np.radians(current.port.orientation) rad = np.radians(current.port.orientation)
@ -174,9 +174,9 @@ class AStarRouter:
for radius in self.config.sbend_radii: for radius in self.config.sbend_radii:
try: try:
res = SBend.generate( res = SBend.generate(
current.port, current.port,
perp, perp,
radius, radius,
net_width, net_width,
collision_type=self.config.bend_collision_type, collision_type=self.config.bend_collision_type,
clip_margin=self.config.bend_clip_margin clip_margin=self.config.bend_clip_margin
@ -189,8 +189,8 @@ class AStarRouter:
lengths = self.config.straight_lengths lengths = self.config.straight_lengths
if dist < 5.0: if dist < 5.0:
fine_steps = [0.1, 0.5] fine_steps = [0.1, 0.5]
lengths = sorted(list(set(lengths + fine_steps))) lengths = sorted(set(lengths + fine_steps))
for length in lengths: for length in lengths:
res = Straight.generate(current.port, length, net_width) 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}") 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 radius in self.config.bend_radii:
for direction in ["CW", "CCW"]: for direction in ["CW", "CCW"]:
res = Bend90.generate( res = Bend90.generate(
current.port, current.port,
radius, radius,
net_width, net_width,
direction, direction,
collision_type=self.config.bend_collision_type, collision_type=self.config.bend_collision_type,
clip_margin=self.config.bend_clip_margin clip_margin=self.config.bend_clip_margin
@ -213,9 +213,9 @@ class AStarRouter:
for radius in self.config.sbend_radii: for radius in self.config.sbend_radii:
try: try:
res = SBend.generate( res = SBend.generate(
current.port, current.port,
offset, offset,
radius, radius,
net_width, net_width,
collision_type=self.config.bend_collision_type, collision_type=self.config.bend_collision_type,
clip_margin=self.config.bend_clip_margin 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[1] > prev_poly.bounds[3] + dilation or \
dilated_move.bounds[3] < prev_poly.bounds[1] - dilation: dilated_move.bounds[3] < prev_poly.bounds[1] - dilation:
continue continue
dilated_prev = prev_poly.buffer(dilation) dilated_prev = prev_poly.buffer(dilation)
if dilated_move.intersects(dilated_prev): if dilated_move.intersects(dilated_prev):
overlap = dilated_move.intersection(dilated_prev) overlap = dilated_move.intersection(dilated_prev)
@ -286,10 +286,10 @@ class AStarRouter:
seg_idx += 1 seg_idx += 1
move_cost = self.cost_evaluator.evaluate_move( move_cost = self.cost_evaluator.evaluate_move(
result.geometry, result.geometry,
result.end_port, result.end_port,
net_width, net_width,
net_id, net_id,
start_port=parent.port, start_port=parent.port,
length=result.length length=result.length
) )

View file

@ -1,10 +1,8 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field 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 @dataclass

View file

@ -40,7 +40,7 @@ class CostEvaluator:
greedy_h_weight=greedy_h_weight, greedy_h_weight=greedy_h_weight,
congestion_penalty=congestion_penalty, congestion_penalty=congestion_penalty,
) )
# Use config values # Use config values
self.unit_length_cost = self.config.unit_length_cost self.unit_length_cost = self.config.unit_length_cost
self.greedy_h_weight = self.config.greedy_h_weight 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).""" """Calculate the cost of a single move (Straight, Bend, SBend)."""
_ = net_width # Unused _ = net_width # Unused
total_cost = length * self.unit_length_cost 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 # We buffer by the full clearance to ensure distance >= clearance
hard_dilation = self.collision_engine.clearance hard_dilation = self.collision_engine.clearance
for poly in geometry: for poly in geometry:
dilated_poly = poly.buffer(hard_dilation) 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): if self.collision_engine.is_collision_prebuffered(dilated_poly, start_port=start_port, end_port=end_port):
return 1e15 # Impossible cost for hard collisions return 1e15 # Impossible cost for hard collisions

View file

@ -1,6 +1,4 @@
import numpy as np
import pytest import pytest
from shapely.geometry import Point
from inire.geometry.components import Bend90, SBend, Straight from inire.geometry.components import Bend90, SBend, Straight
from inire.geometry.primitives import Port, rotate_port, translate_port from inire.geometry.primitives import Port, rotate_port, translate_port
@ -66,7 +64,7 @@ def test_bend_collision_models() -> None:
# 1. BBox model # 1. BBox model
res_bbox = Bend90.generate(start, radius, width, direction="CCW", collision_type="bbox") 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) # 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 minx, miny, maxx, maxy = res_bbox.geometry[0].bounds
assert minx <= 0.0 + 1e-6 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") res_bbox = SBend.generate(start, offset, radius, width, collision_type="bbox")
# Geometry should be a single bounding box polygon # Geometry should be a single bounding box polygon
assert len(res_bbox.geometry) == 1 assert len(res_bbox.geometry) == 1
res_arc = SBend.generate(start, offset, radius, width, collision_type="arc") res_arc = SBend.generate(start, offset, radius, width, collision_type="arc")
assert res_bbox.geometry[0].area > res_arc.geometry[0].area assert res_bbox.geometry[0].area > res_arc.geometry[0].area
@ -100,15 +98,15 @@ def test_sbend_continuity() -> None:
offset = 4.0 offset = 4.0
radius = 20.0 radius = 20.0
width = 1.0 width = 1.0
res = SBend.generate(start, offset, radius, width) res = SBend.generate(start, offset, radius, width)
# Target orientation should be same as start # Target orientation should be same as start
assert abs(res.end_port.orientation - 90.0) < 1e-6 assert abs(res.end_port.orientation - 90.0) < 1e-6
# For a port at 90 deg, +offset is a shift in -x direction # For a port at 90 deg, +offset is a shift in -x direction
assert abs(res.end_port.x - (10.0 - offset)) < 1e-6 assert abs(res.end_port.x - (10.0 - offset)) < 1e-6
# Geometry should be connected (unary_union results in 1 polygon) # Geometry should be connected (unary_union results in 1 polygon)
assert len(res.geometry) == 1 assert len(res.geometry) == 1
assert res.geometry[0].is_valid assert res.geometry[0].is_valid
@ -119,17 +117,17 @@ def test_arc_sagitta_precision() -> None:
start = Port(0, 0, 0) start = Port(0, 0, 0)
radius = 100.0 # Large radius to make sagitta significant radius = 100.0 # Large radius to make sagitta significant
width = 2.0 width = 2.0
# Coarse: 1um sagitta # Coarse: 1um sagitta
res_coarse = Bend90.generate(start, radius, width, sagitta=1.0) res_coarse = Bend90.generate(start, radius, width, sagitta=1.0)
# Fine: 0.01um (10nm) sagitta # Fine: 0.01um (10nm) sagitta
res_fine = Bend90.generate(start, radius, width, sagitta=0.01) res_fine = Bend90.generate(start, radius, width, sagitta=0.01)
# Number of segments should be significantly higher for fine # Number of segments should be significantly higher for fine
# Exterior points = (segments + 1) * 2 # Exterior points = (segments + 1) * 2
pts_coarse = len(res_coarse.geometry[0].exterior.coords) pts_coarse = len(res_coarse.geometry[0].exterior.coords)
pts_fine = len(res_fine.geometry[0].exterior.coords) pts_fine = len(res_fine.geometry[0].exterior.coords)
assert pts_fine > pts_coarse * 2 assert pts_fine > pts_coarse * 2
@ -139,20 +137,20 @@ def test_component_transform_invariance() -> None:
start0 = Port(0, 0, 0) start0 = Port(0, 0, 0)
radius = 10.0 radius = 10.0
width = 2.0 width = 2.0
res0 = Bend90.generate(start0, radius, width, direction="CCW") res0 = Bend90.generate(start0, radius, width, direction="CCW")
# Transform: Translate (10, 10) then Rotate 90 # Transform: Translate (10, 10) then Rotate 90
dx, dy = 10.0, 5.0 dx, dy = 10.0, 5.0
angle = 90.0 angle = 90.0
# 1. Transform the generated geometry # 1. Transform the generated geometry
p_end_transformed = rotate_port(translate_port(res0.end_port, dx, dy), angle) p_end_transformed = rotate_port(translate_port(res0.end_port, dx, dy), angle)
# 2. Generate at transformed start # 2. Generate at transformed start
start_transformed = rotate_port(translate_port(start0, dx, dy), angle) start_transformed = rotate_port(translate_port(start0, dx, dy), angle)
res_transformed = Bend90.generate(start_transformed, radius, width, direction="CCW") 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.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.y - p_end_transformed.y) < 1e-6
assert abs(res_transformed.end_port.orientation - p_end_transformed.orientation) < 1e-6 assert abs(res_transformed.end_port.orientation - p_end_transformed.orientation) < 1e-6

View file

@ -3,8 +3,7 @@ from __future__ import annotations
import numpy as np import numpy as np
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from shapely.geometry import Point, Polygon from shapely.geometry import Polygon
from shapely.ops import unary_union
if TYPE_CHECKING: if TYPE_CHECKING:
from inire.geometry.primitives import Port from inire.geometry.primitives import Port
@ -28,7 +27,7 @@ def validate_routing_result(
obstacle_collision_geoms = [] obstacle_collision_geoms = []
self_intersection_geoms = [] self_intersection_geoms = []
connectivity_errors = [] connectivity_errors = []
# 1. Connectivity Check # 1. Connectivity Check
total_length = 0.0 total_length = 0.0
for i, comp in enumerate(result.path): for i, comp in enumerate(result.path):
@ -38,7 +37,7 @@ def validate_routing_result(
if expected_end: if expected_end:
last_port = result.path[-1].end_port 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) 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") connectivity_errors.append(f"Final port position mismatch: {dist_to_end*1000:.2f}nm")
if abs(last_port.orientation - expected_end.orientation) > 0.1: 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}") 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 # 2. Geometry Buffering
dilation_half = clearance / 2.0 dilation_half = clearance / 2.0
dilation_full = clearance dilation_full = clearance
dilated_for_self = [] dilated_for_self = []
for i, comp in enumerate(result.path): for i, comp in enumerate(result.path):
for poly in comp.geometry: for poly in comp.geometry:
# Check against obstacles # Check against obstacles
@ -58,7 +57,7 @@ def validate_routing_result(
intersection = d_full.intersection(obs) intersection = d_full.intersection(obs)
if intersection.area > 1e-9: if intersection.area > 1e-9:
obstacle_collision_geoms.append(intersection) obstacle_collision_geoms.append(intersection)
# Save for self-intersection check # Save for self-intersection check
dilated_for_self.append(poly.buffer(dilation_half)) dilated_for_self.append(poly.buffer(dilation_half))
@ -68,13 +67,13 @@ def validate_routing_result(
if j > i + 1: # Non-adjacent if j > i + 1: # Non-adjacent
if seg_i.intersects(seg_j): if seg_i.intersects(seg_j):
overlap = seg_i.intersection(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)) self_intersection_geoms.append((i, j, overlap))
is_valid = (len(obstacle_collision_geoms) == 0 and is_valid = (len(obstacle_collision_geoms) == 0 and
len(self_intersection_geoms) == 0 and len(self_intersection_geoms) == 0 and
len(connectivity_errors) == 0) len(connectivity_errors) == 0)
reasons = [] reasons = []
if obstacle_collision_geoms: if obstacle_collision_geoms:
reasons.append(f"Found {len(obstacle_collision_geoms)} obstacle collisions.") reasons.append(f"Found {len(obstacle_collision_geoms)} obstacle collisions.")