diff --git a/src/freeroute/cli.py b/src/freeroute/cli.py index 938cd55..bbfdc80 100644 --- a/src/freeroute/cli.py +++ b/src/freeroute/cli.py @@ -118,6 +118,14 @@ def build_parser() -> argparse.ArgumentParser: help="grid and exact engines: cells K times finer than trace + clearance " "(default 1); 2 or more reaches fine-pitch pins, at some cost in time", ) + parser.add_argument( + "--heuristic-weight", + type=float, + default=1.0, + metavar="W", + help="exact engine: weighted A* (W >= 1); larger is faster, routes at most " + "W times the shortest length (default 1)", + ) parser.add_argument( "--edge-clearance", type=float, @@ -175,6 +183,7 @@ def main(argv: list[str] | None = None) -> int: max_passes=max_passes, edge_clearance_mm=args.edge_clearance, resolution=args.grid_resolution, + heuristic_weight=args.heuristic_weight, ) elif args.engine == "room": result = build_rooms_routing_result( diff --git a/src/freeroute/route/exact_router.py b/src/freeroute/route/exact_router.py index db79c49..365b001 100644 --- a/src/freeroute/route/exact_router.py +++ b/src/freeroute/route/exact_router.py @@ -93,6 +93,7 @@ def route_board_exact( via_half: int | None = None, edge_clearance: int | None = None, resolution: int = 1, + heuristic_weight: float = 1.0, ) -> ExactRouteResult: """Route ``board`` orthogonally and verify the result with exact geometry. @@ -120,6 +121,7 @@ def route_board_exact( via_half=via_half, edge_clearance=edge_clearance, resolution=resolution, + heuristic_weight=heuristic_weight, ) edge = 0 if edge_clearance is None else edge_clearance outline = board.outline diff --git a/src/freeroute/route/grid_router.py b/src/freeroute/route/grid_router.py index 8d6c8e2..57d7f9c 100644 --- a/src/freeroute/route/grid_router.py +++ b/src/freeroute/route/grid_router.py @@ -138,8 +138,10 @@ class GridRouter: via_half: int | None = None, edge_clearance: int | None = None, resolution: int = 1, + heuristic_weight: float = 1.0, ) -> None: self.board = board + self.heuristic_weight = max(float(heuristic_weight), 1.0) self.trace_width = max(trace_width, 1) self.clearance = max(clearance, 0) self.half_width = self.trace_width // 2 @@ -471,7 +473,13 @@ class GridRouter: heapq.heappush(open_heap, (tentative + self._h(nxt, goal_cell), nxt)) def _h(self, node: Node, goal_cell: Cell) -> float: - return math.hypot(node[0] - goal_cell[0], node[1] - goal_cell[1]) + dx, dy = abs(node[0] - goal_cell[0]), abs(node[1] - goal_cell[1]) + # the tightest estimate the allowed moves admit (Manhattan when only + # orthogonal steps exist), scaled by heuristic_weight: above 1 the + # search heads for the goal instead of flooding open board, for a + # route at most that factor longer than the shortest + base = dx + dy if self._orthogonal else math.hypot(dx, dy) + return self.heuristic_weight * base # --- commit / rip ------------------------------------------------------- @@ -687,6 +695,7 @@ def route_board( via_half: int | None = None, edge_clearance: int | None = None, resolution: int = 1, + heuristic_weight: float = 1.0, ) -> RouteResult: """Convenience wrapper: build a :class:`GridRouter` and route the board.""" router = GridRouter( @@ -701,5 +710,6 @@ def route_board( via_half=via_half, edge_clearance=edge_clearance, resolution=resolution, + heuristic_weight=heuristic_weight, ) return router.route() diff --git a/src/freeroute/route/pipeline.py b/src/freeroute/route/pipeline.py index 94725af..f5cf203 100644 --- a/src/freeroute/route/pipeline.py +++ b/src/freeroute/route/pipeline.py @@ -150,6 +150,7 @@ def route_dsn_board_exact( dsn: DsnBoard, *, resolution: int = 1, + heuristic_weight: float = 1.0, edge_clearance_mm: float | None = None, layers: list[int] | None = None, max_passes: int = 10, @@ -170,6 +171,7 @@ def route_dsn_board_exact( exact = route_board_exact( board, resolution=resolution, + heuristic_weight=heuristic_weight, via_half=_via_half_board(dsn, scale), edge_clearance=edge_clearance_board, trace_width=width_board, @@ -187,6 +189,7 @@ def build_exact_routing_result( dsn: DsnBoard, *, resolution: int = 1, + heuristic_weight: float = 1.0, layers: list[int] | None = None, shove: bool = False, diagonal: bool = False, @@ -204,6 +207,7 @@ def build_exact_routing_result( max_passes=max_passes, edge_clearance_mm=edge_clearance_mm, resolution=resolution, + heuristic_weight=heuristic_weight, ) return _to_routing_result(exact.result, dsn, scale, layer_names) diff --git a/tests/route/test_real_board_geometry.py b/tests/route/test_real_board_geometry.py index 1a9eb86..f4ed1cc 100644 --- a/tests/route/test_real_board_geometry.py +++ b/tests/route/test_real_board_geometry.py @@ -73,3 +73,24 @@ def test_resolution_one_has_no_halo_so_the_grid_is_unchanged(): assert GridRouter(board, trace_width=2500, clearance=1500)._halo_offsets == [] fine = GridRouter(board, trace_width=2500, clearance=1500, resolution=2) assert fine.step == 2000 and (1, 0) in fine._halo_offsets and (2, 0) not in fine._halo_offsets + + +def test_heuristic_weight_bounds_route_length(): + # weighted A* may take a longer route, but never more than W times the shortest + places = "(place A 3000 -3000 front 0) (place B 17000 -17000 front 0)" + + def length(weight): + result, _, _ = route_dsn_board_exact( + parse_dsn(_dsn(f"(component P {places})", "(net N (pins A-1 B-1))")), + heuristic_weight=weight, + ) + return sum( + math.hypot(q.x - p.x, q.y - p.y) + for segs in result.result.wires.values() + for _layer, pts in segs + for p, q in zip(pts, pts[1:], strict=False) + ) + + shortest = length(1.0) + assert shortest > 0 + assert length(2.0) <= 2.0 * shortest