Full solder joint at every populated THT pad, read from KiCad
Build PCM package / build (push) Successful in 8s
Build PCM package / build (push) Successful in 8s
No size-threshold guessing: whether a hole is a via or a THT pad already comes from KiCad (board.get_vias vs drilled board.get_pads). Every populated THT pad of the net now carries the complete joint the contacts got: solder-filled barrel, average-thickness coat over a pad-diameter disc on the outer layers, and the protruding-lead cone on the side opposite its owning footprint. The footprint side and the Do-not-populate flag are read from KiCad (footprint pads store absolute positions, so owner lookup is an exact (x, y, number) map); DNP pads stay plating-only with no joint. Contact pads are deduplicated by barrel center so their cone/coat is never applied twice. Barrels are now gathered in single-layer runs too: via rings and drill mouths perforate a lone plane, THT joints stiffen it locally. ViaLink gains solder_filled + protrusion_side (legacy dumps load with the old every-THT-pad-filled semantics). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -85,11 +85,18 @@ SWIG API. Requires KiCad **10.0.1+**.
|
|||||||
only affects in-plane conduction across outer-layer mouths. Sub-cell
|
only affects in-plane conduction across outer-layer mouths. Sub-cell
|
||||||
mouths scale their cells' sheet conductance by the true covered
|
mouths scale their cells' sheet conductance by the true covered
|
||||||
fraction (4×4 supersampling), so coarse grids see the correct small
|
fraction (4×4 supersampling), so coarse grids see the correct small
|
||||||
perturbation instead of a whole-cell hole. THT-pad copper and drills
|
perturbation instead of a whole-cell hole. Barrels are gathered in
|
||||||
remain outside the model, but THT-pad **barrels are solder-filled**
|
**single-layer runs too** (drill mouths perforate a lone plane).
|
||||||
(a soldered component lead): the solder core (SAC305) conducts in
|
THT-pad copper and drills remain outside the model, but every
|
||||||
parallel with the plating annulus. At f > 0 the thickness scaling is
|
**populated THT pad** of the net carries its full **soldered
|
||||||
applied multiplicatively to the skin-corrected sheet conductance
|
joint**: a solder-filled barrel (SAC305 core in parallel with the
|
||||||
|
plating), the average-thickness solder coat over a pad-diameter disc
|
||||||
|
on the outer layers, and the protruding-lead cone on the side
|
||||||
|
opposite its footprint (see barrel contacts below). Whether a hole
|
||||||
|
is a via or a THT pad, the owning footprint's side, and its **Do not
|
||||||
|
populate** flag are all read from KiCad — DNP pads stay plating-only
|
||||||
|
with no joint. At f > 0 the thickness scaling is applied
|
||||||
|
multiplicatively to the skin-corrected sheet conductance
|
||||||
(approximation). Per layer a barrel attaches to
|
(approximation). Per layer a barrel attaches to
|
||||||
the fill cell under it, or to the nearest copper cell within the pad
|
the fill cell under it, or to the nearest copper cell within the pad
|
||||||
footprint plus one grid cell — fills joined by **thermal-relief
|
footprint plus one grid cell — fills joined by **thermal-relief
|
||||||
|
|||||||
+81
-37
@@ -23,7 +23,8 @@ from . import config
|
|||||||
from .errors import ApiVersionError, CandidateError, SelectionError
|
from .errors import ApiVersionError, CandidateError, SelectionError
|
||||||
from .geometry import (Electrode, LayerFill, Polygon, Problem, Rect,
|
from .geometry import (Electrode, LayerFill, Polygon, Problem, Rect,
|
||||||
SurfaceBuildup, TrackSeg, ViaLink,
|
SurfaceBuildup, TrackSeg, ViaLink,
|
||||||
contact_solder_buildups, linearize_ring)
|
contact_solder_buildups, linearize_ring,
|
||||||
|
tht_joint_buildups)
|
||||||
|
|
||||||
MASK_TO_COPPER = {"F.Mask": "F.Cu", "B.Mask": "B.Cu"}
|
MASK_TO_COPPER = {"F.Mask": "F.Cu", "B.Mask": "B.Cu"}
|
||||||
|
|
||||||
@@ -180,29 +181,42 @@ def _pad_polygons(board: Board, pad: Pad, contact: str) -> list[Polygon] | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _tht_protrusion_side(pad: Pad, footprints) -> str:
|
def _footprint_pad_map(footprints) -> dict:
|
||||||
"""Outer layer where the clipped THT lead protrudes (tent + solder
|
"""(x, y, number) -> owning FootprintInstance. Footprint pads are
|
||||||
cone): the side OPPOSITE the component. Footprint pads are stored
|
stored with absolute positions, so the lookup is exact."""
|
||||||
with absolute positions, so the owning footprint is matched by pad
|
out = {}
|
||||||
number + position. Unknown owner -> assume the component sits on
|
for fp in footprints or []:
|
||||||
F.Cu (lead tents on B.Cu)."""
|
try:
|
||||||
try:
|
|
||||||
for fp in footprints or []:
|
|
||||||
for fpad in fp.definition.pads:
|
for fpad in fp.definition.pads:
|
||||||
if fpad.number == pad.number \
|
out[(fpad.position.x, fpad.position.y, fpad.number)] = fp
|
||||||
and fpad.position.x == pad.position.x \
|
except Exception:
|
||||||
and fpad.position.y == pad.position.y:
|
continue
|
||||||
side = canonical_name(fp.layer)
|
return out
|
||||||
return "F.Cu" if side == "B.Cu" else "B.Cu"
|
|
||||||
except Exception:
|
|
||||||
pass
|
def _pad_owner(pad: Pad, pad_map: dict):
|
||||||
print(f"note: no footprint found for pad {pad.number} - assuming its "
|
return pad_map.get((pad.position.x, pad.position.y, pad.number))
|
||||||
f"lead protrudes on B.Cu")
|
|
||||||
|
|
||||||
|
def _tht_protrusion_side(pad: Pad, pad_map: dict, quiet: bool = False) -> str:
|
||||||
|
"""Outer layer where the clipped THT lead protrudes (tent + solder
|
||||||
|
cone): the side OPPOSITE the component. Unknown owner -> assume the
|
||||||
|
component sits on F.Cu (lead tents on B.Cu)."""
|
||||||
|
fp = _pad_owner(pad, pad_map)
|
||||||
|
if fp is not None:
|
||||||
|
try:
|
||||||
|
side = canonical_name(fp.layer)
|
||||||
|
return "F.Cu" if side == "B.Cu" else "B.Cu"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if not quiet:
|
||||||
|
print(f"note: no footprint found for pad {pad.number} - assuming "
|
||||||
|
f"its lead protrudes on B.Cu")
|
||||||
return "B.Cu"
|
return "B.Cu"
|
||||||
|
|
||||||
|
|
||||||
def _to_electrode(board: Board, item, stackup: StackupInfo | None = None,
|
def _to_electrode(board: Board, item, stackup: StackupInfo | None = None,
|
||||||
footprints=None) -> Electrode:
|
pad_map: dict | None = None) -> Electrode:
|
||||||
if isinstance(item, BoardRectangle):
|
if isinstance(item, BoardRectangle):
|
||||||
tl, br = item.top_left, item.bottom_right
|
tl, br = item.top_left, item.bottom_right
|
||||||
rect = Rect.normalized(tl.x, tl.y, br.x, br.y,
|
rect = Rect.normalized(tl.x, tl.y, br.x, br.y,
|
||||||
@@ -245,7 +259,7 @@ def _to_electrode(board: Board, item, stackup: StackupInfo | None = None,
|
|||||||
drill_nm=drill, pad_nm=_padstack_pad_nm(pad),
|
drill_nm=drill, pad_nm=_padstack_pad_nm(pad),
|
||||||
center=(pad.position.x, pad.position.y),
|
center=(pad.position.x, pad.position.y),
|
||||||
solder=drill > 0,
|
solder=drill > 0,
|
||||||
protrusion_side=(_tht_protrusion_side(pad, footprints)
|
protrusion_side=(_tht_protrusion_side(pad, pad_map or {})
|
||||||
if drill > 0 else None))
|
if drill > 0 else None))
|
||||||
|
|
||||||
|
|
||||||
@@ -282,9 +296,9 @@ def get_electrodes(board: Board, stackup: StackupInfo | None = None
|
|||||||
rects = [s for s in selection if isinstance(s, BoardRectangle)]
|
rects = [s for s in selection if isinstance(s, BoardRectangle)]
|
||||||
pads = [s for s in selection if isinstance(s, (Pad, Via))]
|
pads = [s for s in selection if isinstance(s, (Pad, Via))]
|
||||||
# protrusion-side lookup needs the owning footprints (THT pads only)
|
# protrusion-side lookup needs the owning footprints (THT pads only)
|
||||||
footprints = (board.get_footprints()
|
pad_map = (_footprint_pad_map(board.get_footprints())
|
||||||
if any(isinstance(s, Pad) and _pad_drill_nm(s) > 0
|
if any(isinstance(s, Pad) and _pad_drill_nm(s) > 0
|
||||||
for s in pads) else None)
|
for s in pads) else {})
|
||||||
|
|
||||||
if not selection:
|
if not selection:
|
||||||
allr = [s for s in board.get_shapes() if isinstance(s, BoardRectangle)]
|
allr = [s for s in board.get_shapes() if isinstance(s, BoardRectangle)]
|
||||||
@@ -320,7 +334,7 @@ def get_electrodes(board: Board, stackup: StackupInfo | None = None
|
|||||||
f"only for a side that has none."
|
f"only for a side that has none."
|
||||||
)
|
)
|
||||||
if pads:
|
if pads:
|
||||||
pad_parts = [_to_electrode(board, p, stackup, footprints)
|
pad_parts = [_to_electrode(board, p, stackup, pad_map)
|
||||||
for p in pads]
|
for p in pads]
|
||||||
if not es1:
|
if not es1:
|
||||||
es1 = pad_parts
|
es1 = pad_parts
|
||||||
@@ -335,8 +349,8 @@ def get_electrodes(board: Board, stackup: StackupInfo | None = None
|
|||||||
|
|
||||||
items = rects + pads
|
items = rects + pads
|
||||||
if len(items) == 2:
|
if len(items) == 2:
|
||||||
return ([_to_electrode(board, items[0], stackup, footprints)],
|
return ([_to_electrode(board, items[0], stackup, pad_map)],
|
||||||
[_to_electrode(board, items[1], stackup, footprints)],
|
[_to_electrode(board, items[1], stackup, pad_map)],
|
||||||
_net_hint_of(pads))
|
_net_hint_of(pads))
|
||||||
raise SelectionError(
|
raise SelectionError(
|
||||||
f"The selection has {len(rects)} rectangle(s) (none on the marker "
|
f"The selection has {len(rects)} rectangle(s) (none on the marker "
|
||||||
@@ -503,16 +517,35 @@ def gather_barrels(board: Board, net_name: str,
|
|||||||
z_bot_nm=z_bot, kind="via",
|
z_bot_nm=z_bot, kind="via",
|
||||||
pad_nm=_padstack_pad_nm(via)))
|
pad_nm=_padstack_pad_nm(via)))
|
||||||
if config.INCLUDE_TH_PADS:
|
if config.INCLUDE_TH_PADS:
|
||||||
for pad in board.get_pads():
|
net_pads = [pad for pad in board.get_pads()
|
||||||
if pad.net is None or pad.net.name != net_name:
|
if pad.net is not None and pad.net.name == net_name
|
||||||
continue
|
and _pad_drill_nm(pad) > 0]
|
||||||
drill = _pad_drill_nm(pad)
|
# populated (non-DNP) THT pads carry a soldered joint: filled
|
||||||
if drill <= 0:
|
# hole + coat + lead cone on the side opposite the component
|
||||||
continue
|
pad_map = (_footprint_pad_map(board.get_footprints())
|
||||||
barrels.append(ViaLink(x=pad.position.x, y=pad.position.y,
|
if net_pads else {})
|
||||||
drill_nm=drill, z_top_nm=-1,
|
unknown = 0
|
||||||
z_bot_nm=stackup.z_bot_nm + 1, kind="pad",
|
for pad in net_pads:
|
||||||
pad_nm=_padstack_pad_nm(pad)))
|
fp = _pad_owner(pad, pad_map)
|
||||||
|
unknown += fp is None
|
||||||
|
populated = True
|
||||||
|
if fp is not None:
|
||||||
|
try:
|
||||||
|
populated = not fp.attributes.do_not_populate
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
barrels.append(ViaLink(
|
||||||
|
x=pad.position.x, y=pad.position.y,
|
||||||
|
drill_nm=_pad_drill_nm(pad), z_top_nm=-1,
|
||||||
|
z_bot_nm=stackup.z_bot_nm + 1, kind="pad",
|
||||||
|
pad_nm=_padstack_pad_nm(pad),
|
||||||
|
solder_filled=populated,
|
||||||
|
protrusion_side=(_tht_protrusion_side(pad, pad_map,
|
||||||
|
quiet=True)
|
||||||
|
if populated else None)))
|
||||||
|
if unknown:
|
||||||
|
print(f"note: {unknown} THT pad(s) without an identifiable "
|
||||||
|
f"footprint - assumed populated, leads on B.Cu")
|
||||||
return barrels
|
return barrels
|
||||||
|
|
||||||
|
|
||||||
@@ -550,7 +583,9 @@ def build_problem(board: Board, net: str, layer_names: list[str],
|
|||||||
f"Net {net} has no fill on any of the selected layers "
|
f"Net {net} has no fill on any of the selected layers "
|
||||||
f"({', '.join(layer_names)})."
|
f"({', '.join(layer_names)})."
|
||||||
)
|
)
|
||||||
vias = gather_barrels(board, net, stackup) if len(layers) > 1 else []
|
# barrels matter on a single layer too: via rings + drill mouths
|
||||||
|
# perforate the plane, THT joints locally stiffen it
|
||||||
|
vias = gather_barrels(board, net, stackup)
|
||||||
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)
|
||||||
@@ -597,6 +632,15 @@ def build_problem(board: Board, net: str, layer_names: list[str],
|
|||||||
print(f"THT contact(s): solder-filled hole + "
|
print(f"THT contact(s): solder-filled hole + "
|
||||||
f"{config.SOLDER_THICKNESS_UM:g} um average solder coat on the "
|
f"{config.SOLDER_THICKNESS_UM:g} um average solder coat on the "
|
||||||
f"pad face ({', '.join(solder_layers)}){cone}")
|
f"pad face ({', '.join(solder_layers)}){cone}")
|
||||||
|
tht_joint_buildups(problem)
|
||||||
|
n_joint = sum(1 for v in problem.vias
|
||||||
|
if v.kind == "pad" and v.solder_filled)
|
||||||
|
n_dnp = sum(1 for v in problem.vias
|
||||||
|
if v.kind == "pad" and not v.solder_filled)
|
||||||
|
if n_joint or n_dnp:
|
||||||
|
print(f"{n_joint} populated THT pad joint(s): solder-filled hole + "
|
||||||
|
f"coat + lead cone"
|
||||||
|
+ (f"; {n_dnp} DNP pad(s) plating-only" if n_dnp else ""))
|
||||||
return problem
|
return problem
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -134,6 +134,14 @@ class ViaLink:
|
|||||||
z_bot_nm: int
|
z_bot_nm: int
|
||||||
kind: str = "via" # "via" | "pad"
|
kind: str = "via" # "via" | "pad"
|
||||||
pad_nm: int = 0 # pad/annular diameter; 0 = unknown
|
pad_nm: int = 0 # pad/annular diameter; 0 = unknown
|
||||||
|
solder_filled: bool = False # populated THT pad: the hole is
|
||||||
|
# solder-filled (core in parallel
|
||||||
|
# with the plating); False for
|
||||||
|
# vias and DNP footprints
|
||||||
|
protrusion_side: str | None = None # populated THT pad: outer layer
|
||||||
|
# where the clipped lead tents
|
||||||
|
# (solder cone), opposite the
|
||||||
|
# component side
|
||||||
|
|
||||||
def spans(self, z_nm: int) -> bool:
|
def spans(self, z_nm: int) -> bool:
|
||||||
return self.z_top_nm - 1 <= z_nm <= self.z_bot_nm + 1
|
return self.z_top_nm - 1 <= z_nm <= self.z_bot_nm + 1
|
||||||
@@ -227,6 +235,38 @@ def contact_solder_buildups(problem: Problem) -> list[str]:
|
|||||||
return sorted(set(touched))
|
return sorted(set(touched))
|
||||||
|
|
||||||
|
|
||||||
|
def _disc_polygon(x_nm: float, y_nm: float, r_nm: float,
|
||||||
|
n: int = 32) -> Polygon:
|
||||||
|
th = np.linspace(0.0, 2.0 * math.pi, n, endpoint=False)
|
||||||
|
return Polygon(outline=np.round(np.stack(
|
||||||
|
[x_nm + r_nm * np.cos(th), y_nm + r_nm * np.sin(th)],
|
||||||
|
axis=1)).astype(np.int64))
|
||||||
|
|
||||||
|
|
||||||
|
def tht_joint_buildups(problem: Problem) -> list[str]:
|
||||||
|
"""Solder coat of the net's populated STITCHING through-hole pads
|
||||||
|
(ViaLink kind 'pad' with solder_filled): one pad-diameter disc per
|
||||||
|
outer layer - the exact pad shape is unknown for non-contact pads,
|
||||||
|
and the coat intersects the modeled copper at raster time anyway.
|
||||||
|
Contact pads are skipped: contact_solder_buildups already coats
|
||||||
|
them with the exact pad shape. Returns the affected layer names."""
|
||||||
|
included = {l.layer_name for l in problem.layers}
|
||||||
|
outer = [n for n in ("F.Cu", "B.Cu") if n in included]
|
||||||
|
contacts = {e.center for e in problem.electrodes1 + problem.electrodes2
|
||||||
|
if e.drill_nm > 0 and e.center is not None}
|
||||||
|
touched = []
|
||||||
|
for v in problem.vias:
|
||||||
|
if v.kind != "pad" or not v.solder_filled \
|
||||||
|
or v.pad_nm <= v.drill_nm or (v.x, v.y) in contacts:
|
||||||
|
continue
|
||||||
|
disc = _disc_polygon(v.x, v.y, v.pad_nm / 2.0)
|
||||||
|
for name in outer:
|
||||||
|
problem.buildups.append(
|
||||||
|
SurfaceBuildup(layer_name=name, polygons=[disc]))
|
||||||
|
touched.append(name)
|
||||||
|
return sorted(set(touched))
|
||||||
|
|
||||||
|
|
||||||
def _arc_params(start, mid, end) -> tuple[float, float, float, float, float] | None:
|
def _arc_params(start, mid, end) -> tuple[float, float, float, float, float] | None:
|
||||||
"""Circle through three points: (cx, cy, r, a0, sweep) with a0 the
|
"""Circle through three points: (cx, cy, r, a0, sweep) with a0 the
|
||||||
start angle and sweep signed; None if the points are collinear."""
|
start angle and sweep signed; None if the points are collinear."""
|
||||||
@@ -485,7 +525,11 @@ def problem_from_json(d: dict) -> Problem:
|
|||||||
ViaLink(x=int(vd["x"]), y=int(vd["y"]), drill_nm=int(vd["drill_nm"]),
|
ViaLink(x=int(vd["x"]), y=int(vd["y"]), drill_nm=int(vd["drill_nm"]),
|
||||||
z_top_nm=int(vd["z_top_nm"]), z_bot_nm=int(vd["z_bot_nm"]),
|
z_top_nm=int(vd["z_top_nm"]), z_bot_nm=int(vd["z_bot_nm"]),
|
||||||
kind=vd.get("kind", "via"),
|
kind=vd.get("kind", "via"),
|
||||||
pad_nm=int(vd.get("pad_nm", 0)))
|
pad_nm=int(vd.get("pad_nm", 0)),
|
||||||
|
# older dumps: every THT pad counted as solder-filled
|
||||||
|
solder_filled=bool(vd.get(
|
||||||
|
"solder_filled", vd.get("kind", "via") == "pad")),
|
||||||
|
protrusion_side=vd.get("protrusion_side"))
|
||||||
for vd in d["vias"]
|
for vd in d["vias"]
|
||||||
],
|
],
|
||||||
electrodes1=(
|
electrodes1=(
|
||||||
|
|||||||
@@ -238,9 +238,10 @@ def rasterize_stack(problem: Problem, h_nm: float) -> RasterStack:
|
|||||||
|
|
||||||
|
|
||||||
def _paint_lead_fillets(stack: RasterStack, problem: Problem) -> None:
|
def _paint_lead_fillets(stack: RasterStack, problem: Problem) -> None:
|
||||||
"""Protruding THT leads of soldered barrel contacts: the clipped
|
"""Protruding THT leads (barrel contacts AND the net's populated
|
||||||
lead sticks tht_protrusion_nm out of the hole on the side opposite
|
stitching through-hole pads): the clipped lead sticks
|
||||||
the component, wrapped by a solder cone - full protrusion height at
|
tht_protrusion_nm out of the hole on the side opposite the
|
||||||
|
component, wrapped by a solder cone - full protrusion height at
|
||||||
the drill wall, tapering linearly to zero at the pad edge. Modeled
|
the drill wall, tapering linearly to zero at the pad edge. Modeled
|
||||||
as extra conduction-equivalent copper via stack.thick_scale: the
|
as extra conduction-equivalent copper via stack.thick_scale: the
|
||||||
tall solder column next to the wall pulls those cells to lead
|
tall solder column next to the wall pulls those cells to lead
|
||||||
@@ -254,18 +255,32 @@ def _paint_lead_fillets(stack: RasterStack, problem: Problem) -> None:
|
|||||||
ny, nx = stack.shape2d
|
ny, nx = stack.shape2d
|
||||||
h = stack.h_nm
|
h = stack.h_nm
|
||||||
index = {name: li for li, name in enumerate(stack.layer_names)}
|
index = {name: li for li, name in enumerate(stack.layer_names)}
|
||||||
|
|
||||||
|
# one cone per joint: contact electrodes first (exact data), then the
|
||||||
|
# net's populated stitching THT pads, skipping the contacts' barrels
|
||||||
|
jobs = []
|
||||||
|
seen = set()
|
||||||
for e in problem.electrodes1 + problem.electrodes2:
|
for e in problem.electrodes1 + problem.electrodes2:
|
||||||
if not (e.solder and e.drill_nm > 0 and e.protrusion_side):
|
if e.drill_nm <= 0:
|
||||||
continue
|
|
||||||
li = index.get(e.protrusion_side)
|
|
||||||
if li is None or e.pad_nm <= e.drill_nm:
|
|
||||||
continue
|
continue
|
||||||
if e.center is not None:
|
if e.center is not None:
|
||||||
x, y = e.center
|
x, y = e.center
|
||||||
else:
|
else:
|
||||||
x = (e.rect.x0 + e.rect.x1) / 2.0
|
x = (e.rect.x0 + e.rect.x1) / 2.0
|
||||||
y = (e.rect.y0 + e.rect.y1) / 2.0
|
y = (e.rect.y0 + e.rect.y1) / 2.0
|
||||||
ra, rb = e.drill_nm / 2.0, e.pad_nm / 2.0
|
seen.add((int(x), int(y)))
|
||||||
|
if e.solder and e.protrusion_side:
|
||||||
|
jobs.append((x, y, e.drill_nm, e.pad_nm, e.protrusion_side))
|
||||||
|
for v in problem.vias:
|
||||||
|
if v.kind == "pad" and v.solder_filled and v.protrusion_side \
|
||||||
|
and (v.x, v.y) not in seen:
|
||||||
|
jobs.append((v.x, v.y, v.drill_nm, v.pad_nm, v.protrusion_side))
|
||||||
|
|
||||||
|
for x, y, drill_nm, pad_nm, side in jobs:
|
||||||
|
li = index.get(side)
|
||||||
|
if li is None or pad_nm <= drill_nm:
|
||||||
|
continue
|
||||||
|
ra, rb = drill_nm / 2.0, pad_nm / 2.0
|
||||||
j0 = max(0, math.floor((x - rb - stack.x0_nm) / h))
|
j0 = max(0, math.floor((x - rb - stack.x0_nm) / h))
|
||||||
j1 = min(nx, math.floor((x + rb - stack.x0_nm) / h) + 1)
|
j1 = min(nx, math.floor((x + rb - stack.x0_nm) / h) + 1)
|
||||||
i0 = max(0, math.floor((y - rb - stack.y0_nm) / h))
|
i0 = max(0, math.floor((y - rb - stack.y0_nm) / h))
|
||||||
|
|||||||
@@ -178,12 +178,13 @@ def _barrel_links(stack: RasterStack, problem: Problem
|
|||||||
length = problem.layers[lb].z_nm - problem.layers[la].z_nm
|
length = problem.layers[lb].z_nm - problem.layers[la].z_nm
|
||||||
if length <= 0:
|
if length <= 0:
|
||||||
continue
|
continue
|
||||||
# THT pads carry a soldered component lead: the hole is
|
# populated THT pads carry a soldered component lead: the hole
|
||||||
# solder-filled, the core conducts in parallel with the plating
|
# is solder-filled, the core conducts in parallel with the
|
||||||
|
# plating (DNP pads and vias stay plating-only)
|
||||||
r_dc = via.barrel_resistance(
|
r_dc = via.barrel_resistance(
|
||||||
length, problem.rho_ohm_m, problem.plating_nm,
|
length, problem.rho_ohm_m, problem.plating_nm,
|
||||||
solder_rho_ohm_m=(problem.solder_rho_ohm_m
|
solder_rho_ohm_m=(problem.solder_rho_ohm_m
|
||||||
if via.kind == "pad" else None))
|
if via.solder_filled else None))
|
||||||
links.append((vi, la, ia, ja, lb, ib, jb, r_dc))
|
links.append((vi, la, ia, ja, lb, ib, jb, r_dc))
|
||||||
return links, dead
|
return links, dead
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import pytest
|
|||||||
from fill_resistance import raster, solver
|
from fill_resistance import raster, solver
|
||||||
from fill_resistance.geometry import (Electrode, Polygon, ViaLink,
|
from fill_resistance.geometry import (Electrode, Polygon, ViaLink,
|
||||||
contact_solder_buildups, load_problem,
|
contact_solder_buildups, load_problem,
|
||||||
save_problem)
|
problem_from_json, problem_to_json,
|
||||||
|
save_problem, tht_joint_buildups)
|
||||||
from tests.util import NM, make_problem, rect_mm, ring_mm
|
from tests.util import NM, make_problem, rect_mm, ring_mm
|
||||||
|
|
||||||
PLATE20 = [(0, 0), (20, 0), (20, 20), (0, 20)]
|
PLATE20 = [(0, 0), (20, 0), (20, 20), (0, 20)]
|
||||||
@@ -195,6 +196,72 @@ def test_lead_fillet_lowers_resistance(monkeypatch):
|
|||||||
assert r_ada.R_ohm == pytest.approx(r_cone.R_ohm, rel=2e-3)
|
assert r_ada.R_ohm == pytest.approx(r_cone.R_ohm, rel=2e-3)
|
||||||
|
|
||||||
|
|
||||||
|
def _pad_link(populated=True):
|
||||||
|
return ViaLink(x=10 * NM, y=10 * NM, drill_nm=1_000_000, z_top_nm=-1,
|
||||||
|
z_bot_nm=1, kind="pad", pad_nm=2_400_000,
|
||||||
|
solder_filled=populated,
|
||||||
|
protrusion_side="F.Cu" if populated else None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stitching_pad_joint():
|
||||||
|
"""A populated THT pad on the net (not a contact) gets the full
|
||||||
|
joint: coat discs on the outer layers and a cone on its protrusion
|
||||||
|
side; a DNP pad gets neither."""
|
||||||
|
def prob(populated=True):
|
||||||
|
p = make_problem([(PLATE20, [])],
|
||||||
|
rect1_mm=(0, 0, 1, 20), rect2_mm=(19, 0, 20, 20))
|
||||||
|
p.vias = [_pad_link(populated)]
|
||||||
|
return p
|
||||||
|
|
||||||
|
p = prob()
|
||||||
|
assert tht_joint_buildups(p) == ["F.Cu"]
|
||||||
|
assert len(p.buildups) == 1
|
||||||
|
r_joint, stack = _solve(p, 0.1)
|
||||||
|
assert stack.thick_scale is not None and stack.thick_scale.max() > 3.0
|
||||||
|
assert stack.buildup is not None and stack.buildup.any()
|
||||||
|
|
||||||
|
q = prob(populated=False)
|
||||||
|
assert tht_joint_buildups(q) == []
|
||||||
|
r_bare, s2 = _solve(q, 0.1)
|
||||||
|
assert s2.thick_scale is None and s2.buildup is None
|
||||||
|
assert r_joint.R_ohm < r_bare.R_ohm
|
||||||
|
|
||||||
|
|
||||||
|
def test_cone_not_doubled_at_contact():
|
||||||
|
"""A contact THT pad also appears in the net's pad list (ViaLink):
|
||||||
|
the cone and coat must be applied once, not squared/stacked."""
|
||||||
|
p = make_problem([(PLATE20, [])],
|
||||||
|
rect1_mm=(0, 0, 1, 20), rect2_mm=(19, 0, 20, 20))
|
||||||
|
p.electrodes1 = [_barrel(10, 10, drill_mm=1.0, pad_mm=2.4, solder=True,
|
||||||
|
polygons=[_disc(10, 10, 1.2)])]
|
||||||
|
p.electrodes1[0].protrusion_side = "F.Cu"
|
||||||
|
p.vias = [_pad_link()]
|
||||||
|
assert contact_solder_buildups(p) == ["F.Cu"]
|
||||||
|
assert tht_joint_buildups(p) == [] # contact center is skipped
|
||||||
|
stack = raster.rasterize_stack(p, 0.1 * NM)
|
||||||
|
wall = 1.0 + p.tht_protrusion_nm \
|
||||||
|
* (p.rho_ohm_m / p.solder_rho_ohm_m) / p.layers[0].thickness_nm
|
||||||
|
assert stack.thick_scale.max() == pytest.approx(wall, rel=1e-12)
|
||||||
|
|
||||||
|
|
||||||
|
def test_vialink_solder_json():
|
||||||
|
p = make_problem([(PLATE20, [])],
|
||||||
|
rect1_mm=(0, 0, 1, 20), rect2_mm=(19, 0, 20, 20))
|
||||||
|
p.vias = [_pad_link()]
|
||||||
|
d = problem_to_json(p)
|
||||||
|
q = problem_from_json(d)
|
||||||
|
assert q.vias[0].solder_filled is True
|
||||||
|
assert q.vias[0].protrusion_side == "F.Cu"
|
||||||
|
# legacy dumps without the flag: THT pads counted as solder-filled,
|
||||||
|
# vias as plating-only
|
||||||
|
del d["vias"][0]["solder_filled"], d["vias"][0]["protrusion_side"]
|
||||||
|
q = problem_from_json(d)
|
||||||
|
assert q.vias[0].solder_filled is True
|
||||||
|
assert q.vias[0].protrusion_side is None
|
||||||
|
d["vias"][0]["kind"] = "via"
|
||||||
|
assert problem_from_json(d).vias[0].solder_filled is False
|
||||||
|
|
||||||
|
|
||||||
def test_barrel_electrode_json_roundtrip(tmp_path):
|
def test_barrel_electrode_json_roundtrip(tmp_path):
|
||||||
p = make_problem([(PLATE20, [])],
|
p = make_problem([(PLATE20, [])],
|
||||||
rect1_mm=(0, 0, 1, 20), rect2_mm=(19, 0, 20, 20))
|
rect1_mm=(0, 0, 1, 20), rect2_mm=(19, 0, 20, 20))
|
||||||
|
|||||||
@@ -50,11 +50,9 @@ def test_cap_at_foil_thickness_is_identity():
|
|||||||
so the result equals the feature-off reference (a 'pad'-kind barrel,
|
so the result equals the feature-off reference (a 'pad'-kind barrel,
|
||||||
which skips rings and mouths) with the mouth fully inside copper."""
|
which skips rings and mouths) with the mouth fully inside copper."""
|
||||||
r_cap, _ = _solve(_two_layer(capped=True, cap_um=70.0), 0.1)
|
r_cap, _ = _solve(_two_layer(capped=True, cap_um=70.0), 0.1)
|
||||||
ref = _two_layer(kind="pad")
|
# a bare 'pad' barrel (solder_filled defaults False) is plating-only,
|
||||||
# 'pad' barrels are solder-filled; kill the core so the reference
|
# exactly like the via's
|
||||||
# barrel matches the via's plating-only resistance exactly
|
r_ref, _ = _solve(_two_layer(kind="pad"), 0.1)
|
||||||
ref.solder_rho_ohm_m = 1e30
|
|
||||||
r_ref, _ = _solve(ref, 0.1)
|
|
||||||
assert r_cap.R_ohm == pytest.approx(r_ref.R_ohm, rel=1e-9)
|
assert r_cap.R_ohm == pytest.approx(r_ref.R_ohm, rel=1e-9)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user