From 22ec194560c39d41ed943ef5dd3a7a59a594d3fe Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 18 Mar 2026 23:30:15 -0700 Subject: [PATCH] parametrized s-bend --- DOCS.md | 7 +++-- examples/07_large_scale_routing.py | 2 +- inire/router/astar.py | 49 ++++++++++++++++-------------- inire/router/config.py | 5 ++- inire/tests/test_cost.py | 28 ++++++++++++----- 5 files changed, 55 insertions(+), 36 deletions(-) diff --git a/DOCS.md b/DOCS.md index b6c8a1b..668bde9 100644 --- a/DOCS.md +++ b/DOCS.md @@ -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. | | `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_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). | | `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. | @@ -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. ### 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. diff --git a/examples/07_large_scale_routing.py b/examples/07_large_scale_routing.py index 082bb01..a0bbe68 100644 --- a/examples/07_large_scale_routing.py +++ b/examples/07_large_scale_routing.py @@ -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) - 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) # 2. Define Netlist diff --git a/inire/router/astar.py b/inire/router/astar.py index b74b8d6..ec84efe 100644 --- a/inire/router/astar.py +++ b/inire/router/astar.py @@ -204,24 +204,6 @@ class AStarRouter: 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) - # 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) max_reach = self.cost_evaluator.collision_engine.ray_cast(cp, base_ori, self.config.max_straight_length) @@ -309,14 +291,35 @@ class AStarRouter: continue 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: - offsets = set(self.config.sbend_offsets) + # 4. SBENDS + 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 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: 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) diff --git a/inire/router/config.py b/inire/router/config.py index b240ba9..6e9c25d 100644 --- a/inire/router/config.py +++ b/inire/router/config.py @@ -16,8 +16,8 @@ class RouterConfig: num_straight_samples: int = 5 min_straight_length: float = 5.0 - # Offsets for SBends (still list-based for now, or could range) - sbend_offsets: list[float] = field(default_factory=lambda: [-100.0, -50.0, -10.0, 10.0, 50.0, 100.0]) + # Offsets for SBends (None = automatic grid-based selection) + sbend_offsets: list[float] | None = None # Deprecated but kept for compatibility during refactor 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]) sbend_radii: list[float] = field(default_factory=lambda: [50.0, 100.0, 500.0]) snap_to_target_dist: float = 1000.0 - use_analytical_sbends: bool = True bend_penalty: float = 250.0 sbend_penalty: float = 500.0 bend_collision_type: Literal["arc", "bbox", "clipped_bbox"] | Any = "arc" diff --git a/inire/tests/test_cost.py b/inire/tests/test_cost.py index 73ef503..8158c50 100644 --- a/inire/tests/test_cost.py +++ b/inire/tests/test_cost.py @@ -9,17 +9,31 @@ def test_cost_calculation() -> None: # 50x50 um area, 1um resolution danger_map = DangerMap(bounds=(0, 0, 50, 50)) 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) p2 = Port(10, 10, 0) h = evaluator.h_manhattan(p1, p2) - # Manhattan distance = 20. Orientation penalty = 0. - # Weighted by 1.1 -> 22.0 - assert abs(h - 22.0) < 1e-6 + # Manhattan distance = 20. + # Jog alignment penalty = 2*bp = 20. + # 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) - h_wrong = evaluator.h_manhattan(p1, p3) - assert h_wrong > h + h_90 = evaluator.h_manhattan(p1, p3) + # 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