add s-bend example and surface config
This commit is contained in:
parent
82aaf066e2
commit
18b2f83a7b
7 changed files with 212 additions and 39 deletions
81
examples/04_sbends_and_radii.py
Normal file
81
examples/04_sbends_and_radii.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
from shapely.geometry import Polygon
|
||||
|
||||
from inire.geometry.collision import CollisionEngine
|
||||
from inire.geometry.primitives import Port
|
||||
from inire.router.astar import AStarRouter
|
||||
from inire.router.config import CostConfig, RouterConfig
|
||||
from inire.router.cost import CostEvaluator
|
||||
from inire.router.danger_map import DangerMap
|
||||
from inire.router.pathfinder import PathFinder
|
||||
from inire.utils.visualization import plot_routing_results
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print("Running Example 04: S-Bends and Multiple Radii...")
|
||||
|
||||
# 1. Setup Environment
|
||||
bounds = (0, 0, 150, 100)
|
||||
engine = CollisionEngine(clearance=2.0)
|
||||
danger_map = DangerMap(bounds=bounds)
|
||||
|
||||
# Create obstacles that force S-bends and turns
|
||||
# Obstacle 1: Forces a vertical jog (S-bend)
|
||||
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(
|
||||
engine,
|
||||
danger_map,
|
||||
unit_length_cost=1.0,
|
||||
greedy_h_weight=1.2,
|
||||
)
|
||||
|
||||
router = AStarRouter(
|
||||
evaluator,
|
||||
node_limit=500000,
|
||||
bend_radii=[10.0, 30.0], # Allow standard and large bends
|
||||
sbend_offsets=[-10.0, -5.0, 5.0, 10.0], # Allow larger S-bend offsets
|
||||
sbend_radii=[20.0, 50.0], # Large S-bends
|
||||
bend_penalty=10.0, # Lower penalty to encourage using the right bend
|
||||
)
|
||||
|
||||
pf = PathFinder(router, evaluator)
|
||||
|
||||
# 3. Define Netlist
|
||||
# Net 1: Needs to S-bend around obs1 (gap at y=60-100? No, obs1 is y=20-60).
|
||||
# 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 = {
|
||||
"large_bend_net": (Port(10, 10, 0), Port(140, 80, 0)),
|
||||
"sbend_net": (Port(10, 50, 0), Port(70, 70, 0)),
|
||||
}
|
||||
net_widths = {"large_bend_net": 2.0, "sbend_net": 2.0}
|
||||
|
||||
# 4. Route
|
||||
results = pf.route_all(netlist, net_widths)
|
||||
|
||||
# 5. Check Results
|
||||
for nid, res in results.items():
|
||||
status = "Success" if res.is_valid else "Failed"
|
||||
print(f"{nid}: {status}, collisions={res.collisions}")
|
||||
|
||||
# 6. Visualize
|
||||
fig, ax = plot_routing_results(results, obstacles, bounds)
|
||||
fig.savefig("examples/sbends_radii.png")
|
||||
print("Saved plot to examples/sbends_radii.png")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
BIN
examples/sbends_radii.png
Normal file
BIN
examples/sbends_radii.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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,28 +165,35 @@ 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:
|
||||
for radius in self.config.sbend_radii:
|
||||
try:
|
||||
res = SBend.generate(current.port, perp, 10.0, net_width)
|
||||
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]:
|
||||
for offset in self.config.sbend_offsets:
|
||||
for radius in self.config.sbend_radii:
|
||||
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}")
|
||||
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
|
||||
|
||||
|
|
@ -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
26
inire/router/config.py
Normal 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
|
||||
|
|
@ -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."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue