more s-bend fixes and add docs
This commit is contained in:
parent
18b2f83a7b
commit
41a2d9f058
10 changed files with 249 additions and 116 deletions
53
DOCS.md
Normal file
53
DOCS.md
Normal 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**.
|
||||||
|
|
@ -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 |
|
|
@ -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)
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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])
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue