Release 1.4.0: PDN mode, the config-file workflow, and the dialog editor
tests / archlinux:latest (push) Successful in 39s
tests / debian:12 (push) Successful in 1m17s
tests / fedora:latest (push) Successful in 1m8s
tests / ubuntu:24.04 (push) Successful in 1m23s
tests / ubuntu-latest · py3.11 (push) Successful in 1m25s
tests / ubuntu-latest · py3.13 (push) Successful in 1m5s
tests / NixOS (FHS wrapper from docs/NIXOS.md) (push) Skipped
Build PCM package / build (push) Successful in 11s

Multiple Thevenin supplies and prescribed-current loads on one net,
solved in absolute volts with the Tellegen power balance verified per
run; a source-sink pair table (effective copper resistance per
supply x load pair plus an exactly-summing proportional-sharing loss
attribution), in summary.txt and as its own figure. Bonded terminals
short a package's contacts into one lug so the per-pin split becomes
a solve outcome. Geometry dumps carry the terminal set (schema v8).

The dialog gained a Classic/PDN mode selector and a full PDN editor:
per-role supply/load tables built from the marker rectangles (or a
config's terminal set, which never pins mode or net), with Component
hints, per-terminal Layer scopes, Active checkboxes, comments, a
per-net row filter, resizable tables and a scrolling, screen-sized
dialog. Numbers accept SI suffixes (50m, 4.7k) everywhere.

fill_res_config.json fully specifies a run (classic or PDN) with
validation, comments, named side-by-side configs (the one called
default auto-loads), Load/Save buttons with an editable file name,
and saves that never drop anything drawn on the board.

347 tests, green on Python 3.13 and on the 3.9 macOS wheel stack.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
janik
2026-08-27 17:01:24 +07:00
co-authored by Claude Fable 5
parent 31ef356345
commit 26b1cfaa45
28 changed files with 7363 additions and 406 deletions
+53 -1
View File
@@ -281,7 +281,7 @@ def _paint_lead_fillets(stack: RasterStack, problem: Problem) -> None:
# net's populated stitching THT pads, skipping the contacts' barrels
jobs = []
seen = set()
for e in problem.electrodes1 + problem.electrodes2:
for e in problem.contact_electrodes():
if e.drill_nm <= 0:
continue
if e.center is not None:
@@ -629,6 +629,58 @@ def electrode_masks(stack: RasterStack, problem: Problem
return e1, e2
def terminal_masks(stack: RasterStack, problem: Problem) -> list:
"""PDN mode: one (L, ny, nx) contact mask per problem.terminals
entry, same order. Same part semantics as electrode_masks (every
part must land on copper); additionally NO two terminals may share
a cell - each cell's injection/attachment must belong to exactly one
terminal or the currents would be ill-defined."""
out = []
for t in problem.terminals:
m = np.zeros_like(stack.masks)
for el in t.electrodes:
part = _part_mask3d(stack, problem, el)
if not part.any():
where = ("near its barrel (drill-wall ring / pad footprint)"
if el.drill_nm > 0 else
"(or is smaller than one grid cell)")
raise ElectrodeError(
f"{t.role} '{t.label}': contact part ({el.label}) does "
f"not overlap any copper of the selected fill on "
f"contact layer(s) '{el.contact}' {where}."
)
m |= part
out.append(m)
for i, ti in enumerate(problem.terminals):
for j in range(i + 1, len(problem.terminals)):
if (out[i] & out[j]).any():
tj = problem.terminals[j]
raise ElectrodeError(
f"The contact areas of {ti.role} '{ti.label}' and "
f"{tj.role} '{tj.label}' overlap on the copper grid. "
f"Move them apart."
)
return out
def terminal_partition(stack: RasterStack, problem: Problem) -> list:
"""PDN mode: per-part cell masks for each terminal, as a list (one
entry per terminal) of [(label, mask3d), ...]. Within one terminal
overlapping parts keep the first-wins attribution of
electrode_partition, so part currents sum to the terminal current."""
out = []
for t in problem.terminals:
parts = []
claimed = np.zeros_like(stack.masks)
for el in t.electrodes:
m = _part_mask3d(stack, problem, el)
m &= ~claimed
claimed |= m
parts.append((el.label, m))
out.append(parts)
return out
def electrode_partition(stack: RasterStack, problem: Problem
) -> tuple[list, list]:
"""Per-part cell masks for both terminals, as [(label, mask3d), ...].