"""Routing behaviours found on the TerraHawk rc4 boards (2026-09-27).""" from __future__ import annotations import math import pytest from freeroute.board import build_board from freeroute.dsn import parse_dsn from freeroute.route.pipeline import route_dsn_board_exact # 20 x 20 mm board whose corners are rounded with a 3 mm radius, as polylines _R = 3000.0 _arc = [(_R - _R * math.cos(math.radians(a)), _R - _R * math.sin(math.radians(a))) for a in range(0, 91, 15)] def _outline_coords(): pts = [] for cx, cy, sx, sy in ((0, 0, 1, 1), (20000, 0, -1, 1), (20000, -20000, -1, -1), (0, -20000, 1, -1)): corner = [(cx + sx * x, cy - sy * y) for x, y in _arc] pts.extend(corner if sx * sy > 0 else corner[::-1]) return " ".join(f"{x:.1f} {y:.1f}" for x, y in pts) def _dsn(placement, network, extra=""): return f"""(pcb t (parser) (resolution um 10) (unit um) (structure (layer F.Cu (type signal)) (layer B.Cu (type signal)) (boundary (path pcb 0 {_outline_coords()})) (via "Via[0-1]_600:300_um") (rule (width 250) (clearance 150)) {extra}) (placement {placement}) (library (image P (pin pad 1 0 0)) (padstack pad (shape (rect F.Cu -150 -500 150 500)) (attach off)) (padstack "Via[0-1]_600:300_um" (shape (circle F.Cu 600)) (shape (circle B.Cu 600)) (attach off))) (network {network}))""" def test_routes_keep_edge_clearance_at_a_rounded_corner(): # two pads near the top-left corner: the direct path cuts the corner arc text = _dsn("(component P (place A 1800 -6000 front 0) (place B 6000 -1800 front 90))", "(net N (pins A-1 B-1))") dsn = parse_dsn(text) result, scale, _ = route_dsn_board_exact(dsn, edge_clearance_mm=0.5) assert result.routed_net_numbers outline = build_board(dsn).outline half = 125 * scale for segs in result.result.wires.values(): for _layer, pts in segs: for p in pts: # every trace point, plus half the trace, keeps 0.5 mm to the edge assert outline.edge_distance(p) >= half + 500 * scale - 1 @pytest.mark.parametrize(("resolution", "escape"), [(1, 2), (4, 5)]) def test_every_pin_of_a_half_millimetre_pitch_row_escapes(resolution, escape): # Five 0.3 mm pads at 0.5 mm pitch, each wired to its own pad 8 mm away. # Between two neighbours a trace's centre has a legal band only 0.15 mm # wide. At resolution 1 the 0.4 mm grid misses most bands; at 4 its # 0.1 mm columns cannot, and every pin gets out, as the JAR manages. places = " ".join(f"(place R{i} {6000 + 500 * i} -6000 front 0)" for i in range(5)) places += " " + " ".join(f"(place F{i} {6000 + 500 * i} -14000 front 0)" for i in range(5)) nets = " ".join(f"(net N{i} (pins R{i}-1 F{i}-1))" for i in range(5)) result, _, _ = route_dsn_board_exact( parse_dsn(_dsn(f"(component P {places})", nets)), resolution=resolution ) assert len(result.routed_net_numbers) == escape def test_resolution_one_has_no_halo_so_the_grid_is_unchanged(): from freeroute.route.grid_router import GridRouter board = build_board(parse_dsn(_dsn("", ""))) assert GridRouter(board, trace_width=2500, clearance=1500)._halo_offsets == [] fine = GridRouter(board, trace_width=2500, clearance=1500, resolution=2) assert fine.step == 2000 and (1, 0) in fine._halo_offsets and (2, 0) not in fine._halo_offsets def test_heuristic_weight_bounds_route_length(): # weighted A* may take a longer route, but never more than W times the shortest places = "(place A 3000 -3000 front 0) (place B 17000 -17000 front 0)" def length(weight): result, _, _ = route_dsn_board_exact( parse_dsn(_dsn(f"(component P {places})", "(net N (pins A-1 B-1))")), heuristic_weight=weight, ) return sum( math.hypot(q.x - p.x, q.y - p.y) for segs in result.result.wires.values() for _layer, pts in segs for p, q in zip(pts, pts[1:], strict=False) ) shortest = length(1.0) assert shortest > 0 assert length(2.0) <= 2.0 * shortest # --- copper zones (DSN planes) ----------------------------------------------- # rc4 power stage: pads inside "keep router off" pours connect through the pour, # and freeroute, not knowing zones, dropped every such connection. _ZONE = "(plane N (polygon F.Cu 0 4000 -4000 10000 -4000 10000 -10000 4000 -10000 4000 -4000))" _NO_TRACKS = "(wire_keepout \"pour\" (polygon F.Cu 0 4000 -4000 10000 -4000 10000 -10000 4000 -10000))" def _zone_board(): places = "(place P1 5500 -7000 front 0) (place P2 8500 -7000 front 0) (place P3 16000 -16000 front 0)" return parse_dsn(_dsn(f"(component P {places})", "(net N (pins P1-1 P2-1 P3-1))", _ZONE + " " + _NO_TRACKS)) def test_planes_are_read_and_are_not_obstacles(): dsn = _zone_board() assert [(p.net, p.shape.layer) for p in dsn.planes] == [("N", "F.Cu")] board = build_board(dsn) assert len(board.get_conduction_areas()) == 1 assert all(not a.is_obstacle() or a.name == "pour" for a in board.get_obstacle_areas()) def test_pads_in_one_zone_need_no_route_between_them(): from freeroute.route.grid_router import _connected_groups board = build_board(_zone_board()) pins = sorted(board.get_pins(), key=lambda p: p.component_ref) groups = _connected_groups(pins, board.get_conduction_areas()) assert [sorted(p.component_ref for p in g[0]) for g in groups] == [["P1", "P2"], ["P3"]] def test_a_pad_reaches_a_zone_under_a_no_tracks_area_by_a_via(): dsn = _zone_board() result, scale, names = route_dsn_board_exact(dsn) net = next(iter(result.routed_net_numbers)) assert result.result.vias.get(net) # it had to drop into the zone board = build_board(dsn) pour = next(a for a in board.get_obstacle_areas() if a.name == "pour") f_cu = names.index("F.Cu") for layer, pts in result.result.wires[net]: if layer == f_cu: # no track runs inside the no-tracks area assert not any(t.contains(p) and not t.bounding_box().is_empty() for t in pour.tiles for p in pts[1:-1]) def test_a_pour_other_nets_can_cross_is_not_trusted_to_connect(): # the same zone with no no-tracks area over it: its fill could be split by # other nets' tracks, so its pads are joined by routing, not assumed places = "(place P1 5500 -7000 front 0) (place P2 8500 -7000 front 0)" dsn = parse_dsn(_dsn(f"(component P {places})", "(net N (pins P1-1 P2-1))", _ZONE)) 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