Merge fix/roomba: grid and exact check agree on rotated pads, shared vias and wide nets
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.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", []):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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