Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a50bde531 | ||
|
|
0f6a2a0812 | ||
|
|
245cd91c0e | ||
|
|
019b043cd5 |
@@ -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)]
|
||||
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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``)
|
||||
|
||||
@@ -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 <net> (polygon <layer> <width> x y ...))`` -> :class:`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)
|
||||
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
|
||||
|
||||
@@ -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(
|
||||
@@ -451,10 +462,11 @@ class GridRouter:
|
||||
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(p, n) for n, p in goals.items() if p is not None}
|
||||
# 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)}
|
||||
for s in sorted(starts):
|
||||
pin = starts[s]
|
||||
g_score[s] = self._stub_cost(pin, s) if pin is not None else 0.0
|
||||
g_score[s] = self._stub_cost(pin, s) 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -202,6 +202,18 @@ def test_smd_pads_reach_an_inner_plane_by_their_own_via():
|
||||
assert all(names[layer] in ("F.Cu", "B.Cu") for segs in result.result.wires.values() for layer, _ in segs)
|
||||
|
||||
|
||||
def test_a_pad_outside_the_plane_routes_to_it():
|
||||
# G1 sits left of a plane that covers only the right half, so its route
|
||||
# has to reach the plane's cells: those ends are the plane, not a pin
|
||||
dsn_text = _four_layer_dsn().replace(
|
||||
"(plane GND (polygon In1.Cu 0 500 -500 29500 -500 29500 -29500 500 -29500 500 -500))",
|
||||
"(plane GND (polygon In1.Cu 0 15000 -500 29500 -500 29500 -29500 15000 -29500 15000 -500))",
|
||||
)
|
||||
result, _scale, _names = route_dsn_board_exact(parse_dsn(dsn_text))
|
||||
gnd = next(i + 1 for i, n in enumerate(parse_dsn(dsn_text).nets) if n.name == "GND")
|
||||
assert gnd in result.result.routed_net_numbers
|
||||
|
||||
|
||||
def test_a_through_hole_pad_inside_a_plane_needs_no_route():
|
||||
from freeroute.route.grid_router import _connected_groups
|
||||
|
||||
|
||||
Reference in New Issue
Block a user