add s-bend example and surface config

This commit is contained in:
Jan Petykiewicz 2026-03-08 20:18:53 -07:00
commit 18b2f83a7b
7 changed files with 212 additions and 39 deletions

View file

@ -16,9 +16,10 @@ if TYPE_CHECKING:
class CollisionEngine:
"""Manages spatial queries for collision detection."""
def __init__(self, clearance: float, max_net_width: float = 2.0) -> None:
def __init__(self, clearance: float, max_net_width: float = 2.0, safety_zone_radius: float = 0.0021) -> None:
self.clearance = clearance
self.max_net_width = max_net_width
self.safety_zone_radius = safety_zone_radius
self.static_obstacles = rtree.index.Index()
# To store geometries for precise checks
self.obstacle_geometries: dict[int, Polygon] = {} # ID -> Polygon
@ -131,14 +132,14 @@ class CollisionEngine:
is_near_start = False
if start_port:
if (abs(ix_minx - start_port.x) < 0.0021 and abs(ix_maxx - start_port.x) < 0.0021 and
abs(ix_miny - start_port.y) < 0.0021 and abs(ix_maxy - start_port.y) < 0.0021):
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
is_near_end = False
if end_port:
if (abs(ix_minx - end_port.x) < 0.0021 and abs(ix_maxx - end_port.x) < 0.0021 and
abs(ix_miny - end_port.y) < 0.0021 and abs(ix_maxy - end_port.y) < 0.0021):
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 is_near_start or is_near_end:

View file

@ -34,7 +34,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)
@ -89,10 +89,10 @@ class Bend90:
# End port (snapped to lattice)
ex = snap_search_grid(cx + radius * np.cos(t_end))
ey = snap_search_grid(cy + radius * np.sin(t_end))
end_orientation = (start_port.orientation + turn_angle) % 360
end_port = Port(ex, ey, float(end_orientation))
actual_length = radius * np.pi / 2.0
# Generate arc geometry
@ -124,12 +124,12 @@ 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)
actual_length = 2 * radius * theta
# Arc centers and angles (Relative to start orientation)
direction = 1 if offset > 0 else -1
# Arc 1
c1_angle = rad_start + direction * np.pi / 2
cx1 = start_port.x + radius * np.cos(c1_angle)

View file

@ -7,6 +7,7 @@ from typing import TYPE_CHECKING
import numpy as np
from inire.geometry.components import Bend90, SBend, Straight
from inire.router.config import RouterConfig
if TYPE_CHECKING:
from inire.geometry.components import ComponentResult
@ -46,9 +47,46 @@ class AStarNode:
class AStarRouter:
def __init__(self, cost_evaluator: CostEvaluator) -> None:
"""Hybrid State-Lattice A* Router."""
def __init__(
self,
cost_evaluator: CostEvaluator,
node_limit: int = 1000000,
straight_lengths: list[float] | None = None,
bend_radii: list[float] | None = None,
sbend_offsets: list[float] | None = None,
sbend_radii: list[float] | None = None,
snap_to_target_dist: float = 20.0,
bend_penalty: float = 50.0,
sbend_penalty: float = 100.0,
) -> None:
"""
Initialize the A* Router.
Args:
cost_evaluator: The evaluator for path and proximity costs.
node_limit: Maximum number of nodes to expand before failing.
straight_lengths: List of lengths for straight move expansion.
bend_radii: List of radii for 90-degree bend moves.
sbend_offsets: List of lateral offsets for S-bend moves.
sbend_radii: List of radii for S-bend moves.
snap_to_target_dist: Distance threshold for lookahead snapping.
bend_penalty: Flat cost penalty for each 90-degree bend.
sbend_penalty: Flat cost penalty for each S-bend.
"""
self.cost_evaluator = cost_evaluator
self.node_limit = 1000000
self.config = RouterConfig(
node_limit=node_limit,
straight_lengths=straight_lengths if straight_lengths is not None else [1.0, 5.0, 25.0],
bend_radii=bend_radii if bend_radii is not None else [10.0],
sbend_offsets=sbend_offsets if sbend_offsets is not None else [-5.0, -2.0, 2.0, 5.0],
sbend_radii=sbend_radii if sbend_radii is not None else [10.0],
snap_to_target_dist=snap_to_target_dist,
bend_penalty=bend_penalty,
sbend_penalty=sbend_penalty,
)
self.node_limit = self.config.node_limit
self.total_nodes_expanded = 0
self._collision_cache: dict[tuple[float, float, float, str, float, str], bool] = {}
@ -107,7 +145,7 @@ class AStarRouter:
) -> None:
# 1. Snap-to-Target Look-ahead
dist = np.sqrt((current.port.x - target.x) ** 2 + (current.port.y - target.y) ** 2)
if dist < 30.0:
if dist < self.config.snap_to_target_dist:
# A. Try straight exact reach
if abs(current.port.orientation - target.orientation) < 0.1:
rad = np.radians(current.port.orientation)
@ -127,30 +165,37 @@ class AStarRouter:
proj = dx * np.cos(rad) + dy * np.sin(rad)
perp = -dx * np.sin(rad) + dy * np.cos(rad)
if proj > 0 and 0.5 <= abs(perp) < 20.0:
try:
res = SBend.generate(current.port, perp, 10.0, net_width)
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, "SnapSBend")
except ValueError:
pass
for radius in self.config.sbend_radii:
try:
res = SBend.generate(current.port, perp, radius, net_width)
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, "SnapSBend")
except ValueError:
pass
# 2. Lattice Straights
for length in [1.0, 5.0, 25.0]:
lengths = self.config.straight_lengths
if dist < 5.0:
fine_steps = [0.1, 0.5]
lengths = sorted(list(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}")
# 3. Lattice Bends
for radius in [10.0]:
for radius in self.config.bend_radii:
for direction in ["CW", "CCW"]:
res = Bend90.generate(current.port, radius, net_width, direction)
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f"B{radius}{direction}")
# 4. Discrete SBends
for offset in [-5.0, -2.0, 2.0, 5.0]:
try:
res = SBend.generate(current.port, offset, 10.0, net_width)
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f"SB{offset}")
except ValueError:
pass
for offset in self.config.sbend_offsets:
for radius in self.config.sbend_radii:
try:
res = SBend.generate(current.port, offset, radius, net_width)
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, f"SB{offset}R{radius}")
except ValueError:
pass
def _add_node(
self,
@ -194,12 +239,10 @@ class AStarRouter:
for move_poly in result.geometry:
dilated_move = move_poly.buffer(dilation)
curr_p = parent
# Skip immediate parent
seg_idx = 0
while curr_p and curr_p.component_result and seg_idx < 100:
if seg_idx > 0:
for prev_poly in curr_p.component_result.geometry:
# Optimization: fast bounding box check
if dilated_move.bounds[0] > prev_poly.bounds[2] + dilation or \
dilated_move.bounds[2] < prev_poly.bounds[0] - dilation or \
dilated_move.bounds[1] > prev_poly.bounds[3] + dilation or \
@ -226,12 +269,10 @@ class AStarRouter:
if move_cost > 1e12:
return
# Substantial penalties for turns to favor straights,
# but low enough to allow detours in complex environments.
if "B" in move_type:
move_cost += 50.0
move_cost += self.config.bend_penalty
if "SB" in move_type:
move_cost += 100.0
move_cost += self.config.sbend_penalty
g_cost = parent.g_cost + move_cost
h_cost = self.cost_evaluator.h_manhattan(result.end_port, target)

26
inire/router/config.py Normal file
View file

@ -0,0 +1,26 @@
from __future__ import annotations
from dataclasses import dataclass, field
@dataclass
class RouterConfig:
"""Configuration parameters for the A* Router."""
node_limit: int = 500000
straight_lengths: list[float] = field(default_factory=lambda: [1.0, 5.0, 25.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_radii: list[float] = field(default_factory=lambda: [10.0])
snap_to_target_dist: float = 20.0
bend_penalty: float = 50.0
sbend_penalty: float = 100.0
@dataclass
class CostConfig:
"""Configuration parameters for the Cost Evaluator."""
unit_length_cost: float = 1.0
greedy_h_weight: float = 1.1
congestion_penalty: float = 10000.0

View file

@ -2,6 +2,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from inire.router.config import CostConfig
if TYPE_CHECKING:
from shapely.geometry import Polygon
@ -11,16 +13,38 @@ if TYPE_CHECKING:
class CostEvaluator:
"""Calculates total cost f(n) = g(n) + h(n)."""
"""Calculates total path and proximity costs."""
def __init__(self, collision_engine: CollisionEngine, danger_map: DangerMap) -> None:
def __init__(
self,
collision_engine: CollisionEngine,
danger_map: DangerMap,
unit_length_cost: float = 1.0,
greedy_h_weight: float = 1.1,
congestion_penalty: float = 10000.0,
) -> None:
"""
Initialize the Cost Evaluator.
Args:
collision_engine: The engine for intersection checks.
danger_map: Pre-computed grid for heuristic proximity costs.
unit_length_cost: Cost multiplier per micrometer of path length.
greedy_h_weight: Heuristic weighting (A* greedy factor).
congestion_penalty: Multiplier for path overlaps in negotiated congestion.
"""
self.collision_engine = collision_engine
self.danger_map = danger_map
# Cost weights
self.unit_length_cost = 1.0
self.bend_cost_multiplier = 100.0 # Per turn penalty
self.greedy_h_weight = 1.1
self.congestion_penalty = 10000.0 # Massive multiplier for overlaps
self.config = CostConfig(
unit_length_cost=unit_length_cost,
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
self.congestion_penalty = self.config.congestion_penalty
def g_proximity(self, x: float, y: float) -> float:
"""Get proximity cost from the Danger Map."""