Version 2.2.0#
pyCSAMT 2.2.0 Feature New Fix Enhancement API Change Breaking Docs Build Tests#
Released 2026-08-05.
This release lands genuine 3-D and triangular-mesh 2-D Maxwell
training-data pipelines, an opt-in physics="mt3d" mode for
Inv3DAgent and a physics="mt2d_tri" mode for
Inv2DAgent, fixes the research-only 3-D solver’s
layered-earth benchmark, and gets
ModEm3DAdapter and
Mare2DEMAdapter – the
project’s two production external-solver backends – physics-validated
against real compiled binaries for the first time. Real topography is
now threaded end to end through the triangular-mesh solver and the AI
agents, three new tutorials exercise this against real field data
(TEM/TEMAVG, Zonge AVG K1/K2, and CSAMT groundwater mapping), and two
independent sweeps through pycsamt.emtools (and the EDI/spectra
layer underneath it) fixed several real phase-tensor, frequency-editing,
coordinate-recovery, and tipper-plotting bugs. It also adds
cross-platform build tooling so users no longer have to hand-run
make against the vendored Fortran sources to get there.
Why this release matters Fix#
pyCSAMT vendors ModEM’s Fortran source rather than shipping a compiled
binary, and its own in-house 3-D solver (mt3d)
is deliberately research-only – a real, non-uniform-grid curl-curl
solver, but capped at a small cell budget that direct sparse solves
cannot outgrow in 3-D (see the project’s M6 architecture-decision
record, docs/source/development/adr/AI-INVERSION-M6-3D-ADR.md).
Two things stood between that architecture and a trustworthy 3-D
inversion workflow: mt3d.py’s own benchmark had a real,
misdiagnosed failure, and ModEm3DAdapter – built as file I/O
plumbing around the vendored ModEM source – had never actually been
run against a compiled binary, so nobody had confirmed its generated
files and parsed responses were correct, only that they were
well-formed. This release closes both gaps and adds the tooling to
compile ModEM (and Occam2D, and MARE2DEM) without hand-editing a
Makefile.
Fixing mt3d.py’s layered-earth benchmark Fix#
mt3d.py’s layered_earth_benchmark had been failing at 30-45%
error, and the project’s own M6 architecture decision record recorded
that as a physics defect in the solver’s single-layer boundary
approximation – “refining the mesh does not reduce them.” That
diagnosis was wrong. The boundary approximation is exact for a
laterally uniform half-space (which is why the half-space benchmark
always passed) and stays negligible-error for a layered earth wherever
the domain boundary has already decayed close to zero, the same
argument mt2d already relies on for its
own lateral boundaries.
The real cause: mt3d.py supported uniform cell spacing only.
Because 3-D cell count scales as the cube of resolution, a uniform mesh
cannot simultaneously reach several skin depths of lateral extent and
resolve a few-hundred-metre layer interface within the adapter’s cell
budget. Refining resolution while holding the same small domain fixed
– exactly what the original investigation tried – cannot fix that,
which produced precisely the “refining doesn’t help” symptom that got
misread as a boundary bias.
The fix generalizes the discrete curl operators (_curl_e2h,
_curl_h2e) and edge-conductivity averaging to per-axis non-uniform
cell widths, the same padded-mesh strategy mt2d.py already uses:
fine cells near the receiver and structure, geometrically growing cells
outward. On a 4,096-cell padded mesh (well under the 6,000-cell
ceiling), both analytic benchmarks now pass – half-space at ~1.9%
normalized RMS, layered-earth at ~3.5% – and
supports_nonuniform_mesh is True.
3-D training data and Inv3DAgent(physics="mt3d") Feature#
With mt3d.py trustworthy, pycsamt.ai.training.dataset3d wires
correlated 3-D geological volumes (pycsamt.ai.geology) into real
MT3DAdapter solves via
solve_batch(), mirroring
dataset2d’s design. The solver mesh it
builds is padded and non-uniform rather than uniform, because
MT3DAdapter’s cell budget (6,000, versus mt2d’s 200,000) cannot
afford a uniform mesh reaching the same physical extent – a direct
reuse of the fix above.
>>> from pycsamt.ai.training.dataset3d import (
... Maxwell3DDatasetConfig, generate_3d_maxwell_dataset,
... )
>>> config = Maxwell3DDatasetConfig(
... n_realizations=8, freqs_hz=[1.0, 2.0],
... )
>>> dataset = generate_3d_maxwell_dataset(config)
>>> len(dataset.train)
6
Inv3DAgent gained the same opt-in pattern
Inv2DAgent established for physics="mt2d":
physics="mt3d" trains the GCN inverter against this dataset and
reports a genuine held-out recovery check, since the synthetic
resistivity is known. It goes one step further than the 2-D precedent,
though – because real station (x, y) coordinates are already
extracted to build the GCN adjacency graph, physics="mt3d" reuses
those exact coordinates for the synthetic dataset rather than a
synthetic uniform layout.
>>> from pycsamt.agents import Inv3DAgent
>>> agent = Inv3DAgent(physics="mt3d", n_train_profiles=6)
>>> result = agent.execute({"sites": sites})
>>> result.data["mt3d_recovery"]
{'rmse': 0.28, 'mae': 0.19, 'r2': 0.74, 'n_samples': 6}
physics="mt1d" (tiled 1-D forwards + GCN spatial smoothing) remains
the unchanged default. A real, open accuracy limitation is documented
rather than glossed over: at the dataset’s default core resolution,
half-space error is ~1-2% at 1-2 Hz but ~6-10% at 20-50 Hz, since a
smaller skin depth needs finer core resolution than a fixed cell budget
affords at a fixed domain-extent safety margin. The new opt-in
cells_per_skin_depth config (on both Maxwell3DDatasetConfig and
Inv3DAgent) narrows this – ~8.8%/5.2% at the same cell budget,
~6%/3.6% with more cells – without fully closing it; low-frequency-
concentrated surveys are recommended until it is.
ModEM 3-D: from plumbing to validated physics Fix#
ModEm3DAdapter existed before this release as file I/O plumbing
around the vendored ModEM Fortran source, reusing
pycsamt.models.modem’s real ModEmConfig/ModEmData/
ModEmModel3D readers and writers – but with no compiled Mod3DMT
binary available anywhere the adapter had been built, its generated
files and parsed responses had never been checked end to end.
Compiling ModEM’s vendored 3-D source and running it for real surfaced
and fixed five previously-latent bugs:
The vendored Makefile was missing a build rule for
sg_spherical.f90and never compiledDeclaration_MPI/Sub_MPI/Main_MPI.f90– unconditionallyused by several files despite their bodies being wrapped in#ifdef MPI– nor passed-cppto strip those guards for a serial build. The 2-D Makefile had the identical MPI-stub gap.resolve_executable()’ssearch_pathsfallback didn’t apply Windows’PATHEXT, so a bare name like"Mod3DMT"never resolved to"Mod3DMT.exe".ModEm3DAdapter._build_commandomitted the third positional argument ModEM’s-Fforward mode requires – the predicted-data output filename – soMod3DMTprinted its usage banner and exited 0 having written nothing, which looked like a generic missing-output-file failure rather than a malformed command.ModEmModel3D’s WS-format reader and writer were missing a mandatory leading header line ModEM’s own Fortran reader unconditionally reads and discards before the dimensions line – a real binary rejected the header-less file with a Fortran runtime error.ModEM’s own
setup_airlayers(vendored Fortran) hardcodes 10 air layers and 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 NaNs deep inside the solver. This is a genuine bug in ModEM’s own source, not patched there;ModEm3DAdapter.assess()now rejects problems with fewer than 10 earth z-cells before any file is written.
With those fixed, ModEm3DAdapter passes both analytic benchmarks
with real margin on a plain uniform 8x8x10 grid – no padded mesh
needed, since ModEM synthesizes its own air layers and isn’t subject to
this project’s small-grid research restriction.
pycsamt/forward/tests/test_maxwell_modem3d.py gained a
requires_real_modem-marked section that runs both benchmarks for
real and skips (not fails) when no local binary is present, so this
validation is reproducible by anyone who builds ModEM locally without
becoming a hard CI dependency.
The compiled binary itself is not distributed – it is a local build artifact, gitignored like any other – and ModEM’s exact redistribution/production-use license terms with the original OSU authors are still an open item to confirm before relying on this path in production.
Compiling the external solvers Feature Build#
Validating ModEm3DAdapter required hand-compiling ModEM’s vendored
Fortran source. This release turns that one-off process into reusable
tooling: cross-platform bash scripts under
pycsamt/models/_solver_build/ that auto-detect a Fortran toolchain,
offer an opt-in auto-installer, and drive a resilient, retrying
make for ModEM 2-D/3-D and Occam2D (MARE2DEM needs Intel’s
mpiifort/MKL toolchain and cannot build natively on Windows, so its
script instead drives the existing
pycsamt.models.mare2dem.SourceManager).
pycsamt build modem3d
# or, from a checkout:
make modem3d
See Compiling the External Solvers for toolchain requirements and per-solver notes.
A second 2-D path: triangular meshes and a real MARE2DEM adapter Feature#
Everything above validates the rectilinear 2-D/3-D Maxwell path
(mt2d/mt3d/modem3d). MARE2DEM,
the project’s other production 2-D backend, is a 2.5-D finite-element
solver on an unstructured triangular mesh – it does not use a
rectilinear grid at all, so validating it required a parallel mesh and
solver-contract track rather than reusing the rectilinear one.
pycsamt.forward.maxwell.contracts_tri adds TriMesh/TriProblem,
a solver-neutral triangular-mesh contract that sits beside the existing
rectilinear MaxwellProblem (BackendCapabilities
and MaxwellResultCache were widened
to accept either). On top of it:
pycsamt.forward.maxwell.tri_mesh_gen.build_graded_tri_mesh()builds a real graded mesh with Shewchuk’s Triangle library, refining cell size by distance to the nearest station instead of tiling the whole domain at one resolution – replacing an earlier placeholder mesh generator that produced a uniform checkerboard regardless of station geometry. Building it surfaced a real Triangle segfault: an unconstrained vertex resting exactly in the interior of a PSLG segment could crash quality refinement; the mesh builder now splits the segment at that vertex explicitly instead of leaving it encroaching.pycsamt.forward.maxwell.tri_fem2daddsTriFEM2DAdapter, a real, in-house P1 Galerkin TE/TM finite-element solver on this mesh. Its first version failed both analytic benchmarks with a sign-flipped field; the weak form’s integration-by-parts step had flipped the stiffness term’s sign relative to the strong-form PDE, caught by comparing interior-vs-boundary residuals against a known analytic field rather than by code inspection. Both benchmarks pass after the fix.pycsamt.forward.maxwell.mare2dem.Mare2DEMAdapterwraps the real, compiled MARE2DEM binary itself, reusingpycsamt.models.mare2dem’s existing I/O layers plus two new pieces:pycsamt.models.mare2dem.tri_mesh(a topography-aware triangular FEM mesh builder that completes a piecebuilder.py’s own docstring had flagged as unimplemented) andpycsamt.models.mare2dem.triangle_exec(a subprocess wrapper for the Triangle executable MARE2DEM’s own source build compiles as a byproduct). As withModEm3DAdapterin 2.1.0, this had never actually been run against a compiled binary before this release; building it for real (in an isolated WSL2 toolchain, since MARE2DEM needs Intel’smpiifx/mpiicxand cannot build natively on Windows) surfaced a realSourceManagertoolchain bug – it invokedmpiifort/xiar, names Intel’s current oneAPI toolchain no longer ships, instead ofmpiifx/mpiicx. Both analytic benchmarks now pass against the real binary.pycsamt.ai.training.dataset2d_triis the triangular-mesh counterpart ofdataset2d: it resamples correlated resistivity fields onto triangle centroids and solves each realization throughTriFEM2DAdapter, exposed asInv2DAgent(physics="mt2d_tri").
Real topography, end to end Fix Feature#
TriFEM2DAdapter originally assumed every receiver sat exactly at
z = 0 – a real bug, not a simplification: stations on real terrain
were hard-rejected rather than draped onto it. topo_x_m/topo_z_m
are now threaded end to end, from build_graded_tri_mesh through
dataset2d_tri into Inv2DAgent, and a new datum-shift-invariance
benchmark checks that translating an entire survey’s elevation datum
does not change the recovered model.
Visualizing that topography-draped output needed a mesh-drawing tool
pycsamt.api didn’t have. pycsamt.api.mesh.draw_mesh() and
PYCSAMT_MESH draw structured-mesh cell boundaries with independent
fill/edge alpha via a two-layer pcolormesh, with adapters for
MaxwellMesh and GeologyGrid, and an opt-in show_mesh toggle
on plot_inversion_result_2d. Its first version had a real bug of its
own, found immediately by using it on a draped section: the edge-overlay
layer only handled 1-D rectilinear cell edges, not the 2-D coordinate
arrays a topography-draped section returns.
Three new tutorials on real field data New Docs#
Process A TEMAVG Survey: TEM To Corrected EDI walks a real 51-station TEM100
profile (TDEM Basics’s TEM/TEMAVG family) from raw
soundings through late-time apparent-resistivity conversion, geographic-
coordinate attachment, EDI export and correction, a graded triangular
mesh, and a gated Maxwell AI inversion. Building it surfaced three real
bugs in pycsamt.tdem (below) and one CLI bug, all fixed with
regression tests.
Process Zonge AVG Lines K1 and K2 walks two real Zonge AVG CSAMT lines – K1 (legacy encoding, 47 stations) and K2 (modern encoding, 28 stations sampled at 25 m receiver-midpoint intervals against 50 m survey pegs, requiring topography interpolation) – through EDI conversion, native-AVG QC, and conditioning. A static-shift trial is run and explicitly rejected as too unstable (correction factors spanning 0.10–14.51 for K1 and 0.13–45.67 for K2); the accepted chain is frequency-domain Hampel despiking followed by a Torres-Verdín-Bostick Hanning spatial static-shift correction. Both the AI-inversion gate and several diagnostics (capacitive-coupling correction, source-effect screening, phase-tensor rotation) are shown as explicit, honestly- labeled placeholders rather than fabricated, because the bundled data does not carry the measurements those diagnostics need.
Map Groundwater Geology From CSAMT walks a real single-component (Zxy-only) CSAMT survey through QC, static-shift and EMAP correction, a graded triangular mesh, and a gated AI inversion. Phase-tensor and Groom-Bailey distortion diagnostics are deliberately not shown: because only one of the four tensor components was ever measured, \(\mathrm{Re}(Z)\) is singular, and the pinv-based fallback these diagnostics previously used produced a fixed, physically meaningless result regardless of the input data (verified by a closed- form proof reproduced in the tutorial). The tutorial shows that proof instead of the diagnostic plots it used to fabricate.
Scaled-up AI-inversion reruns Enhancement#
Three previously-published tutorials reran their AI inversions at larger scale using the machinery above:
Map Porphyry Mineralization From Noisy AMT gained a genuine combined-line 3-D inversion (both WILLY lines in one volume, with topography and station labels, via
Inv3DAgent(physics="mt3d")), replacing three “rejected legacy” sections that documented the graph-only tiled 3-D fallback the new non-uniform-mesh 3-D path no longer needs. Its 2-D inversion also reran at 100 Maxwell realizations and a 100-epoch ceiling (up from 10 and 30); recovery \(R^2\) moved from effectively zero to a small positive value and the held-out set grew from 1 to 10 samples per line, still short of a promotable result. A new “Tune this configuration for a real deployment” section gives a concrete before/after checklist plus a measured compute-cost breakdown: on the documentation machine, generating each Maxwell realization costs roughly 32 CPU-seconds versus well under 0.1 CPU-seconds per realization-epoch trained – raisingn_train_profiles, notepochs, is what makes a rerun expensive.Condition an MT Line With Tipper and Rotation reran both the triangular-mesh MT2D and rectilinear MT3D KAP03 AI inversions at 200 realizations and a 100-epoch ceiling, with the mesh coarsened to 100 km depth (down from 250 km) to keep the MT3D run tractable, and added station labels and topography to the comparison figure.
A sweep of real bugs across emtools Fix#
Building and reviewing the tutorials above surfaced several real,
independent bugs in pycsamt.emtools:
Phase-tensor alpha/beta swap –
pycsamt.emtools.tensor._angles_deg()had the skew anglebetaand rotation anglealphaswapped, with a sign flip, so every phase-tensor ellipse orientation and skew value the module reported was wrong. Worked examples and figures were regenerated with corrected numbers.Phase-tensor pseudosection display –
plot_phase_tensor_psectionhad two independent display bugs: its colorbar range forc_by in ("skew", "beta")was hardcoded to+/-skew_threshold(effectively always+/-3 deg) instead of the data’s own range, silently clipping any larger values; and its reserved legend strip could stretch the y-frame far beyond the real data if a single period was an outlier. Fixed to derive the colorbar from data percentiles and draw legend artists in axes-fraction coordinates.Frequency-drop left a stale rotation angle array – dropping or masking frequencies re-sliced every per-frequency array except
rotation_angle, leaving it at its original length while the frequency axis shrank underneath it – a latent shape mismatch for any downstream code zipping the two together. The same fix (re-slice with the samekeepmask) was needed at three independent call sites infrequencyandremove_noise, which is itself evidence this was a real cross-cutting bug rather than a one-off typo.Strike-rose panel for tipper-less surveys –
plot_strike_analysisalways drew three rose panels (strike/phase-tensor/tipper) even when a survey carried no tipper data at all – the common case for AMT/CSAMT – leaving a permanently empty third panel. It now detects tipper availability and draws two panels when there is none. This changes the returned figure’s axes count for tipper-less surveys.
A second sweep: coordinates, transfer functions, and legends Fix#
A deep rewrite of the strike, spectra, tensor, and tf
user-guide pages – converting every example to real, byte-verified
output and reaching for other bundled datasets wherever that made a
page richer – surfaced a second, independent round of real bugs, one
of them wide enough to change what several figures on this page’s own
tutorials actually plot.
EDI coordinates ignored ``>=DEFINEMEAS`` –
pycsamt.site.utils.get_coords()only ever readLAT=/LONG=from>HEAD. Older BIRRP-processed files that leave those keys empty but carry a real station position asREFLAT/REFLONGin>=DEFINEMEAS– true for every file in the bundled KAP03 SAMTEX survey – reported(nan, nan)for every station, silently disabling every coordinate-based plot for that survey. A fallback to>=DEFINEMEASper missing field now recovers real positions for all 26 KAP03 stations, which is what makes the tensor and tipper pages’ new geographic maps possible at all.Spectra DoF fallback ignored real metadata –
pycsamt.seg.ops.effective_dof_from_meta()treated a zerosegnum– the common case, since most real>=SPECTRASECTblocks never populate it – as an explicit zero-DoF count rather than “not provided,” silently skipping its own documentedavgt * bwfallback even when both were genuinely available.Spectra.to_Z(estimate_error=True)returnedz_err=Nonefor essentially every real file as a result; the docstring’s claim that errors were “left as NaN” was also corrected to describe the realNonebehaviour.Tipper nearest-period lookup assumed sorted frequency –
pycsamt.emtools.tf._nearest_idxcallednp.searchsortedagainst a raw, potentially unsorted per-station frequency array. Two real KAP03 stations concatenate runs recorded at different sample rates and are genuinely non-monotonic, corrupting nearest-period matching for every period-based tipper plot at those stations. Fixed to sort internally in log-period space – the same fix already shipped forss.py’s analogous helper in this release.Induction-map axes mislabeled as metres –
plot_induction_mapandplot_induction_conventionhardcoded"x (m)"/"y / Northing (m)"axis labels regardless of the actual coordinate source. Once real EDI coordinates flow through (see the>=DEFINEMEASfix above), those are usually geographic degrees, not projected metres. Both functions now detect the coordinate source and label the axes “Longitude”/”Latitude” when that is what is actually plotted.Normalized-response pseudosection didn’t match its sibling plot –
plot_normalized_responsedrew station labels along the bottom axis and put short period at the bottom (long period at top), unlike every other pseudosection in the package – includingplot_overprint_section, in the same module. Switched to the shared top-mounted station-marker convention and inverted the period axis so shallow/short-period sits on top, matching the log10-period axis label convention used everywhere else.Phase-tensor legend didn’t show an ellipse –
phase_tensor_legenddrew a plain circle (equal width and height can never show ellipticity) with one unlabeled line and no docstring. Rewrote it to draw a real, labeled reference ellipse – \(\phi_{\max}\)/\(\phi_{\min}\) axes and athetaorientation arc – matching the exact convention the pseudosection and map plots already use.
Fixed#
Fix mt3d.py layered-earth benchmark – generalized the discrete curl operators to non-uniform per-axis cell widths; both analytic benchmarks now pass.
Fix ModEM 2-D and 3-D Makefiles – added the missing
sg_spherical.f90build rule and compiled the previously-skipped MPI stub modules with-cpp.Fix Windows executable resolution –
resolve_executable’ssearch_pathsfallback now appliesPATHEXT.Fix ModEM 3-D forward command – added the missing predicted-data output filename argument to the
-Fforward-mode invocation.Fix ModEM WS-format model file – reader and writer now include the mandatory leading header line ModEM’s Fortran reader requires.
Fix ModEM air-layer out-of-bounds read –
ModEm3DAdapternow preflights a minimum of 10 earth z-cells, guarding a genuine latent bug in ModEM’s own vendored Fortran.Fix ModEM model3d footer-detection heuristic – fixed a hardcoded threshold that silently truncated the resistivity grid for any model with
nx <= 3.Fix .gitignore build-artifact collisions – anchored several long-standing, unanchored
build/_build/lib/patterns to the repo root; they had been silently swallowing new directories with matching names.Fix Triangular FEM solver sign error – the P1 Galerkin weak form’s stiffness term had the wrong sign relative to the strong-form PDE; both analytic benchmarks now pass.
Fix Triangle mesh generator segfault – an unconstrained vertex encroaching on a PSLG segment could crash Triangle’s quality refinement; the segment is now split at that vertex explicitly.
Fix MARE2DEM toolchain resolution –
SourceManagerinvokedmpiifort/xiar, names current Intel oneAPI no longer ships; fixed tompiifx/mpiicx.Fix Triangular-mesh solver rejected real topography –
TriFEM2DAdapterhard-rejected receivers not sitting exactly atz = 0;topo_x_m/topo_z_mare now threaded through the mesh builder, the dataset pipeline, andInv2DAgent.Fix Mesh-display edge overlay on draped sections –
draw_meshonly handled 1-D rectilinear cell edges, not the 2-D coordinate arrays a topography-draped section returns.Fix 3-D AI inversion rendering for single-line surveys –
Inv3DAgentrendered a degenerate triangulated sliver for single-line geometries; a profile-geometry detector now renders a continuous, PCHIP-interpolated section instead.Fix 2-D AI-inversion station labels collided with the title – fixed by switching from
fig.subplots_adjust(defeated byPlotConfig’s defaultbbox_inches="tight") to a points-basedax.set_titlepad.Fix Phase-tensor alpha/beta swap and pseudosection display bugs – see “A sweep of real bugs across
emtools” above.Fix Frequency-drop left a stale rotation angle array – fixed at three independent call sites in
frequencyandremove_noise.Fix Strike-rose panel for tipper-less surveys – two panels instead of three when no station carries tipper data; changes the returned figure’s axes count for tipper-less surveys.
Fix TEM-derived EDI files –
TEMtoEDIwas silently dropping its>HEADsection for non-geographic coordinates, never wrote the mandatory>=DEFINEMEAS/>=MTSECTsections, and built impedance magnitude with the SI convention instead of pyCSAMT’s field-unit convention (resistivity wrong by a factor of roughly 6e5). All three fixed with regression tests.Fix ``pycsamt tdem info`` coordinate count – read a nonexistent
coord.recordsattribute instead ofcoord.n_points, always reporting zero points.Fix EDI coordinates ignored ``>=DEFINEMEAS``, spectra DoF fallback, tipper nearest-period lookup, induction-map axis labels, the normalized-response pseudosection’s orientation, and the phase-tensor legend – see “A second sweep: coordinates, transfer functions, and legends” above.
Added#
Feature
pycsamt.ai.training.dataset3d– a 3-D Maxwell training-data pipeline with an opt-in, frequency-awarecells_per_skin_depthsolver-mesh resolution control.API Change
pycsamt.agents.Inv3DAgentgainedphysics="mt3d"(physics="mt1d"remains the unchanged default), plusgeology_grid_nx_ny/geology_grid_nz/cells_per_skin_depthconstructor parameters.Feature
pycsamt/models/_solver_build/cross-platform build scripts, exposed as apycsamt build <solver>CLI subcommand group and a rootMakefile.Tests Regression coverage for
dataset3d, the non-uniform-mesh curl operators,ModEm3DAdapter(including a real-binary-gated benchmark section), theInv3DAgentphysics="mt3d"path, and thepycsamt buildCLI.Docs Compiling the External Solvers and 3-D Maxwell training-data generation.
Feature
pycsamt.forward.maxwell.contracts_tri,tri_fem2d, andbuild_graded_tri_mesh()– a triangular-mesh 2-D Maxwell FEM solver and graded mesh generator.Feature
pycsamt.forward.maxwell.mare2dem.Mare2DEMAdapter,pycsamt.models.mare2dem.tri_mesh, andpycsamt.models.mare2dem.triangle_exec– a real MARE2DEM external-solver adapter, physics-validated against a compiled binary.Feature
pycsamt.ai.training.dataset2d_tri– triangular-mesh 2-D training data, exposed asInv2DAgent(physics="mt2d_tri").Feature
pycsamt.api.mesh.draw_mesh()/PYCSAMT_MESH– reusable mesh-display utility, plus an opt-inshow_meshtoggle onplot_inversion_result_2d.Feature
pycsamt.emtools.csumt.plot_frequency_schedule().New Three tutorials: Process A TEMAVG Survey: TEM To Corrected EDI, Process Zonge AVG Lines K1 and K2, and Map Groundwater Geology From CSAMT.
Tests Regression coverage for the triangular-mesh contracts, solver, mesh generator, MARE2DEM adapter,
dataset2d_tri, the mesh-display utility, and everyemtools/tdem/CLI fix above.Docs Mesh Display and a deepened AI inversion (ten pages, new glossary entries).
Docs Deepened Geoelectric Strike, Cross-Spectra Analysis, Phase Tensor And Impedance Tensor Tools, and Transfer Functions And Tipper Diagnostics – every example converted to real, byte-verified output; new real-data examples (a real KAP03 induction map with genuine tipper, a real single-component CSAMT degenerate phase-tensor case); long examples moved to
docs/scripts/behindcode-dropdown; and new glossary entries (induction vector, Parkinson/Wiese convention, hodogram).
Removed#
Breaking Rejected-legacy 3-D AI-inversion sections – dropped from Map Porphyry Mineralization From Noisy AMT, superseded by the genuine combined-line 3-D inversion described above.
Docs & tooling#
Build Stricter line length –
ruff’s configured line length tightened from 97 to 79 characters, with a repo-wide reflow to match.Build New dependencies –
triangle>=20220202(core, backs the graded triangular mesh) andxlrd>=2.0(docsextra, legacy.xlsTEM coordinate tables).Build Netlify docs deploys gated to a dedicated branch – the release Build Hook now force-pushes the tagged commit to a bot-only
docs-deploybranch instead of settingtrigger_branchto the tag name, which Netlify cannot resolve.Build CI “interfaces” shard – split into
interfaces-desktopandinterfaces-cli, and fixed two tests that were crawling the full 2,790-station JIANGSU survey instead of a subset.
Compatibility#
Default behaviour is unchanged for existing users. Inv3DAgent and
Inv2DAgent both still default to physics="mt1d"; the new
physics="mt3d"/physics="mt2d"/physics="mt2d_tri" modes and
every new cells_per_skin_depth-/topo_x_m-style parameter are
opt-in. Existing ModEm3DAdapter users on grids with fewer than 10
earth z-cells will now see an explicit, actionable preflight error
instead of a solver crash producing NaNs – add earth z-cells rather
than treating this as a regression.
Three fixes are worth a closer look before upgrading:
Any resistivity value previously reloaded from a
TEMtoEDI-produced EDI file was wrong by a factor of roughly6e5(an SI-vs-field-unit convention bug); those files should be regenerated from source, not patched.pycsamt.emtools.tensor’s phase-tensor orientation/skew values were wrong for every user of that module before this release (the alpha/beta swap); anything derived from them – saved figures, strike analyses, written reports – should be treated as suspect and regenerated.plot_strike_analysisnow returns a 2-axes figure instead of 3 for tipper-less surveys; code indexing its returned axes by position should be checked.
See 3-D Maxwell training-data generation for the 3-D training-data
walkthrough and Compiling the External Solvers for build
instructions. The triangular-mesh 2-D path
(dataset2d_tri,
Inv2DAgent(physics="mt2d_tri")) does not yet have a dedicated
user-guide page – see Process A TEMAVG Survey: TEM To Corrected EDI and
Map Groundwater Geology From CSAMT for worked examples
in the meantime.