Say which connections were not routed
freeroute exited 0 and said nothing when it dropped connections, which cost real diagnosis time: on rc4 two simple two-pin nets came back with no copper and no explanation. The grid now records each connection it could not route, the exact check adds each it rejects, and the CLI lists them on stderr (the SES is still written; the exit code stays 0, as the JAR's). A connection whose ends both sit in one zone of its net is listed apart, as left to the zone fill: freeroute does not route through a zone it cannot trust to be solid, but KiCad's fill often joins those pads anyway.
This commit is contained in:
@@ -198,6 +198,20 @@ def main(argv: list[str] | None = None) -> int:
|
||||
)
|
||||
|
||||
ses_text = write_ses(dsn, result)
|
||||
missing = [u for u in result.unrouted if not u[3]]
|
||||
in_zone = [u for u in result.unrouted if u[3]]
|
||||
if missing:
|
||||
# the SES is still written; say plainly what it is missing
|
||||
print(f"freeroute: {len(missing)} connection(s) not routed:", file=sys.stderr)
|
||||
for net, a, b, _zone in missing:
|
||||
print(f" {net}: {a} to {b}", file=sys.stderr)
|
||||
if in_zone:
|
||||
print(
|
||||
f"freeroute: {len(in_zone)} more connection(s) left to a zone fill that may join them:",
|
||||
file=sys.stderr,
|
||||
)
|
||||
for net, a, b, zone in in_zone:
|
||||
print(f" {net}: {a} to {b} (both in zone {zone})", file=sys.stderr)
|
||||
|
||||
if args.output:
|
||||
with open(args.output, "w", encoding="utf-8") as f:
|
||||
|
||||
@@ -159,7 +159,11 @@ def route_board_exact(
|
||||
|
||||
# Verify connection by connection, so one conflicting segment of a
|
||||
# 25-pin supply net costs that one connection, not the whole net.
|
||||
for net_no, wires, vias in sorted(result.connections, key=lambda c: c[0]):
|
||||
rejected: list[tuple[int, str, str]] = []
|
||||
ends_list = result.connection_ends or [("?", "?", "")] * len(result.connections)
|
||||
for (net_no, wires, vias), ends in sorted(
|
||||
zip(result.connections, ends_list, strict=False), key=lambda c: c[0][0]
|
||||
):
|
||||
boxes = _copper_boxes(wires, vias, half_width, via_layers, via_half)
|
||||
wire_count = len(_copper_boxes(wires, [], half_width, via_layers, via_half))
|
||||
if any(
|
||||
@@ -169,6 +173,7 @@ def route_board_exact(
|
||||
for i, (box, box_layers) in enumerate(boxes)
|
||||
for layer in box_layers
|
||||
) or (outline is not None and not all(outline.box_clear(b, edge) for b, _ in boxes)):
|
||||
rejected.append((net_no, *ends))
|
||||
continue # exact-clearance conflict -> drop (may be recovered by shove)
|
||||
acc = accepted.get(net_no)
|
||||
if acc is None:
|
||||
@@ -180,6 +185,7 @@ def route_board_exact(
|
||||
tree.insert(TreeShape(oid, net_no, box_layers, box, routed=True))
|
||||
acc.owners.append(oid)
|
||||
|
||||
accepted_before_recovery = set(accepted)
|
||||
# a net is dropped if it has a real ratsnest (>= 2 pins on a signal layer)
|
||||
# but was not accepted — whether the grid failed to route it or its exact
|
||||
# copper conflicted.
|
||||
@@ -214,6 +220,12 @@ def route_board_exact(
|
||||
)
|
||||
|
||||
clean = RouteResult(half_width)
|
||||
# what is still missing: what the grid could not route, plus what the
|
||||
# exact check rejected, less any net shove or the diagonal pass recovered
|
||||
clean.failed = [
|
||||
*result.failed,
|
||||
*(r for r in rejected if not (r[0] in accepted and r[0] not in accepted_before_recovery)),
|
||||
]
|
||||
for net_no, acc in accepted.items():
|
||||
clean.wires[net_no] = acc.wires
|
||||
clean.vias[net_no] = acc.vias
|
||||
|
||||
@@ -59,7 +59,7 @@ class RouteResult:
|
||||
``routed_net_numbers`` are the nets that got any wire or via.
|
||||
"""
|
||||
|
||||
__slots__ = ("wires", "vias", "half_width", "unrouted", "connections")
|
||||
__slots__ = ("wires", "vias", "half_width", "unrouted", "connections", "connection_ends", "failed")
|
||||
|
||||
def __init__(self, half_width: int) -> None:
|
||||
self.wires: dict[int, list[tuple[int, list[IntPoint]]]] = {}
|
||||
@@ -69,6 +69,11 @@ class RouteResult:
|
||||
self.unrouted = 0
|
||||
#: the same copper per routed connection: (net_no, wires, vias)
|
||||
self.connections: list[tuple[int, list[tuple[int, list[IntPoint]]], list[IntPoint]]] = []
|
||||
#: what each routed connection joins, and the zone both ends sit in
|
||||
#: (or ""), parallel to ``connections``
|
||||
self.connection_ends: list[tuple[str, str, str]] = []
|
||||
#: connections left unrouted: (net_no, one end, the other end)
|
||||
self.failed: list[tuple[int, str, str]] = []
|
||||
|
||||
def add_wire(self, net_no: int, layer: int, path: list[IntPoint]) -> None:
|
||||
self.wires.setdefault(net_no, []).append((layer, path))
|
||||
@@ -523,6 +528,16 @@ class GridRouter:
|
||||
covered = sum((*cell, zone.layer) in self._keepout for cell in cells)
|
||||
return covered >= coverage * len(cells)
|
||||
|
||||
def _left_to_zone(self, conn) -> str:
|
||||
"""The zone both ends of an unrouted connection sit in, if any: its
|
||||
fill may well join them, even when it is not trusted for routing."""
|
||||
for zone in self._every_zone_by_net.get(conn.net_no, []):
|
||||
if any(_pin_touches_zone(p, zone) for p in conn.a_group[0]) and any(
|
||||
_pin_touches_zone(p, zone) for p in conn.b_group[0]
|
||||
):
|
||||
return zone.name or "zone"
|
||||
return ""
|
||||
|
||||
def _group_ends(self, group, net_no: int, conns) -> dict[Node, Pin | None]:
|
||||
"""End nodes of a group, each mapped to its pin (None for a zone cell)."""
|
||||
ends: dict[Node, Pin | None] = {}
|
||||
@@ -668,6 +683,7 @@ class GridRouter:
|
||||
def route(self) -> RouteResult:
|
||||
"""Route all ratsnest connections, using rip-up-and-retry across passes."""
|
||||
conns = self._build_connections()
|
||||
self._conns = conns
|
||||
if not conns:
|
||||
return RouteResult(self.half_width)
|
||||
|
||||
@@ -709,6 +725,10 @@ class GridRouter:
|
||||
def _build_connections(self) -> list[_Connection]:
|
||||
pins_by_net = self._pins_by_net()
|
||||
zones_by_net: dict[int, list] = {}
|
||||
self._every_zone_by_net: dict[int, list] = {}
|
||||
for zone in self.board.get_conduction_areas():
|
||||
if zone.layer in self.layers and zone.net_nos:
|
||||
self._every_zone_by_net.setdefault(zone.net_nos[0], []).append(zone)
|
||||
for zone in self.board.get_conduction_areas():
|
||||
if zone.layer in self.layers and zone.net_nos and self._zone_is_solid(zone):
|
||||
zones_by_net.setdefault(zone.net_nos[0], []).append(zone)
|
||||
@@ -732,9 +752,10 @@ class GridRouter:
|
||||
by_net.setdefault(net_no, []).append(pin)
|
||||
return by_net
|
||||
|
||||
def _snapshot(self, conns: list[_Connection]) -> list[tuple[int, list, list]]:
|
||||
def _snapshot(self, conns: list[_Connection]) -> list[tuple]:
|
||||
return [
|
||||
(c.net_no, [(layer, list(pts)) for layer, pts in c.wires], list(c.vias))
|
||||
(c.net_no, [(layer, list(pts)) for layer, pts in c.wires], list(c.vias),
|
||||
(*_ends_of(c), self._left_to_zone(c)))
|
||||
for c in conns
|
||||
if c.routed
|
||||
]
|
||||
@@ -742,12 +763,19 @@ class GridRouter:
|
||||
def _result_from(self, snapshot, unrouted: int) -> RouteResult:
|
||||
result = RouteResult(self.half_width)
|
||||
result.unrouted = unrouted
|
||||
for net_no, wires, vias in snapshot:
|
||||
for net_no, wires, vias, ends in snapshot:
|
||||
for layer, points in wires:
|
||||
result.add_wire(net_no, layer, points)
|
||||
for location in vias:
|
||||
result.add_via(net_no, location)
|
||||
result.connections.append((net_no, wires, vias))
|
||||
result.connection_ends.append(ends)
|
||||
routed = {ends[:2] + (net_no,) for net_no, _w, _v, ends in snapshot}
|
||||
result.failed = [
|
||||
(c.net_no, *_ends_of(c), self._left_to_zone(c))
|
||||
for c in self._conns
|
||||
if _ends_of(c) + (c.net_no,) not in routed
|
||||
]
|
||||
return result
|
||||
|
||||
|
||||
@@ -755,6 +783,20 @@ def _first(items):
|
||||
return items[0] if items else None
|
||||
|
||||
|
||||
def _group_label(group) -> str:
|
||||
pins, zones = group
|
||||
parts = [f"{p.component_ref}-{p.name}" for p in pins[:3]]
|
||||
if len(pins) > 3:
|
||||
parts.append(f"+{len(pins) - 3} pins")
|
||||
parts += [f"zone {z.name}" for z in zones[:1]]
|
||||
return "/".join(parts) or "?"
|
||||
|
||||
|
||||
def _ends_of(conn) -> tuple[str, str]:
|
||||
"""A readable description of what a connection joins."""
|
||||
return _group_label(conn.a_group), _group_label(conn.b_group)
|
||||
|
||||
|
||||
def _pin_touches_zone(pin: Pin, zone) -> bool:
|
||||
"""A pad on the zone's layer whose centre or a corner lies in the zone."""
|
||||
if zone.layer not in pin.layers or zone.polygon is None:
|
||||
|
||||
@@ -126,6 +126,9 @@ def _to_routing_result(route, dsn: DsnBoard, scale: int, layer_names: list[str])
|
||||
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", [])
|
||||
]
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -57,6 +57,9 @@ class RoutingResult:
|
||||
"""The complete routing output for a board, grouped by net."""
|
||||
|
||||
nets: list[RoutedNet] = field(default_factory=list)
|
||||
#: connections the router could not make: (net name, one end, other end,
|
||||
#: the zone both ends sit in or "") -- a zone's fill may join the latter
|
||||
unrouted: list[tuple[str, str, str, str]] = field(default_factory=list)
|
||||
|
||||
def net(self, name: str, *, create: bool = False) -> RoutedNet | None:
|
||||
"""Return the :class:`RoutedNet` with ``name`` (creating it if asked)."""
|
||||
|
||||
@@ -148,3 +148,19 @@ def test_a_pour_other_nets_can_cross_is_not_trusted_to_connect():
|
||||
result, _, _ = route_dsn_board_exact(dsn)
|
||||
net = next(iter(result.routed_net_numbers))
|
||||
assert result.result.wires.get(net) # P1 and P2 got a track between them
|
||||
|
||||
|
||||
def test_cli_names_the_connections_it_could_not_route(tmp_path, capsys):
|
||||
# a keepout wall across the board on both layers: A and B cannot be joined
|
||||
from freeroute.cli import main
|
||||
|
||||
wall = " ".join(
|
||||
f"(keepout \"wall\" (polygon {layer} 0 9000 1000 11000 1000 11000 -21000 9000 -21000))"
|
||||
for layer in ("F.Cu", "B.Cu")
|
||||
)
|
||||
dsn = tmp_path / "wall.dsn"
|
||||
dsn.write_text(_dsn("(component P (place A 4000 -10000 front 0) (place B 16000 -10000 front 0))",
|
||||
"(net N (pins A-1 B-1))", wall))
|
||||
assert main([str(dsn), "--engine", "exact", "-do", str(tmp_path / "out.ses")]) == 0
|
||||
err = capsys.readouterr().err
|
||||
assert "1 connection(s) not routed" in err and "N: A-1 to B-1" in err
|
||||
|
||||
Reference in New Issue
Block a user