Make the grid and the exact check agree on rotated pads, shared vias and wide nets

Rotated oval pads were built from padded corners that are not in outline
order, so they fell back to their bounding box; neighbouring boxes of a
rotated socket overlap, and the grid's own-pad exemption opened the
overlap to the pin's net, which the exact check then refused. Pads are now
the convex hull of their corners, the exact tree checks a rotated pad
against that outline, and the grid blocks cells against the same outline.

A via reused by a second connection of its net left that connection with
no claim on the via's footprint, so ripping the first owner freed cells
around a via still on the board. Footprint cells now keep every claimant.

A net class wider than the default was searched as a default trace: its
spread was empty below half a cell, halos were circular while the exact
check compares boxes, and a rip-up route only counted what its centre
crossed. The spread now covers the extra width in whole cells, halos are
square, and rip-up victims include what the spread crosses.

bench/diagnose.py now routes with net-class widths and hole spacing, as
the CLI does.
This commit is contained in:
2026-09-30 12:08:25 -06:00
parent ef8398c9b2
commit f79134fee6
6 changed files with 331 additions and 21 deletions
+9 -2
View File
@@ -32,8 +32,10 @@ from freeroute.route.exact_router import route_board_exact
from freeroute.route.grid_router import GridRouter, _ends_of from freeroute.route.grid_router import GridRouter, _ends_of
from freeroute.route.pipeline import ( from freeroute.route.pipeline import (
_mm_to_board, _mm_to_board,
_net_widths_board,
_rule_clearance_dsn, _rule_clearance_dsn,
_rule_width_dsn, _rule_width_dsn,
_via_drill_board,
_via_half_board, _via_half_board,
) )
@@ -183,7 +185,7 @@ def _flood(router: GridRouter, pin, net_no: int, conns) -> tuple[int, Counter]:
def diagnose( def diagnose(
dsn_text: str, edge_clearance_mm: float | None, resolution: int = 2, shove: bool = False, dsn_text: str, edge_clearance_mm: float | None, resolution: int = 2, shove: bool = False,
access_cost: float = 0.0, access_cost: float = 0.0, hole_to_hole_mm: float = 0.25,
) -> dict: ) -> dict:
dsn = parse_dsn(dsn_text) dsn = parse_dsn(dsn_text)
names = {i + 1: n.name for i, n in enumerate(dsn.nets)} names = {i + 1: n.name for i, n in enumerate(dsn.nets)}
@@ -199,6 +201,10 @@ def diagnose(
resolution=resolution, resolution=resolution,
shove=shove, shove=shove,
access_cost=access_cost, access_cost=access_cost,
# the product's configuration: net-class widths and hole spacing
net_widths=_net_widths_board(dsn, scale),
via_drill=_via_drill_board(dsn, scale),
hole_to_hole=_mm_to_board(dsn, scale, hole_to_hole_mm) or 0,
) )
routed = seen["router"] routed = seen["router"]
best = routed._diag_best best = routed._diag_best
@@ -270,11 +276,12 @@ def main(argv: list[str] | None = None) -> int:
ap.add_argument("--grid-resolution", type=int, default=2, metavar="K") ap.add_argument("--grid-resolution", type=int, default=2, metavar="K")
ap.add_argument("--shove", action="store_true", help="as mckicad routes") ap.add_argument("--shove", action="store_true", help="as mckicad routes")
ap.add_argument("--access-cost", type=float, default=0.0) ap.add_argument("--access-cost", type=float, default=0.0)
ap.add_argument("--hole-to-hole", type=float, default=0.25, metavar="MM")
ap.add_argument("--json", type=Path, help="also write the full table here") ap.add_argument("--json", type=Path, help="also write the full table here")
args = ap.parse_args(argv) args = ap.parse_args(argv)
report = diagnose(args.dsn.read_text(), args.edge_clearance, args.grid_resolution, args.shove, report = diagnose(args.dsn.read_text(), args.edge_clearance, args.grid_resolution, args.shove,
args.access_cost) args.access_cost, args.hole_to_hole)
for row in report["failures"]: for row in report["failures"]:
extra = "" extra = ""
for w in row.get("walled", []): for w in row.get("walled", []):
+28 -4
View File
@@ -268,15 +268,39 @@ def _placed_pad_tile(shape: Shape, place, dsn_pin, transform: CoordinateTransfor
for x, y in corners: for x, y in corners:
p = _to_board_offset(place, x, y, dsn_pin.rotation) p = _to_board_offset(place, x, y, dsn_pin.rotation)
pts.append(IntPoint(round(transform.dsn_to_board(p.x)), round(transform.dsn_to_board(p.y)))) pts.append(IntPoint(round(transform.dsn_to_board(p.x)), round(transform.dsn_to_board(p.y))))
if len(pts) >= 3: # An oval's padded corners are not in outline order, and a mirrored
simplex = Simplex.from_corners(pts) # part reverses the winding: take the convex hull, then either winding.
if not simplex.is_empty(): hull = _convex_hull(pts)
return simplex if len(hull) >= 3:
for corners in (hull, hull[::-1]):
simplex = Simplex.from_corners(corners)
if not simplex.is_empty():
return simplex
xs = [p.x for p in pts] or [0] xs = [p.x for p in pts] or [0]
ys = [p.y for p in pts] or [0] ys = [p.y for p in pts] or [0]
return IntBox(min(xs), min(ys), max(xs), max(ys)) return IntBox(min(xs), min(ys), max(xs), max(ys))
def _convex_hull(pts: list[IntPoint]) -> list[IntPoint]:
"""The convex hull of ``pts`` counter-clockwise, collinear points dropped."""
ps = sorted({(p.x, p.y) for p in pts})
if len(ps) < 3:
return [IntPoint(x, y) for x, y in ps]
def cross(o, a, b):
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
def half(points):
out: list[tuple[int, int]] = []
for p in points:
while len(out) >= 2 and cross(out[-2], out[-1], p) <= 0:
out.pop()
out.append(p)
return out[:-1]
return [IntPoint(x, y) for x, y in half(ps) + half(ps[::-1])]
def _pad_tile(shape: Shape, transform: CoordinateTransform): def _pad_tile(shape: Shape, transform: CoordinateTransform):
"""A board-relative pad tile from a DSN padstack shape (centred near origin).""" """A board-relative pad tile from a DSN padstack shape (centred near origin)."""
if isinstance(shape, Rectangle): if isinstance(shape, Rectangle):
+2
View File
@@ -188,6 +188,8 @@ class ShapeSearchTree:
overlap). Static pad-vs-pad spacing in the source design is not counted. overlap). Static pad-vs-pad spacing in the source design is not counted.
""" """
for shape in self.all_shapes(): for shape in self.all_shapes():
if not shape.routed:
continue # a pair with routed copper is checked from that side
expanded = shape.tile.offset(clearance) expanded = shape.tile.offset(clearance)
grown = None # exact clearance region, built lazily only for diagonals grown = None # exact clearance region, built lazily only for diagonals
for other in self.overlapping(expanded): for other in self.overlapping(expanded):
+8 -1
View File
@@ -38,6 +38,7 @@ from freeroute.geometry import (
IntPoint, IntPoint,
Polyline, Polyline,
PolylineShape, PolylineShape,
Simplex,
segment_box, segment_box,
segment_octagon, segment_octagon,
shove_segment, shove_segment,
@@ -154,7 +155,9 @@ def route_board_exact(
net = pin.net_nos[0] if pin.net_nos else -1 net = pin.net_nos[0] if pin.net_nos else -1
pin_layers = frozenset(layer for layer in pin.layers if layer in signal_layers) pin_layers = frozenset(layer for layer in pin.layers if layer in signal_layers)
if pin_layers: if pin_layers:
tree.insert(TreeShape(next(owner), net, pin_layers, box)) # a rotated pad is checked against its outline, not its box
exact = pin.shape if _is_rotated(pin.shape) else None
tree.insert(TreeShape(next(owner), net, pin_layers, box, exact=exact))
for net, copper_layers, box in board.get_fixed_copper(): for net, copper_layers, box in board.get_fixed_copper():
on = frozenset(layer for layer in copper_layers if layer in signal_layers) on = frozenset(layer for layer in copper_layers if layer in signal_layers)
if on and not box.is_empty(): if on and not box.is_empty():
@@ -667,6 +670,10 @@ def _copper_boxes(wires, vias, half_width, via_layers, via_half=None):
return boxes return boxes
def _is_rotated(shape) -> bool:
return isinstance(shape, Simplex) and not shape.is_int_box()
def _net_pins(board: BasicBoard, net_no: int, signal_layers) -> list[Pin]: def _net_pins(board: BasicBoard, net_no: int, signal_layers) -> list[Pin]:
return [ return [
pin pin
+57 -14
View File
@@ -33,7 +33,8 @@ import random
from freeroute.board.board import BasicBoard from freeroute.board.board import BasicBoard
from freeroute.board.items import ConductionArea, ObstacleArea, Pin, Via from freeroute.board.items import ConductionArea, ObstacleArea, Pin, Via
from freeroute.geometry import IntBox, IntPoint from freeroute.geometry import IntBox, IntPoint, Simplex
from freeroute.geometry.octagon import overlaps_2d
from freeroute.geometry.polyline import corner_box from freeroute.geometry.polyline import corner_box
__all__ = ["GridRouter", "RouteResult", "route_board"] __all__ = ["GridRouter", "RouteResult", "route_board"]
@@ -233,7 +234,7 @@ class GridRouter:
(dx, dy) (dx, dy)
for dx in range(-reach, reach + 1) for dx in range(-reach, reach + 1)
for dy in range(-reach, reach + 1) for dy in range(-reach, reach + 1)
if (dx or dy) and math.hypot(dx, dy) * self.step < need if (dx or dy) and max(abs(dx), abs(dy)) * self.step < need
] ]
# A via's copper is wider than a trace. Another net's trace needs its # A via's copper is wider than a trace. Another net's trace needs its
# centre via_half + clearance + half_width from the via's centre, so a # centre via_half + clearance + half_width from the via's centre, so a
@@ -299,6 +300,9 @@ class GridRouter:
# per-connection occupancy (owner = connection index) # per-connection occupancy (owner = connection index)
self._cell_owner: dict[Node, int] = {} self._cell_owner: dict[Node, int] = {}
self._via_owner: dict[Cell, int] = {} self._via_owner: dict[Cell, int] = {}
# every connection whose via covers a cell, in claim order: a via two
# connections share still blocks after the first of them is ripped
self._via_claims: dict[Cell, list[int]] = {}
# trace halos: node -> indices of the connections claiming it # trace halos: node -> indices of the connections claiming it
self._halo: dict[Node, list[int]] = {} self._halo: dict[Node, list[int]] = {}
self._rasterize_obstacles() self._rasterize_obstacles()
@@ -379,6 +383,19 @@ class GridRouter:
shape.contains(IntPoint(c.x + dx, c.y + dy)) for dx in (-h, h) for dy in (-h, h) shape.contains(IntPoint(c.x + dx, c.y + dy)) for dx in (-h, h) for dy in (-h, h)
) )
def _pad_cells(self, shape, box: IntBox, margin: int):
"""Cells where a trace square ``margin`` wide around the centre overlaps
the pad: its box when it is one, else its exact (rotated) outline,
which is what the exact engine checks against."""
cells = self._cells_in_box(box, margin)
if not isinstance(shape, Simplex) or shape.is_int_box():
yield from cells
return
for cell in cells:
c = self._cell_center(cell)
if overlaps_2d(shape, IntBox(c.x - margin, c.y - margin, c.x + margin, c.y + margin)):
yield cell
def _rasterize_obstacles(self) -> None: def _rasterize_obstacles(self) -> None:
margin = self.clearance + self.half_width margin = self.clearance + self.half_width
outline = self.board.outline outline = self.board.outline
@@ -404,7 +421,7 @@ class GridRouter:
continue continue
net = item.net_nos[0] if item.net_nos else 0 net = item.net_nos[0] if item.net_nos else 0
pin_layers = [layer for layer in item.layers if layer in self.layers] pin_layers = [layer for layer in item.layers if layer in self.layers]
for cell in self._cells_in_box(box, margin): for cell in self._pad_cells(item.shape, box, margin):
for layer in pin_layers: for layer in pin_layers:
self._pad_block.setdefault((*cell, layer), set()).add(net) self._pad_block.setdefault((*cell, layer), set()).add(net)
# Copper of the pin's own net: a trace inside it adds nothing, # Copper of the pin's own net: a trace inside it adds nothing,
@@ -593,18 +610,19 @@ class GridRouter:
return self.half_width if conn.narrow else self._net_half.get(conn.net_no, self.half_width) return self.half_width if conn.narrow else self._net_half.get(conn.net_no, self.half_width)
def _spread_for(self, half: int) -> list[tuple[int, int]]: def _spread_for(self, half: int) -> list[tuple[int, int]]:
"""Cells a trace of ``half`` also needs free: those whose centre is """Cells a trace of ``half`` also needs free: every cell out to its
within its extra half width (plus half a cell, for the grid).""" extra half width, rounded up to whole cells. Blocks, halos and via
footprints are sized for a default-width trace, and copper is checked
as boxes, so anything within that square could be too close."""
extra = half - self.half_width extra = half - self.half_width
if extra <= 0: if extra <= 0:
return [] return []
reach = math.ceil(extra / self.step) + 1 reach = math.ceil(extra / self.step)
limit = extra + self.step / 2
return [ return [
(dx, dy) (dx, dy)
for dx in range(-reach, reach + 1) for dx in range(-reach, reach + 1)
for dy in range(-reach, reach + 1) for dy in range(-reach, reach + 1)
if (dx or dy) and math.hypot(dx, dy) * self.step <= limit if dx or dy
] ]
def _halo_for(self, half: int) -> list[tuple[int, int]]: def _halo_for(self, half: int) -> list[tuple[int, int]]:
@@ -617,7 +635,7 @@ class GridRouter:
(dx, dy) (dx, dy)
for dx in range(-reach, reach + 1) for dx in range(-reach, reach + 1)
for dy in range(-reach, reach + 1) for dy in range(-reach, reach + 1)
if (dx or dy) and math.hypot(dx, dy) * self.step < need if (dx or dy) and max(abs(dx), abs(dy)) * self.step < need
] ]
return offsets return offsets
@@ -1184,9 +1202,11 @@ class GridRouter:
conn.via_halo = [] conn.via_halo = []
for cell in conn.via_cells: for cell in conn.via_cells:
for c in self._via_cells(cell): for c in self._via_cells(cell):
if c not in self._via_owner: claims = self._via_claims.setdefault(c, [])
self._via_owner[c] = conn.index if conn.index not in claims:
claims.append(conn.index)
conn.via_halo.append(c) conn.via_halo.append(c)
self._via_owner.setdefault(c, conn.index)
ends = ((conn.a_end, wire_segs[0], 0), (conn.b_end, wire_segs[-1], -1)) if wire_segs else () ends = ((conn.a_end, wire_segs[0], 0), (conn.b_end, wire_segs[-1], -1)) if wire_segs else ()
conn.stubs = [] conn.stubs = []
for pin, (layer, cells), at in ends: for pin, (layer, cells), at in ends:
@@ -1236,8 +1256,16 @@ class GridRouter:
if self._cell_owner.get((*cell, layer)) == conn.index: if self._cell_owner.get((*cell, layer)) == conn.index:
del self._cell_owner[(*cell, layer)] del self._cell_owner[(*cell, layer)]
for cell in conn.via_halo: for cell in conn.via_halo:
claims = self._via_claims.get(cell)
if claims is not None and conn.index in claims:
claims.remove(conn.index)
if self._via_owner.get(cell) == conn.index: if self._via_owner.get(cell) == conn.index:
del self._via_owner[cell] if claims:
self._via_owner[cell] = claims[0]
else:
del self._via_owner[cell]
if claims is not None and not claims:
del self._via_claims[cell]
conn.via_halo = [] conn.via_halo = []
for node in conn.halo_nodes: for node in conn.halo_nodes:
claims = self._halo.get(node) claims = self._halo.get(node)
@@ -1261,7 +1289,20 @@ class GridRouter:
conn.rip_count += 1 conn.rip_count += 1
self._settle(conn, +1) self._settle(conn, +1)
def _crossed_owners(self, path: list[Node], net_no: int, conns: list[_Connection]) -> set[int]: def _crossed_owners(
self, path: list[Node], net_no: int, conns: list[_Connection], spread=None
) -> set[int]:
"""Connections whose copper or clearance ``path`` crosses; a trace
wider than the default crosses them on its ``spread`` cells too."""
spread = self._spread if spread is None else spread
if spread:
path = list(
dict.fromkeys(
(x + dx, y + dy, layer)
for x, y, layer in path
for dx, dy in ((0, 0), *spread)
)
)
owners: set[int] = set() owners: set[int] = set()
for cx, cy, cl in path: for cx, cy, cl in path:
owner = self._cell_owner.get((cx, cy, cl)) owner = self._cell_owner.get((cx, cy, cl))
@@ -1380,7 +1421,9 @@ class GridRouter:
path = self._search(conn, allow_ripup=True, conns=conns) path = self._search(conn, allow_ripup=True, conns=conns)
if path is None: if path is None:
return False return False
crossed = self._crossed_owners(path, conn.net_no, conns) crossed = self._crossed_owners(
path, conn.net_no, conns, self._spread_for(self._conn_half(conn))
)
# and the routes on the stubs from the pins to the path's ends # and the routes on the stubs from the pins to the path's ends
for end, node in ((conn.a_end, path[0]), (conn.b_end, path[-1])): for end, node in ((conn.a_end, path[0]), (conn.b_end, path[-1])):
if isinstance(end, Pin): if isinstance(end, Pin):
+227
View File
@@ -0,0 +1,227 @@
"""The grid must not believe copper legal that the exact check then refuses.
Each case is one way the two used to disagree on roomba-ESP12E: a rotated oval
pad modelled as an inflated box (and opened to its own net), a via shared by
two connections of a net losing its footprint when one of them is ripped, and
a wider net class searched as if it were the default width.
"""
from __future__ import annotations
import math
import pytest
from freeroute.board import build_board
from freeroute.dsn import parse_dsn
from freeroute.geometry import IntBox, Simplex
from freeroute.route.grid_router import GridRouter
from freeroute.route.pipeline import (
_rule_clearance_dsn,
_rule_width_dsn,
_via_half_board,
build_exact_routing_result,
)
# --- rotated oval pads -------------------------------------------------------
def _socket_dsn(rotation: float, layout: str) -> str:
"""A 1x4 2.54 mm socket strip (rect pin 1, oval pins 2-4) at ``rotation``,
each pin its own net, routed to a small SMD pad 0.9 mm away."""
cx, cy = 100000, -100000
t = math.radians(rotation)
ax = (math.cos(t), math.sin(t))
perp = (-math.sin(t), math.cos(t))
places = []
for i in range(4):
px, py = cx + 2540 * i * ax[0], cy + 2540 * i * ax[1]
side = 1 if i % 2 == 0 else -1
if layout == "y":
tx, ty = px, py + side * 9000
else:
tx, ty = px + side * 8000 * perp[0], py + side * 8000 * perp[1]
places.append(f"(place T{i + 1} {round(tx)} {round(ty)} front 0)")
nets = " ".join(f"(net N{i} (pins P3-{i + 1} T{i + 1}-1))" for i in range(4))
return f"""(pcb socket (parser) (resolution um 10) (unit um)
(structure
(layer F.Cu (type signal)) (layer B.Cu (type signal))
(boundary (path pcb 0 125000 -115000 85000 -115000 85000 -75000 125000 -75000
125000 -115000))
(via "Via[0-1]_800:400_um")
(rule (width 300) (clearance 250)))
(placement
(component S (place P3 {cx} {cy} front {rotation}))
(component T {" ".join(places)}))
(library
(image S (pin R 1 0 0) (pin O 2 2540 0) (pin O 3 5080 0) (pin O 4 7620 0))
(image T (pin t 1 0 0))
(padstack O (shape (path F.Cu 1727.2 0 -152.4 0 152.4))
(shape (path B.Cu 1727.2 0 -152.4 0 152.4)) (attach off))
(padstack R (shape (rect F.Cu -863.6 -1016 863.6 1016))
(shape (rect B.Cu -863.6 -1016 863.6 1016)) (attach off))
(padstack t (shape (rect F.Cu -500 -500 500 500)) (attach off))
(padstack "Via[0-1]_800:400_um" (shape (circle F.Cu 800)) (shape (circle B.Cu 800))
(attach off)))
(network {nets}
(class c N0 N1 N2 N3 (circuit (use_via "Via[0-1]_800:400_um"))
(rule (width 300) (clearance 250)))))"""
def test_a_rotated_oval_pad_is_its_outline_not_its_bounding_box():
board = build_board(parse_dsn(_socket_dsn(60, "y")))
oval = next(p for p in board.get_pins() if p.component_ref == "P3" and p.name == "2")
assert isinstance(oval.shape, Simplex) and not oval.shape.is_int_box()
box = oval.shape.bounding_box()
assert oval.shape.contains(oval.location)
assert not oval.shape.contains(box.ll) and not oval.shape.contains(box.ur)
@pytest.mark.parametrize("layout", ["y", "perp"])
def test_routes_leaving_a_rotated_socket_pass_the_exact_check(layout):
result = build_exact_routing_result(
parse_dsn(_socket_dsn(60, layout)), resolution=2, shove=True
)
assert result.unrouted == []
# --- a via two connections share --------------------------------------------
_SHARED_VIA = """(pcb shared (parser) (resolution um 10) (unit um)
(structure
(layer Top (type signal)) (layer Bottom (type signal))
(boundary (path pcb 0 0 0 20000 0 20000 -20000 0 -20000 0 0))
(via "Via[0-1]_800:400_um")
(rule (width 300) (clearance 250)))
(library
(padstack p (shape (rect Top -400 -400 400 400)) (attach off))
(padstack "Via[0-1]_800:400_um" (shape (circle Top 800)) (shape (circle Bottom 800))
(attach off))
(image PAD (pin p 1 0 0)))
(placement (component PAD
(place G1 3000 -5000 front 0) (place G2 3000 -15000 front 0)
(place G3 17000 -10000 front 0) (place S1 3000 -10000 front 0)
(place S2 17000 -3000 front 0)))
(network (net GND (pins G1-1 G2-1 G3-1)) (net SIG (pins S1-1 S2-1))))"""
def _line(a, b, layer):
(x, y), (x1, y1) = a, b
out = [(x, y, layer)]
while x != x1:
x += 1 if x1 > x else -1
out.append((x, y, layer))
while y != y1:
y += 1 if y1 > y else -1
out.append((x, y, layer))
return out
def test_a_shared_via_keeps_its_footprint_when_one_user_is_ripped():
dsn = parse_dsn(_SHARED_VIA)
s = dsn.resolution.value
router = GridRouter(
build_board(dsn),
trace_width=round(_rule_width_dsn(dsn) * s),
clearance=round(_rule_clearance_dsn(dsn) * s),
via_half=_via_half_board(dsn, s),
)
conns = router._conns = router._build_connections()
gnd = [c for c in conns if len({c.a.component_ref, c.b.component_ref} & {"G1", "G2", "G3"})]
sig = next(c for c in conns if c not in gnd)
first, second = gnd
g3 = router._cell_of(next(p for p in router.board.get_pins() if p.component_ref == "G3").location)
via = (g3[0] - 10, g3[1])
def through_via(conn):
a, b = router._cell_of(conn.a.location), router._cell_of(conn.b.location)
return _line(a, via, 0) + _line(via, b, 1)
router._commit(first, through_via(first))
router._commit(second, through_via(second))
footprint = [c for c in router._via_cells(via) if c != via]
router._rip(first)
assert second.routed and via in second.via_cells
assert all(router._via_owner.get(c) == second.index for c in footprint)
# another net still keeps its distance from the via that is left
assert all(router._enter_cost_base(c, 0, sig.net_no, False, conns) is None for c in footprint)
# --- a wider net class --------------------------------------------------------
_WIDE = """(pcb wide (parser) (resolution um 10) (unit um)
(structure
(layer F.Cu (type signal))
(boundary (path pcb 0 0 0 20000 0 20000 -10000 0 -10000 0 0))
(rule (width 300) (clearance 250)))
(placement (component S (place P1 2000 -5000 front 0) (place P2 18000 -5000 front 0)
(place Q1 10000 {ylo} front 0) (place Q2 10000 {yhi} front 0)
(place Q3 10000 -1000 front 0) (place Q4 10000 -9000 front 0)))
(library (image S (pin pad 1 0 0)) (padstack pad (shape (rect F.Cu -600 -600 600 600)) (attach off)))
(network (net PWR (pins P1-1 P2-1)) (net SIG (pins Q1-1 Q3-1)) (net SIG2 (pins Q2-1 Q4-1))
(class Power PWR (rule (width 500) (clearance 250)))))"""
@pytest.mark.parametrize("resolution", [1, 2])
@pytest.mark.parametrize("gap", [900, 1000, 1075])
def test_a_wide_net_past_pads_it_does_not_fit_between_is_not_refused(gap, resolution):
# 0.3 mm fits the gap between the two pads, 0.5 mm does not: the grid
# must see that, rather than lay it wide and have the exact check drop it
ylo, yhi = -5000 - gap // 2 - 600, -5000 + gap // 2 + 600
result = build_exact_routing_result(
parse_dsn(_WIDE.format(ylo=ylo, yhi=yhi)), resolution=resolution
)
assert result.unrouted == []
def _wide_router():
dsn = parse_dsn(_WIDE.format(ylo=-6050, yhi=-3950))
s = dsn.resolution.value
pwr = next(i for i, net in enumerate(dsn.nets, 1) if net.name == "PWR")
return GridRouter(
build_board(dsn),
trace_width=round(_rule_width_dsn(dsn) * s),
clearance=round(_rule_clearance_dsn(dsn) * s),
resolution=2,
net_widths={pwr: 5000},
), pwr
def test_a_wide_trace_keeps_clear_of_a_default_one_at_the_grid_diagonal():
router, _pwr = _wide_router()
# 0.5 mm against 0.3 mm needs 0.25 + 0.25 + 0.15 = 0.65 mm between centres,
# on either axis, since the exact check compares boxes
need = 2500 + router.clearance + router.half_width
for dx, dy in [(2, 2), (2, 1), (1, 2)]:
assert max(dx, dy) * router.step < need
assert (dx, dy) in router._halo_for(2500)
def test_a_wide_rip_up_route_counts_what_its_extra_width_crosses():
router, pwr_net = _wide_router()
conns = router._conns = router._build_connections()
pwr = next(c for c in conns if c.net_no == pwr_net)
other = next(c for c in conns if c.net_no != pwr_net)
# a default-width trace two cells from the wide route's path
x, y = 20, 5
router._commit(other, _line((x - 3, y + 2), (x + 3, y + 2), 0))
path = _line((x - 3, y), (x + 3, y), 0)
spread = router._spread_for(2500)
assert other.index not in router._crossed_owners(path, pwr.net_no, conns)
assert other.index in router._crossed_owners(path, pwr.net_no, conns, spread)
def test_a_pad_cell_check_uses_the_rotated_outline():
board = build_board(parse_dsn(_socket_dsn(60, "y")))
router = GridRouter(board, trace_width=3000, clearance=2500, resolution=2)
oval = next(p for p in board.get_pins() if p.component_ref == "P3" and p.name == "2")
box = oval.shape.bounding_box()
margin = router.clearance + router.half_width
exact = set(router._pad_cells(oval.shape, box, margin))
boxed = set(router._cells_in_box(box, margin))
assert exact < boxed
for cell in boxed - exact:
c = router._cell_center(cell)
square = IntBox(c.x - margin, c.y - margin, c.x + margin, c.y + margin)
assert oval.shape.intersection(square).dimension() != 2