Count copper already on the board as connections already made

Fixed tracks and vias only kept other nets away, so a pin fanned out to
its plane was routed to the plane again, beside its own fan-out. Pins,
zones and planes that fixed copper of their net touches now form one
connected group: a fanned-out supply pin needs nothing more, a fanned-out
signal routes from its via, and copper touching no pin or zone adds
nothing to route.
This commit is contained in:
2026-09-29 00:16:56 -06:00
parent 7d8e5d5c10
commit 45b3753cff
2 changed files with 139 additions and 5 deletions
+96 -5
View File
@@ -1253,7 +1253,9 @@ class GridRouter:
# deterministic: fewer-pin nets first, then by net number
for net_no in sorted(nets, key=lambda n: (len(pins_by_net.get(n, [])), n)):
zones = zones_by_net.get(net_no, []) + planes_by_net.get(net_no, [])
groups = _connected_groups(pins_by_net.get(net_no, []), zones, plane_ids)
groups = _connected_groups(
pins_by_net.get(net_no, []), zones, plane_ids, self._fixed_pieces(net_no)
)
hubs = [g for g in groups if any(id(z) in plane_ids for z in g[1])]
if hubs:
# a plane net: every other group goes straight to the plane
@@ -1267,6 +1269,28 @@ class GridRouter:
conns.append(conn)
return conns
def _fixed_pieces(self, net_no: int) -> list[_FixedPiece]:
"""The net's tracks and vias already on the board, as connectivity."""
cache = getattr(self, "_fixed_cache", None)
if cache is None:
from freeroute.board.items import Trace
from freeroute.geometry.polyline import Polyline, PolylineShape, corner_box
cache = self._fixed_cache = {}
for item in self.board.get_items():
if not item.fixed or not item.net_nos:
continue
if isinstance(item, Via) and item.shape is not None:
piece = _FixedPiece(item.layers, [item.shape.bounding_box()], via=item)
elif isinstance(item, Trace) and not item.is_empty():
boxes = list(PolylineShape(Polyline(item.corners), item.half_width).tiles())
boxes += [corner_box(e, item.half_width) for e in (item.corners[0], item.corners[-1])]
piece = _FixedPiece([item.layer], boxes)
else:
continue
cache.setdefault(item.net_nos[0], []).append(piece)
return cache.get(net_no, [])
def _pins_by_net(self) -> dict[int, list[Pin]]:
by_net: dict[int, list[Pin]] = {}
for pin in self.board.get_pins():
@@ -1361,13 +1385,49 @@ def _pin_touches_zone(pin: Pin, zone, plane: bool = False) -> bool:
return any(zone.polygon.contains(c) for c in corners)
def _touches(a: IntBox, b: IntBox) -> bool:
return not (a.ur.x < b.ll.x or b.ur.x < a.ll.x or a.ur.y < b.ll.y or b.ur.y < a.ll.y)
def _fixed_joins_zone(piece, zone, plane: bool) -> bool:
"""A through via inside a plane joins it; copper inside a zone on its
own layer joins that zone."""
if zone.polygon is None:
return False
if piece.via is not None:
if plane or zone.layer in piece.layers:
return zone.polygon.contains(piece.via.location)
return False
return zone.layer in piece.layers and any(
zone.polygon.contains(IntPoint((b.ll.x + b.ur.x) // 2, (b.ll.y + b.ur.y) // 2))
for b in piece.boxes
)
class _FixedPiece:
"""One track or via already on the board, as connectivity."""
__slots__ = ("layers", "boxes", "via")
def __init__(self, layers, boxes, via=None) -> None:
self.layers = frozenset(layers)
self.boxes = boxes
self.via = via # the Via item, for a via
def _connected_groups(
pins: list[Pin], zones: list, planes: frozenset = frozenset()
pins: list[Pin], zones: list, planes: frozenset = frozenset(), fixed: list | None = None
) -> list[tuple[list[Pin], list]]:
"""Pins and zones of one net that copper already joins: a pad in a zone
is connected to it, and two zones sharing a pad are one. Only the groups
need routing between them. Pin groups come first, in pin order."""
parent = list(range(len(pins) + len(zones)))
is connected to it, two zones sharing a pad are one, and copper already
on the board (tracks and vias, ``fixed``) joins whatever it touches. Only
the groups need routing between them. Pin groups come first, in pin
order. A group's fixed vias join its pins as extra route ends, so a
fanned-out pin is routed from its via; copper that touches no pin and no
zone adds nothing to route."""
fixed = fixed or []
n_pins, n_zones = len(pins), len(zones)
parent = list(range(n_pins + n_zones + len(fixed)))
def find(i: int) -> int:
while parent[i] != i:
@@ -1379,6 +1439,24 @@ def _connected_groups(
for i, pin in enumerate(pins):
if _pin_touches_zone(pin, zone, id(zone) in planes):
parent[find(i)] = find(len(pins) + j)
base = n_pins + n_zones
for k, piece in enumerate(fixed):
me = base + k
for i, pin in enumerate(pins):
if pin.shape is None or not piece.layers & set(pin.layers):
continue
pad = pin.shape.bounding_box()
if any(_touches(pad, b) for b in piece.boxes):
parent[find(i)] = find(me)
for j, zone in enumerate(zones):
if _fixed_joins_zone(piece, zone, id(zone) in planes):
parent[find(n_pins + j)] = find(me)
for k2 in range(k):
other = fixed[k2]
if piece.layers & other.layers and any(
_touches(a, b) for a in piece.boxes for b in other.boxes
):
parent[find(base + k2)] = find(me)
groups: dict[int, tuple[list[Pin], list]] = {}
order: list[int] = []
for i, pin in enumerate(pins):
@@ -1393,9 +1471,22 @@ def _connected_groups(
groups[root] = ([], [])
order.append(root)
groups[root][1].append(zone)
for k, piece in enumerate(fixed):
root = find(base + k)
if piece.via is not None and root in groups:
groups[root][0].append(_via_end(piece.via))
return [groups[r] for r in order]
def _via_end(via) -> Pin:
"""A fixed via as a place a route may start or end (not a pad to route)."""
return Pin(
net_nos=list(via.net_nos), fixed=True, name="via", component_ref="",
padstack_name=via.padstack_name, location=via.location,
layers=list(via.layers), shape=via.shape,
)
def _split_path(path: list[Node]) -> tuple[list[tuple[int, list[Cell]]], list[Cell]]:
"""Split a node path into per-layer wire segments and via cells."""
wires: list[tuple[int, list[Cell]]] = []
+43
View File
@@ -48,3 +48,46 @@ def test_other_nets_route_around_a_fixed_track():
continue
for p, q in zip(pts, pts[1:], strict=False):
assert not (min(p.x, q.x) < wall < max(p.x, q.x)), "crossed the GND track on F.Cu"
# --- fixed wiring as connectivity ------------------------------------------
_FANOUT = """(pcb fanout (parser) (resolution um 10) (unit um)
(structure
(layer F.Cu (type signal)) (layer In1.Cu (type power)) (layer In2.Cu (type power))
(layer B.Cu (type signal))
(boundary (path pcb 0 0 0 30000 0 30000 -20000 0 -20000 0 0))
(plane GND (polygon In1.Cu 0 500 -500 29500 -500 29500 -19500 500 -19500 500 -500))
(via "Via[0-3]_600:300_um") (rule (width 150) (clearance 150)))
(placement (component S (place G1 5000 -10000 front 0) (place A 10000 -10000 front 0)
(place B 25000 -11500 back 0)))
(library
(image S (pin smd 1 0 0))
(padstack smd (shape (rect F.Cu -300 -300 300 300)) (attach off))
(padstack "Via[0-3]_600:300_um" (shape (circle F.Cu 600)) (shape (circle B.Cu 600)) (attach off)))
(network (net GND (pins G1-1)) (net SIG (pins A-1 B-1)))
(wiring
(wire (path F.Cu 150 5000 -10000 5000 -11500) (net GND) (type route))
(via "Via[0-3]_600:300_um" 5000 -11500 (net GND) (type route))
(wire (path F.Cu 150 10000 -10000 10000 -11500) (net SIG) (type route))
(via "Via[0-3]_600:300_um" 10000 -11500 (net SIG) (type route))
(wire (path F.Cu 300 2000 -2000 8000 -2000) (net GND) (type route))))"""
def test_a_pad_fanned_out_to_its_plane_needs_no_route():
dsn = parse_dsn(_FANOUT)
exact, _scale, _names = route_dsn_board_exact(dsn, resolution=2)
gnd = next(i + 1 for i, n in enumerate(dsn.nets) if n.name == "GND")
assert exact.result.failed == []
assert not exact.result.vias.get(gnd) and not exact.result.wires.get(gnd)
def test_a_fanned_out_signal_is_routed_from_its_via():
# B is on the bottom face, level with A's fan-out via: starting from that
# via on B.Cu is the cheapest way, and needs no via of its own
dsn = parse_dsn(_FANOUT)
exact, scale, names = route_dsn_board_exact(dsn, resolution=2)
sig = next(i + 1 for i, n in enumerate(dsn.nets) if n.name == "SIG")
assert sig in exact.result.routed_net_numbers
assert not exact.result.vias.get(sig), "the fan-out via is already there"
assert {names[layer] for layer, _ in exact.result.wires[sig]} == {"B.Cu"}