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:
+64
-18
@@ -4,8 +4,10 @@ from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
from . import config, plots, progress, raster, report, solver, trim
|
||||
from .errors import UserFacingError
|
||||
from .errors import ElectrodeError, UserFacingError
|
||||
from .geometry import Problem
|
||||
from .solver import Result
|
||||
|
||||
@@ -14,7 +16,7 @@ def run(problem: Problem, outdir: Path | None, show: bool = True,
|
||||
i_test: float | None = None, freq_hz: float = 0.0,
|
||||
contact_model: str | None = None, overlay=None,
|
||||
trim_pct: float | None = None, trim_abs: float | None = None,
|
||||
trim_push=None) -> Result:
|
||||
trim_push=None, v_nominal: float | None = None) -> Result:
|
||||
"""overlay: optional callback(stack, result) run after the solve
|
||||
(EXPERIMENTAL in-KiCad overlays); its failures are non-fatal.
|
||||
trim_pct / trim_abs: mark copper below this threshold (% of the
|
||||
@@ -22,11 +24,22 @@ def run(problem: Problem, outdir: Path | None, show: bool = True,
|
||||
areas are printed, polygons saved to
|
||||
<outdir>/low_current_copper.json and handed to trim_push, an
|
||||
optional callback(trim_result) that pushes them into the board
|
||||
(failures non-fatal)."""
|
||||
if i_test is None:
|
||||
i_test = config.TEST_CURRENT_A
|
||||
if i_test <= 0:
|
||||
raise UserFacingError(f"Test current must be > 0 A (got {i_test:g}).")
|
||||
(failures non-fatal). Problems with terminals run in PDN mode:
|
||||
i_test/contact_model are ignored there (draws come from the
|
||||
terminals, the contact models are fixed) and v_nominal is the
|
||||
default supply open-circuit voltage."""
|
||||
pdn = bool(problem.terminals)
|
||||
if pdn and (problem.electrodes1 or problem.electrodes2):
|
||||
raise ElectrodeError(
|
||||
"The problem carries both classic V+/V- electrodes and PDN "
|
||||
"terminals - exactly one terminal scheme must be used."
|
||||
)
|
||||
if not pdn:
|
||||
if i_test is None:
|
||||
i_test = config.TEST_CURRENT_A
|
||||
if i_test <= 0:
|
||||
raise UserFacingError(
|
||||
f"Test current must be > 0 A (got {i_test:g}).")
|
||||
h = raster.choose_cell_size(problem.copper_bbox(), len(problem.layers))
|
||||
progress.stage(f"rasterizing {len(problem.layers)} layer(s) at cell "
|
||||
f"size {h / 1000:.1f} um ...")
|
||||
@@ -35,18 +48,49 @@ def run(problem: Problem, outdir: Path | None, show: bool = True,
|
||||
f"{int(stack.masks.sum())} copper cells, {len(problem.vias)} "
|
||||
f"via/pad barrel(s)")
|
||||
|
||||
e1, e2 = raster.electrode_masks(stack, problem)
|
||||
parts1, parts2 = raster.electrode_partition(stack, problem)
|
||||
if pdn:
|
||||
if contact_model is not None:
|
||||
print("PDN mode: contact models are fixed (Thevenin supplies "
|
||||
"/ uniform-injection loads) - ignoring the setting")
|
||||
tmasks = raster.terminal_masks(stack, problem)
|
||||
tparts = raster.terminal_partition(stack, problem)
|
||||
draw = sum(t.i_draw_a for t in problem.terminals
|
||||
if t.role == "load")
|
||||
progress.stage(f"solving PDN, {draw:g} A total draw"
|
||||
+ (f", {freq_hz:g} Hz" if freq_hz > 0 else " DC")
|
||||
+ " ...")
|
||||
result = solver.run_solve_pdn(problem, stack, tmasks, tparts,
|
||||
freq_hz, v_nominal)
|
||||
for s_ in result.supplies:
|
||||
print(f" {s_.label}: {s_.i_a:.4g} A @ {s_.v_contact:.4g} V "
|
||||
f"(v_oc {s_.v_oc:g} V, r_out {s_.r_out_ohm:g} ohm, "
|
||||
f"P_int {s_.p_internal_w:.3g} W)")
|
||||
for l_ in result.loads:
|
||||
print(f" {l_.label}: {l_.i_a:.4g} A, V {l_.v_mean:.4g} V "
|
||||
f"(min {l_.v_min:.4g}), P {l_.p_w:.4g} W")
|
||||
# figures reuse the two-terminal color scheme: supplies as V+,
|
||||
# loads as V- (masks already follow the solve's restriction)
|
||||
e1 = np.zeros_like(stack.masks)
|
||||
e2 = np.zeros_like(stack.masks)
|
||||
for t, m in zip(problem.terminals, tmasks):
|
||||
if t.role == "supply":
|
||||
e1 |= m
|
||||
else:
|
||||
e2 |= m
|
||||
else:
|
||||
e1, e2 = raster.electrode_masks(stack, problem)
|
||||
parts1, parts2 = raster.electrode_partition(stack, problem)
|
||||
|
||||
progress.stage(f"solving @ {i_test:g} A"
|
||||
+ (f", {freq_hz:g} Hz" if freq_hz > 0 else " DC") + " ...")
|
||||
result = solver.run_solve(problem, stack, e1, e2, i_test, freq_hz,
|
||||
contact_model, parts1, parts2)
|
||||
for prefix, pcs in (("P", result.part_currents1),
|
||||
("N", result.part_currents2)):
|
||||
for i, (label, amps) in enumerate(pcs):
|
||||
print(f" {prefix}{i + 1} ({label}): {amps:.4g} A "
|
||||
f"({100 * amps / i_test:.1f}%)")
|
||||
progress.stage(f"solving @ {i_test:g} A"
|
||||
+ (f", {freq_hz:g} Hz" if freq_hz > 0 else " DC")
|
||||
+ " ...")
|
||||
result = solver.run_solve(problem, stack, e1, e2, i_test, freq_hz,
|
||||
contact_model, parts1, parts2)
|
||||
for prefix, pcs in (("P", result.part_currents1),
|
||||
("N", result.part_currents2)):
|
||||
for i, (label, amps) in enumerate(pcs):
|
||||
print(f" {prefix}{i + 1} ({label}): {amps:.4g} A "
|
||||
f"({100 * amps / i_test:.1f}%)")
|
||||
|
||||
if outdir is not None:
|
||||
outdir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -78,5 +122,7 @@ def run(problem: Problem, outdir: Path | None, show: bool = True,
|
||||
"3_current_density"),
|
||||
(plots.fig_power(result, stack, e1, e2, problem), "4_power_density"),
|
||||
]
|
||||
if result.mode == "pdn" and result.pairs:
|
||||
figs.append((plots.fig_pdn_pairs(result), "5_source_sink_pairs"))
|
||||
plots.save_and_show(figs, outdir, show=show) # closes the window itself
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user