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:
+410
-51
@@ -1,8 +1,13 @@
|
||||
"""Top-level orchestration for the KiCad-launched action.
|
||||
|
||||
Flow: connect -> read the two selected contacts (rectangles/pads) ->
|
||||
gather fills -> selection dialog (net, layers, contacts, current, cell)
|
||||
-> extract vias -> solve -> figures + report.
|
||||
Flow: connect -> load the config named "default" (or the board-specific
|
||||
one) -> derive BOTH modes' terminals (classic: selection / marker
|
||||
rectangles / config refs; PDN: per-rectangle marker scan, or the
|
||||
config's terminal set) -> gather fills -> dialog with a Classic/PDN
|
||||
mode selector (classic: the two-contact form; PDN: editable per-role
|
||||
terminal tables) -> extract vias -> solve -> figures + report. The
|
||||
dialog's "Load config…" button loops back to the derivation with the
|
||||
picked file, so a run can be set up from any saved config.
|
||||
|
||||
Every failure is reported twice: on stdout (lands in the KiCad status-bar
|
||||
warning list) and as a matplotlib error figure, so it cannot be missed.
|
||||
@@ -11,9 +16,11 @@ from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
from . import config, pipeline, progress, report
|
||||
from .errors import CandidateError, UserFacingError
|
||||
from .errors import ConfigError, SelectionError, UserFacingError
|
||||
from .geometry import Terminal
|
||||
|
||||
|
||||
def _fail(message: str, outdir) -> None:
|
||||
@@ -36,13 +43,21 @@ def _fail(message: str, outdir) -> None:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# config globals a config file may override; "Load config…" re-derives
|
||||
# from a fresh baseline so one file's physics/marker layers never leak
|
||||
# into the next
|
||||
_CFG_GLOBALS = ("RHO_CU_OHM_M", "COPPER_THICKNESS_UM", "VIA_PLATING_UM",
|
||||
"ELECTRODE_POS_LAYER", "ELECTRODE_NEG_LAYER",
|
||||
"ELECTRODE_PDN_LAYER")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
outdir = None
|
||||
try:
|
||||
try:
|
||||
from kipy.errors import ApiError
|
||||
|
||||
from . import board_io, dialog
|
||||
from . import board_io, configfile, dialog
|
||||
except ImportError as e:
|
||||
if "cannot open shared object file" not in str(e):
|
||||
raise
|
||||
@@ -60,47 +75,327 @@ def main() -> None:
|
||||
try:
|
||||
kicad, board = board_io.connect()
|
||||
stackup = board_io.get_stackup_info(board)
|
||||
es1, es2, net_hint = board_io.get_electrodes(board, stackup)
|
||||
if board_io.any_zone_unfilled(board) or config.ALWAYS_REFILL:
|
||||
board_io.refill(board)
|
||||
fills = board_io.gather_net_fills(board)
|
||||
tracks = board_io.gather_net_tracks(board)
|
||||
copper = board_io.merge_copper(
|
||||
fills, board_io.tracks_as_polygons(tracks))
|
||||
candidate_nets = board_io.nets_overlapping(copper, es1, es2)
|
||||
buildups = board_io.gather_mask_buildups(board)
|
||||
except ApiError as e:
|
||||
raise UserFacingError(
|
||||
f"KiCad API error: {e}\nIf KiCad is showing a dialog, close "
|
||||
f"it and run again."
|
||||
)
|
||||
cfg_path = configfile.find_config(
|
||||
board_io.board_dir(board),
|
||||
getattr(board, "name", "") or "")
|
||||
base_globals = {name: getattr(config, name)
|
||||
for name in _CFG_GLOBALS}
|
||||
while True:
|
||||
for name, value in base_globals.items():
|
||||
setattr(config, name, value)
|
||||
try:
|
||||
cfg = configfile.load_config(cfg_path) if cfg_path else None
|
||||
# a config with a terminals section is the PDN source;
|
||||
# cfg.mode is only the STARTING mode - nothing is
|
||||
# pinned, the dialog switches modes and nets freely
|
||||
pdn_cfg = cfg is not None and bool(cfg.terminals)
|
||||
if cfg is not None:
|
||||
print(f"using config {cfg_path.name} ({cfg.mode} mode)")
|
||||
# before any geometry: the marker layers steer
|
||||
# get_electrodes, the physics steers build_problem
|
||||
configfile.apply_physics(cfg)
|
||||
|
||||
if not candidate_nets:
|
||||
raise CandidateError(
|
||||
"No copper (zone fill or trace) overlaps both contacts. "
|
||||
"Check that both sit over copper of the same net and that "
|
||||
"the fills are up to date (press B in the board editor)."
|
||||
# BOTH terminal derivations always run; a failure only
|
||||
# disables that mode's radio (with the reason shown) - the
|
||||
# launch dies only when neither mode is possible
|
||||
classic_reason = pdn_reason = None
|
||||
es1: list = []
|
||||
es2: list = []
|
||||
net_hint = None
|
||||
terminals: list = [] # resolved config terminals
|
||||
marker_terms = None # live-scan MarkerTerminal list
|
||||
new_terms: list = [] # rects not in the config yet
|
||||
merge_note = ""
|
||||
pdn_groups: list = [] # electrode groups, either way
|
||||
pdn_hints: list = [] # per-terminal Component text
|
||||
term_nets: list = [] # per-terminal overlapped nets
|
||||
has_selection = bool(list(board.get_selection()))
|
||||
try:
|
||||
if cfg is not None and cfg.pos_parts is not None:
|
||||
es1, es2 = board_io.resolve_classic_parts(
|
||||
board, stackup, cfg.pos_parts,
|
||||
cfg.neg_parts, cfg.net)
|
||||
net_hint = cfg.net
|
||||
else:
|
||||
es1, es2, net_hint = board_io.get_electrodes(
|
||||
board, stackup)
|
||||
except SelectionError as e:
|
||||
classic_reason = str(e)
|
||||
try:
|
||||
if pdn_cfg:
|
||||
# config refs resolve against run.net; a broken
|
||||
# ref disables PDN mode instead of killing the
|
||||
# launch (classic may still work)
|
||||
if not cfg.net:
|
||||
raise ConfigError(
|
||||
f"{cfg_path.name}: run.net is required "
|
||||
f"to resolve the config terminals")
|
||||
terminals = board_io.resolve_terminal_specs(
|
||||
board, stackup, cfg.terminals, cfg.net)
|
||||
# rectangles drawn since the save become NEW
|
||||
# terminals - the file freezes nothing. A scan
|
||||
# problem only forfeits the new ones, never
|
||||
# the config set
|
||||
try:
|
||||
new_terms = board_io.new_marker_terminals(
|
||||
cfg.terminals,
|
||||
board_io.scan_marker_terminals(
|
||||
board, require_both=False))
|
||||
except (SelectionError, ConfigError) as e:
|
||||
merge_note = (f"rectangle scan failed ({e})"
|
||||
f" - new rectangles not "
|
||||
f"offered")
|
||||
print(f"note: {merge_note}")
|
||||
pdn_groups = (
|
||||
[t.electrodes for t in terminals]
|
||||
+ [mt.electrodes for mt in new_terms])
|
||||
else:
|
||||
marker_terms = board_io.scan_marker_terminals(
|
||||
board)
|
||||
pdn_groups = [mt.electrodes
|
||||
for mt in marker_terms]
|
||||
pdn_hints = board_io.component_hints(board,
|
||||
pdn_groups)
|
||||
except (SelectionError, ConfigError) as e:
|
||||
pdn_reason = str(e)
|
||||
|
||||
if board_io.any_zone_unfilled(board) or config.ALWAYS_REFILL:
|
||||
board_io.refill(board)
|
||||
fills = board_io.gather_net_fills(board)
|
||||
tracks = board_io.gather_net_tracks(board)
|
||||
copper = board_io.merge_copper(
|
||||
fills, board_io.tracks_as_polygons(tracks))
|
||||
|
||||
classic_nets: list = []
|
||||
pdn_nets: list = []
|
||||
if classic_reason is None:
|
||||
classic_nets = board_io.nets_overlapping(
|
||||
copper, es1, es2)
|
||||
if not classic_nets:
|
||||
classic_reason = (
|
||||
"No copper (zone fill or trace) overlaps "
|
||||
"both contacts. Check that both sit over "
|
||||
"copper of the same net and that the fills "
|
||||
"are up to date (press B in the board "
|
||||
"editor).")
|
||||
if pdn_reason is None:
|
||||
# per-terminal net sets drive BOTH the candidate
|
||||
# list (a net qualifies with >= 1 supply and >= 1
|
||||
# load terminal on it) and the dialog's row filter
|
||||
# (only terminals on the selected net are shown
|
||||
# and solved) - config and live sources alike
|
||||
term_nets = board_io.group_nets(copper, pdn_groups)
|
||||
roles = (([t.role for t in terminals]
|
||||
+ [mt.role for mt in new_terms]) if pdn_cfg
|
||||
else [mt.role for mt in marker_terms])
|
||||
sup_nets: set = set()
|
||||
load_nets: set = set()
|
||||
for role, tn in zip(roles, term_nets):
|
||||
(sup_nets if role == "supply"
|
||||
else load_nets).update(tn)
|
||||
pdn_nets = sorted(sup_nets & load_nets)
|
||||
if not pdn_nets:
|
||||
pdn_reason = (
|
||||
"no net's copper overlaps at least one "
|
||||
"supply and one load "
|
||||
+ (f"terminal of {cfg_path.name}"
|
||||
if pdn_cfg else "rectangle"))
|
||||
elif pdn_cfg and cfg.net not in pdn_nets:
|
||||
print(f"note: run.net '{cfg.net}' has no "
|
||||
f"workable supply+load copper; PDN "
|
||||
f"candidates: {', '.join(pdn_nets)}")
|
||||
if classic_reason is not None and pdn_reason is not None:
|
||||
raise SelectionError(
|
||||
f"{classic_reason}\n(PDN mode is also "
|
||||
f"unavailable: {pdn_reason})")
|
||||
buildups = board_io.gather_mask_buildups(board)
|
||||
except ApiError as e:
|
||||
raise UserFacingError(
|
||||
f"KiCad API error: {e}\nIf KiCad is showing a dialog, "
|
||||
f"close it and run again."
|
||||
)
|
||||
|
||||
def group_label(parts):
|
||||
names = [p.label for p in parts[:3]]
|
||||
more = f" +{len(parts) - 3}" if len(parts) > 3 else ""
|
||||
return f"{len(parts)}× " + ", ".join(names) + more
|
||||
|
||||
def group_contact(parts):
|
||||
contacts = {p.contact for p in parts}
|
||||
return contacts.pop() if len(contacts) == 1 else "auto"
|
||||
|
||||
def rect_desc(e):
|
||||
r = e.rect
|
||||
return (f"rect ({r.x0 / 1e6:.1f}, {r.y0 / 1e6:.1f}).."
|
||||
f"({r.x1 / 1e6:.1f}, {r.y1 / 1e6:.1f}) mm")
|
||||
|
||||
def marker_desc(mt):
|
||||
if len(mt.electrodes) == 1:
|
||||
return rect_desc(mt.electrodes[0])
|
||||
# same-named rectangles grouped into one bonded lug
|
||||
return (f"{len(mt.electrodes)}× "
|
||||
f"{rect_desc(mt.electrodes[0])} … — bonded")
|
||||
|
||||
def live_row(mt, hint, tn):
|
||||
return dialog.PdnTerminalRow(
|
||||
name=mt.name, role=mt.role,
|
||||
resolved=marker_desc(mt), component=hint,
|
||||
bonded=mt.bonded, nets=tn)
|
||||
|
||||
defaults = configfile.dialog_defaults(cfg)
|
||||
pdn_setup = None
|
||||
if pdn_cfg and pdn_reason is None:
|
||||
n_cfg = len(terminals)
|
||||
rows = [dialog.PdnTerminalRow(
|
||||
name=t.label, role=t.role,
|
||||
resolved=(group_label(t.electrodes)
|
||||
+ (" — bonded" if t.bonded else "")),
|
||||
component=hint,
|
||||
i_draw_a=(t.i_draw_a if t.role == "load"
|
||||
else None),
|
||||
r_out_ohm=(t.r_out_ohm if t.role == "supply"
|
||||
else None),
|
||||
v_oc=t.v_oc, bonded=t.bonded,
|
||||
contact=spec.contact or "auto",
|
||||
active=spec.active, comment=spec.comment,
|
||||
nets=tn, from_config=True)
|
||||
for t, spec, hint, tn in zip(
|
||||
terminals, cfg.terminals, pdn_hints[:n_cfg],
|
||||
term_nets[:n_cfg])]
|
||||
# newly drawn rectangles append as live rows: a save
|
||||
# writes them into the config alongside the file's set
|
||||
rows += [live_row(mt, hint, tn)
|
||||
for mt, hint, tn in zip(new_terms,
|
||||
pdn_hints[n_cfg:],
|
||||
term_nets[n_cfg:])]
|
||||
notes = []
|
||||
if new_terms:
|
||||
notes.append(f"{len(new_terms)} new rectangle(s) "
|
||||
f"not in {cfg_path.name} yet - "
|
||||
f"Save config… adds them")
|
||||
if merge_note:
|
||||
notes.append(merge_note)
|
||||
pdn_setup = dialog.PdnSetup(
|
||||
rows=rows, source=cfg_path.name, from_config=True,
|
||||
note="; ".join(notes))
|
||||
elif pdn_reason is None:
|
||||
note = ""
|
||||
if has_selection:
|
||||
note = ("board selection ignored in PDN mode - "
|
||||
"terminals are the marker rectangles")
|
||||
print(f"note: {note}")
|
||||
pdn_setup = dialog.PdnSetup(
|
||||
rows=[live_row(mt, hint, tn)
|
||||
for mt, hint, tn in zip(marker_terms,
|
||||
pdn_hints,
|
||||
term_nets)],
|
||||
source=(f"marker rectangles on "
|
||||
f"{config.ELECTRODE_POS_LAYER}/"
|
||||
f"{config.ELECTRODE_NEG_LAYER}"),
|
||||
note=note)
|
||||
|
||||
# cfg.mode is only the starting radio - never a pin
|
||||
start_pdn = ((cfg is not None and cfg.mode == "pdn"
|
||||
and pdn_reason is None)
|
||||
or classic_reason is not None)
|
||||
if start_pdn:
|
||||
default_net = (defaults.net if defaults.net in pdn_nets
|
||||
else (pdn_nets[0] if pdn_nets else ""))
|
||||
else:
|
||||
default_net = (defaults.net if defaults.net in classic_nets
|
||||
else net_hint if net_hint in classic_nets
|
||||
else classic_nets[0])
|
||||
|
||||
def rect_infos(terms):
|
||||
# unlabeled terminals are always single rectangles, so
|
||||
# freezing the first rect's coordinates is exact
|
||||
return [(mt.labeled,
|
||||
(mt.electrodes[0].rect.x0 / 1e6,
|
||||
mt.electrodes[0].rect.y0 / 1e6,
|
||||
mt.electrodes[0].rect.x1 / 1e6,
|
||||
mt.electrodes[0].rect.y1 / 1e6))
|
||||
for mt in terms]
|
||||
|
||||
def save_cb(sel, target, cfg=cfg, cfg_path=cfg_path,
|
||||
pdn_cfg=pdn_cfg, marker_terms=marker_terms,
|
||||
new_terms=new_terms):
|
||||
if sel.mode == "classic":
|
||||
configfile.save_classic_config(target, sel)
|
||||
elif pdn_cfg:
|
||||
# the config rows update positionally; newly drawn
|
||||
# rectangles append as fresh terminal entries
|
||||
n = len(cfg.raw["terminals"])
|
||||
tj = configfile.updated_terminals_json(
|
||||
cfg.raw["terminals"], sel.pdn_rows[:n])
|
||||
if sel.pdn_rows[n:]:
|
||||
tj += configfile.rect_terminals_json(
|
||||
sel.pdn_rows[n:], rect_infos(new_terms))
|
||||
print(f"note: {len(sel.pdn_rows[n:])} new "
|
||||
f"terminal(s) added to the config")
|
||||
configfile.save_pdn_config(target, sel, tj)
|
||||
else:
|
||||
# EVERY row is saved - off-net ones arrive from the
|
||||
# dialog as active: false (nothing drawn on the
|
||||
# board is lost by a save); labeled (possibly
|
||||
# grouped) rectangles save as rect:NAME
|
||||
configfile.save_pdn_config(
|
||||
target, sel,
|
||||
configfile.rect_terminals_json(
|
||||
sel.pdn_rows, rect_infos(marker_terms)))
|
||||
print("note: the saved config now provides the "
|
||||
"terminal set on later launches - labeled "
|
||||
"rectangles stay live (rect:NAME), unlabeled "
|
||||
"ones were frozen as coordinates; remove the "
|
||||
"terminals section (or the file) to return "
|
||||
"to the live rectangle scan")
|
||||
print(f"config saved to {target}")
|
||||
# only "default" (or its legacy plain spelling) and the
|
||||
# board-stem name load on launch; other names need the
|
||||
# Load config… button - say so before it surprises
|
||||
auto = {config.CONFIG_FILENAME,
|
||||
configfile.named_config_filename("default")}
|
||||
stem = Path(getattr(board, "name", "") or "").stem
|
||||
if stem:
|
||||
auto.add(f"{stem}.{config.CONFIG_FILENAME}")
|
||||
if target.name not in auto:
|
||||
print("note: this name does not load automatically "
|
||||
"- pull it in with Load config…")
|
||||
return target.name
|
||||
|
||||
selection = dialog.ask(
|
||||
candidates={n: list(copper[n].keys())
|
||||
for n in classic_nets},
|
||||
layer_order=stackup.names,
|
||||
default_net=default_net,
|
||||
e1_label=(group_label(es1) if es1 else ""),
|
||||
e2_label=(group_label(es2) if es2 else ""),
|
||||
contact1=((defaults.contact1 or group_contact(es1))
|
||||
if es1 else "auto"),
|
||||
contact2=((defaults.contact2 or group_contact(es2))
|
||||
if es2 else "auto"),
|
||||
buildup_layers=sorted(buildups.keys()),
|
||||
defaults=defaults, pdn=pdn_setup,
|
||||
pdn_candidates={n: list(copper[n].keys())
|
||||
for n in pdn_nets},
|
||||
classic_reason=classic_reason, pdn_reason=pdn_reason,
|
||||
save_callback=save_cb,
|
||||
save_target=(cfg_path if cfg_path is not None else
|
||||
board_io.board_dir(board)
|
||||
/ config.CONFIG_FILENAME),
|
||||
load_dir=board_io.board_dir(board),
|
||||
start_mode=("pdn" if start_pdn else "classic"),
|
||||
)
|
||||
|
||||
def group_label(parts):
|
||||
names = [p.label for p in parts[:3]]
|
||||
more = f" +{len(parts) - 3}" if len(parts) > 3 else ""
|
||||
return f"{len(parts)}× " + ", ".join(names) + more
|
||||
|
||||
def group_contact(parts):
|
||||
contacts = {p.contact for p in parts}
|
||||
return contacts.pop() if len(contacts) == 1 else "auto"
|
||||
|
||||
default_net = (net_hint if net_hint in candidate_nets
|
||||
else candidate_nets[0])
|
||||
selection = dialog.ask(
|
||||
candidates={n: list(copper[n].keys()) for n in candidate_nets},
|
||||
layer_order=stackup.names,
|
||||
default_net=default_net,
|
||||
e1_label=group_label(es1), e2_label=group_label(es2),
|
||||
contact1=group_contact(es1), contact2=group_contact(es2),
|
||||
buildup_layers=sorted(buildups.keys()),
|
||||
)
|
||||
if isinstance(selection, dialog.LoadRequest):
|
||||
# re-derive everything from the picked file; its validity
|
||||
# was already checked by the dialog before it closed
|
||||
cfg_path = selection.path
|
||||
continue
|
||||
break
|
||||
if selection is None:
|
||||
print("cancelled")
|
||||
return
|
||||
@@ -108,25 +403,86 @@ def main() -> None:
|
||||
# looks like it did nothing until the figures appear
|
||||
progress.start()
|
||||
|
||||
if selection.contact1 != "auto":
|
||||
for e in es1:
|
||||
e.contact = selection.contact1
|
||||
if selection.contact2 != "auto":
|
||||
for e in es2:
|
||||
e.contact = selection.contact2
|
||||
run_pdn = selection.mode == "pdn"
|
||||
if run_pdn:
|
||||
def live_terminal(mt, row):
|
||||
if row.contact not in ("", "all", "auto"):
|
||||
# dialog Layer pick: this terminal's rectangles
|
||||
# contact only that copper layer
|
||||
for e in mt.electrodes:
|
||||
e.contact = row.contact
|
||||
return Terminal(
|
||||
role=row.role, electrodes=mt.electrodes,
|
||||
label=row.name,
|
||||
i_draw_a=(row.i_draw_a
|
||||
if row.i_draw_a is not None else 0.0),
|
||||
r_out_ohm=(row.r_out_ohm
|
||||
if row.r_out_ohm is not None else 0.0),
|
||||
v_oc=row.v_oc, bonded=mt.bonded,
|
||||
component=row.component, comment=row.comment)
|
||||
if pdn_cfg:
|
||||
cfg_rows = selection.pdn_rows[:len(terminals)]
|
||||
new_rows = selection.pdn_rows[len(terminals):]
|
||||
# a changed Layer scope is geometry: push it onto the
|
||||
# specs and re-resolve (part-level contacts inside the
|
||||
# file still win, exactly as the schema promises)
|
||||
changed = False
|
||||
for spec, row in zip(cfg.terminals, cfg_rows):
|
||||
if (row.contact or "auto") != (spec.contact or "auto"):
|
||||
spec.contact = row.contact
|
||||
changed = True
|
||||
if changed:
|
||||
try:
|
||||
terminals = board_io.resolve_terminal_specs(
|
||||
board, stackup, cfg.terminals, cfg.net)
|
||||
except ApiError as e:
|
||||
raise UserFacingError(f"KiCad API error: {e}")
|
||||
# dialog value edits win for the run: write them back
|
||||
# onto the resolved terminals (positional, same order),
|
||||
# then drop the unchecked ones - they stay in the file
|
||||
# but take no part in the solve; newly drawn
|
||||
# rectangles run as live terminals
|
||||
for t, row in zip(terminals, cfg_rows):
|
||||
if t.role == "load":
|
||||
t.i_draw_a = row.i_draw_a
|
||||
else:
|
||||
t.r_out_ohm = row.r_out_ohm
|
||||
t.v_oc = row.v_oc
|
||||
t.component = row.component
|
||||
t.comment = row.comment
|
||||
terminals = (
|
||||
[t for t, row in zip(terminals, cfg_rows)
|
||||
if row.active]
|
||||
+ [live_terminal(mt, row)
|
||||
for mt, row in zip(new_terms, new_rows)
|
||||
if row.active])
|
||||
else:
|
||||
terminals = [live_terminal(mt, row)
|
||||
for mt, row in zip(marker_terms,
|
||||
selection.pdn_rows)
|
||||
if row.active]
|
||||
else:
|
||||
if selection.contact1 != "auto":
|
||||
for e in es1:
|
||||
e.contact = selection.contact1
|
||||
if selection.contact2 != "auto":
|
||||
for e in es2:
|
||||
e.contact = selection.contact2
|
||||
if selection.cell_um is not None:
|
||||
config.CELL_UM_OVERRIDE = selection.cell_um
|
||||
config.ADAPTIVE_CELLS = selection.adaptive
|
||||
|
||||
try:
|
||||
problem = board_io.build_problem(
|
||||
board, selection.net, selection.layers, es1, es2, stackup,
|
||||
fills,
|
||||
board, selection.net, selection.layers,
|
||||
([] if run_pdn else es1), ([] if run_pdn else es2),
|
||||
stackup, fills,
|
||||
buildups=(buildups if selection.include_buildup else None),
|
||||
extra_cu_um=selection.extra_cu_um,
|
||||
tracks=(tracks if selection.include_tracks else None),
|
||||
vias_capped=selection.vias_capped,
|
||||
cap_max_drill_mm=selection.cap_max_drill_mm)
|
||||
cap_max_drill_mm=selection.cap_max_drill_mm,
|
||||
terminals=(terminals if run_pdn else None))
|
||||
outdir = report.make_output_dir(board_io.board_dir(board))
|
||||
except ApiError as e:
|
||||
raise UserFacingError(f"KiCad API error: {e}")
|
||||
@@ -145,12 +501,15 @@ def main() -> None:
|
||||
trim_abs = selection.trim_value
|
||||
else:
|
||||
trim_pct = selection.trim_value
|
||||
pipeline.run(problem, outdir, show=True, i_test=selection.current_a,
|
||||
pipeline.run(problem, outdir, show=True,
|
||||
i_test=(None if run_pdn else selection.current_a),
|
||||
freq_hz=selection.freq_hz,
|
||||
contact_model=selection.contact_model,
|
||||
contact_model=(None if run_pdn
|
||||
else selection.contact_model),
|
||||
overlay=overlay_cb,
|
||||
trim_pct=trim_pct, trim_abs=trim_abs,
|
||||
trim_push=trim_cb)
|
||||
trim_push=trim_cb,
|
||||
v_nominal=(selection.v_nominal if run_pdn else None))
|
||||
except progress.Cancelled:
|
||||
print("cancelled") # user's own doing: no error figure
|
||||
except UserFacingError as e:
|
||||
|
||||
Reference in New Issue
Block a user