more s-bend fixes and add docs

This commit is contained in:
jan 2026-03-08 22:02:07 -07:00
commit 41a2d9f058
10 changed files with 249 additions and 116 deletions

53
DOCS.md Normal file
View file

@ -0,0 +1,53 @@
# Inire Configuration & API Documentation
This document describes the user-tunable parameters for the `inire` auto-router.
## 1. AStarRouter Parameters
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. |
### 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.
---
## 2. CostEvaluator Parameters
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. |
---
## 3. 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. |
---
## 4. Physical Units & Precision
- **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**.

View file

@ -14,54 +14,43 @@ def main() -> None:
print("Running Example 04: S-Bends and Multiple Radii...") print("Running Example 04: S-Bends and Multiple Radii...")
# 1. Setup Environment # 1. Setup Environment
bounds = (0, 0, 150, 100) bounds = (0, 0, 100, 100)
engine = CollisionEngine(clearance=2.0) engine = CollisionEngine(clearance=2.0)
danger_map = DangerMap(bounds=bounds) danger_map = DangerMap(bounds=bounds)
danger_map.precompute([])
# Create obstacles that force S-bends and turns
# Obstacle 1: Forces a vertical jog (S-bend) # 2. Configure Router
obs1 = Polygon([(40, 20), (60, 20), (60, 60), (40, 60)])
# Obstacle 2: Forces a large radius turn
obs2 = Polygon([(80, 0), (100, 0), (100, 40), (80, 40)])
obstacles = [obs1, obs2]
for obs in obstacles:
engine.add_static_obstacle(obs)
danger_map.precompute(obstacles)
# 2. Configure Router with custom parameters (Directly via constructor)
evaluator = CostEvaluator( evaluator = CostEvaluator(
engine, engine,
danger_map, danger_map,
unit_length_cost=1.0, unit_length_cost=1.0,
greedy_h_weight=1.2, greedy_h_weight=1.5,
) )
# We want a 45 degree switchover for S-bend.
# Offset O = 2 * R * (1 - cos(theta))
# If R = 10, O = 5.86
router = AStarRouter( router = AStarRouter(
evaluator, evaluator,
node_limit=500000, node_limit=50000,
bend_radii=[10.0, 30.0], # Allow standard and large bends bend_radii=[10.0, 30.0],
sbend_offsets=[-10.0, -5.0, 5.0, 10.0], # Allow larger S-bend offsets sbend_offsets=[5.0], # Use a simpler offset
sbend_radii=[20.0, 50.0], # Large S-bends sbend_radii=[10.0],
bend_penalty=10.0, # Lower penalty to encourage using the right bend bend_penalty=10.0,
sbend_penalty=20.0,
snap_to_target_dist=50.0, # Large snap range
) )
pf = PathFinder(router, evaluator) pf = PathFinder(router, evaluator)
# 3. Define Netlist # 3. Define Netlist
# Net 1: Needs to S-bend around obs1 (gap at y=60-100? No, obs1 is y=20-60). # start (10, 50), target (60, 55) -> 5um offset
# Start at (10, 40), End at (140, 40).
# Obstacle 1 blocks 40-60. Net must go above or below.
# Obstacle 2 blocks 80-100 x 0-40.
# Let's force a path that requires a large bend.
netlist = { netlist = {
"large_bend_net": (Port(10, 10, 0), Port(140, 80, 0)), "sbend_only": (Port(10, 50, 0), Port(60, 55, 0)),
"sbend_net": (Port(10, 50, 0), Port(70, 70, 0)), "multi_radii": (Port(10, 10, 0), Port(90, 90, 0)),
} }
net_widths = {"large_bend_net": 2.0, "sbend_net": 2.0} net_widths = {"sbend_only": 2.0, "multi_radii": 2.0}
# 4. Route # 4. Route
results = pf.route_all(netlist, net_widths) results = pf.route_all(netlist, net_widths)
@ -72,7 +61,7 @@ def main() -> None:
print(f"{nid}: {status}, collisions={res.collisions}") print(f"{nid}: {status}, collisions={res.collisions}")
# 6. Visualize # 6. Visualize
fig, ax = plot_routing_results(results, obstacles, bounds) fig, ax = plot_routing_results(results, [], bounds)
fig.savefig("examples/sbends_radii.png") fig.savefig("examples/sbends_radii.png")
print("Saved plot to examples/sbends_radii.png") print("Saved plot to examples/sbends_radii.png")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Before After
Before After

View file

@ -1,9 +1,10 @@
from __future__ import annotations from __future__ import annotations
from typing import NamedTuple from typing import NamedTuple, Literal, Union
import numpy as np import numpy as np
from shapely.geometry import Polygon from shapely.geometry import Polygon, box
from shapely.ops import unary_union
from .primitives import Port from .primitives import Port
@ -34,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)
@ -71,46 +72,85 @@ def _get_num_segments(radius: float, angle_deg: float, sagitta: float = 0.01) ->
return max(8, num) return max(8, num)
def _get_arc_polygons(cx: float, cy: float, radius: float, width: float, t_start: float, t_end: float, sagitta: float = 0.01) -> list[Polygon]:
"""Helper to generate arc-shaped polygons."""
num_segments = _get_num_segments(radius, float(np.degrees(abs(t_end - t_start))), sagitta)
angles = np.linspace(t_start, t_end, num_segments + 1)
inner_radius = radius - width / 2.0
outer_radius = radius + width / 2.0
inner_points = [(cx + inner_radius * np.cos(a), cy + inner_radius * np.sin(a)) for a in angles]
outer_points = [(cx + outer_radius * np.cos(a), cy + outer_radius * np.sin(a)) for a in reversed(angles)]
return [Polygon(inner_points + outer_points)]
def _apply_collision_model(
arc_poly: Polygon,
collision_type: Literal["arc", "bbox", "clipped_bbox"] | Polygon,
radius: float,
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)]
return [arc_poly]
class Bend90: class Bend90:
@staticmethod @staticmethod
def generate(start_port: Port, radius: float, width: float, direction: str = "CW", sagitta: float = 0.01) -> ComponentResult: def generate(
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
) -> ComponentResult:
"""Generate a 90-degree bend.""" """Generate a 90-degree bend."""
turn_angle = -90 if direction == "CW" else 90 turn_angle = -90 if direction == "CW" else 90
# Calculate center
rad_start = np.radians(start_port.orientation) rad_start = np.radians(start_port.orientation)
c_angle = rad_start + (np.pi / 2 if direction == "CCW" else -np.pi / 2) c_angle = rad_start + (np.pi / 2 if direction == "CCW" else -np.pi / 2)
cx = start_port.x + radius * np.cos(c_angle) cx = start_port.x + radius * np.cos(c_angle)
cy = start_port.y + radius * np.sin(c_angle) cy = start_port.y + radius * np.sin(c_angle)
t_start = c_angle + np.pi t_start = c_angle + np.pi
t_end = t_start + (np.pi / 2 if direction == "CCW" else -np.pi / 2) t_end = t_start + (np.pi / 2 if direction == "CCW" else -np.pi / 2)
# End port (snapped to lattice)
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))
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)
end_orientation = (start_port.orientation + turn_angle) % 360 return ComponentResult(geometry=collision_polys, end_port=end_port, length=radius * np.pi / 2.0)
end_port = Port(ex, ey, float(end_orientation))
actual_length = radius * np.pi / 2.0
# Generate arc geometry
num_segments = _get_num_segments(radius, 90, sagitta)
angles = np.linspace(t_start, t_end, num_segments + 1)
inner_radius = radius - width / 2.0
outer_radius = radius + width / 2.0
inner_points = [(cx + inner_radius * np.cos(a), cy + inner_radius * np.sin(a)) for a in angles]
outer_points = [(cx + outer_radius * np.cos(a), cy + outer_radius * np.sin(a)) for a in reversed(angles)]
return ComponentResult(geometry=[Polygon(inner_points + outer_points)], end_port=end_port, length=actual_length)
class SBend: class SBend:
@staticmethod @staticmethod
def generate(start_port: Port, offset: float, radius: float, width: float, sagitta: float = 0.01) -> ComponentResult: def generate(
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
) -> ComponentResult:
"""Generate a parametric S-bend (two tangent arcs).""" """Generate a parametric S-bend (two tangent arcs)."""
if abs(offset) >= 2 * radius: if abs(offset) >= 2 * radius:
raise ValueError(f"SBend offset {offset} must be less than 2*radius {2 * radius}") raise ValueError(f"SBend offset {offset} must be less than 2*radius {2 * radius}")
@ -118,42 +158,28 @@ class SBend:
theta = np.arccos(1 - abs(offset) / (2 * radius)) theta = np.arccos(1 - abs(offset) / (2 * radius))
dx = 2 * radius * np.sin(theta) dx = 2 * radius * np.sin(theta)
dy = offset dy = offset
# End port (snapped to lattice)
rad_start = np.radians(start_port.orientation) rad_start = np.radians(start_port.orientation)
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)
actual_length = 2 * radius * theta
# Arc centers and angles (Relative to start orientation)
direction = 1 if offset > 0 else -1 direction = 1 if offset > 0 else -1
# Arc 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)
cy1 = start_port.y + radius * np.sin(c1_angle) cy1 = start_port.y + radius * np.sin(c1_angle)
t_start1 = c1_angle + np.pi ts1, te1 = c1_angle + np.pi, c1_angle + np.pi + direction * theta
t_end1 = t_start1 + direction * theta
# Arc 2 (Calculated relative to un-snapped end to ensure perfect tangency)
ex_raw = start_port.x + dx * np.cos(rad_start) - dy * np.sin(rad_start) ex_raw = start_port.x + dx * np.cos(rad_start) - dy * np.sin(rad_start)
ey_raw = start_port.y + dx * np.sin(rad_start) + dy * np.cos(rad_start) ey_raw = start_port.y + dx * np.sin(rad_start) + dy * np.cos(rad_start)
c2_angle = rad_start - direction * np.pi / 2 c2_angle = rad_start - direction * np.pi / 2
cx2 = ex_raw + radius * np.cos(c2_angle) cx2 = ex_raw + radius * np.cos(c2_angle)
cy2 = ey_raw + radius * np.sin(c2_angle) cy2 = ey_raw + radius * np.sin(c2_angle)
t_end2 = c2_angle + np.pi te2 = c2_angle + np.pi
t_start2 = t_end2 + direction * theta ts2 = te2 + direction * theta
def get_arc_points(cx: float, cy: float, r_inner: float, r_outer: float, ts: float, te: float) -> list[tuple[float, float]]: arc1 = _get_arc_polygons(cx1, cy1, radius, width, ts1, te1, sagitta)[0]
num_segments = _get_num_segments(radius, float(np.degrees(theta)), sagitta) arc2 = _get_arc_polygons(cx2, cy2, radius, width, ts2, te2, sagitta)[0]
angles = np.linspace(ts, te, num_segments + 1) combined_arc = unary_union([arc1, arc2])
inner = [(cx + r_inner * np.cos(a), cy + r_inner * np.sin(a)) for a in angles]
outer = [(cx + r_outer * np.cos(a), cy + r_outer * np.sin(a)) for a in reversed(angles)] collision_polys = _apply_collision_model(combined_arc, collision_type, radius, clip_margin)
return inner + outer return ComponentResult(geometry=collision_polys, end_port=end_port, length=2 * radius * theta)
poly1 = Polygon(get_arc_points(cx1, cy1, radius - width / 2, radius + width / 2, t_start1, t_end1))
poly2 = Polygon(get_arc_points(cx2, cy2, radius - width / 2, radius + width / 2, t_start2, t_end2))
return ComponentResult(geometry=[poly1, poly2], end_port=end_port, length=actual_length)

View file

@ -2,7 +2,7 @@ from __future__ import annotations
import heapq import heapq
import logging import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Literal
import numpy as np import numpy as np
@ -60,6 +60,8 @@ class AStarRouter:
snap_to_target_dist: float = 20.0, snap_to_target_dist: float = 20.0,
bend_penalty: float = 50.0, bend_penalty: float = 50.0,
sbend_penalty: float = 100.0, sbend_penalty: float = 100.0,
bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] = "arc",
bend_clip_margin: float = 10.0,
) -> None: ) -> None:
""" """
Initialize the A* Router. Initialize the A* Router.
@ -74,6 +76,8 @@ class AStarRouter:
snap_to_target_dist: Distance threshold for lookahead snapping. snap_to_target_dist: Distance threshold for lookahead snapping.
bend_penalty: Flat cost penalty for each 90-degree bend. bend_penalty: Flat cost penalty for each 90-degree bend.
sbend_penalty: Flat cost penalty for each S-bend. sbend_penalty: Flat cost penalty for each S-bend.
bend_collision_type: Type of collision model for bends ('arc', 'bbox', 'clipped_bbox').
bend_clip_margin: Margin for 'clipped_bbox' collision model.
""" """
self.cost_evaluator = cost_evaluator self.cost_evaluator = cost_evaluator
self.config = RouterConfig( self.config = RouterConfig(
@ -85,6 +89,8 @@ class AStarRouter:
snap_to_target_dist=snap_to_target_dist, snap_to_target_dist=snap_to_target_dist,
bend_penalty=bend_penalty, bend_penalty=bend_penalty,
sbend_penalty=sbend_penalty, sbend_penalty=sbend_penalty,
bend_collision_type=bend_collision_type,
bend_clip_margin=bend_clip_margin,
) )
self.node_limit = self.config.node_limit self.node_limit = self.config.node_limit
self.total_nodes_expanded = 0 self.total_nodes_expanded = 0
@ -167,8 +173,15 @@ class AStarRouter:
if proj > 0 and 0.5 <= abs(perp) < 20.0: if proj > 0 and 0.5 <= abs(perp) < 20.0:
for radius in self.config.sbend_radii: for radius in self.config.sbend_radii:
try: try:
res = SBend.generate(current.port, perp, radius, net_width) res = SBend.generate(
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, "SnapSBend") current.port,
perp,
radius,
net_width,
collision_type=self.config.bend_collision_type,
clip_margin=self.config.bend_clip_margin
)
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, "SnapSBend", move_radius=radius)
except ValueError: except ValueError:
pass pass
@ -185,15 +198,29 @@ class AStarRouter:
# 3. Lattice Bends # 3. Lattice Bends
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(current.port, radius, net_width, direction) res = Bend90.generate(
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f"B{radius}{direction}") current.port,
radius,
net_width,
direction,
collision_type=self.config.bend_collision_type,
clip_margin=self.config.bend_clip_margin
)
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f"B{radius}{direction}", move_radius=radius)
# 4. Discrete SBends # 4. Discrete SBends
for offset in self.config.sbend_offsets: for offset in self.config.sbend_offsets:
for radius in self.config.sbend_radii: for radius in self.config.sbend_radii:
try: try:
res = SBend.generate(current.port, offset, radius, net_width) res = SBend.generate(
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f"SB{offset}R{radius}") current.port,
offset,
radius,
net_width,
collision_type=self.config.bend_collision_type,
clip_margin=self.config.bend_clip_margin
)
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f"SB{offset}R{radius}", move_radius=radius)
except ValueError: except ValueError:
pass pass
@ -207,6 +234,7 @@ class AStarRouter:
open_set: list[AStarNode], open_set: list[AStarNode],
closed_set: set[tuple[float, float, float]], closed_set: set[tuple[float, float, float]],
move_type: str, move_type: str,
move_radius: float | None = None,
) -> None: ) -> None:
# Check closed set before adding to open set # Check closed set before adding to open set
state = (round(result.end_port.x, 3), round(result.end_port.y, 3), round(result.end_port.orientation, 2)) state = (round(result.end_port.x, 3), round(result.end_port.y, 3), round(result.end_port.orientation, 2))
@ -269,9 +297,19 @@ class AStarRouter:
if move_cost > 1e12: if move_cost > 1e12:
return return
if "B" in move_type: # Turn penalties scaled by radius to favor larger turns
ref_radius = 10.0
if "B" in move_type and move_radius is not None:
# Scale penalty: larger radius -> smaller penalty
# e.g. radius 10 -> factor 1.0, radius 30 -> factor 0.33
penalty_factor = ref_radius / move_radius
move_cost += self.config.bend_penalty * penalty_factor
elif "SB" in move_type and move_radius is not None:
penalty_factor = ref_radius / move_radius
move_cost += self.config.sbend_penalty * penalty_factor
elif "B" in move_type:
move_cost += self.config.bend_penalty move_cost += self.config.bend_penalty
if "SB" in move_type: elif "SB" in move_type:
move_cost += self.config.sbend_penalty move_cost += self.config.sbend_penalty
g_cost = parent.g_cost + move_cost g_cost = parent.g_cost + move_cost

View file

@ -1,13 +1,17 @@
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
if TYPE_CHECKING:
from shapely.geometry import Polygon
@dataclass @dataclass
class RouterConfig: class RouterConfig:
"""Configuration parameters for the A* Router.""" """Configuration parameters for the A* Router."""
node_limit: int = 500000 node_limit: int = 1000000
straight_lengths: list[float] = field(default_factory=lambda: [1.0, 5.0, 25.0]) straight_lengths: list[float] = field(default_factory=lambda: [1.0, 5.0, 25.0])
bend_radii: list[float] = field(default_factory=lambda: [10.0]) bend_radii: list[float] = field(default_factory=lambda: [10.0])
sbend_offsets: list[float] = field(default_factory=lambda: [-5.0, -2.0, 2.0, 5.0]) sbend_offsets: list[float] = field(default_factory=lambda: [-5.0, -2.0, 2.0, 5.0])
@ -15,6 +19,8 @@ class RouterConfig:
snap_to_target_dist: float = 20.0 snap_to_target_dist: float = 20.0
bend_penalty: float = 50.0 bend_penalty: float = 50.0
sbend_penalty: float = 100.0 sbend_penalty: float = 100.0
bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | Any = "arc"
bend_clip_margin: float = 10.0
@dataclass @dataclass

View file

@ -80,7 +80,6 @@ class CostEvaluator:
for poly in geometry: for poly in geometry:
dilated_poly = poly.buffer(hard_dilation) dilated_poly = poly.buffer(hard_dilation)
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):
# print(f"DEBUG: Hard collision detected at {end_port}")
return 1e15 # Impossible cost for hard collisions return 1e15 # Impossible cost for hard collisions
# 2. Soft Collision check (Negotiated Congestion) # 2. Soft Collision check (Negotiated Congestion)

View file

@ -50,19 +50,43 @@ def test_sbend_generation() -> None:
result = SBend.generate(start, offset, radius, width) result = SBend.generate(start, offset, radius, width)
assert result.end_port.y == 5.0 assert result.end_port.y == 5.0
assert result.end_port.orientation == 0.0 assert result.end_port.orientation == 0.0
assert len(result.geometry) == 2 assert len(result.geometry) == 1 # Now uses unary_union
# Verify failure for large offset # Verify failure for large offset
with pytest.raises(ValueError, match=r"SBend offset .* must be less than 2\*radius"): with pytest.raises(ValueError, match=r"SBend offset .* must be less than 2\*radius"):
SBend.generate(start, 25.0, 10.0, 2.0) SBend.generate(start, 25.0, 10.0, 2.0)
def test_bend_snapping() -> None: def test_bend_collision_models() -> None:
# Radius that results in non-integer coords
radius = 10.1234
start = Port(0, 0, 0) start = Port(0, 0, 0)
result = Bend90.generate(start, radius, width=2.0, direction="CCW") radius = 10.0
width = 2.0
# Target x is 10.1234, should snap to 10.0 (assuming 1.0um grid) # 1. BBox model
assert result.end_port.x == 10.0 res_bbox = Bend90.generate(start, radius, width, direction="CCW", collision_type="bbox")
assert result.end_port.y == 10.0 # 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
assert maxx >= 10.0 - 1e-6
assert miny <= 0.0 + 1e-6
assert maxy >= 10.0 - 1e-6
# 2. Clipped BBox model
res_clipped = Bend90.generate(start, radius, width, direction="CCW", collision_type="clipped_bbox", clip_margin=1.0)
# Area should be less than full bbox
assert res_clipped.geometry[0].area < res_bbox.geometry[0].area
def test_sbend_collision_models() -> None:
start = Port(0, 0, 0)
offset = 5.0
radius = 10.0
width = 2.0
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

View file

@ -30,9 +30,9 @@ def test_astar_sbend(basic_evaluator: CostEvaluator) -> None:
# Check if any component in the path is an SBend # Check if any component in the path is an SBend
found_sbend = False found_sbend = False
for res in path: for res in path:
# Check if it has 2 polygons (characteristic of our SBend implementation) # Check if the end port orientation is same as start
# and end port orientation is same as start # and it's not a single straight (which would have y=0)
if len(res.geometry) == 2: if abs(res.end_port.y - start.y) > 0.1 and abs(res.end_port.orientation - start.orientation) < 0.1:
found_sbend = True found_sbend = True
break break
assert found_sbend assert found_sbend
@ -50,11 +50,6 @@ def test_pathfinder_negotiated_congestion_resolution(basic_evaluator: CostEvalua
net_widths = {"net1": 2.0, "net2": 2.0} net_widths = {"net1": 2.0, "net2": 2.0}
# Force them into a narrow corridor that only fits ONE. # Force them into a narrow corridor that only fits ONE.
# Obstacles creating a wide wall with a narrow 2um gap at y=5.
# Gap y: 4 to 6. Center y=5.
# Net 1 (y=0) and Net 2 (y=10) both want to go to y=5 to pass.
# But only ONE fits at y=5.
obs_top = Polygon([(20, 6), (30, 6), (30, 15), (20, 10)]) # Lower wall obs_top = Polygon([(20, 6), (30, 6), (30, 15), (20, 10)]) # Lower wall
obs_bottom = Polygon([(20, 4), (30, 4), (30, -15), (20, -10)]) obs_bottom = Polygon([(20, 4), (30, 4), (30, -15), (20, -10)])

View file

@ -36,9 +36,12 @@ def plot_routing_results(
label_added = False label_added = False
for comp in res.path: for comp in res.path:
for poly in comp.geometry: for poly in comp.geometry:
x, y = poly.exterior.xy # Handle both Polygon and MultiPolygon (e.g. from SBend)
ax.fill(x, y, alpha=0.7, fc=color, ec="black", label=net_id if not label_added else "") geoms = [poly] if hasattr(poly, "exterior") else poly.geoms
label_added = True for g in geoms:
x, y = g.exterior.xy
ax.fill(x, y, alpha=0.7, fc=color, ec="black", label=net_id if not label_added else "")
label_added = True
ax.set_xlim(bounds[0], bounds[2]) ax.set_xlim(bounds[0], bounds[2])
ax.set_ylim(bounds[1], bounds[3]) ax.set_ylim(bounds[1], bounds[3])