Model SMD pad copper: exact net pad shapes stamped on their layers
Build PCM package / build (push) Successful in 6s

Pads are the junctions where traces and thermal-relief spokes actually
meet; without their copper a multi-track junction necks down to the
accidental overlap of the rounded track ends, or is severed outright.
gather_smd_pad_copper fetches the exact shape of every undrilled pad
on the net (one API call per pad, layer from the padstack) and
build_problem stamps them onto their own layer (INCLUDE_SMD_PADS).
Selected SMD-pad contacts get their real copper as a side effect
(previously electrode-shape intersected whatever lay underneath).
Dead-end pads become floating islands that the existing connectivity
restriction drops.

Closes the documented SMD-pad-copper limitation (prompted by the
padne comparison).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
janik
2026-07-17 13:37:06 +07:00
parent 38f8cdf7be
commit bc95b444b4
4 changed files with 70 additions and 3 deletions
+7 -3
View File
@@ -137,9 +137,13 @@ SWIG API. Requires KiCad **10.0.1+**.
series resistance carries no discretization error and no cell-size series resistance carries no discretization error and no cell-size
tuning is needed for thin traces. 1D-modeled traces show potential, tuning is needed for thin traces. 1D-modeled traces show potential,
power density, and |J| (the true in-trace density from the link power density, and |J| (the true in-trace density from the link
currents, |ΔV|/(ρ·Δl)). THT pad copper is part of the conductor currents, |ΔV|/(ρ·Δl)). Pad copper is part of the conductor: THT pad
(exact shapes, see above); **SMD** pad copper other than the shapes are stamped on every included layer (see above), **SMD** pad
selected contacts is still **not**. 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 - **Solder buildup on mask openings** (dialog checkbox, **off by
default**; `INCLUDE_MASK_BUILDUP`): zones drawn on `F.Mask`/`B.Mask` default**; `INCLUDE_MASK_BUILDUP`): zones drawn on `F.Mask`/`B.Mask`
are treated as mask openings that collect `SOLDER_THICKNESS_UM` are treated as mask openings that collect `SOLDER_THICKNESS_UM`
+36
View File
@@ -563,6 +563,29 @@ def gather_barrels(board: Board, net_name: str,
return barrels 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 def gather_tht_pad_copper(board: Board, net_name: str
) -> dict[tuple[int, int], list[Polygon]]: ) -> dict[tuple[int, int], list[Polygon]]:
"""(x, y) -> exact copper shape(s) of every drilled (THT) pad on the """(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 layer.polygons = list(layer.polygons) + extra
print(f"{len(pad_shapes)} THT pad shape(s) stamped on every " print(f"{len(pad_shapes)} THT pad shape(s) stamped on every "
f"included layer") 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} included = {l.layer_name for l in layers}
buildup_list = [ buildup_list = [
SurfaceBuildup(layer_name=name, polygons=polys) SurfaceBuildup(layer_name=name, polygons=polys)
+5
View File
@@ -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 CAP_MAX_DRILL_MM = 0.5 # fab caps only small vias: drills above this
# stay open even with VIAS_CAPPED # stay open even with VIAS_CAPPED
# (dialog-settable) # (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; INCLUDE_TH_PADS = True # plated through-hole pads stitch layers too;
# their holes are modeled solder-filled (a # their holes are modeled solder-filled (a
# soldered component lead), so the solder core # soldered component lead), so the solder core
+22
View File
@@ -259,3 +259,25 @@ def test_track_unions_with_fill():
assert int(s_both.masks.sum()) > int(s_plate.masks.sum()) 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.R_ohm < 0.75 * r_plate.R_ohm # bridge shortens the detour
assert r_both.power_balance_rel < 1e-9 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)