pycsamt.forward.maxwell.modem3d#
External ModEM adapter: the trusted production 3-D backend.
Per AI-INVERSION (Decision 1), ModEM — not an
in-house solver — is the recommended path for genuine 3-D production
forward modeling. ModEm3DAdapter bridges the solver-neutral
MaxwellProblem /
ForwardResult contract to
the vendored ModEM Fortran executable
(pycsamt/models/modem/_source/3D/Mod3DMT.f90, v6.2.6; Egbert,
Kelbert & Meqbel) via
BaseExternalMaxwellAdapter,
reusing the file I/O already built in pycsamt.models.modem
(ModEmData,
ModEmModel3D) rather than a
second, competing implementation of the ModEM file formats.
Status: physics-validated against a real compiled binary (2026-07-29).
No compiled Mod3DMT binary is committed to this repository (it is
a local build artifact, gitignored — see
pycsamt/models/modem/_source/README for build instructions), but
one was built here with a MinGW-w64 gfortran/OpenBLAS toolchain and
used to run this project’s own analytic benchmarks end-to-end. Doing
so surfaced and fixed several real, previously-latent bugs — this
adapter’s generated files, and the vendored Fortran source itself,
had never actually been exercised against a live ModEM run before:
the vendored
Makefilewas missing a build rule forsg_spherical.f90(use`d directly by ``GridCalc.f90`) and never compiledDeclaration_MPI/Sub_MPI/Main_MPI.f90(whose#ifdef MPI-guarded bodies are unconditionally use`d by several files regardless of MPI) nor passed `-cpp`` to strip those guards for a serial build — all reproduce identically on Linux/Mac, not Windows-specific;resolve_executable()’ssearch_pathsfallback checked a literalPath(directory) / namerather than applyingPATHEXTthe way its ownPATHlookup (viashutil.which()) already did, so a bare name like"Mod3DMT"never resolved to"Mod3DMT.exe"on Windows;ModEm3DAdapter._build_command()omitted the third positional argument ModEM’s own-Fforward-mode requires (the predicted-data output filename), so it printed its own usage banner and exited 0 having written nothing, indistinguishable from success untilModEm3DAdapter._locate_predicted_file()found no output file;ModEmModel3D’s WS-format reader/writer were missing a mandatory leading comment line ModEM’s Fortranread_modelParam_ws(WS.inc) unconditionally reads and discards before the dimensions line — a real compiled binary immediately rejected the header-less file with a Fortran runtime error;ModEM’s own
setup_airlayers(GridDef.f90) hardcodes 10 air layers and its default “mirror” sizing method reads that many earth-layer widths with no bounds check against the actual earth cell count — fewer than 10 earth z-cells reads past the end of the array, producing garbage/NaN values that crash the solver with “b in QMR contains NaNs”.ModEm3DAdapter.assess()now rejects this before ever writing a file (see_MIN_EARTH_Z_CELLS); this is a real latent bug in the vendored ModEM source itself, worked around here rather than patched there (patching unfamiliar, decades-old numerical Fortran to add a defensive bounds check was judged riskier than simply requiring enough earth cells).
With those fixed and at least _MIN_EARTH_Z_CELLS earth
z-cells, this adapter passes both
half_space_benchmark() and
layered_earth_benchmark()
with real margin — see “Measured accuracy” below and
pycsamt/forward/tests/test_maxwell_modem3d.py’s
requires_real_modem-gated tests, which are skipped (not failed)
when no local binary is present, and were confirmed passing against
the real one built for this validation.
Scope and mapping decisions#
No separate air layers (
ModEmModel3D.n_air = 0): the wholeMaxwellProblemmesh is written as ModEM “earth” cells, matching the same “mesh top = physical surface” convention already used byMT2DAdapterandMT3DAdapter— not a ModEM limitation (ModEM supports real air/topography), a deliberate v1 scope reduction for consistency and lower risk. Topography support is future work.Non-uniform meshes are supported: ModEM’s own solver has no uniform-grid restriction, so
supports_nonuniform_mesh=True(as of 2026-07-29,mt3d.pyalso supports non-uniform meshes, so this is no longer a point of difference between the two adapters).Station and model coordinates are shifted so the mesh’s own minimum x/y edge maps to ModEM-local
(0, 0)— the vendoredModEmData/ModEmModel3Dpair has no shared origin/rotation field of its own (confirmed by reading both writers), so this adapter is the thing responsible for keeping station coordinates and model-grid coordinates in the same frame.Units: requests are written in ModEM’s native
[mV/km]/[nT]field-unit convention (the well-established default, unlike an unverified[V/A]request), and the response is converted to SIV/Aon the way back using the standard4*pi*1e-4factor.Full impedance tensor is always requested from ModEM regardless of
problem.components(no cost difference internally), then filtered down to the requested subset when building the result.Diagnostics are honest about what ModEM’s plain predicted-data file does not expose:
iterationsis always 0 (forward mode does not iterate) andrelative_residualis always 0.0 — not a measured quantity (the contract requires a finite value;0.0is a documented placeholder, not a claim of exact agreement). Onlyruntime_s(the external process’s real wall-clock time) is a genuine measurement.
Measured accuracy#
On a small uniform 8x8x10 grid (300 m cells, 10 earth z-cells — the
minimum _MIN_EARTH_Z_CELLS requires), against a real compiled
Mod3DMT: half_space_benchmark()
and layered_earth_benchmark()
both pass the default
BenchmarkThresholds with
real margin (normalized RMS under 5%). Unlike
mt3d’s research-only solver, no padded
mesh was needed here — ModEM synthesizes its own air layers and, as
an iterative-solver production code, does not carry this project’s
own small-grid cell-budget restriction, so a plain uniform mesh
sufficed for this validation. This does not by itself validate every
mesh configuration (e.g. non-uniform meshes, receivers off-centre) –
only what the cited benchmarks actually exercise.
Functions
|
Register |
Classes
|
Adapter wrapping the external ModEM 3-D forward solver. |
- class pycsamt.forward.maxwell.modem3d.ModEm3DAdapter(*, config=None, run_policy=None, policy=None, predicted_data_filename=None, version='modem-v6.2.6-adapter-1.0')[source]
Bases:
BaseExternalMaxwellAdapterAdapter wrapping the external ModEM 3-D forward solver.
- Parameters:
config (ModEmConfig or None, optional) – ModEM configuration (executable names, MPI settings, units, sign convention). Defaults to
ModEmConfig(mode="3d").run_policy (ExternalRunPolicy or None, optional) – Executable resolution, timeout, retry, and working-directory rules. When omitted, a policy is built from
config(config.mpi_commandifconfig.use_mpielseconfig.binary_3d), with the vendoredpycsamt/models/modem/_source/3Ddirectory as a search path fallback.policy (AdapterPolicy or None, optional) – Solver-independent result acceptance policy.
predicted_data_filename (str or None, optional) – Exact name of ModEM’s predicted-response output file within the run’s working directory. When omitted, the adapter auto-detects it as the only
*.datfile other than the one it wrote itself, and raises a clear error if that is ambiguous (zero or more than one candidate) — set this explicitly once you know your ModEM build’s naming convention.version (str, default="modem-v6.2.6-adapter-1.0") – Adapter version reported in every
ForwardResult. Encodes both the vendored ModEM release this adapter targets and this adapter code’s own revision.
Examples
>>> from pycsamt.forward.maxwell import ExternalRunPolicy >>> adapter = ModEm3DAdapter( ... run_policy=ExternalRunPolicy("does-not-exist-xyz") ... ) >>> adapter.capabilities.name 'modem3d'
- assess(problem)[source]
Assess a problem, adding this adapter’s mapping checks.
- Parameters:
problem (MaxwellProblem) – Candidate simulation problem.
- Returns:
The generic capability report from
assess(), extended with surface-receiver, horizontal-bounds, surface-aligned-mesh, and vacuum-permeability checks.- Return type:
Examples
See
ModEm3DAdapterfor construction; a problem with a buried receiver is rejected before any file is written.
- pycsamt.forward.maxwell.modem3d.register_modem3d_backend(*, config=None, replace=False)[source]
Register
ModEm3DAdapterin the process-wide registry.- Parameters:
config (ModEmConfig or None, optional) – Configuration used both to build the registered adapter and to derive its availability probe (whether the configured executable can currently be resolved).
replace (bool, default=False) – Explicitly replace an existing
"modem3d"registration.
- Return type:
None
Examples
>>> register_modem3d_backend(replace=True) >>> from pycsamt.forward.maxwell import list_backends >>> "modem3d" in list_backends() True >>> list_backends()["modem3d"]["available"] False