Fix solver correctness and input-validation issues from code review

- Refuse the uniform contact model when the fills form multiple
  disconnected copper groups that each touch both terminals: the
  prescribed injection split is ill-posed and the grounded system was
  singular, silently returning garbage (e.g. negative gigaohms).
  connected_restrict now reports the component count; a power-balance
  backstop (SolverError) catches any other inconsistent solve.
- Connect via/pad barrels to the nearest fill copper within the pad
  footprint (+1 cell) instead of only the exact center cell, so
  thermal-relief spokes still stitch layers; barrels that reach fill on
  fewer than two layers are warned about. ViaLink gains pad_nm
  (extracted from the padstack, JSON-roundtripped).
- Validate dialog input on OK (layers, current > 0, cell > 0, parseable
  frequency, extra Cu >= 0) with an inline error instead of silently
  substituting defaults; parse_frequency raises on garbage; pipeline
  rejects i_test <= 0; choose_cell_size rejects non-positive overrides.
- Warn when a contact part is dropped by the connectivity restriction;
  floor instead of truncate in cell_of; correct the uniform-model
  summary line; drop an unused variable; refresh plugin.json wording.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
janik
2026-07-15 14:54:45 +07:00
parent 06c62e04f8
commit e7627352c1
18 changed files with 262 additions and 60 deletions
+41 -3
View File
@@ -74,9 +74,10 @@ def test_parallel_vias_halve_barrel_resistance():
def test_antipad_bridging():
"""3 layers; the middle layer has an antipad hole at the via cell, so
the barrel bridges L0 -> L2 directly with DOUBLE the length."""
mid_with_hole = [(STRIP, [[(5, 0), (6, 0), (6, 1), (5, 1)]])]
"""3 layers; the middle layer has an antipad hole at the via, WIDER
than the barrel connection search (pad footprint + 1 cell), so the
barrel bridges L0 -> L2 directly with DOUBLE the length."""
mid_with_hole = [(STRIP, [[(4, 0), (7, 0), (7, 1), (4, 1)]])]
p = make_multilayer(
[[(STRIP, [])], mid_with_hole, [(STRIP, [])]],
rect1_mm=(0, 0, 1, 1), rect2_mm=(9, 0, 10, 1),
@@ -88,6 +89,43 @@ def test_antipad_bridging():
assert res.R_ohm == pytest.approx(r_exact, rel=1e-9)
def test_thermal_gap_via_connects_to_nearby_copper():
"""The cell under the via is not copper (thermal-relief knockout),
but fill copper within the pad footprint (+1 cell) still reaches the
barrel: the link lands on the nearest copper cell instead of being
silently dropped."""
top_with_gap = [(STRIP, [[(5, 0), (7, 0), (7, 1), (5, 1)]])]
p = make_multilayer(
[top_with_gap, [(STRIP, [])]],
rect1_mm=(0, 0, 1, 1), rect2_mm=(9, 0, 10, 1),
contact1="L0", contact2="L1",
vias_mm=[(5.5, 0.5)], gap_mm=1.0)
res, _ = _solve(p, 1.0)
sig = sigma_s()
# L0 attaches at col 4 (nearest copper, 1.0 mm from the barrel),
# L1 at col 5: 4 faces on L0, the barrel, 4 faces on L1 - exact
r_exact = (4 + 4) / sig + _r_via(1.0)
assert res.R_ohm == pytest.approx(r_exact, rel=1e-9)
assert len(res.via_reports) == 1
assert res.via_reports[0].current_a == pytest.approx(1.0, rel=1e-9)
def test_dead_barrel_is_warned(capsys):
"""A via isolated from the fill by an antipad wider than the search
radius on all but one layer carries nothing and is reported."""
bot_with_hole = [(STRIP, [[(3, 0), (8, 0), (8, 1), (3, 1)]])]
p = make_multilayer(
[[(STRIP, [])], bot_with_hole],
rect1_mm=(0, 0, 1, 1), rect2_mm=(9, 0, 10, 1),
contact1="L0", contact2="L0",
vias_mm=[(5.5, 0.5)], gap_mm=1.0)
res, _ = _solve(p, 1.0)
sig = sigma_s()
assert res.R_ohm == pytest.approx(9 / sig, rel=1e-9) # L0 alone
assert res.via_reports == []
assert "carry no current" in capsys.readouterr().out
def test_via_short_between_electrodes_raises():
"""Both electrodes over the SAME cell on different layers with a via
there = direct short, no free copper -> error."""