Read the DSN's wiring as fixed copper of its nets
Tracks and vias already on the board were ignored, so other nets could be routed straight through them (the flight controller's 82 GND stitching vias and its guard tracks). They are now copper of their net: other nets keep a pad's distance from them in the grid, the exact check and the room engine, and they add no connections to route.
This commit is contained in:
@@ -70,6 +70,27 @@ class BasicBoard:
|
|||||||
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)]
|
||||||
|
|
||||||
|
def get_fixed_copper(self) -> list[tuple[int, frozenset[int], IntBox]]:
|
||||||
|
"""Copper already routed on the board (the DSN's ``wiring``), as
|
||||||
|
``(net, layers, box)``. A track is its segments, its corners and the
|
||||||
|
round caps at its ends (as squares); a via is its largest pad."""
|
||||||
|
from freeroute.geometry.polyline import Polyline, PolylineShape, corner_box
|
||||||
|
|
||||||
|
out: list[tuple[int, frozenset[int], IntBox]] = []
|
||||||
|
for item in self._items:
|
||||||
|
if not item.fixed or not item.net_nos:
|
||||||
|
continue
|
||||||
|
net = item.net_nos[0]
|
||||||
|
if isinstance(item, Trace) and not item.is_empty():
|
||||||
|
layers = frozenset({item.layer})
|
||||||
|
for box in PolylineShape(Polyline(item.corners), item.half_width).tiles():
|
||||||
|
out.append((net, layers, box))
|
||||||
|
for end in (item.corners[0], item.corners[-1]):
|
||||||
|
out.append((net, layers, corner_box(end, item.half_width)))
|
||||||
|
elif isinstance(item, Via) and item.shape is not None:
|
||||||
|
out.append((net, frozenset(item.layers), item.shape.bounding_box()))
|
||||||
|
return out
|
||||||
|
|
||||||
def get_vias(self) -> list[Via]:
|
def get_vias(self) -> list[Via]:
|
||||||
return [it for it in self._items if isinstance(it, Via)]
|
return [it for it in self._items if isinstance(it, Via)]
|
||||||
|
|
||||||
|
|||||||
@@ -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 ConductionArea, ObstacleArea, Pin
|
from .items import ConductionArea, ObstacleArea, Pin, Trace, Via
|
||||||
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
|
||||||
@@ -56,6 +56,7 @@ def build_board(dsn: DsnBoard) -> BasicBoard:
|
|||||||
_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)
|
_insert_zones(dsn, board, transform, nets)
|
||||||
|
_insert_wiring(dsn, board, transform, nets)
|
||||||
return board
|
return board
|
||||||
|
|
||||||
|
|
||||||
@@ -471,6 +472,47 @@ def _placed_area_tiles(shape: Shape, place, transform: CoordinateTransform) -> l
|
|||||||
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_wiring(dsn: DsnBoard, board: BasicBoard, transform: CoordinateTransform, nets) -> None:
|
||||||
|
"""Tracks and vias already on the board, as fixed copper of their nets."""
|
||||||
|
layer_no = {layer.name: i for i, layer in enumerate(dsn.layers)}
|
||||||
|
signal_layers = [i for i, layer in enumerate(dsn.layers) if layer.is_signal]
|
||||||
|
numbers = {net.name: net.net_number for net in nets}
|
||||||
|
for wire in dsn.wires:
|
||||||
|
if wire.net not in numbers or wire.layer not in layer_no:
|
||||||
|
continue
|
||||||
|
c = wire.coords
|
||||||
|
corners = [transform.dsn_to_board_point(c[i], c[i + 1]) for i in range(0, len(c) - 1, 2)]
|
||||||
|
if len(corners) < 2:
|
||||||
|
continue
|
||||||
|
board.insert(
|
||||||
|
Trace(
|
||||||
|
net_nos=[numbers[wire.net]], fixed=True, layer=layer_no[wire.layer],
|
||||||
|
half_width=max(round(transform.dsn_to_board(wire.width / 2)), 1), corners=corners,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for via in dsn.vias:
|
||||||
|
padstack = dsn.padstack(via.padstack)
|
||||||
|
if via.net not in numbers or padstack is None or not padstack.shapes:
|
||||||
|
continue
|
||||||
|
layers: set[int] = set()
|
||||||
|
for shape in padstack.shapes:
|
||||||
|
if shape.layer in ("signal", "pcb"):
|
||||||
|
layers.update(signal_layers)
|
||||||
|
elif shape.layer in layer_no:
|
||||||
|
layers.add(layer_no[shape.layer])
|
||||||
|
location = transform.dsn_to_board_point(via.x, via.y)
|
||||||
|
pad = _pad_tile(max(padstack.shapes, key=_shape_extent), transform)
|
||||||
|
box = pad.bounding_box()
|
||||||
|
board.insert(
|
||||||
|
Via(
|
||||||
|
net_nos=[numbers[via.net]], fixed=True, padstack_name=via.padstack,
|
||||||
|
location=location, layers=sorted(layers or signal_layers),
|
||||||
|
shape=IntBox(box.ll.x + location.x, box.ll.y + location.y,
|
||||||
|
box.ur.x + location.x, box.ur.y + location.y),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _insert_zones(dsn: DsnBoard, board: BasicBoard, transform: CoordinateTransform, nets) -> None:
|
def _insert_zones(dsn: DsnBoard, board: BasicBoard, transform: CoordinateTransform, nets) -> None:
|
||||||
"""Each DSN plane as a ConductionArea of its net on its layer."""
|
"""Each DSN plane as a ConductionArea of its net on its layer."""
|
||||||
layer_no = {layer.name: i for i, layer in enumerate(dsn.layers)}
|
layer_no = {layer.name: i for i, layer in enumerate(dsn.layers)}
|
||||||
|
|||||||
@@ -114,6 +114,9 @@ class Via(Item):
|
|||||||
|
|
||||||
padstack_name: str = ""
|
padstack_name: str = ""
|
||||||
location: IntPoint = field(default_factory=lambda: IntPoint(0, 0))
|
location: IntPoint = field(default_factory=lambda: IntPoint(0, 0))
|
||||||
|
#: layer indices the via's pads occupy, and the largest pad as a box
|
||||||
|
layers: list[int] = field(default_factory=list)
|
||||||
|
shape: object = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ __all__ = [
|
|||||||
"NetPin",
|
"NetPin",
|
||||||
"Net",
|
"Net",
|
||||||
"NetClass",
|
"NetClass",
|
||||||
|
"WiringVia",
|
||||||
|
"WiringWire",
|
||||||
"Keepout",
|
"Keepout",
|
||||||
"DsnBoard",
|
"DsnBoard",
|
||||||
]
|
]
|
||||||
@@ -186,6 +188,26 @@ class Keepout:
|
|||||||
kind: str = "keepout" # keepout | via_keepout | wire_keepout | place_keepout
|
kind: str = "keepout" # keepout | via_keepout | wire_keepout | place_keepout
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WiringWire:
|
||||||
|
"""A track already on the board: ``(wire (path LAYER WIDTH x y ...) (net N))``."""
|
||||||
|
|
||||||
|
net: str
|
||||||
|
layer: str
|
||||||
|
width: float
|
||||||
|
coords: list[float] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WiringVia:
|
||||||
|
"""A via already on the board: ``(via PADSTACK x y (net N))``."""
|
||||||
|
|
||||||
|
net: str
|
||||||
|
padstack: str
|
||||||
|
x: float
|
||||||
|
y: float
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class DsnBoard:
|
class DsnBoard:
|
||||||
"""The fully parsed DSN file."""
|
"""The fully parsed DSN file."""
|
||||||
@@ -197,6 +219,9 @@ class DsnBoard:
|
|||||||
layers: list[Layer] = field(default_factory=list)
|
layers: list[Layer] = field(default_factory=list)
|
||||||
#: copper zones, as planes of a net
|
#: copper zones, as planes of a net
|
||||||
planes: list[Plane] = field(default_factory=list)
|
planes: list[Plane] = field(default_factory=list)
|
||||||
|
#: copper already routed (the ``wiring`` scope), kept fixed
|
||||||
|
wires: list[WiringWire] = field(default_factory=list)
|
||||||
|
vias: list[WiringVia] = 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``)
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ from .model import (
|
|||||||
Pin,
|
Pin,
|
||||||
Resolution,
|
Resolution,
|
||||||
WidthRule,
|
WidthRule,
|
||||||
|
WiringVia,
|
||||||
|
WiringWire,
|
||||||
)
|
)
|
||||||
from .sexp import SExp, parse
|
from .sexp import SExp, parse
|
||||||
from .shapes import Shape, read_area, read_shape
|
from .shapes import Shape, read_area, read_shape
|
||||||
@@ -531,11 +533,45 @@ def parse_dsn(text: str) -> DsnBoard:
|
|||||||
_read_placement(it, board)
|
_read_placement(it, board)
|
||||||
elif head == "network":
|
elif head == "network":
|
||||||
_read_network(it, board)
|
_read_network(it, board)
|
||||||
# `wiring` and unknown scopes are intentionally ignored for now.
|
elif head == "wiring":
|
||||||
|
_read_wiring(it, board)
|
||||||
|
# unknown scopes are ignored
|
||||||
|
|
||||||
return board
|
return board
|
||||||
|
|
||||||
|
|
||||||
|
def _read_wiring(node: SExp, board: DsnBoard) -> None:
|
||||||
|
"""Tracks and vias already on the board. They stay where they are: the
|
||||||
|
router treats them as copper of their net that other nets keep clear of."""
|
||||||
|
for it in node.items:
|
||||||
|
if not isinstance(it, SExp):
|
||||||
|
continue
|
||||||
|
net_scope = it.child("net")
|
||||||
|
net = _first_value(net_scope) if net_scope is not None else None
|
||||||
|
if not net:
|
||||||
|
continue # netless copper would need a keepout, not a wire
|
||||||
|
if it.head == "wire":
|
||||||
|
path = it.child("path") or it.child("polyline_path")
|
||||||
|
vals = [v.text for v in path.values()] if path is not None else []
|
||||||
|
if len(vals) < 6:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
width = float(vals[1])
|
||||||
|
coords = [float(v) for v in vals[2:]]
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
board.wires.append(WiringWire(net=net, layer=vals[0], width=width, coords=coords))
|
||||||
|
elif it.head == "via":
|
||||||
|
vals = [v.text for v in it.values()]
|
||||||
|
if len(vals) < 3:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
x, y = float(vals[1]), float(vals[2])
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
board.vias.append(WiringVia(net=net, padstack=vals[0], x=x, y=y))
|
||||||
|
|
||||||
|
|
||||||
def _read_plane(node: SExp):
|
def _read_plane(node: SExp):
|
||||||
"""``(plane <net> (polygon <layer> <width> x y ...))`` -> :class:`Plane`."""
|
"""``(plane <net> (polygon <layer> <width> x y ...))`` -> :class:`Plane`."""
|
||||||
from .model import Plane
|
from .model import Plane
|
||||||
|
|||||||
@@ -149,6 +149,10 @@ def route_board_exact(
|
|||||||
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))
|
tree.insert(TreeShape(next(owner), net, pin_layers, box))
|
||||||
|
for net, copper_layers, box in board.get_fixed_copper():
|
||||||
|
on = frozenset(layer for layer in copper_layers if layer in signal_layers)
|
||||||
|
if on and not box.is_empty():
|
||||||
|
tree.insert(TreeShape(next(owner), net, on, box))
|
||||||
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
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ class GridRouter:
|
|||||||
# 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()
|
||||||
|
self._rasterize_fixed_copper()
|
||||||
|
|
||||||
# --- grid helpers -------------------------------------------------------
|
# --- grid helpers -------------------------------------------------------
|
||||||
|
|
||||||
@@ -360,6 +361,16 @@ class GridRouter:
|
|||||||
if item.blocks_vias:
|
if item.blocks_vias:
|
||||||
self._via_keepout.add((*cell, item.layer))
|
self._via_keepout.add((*cell, item.layer))
|
||||||
|
|
||||||
|
def _rasterize_fixed_copper(self) -> None:
|
||||||
|
"""Tracks and vias already on the board: copper of their net, which
|
||||||
|
other nets keep the same distance from as from a pad."""
|
||||||
|
margin = self.clearance + self.half_width
|
||||||
|
for net, layers, box in self.board.get_fixed_copper():
|
||||||
|
on = [layer for layer in layers if layer in self.layers]
|
||||||
|
for cell in self._cells_in_box(box, margin):
|
||||||
|
for layer in on:
|
||||||
|
self._pad_block.setdefault((*cell, layer), set()).add(net)
|
||||||
|
|
||||||
# --- occupancy cost -----------------------------------------------------
|
# --- occupancy cost -----------------------------------------------------
|
||||||
|
|
||||||
def _enter_cost(
|
def _enter_cost(
|
||||||
|
|||||||
@@ -515,6 +515,10 @@ def route_board_rooms(
|
|||||||
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))
|
tree.insert(TreeShape(next(owner), net, pin_layers, box))
|
||||||
|
for net, copper_layers, box in board.get_fixed_copper():
|
||||||
|
on = frozenset(layer for layer in copper_layers if layer in signal_layers)
|
||||||
|
if on and not box.is_empty():
|
||||||
|
tree.insert(TreeShape(next(owner), net, on, box))
|
||||||
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
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"""Tracks and vias already on the board (the DSN's wiring) are fixed copper."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from freeroute.board import build_board
|
||||||
|
from freeroute.dsn import parse_dsn
|
||||||
|
from freeroute.route.pipeline import route_dsn_board_exact
|
||||||
|
|
||||||
|
_DSN = """(pcb wired (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:300_um") (rule (width 250) (clearance 200)))
|
||||||
|
(placement (component S (place A1 5000 -10000 front 0) (place A2 25000 -10000 front 0)))
|
||||||
|
(library
|
||||||
|
(image S (pin smd 1 0 0))
|
||||||
|
(padstack smd (shape (rect F.Cu -400 -400 400 400)) (attach off))
|
||||||
|
(padstack "Via[0-1]_600:300_um" (shape (circle F.Cu 600)) (shape (circle B.Cu 600)) (attach off)))
|
||||||
|
(network (net SIG (pins A1-1 A2-1)) (net GND))
|
||||||
|
(wiring
|
||||||
|
(wire (path F.Cu 400 15000 0 15000 -20000) (net GND) (type route))
|
||||||
|
(via "Via[0-1]_600:300_um" 8000 -14000 (net GND) (type route))))"""
|
||||||
|
|
||||||
|
|
||||||
|
def test_wiring_is_read_with_its_nets():
|
||||||
|
dsn = parse_dsn(_DSN)
|
||||||
|
assert [(w.net, w.layer, w.width) for w in dsn.wires] == [("GND", "F.Cu", 400.0)]
|
||||||
|
assert [(v.net, v.x, v.y) for v in dsn.vias] == [("GND", 8000.0, -14000.0)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_wiring_is_fixed_copper_and_not_a_connection_to_route():
|
||||||
|
board = build_board(parse_dsn(_DSN))
|
||||||
|
copper = board.get_fixed_copper()
|
||||||
|
gnd = next(n.net_number for n in board.nets if n.name == "GND")
|
||||||
|
assert copper and all(net == gnd for net, _, _ in copper)
|
||||||
|
assert not any(gnd in p.net_nos for p in board.get_pins())
|
||||||
|
|
||||||
|
|
||||||
|
def test_other_nets_route_around_a_fixed_track():
|
||||||
|
dsn = parse_dsn(_DSN)
|
||||||
|
exact, scale, names = route_dsn_board_exact(dsn, resolution=2)
|
||||||
|
sig = next(i + 1 for i, n in enumerate(dsn.nets) if n.name == "SIG")
|
||||||
|
assert sig in exact.result.routed_net_numbers
|
||||||
|
assert exact.drc_clean
|
||||||
|
wall = 15000 * scale
|
||||||
|
for layer, pts in exact.result.wires.get(sig, []):
|
||||||
|
if names[layer] != "F.Cu":
|
||||||
|
continue
|
||||||
|
for p, q in zip(pts, pts[1:], strict=False):
|
||||||
|
assert not (min(p.x, q.x) < wall < max(p.x, q.x)), "crossed the GND track on F.Cu"
|
||||||
Reference in New Issue
Block a user