Window re-route: time budget, window cap, smallest first, window boards
A window's board now holds every pin and piece of copper of the nets it reroutes and, of the rest, only items near the window, instead of the whole board rebuilt per window. The smallest windows go first, at most --window-cap per round (40), and no window starts once --window-budget of the routing's own time is spent (0.25). On th-eda's flight controller the stage had taken 12x the routing time for no gain.
This commit is contained in:
@@ -214,6 +214,21 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
metavar="K",
|
||||
help="the window re-route's grid resolution (default 4)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--window-budget",
|
||||
type=float,
|
||||
default=0.25,
|
||||
metavar="F",
|
||||
help="the window re-route stops starting windows once it has spent this share "
|
||||
"of the routing's own time (default 0.25)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--window-cap",
|
||||
type=int,
|
||||
default=40,
|
||||
metavar="W",
|
||||
help="at most this many windows per round, smallest first (default 40)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--window-margin",
|
||||
type=float,
|
||||
@@ -303,6 +318,8 @@ def main(argv: list[str] | None = None) -> int:
|
||||
"window_reroute": args.window_reroute or 0,
|
||||
"window_resolution": args.window_resolution,
|
||||
"window_margin_mm": args.window_margin,
|
||||
"window_budget": args.window_budget,
|
||||
"window_cap": args.window_cap,
|
||||
}
|
||||
elif args.engine == "grid":
|
||||
options = {
|
||||
|
||||
@@ -13,6 +13,7 @@ to DSN units (dividing by the resolution) for the SES ``(wire (path ...))`` and
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import time
|
||||
|
||||
from freeroute.board import build_board
|
||||
from freeroute.dsn import DsnBoard, parse_dsn
|
||||
@@ -241,6 +242,8 @@ def route_dsn_board_exact(
|
||||
window_reroute: int = 0,
|
||||
window_resolution: int = 4,
|
||||
window_margin_mm: float = 1.5,
|
||||
window_budget: float = 0.25,
|
||||
window_cap: int = 40,
|
||||
) -> 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.
|
||||
@@ -270,6 +273,7 @@ def route_dsn_board_exact(
|
||||
"via_drill": _via_drill_board(dsn, scale),
|
||||
"hole_to_hole": _mm_to_board(dsn, scale, hole_to_hole_mm) or 0,
|
||||
}
|
||||
started = time.monotonic()
|
||||
exact = route_board_exact(
|
||||
board,
|
||||
resolution=resolution,
|
||||
@@ -289,6 +293,9 @@ def route_dsn_board_exact(
|
||||
via_half=via_half,
|
||||
via_drill=common["via_drill"] or 2 * via_half,
|
||||
via_name=_via_padstack(dsn),
|
||||
# a share of what the routing itself took
|
||||
budget_s=max(float(window_budget), 0.0) * (time.monotonic() - started),
|
||||
max_windows=window_cap,
|
||||
)
|
||||
return exact, scale, [layer.name for layer in dsn.layers]
|
||||
|
||||
@@ -314,6 +321,8 @@ def build_exact_routing_result(
|
||||
window_reroute: int = 0,
|
||||
window_resolution: int = 4,
|
||||
window_margin_mm: float = 1.5,
|
||||
window_budget: float = 0.25,
|
||||
window_cap: int = 40,
|
||||
) -> RoutingResult:
|
||||
"""Route ``dsn`` (exact track) and convert to a DSN-unit RoutingResult."""
|
||||
exact, scale, layer_names = route_dsn_board_exact(
|
||||
@@ -336,6 +345,8 @@ def build_exact_routing_result(
|
||||
window_reroute=window_reroute,
|
||||
window_resolution=window_resolution,
|
||||
window_margin_mm=window_margin_mm,
|
||||
window_budget=window_budget,
|
||||
window_cap=window_cap,
|
||||
)
|
||||
return _to_routing_result(exact.result, dsn, scale, layer_names)
|
||||
|
||||
|
||||
@@ -8,6 +8,12 @@ stuck connection's net and the ripped nets only. The result is kept only
|
||||
if fewer connections are left unrouted than before; otherwise the board is
|
||||
as it was. Each round doubles the margin for the connections still stuck.
|
||||
|
||||
It is a last stage, so it is kept cheap: a window's board holds only what
|
||||
the window needs (every pin and piece of copper of the nets it reroutes,
|
||||
so their connectivity is unchanged, and of the other items only those near
|
||||
the window), the smallest windows go first, at most ``max_windows`` run per
|
||||
round, and no new window starts once ``budget_s`` seconds are spent.
|
||||
|
||||
What makes it differ from re-running the same search near the failure: the
|
||||
finer grid finds gaps the board's grid steps over, and the ripped nets go in
|
||||
again in a new order around the stuck one. A finer grid on the whole board
|
||||
@@ -17,10 +23,11 @@ a small window it only ever replaces what it can improve.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from freeroute.board import build_board
|
||||
from freeroute.board.items import Trace, Via
|
||||
from freeroute.board import BasicBoard, build_board
|
||||
from freeroute.board.items import ObstacleArea, Pin, Trace, Via
|
||||
from freeroute.dsn import DsnBoard
|
||||
from freeroute.geometry import IntBox, IntPoint
|
||||
|
||||
@@ -52,25 +59,58 @@ def _inside(copper: Copper, window: IntBox) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def _board_with(
|
||||
dsn: DsnBoard, copper: dict[int, Copper], keep: set[int], half: dict[int, int],
|
||||
default_half: int, via_half: int, via_drill: int, layers: list[int], via_name: str,
|
||||
):
|
||||
"""The board with the routed copper of ``keep`` laid down as fixed copper."""
|
||||
board = build_board(dsn)
|
||||
def _item_box(item) -> IntBox | None:
|
||||
if isinstance(item, (Pin, Via)):
|
||||
return item.shape.bounding_box() if item.shape is not None else None
|
||||
if isinstance(item, ObstacleArea):
|
||||
return item.bounding_box()
|
||||
if isinstance(item, Trace) and item.corners:
|
||||
h = item.half_width
|
||||
xs, ys = [p.x for p in item.corners], [p.y for p in item.corners]
|
||||
return IntBox(min(xs) - h, min(ys) - h, max(xs) + h, max(ys) + h)
|
||||
return None
|
||||
|
||||
|
||||
def _meets(box: IntBox | None, view: IntBox) -> bool:
|
||||
return box is not None and box.ll.x <= view.ur.x and view.ll.x <= box.ur.x and (
|
||||
box.ll.y <= view.ur.y and view.ll.y <= box.ur.y
|
||||
)
|
||||
|
||||
|
||||
def _path_box(path: list[IntPoint], grow: int) -> IntBox:
|
||||
xs, ys = [p.x for p in path], [p.y for p in path]
|
||||
return IntBox(min(xs) - grow, min(ys) - grow, max(xs) + grow, max(ys) + grow)
|
||||
|
||||
|
||||
def _window_board(
|
||||
base: BasicBoard, base_items: list, view: IntBox, affected: set[int],
|
||||
copper: dict[int, Copper], keep: set[int], half: dict[int, int], default_half: int,
|
||||
via_half: int, via_drill: int, layers: list[int], via_name: str,
|
||||
) -> BasicBoard:
|
||||
"""The board a window is routed on: the base board's items of the nets
|
||||
being rerouted and those near ``view``, plus the routed copper being kept
|
||||
(all of an affected net's, so its connectivity is the whole board's, and
|
||||
of other nets only what is near ``view``), as fixed copper."""
|
||||
board = BasicBoard(base.layer_structure, base.nets, base.clearance_matrix, base.bounding_box)
|
||||
board.outline = base.outline
|
||||
for item, box in base_items:
|
||||
if (item.net_nos and item.net_nos[0] in affected) or _meets(box, view):
|
||||
board.insert(item)
|
||||
for net in keep:
|
||||
wires, vias = copper.get(net, ([], []))
|
||||
width = half.get(net, default_half)
|
||||
mine = net in affected
|
||||
for layer, path in wires:
|
||||
if len(path) >= 2:
|
||||
if len(path) >= 2 and (mine or _meets(_path_box(path, width), view)):
|
||||
board.insert(Trace(net_nos=[net], fixed=True, layer=layer, half_width=width, corners=list(path)))
|
||||
for location in dict.fromkeys(vias):
|
||||
board.insert(Via(
|
||||
net_nos=[net], fixed=True, padstack_name=via_name, drill=via_drill,
|
||||
location=location, layers=list(layers),
|
||||
shape=IntBox(location.x - via_half, location.y - via_half,
|
||||
location.x + via_half, location.y + via_half),
|
||||
))
|
||||
shape = IntBox(location.x - via_half, location.y - via_half,
|
||||
location.x + via_half, location.y + via_half)
|
||||
if mine or _meets(shape, view):
|
||||
board.insert(Via(
|
||||
net_nos=[net], fixed=True, padstack_name=via_name, drill=via_drill,
|
||||
location=location, layers=list(layers), shape=shape,
|
||||
))
|
||||
return board
|
||||
|
||||
|
||||
@@ -90,12 +130,17 @@ def window_reroute(
|
||||
via_half: int,
|
||||
via_drill: int,
|
||||
via_name: str,
|
||||
budget_s: float | None = None,
|
||||
max_windows: int = 40,
|
||||
) -> RouteResult:
|
||||
"""Re-route a window around each still-failing connection; keep only gains.
|
||||
|
||||
``route_args`` are the exact engine's settings for the whole board (width,
|
||||
clearance, layers, shove, ...); the window runs them at ``resolution``.
|
||||
``margin`` is the first round's window margin in board units."""
|
||||
``margin`` is the first round's window margin in board units. No new
|
||||
window starts once ``budget_s`` seconds have gone by (None: no limit),
|
||||
and at most ``max_windows`` run per round."""
|
||||
started = time.monotonic()
|
||||
if rounds < 1 or not _open(result.failed):
|
||||
return result
|
||||
default_half = result.half_width
|
||||
@@ -105,32 +150,54 @@ def window_reroute(
|
||||
}
|
||||
half = dict(result.net_half)
|
||||
failed = list(result.failed)
|
||||
base = build_board(dsn)
|
||||
base_items = [(item, _item_box(item)) for item in base.get_items()]
|
||||
layers = route_args.get("layers") or [
|
||||
i for i, layer in enumerate(build_board(dsn).layer_structure.arr) if layer.is_signal
|
||||
i for i, layer in enumerate(base.layer_structure.arr) if layer.is_signal
|
||||
]
|
||||
labels = _pin_labels(build_board(dsn))
|
||||
labels = _pin_labels(base)
|
||||
# the copper a window's routing can feel from outside it
|
||||
context = 2 * (route_args["trace_width"] + route_args["clearance"]) + 2 * via_half + (
|
||||
route_args.get("edge_clearance") or 0
|
||||
)
|
||||
|
||||
changed = False
|
||||
def window_of(entry, reach: int) -> IntBox | None:
|
||||
ends = [_end_point(labels, entry[0], label) for label in entry[1:3]]
|
||||
ends = [p for p in ends if p is not None]
|
||||
if not ends:
|
||||
return None
|
||||
return IntBox(
|
||||
min(p.x for p in ends) - reach, min(p.y for p in ends) - reach,
|
||||
max(p.x for p in ends) + reach, max(p.y for p in ends) + reach,
|
||||
)
|
||||
|
||||
def area(box: IntBox | None) -> float:
|
||||
return float("inf") if box is None else (box.ur.x - box.ll.x) * (box.ur.y - box.ll.y)
|
||||
|
||||
changed = out_of_time = False
|
||||
for rnd in range(rounds):
|
||||
reach = margin * (2 ** rnd)
|
||||
progress = False
|
||||
for entry in _open(list(failed)):
|
||||
|
||||
# the cheap wins first: the smallest windows
|
||||
todo = sorted(_open(list(failed)), key=lambda f: area(window_of(f, reach)))
|
||||
for entry in todo[:max(int(max_windows), 1)]:
|
||||
if budget_s is not None and time.monotonic() - started >= budget_s:
|
||||
out_of_time = True
|
||||
break
|
||||
if entry not in failed:
|
||||
continue # an earlier window routed it
|
||||
net = entry[0]
|
||||
ends = [_end_point(labels, net, label) for label in entry[1:3]]
|
||||
ends = [p for p in ends if p is not None]
|
||||
if not ends:
|
||||
window = window_of(entry, reach)
|
||||
if window is None:
|
||||
continue
|
||||
window = IntBox(
|
||||
min(p.x for p in ends) - reach, min(p.y for p in ends) - reach,
|
||||
max(p.x for p in ends) + reach, max(p.y for p in ends) + reach,
|
||||
)
|
||||
victims = {n for n, c in copper.items() if _inside(c, window)}
|
||||
affected = victims | {net}
|
||||
before = _open(f for f in failed if f[0] in affected)
|
||||
keep = set(copper) - victims
|
||||
board = _board_with(dsn, copper, keep, half, default_half, via_half, via_drill, layers, via_name)
|
||||
view = IntBox(window.ll.x - context, window.ll.y - context,
|
||||
window.ur.x + context, window.ur.y + context)
|
||||
board = _window_board(base, base_items, view, affected, copper, keep, half,
|
||||
default_half, via_half, via_drill, layers, via_name)
|
||||
sub = route_board_exact(
|
||||
board, **route_args, resolution=resolution,
|
||||
only_nets=frozenset(affected), bounds=window,
|
||||
@@ -157,8 +224,8 @@ def window_reroute(
|
||||
failed = [f for f in failed if f[0] not in affected] + [
|
||||
f for f in sub.failed if f[0] in affected
|
||||
]
|
||||
progress = changed = True
|
||||
if not progress and not _open(failed):
|
||||
changed = True
|
||||
if out_of_time or not _open(failed):
|
||||
break
|
||||
|
||||
if not changed:
|
||||
|
||||
@@ -50,9 +50,9 @@ def _slot(gap: int = 640, far: bool = False) -> str:
|
||||
"""
|
||||
|
||||
|
||||
def _route(text: str, rounds: int):
|
||||
def _route(text: str, rounds: int, **window):
|
||||
exact, _scale, _layers = route_dsn_board_exact(
|
||||
parse_dsn(text), resolution=1, window_reroute=rounds, window_resolution=4,
|
||||
parse_dsn(text), resolution=1, window_reroute=rounds, window_resolution=4, **window,
|
||||
)
|
||||
return exact.result
|
||||
|
||||
@@ -93,3 +93,20 @@ def test_a_router_limited_to_some_nets_and_a_box_builds_only_those():
|
||||
assert {c.net_no for c in whole._build_connections()} == {1, 2}
|
||||
assert {c.net_no for c in part._build_connections()} == {2}
|
||||
assert part.cols * part.rows < whole.cols * whole.rows
|
||||
|
||||
|
||||
def test_a_spent_budget_starts_no_window():
|
||||
assert len(_route(_slot(), 1, window_budget=0.0).failed) == 1
|
||||
assert _route(_slot(), 1, window_budget=1000.0).failed == []
|
||||
|
||||
|
||||
def test_window_board_keeps_far_items_of_the_rerouted_nets_only():
|
||||
from freeroute.route.window import _item_box, _window_board
|
||||
|
||||
board = build_board(parse_dsn(_slot(far=True)))
|
||||
items = [(item, _item_box(item)) for item in board.get_items()]
|
||||
view = IntBox(80000, -60000, 120000, -40000) # around the slot, far from Q1/Q2
|
||||
sub = _window_board(board, items, view, {1}, {}, set(), {}, 100, 300, 300, [0], "via")
|
||||
refs = {p.component_ref for p in sub.get_pins()}
|
||||
assert {"P1", "P2"} <= refs # net N's pins, though outside the view
|
||||
assert not refs & {"Q1", "Q2"} # net M's, far away, left out
|
||||
|
||||
Reference in New Issue
Block a user