--seed: shuffle the order connections are routed in
The router is deterministic, but its result depends heavily on which connection routes first. A seed shuffles that order reproducibly, so a change can be judged over several orders instead of the one it happened to get.
This commit is contained in:
+13
-2
@@ -29,8 +29,8 @@ from freeroute.ses import write_ses
|
||||
#: which optional flags each engine actually implements. Anything not listed for
|
||||
#: an engine is rejected by :func:`_validate` rather than quietly ignored.
|
||||
ENGINE_OPTIONS: dict[str, frozenset[str]] = {
|
||||
"grid": frozenset({"rip_up", "max_passes"}),
|
||||
"exact": frozenset({"shove", "diagonal", "rip_up", "max_passes"}),
|
||||
"grid": frozenset({"rip_up", "max_passes", "seed"}),
|
||||
"exact": frozenset({"shove", "diagonal", "rip_up", "max_passes", "seed"}),
|
||||
"room": frozenset({"shove", "pack"}),
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ _FLAG_NAMES = {
|
||||
"diagonal": "--diagonal",
|
||||
"rip_up": "--no-rip-up",
|
||||
"max_passes": "-mp/--max-passes",
|
||||
"seed": "--seed",
|
||||
}
|
||||
|
||||
|
||||
@@ -133,6 +134,13 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
help="copper to board-edge clearance in mm, e.g. KiCad's min_copper_edge_clearance "
|
||||
"(the DSN does not carry it); default 0: copper only has to stay inside the outline",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--seed",
|
||||
type=int,
|
||||
metavar="N",
|
||||
help="grid and exact engines: shuffle the order connections are routed in; "
|
||||
"the same seed gives the same result (default: fewer-pin nets first)",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
@@ -152,6 +160,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
("diagonal", args.diagonal),
|
||||
("rip_up", args.no_rip_up),
|
||||
("max_passes", args.max_passes is not None),
|
||||
("seed", args.seed is not None),
|
||||
)
|
||||
if asked
|
||||
]
|
||||
@@ -184,6 +193,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
edge_clearance_mm=args.edge_clearance,
|
||||
resolution=2 if args.grid_resolution is None else args.grid_resolution,
|
||||
heuristic_weight=args.heuristic_weight,
|
||||
seed=args.seed,
|
||||
)
|
||||
elif args.engine == "room":
|
||||
result = build_rooms_routing_result(
|
||||
@@ -195,6 +205,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
dsn, layers=layers, rip_up=rip_up, max_passes=max_passes,
|
||||
edge_clearance_mm=args.edge_clearance,
|
||||
resolution=1 if args.grid_resolution is None else args.grid_resolution,
|
||||
seed=args.seed,
|
||||
)
|
||||
|
||||
ses_text = write_ses(dsn, result)
|
||||
|
||||
@@ -94,6 +94,7 @@ def route_board_exact(
|
||||
edge_clearance: int | None = None,
|
||||
resolution: int = 1,
|
||||
heuristic_weight: float = 1.0,
|
||||
seed: int | None = None,
|
||||
) -> ExactRouteResult:
|
||||
"""Route ``board`` orthogonally and verify the result with exact geometry.
|
||||
|
||||
@@ -122,6 +123,7 @@ def route_board_exact(
|
||||
edge_clearance=edge_clearance,
|
||||
resolution=resolution,
|
||||
heuristic_weight=heuristic_weight,
|
||||
seed=seed,
|
||||
)
|
||||
edge = 0 if edge_clearance is None else edge_clearance
|
||||
outline = board.outline
|
||||
|
||||
@@ -28,6 +28,7 @@ from __future__ import annotations
|
||||
|
||||
import heapq
|
||||
import math
|
||||
import random
|
||||
|
||||
from freeroute.board.board import BasicBoard
|
||||
from freeroute.board.items import ConductionArea, ObstacleArea, Pin
|
||||
@@ -154,8 +155,12 @@ class GridRouter:
|
||||
edge_clearance: int | None = None,
|
||||
resolution: int = 1,
|
||||
heuristic_weight: float = 1.0,
|
||||
seed: int | None = None,
|
||||
) -> None:
|
||||
self.board = board
|
||||
# None keeps the deterministic order; a seed shuffles which connection
|
||||
# routes first, to measure how much a result owes to the order
|
||||
self.seed = seed
|
||||
# how far (in cells) an escape stub may run to leave a walled-in pin
|
||||
self._escape_cells = 12
|
||||
self._crowded_cache: dict = {}
|
||||
@@ -819,9 +824,13 @@ class GridRouter:
|
||||
best_count = -1
|
||||
best_snapshot: list[tuple[int, list, list]] | None = None
|
||||
best_unrouted = len(conns)
|
||||
# conns[i].index == i is what rip-up relies on, so shuffle a view
|
||||
order = list(conns)
|
||||
if self.seed is not None:
|
||||
random.Random(self.seed).shuffle(order)
|
||||
for _ in range(passes):
|
||||
changed = False
|
||||
for conn in conns: # deterministic: connection creation order
|
||||
for conn in order:
|
||||
if conn.routed:
|
||||
continue
|
||||
if self._try_route(conn, conns):
|
||||
@@ -1050,6 +1059,7 @@ def route_board(
|
||||
edge_clearance: int | None = None,
|
||||
resolution: int = 1,
|
||||
heuristic_weight: float = 1.0,
|
||||
seed: int | None = None,
|
||||
) -> RouteResult:
|
||||
"""Convenience wrapper: build a :class:`GridRouter` and route the board."""
|
||||
router = GridRouter(
|
||||
@@ -1065,5 +1075,6 @@ def route_board(
|
||||
edge_clearance=edge_clearance,
|
||||
resolution=resolution,
|
||||
heuristic_weight=heuristic_weight,
|
||||
seed=seed,
|
||||
)
|
||||
return router.route()
|
||||
|
||||
@@ -80,6 +80,7 @@ def route_dsn_board(
|
||||
layers: list[int] | None = None,
|
||||
rip_up: bool = True,
|
||||
max_passes: int = 10,
|
||||
seed: int | None = None,
|
||||
) -> tuple[RouteResult, int, list[str]]:
|
||||
"""Route a parsed :class:`DsnBoard`; return the board-unit result, the scale,
|
||||
and the layer names. ``layers`` restricts routing to those signal-layer
|
||||
@@ -100,6 +101,7 @@ def route_dsn_board(
|
||||
layers=layers,
|
||||
rip_up=rip_up,
|
||||
max_passes=max_passes,
|
||||
seed=seed,
|
||||
)
|
||||
return result, scale, [layer.name for layer in dsn.layers]
|
||||
|
||||
@@ -140,11 +142,12 @@ def build_routing_result(
|
||||
rip_up: bool = True,
|
||||
max_passes: int = 10,
|
||||
edge_clearance_mm: float | None = None,
|
||||
seed: int | None = None,
|
||||
) -> RoutingResult:
|
||||
"""Route ``dsn`` (grid track) and convert to a DSN-unit RoutingResult."""
|
||||
route, scale, layer_names = route_dsn_board(
|
||||
dsn, layers=layers, rip_up=rip_up, max_passes=max_passes,
|
||||
edge_clearance_mm=edge_clearance_mm, resolution=resolution,
|
||||
edge_clearance_mm=edge_clearance_mm, resolution=resolution, seed=seed,
|
||||
)
|
||||
return _to_routing_result(route, dsn, scale, layer_names)
|
||||
|
||||
@@ -160,6 +163,7 @@ def route_dsn_board_exact(
|
||||
rip_up: bool = True,
|
||||
shove: bool = False,
|
||||
diagonal: bool = False,
|
||||
seed: int | None = None,
|
||||
) -> tuple[ExactRouteResult, int, list[str]]:
|
||||
"""Route a parsed :class:`DsnBoard` with the exact-geometry (orthogonal,
|
||||
DRC-verified) track; return the exact result, the scale, and layer names.
|
||||
@@ -184,6 +188,7 @@ def route_dsn_board_exact(
|
||||
rip_up=rip_up,
|
||||
shove=shove,
|
||||
diagonal=diagonal,
|
||||
seed=seed,
|
||||
)
|
||||
return exact, scale, [layer.name for layer in dsn.layers]
|
||||
|
||||
@@ -199,6 +204,7 @@ def build_exact_routing_result(
|
||||
rip_up: bool = True,
|
||||
max_passes: int = 10,
|
||||
edge_clearance_mm: float | None = None,
|
||||
seed: int | None = None,
|
||||
) -> RoutingResult:
|
||||
"""Route ``dsn`` (exact track) and convert to a DSN-unit RoutingResult."""
|
||||
exact, scale, layer_names = route_dsn_board_exact(
|
||||
@@ -211,6 +217,7 @@ def build_exact_routing_result(
|
||||
edge_clearance_mm=edge_clearance_mm,
|
||||
resolution=resolution,
|
||||
heuristic_weight=heuristic_weight,
|
||||
seed=seed,
|
||||
)
|
||||
return _to_routing_result(exact.result, dsn, scale, layer_names)
|
||||
|
||||
|
||||
@@ -264,6 +264,19 @@ def test_larger_board_still_connects_its_multipin_nets():
|
||||
assert len(result.routed_net_numbers) == multi_pin == 4
|
||||
|
||||
|
||||
def test_a_seed_is_repeatable_and_does_change_the_order():
|
||||
# The benchmark samples seeds to measure how much a result owes to the
|
||||
# routing order. A seed that changed nothing would make every sample the
|
||||
# same run and report no spread at all.
|
||||
text = (FIXTURES / "kicad_routable.dsn").read_text()
|
||||
default = _wire_key(route_dsn_board(parse_dsn(text))[0])
|
||||
runs = {s: route_dsn_board(parse_dsn(text), seed=s)[0] for s in range(4)}
|
||||
again = route_dsn_board(parse_dsn(text), seed=1)[0]
|
||||
assert _wire_key(again) == _wire_key(runs[1])
|
||||
assert any(_wire_key(r) != default for r in runs.values())
|
||||
assert all(r.unrouted == 0 for r in runs.values())
|
||||
|
||||
|
||||
# --- oracle parity ----------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -154,6 +154,7 @@ def test_cli_supported_combinations_are_accepted(tmp_path):
|
||||
"diagonal": ["--diagonal"],
|
||||
"rip_up": ["--no-rip-up"],
|
||||
"max_passes": ["-mp", "4"],
|
||||
"seed": ["--seed", "3"],
|
||||
}
|
||||
for engine, options in ENGINE_OPTIONS.items():
|
||||
for option in options:
|
||||
|
||||
Reference in New Issue
Block a user