Merge fix/ttn-9: straight pin exits, rip-up can reach a pin's way out, exact checks between stubs
This commit is contained in:
@@ -27,6 +27,7 @@ DRC-clean geometry, and blind/buried via spans.
|
||||
from __future__ import annotations
|
||||
|
||||
import heapq
|
||||
import itertools
|
||||
import math
|
||||
import random
|
||||
|
||||
@@ -123,6 +124,7 @@ class _Connection:
|
||||
"path",
|
||||
"narrow",
|
||||
"half",
|
||||
"stubs",
|
||||
)
|
||||
|
||||
def __init__(self, index: int, net_no: int, a: Pin, b: Pin) -> None:
|
||||
@@ -138,6 +140,8 @@ class _Connection:
|
||||
self.via_cells: list[Cell] = []
|
||||
self.via_halo: list[Cell] = []
|
||||
self.halo_nodes: list[Node] = []
|
||||
# each end's stub: its layer, its copper tiles, and the nodes it claims
|
||||
self.stubs: list[tuple[int, list[IntBox], set[Node]]] = []
|
||||
# the pins and zones each end may use, and which pin the route ended
|
||||
# on (None when it ended on a zone)
|
||||
self.a_group: tuple[list[Pin], list] = ([a], [])
|
||||
@@ -634,8 +638,8 @@ class GridRouter:
|
||||
if not conn.b_group[0] and any(z.layer in self._power_layers for z in conn.b_group[1]):
|
||||
# the plane is everywhere: aim at where the route starts instead
|
||||
goal_cell = self._group_target(conn.a_group)
|
||||
starts = self._group_ends(conn.a_group, conn.net_no, conns)
|
||||
goals = self._group_ends(conn.b_group, conn.net_no, conns)
|
||||
starts = self._group_ends(conn.a_group, conn.net_no, conns, allow_ripup)
|
||||
goals = self._group_ends(conn.b_group, conn.net_no, conns, allow_ripup)
|
||||
if not starts or not goals:
|
||||
return None
|
||||
# A start that is already an end joins at once, except that ending on
|
||||
@@ -656,10 +660,19 @@ class GridRouter:
|
||||
came_from: dict[Node, Node] = {}
|
||||
# a stub costs its length, so the nearest cell wins unless it is blocked
|
||||
# a pin end costs its stub; a zone or plane end is entered normally
|
||||
goal_stub = {n: self._stub_cost(p, n) for n, p in goals.items() if isinstance(p, Pin)}
|
||||
# (and in rip-up mode the rip penalty of whatever lies on it)
|
||||
goal_stub = {
|
||||
n: self._stub_cost(p, n) + self._end_penalty(p, n, net_no, allow_ripup, conns)
|
||||
for n, p in goals.items()
|
||||
if isinstance(p, Pin)
|
||||
}
|
||||
for s in sorted(starts):
|
||||
pin = starts[s]
|
||||
g_score[s] = self._stub_cost(pin, s) if isinstance(pin, Pin) else 0.0
|
||||
g_score[s] = (
|
||||
self._stub_cost(pin, s) + self._end_penalty(pin, s, net_no, allow_ripup, conns)
|
||||
if isinstance(pin, Pin)
|
||||
else 0.0
|
||||
)
|
||||
heapq.heappush(open_heap, (g_score[s] + self._h(s, goal_cell), s))
|
||||
|
||||
# Via footprints are checked lazily: a layer change goes on the heap as
|
||||
@@ -790,24 +803,52 @@ class GridRouter:
|
||||
math.hypot(loc.x - point.x, loc.y - point.y) < reach for loc, reach in self._tht_holes
|
||||
)
|
||||
|
||||
def _stub_points(self, pin_location: IntPoint, cell: Cell) -> list[IntPoint]:
|
||||
"""The stub's corners, from the pin to its cell centre.
|
||||
|
||||
The direct segment is fine for 45-degree routing. Orthogonal routing
|
||||
leaves the pin straight along the axis it is further off the cell on
|
||||
and jogs across to the centre at the end: an L of two flush boxes,
|
||||
which is what the exact check sees. A single slanted segment is
|
||||
checked as its bounding box there, and a 1 mm trace to a column
|
||||
0.1 mm off a 1.27 mm pitch pin's centreline then lands inside the
|
||||
clearance of the pad next door, though the straight exit clears it.
|
||||
"""
|
||||
end = self._cell_center(cell)
|
||||
dx, dy = end.x - pin_location.x, end.y - pin_location.y
|
||||
if not self._orthogonal or dx == 0 or dy == 0:
|
||||
return [pin_location, end]
|
||||
if abs(dx) >= abs(dy):
|
||||
return [pin_location, IntPoint(end.x, pin_location.y), end]
|
||||
return [pin_location, IntPoint(pin_location.x, end.y), end]
|
||||
|
||||
def _stub_cells(self, pin_location: IntPoint, cell: Cell, half: int | None = None) -> list[Cell]:
|
||||
"""Cells where another net's trace would come too close to the stub."""
|
||||
from freeroute.board.outline import _point_segment
|
||||
|
||||
end = self._cell_center(cell)
|
||||
points = self._stub_points(pin_location, cell)
|
||||
end = points[-1]
|
||||
reach = (half or self._cur_half) + self.half_width + self.clearance
|
||||
box = IntBox(
|
||||
min(pin_location.x, end.x), min(pin_location.y, end.y),
|
||||
max(pin_location.x, end.x), max(pin_location.y, end.y),
|
||||
)
|
||||
legs = list(zip(points, points[1:], strict=False))
|
||||
return [
|
||||
c
|
||||
for c in self._cells_in_box(box, reach)
|
||||
if _point_segment(self._cell_center(c), pin_location, end) < reach
|
||||
if min(_point_segment(self._cell_center(c), a, b) for a, b in legs) < reach
|
||||
]
|
||||
|
||||
def _stub_clear(
|
||||
self, pin: Pin, cell: Cell, layer: int, net_no: int, conns, keepouts_only: bool = False
|
||||
self,
|
||||
pin: Pin,
|
||||
cell: Cell,
|
||||
layer: int,
|
||||
net_no: int,
|
||||
conns,
|
||||
keepouts_only: bool = False,
|
||||
allow_ripup: bool = False,
|
||||
) -> bool:
|
||||
keepouts = self._keepout_boxes.get(layer)
|
||||
if keepouts:
|
||||
@@ -815,9 +856,9 @@ class GridRouter:
|
||||
# engine will: its copper grown by clearance must miss every keepout
|
||||
from freeroute.geometry.polyline import Polyline, PolylineShape
|
||||
|
||||
end = self._cell_center(cell)
|
||||
if end != pin.location:
|
||||
for tile in PolylineShape(Polyline([pin.location, end]), self._cur_half).tiles():
|
||||
points = self._stub_points(pin.location, cell)
|
||||
if points[-1] != pin.location:
|
||||
for tile in PolylineShape(Polyline(points), self._cur_half).tiles():
|
||||
grown = tile.offset(self.clearance)
|
||||
if any(k.overlaps(grown if needs else tile) for k, needs in keepouts):
|
||||
return False
|
||||
@@ -827,14 +868,60 @@ class GridRouter:
|
||||
return False
|
||||
if keepouts_only:
|
||||
return True
|
||||
for c in self._stub_cells(pin.location, cell):
|
||||
owner = self._cell_owner.get((*c, layer))
|
||||
if owner is not None and conns[owner].net_no != net_no:
|
||||
return False
|
||||
owners = self._stub_owners(pin, cell, layer, net_no, conns)
|
||||
# in rip-up mode a route on the stub is one more to rip, unless it may not be
|
||||
return not owners or (
|
||||
allow_ripup and all(conns[i].rip_count < self.rip_cap for i in owners)
|
||||
)
|
||||
|
||||
def _stub_tiles(self, pin_location: IntPoint, cell: Cell, half: int) -> list[IntBox]:
|
||||
"""The stub's copper: its legs, and a square at each corner and at the
|
||||
cell end, where the route carries on."""
|
||||
points = self._stub_points(pin_location, cell)
|
||||
tiles = [corner_box(point, half) for point in points[1:]]
|
||||
if len(points) > 1 and points[-1] != pin_location:
|
||||
from freeroute.geometry.polyline import Polyline, PolylineShape
|
||||
|
||||
tiles += PolylineShape(Polyline(points), half).tiles()
|
||||
return tiles
|
||||
|
||||
def _stub_owners(
|
||||
self, pin: Pin, cell: Cell, layer: int, net_no: int, conns, half: int | None = None
|
||||
) -> set[int]:
|
||||
"""Other nets' routes whose copper the stub from ``pin`` to ``cell`` would touch."""
|
||||
owners: set[int] = set()
|
||||
grown = None
|
||||
for c in self._stub_cells(pin.location, cell, half):
|
||||
node = (*c, layer)
|
||||
owner = self._cell_owner.get(node)
|
||||
if owner is not None and conns[owner].net_no != net_no and owner not in owners:
|
||||
# Both stubs are off the grid, and each claims the cells a
|
||||
# trace centred there would crowd, so two stubs side by side
|
||||
# (neighbouring pins leaving straight) overlap in claims well
|
||||
# before their copper comes near. Check those exactly.
|
||||
other = [tiles for lay, tiles, nodes in conns[owner].stubs if node in nodes]
|
||||
if other:
|
||||
if grown is None:
|
||||
grown = [
|
||||
t.offset(self.clearance)
|
||||
for t in self._stub_tiles(pin.location, cell, half or self._cur_half)
|
||||
]
|
||||
if not any(g.overlaps(t) for tiles in other for t in tiles for g in grown):
|
||||
continue
|
||||
owners.add(owner)
|
||||
via_owner = self._via_owner.get(c)
|
||||
if via_owner is not None and conns[via_owner].net_no != net_no:
|
||||
return False
|
||||
return True
|
||||
owners.add(via_owner)
|
||||
return owners
|
||||
|
||||
def _end_penalty(self, pin: Pin, node: Node, net_no: int, allow_ripup: bool, conns) -> float:
|
||||
"""Rip-up cost of starting or ending on ``node``: the routes on its
|
||||
cell and on the stub to it, each at the penalty entering it would cost."""
|
||||
if not allow_ripup:
|
||||
return 0.0
|
||||
owners = self._stub_owners(pin, node[:2], node[2], net_no, conns)
|
||||
owners |= self._crossed_owners([node], net_no, conns)
|
||||
return sum(self.rip_penalty * (1 + conns[i].rip_count) for i in owners)
|
||||
|
||||
def _zone_nodes(self, zone) -> list[Node]:
|
||||
"""Cells of a zone where a via of this net fits inside its copper; for a
|
||||
@@ -882,11 +969,13 @@ class GridRouter:
|
||||
return zone.name or "zone"
|
||||
return ""
|
||||
|
||||
def _group_ends(self, group, net_no: int, conns) -> dict[Node, Pin | None]:
|
||||
def _group_ends(
|
||||
self, group, net_no: int, conns, allow_ripup: bool = False
|
||||
) -> dict[Node, Pin | None]:
|
||||
"""End nodes of a group, each mapped to its pin (None for a zone cell)."""
|
||||
ends: dict[Node, Pin | None] = {}
|
||||
for pin in group[0]:
|
||||
for node in sorted(self._end_nodes(pin, net_no, conns)):
|
||||
for node in sorted(self._end_nodes(pin, net_no, conns, allow_ripup)):
|
||||
ends.setdefault(node, pin)
|
||||
for zone in group[1]:
|
||||
plane = zone.layer in self._power_layers
|
||||
@@ -908,10 +997,13 @@ class GridRouter:
|
||||
return (dx + dy) / self.step
|
||||
return math.hypot(dx, dy) / self.step
|
||||
|
||||
def _end_nodes(self, pin: Pin, net_no: int, conns) -> set[Node]:
|
||||
def _end_nodes(self, pin: Pin, net_no: int, conns, allow_ripup: bool = False) -> set[Node]:
|
||||
"""Nodes a route may start or end on for ``pin``: the nearest cell and
|
||||
its neighbours, where the cell is enterable and the stub to it is clear.
|
||||
A crowded fine-pitch pin gets more than one way out."""
|
||||
A crowded fine-pitch pin gets more than one way out. In rip-up mode a
|
||||
cell or stub another route is on counts as enterable, as it does
|
||||
everywhere else, or a pin whose only way out another net has taken
|
||||
could never have it back."""
|
||||
layers = [layer for layer in pin.layers if layer in self.layers]
|
||||
cx, cy = self._cell_of(pin.location)
|
||||
nodes: set[Node] = set()
|
||||
@@ -920,16 +1012,16 @@ class GridRouter:
|
||||
if not (0 <= cell[0] < self.cols and 0 <= cell[1] < self.rows):
|
||||
continue
|
||||
for layer in layers:
|
||||
if (dx or dy) and self._enter_cost(cell, layer, net_no, False, conns) is None:
|
||||
if (dx or dy) and self._enter_cost(cell, layer, net_no, allow_ripup, conns) is None:
|
||||
continue
|
||||
if self._stub_clear(pin, cell, layer, net_no, conns):
|
||||
if self._stub_clear(pin, cell, layer, net_no, conns, allow_ripup=allow_ripup):
|
||||
nodes.add((*cell, layer))
|
||||
# Also offer exact escape stubs: a fine-pitch pin's nearby cells can be
|
||||
# a pocket inside its own pad that leads nowhere (the grid columns miss
|
||||
# the narrow legal band past the pad tip). The stub's length is its
|
||||
# cost, so it is only taken where a short step would not do.
|
||||
if self._orthogonal and self._crowded(pin, net_no) and self._trapped(pin, net_no, nodes):
|
||||
nodes |= self._escape_nodes(pin, net_no, conns, layers, (cx, cy))
|
||||
nodes |= self._escape_nodes(pin, net_no, conns, layers, (cx, cy), allow_ripup)
|
||||
if not nodes: # nothing clear: fall back to the nearest cell, as before
|
||||
nodes = {(cx, cy, layer) for layer in layers}
|
||||
if self._keepout_boxes and not any(
|
||||
@@ -995,14 +1087,16 @@ class GridRouter:
|
||||
self._trapped_cache[key] = trapped
|
||||
return trapped
|
||||
|
||||
def _escape_nodes(self, pin: Pin, net_no: int, conns, layers, origin: Cell) -> set[Node]:
|
||||
def _escape_nodes(
|
||||
self, pin: Pin, net_no: int, conns, layers, origin: Cell, allow_ripup: bool = False
|
||||
) -> set[Node]:
|
||||
"""For a pin walled in on the grid (a fine-pitch pin whose legal band
|
||||
the grid columns miss): look along each axis for the first open cell
|
||||
whose straight stub from the pin clears every other net's pad by the
|
||||
full margin, measured exactly rather than on the grid."""
|
||||
from freeroute.board.outline import _segment_box
|
||||
whose stub from the pin clears every other net's pad by the full
|
||||
clearance, checked on the stub's copper tiles as the exact engine
|
||||
will rather than on the grid."""
|
||||
from freeroute.geometry.polyline import Polyline, PolylineShape
|
||||
|
||||
margin = self.half_width + self.clearance
|
||||
reach = self._escape_cells
|
||||
near = [
|
||||
(p.shape.bounding_box(), set(p.layers))
|
||||
@@ -1014,24 +1108,33 @@ class GridRouter:
|
||||
]
|
||||
cx, cy = origin
|
||||
nodes: set[Node] = set()
|
||||
for dx, dy in _NEIGHBOURS[:4]:
|
||||
# the pin's own column or row and the one either side: two pins side
|
||||
# by side can then jog apart at the end of their straight exits
|
||||
for (dx, dy), side in itertools.product(_NEIGHBOURS[:4], (0, -1, 1)):
|
||||
sx, sy = (side, 0) if dx == 0 else (0, side)
|
||||
nodes_before = len(nodes)
|
||||
for k in range(2, reach + 1):
|
||||
cell = (cx + dx * k, cy + dy * k)
|
||||
cell = (cx + dx * k + sx, cy + dy * k + sy)
|
||||
if not (0 <= cell[0] < self.cols and 0 <= cell[1] < self.rows):
|
||||
break
|
||||
end = self._cell_center(cell)
|
||||
grown = [
|
||||
tile.offset(self.clearance)
|
||||
for tile in PolylineShape(
|
||||
Polyline(self._stub_points(pin.location, cell)), self._cur_half
|
||||
).tiles()
|
||||
]
|
||||
for layer in layers:
|
||||
if self._enter_cost(cell, layer, net_no, False, conns) is None:
|
||||
if self._enter_cost(cell, layer, net_no, allow_ripup, conns) is None:
|
||||
continue
|
||||
if any(
|
||||
layer in box_layers and _segment_box(pin.location, end, box) < margin
|
||||
layer in box_layers and any(g.overlaps(box) for g in grown)
|
||||
for box, box_layers in near
|
||||
):
|
||||
continue
|
||||
if self._stub_clear(pin, cell, layer, net_no, conns):
|
||||
if self._stub_clear(pin, cell, layer, net_no, conns, allow_ripup=allow_ripup):
|
||||
nodes.add((*cell, layer))
|
||||
if any(n[:2] == cell for n in nodes):
|
||||
break # the first open cell along this axis is enough
|
||||
if len(nodes) > nodes_before:
|
||||
break # the first open cell along this line is enough
|
||||
return nodes
|
||||
|
||||
def _relax(self, current, nxt, tentative, goal_cell, g_score, came_from, open_heap):
|
||||
@@ -1085,15 +1188,19 @@ class GridRouter:
|
||||
self._via_owner[c] = conn.index
|
||||
conn.via_halo.append(c)
|
||||
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:
|
||||
if not isinstance(pin, Pin):
|
||||
continue # ended on a zone or a plane: no stub
|
||||
cell = cells[at]
|
||||
claimed: set[Node] = set()
|
||||
for c in self._stub_cells(pin.location, cell, half):
|
||||
node = (*c, layer)
|
||||
if node not in self._cell_owner:
|
||||
self._cell_owner[node] = conn.index
|
||||
conn.trace_cells.append((c, layer))
|
||||
claimed.add(node)
|
||||
conn.stubs.append((layer, self._stub_tiles(pin.location, cell, half), claimed))
|
||||
# A pin inside a wire keepout can only leave by a via. Its stub to the
|
||||
# cell centre would be track inside the keepout, so the via goes on
|
||||
# the pin itself (a via in its own pad) and the stub is left out.
|
||||
@@ -1111,9 +1218,9 @@ class GridRouter:
|
||||
continue
|
||||
points = [self._cell_center(c) for c in cells]
|
||||
if idx == 0 and isinstance(conn.a_end, Pin):
|
||||
points.insert(0, conn.a_end.location)
|
||||
points[0:0] = self._stub_points(conn.a_end.location, cells[0])[:-1]
|
||||
if idx == len(wire_segs) - 1 and isinstance(conn.b_end, Pin):
|
||||
points.append(conn.b_end.location)
|
||||
points.extend(self._stub_points(conn.b_end.location, cells[-1])[-2::-1])
|
||||
points = _simplify(points)
|
||||
if len(points) >= 2:
|
||||
conn.wires.append((layer, points))
|
||||
@@ -1140,6 +1247,7 @@ class GridRouter:
|
||||
del self._halo[node]
|
||||
conn.halo_nodes = []
|
||||
conn.trace_cells = []
|
||||
conn.stubs = []
|
||||
for cell in conn.via_cells:
|
||||
count = self._via_nets.get(cell)
|
||||
if count and count.get(conn.net_no):
|
||||
@@ -1272,7 +1380,14 @@ class GridRouter:
|
||||
path = self._search(conn, allow_ripup=True, conns=conns)
|
||||
if path is None:
|
||||
return False
|
||||
victims = [conns[i] for i in sorted(self._crossed_owners(path, conn.net_no, conns))]
|
||||
crossed = self._crossed_owners(path, conn.net_no, conns)
|
||||
# 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):
|
||||
crossed |= self._stub_owners(
|
||||
end, node[:2], node[2], conn.net_no, conns, self._conn_half(conn)
|
||||
)
|
||||
victims = [conns[i] for i in sorted(crossed)]
|
||||
if self.rollback:
|
||||
return self._ripup_transaction(conn, path, victims, conns)
|
||||
for victim in victims:
|
||||
|
||||
@@ -51,19 +51,22 @@ def test_routes_keep_edge_clearance_at_a_rounded_corner():
|
||||
assert outline.edge_distance(p) >= half + 500 * scale - 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("resolution", "escape"), [(1, 2), (4, 5)])
|
||||
def test_every_pin_of_a_half_millimetre_pitch_row_escapes(resolution, escape):
|
||||
@pytest.mark.parametrize("resolution", [1, 4])
|
||||
def test_every_pin_of_a_half_millimetre_pitch_row_escapes(resolution):
|
||||
# Five 0.3 mm pads at 0.5 mm pitch, each wired to its own pad 8 mm away.
|
||||
# Between two neighbours a trace's centre has a legal band only 0.15 mm
|
||||
# wide. At resolution 1 the 0.4 mm grid misses most bands; at 4 its
|
||||
# 0.1 mm columns cannot, and every pin gets out, as the JAR manages.
|
||||
# wide. At resolution 4 the 0.1 mm columns cannot miss it. At resolution
|
||||
# 1 the 0.4 mm grid misses most bands, but a stub leaves its pin straight
|
||||
# along the band and jogs to the column only past the pads, so every pin
|
||||
# gets out there too, as the JAR manages (only 2 did before).
|
||||
places = " ".join(f"(place R{i} {6000 + 500 * i} -6000 front 0)" for i in range(5))
|
||||
places += " " + " ".join(f"(place F{i} {6000 + 500 * i} -14000 front 0)" for i in range(5))
|
||||
nets = " ".join(f"(net N{i} (pins R{i}-1 F{i}-1))" for i in range(5))
|
||||
result, _, _ = route_dsn_board_exact(
|
||||
parse_dsn(_dsn(f"(component P {places})", nets)), resolution=resolution
|
||||
)
|
||||
assert len(result.routed_net_numbers) == escape
|
||||
assert len(result.routed_net_numbers) == 5
|
||||
assert result.drc_clean
|
||||
|
||||
|
||||
def test_resolution_one_has_no_halo_so_the_grid_is_unchanged():
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Pins of a 1.27 mm pitch row routed at 1.0 mm (the RN2483 module on the
|
||||
TTN Enschede mote). A trace centre has to stay within 0.06 mm of a pin's
|
||||
centreline to pass its neighbours, which no grid column hits, so a route
|
||||
leaves each pin straight along that line and only jogs to its column once
|
||||
past the pads. FreeRouting routes this row in full the same way."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from freeroute.board import build_board
|
||||
from freeroute.board.items import Pin
|
||||
from freeroute.dsn import parse_dsn
|
||||
from freeroute.geometry import IntPoint
|
||||
from freeroute.route.grid_router import GridRouter
|
||||
from freeroute.route.pipeline import route_dsn_board_exact
|
||||
|
||||
|
||||
def _row(x0: int, pins: int = 6, skew: int = 3000) -> str:
|
||||
places = " ".join(f"(place R{i} {x0 + 1270 * i} -5000 front 0)" for i in range(pins))
|
||||
places += " " + " ".join(
|
||||
f"(place T{i} {x0 + 1270 * i + skew} -15000 front 0)" for i in range(pins)
|
||||
)
|
||||
nets = " ".join(f"(net N{i} (pins R{i}-1 T{i}-1))" for i in range(pins))
|
||||
return f"""(pcb t (parser) (resolution um 10) (unit um)
|
||||
(structure (layer F.Cu (type signal)) (layer B.Cu (type signal))
|
||||
(boundary (path pcb 0 0 0 30000 0 30000 -20000 0 -20000 0 0))
|
||||
(via "Via[0-1]_600:400_um") (rule (width 1000) (clearance 200)))
|
||||
(placement (component P {places}))
|
||||
(library (image P (pin pad 1 0 0))
|
||||
(padstack pad (shape (rect F.Cu -508 -762 508 762)) (attach off))
|
||||
(padstack "Via[0-1]_600:400_um" (shape (circle F.Cu 600)) (shape (circle B.Cu 600))
|
||||
(attach off)))
|
||||
(network {nets}))"""
|
||||
|
||||
|
||||
@pytest.mark.parametrize("x0", [5000, 5150, 5300])
|
||||
def test_every_pin_of_a_row_leaves_straight_and_routes(x0):
|
||||
# three offsets of the row against the grid; before, 2 or 3 of 6 routed
|
||||
result, _, _ = route_dsn_board_exact(parse_dsn(_row(x0)), resolution=2)
|
||||
assert len(result.routed_net_numbers) == 6
|
||||
assert result.drc_clean
|
||||
|
||||
|
||||
def test_a_walled_in_pin_may_escape_to_the_column_either_side():
|
||||
# two pins side by side leaving the same way can then jog apart
|
||||
board = build_board(parse_dsn(_row(5000)))
|
||||
router = GridRouter(board, trace_width=10000, clearance=2000, orthogonal=True, resolution=2)
|
||||
router._conns = router._build_connections()
|
||||
pin = next(
|
||||
p for p in board.get_items()
|
||||
if isinstance(p, Pin) and p.location == IntPoint(50000 + 2 * 12700, -50000)
|
||||
)
|
||||
net = pin.net_nos[0]
|
||||
cx, cy = router._cell_of(pin.location)
|
||||
nodes = router._escape_nodes(pin, net, router._conns, [0], (cx, cy))
|
||||
columns = {x for x, y, _ in nodes if y != cy}
|
||||
assert {cx - 1, cx, cx + 1} <= columns
|
||||
Reference in New Issue
Block a user