Reference model for electromagnetic field transient simulations — codename TUPÃ.
TUPÃ computes the frequency-domain (and, via inverse transform, time-domain) response of networks of thin cylindrical conductors in air and soil — tower footings, counterpoises, grounding grids — under lightning-type excitation. It is an application of the Method of Moments (MoM), known in the power community as the Hybrid Electromagnetic Model (HEM).
The model follows the author's M.Sc. dissertation — Modelagem de Linhas de Transmissão para Análise de Comportamento Quanto a Descargas Atmosféricas (UFRJ, 2003, DOI:10.13140/RG.2.2.19894.56644) — building on the advisor's original work:
- Portela, C. "Frequency and Transient Behavior of Grounding Systems, Part I — Physical and Methodological Aspects; Part II — Practical Application Examples", Proc. IEEE International Symposium on Electromagnetic Compatibility, pp. 379–390, Austin, USA, August 1997.
and acknowledging colleagues' work in the field:
- Salari, J. C.; Azevedo, R. M.; Portela, C. "An efficient modeling of transmission lines towers and grounding systems for lightning propagation studies", IX SIPDA, Foz do Iguaçu, Brazil, Nov. 2007.
- Visacro, S.; Soares, A., Jr. "HEM: A model for simulation of lightning related engineering problems", IEEE Trans. Power Del., vol. 20, no. 2, pp. 1206–1208, Apr. 2005.
Problem. Lightning performance of power systems hinges on how grounding structures behave at frequencies from DC to a few MHz, where soil dispersion, propagation delay and electromagnetic coupling between conductor segments all matter. Circuit-level models lose accuracy exactly where it counts; full-wave solvers are opaque and heavy. The HEM occupies the middle ground, and TUPÃ is a reference implementation of it: readable, verifiable against theory, and portable across languages.
Scope. MVP: tower-footing grounding electrodes under lightning. Full application tier: complete transmission lines and substation grids. By design the model is linear (no soil ionisation, arresters or corona), frequency-domain, thin-wire, with a two-half-space air/soil medium — the boundaries are stated precisely in docs/theory.md §10. The project's primary role is a scientifically citable implementation; engineering-tool convenience is secondary.
Metrics. An implementation is correct when it reproduces the validation anchors of docs/theory.md §9 within stated tolerances — DC grounding resistance (Sunde), the Portela 1997 harmonic-impedance case, internal-consistency checks, and cross-code agreement with the open-source TAGS/PRTL-mHEM solvers. Current status: the end-to-end frequency-sweep and time-domain pipeline are wired and green (ROADMAP Phases 0–7, 9, 10 and 10b — Phase 9 adds scan-fed transients, windows, multiple injections and the Numerical Laplace Transform; Phase 10 makes the single-integral mHEM kernel and frequency-dependent image coefficients the defaults, threads the frequency sweep and adds a segment-length target; Phase 10b adds the lightning-channel element (speed-calibrated series loading) and two-node current/voltage sources, validated against Chen's analytic current; Phase 8, the Rust port, implemented — every golden fixture matched at 1e-6); six comparisons against published papers' own figures — Silva et al. 2025, Grcev et al. 2018, Lima et al. 2020 and Poljak & Doric 2006 — mostly agree within ±10-20% (closer for some cases, see docs/validation/), and the cross-code check against TAGS agrees to 0.3 % or better below 1 MHz (docs/validation/tags-xval.md). The formal anchors needing tabulated data (Sunde, Portela 1997, Grcev & Heimbach 1997) are still open — see docs/BENCHMARKS.md.
| docs/README.md | Documentation index |
| docs/theory.md | Normative physics reference |
| docs/ARCHITECTURE.md | Components, layers, flows, data management |
| docs/ROADMAP.md | Gap analysis, phased plan, decisions |
| docs/CONVENTIONS.md | Coding and project conventions |
| docs/BENCHMARKS.md | Validation status and benchmark policy |
| docs/GLOSSARY.md | Terminology |
| docs/adr/ | Architecture Decision Records |
| docs/validation/ | External-reference comparisons |
| common/README.md | Shared JSON cases and schema (the public contract) |
| fortran/README.md | Building and testing the Fortran implementation |
| rust/README.md | Building, testing and conformance status of the Rust implementation |
| gui/README.md | Solver-agnostic GUI (viewer); design in docs/GUI_SDD.md |
Fortran (2008+, built with FPM) — based in the original numerical core, cleaned up and modernised (ADR 0001).
cd fortran
bash build.sh # fetch+build SLATEC, optimised build
./build/gfortran_*/app/Tupa ../common/portela1997.json # run the solver on a JSON case
./build/gfortran_*/app/Tupa ../common/channel_tower.json # e.g. the lightning-channel caseThe solver takes one argument, the path to a study JSON, and writes
<case>_results.csv and <case>_results.json into the current directory
(git-ignored under fortran/). The commands above assume you are in fortran/:
both the ./build/... binary path and the ../common/ case path are relative to
it. From another directory, give the full path to both.
Run the binary that build.sh produced. The build/gfortran_* glob only
works when a single such folder exists; if there are several (one per compiler
flag set), pick the newest, which is the one just built:
BIN=$(ls -td build/gfortran_*/app/Tupa | head -1)
$BIN ../common/channel_tower.jsonA bare fpm run / fpm test rebuilds with fpm's default (debug) profile and
different flags, and needs the SLATEC library on the linker path, which
build.sh sets only for its own process. To use fpm directly:
export LIBRARY_PATH=$HOME/.local/lib:$LIBRARY_PATH
fpm run --profile release \
--flag "-ffree-line-length-none -fno-range-check -fopenmp" \
--link-flag "-Wl,--no-as-needed -llapack -lblas" \
-- ../common/portela1997.jsonFor fpm test (fast/slow split in docs/ROADMAP.md §5) use the same flags; see
fortran/README.md.
Rust — only cargo is needed (rust/README.md):
cd rust
cargo build --release
./target/release/tupa ../common/portela1997.json # writes portela1997_results.{csv,json}
cargo test --release # testsJulia (≥ 1.10) — no build step (julia/README.md):
cd julia
julia --project=. -e 'using Pkg; Pkg.instantiate()' # once
julia bin/tupa.jl ../common/portela1997.json # writes portela1997_results.{csv,json}
julia --project=. -e 'using Pkg; Pkg.test()' # testsGUI — Python ≥ 3.11 with uv (gui/README.md). It views a case and, optionally, the result file written by any of the solvers above:
cd gui
uv sync
uv run tupa-gui ../common/buried_conductor_short.json
uv run tupa-gui ../common/portela1997.json --results path/to/portela1997_results.json
uv run pytest # headless data-layer testsAll three solvers take the same JSON case and write result files with the same names, so the GUI reads any of them unchanged.
build.sh enables OpenMP: the frequency sweep runs on all cores (results are
bit-identical for any thread count; OMP_NUM_THREADS or --threads <n>
choose the count). Per-study numerics — geometry kernel, image model and a
segment-length target — are set by the optional numerics block of the case
file (common/README.md, ADR 0024).
See fortran/README.md for the full setup (Windows/Linux)
and for the bundled Fortran demo programs (fpm run --example example1).
The JSON case files under common/ are the shared,
language-neutral inputs every implementation must reproduce:
| Case | Description |
|---|---|
buried_conductor_short.json |
Smallest smoke case: 2 m buried conductor, 2 segments (structure-only) |
buried_conductor_long.json |
Two collinear buried conductors, 2 × 10 m (structure-only) |
portela1997.json |
Phase 2 validation conductor (10 m, εr = 10 soil), 10 Hz–1 MHz sweep |
rod.json |
Single vertical buried rod, same soil, 10 Hz–1 MHz sweep |
grid.json |
Small buried grounding grid (one square mesh), 100 Hz–100 kHz sweep |
This is a starter sample; common/ also holds the time-domain (Heidler-driven) cases and the literature-validation cases — Silva et al. 2025, Grcev et al. 2018, Lima et al. 2020, Poljak & Doric 2006 — compared against the source papers' own figures in docs/validation/.
According to Wikipedia, Tupã (or Tupan, Tupave, Tenondete) is the word for God in the Tupi and Guarani languages, one of whose manifestations is thunder — the name itself probably means "the sound of thunder". As the model relates to lightning and was conceived in Brazil, the name TUPÃ was used in the original Matlab routine; to avoid encoding problems it can be written "TUPA".
GPLv3 — see LICENSE.
Fortran is the reference implementation; Rust follows it to the 1e-6 conformance rule on every golden fixture; Julia is a lag-guarded third port (it refuses the schema features it does not implement rather than ignoring them). State as of 2026-10-01 (Phase 11, Fortran package 0.7.0 plus unreleased work).
| Feature | Fortran (fortran/) |
Rust (rust/) |
Julia (julia/) |
|---|---|---|---|
| Harmonic sweep, current and voltage sources (ADR 0016) | ✔ | ✔ | ✔ |
Elements: line, mesh grid (ADR 0020), catenary (ADR 0023) |
✔ | ✔ | ✔ |
| Transient by FFT, Heidler / double-exponential / Portela waveforms | ✔ | ✔ | ✔ |
| Phase 9 transients: scan-fed transfer function, windows, multiple injections, Numerical Laplace Transform | ✔ | ✔ | lag |
Independent transient signals (signal.signals, ADR 0026): one transfer function, a response set per signal |
✔ | ✔ | ✔ (one sweep per distinct node; no returnNode/quantity on an entry) |
Phase 10 numerics: single-integral kernel, Γ(ω) images, numerics block, segment-length target |
✔ | ✔ | ✔ |
| Threaded frequency sweep (OpenMP, Phase 10 item 4) | ✔ | serial | serial |
Lightning channel element, speed calibration (Phase 10b, ADR 0025) |
✔ | ✔ | lag (refuses the element) |
Two-node sources: returnNode current dipole, delta-gap voltage, quantity (ADR 0025) |
✔ | ✔ | lag (refuses the fields) |
channels block in the results JSON |
✔ | ✔ | — |
Grounding-safety outputs (Phase 11, ADR 0027): observation block — surface potentials, GPR, touch and step voltage, step map — and <case>_potentials.* |
✔ (harmonic) | ✔ (harmonic) | lag (ignores the block) |
Golden fixtures in common/ met at 1e-6 |
all but channel_loaded (its fixture predates the case's later 2048-sample setting; fails in Fortran and Rust alike until regenerated) |
all but channel_loaded (same) |
harmonic ones (grid, portela1997, portela1997_ideal, rod, portelaMesh); the Phase 9 transient and Phase 10b channel ones lag (julia/README.md) |
| Test suite (2026-10-01) | 19 programs; 18 pass, test_common_cases fails on the stale channel_loaded fixture only |
112 tests; all pass but channel_loaded |
253 tests, all pass |
| Linear algebra / quadrature | LAPACK, SLATEC | in-repo LU, line-by-line GK 7/15 port, no external numerics | LAPACK LU via LinearAlgebra, line-by-line GK 7/15 port |
| Role | reference; also builds the CLI used by the GUI | conformance port (ADR 0022) | contributed prototype, follow-along port (Phase 8J) |
