Route with copper zones, and let vias into no-tracks areas
freeroute ignored the DSN's planes (KiCad's zones), so it never knew that pads in a pour are joined by it. On the rc4 power stage those pads sit inside "keep router off" areas as well, and every connection to them was dropped without a word. - Read (plane <net> <shape>) and put each zone on the board as a ConductionArea of its net. Zones are not obstacles: KiCad refills them around other nets' copper. - The grid groups each net's pads and zone pieces that copper already joins (a pad in a zone, two zones sharing a pad) and routes only between groups. A connection may end on a zone by a via inside its copper, or by a track where tracks are allowed. - wire_keepout now blocks traces only, as KiCad's "no tracks" rule area does; via_keepout only vias; keepout both, in the grid and in the exact verifier. rc4 power stage, exact engine, resolution 2: 2 unconnected, down from 8 (FreeRouting JAR: 12), no DRC errors.
This commit is contained in:
@@ -12,7 +12,7 @@ from __future__ import annotations
|
||||
from freeroute.geometry import IntBox
|
||||
|
||||
from .clearance import ClearanceMatrix
|
||||
from .items import Item, ObstacleArea, Pin, Trace, Via
|
||||
from .items import ConductionArea, Item, ObstacleArea, Pin, Trace, Via
|
||||
from .layer import LayerStructure
|
||||
from .net import Nets
|
||||
|
||||
@@ -57,7 +57,15 @@ class BasicBoard:
|
||||
return [it for it in self._items if isinstance(it, Pin)]
|
||||
|
||||
def get_obstacle_areas(self) -> list[ObstacleArea]:
|
||||
return [it for it in self._items if isinstance(it, ObstacleArea)]
|
||||
"""Keepouts. Copper zones are not obstacles; see get_conduction_areas."""
|
||||
return [
|
||||
it
|
||||
for it in self._items
|
||||
if isinstance(it, ObstacleArea) and not isinstance(it, ConductionArea)
|
||||
]
|
||||
|
||||
def get_conduction_areas(self) -> list[ConductionArea]:
|
||||
return [it for it in self._items if isinstance(it, ConductionArea)]
|
||||
|
||||
def get_traces(self) -> list[Trace]:
|
||||
return [it for it in self._items if isinstance(it, Trace)]
|
||||
|
||||
@@ -31,7 +31,7 @@ from freeroute.geometry.polygon_shape import PolygonShape
|
||||
|
||||
from .board import BasicBoard
|
||||
from .clearance import ClearanceMatrix
|
||||
from .items import ObstacleArea, Pin
|
||||
from .items import ConductionArea, ObstacleArea, Pin
|
||||
from .layer import Layer, LayerStructure
|
||||
from .net import Nets
|
||||
from .transform import CoordinateTransform
|
||||
@@ -55,6 +55,7 @@ def build_board(dsn: DsnBoard) -> BasicBoard:
|
||||
_insert_pins(dsn, board, transform, terminal_to_net)
|
||||
_insert_keepouts(dsn, board, transform)
|
||||
_insert_image_keepouts(dsn, board, transform)
|
||||
_insert_zones(dsn, board, transform, nets)
|
||||
return board
|
||||
|
||||
|
||||
@@ -330,6 +331,8 @@ def _insert_keepouts(dsn: DsnBoard, board: BasicBoard, transform: CoordinateTran
|
||||
name=keepout.area.name,
|
||||
layer=layer_index,
|
||||
tiles=tiles,
|
||||
blocks_wires=keepout.kind != "via_keepout",
|
||||
blocks_vias=keepout.kind != "wire_keepout",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -402,13 +405,17 @@ def _insert_image_keepouts(dsn: DsnBoard, board: BasicBoard, transform: Coordina
|
||||
image = images.get(placement.lib_name)
|
||||
if image is None:
|
||||
continue
|
||||
areas = [*image.keepouts, *image.via_keepouts, *image.wire_keepouts]
|
||||
areas = [
|
||||
*((a, True, True) for a in image.keepouts),
|
||||
*((a, False, True) for a in image.via_keepouts),
|
||||
*((a, True, False) for a in image.wire_keepouts),
|
||||
]
|
||||
if not areas:
|
||||
continue
|
||||
for place in placement.places:
|
||||
if place.x is None or place.y is None:
|
||||
continue
|
||||
for area in areas:
|
||||
for area, blocks_wires, blocks_vias in areas:
|
||||
border = area.border
|
||||
if border is None:
|
||||
continue
|
||||
@@ -423,7 +430,12 @@ def _insert_image_keepouts(dsn: DsnBoard, board: BasicBoard, transform: Coordina
|
||||
top, bottom = min(signal_layers), max(signal_layers)
|
||||
layers = [top + bottom - layer for layer in layers]
|
||||
for layer_index in layers:
|
||||
board.insert(ObstacleArea(name=area.name, layer=layer_index, tiles=tiles))
|
||||
board.insert(
|
||||
ObstacleArea(
|
||||
name=area.name, layer=layer_index, tiles=tiles,
|
||||
blocks_wires=blocks_wires, blocks_vias=blocks_vias,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _placed_area_tiles(shape: Shape, place, transform: CoordinateTransform) -> list:
|
||||
@@ -447,3 +459,29 @@ def _placed_area_tiles(shape: Shape, place, transform: CoordinateTransform) -> l
|
||||
if len(pts) < 3:
|
||||
return []
|
||||
return [tile for tile in PolygonShape(pts).split_to_convex() if not tile.is_empty()]
|
||||
|
||||
|
||||
def _insert_zones(dsn: DsnBoard, board: BasicBoard, transform: CoordinateTransform, nets) -> None:
|
||||
"""Each DSN plane as a ConductionArea of its net on its layer."""
|
||||
layer_no = {layer.name: i for i, layer in enumerate(dsn.layers)}
|
||||
numbers = {net.name: net.net_number for net in nets}
|
||||
for plane in dsn.planes:
|
||||
shape = plane.shape
|
||||
if plane.net not in numbers or shape.layer not in layer_no:
|
||||
continue
|
||||
if not isinstance(shape, (Polygon, Path)):
|
||||
continue
|
||||
c = shape.coords
|
||||
pts = [transform.dsn_to_board_point(c[i], c[i + 1]) for i in range(0, len(c) - 1, 2)]
|
||||
if len(pts) < 3:
|
||||
continue
|
||||
polygon = PolygonShape(pts)
|
||||
tiles = [t for t in polygon.split_to_convex() if not t.is_empty()]
|
||||
if not tiles:
|
||||
continue
|
||||
board.insert(
|
||||
ConductionArea(
|
||||
net_nos=[numbers[plane.net]], name=plane.net, layer=layer_no[shape.layer],
|
||||
tiles=tiles, polygon=polygon, blocks_wires=False, blocks_vias=False,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -72,6 +72,10 @@ class ObstacleArea(Item):
|
||||
name: str | None = None
|
||||
layer: int = 0
|
||||
tiles: list = field(default_factory=list)
|
||||
#: a DSN keepout blocks both; wire_keepout only traces (KiCad's "no
|
||||
#: tracks" rule area, where vias are allowed), via_keepout only vias
|
||||
blocks_wires: bool = True
|
||||
blocks_vias: bool = True
|
||||
|
||||
def is_obstacle(self) -> bool:
|
||||
return True
|
||||
@@ -87,7 +91,16 @@ class ObstacleArea(Item):
|
||||
|
||||
@dataclass
|
||||
class ConductionArea(ObstacleArea):
|
||||
"""A power-plane conduction area (an obstacle carrying a net)."""
|
||||
"""A copper zone of one net (a DSN plane): not an obstacle to routing,
|
||||
since KiCad refills it around other nets' copper, but copper of its net:
|
||||
pads it touches are connected through it, and a route of its net ends on
|
||||
it by placing a via inside it."""
|
||||
|
||||
#: the zone outline, for point containment
|
||||
polygon: object = None
|
||||
|
||||
def is_obstacle(self) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -47,6 +47,9 @@ class TreeShape:
|
||||
routed: bool = False
|
||||
exact: Simplex | None = None
|
||||
seg: tuple | None = None # (a: IntPoint, b: IntPoint, half_width: int)
|
||||
#: which copper a keepout tile stops: "all", "wires" (a wire_keepout,
|
||||
#: vias allowed) or "vias" (a via_keepout)
|
||||
blocks: str = "all"
|
||||
|
||||
def on_layer(self, layer: int) -> bool:
|
||||
return layer in self.layers
|
||||
@@ -118,7 +121,9 @@ class ShapeSearchTree:
|
||||
result.append(shape)
|
||||
return result
|
||||
|
||||
def clearance_conflict(self, box: IntBox, net_no: int, layer: int, clearance: int) -> bool:
|
||||
def clearance_conflict(
|
||||
self, box: IntBox, net_no: int, layer: int, clearance: int, kind: str = "wire"
|
||||
) -> bool:
|
||||
"""True if placing ``box`` (of ``net_no`` on ``layer``) would come *closer
|
||||
than* ``clearance`` to a different net's tile on that layer (exact).
|
||||
|
||||
@@ -129,6 +134,8 @@ class ShapeSearchTree:
|
||||
for shape in self.overlapping(expanded):
|
||||
if shape.net_no == net_no or not shape.on_layer(layer):
|
||||
continue
|
||||
if shape.blocks != "all" and shape.blocks != kind + "s":
|
||||
continue # a wire-only keepout does not stop a via, and so on
|
||||
if shape.exact is not None:
|
||||
if overlaps_2d(shape.exact, expanded):
|
||||
return True
|
||||
|
||||
@@ -170,6 +170,14 @@ class NetClass:
|
||||
via_rule: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Plane:
|
||||
"""A ``(plane <net> <shape>)``: a copper zone of that net (KiCad's zones)."""
|
||||
|
||||
net: str
|
||||
shape: Shape
|
||||
|
||||
|
||||
@dataclass
|
||||
class Keepout:
|
||||
"""A structure-level keepout area with its kind."""
|
||||
@@ -187,6 +195,8 @@ class DsnBoard:
|
||||
resolution: Resolution = field(default_factory=Resolution)
|
||||
unit: str = "mil"
|
||||
layers: list[Layer] = field(default_factory=list)
|
||||
#: copper zones, as planes of a net
|
||||
planes: list[Plane] = field(default_factory=list)
|
||||
#: bounding-box boundary shape (layer ``pcb``), if present
|
||||
boundary: Shape | None = None
|
||||
#: outline shapes (layer ``signal``)
|
||||
|
||||
@@ -183,6 +183,11 @@ def _read_structure(node: SExp, board: DsnBoard) -> None:
|
||||
area = read_area(it)
|
||||
if area is not None:
|
||||
board.keepouts.append(Keepout(area=area, kind="wire_keepout"))
|
||||
elif head == "plane":
|
||||
# (plane <net> <shape>): KiCad exports each zone this way
|
||||
plane = _read_plane(it)
|
||||
if plane is not None:
|
||||
board.planes.append(plane)
|
||||
elif head == "place_keepout":
|
||||
area = read_area(it)
|
||||
if area is not None:
|
||||
@@ -513,3 +518,19 @@ def parse_dsn(text: str) -> DsnBoard:
|
||||
# `wiring` and unknown scopes are intentionally ignored for now.
|
||||
|
||||
return board
|
||||
|
||||
|
||||
def _read_plane(node: SExp):
|
||||
"""``(plane <net> (polygon <layer> <width> x y ...))`` -> :class:`Plane`."""
|
||||
from .model import Plane
|
||||
|
||||
items = node.items
|
||||
if not items or not isinstance(items[0], Token):
|
||||
return None
|
||||
net = items[0].text
|
||||
for sub in items[1:]:
|
||||
if isinstance(sub, SExp):
|
||||
shape = read_shape(sub)
|
||||
if shape is not None:
|
||||
return Plane(net=net, shape=shape)
|
||||
return None
|
||||
|
||||
@@ -143,10 +143,16 @@ def route_board_exact(
|
||||
for obstacle in board.get_obstacle_areas():
|
||||
if obstacle.layer not in signal_layers:
|
||||
continue
|
||||
blocks = (
|
||||
"all" if obstacle.blocks_wires and obstacle.blocks_vias
|
||||
else "wires" if obstacle.blocks_wires else "vias"
|
||||
)
|
||||
for tile in obstacle.tiles:
|
||||
box = tile.bounding_box()
|
||||
if not box.is_empty():
|
||||
tree.insert(TreeShape(next(owner), -1, frozenset({obstacle.layer}), box))
|
||||
tree.insert(
|
||||
TreeShape(next(owner), -1, frozenset({obstacle.layer}), box, blocks=blocks)
|
||||
)
|
||||
|
||||
via_layers = frozenset(signal_layers)
|
||||
accepted: dict[int, _Accepted] = {}
|
||||
@@ -155,9 +161,12 @@ def route_board_exact(
|
||||
# 25-pin supply net costs that one connection, not the whole net.
|
||||
for net_no, wires, vias in sorted(result.connections, key=lambda c: c[0]):
|
||||
boxes = _copper_boxes(wires, vias, half_width, via_layers, via_half)
|
||||
wire_count = len(_copper_boxes(wires, [], half_width, via_layers, via_half))
|
||||
if any(
|
||||
tree.clearance_conflict(box, net_no, layer, clearance)
|
||||
for box, box_layers in boxes
|
||||
tree.clearance_conflict(
|
||||
box, net_no, layer, clearance, "wire" if i < wire_count else "via"
|
||||
)
|
||||
for i, (box, box_layers) in enumerate(boxes)
|
||||
for layer in box_layers
|
||||
) or (outline is not None and not all(outline.box_clear(b, edge) for b, _ in boxes)):
|
||||
continue # exact-clearance conflict -> drop (may be recovered by shove)
|
||||
|
||||
@@ -30,7 +30,7 @@ import heapq
|
||||
import math
|
||||
|
||||
from freeroute.board.board import BasicBoard
|
||||
from freeroute.board.items import ObstacleArea, Pin
|
||||
from freeroute.board.items import ConductionArea, ObstacleArea, Pin
|
||||
from freeroute.geometry import IntBox, IntPoint
|
||||
|
||||
__all__ = ["GridRouter", "RouteResult", "route_board"]
|
||||
@@ -102,6 +102,10 @@ class _Connection:
|
||||
"via_cells",
|
||||
"via_halo",
|
||||
"halo_nodes",
|
||||
"a_group",
|
||||
"b_group",
|
||||
"a_end",
|
||||
"b_end",
|
||||
)
|
||||
|
||||
def __init__(self, index: int, net_no: int, a: Pin, b: Pin) -> None:
|
||||
@@ -117,6 +121,12 @@ class _Connection:
|
||||
self.via_cells: list[Cell] = []
|
||||
self.via_halo: list[Cell] = []
|
||||
self.halo_nodes: list[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], [])
|
||||
self.b_group: tuple[list[Pin], list] = ([b], [])
|
||||
self.a_end: Pin | None = a
|
||||
self.b_end: Pin | None = b
|
||||
|
||||
|
||||
class GridRouter:
|
||||
@@ -192,6 +202,8 @@ class GridRouter:
|
||||
self._pad_block: dict[Node, set[int]] = {}
|
||||
self._pad_own: dict[Node, set[int]] = {}
|
||||
self._keepout: set[Node] = set()
|
||||
self._via_keepout: set[Node] = set()
|
||||
self._zone_cells: dict[int, list[Node]] = {}
|
||||
# per-connection occupancy (owner = connection index)
|
||||
self._cell_owner: dict[Node, int] = {}
|
||||
self._via_owner: dict[Cell, int] = {}
|
||||
@@ -280,6 +292,8 @@ class GridRouter:
|
||||
if self._inside_own_pad(cell, item.shape):
|
||||
for layer in pin_layers:
|
||||
self._pad_own.setdefault((*cell, layer), set()).add(net)
|
||||
elif isinstance(item, ConductionArea):
|
||||
continue # a zone is copper of its net, not an obstacle
|
||||
elif isinstance(item, ObstacleArea):
|
||||
if item.layer not in self.layers:
|
||||
continue
|
||||
@@ -291,12 +305,21 @@ class GridRouter:
|
||||
continue
|
||||
# the same distance the exact engine verifies against
|
||||
for cell in self._cells_in_box(box, margin):
|
||||
self._keepout.add((*cell, item.layer))
|
||||
if item.blocks_wires:
|
||||
self._keepout.add((*cell, item.layer))
|
||||
if item.blocks_vias:
|
||||
self._via_keepout.add((*cell, item.layer))
|
||||
|
||||
# --- occupancy cost -----------------------------------------------------
|
||||
|
||||
def _enter_cost(
|
||||
self, cell: Cell, layer: int, net_no: int, allow_ripup: bool, conns: list[_Connection]
|
||||
self,
|
||||
cell: Cell,
|
||||
layer: int,
|
||||
net_no: int,
|
||||
allow_ripup: bool,
|
||||
conns: list[_Connection],
|
||||
wire_keepout: bool = True,
|
||||
) -> float | None:
|
||||
"""Cost to enter ``(cell, layer)`` for ``net_no``; ``None`` if blocked.
|
||||
|
||||
@@ -306,7 +329,7 @@ class GridRouter:
|
||||
already hit ``rip_cap`` (then it hardens into a wall).
|
||||
"""
|
||||
node = (*cell, layer)
|
||||
if node in self._keepout:
|
||||
if wire_keepout and node in self._keepout:
|
||||
return None
|
||||
pads = self._pad_block.get(node)
|
||||
if pads and any(n != net_no for n in pads) and net_no not in self._pad_own.get(node, ()):
|
||||
@@ -347,33 +370,38 @@ class GridRouter:
|
||||
"""A through via needs its whole footprint free of other nets on every layer."""
|
||||
if cell in self._via_edge_block:
|
||||
return False
|
||||
return all(
|
||||
self._enter_cost(c, layer, net_no, False, conns) == 0.0
|
||||
for c in self._via_cells(cell)
|
||||
for layer in self.layers
|
||||
)
|
||||
for c in self._via_cells(cell):
|
||||
for layer in self.layers:
|
||||
if (*c, layer) in self._via_keepout:
|
||||
return False
|
||||
if self._enter_cost(c, layer, net_no, False, conns, wire_keepout=False) != 0.0:
|
||||
return False
|
||||
return True
|
||||
|
||||
# --- A* search ----------------------------------------------------------
|
||||
|
||||
def _search(
|
||||
self, conn: _Connection, allow_ripup: bool, conns: list[_Connection]
|
||||
) -> list[Node] | None:
|
||||
goal_cell = self._cell_of(conn.b.location)
|
||||
starts = self._end_nodes(conn.a, conn.net_no, conns)
|
||||
goals = self._end_nodes(conn.b, conn.net_no, conns)
|
||||
goal_cell = self._group_target(conn.b_group)
|
||||
starts = self._group_ends(conn.a_group, conn.net_no, conns)
|
||||
goals = self._group_ends(conn.b_group, conn.net_no, conns)
|
||||
if not starts or not goals:
|
||||
return None
|
||||
if starts & goals:
|
||||
return [min(starts & goals)]
|
||||
common = sorted(set(starts) & set(goals))
|
||||
if common:
|
||||
conn.a_end, conn.b_end = starts[common[0]], goals[common[0]]
|
||||
return [common[0]]
|
||||
net_no = conn.net_no
|
||||
|
||||
open_heap: list[tuple[float, Node]] = []
|
||||
g_score: dict[Node, float] = {}
|
||||
came_from: dict[Node, Node] = {}
|
||||
# a stub costs its length, so the nearest cell wins unless it is blocked
|
||||
goal_stub = {n: self._stub_cost(conn.b, n) for n in goals}
|
||||
goal_stub = {n: self._stub_cost(p, n) for n, p in goals.items() if p is not None}
|
||||
for s in sorted(starts):
|
||||
g_score[s] = self._stub_cost(conn.a, s)
|
||||
pin = starts[s]
|
||||
g_score[s] = self._stub_cost(pin, s) if pin is not None 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
|
||||
@@ -397,7 +425,9 @@ class GridRouter:
|
||||
continue
|
||||
current = entry[1]
|
||||
if current in goals:
|
||||
return _reconstruct(came_from, current)
|
||||
path = _reconstruct(came_from, current)
|
||||
conn.a_end, conn.b_end = starts.get(path[0]), goals[current]
|
||||
return path
|
||||
cx, cy, cl = current
|
||||
base = g_score[current]
|
||||
for dx, dy in self._neighbours:
|
||||
@@ -405,7 +435,7 @@ class GridRouter:
|
||||
if not (0 <= cell[0] < self.cols and 0 <= cell[1] < self.rows):
|
||||
continue
|
||||
nxt = (*cell, cl)
|
||||
if nxt in goals:
|
||||
if nxt in goal_stub: # a pin end; a zone end is entered normally
|
||||
enter = goal_stub[nxt]
|
||||
else:
|
||||
enter = self._enter_cost(cell, cl, net_no, allow_ripup, conns)
|
||||
@@ -459,6 +489,40 @@ class GridRouter:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _zone_nodes(self, zone) -> list[Node]:
|
||||
"""Cells of a zone where a via of this net fits inside its copper."""
|
||||
key = id(zone)
|
||||
if key not in self._zone_cells:
|
||||
h = self.via_half
|
||||
nodes = []
|
||||
for tile in zone.tiles:
|
||||
for cell in self._cells_in_box(tile.bounding_box(), 0):
|
||||
c = self._cell_center(cell)
|
||||
if all(
|
||||
zone.polygon.contains(IntPoint(c.x + dx, c.y + dy))
|
||||
for dx in (-h, h) for dy in (-h, h)
|
||||
):
|
||||
nodes.append((*cell, zone.layer))
|
||||
self._zone_cells[key] = sorted(set(nodes))
|
||||
return self._zone_cells[key]
|
||||
|
||||
def _group_ends(self, group, net_no: int, conns) -> 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)):
|
||||
ends.setdefault(node, pin)
|
||||
for zone in group[1]:
|
||||
for node in self._zone_nodes(zone):
|
||||
ends.setdefault(node, None)
|
||||
return ends
|
||||
|
||||
def _group_target(self, group) -> Cell:
|
||||
if group[0]:
|
||||
return self._cell_of(group[0][0].location)
|
||||
box = group[1][0].bounding_box()
|
||||
return self._cell_of(IntPoint((box.ll.x + box.ur.x) // 2, (box.ll.y + box.ur.y) // 2))
|
||||
|
||||
def _stub_cost(self, pin: Pin, node: Node) -> float:
|
||||
c = self._cell_center(node[:2])
|
||||
dx, dy = abs(c.x - pin.location.x), abs(c.y - pin.location.y)
|
||||
@@ -523,8 +587,11 @@ class GridRouter:
|
||||
if c not in self._via_owner:
|
||||
self._via_owner[c] = conn.index
|
||||
conn.via_halo.append(c)
|
||||
for pin, (layer, cells) in ((conn.a, wire_segs[0]), (conn.b, wire_segs[-1])) if wire_segs else ():
|
||||
cell = cells[0] if pin is conn.a else cells[-1]
|
||||
ends = ((conn.a_end, wire_segs[0], 0), (conn.b_end, wire_segs[-1], -1)) if wire_segs else ()
|
||||
for pin, (layer, cells), at in ends:
|
||||
if pin is None:
|
||||
continue # ended on a zone: no stub
|
||||
cell = cells[at]
|
||||
for c in self._stub_cells(pin.location, cell):
|
||||
node = (*c, layer)
|
||||
if node not in self._cell_owner:
|
||||
@@ -533,10 +600,10 @@ class GridRouter:
|
||||
conn.wires = []
|
||||
for idx, (layer, cells) in enumerate(wire_segs):
|
||||
points = [self._cell_center(c) for c in cells]
|
||||
if idx == 0:
|
||||
points.insert(0, conn.a.location)
|
||||
if idx == len(wire_segs) - 1:
|
||||
points.append(conn.b.location)
|
||||
if idx == 0 and conn.a_end is not None:
|
||||
points.insert(0, conn.a_end.location)
|
||||
if idx == len(wire_segs) - 1 and conn.b_end is not None:
|
||||
points.append(conn.b_end.location)
|
||||
points = _simplify(points)
|
||||
if len(points) >= 2:
|
||||
conn.wires.append((layer, points))
|
||||
@@ -624,12 +691,19 @@ class GridRouter:
|
||||
|
||||
def _build_connections(self) -> list[_Connection]:
|
||||
pins_by_net = self._pins_by_net()
|
||||
zones_by_net: dict[int, list] = {}
|
||||
for zone in self.board.get_conduction_areas():
|
||||
if zone.layer in self.layers and zone.net_nos:
|
||||
zones_by_net.setdefault(zone.net_nos[0], []).append(zone)
|
||||
conns: list[_Connection] = []
|
||||
nets = set(pins_by_net) | set(zones_by_net)
|
||||
# deterministic: fewer-pin nets first, then by net number
|
||||
for net_no in sorted(pins_by_net, key=lambda n: (len(pins_by_net[n]), n)):
|
||||
pins = pins_by_net[net_no]
|
||||
for a, b in zip(pins, pins[1:], strict=False):
|
||||
conns.append(_Connection(len(conns), net_no, a, b))
|
||||
for net_no in sorted(nets, key=lambda n: (len(pins_by_net.get(n, [])), n)):
|
||||
groups = _connected_groups(pins_by_net.get(net_no, []), zones_by_net.get(net_no, []))
|
||||
for ga, gb in zip(groups, groups[1:], strict=False):
|
||||
conn = _Connection(len(conns), net_no, _first(ga[0]), _first(gb[0]))
|
||||
conn.a_group, conn.b_group = ga, gb
|
||||
conns.append(conn)
|
||||
return conns
|
||||
|
||||
def _pins_by_net(self) -> dict[int, list[Pin]]:
|
||||
@@ -660,6 +734,56 @@ class GridRouter:
|
||||
return result
|
||||
|
||||
|
||||
def _first(items):
|
||||
return items[0] if items else None
|
||||
|
||||
|
||||
def _pin_touches_zone(pin: Pin, zone) -> bool:
|
||||
"""A pad on the zone's layer whose centre or a corner lies in the zone."""
|
||||
if zone.layer not in pin.layers or zone.polygon is None:
|
||||
return False
|
||||
if zone.polygon.contains(pin.location):
|
||||
return True
|
||||
box = pin.shape.bounding_box() if pin.shape is not None else None
|
||||
if box is None:
|
||||
return False
|
||||
corners = (box.ll, IntPoint(box.ur.x, box.ll.y), box.ur, IntPoint(box.ll.x, box.ur.y))
|
||||
return any(zone.polygon.contains(c) for c in corners)
|
||||
|
||||
|
||||
def _connected_groups(pins: list[Pin], zones: list) -> list[tuple[list[Pin], list]]:
|
||||
"""Pins and zones of one net that copper already joins: a pad in a zone
|
||||
is connected to it, and two zones sharing a pad are one. Only the groups
|
||||
need routing between them. Pin groups come first, in pin order."""
|
||||
parent = list(range(len(pins) + len(zones)))
|
||||
|
||||
def find(i: int) -> int:
|
||||
while parent[i] != i:
|
||||
parent[i] = parent[parent[i]]
|
||||
i = parent[i]
|
||||
return i
|
||||
|
||||
for j, zone in enumerate(zones):
|
||||
for i, pin in enumerate(pins):
|
||||
if _pin_touches_zone(pin, zone):
|
||||
parent[find(i)] = find(len(pins) + j)
|
||||
groups: dict[int, tuple[list[Pin], list]] = {}
|
||||
order: list[int] = []
|
||||
for i, pin in enumerate(pins):
|
||||
root = find(i)
|
||||
if root not in groups:
|
||||
groups[root] = ([], [])
|
||||
order.append(root)
|
||||
groups[root][0].append(pin)
|
||||
for j, zone in enumerate(zones):
|
||||
root = find(len(pins) + j)
|
||||
if root not in groups:
|
||||
groups[root] = ([], [])
|
||||
order.append(root)
|
||||
groups[root][1].append(zone)
|
||||
return [groups[r] for r in order]
|
||||
|
||||
|
||||
def _split_path(path: list[Node]) -> tuple[list[tuple[int, list[Cell]]], list[Cell]]:
|
||||
"""Split a node path into per-layer wire segments and via cells."""
|
||||
wires: list[tuple[int, list[Cell]]] = []
|
||||
|
||||
@@ -94,3 +94,47 @@ def test_heuristic_weight_bounds_route_length():
|
||||
shortest = length(1.0)
|
||||
assert shortest > 0
|
||||
assert length(2.0) <= 2.0 * shortest
|
||||
|
||||
|
||||
# --- copper zones (DSN planes) -----------------------------------------------
|
||||
# rc4 power stage: pads inside "keep router off" pours connect through the pour,
|
||||
# and freeroute, not knowing zones, dropped every such connection.
|
||||
|
||||
_ZONE = "(plane N (polygon F.Cu 0 4000 -4000 10000 -4000 10000 -10000 4000 -10000 4000 -4000))"
|
||||
_NO_TRACKS = "(wire_keepout \"pour\" (polygon F.Cu 0 4000 -4000 10000 -4000 10000 -10000 4000 -10000))"
|
||||
|
||||
|
||||
def _zone_board():
|
||||
places = "(place P1 5500 -7000 front 0) (place P2 8500 -7000 front 0) (place P3 16000 -16000 front 0)"
|
||||
return parse_dsn(_dsn(f"(component P {places})", "(net N (pins P1-1 P2-1 P3-1))", _ZONE + " " + _NO_TRACKS))
|
||||
|
||||
|
||||
def test_planes_are_read_and_are_not_obstacles():
|
||||
dsn = _zone_board()
|
||||
assert [(p.net, p.shape.layer) for p in dsn.planes] == [("N", "F.Cu")]
|
||||
board = build_board(dsn)
|
||||
assert len(board.get_conduction_areas()) == 1
|
||||
assert all(not a.is_obstacle() or a.name == "pour" for a in board.get_obstacle_areas())
|
||||
|
||||
|
||||
def test_pads_in_one_zone_need_no_route_between_them():
|
||||
from freeroute.route.grid_router import _connected_groups
|
||||
|
||||
board = build_board(_zone_board())
|
||||
pins = sorted(board.get_pins(), key=lambda p: p.component_ref)
|
||||
groups = _connected_groups(pins, board.get_conduction_areas())
|
||||
assert [sorted(p.component_ref for p in g[0]) for g in groups] == [["P1", "P2"], ["P3"]]
|
||||
|
||||
|
||||
def test_a_pad_reaches_a_zone_under_a_no_tracks_area_by_a_via():
|
||||
dsn = _zone_board()
|
||||
result, scale, names = route_dsn_board_exact(dsn)
|
||||
net = next(iter(result.routed_net_numbers))
|
||||
assert result.result.vias.get(net) # it had to drop into the zone
|
||||
board = build_board(dsn)
|
||||
pour = next(a for a in board.get_obstacle_areas() if a.name == "pour")
|
||||
f_cu = names.index("F.Cu")
|
||||
for layer, pts in result.result.wires[net]:
|
||||
if layer == f_cu: # no track runs inside the no-tracks area
|
||||
assert not any(t.contains(p) and not t.bounding_box().is_empty()
|
||||
for t in pour.tiles for p in pts[1:-1])
|
||||
|
||||
Reference in New Issue
Block a user