diff --git a/src/freeroute/board/board.py b/src/freeroute/board/board.py index 08f0236..a49effa 100644 --- a/src/freeroute/board/board.py +++ b/src/freeroute/board/board.py @@ -70,6 +70,27 @@ class BasicBoard: def get_traces(self) -> list[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]: return [it for it in self._items if isinstance(it, Via)] diff --git a/src/freeroute/board/build.py b/src/freeroute/board/build.py index e8fc6c3..4f684cb 100644 --- a/src/freeroute/board/build.py +++ b/src/freeroute/board/build.py @@ -31,7 +31,7 @@ from freeroute.geometry.polygon_shape import PolygonShape from .board import BasicBoard from .clearance import ClearanceMatrix -from .items import ConductionArea, ObstacleArea, Pin +from .items import ConductionArea, ObstacleArea, Pin, Trace, Via from .layer import Layer, LayerStructure from .net import Nets from .transform import CoordinateTransform @@ -56,6 +56,7 @@ def build_board(dsn: DsnBoard) -> BasicBoard: _insert_keepouts(dsn, board, transform) _insert_image_keepouts(dsn, board, transform) _insert_zones(dsn, board, transform, nets) + _insert_wiring(dsn, board, transform, nets) 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()] +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: """Each DSN plane as a ConductionArea of its net on its layer.""" layer_no = {layer.name: i for i, layer in enumerate(dsn.layers)} diff --git a/src/freeroute/board/items.py b/src/freeroute/board/items.py index 8ca35a6..9ab415d 100644 --- a/src/freeroute/board/items.py +++ b/src/freeroute/board/items.py @@ -114,6 +114,9 @@ class Via(Item): padstack_name: str = "" 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 diff --git a/src/freeroute/dsn/model.py b/src/freeroute/dsn/model.py index 38f6fd3..ec99595 100644 --- a/src/freeroute/dsn/model.py +++ b/src/freeroute/dsn/model.py @@ -27,6 +27,8 @@ __all__ = [ "NetPin", "Net", "NetClass", + "WiringVia", + "WiringWire", "Keepout", "DsnBoard", ] @@ -186,6 +188,26 @@ class 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 class DsnBoard: """The fully parsed DSN file.""" @@ -197,6 +219,9 @@ class DsnBoard: layers: list[Layer] = field(default_factory=list) #: copper zones, as planes of a net 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 boundary: Shape | None = None #: outline shapes (layer ``signal``) diff --git a/src/freeroute/dsn/reader.py b/src/freeroute/dsn/reader.py index 356ac10..7c3ce4f 100644 --- a/src/freeroute/dsn/reader.py +++ b/src/freeroute/dsn/reader.py @@ -27,6 +27,8 @@ from .model import ( Pin, Resolution, WidthRule, + WiringVia, + WiringWire, ) from .sexp import SExp, parse from .shapes import Shape, read_area, read_shape @@ -531,11 +533,45 @@ def parse_dsn(text: str) -> DsnBoard: _read_placement(it, board) elif head == "network": _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 +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): """``(plane (polygon x y ...))`` -> :class:`Plane`.""" from .model import Plane diff --git a/src/freeroute/route/exact_router.py b/src/freeroute/route/exact_router.py index fb18ac9..2a8840e 100644 --- a/src/freeroute/route/exact_router.py +++ b/src/freeroute/route/exact_router.py @@ -149,6 +149,10 @@ def route_board_exact( pin_layers = frozenset(layer for layer in pin.layers if layer in signal_layers) if pin_layers: 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(): if obstacle.layer not in signal_layers: continue diff --git a/src/freeroute/route/grid_router.py b/src/freeroute/route/grid_router.py index 81bfdd9..283258d 100644 --- a/src/freeroute/route/grid_router.py +++ b/src/freeroute/route/grid_router.py @@ -250,6 +250,7 @@ class GridRouter: # trace halos: node -> indices of the connections claiming it self._halo: dict[Node, list[int]] = {} self._rasterize_obstacles() + self._rasterize_fixed_copper() # --- grid helpers ------------------------------------------------------- @@ -360,6 +361,16 @@ class GridRouter: if item.blocks_vias: 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 ----------------------------------------------------- def _enter_cost( diff --git a/src/freeroute/route/room_router.py b/src/freeroute/route/room_router.py index 2156148..04254ac 100644 --- a/src/freeroute/route/room_router.py +++ b/src/freeroute/route/room_router.py @@ -515,6 +515,10 @@ def route_board_rooms( pin_layers = frozenset(layer for layer in pin.layers if layer in signal_layers) if pin_layers: 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(): if obstacle.layer not in signal_layers: continue diff --git a/tests/route/test_fixed_wiring.py b/tests/route/test_fixed_wiring.py new file mode 100644 index 0000000..2888de5 --- /dev/null +++ b/tests/route/test_fixed_wiring.py @@ -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"