import pytest from inire.geometry.collision import CollisionEngine from inire.geometry.components import Bend90, Straight from inire.geometry.primitives import Port from inire.router.astar import AStarContext from inire.router.cost import CostEvaluator from inire.router.danger_map import DangerMap from inire.router.outcomes import RoutingOutcome from inire.router.pathfinder import PathFinder, RoutingResult from inire.router.session import ( create_routing_session_state, prepare_routing_session_state, run_routing_iteration, ) @pytest.fixture def basic_evaluator() -> CostEvaluator: engine = CollisionEngine(clearance=2.0) danger_map = DangerMap(bounds=(0, 0, 100, 100)) danger_map.precompute([]) return CostEvaluator(engine, danger_map) def _build_manual_path(start: Port, width: float, clearance: float, steps: list[tuple[str, float | str]]) -> list: path = [] curr = start dilation = clearance / 2.0 for kind, value in steps: if kind == "B": comp = Bend90.generate(curr, 5.0, width, value, dilation=dilation) else: comp = Straight.generate(curr, value, width, dilation=dilation) path.append(comp) curr = comp.end_port return path def test_pathfinder_parallel(basic_evaluator: CostEvaluator) -> None: context = AStarContext(basic_evaluator) pf = PathFinder(context) netlist = { "net1": (Port(0, 0, 0), Port(50, 0, 0)), "net2": (Port(0, 10, 0), Port(50, 10, 0)), } net_widths = {"net1": 2.0, "net2": 2.0} results = pf.route_all(netlist, net_widths) assert len(results) == 2 assert results["net1"].is_valid assert results["net2"].is_valid assert results["net1"].collisions == 0 assert results["net2"].collisions == 0 def test_pathfinder_crossing_detection(basic_evaluator: CostEvaluator) -> None: context = AStarContext(basic_evaluator) # Force a crossing by setting low iterations and low penalty pf = PathFinder(context, max_iterations=1, base_congestion_penalty=1.0, warm_start=None) # Net 1: (0, 25) -> (100, 25) Horizontal # Net 2: (50, 0) -> (50, 50) Vertical netlist = { "net1": (Port(0, 25, 0), Port(100, 25, 0)), "net2": (Port(50, 0, 90), Port(50, 50, 90)), } net_widths = {"net1": 2.0, "net2": 2.0} results = pf.route_all(netlist, net_widths) # Both should be invalid because they cross assert not results["net1"].is_valid assert not results["net2"].is_valid assert results["net1"].collisions > 0 assert results["net2"].collisions > 0 def test_prepare_routing_session_state_builds_warm_start_and_sorts_nets( basic_evaluator: CostEvaluator, monkeypatch: pytest.MonkeyPatch, ) -> None: context = AStarContext(basic_evaluator) pf = PathFinder(context) calls: list[tuple[str, list[str]]] = [] cleared: list[bool] = [] def fake_build( netlist: dict[str, tuple[Port, Port]], net_widths: dict[str, float], order: str, ) -> dict[str, list]: calls.append((order, list(netlist.keys()))) return {"warm": []} monkeypatch.setattr(PathFinder, "_build_greedy_warm_start_paths", lambda self, netlist, net_widths, order: fake_build(netlist, net_widths, order)) monkeypatch.setattr(AStarContext, "clear_static_caches", lambda self: cleared.append(True)) netlist = { "short": (Port(0, 0, 0), Port(10, 0, 0)), "long": (Port(0, 0, 0), Port(40, 10, 0)), "mid": (Port(0, 0, 0), Port(20, 0, 0)), } state = create_routing_session_state( pf, netlist, {net_id: 2.0 for net_id in netlist}, store_expanded=False, iteration_callback=None, shuffle_nets=False, sort_nets="longest", initial_paths=None, seed=None, ) prepare_routing_session_state(pf, state) assert calls == [("longest", ["short", "long", "mid"])] assert cleared == [True] assert state.initial_paths == {"warm": []} assert state.all_net_ids == ["long", "mid", "short"] def test_run_routing_iteration_updates_results_and_invokes_callback( basic_evaluator: CostEvaluator, monkeypatch: pytest.MonkeyPatch, ) -> None: context = AStarContext(basic_evaluator) pf = PathFinder(context) callback_results: list[dict[str, RoutingResult]] = [] def fake_route_once( net_id: str, start: Port, target: Port, width: float, iteration: int, initial_paths: dict[str, list] | None, store_expanded: bool, needs_self_collision_check: set[str], ) -> tuple[RoutingResult, RoutingOutcome]: _ = (start, target, width, iteration, initial_paths, store_expanded, needs_self_collision_check) result = RoutingResult( net_id, [], net_id == "net1", int(net_id == "net2"), reached_target=True, outcome="completed" if net_id == "net1" else "colliding", ) return result, result.outcome monkeypatch.setattr( PathFinder, "_route_net_once", lambda self, net_id, start, target, width, iteration, initial_paths, store_expanded, needs_self_collision_check: fake_route_once( net_id, start, target, width, iteration, initial_paths, store_expanded, needs_self_collision_check, ), ) state = create_routing_session_state( pf, {"net1": (Port(0, 0, 0), Port(10, 0, 0)), "net2": (Port(0, 10, 0), Port(10, 10, 0))}, {"net1": 2.0, "net2": 2.0}, store_expanded=True, iteration_callback=lambda iteration, results: callback_results.append(dict(results)), shuffle_nets=False, sort_nets=None, initial_paths={"seeded": []}, seed=None, ) outcomes = run_routing_iteration(pf, state, iteration=0) assert outcomes == {"net1": "completed", "net2": "colliding"} assert set(state.results) == {"net1", "net2"} assert callback_results and set(callback_results[0]) == {"net1", "net2"} assert state.results["net1"].is_valid assert not state.results["net2"].is_valid assert state.results["net2"].outcome == "colliding" def test_run_routing_iteration_timeout_finalizes_tree( basic_evaluator: CostEvaluator, monkeypatch: pytest.MonkeyPatch, ) -> None: context = AStarContext(basic_evaluator) pf = PathFinder(context) finalized: list[bool] = [] monkeypatch.setattr(type(pf.path_state), "finalize_dynamic_tree", lambda self: finalized.append(True)) state = create_routing_session_state( pf, {"net1": (Port(0, 0, 0), Port(10, 0, 0))}, {"net1": 2.0}, store_expanded=False, iteration_callback=None, shuffle_nets=False, sort_nets=None, initial_paths={}, seed=None, ) state.start_time = 0.0 state.session_timeout = 0.0 result = run_routing_iteration(pf, state, iteration=0) assert result is None assert finalized == [True] def test_route_all_retries_partial_paths_across_iterations( basic_evaluator: CostEvaluator, monkeypatch: pytest.MonkeyPatch, ) -> None: context = AStarContext(basic_evaluator) pf = PathFinder(context, max_iterations=3, warm_start=None, refine_paths=False) calls: list[int] = [] class FakeComponent: def __init__(self, start_port: Port, end_port: Port) -> None: self.start_port = start_port self.end_port = end_port def fake_route_astar( start: Port, target: Port, width: float, *, context: AStarContext, metrics: object, net_id: str, bend_collision_type: str, return_partial: bool, store_expanded: bool, skip_congestion: bool, self_collision_check: bool, node_limit: int, ) -> list[FakeComponent]: _ = ( width, context, metrics, net_id, bend_collision_type, return_partial, store_expanded, skip_congestion, self_collision_check, node_limit, ) calls.append(len(calls)) if len(calls) == 1: return [FakeComponent(start, Port(5, 0, 0))] return [FakeComponent(start, target)] monkeypatch.setattr("inire.router.pathfinder.route_astar", fake_route_astar) monkeypatch.setattr(type(pf.path_state), "install_path", lambda self, net_id, path: None) monkeypatch.setattr(type(pf.path_state), "remove_path", lambda self, net_id: None) monkeypatch.setattr( type(pf.path_state), "verify_path_report", lambda self, net_id, path: basic_evaluator.collision_engine.verify_path_report(net_id, []), ) monkeypatch.setattr(type(pf.path_state), "finalize_dynamic_tree", lambda self: None) results = pf.route_all({"net": (Port(0, 0, 0), Port(10, 0, 0))}, {"net": 2.0}) assert calls == [0, 1] assert results["net"].reached_target assert results["net"].is_valid assert results["net"].outcome == "completed" def test_pathfinder_refine_paths_reduces_locked_detour_bends() -> None: bounds = (0, -50, 100, 50) def build_pathfinder(*, refine_paths: bool) -> tuple[CollisionEngine, PathFinder]: engine = CollisionEngine(clearance=2.0) danger_map = DangerMap(bounds=bounds) danger_map.precompute([]) evaluator = CostEvaluator(engine, danger_map, bend_penalty=250.0, sbend_penalty=500.0) context = AStarContext(evaluator, bend_radii=[10.0]) return engine, PathFinder(context, refine_paths=refine_paths) base_engine, base_pf = build_pathfinder(refine_paths=False) base_pf.route_all({"netA": (Port(10, 0, 0), Port(90, 0, 0))}, {"netA": 2.0}) base_engine.lock_net("netA") base_result = base_pf.route_all({"netB": (Port(50, -20, 90), Port(50, 20, 90))}, {"netB": 2.0})["netB"] refined_engine, refined_pf = build_pathfinder(refine_paths=True) refined_pf.route_all({"netA": (Port(10, 0, 0), Port(90, 0, 0))}, {"netA": 2.0}) refined_engine.lock_net("netA") refined_result = refined_pf.route_all({"netB": (Port(50, -20, 90), Port(50, 20, 90))}, {"netB": 2.0})["netB"] base_bends = sum(1 for comp in base_result.path if comp.move_type == "Bend90") refined_bends = sum(1 for comp in refined_result.path if comp.move_type == "Bend90") assert base_result.is_valid assert refined_result.is_valid assert refined_bends < base_bends assert refined_pf._path_cost(refined_result.path) < base_pf._path_cost(base_result.path) def test_pathfinder_refine_paths_simplifies_triple_crossing_detours() -> None: bounds = (0, 0, 100, 100) netlist = { "horizontal": (Port(10, 50, 0), Port(90, 50, 0)), "vertical_up": (Port(45, 10, 90), Port(45, 90, 90)), "vertical_down": (Port(55, 90, 270), Port(55, 10, 270)), } net_widths = {net_id: 2.0 for net_id in netlist} def build_pathfinder(*, refine_paths: bool) -> PathFinder: engine = CollisionEngine(clearance=2.0) danger_map = DangerMap(bounds=bounds) danger_map.precompute([]) evaluator = CostEvaluator(engine, danger_map, greedy_h_weight=1.5, bend_penalty=250.0, sbend_penalty=500.0) context = AStarContext(evaluator, bend_radii=[10.0], sbend_radii=[10.0]) return PathFinder(context, base_congestion_penalty=1000.0, refine_paths=refine_paths) base_results = build_pathfinder(refine_paths=False).route_all(netlist, net_widths) refined_results = build_pathfinder(refine_paths=True).route_all(netlist, net_widths) for net_id in ("vertical_up", "vertical_down"): base_result = base_results[net_id] refined_result = refined_results[net_id] base_bends = sum(1 for comp in base_result.path if comp.move_type == "Bend90") refined_bends = sum(1 for comp in refined_result.path if comp.move_type == "Bend90") assert base_result.is_valid assert refined_result.is_valid assert refined_bends < base_bends def test_refine_path_handles_same_orientation_lateral_offset() -> None: engine = CollisionEngine(clearance=2.0) danger_map = DangerMap(bounds=(-20, -20, 120, 120)) danger_map.precompute([]) evaluator = CostEvaluator(engine, danger_map, bend_penalty=250.0, sbend_penalty=500.0) context = AStarContext(evaluator, bend_radii=[5.0, 10.0]) pf = PathFinder(context, refine_paths=True) start = Port(0, 0, 0) width = 2.0 path = _build_manual_path( start, width, engine.clearance, [ ("B", "CCW"), ("S", 10.0), ("B", "CW"), ("S", 20.0), ("B", "CW"), ("S", 10.0), ("B", "CCW"), ("S", 10.0), ("B", "CCW"), ("S", 5.0), ("B", "CW"), ], ) target = path[-1].end_port refined = pf._refine_path("net", start, target, width, path) assert target == Port(60, 15, 0) assert sum(1 for comp in path if comp.move_type == "Bend90") == 6 assert sum(1 for comp in refined if comp.move_type == "Bend90") == 4 assert refined[-1].end_port == target assert pf._path_cost(refined) < pf._path_cost(path) def test_refine_path_can_simplify_subpath_with_different_global_orientation() -> None: engine = CollisionEngine(clearance=2.0) danger_map = DangerMap(bounds=(-20, -20, 120, 120)) danger_map.precompute([]) evaluator = CostEvaluator(engine, danger_map, bend_penalty=250.0, sbend_penalty=500.0) context = AStarContext(evaluator, bend_radii=[5.0, 10.0]) pf = PathFinder(context, refine_paths=True) start = Port(0, 0, 0) width = 2.0 path = _build_manual_path( start, width, engine.clearance, [ ("B", "CCW"), ("S", 10.0), ("B", "CW"), ("S", 20.0), ("B", "CW"), ("S", 10.0), ("B", "CCW"), ("S", 10.0), ("B", "CCW"), ("S", 5.0), ("B", "CW"), ("B", "CCW"), ("S", 10.0), ], ) target = path[-1].end_port refined = pf._refine_path("net", start, target, width, path) assert target == Port(65, 30, 90) assert sum(1 for comp in path if comp.move_type == "Bend90") == 7 assert sum(1 for comp in refined if comp.move_type == "Bend90") == 5 assert refined[-1].end_port == target assert pf._path_cost(refined) < pf._path_cost(path)