Model every THT pad hole: exact pad copper, lead conductor, DNP holes
Build PCM package / build (push) Successful in 6s
Build PCM package / build (push) Successful in 6s
- THT pad copper is now part of the conductor: the exact pad outline (incl. oblong/custom shapes) is fetched from KiCad once per pad and stamped onto every included layer (the outer shape stands in for inner rings). Annular rings bridge antipads, and joints land on real copper instead of only pour coverage. - The internal lead conductor is modeled in every solder-filled hole: a cylinder of drill - THT_LEAD_CLEARANCE_MM (0.25 fab rule) with THT_LEAD_RHO_OHM_M (copper default; config for brass/steel leads), in parallel with the solder annulus and the plating. - Drill mouths of THT pads: populated pads keep conducting mouth copper (stands in for the solder plug - conservative, the plug is worth ~200 um of copper equivalent); DNP pad holes are cut open on every layer like uncapped via mouths. - Oblong pads: the coat uses the exact pad shape; the lead cone tapers within the inscribed circle (new pad_min_nm on ViaLink/Electrode) so the long axis is not overstated sideways. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -211,8 +211,8 @@ def _pad_link(populated=True):
|
||||
|
||||
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."""
|
||||
joint: solder-side coat, cone, and a conducting (plugged) mouth;
|
||||
a DNP pad gets an open hole and nothing else."""
|
||||
def prob(populated=True):
|
||||
p = make_problem([(PLATE20, [])],
|
||||
rect1_mm=(0, 0, 1, 20), rect2_mm=(19, 0, 20, 20))
|
||||
@@ -225,11 +225,13 @@ def test_stitching_pad_joint():
|
||||
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()
|
||||
assert stack.masks[0][stack.cell_of(10 * NM, 10 * NM)] # plugged mouth
|
||||
|
||||
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 s2.buildup is None
|
||||
assert not s2.masks[0][s2.cell_of(10 * NM, 10 * NM)] # DNP: open hole
|
||||
assert r_joint.R_ohm < r_bare.R_ohm
|
||||
|
||||
|
||||
@@ -250,6 +252,54 @@ def test_cone_not_doubled_at_contact():
|
||||
assert stack.thick_scale.max() == pytest.approx(wall, rel=1e-12)
|
||||
|
||||
|
||||
def test_lead_in_barrel_resistance():
|
||||
"""Populated hole: plating || lead cylinder || solder annulus, with
|
||||
the lead clipped to the plating bore."""
|
||||
v = ViaLink(x=0, y=0, drill_nm=1_000_000, z_top_nm=-1, z_bot_nm=1)
|
||||
rho, sn = 1.68e-8, 1.32e-7
|
||||
r_solder = v.barrel_resistance(1_600_000, rho, 18_000,
|
||||
solder_rho_ohm_m=sn)
|
||||
r_lead = v.barrel_resistance(1_600_000, rho, 18_000,
|
||||
solder_rho_ohm_m=sn,
|
||||
lead_nm=750_000, lead_rho_ohm_m=rho)
|
||||
rl, rc = 0.375e-3, 0.5e-3 - 18e-6
|
||||
ga = math.pi * 1e-3 * 18e-6 / rho
|
||||
ga += math.pi * rl ** 2 / rho + math.pi * (rc ** 2 - rl ** 2) / sn
|
||||
assert r_lead == pytest.approx(1.6e-3 / ga, rel=1e-12)
|
||||
assert r_lead < r_solder
|
||||
# a lead wider than the bore is clipped to it
|
||||
r_big = v.barrel_resistance(1_600_000, rho, 18_000,
|
||||
solder_rho_ohm_m=sn,
|
||||
lead_nm=2_000_000, lead_rho_ohm_m=rho)
|
||||
ga2 = math.pi * 1e-3 * 18e-6 / rho + math.pi * rc ** 2 / rho
|
||||
assert r_big == pytest.approx(1.6e-3 / ga2, rel=1e-12)
|
||||
|
||||
|
||||
def test_oblong_pad_cone_uses_inscribed_dim():
|
||||
"""Oblong pads: the cone tapers to the inscribed circle (pad_min),
|
||||
never past it, so the long pad axis is not overstated sideways."""
|
||||
p = make_problem([(PLATE20, [])],
|
||||
rect1_mm=(0, 0, 1, 20), rect2_mm=(19, 0, 20, 20))
|
||||
p.vias = [_pad_link()]
|
||||
p.vias[0].pad_min_nm = 1_600_000 # 2.4 mm max, 1.6 mm min
|
||||
stack = raster.rasterize_stack(p, 0.1 * NM)
|
||||
ii, jj = np.nonzero(stack.thick_scale[0] != 1.0)
|
||||
d = np.hypot(stack.x0_nm + (jj + 0.5) * stack.h_nm - 10 * NM,
|
||||
stack.y0_nm + (ii + 0.5) * stack.h_nm - 10 * NM)
|
||||
assert len(d) and d.max() < 0.8 * NM
|
||||
|
||||
|
||||
def test_stitching_coat_exact_shape():
|
||||
"""When KiCad supplies the exact pad polygon, the coat uses it
|
||||
instead of the pad-diameter disc (oblong pads stay honest)."""
|
||||
p = make_problem([(PLATE20, [])],
|
||||
rect1_mm=(0, 0, 1, 20), rect2_mm=(19, 0, 20, 20))
|
||||
p.vias = [_pad_link()]
|
||||
shape = _disc(10, 10, 0.9)
|
||||
assert tht_joint_buildups(p, {(10 * NM, 10 * NM): [shape]}) == ["F.Cu"]
|
||||
assert p.buildups[0].polygons[0] is shape
|
||||
|
||||
|
||||
def test_vialink_solder_json():
|
||||
p = make_problem([(PLATE20, [])],
|
||||
rect1_mm=(0, 0, 1, 20), rect2_mm=(19, 0, 20, 20))
|
||||
|
||||
@@ -50,9 +50,13 @@ def test_cap_at_foil_thickness_is_identity():
|
||||
so the result equals the feature-off reference (a 'pad'-kind barrel,
|
||||
which skips rings and mouths) with the mouth fully inside copper."""
|
||||
r_cap, _ = _solve(_two_layer(capped=True, cap_um=70.0), 0.1)
|
||||
# a bare 'pad' barrel (solder_filled defaults False) is plating-only,
|
||||
# exactly like the via's
|
||||
r_ref, _ = _solve(_two_layer(kind="pad"), 0.1)
|
||||
ref = _two_layer(kind="pad")
|
||||
# populated pads skip rings and mouths; kill the lead + solder core
|
||||
# so the reference barrel matches the via's plating-only resistance
|
||||
ref.vias[0].solder_filled = True
|
||||
ref.solder_rho_ohm_m = 1e30
|
||||
ref.tht_lead_clearance_nm = 10 ** 9
|
||||
r_ref, _ = _solve(ref, 0.1)
|
||||
assert r_cap.R_ohm == pytest.approx(r_ref.R_ohm, rel=1e-9)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user