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:
+9
-2
@@ -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.pipeline import (
|
||||
_mm_to_board,
|
||||
_net_widths_board,
|
||||
_rule_clearance_dsn,
|
||||
_rule_width_dsn,
|
||||
_via_drill_board,
|
||||
_via_half_board,
|
||||
)
|
||||
|
||||
@@ -183,7 +185,7 @@ def _flood(router: GridRouter, pin, net_no: int, conns) -> tuple[int, Counter]:
|
||||
|
||||
def diagnose(
|
||||
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:
|
||||
dsn = parse_dsn(dsn_text)
|
||||
names = {i + 1: n.name for i, n in enumerate(dsn.nets)}
|
||||
@@ -199,6 +201,10 @@ def diagnose(
|
||||
resolution=resolution,
|
||||
shove=shove,
|
||||
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"]
|
||||
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("--shove", action="store_true", help="as mckicad routes")
|
||||
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")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
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"]:
|
||||
extra = ""
|
||||
for w in row.get("walled", []):
|
||||
|
||||
@@ -268,15 +268,39 @@ def _placed_pad_tile(shape: Shape, place, dsn_pin, transform: CoordinateTransfor
|
||||
for x, y in corners:
|
||||
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))))
|
||||
if len(pts) >= 3:
|
||||
simplex = Simplex.from_corners(pts)
|
||||
if not simplex.is_empty():
|
||||
return simplex
|
||||
# An oval's padded corners are not in outline order, and a mirrored
|
||||
# part reverses the winding: take the convex hull, then either winding.
|
||||
hull = _convex_hull(pts)
|
||||
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]
|
||||
ys = [p.y for p in pts] or [0]
|
||||
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):
|
||||
"""A board-relative pad tile from a DSN padstack shape (centred near origin)."""
|
||||
if isinstance(shape, Rectangle):
|
||||
|
||||
@@ -188,6 +188,8 @@ class ShapeSearchTree:
|
||||
overlap). Static pad-vs-pad spacing in the source design is not counted.
|
||||
"""
|
||||
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)
|
||||
grown = None # exact clearance region, built lazily only for diagonals
|
||||
for other in self.overlapping(expanded):
|
||||
|
||||
@@ -38,6 +38,7 @@ from freeroute.geometry import (
|
||||
IntPoint,
|
||||
Polyline,
|
||||
PolylineShape,
|
||||
Simplex,
|
||||
segment_box,
|
||||
segment_octagon,
|
||||
shove_segment,
|
||||
@@ -154,7 +155,9 @@ def route_board_exact(
|
||||
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)
|
||||
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():
|
||||
on = frozenset(layer for layer in copper_layers if layer in signal_layers)
|
||||
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
|
||||
|
||||
|
||||
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]:
|
||||
return [
|
||||
pin
|
||||
|
||||
@@ -33,7 +33,8 @@ import random
|
||||
|
||||
from freeroute.board.board import BasicBoard
|
||||
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
|
||||
|
||||
__all__ = ["GridRouter", "RouteResult", "route_board"]
|
||||
@@ -233,7 +234,7 @@ class GridRouter:
|
||||
(dx, dy)
|
||||
for dx 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
|
||||
# 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)
|
||||
self._cell_owner: dict[Node, 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
|
||||
self._halo: dict[Node, list[int]] = {}
|
||||
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)
|
||||
)
|
||||
|
||||
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:
|
||||
margin = self.clearance + self.half_width
|
||||
outline = self.board.outline
|
||||
@@ -404,7 +421,7 @@ class GridRouter:
|
||||
continue
|
||||
net = item.net_nos[0] if item.net_nos else 0
|
||||
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:
|
||||
self._pad_block.setdefault((*cell, layer), set()).add(net)
|
||||
# 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)
|
||||
|
||||
def _spread_for(self, half: int) -> list[tuple[int, int]]:
|
||||
"""Cells a trace of ``half`` also needs free: those whose centre is
|
||||
within its extra half width (plus half a cell, for the grid)."""
|
||||
"""Cells a trace of ``half`` also needs free: every cell out to its
|
||||
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
|
||||
if extra <= 0:
|
||||
return []
|
||||
reach = math.ceil(extra / self.step) + 1
|
||||
limit = extra + self.step / 2
|
||||
reach = math.ceil(extra / self.step)
|
||||
return [
|
||||
(dx, dy)
|
||||
for dx 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]]:
|
||||
@@ -617,7 +635,7 @@ class GridRouter:
|
||||
(dx, dy)
|
||||
for dx 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
|
||||
|
||||
@@ -1184,9 +1202,11 @@ class GridRouter:
|
||||
conn.via_halo = []
|
||||
for cell in conn.via_cells:
|
||||
for c in self._via_cells(cell):
|
||||
if c not in self._via_owner:
|
||||
self._via_owner[c] = conn.index
|
||||
claims = self._via_claims.setdefault(c, [])
|
||||
if conn.index not in claims:
|
||||
claims.append(conn.index)
|
||||
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 ()
|
||||
conn.stubs = []
|
||||
for pin, (layer, cells), at in ends:
|
||||
@@ -1236,8 +1256,16 @@ class GridRouter:
|
||||
if self._cell_owner.get((*cell, layer)) == conn.index:
|
||||
del self._cell_owner[(*cell, layer)]
|
||||
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:
|
||||
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 = []
|
||||
for node in conn.halo_nodes:
|
||||
claims = self._halo.get(node)
|
||||
@@ -1261,7 +1289,20 @@ class GridRouter:
|
||||
conn.rip_count += 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()
|
||||
for cx, cy, cl in path:
|
||||
owner = self._cell_owner.get((cx, cy, cl))
|
||||
@@ -1380,7 +1421,9 @@ class GridRouter:
|
||||
path = self._search(conn, allow_ripup=True, conns=conns)
|
||||
if path is None:
|
||||
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
|
||||
for end, node in ((conn.a_end, path[0]), (conn.b_end, path[-1])):
|
||||
if isinstance(end, Pin):
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user