Add MVP grid maze router and DSN->SES routing pipeline

Implements a working autorouter and the Java-free replacement for the
freerouting.jar step: dsn_text -> parse_dsn -> build_board -> route ->
write_ses.

GridRouter is a single-layer A* maze search over a uniform occupancy
grid: a cell is blocked by another net's pad (inflated by clearance +
half trace width) or a keepout; each net's ratsnest is connected pin to
pin; a routed trace then blocks other nets. The cell path becomes a
trace polyline whose endpoints are the exact pin locations. Board-unit
paths are converted back to DSN units for the SES (wire (path ...))
scopes.

This is an MVP, not a port of FreeRouting's expansion-room maze:
free-space rooms, rip-up-and-retry, multi-layer via search, and shove
are deferred (they raise quality/coverage, not the connectivity
milestone). It reaches connectivity on boards whose nets route on one
layer without crossing.

Adds tests/dsn/fixtures/simple_2net.dsn (a guaranteed-routable 2-net
board). Invariant tests: both nets route, the emitted SES parses, every
trace stays on a valid layer and within the board outline, and every
routed net's trace endpoints sit exactly on its two pads. An oracle-gated
test asserts connectivity parity with the reference FreeRouting JAR on
the simple board.
This commit is contained in:
2026-07-12 11:25:03 -06:00
parent 4220fe0cd6
commit 89c0481ccc
5 changed files with 580 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
"""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; it is converted back to DSN units (dividing by
the resolution) for the SES ``(wire (path ...))`` scopes.
"""
from __future__ import annotations
from freeroute.board import build_board
from freeroute.dsn import DsnBoard, parse_dsn
from freeroute.ses import RoutedWire, RoutingResult, write_ses
from .grid_router import RouteResult, route_board
__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
def _rule_width_dsn(dsn: DsnBoard) -> float:
rules = dsn.structure_rules.width_rules
return rules[0].value if rules else _DEFAULT_WIDTH_DSN
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 route_dsn_board(dsn: DsnBoard) -> tuple[RouteResult, int, list[str]]:
"""Route a parsed :class:`DsnBoard`; return the board-unit result, the scale,
and the layer names."""
board = build_board(dsn)
scale = max(dsn.resolution.value, 1)
width_board = round(_rule_width_dsn(dsn) * scale)
clearance_board = round(_rule_clearance_dsn(dsn) * scale)
result = route_board(board, trace_width=width_board, clearance=clearance_board, layer=0)
return result, scale, [layer.name for layer in dsn.layers]
def build_routing_result(dsn: DsnBoard) -> RoutingResult:
"""Route ``dsn`` and convert the board-unit paths to a DSN-unit
:class:`~freeroute.ses.RoutingResult` for SES emission."""
route, scale, layer_names = route_dsn_board(dsn)
layer_name = layer_names[route.layer] if layer_names else "F.Cu"
width_dsn = _rule_width_dsn(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, paths in route.wires.items():
name = numbers_to_names.get(net_no, str(net_no))
for path in paths:
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=width_dsn, coords=coords))
return result
def route(dsn_text: str) -> str:
"""Route a Specctra DSN string and return the routed SES string."""
dsn = parse_dsn(dsn_text)
result = build_routing_result(dsn)
return write_ses(dsn, result)