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:

  1. The vendored Makefile was missing a build rule for sg_spherical.f90 and never compiled Declaration_MPI/ Sub_MPI/Main_MPI.f90 – unconditionally used by several files despite their bodies being wrapped in #ifdef MPI – nor passed -cpp to strip those guards for a serial build. The 2-D Makefile had the identical MPI-stub gap.

  2. resolve_executable()’s search_paths fallback didn’t apply Windows’ PATHEXT, so a bare name like "Mod3DMT" never resolved to "Mod3DMT.exe".

  3. ModEm3DAdapter._build_command omitted the third positional argument ModEM’s -F forward mode requires – the predicted-data output filename – so Mod3DMT printed its usage banner and exited 0 having written nothing, which looked like a generic missing-output-file failure rather than a malformed command.

  4. 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.

  5. 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_fem2d adds TriFEM2DAdapter, 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.Mare2DEMAdapter wraps the real, compiled MARE2DEM binary itself, reusing pycsamt.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 piece builder.py’s own docstring had flagged as unimplemented) and pycsamt.models.mare2dem.triangle_exec (a subprocess wrapper for the Triangle executable MARE2DEM’s own source build compiles as a byproduct). As with ModEm3DAdapter in 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’s mpiifx/mpiicx and cannot build natively on Windows) surfaced a real SourceManager toolchain bug – it invoked mpiifort/xiar, names Intel’s current oneAPI toolchain no longer ships, instead of mpiifx/ mpiicx. Both analytic benchmarks now pass against the real binary.

  • pycsamt.ai.training.dataset2d_tri is the triangular-mesh counterpart of dataset2d: it resamples correlated resistivity fields onto triangle centroids and solves each realization through TriFEM2DAdapter, exposed as Inv2DAgent(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 – raising n_train_profiles, not epochs, 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 swappycsamt.emtools.tensor._angles_deg() had the skew angle beta and rotation angle alpha swapped, 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 displayplot_phase_tensor_psection had two independent display bugs: its colorbar range for c_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 same keep mask) was needed at three independent call sites in frequency and remove_noise, which is itself evidence this was a real cross-cutting bug rather than a one-off typo.

  • Strike-rose panel for tipper-less surveysplot_strike_analysis always 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 read LAT=/ LONG= from >HEAD. Older BIRRP-processed files that leave those keys empty but carry a real station position as REFLAT/ REFLONG in >=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 >=DEFINEMEAS per 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 metadatapycsamt.seg.ops.effective_dof_from_meta() treated a zero segnum – the common case, since most real >=SPECTRASECT blocks never populate it – as an explicit zero-DoF count rather than “not provided,” silently skipping its own documented avgt * bw fallback even when both were genuinely available. Spectra.to_Z(estimate_error=True) returned z_err=None for essentially every real file as a result; the docstring’s claim that errors were “left as NaN” was also corrected to describe the real None behaviour.

  • Tipper nearest-period lookup assumed sorted frequencypycsamt.emtools.tf._nearest_idx called np.searchsorted against 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 for ss.py’s analogous helper in this release.

  • Induction-map axes mislabeled as metresplot_induction_map and plot_induction_convention hardcoded "x (m)"/"y / Northing (m)" axis labels regardless of the actual coordinate source. Once real EDI coordinates flow through (see the >=DEFINEMEAS fix 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 plotplot_normalized_response drew station labels along the bottom axis and put short period at the bottom (long period at top), unlike every other pseudosection in the package – including plot_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 ellipsephase_tensor_legend drew 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 a theta orientation 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.f90 build rule and compiled the previously-skipped MPI stub modules with -cpp.

  • Fix Windows executable resolutionresolve_executable’s search_paths fallback now applies PATHEXT.

  • Fix ModEM 3-D forward command – added the missing predicted-data output filename argument to the -F forward-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 readModEm3DAdapter now 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 resolutionSourceManager invoked mpiifort/xiar, names current Intel oneAPI no longer ships; fixed to mpiifx/mpiicx.

  • Fix Triangular-mesh solver rejected real topographyTriFEM2DAdapter hard-rejected receivers not sitting exactly at z = 0; topo_x_m/topo_z_m are now threaded through the mesh builder, the dataset pipeline, and Inv2DAgent.

  • Fix Mesh-display edge overlay on draped sectionsdraw_mesh only 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 surveysInv3DAgent rendered 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 by PlotConfig’s default bbox_inches="tight") to a points-based ax.set_title pad.

  • 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 frequency and remove_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 filesTEMtoEDI was silently dropping its >HEAD section for non-geographic coordinates, never wrote the mandatory >=DEFINEMEAS/>=MTSECT sections, 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.records attribute instead of coord.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-aware cells_per_skin_depth solver-mesh resolution control.

  • API Change pycsamt.agents.Inv3DAgent gained physics="mt3d" (physics="mt1d" remains the unchanged default), plus geology_grid_nx_ny/geology_grid_nz/ cells_per_skin_depth constructor parameters.

  • Feature pycsamt/models/_solver_build/ cross-platform build scripts, exposed as a pycsamt build <solver> CLI subcommand group and a root Makefile.

  • Tests Regression coverage for dataset3d, the non-uniform-mesh curl operators, ModEm3DAdapter (including a real-binary-gated benchmark section), the Inv3DAgent physics="mt3d" path, and the pycsamt build CLI.

  • Docs Compiling the External Solvers and 3-D Maxwell training-data generation.

  • Feature pycsamt.forward.maxwell.contracts_tri, tri_fem2d, and build_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, and pycsamt.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 as Inv2DAgent(physics="mt2d_tri").

  • Feature pycsamt.api.mesh.draw_mesh() / PYCSAMT_MESH – reusable mesh-display utility, plus an opt-in show_mesh toggle on plot_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 every emtools/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/ behind code-dropdown; and new glossary entries (induction vector, Parkinson/Wiese convention, hodogram).

Removed#

Docs & tooling#

  • Build Stricter line lengthruff’s configured line length tightened from 97 to 79 characters, with a repo-wide reflow to match.

  • Build New dependenciestriangle>=20220202 (core, backs the graded triangular mesh) and xlrd>=2.0 (docs extra, legacy .xls TEM 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-deploy branch instead of setting trigger_branch to the tag name, which Netlify cannot resolve.

  • Build CI “interfaces” shard – split into interfaces-desktop and interfaces-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 roughly 6e5 (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_analysis now 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.