"""The grid must not believe copper legal that the exact check then refuses. Each case is one way the two used to disagree on roomba-ESP12E: a rotated oval pad modelled as an inflated box (and opened to its own net), a via shared by two connections of a net losing its footprint when one of them is ripped, and a wider net class searched as if it were the default width. """ from __future__ import annotations import math import pytest from freeroute.board import build_board from freeroute.dsn import parse_dsn from freeroute.geometry import IntBox, Simplex from freeroute.route.grid_router import GridRouter from freeroute.route.pipeline import ( _rule_clearance_dsn, _rule_width_dsn, _via_half_board, build_exact_routing_result, ) # --- rotated oval pads ------------------------------------------------------- def _socket_dsn(rotation: float, layout: str) -> str: """A 1x4 2.54 mm socket strip (rect pin 1, oval pins 2-4) at ``rotation``, each pin its own net, routed to a small SMD pad 0.9 mm away.""" cx, cy = 100000, -100000 t = math.radians(rotation) ax = (math.cos(t), math.sin(t)) perp = (-math.sin(t), math.cos(t)) places = [] for i in range(4): px, py = cx + 2540 * i * ax[0], cy + 2540 * i * ax[1] side = 1 if i % 2 == 0 else -1 if layout == "y": tx, ty = px, py + side * 9000 else: tx, ty = px + side * 8000 * perp[0], py + side * 8000 * perp[1] places.append(f"(place T{i + 1} {round(tx)} {round(ty)} front 0)") nets = " ".join(f"(net N{i} (pins P3-{i + 1} T{i + 1}-1))" for i in range(4)) return f"""(pcb socket (parser) (resolution um 10) (unit um) (structure (layer F.Cu (type signal)) (layer B.Cu (type signal)) (boundary (path pcb 0 125000 -115000 85000 -115000 85000 -75000 125000 -75000 125000 -115000)) (via "Via[0-1]_800:400_um") (rule (width 300) (clearance 250))) (placement (component S (place P3 {cx} {cy} front {rotation})) (component T {" ".join(places)})) (library (image S (pin R 1 0 0) (pin O 2 2540 0) (pin O 3 5080 0) (pin O 4 7620 0)) (image T (pin t 1 0 0)) (padstack O (shape (path F.Cu 1727.2 0 -152.4 0 152.4)) (shape (path B.Cu 1727.2 0 -152.4 0 152.4)) (attach off)) (padstack R (shape (rect F.Cu -863.6 -1016 863.6 1016)) (shape (rect B.Cu -863.6 -1016 863.6 1016)) (attach off)) (padstack t (shape (rect F.Cu -500 -500 500 500)) (attach off)) (padstack "Via[0-1]_800:400_um" (shape (circle F.Cu 800)) (shape (circle B.Cu 800)) (attach off))) (network {nets} (class c N0 N1 N2 N3 (circuit (use_via "Via[0-1]_800:400_um")) (rule (width 300) (clearance 250)))))""" def test_a_rotated_oval_pad_is_its_outline_not_its_bounding_box(): board = build_board(parse_dsn(_socket_dsn(60, "y"))) oval = next(p for p in board.get_pins() if p.component_ref == "P3" and p.name == "2") assert isinstance(oval.shape, Simplex) and not oval.shape.is_int_box() box = oval.shape.bounding_box() assert oval.shape.contains(oval.location) assert not oval.shape.contains(box.ll) and not oval.shape.contains(box.ur) @pytest.mark.parametrize("layout", ["y", "perp"]) def test_routes_leaving_a_rotated_socket_pass_the_exact_check(layout): result = build_exact_routing_result( parse_dsn(_socket_dsn(60, layout)), resolution=2, shove=True ) assert result.unrouted == [] # --- a via two connections share -------------------------------------------- _SHARED_VIA = """(pcb shared (parser) (resolution um 10) (unit um) (structure (layer Top (type signal)) (layer Bottom (type signal)) (boundary (path pcb 0 0 0 20000 0 20000 -20000 0 -20000 0 0)) (via "Via[0-1]_800:400_um") (rule (width 300) (clearance 250))) (library (padstack p (shape (rect Top -400 -400 400 400)) (attach off)) (padstack "Via[0-1]_800:400_um" (shape (circle Top 800)) (shape (circle Bottom 800)) (attach off)) (image PAD (pin p 1 0 0))) (placement (component PAD (place G1 3000 -5000 front 0) (place G2 3000 -15000 front 0) (place G3 17000 -10000 front 0) (place S1 3000 -10000 front 0) (place S2 17000 -3000 front 0))) (network (net GND (pins G1-1 G2-1 G3-1)) (net SIG (pins S1-1 S2-1))))""" def _line(a, b, layer): (x, y), (x1, y1) = a, b out = [(x, y, layer)] while x != x1: x += 1 if x1 > x else -1 out.append((x, y, layer)) while y != y1: y += 1 if y1 > y else -1 out.append((x, y, layer)) return out def test_a_shared_via_keeps_its_footprint_when_one_user_is_ripped(): dsn = parse_dsn(_SHARED_VIA) s = dsn.resolution.value router = GridRouter( build_board(dsn), trace_width=round(_rule_width_dsn(dsn) * s), clearance=round(_rule_clearance_dsn(dsn) * s), via_half=_via_half_board(dsn, s), ) conns = router._conns = router._build_connections() gnd = [c for c in conns if len({c.a.component_ref, c.b.component_ref} & {"G1", "G2", "G3"})] sig = next(c for c in conns if c not in gnd) first, second = gnd g3 = router._cell_of(next(p for p in router.board.get_pins() if p.component_ref == "G3").location) via = (g3[0] - 10, g3[1]) def through_via(conn): a, b = router._cell_of(conn.a.location), router._cell_of(conn.b.location) return _line(a, via, 0) + _line(via, b, 1) router._commit(first, through_via(first)) router._commit(second, through_via(second)) footprint = [c for c in router._via_cells(via) if c != via] router._rip(first) assert second.routed and via in second.via_cells assert all(router._via_claims.get(c) == [second.index] for c in footprint) # another net still keeps its distance from the via that is left assert all(router._enter_cost_base(c, 0, sig.net_no, False, conns) is None for c in footprint) def test_a_route_through_a_shared_via_rips_every_user(): """Two connections of a net share a via. Another net routed through it in rip-up mode has to rip both, or the one left keeps its via under the new trace and the exact check throws one of them out.""" dsn = parse_dsn(_SHARED_VIA) s = dsn.resolution.value router = GridRouter( build_board(dsn), trace_width=round(_rule_width_dsn(dsn) * s), clearance=round(_rule_clearance_dsn(dsn) * s), via_half=_via_half_board(dsn, s), ) conns = router._conns = router._build_connections() gnd = [c for c in conns if len({c.a.component_ref, c.b.component_ref} & {"G1", "G2", "G3"})] sig = next(c for c in conns if c not in gnd) first, second = gnd g3 = router._cell_of(next(p for p in router.board.get_pins() if p.component_ref == "G3").location) via = (g3[0] - 10, g3[1]) def through_via(conn): a, b = router._cell_of(conn.a.location), router._cell_of(conn.b.location) return _line(a, via, 0) + _line(via, b, 1) router._commit(first, through_via(first)) router._commit(second, through_via(second)) # the via's copper alone: footprint cells no trace or trace halo covers crossing = [ node for c in router._via_cells(via) for node in ((*c, 0), (*c, 1)) if node not in router._cell_owner and node not in router._halo ] assert crossing assert router._crossed_owners(crossing, sig.net_no, conns) == {first.index, second.index} # --- a wider net class -------------------------------------------------------- _WIDE = """(pcb wide (parser) (resolution um 10) (unit um) (structure (layer F.Cu (type signal)) (boundary (path pcb 0 0 0 20000 0 20000 -10000 0 -10000 0 0)) (rule (width 300) (clearance 250))) (placement (component S (place P1 2000 -5000 front 0) (place P2 18000 -5000 front 0) (place Q1 10000 {ylo} front 0) (place Q2 10000 {yhi} front 0) (place Q3 10000 -1000 front 0) (place Q4 10000 -9000 front 0))) (library (image S (pin pad 1 0 0)) (padstack pad (shape (rect F.Cu -600 -600 600 600)) (attach off))) (network (net PWR (pins P1-1 P2-1)) (net SIG (pins Q1-1 Q3-1)) (net SIG2 (pins Q2-1 Q4-1)) (class Power PWR (rule (width 500) (clearance 250)))))""" @pytest.mark.parametrize("resolution", [1, 2]) @pytest.mark.parametrize("gap", [900, 1000, 1075]) def test_a_wide_net_past_pads_it_does_not_fit_between_is_not_refused(gap, resolution): # 0.3 mm fits the gap between the two pads, 0.5 mm does not: the grid # must see that, rather than lay it wide and have the exact check drop it ylo, yhi = -5000 - gap // 2 - 600, -5000 + gap // 2 + 600 result = build_exact_routing_result( parse_dsn(_WIDE.format(ylo=ylo, yhi=yhi)), resolution=resolution ) assert result.unrouted == [] def _wide_router(): dsn = parse_dsn(_WIDE.format(ylo=-6050, yhi=-3950)) s = dsn.resolution.value pwr = next(i for i, net in enumerate(dsn.nets, 1) if net.name == "PWR") return GridRouter( build_board(dsn), trace_width=round(_rule_width_dsn(dsn) * s), clearance=round(_rule_clearance_dsn(dsn) * s), resolution=2, net_widths={pwr: 5000}, ), pwr def test_a_wide_trace_keeps_clear_of_a_default_one_at_the_grid_diagonal(): router, _pwr = _wide_router() # 0.5 mm against 0.3 mm needs 0.25 + 0.25 + 0.15 = 0.65 mm between centres, # on either axis, since the exact check compares boxes need = 2500 + router.clearance + router.half_width for dx, dy in [(2, 2), (2, 1), (1, 2)]: assert max(dx, dy) * router.step < need assert (dx, dy) in router._halo_for(2500) def test_a_wide_rip_up_route_counts_what_its_extra_width_crosses(): router, pwr_net = _wide_router() conns = router._conns = router._build_connections() pwr = next(c for c in conns if c.net_no == pwr_net) other = next(c for c in conns if c.net_no != pwr_net) # a default-width trace two cells from the wide route's path x, y = 20, 5 router._commit(other, _line((x - 3, y + 2), (x + 3, y + 2), 0)) path = _line((x - 3, y), (x + 3, y), 0) spread = router._spread_for(2500) assert other.index not in router._crossed_owners(path, pwr.net_no, conns) assert other.index in router._crossed_owners(path, pwr.net_no, conns, spread) def test_a_pad_cell_check_uses_the_rotated_outline(): board = build_board(parse_dsn(_socket_dsn(60, "y"))) router = GridRouter(board, trace_width=3000, clearance=2500, resolution=2) oval = next(p for p in board.get_pins() if p.component_ref == "P3" and p.name == "2") box = oval.shape.bounding_box() margin = router.clearance + router.half_width exact = set(router._pad_cells(oval.shape, box, margin)) boxed = set(router._cells_in_box(box, margin)) assert exact < boxed for cell in boxed - exact: c = router._cell_center(cell) square = IntBox(c.x - margin, c.y - margin, c.x + margin, c.y + margin) assert oval.shape.intersection(square).dimension() != 2 def test_a_via_by_a_chamfered_corner_is_judged_round(): """The grid keeps a via's centre its radius plus the edge clearance from the outline. The exact check used the via's square, whose corner reaches past a 45-degree chamfer where the round via does not.""" from freeroute.board.outline import BoardOutline from freeroute.geometry import IntBox, IntPoint from freeroute.route.exact_router import _inside_outline # a 10 mm square with its lower-left corner cut at 45 degrees outline = BoardOutline([IntPoint(*p) for p in [(30, 0), (100, 0), (100, 100), (0, 100), (0, 30)]]) half, edge = 4, 1 via = IntPoint(19, 20) # its centre 6.4 from the chamfer x + y = 30 square = IntBox(via.x - half, via.y - half, via.x + half, via.y + half) assert not outline.box_clear(square, edge) # the square's corner: 0.7 away assert _inside_outline(outline, [], [via], half + edge, edge) # 4.2 from the chamfer: the round via itself is too close assert not _inside_outline(outline, [], [IntPoint(18, 18)], half + edge, edge)