v2.x changelog#

Every notable change in the v2 line, newest first. See the Changelog index for the badge legend and links to other series, or Release Notes for the narrative behind each release.


2.5.0 New Fix Docs#

Released 2026-08-28.

The defining feature of 2.5.0 is a pair of complementary common-format families. PCSF/PCSM give subsurface models and inversion results one backend-neutral scientific contract in binary HDF5 and human-readable ASCII; PCBH gives spatial multi-borehole observations and geological evidence a human-readable JSON contract. They remain independently valid, but an explicit, checksummed association and CRS/vertical alignment allow boreholes to enter shared 3-D model views without conflating evidence and inversion cells. The release also expands confidence-ratio QC into a survey-scale plotting and export toolkit for single and multiple lines. Real bugs were fixed in ModEmModel3D, the web 3-D view’s topography uploader, and confidence-map/profile geometry. Running real, full-scale Occam2D and ModEM 3-D inversions against a real 128-station survey found and fixed four more real solver-launch bugs, including a new ModEmForwardControl that finally lets a 3-D covariance file reach Mod3DMT at all. With a real resistivity volume now reaching the viewer, the Map View’s resistivity modes were reworked to behave like an inversion viewer – 3-D resistivity/depth filters that mask instead of recolour, block/iso-surface modes that render under topography at all, depth slices kept inside the real model, an inversion “Resistivity @ depth” plan-slice over the basemap, plus a Windows logging-flood fix. PCBH adds CSV/LAS and legacy-geology adapters, shared web/MapView 3-D rendering, and an accessible Borehole Builder. PCSF/PCSM convert Occam2D, ModEM 3-D, MARE2DEM, multiline models, and generic AI/DL arrays into the same explicit geometry and canonical linear-resistivity representation.

Added#

  • New Two common formats, one model-plus-evidence workflow – PCSF/PCSM are the authoritative subsurface-model contract and PCBH is the authoritative borehole-evidence contract. Either can travel independently; together they preserve their separate provenance while supporting explicit checksum association, coordinate/datum alignment, and consistent 3-D rendering across Python, web, and MapView consumers.

  • New PCBH, the pyCSAMT Common Borehole Format – a versioned, human-readable .pcbh.json contract stores many water, mining, geotechnical, geothermal, petroleum, monitoring, or exploration holes in one document with a document-wide CRS, explicit vertical reference and units, collars, measured/deviated trajectories, categorical interval-log families, structures, vocabularies, provenance, and namespaced domain extensions. The reference package provides canonical JSON and JSON Schema, semantic validation and bounded readers, combined and manifested relational CSV import/export, LAS 2.0 adaptation, legacy pycsamt.geology.Borehole views, deterministic minimum-curvature desurveying, and loss-reporting GeoJSON/VTP/glTF/GLB exports. PCBH can be embedded in or checksum-referenced from PCSF without conflating observed geology with inversion cells; explicit CRS/vertical-datum alignment inserts the paths into native 3-D blocks. The web Borehole Builder supports CSV mapping, row-linked validation, 2-D log and 3-D trajectory previews, autosave/recovery, canonical download, and PCSF embedding. Web and MapView consumers use the same application-neutral render contract for centerlines, interval tubes, contacts, structures, colors, selection, and scientific hover values. See PCBH — Common Borehole Format.

  • New PCSF/PCSM, universal subsurface-model exchange – PCSF is the compact HDF5 encoding and PCSM (plain or .pcsm.gz) is its lossless, human-readable ASCII projection; both reconstruct the same validated PCSFModel. pycsamt.format converts Occam2D, ModEM 3-D, and MARE2DEM results (and a DUHI-prepared result, via the Occam2D path) into one self-describing HDF5 container instead of five disagreeing native conventions: canonical resistivity is always linear ohm.m, geometry is discriminated explicitly through one of four kinds (grid2d, grid3d, mesh_unstructured, multiline) rather than inferred from array shape, and every second-level module (adapters, schema, io, multiline, topography, pointcloud) is public right after import pycsamt.format. pcsf_to_point_cloud() flattens any geometry kind into one point cloud with reproducible seeded subsampling, powering three new consumers with no backend-specific view code: the desktop app’s first 3-D/volume panel (Pcsf3DWindow), a new “PCSF file” data source in the web 3-D view that renders with no session cache at all, and pycsamt.map.MapView.from_pcsf(). pycsamt.format.topography wires .pcsf topography through the existing pycsamt.map.topo elevation parser rather than adding a third one.

  • New ``pycsamt format`` command group – a smart, robust CLI face for pycsamt.format. pycsamt format convert SOURCE [TARGET] auto-detects the source (an Occam2D / ModEM 3-D / MARE2DEM working directory, a single signature file inside one, an AI/DL .npz / .npy array bundle, or an existing .pcsf / .pcsm) and writes .pcsf or .pcsm through the matching adapter – rebuilding the MARE2DEM mesh from the run’s .poly PSLG, recognising log10 / ln resistivity keys, and carrying --topo / --epsg / --origin through. pycsamt format detect reports the classification without converting (non-zero exit when unconvertible), pycsamt format info summarises a file, and pycsamt format validate runs a header / load / schema / round-trip check. The shared detector is public as pycsamt.format.detect_source() / SourceKind.

  • New Survey-scale confidence evaluation – ten public plotting functions add route/contour and regular-grid maps, component panels, composite-vs-presence comparison, before/after comparison, risk, station-by-line heatmap, distribution, ranking, and threshold-retention curves. Contour levels, line appearance, and labels are configurable; colour limits adapt to finite confidence values and boundary isolines are drawn only when crossed. export_confidence_map exports station CSV and Surfer ASCII DSAA grids.

  • New ``ModEmForwardControl`` – writes Mod3DMT’s forward-solver control file, matching the executable’s own compiled-in solver defaults exactly. Required by Mod3DMT’s own CLI argument order before a covariance file is reachable at all; InputBuilder and ModEmRunner now build/reference it automatically. See Fixed below.

  • New Map View 2-D “Resistivity @ depth” – with an inversion result loaded, pycsamt.map.resistivity_at_depth() / pycsamt.map.inversion_depth_range() slice the model at a chosen depth and the Map view draws it as a filled-contour raster with isolines over the basemap, on a fixed whole-model log colour scale, with a slice-depth slider and a “Station markers” toggle (StationMapOptions.show_markers).

  • New Map View 3-D label and smoothing optionsVolumeMapOptions.station_label_angle (rotate labels, drawn as scene annotations), station_label_fraction / station_label_names / max_stations (thin labels or name a subset without hiding markers), and volume_smoothing – opt-in structure smoothing of the block / iso-surface / anomaly volume (finer render lattice + Gaussian), 0.0 (unchanged) by default. New “Smoothing” panel; the “Stations (3-D)” panel gains the label controls.

  • New ``SafeRotatingFileHandler`` – swallows a failed log rollover instead of crashing logging; used by the bundled logging config.

Fixed#

  • Fix Web 3-D view’s topography upload silently rejected a valid ``station_names`` columnapp/web/callbacks/map3d.py’s own CSV/HDF5/NPZ elevation parser reimplemented pycsamt.map.topo’s parser independently and had quietly drifted from it: its id-column list never recognised a station_names column/dataset that pycsamt.map.topo.parse_elevation_file() already did. It now delegates to that shared parser entirely instead of keeping a second copy.

  • Fix ``ModEmModel3D`` silently discarded a real ModEM model’s grid centre and rotation – a real ModEM writer appends the grid’s real-world centre and an optional rotation angle after the resistivity volume; the WS-format reader parsed but never stored either. Now exposed as ModEmModel3D.origin/.rotation (metres/degrees, default zeros(3)/0.0, matching the convention read_mackie3d() already used on the same class – an initial None default broke that convention’s existing getattr(model, "origin", [0, 0, 0]) fallback in iotools.export/iotools.interpolate).

  • Fix Confidence geographic and chainage geometry – multi-line maps preserve real longitude/latitude station positions, including L22, and the regenerated L18 profile uses corrected coordinate-derived chainage (about 2.4 km rather than the former 5 km display).

  • Fix Occam2D/ModEM 3-D meshes wrote spurious air-layer rowsOccamMesh.from_data and ModEmModel3D.halfspace both inserted air rows that Occam2D’s PW2D and ModEM’s WS formats never expect, crashing a real compiled Occam2D and Mod3DMT. Air rows/n_air now always 0.

  • Fix ``ModEmControl`` inversion-control column misalignment – the key-field width (44 columns) pushed the colon past Fortran’s fixed a36 boundary, causing a real “Bad value during floating point read”. Fixed to 36 columns.

  • Fix ModEM 3-D covariance file was unreachable from ``ModEmRunner`` – Mod3DMT’s own CLI argument order needs a forward-solver control file before covariance is reachable at all; nothing built one, so covariance= silently landed in the wrong slot and a real Mod3DMT aborted trying to parse it as forward-solver settings. Fixed via the new ModEmForwardControl.

  • Fix ``ModEmControl``/``ModEmForwardControl`` numeric fields lost their decimal point, silently corrupting real solver runs – Fortran’s G input editing needs an explicit . or its own decimal-digit count re-places one; a real Mod3DMT echoed a written 1e-7 back as 0.1000000E-13, and target_rms itself had been written as a bare "1" in every prior 3-D run – a likely root cause of an earlier stuck-optimizer run. Both writers now use %.6E, always including a literal ..

  • Fix Map View 3-D resistivity filter recoloured instead of masking – selecting a resistivity band left the whole fence / depth-slice panel visible in altered colours (the smoothing spline refilled the mask). In-range cells keep their exact hue; out-of-range cells become real holes; the colour scale is unchanged.

  • Fix Map View 3-D block and iso-surface rendered nothinggo.Volume/go.Isosurface were handed the topography drape’s NaN cells and an unclamped iso window. Both now render under topography and any filter; a resistivity band is drawn as one closed iso-surface body, not a whole-model volume tinted by it.

  • Fix Map View 3-D depth slices – ghost all-NaN slices at depths the model never sampled, a colour scale that re-graduated per slice, n_slices=1 cutting the window floor, and a resistivity filter that bridged across out-of-band layers. Slices now stay inside the real depth extent, a single slice cuts the window middle, the colour scale is fixed to the whole model, and the filter is applied at the interpolated slice depth.

  • Fix Map View 3-D station markers/labels – block/iso markers drifted outside the block footprint; rotated labels overlapped the marker glyph. Fixed.

  • Fix Surfer-style contour overlays carried an opaque white backgroundbuild_geo_contour_image rasterised onto a white Matplotlib figure, so the “transparent” PNG hid the basemap outside the contoured hull. Now genuinely transparent; the 2-D station map also drops its solid plot background.

  • Fix ``pycsamt-mapview`` flooded the console with rotating-log errors on Windows – the rotating file handlers sat on the root logger, so werkzeug per-request logging forced an infos.log rollover on nearly every request and Windows refused the rename. The werkzeug logger is pinned at WARNING (no propagation) and SafeRotatingFileHandler skips a failed rollover.

Docs & tooling#

  • Docs PCBH format guide and public contractPCBH — Common Borehole Format explains the format architecture, scientific conventions, Python and Borehole Builder workflows, CSV import, legacy adapters, PCSF alignment, and derived exports. The checked-in schema, canonical fixture, independent-reader guide, governance/extension registry, interoperability matrix, fuzz tests, and repeatable 1,000-hole benchmark complete the pre-1.0 publication gate.

  • Docs ``user_guide/models/pcsf_format.rst`` – new page covering PCSF’s four geometry kinds, converting a real Occam2D/ModEM/MARE2DEM result with real captured output, multiline/topography construction, and the pcsf_to_point_cloud consumers, plus a real two-panel figure rendering an Occam2D grid2d model and a ModEM grid3d volume through the same function call. New bundled examples/pcsf_conversion_demo/ converts all three real backends’ bundled sample data to .pcsf, with and without topography, and round-trip-verifies every file it writes.

  • Docs Confidence QC guide and examplesQuality-Control Confidence Scoring now gives labelled formulations for both confidence methods and their components, explains and interprets every new plot, and documents interpolation/threshold limitations. examples/confidence/ and the documentation figure generator recreate the complete figure set from bundled WILLY data.


2.4.0 New Fix Enhancement Docs Build#

Released 2026-08-19.

Four independent pieces of work: the station confidence profile (plot_confidence_profile, station_confidence_table, frequency_confidence_table) now computes real inter-station distance from EDI coordinates via a dependency-free UTM projection, instead of silently defaulting every station to a hardcoded 200 m spacing whenever east/north attributes were absent – which they always are for ordinary EDI-backed Site objects. A new opt-in force_spacing parameter lets callers bypass coordinates entirely for surveys with unreliable positioning, and low-confidence point labels now auto-declutter instead of stacking one per station. Separately, Site/Sites now accept EMTF-XML transfer functions symmetrically with SEG-EDI, validated against a real compiled EMTF-FCU v4.1 reference implementation and a new round-trip example, which surfaced and fixed five further real bugs – including one where Sites.write() had been silently writing placeholder files instead of real EDI content since the method was introduced. Third, pycsamt.airborne went through a full guideline-compliance pass – a new shared pycsamt.airborne.validation module removed duplicated boundary validation across the mobilemt/ztem/afmag adapters and every public member gained complete docstrings – alongside a new pycsamt.emtools.afmag module implementing the Liu et al. (2018) motion-induced-noise method plus AFMAG tilt-angle diagnostics. Fourth, a new pycsamt.models.occam1d package brings a native, pure Python/NumPy 1-D Occam smooth-model inversion engine (forward model, analytic Jacobian, regularization, nonlinear inversion loop) with no external binary required, covering both single-station and whole-survey batch inversion, backed by a new bundled example against three real Gabbs Valley soundings. Fifth, a docs-only follow-up: pycsamt.airborne gains a complete six-page user guide and a new API reference page, and Loading electromagnetic data now documents that EMTF-XML loads through the same Site/Sites boundary as EDI, alongside the separate airborne loading boundary.

Added#

  • New Low-confidence point label declutteringplot_confidence_profile gains annotate_low_step, which thins the rotated station-name labels drawn above points below ci_lo the same way station_label_step already thins the top station axis. Left at its default None, it auto-thins once more than 18 stations are flagged low, so a survey where most stations are below threshold no longer plots an overlapping label at every single point; annotate_low_step=1 restores the previous label-every-point behaviour.

  • New EMTF-XML support in ``Site``/``Sites`` (pycsamt.site.base) – Site now wraps either a SEG-EDI object or an EMTF XML document, via a lazy dual backend: Site.edi/Site.tf are both always available, materialized from each other on first access and cached, so every existing EDI-only call site across pycsamt.emtools, the CLI, and the desktop app keeps working unmodified. New: Site.from_xml/Site.to_xml, Site.backend, typed pycsamt.metadata properties (site_meta, site_layout, provenance, processing, copyright, quality_meta), Sites.to_emtf_list, Sites.write_xml. to_sites/ensure_sites now recognize .xml paths, EMTF objects, and mixed EDI+XML directories.

  • New ``pycsamt/emtf/tests/test_fcu_oracle.py`` – validates pycsamt’s EDI<->EMTF-XML conversion (periods, impedance, tipper, variance, site coordinates, channel geometry, rotation) against a real, locally compiled EMTF-FCU v4.1 reference implementation; skips rather than fails when no local build is present.

  • New ``examples/emtf_xml_roundtrip_demo/`` – three real field stations round-tripped EDI -> XML -> EDI through both the Site/Sites API and the raw EMTF document API, with full numeric verification including a re-read of the files actually written to disk.

  • New ``pycsamt.emtools.afmag`` – AFMAG-specific processing, diagnostics, and plotting, built on Site.tipper (AFMAG has no electric-field channel): a from-scratch implementation of the Liu et al. (2018) rotation-matrix motion-induced-noise method (euler_rotation_matrix, geomagnetic_field_direction, motion_coupling_cosine/_angle, simulate_motion_induced_voltage, correct_motion_induced_noise); classical tilt-angle diagnostics (afmag_tilt_angles) and three plots (plot_afmag_tilt_profile, plot_afmag_tilt_psection, plot_afmag_tilt_polar); and motion-coupling QC (motion_susceptibility_table, the one mutating flag_motion_susceptible_band, plus plot_motion_susceptibility_map and plot_afmag_correction_comparison). Follows every existing emtools convention: sites in via ensure_sites, ax/ axes in and Axes/Figure out for every plot (mechanically enforced by the package’s existing plot-signature test).

  • New ``pycsamt.airborne.validation`` – shared boundary-validation and reference-metadata-mapping module for the mobilemt/ztem/ afmag adapters, and ``pycsamt.emtf.datatypes .ensure_emtf_datatype_registered``, a shared idempotent EMTF-datatype-registration helper factored out of two adapters that had each reimplemented it.

  • New ``pycsamt.models.occam1d`` – native 1-D Occam inversion engine: Occam1DForwardModel (isotropic layered-earth recursion, optionally Numba-compiled), Occam1DJacobian (analytic sensitivities), Occam1DRegularization/ Occam1DInversion (roughness penalty and the nonlinear Lagrange-multiplier Occam loop, scoring every trial candidate through the full nonlinear forward model), plus Occam1DInputBuilder/ Occam1DBatch for native-file construction and whole-survey batch inversion (optionally parallel via joblib), Occam1DRunner for driving an external Occam1D-compatible binary instead, and PlotModel/PlotResponse/PlotConvergence/PlotSummary for review figures. Optional Numba/joblib acceleration via pip install pycsamt[perf].

  • New ``examples/occam1_demo/`` – builds and inverts three real Gabbs Valley EDI soundings with the native Occam1D engine end to end, writing native files, text/JSON result products, and review figures.

Fixed#

  • Fix Station confidence-profile distance defaulted to a hardcoded 200 m spacingplot_confidence_profile(), station_confidence_table(), and frequency_confidence_table() derived station distance only from east/north/x/y attributes, which real EDI-backed Site objects never carry (they expose lat/lon instead). Every real survey therefore silently fell back to index * spacing_m, stretching a real ~2050 m AMT line out to a plotted ~8000 m. Distance is now derived from real station coordinates: EDI HEAD latitude/longitude projected to true UTM easting/northing via the existing, dependency-free ll_to_utm(), whenever at least two stations carry usable coordinates, and cached per EDI object so repeated calls on the same survey don’t re-project. spacing_m is now only ever a per-station fallback for stations without usable coordinates. Thanks to @shahidalishah130-hub for the clear report with a reproducing figure (#76).

  • Fix ``ensure_sites``/``to_sites`` dropped a list of ``Site`` objects, silently resolving to an empty Sites with no error – root-caused and fixed.

  • Fix ``Sites.ordered()``/``Sites.select()`` silently downgraded XML-native sites to EDI on every non-inplace call (including the one ensure_sites always performs) by routing through the materialized EDI view instead of preserving the existing Site objects.

  • Fix ``Sites.write()`` wrote placeholder files instead of real EDI content, since the method was introduced – it looked for a nonexistent EDIFile.to_file() method; fixed to use pycsamt.emtf.converters.edi.write_edi().

  • Fix EDI missing-data sentinel mapped to ``0.0`` instead of ``NaN`` (pycsamt.seg.edi) – affects any EDI with real gaps (dead bands, dropped channels), not only EMTF-XML code paths.

  • Fix ``compute_resistivity_phase`` rejected legitimate missing uncertainty (pycsamt.z.resphase) – a valid impedance with an unavailable (NaN) uncertainty raised instead of propagating the missing value; exposed once the EDI sentinel fix above landed.

  • Fix ``pycsamt.api.occam1d``’s public style API was never wired into ``pycsamt.api``’s top-level namespacePYCSAMT_OCCAM1D and friends were not importable via from pycsamt.api import PYCSAMT_OCCAM1D, the exact form documented in examples/occam1_demo/README.md. Fixed to match every other style registry in the package.

Changed#

  • Enhancement User-controlled station spacingstation_confidence_table, frequency_confidence_table, and plot_confidence_profile (pycsamt.emtools.qc) gain a force_spacing parameter: when True, coordinate lookup is skipped entirely and every station is laid out at uniform spacing_m steps, for surveys whose EDI coordinates are known to be unreliable.

  • Enhancement ``pycsamt.airborne`` guideline-compliance passAirborneEMDataset now inherits CoreObject instead of MTBase (it performs no EM arithmetic itself); the registry definitions and AirborneQCIssue now inherit PyCSAMTObject, matching the equivalent objects in pycsamt.emtf/pycsamt.metadata; ztem and afmag were migrated onto pycsamt.airborne.validation, removing several dozen lines of near-duplicate boundary-validation logic. Verified against the full existing test suite at every step; no behaviour change outside the AirborneEMDataset base class.

Docs & tooling#

  • Docs ``user_guide/site/`` updated for EMTF-XMLcontainers.rst gains a “Working With EMTF-XML” section (Site.from_xml, lazy Site.edi/Site.tf materialization, the typed metadata properties, Sites.write_xml, directory discovery), with real captured output against the bundled Gabbs Valley station; metadata.rst, export_reporting.rst, selection.rst, and the section index note where EDI-only tooling still applies and where the new EMTF-XML surface takes over. Also fixed a stale ../metadata/index.rst cross-reference to “the upcoming EMTF XML guide”, which now exists (EMTF).

  • Docs ``pycsamt.airborne`` docstrings – every public class and function across base, registry, qc, io, and the ztem/afmag adapter and metadata modules gained complete NumPy-style docstrings (Parameters, Returns, Raises, and Notes explaining non-obvious design choices).

  • Build ``pyproject.toml`` gains ``norecursedirs`` excluding the local-only vendored EMTF-FCU checkout (pycsamt/emtf/fcu-v4.1/) from pytest collection – without it, a plain pytest run breaks entirely on Windows for anyone who builds that reference oracle locally.

  • Docs ``user_guide/models/occam1d.rst`` – new page covering configuration, native-file construction, single-station and batch inversion, and text/image result products, with real captured output and figures against the Gabbs Valley survey.

  • Docs ``user_guide/airborne/`` completed – 6 pages (index, overview, data_model, site, registry_and_io, quality_control), real captured output and figures against the committed synthetic ZTEM/ AFMAG/MobileMT sample surveys, including a three-panel diagnostic composite in site.rst that calls each technology’s own pycsamt.emtools literature-grade plotting function directly on the containers this page builds.

  • Docs ``api/airborne.rst`` – new API reference page for pycsamt.airborne, registered in API reference.

  • Docs ``user_guide/data_loading.rst`` now documents that ensure_sites normalizes EMTF-XML exactly like EDI into the same Sites container (with a symmetric write-back example), and introduces ensure_asites as airborne surveys’ own, separate loading boundary.


2.3.0 New Fix API Change Docs Build#

Released 2026-08-14.

Minor release. New pycsamt.geology package: general-purpose lithology classification, a pluggable rock-property provider architecture, and new structural-geology primitives (strike/dip, trend/plunge, fault traces), split out of pycsamt.interp with backward-compatible re-exports. Deep-filled the lithology, petrophysics, and monitoring pages of the interpretation user guide with real, doctest-verified examples, which surfaced and fixed several stale docstring examples and a real ``RockDatabase`` iteration bug. Also new: a Dual-Uncertainty Hybrid Inversion (DUHI) pathway – DUHIInverter2D and a native Occam2D prejudice-file reader/writer connect a trained AI ensemble’s mean/uncertainty to the physical solver’s data- and model-space regularization, alongside a reusable synthetic benchmark-geology generator and a field-calibrated corruption model – exercised end to end against a real compiled Occam2D binary and a real trained ensemble, which surfaced and fixed two further real bugs. Running a real legacy CSAMT survey line (AVG + ``.stn`` topography) end to end through AVG-to-EDI conversion and the processing pipeline surfaced and fixed six further bugs across the Zonge readers, the AVG-to-EDI unit conversion, and two pipeline steps, plus a packaging gap that dropped the fallback EPSG table from installed wheels. The pipeline itself also gains four method-aware presets (``mt_qc``/``amt_qc``/``csamt_qc``/``csumt_qc``) with real near-field correction and data-driven QC, a branded dashboard report, and data-driven power-line-harmonic detection (``mains_hz=”auto”``) – building real, runnable examples for the rewritten config-driven-pipeline tutorial and CLI reference surfaced three further bugs, including one where a pipeline step silently corrupted its site count. See Version 2.3.0 for the full account.

Added#

  • New pycsamt.geology package – a general-purpose geology package with no electromagnetic dependency, holding earth-science domain knowledge that pycsamt.interp builds on rather than owns: resistivity-to-lithology classification (lithology), the literature-compiled rock/fluid property table behind it (rock_library), ground-truth borehole logs (borehole), and field structural measurements (structural). pycsamt.interp re-exports Borehole, Interval, RockDatabase, RockEntry, StratigraphicLog and Layer from their new home for backward compatibility; new code should import them from pycsamt.geology.

  • New Pluggable rock-property providersRockPropertyProvider protocol with LocalRockPropertyProvider and RemoteRockPropertyProvider implementations, exposed as RockDatabase.from_url and RockDatabase.from_provider. Remote lookups are cached under ~/.pycsamt/rock_db/ with a TTL and fall back to the built-in table on fetch failure, mirroring the existing model-cache convention in pycsamt.ai._zoo. The built-in table itself (BUILTIN_ROCKS) was expanded from literature (Palacky, Telford, Keller) and given a public read-only RockDatabase.entries accessor.

  • New Structural geology primitivesStructuralMeasurement (planar strike/dip/dip-direction, cross-validated against each other), LinearMeasurement (trend/plunge), and FaultTrace (where a fault crosses a profile, with downthrown side and sense) record field structural evidence against a profile position, matching the recording convention used by modern digital field-mapping tools. StructuralModel collects all three per profile with from_csv (three independent optional schemas) and nearest/within queries, ready to back the structural-continuity review step described in Interpretation workflow.

  • New DUHI hybrid AI-physics inversionDUHIInverter2D and DUHIPreparation (pycsamt.ai.inversion) connect a trained AI ensemble’s mean/uncertainty grids to Occam2D’s data-space errors (apply_observation_reliability) and model-space prejudice, via the new map_ai_grid_to_occam() and OccamPrejudice – a full reader/writer for the sparse OCCAM2MTPREJ_2.0 model-prejudice format. Reliability itself is now independently testable via dimensionality_reliability() and combine_observation_reliability() (pycsamt.ai.inversion), and run_forward() runs the bundled solver’s native forward-only -F mode directly.

  • New Reproducible synthetic benchmark infrastructuregenerate_benchmark_geology() and BenchmarkGeology (pycsamt.ai.geology.benchmark) generate the six in-distribution and six out-of-distribution geological families (ID_BENCHMARK_FAMILIES, OOD_BENCHMARK_FAMILIES); apply_empirical_corruption() (pycsamt.ai.domain_gap.empirical) applies a field-calibrated, jointly-sampled static-shift/noise/missing-observation corruption model; build_2d_maxwell_problem() factors out the validated geology-grid-to-solver-problem construction this is built on.

  • New MTBase.z_ohms_to_mvk_nt() (pycsamt.core.base) – inverse of the existing z_mvk_nt_to_ohms, converting SI-ohm impedance to the standard EDI (mV/km)/nT field-unit convention.

  • New Branded pipeline dashboard report – opt-in dashboard.html (CLI --dashboard / report_formats) with KPI stat tiles and three native inline-SVG charts, alongside the existing plain report (whose palette also moved to the same real, validated pyCSAMT brand tokens).

  • New Method-aware pipeline presetsmt_qc, amt_qc, csamt_qc, csumt_qc, and get_preset_for_method() (pycsamt.pipeline). CSAMT/CSUMT presets wire in real near-field correction (SRC001) for the first time; all four add data-driven “smart” tensor/tipper/strike QC steps (QC005-QC007) and a raw-vs-processed station preview (PRE001/PRE002), both usable by any pipeline.

  • New notch_powerline(..., mains_hz="auto") (pycsamt.emtools.remove_noise, pipeline step NR001) – detects 50 vs 60 Hz from the survey’s own frequency grid and snaps each harmonic to its nearest real sample (snap_frac tolerance) instead of requiring an exact match. Plain numeric mains_hz is unaffected.

Fixed#

  • Fix Corrected stale, incorrect example outputs in the docstrings of ArchieModel.forward/.saturation, WaxmanSmitsModel.forward, kozeny_carman_K and HashinShtrikmanBounds (pycsamt.geology.lithology, pycsamt.interp.petrophysics), found while building doctest-verified examples for Petrophysical toolkit.

  • Fix RockDatabase had no public way to iterate its entries; the desktop app’s InterpController.plot_rock_db() and the pycsamt rocks CLI command both worked around this by reaching into the private _entries attribute directly, and the desktop call site called list(db) on a non-iterable object outright, raising at plot time. Added a public RockDatabase.entries read-only tuple property and switched both call sites to it; the CLI table also gained a source column.

  • Fix PosteriorCalibrator.calibrated_std/.predict_posterior divided the raw ensemble standard deviation by the learned dispersion scale instead of multiplying, shrinking an already under-dispersed estimate instead of widening it; no existing test exercised an under-dispersed case. Fixed both methods and added a regression test that specifically does.

  • Fix InversionResult.rho_2d assigned model-layer values starting at mesh row zero instead of the first row after the air layers, silently shifting the recovered resistivity section by the air-layer count whenever a mesh had any.

  • Fix The legacy (kind-1) AVG parser (pycsamt.zonge.utils._parse_kind1) raised on real AMTAVG 7.40 files whose data rows carry an unlabeled trailing field beyond the header’s sPhz column; it now extends the header with generic names when every row consistently carries the same surplus instead of raising a column-count mismatch.

  • Fix The .stn topography reader (pycsamt.zonge.utils.read_stn, Topography._normalize_stn_columns) crashed on files using GridE/ GridN grid-coordinate column names, and mis-parsed files with a stray HTML fragment pasted into the header line.

  • Fix AVGtoEDI wrote Zonge’s SI-ohm impedance directly into EDI Z sections, which the EDI/SEG standard and the rho = 0.2|Z|^2/f formula used to derive apparent resistivity both assume are in (mV/km)/nT field units – every AVG-derived EDI’s resistivity/phase was wrong by a factor of about (mu_0 x 1e3)^2. Fixed via the new MTBase.z_ohms_to_mvk_nt().

  • Fix Pipeline steps SK001/SK002 (mask_by_skew, keep_longest_low_skew) had registry defaults keyed threshold= instead of the functions’ actual thresh= parameter, crashing the skew-masking step of the full_processing/publication_ready presets on every run (silently downgraded to a no-op warning by the pipeline’s default error handling).

  • Fix pycsamt.emtools.ss._nearest_idx was called with its arguments reversed at four call sites (estimate_ss_ama, estimate_ss_loess/ estimate_ss_refmedian, detect_near_surface, _pt_phi_for_station), aligning a station’s own frequency axis against phase-tensor-table row positions instead of the other way around; this crashed static-shift correction (SS001) with an IndexError whenever it ran downstream of skew masking.

  • Fix nr_qc_harmonic_waterfall silently rendered a blank heatmap when no station frequency fell within tolerance of a mains harmonic – as for any CSAMT log-decade frequency sweep; it now shows an explanatory message instead.

  • Fix Pipeline step SRC002 (normalize_response) was registered returns_sites=True but its function actually returns a DataFrame, not a Sites object; chaining it (as csamt_qc/csumt_qc are the first presets to do) silently inflated the site count to n_stations x n_frequencies then collapsed it to zero one step later, with no error raised. Fixed by registering it returns_sites=False.

  • Fix pipe run --help’s --preset option listed only 6 of 11 registered presets; pipe steps --help claimed “33 registered steps” and 8 categories where there are now 55 steps across 10 categories. Both corrected.

Changed#

  • API Change ResistivityModel (pycsamt.interp) now inherits PyCSAMTObject/MetadataMixin like the rest of the interpretation layer, gaining auto-repr, to_dict(), clone(), update(), and a new metadata field for free-form provenance (coordinate reference, backend version, original file paths).

Docs & tooling#

  • Docs Deep-filled three previously stub pages under Interpretation with narrative content, real doctest-verified pycon examples, and regenerated figures: Lithology classification, Petrophysical toolkit (Archie’s law, Waxman-Smits clay conductivity, Hashin-Shtrikman bounds, Kozeny-Carman hydraulic conductivity, water-table detection), and Monitoring and fusion (time-lapse EM, multi-method fusion). Added the “Nearest-midpoint classification” glossary term.

  • Docs Added the pycsamt.geology reference page and updated pycsamt.interp, Interpretation Commands, and the interpretation user guide’s toctree to match the pycsamt.geology split.

  • Docs Rewrote Run a Pipeline From Config and Pipeline Commands to cover method-aware presets, the step cache, live progress, the run-history log, and the dashboard report; converted every interactive example on both pages to pycon per the documentation guidelines, with output re-captured from real runs. Added the “Run history log” and “Dashboard report” glossary terms and a new real field-zone pseudosection figure.

  • Build pycsamt/gis/epsg.npy – the fallback EPSG-code table used when pyproj’s legacy flat epsg file isn’t available – was missing from pyproject.toml’s package-data and MANIFEST.in, so installed wheels silently shipped without it and any EPSG lookup that fell back to it failed at import time with “Failed to load EPSG definitions”.


2.2.2 Fix Docs Enhancement#

Released 2026-08-07.

Patch release. A real, validated Occam2D compile-and-run pass exposed and fixed two solver-integration bugs; building faithful, doctest-verified examples for the API view layer exposed and fixed a custom-wrapper bypass. The bulk of this release is documentation: a new end-to-end Stratagem hardware-to-inversion tutorial built on a real Dafang field survey, a rewritten API Views page, new FAQ entries, and repository cleanup. See Version 2.2.2 for the full account.

Added#

  • Feature Stratagem hardware-to-inversion tutorial – new Stratagem Field Data To Occam2D Inversion, an end-to-end worked example on a real Dafang line-5 field survey: raw Stratagem hardware import, coordinate injection with an empirically confirmed EPSG, static-shift correction, frequency filtering, noise removal, cross- validation against an independent tool and against smooth_logfreq(), QC export, and a real compiled Occam2D inversion. Bundled as data/stratagem/Dafang/ with its own README.md.

  • Docs Rewrote API Views with real, doctest-verified examples throughout, correcting its previous claim that table-returning functions return plain pandas.DataFrame objects by default – they resolve through PYCSAMT_API_VIEW, whose default backend already wraps into APIFrame / APIResult.

  • Docs Added FAQ entries on the API view default behaviour and the new Stratagem tutorial to Frequently asked questions.

Fixed#

  • Fix OccamRunner.compile() looked for a binary named Occam2D rather than the module’s own .exe-suffixed constant on Windows, reporting a false “binary was not produced” error after a genuinely successful make.

  • Fix OccamMesh read and wrote the mesh header’s 4th control field as an air-layer count; the real vendored Occam2D solver (MT2D.f90) uses that field for the fixed-resistivity count (nrfix, always 0 for pycsamt-generated meshes) and manages air layers internally, desynchronizing every subsequent mesh read. Confirmed against two independent, bundled reference mesh files and fixed in mesh.py (parsing, writing, and docstring).

  • Fix maybe_wrap_frame(..., api=True) ignored a custom wrapper configured via configure_api_view(wrapper=...), always producing an APIFrame instead of the user’s callable’s own return type.

  • Fix Removed stray root-level artefacts (figures/, generated .npz/.png outputs, all_blocks.pycon) left over from example runs; all_blocks.pycon is now gitignored so it stops reappearing.

Changed#

  • Enhancement TEM1DForward now delegates its Hankel/Fourier transform to empymod’s validated digital linear filters instead of a hand-rolled quadrature; empymod moved from an optional extra to a core dependency.


2.2.0 Feature New Fix Enhancement API Change Breaking Docs Build Tests#

Released 2026-08-05.

Feature release adding genuine 3-D and triangular-mesh 2-D Maxwell training-data pipelines, an Inv3DAgent physics=”mt3d” mode, a real MARE2DEM external-solver adapter, real topography support in the mesh and AI agents, cross-platform build tooling for the external ModEM / Occam2D / MARE2DEM solvers, three new tutorials (TEM/TEMAVG, Zonge AVG K1/K2, CSAMT groundwater mapping), and two independent rounds of real bug fixes across pycsamt.emtools (phase-tensor, frequency-editing, EDI-coordinate, spectra, and tipper-plotting) – plus the fixes that made ModEm3DAdapter and Mare2DEMAdapter physics-validated against real compiled binaries for the first time. See Version 2.2.0 for the full account.

Added#

  • Feature 3-D Maxwell training-data pipeline – added pycsamt.ai.training.dataset3d (generate_3d_maxwell_dataset(), Maxwell3DDatasetConfig, Maxwell3DSample, Maxwell3DDataset), generating spatially correlated 3-D geological volumes and solving them with MT3DAdapter via solve_batch(), packaged into a realization-split, resumable-cache dataset. The solver mesh is built padded and non-uniform (fine core at the geology grid’s native resolution, geometrically padded out to a skin-depth safety margin) rather than uniform, since MT3DAdapter’s cell budget cannot afford a uniform mesh reaching the same extent. An opt-in cells_per_skin_depth config decouples solver-core resolution from the geology grid’s own spacing for a frequency-aware accuracy/cost trade-off.

  • Feature External-solver build tooling – added pycsamt/models/_solver_build/, cross-platform (Windows/Linux/macOS) bash scripts (modem2d.sh, modem3d.sh, occam2d.sh, mare2dem.sh) that auto-detect a Fortran toolchain, offer an opt-in auto-installer, and compile the vendored ModEM and Occam2D sources (or drive pycsamt.models.mare2dem.SourceManager for MARE2DEM) with resilient retrying make passes. Exposed two ways: a pycsamt build <modem2d|modem3d|occam2d|mare2dem> CLI subcommand group, and a root Makefile (make modem2d, make modem3d, make occam2d, make mare2dem) for checkout-based contributors. See Compiling the External Solvers.

  • Feature Triangular-mesh 2-D Maxwell FEM solverpycsamt.forward.maxwell.tri_fem2d adds a real, in-house P1 Galerkin TE/TM finite-element solver (TriFEM2DAdapter) on unstructured triangular meshes, alongside a new solver-neutral TriMesh/TriProblem contract (pycsamt.forward.maxwell.contracts_tri) that sits beside the existing rectilinear MaxwellProblem contract. Both analytic half-space and layered-earth benchmarks pass.

  • Feature Graded triangular mesh generationpycsamt.forward.maxwell.tri_mesh_gen.build_graded_tri_mesh() replaces the previous uniform-checkerboard mesh with a real graded mesh built on Shewchuk’s Triangle library, refining cell size by distance to the nearest station instead of tiling the whole domain at one resolution.

  • Feature MARE2DEM external-solver adapterpycsamt.forward.maxwell.mare2dem.Mare2DEMAdapter wraps the real compiled MARE2DEM 2.5-D FEM binary as a Maxwell backend, built on a new topography-aware triangular mesh builder (pycsamt.models.mare2dem.tri_mesh) and a Triangle-executable wrapper (pycsamt.models.mare2dem.triangle_exec). Both analytic benchmarks now pass against a real compiled binary, built from the vendored source in an isolated WSL2 toolchain.

  • Feature Triangular-mesh 2-D training datapycsamt.ai.training.dataset2d_tri generates versioned 2-D AI-inversion training datasets on the new graded triangular mesh, resampling correlated resistivity fields onto triangle centroids and solving each realization through TriFEM2DAdapter. Exposed as Inv2DAgent(physics="mt2d_tri").

  • Feature Reusable mesh-display utilitypycsamt.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. plot_inversion_result_2d gained an opt-in show_mesh toggle that uses it.

  • Feature CSAMT frequency-schedule plotpycsamt.emtools.csumt.plot_frequency_schedule() visualizes which target depths survive the CSAMT band filter.

  • New TEM/TEMAVG-to-EDI tutorialProcess A TEMAVG Survey: TEM To Corrected EDI walks a real 51-station TEM100 profile from raw TEMAVG soundings through late-time apparent-resistivity conversion, geographic-coordinate attachment, EDI export, correction, and a graded triangular mesh, and gates a Maxwell AI inversion on it.

  • New Zonge AVG (K1/K2) tutorialProcess Zonge AVG Lines K1 and K2 walks two real Zonge AVG CSAMT lines (legacy K1 and modern-encoding K2, with midpoint-vs-survey-peg coordinate handling) through EDI conversion, native-AVG QC, an explicitly-rejected static-shift trial, the accepted Hampel-despike-plus-spatial-correction chain, and a gated AI inversion – including honest documentation of where source-effect and phase-tensor diagnostics cannot be run for this dataset.

  • New CSAMT groundwater-mapping tutorialMap 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 omitted with a worked mathematical proof that they are meaningless for Zxx=Zyx=Zyy=0 data (Re(Z) is singular), rather than showing their previously-fabricated output.

Fixed#

  • Fix .gitignore build-artifact collisions – long-standing, unanchored build/_build/lib/ patterns (accumulated from several pasted-in template blocks) silently swallowed new top-level-ish directories with matching names – pycsamt/models/_solver_build/lib/common.sh (the shared library every build script sources) and pycsamt/cli/commands/build/ never showed up in git status at all. Anchored every generic pattern of this kind to the repo root (/build, /dist/, /_build, /.build, /lib/, /lib64/, /parts/, /sdist/, /var/); confirmed docs/build/, docs/_build/, and root dist/ are still ignored via their own more specific rules.

  • Fix ModEM 2-D and 3-D Makefiles – the vendored serial-build Makefiles (pycsamt/models/modem/_source/{2D,3D}/Makefile) never compiled Declaration_MPI/Sub_MPI/Main_MPI.f90, despite being unconditionally used by several other files, nor passed -cpp to strip their #ifdef MPI guards for a serial build; the 3-D Makefile was also missing a build rule for sg_spherical.f90. Fixed in both Makefiles.

  • Fix ModEM model3d footer-detection heuristicpycsamt.models.modem.model3d._parse_model3d’s trailing centre/rotation-footer detection used a hardcoded len(parts) <= 3 threshold instead of comparing against nx, silently truncating the resistivity grid for any model with nx <= 3. Fixed to compare against nx instead.

  • Fix ModEM WS-format model filepycsamt.models.modem.model3d.ModEmModel3D’s reader and writer omitted a mandatory leading comment line that ModEM’s own Fortran reader unconditionally reads and discards before the dimensions line. A real compiled binary rejected the header-less file with a Fortran runtime error (“Bad integer for item 1 in list input”), reading the dx-widths line as the dimensions line instead.

  • Fix ModEM air-layer out-of-bounds read – ModEM’s own Fortran setup_airlayers (the “mirror” method) 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 (“b in QMR contains NaNs”). This is a genuine latent bug in the vendored ModEM Fortran itself, not patched there; instead ModEm3DAdapter.assess() now rejects problems with fewer than _MIN_EARTH_Z_CELLS (10) earth z-cells before any file is written, with an error message explaining why.

  • Fix ModEM 3-D forward commandModEm3DAdapter._build_command omitted the third positional argument ModEM’s -F forward mode requires (the predicted-data output filename). Without it, Mod3DMT printed its own usage banner and exited 0 having written nothing, which surfaced downstream as a generic “no predicted-data file was found” error rather than a malformed-command error.

  • Fix mt3d.py layered-earth benchmarkpycsamt.forward.maxwell.mt3d.MT3DAdapter’s layered_earth_benchmark failure (30-45% error) had been misdiagnosed as a physics defect in its boundary approximation; the real cause was that it supported uniform cell spacing only, which cannot simultaneously reach several skin depths of lateral extent and resolve a thin layer interface within its cell budget. Generalized the discrete curl operators and edge-conductivity averaging to non-uniform per-axis cell widths (the same padded-mesh strategy MT2DAdapter already uses; supports_nonuniform_mesh is now True). Both half-space and layered-earth analytic benchmarks now pass (~1.9% and ~3.5% normalized RMS respectively) on a 4,096-cell padded mesh.

  • Fix Windows executable resolutionpycsamt.forward.maxwell.external.resolve_executable()’s search_paths fallback checked a literal Path(directory) / name instead of applying PATHEXT the way its PATH lookup already did via shutil.which(), so a bare name like "Mod3DMT" never resolved to the real "Mod3DMT.exe" file on Windows. Fixed additively (try shutil.which first, keep the literal check as a fallback), so non-Windows platforms and extension-less test doubles are unaffected. A real, previously-latent bug for any external-solver adapter using search_paths, not ModEM-specific.

  • Fix Triangular FEM solver sign error – the P1 Galerkin weak form’s integration-by-parts step flipped the stiffness term’s sign relative to the strong-form PDE, caught via interior-vs-boundary residual checks against a known analytic field rather than by inspection. Both analytic benchmarks now pass.

  • Fix Triangle mesh generator segfault – an unconstrained vertex resting exactly in the interior of a PSLG segment could crash Triangle’s quality refinement. build_graded_tri_mesh() now splits the segment at that vertex explicitly instead of leaving it encroaching.

  • Fix Triangular-mesh solver rejected real topographyTriFEM2DAdapter assumed every receiver sat exactly at z = 0, so stations on real terrain were hard-rejected. topo_x_m/topo_z_m are now threaded end to end through build_graded_tri_mesh(), dataset2d_tri, and Inv2DAgent, and a new datum-shift-invariance benchmark checks the fix.

  • Fix Mesh-display edge overlay on draped sectionsdraw_mesh’s edge overlay only handled 1-D rectilinear cell edges, not the 2-D coordinate arrays a topography-draped section returns, found while using it on a draped inversion result.

  • Fix 3-D AI inversion rendering for single-line surveysInv3DAgent rendered a degenerate triangulated sliver for single-line survey geometries (e.g. a lone profile). New _profile_distance_km, _is_profile_geometry, and _smooth_depth_axis helpers detect this case and render a continuous, PCHIP-interpolated section instead.

  • Fix 2-D AI-inversion station labels collided with the titleInv2DAgent’s triangular-mesh section figure used fig.subplots_adjust to reserve headroom for station labels, which PlotConfig’s default bbox_inches="tight" cropped away again at save time. Station names now use StationAxisStyle label-thinning plus a points-based ax.set_title pad, which survives the tight-bbox crop; markers changed from generic “Electrodes” to labeled “Stations”.

  • Fix 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 reported by pycsamt.emtools.tensor was wrong. Fixed; worked examples and figures regenerated with corrected numbers.

  • Fix Phase-tensor pseudosection displayplot_phase_tensor_psection had two real display bugs: the 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 skew/beta values; and the reserved legend strip could stretch the y-frame far beyond the real data if a single period was an outlier. The colorbar now always derives from data percentiles (clim_pct), and legend/reference- ellipse artists are drawn in axes-fraction coordinates so they no longer distort the data axis.

  • Fix Strike-rose panel for tipper-less surveysplot_strike_analysis always drew three rose panels (strike/phase-tensor/tipper), leaving a permanently empty panel for tipper-less surveys (most AMT/CSAMT). It now detects whether any station carries a tipper channel and draws two panels when none do. This changes the returned figure’s axes count for tipper-less surveys.

  • Fix Frequency-drop left a stale rotation angle array – dropping or masking frequencies (manual frequency drop, frequency editing) re-sliced every per-frequency array except rotation_angle, leaving it at its original length while the frequency axis shrank – a latent shape mismatch for any downstream code zipping rotation_angle against frequency-indexed data. Fixed at all three call sites in pycsamt.emtools.frequency/pycsamt.emtools.remove_noise.

  • Fix TEM-derived EDI files silently lost their HEAD sectionTEMtoEDI._set_head assigned head.lon/head.lat directly from local, non-geographic survey coordinates, raising a LocationError that a bare except Exception: pass then swallowed – dropping the entire >HEAD section with no warning. A geographic-range guard now only sets lon/lat when the coordinates are actually in range.

  • Fix TEM-derived EDI files were missing mandatory sectionsTEMtoEDI never wrote the >=DEFINEMEAS/>=MTSECT sections at all. A new _set_meas step builds them from placeholder Hx/Hy/Ex/Ey channels.

  • Fix TEM-derived EDI resistivity was wrong by a factor of ~6e5TEMtoEDI built its impedance magnitude with the SI convention (Z = sqrt(rho_a * omega * mu0)), but pyCSAMT’s EDI ecosystem (ResPhase.compute_resistivity_phase) expects the field-unit convention rho = 0.2 * |Z|^2 / f. Fixed to Z = sqrt(5.0 * freq * rho_a) so reloaded apparent resistivity round-trips correctly.

  • Fix ``pycsamt tdem info`` always reported zero coordinate points – it read a nonexistent coord.records attribute instead of coord.n_points.

  • Fix EDI coordinates ignored ``>=DEFINEMEAS``pycsamt.site.utils.get_coords() only read LAT=/LONG= from >HEAD; older BIRRP-processed files that leave those empty but carry a real 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. Added a fallback to >=DEFINEMEAS per missing field.

  • Fix 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.

  • Fix 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. Fixed to sort internally in log-period space, matching ss.py’s analogous helper.

  • Fix 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; both now detect whether the plotted coordinates are projected metres or geographic degrees and label the axes accordingly.

  • Fix 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, 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.

  • Fix Phase-tensor legend didn’t show an ellipsephase_tensor_legend drew a plain circle with one unlabeled line and no docstring; rewrote it to draw a real, labeled reference ellipse matching the convention the pseudosection and map plots use.

Changed#

  • API Change Inv3DAgent physics mode – added physics="mt3d" to pycsamt.agents.Inv3DAgent, training the GCN inverter against real 3-D Maxwell forward solves generated from dataset3d, reusing each survey’s actual station (x, y) coordinates (not a synthetic layout, unlike Inv2DAgent’s physics="mt2d"). New constructor parameters geology_grid_nx_ny/geology_grid_nz (training-grid resolution) and cells_per_skin_depth (opt-in frequency-aware solver-mesh resolution, default None preserves prior behaviour) and a genuine held-out recovery check reported in AgentResult.data["mt3d_recovery"]. physics="mt1d" (tiled 1-D forward models + GCN spatial smoothing) remains the unchanged default.

  • Enhancement Phase-tensor ellipse stylingplot_phase_tensor_psection gained an MTpy-style normalise_by="shape" sizing mode (constant major axis, s2/s1-ratio-only shape), a color_mode="segmented" discrete color mode, and ellipse_kws/cb_kws passthrough.

  • Enhancement Induction-vector plot labelingplot_tipper_hodograms gained a period-band legend, and plot_induction_multiperiod_map gained configurable xlabel/ylabel parameters instead of a hardcoded "x (m)"/"y (m)" regardless of coordinate system.

Removed#

  • Breaking Rejected-legacy 3-D AI-inversion sections – dropped from Map Porphyry Mineralization From Noisy AMT, obsolete now that Inv3DAgent(physics="mt3d") supports non-uniform, padded meshes and no longer needs the graph-only tiled 3-D fallback they documented.

Docs & tooling#

  • Docs Solver compilation guide – added Compiling the External Solvers, covering the pycsamt build CLI, the root Makefile, toolchain auto-detection/installation, and per-solver notes (ModEM 2-D/3-D, Occam2D, MARE2DEM), with real captured transcripts.

  • Docs 3-D Maxwell training-data guide – added 3-D Maxwell training-data generation (mirrors 2-D Maxwell training-data generation’s depth and style), with real captured pycon output verified byte-exact via a shared-namespace doctest harness, and two new generated figures (realization gallery, response anatomy).

  • Tests 3-D Maxwell regression coverage – added tests for pycsamt.ai.training.dataset3d, MT3DAdapter’s non-uniform-mesh curl operators, and ModEm3DAdapter including a requires_real_modem-marked section that runs the half-space and layered-earth analytic benchmarks against a real compiled binary when present and skips (not fails) otherwise, plus the Inv3DAgent physics="mt3d" end-to-end path.

  • Build Stricter line length[tool.ruff] line-length tightened from 97 to 79 characters, with a repo-wide reflow to match.

  • Tests Solver build tooling coverage – added tests for the pycsamt build CLI subcommand group (pycsamt/cli/tests/test_cmd_build.py).

  • Docs AI-inversion user guide deepened – ten AI inversion pages (agents, concepts, data, and more) gained worked detail and regenerated figures, and the glossary gained “Geological grid”, “Solver mesh”, and “Output grid” entries.

  • Docs Genuine combined-line 3-D porphyry AI inversionMap Porphyry Mineralization From Noisy AMT now documents a real, executed Inv3DAgent(physics="mt3d") run combining both survey lines into one volume with topography and station labels, replacing five figures generated from tiny, unreviewed training budgets (2-epoch/4-realization runs) that risked being misread as validated results.

  • Docs Scaled-up WILLY 2-D AI inversion, plus a tuning guideMap Porphyry Mineralization From Noisy AMT reruns both 2-D lines at 100 Maxwell realizations and a 100-epoch ceiling (up from 10 and 30) and adds a “Tune this configuration for a real deployment” section – a checklist of what to widen for a production run, plus a measured compute-cost breakdown (generation, not epoch count, dominates wall-clock time).

  • Docs Coarser-mesh KAP03 MT2D/MT3D AI rerunsCondition an MT Line With Tipper and Rotation reruns both the triangular-mesh MT2D and rectilinear MT3D AI inversions at 200 realizations and a 100-epoch ceiling with a coarser mesh capped at 100 km depth (down from 250 km), and adds station labels and topography to the comparison figure.

  • Build Netlify docs deploys gated to a dedicated branch – the release workflow’s Build Hook set trigger_branch to the tag name, which Netlify resolves as refs/heads/<tag> and always fails since tags aren’t branches. The tagged commit is now force-pushed to a bot-only docs-deploy branch that Netlify treats as its production branch, so ordinary master pushes can no longer trigger a docs build.

  • Build CI “interfaces” shard’s multi-hour crawl – two CLI tests ran expensive per-sounding work against the full 2,790-station JIANGSU survey instead of a --stems subset like every other test in the file (one test dropped from roughly two minutes to six seconds). The shard is also now split into interfaces-desktop (Qt/PySide6) and interfaces-cli (no Qt) for parallelism.

  • Build New dependenciestriangle>=20220202 (Shewchuk’s mesh generator, core dependency, backs the graded triangular mesh – no compiled external binary required) and xlrd>=2.0 (docs extra, reads legacy .xls TEM coordinate tables).

  • Docs Deepened ``strike``/``spectra``/``tensor``/``tf`` user guide pagesGeoelectric Strike, Cross-Spectra Analysis, Phase Tensor And Impedance Tensor Tools, and Transfer Functions And Tipper Diagnostics converted to real, byte-verified pycon output throughout, with 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).


2.1.0 Feature Fix API Change Docs Tests#

Released 2026-07-29.

Feature release adding deterministic, spatially correct station ordering, validated auditable station-metadata editing, and the first physics-grounded building blocks of AI inversion. See Version 2.1.0 for upgrade guidance and migration examples.

Added#

  • Feature Staged inversion loss package – added pycsamt.ai.losses (model, spatial, response, boundary, uncertainty) implementing the staged objective L = w_m*L_model + lambda_x*L_grad_x + lambda_z*L_grad_z + lambda_tv*L_TV + lambda_d*L_response.

  • Feature Scientific validation package – added pycsamt.ai.validation (recovery, residuals, calibration, ood) for recovery metrics, response residuals, predictive calibration, and out-of-distribution screening.

  • Feature 2-D Maxwell training-data pipeline – added pycsamt.ai.training.dataset2d.generate_2d_maxwell_dataset() and Maxwell2DDatasetConfig, generating spatially correlated 2-D geological realizations and solving them with MT2DAdapter into a versioned, realization-split, resumable-cache dataset.

  • API Change Inv2DAgent physics mode – added physics="mt2d" to pycsamt.agents.Inv2DAgent, training on the new dataset generator with the staged spatial-regularization loss and reporting a held-out recovery check. physics="mt1d" (tiled 1-D forward models) remains the unchanged default. pycsamt.ai.inversion.inv2d.EMInverter2D.fit() gained lambda_x, lambda_z, and lambda_tv staged-loss weights (PyTorch backend only).

  • API Change Global station-ordering policy – added pycsamt.api.configure_ordering(), pycsamt.api.reset_ordering(), PYCSAMT_ORDERING, and pycsamt.api.SiteOrderingConfig. Configure mode="auto" once and APIs using pycsamt.emtools.ensure_sites() inherit it.

  • API Change Sites ordering API – added pycsamt.site.Sites.ordered() with auto, chainage, input, natural station, latitude, and longitude strategies, plus pycsamt.site.Sites.ordering diagnostics.

  • API Change Transactional metadata API – added pycsamt.site.metadata.SiteMetadataEditor, pycsamt.site.metadata.MetadataChange, pycsamt.site.metadata.rename_sites(), pycsamt.site.metadata.update_metadata(), and pycsamt.site.metadata.update_metadata_all(). The editor supports copy-on-write staging, dry-run plans, batch validation, atomic in-place commits, audit records, and metadata-aware export.

  • API Change Container metadata conveniences – added pycsamt.site.Site.update_metadata(), pycsamt.site.Sites.update_metadata(), and pycsamt.site.Sites.rename(). Renaming changes station identity without reordering the collection or changing station coordinates.

  • Tests Spatial regression coverage – added synthetic edge cases and real-data tests for the bundled L18PLT and L22PLT survey lines, including a combined multiple-line collection.

  • Tests Metadata regression coverage – added mapping, sequence, callable, DataFrame, and CSV sources; nested HEAD/INFO changes; coordinate and duplicate-name validation; error policies; atomic commits; planning, auditing, export/reload; and real L18PLT and L22PLT workflows.

Fixed#

  • Fix Oblique survey lines – order is derived from both geographic coordinates and projected chainage instead of longitude, latitude, or lexical station names alone.

  • Fix Unsafe geometry guessesauto validates coordinate coverage, linearity, and cross-track spread and preserves input order when the spatial evidence is insufficient.

  • Fix Multiple-line interleaving – separated parallel profiles are ordered independently.

  • Fix Pseudosection station order – dataframe pivots no longer replace canonical profile order with lexical column order.

  • Fix Processing consistency – static-shift and near-surface methods, field-zone and CS/AMT sections, strike profiles, and pipeline presets now inherit the shared ordering strategy by default.

  • Fix 2-D mesh/receiver air-layer mismatch – the 2-D training-data mesh builder no longer relies on build_solver_mesh’s air layers, which were incompatible with MT2DAdapter (receivers must sit exactly at z=0) and produced spurious 393x errors despite an apparently converged solve.

  • Fix Depth-zero float precisionMaxwell2DDatasetConfig’s surface-depth check now uses a numerical tolerance instead of strict equality, so floating-point-derived grid spacings no longer raise spuriously.

  • Fix BatchNorm crash on a trailing batch of size 1pycsamt.ai.inversion.inv2d.EMInverter2D.fit()’s PyTorch training loop now drops a trailing size-1 batch instead of crashing with “Expected more than 1 value per channel when training”.

  • Fix TM-mode interface averagingpycsamt.forward.em2d._assemble_tm() now uses a thickness-weighted harmonic mean, not an arithmetic one, for the resistivity coefficient at a cell interface, matching the parallel-current-path physics there. Maxwell2DDatasetConfig generates and trains on both the TE-mode (zxy) and TM-mode (zyx) response by default again.

  • Fix Field-survey impedance unitspycsamt.ai.domain_gap.survey_fit.survey_data_from_sites() now converts z from the EDI-native [mV/km]/[nT] field convention to the SI convention SurveyData declares, instead of copying it unconverted.

Changed#

  • API Change pycsamt.ai.domain_gap.willy_fit is renamed to pycsamt.ai.domain_gap.survey_fit – the field-survey-to-domain-gap bridge works for any AMT, CSAMT, or MT survey, not only the bundled WILLY line.

  • API Change Processing sort_by=None and pycsamt.emtools.ensure_sites() order_by=None now mean “use the global policy”. Explicit arguments still override it; select input to retain loader order.

  • Docs Added configuration, migration, override, threshold, reset, and compatibility guidance for the ordering policy. Reworked the complete Site Tools guide with executable pycon transcripts, captured outputs, labeled equations, generated figures, and dedicated metadata guidance including the L22PLT rename-versus-order comparison.

  • API Change pycsamt.site.Sites.map() remains a callable mapper and does not accept a dictionary as a rename table. Use pycsamt.site.Sites.rename() for explicit station-name mappings.

Docs & tooling#


2.0.0 Feature API Change Breaking#

First stable release of the v2 line. It consolidates 2.0.0rc1 and 2.0.0rc2 — see those entries for the full v2 feature set — plus the changes below. The public API is now stable; v1 users should read Migration from v1 for the v1 → v2 name map.

Fixed#

  • Fix Tests Python 3.9 Qt-toolbar segfault — stub matplotlib’s Qt navigation toolbar under the offscreen platform so the CI interfaces shard no longer crashes on the Python 3.9 / matplotlib 3.9.x combination.

Changed#

  • Docs PyPI summary rescoped to the v2 feature set — “Scientific Python for electromagnetic geophysics — processing, inversion, AI agents, and apps”.


2.0.0rc2 Fix Build Docs#

Second pre-release of the v2 line, published to PyPI and TestPyPI for community testing ahead of the 2.0.0 final. Bug fixes, packaging, and docs only — no public API change since 2.0.0rc1.

Fixed#

  • Fix Tests Python 3.9 interpreter segfaults — bounded runaway matplotlib figure accumulation across the test session (a per-test plt.close("all")) and stubbed matplotlib’s Qt navigation toolbar under the offscreen platform. Both crashed the process on the Python 3.9 / matplotlib 3.9.x combination the CI interfaces shard runs.

  • Fix SEG parsing and plotting (pycsamt.seg) — consistent mixin MRO discovery, >=-prefixed section-name normalisation, DMS hemisphere parsing, and trend carry-through on topography resample.

Changed#

  • Build Lean distributions — the source distribution no longer bundles the documentation tree (which pushed it past PyPI’s 100 MB per-file limit) or the test suites; the sdist drops ~153 MB → ~4 MB and the wheel 5.8 MB → ~4.4 MB, with every runtime resource retained.

  • Docs Hosted-applications status — the Applications cards now reflect the in-progress hosted rollout and link each app’s installation guide rather than promising a live instance.

2.0.0rc1 Feature API Change Breaking Docs#

Release candidate — the first pre-stable tag of the v2 line. The public API is not backwards-compatible with v1 and may still change before 2.0.0 final; see Migration from v1 for a v1 → v2 name map.

Added#

  • Feature Multi-backend AI engine — every neural model (FCN, ResNet, CNN1D, UNet2D, DRCNN) dispatches transparently to PyTorch or TensorFlow through a unified abstraction layer (pycsamt.backends).

  • Feature Named-step processing pipelinepycsamt.pipeline chains catalogued steps (QC, notch, band selection, static shift, rotation, …) into reproducible runs with reports, presets, and YAML round-tripping.

  • Feature EM analytics toolboxpycsamt.emtools: QC tables, dimensionality, skew, strike, phase-tensor diagnostics, corrections, and one-call plotting for every view.

  • Feature Modeling stack — synthetic forward modelling (pycsamt.forward), external engines Occam2D / ModEM / MARE2DEM under pycsamt.models, and deep-learning inversion in pycsamt.ai (1-D/2-D/3-D nets, ensembles, uncertainty).

  • Feature Geological interpretationpycsamt.interp supersedes geodrill: method-agnostic model calibration, a 25-rock EM database, and Oasis Montaj / LAS 2.0 / VTK export.

  • Feature AI agents — specialised loader / QC / processing / inversion agents plus a workflow orchestrator that turns a plain-text request into a previewable agent chain (pycsamt.agents).

  • Feature IoT field telemetrypycsamt.iot: edge QC on the recorder, telemetry schemas and transports, power budgeting, GPS-sync audit, and a live field-session dashboard.

  • New Four applications, one engine — the desktop GUI, the web app, the Agent Master chat surface, and the MapView workbench (see Applications).

  • New Offline agent example gallery — seven runnable, zero-cost examples covering routing, context parsing, the model zoo, forward modelling, plan validation, coordination, and the AgentMaster front door (see Examples).

Changed#

  • API Change Configuration is explicit — runtime behaviour (output directories, plot styles, axis conventions, agent budgets) is set through the documented configure_* / reset_* families instead of scattered keyword arguments; see the API configuration guide.

  • API Change EDI is the source of truth — the impedance Z object and EDICollection now drive every processing and inversion pipeline.

Fixed#

  • Fix Encoding-safe figure saving — saving a figure or writing the plot config no longer raises UnicodeEncodeError on a legacy Windows (cp1252) console; the file is already on disk, so the save is no longer reported as failed (pycsamt.api.plot).

  • Fix Robust agent previewsAgentCoordinator dry-run previews route their output through an encoding-safe printer, so a preview never crashes on non-ASCII glyphs (pycsamt.agents).

  • Fix Qt-free headless imports — matplotlib Qt backends are imported lazily, so CI runners and documentation builds never pull Qt in.

  • Fix Docs sidebar handler crash on full rebuilds — pages whose HTML context carries meta=None no longer break the html-page-context hook ('NoneType' object has no attribute 'pop').

Removed#

  • Breaking pycsamt.geodrill — removed; use pycsamt.interp.

  • Breaking pycsamt.ff — removed; use pycsamt.emtools.

  • Breaking pycsamt.viewer — removed; use module-level .plot() methods on result objects.

  • Breaking pycsamt.modeling — removed; use pycsamt.models.

  • Breaking All v1 CLI entry points — removed and replaced by the v2 command set.

Docs & tooling#

  • Docs Documentation rebuilt — executed example galleries for every tool family (Examples), task-based user guides, application manuals, and a full autodoc reference.

  • Docs Documentation moved to pycsamt.org — hosted on Netlify with cached incremental builds (only changed gallery examples re-execute). Canonical URLs, the version switcher, README badges, and every in-app Documentation link now point at the new domain; the Read the Docs configuration is retired and its site remains as the v1 legacy archive.

  • Docs scikit-learn-style landing page — full-bleed hero carousel with a rotating survey-method keyword and clickable workflow strip; the six capability cards show real package output (noise removal, forward responses, a ModEM section beside a learned inversion, pseudosection and stratigraphic fence, pipeline timings with an agent dry-run chain, and a QC coverage audit), each regenerable via scripts/home_card_*.py; the “Code in action” panel is a themed editor window in light and dark mode.

  • Docs Navigation polish — the right “On this page” sidebar is back on ordinary pages (only the home page, the API reference tables, and the gallery index pages stay full-width); Map Tools and Tutorials indexes gained icon card grids; Site Tools uses a two-column grid; the desktop and web application entries point directly to their deep guides; header icon links now include the issue tracker and Stack Overflow.

  • Build Faster documentation builds — third-party module highlighting disabled in viewcode, the Sphinx environment and the executed gallery persist between Netlify deploys, and the docs environment installs CPU-only torch so the AI examples run without CUDA wheels.

  • Build Test-coverage lift — offline agent contract batteries, TDEM/AVG parser suites, and map topography/export tests; coverage now omits externally sourced shims and static configuration modules (compat/, _typing.py, config.py, projection.py).

  • Build PyPI-installable — pure pyproject.toml; setup.py removed.

Migration from v1#

The v1 module tree is replaced by the subsystem packages above. v1 scripts do not run unchanged against v2 — start from the user guide equivalents of your workflow, and use this map for the most common names:

v1

v2 equivalent

pycsamt.geodrill.geocore.GeoStratigraphy

pycsamt.interp.ModelCalibrator

pycsamt.geodrill.geocore.Geodrill

pycsamt.interp.ResistivityModel + ModelCalibrator

pycsamt.geodrill.geodatabase.GeoDataBase

pycsamt.interp.RockDatabase

pycsamt.modeling.occam2d

pycsamt.models.occam2d

pycsamt.viewer.plot

Module-level plot_*() methods on result objects

pycsamt.ff.processing

pycsamt.emtools