import pytest from shapely.geometry import Polygon from inire.geometry.collision import CollisionEngine from inire.geometry.primitives import Port from inire.router.astar import AStarContext, route_astar from inire.router.cost import CostEvaluator from inire.router.danger_map import DangerMap from inire.router.pathfinder import RoutingResult from inire.utils.validation import validate_routing_result @pytest.fixture def basic_evaluator() -> CostEvaluator: engine = CollisionEngine(clearance=2.0) danger_map = DangerMap(bounds=(0, -50, 150, 150)) danger_map.precompute([]) return CostEvaluator(engine, danger_map, bend_penalty=50.0, sbend_penalty=150.0) def test_astar_straight(basic_evaluator: CostEvaluator) -> None: context = AStarContext(basic_evaluator, snap_size=1.0) start = Port(0, 0, 0) target = Port(50, 0, 0) path = route_astar(start, target, net_width=2.0, context=context) assert path is not None result = RoutingResult(net_id="test", path=path, is_valid=True, collisions=0) validation = validate_routing_result(result, [], clearance=2.0, expected_start=start, expected_end=target) assert validation["is_valid"], f"Validation failed: {validation.get('reason')}" assert validation["connectivity_ok"] # Path should be exactly 50um (or slightly more if it did weird things, but here it's straight) assert abs(validation["total_length"] - 50.0) < 1e-6 def test_astar_bend(basic_evaluator: CostEvaluator) -> None: context = AStarContext(basic_evaluator, snap_size=1.0, bend_radii=[10.0]) start = Port(0, 0, 0) # 20um right, 20um up. Needs a 10um bend and a 10um bend. target = Port(20, 20, 0) path = route_astar(start, target, net_width=2.0, context=context) assert path is not None result = RoutingResult(net_id="test", path=path, is_valid=True, collisions=0) validation = validate_routing_result(result, [], clearance=2.0, expected_start=start, expected_end=target) assert validation["is_valid"], f"Validation failed: {validation.get('reason')}" assert validation["connectivity_ok"] def test_astar_obstacle(basic_evaluator: CostEvaluator) -> None: # Add an obstacle in the middle of a straight path # Obstacle from x=20 to 40, y=-20 to 20 obstacle = Polygon([(20, -20), (40, -20), (40, 20), (20, 20)]) basic_evaluator.collision_engine.add_static_obstacle(obstacle) basic_evaluator.danger_map.precompute([obstacle]) context = AStarContext(basic_evaluator, snap_size=1.0, bend_radii=[10.0], node_limit=1000000) start = Port(0, 0, 0) target = Port(60, 0, 0) path = route_astar(start, target, net_width=2.0, context=context) assert path is not None result = RoutingResult(net_id="test", path=path, is_valid=True, collisions=0) validation = validate_routing_result(result, [obstacle], clearance=2.0, expected_start=start, expected_end=target) assert validation["is_valid"], f"Validation failed: {validation.get('reason')}" # Path should have detoured, so length > 50 assert validation["total_length"] > 50.0 def test_astar_snap_to_target_lookahead(basic_evaluator: CostEvaluator) -> None: context = AStarContext(basic_evaluator, snap_size=1.0) # Target is NOT on 1um grid start = Port(0, 0, 0) target = Port(10.1, 0, 0) path = route_astar(start, target, net_width=2.0, context=context) assert path is not None result = RoutingResult(net_id="test", path=path, is_valid=True, collisions=0) validation = validate_routing_result(result, [], clearance=2.0, expected_start=start, expected_end=target) assert validation["is_valid"], f"Validation failed: {validation.get('reason')}"