diff --git a/README.md b/README.md index cc3c0a8..8df7dfc 100644 --- a/README.md +++ b/README.md @@ -137,9 +137,13 @@ SWIG API. Requires KiCad **10.0.1+**. series resistance carries no discretization error and no cell-size tuning is needed for thin traces. 1D-modeled traces show potential, power density, and |J| (the true in-trace density from the link - currents, |ΔV|/(ρ·Δl)). THT pad copper is part of the conductor - (exact shapes, see above); **SMD** pad copper other than the - selected contacts is still **not**. + currents, |ΔV|/(ρ·Δl)). Pad copper is part of the conductor: THT pad + shapes are stamped on every included layer (see above), **SMD** pad + shapes on their own layer (`INCLUDE_SMD_PADS`) — pads are the + junctions where traces and thermal-relief spokes actually meet, so + without them a multi-track junction necks down to the accidental + overlap of the track ends. Dead-end pads (component terminals) are + dropped with the other copper not connected to both contacts. - **Solder buildup on mask openings** (dialog checkbox, **off by default**; `INCLUDE_MASK_BUILDUP`): zones drawn on `F.Mask`/`B.Mask` are treated as mask openings that collect `SOLDER_THICKNESS_UM` diff --git a/fill_resistance/board_io.py b/fill_resistance/board_io.py index a55cc96..7bdaf21 100644 --- a/fill_resistance/board_io.py +++ b/fill_resistance/board_io.py @@ -563,6 +563,29 @@ def gather_barrels(board: Board, net_name: str, return barrels +def gather_smd_pad_copper(board: Board, net_name: str + ) -> dict[str, list[Polygon]]: + """layer name -> exact copper shape(s) of every SMD (undrilled) pad + on the net. Pads are junctions: traces and thermal-relief spokes + meet ON the pad copper, and without it the junction necks down to + the accidental overlap of the track ends - or is severed outright. + Dead-end pads (component terminals) become floating islands that + the solver's connectivity restriction drops. One API call per pad; + pads whose copper layer cannot be determined are skipped.""" + shapes: dict[str, list[Polygon]] = {} + for pad in board.get_pads(): + if pad.net is None or pad.net.name != net_name \ + or _pad_drill_nm(pad) > 0: + continue + layer = _pad_default_contact(pad) # SMD: its own copper layer + if layer == "all": + continue + polys = _pad_polygons(board, pad, layer) + if polys: + shapes.setdefault(layer, []).extend(polys) + return shapes + + def gather_tht_pad_copper(board: Board, net_name: str ) -> dict[tuple[int, int], list[Polygon]]: """(x, y) -> exact copper shape(s) of every drilled (THT) pad on the @@ -629,6 +652,19 @@ def build_problem(board: Board, net: str, layer_names: list[str], layer.polygons = list(layer.polygons) + extra print(f"{len(pad_shapes)} THT pad shape(s) stamped on every " f"included layer") + # SMD pad copper too: pads are the junctions where traces/spokes + # meet (also gives selected SMD-pad contacts their real copper) + smd_shapes = (gather_smd_pad_copper(board, net) + if config.INCLUDE_SMD_PADS else {}) + if smd_shapes: + n = 0 + for layer in layers: + polys = smd_shapes.get(layer.layer_name, []) + if polys: + layer.polygons = list(layer.polygons) + polys + n += len(polys) + if n: + print(f"{n} SMD pad shape(s) stamped on their layers") included = {l.layer_name for l in layers} buildup_list = [ SurfaceBuildup(layer_name=name, polygons=polys) diff --git a/fill_resistance/config.py b/fill_resistance/config.py index a320ff8..d05f8a0 100644 --- a/fill_resistance/config.py +++ b/fill_resistance/config.py @@ -31,6 +31,11 @@ CAP_PLATING_UM = 15.0 # cap plating thickness (fab spec) CAP_MAX_DRILL_MM = 0.5 # fab caps only small vias: drills above this # stay open even with VIAS_CAPPED # (dialog-settable) +INCLUDE_SMD_PADS = True # the net's SMD pad copper conducts too (exact + # shapes on the pad's layer): pads are the + # junctions where traces/spokes meet, and + # selected pad contacts get their real copper. + # Dead-end pads are dropped as floating islands INCLUDE_TH_PADS = True # plated through-hole pads stitch layers too; # their holes are modeled solder-filled (a # soldered component lead), so the solder core diff --git a/tests/test_tracks.py b/tests/test_tracks.py index 9799f54..26f2e6d 100644 --- a/tests/test_tracks.py +++ b/tests/test_tracks.py @@ -259,3 +259,25 @@ def test_track_unions_with_fill(): assert int(s_both.masks.sum()) > int(s_plate.masks.sum()) assert r_both.R_ohm < 0.75 * r_plate.R_ohm # bridge shortens the detour assert r_both.power_balance_rel < 1e-9 + + +def test_pad_copper_bridges_track_junction(): + """Two traces meet ON an SMD pad, their rounded ends 0.5 mm apart: + the junction only exists through the pad copper (board_io stamps + the net's pad shapes onto their layers). Without the pad the net + is severed - at both track models (rasterized and 1D chain).""" + from fill_resistance.errors import ConnectivityError + tabs = [[(0, 4.5), (1, 4.5), (1, 5.5), (0, 5.5)], + [(19, 4.5), (20, 4.5), (20, 5.5), (19, 5.5)]] + pad = [(9.25, 4.4), (10.75, 4.4), (10.75, 5.6), (9.25, 5.6)] + segs = [_seg([(0.5, 5), (9.5, 5)], 0.5), + _seg([(10.5, 5), (19.5, 5)], 0.5)] + r1, r2 = (0, 4.5, 1, 5.5), (19, 4.5, 20, 5.5) + + for h in (0.1, 0.25): # 5 cells: outlines; 2 cells: 1D chains + res, _ = _solve(_seg_problem(segs, r1, r2, fills_mm=tabs + [pad]), h) + # ~36 squares of 0.5 mm trace + tabs/pad: sanity-band the value + assert 0.007 < res.R_ohm < 0.011 + + with pytest.raises(ConnectivityError): + _solve(_seg_problem(segs, r1, r2, fills_mm=tabs), h)