make sure to check self-intersections
This commit is contained in:
parent
d2d619fe1f
commit
de7254f8f5
4 changed files with 154 additions and 10 deletions
|
|
@ -49,7 +49,7 @@ class ComponentResult:
|
||||||
_offsets: list[int] | None = None,
|
_offsets: list[int] | None = None,
|
||||||
_coords_cache: numpy.ndarray | None = None,
|
_coords_cache: numpy.ndarray | None = None,
|
||||||
_base_result: ComponentResult | None = None,
|
_base_result: ComponentResult | None = None,
|
||||||
_offset: list[float] | None = None,
|
_offset: numpy.ndarray | None = None,
|
||||||
snap_size: float = SEARCH_GRID_SNAP_UM,
|
snap_size: float = SEARCH_GRID_SNAP_UM,
|
||||||
rel_gx: int | None = None,
|
rel_gx: int | None = None,
|
||||||
rel_gy: int | None = None,
|
rel_gy: int | None = None,
|
||||||
|
|
@ -216,17 +216,18 @@ class ComponentResult:
|
||||||
base = self._base_result
|
base = self._base_result
|
||||||
if base is not None:
|
if base is not None:
|
||||||
dx, dy = self._offset
|
dx, dy = self._offset
|
||||||
|
shift = numpy.array([dx, dy, dx, dy])
|
||||||
# Vectorized addition is faster if we avoid creating new lists/arrays in the loop
|
# Vectorized addition is faster if we avoid creating new lists/arrays in the loop
|
||||||
if base._bounds is not None:
|
if base._bounds is not None:
|
||||||
self._bounds = base._bounds + [dx, dy, dx, dy]
|
self._bounds = base._bounds + shift
|
||||||
if base._total_bounds is not None:
|
if base._total_bounds is not None:
|
||||||
b = base._total_bounds
|
b = base._total_bounds
|
||||||
self._total_bounds = numpy.array([b[0]+dx, b[1]+dy, b[2]+dx, b[3]+dy])
|
self._total_bounds = b + shift
|
||||||
if base._dilated_bounds is not None:
|
if base._dilated_bounds is not None:
|
||||||
self._dilated_bounds = base._dilated_bounds + [dx, dy, dx, dy]
|
self._dilated_bounds = base._dilated_bounds + shift
|
||||||
if base._total_dilated_bounds is not None:
|
if base._total_dilated_bounds is not None:
|
||||||
b = base._total_dilated_bounds
|
b = base._total_dilated_bounds
|
||||||
self._total_dilated_bounds = numpy.array([b[0]+dx, b[1]+dy, b[2]+dx, b[3]+dy])
|
self._total_dilated_bounds = b + shift
|
||||||
self._bounds_cached = True
|
self._bounds_cached = True
|
||||||
|
|
||||||
def translate(self, dx: float, dy: float, rel_gx: int | None = None, rel_gy: int | None = None, rel_go: int | None = None) -> ComponentResult:
|
def translate(self, dx: float, dy: float, rel_gx: int | None = None, rel_gy: int | None = None, rel_go: int | None = None) -> ComponentResult:
|
||||||
|
|
@ -241,10 +242,10 @@ class ComponentResult:
|
||||||
if self._base_result:
|
if self._base_result:
|
||||||
base = self._base_result
|
base = self._base_result
|
||||||
current_offset = self._offset
|
current_offset = self._offset
|
||||||
new_offset = [current_offset[0] + dx, current_offset[1] + dy]
|
new_offset = numpy.array([current_offset[0] + dx, current_offset[1] + dy])
|
||||||
else:
|
else:
|
||||||
base = self
|
base = self
|
||||||
new_offset = [dx, dy]
|
new_offset = numpy.array([dx, dy])
|
||||||
|
|
||||||
return ComponentResult(
|
return ComponentResult(
|
||||||
end_port=new_port,
|
end_port=new_port,
|
||||||
|
|
@ -607,6 +608,11 @@ class SBend:
|
||||||
|
|
||||||
actual_radius = abs(local_dy) / denom
|
actual_radius = abs(local_dy) / denom
|
||||||
|
|
||||||
|
# Safety Check: Reject SBends with tiny radii that would cause self-overlap
|
||||||
|
# (inner_radius becomes negative if actual_radius < width/2)
|
||||||
|
if actual_radius < width:
|
||||||
|
raise ValueError(f"SBend actual_radius {actual_radius:.3f} is too small (width={width})")
|
||||||
|
|
||||||
# Limit radius to prevent giant arcs
|
# Limit radius to prevent giant arcs
|
||||||
if actual_radius > 100000.0:
|
if actual_radius > 100000.0:
|
||||||
actual_len = numpy.sqrt(local_dx**2 + local_dy**2)
|
actual_len = numpy.sqrt(local_dx**2 + local_dy**2)
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,8 @@ class AStarRouter:
|
||||||
"""
|
"""
|
||||||
__slots__ = ('cost_evaluator', 'config', 'node_limit', 'visibility_manager',
|
__slots__ = ('cost_evaluator', 'config', 'node_limit', 'visibility_manager',
|
||||||
'_hard_collision_set', '_congestion_cache', '_static_safe_cache',
|
'_hard_collision_set', '_congestion_cache', '_static_safe_cache',
|
||||||
'_move_cache', 'total_nodes_expanded', 'last_expanded_nodes', 'metrics')
|
'_move_cache', 'total_nodes_expanded', 'last_expanded_nodes', 'metrics',
|
||||||
|
'_self_collision_check')
|
||||||
|
|
||||||
def __init__(self, cost_evaluator: CostEvaluator, node_limit: int | None = None, **kwargs) -> None:
|
def __init__(self, cost_evaluator: CostEvaluator, node_limit: int | None = None, **kwargs) -> None:
|
||||||
self.cost_evaluator = cost_evaluator
|
self.cost_evaluator = cost_evaluator
|
||||||
|
|
@ -121,11 +122,25 @@ class AStarRouter:
|
||||||
return_partial: bool = False,
|
return_partial: bool = False,
|
||||||
store_expanded: bool = False,
|
store_expanded: bool = False,
|
||||||
skip_congestion: bool = False,
|
skip_congestion: bool = False,
|
||||||
max_cost: float | None = None
|
max_cost: float | None = None,
|
||||||
|
self_collision_check: bool = False,
|
||||||
) -> list[ComponentResult] | None:
|
) -> list[ComponentResult] | None:
|
||||||
"""
|
"""
|
||||||
Route a single net using A*.
|
Route a single net using A*.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
start: Starting port.
|
||||||
|
target: Target port.
|
||||||
|
net_width: Waveguide width.
|
||||||
|
net_id: Identifier for the net.
|
||||||
|
bend_collision_type: Type of collision model to use for bends.
|
||||||
|
return_partial: If True, returns the best-effort path if target not reached.
|
||||||
|
store_expanded: If True, keep track of all expanded nodes for visualization.
|
||||||
|
skip_congestion: If True, ignore other nets' paths (greedy mode).
|
||||||
|
max_cost: Hard limit on f_cost to prune search.
|
||||||
|
self_collision_check: If True, prevent the net from crossing its own path.
|
||||||
"""
|
"""
|
||||||
|
self._self_collision_check = self_collision_check
|
||||||
self._congestion_cache.clear()
|
self._congestion_cache.clear()
|
||||||
if store_expanded:
|
if store_expanded:
|
||||||
self.last_expanded_nodes = []
|
self.last_expanded_nodes = []
|
||||||
|
|
@ -440,6 +455,22 @@ class AStarRouter:
|
||||||
total_overlaps = self.cost_evaluator.collision_engine.check_move_congestion(result, net_id)
|
total_overlaps = self.cost_evaluator.collision_engine.check_move_congestion(result, net_id)
|
||||||
self._congestion_cache[cache_key] = total_overlaps
|
self._congestion_cache[cache_key] = total_overlaps
|
||||||
|
|
||||||
|
# SELF-COLLISION CHECK (Optional for performance)
|
||||||
|
if getattr(self, '_self_collision_check', False):
|
||||||
|
curr_p = parent
|
||||||
|
new_tb = result.total_bounds
|
||||||
|
while curr_p and curr_p.parent:
|
||||||
|
ancestor_res = curr_p.component_result
|
||||||
|
if ancestor_res:
|
||||||
|
anc_tb = ancestor_res.total_bounds
|
||||||
|
if (new_tb[0] < anc_tb[2] and new_tb[2] > anc_tb[0] and
|
||||||
|
new_tb[1] < anc_tb[3] and new_tb[3] > anc_tb[1]):
|
||||||
|
for p_anc in ancestor_res.geometry:
|
||||||
|
for p_new in result.geometry:
|
||||||
|
if p_new.intersects(p_anc) and not p_new.touches(p_anc):
|
||||||
|
return
|
||||||
|
curr_p = curr_p.parent
|
||||||
|
|
||||||
penalty = 0.0
|
penalty = 0.0
|
||||||
if 'SB' in move_type: penalty = self.config.sbend_penalty
|
if 'SB' in move_type: penalty = self.config.sbend_penalty
|
||||||
elif 'B' in move_type: penalty = self.config.bend_penalty
|
elif 'B' in move_type: penalty = self.config.bend_penalty
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,31 @@ class PathFinder:
|
||||||
logger.info(f"PathFinder: Greedy Warm-Start finished. Seeding {len(greedy_paths)}/{len(netlist)} nets.")
|
logger.info(f"PathFinder: Greedy Warm-Start finished. Seeding {len(greedy_paths)}/{len(netlist)} nets.")
|
||||||
return greedy_paths
|
return greedy_paths
|
||||||
|
|
||||||
|
def _has_self_collision(self, path: list[ComponentResult]) -> bool:
|
||||||
|
"""
|
||||||
|
Quickly check if a path intersects itself.
|
||||||
|
"""
|
||||||
|
if not path:
|
||||||
|
return False
|
||||||
|
|
||||||
|
num_components = len(path)
|
||||||
|
for i in range(num_components):
|
||||||
|
comp_i = path[i]
|
||||||
|
tb_i = comp_i.total_bounds
|
||||||
|
for j in range(i + 2, num_components): # Skip immediate neighbors
|
||||||
|
comp_j = path[j]
|
||||||
|
tb_j = comp_j.total_bounds
|
||||||
|
|
||||||
|
# AABB Check
|
||||||
|
if (tb_i[0] < tb_j[2] and tb_i[2] > tb_j[0] and
|
||||||
|
tb_i[1] < tb_j[3] and tb_i[3] > tb_j[1]):
|
||||||
|
# Real geometry check
|
||||||
|
for p_i in comp_i.geometry:
|
||||||
|
for p_j in comp_j.geometry:
|
||||||
|
if p_i.intersects(p_j) and not p_i.touches(p_j):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def route_all(
|
def route_all(
|
||||||
self,
|
self,
|
||||||
netlist: dict[str, tuple[Port, Port]],
|
netlist: dict[str, tuple[Port, Port]],
|
||||||
|
|
@ -180,6 +205,7 @@ class PathFinder:
|
||||||
session_timeout = max(60.0, 10.0 * num_nets * self.max_iterations)
|
session_timeout = max(60.0, 10.0 * num_nets * self.max_iterations)
|
||||||
|
|
||||||
all_net_ids = list(netlist.keys())
|
all_net_ids = list(netlist.keys())
|
||||||
|
needs_sc = set() # Nets requiring self-collision avoidance
|
||||||
|
|
||||||
# Determine initial paths (Warm Start)
|
# Determine initial paths (Warm Start)
|
||||||
if initial_paths is None:
|
if initial_paths is None:
|
||||||
|
|
@ -249,7 +275,7 @@ class PathFinder:
|
||||||
original_limit = self.router.node_limit
|
original_limit = self.router.node_limit
|
||||||
self.router.node_limit = current_node_limit
|
self.router.node_limit = current_node_limit
|
||||||
|
|
||||||
path = self.router.route(start, target, width, net_id=net_id, bend_collision_type=coll_model, return_partial=True, store_expanded=store_expanded, skip_congestion=skip_cong)
|
path = self.router.route(start, target, width, net_id=net_id, bend_collision_type=coll_model, return_partial=True, store_expanded=store_expanded, skip_congestion=skip_cong, self_collision_check=(net_id in needs_sc))
|
||||||
|
|
||||||
if store_expanded and self.router.last_expanded_nodes:
|
if store_expanded and self.router.last_expanded_nodes:
|
||||||
self.accumulated_expanded_nodes.extend(self.router.last_expanded_nodes)
|
self.accumulated_expanded_nodes.extend(self.router.last_expanded_nodes)
|
||||||
|
|
@ -258,6 +284,13 @@ class PathFinder:
|
||||||
logger.debug(f' Net {net_id} routed in {time.monotonic() - net_start:.4f}s using {coll_model}')
|
logger.debug(f' Net {net_id} routed in {time.monotonic() - net_start:.4f}s using {coll_model}')
|
||||||
|
|
||||||
if path:
|
if path:
|
||||||
|
# Check for self-collision if not already handled by router
|
||||||
|
if net_id not in needs_sc:
|
||||||
|
if self._has_self_collision(path):
|
||||||
|
logger.info(f' Net {net_id} detected self-collision. Enabling protection for next iteration.')
|
||||||
|
needs_sc.add(net_id)
|
||||||
|
any_congestion = True
|
||||||
|
|
||||||
# Check if reached exactly
|
# Check if reached exactly
|
||||||
last_p = path[-1].end_port
|
last_p = path[-1].end_port
|
||||||
reached = (abs(last_p.x - target.x) < 1e-6 and
|
reached = (abs(last_p.x - target.x) < 1e-6 and
|
||||||
|
|
|
||||||
74
inire/tests/test_failed_net_congestion.py
Normal file
74
inire/tests/test_failed_net_congestion.py
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import numpy
|
||||||
|
from inire.geometry.primitives import Port
|
||||||
|
from inire.geometry.collision import CollisionEngine
|
||||||
|
from inire.router.cost import CostEvaluator
|
||||||
|
from inire.router.astar import AStarRouter
|
||||||
|
from inire.router.pathfinder import PathFinder
|
||||||
|
from inire.router.danger_map import DangerMap
|
||||||
|
|
||||||
|
def test_failed_net_visibility():
|
||||||
|
"""
|
||||||
|
Verifies that nets that fail to reach their target (return partial paths)
|
||||||
|
ARE added to the collision engine, making them visible to other nets
|
||||||
|
for negotiated congestion.
|
||||||
|
"""
|
||||||
|
# 1. Setup
|
||||||
|
engine = CollisionEngine(clearance=2.0)
|
||||||
|
|
||||||
|
# Create a simple danger map (bounds 0-100)
|
||||||
|
# We don't strictly need obstacles in it for this test.
|
||||||
|
dm = DangerMap(bounds=(0, 0, 100, 100))
|
||||||
|
|
||||||
|
evaluator = CostEvaluator(engine, dm)
|
||||||
|
|
||||||
|
# 2. Configure Router with low limit to FORCE failure
|
||||||
|
# node_limit=5 is extremely low, likely allowing only a few moves.
|
||||||
|
# Start (0,0) -> Target (100,0) is 100um away.
|
||||||
|
# If snap is 1.0, direct jump S100 might be tried.
|
||||||
|
# If direct jump works, it might succeed in 1 expansion.
|
||||||
|
# So we need to block the direct jump or make the limit VERY small (0?).
|
||||||
|
# Or place a static obstacle that forces a search.
|
||||||
|
|
||||||
|
# Let's add a static obstacle that blocks the direct path.
|
||||||
|
from shapely.geometry import box
|
||||||
|
obstacle = box(40, -10, 60, 10) # Wall at x=50
|
||||||
|
engine.add_static_obstacle(obstacle)
|
||||||
|
|
||||||
|
# With obstacle, direct jump fails. A* must search around.
|
||||||
|
# Limit=10 should be enough to fail to find a path around.
|
||||||
|
router = AStarRouter(evaluator, node_limit=10)
|
||||||
|
|
||||||
|
# 3. Configure PathFinder
|
||||||
|
# max_iterations=1 because we only need to check the state after the first attempt.
|
||||||
|
pf = PathFinder(router, evaluator, max_iterations=1, warm_start=None)
|
||||||
|
|
||||||
|
netlist = {
|
||||||
|
"net1": (Port(0, 0, 0), Port(100, 0, 0))
|
||||||
|
}
|
||||||
|
net_widths = {"net1": 1.0}
|
||||||
|
|
||||||
|
# 4. Route
|
||||||
|
print("\nStarting Route...")
|
||||||
|
results = pf.route_all(netlist, net_widths)
|
||||||
|
|
||||||
|
res = results["net1"]
|
||||||
|
print(f"Result: is_valid={res.is_valid}, reached={res.reached_target}, path_len={len(res.path)}")
|
||||||
|
|
||||||
|
# 5. Verify Failure Condition
|
||||||
|
# We expect reached_target to be False because of node_limit + obstacle
|
||||||
|
assert not res.reached_target, "Test setup failed: Net reached target despite low limit!"
|
||||||
|
assert len(res.path) > 0, "Test setup failed: No partial path returned!"
|
||||||
|
|
||||||
|
# 6. Verify Visibility
|
||||||
|
# Check if net1 is in the collision engine
|
||||||
|
found_nets = set()
|
||||||
|
# CollisionEngine.dynamic_geometries: dict[obj_id, (net_id, poly)]
|
||||||
|
for obj_id, (nid, poly) in engine.dynamic_geometries.items():
|
||||||
|
found_nets.add(nid)
|
||||||
|
|
||||||
|
print(f"Nets found in engine: {found_nets}")
|
||||||
|
|
||||||
|
# The FIX Expectation: "net1" SHOULD be present
|
||||||
|
assert "net1" in found_nets, "Bug present: Net1 is invisible despite having partial path!"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue