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:
@@ -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)
|
||||
Reference in New Issue
Block a user