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.jsoncontract 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, legacypycsamt.geology.Boreholeviews, 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 validatedPCSFModel.pycsamt.formatconverts 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 afterimport 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, andpycsamt.map.MapView.from_pcsf().pycsamt.format.topographywires.pcsftopography through the existingpycsamt.map.topoelevation 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/.npyarray bundle, or an existing.pcsf/.pcsm) and writes.pcsfor.pcsmthrough the matching adapter – rebuilding the MARE2DEM mesh from the run’s.polyPSLG, recognisinglog10/lnresistivity keys, and carrying--topo/--epsg/--originthrough.pycsamt format detectreports the classification without converting (non-zero exit when unconvertible),pycsamt format infosummarises a file, andpycsamt format validateruns a header / load / schema / round-trip check. The shared detector is public aspycsamt.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_mapexports station CSV and Surfer ASCIIDSAAgrids.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;
InputBuilderandModEmRunnernow 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 options –
VolumeMapOptions.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), andvolume_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`` column –
app/web/callbacks/map3d.py’s own CSV/HDF5/NPZ elevation parser reimplementedpycsamt.map.topo’s parser independently and had quietly drifted from it: its id-column list never recognised astation_namescolumn/dataset thatpycsamt.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, defaultzeros(3)/0.0, matching the conventionread_mackie3d()already used on the same class – an initialNonedefault broke that convention’s existinggetattr(model, "origin", [0, 0, 0])fallback iniotools.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 rows –
OccamMesh.from_dataandModEmModel3D.halfspaceboth inserted air rows that Occam2D’s PW2D and ModEM’s WS formats never expect, crashing a real compiled Occam2D and Mod3DMT. Air rows/n_airnow always0.Fix ``ModEmControl`` inversion-control column misalignment – the key-field width (44 columns) pushed the colon past Fortran’s fixed
a36boundary, 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 newModEmForwardControl.Fix ``ModEmControl``/``ModEmForwardControl`` numeric fields lost their decimal point, silently corrupting real solver runs – Fortran’s
Ginput editing needs an explicit.or its own decimal-digit count re-places one; a real Mod3DMT echoed a written1e-7back as0.1000000E-13, andtarget_rmsitself 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 nothing –
go.Volume/go.Isosurfacewere handed the topography drape’sNaNcells 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-
NaNslices at depths the model never sampled, a colour scale that re-graduated per slice,n_slices=1cutting 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 background –
build_geo_contour_imagerasterised 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.logrollover on nearly every request and Windows refused the rename. Thewerkzeuglogger is pinned atWARNING(no propagation) andSafeRotatingFileHandlerskips a failed rollover.
Docs & tooling#
Docs PCBH format guide and public contract – PCBH — 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_cloudconsumers, plus a real two-panel figure rendering an Occam2Dgrid2dmodel and a ModEMgrid3dvolume through the same function call. New bundledexamples/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 examples – Quality-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 decluttering –
plot_confidence_profilegainsannotate_low_step, which thins the rotated station-name labels drawn above points belowci_lothe same waystation_label_stepalready thins the top station axis. Left at its defaultNone, 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=1restores the previous label-every-point behaviour.New EMTF-XML support in ``Site``/``Sites`` (
pycsamt.site.base) –Sitenow wraps either a SEG-EDI object or anEMTFXML document, via a lazy dual backend:Site.edi/Site.tfare both always available, materialized from each other on first access and cached, so every existing EDI-only call site acrosspycsamt.emtools, the CLI, and the desktop app keeps working unmodified. New:Site.from_xml/Site.to_xml,Site.backend, typedpycsamt.metadataproperties (site_meta,site_layout,provenance,processing,copyright,quality_meta),Sites.to_emtf_list,Sites.write_xml.to_sites/ensure_sitesnow recognize.xmlpaths,EMTFobjects, 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/SitesAPI and the rawEMTFdocument 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 mutatingflag_motion_susceptible_band, plusplot_motion_susceptibility_mapandplot_afmag_correction_comparison). Follows every existingemtoolsconvention:sitesin viaensure_sites,ax/axesin andAxes/Figureout 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/afmagadapters, 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), plusOccam1DInputBuilder/Occam1DBatchfor native-file construction and whole-survey batch inversion (optionally parallel viajoblib),Occam1DRunnerfor driving an externalOccam1D-compatible binary instead, andPlotModel/PlotResponse/PlotConvergence/PlotSummaryfor review figures. Optional Numba/joblib acceleration viapip 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 spacing –
plot_confidence_profile(),station_confidence_table(), andfrequency_confidence_table()derived station distance only fromeast/north/x/yattributes, which real EDI-backedSiteobjects never carry (they exposelat/loninstead). Every real survey therefore silently fell back toindex * 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-freell_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_mis 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
Siteswith no error – root-caused and fixed.Fix ``Sites.ordered()``/``Sites.select()`` silently downgraded XML-native sites to EDI on every non-
inplacecall (including the oneensure_sitesalways performs) by routing through the materialized EDI view instead of preserving the existingSiteobjects.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 usepycsamt.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 namespace –
PYCSAMT_OCCAM1Dand friends were not importable viafrom pycsamt.api import PYCSAMT_OCCAM1D, the exact form documented inexamples/occam1_demo/README.md. Fixed to match every other style registry in the package.
Changed#
Enhancement User-controlled station spacing –
station_confidence_table,frequency_confidence_table, andplot_confidence_profile(pycsamt.emtools.qc) gain aforce_spacingparameter: whenTrue, coordinate lookup is skipped entirely and every station is laid out at uniformspacing_msteps, for surveys whose EDI coordinates are known to be unreliable.Enhancement ``pycsamt.airborne`` guideline-compliance pass –
AirborneEMDatasetnow inheritsCoreObjectinstead ofMTBase(it performs no EM arithmetic itself); the registry definitions andAirborneQCIssuenow inheritPyCSAMTObject, matching the equivalent objects inpycsamt.emtf/pycsamt.metadata;ztemandafmagwere migrated ontopycsamt.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 theAirborneEMDatasetbase class.
Docs & tooling#
Docs ``user_guide/site/`` updated for EMTF-XML –
containers.rstgains a “Working With EMTF-XML” section (Site.from_xml, lazySite.edi/Site.tfmaterialization, 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.rstcross-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 theztem/afmagadapter 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 plainpytestrun 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.rstthat calls each technology’s ownpycsamt.emtoolsliterature-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_sitesnormalizes EMTF-XML exactly like EDI into the sameSitescontainer (with a symmetric write-back example), and introducesensure_asitesas 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.geologypackage – a general-purpose geology package with no electromagnetic dependency, holding earth-science domain knowledge thatpycsamt.interpbuilds 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.interpre-exportsBorehole,Interval,RockDatabase,RockEntry,StratigraphicLogandLayerfrom their new home for backward compatibility; new code should import them frompycsamt.geology.New Pluggable rock-property providers –
RockPropertyProviderprotocol withLocalRockPropertyProviderandRemoteRockPropertyProviderimplementations, exposed asRockDatabase.from_urlandRockDatabase.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 inpycsamt.ai._zoo. The built-in table itself (BUILTIN_ROCKS) was expanded from literature (Palacky, Telford, Keller) and given a public read-onlyRockDatabase.entriesaccessor.New Structural geology primitives –
StructuralMeasurement(planar strike/dip/dip-direction, cross-validated against each other),LinearMeasurement(trend/plunge), andFaultTrace(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.StructuralModelcollects all three per profile withfrom_csv(three independent optional schemas) andnearest/withinqueries, ready to back the structural-continuity review step described in Interpretation workflow.New DUHI hybrid AI-physics inversion –
DUHIInverter2DandDUHIPreparation(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 newmap_ai_grid_to_occam()andOccamPrejudice– a full reader/writer for the sparseOCCAM2MTPREJ_2.0model-prejudice format. Reliability itself is now independently testable viadimensionality_reliability()andcombine_observation_reliability()(pycsamt.ai.inversion), andrun_forward()runs the bundled solver’s native forward-only-Fmode directly.New Reproducible synthetic benchmark infrastructure –
generate_benchmark_geology()andBenchmarkGeology(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 existingz_mvk_nt_to_ohms, converting SI-ohm impedance to the standard EDI(mV/km)/nTfield-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 presets –
mt_qc,amt_qc,csamt_qc,csumt_qc, andget_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 stepNR001) – detects 50 vs 60 Hz from the survey’s own frequency grid and snaps each harmonic to its nearest real sample (snap_fractolerance) instead of requiring an exact match. Plain numericmains_hzis unaffected.
Fixed#
Fix Corrected stale, incorrect example outputs in the docstrings of
ArchieModel.forward/.saturation,WaxmanSmitsModel.forward,kozeny_carman_KandHashinShtrikmanBounds(pycsamt.geology.lithology,pycsamt.interp.petrophysics), found while building doctest-verified examples for Petrophysical toolkit.Fix
RockDatabasehad no public way to iterate its entries; the desktop app’sInterpController.plot_rock_db()and thepycsamt rocksCLI command both worked around this by reaching into the private_entriesattribute directly, and the desktop call site calledlist(db)on a non-iterable object outright, raising at plot time. Added a publicRockDatabase.entriesread-only tuple property and switched both call sites to it; the CLI table also gained asourcecolumn.Fix
PosteriorCalibrator.calibrated_std/.predict_posteriordivided 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_2dassigned 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’ssPhzcolumn; 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
.stntopography reader (pycsamt.zonge.utils.read_stn,Topography._normalize_stn_columns) crashed on files usingGridE/GridNgrid-coordinate column names, and mis-parsed files with a stray HTML fragment pasted into the header line.Fix
AVGtoEDIwrote Zonge’s SI-ohm impedance directly into EDIZsections, which the EDI/SEG standard and therho = 0.2|Z|^2/fformula used to derive apparent resistivity both assume are in(mV/km)/nTfield units – every AVG-derived EDI’s resistivity/phase was wrong by a factor of about(mu_0 x 1e3)^2. Fixed via the newMTBase.z_ohms_to_mvk_nt().Fix Pipeline steps
SK001/SK002(mask_by_skew,keep_longest_low_skew) had registry defaults keyedthreshold=instead of the functions’ actualthresh=parameter, crashing the skew-masking step of thefull_processing/publication_readypresets on every run (silently downgraded to a no-op warning by the pipeline’s default error handling).Fix
pycsamt.emtools.ss._nearest_idxwas 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 anIndexErrorwhenever it ran downstream of skew masking.Fix
nr_qc_harmonic_waterfallsilently 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 registeredreturns_sites=Truebut its function actually returns aDataFrame, not aSitesobject; chaining it (ascsamt_qc/csumt_qcare the first presets to do) silently inflated the site count ton_stations x n_frequenciesthen collapsed it to zero one step later, with no error raised. Fixed by registering itreturns_sites=False.Fix
pipe run --help’s--presetoption listed only 6 of 11 registered presets;pipe steps --helpclaimed “33 registered steps” and 8 categories where there are now 55 steps across 10 categories. Both corrected.
Changed#
API Change
ResistivityModel(pycsamt.interp) now inheritsPyCSAMTObject/MetadataMixinlike the rest of the interpretation layer, gaining auto-repr,to_dict(),clone(),update(), and a newmetadatafield 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
pyconexamples, 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.geologysplit.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
pyconper 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 whenpyproj’s legacy flatepsgfile isn’t available – was missing frompyproject.toml’s package-data andMANIFEST.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 asdata/stratagem/Dafang/with its ownREADME.md.Docs Rewrote API Views with real, doctest-verified examples throughout, correcting its previous claim that table-returning functions return plain
pandas.DataFrameobjects by default – they resolve throughPYCSAMT_API_VIEW, whose default backend already wraps intoAPIFrame/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 namedOccam2Drather than the module’s own.exe-suffixed constant on Windows, reporting a false “binary was not produced” error after a genuinely successfulmake.Fix
OccamMeshread 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, always0for pycsamt-generated meshes) and manages air layers internally, desynchronizing every subsequent mesh read. Confirmed against two independent, bundled reference mesh files and fixed inmesh.py(parsing, writing, and docstring).Fix
maybe_wrap_frame(..., api=True)ignored a custom wrapper configured viaconfigure_api_view(wrapper=...), always producing anAPIFrameinstead of the user’s callable’s own return type.Fix Removed stray root-level artefacts (
figures/, generated.npz/.pngoutputs,all_blocks.pycon) left over from example runs;all_blocks.pyconis now gitignored so it stops reappearing.
Changed#
Enhancement
TEM1DForwardnow delegates its Hankel/Fourier transform toempymod’s validated digital linear filters instead of a hand-rolled quadrature;empymodmoved 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 withMT3DAdapterviasolve_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, sinceMT3DAdapter’s cell budget cannot afford a uniform mesh reaching the same extent. An opt-incells_per_skin_depthconfig 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 drivepycsamt.models.mare2dem.SourceManagerfor MARE2DEM) with resilient retryingmakepasses. Exposed two ways: apycsamt build <modem2d|modem3d|occam2d|mare2dem>CLI subcommand group, and a rootMakefile(make modem2d,make modem3d,make occam2d,make mare2dem) for checkout-based contributors. See Compiling the External Solvers.Feature Triangular-mesh 2-D Maxwell FEM solver –
pycsamt.forward.maxwell.tri_fem2dadds a real, in-house P1 Galerkin TE/TM finite-element solver (TriFEM2DAdapter) on unstructured triangular meshes, alongside a new solver-neutralTriMesh/TriProblemcontract (pycsamt.forward.maxwell.contracts_tri) that sits beside the existing rectilinearMaxwellProblemcontract. Both analytic half-space and layered-earth benchmarks pass.Feature Graded triangular mesh generation –
pycsamt.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 adapter –
pycsamt.forward.maxwell.mare2dem.Mare2DEMAdapterwraps 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 data –
pycsamt.ai.training.dataset2d_trigenerates versioned 2-D AI-inversion training datasets on the new graded triangular mesh, resampling correlated resistivity fields onto triangle centroids and solving each realization throughTriFEM2DAdapter. Exposed asInv2DAgent(physics="mt2d_tri").Feature Reusable mesh-display utility –
pycsamt.api.mesh.draw_mesh()andPYCSAMT_MESHdraw structured-mesh cell boundaries with independent fill/edge alpha via a two-layer pcolormesh, with adapters forMaxwellMeshandGeologyGrid.plot_inversion_result_2dgained an opt-inshow_meshtoggle that uses it.Feature CSAMT frequency-schedule plot –
pycsamt.emtools.csumt.plot_frequency_schedule()visualizes which target depths survive the CSAMT band filter.New TEM/TEMAVG-to-EDI tutorial – Process 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) tutorial – Process 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 tutorial – 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 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) andpycsamt/cli/commands/build/never showed up ingit statusat all. Anchored every generic pattern of this kind to the repo root (/build,/dist/,/_build,/.build,/lib/,/lib64/,/parts/,/sdist/,/var/); confirmeddocs/build/,docs/_build/, and rootdist/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 compiledDeclaration_MPI/Sub_MPI/Main_MPI.f90, despite being unconditionallyused by several other files, nor passed-cppto strip their#ifdef MPIguards for a serial build; the 3-D Makefile was also missing a build rule forsg_spherical.f90. Fixed in both Makefiles.Fix ModEM model3d footer-detection heuristic –
pycsamt.models.modem.model3d._parse_model3d’s trailing centre/rotation-footer detection used a hardcodedlen(parts) <= 3threshold instead of comparing againstnx, silently truncating the resistivity grid for any model withnx <= 3. Fixed to compare againstnxinstead.Fix ModEM WS-format model file –
pycsamt.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; insteadModEm3DAdapter.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 command –
ModEm3DAdapter._build_commandomitted the third positional argument ModEM’s-Fforward mode requires (the predicted-data output filename). Without it,Mod3DMTprinted 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 benchmark –
pycsamt.forward.maxwell.mt3d.MT3DAdapter’slayered_earth_benchmarkfailure (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 strategyMT2DAdapteralready uses;supports_nonuniform_meshis nowTrue). Bothhalf-spaceandlayered-earthanalytic benchmarks now pass (~1.9% and ~3.5% normalized RMS respectively) on a 4,096-cell padded mesh.Fix Windows executable resolution –
pycsamt.forward.maxwell.external.resolve_executable()’ssearch_pathsfallback checked a literalPath(directory) / nameinstead of applyingPATHEXTthe way itsPATHlookup already did viashutil.which(), so a bare name like"Mod3DMT"never resolved to the real"Mod3DMT.exe"file on Windows. Fixed additively (tryshutil.whichfirst, 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 usingsearch_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 topography –
TriFEM2DAdapterassumed every receiver sat exactly atz = 0, so stations on real terrain were hard-rejected.topo_x_m/topo_z_mare now threaded end to end throughbuild_graded_tri_mesh(),dataset2d_tri, andInv2DAgent, and a new datum-shift-invariance benchmark checks the fix.Fix Mesh-display edge overlay on draped sections –
draw_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 surveys –
Inv3DAgentrendered a degenerate triangulated sliver for single-line survey geometries (e.g. a lone profile). New_profile_distance_km,_is_profile_geometry, and_smooth_depth_axishelpers detect this case and render a continuous, PCHIP-interpolated section instead.Fix 2-D AI-inversion station labels collided with the title –
Inv2DAgent’s triangular-mesh section figure usedfig.subplots_adjustto reserve headroom for station labels, whichPlotConfig’s defaultbbox_inches="tight"cropped away again at save time. Station names now useStationAxisStylelabel-thinning plus a points-basedax.set_titlepad, which survives the tight-bbox crop; markers changed from generic “Electrodes” to labeled “Stations”.Fix Phase-tensor alpha/beta swap –
pycsamt.emtools.tensor._angles_deg()had the skew anglebetaand rotation anglealphaswapped, with a sign flip, so every phase-tensor ellipse orientation and skew value reported bypycsamt.emtools.tensorwas wrong. Fixed; worked examples and figures regenerated with corrected numbers.Fix Phase-tensor pseudosection display –
plot_phase_tensor_psectionhad two real display bugs: the colorbar range forc_by in ("skew", "beta")was hardcoded to+/-skew_threshold(effectively always+/-3 deg) instead of the data’s own range, silently clipping any larger 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 surveys –
plot_strike_analysisalways 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 zippingrotation_angleagainst frequency-indexed data. Fixed at all three call sites inpycsamt.emtools.frequency/pycsamt.emtools.remove_noise.Fix TEM-derived EDI files silently lost their HEAD section –
TEMtoEDI._set_headassignedhead.lon/head.latdirectly from local, non-geographic survey coordinates, raising aLocationErrorthat a bareexcept Exception: passthen swallowed – dropping the entire>HEADsection 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 sections –
TEMtoEDInever wrote the>=DEFINEMEAS/>=MTSECTsections at all. A new_set_measstep builds them from placeholder Hx/Hy/Ex/Ey channels.Fix TEM-derived EDI resistivity was wrong by a factor of ~6e5 –
TEMtoEDIbuilt 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 conventionrho = 0.2 * |Z|^2 / f. Fixed toZ = 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.recordsattribute instead ofcoord.n_points.Fix EDI coordinates ignored ``>=DEFINEMEAS`` –
pycsamt.site.utils.get_coords()only readLAT=/LONG=from>HEAD; older BIRRP-processed files that leave those empty but carry a real position asREFLAT/REFLONGin>=DEFINEMEAS(true for every file in the bundled KAP03 SAMTEX survey) reported(nan, nan)for every station, silently disabling every coordinate-based plot. Added a fallback to>=DEFINEMEASper missing field.Fix Spectra DoF fallback ignored real metadata –
pycsamt.seg.ops.effective_dof_from_meta()treated a zerosegnum(the common case, since most real>=SPECTRASECTblocks never populate it) as an explicit zero-DoF count rather than “not provided,” silently skipping its own documentedavgt * bwfallback even when both were genuinely available –Spectra.to_Z(estimate_error=True)returnedz_err=Nonefor essentially every real file as a result.Fix Tipper nearest-period lookup assumed sorted frequency –
pycsamt.emtools.tf._nearest_idxcallednp.searchsortedagainst a raw, potentially unsorted per-station frequency array; two real KAP03 stations concatenate runs recorded at different sample rates and are genuinely non-monotonic. Fixed to sort internally in log-period space, matchingss.py’s analogous helper.Fix Induction-map axes mislabeled as metres –
plot_induction_mapandplot_induction_conventionhardcoded"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 plot –
plot_normalized_responsedrew station labels along the bottom axis and put short period at the bottom, unlike every other pseudosection in the package (includingplot_overprint_sectionin 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 ellipse –
phase_tensor_legenddrew 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"topycsamt.agents.Inv3DAgent, training the GCN inverter against real 3-D Maxwell forward solves generated fromdataset3d, reusing each survey’s actual station(x, y)coordinates (not a synthetic layout, unlikeInv2DAgent’sphysics="mt2d"). New constructor parametersgeology_grid_nx_ny/geology_grid_nz(training-grid resolution) andcells_per_skin_depth(opt-in frequency-aware solver-mesh resolution, defaultNonepreserves prior behaviour) and a genuine held-out recovery check reported inAgentResult.data["mt3d_recovery"].physics="mt1d"(tiled 1-D forward models + GCN spatial smoothing) remains the unchanged default.Enhancement Phase-tensor ellipse styling –
plot_phase_tensor_psectiongained an MTpy-stylenormalise_by="shape"sizing mode (constant major axis,s2/s1-ratio-only shape), acolor_mode="segmented"discrete color mode, andellipse_kws/cb_kwspassthrough.Enhancement Induction-vector plot labeling –
plot_tipper_hodogramsgained a period-band legend, andplot_induction_multiperiod_mapgained configurablexlabel/ylabelparameters 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 buildCLI, the rootMakefile, 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
pyconoutput 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, andModEm3DAdapterincluding arequires_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 theInv3DAgentphysics="mt3d"end-to-end path.Build Stricter line length –
[tool.ruff] line-lengthtightened from 97 to 79 characters, with a repo-wide reflow to match.Tests Solver build tooling coverage – added tests for the
pycsamt buildCLI 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 inversion – Map 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 guide – Map 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 reruns – Condition 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_branchto the tag name, which Netlify resolves asrefs/heads/<tag>and always fails since tags aren’t branches. The tagged commit is now force-pushed to a bot-onlydocs-deploybranch that Netlify treats as its production branch, so ordinarymasterpushes 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
--stemssubset like every other test in the file (one test dropped from roughly two minutes to six seconds). The shard is also now split intointerfaces-desktop(Qt/PySide6) andinterfaces-cli(no Qt) for parallelism.Build New dependencies –
triangle>=20220202(Shewchuk’s mesh generator, core dependency, backs the graded triangular mesh – no compiled external binary required) andxlrd>=2.0(docsextra, reads legacy.xlsTEM coordinate tables).Docs Deepened ``strike``/``spectra``/``tensor``/``tf`` user guide pages – Geoelectric Strike, Cross-Spectra Analysis, Phase Tensor And Impedance Tensor Tools, and Transfer Functions And Tipper Diagnostics converted to real, byte-verified
pyconoutput 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 todocs/scripts/behindcode-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 objectiveL = 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()andMaxwell2DDatasetConfig, generating spatially correlated 2-D geological realizations and solving them withMT2DAdapterinto a versioned, realization-split, resumable-cache dataset.API Change Inv2DAgent physics mode – added
physics="mt2d"topycsamt.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()gainedlambda_x,lambda_z, andlambda_tvstaged-loss weights (PyTorch backend only).API Change Global station-ordering policy – added
pycsamt.api.configure_ordering(),pycsamt.api.reset_ordering(),PYCSAMT_ORDERING, andpycsamt.api.SiteOrderingConfig. Configuremode="auto"once and APIs usingpycsamt.emtools.ensure_sites()inherit it.API Change Sites ordering API – added
pycsamt.site.Sites.ordered()withauto,chainage,input, naturalstation,latitude, andlongitudestrategies, pluspycsamt.site.Sites.orderingdiagnostics.API Change Transactional metadata API – added
pycsamt.site.metadata.SiteMetadataEditor,pycsamt.site.metadata.MetadataChange,pycsamt.site.metadata.rename_sites(),pycsamt.site.metadata.update_metadata(), andpycsamt.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(), andpycsamt.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/INFOchanges; 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 guesses –
autovalidates 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 withMT2DAdapter(receivers must sit exactly atz=0) and produced spurious 393x errors despite an apparently converged solve.Fix Depth-zero float precision –
Maxwell2DDatasetConfig’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 1 –
pycsamt.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 averaging –
pycsamt.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.Maxwell2DDatasetConfiggenerates and trains on both the TE-mode (zxy) and TM-mode (zyx) response by default again.Fix Field-survey impedance units –
pycsamt.ai.domain_gap.survey_fit.survey_data_from_sites()now convertszfrom the EDI-native[mV/km]/[nT]field convention to the SI conventionSurveyDatadeclares, instead of copying it unconverted.
Changed#
API Change
pycsamt.ai.domain_gap.willy_fitis renamed topycsamt.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=Noneandpycsamt.emtools.ensure_sites()order_by=Nonenow mean “use the global policy”. Explicit arguments still override it; selectinputto 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
pycontranscripts, 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. Usepycsamt.site.Sites.rename()for explicit station-name mappings.
Docs & tooling#
Docs Code of Conduct and Netlify attribution – added a project Code of Conduct and a “powered by Netlify” link in the footer of every documentation page, meeting the requirements of Netlify’s Open Source Plan.
Build Release-gated docs deploys –
pycsamt.orgnow rebuilds only on avX.Y.Ztag push (a GitHub Actions workflow calling a Netlify build hook), rather than on every commit tomaster. The previous per-commit pattern exhausted the hosting team’s free-plan credits and suspended the site.Docs AI-inversion user guide, written in full – Architecture roadmap, Canonical data contracts, Reproducible experiment configuration, Solver-neutral Maxwell contracts, 2-D Maxwell training-data generation, Domain-gap and noise simulation, Loss functions for scientific inversion, and Recovery, residual, and OOD diagnostics are now complete guides covering
pycsamt.ai.data,pycsamt.ai.domain_gap,pycsamt.ai.experiments,pycsamt.ai.geology,pycsamt.ai.losses,pycsamt.ai.training.dataset2d,pycsamt.ai.validation, andpycsamt.forward.maxwellend to end, with real, externally captured code output, generated figures, and labeled equations throughout. Correlated geological priors remains a stub.Tests AI-inversion regression coverage – added tests for
pycsamt.ai.losses,pycsamt.ai.validation,pycsamt.ai.training.dataset2d(including analytic half-space regression checks for both the TE- and TM-mode response),pycsamt.forward.em2d._assemble_tm()’s interface-averaging coefficients, the staged-lossEMInverter2Dfit path, and theInv2DAgentphysics="mt2d"end-to-end path.
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 pipeline —
pycsamt.pipelinechains catalogued steps (QC, notch, band selection, static shift, rotation, …) into reproducible runs with reports, presets, and YAML round-tripping.Feature EM analytics toolbox —
pycsamt.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 underpycsamt.models, and deep-learning inversion inpycsamt.ai(1-D/2-D/3-D nets, ensembles, uncertainty).Feature Geological interpretation —
pycsamt.interpsupersedesgeodrill: 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 telemetry —
pycsamt.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
AgentMasterfront 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
Zobject andEDICollectionnow drive every processing and inversion pipeline.
Fixed#
Fix Encoding-safe figure saving — saving a figure or writing the plot config no longer raises
UnicodeEncodeErroron 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 previews —
AgentCoordinatordry-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=Noneno longer break thehtml-page-contexthook ('NoneType' object has no attribute 'pop').
Removed#
Breaking
pycsamt.geodrill— removed; usepycsamt.interp.Breaking
pycsamt.ff— removed; usepycsamt.emtools.Breaking
pycsamt.viewer— removed; use module-level.plot()methods on result objects.Breaking
pycsamt.modeling— removed; usepycsamt.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.pyremoved.
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 |
|---|---|
|
|
|
|
|
|
|
|
|
Module-level |
|