From 83ad0f8ef70934f8b0115af31fd7384e7ab6bae8 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Tue, 29 Sep 2026 03:10:52 -0600 Subject: [PATCH] Keep a route's own vias apart while it is searched Committed vias were checked for hole spacing, but a route's own vias are not committed until it is done, so a route hopping layers (more of them on 6 layers) could put two of its vias 0.41 mm apart, as th-eda's 6-layer flight controller showed. A via is now also checked against layer changes earlier on its own route. --- src/freeroute/route/grid_router.py | 24 ++++++++++++++++++++++++ tests/route/test_via_spacing.py | 10 ++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/freeroute/route/grid_router.py b/src/freeroute/route/grid_router.py index ef150b9..96facb7 100644 --- a/src/freeroute/route/grid_router.py +++ b/src/freeroute/route/grid_router.py @@ -538,6 +538,26 @@ class GridRouter: return False return self._holes_clear(cell, net_no) + def _near_path_via(self, node: Node, cell: Cell, came_from: dict) -> bool: + """A layer change earlier on the route that reaches ``node`` sits + within hole spacing of ``cell`` (and is not the same through via). + Committed vias are checked elsewhere; a route's own are not yet + committed while it is searched. Looks back far enough to cover the + hole spacing twice over, not along the whole route.""" + limit = 4 * (math.ceil(self._hole_need / self.step) + 1) + need = self._hole_need / self.step + steps = 0 + while node in came_from and steps < limit: + prev = came_from[node] + changes_layer = prev[:2] == node[:2] and prev[2] != node[2] + if changes_layer and node[:2] != cell and ( + math.hypot(node[0] - cell[0], node[1] - cell[1]) < need + ): + return True + node = prev + steps += 1 + return False + def _via_nets_at(self, cell: Cell) -> set[int]: nets = {net for net, n in self._via_nets.get(cell, {}).items() if n > 0} fixed = self._fixed_vias.get(cell) @@ -664,6 +684,8 @@ class GridRouter: pin = ends.get(node) if ok and isinstance(pin, Pin) and self._walled(node): ok = self._via_fits_at(pin.location, net_no) + if ok and self._near_path_via(source, cell, came_from): + ok = False # too close to a via earlier on this same route if ok: self._relax(source, target, g_via, goal_cell, g_score, came_from, open_heap) continue @@ -675,6 +697,8 @@ class GridRouter: ok = via_ok.get(cell) if ok is None: ok = via_ok[cell] = self._via_placeable(cell, net_no, conns) + if ok and self._near_path_via(current, cell, came_from): + ok = False else: ok = True if ok: diff --git a/tests/route/test_via_spacing.py b/tests/route/test_via_spacing.py index cd81987..7f7cfdf 100644 --- a/tests/route/test_via_spacing.py +++ b/tests/route/test_via_spacing.py @@ -124,3 +124,13 @@ def test_hole_spacing_from_a_fixed_via_is_measured_from_where_it_really_is(): assert not router._holes_clear(cell, 1), f"{d / s:.0f} um from the fixed via" elif 560 * s < d < 700 * s and cell not in router._tht_block: assert router._holes_clear(cell, 1) + + +def test_a_route_keeps_its_own_vias_apart(): + # a route that changed layer at (10, 10) may not change layer again one + # cell away; the same cell is the same through via, and far is fine + router, _s = _router(_THT) + came_from = {(10, 10, 1): (10, 10, 0), (11, 10, 1): (10, 10, 1), (12, 10, 1): (11, 10, 1)} + assert router._near_path_via((12, 10, 1), (11, 10), came_from) + assert not router._near_path_via((12, 10, 1), (10, 10), came_from) + assert not router._near_path_via((12, 10, 1), (40, 10), came_from)