"""Tests for the multi-layer grid maze router and the DSN -> SES pipeline. Fast tests assert the routing invariants (traces connect their pads, stay on valid layers, stay in bounds, vias sit at real layer transitions, no same-layer crossing between nets). Oracle-gated tests check connectivity parity against the reference FreeRouting JAR. Fixtures: ``simple_2net.dsn`` (single-layer routable) and ``crossing_2net.dsn`` (a full-width NET_A wall that NET_B must cross, forcing a via). """ from __future__ import annotations from pathlib import Path import sys import pytest from freeroute.board import build_board from freeroute.dsn import parse_dsn from freeroute.dsn.sexp import parse from freeroute.route import route, route_dsn_board sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) FIXTURES = Path(__file__).resolve().parent.parent / "dsn" / "fixtures" SIMPLE = FIXTURES / "simple_2net.dsn" CROSSING = FIXTURES / "crossing_2net.dsn" RIPUP = FIXTURES / "ripup_needed.dsn" def _wire_key(result): """A hashable snapshot of a routing result for determinism checks.""" return { net: [(layer, [(p.x, p.y) for p in pts]) for layer, pts in segs] for net, segs in result.wires.items() } def _network_out(ses_text: str): root = parse(ses_text) network = root.child("routes").child("network_out") return network.children("net") def _net_scopes(ses_text: str): result = {} for net in _network_out(ses_text): name = net.values()[0].text result[name] = {"wires": net.children("wire"), "vias": net.children("via")} return result def _wire_layer_and_coords(wire): path = wire.child("path") vals = [v.text for v in path.values()] nums = [float(v) for v in vals[2:]] coords = list(zip(nums[0::2], nums[1::2], strict=False)) return vals[0], coords def _orient(a, b, c): return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]) def _segments_properly_cross(p1, p2, p3, p4) -> bool: d1 = _orient(p3, p4, p1) d2 = _orient(p3, p4, p2) d3 = _orient(p1, p2, p3) d4 = _orient(p1, p2, p4) return ((d1 > 0) != (d2 > 0)) and ((d3 > 0) != (d4 > 0)) # --- single-layer / no-crossing case ---------------------------------------- def test_simple_board_routes_both_nets_without_vias(): result, _, _ = route_dsn_board(parse_dsn(SIMPLE.read_text())) assert result.routed_net_numbers == {1, 2} assert result.via_count() == 0 # no crossing -> no layer change needed def test_pipeline_emits_valid_ses_with_both_nets(): ses = route(SIMPLE.read_text()) assert parse(ses).head == "session" nets = _net_scopes(ses) assert set(nets) == {"NET_A", "NET_B"} assert all(n["wires"] for n in nets.values()) def test_simple_trace_endpoints_sit_on_the_net_pads(): pads = { "NET_A": {(20000.0, -20000.0), (180000.0, -20000.0)}, "NET_B": {(20000.0, -60000.0), (180000.0, -60000.0)}, } for net_name, scope in _net_scopes(route(SIMPLE.read_text())).items(): endpoints = set() for wire in scope["wires"]: _, coords = _wire_layer_and_coords(wire) endpoints.add(coords[0]) endpoints.add(coords[-1]) assert endpoints == pads[net_name] # --- multi-layer crossing case ---------------------------------------------- def test_greedy_single_layer_strands_a_net_on_the_crossing_board(): # greedy (no rip-up) on one layer: NET_A's straight wall strands NET_B result, _, _ = route_dsn_board(parse_dsn(CROSSING.read_text()), layers=[0], rip_up=False) assert len(result.routed_net_numbers) < 2 assert result.via_count() == 0 def test_multilayer_routes_the_crossing_board_with_a_via(): result, _, _ = route_dsn_board(parse_dsn(CROSSING.read_text())) assert result.routed_net_numbers == {1, 2} # both nets connect assert result.via_count() >= 1 # the crossing net changes layers def test_crossing_ses_contains_a_via(): ses = route(CROSSING.read_text()) nets = _net_scopes(ses) assert set(nets) == {"NET_A", "NET_B"} total_vias = sum(len(n["vias"]) for n in nets.values()) assert total_vias >= 1 def test_vias_sit_at_real_layer_transitions(): # every via location must be shared by two wire segments of the same net on # different layers (i.e. a genuine layer transition of that net's path). result, _, _ = route_dsn_board(parse_dsn(CROSSING.read_text())) for net_no, locations in result.vias.items(): segments = result.wires.get(net_no, []) for via in locations: point = (via.x, via.y) layers_touching = { layer for layer, pts in segments if any((p.x, p.y) == point for p in pts) } assert len(layers_touching) >= 2, "via not at a layer transition" def test_via_endpoints_come_from_the_net_pads_across_layers(): # the crossing net's overall path still starts and ends on its two pads result, _, _ = route_dsn_board(parse_dsn(CROSSING.read_text())) board = build_board(parse_dsn(CROSSING.read_text())) pins_by_net: dict[int, set[tuple[int, int]]] = {} for pin in board.get_pins(): for net_no in pin.net_nos: pins_by_net.setdefault(net_no, set()).add((pin.location.x, pin.location.y)) for net_no, segments in result.wires.items(): all_points = [(p.x, p.y) for _, pts in segments for p in pts] endpoints = {all_points[0], all_points[-1]} assert endpoints == pins_by_net[net_no] # --- rip-up-and-retry (single-layer congestion) ----------------------------- def test_greedy_strands_a_net_on_the_congested_board(): # ripup_needed has one signal layer (no vias); greedy net order strands NET_B result, _, _ = route_dsn_board(parse_dsn(RIPUP.read_text()), rip_up=False) assert result.unrouted >= 1 assert len(result.routed_net_numbers) < 2 assert result.via_count() == 0 # single signal layer -> vias impossible def test_ripup_connects_all_on_the_congested_board(): result, _, _ = route_dsn_board(parse_dsn(RIPUP.read_text()), rip_up=True) assert result.unrouted == 0 assert result.routed_net_numbers == {1, 2} # rip-up reroutes to connect both assert result.via_count() == 0 def test_ripup_result_is_deterministic(): a, _, _ = route_dsn_board(parse_dsn(RIPUP.read_text()), rip_up=True) b, _, _ = route_dsn_board(parse_dsn(RIPUP.read_text()), rip_up=True) assert _wire_key(a) == _wire_key(b) def test_ripup_endpoints_still_on_pads_and_no_same_layer_crossing(): result, _, _ = route_dsn_board(parse_dsn(RIPUP.read_text()), rip_up=True) board = build_board(parse_dsn(RIPUP.read_text())) pins_by_net: dict[int, set[tuple[int, int]]] = {} for pin in board.get_pins(): for net_no in pin.net_nos: pins_by_net.setdefault(net_no, set()).add((pin.location.x, pin.location.y)) per_layer: dict[int, list[tuple[int, list]]] = {} for net_no, segments in result.wires.items(): pts = [(p.x, p.y) for _, seg in segments for p in seg] assert {pts[0], pts[-1]} == pins_by_net[net_no] # endpoints on pads for layer, seg in segments: per_layer.setdefault(layer, []).append((net_no, [(p.x, p.y) for p in seg])) for entries in per_layer.values(): for i in range(len(entries)): net_i, poly_i = entries[i] for j in range(i + 1, len(entries)): net_j, poly_j = entries[j] if net_i == net_j: continue for a in range(len(poly_i) - 1): for b in range(len(poly_j) - 1): assert not _segments_properly_cross( poly_i[a], poly_i[a + 1], poly_j[b], poly_j[b + 1] ) # --- cross-net invariants --------------------------------------------------- def test_no_two_nets_cross_on_the_same_layer(): result, _, _ = route_dsn_board(parse_dsn(CROSSING.read_text())) per_layer: dict[int, list[tuple[int, list]]] = {} for net_no, segments in result.wires.items(): for layer, pts in segments: coords = [(p.x, p.y) for p in pts] per_layer.setdefault(layer, []).append((net_no, coords)) for entries in per_layer.values(): for i in range(len(entries)): net_i, poly_i = entries[i] for j in range(i + 1, len(entries)): net_j, poly_j = entries[j] if net_i == net_j: continue for a in range(len(poly_i) - 1): for b in range(len(poly_j) - 1): assert not _segments_properly_cross( poly_i[a], poly_i[a + 1], poly_j[b], poly_j[b + 1] ), "different nets cross on the same layer" def test_traces_stay_within_the_board_outline(): for path in (SIMPLE, CROSSING): ses = route(path.read_text()) for scope in _net_scopes(ses).values(): for wire in scope["wires"]: _, coords = _wire_layer_and_coords(wire) for x, y in coords: assert 0 <= x <= 200000 assert -80000 <= y <= 0 def test_traces_stay_on_valid_layers(): ses = route(CROSSING.read_text()) for scope in _net_scopes(ses).values(): for wire in scope["wires"]: layer, _ = _wire_layer_and_coords(wire) assert layer in {"Top", "Bottom"} def test_larger_board_still_connects_its_multipin_nets(): # regression: rip-up must not lose coverage on the real board. It has 4 # multi-pin nets, all of which the router connects. dsn = parse_dsn((FIXTURES / "kicad_routable.dsn").read_text()) result, _, _ = route_dsn_board(dsn) multi_pin = sum(1 for n in dsn.nets if len(n.pins) >= 2) assert len(result.routed_net_numbers) == multi_pin == 4 # --- oracle parity ---------------------------------------------------------- @pytest.mark.oracle def test_connectivity_parity_on_congested_board(): from oracle import HAS_ORACLE, route_dsn, routed_net_set if not HAS_ORACLE: pytest.skip("FreeRouting oracle unavailable") our_nets = routed_net_set(route(RIPUP.read_text())) assert our_nets == {"NET_A", "NET_B"} # freeroute connects both via rip-up oracle_nets = routed_net_set(route_dsn(RIPUP, max_passes=3, timeout=300)) if not oracle_nets: # the reference JAR does not route this synthetic single-signal-layer # board (it expects a routing/via layer); freeroute's rip-up connecting # both nets is already asserted above and by the fast tests. pytest.skip("reference JAR routed nothing on the synthetic single-layer board") assert oracle_nets <= our_nets # rip-up matches the JAR's connectivity @pytest.mark.oracle def test_connectivity_parity_on_crossing_board(): from oracle import HAS_ORACLE, route_dsn, routed_net_set if not HAS_ORACLE: pytest.skip("FreeRouting oracle unavailable") our_nets = routed_net_set(route(CROSSING.read_text())) oracle_nets = routed_net_set(route_dsn(CROSSING, max_passes=3, timeout=300)) assert oracle_nets, "oracle routed nothing on a routable board" assert oracle_nets <= our_nets # freeroute connects every net the JAR does @pytest.mark.oracle def test_connectivity_parity_on_simple_board(): from oracle import HAS_ORACLE, route_dsn, routed_net_set if not HAS_ORACLE: pytest.skip("FreeRouting oracle unavailable") our_nets = routed_net_set(route(SIMPLE.read_text())) oracle_nets = routed_net_set(route_dsn(SIMPLE, max_passes=3, timeout=300)) assert oracle_nets, "oracle routed nothing on a routable board" assert oracle_nets <= our_nets