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,193 @@
|
||||
"""Qt selection dialog shown on plugin launch: net, layers, per-electrode
|
||||
contact, test current, optional cell size. PySide6 is already a plugin
|
||||
dependency (matplotlib QtAgg backend); the QApplication created here is
|
||||
reused by matplotlib afterwards.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import (QApplication, QCheckBox, QComboBox, QDialog,
|
||||
QDialogButtonBox, QFormLayout, QLabel,
|
||||
QLineEdit, QListWidget, QListWidgetItem,
|
||||
QVBoxLayout)
|
||||
|
||||
from . import config, skin
|
||||
|
||||
ALL_LAYERS = "All selected layers"
|
||||
AUTO_CONTACT = "(auto: per contact part)"
|
||||
MODEL_LABELS = {
|
||||
"uniform": "Uniform injection (conductor pressed on top)",
|
||||
"equipotential": "Equipotential (ideal bonded lug)",
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Selection:
|
||||
net: str
|
||||
layers: list[str]
|
||||
contact1: str # "auto", "all" or layer name
|
||||
contact2: str
|
||||
current_a: float
|
||||
cell_um: float | None
|
||||
freq_hz: float = 0.0
|
||||
contact_model: str = "uniform"
|
||||
include_buildup: bool = False
|
||||
extra_cu_um: float = 0.0
|
||||
|
||||
|
||||
class _Dialog(QDialog):
|
||||
def __init__(self, candidates: dict[str, list[str]], layer_order: list[str],
|
||||
default_net: str, e1_label: str, e2_label: str,
|
||||
contact1: str, contact2: str, buildup_layers: list[str]):
|
||||
super().__init__()
|
||||
self.setWindowTitle("Fill Resistance")
|
||||
self.setWindowFlag(Qt.WindowStaysOnTopHint, True)
|
||||
self._candidates = candidates
|
||||
self._layer_order = layer_order
|
||||
|
||||
form = QFormLayout()
|
||||
|
||||
self.net_box = QComboBox()
|
||||
for net in sorted(candidates):
|
||||
self.net_box.addItem(net)
|
||||
self.net_box.setCurrentText(default_net)
|
||||
form.addRow("Signal (net):", self.net_box)
|
||||
|
||||
self.layer_list = QListWidget()
|
||||
self.layer_list.setMaximumHeight(120)
|
||||
form.addRow("Layers:", self.layer_list)
|
||||
|
||||
self.contact1_box = QComboBox()
|
||||
self.contact2_box = QComboBox()
|
||||
form.addRow(f"V+ ({e1_label}):", self.contact1_box)
|
||||
form.addRow(f"V− ({e2_label}):", self.contact2_box)
|
||||
|
||||
self.model_box = QComboBox()
|
||||
for key in ("uniform", "equipotential"):
|
||||
self.model_box.addItem(MODEL_LABELS[key], key)
|
||||
default_index = 0 if config.CONTACT_MODEL == "uniform" else 1
|
||||
self.model_box.setCurrentIndex(default_index)
|
||||
form.addRow("Contact model:", self.model_box)
|
||||
|
||||
self.current_edit = QLineEdit(f"{config.TEST_CURRENT_A:g}")
|
||||
form.addRow("Test current [A]:", self.current_edit)
|
||||
|
||||
self.freq_edit = QLineEdit("")
|
||||
self.freq_edit.setPlaceholderText("0 = DC (e.g. 142k, 1.5M)")
|
||||
form.addRow("Frequency [Hz]:", self.freq_edit)
|
||||
|
||||
self.cell_edit = QLineEdit("")
|
||||
self.cell_edit.setPlaceholderText("auto")
|
||||
form.addRow("Cell size [µm]:", self.cell_edit)
|
||||
|
||||
self.buildup_check = QCheckBox(
|
||||
f"{config.SOLDER_THICKNESS_UM:g} µm solder on mask openings"
|
||||
+ (f" ({', '.join(buildup_layers)})" if buildup_layers
|
||||
else " (none found)"))
|
||||
self.buildup_check.setChecked(bool(buildup_layers)
|
||||
and config.INCLUDE_MASK_BUILDUP)
|
||||
self.buildup_check.setEnabled(bool(buildup_layers))
|
||||
form.addRow("Buildup:", self.buildup_check)
|
||||
|
||||
self.extracu_edit = QLineEdit(f"{config.BUILDUP_EXTRA_CU_UM:g}")
|
||||
self.extracu_edit.setEnabled(bool(buildup_layers))
|
||||
form.addRow("Extra Cu in openings [µm]:", self.extracu_edit)
|
||||
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
buttons.accepted.connect(self.accept)
|
||||
buttons.rejected.connect(self.reject)
|
||||
|
||||
lay = QVBoxLayout(self)
|
||||
lay.addLayout(form)
|
||||
note = QLabel("Multiple layers are coupled through the net's "
|
||||
"via/through-pad barrels. At f > 0 the foil-thickness "
|
||||
"skin effect is applied per layer; lateral (proximity) "
|
||||
"redistribution is not modeled, so AC results are a "
|
||||
"lower bound.")
|
||||
note.setWordWrap(True)
|
||||
note.setStyleSheet("color: gray; font-size: 10px;")
|
||||
lay.addWidget(note)
|
||||
lay.addWidget(buttons)
|
||||
|
||||
self._desired1, self._desired2 = contact1, contact2
|
||||
self.net_box.currentTextChanged.connect(self._refresh)
|
||||
self._refresh()
|
||||
|
||||
def _refresh(self):
|
||||
net = self.net_box.currentText()
|
||||
layers = [n for n in self._layer_order
|
||||
if n in self._candidates.get(net, [])]
|
||||
self.layer_list.clear()
|
||||
for name in layers:
|
||||
item = QListWidgetItem(name)
|
||||
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
|
||||
item.setCheckState(Qt.Checked)
|
||||
self.layer_list.addItem(item)
|
||||
for box, desired in ((self.contact1_box, self._desired1),
|
||||
(self.contact2_box, self._desired2)):
|
||||
box.clear()
|
||||
box.addItem(AUTO_CONTACT)
|
||||
box.addItem(ALL_LAYERS)
|
||||
box.addItems(layers)
|
||||
if desired == "all":
|
||||
box.setCurrentText(ALL_LAYERS)
|
||||
elif desired in layers:
|
||||
box.setCurrentText(desired)
|
||||
|
||||
def checked_layers(self) -> list[str]:
|
||||
out = []
|
||||
for i in range(self.layer_list.count()):
|
||||
item = self.layer_list.item(i)
|
||||
if item.checkState() == Qt.Checked:
|
||||
out.append(item.text())
|
||||
return out
|
||||
|
||||
def selection(self) -> Selection | None:
|
||||
layers = self.checked_layers()
|
||||
if not layers:
|
||||
return None
|
||||
try:
|
||||
current = float(self.current_edit.text().replace(",", "."))
|
||||
except ValueError:
|
||||
current = config.TEST_CURRENT_A
|
||||
cell_text = self.cell_edit.text().strip()
|
||||
try:
|
||||
cell = float(cell_text.replace(",", ".")) if cell_text else None
|
||||
except ValueError:
|
||||
cell = None
|
||||
|
||||
def contact(box: QComboBox) -> str:
|
||||
t = box.currentText()
|
||||
if t == AUTO_CONTACT:
|
||||
return "auto"
|
||||
return "all" if t == ALL_LAYERS else t
|
||||
|
||||
try:
|
||||
extra_cu = float(self.extracu_edit.text().replace(",", "."))
|
||||
except ValueError:
|
||||
extra_cu = 0.0
|
||||
return Selection(net=self.net_box.currentText(), layers=layers,
|
||||
contact1=contact(self.contact1_box),
|
||||
contact2=contact(self.contact2_box),
|
||||
current_a=current, cell_um=cell,
|
||||
freq_hz=skin.parse_frequency(self.freq_edit.text()),
|
||||
contact_model=self.model_box.currentData(),
|
||||
include_buildup=self.buildup_check.isChecked(),
|
||||
extra_cu_um=max(0.0, extra_cu))
|
||||
|
||||
|
||||
def ask(candidates: dict[str, list[str]], layer_order: list[str],
|
||||
default_net: str, e1_label: str, e2_label: str,
|
||||
contact1: str, contact2: str,
|
||||
buildup_layers: list[str] | None = None) -> Selection | None:
|
||||
"""Show the dialog; returns None on cancel."""
|
||||
app = QApplication.instance() or QApplication([])
|
||||
dlg = _Dialog(candidates, layer_order, default_net, e1_label, e2_label,
|
||||
contact1, contact2, buildup_layers or [])
|
||||
dlg.raise_()
|
||||
dlg.activateWindow()
|
||||
if dlg.exec() != QDialog.Accepted:
|
||||
return None
|
||||
return dlg.selection()
|
||||
Reference in New Issue
Block a user