--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
|
#: which optional flags each engine actually implements. Anything not listed for
|
||||||
#: an engine is rejected by :func:`_validate` rather than quietly ignored.
|
#: an engine is rejected by :func:`_validate` rather than quietly ignored.
|
||||||
ENGINE_OPTIONS: dict[str, frozenset[str]] = {
|
ENGINE_OPTIONS: dict[str, frozenset[str]] = {
|
||||||
"grid": frozenset({"rip_up", "max_passes"}),
|
"grid": frozenset({"rip_up", "max_passes", "seed"}),
|
||||||
"exact": frozenset({"shove", "diagonal", "rip_up", "max_passes"}),
|
"exact": frozenset({"shove", "diagonal", "rip_up", "max_passes", "seed"}),
|
||||||
"room": frozenset({"shove", "pack"}),
|
"room": frozenset({"shove", "pack"}),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +41,7 @@ _FLAG_NAMES = {
|
|||||||
"diagonal": "--diagonal",
|
"diagonal": "--diagonal",
|
||||||
"rip_up": "--no-rip-up",
|
"rip_up": "--no-rip-up",
|
||||||
"max_passes": "-mp/--max-passes",
|
"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 "
|
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",
|
"(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
|
return parser
|
||||||
|
|
||||||
|
|
||||||
@@ -152,6 +160,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
("diagonal", args.diagonal),
|
("diagonal", args.diagonal),
|
||||||
("rip_up", args.no_rip_up),
|
("rip_up", args.no_rip_up),
|
||||||
("max_passes", args.max_passes is not None),
|
("max_passes", args.max_passes is not None),
|
||||||
|
("seed", args.seed is not None),
|
||||||
)
|
)
|
||||||
if asked
|
if asked
|
||||||
]
|
]
|
||||||
@@ -184,6 +193,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
edge_clearance_mm=args.edge_clearance,
|
edge_clearance_mm=args.edge_clearance,
|
||||||
resolution=2 if args.grid_resolution is None else args.grid_resolution,
|
resolution=2 if args.grid_resolution is None else args.grid_resolution,
|
||||||
heuristic_weight=args.heuristic_weight,
|
heuristic_weight=args.heuristic_weight,
|
||||||
|
seed=args.seed,
|
||||||
)
|
)
|
||||||
elif args.engine == "room":
|
elif args.engine == "room":
|
||||||
result = build_rooms_routing_result(
|
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,
|
dsn, layers=layers, rip_up=rip_up, max_passes=max_passes,
|
||||||
edge_clearance_mm=args.edge_clearance,
|
edge_clearance_mm=args.edge_clearance,
|
||||||
resolution=1 if args.grid_resolution is None else args.grid_resolution,
|
resolution=1 if args.grid_resolution is None else args.grid_resolution,
|
||||||
|
seed=args.seed,
|
||||||
)
|
)
|
||||||
|
|
||||||
ses_text = write_ses(dsn, result)
|
ses_text = write_ses(dsn, result)
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ def route_board_exact(
|
|||||||
edge_clearance: int | None = None,
|
edge_clearance: int | None = None,
|
||||||
resolution: int = 1,
|
resolution: int = 1,
|
||||||
heuristic_weight: float = 1.0,
|
heuristic_weight: float = 1.0,
|
||||||
|
seed: int | None = None,
|
||||||
) -> ExactRouteResult:
|
) -> ExactRouteResult:
|
||||||
"""Route ``board`` orthogonally and verify the result with exact geometry.
|
"""Route ``board`` orthogonally and verify the result with exact geometry.
|
||||||
|
|
||||||
@@ -122,6 +123,7 @@ def route_board_exact(
|
|||||||
edge_clearance=edge_clearance,
|
edge_clearance=edge_clearance,
|
||||||
resolution=resolution,
|
resolution=resolution,
|
||||||
heuristic_weight=heuristic_weight,
|
heuristic_weight=heuristic_weight,
|
||||||
|
seed=seed,
|
||||||
)
|
)
|
||||||
edge = 0 if edge_clearance is None else edge_clearance
|
edge = 0 if edge_clearance is None else edge_clearance
|
||||||
outline = board.outline
|
outline = board.outline
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import heapq
|
import heapq
|
||||||
import math
|
import math
|
||||||
|
import random
|
||||||
|
|
||||||
from freeroute.board.board import BasicBoard
|
from freeroute.board.board import BasicBoard
|
||||||
from freeroute.board.items import ConductionArea, ObstacleArea, Pin
|
from freeroute.board.items import ConductionArea, ObstacleArea, Pin
|
||||||
@@ -154,8 +155,12 @@ class GridRouter:
|
|||||||
edge_clearance: int | None = None,
|
edge_clearance: int | None = None,
|
||||||
resolution: int = 1,
|
resolution: int = 1,
|
||||||
heuristic_weight: float = 1.0,
|
heuristic_weight: float = 1.0,
|
||||||
|
seed: int | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.board = board
|
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
|
# how far (in cells) an escape stub may run to leave a walled-in pin
|
||||||
self._escape_cells = 12
|
self._escape_cells = 12
|
||||||
self._crowded_cache: dict = {}
|
self._crowded_cache: dict = {}
|
||||||
@@ -819,9 +824,13 @@ class GridRouter:
|
|||||||
best_count = -1
|
best_count = -1
|
||||||
best_snapshot: list[tuple[int, list, list]] | None = None
|
best_snapshot: list[tuple[int, list, list]] | None = None
|
||||||
best_unrouted = len(conns)
|
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):
|
for _ in range(passes):
|
||||||
changed = False
|
changed = False
|
||||||
for conn in conns: # deterministic: connection creation order
|
for conn in order:
|
||||||
if conn.routed:
|
if conn.routed:
|
||||||
continue
|
continue
|
||||||
if self._try_route(conn, conns):
|
if self._try_route(conn, conns):
|
||||||
@@ -1050,6 +1059,7 @@ def route_board(
|
|||||||
edge_clearance: int | None = None,
|
edge_clearance: int | None = None,
|
||||||
resolution: int = 1,
|
resolution: int = 1,
|
||||||
heuristic_weight: float = 1.0,
|
heuristic_weight: float = 1.0,
|
||||||
|
seed: int | None = None,
|
||||||
) -> RouteResult:
|
) -> RouteResult:
|
||||||
"""Convenience wrapper: build a :class:`GridRouter` and route the board."""
|
"""Convenience wrapper: build a :class:`GridRouter` and route the board."""
|
||||||
router = GridRouter(
|
router = GridRouter(
|
||||||
@@ -1065,5 +1075,6 @@ def route_board(
|
|||||||
edge_clearance=edge_clearance,
|
edge_clearance=edge_clearance,
|
||||||
resolution=resolution,
|
resolution=resolution,
|
||||||
heuristic_weight=heuristic_weight,
|
heuristic_weight=heuristic_weight,
|
||||||
|
seed=seed,
|
||||||
)
|
)
|
||||||
return router.route()
|
return router.route()
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ def route_dsn_board(
|
|||||||
layers: list[int] | None = None,
|
layers: list[int] | None = None,
|
||||||
rip_up: bool = True,
|
rip_up: bool = True,
|
||||||
max_passes: int = 10,
|
max_passes: int = 10,
|
||||||
|
seed: int | None = None,
|
||||||
) -> tuple[RouteResult, int, list[str]]:
|
) -> tuple[RouteResult, int, list[str]]:
|
||||||
"""Route a parsed :class:`DsnBoard`; return the board-unit result, the scale,
|
"""Route a parsed :class:`DsnBoard`; return the board-unit result, the scale,
|
||||||
and the layer names. ``layers`` restricts routing to those signal-layer
|
and the layer names. ``layers`` restricts routing to those signal-layer
|
||||||
@@ -100,6 +101,7 @@ def route_dsn_board(
|
|||||||
layers=layers,
|
layers=layers,
|
||||||
rip_up=rip_up,
|
rip_up=rip_up,
|
||||||
max_passes=max_passes,
|
max_passes=max_passes,
|
||||||
|
seed=seed,
|
||||||
)
|
)
|
||||||
return result, scale, [layer.name for layer in dsn.layers]
|
return result, scale, [layer.name for layer in dsn.layers]
|
||||||
|
|
||||||
@@ -140,11 +142,12 @@ def build_routing_result(
|
|||||||
rip_up: bool = True,
|
rip_up: bool = True,
|
||||||
max_passes: int = 10,
|
max_passes: int = 10,
|
||||||
edge_clearance_mm: float | None = None,
|
edge_clearance_mm: float | None = None,
|
||||||
|
seed: int | None = None,
|
||||||
) -> RoutingResult:
|
) -> RoutingResult:
|
||||||
"""Route ``dsn`` (grid track) and convert to a DSN-unit RoutingResult."""
|
"""Route ``dsn`` (grid track) and convert to a DSN-unit RoutingResult."""
|
||||||
route, scale, layer_names = route_dsn_board(
|
route, scale, layer_names = route_dsn_board(
|
||||||
dsn, layers=layers, rip_up=rip_up, max_passes=max_passes,
|
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)
|
return _to_routing_result(route, dsn, scale, layer_names)
|
||||||
|
|
||||||
@@ -160,6 +163,7 @@ def route_dsn_board_exact(
|
|||||||
rip_up: bool = True,
|
rip_up: bool = True,
|
||||||
shove: bool = False,
|
shove: bool = False,
|
||||||
diagonal: bool = False,
|
diagonal: bool = False,
|
||||||
|
seed: int | None = None,
|
||||||
) -> tuple[ExactRouteResult, int, list[str]]:
|
) -> tuple[ExactRouteResult, int, list[str]]:
|
||||||
"""Route a parsed :class:`DsnBoard` with the exact-geometry (orthogonal,
|
"""Route a parsed :class:`DsnBoard` with the exact-geometry (orthogonal,
|
||||||
DRC-verified) track; return the exact result, the scale, and layer names.
|
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,
|
rip_up=rip_up,
|
||||||
shove=shove,
|
shove=shove,
|
||||||
diagonal=diagonal,
|
diagonal=diagonal,
|
||||||
|
seed=seed,
|
||||||
)
|
)
|
||||||
return exact, scale, [layer.name for layer in dsn.layers]
|
return exact, scale, [layer.name for layer in dsn.layers]
|
||||||
|
|
||||||
@@ -199,6 +204,7 @@ def build_exact_routing_result(
|
|||||||
rip_up: bool = True,
|
rip_up: bool = True,
|
||||||
max_passes: int = 10,
|
max_passes: int = 10,
|
||||||
edge_clearance_mm: float | None = None,
|
edge_clearance_mm: float | None = None,
|
||||||
|
seed: int | None = None,
|
||||||
) -> RoutingResult:
|
) -> RoutingResult:
|
||||||
"""Route ``dsn`` (exact track) and convert to a DSN-unit RoutingResult."""
|
"""Route ``dsn`` (exact track) and convert to a DSN-unit RoutingResult."""
|
||||||
exact, scale, layer_names = route_dsn_board_exact(
|
exact, scale, layer_names = route_dsn_board_exact(
|
||||||
@@ -211,6 +217,7 @@ def build_exact_routing_result(
|
|||||||
edge_clearance_mm=edge_clearance_mm,
|
edge_clearance_mm=edge_clearance_mm,
|
||||||
resolution=resolution,
|
resolution=resolution,
|
||||||
heuristic_weight=heuristic_weight,
|
heuristic_weight=heuristic_weight,
|
||||||
|
seed=seed,
|
||||||
)
|
)
|
||||||
return _to_routing_result(exact.result, dsn, scale, layer_names)
|
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
|
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 ----------------------------------------------------------
|
# --- oracle parity ----------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ def test_cli_supported_combinations_are_accepted(tmp_path):
|
|||||||
"diagonal": ["--diagonal"],
|
"diagonal": ["--diagonal"],
|
||||||
"rip_up": ["--no-rip-up"],
|
"rip_up": ["--no-rip-up"],
|
||||||
"max_passes": ["-mp", "4"],
|
"max_passes": ["-mp", "4"],
|
||||||
|
"seed": ["--seed", "3"],
|
||||||
}
|
}
|
||||||
for engine, options in ENGINE_OPTIONS.items():
|
for engine, options in ENGINE_OPTIONS.items():
|
||||||
for option in options:
|
for option in options:
|
||||||
|
|||||||
Reference in New Issue
Block a user