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
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:
@@ -7,6 +7,9 @@ so the whole pipeline downstream of board_io runs without KiCad.
|
||||
|
||||
Schema v2 is multi-layer: per-layer fills at stackup depths, linked by
|
||||
via/through-pad barrels. v1 dumps (single layer, no vias) still load.
|
||||
Schema v7 adds PDN terminals (supplies/loads); dumps <= v6 load with
|
||||
terminals=[] and run the classic two-terminal solve unchanged. v8 adds
|
||||
the per-terminal `bonded` flag (v7 dumps load with bonded=False).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -17,7 +20,7 @@ from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
JSON_SCHEMA_VERSION = 6
|
||||
JSON_SCHEMA_VERSION = 8
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -133,6 +136,39 @@ class Electrode:
|
||||
# Problem.tht_protrusion_nm
|
||||
|
||||
|
||||
@dataclass
|
||||
class Terminal:
|
||||
"""One PDN-mode terminal: a supply (Thevenin source: open-circuit
|
||||
volts v_oc behind r_out_ohm) or a load (prescribed current draw
|
||||
i_draw_a). Contact geometry is a list of Electrode parts. In PDN
|
||||
mode Problem.terminals replaces electrodes1/electrodes2; supply
|
||||
currents are solve OUTCOMES, load draws are prescribed.
|
||||
|
||||
bonded: all the terminal's contact cells are shorted into one
|
||||
super-node (an externally bonded lug - a multi-pin package with
|
||||
internal metal). The TOTAL current is prescribed as usual, but the
|
||||
per-part/per-cell split becomes a solve outcome instead of the
|
||||
default per-cell area share (loads) / per-cell Thevenin attachment
|
||||
(supplies). The contact face is then equipotential."""
|
||||
role: str # "supply" | "load"
|
||||
electrodes: list[Electrode]
|
||||
label: str = "" # display name; "" gets an
|
||||
# S1/L1 tag at solve time
|
||||
i_draw_a: float = 0.0 # loads: prescribed draw [A]
|
||||
r_out_ohm: float = 0.0 # supplies: Thevenin output
|
||||
# resistance [ohm]
|
||||
v_oc: float | None = None # supplies: open-circuit
|
||||
# volts; None -> the run's
|
||||
# v_nominal at solve time
|
||||
bonded: bool = False # short all contact cells
|
||||
# into one lug (see above)
|
||||
component: str = "" # display only: the owner
|
||||
# hint ("U5" / "near U5",
|
||||
# board_io.component_hints)
|
||||
comment: str = "" # display only: the user's
|
||||
# free-text note
|
||||
|
||||
|
||||
@dataclass
|
||||
class ViaLink:
|
||||
"""A conductive barrel (via or plated through-hole pad) linking copper
|
||||
@@ -201,6 +237,10 @@ class Problem:
|
||||
electrodes1: list[Electrode] # V+ terminal parts (merged)
|
||||
electrodes2: list[Electrode] # V- terminal parts (merged)
|
||||
thickness_source: str = "stackup"
|
||||
# PDN mode: non-empty replaces electrodes1/2 entirely (the pipeline
|
||||
# rejects a problem carrying both) - N supplies + M loads instead of
|
||||
# one driven terminal pair
|
||||
terminals: list[Terminal] = field(default_factory=list)
|
||||
buildups: list[SurfaceBuildup] = field(default_factory=list)
|
||||
solder_thickness_nm: int = 50_000
|
||||
solder_rho_ohm_m: float = 1.32e-7
|
||||
@@ -231,6 +271,15 @@ class Problem:
|
||||
def layer_names(self) -> list[str]:
|
||||
return [l.layer_name for l in self.layers]
|
||||
|
||||
def contact_electrodes(self) -> list[Electrode]:
|
||||
"""Every contact part regardless of mode: classic V+/V- lists
|
||||
plus all PDN terminal parts (exactly one group is non-empty in a
|
||||
valid problem). Use this wherever per-contact geometry features
|
||||
(solder coats, lead cones) are collected, so PDN terminals get
|
||||
the same treatment as classic ones."""
|
||||
return (self.electrodes1 + self.electrodes2
|
||||
+ [e for t in self.terminals for e in t.electrodes])
|
||||
|
||||
def sigma_s(self, layer_index: int) -> float:
|
||||
"""Sheet conductance of one layer [S per square]."""
|
||||
return (self.layers[layer_index].thickness_nm * 1e-9) / self.rho_ohm_m
|
||||
@@ -262,7 +311,7 @@ def contact_solder_buildups(problem: Problem) -> list[str]:
|
||||
names. Called once when the problem is built."""
|
||||
included = {l.layer_name for l in problem.layers}
|
||||
touched = []
|
||||
for e in problem.electrodes1 + problem.electrodes2:
|
||||
for e in problem.contact_electrodes():
|
||||
if not e.solder or not e.polygons \
|
||||
or e.protrusion_side not in included:
|
||||
continue
|
||||
@@ -318,7 +367,7 @@ def tht_joint_buildups(problem: Problem,
|
||||
coats them with the exact pad shape. Returns the affected layer
|
||||
names."""
|
||||
included = {l.layer_name for l in problem.layers}
|
||||
contacts = {e.center for e in problem.electrodes1 + problem.electrodes2
|
||||
contacts = {e.center for e in problem.contact_electrodes()
|
||||
if e.drill_nm > 0 and e.center is not None}
|
||||
touched = []
|
||||
for v in problem.vias:
|
||||
@@ -528,6 +577,39 @@ def _electrode_from_json(d: dict) -> Electrode:
|
||||
)
|
||||
|
||||
|
||||
def _terminal_to_json(t: Terminal) -> dict:
|
||||
d = {
|
||||
"role": t.role,
|
||||
"label": t.label,
|
||||
"i_draw_a": t.i_draw_a,
|
||||
"r_out_ohm": t.r_out_ohm,
|
||||
"v_oc": t.v_oc,
|
||||
"bonded": t.bonded,
|
||||
"electrodes": [_electrode_to_json(e) for e in t.electrodes],
|
||||
}
|
||||
# display-only metadata, written when present (still schema v8:
|
||||
# optional keys, older loaders simply ignore them)
|
||||
if t.component:
|
||||
d["component"] = t.component
|
||||
if t.comment:
|
||||
d["comment"] = t.comment
|
||||
return d
|
||||
|
||||
|
||||
def _terminal_from_json(d: dict) -> Terminal:
|
||||
return Terminal(
|
||||
role=d["role"],
|
||||
electrodes=[_electrode_from_json(ed) for ed in d["electrodes"]],
|
||||
label=d.get("label", ""),
|
||||
i_draw_a=float(d.get("i_draw_a", 0.0)),
|
||||
r_out_ohm=float(d.get("r_out_ohm", 0.0)),
|
||||
v_oc=(None if d.get("v_oc") is None else float(d["v_oc"])),
|
||||
bonded=bool(d.get("bonded", False)), # <= v7: not bonded
|
||||
component=str(d.get("component", "")),
|
||||
comment=str(d.get("comment", "")),
|
||||
)
|
||||
|
||||
|
||||
def problem_to_json(p: Problem) -> dict:
|
||||
return {
|
||||
"schema_version": JSON_SCHEMA_VERSION,
|
||||
@@ -538,6 +620,7 @@ def problem_to_json(p: Problem) -> dict:
|
||||
"thickness_source": p.thickness_source,
|
||||
"electrodes1": [_electrode_to_json(e) for e in p.electrodes1],
|
||||
"electrodes2": [_electrode_to_json(e) for e in p.electrodes2],
|
||||
"terminals": [_terminal_to_json(t) for t in p.terminals],
|
||||
"layers": [
|
||||
{
|
||||
"layer_name": l.layer_name,
|
||||
@@ -629,6 +712,8 @@ def problem_from_json(d: dict) -> Problem:
|
||||
electrodes2=(
|
||||
[_electrode_from_json(ed) for ed in d["electrodes2"]]
|
||||
if version >= 3 else [_electrode_from_json(d["electrode2"])]),
|
||||
# v7: PDN terminals; dumps <= v6 predate them and load classic
|
||||
terminals=[_terminal_from_json(td) for td in d.get("terminals", [])],
|
||||
thickness_source=d.get("thickness_source", "unknown"),
|
||||
buildups=[
|
||||
SurfaceBuildup(
|
||||
|
||||
Reference in New Issue
Block a user