The router began as a port of FreeRouting and has grown its own engine (negotiated congestion, tree nets, exact checks, axis escapes), and 'freeroute' was one letter from the project it no longer follows. The package, module and command are now rpmroute; the freeroute command stays as an alias, and the RPMROUTE / RPMROUTE_ORACLE_JAVA variables fall back to the old FREEROUTE names. Version 2026.10.07. Routing is unchanged: a PCBench board routes to a byte-identical session either way.
129 lines
5.0 KiB
Python
129 lines
5.0 KiB
Python
"""Window re-route: a finer grid around a stuck connection, kept only when
|
|
it leaves fewer connections unrouted."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from rpmroute.board import build_board
|
|
from rpmroute.dsn import parse_dsn
|
|
from rpmroute.geometry import IntBox
|
|
from rpmroute.route.grid_router import GridRouter
|
|
from rpmroute.route.pipeline import _rule_clearance_dsn, _rule_width_dsn, route_dsn_board_exact
|
|
|
|
|
|
def _slot(gap: int = 640, far: bool = False) -> str:
|
|
"""A wall across the board with a slot in it. A trace fits the slot
|
|
only along a narrow band of heights that the board's 1x grid has no row
|
|
in and a 4x grid does. ``far`` adds a second net nowhere near the wall."""
|
|
yc = -5000
|
|
lo, hi = yc - gap // 2, yc + gap // 2
|
|
extra_place = "(place Q1 2000 -1000 front 0) (place Q2 6000 -1000 front 0)" if far else ""
|
|
extra_net = "(net M (pins Q1-1 Q2-1))" if far else ""
|
|
return f"""(pcb "slot.dsn"
|
|
(parser (string_quote ") (space_in_quoted_tokens on) (host_cad "t") (host_version "1"))
|
|
(resolution um 10)
|
|
(unit um)
|
|
(structure
|
|
(layer F.Cu (type signal) (property (index 0)))
|
|
(boundary (path pcb 0 0 0 20000 0 20000 -10000 0 -10000 0 0))
|
|
(keepout "" (rect F.Cu 9000 -10000 11000 {lo}))
|
|
(keepout "" (rect F.Cu 9000 {hi} 11000 0))
|
|
(rule (width 200) (clearance 200))
|
|
)
|
|
(library
|
|
(padstack Rect_Pad (shape (rect F.Cu -100 -100 100 100)) (attach off))
|
|
(image PAD (pin Rect_Pad 1 0 0))
|
|
)
|
|
(placement
|
|
(component PAD
|
|
(place P1 3000 {yc} front 0)
|
|
(place P2 17000 {yc} front 0)
|
|
{extra_place}
|
|
)
|
|
)
|
|
(network
|
|
(net N (pins P1-1 P2-1))
|
|
{extra_net}
|
|
(class default (rule (width 200) (clearance 200)))
|
|
)
|
|
(wiring)
|
|
)
|
|
"""
|
|
|
|
|
|
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, **window,
|
|
)
|
|
return exact.result
|
|
|
|
|
|
def test_the_board_grid_misses_the_slot_and_a_finer_window_finds_it():
|
|
plain = _route(_slot(), 0)
|
|
assert len(plain.failed) == 1
|
|
windowed = _route(_slot(), 1)
|
|
assert windowed.failed == []
|
|
assert windowed.wires # and the copper it found is in the result
|
|
|
|
|
|
def test_a_window_that_cannot_help_leaves_the_board_as_it_was():
|
|
plain = _route(_slot(gap=500, far=True), 0) # too narrow for any grid
|
|
windowed = _route(_slot(gap=500, far=True), 2)
|
|
assert len(windowed.failed) == len(plain.failed) == 1
|
|
assert windowed.wires == plain.wires
|
|
assert windowed.vias == plain.vias
|
|
|
|
|
|
def test_copper_outside_the_window_is_kept_as_it_was():
|
|
plain = _route(_slot(far=True), 0)
|
|
windowed = _route(_slot(far=True), 1)
|
|
assert windowed.failed == []
|
|
far_net = 2 # M, the second net in the DSN
|
|
assert windowed.wires[far_net] == plain.wires[far_net]
|
|
|
|
|
|
def test_a_router_limited_to_some_nets_and_a_box_builds_only_those():
|
|
dsn = parse_dsn(_slot(far=True))
|
|
s = dsn.resolution.value
|
|
board = build_board(dsn)
|
|
whole = GridRouter(board, trace_width=round(_rule_width_dsn(dsn) * s),
|
|
clearance=round(_rule_clearance_dsn(dsn) * s), orthogonal=True)
|
|
part = GridRouter(board, trace_width=round(_rule_width_dsn(dsn) * s),
|
|
clearance=round(_rule_clearance_dsn(dsn) * s), orthogonal=True,
|
|
only_nets=frozenset({2}), bounds=IntBox(0, -20000, 80000, 0))
|
|
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 rpmroute.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
|
|
|
|
|
|
def test_a_connection_ending_outside_the_window_gets_no_copper():
|
|
"""Its far end has no grid cells; a stub out to it would cross items the
|
|
window board never saw."""
|
|
from rpmroute.route.exact_router import route_board_exact
|
|
|
|
dsn = parse_dsn(_slot(far=True))
|
|
s = dsn.resolution.value
|
|
window = IntBox(70000, -70000, 130000, -30000) # the slot only; net M's pins are far off
|
|
result = route_board_exact(
|
|
build_board(dsn), trace_width=round(_rule_width_dsn(dsn) * s),
|
|
clearance=round(_rule_clearance_dsn(dsn) * s), only_nets=frozenset({2}), bounds=window,
|
|
).result
|
|
assert [f[0] for f in result.failed] == [2]
|
|
assert not result.wires.get(2) and not result.vias.get(2)
|