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.
| 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.
---

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/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:

View file

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

View file

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

View file

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

View file

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

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
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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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.")