Initial import: KiCad zone resistance plugin
DC/AC resistance, power dissipation, and via/injection-area currents of copper zone fills. KiCad 10 IPC-API plugin (kicad-python/kipy): multi-layer via-coupled FDM solver, multi-part terminals via User.1/User.2 marker layers, pads as contacts, uniform-injection and equipotential contact models, per-foil skin effect, optional solder/copper buildup on mask openings. 54-case test suite incl. exact analytic references. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
"""Output directory, summary.txt, geometry dump, stdout one-liner."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
from . import config
|
||||
from .geometry import Problem, save_problem
|
||||
from .raster import RasterStack
|
||||
from .solver import Result
|
||||
|
||||
|
||||
def make_output_dir(board_dir: Path) -> Path:
|
||||
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
out = Path(board_dir) / config.OUTPUT_DIRNAME / stamp
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
return out
|
||||
|
||||
|
||||
def write_geometry_dump(outdir: Path, problem: Problem) -> Path:
|
||||
p = outdir / "geometry_dump.json"
|
||||
save_problem(problem, p)
|
||||
return p
|
||||
|
||||
|
||||
def result_line(result: Result, problem: Problem, stack: RasterStack) -> str:
|
||||
ny, nx = stack.shape2d
|
||||
ac = (f" @ {result.freq_hz / 1e3:g} kHz (lower bound)"
|
||||
if result.freq_hz > 0 else "")
|
||||
return (f"R = {result.R_ohm * 1000:.4g} mOhm{ac}, "
|
||||
f"P = {result.P_total:.4g} W @ {result.i_test:g} A "
|
||||
f"(net {problem.net_name}, {'+'.join(stack.layer_names)}, "
|
||||
f"grid {nx}x{ny}x{stack.nlayers}, cell {stack.h_nm / 1000:.0f} um)")
|
||||
|
||||
|
||||
def _electrode_line(e) -> str:
|
||||
r = e.rect
|
||||
return (f"{e.label:12s} contact={e.contact:8s} "
|
||||
f"x [{r.x0 / 1e6:.2f}, {r.x1 / 1e6:.2f}] "
|
||||
f"y [{r.y0 / 1e6:.2f}, {r.y1 / 1e6:.2f}] mm")
|
||||
|
||||
|
||||
def write_summary(outdir: Path, problem: Problem, stack: RasterStack,
|
||||
result: Result) -> Path:
|
||||
ny, nx = stack.shape2d
|
||||
info = result.solve_info
|
||||
lines = [
|
||||
"fill_resistance summary",
|
||||
"=======================",
|
||||
f"board: {problem.board_path}",
|
||||
f"net: {problem.net_name}",
|
||||
f"test current: {result.i_test:g} A",
|
||||
f"resistivity: {problem.rho_ohm_m:.3e} ohm*m",
|
||||
f"via plating: {problem.plating_nm / 1000:.0f} um",
|
||||
"",
|
||||
(f"frequency: "
|
||||
+ (f"{result.freq_hz:g} Hz (skin depth {result.skin_depth_um:.0f} um)"
|
||||
if result.freq_hz > 0 else "DC")),
|
||||
f"RESISTANCE: {result.R_ohm * 1000:.6g} mOhm"
|
||||
+ (" (AC LOWER BOUND: lateral/proximity redistribution not modeled)"
|
||||
if result.freq_hz > 0 else ""),
|
||||
f"VOLTAGE DROP: {result.R_ohm * result.i_test * 1000:.4g} mV "
|
||||
f"@ {result.i_test:g} A",
|
||||
f"TOTAL POWER: {result.P_total:.6g} W @ {result.i_test:g} A",
|
||||
f" in vias: {result.P_vias:.4g} W",
|
||||
f" power balance: {result.power_balance_rel:.2e} (consistency)",
|
||||
"",
|
||||
"layers (top to bottom):",
|
||||
]
|
||||
if problem.buildups and stack.buildup is not None:
|
||||
eq_um = (problem.solder_thickness_nm / 1000
|
||||
* problem.rho_ohm_m / problem.solder_rho_ohm_m
|
||||
+ problem.extra_cu_nm / 1000)
|
||||
cell_mm2 = (stack.h_nm * 1e-6) ** 2
|
||||
per_layer = {name: float(stack.buildup[li].sum()) * cell_mm2
|
||||
for li, name in enumerate(stack.layer_names)
|
||||
if stack.buildup[li].any()}
|
||||
areas = ", ".join(f"{n}: {a:.0f} mm^2" for n, a in per_layer.items())
|
||||
lines.insert(-1, f"solder buildup: "
|
||||
f"{problem.solder_thickness_nm / 1000:.0f} um solder"
|
||||
+ (f" + {problem.extra_cu_nm / 1000:.0f} um Cu"
|
||||
if problem.extra_cu_nm else "")
|
||||
+ f" = {eq_um:.1f} um equivalent Cu ({areas})")
|
||||
for li, layer in enumerate(problem.layers):
|
||||
ac = (f" Rs_AC/Rs_DC={result.rs_ratios[li]:.2f}"
|
||||
if result.freq_hz > 0 else "")
|
||||
lines.append(
|
||||
f" {layer.layer_name:8s} t={layer.thickness_nm / 1000:5.1f} um "
|
||||
f"z={layer.z_nm / 1000:7.1f} um "
|
||||
f"P={result.P_layers[li]:.4g} W "
|
||||
f"maxJ={float(np.nanmax(result.Jmag[li])) * 1e-6 if np.isfinite(result.Jmag[li]).any() else 0:.4g} A/mm^2"
|
||||
+ ac
|
||||
)
|
||||
lines += [
|
||||
"",
|
||||
f"grid: {nx} x {ny} x {stack.nlayers} cells @ "
|
||||
f"{stack.h_nm / 1000:.1f} um",
|
||||
f"copper cells: {int(stack.masks.sum())}",
|
||||
f"free unknowns: {result.n_free}",
|
||||
f"solver: {info.method}"
|
||||
+ (f", {info.iterations} iters, residual {info.residual:.2e}"
|
||||
if info.iterations is not None else ""),
|
||||
f"I1/I2 @ 1V: {result.I1_a:.9g} / {result.I2_a:.9g} A "
|
||||
f"(mismatch {result.mismatch_rel:.2e})",
|
||||
f"timings [s]: "
|
||||
f"{', '.join(f'{k}={v:.2f}' for k, v in result.timings.items())}",
|
||||
"",
|
||||
f"contact model: {result.contact_model}"
|
||||
+ (" (uniform orthogonal injection; R is the upper contact bound)"
|
||||
if result.contact_model == "uniform" else " (ideal bonded lug)"),
|
||||
f"terminals:",
|
||||
f" V+ ({len(problem.electrodes1)} injection area(s)):",
|
||||
*(f" {_electrode_line(e)}" for e in problem.electrodes1),
|
||||
f" V- ({len(problem.electrodes2)} injection area(s)):",
|
||||
*(f" {_electrode_line(e)}" for e in problem.electrodes2),
|
||||
]
|
||||
if result.part_currents1 or result.part_currents2:
|
||||
how = ("prescribed by area share (uniform model)"
|
||||
if result.contact_model == "uniform"
|
||||
else "computed flux (equipotential model)")
|
||||
lines += ["", f"current per injection area @ {result.i_test:g} A "
|
||||
f"({how}):"]
|
||||
for sign, pcs in (("+", result.part_currents1),
|
||||
("-", result.part_currents2)):
|
||||
for i, (label, amps) in enumerate(pcs):
|
||||
tag = f"{'P' if sign == '+' else 'N'}{i + 1}"
|
||||
lines.append(f" {tag:4s} {label:24s} {amps:9.4g} A "
|
||||
f"({100 * amps / result.i_test:5.1f}%)")
|
||||
if result.via_reports:
|
||||
n_shown = min(10, len(result.via_reports))
|
||||
lines += [
|
||||
"",
|
||||
f"vias/pads carrying current (top {n_shown} of "
|
||||
f"{len(result.via_reports)}, @ {result.i_test:g} A):",
|
||||
" x [mm] y [mm] kind drill I [A] P [W]",
|
||||
]
|
||||
for v in result.via_reports[:n_shown]:
|
||||
lines.append(
|
||||
f" {v.x_mm:8.2f} {v.y_mm:8.2f} {v.kind:5s} "
|
||||
f"{v.drill_mm:5.2f} {v.current_a:8.4g} {v.power_w:.4g}"
|
||||
)
|
||||
p = outdir / "summary.txt"
|
||||
p.write_text("\n".join(lines), encoding="utf-8")
|
||||
return p
|
||||
Reference in New Issue
Block a user