Full solder joint at every populated THT pad, read from KiCad
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:
janik
2026-07-16 19:00:09 +07:00
parent 9e307f2818
commit b2277f65bf
7 changed files with 236 additions and 60 deletions
+12 -5
View File
@@ -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
+74 -30
View File
@@ -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
F.Cu (lead tents on B.Cu)."""
try:
for fp in footprints or []: for fp in footprints or []:
try:
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
return out
def _pad_owner(pad: Pad, pad_map: dict):
return pad_map.get((pad.position.x, pad.position.y, pad.number))
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) side = canonical_name(fp.layer)
return "F.Cu" if side == "B.Cu" else "B.Cu" return "F.Cu" if side == "B.Cu" else "B.Cu"
except Exception: except Exception:
pass pass
print(f"note: no footprint found for pad {pad.number} - assuming its " if not quiet:
f"lead protrudes on B.Cu") 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
for pad in net_pads:
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", z_bot_nm=stackup.z_bot_nm + 1, kind="pad",
pad_nm=_padstack_pad_nm(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
+45 -1
View File
@@ -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=(
+23 -8
View File
@@ -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))
+4 -3
View File
@@ -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
+68 -1
View File
@@ -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))
+3 -5
View File
@@ -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)