parametrized s-bend

This commit is contained in:
Jan Petykiewicz 2026-03-18 23:30:15 -07:00
commit 22ec194560
5 changed files with 55 additions and 36 deletions

View file

@ -11,7 +11,7 @@ The `AStarRouter` is the core pathfinding engine. It can be configured directly
| `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. | | `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. | | `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] \| None` | `None` (Auto) | Lateral offsets for parametric S-bends. `None` uses automatic grid-aligned steps. |
| `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. |
@ -87,4 +87,7 @@ In multi-net designs, if nets are overlapping:
3. If a solution is still not found, check if the `clearance` is physically possible given the design's narrowest bottlenecks. 3. If a solution is still not found, check if the `clearance` is physically possible given the design's narrowest bottlenecks.
### S-Bend Usage ### S-Bend Usage
Parametric S-bends are triggered by the `sbend_offsets` list. If you need a specific lateral shift (e.g., 5.86µm for a 45° switchover), add it to `sbend_offsets`. The router will only use an S-bend if it can reach a state that is exactly on the lattice or the target. Parametric S-bends bridge lateral gaps without changing the waveguide's orientation.
- **Automatic Selection**: If `sbend_offsets` is set to `None` (the default), the router automatically chooses from a set of "natural" offsets (Fibonacci-aligned grid steps) and the offset needed to hit the target.
- **Specific Offsets**: To use specific offsets (e.g., 5.86µm for a 45° switchover), provide them in the `sbend_offsets` list. The router will prioritize these but will still try to align with the target if possible.
- **Constraints**: S-bends are only used for offsets $O < 2R$. For larger shifts, the router naturally combines two 90° bends and a straight segment.

View file

@ -29,7 +29,7 @@ def main() -> None:
evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.5, unit_length_cost=0.1, bend_penalty=100.0, sbend_penalty=400.0, congestion_penalty=100.0) evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.5, unit_length_cost=0.1, bend_penalty=100.0, sbend_penalty=400.0, congestion_penalty=100.0)
router = AStarRouter(evaluator, node_limit=2000000, snap_size=5.0, bend_radii=[50.0], sbend_radii=[50.0], use_analytical_sbends=False) router = AStarRouter(evaluator, node_limit=2000000, snap_size=5.0, bend_radii=[50.0], sbend_radii=[50.0])
pf = PathFinder(router, evaluator, max_iterations=15, base_congestion_penalty=100.0, congestion_multiplier=1.4) pf = PathFinder(router, evaluator, max_iterations=15, base_congestion_penalty=100.0, congestion_multiplier=1.4)
# 2. Define Netlist # 2. Define Netlist

View file

@ -204,24 +204,6 @@ class AStarRouter:
if max_reach >= proj_t - 0.01: if max_reach >= proj_t - 0.01:
self._process_move(current, target, net_width, net_id, open_set, closed_set, snap, f'S{proj_t}', 'S', (proj_t,), skip_congestion, skip_static=True, snap_to_grid=False) self._process_move(current, target, net_width, net_id, open_set, closed_set, snap, f'S{proj_t}', 'S', (proj_t,), skip_congestion, skip_static=True, snap_to_grid=False)
# B. SBend Jump (Direct to Target)
if self.config.use_analytical_sbends and proj_t > 0 and abs(cp.orientation - target.orientation) < 0.1 and abs(perp_t) > 1e-3:
# Calculate required radius to hit target exactly: R = (dx^2 + dy^2) / (4*|dy|)
req_radius = (proj_t**2 + perp_t**2) / (4.0 * abs(perp_t))
min_radius = min(self.config.sbend_radii) if self.config.sbend_radii else 50.0
if req_radius >= min_radius:
# We can hit it exactly!
self._process_move(current, target, net_width, net_id, open_set, closed_set, snap, f'SB_Direct_R{req_radius:.1f}', 'SB', (perp_t, req_radius), skip_congestion, snap_to_grid=False)
else:
# Required radius is too small. We must use a larger radius and some straight segments.
# A* will handle this through Priority 3 SBends + Priority 2 Straights.
pass
# In super sparse mode, we can return here, but A* needs other options for optimality.
# return
# 2. VISIBILITY JUMPS & MAX REACH (Priority 2) # 2. VISIBILITY JUMPS & MAX REACH (Priority 2)
max_reach = self.cost_evaluator.collision_engine.ray_cast(cp, base_ori, self.config.max_straight_length) max_reach = self.cost_evaluator.collision_engine.ray_cast(cp, base_ori, self.config.max_straight_length)
@ -309,14 +291,35 @@ class AStarRouter:
continue continue
self._process_move(current, target, net_width, net_id, open_set, closed_set, snap, f'B{radius}{direction}', 'B', (radius, direction), skip_congestion) self._process_move(current, target, net_width, net_id, open_set, closed_set, snap, f'B{radius}{direction}', 'B', (radius, direction), skip_congestion)
if dist_sq < 400*400: # 4. SBENDS
offsets = set(self.config.sbend_offsets) max_sbend_r = max(self.config.sbend_radii) if self.config.sbend_radii else 0
if max_sbend_r > 0:
user_offsets = self.config.sbend_offsets
offsets: set[float] = set(user_offsets) if user_offsets is not None else set()
dx_local = (target.x - cp.x) * cos_v + (target.y - cp.y) * sin_v dx_local = (target.x - cp.x) * cos_v + (target.y - cp.y) * sin_v
dy_local = -(target.x - cp.x) * sin_v + (target.y - cp.y) * cos_v dy_local = -(target.x - cp.x) * sin_v + (target.y - cp.y) * cos_v
if 0 < dx_local < self.config.snap_to_target_dist:
offsets.add(dy_local)
for offset in offsets: # Always try aligning with target if it's forward and within reach
if dx_local > 0 and abs(dy_local) < 2 * max_sbend_r:
# Check if we have enough distance for the SBend
# Min distance D = sqrt(4RO - O^2). Smallest R is O/2.
min_d = numpy.sqrt(max(0, 4 * (abs(dy_local)/2.0) * abs(dy_local) - dy_local**2))
if dx_local >= min_d:
offsets.add(dy_local)
# If no offsets provided by user (None), the router "chooses" offsets
# by trying grid-aligned steps up to the reach of the largest radius.
if user_offsets is None:
# Try a selection of grid-aligned offsets.
# Fibonacci-ish steps are useful to cover different scales efficiently.
for sign in [-1, 1]:
for i in [1, 2, 3, 5, 8, 13, 21, 34, 55, 89]:
o = sign * i * snap
if abs(o) < 2 * max_sbend_r:
offsets.add(o)
for offset in sorted(offsets):
for radius in self.config.sbend_radii: for radius in self.config.sbend_radii:
if abs(offset) >= 2 * radius: continue if abs(offset) >= 2 * radius: continue
self._process_move(current, target, net_width, net_id, open_set, closed_set, snap, f'SB{offset}R{radius}', 'SB', (offset, radius), skip_congestion) self._process_move(current, target, net_width, net_id, open_set, closed_set, snap, f'SB{offset}R{radius}', 'SB', (offset, radius), skip_congestion)

View file

@ -16,8 +16,8 @@ class RouterConfig:
num_straight_samples: int = 5 num_straight_samples: int = 5
min_straight_length: float = 5.0 min_straight_length: float = 5.0
# Offsets for SBends (still list-based for now, or could range) # Offsets for SBends (None = automatic grid-based selection)
sbend_offsets: list[float] = field(default_factory=lambda: [-100.0, -50.0, -10.0, 10.0, 50.0, 100.0]) sbend_offsets: list[float] | None = None
# Deprecated but kept for compatibility during refactor # Deprecated but kept for compatibility during refactor
straight_lengths: list[float] = field(default_factory=list) straight_lengths: list[float] = field(default_factory=list)
@ -25,7 +25,6 @@ class RouterConfig:
bend_radii: list[float] = field(default_factory=lambda: [50.0, 100.0]) bend_radii: list[float] = field(default_factory=lambda: [50.0, 100.0])
sbend_radii: list[float] = field(default_factory=lambda: [50.0, 100.0, 500.0]) sbend_radii: list[float] = field(default_factory=lambda: [50.0, 100.0, 500.0])
snap_to_target_dist: float = 1000.0 snap_to_target_dist: float = 1000.0
use_analytical_sbends: bool = True
bend_penalty: float = 250.0 bend_penalty: float = 250.0
sbend_penalty: float = 500.0 sbend_penalty: float = 500.0
bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | Any = "arc" bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | Any = "arc"

View file

@ -9,17 +9,31 @@ def test_cost_calculation() -> None:
# 50x50 um area, 1um resolution # 50x50 um area, 1um resolution
danger_map = DangerMap(bounds=(0, 0, 50, 50)) danger_map = DangerMap(bounds=(0, 0, 50, 50))
danger_map.precompute([]) danger_map.precompute([])
evaluator = CostEvaluator(engine, danger_map) # Use small penalties for testing
evaluator = CostEvaluator(engine, danger_map, bend_penalty=10.0)
p1 = Port(0, 0, 0) p1 = Port(0, 0, 0)
p2 = Port(10, 10, 0) p2 = Port(10, 10, 0)
h = evaluator.h_manhattan(p1, p2) h = evaluator.h_manhattan(p1, p2)
# Manhattan distance = 20. Orientation penalty = 0. # Manhattan distance = 20.
# Weighted by 1.1 -> 22.0 # Jog alignment penalty = 2*bp = 20.
assert abs(h - 22.0) < 1e-6 # Side check penalty = 2*bp = 20.
# Total = 1.1 * (20 + 40) = 66.0
assert abs(h - 66.0) < 1e-6
# Orientation penalty # Orientation difference
p3 = Port(10, 10, 90) p3 = Port(10, 10, 90)
h_wrong = evaluator.h_manhattan(p1, p3) h_90 = evaluator.h_manhattan(p1, p3)
assert h_wrong > h # diff = 90. penalty += 1*bp = 10.
# Side check: 2*bp = 20. (Total penalty = 30)
# Total = 1.1 * (20 + 30) = 55.0
assert abs(h_90 - 55.0) < 1e-6
# Traveling away
p4 = Port(10, 10, 180)
h_away = evaluator.h_manhattan(p1, p4)
# diff = 180. penalty += 2*bp = 20.
# Side check: 2*bp = 20.
# Total = 1.1 * (20 + 40) = 66.0
assert h_away >= h_90