L2_Electric — context contract#
Module mod_electric, published to applications as mod_mupro_electric with the
context type type_mupro_ElectricContext and the named BC constants
mupro_electric_bc_unset / mupro_electric_bc_min / mupro_electric_bc_max
(library/L2_Electric/interface.f90:4-10). Depends on the L0/L1 base, FFT, log,
utilities and license modules (library/L2_Electric/electric.f90:4-8).
Audience: a developer writing a new application against this solver (as
apps/muFerro does), who needs to know what to put in the context before
calling setup/solve, and what happens when they get it wrong.
1. What the solver computes#
The solver finds the electrostatic potential and electric field from a bound
charge distribution by solving the Poisson equation with a homogeneous
anisotropic permittivity in Fourier space (electric.f90:1071,
electric.f90:492, electric.f90:615-619). For a thin film
(flag_bulk = .false.) it adds the plate (thin-film boundary-condition)
correction — the A-matrix / B-matrix general solution of Laplace’s equation
in the film, following “YL’s paper on effect of substrate conditions”
(electric.f90:511, electric.f90:690-893), with optional surface charge
screening (electric.f90:968-993). For bulk (flag_bulk = .true.) it instead
adds a uniform applied field (electric.f90:895-917). Both paths finish by
evaluating the electrostatic energy density
-E·P - ½ εᵢⱼEᵢEⱼ (electric.f90:1058-1061), zeroed outside the film layers
k1..k2 on the film path (electric.f90:1063-1066).
⚠️ Known defect in the energy diagnostic — do not trust ε₂₂/ε₁₂ there. The call that feeds the formula passes
context%epsilon(2, 3)three times — into the ε₂₂, ε₂₃ and ε₁₂ slots (electric.f90:1042-1043; the intended mapping survives in the commented block at:444-449). Soepsilon(2,2)andepsilon(1,2)are never read by the energy path, and for a diagonal permittivity the½ε₂₂E₂²term is silently missing from the reported energy. The field solve is unaffected (qq_mpi,:426, maps the diagonal correctly); only the energy number is wrong. The same argument list appears at:925-926(film BC), where it is latent because those formulas read only ε₃₃/ε₂₃/ε₁₃. Numerical-behaviour change — awaiting the owner’s confirmation; see TASK.md. Until fixed, a contract reader must not conclude that settingepsilon(2,2)orepsilon(1,2)affects the energy. The variable-permittivity (iterative, inhomogeneous) solver is a separate context,type_InhomoElectricContextinelectric_inhomo.f90:5, which extends the same base type but is not documented here.
2. The context contract table#
type_ElectricContext extends type_ElectricBase
(electric.f90:87-118, electric.f90:131-149). Every component carries a
default initialiser (written 2026-08-06, PR-054): that is the
forward-compatibility mechanism — a field your application does not know about
takes its default instead of being read undefined off the stack
(electric.f90:60-86). Two kinds of default:
Neutral — “feature off”; an application that never sets it gets the behaviour it had before the field existed.
Sentinel — deliberately invalid, so
validate_setupcan refuse it by name rather than the solver inventing a value.
Shapes use the FFT extents from mod_lib_fft: real-space local block
(Rn3, Rn2, Rn1), film-surface plane (Rn2, Rn1).
Component |
Kind / shape |
Required? |
Default and what it means |
Read by |
|---|---|---|---|---|
|
|
Required on the film path ( |
|
|
|
|
Optional (film only) |
|
film BC application ( |
|
|
Optional (film only) |
|
film BC application ( |
|
|
Required for solve (output: potential φ) |
|
written by the Poisson solve ( |
|
|
Unused — no live reader in |
|
nothing (legacy component; the RHS is |
|
|
Required for solve (output: energy density) |
|
written by |
|
|
Required for solve (output: E-field; first index ≥ 3) |
|
all four solve stages ( |
|
|
Required for film solve (applied potential map, top surface) |
|
film BC application ( |
|
|
Required for film solve (applied potential map, bottom/substrate) |
|
film BC application ( |
|
|
Required for solve (input P) |
|
film BC ( |
|
|
Required for solve (input: Poisson RHS charge) |
|
the Poisson solve ( |
|
|
Optional (bulk only) |
|
bulk constraint ( |
|
|
Optional |
|
not read by this context — only the inhomogeneous solver reads it ( |
|
allocatables |
Internal — do not set. Allocated and filled by |
unallocated |
the solve chain ( |
|
|
Optional (selects the path) |
|
|
|
|
Required (relative permittivity tensor) |
|
|
|
|
Unused — marked “below is not used” ( |
|
nothing |
BC values (the validator accepts exactly 1…4, electric.f90:56-58):
1 = electric displacement Dₙ=0 at both surfaces (electric.f90:760, :968);
2 = given electric potential at both surfaces (electric.f90:772, :976);
3 = mixed, potential at bottom / Dₙ=0 at top (electric.f90:785, :982);
4 = mixed, Dₙ=0 at bottom / potential at top (electric.f90:798, :988).
A BC = 5 (“finite-size b.c.”) appears in dead branches
(electric.f90:548, :736, :994) but is refused by the validator
(mupro_electric_bc_max = 4, electric.f90:58).
3. The validator contract#
setup and solve take only context and have nowhere to put a status, so
the validators are a separate entry point the caller invokes first to learn
why before committing (electric.f90:169-172; TASK.md:227).
Why the split is where it is: every check a validator makes corresponds to
something its routine actually reads (electric.f90:154-168, :245-255;
TASK.md:1352). The converse is not guaranteed — the validators do not cover
every read. electric_setup reads epsilon, BC, flag_bulk and the FFT
globals (Cn*, mqk*_3, and on the film path mqk*_2 for rk_mpi at
:508, plus h1/h2 and dz for the layer geometry, :514-570) to build
qq_mpi and the A/B matrices — it dereferences none of the field
pointers, so you may bind potential/elec/etc. after setup and before
solve. The mqk*_2 allocation and the solve-time BC range are checked as
of 2026-08-06 (both gaps were found by this document’s own fact-check);
h1/h2/dz are plain module values with no unbound state to detect. The solve chain
(context_electric_heterogeneous, context_electric_apply_film_bc /
..._apply_bulk_constraint, context_electric_energy; electric.f90:1149-1160)
dereferences the field pointers and the setup products, so validate_solve
demands those. A component declared but never read (charge, md,
choice_fdm_fft) is demanded by neither.
validate_setup (electric.f90:173-243) — checks in order, with exact refusal messages#
# |
Check |
Status code |
Message |
|---|---|---|---|
1 |
|
|
|
2 |
|
|
|
3 |
|
|
|
4 |
film only: |
|
|
5 |
film only: |
|
|
6 |
film only: |
|
|
7 |
|
|
|
8 |
|
|
|
9 |
|
|
|
Checks 5-6 are asked only on the film path because bulk setup returns
before the A-matrix branch (electric.f90:499, :205-208); demanding a BC of
a bulk context would refuse valid code (tests/unit/test_l2_electric.f90:146-154).
Note setup itself still terminates by name (not via status) in two cases even
if you skip the validator: FFT not set up (mupro_require_fft,
electric.f90:408) and, since PR-054, an out-of-range BC reaching the
A-matrix branch (mupro_fatal, electric.f90:533-541).
validate_solve (electric.f90:256-314) — checks in order#
# |
Check |
Status code |
Message |
|---|---|---|---|
1 |
|
|
|
2 |
|
|
|
3 |
|
|
|
4 |
each shaped |
|
|
5 |
|
|
|
6 |
each shaped |
|
|
7 |
film only: |
|
|
8 |
film only: |
|
|
9 |
film only: both shaped |
|
|
The list is derived from what the four solve-chain routines dereference, not
from the type definition (electric.f90:245-255). Film arrays are demanded
only on the film path: a bulk solve never reaches the boundary-condition
routine, so demanding them would reject a valid bulk context
(electric.f90:286-288).
4. Minimal working example#
Based on the unit test’s own runtime bring-up and prepare_context helper
(tests/unit/test_l2_electric.f90:26-31, :63-79) — everything not assigned
here is deliberately left at its default, which is the point of the defaults.
program minimal_electric
use mod_base_double_precision, only: isp, rdp
use mod_mupro_runtime
use mod_mupro_error
use mod_mupro_electric, only: type_mupro_ElectricContext
implicit none
type(type_mupro_RuntimeContext) :: runtime
type(type_mupro_ElectricContext) :: context
type(type_mupro_Status) :: status
integer :: i
! The FFT must exist before electric setup (validate_setup checks 2-4).
call mupro_runtime_default(runtime)
call mupro_runtime_setup_size(runtime, 4_isp, 4_isp, 4_isp, 0_isp, 0_isp, &
1.0_rdp, 1.0_rdp, 1.0_rdp, status)
if (.not. mupro_status_ok(status)) stop 1
call mupro_runtime_setup_fft(runtime, status)
if (.not. mupro_status_ok(status)) stop 1
! Only what has no safe default: a permittivity (sentinel 0 otherwise) ...
context%epsilon = 0.0_rdp
do i = 1, 3
context%epsilon(i, i) = 1.0_rdp
end do
! ... and, because flag_bulk defaults to .false. (film), a boundary
! condition. 1 = electric displacement Dn=0 at both surfaces.
context%BC = 1
call context%validate_setup(status)
if (.not. mupro_status_ok(status)) then
print *, trim(status%message)
stop 1
end if
call context%setup()
! Before solve: bind potential/energy/chargeRHS (Rn3,Rn2,Rn1),
! elec/polarization (3,Rn3,Rn2,Rn1), filmPotentialTop/Bot (Rn2,Rn1),
! then call context%validate_solve(status) and context%solve().
call mupro_runtime_finalize(runtime, status)
end program
For the bulk variant set context%flag_bulk = .true. and skip BC entirely —
the validator will not ask for it (tests/unit/test_l2_electric.f90:146-154).
5. Failure modes with evidence#
Real defects from this module’s history. Each is the reason one of the checks or defaults above exists.
backward2_mpireturned an identically zero field (PR-030, fixed 2026-07-28).fftwSize2D = 1/(nx*ny)was integer division, always 0, from 2022-11-11 to 2026-07-28 (practical exposure window 2023-02-01 → 2026-07-28). This module’s film path is a direct consumer: the plate solution returns through fourbackward2_mpicalls (electric.f90:869-872), so the thin-film boundary-condition correction was silently zero. Bulk electric (flag_bulk— whatapps/muFerrosets) is provably unaffected because setup and solve return before the film branch (electric.f90:499,:1154-1156). Film electric remains listed as exposed in the historical-results review. (TASK.md:997, TASK.md:1001-1006)The BC A-matrix branch had no
else(found by PR-054, 2026-08-06). The branch selects one of four A-matrices forBC1…4 (electric.f90:513-532); any other value fell through, leftAelec_mpiholding whateverallocatereturned, and the determinant was taken from uninitialised memory (electric.f90:543). muFerro never hit it only because it setsflag_bulk, which returns earlier — every thin-film application was exposed. Now: sentinel defaultBC = mupro_electric_bc_unset(electric.f90:91), a namedvalidate_setuprefusal (electric.f90:209-222), and a terminatingelse(electric.f90:533-541). The unit test itself went red on the newelsebecause it had been calling the film setup withBC = 0all along (tests/unit/test_l2_electric.f90:74-77; TASK.md:1424-1425)Undefined context components on every muFerro run (PR-054). No component of this type had a default initialiser and no pointer had
=> null(). muFerro assigns 10 of Electric’s 20 components, soBC,filmScreenTop,filmScreenBot,md,chargeandchoice_fdm_fftwere undefined on every run, read off the stack. Intel’s-init=arrays -init=zerodebug flags made them read as zero and hid it; gfortran and nvfortran do not, and nvfortran is the production GPU compiler (project rule 8).associated()on those pointers was itself undefined, so an application could not even check its own context. The defaults in the table above are the fix;test_defaults_are_neutralpins each one (electric.f90:63-75;tests/unit/test_l2_electric.f90:87-121; TASK.md:1422)electric_setupread FFT globals with no precondition check (fixed PR-051/PR-052). It reads them 14 times; calling it before the FFT existed allocated from zeroedCn*extents and then indexed into the result — undefined behaviour surfacing far from the cause. Nowmupro_require_fftterminates by name (electric.f90:404-408) andvalidate_setupreports the same condition non-fatally (electric.f90:185-196). (TASK.md:1267)The inhomogeneous path’s license verdict was computed and discarded (fixed PR-028).
electric_inhomo.f90calledcheck_validity("electric")and threw the result away, and had noMUPRODEVgate at all — so it ran the check in developer builds and enforced nothing in release builds. Now gated and acted on (electric_inhomo.f90:60-68). (TASK.md:977-978)This module’s committed test key material leaked into the CPack source package (W2 finding).
library/L2_Electric/tests/key/was among the directories a source tarball would carry — includingpri_dev.pem, the developer signing key — becauseCPACK_SOURCE_IGNORE_FILESexcluded only VCS directories. Fixed in the packaging, not in this module; the directory still exists here and project rule 4 applies: never read, print or commit its contents. (TASK.md:928, TASK.md:1451)
Appendix: helper routine#
mupro_electric_read_permittivity(table, name, permittivity)
(electric.f90:352-381, exported at interface.f90:5) reads a 3×3 relative
permittivity tensor for material name from a TOML table into
permittivity(3,3). Signature: table is type(toml_table), pointer, intent(inout) (changed from allocatable 2023-05-10, electric.f90:359);
name is character(len=*), intent(in); permittivity is
real(kind=rdp), intent(out), dimension(3,3). Malformed rows are reported by
print only — it does not stop, and absent entries leave permittivity
unwritten (electric.f90:362-380).