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 freeroute.geometry import IntBox
|
||||||
|
|
||||||
from .clearance import ClearanceMatrix
|
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 .layer import LayerStructure
|
||||||
from .net import Nets
|
from .net import Nets
|
||||||
|
|
||||||
@@ -57,7 +57,15 @@ class BasicBoard:
|
|||||||
return [it for it in self._items if isinstance(it, Pin)]
|
return [it for it in self._items if isinstance(it, Pin)]
|
||||||
|
|
||||||
def get_obstacle_areas(self) -> list[ObstacleArea]:
|
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]:
|
def get_traces(self) -> list[Trace]:
|
||||||
return [it for it in self._items if isinstance(it, 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 .board import BasicBoard
|
||||||
from .clearance import ClearanceMatrix
|
from .clearance import ClearanceMatrix
|
||||||
from .items import ObstacleArea, Pin
|
from .items import ConductionArea, ObstacleArea, Pin
|
||||||
from .layer import Layer, LayerStructure
|
from .layer import Layer, LayerStructure
|
||||||
from .net import Nets
|
from .net import Nets
|
||||||
from .transform import CoordinateTransform
|
from .transform import CoordinateTransform
|
||||||
@@ -55,6 +55,7 @@ def build_board(dsn: DsnBoard) -> BasicBoard:
|
|||||||
_insert_pins(dsn, board, transform, terminal_to_net)
|
_insert_pins(dsn, board, transform, terminal_to_net)
|
||||||
_insert_keepouts(dsn, board, transform)
|
_insert_keepouts(dsn, board, transform)
|
||||||
_insert_image_keepouts(dsn, board, transform)
|
_insert_image_keepouts(dsn, board, transform)
|
||||||
|
_insert_zones(dsn, board, transform, nets)
|
||||||
return board
|
return board
|
||||||
|
|
||||||
|
|
||||||
@@ -330,6 +331,8 @@ def _insert_keepouts(dsn: DsnBoard, board: BasicBoard, transform: CoordinateTran
|
|||||||
name=keepout.area.name,
|
name=keepout.area.name,
|
||||||
layer=layer_index,
|
layer=layer_index,
|
||||||
tiles=tiles,
|
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)
|
image = images.get(placement.lib_name)
|
||||||
if image is None:
|
if image is None:
|
||||||
continue
|
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:
|
if not areas:
|
||||||
continue
|
continue
|
||||||
for place in placement.places:
|
for place in placement.places:
|
||||||
if place.x is None or place.y is None:
|
if place.x is None or place.y is None:
|
||||||
continue
|
continue
|
||||||
for area in areas:
|
for area, blocks_wires, blocks_vias in areas:
|
||||||
border = area.border
|
border = area.border
|
||||||
if border is None:
|
if border is None:
|
||||||
continue
|
continue
|
||||||
@@ -423,7 +430,12 @@ def _insert_image_keepouts(dsn: DsnBoard, board: BasicBoard, transform: Coordina
|
|||||||
top, bottom = min(signal_layers), max(signal_layers)
|
top, bottom = min(signal_layers), max(signal_layers)
|
||||||
layers = [top + bottom - layer for layer in layers]
|
layers = [top + bottom - layer for layer in layers]
|
||||||
for layer_index 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:
|
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:
|
if len(pts) < 3:
|
||||||
return []
|
return []
|
||||||
return [tile for tile in PolygonShape(pts).split_to_convex() if not tile.is_empty()]
|
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
|
name: str | None = None
|
||||||
layer: int = 0
|
layer: int = 0
|
||||||
tiles: list = field(default_factory=list)
|
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:
|
def is_obstacle(self) -> bool:
|
||||||
return True
|
return True
|
||||||
@@ -87,7 +91,16 @@ class ObstacleArea(Item):
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ConductionArea(ObstacleArea):
|
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
|
@dataclass
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ class TreeShape:
|
|||||||
routed: bool = False
|
routed: bool = False
|
||||||
exact: Simplex | None = None
|
exact: Simplex | None = None
|
||||||
seg: tuple | None = None # (a: IntPoint, b: IntPoint, half_width: int)
|
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:
|
def on_layer(self, layer: int) -> bool:
|
||||||
return layer in self.layers
|
return layer in self.layers
|
||||||
@@ -118,7 +121,9 @@ class ShapeSearchTree:
|
|||||||
result.append(shape)
|
result.append(shape)
|
||||||
return result
|
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
|
"""True if placing ``box`` (of ``net_no`` on ``layer``) would come *closer
|
||||||
than* ``clearance`` to a different net's tile on that layer (exact).
|
than* ``clearance`` to a different net's tile on that layer (exact).
|
||||||
|
|
||||||
@@ -129,6 +134,8 @@ class ShapeSearchTree:
|
|||||||
for shape in self.overlapping(expanded):
|
for shape in self.overlapping(expanded):
|
||||||
if shape.net_no == net_no or not shape.on_layer(layer):
|
if shape.net_no == net_no or not shape.on_layer(layer):
|
||||||
continue
|
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 shape.exact is not None:
|
||||||
if overlaps_2d(shape.exact, expanded):
|
if overlaps_2d(shape.exact, expanded):
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -170,6 +170,14 @@ class NetClass:
|
|||||||
via_rule: str | None = None
|
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
|
@dataclass
|
||||||
class Keepout:
|
class Keepout:
|
||||||
"""A structure-level keepout area with its kind."""
|
"""A structure-level keepout area with its kind."""
|
||||||
@@ -187,6 +195,8 @@ class DsnBoard:
|
|||||||
resolution: Resolution = field(default_factory=Resolution)
|
resolution: Resolution = field(default_factory=Resolution)
|
||||||
unit: str = "mil"
|
unit: str = "mil"
|
||||||
layers: list[Layer] = field(default_factory=list)
|
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
|
#: bounding-box boundary shape (layer ``pcb``), if present
|
||||||
boundary: Shape | None = None
|
boundary: Shape | None = None
|
||||||
#: outline shapes (layer ``signal``)
|
#: outline shapes (layer ``signal``)
|
||||||
|
|||||||
@@ -183,6 +183,11 @@ def _read_structure(node: SExp, board: DsnBoard) -> None:
|
|||||||
area = read_area(it)
|
area = read_area(it)
|
||||||
if area is not None:
|
if area is not None:
|
||||||
board.keepouts.append(Keepout(area=area, kind="wire_keepout"))
|
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":
|
elif head == "place_keepout":
|
||||||
area = read_area(it)
|
area = read_area(it)
|
||||||
if area is not None:
|
if area is not None:
|
||||||
@@ -513,3 +518,19 @@ def parse_dsn(text: str) -> DsnBoard:
|
|||||||
# `wiring` and unknown scopes are intentionally ignored for now.
|
# `wiring` and unknown scopes are intentionally ignored for now.
|
||||||
|
|
||||||
return board
|
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():
|
for obstacle in board.get_obstacle_areas():
|
||||||
if obstacle.layer not in signal_layers:
|
if obstacle.layer not in signal_layers:
|
||||||
continue
|
continue
|
||||||
|
blocks = (
|
||||||
|
"all" if obstacle.blocks_wires and obstacle.blocks_vias
|
||||||
|
else "wires" if obstacle.blocks_wires else "vias"
|
||||||
|
)
|
||||||
for tile in obstacle.tiles:
|
for tile in obstacle.tiles:
|
||||||
box = tile.bounding_box()
|
box = tile.bounding_box()
|
||||||
if not box.is_empty():
|
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)
|
via_layers = frozenset(signal_layers)
|
||||||
accepted: dict[int, _Accepted] = {}
|
accepted: dict[int, _Accepted] = {}
|
||||||
@@ -155,9 +161,12 @@ def route_board_exact(
|
|||||||
# 25-pin supply net costs that one connection, not the whole net.
|
# 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]):
|
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)
|
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(
|
if any(
|
||||||
tree.clearance_conflict(box, net_no, layer, clearance)
|
tree.clearance_conflict(
|
||||||
for box, box_layers in boxes
|
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
|
for layer in box_layers
|
||||||
) or (outline is not None and not all(outline.box_clear(b, edge) for b, _ in boxes)):
|
) 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)
|
continue # exact-clearance conflict -> drop (may be recovered by shove)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import heapq
|
|||||||
import math
|
import math
|
||||||
|
|
||||||
from freeroute.board.board import BasicBoard
|
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
|
from freeroute.geometry import IntBox, IntPoint
|
||||||
|
|
||||||
__all__ = ["GridRouter", "RouteResult", "route_board"]
|
__all__ = ["GridRouter", "RouteResult", "route_board"]
|
||||||
@@ -102,6 +102,10 @@ class _Connection:
|
|||||||
"via_cells",
|
"via_cells",
|
||||||
"via_halo",
|
"via_halo",
|
||||||
"halo_nodes",
|
"halo_nodes",
|
||||||
|
"a_group",
|
||||||
|
"b_group",
|
||||||
|
"a_end",
|
||||||
|
"b_end",
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, index: int, net_no: int, a: Pin, b: Pin) -> None:
|
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_cells: list[Cell] = []
|
||||||
self.via_halo: list[Cell] = []
|
self.via_halo: list[Cell] = []
|
||||||
self.halo_nodes: list[Node] = []
|
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:
|
class GridRouter:
|
||||||
@@ -192,6 +202,8 @@ class GridRouter:
|
|||||||
self._pad_block: dict[Node, set[int]] = {}
|
self._pad_block: dict[Node, set[int]] = {}
|
||||||
self._pad_own: dict[Node, set[int]] = {}
|
self._pad_own: dict[Node, set[int]] = {}
|
||||||
self._keepout: set[Node] = set()
|
self._keepout: set[Node] = set()
|
||||||
|
self._via_keepout: set[Node] = set()
|
||||||
|
self._zone_cells: dict[int, list[Node]] = {}
|
||||||
# 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] = {}
|
||||||
@@ -280,6 +292,8 @@ class GridRouter:
|
|||||||
if self._inside_own_pad(cell, item.shape):
|
if self._inside_own_pad(cell, item.shape):
|
||||||
for layer in pin_layers:
|
for layer in pin_layers:
|
||||||
self._pad_own.setdefault((*cell, layer), set()).add(net)
|
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):
|
elif isinstance(item, ObstacleArea):
|
||||||
if item.layer not in self.layers:
|
if item.layer not in self.layers:
|
||||||
continue
|
continue
|
||||||
@@ -291,12 +305,21 @@ class GridRouter:
|
|||||||
continue
|
continue
|
||||||
# the same distance the exact engine verifies against
|
# the same distance the exact engine verifies against
|
||||||
for cell in self._cells_in_box(box, margin):
|
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 -----------------------------------------------------
|
# --- occupancy cost -----------------------------------------------------
|
||||||
|
|
||||||
def _enter_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:
|
) -> float | None:
|
||||||
"""Cost to enter ``(cell, layer)`` for ``net_no``; ``None`` if blocked.
|
"""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).
|
already hit ``rip_cap`` (then it hardens into a wall).
|
||||||
"""
|
"""
|
||||||
node = (*cell, layer)
|
node = (*cell, layer)
|
||||||
if node in self._keepout:
|
if wire_keepout and node in self._keepout:
|
||||||
return None
|
return None
|
||||||
pads = self._pad_block.get(node)
|
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, ()):
|
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."""
|
"""A through via needs its whole footprint free of other nets on every layer."""
|
||||||
if cell in self._via_edge_block:
|
if cell in self._via_edge_block:
|
||||||
return False
|
return False
|
||||||
return all(
|
for c in self._via_cells(cell):
|
||||||
self._enter_cost(c, layer, net_no, False, conns) == 0.0
|
for layer in self.layers:
|
||||||
for c in self._via_cells(cell)
|
if (*c, layer) in self._via_keepout:
|
||||||
for layer in self.layers
|
return False
|
||||||
)
|
if self._enter_cost(c, layer, net_no, False, conns, wire_keepout=False) != 0.0:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
# --- A* search ----------------------------------------------------------
|
# --- A* search ----------------------------------------------------------
|
||||||
|
|
||||||
def _search(
|
def _search(
|
||||||
self, conn: _Connection, allow_ripup: bool, conns: list[_Connection]
|
self, conn: _Connection, allow_ripup: bool, conns: list[_Connection]
|
||||||
) -> list[Node] | None:
|
) -> list[Node] | None:
|
||||||
goal_cell = self._cell_of(conn.b.location)
|
goal_cell = self._group_target(conn.b_group)
|
||||||
starts = self._end_nodes(conn.a, conn.net_no, conns)
|
starts = self._group_ends(conn.a_group, conn.net_no, conns)
|
||||||
goals = self._end_nodes(conn.b, conn.net_no, conns)
|
goals = self._group_ends(conn.b_group, conn.net_no, conns)
|
||||||
if not starts or not goals:
|
if not starts or not goals:
|
||||||
return None
|
return None
|
||||||
if starts & goals:
|
common = sorted(set(starts) & set(goals))
|
||||||
return [min(starts & goals)]
|
if common:
|
||||||
|
conn.a_end, conn.b_end = starts[common[0]], goals[common[0]]
|
||||||
|
return [common[0]]
|
||||||
net_no = conn.net_no
|
net_no = conn.net_no
|
||||||
|
|
||||||
open_heap: list[tuple[float, Node]] = []
|
open_heap: list[tuple[float, Node]] = []
|
||||||
g_score: dict[Node, float] = {}
|
g_score: dict[Node, float] = {}
|
||||||
came_from: dict[Node, Node] = {}
|
came_from: dict[Node, Node] = {}
|
||||||
# a stub costs its length, so the nearest cell wins unless it is blocked
|
# 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):
|
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))
|
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
|
# Via footprints are checked lazily: a layer change goes on the heap as
|
||||||
@@ -397,7 +425,9 @@ class GridRouter:
|
|||||||
continue
|
continue
|
||||||
current = entry[1]
|
current = entry[1]
|
||||||
if current in goals:
|
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
|
cx, cy, cl = current
|
||||||
base = g_score[current]
|
base = g_score[current]
|
||||||
for dx, dy in self._neighbours:
|
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):
|
if not (0 <= cell[0] < self.cols and 0 <= cell[1] < self.rows):
|
||||||
continue
|
continue
|
||||||
nxt = (*cell, cl)
|
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]
|
enter = goal_stub[nxt]
|
||||||
else:
|
else:
|
||||||
enter = self._enter_cost(cell, cl, net_no, allow_ripup, conns)
|
enter = self._enter_cost(cell, cl, net_no, allow_ripup, conns)
|
||||||
@@ -459,6 +489,40 @@ class GridRouter:
|
|||||||
return False
|
return False
|
||||||
return True
|
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:
|
def _stub_cost(self, pin: Pin, node: Node) -> float:
|
||||||
c = self._cell_center(node[:2])
|
c = self._cell_center(node[:2])
|
||||||
dx, dy = abs(c.x - pin.location.x), abs(c.y - pin.location.y)
|
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:
|
if c not in self._via_owner:
|
||||||
self._via_owner[c] = conn.index
|
self._via_owner[c] = conn.index
|
||||||
conn.via_halo.append(c)
|
conn.via_halo.append(c)
|
||||||
for pin, (layer, cells) in ((conn.a, wire_segs[0]), (conn.b, wire_segs[-1])) if wire_segs else ():
|
ends = ((conn.a_end, wire_segs[0], 0), (conn.b_end, wire_segs[-1], -1)) if wire_segs else ()
|
||||||
cell = cells[0] if pin is conn.a else cells[-1]
|
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):
|
for c in self._stub_cells(pin.location, cell):
|
||||||
node = (*c, layer)
|
node = (*c, layer)
|
||||||
if node not in self._cell_owner:
|
if node not in self._cell_owner:
|
||||||
@@ -533,10 +600,10 @@ class GridRouter:
|
|||||||
conn.wires = []
|
conn.wires = []
|
||||||
for idx, (layer, cells) in enumerate(wire_segs):
|
for idx, (layer, cells) in enumerate(wire_segs):
|
||||||
points = [self._cell_center(c) for c in cells]
|
points = [self._cell_center(c) for c in cells]
|
||||||
if idx == 0:
|
if idx == 0 and conn.a_end is not None:
|
||||||
points.insert(0, conn.a.location)
|
points.insert(0, conn.a_end.location)
|
||||||
if idx == len(wire_segs) - 1:
|
if idx == len(wire_segs) - 1 and conn.b_end is not None:
|
||||||
points.append(conn.b.location)
|
points.append(conn.b_end.location)
|
||||||
points = _simplify(points)
|
points = _simplify(points)
|
||||||
if len(points) >= 2:
|
if len(points) >= 2:
|
||||||
conn.wires.append((layer, points))
|
conn.wires.append((layer, points))
|
||||||
@@ -624,12 +691,19 @@ class GridRouter:
|
|||||||
|
|
||||||
def _build_connections(self) -> list[_Connection]:
|
def _build_connections(self) -> list[_Connection]:
|
||||||
pins_by_net = self._pins_by_net()
|
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] = []
|
conns: list[_Connection] = []
|
||||||
|
nets = set(pins_by_net) | set(zones_by_net)
|
||||||
# deterministic: fewer-pin nets first, then by net number
|
# 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)):
|
for net_no in sorted(nets, key=lambda n: (len(pins_by_net.get(n, [])), n)):
|
||||||
pins = pins_by_net[net_no]
|
groups = _connected_groups(pins_by_net.get(net_no, []), zones_by_net.get(net_no, []))
|
||||||
for a, b in zip(pins, pins[1:], strict=False):
|
for ga, gb in zip(groups, groups[1:], strict=False):
|
||||||
conns.append(_Connection(len(conns), net_no, a, b))
|
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
|
return conns
|
||||||
|
|
||||||
def _pins_by_net(self) -> dict[int, list[Pin]]:
|
def _pins_by_net(self) -> dict[int, list[Pin]]:
|
||||||
@@ -660,6 +734,56 @@ class GridRouter:
|
|||||||
return result
|
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]]:
|
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."""
|
"""Split a node path into per-layer wire segments and via cells."""
|
||||||
wires: list[tuple[int, list[Cell]]] = []
|
wires: list[tuple[int, list[Cell]]] = []
|
||||||
|
|||||||
@@ -94,3 +94,47 @@ def test_heuristic_weight_bounds_route_length():
|
|||||||
shortest = length(1.0)
|
shortest = length(1.0)
|
||||||
assert shortest > 0
|
assert shortest > 0
|
||||||
assert length(2.0) <= 2.0 * shortest
|
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