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:
+102
-6
@@ -83,7 +83,15 @@ def _fmt_si(value: float, unit: str) -> str:
|
||||
def _suptitle(problem, stack, result=None) -> str:
|
||||
ny, nx = stack.shape2d
|
||||
parts = []
|
||||
if result is not None:
|
||||
if result is not None and result.mode == "pdn":
|
||||
# no single two-terminal R in PDN mode (R_ohm is NaN)
|
||||
parts.append(f"PDN {len(result.supplies)}S/{len(result.loads)}L, "
|
||||
f"ΣI = {result.i_test:g} A")
|
||||
parts.append(f"P_Cu = {_fmt_si(result.P_total, 'W')}")
|
||||
if result.freq_hz > 0:
|
||||
parts.append(f"f = {result.freq_hz / 1e3:g} kHz "
|
||||
f"(δ={result.skin_depth_um:.0f} µm, lower bound)")
|
||||
elif result is not None:
|
||||
parts.append(f"R = {result.R_ohm * 1000:.4g} mΩ")
|
||||
parts.append(f"P = {_fmt_si(result.P_total, 'W')} @ "
|
||||
f"{result.i_test:g} A")
|
||||
@@ -286,8 +294,22 @@ def fig_raster(stack, e1, e2, problem, result=None):
|
||||
if has_plug:
|
||||
handles.append(Patch(
|
||||
fc=_PLUG, label="solder-filled THT hole (lead + solder)"))
|
||||
if result is not None and (result.part_currents1
|
||||
or result.part_currents2):
|
||||
if result is not None and result.mode == "pdn":
|
||||
entries = ([(f"S{i + 1}", _E1_COLOR, s_.label, s_.i_a)
|
||||
for i, s_ in enumerate(result.supplies)]
|
||||
+ [(f"L{i + 1}", _E2_COLOR, l_.label, l_.i_a)
|
||||
for i, l_ in enumerate(result.loads)])
|
||||
shown = entries[:14]
|
||||
for tag, color, label, amps in shown:
|
||||
handles.append(Patch(
|
||||
fc=color, label=f"{tag} {label}: {amps:.3g} A"))
|
||||
if len(entries) > len(shown):
|
||||
handles.append(Patch(
|
||||
fc="#00000000",
|
||||
label=f"... +{len(entries) - len(shown)} "
|
||||
f"more in summary.txt"))
|
||||
elif result is not None and (result.part_currents1
|
||||
or result.part_currents2):
|
||||
entries = ([("+", _E1_COLOR, i, amps)
|
||||
for i, (_, amps) in
|
||||
enumerate(result.part_currents1)]
|
||||
@@ -321,9 +343,14 @@ def fig_raster(stack, e1, e2, problem, result=None):
|
||||
|
||||
def fig_potential(result, stack, e1, e2, problem):
|
||||
vmax = float(np.nanmax(result.V))
|
||||
# uniform model: <V-> = 0 is the reference, individual V- cells can
|
||||
# sit slightly below it - keep them in range instead of clipping
|
||||
vmin = min(0.0, float(np.nanmin(result.V)))
|
||||
if result.mode == "pdn":
|
||||
# absolute volts (e.g. 3.3 V nominal): anchoring the scale at
|
||||
# 0 V would flatten the map into one color - auto-range instead
|
||||
vmin = float(np.nanmin(result.V))
|
||||
else:
|
||||
# uniform model: <V-> = 0 is the reference, individual V- cells
|
||||
# can sit slightly below it - keep them in range, don't clip
|
||||
vmin = min(0.0, float(np.nanmin(result.V)))
|
||||
unit, scale = ("mV", 1e3) if vmax < 0.1 else ("V", 1.0)
|
||||
cmap = matplotlib.colormaps[config.CMAP_POTENTIAL].copy()
|
||||
cmap.set_bad(_BG)
|
||||
@@ -444,6 +471,75 @@ def fig_power(result, stack, e1, e2, problem):
|
||||
paint_extra=paint_extra)
|
||||
|
||||
|
||||
def _style_table(tbl):
|
||||
tbl.auto_set_font_size(False)
|
||||
tbl.set_fontsize(9)
|
||||
tbl.scale(1.0, 1.5)
|
||||
tbl.auto_set_column_width(col=sorted({c for _r, c
|
||||
in tbl.get_celld()}))
|
||||
for (r, _c), cell in tbl.get_celld().items():
|
||||
cell.set_edgecolor("#cccccc")
|
||||
if r == 0:
|
||||
cell.set_text_props(fontweight="bold", color=_INK)
|
||||
cell.set_facecolor("#eeeeee")
|
||||
elif r % 2 == 0:
|
||||
cell.set_facecolor("#f7f7f7")
|
||||
|
||||
|
||||
def fig_pdn_pairs(result):
|
||||
"""The PDN source-sink pair table as a figure: effective copper
|
||||
resistance between every supply and every load plus the
|
||||
proportional-sharing loss attribution - the same numbers and
|
||||
conventions as the summary.txt table. Terminals are keyed by their
|
||||
(unique) labels alone; a legend table underneath notes each
|
||||
terminal's component hint and comment when there are any."""
|
||||
header = ["supply", "load", "R (copper)", "I attributed",
|
||||
"P attributed"]
|
||||
rows = [[pr.supply, pr.load,
|
||||
(_fmt_si(pr.r_ohm, "Ω") if pr.r_ohm is not None
|
||||
else "no path"),
|
||||
_fmt_si(pr.i_share_a, "A"),
|
||||
_fmt_si(pr.p_w, "W")] for pr in result.pairs]
|
||||
legend = [[t.label, role, t.component, t.comment]
|
||||
for role, terms in (("supply", result.supplies),
|
||||
("load", result.loads))
|
||||
for t in terms if t.component or t.comment]
|
||||
h1 = 1.8 + 0.32 * len(rows)
|
||||
h2 = 0.9 + 0.30 * len(legend)
|
||||
if legend:
|
||||
fig, (ax, ax2) = plt.subplots(
|
||||
2, 1, figsize=(9.0, h1 + h2), layout="constrained",
|
||||
gridspec_kw={"height_ratios": [h1, h2]})
|
||||
else:
|
||||
fig, ax = plt.subplots(figsize=(9.0, h1), layout="constrained")
|
||||
ax2 = None
|
||||
ax.axis("off")
|
||||
ax.set_title("Fill Resistance - source→sink pairs", fontsize=13,
|
||||
color=_INK, loc="left")
|
||||
_style_table(ax.table(cellText=rows, colLabels=header,
|
||||
loc="upper center", cellLoc="left",
|
||||
colLoc="left"))
|
||||
p_attr = sum(pr.p_w for pr in result.pairs)
|
||||
ax.text(0.0, 0.02,
|
||||
f"attributed copper loss total: {_fmt_si(p_attr, 'W')} "
|
||||
f"(copper loss {_fmt_si(result.P_total, 'W')})\n"
|
||||
"R: effective copper resistance between the two contacts - "
|
||||
"operating-point independent, source R_out excluded.\n"
|
||||
"I/P attributed by proportional sharing per copper island: "
|
||||
"a convention (the pair split is not unique physics), but "
|
||||
"exact in total.",
|
||||
transform=ax.transAxes, fontsize=8, color="#666666",
|
||||
va="bottom", ha="left")
|
||||
if ax2 is not None:
|
||||
ax2.axis("off")
|
||||
ax2.set_title("terminals", fontsize=10, color=_INK, loc="left")
|
||||
_style_table(ax2.table(
|
||||
cellText=legend,
|
||||
colLabels=["terminal", "role", "component", "comment"],
|
||||
loc="upper center", cellLoc="left", colLoc="left"))
|
||||
return fig
|
||||
|
||||
|
||||
def fig_error(message: str):
|
||||
fig, ax = plt.subplots(figsize=(9, 4.5), layout="constrained")
|
||||
ax.axis("off")
|
||||
|
||||
Reference in New Issue
Block a user