first sparse steps

This commit is contained in:
Jan Petykiewicz 2026-03-13 20:13:05 -07:00
commit 24ca402f67
6 changed files with 139 additions and 12 deletions

View file

@ -245,3 +245,67 @@ class CollisionEngine:
if other_net_id != net_id and dynamic_prepared[obj_id].intersects(test_poly):
count += 1
return count
def ray_cast(self, origin: Port, angle_deg: float, max_dist: float = 2000.0) -> float:
"""
Cast a ray and find the distance to the nearest static obstacle.
Args:
origin: Starting port (x, y).
angle_deg: Ray direction in degrees.
max_dist: Maximum lookahead distance.
Returns:
Distance to first collision, or max_dist if clear.
"""
import numpy
from shapely.geometry import LineString
rad = numpy.radians(angle_deg)
dx = max_dist * numpy.cos(rad)
dy = max_dist * numpy.sin(rad)
# Ray geometry
ray_line = LineString([(origin.x, origin.y), (origin.x + dx, origin.y + dy)])
# 1. Query R-Tree
candidates = self.static_index.intersection(ray_line.bounds)
min_dist = max_dist
# 2. Check Intersections
# Note: We intersect with DILATED obstacles to account for clearance
for obj_id in candidates:
obstacle = self.static_dilated[obj_id]
# Fast check with prepared geom? intersects() is fast, intersection() gives point
if self.static_prepared[obj_id].intersects(ray_line):
# Calculate exact intersection distance
intersection = ray_line.intersection(obstacle)
if intersection.is_empty:
continue
# Intersection could be MultiLineString or LineString or Point
# We want the point closest to origin
# Helper to get dist
def get_dist(geom):
if hasattr(geom, 'geoms'): # Multi-part
return min(get_dist(g) for g in geom.geoms)
# For line string, the intersection is the segment INSIDE the obstacle.
# The distance is the distance to the start of that segment.
# Or if it's a touch (Point), distance to point.
coords = geom.coords
# Distance to the first point of the intersection geometry
# (Assuming simple overlap, first point is entry)
p1 = coords[0]
return numpy.sqrt((p1[0] - origin.x)**2 + (p1[1] - origin.y)**2)
try:
d = get_dist(intersection)
# Subtract safety margin to be safe? No, let higher level handle margins.
if d < min_dist:
min_dist = d
except Exception:
pass # Robustness
return min_dist

View file

@ -445,8 +445,25 @@ class SBend:
# tan(theta / 2) = local_dy / local_dx
theta = 2 * numpy.arctan2(abs(local_dy), local_dx)
if abs(theta) < 1e-9:
# Practically straight, but offset implies we need a bend.
# If offset is also tiny, return a straight?
if abs(offset) < 1e-6:
# Degenerate case: effectively straight
return Straight.generate(start_port, numpy.sqrt(local_dx**2 + local_dy**2), width, snap_to_grid=False, dilation=dilation)
raise ValueError("SBend calculation failed: theta close to zero")
# Avoid division by zero if theta is 0 (though unlikely due to offset check)
actual_radius = abs(local_dy) / (2 * (1 - numpy.cos(theta))) if theta > 1e-9 else radius
denom = (2 * (1 - numpy.cos(theta)))
if abs(denom) < 1e-9:
raise ValueError("SBend calculation failed: radius denominator zero")
actual_radius = abs(local_dy) / denom
# Limit radius to prevent giant arcs
if actual_radius > 100000.0:
raise ValueError("SBend calculation failed: radius too large")
direction = 1 if local_dy > 0 else -1
c1_angle = rad_start + direction * numpy.pi / 2

View file

@ -154,6 +154,7 @@ class AStarRouter:
while open_set:
if nodes_expanded >= node_limit:
# logger.warning(f' AStar failed: node limit {node_limit} reached.')
return reconstruct_path(best_node) if return_partial else None
current = heapq.heappop(open_set)
@ -222,6 +223,7 @@ class AStarRouter:
proj = dx_t * cos_r + dy_t * sin_r
perp = -dx_t * sin_r + dy_t * cos_r
if proj > 0 and 0.5 <= abs(perp) < snap_dist:
# Try a few candidate radii
for radius in self.config.sbend_radii:
try:
res = SBend.generate(
@ -238,15 +240,29 @@ class AStarRouter:
except ValueError:
pass
# 2. Lattice Straights
# 2. Parametric Straights
cp = current.port
base_ori = round(cp.orientation, 2)
state_key = (int(cp.x / snap), int(cp.y / snap), int(base_ori / 1.0))
# Backwards pruning
allow_backwards = (dist_sq < 200*200)
# Ray cast to find max length
max_reach = self.cost_evaluator.collision_engine.ray_cast(cp, base_ori, self.config.max_straight_length)
# Subtract buffer for bend radius + margin
effective_max = max(self.config.min_straight_length, max_reach - 50.0) # Assume 50um bend radius
# Generate samples
lengths = [effective_max]
if self.config.num_straight_samples > 1 and effective_max > self.config.min_straight_length * 2:
# Add intermediate step
lengths.append(effective_max / 2.0)
# Add min length for maneuvering
lengths.append(self.config.min_straight_length)
# Deduplicate and sort
lengths = sorted(list(set(lengths)), reverse=True)
for length in self.config.straight_lengths:
for length in lengths:
# Level 1: Absolute cache (exact location)
abs_key = (state_key, 'S', length, net_width)
if abs_key in self._move_cache:
@ -256,7 +272,7 @@ class AStarRouter:
# Level 2: Relative cache (orientation only)
rel_key = (base_ori, 'S', length, net_width, self._self_dilation)
# OPTIMIZATION: Check hard collision set
# OPTIMIZATION: Check hard collision set BEFORE anything else
move_type = f'S{length}'
cache_key = (state_key[0], state_key[1], base_ori, move_type, net_width)
if cache_key in self._hard_collision_set:
@ -279,7 +295,10 @@ class AStarRouter:
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, move_type, snap=snap)
# 3. Lattice Bends
# Backwards pruning
angle_to_target = numpy.degrees(numpy.arctan2(dy_t, dx_t))
allow_backwards = (dist_sq < 200*200)
for radius in self.config.bend_radii:
for direction in ['CW', 'CCW']:
if not allow_backwards:
@ -325,9 +344,27 @@ class AStarRouter:
self._move_cache[abs_key] = res
self._add_node(current, res, target, net_width, net_id, open_set, closed_set, move_type, move_radius=radius, snap=snap)
# 4. Discrete SBends
for offset in self.config.sbend_offsets:
# 4. Parametric SBends
# Try both positive and negative offsets
offsets = self.config.sbend_offsets
# Dynamically add target alignment offset if within range
# Project target onto current frame
rad = numpy.radians(cp.orientation)
dx_local = (target.x - cp.x) * numpy.cos(rad) + (target.y - cp.y) * numpy.sin(rad)
dy_local = -(target.x - cp.x) * numpy.sin(rad) + (target.y - cp.y) * numpy.cos(rad)
if 0 < dx_local < snap_dist:
# If target is ahead, try to align Y
offsets = list(offsets) + [dy_local]
offsets = sorted(list(set(offsets))) # Uniquify
for offset in offsets:
for radius in self.config.sbend_radii:
# Validity check: offset < 2*R
if abs(offset) >= 2 * radius:
continue
move_type = f'SB{offset}R{radius}'
abs_key = (state_key, 'SB', offset, radius, net_width, self.config.bend_collision_type)
if abs_key in self._move_cache:

View file

@ -11,9 +11,18 @@ class RouterConfig:
node_limit: int = 1000000
snap_size: float = 5.0
straight_lengths: list[float] = field(default_factory=lambda: [10.0, 50.0, 100.0, 500.0, 1000.0])
bend_radii: list[float] = field(default_factory=lambda: [50.0, 100.0])
# Sparse Sampling Configuration
max_straight_length: float = 2000.0
num_straight_samples: int = 3
min_straight_length: float = 10.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])
# Deprecated but kept for compatibility during refactor
straight_lengths: list[float] = field(default_factory=list)
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
bend_penalty: float = 250.0