Every pin of a supply drops its own via to the plane, and nothing kept two of them apart: the flight controller came back with 199 co-located holes and 109 hole-to-hole errors, nearly all VDD_3V3 and GND. A via now keeps its drill plus --hole-to-hole (default 0.25 mm, KiCad's) from every via centre, fixed ones included, except an own-net via at the same cell, which it reuses; such a via is written once, and none is written on top of a via already on the board.
383 lines
14 KiB
Python
383 lines
14 KiB
Python
"""End-to-end routing pipeline: DSN text in, SES text out.
|
|
|
|
Ties together the parser, board construction, the grid router, and the SES
|
|
writer — the Java-free replacement for the ``freerouting.jar`` step:
|
|
|
|
dsn_text -> parse_dsn -> build_board -> route_board -> write_ses -> ses_text
|
|
|
|
Router output is in board units; wire paths and via locations are converted back
|
|
to DSN units (dividing by the resolution) for the SES ``(wire (path ...))`` and
|
|
``(via <padstack> x y)`` scopes.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
from freeroute.board import build_board
|
|
from freeroute.dsn import DsnBoard, parse_dsn
|
|
from freeroute.ses import RoutedVia, RoutedWire, RoutingResult, write_ses
|
|
|
|
from .exact_router import ExactRouteResult, route_board_exact
|
|
from .grid_router import RouteResult, route_board
|
|
from .room_router import route_board_rooms
|
|
|
|
__all__ = ["route", "route_dsn_board", "build_routing_result"]
|
|
|
|
#: fallback trace width / clearance in DSN units when the DSN has no rules
|
|
_DEFAULT_WIDTH_DSN = 2000
|
|
_DEFAULT_CLEARANCE_DSN = 2000
|
|
#: fallback via padstack name when the DSN declares none
|
|
_DEFAULT_VIA = "Via"
|
|
|
|
|
|
def _rule_width_dsn(dsn: DsnBoard) -> float:
|
|
rules = dsn.structure_rules.width_rules
|
|
return rules[0].value if rules else _DEFAULT_WIDTH_DSN
|
|
|
|
|
|
def _net_widths_board(dsn: DsnBoard, scale: int) -> dict[int, int]:
|
|
"""Track width of every net whose class asks for more than the default,
|
|
in board units, by net number (build_board numbers nets in DSN order)."""
|
|
default = _rule_width_dsn(dsn)
|
|
by_name: dict[str, float] = {}
|
|
for net_class in dsn.net_classes:
|
|
if not net_class.width_rules:
|
|
continue
|
|
width = net_class.width_rules[0].value
|
|
if width > default:
|
|
for name in net_class.net_names:
|
|
by_name[name] = max(by_name.get(name, 0.0), width)
|
|
return {
|
|
i + 1: round(by_name[net.name] * scale)
|
|
for i, net in enumerate(dsn.nets)
|
|
if net.name in by_name
|
|
}
|
|
|
|
|
|
def _rule_clearance_dsn(dsn: DsnBoard) -> float:
|
|
for rule in dsn.structure_rules.clearance_rules:
|
|
if not rule.class_pairs: # the layer-wide default clearance
|
|
return rule.value
|
|
return _DEFAULT_CLEARANCE_DSN
|
|
|
|
|
|
def _via_padstack(dsn: DsnBoard) -> str:
|
|
return dsn.via_padstack_names[0] if dsn.via_padstack_names else _DEFAULT_VIA
|
|
|
|
|
|
def _mm_to_board(dsn: DsnBoard, scale: int, value_mm: float | None) -> int | None:
|
|
"""A length in millimetres as board units, or None."""
|
|
if value_mm is None:
|
|
return None
|
|
unit_mm = {"inch": 25.4, "mil": 0.0254, "mm": 1.0, "cm": 10.0, "um": 0.001}[dsn.unit]
|
|
return round(value_mm / unit_mm * scale)
|
|
|
|
|
|
def _via_drill_board(dsn: DsnBoard, scale: int) -> int | None:
|
|
"""The via's drill in board units, from KiCad's padstack name
|
|
(``Via[0-1]_600:300_um`` is a 600 um pad with a 300 um drill); None when
|
|
the name does not say, and the pad size stands in for it."""
|
|
match = re.search(r"_([\d.]+):([\d.]+)_um", _via_padstack(dsn))
|
|
if not match:
|
|
return None
|
|
return round(float(match.group(2)) * _dsn_units_per_um(dsn) * scale)
|
|
|
|
|
|
def _dsn_units_per_um(dsn: DsnBoard) -> float:
|
|
return {"um": 1.0, "mm": 0.001, "mil": 1 / 25.4, "inch": 1 / 25400}.get(dsn.unit, 1.0)
|
|
|
|
|
|
def _via_half_board(dsn: DsnBoard, scale: int) -> int | None:
|
|
"""Half the via pad's size in board units, from the DSN's via padstack.
|
|
|
|
None when the DSN names no via padstack with a shape, so the engines fall
|
|
back to their old estimate.
|
|
"""
|
|
padstack = dsn.padstack(_via_padstack(dsn))
|
|
if padstack is None or not padstack.shapes:
|
|
return None
|
|
half = 0.0
|
|
for shape in padstack.shapes:
|
|
if hasattr(shape, "diameter"):
|
|
half = max(half, shape.diameter / 2)
|
|
elif len(getattr(shape, "coords", [])) >= 4:
|
|
xs, ys = shape.coords[0::2], shape.coords[1::2]
|
|
half = max(half, (max(xs) - min(xs)) / 2, (max(ys) - min(ys)) / 2)
|
|
return round(half * scale) or None
|
|
|
|
|
|
def route_dsn_board(
|
|
dsn: DsnBoard,
|
|
*,
|
|
resolution: int = 1,
|
|
edge_clearance_mm: float | None = None,
|
|
layers: list[int] | None = None,
|
|
rip_up: bool = True,
|
|
max_passes: int = 10,
|
|
seed: int | None = None,
|
|
route_first: frozenset | None = None,
|
|
access_cost: float = 0.0,
|
|
hole_to_hole_mm: float = 0.25,
|
|
rollback: bool = True,
|
|
) -> tuple[RouteResult, int, list[str]]:
|
|
"""Route a parsed :class:`DsnBoard`; return the board-unit result, the scale,
|
|
and the layer names. ``layers`` restricts routing to those signal-layer
|
|
indices (default: all signal layers, i.e. multi-layer with vias); ``rip_up``
|
|
toggles rip-up-and-retry (``False`` = greedy single pass)."""
|
|
board = build_board(dsn)
|
|
scale = max(dsn.resolution.value, 1)
|
|
edge_clearance_board = _mm_to_board(dsn, scale, edge_clearance_mm)
|
|
width_board = round(_rule_width_dsn(dsn) * scale)
|
|
clearance_board = round(_rule_clearance_dsn(dsn) * scale)
|
|
result = route_board(
|
|
board,
|
|
resolution=resolution,
|
|
via_half=_via_half_board(dsn, scale),
|
|
edge_clearance=edge_clearance_board,
|
|
trace_width=width_board,
|
|
clearance=clearance_board,
|
|
layers=layers,
|
|
rip_up=rip_up,
|
|
max_passes=max_passes,
|
|
seed=seed,
|
|
route_first=route_first,
|
|
access_cost=access_cost,
|
|
rollback=rollback,
|
|
net_widths=_net_widths_board(dsn, scale),
|
|
via_drill=_via_drill_board(dsn, scale),
|
|
hole_to_hole=_mm_to_board(dsn, scale, hole_to_hole_mm) or 0,
|
|
)
|
|
return result, scale, [layer.name for layer in dsn.layers]
|
|
|
|
|
|
def _to_routing_result(route, dsn: DsnBoard, scale: int, layer_names: list[str]) -> RoutingResult:
|
|
"""Convert a board-unit :class:`RouteResult` to a DSN-unit RoutingResult."""
|
|
width_dsn = _rule_width_dsn(dsn)
|
|
via_name = _via_padstack(dsn)
|
|
# net_number is assigned in DSN order by build_board, so index i -> number i+1
|
|
numbers_to_names = {i + 1: n.name for i, n in enumerate(dsn.nets)}
|
|
|
|
result = RoutingResult()
|
|
for net_no, segments in route.wires.items():
|
|
name = numbers_to_names.get(net_no, str(net_no))
|
|
# its class's width when every connection of the net went in at it
|
|
half = getattr(route, "net_half", {}).get(net_no)
|
|
net_width = 2 * half / scale if half else width_dsn
|
|
for layer_index, path in segments:
|
|
layer_name = layer_names[layer_index] if layer_index < len(layer_names) else "F.Cu"
|
|
coords: list[float] = []
|
|
for point in path:
|
|
coords.append(point.x / scale)
|
|
coords.append(point.y / scale)
|
|
if len(coords) >= 4:
|
|
result.add_wire(name, RoutedWire(layer=layer_name, width=net_width, coords=coords))
|
|
for net_no, locations in route.vias.items():
|
|
name = numbers_to_names.get(net_no, str(net_no))
|
|
for loc in locations:
|
|
result.add_via(name, RoutedVia(padstack=via_name, x=loc.x / scale, y=loc.y / scale))
|
|
result.unrouted = [
|
|
(numbers_to_names.get(n, str(n)), a, b, zone) for n, a, b, zone in getattr(route, "failed", [])
|
|
]
|
|
result.narrowed = [
|
|
(numbers_to_names.get(n, str(n)), a, b) for n, a, b in getattr(route, "narrowed", [])
|
|
]
|
|
return result
|
|
|
|
|
|
def build_routing_result(
|
|
dsn: DsnBoard,
|
|
*,
|
|
resolution: int = 1,
|
|
layers: list[int] | None = None,
|
|
rip_up: bool = True,
|
|
max_passes: int = 10,
|
|
edge_clearance_mm: float | None = None,
|
|
seed: int | None = None,
|
|
route_first: frozenset | None = None,
|
|
access_cost: float = 0.0,
|
|
hole_to_hole_mm: float = 0.25,
|
|
rollback: bool = True,
|
|
) -> RoutingResult:
|
|
"""Route ``dsn`` (grid track) and convert to a DSN-unit RoutingResult."""
|
|
route, scale, layer_names = route_dsn_board(
|
|
dsn, layers=layers, rip_up=rip_up, max_passes=max_passes,
|
|
edge_clearance_mm=edge_clearance_mm, resolution=resolution, seed=seed,
|
|
route_first=route_first,
|
|
access_cost=access_cost,
|
|
hole_to_hole_mm=hole_to_hole_mm,
|
|
rollback=rollback,
|
|
)
|
|
return _to_routing_result(route, dsn, scale, layer_names)
|
|
|
|
|
|
def route_dsn_board_exact(
|
|
dsn: DsnBoard,
|
|
*,
|
|
resolution: int = 1,
|
|
heuristic_weight: float = 1.0,
|
|
edge_clearance_mm: float | None = None,
|
|
layers: list[int] | None = None,
|
|
max_passes: int = 10,
|
|
rip_up: bool = True,
|
|
shove: bool = False,
|
|
diagonal: bool = False,
|
|
seed: int | None = None,
|
|
route_first: frozenset | None = None,
|
|
access_cost: float = 0.0,
|
|
hole_to_hole_mm: float = 0.25,
|
|
rollback: bool = True,
|
|
) -> tuple[ExactRouteResult, int, list[str]]:
|
|
"""Route a parsed :class:`DsnBoard` with the exact-geometry (orthogonal,
|
|
DRC-verified) track; return the exact result, the scale, and layer names.
|
|
``shove`` enables moving existing traces aside to recover dropped nets;
|
|
``rip_up`` toggles the underlying grid's rip-up-and-retry; ``diagonal`` enables
|
|
the 45-degree recovery pass for still-dropped 2-pin nets."""
|
|
board = build_board(dsn)
|
|
scale = max(dsn.resolution.value, 1)
|
|
edge_clearance_board = _mm_to_board(dsn, scale, edge_clearance_mm)
|
|
width_board = round(_rule_width_dsn(dsn) * scale)
|
|
clearance_board = round(_rule_clearance_dsn(dsn) * scale)
|
|
exact = route_board_exact(
|
|
board,
|
|
resolution=resolution,
|
|
heuristic_weight=heuristic_weight,
|
|
via_half=_via_half_board(dsn, scale),
|
|
edge_clearance=edge_clearance_board,
|
|
trace_width=width_board,
|
|
clearance=clearance_board,
|
|
layers=layers,
|
|
max_passes=max_passes,
|
|
rip_up=rip_up,
|
|
shove=shove,
|
|
diagonal=diagonal,
|
|
seed=seed,
|
|
route_first=route_first,
|
|
access_cost=access_cost,
|
|
rollback=rollback,
|
|
net_widths=_net_widths_board(dsn, scale),
|
|
via_drill=_via_drill_board(dsn, scale),
|
|
hole_to_hole=_mm_to_board(dsn, scale, hole_to_hole_mm) or 0,
|
|
)
|
|
return exact, scale, [layer.name for layer in dsn.layers]
|
|
|
|
|
|
def build_exact_routing_result(
|
|
dsn: DsnBoard,
|
|
*,
|
|
resolution: int = 1,
|
|
heuristic_weight: float = 1.0,
|
|
layers: list[int] | None = None,
|
|
shove: bool = False,
|
|
diagonal: bool = False,
|
|
rip_up: bool = True,
|
|
max_passes: int = 10,
|
|
edge_clearance_mm: float | None = None,
|
|
seed: int | None = None,
|
|
route_first: frozenset | None = None,
|
|
access_cost: float = 0.0,
|
|
hole_to_hole_mm: float = 0.25,
|
|
rollback: bool = True,
|
|
) -> RoutingResult:
|
|
"""Route ``dsn`` (exact track) and convert to a DSN-unit RoutingResult."""
|
|
exact, scale, layer_names = route_dsn_board_exact(
|
|
dsn,
|
|
layers=layers,
|
|
shove=shove,
|
|
diagonal=diagonal,
|
|
rip_up=rip_up,
|
|
max_passes=max_passes,
|
|
edge_clearance_mm=edge_clearance_mm,
|
|
resolution=resolution,
|
|
heuristic_weight=heuristic_weight,
|
|
seed=seed,
|
|
route_first=route_first,
|
|
access_cost=access_cost,
|
|
hole_to_hole_mm=hole_to_hole_mm,
|
|
rollback=rollback,
|
|
)
|
|
return _to_routing_result(exact.result, dsn, scale, layer_names)
|
|
|
|
|
|
def route_dsn_board_rooms(
|
|
dsn: DsnBoard,
|
|
*,
|
|
layers: list[int] | None = None,
|
|
shove: bool = False,
|
|
pack: bool = False,
|
|
edge_clearance_mm: float | None = None,
|
|
) -> tuple[ExactRouteResult, int, list[str]]:
|
|
"""Route a parsed :class:`DsnBoard` with the continuous expansion-room track;
|
|
return the result, the scale, and layer names. ``shove`` enables occupancy-
|
|
aware gate placement and shove-in-rooms recovery of otherwise-dropped nets.
|
|
``pack`` enables the coordinated multi-trace channel packer."""
|
|
board = build_board(dsn)
|
|
scale = max(dsn.resolution.value, 1)
|
|
edge_clearance_board = _mm_to_board(dsn, scale, edge_clearance_mm)
|
|
width_board = round(_rule_width_dsn(dsn) * scale)
|
|
clearance_board = round(_rule_clearance_dsn(dsn) * scale)
|
|
rooms = route_board_rooms(
|
|
board,
|
|
via_half=_via_half_board(dsn, scale),
|
|
edge_clearance=edge_clearance_board,
|
|
trace_width=width_board,
|
|
clearance=clearance_board,
|
|
layers=layers,
|
|
shove=shove,
|
|
pack=pack,
|
|
)
|
|
return rooms, scale, [layer.name for layer in dsn.layers]
|
|
|
|
|
|
def build_rooms_routing_result(
|
|
dsn: DsnBoard,
|
|
*,
|
|
layers: list[int] | None = None,
|
|
shove: bool = False,
|
|
pack: bool = False,
|
|
edge_clearance_mm: float | None = None,
|
|
) -> RoutingResult:
|
|
"""Route ``dsn`` (room track) and convert to a DSN-unit RoutingResult."""
|
|
rooms, scale, layer_names = route_dsn_board_rooms(
|
|
dsn, layers=layers, shove=shove, pack=pack, edge_clearance_mm=edge_clearance_mm
|
|
)
|
|
return _to_routing_result(rooms.result, dsn, scale, layer_names)
|
|
|
|
|
|
def route(
|
|
dsn_text: str,
|
|
*,
|
|
layers: list[int] | None = None,
|
|
engine: str = "grid",
|
|
shove: bool = False,
|
|
pack: bool = False,
|
|
diagonal: bool = False,
|
|
edge_clearance_mm: float | None = None,
|
|
resolution: int = 1,
|
|
) -> str:
|
|
"""Route a Specctra DSN string and return the routed SES string.
|
|
|
|
``engine`` selects the routing track: ``"grid"`` (default; the multi-layer
|
|
rip-up grid MVP — highest coverage, kept for backward compatibility),
|
|
``"exact"`` (orthogonal, exact-geometry, DRC-verified — cleaner output where
|
|
it succeeds), or ``"room"`` (continuous expansion-room — exact free-space
|
|
decomposition, routes off-grid channels the grid/exact tracks cannot).
|
|
``shove`` (exact and room engines) tries moving existing traces aside; on the
|
|
room engine it also turns on occupancy-aware gate placement.
|
|
"""
|
|
dsn = parse_dsn(dsn_text)
|
|
if engine == "exact":
|
|
result = build_exact_routing_result(
|
|
dsn, layers=layers, shove=shove, diagonal=diagonal,
|
|
edge_clearance_mm=edge_clearance_mm, resolution=resolution,
|
|
)
|
|
elif engine == "room":
|
|
result = build_rooms_routing_result(
|
|
dsn, layers=layers, shove=shove, pack=pack, edge_clearance_mm=edge_clearance_mm
|
|
)
|
|
else:
|
|
result = build_routing_result(
|
|
dsn, layers=layers, edge_clearance_mm=edge_clearance_mm, resolution=resolution
|
|
)
|
|
return write_ses(dsn, result)
|