Power-Delivery-Network (PDN) analysis for Altium and Gerber PCB designs.
Extracts copper geometry directly from a .PrjPcb project or from a Gerber
fabrication package, runs a 2-D FEM Laplace solver per copper layer to compute
voltage drop and current density across power rails, and visualises the result
in a custom OpenGL viewer with per-vertex shading.
Name: FYPA stands for FEM Y-parameter Power Analyser — the tool extracts the admittance (Y-parameter) matrix of each copper layer via a finite-element solve, then uses it to compute steady-state voltage, current density, and power dissipation across the PDN.
Based on padne (the FEM solver) and uses altium_monkey to read the Altium project files.
New user? Start with the User Guide for a step-by-step walkthrough of common workflows.
Disclaimer: FYPA is a design-aid tool. Treat its results as guidance and validate against measurement.
You annotate components in your Altium schematic with a small set of
PDN_* parameters (which net is the input, which is the output, the current
or voltage), point this tool at the .PrjPcb, and it:
- Extracts every copper feature (tracks, arcs, regions, pads, vias) along with the net assignments.
- Builds a per-(physical-layer, net) 2-D geometry from the Shapely union of all copper for that net on that layer.
- Triangulates each layer and runs a sparse-direct FEM Laplace solve.
- Solves for the voltage field, current density
|J|, and power-density on every node. - Opens an interactive GPU-accelerated viewer where you can:
- Switch layers / rails / display modes (Voltage / Voltage Drop / Current Density / Power Density)
- Pan and zoom natively on the GPU (no rasterisation; pixel-sharp at every zoom level)
- Drag handles or type values to clamp the colour scale
- Hover for a probe readout
- Cross-check per-pin voltages and per-via currents in sortable tables
- Switch to editor mode to place sources / sinks and re-solve without editing the Altium schematic (see below)
If you just want to run the tool — no Python install, no git clone — grab the latest packaged build from the Releases page:
- Download
FYPA_v<version>.zipfrom the latest release's Assets. - Extract it anywhere permanent (e.g.
C:\Tools\FYPA\). - Double-click
FYPA.exeinside the extracted folder.
On first launch Windows SmartScreen will show a blue warning — click "More info" then "Run anyway" (the exe is unsigned; this is a one-time prompt per machine).
Keep
FYPA.exeand the_internal\folder together — moving the exe alone will break it. Create a desktop shortcut rather than moving the exe.
To build the executable yourself, see Building a standalone executable.
FYPA uses uv for dependency and Python-version management. Install uv once (Windows):
winget install astral-sh.uv # or: pip install uvFirst-time install: restart your terminal. The installer adds
uvto yourPATH, but shells that are already open don't see the change — you'll getuv : The term 'uv' is not recognized. Close and reopen the terminal. If you ran the install from the VS Code terminal, close and reopen VS Code itself (it caches the environment it was launched with; a new terminal tab isn't enough).uv --versionshould then work.
Then clone and sync:
git clone git@github.com:anarthrous-eda/FYPA.git
cd FYPA
uv syncThat's it. uv sync reads pyproject.toml + uv.lock, fetches Python 3.12
if it isn't already available (the version is pinned in .python-version),
creates a .venv\ inside the repo, and installs every runtime + dev
dependency — including altium_monkey, which uv pulls from PyPI at the
version pinned in pyproject.toml.
Python version: 3.12 only. Both FYPA and
altium_monkeypinrequires-python = ">=3.12,<3.13"..python-versionselects 3.12, which uv fetches automatically.
Day-to-day commands:
uv run python FYPA.py gui path\to\YourBoard.PrjPcb # run the app
uv run pytest -m "not slow" # fast test suite
uv run ruff check . # lintuv run automatically syncs the venv if it's out of date, so you don't need
to activate first. If you prefer the classic flow, .venv\Scripts\activate
still works.
PyOpenGL and PyOpenGL_accelerate are required for the GPU viewer.
Annotate your Altium schematic with PDN_* parameters on the components that
define the power-delivery topology:
| Role | Required parameters | What it means |
|---|---|---|
SOURCE |
PDN_V, PDN_P_NET, PDN_N_NET — or PDN_V, PDN_NET |
Voltage source (e.g. a connector pin or regulator) |
SINK |
PDN_I, PDN_P_NET, PDN_N_NET — or PDN_I, PDN_NET |
Current sink (e.g. an IC load) |
SERIES |
PDN_R, PDN_P_NET*, PDN_N_NET* |
Series resistance / fuse / ferrite / inductor DCR (rail bridge) |
REGULATOR |
PDN_V, PDN_REGULATOR_TYPE, PDN_REGULATOR_EFFICIENCY, optional PDN_QUIESCENT — or PDN_GAIN, plus PDN_OUT_* / PDN_IN_* nets |
On-board regulator (LDO / buck) — models BOTH input and output rails |
Optional two-terminal helpers (SOURCE / SINK):
| Parameter | Purpose |
|---|---|
PDN_P_PINS / PDN_N_PINS |
Restrict which pads on the host (or DES-listed parts) couple |
PDN_P_DES / PDN_N_DES |
Pull that terminal's pads from other designators only (host not auto-included); for multi-connector / banana-style sources |
PDN_PIN_R (SINK) |
Package pin-to-pin resistance in ohms, used to share current between a multi-pin terminal's pads. Defaults to 100 mΩ — see Settings → Multi-pin coupling resistance. Set it per part when you know the package: a large BGA's supply grid is far lower than a leadframe's. |
* PDN_P_NET and PDN_N_NET are optional for SERIES on a 2-pin part — the
tool auto-infers them from the component's pad connectivity.
Add PDN_ROLE on the component (e.g. PDN_ROLE=SOURCE) and the required parameters for that role.
For 2-pin parts the tool can auto-infer PDN_P_NET /
PDN_N_NET from connectivity; for ICs you'll need to set them explicitly.
Pads on a rail net are collected automatically. To keep enable/signal ties
off the terminal — especially on multi-rail ICs in a SchLib — set part-wide
PDN_PINS_ONLY (optional PDN_EXTRA_PINS adds to that list on the instance;
EXTRA alone is the full allowlist). Per-terminal PDN_P_PINS / PDN_N_PINS
still override one terminal; PDN_IGNORE / PDN_IGNORE_PINS fine-tune
exclusions. See
Restricting which pins may join
in the user guide.
A SINK can carry an optional PDN_MIN_V parameter giving the minimum
acceptable rail voltage at the part's P pins. The viewer's Nodes tab adds
Min V, Margin and Status columns: margin is the per-pin measured voltage
minus PDN_MIN_V, and rows with a negative margin are highlighted in red
(Status = FAIL). Sinks without PDN_MIN_V show — in those columns; the
check is purely a post-solve report, it doesn't change what the solver does.
U5 (3V3 load):
PDN_ROLE = SINK
PDN_I = 500mA
PDN_P_NET = +3V3
PDN_N_NET = 0V
PDN_MIN_V = 3.2 # fail if any +3V3 pin drops below 3.2 V
Indexed multi-channel sinks support PDN<n>_MIN_V per channel, the same way
PDN<n>_I works.
Sometimes a net has no return reference at all — tracing power from a
connector to a high-side switch, say, where the switch has only a supply
pin. For that case give a SOURCE/SINK a single PDN_NET instead of the
PDN_P_NET / PDN_N_NET pair:
J1 (connector feeding VBATT): U3 (high-side switch input):
PDN_ROLE = SOURCE PDN_ROLE = SINK
PDN_V = 12V PDN_I = 5A
PDN_NET = VBATT PDN_NET = VBATT
The directive then has one terminal on PCB copper; its other terminal is an
ideal 0 Ω return, so the result reflects only that net's copper voltage drop
— the point-to-point IR drop you're after. (PDN_PINS overrides the pad set,
the way PDN_P_PINS / PDN_N_PINS do for two-terminal directives.)
Rules:
PDN_NETandPDN_P_NET/PDN_N_NETare mutually exclusive on one directive — giving both, or neither, is an error.- A single-net analysis still needs a closed loop: at least one
SOURCEand oneSINKon the same net (a lone one is an "open loop" error). - Single-net mode is
SOURCE/SINKonly —SERIESbridges two nets andREGULATORhas four terminals. - Every
SOURCEandSINKthat shares a net must use the same mode; a group can't mix single-net and two-terminal directives. Independent groups may use different modes in the same solve.
SOURCE, SINK, REGULATOR, and SERIES roles support multiple
independent channels on a single part. Channels are addressed by appending
a positive integer to PDN in the parameter prefix:
| Channel | SOURCE / REGULATOR | SINK | SERIES | Net params (examples) |
|---|---|---|---|---|
| legacy | PDN_V |
PDN_I |
PDN_R |
PDN_P_NET, PDN_OUT_P_NET, … |
| 1 | PDN1_V |
PDN1_I |
PDN1_R |
PDN1_P_NET, PDN1_OUT_P_NET, … |
| 2 | PDN2_V |
PDN2_I |
PDN2_R |
PDN2_P_NET, … |
| … | … | … | … | … |
REGULATOR channels also require the four PDN<n>_OUT_* / PDN<n>_IN_* net
(or pin) parameters per channel (IN may be inherited from unindexed
PDN_IN_*; see templates below).
The legacy unindexed channel and any number of indexed channels can coexist
as independent directives when the unindexed form has its own terminals.
Indices are sparse — gaps are allowed (e.g. just PDN_V + PDN2_V). A
channel is present when its value parameter or a channel-defining terminal
is set; unset indexed params inherit from the matching unindexed
PDN_* template. The part-wide PDN_ROLE is the default role for every
channel — a channel can override it with PDN<n>_ROLE (see Mixed-role
parts below).
Templates: the unindexed form stays a real directive alongside indexed
channels only when it is a channel in its own right — its own value
parameter and a complete terminal set (both P and N for
SOURCE/SINK/SERIES; both OUT_* sides for REGULATOR). Otherwise unindexed
PDN_* values are defaults only and no legacy directive is emitted. A
shared PDN_N_NET = GND or PDN_IN_* alone is a template, and so is a
complete PDN_P_NET + PDN_N_NET pair with the values on PDN1_I /
PDN2_I. Unindexed PDN_NET / PDN_PINS with its own value is a complete
single-net channel and is kept alongside indexed channels.
Indexed-only parts (PDNn_ROLE without PDN_ROLE) always treat unindexed
values as templates when indexed channels exist.
Inheritance is scoped to the part-wide role: a channel that overrides it
with PDN<n>_ROLE reads nothing from the template, so a SERIES channel on
a SINK part cannot pick up the sink's nets. The two terminal forms are also
atomic — a channel that names PDN1_P_NET / PDN1_N_NET never inherits a
shared PDN_NET, and vice versa. Example — shared SERIES resistance:
SW1:
PDN_ROLE = SERIES
PDN_R = 0.05
PDN1_P_NET = VIN_A PDN1_N_NET = VOUT_A
PDN2_P_NET = VIN_B PDN2_N_NET = VOUT_B
Example — a SINK with three independent supply rails:
U7 (multi-rail IC load):
PDN_ROLE = SINK
PDN_I = 500mA PDN_P_NET = +3V3 PDN_N_NET = GND
PDN1_I = 250mA PDN1_P_NET = +1V8 PDN1_N_NET = GND
PDN2_I = 50mA PDN2_P_NET = +5V PDN2_N_NET = GND
Example — a PMIC with two regulator outputs on one symbol:
U4:
PDN_ROLE = REGULATOR
PDN_V = 3.3 PDN_GAIN = 0.9
PDN_OUT_P_NET = +3V3 PDN_OUT_N_NET = GND
PDN_IN_P_NET = +5V PDN_IN_N_NET = GND
PDN1_V = 1.8 PDN1_GAIN = 0.85
PDN1_OUT_P_NET = +1V8 PDN1_OUT_N_NET = GND
PDN1_IN_P_NET = +5V PDN1_IN_N_NET = GND
The Setup tab and the Nodes-tab table label indexed channels as
U7#1, U7#2 so they're easy to tell apart from the legacy U7 channel.
For SERIES, auto-inferred P/N nets (2-pin parts) apply only when the
part carries a single channel; multi-channel SERIES requires explicit
PDNn_P_NET / PDNn_N_NET or PDNn_P_PINS / PDNn_N_PINS per channel.
On repeated schematic sheets (Altium REPEAT), PDN_*_NET may use
the local child-sheet net label; FYPA maps each PCB instance to its slot
net via pin connectivity (not via ChannelDesignatorFormatString). See
User guide — Local net names.
Different values per placement use sheet-symbol parameters
PDN_<Designator>_* (e.g. PDN_J1_I) — see
Per-instance overrides on sheet symbols.
The same overrides apply when the shared role/nets live only on the PCB
(Blanket / ECO) and the per-instance value is on the sheet symbol.
PDN_ROLE is the part-wide default role, but any channel may override
it with PDN<n>_ROLE. That lets a single physical part carry channels of
different roles — the case where one component is both a source and a
sink.
The motivating example is a DAC: its supply pins sink current from the
power rails, while its outputs source current into downstream loads.
Put the part's majority role on PDN_ROLE and override only the channels
that differ:
U7 (DAC):
PDN_ROLE = SINK # part-wide default
PDN_I = 80mA PDN_P_NET = AVDD PDN_N_NET = GND
PDN1_I = 20mA PDN1_P_NET = DVDD PDN1_N_NET = GND
PDN2_ROLE = SOURCE # this channel overrides
PDN2_V = 2.5 PDN2_P_NET = DAC_OUT0 PDN2_N_NET = GND
PDN3_ROLE = SOURCE
PDN3_V = 1.8 PDN3_P_NET = DAC_OUT1 PDN3_N_NET = GND
Here channels 0–1 are sinks (AVDD, DVDD supplies) and channels 2–3 are
sources (the two DAC outputs). Each channel's effective role decides which
value parameter marks it present (PDN<n>_V for a SOURCE/REGULATOR channel,
PDN<n>_I for SINK, PDN<n>_R for SERIES) and which net/pin parameters it
reads. PDN<n>_ROLE must be one of SOURCE, SINK, SERIES, REGULATOR.
Notes and pitfalls:
- Uniform parts need nothing new. Two sinks are still just
PDN_ROLE = SINK+PDN_I/PDN1_I— you only writePDN<n>_ROLEfor the channels that diverge from the default. Existing designs are unaffected. - Mix roles across different nets. A SOURCE and a SINK on the same net of one part simply feed current straight back into each other. The mixed-role scheme is for channels on distinct nets (e.g. a supply rail vs a DAC output).
- A genuine input→output converter is a
REGULATOR, not a mixed part. If the part draws on an input rail and supplies a derived output rail (with a current gain between them), model it with a singleREGULATORchannel — see below — rather than a hand-paired SOURCE + SINK. - Current-output loads: a
SOURCEfixes voltage, not current. To impose a known DAC output current, set it on the loadSINKat the far end of the output net; the DAC-output SOURCE then supplies exactly that current through the output copper, and the trace IR drop shows on that rail.
A SOURCE represents a supply edge — the voltage just appears on the
specified net and the solver assumes whatever is upstream of it is infinite.
Use it for a connector pin, a battery terminal, or any spot where you don't
care what's upstream.
A REGULATOR represents an on-board regulator (LDO, buck, etc.) — it
pins its output net at PDN_V AND pulls current from its input net
proportional to the output current. Use it when both the input AND the
output rail are part of the PDN you're analyzing, so you can see the
voltage drop the regulator causes on its input side too.
Set PDN_REGULATOR_TYPE (LDO or SMPS) and optionally
PDN_REGULATOR_EFFICIENCY to auto-compute gain, or set PDN_GAIN manually.
Use Adaptive SMPS gain in the viewer (or --adaptive-regulator-gain on
the CLI) to refine SMPS gain from the solved input voltage.
| Regulator type | Gain |
|---|---|
| LDO | 1.0 |
| SMPS (initial) | Vout / (Vin_nom × efficiency) |
| SMPS (adaptive re-solve) | Vout / (Vin_solved × efficiency) |
Worked example — a board with a 5V connector input feeding a 3V3 LDO:
J1 (input connector):
PDN_ROLE = SOURCE
PDN_V = 5
PDN_P_NET = +5V
PDN_N_NET = 0V
U2 (3V3 LDO):
PDN_ROLE = REGULATOR
PDN_REGULATOR_TYPE = LDO
PDN_V = 3.3
PDN_OUT_P_NET = +3V3
PDN_OUT_N_NET = 0V
PDN_IN_P_NET = +5V
PDN_IN_N_NET = 0V
U5 (3V3 load — your IC):
PDN_ROLE = SINK
PDN_I = 500mA
PDN_P_NET = +3V3
PDN_N_NET = 0V
With this setup the solver models 500 mA flowing from +3V3 into U5
(showing drop on the +3V3 copper between U2 and U5) AND 500 mA being
drawn from +5V into U2 (showing drop on the +5V copper between J1
and U2). Replacing U2 with a SOURCE would zero out that second drop
because no current would flow through the +5V copper.
If every rail on your board comes in pre-regulated (no on-board
regulators), you'll never need REGULATOR — SOURCE for inputs and
SINK for loads is the whole story.
Then run the all-in-one solve + viewer:
python FYPA.py gui path\to\YourBoard.PrjPcbOr use the individual subcommands:
python FYPA.py extract YourBoard.PrjPcb # raw record summary
python FYPA.py annotations YourBoard.PrjPcb # parsed PDN_* directives
python FYPA.py geometry YourBoard.PrjPcb # per-layer copper summary
python FYPA.py load YourBoard.PrjPcb # full pipeline, readiness report
python FYPA.py solve YourBoard.PrjPcb out.pkl # solve + pickle
python FYPA.py show out.pkl # open viewer on a saved pickle
python FYPA.py paraview out.pkl out_dir\ # export to ParaView VTK (writes into a folder)
python FYPA.py gerber-gui path\to\gerber_folder # import a board from Gerber + Excellon filesYou don't have to round-trip through the Altium schematic to set up an analysis. The viewer has an editor mode for placing the PDN directly on the board:
- Click a component to attach a
SOURCEorSINKto it, or drop a free marker on any copper to load a net with no component in the way. - Set the role, voltage / current, and net(s) in the side panel — single-net
or two-net, the same choices the
PDN_*parameters give you. - A component that already carries schematic
PDN_*directives can be unlocked and overridden, for quick "what-if" values without editing Altium. - Hit Resolve to re-run the solver with your edits applied — it reuses the cached design extraction, so it's far quicker than a cold load.
Editor edits are stored in a small .fypa project file beside the board, so
they (and the linked solve) survive a save / reopen and never modify the
Altium source files. A step-by-step walkthrough will follow in a separate
user guide.
On the first run it solves the
FEM and saves the result to FYPA/.cache/<project>_<hash>.pkl
along with a fingerprint of every input that affects the solve:
- The
.PrjPcbplus every.PcbDoc/.SchDocit references (parsed from theDocumentPath=lines) - The tool's own solver-affecting source files (
fypa/altium/extract.py,fypa/altium/annotations.py,fypa/altium/loader.py,fypa/gerber/extract.py,fypa/gerber/loader.py,fypa/altium_geometry.py,fypa/cli.py,fypa/lean_solution.py, and the modifiedpdnsolver/modules —problem.py,mesh.py,solver.py)
On subsequent runs the fingerprint is recomputed and compared. If nothing has changed the cached solution is reused — typical startup drops from ~10–60 s (full solve) to under a second. Editing any referenced PCB / Sch file or any of the tool's source files automatically invalidates the cache.
To force a fresh solve, pass --no-cache:
python FYPA.py gui YourBoard.PrjPcb --no-cacheTo clear all cached solutions, delete the .cache/ directory inside
the tool folder — they'll regenerate on the next run.
A small DelphiScript (Run_FYPA.pas) is bundled
that fires the tool against whichever .PrjPcb is focused in Altium —
no need to copy paths to a console. One-time setup:
-
Point the script at this directory. Open
packaging/Run_FYPA.pasin a text editor and check theSCRIPT_DIRconstant near the top:SCRIPT_DIR = 'C:\path\to\FYPA';Change it to wherever you cloned the repo. The script expects to find
<SCRIPT_DIR>\.venv\Scripts\python.exeand<SCRIPT_DIR>\FYPA.py, so the venv from Installation needs to live inside that directory. -
Optionally change the subcommand via the
SUBCOMMANDconstant. Defaults togui(solve + open viewer). Set toloadfor a solve-readiness report,annotationsto dump the parsedPDN_*directives,extractfor a raw-record summary, etc. -
Register the script in Altium. DXP > Scripting System > Script Projects, add a new script project (or open an existing one) and add
packaging/Run_FYPA.pasto it. -
Run it. With a
.PrjPcbopen and focused in the Projects panel, right-click theRunprocedure in the Script Editor and choose "Run Script". A console window opens and runs:<SCRIPT_DIR>\.venv\Scripts\python.exe FYPA.py gui <FocusedPrjPcb>The console stays open after Python exits (uses
cmd /K) so any tracebacks are readable. Close the console window when you're done.
The script will surface a clear ShowError dialog if any prerequisite
is missing — wrong path, no project open, project unsaved, or the
focused project isn't a .PrjPcb.
The project can be packaged into a self-contained Windows folder using
PyInstaller. The recipient needs no Python
installation — they just extract a zip and double-click the .exe.
PyInstaller is in the opt-in build dependency group (not installed by
default uv sync) because it is only needed by whoever is doing the build,
not by users of the tool. Pull it in once:
uv sync --group buildFrom the project root, double-click packaging\build_dist.bat (or run
it from a terminal). It will:
- Activate the venv
- Auto-install PyInstaller if it is not already present
- Wipe any previous
build/anddist/folders - Run
pyinstaller packaging\FYPA.spec - Copy
README.mdinto the staged output so it travels with the bundle - Delete the intermediate
build/folder - Zip the staged
dist\FYPA\folder into a single distributable archive - Delete the unzipped staging folder so only the zip remains in
dist\
The first build takes 3–5 minutes while PyInstaller scans all packages. Subsequent builds are quicker. The final artefact is:
dist\
FYPA.zip ← share this single file
Inside the zip:
FYPA\
FYPA.exe
README.md
_internal\
[DLLs, .pyd files, Python runtime, ...]
assets\
The standard distribution channel is GitHub Releases, and publishing one
is automated — pushing a version tag runs
.github/workflows/release.yml, which lints,
tests, stamps the tag into __version__, builds the PyInstaller bundle, and
uploads FYPA_v<version>.zip with auto-generated release notes:
git tag -a v1.7.0 -m "FYPA v1.7.0"
git push origin v1.7.0
Run uv run ruff check . and uv run pytest before tagging — the
workflow runs the same gate, and a failure there means no release is created
and the tag has to be deleted locally and on GitHub before the version number
can be reused. Everything in the release must already be merged to main;
the tag is only a pointer to a commit. The release publishes live immediately
(add --draft to the gh release create step if you'd rather review first).
Users follow the Download (prebuilt Windows binary) instructions above.
To build and share a zip without publishing a release, run
packaging\build_dist.bat and send dist\FYPA.zip directly.
Cached solves (.cache\) are written next to FYPA.exe, so they survive
re-extracting a new build over the old folder.
All PyInstaller settings live in packaging\FYPA.spec. Notable options:
| Setting | Default | Notes |
|---|---|---|
console=True |
True |
Shows a terminal window — keeps CLI subcommands usable and errors visible on crash. Set to False for a GUI-only distribution with no console. |
datas |
assets |
Add extra data files/folders here if needed. |
hiddenimports |
PyOpenGL, PySide6 GL, scipy, matplotlib | Extend if the app crashes on launch with ModuleNotFoundError — add the missing module name here. |
excludes |
tkinter, cadquery/OCP/vtk/casadi, unused PySide6 modules |
Trims ~400 MB by dropping the altium_monkey STEP-bounds 3D stack (only used by write-side helpers FYPA never calls) and QML/PDF Qt modules. Extend if you find more bloat. |
After editing the spec, re-run packaging\build_dist.bat to rebuild.
FYPA.py Thin launcher shim (repo root)
fypa/ Application package:
cli.py CLI entry; orchestrates the pipeline
altium/ Altium project front-end:
extract.py altium_monkey adapter → typed dataclasses (mm-based)
annotations.py Parses PDN_* parameters, resolves terminal pins
loader.py Orchestrator; assembles the padne Problem
gerber/ Gerber + Excellon front-end:
extract.py Gerber/drill parser → copper geometry
loader.py Assembles the padne Problem from Gerber input
import_ui.py Layer-mapping / stackup import dialogs
altium_geometry.py Builds per-(layer, net) Shapely MultiPolygons
_clipper_fuse.py Clipper2-based polygon fuse helper (fast union path)
editor_directives.py Editor-mode SOURCE / SINK / SERIES directives
project_file.py Read / write the sidecar .fypa project file
altium_viewer.py Qt viewer (side panel, tabs, scale controller)
gl_mesh_viewer.py Custom QOpenGLWidget — mesh-on-GPU heatmap canvas
paraview_export.py ParaView VTU export
spacemouse_nav.py 3Dconnexion SpaceMouse integration
navlib_camera.py NavLib camera adapter for the SpaceMouse
log_buffer.py In-memory log capture for the Messages tab
lean_solution.py Compact numeric solution (cache / pickle payload)
pdnsolver/ Vendored fork of padne (FEM solver + mesher)
packaging/ PyInstaller spec, build script, Altium launcher
The FEM triangle mesh (vertices, indices, per-vertex scalar values) lives
in OpenGL vertex / index / value buffers. The vertex shader applies a
model-view-projection matrix and normalises the per-vertex value into
[0, 1] using current vmin/vmax uniforms; the fragment shader looks
up the colour from a 1-D RGBA8 LUT texture (sampled from matplotlib's
viridis).
Pan / zoom are pure matrix uniform updates. Colour-scale drag is a uniform
update. Layer / rail / mode changes are a re-upload of the value VBO only
(positions and indices are unchanged within a layer/rail). The CPU keeps a
matplotlib LinearTriInterpolator alongside for the hover-probe lookup and
Voltage Drop reference computation, but never touches the GPU rendering
path.
3Dconnexion SpaceMouse: optional support via the official NavLib on
Windows/macOS (uv sync --extra spacemouse plus 3DxWare)
or spacenavd on Linux (sudo apt install spacenavd libspnav0). Translation
pans; push/pull zooms; rotation orbits in 3D mode. Active while the FYPA
window is open so 3Dconnexion Settings keeps the FYPA profile selected.
Marker overlays (SOURCE / SINK / SERIES / REGULATOR / VIA) and the title /
legend chips are drawn via QPainter inside paintGL so they share the
GL paint engine — no Qt raster-fallback compositor.
AGPL-3.0-or-later. See LICENSE and NOTICE for upstream attributions. The vendored padne fork's modifications are documented in pdnsolver/CHANGES.md.
