Model SMD pad copper: exact net pad shapes stamped on their layers
Build PCM package / build (push) Successful in 6s
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:
@@ -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`
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user