Populated THT holes conduct in-plane as their solder plug + lead

The mouth of a populated THT pad kept only foil copper on every layer:
on the component side and inner layers the joint looked (and conducted)
like plain plane, and current had to crowd through the single barrel
attachment cell. The filled hole - lead cylinder plus solder bore - now
adds conduction-equivalent copper of the full hole depth on EVERY
spanned layer (the pin continues beyond both mouths, so each layer sees
the whole plug cross-section). Side to side the joint differs only by
the solder: coat and cone stay on the protrusion side.

Cone and plug contributions accumulate ADDITIVELY (stack.t_extra_nm)
and fold into thick_scale once; multiplying the factors would overstate
mouth cells carrying both. The raster map draws filled mouths in a
darker tin with their own legend entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
janik
2026-07-17 20:38:01 +07:00
parent c77e3408bd
commit 666abaa50f
4 changed files with 131 additions and 20 deletions
+52 -4
View File
@@ -11,7 +11,7 @@ from fill_resistance.geometry import (Electrode, Polygon, ViaLink,
contact_solder_buildups, load_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_multilayer, make_problem, rect_mm, ring_mm
PLATE20 = [(0, 0), (20, 0), (20, 20), (0, 20)]
@@ -237,7 +237,9 @@ def test_stitching_pad_joint():
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."""
the cone and coat must be applied once, not squared/stacked. The
hole plug (this synthetic barrel spans z = -1..1, so 2 nm of lead)
ADDS to the cone at the mouth instead of multiplying it."""
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,
@@ -247,8 +249,9 @@ def test_cone_not_doubled_at_contact():
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
t_cone = p.tht_protrusion_nm * (p.rho_ohm_m / p.solder_rho_ohm_m)
t_plug = 2.0 * (p.rho_ohm_m / p.tht_lead_rho_ohm_m)
wall = 1.0 + (t_cone + t_plug) / p.layers[0].thickness_nm
assert stack.thick_scale.max() == pytest.approx(wall, rel=1e-12)
@@ -396,6 +399,51 @@ def test_slot_cone_follows_slot():
assert stack.thick_scale[0].max() > 3.0
def test_plug_conducts_on_component_side():
"""A populated THT pad's filled hole (lead + solder plug) conducts
IN-PLANE across the mouth on EVERY spanned layer - the component
side is not bare foil. Each layer carries the FULL hole depth (the
pin continues beyond both mouths, so the whole plug cross-section
spreads current at every layer; side-to-side the only difference
is the solder coat + cone), converted to conduction-equivalent
copper: lead disc at lead resistivity, solder bore around it."""
p = make_multilayer([[(PLATE20, [])], [(PLATE20, [])]],
rect1_mm=(0, 0, 1, 20), rect2_mm=(19, 0, 20, 20))
p.vias = [ViaLink(x=10 * NM, y=10 * NM, drill_nm=1_000_000, z_top_nm=-1,
z_bot_nm=1 * NM + 1, kind="pad", pad_nm=2_400_000,
solder_filled=True, protrusion_side="L0")]
stack = raster.rasterize_stack(p, 0.1 * NM)
c = stack.cell_of(10 * NM, 10 * NM)
assert stack.masks[0][c] and stack.masks[1][c] # plugged, not open
assert stack.plug[0][c] and stack.plug[1][c] # drawn on both sides
t = p.layers[0].thickness_nm
# full hole depth z = -1 .. 1 mm + 1 on both layers; the mouth
# center lies inside the 0.75 mm lead (copper resistivity)
depth = 1 * NM + 2.0
t_cone = p.tht_protrusion_nm * (p.rho_ohm_m / p.solder_rho_ohm_m)
assert stack.thick_scale[0][c] == pytest.approx(
1.0 + (t_cone + depth * (p.rho_ohm_m / p.tht_lead_rho_ohm_m)) / t,
rel=1e-9) # solder side: + cone
assert stack.thick_scale[1][c] == pytest.approx(
1.0 + depth * (p.rho_ohm_m / p.tht_lead_rho_ohm_m) / t, rel=1e-9)
# far from the joint: untouched foil
assert stack.thick_scale[1][stack.cell_of(14 * NM, 10 * NM)] == 1.0
# clearance swallowing the bore -> no lead, solder-only plug
p.tht_lead_clearance_nm = 1_000_000
s_sn = raster.rasterize_stack(p, 0.1 * NM)
assert s_sn.thick_scale[1][c] == pytest.approx(
1.0 + depth * (p.rho_ohm_m / p.solder_rho_ohm_m) / t, rel=1e-9)
p.tht_lead_clearance_nm = 250_000
# a DNP pad still cuts an open hole and gets no plug
p.vias[0].solder_filled = False
p.vias[0].protrusion_side = None
s2 = raster.rasterize_stack(p, 0.1 * NM)
assert not s2.masks[0][c] and not s2.masks[1][c]
assert s2.plug is None
def test_slot_barrel_resistance():
"""Slotted barrel: plating wall = stadium perimeter, solder core =
stadium bore area (both reduce to the circle for dx = dy = 0)."""