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.6.4 Feature Fix Enhancement Docs Build Tests#

Released 2026-10-02.

A new-surfaces release. pyCSAMT Format Studio (pycsamt.app.converter) is a new standalone, freezable app dedicated to file-format conversion, backed by six new pycsamt format CLI sub-commands and a newly public pycsamt.format.convert_engine. A second new CLI group, pycsamt airborne, adds info/diagnose for ZTEM/MobileMT/AFMAG datasets. The desktop suite gains five rebuilt studios (Inversion, Interpretation, QC, Airborne, TDEM), a new Edit menu with undo history and station/point/frequency editors, and a PCSF 3-D view that hands its scene over to Map View; it now ships as a Windows setup .exe and a self-extracting Linux .sh installer. Agent Master carries conversation context across turns, hands work to and from the desktop, checks run preconditions, can cancel a request, and has an answer benchmark with Docker-backed script execution. More than twenty real bugs are fixed, including two EDI parsing bugs, silently broken pycsamt convert/transform j/invert results paths, and frequency regridding that invented values outside a station’s band.

Added#

  • Feature ``pycsamt airborne`` – a new CLI command group for airborne EM datasets (ZTEM, MobileMT, AFMAG/AirMt): info (dataset or single-site summary) and diagnose (auto-detects the technology and builds its canonical diagnostic table – ZTEM total-divergence, MobileMT admittance, or AFMAG/AirMt tilt). Operates on already-decoded EMTF-XML files or directories of them via pycsamt.airborne.site.ensure_asites(); pyCSAMT still has no raw-vendor-file reader for airborne systems.

  • Feature pyCSAMT Format Studio (pycsamt.app.converter) – a new, small, freezable PySide6 application dedicated entirely to file-format conversion, with no survey-loading or processing features. Nine tools: Inversion (Occam2D/ModEM 3-D/MARE2DEM/AI array) to PCSF/PCSM, PCSF ⇄ PCSM transcoding/validation/info, EDI ⇄ EMTF-XML, PCBH/PCGL/PCGS/PCPT document builders, a mixed batch queue, and persisted defaults. Launch with pycsamt-converter / python -m pycsamt.app.converter, or build a standalone double-click binary with the new packaging/pyinstaller/pycsamt_converter.spec. Every page is a thin widget around a plain, Qt-free function in pycsamt.app.converter.jobs that reuses exactly the same engine as the pycsamt format CLI.

  • Feature Six new pycsamt format sub-commands: build-pcbh (CSV / relational CSV directory / XLSX / LAS 2.0 log -> a PCBH borehole document), build-pcgl (CSV of named resistivity units -> a PCGL geology legend), build-pcgs (planar/linear measurement and fault-trace CSVs -> a PCGS structural-evidence document), build-pcpt (CSV/XLSX of named points -> a PCPT points-of-interest document), and edi-to-xml/xml-to-edi (lossless SEG EDI <-> EMTF-XML conversion, one file or a whole directory). These are the CLI face of the same builder functions the new converter app uses, and were previously only reachable from Python.

  • Feature Desktop studios. The Inversion (windows/inversion/; Qt-free engines for Occam1D/2D, ModEM 2-D/3-D and MARE2DEM with a streamed run console), Interpretation (windows/interp/; any PCSF/PCSM file or inversion run folder, Geology/Structure/Hydrology/ Monitoring/Uncertainty/Diagnostics tabs, pinned-figure library), QC (line scope, panel grid, pass/warn/fail summary), Airborne and TDEM windows are rebuilt as studios, and the Advanced Tools studio builds each plot’s options from its signature.

  • Feature Edit menu in the desktop suite: Undo / Redo / History / Revert over every survey change, Save Edited Survey (EDI or EMTF-XML), Find Station, a Station Editor (coordinates with EPSG, Excel paste and CSV import; rename; lines; EDI header metadata), a Point Editor (mask, delete, interpolate, restore, static shift; edits logged to each station’s EDI INFO), and frequency/tensor tools (trim, regrid, decimate, align, dedupe, fill gaps, rotate, rotate to strike) with a per-station preview.

  • Feature PCSF 3-D view hand-off. The PCSF 3-D window gets a compact toolbar, a Controls panel, and topography read from the file (including rasters); “Open in Map View” passes the whole scene to pycsamt-mapview --state. The desktop Map panel also draws PCSF/PCSM depth and resistivity maps.

  • Feature Agent Master: conversation context carried across turns, desktop ⇄ web hand-off, run preconditions checked before execution, request cancellation, and an animated pyCSAMT logo while the agent works.

  • Feature Answer benchmark for the assistant (pycsamt/assistant/evals/answers.py, runner.py and the answer_suites/ set), plus Docker-backed execution of generated scripts against fixtures (pycsamt.assistant.tools.fixture_execution, image recipe in packaging/agent-fixture/).

  • Feature A shared colormap catalogue, pycsamt.api.colormaps, used by every desktop plot picker.

Fixed#

  • Fix pycsamt.seg.heads.Head now normalizes a field written as the literal string "None" (e.g. DECLINATION=None, as some EDI writers stamp an unset value instead of omitting the key) to a real Python None while parsing, matching how write() already treats that string as equivalent to absent. Previously such a field reached its setter as the string "None" and failed converting it to a number.

  • Fix EDI resistivity/phase-error blocks (RHOxx.ERR/PHSxx.ERR) written from an EMTF document (pycsamt.emtf.converters.edi.emtf_to_edi()/write_edi) came out as a literal 0.000000E+00 for every frequency instead of the real, error-propagated uncertainty – misrepresenting “not computed” as “zero uncertainty” – because the impedance error array was attached to pycsamt.z.Z’s private _z_err after construction, bypassing the resistivity/phase error propagation the public z_err setter triggers. The converter now uses that public setter. Separately, pycsamt.seg.edi’s writer now defaults a genuinely absent error to NaN rather than zero, so any future case that still lacks one correctly round-trips to the SEG-EDI 1.000000E+32 EMPTY sentinel instead of a misleading exact zero.

  • Fix pycsamt convert on .avg and J files silently failed (a nonexistent AVGFile import; J files produced an EDI with DATAID='UNKNOWN' and no frequencies). Both now use the working AVGtoEDI/JtoEDI transformers. pycsamt transform j raised TypeError on every single-file or directory conversion.

  • Fix pycsamt invert results reported RMS as None for Occam2D and every field as null for ModEM; pycsamt config set agent.model was a silent no-op; the ModEM backend leaked config fields into InputBuilder.build() and crashed as “ModEM preparation failed”.

  • Fix pycsamt.emtools.frequency: regridding no longer invents values outside a station’s measured band (NaN instead), and decimate_step keeps z/z_err/freq aligned instead of emptying every station.

  • Fix The pipeline’s full_processing no longer wipes the data (descending-frequency interpolation, phase-tensor β threshold, NaN strike).

  • Fix plot_confidence_profile handles several lines (no zig-zag across line boundaries); the ZTEM map draws collinear stations; TDEM depth sections no longer show slivers; pycsamt.map.volume no longer collapses a single-line fence to a zero-width box.

  • Fix MARE2DEM merge deduplicated receiver names separately from their positions, corrupting station names in merged .emdata files.

  • Fix AI: PINN TF station fits used mean instead of nanmean (one missing frequency made the whole fit NaN); EMImputer and the joint inverter failed save/load round trips.

  • Fix Occam1D failed in the installed desktop app (“stream must provide a callable write method”): a windowed build has no sys.stdout. It now falls back to a silent stream, and the frozen entry point gives stdout/stderr a sink.

  • Fix Kind-1 AVG files with a skpStation header (older pyCSAMT exports) were rejected as unrecognised; an empty borehole CSV now reports csv.empty; the trial’s days remaining rounds part-days up.

  • Enhancement The PCSF 3-D view creates its web engine on first show, not at desktop start-up.

  • Fix Smaller fixes: ensure_head_coords returned a stale head on its fallback path; _dedup_freq’s list and NumPy paths disagreed below 1 Hz; Config.writers() broke on pandas without DataFrame.to_gbq; converter Browse buttons crashed on a second click; Map View borehole export no-op’d on an empty draft; duplicate Dash ids in the Borehole Builder; dark desktop menus showed black icons.

Changed#

  • Enhancement ``pycsamt.format.convert_engine`` – the “any source -> PCSFModel” dispatch engine behind pycsamt format convert (detect, resolve_target, build_model, write_model, model_report) is promoted from a private helper buried in pycsamt.cli.commands.format._base into a public, UI-neutral module, so the new converter app (and any other second consumer) can reuse it without reaching into CLI internals. The CLI’s own behaviour, options, and output are unchanged.

Docs & tooling#

  • Docs Added Airborne Commands, documenting the new pycsamt airborne command group – full option tables and real captured output from the bundled ZTEM/MobileMT/AFMAG sample surveys, including the genuine “N flight lines detected, pass --line” error a multi-line ZTEM survey raises without one.

  • Docs Added Format Converter, a complete guide to the new standalone converter app: install/launch, compiling a frozen build, the window chrome, and every one of its nine tool pages with a real screenshot, a field-by-field walkthrough, and the equivalent CLI command – Format Commands is cross-referenced for convert/detect/info/ validate’s existing option reference rather than duplicating it, and the six sub-commands documented nowhere else (edi-to-xml/xml-to-edi/build-pcbh/build-pcgl/build-pcgs/ build-pcpt) get full treatment with real captured output. Applications and Overview now count five applications instead of four.

  • Build Every downloadable script under docs/scripts/ (61 files) now starts with the standard #!/usr/bin/env python / # Author: / # License: header used across the rest of the package, since each one is meant to be downloaded and run standalone from a documentation page’s code-dropdown.

  • Tests Added pycsamt/cli/tests/test_cmd_format_build.py (the four new build-* sub-commands) and pycsamt/cli/tests/test_cmd_airborne.py (airborne info/diagnose), plus session-scoped fixtures for the real bundled ZTEM/MobileMT/AFMAG sample directories in conftest.py; both suites skip automatically when their sample data is absent rather than mocking it.

  • Build Desktop installers: a Windows Inno Setup .exe (packaging/inno/build_installer.ps1) and a self-extracting Linux .sh (packaging/linux/build_installer.sh), both built from the new packaging/pyinstaller/pycsamt_desktop.spec, which bundles the package data the frozen app needs (gis/epsg.npy, logging config, borehole templates, solver sources). The PyInstaller spec files were previously excluded by a generic *.spec ignore rule and are now tracked.

  • Docs New Agent Master pages: Conversation context and stopping work and Evaluating answers and scripts.

  • Tests Coverage pass over 95 files that were below 85 % (all now at about 85 % or higher); the CI desktop shard now runs the converter tests; three failures in the py3.9–3.13 matrix fixed (xdist registry grouping, pandas to_gbq, pandas 3 str dtype).

Compatibility#

No public names or required parameters were removed or renamed. Everything new – the converter app, the six format build-*/edi-to-xml/ xml-to-edi commands, pycsamt airborne, and pycsamt.format.convert_engine – is additive. pycsamt format convert’s own behaviour is unchanged; only the internal module its Click wrapper delegates to moved. The desktop studios, Edit menu and Agent Master features are additive as well. One behaviour change: frequency regridding in pycsamt.emtools.frequency now returns NaN outside a station’s measured band where it previously extrapolated.


2.6.3 Fix Enhancement Tests Docs#

Released 2026-09-14.

A maintenance and API-hardening release covering agent workflows, SEG/Jones/EDI parsing, impedance error handling, Zonge construction and coordinates, pandas compatibility, and broad regression coverage.

Changed#

  • Enhancement Agent workflow validation recognises the supported mare2dem, pinn_inversion, hybrid_inversion, code_gen, and denoise workflow names.

  • Enhancement Removed unused duplicate private implementations from the Jones block parser, SEG xarray exporter, Z determinant code, Zonge variance helpers, and pycsamt.agents.Inv3DAgent (a ~140-line topography-resolution helper superseded by the shared pycsamt.agents._topography module). No public API was removed.

  • Enhancement pycsamt.api.util.round_numeric_values() prefers pandas.DataFrame.map() on modern pandas and retains the legacy fallback for older releases.

Fixed#

  • Fix pycsamt.agents.AgentResult.failure() can preserve warnings, and pycsamt.agents.IoTFieldAgent no longer drops warnings when input resolution fails.

  • Fix Signed IoT manifests pass their signing key through the write path.

  • Fix SEG and Jones parsers tolerate unresolved Windows globs and invalid path strings long enough to report them through their normal parse-error contract.

  • Fix Missing or malformed EDI TROT data falls back to the impedance rotation angle instead of being silently zero-padded.

  • Fix pycsamt.seg.heads.Head coordinates can be cleared with None, and Heads rejects a missing path before calling pathlib.Path.

  • Fix Jones collection n_freq summaries support multi-element NumPy arrays, and the verbose-regex Jones banner parser works under strict Python regex validation.

  • Fix SEG time-series xarray export builds its time index with the correct floating dtype.

  • Fix pycsamt.z.utils.invert_z() and pycsamt.z.Z.remove_distortion() consistently translate singular inversion failures to pycsamt.exceptions.ZError.

  • Fix Directly constructed pycsamt.zonge.survey.Topography and Station objects initialise all inherited coordinate/frame state.

  • Fix pycsamt.zonge.utils.load_avg() uses the documented utm_zone argument when converting latitude/longitude to UTM.

  • Fix pycsamt.api.FlexDict reliably restores attribute access, including after unpickling.

  • Fix pycsamt.api.util.format_text() cannot loop indefinitely when its key exceeds the wrap width, and format_dict_result() accepts empty mappings.

  • Fix The private _collect_rho helper behind pycsamt.agents.StaticShiftAgent no longer replaces a whole station’s before/after apparent-resistivity column with NaN just because its frequency count differs from the first station loaded (the normal case in real surveys); it now interpolates onto the reference grid in log-frequency space.

  • Fix pycsamt.emtools.qc._snr_rows() no longer raises “Mean of empty slice” for a frequency row that is a genuine all-NaN data gap.

  • Fix pycsamt.agents.ReportAgent no longer tries to copy a figure onto itself (and warn about it) when the producing agent already saved it under the exact destination name in the run’s shared output_dir.

  • Fix pycsamt.api.util.format_iterable() and pycsamt.zonge.utils.split_by_station() detect numeric pandas.Series/columns with pandas.api.types.is_numeric_dtype() instead of a dtype string comparison / numpy.issubdtype(), both of which raised TypeError on pandas configurations that infer a string extension dtype.

  • Fix pycsamt.zonge.base.AVGComponentBase.to_json() no longer calls the pandas 2.x-removed pd.io.json.dumps.

Tests#

  • Tests Expanded regression coverage across agents, airborne adapters, API utilities/views, EMTF, emtools, I/O, Jones/SEG, metadata, inversion models, Z, and Zonge.

  • Tests Twenty-one pycsamt.agents modules below 80% branch coverage — static-shift/tensor-rotation correction, frequency decimation, denoising, IoT field monitoring, the 2-D/3-D/joint/PINN/hybrid AI inverters, the physics-backend/Occam2D/MARE2DEM inversion drivers, intent routing, the AgentMaster façade, the processing pipeline, the workflow orchestrator, and forward modelling/metrics/plotting/reporting — now reach 80–100%, several new tests exercising real (non-mocked) forward solves and a full non-dry-run orchestrator run including provenance-file writing. Package-wide pycsamt.agents coverage rose from 83% to 92%.

  • Tests Added focused rendering and edge tests for pycsamt.emtools.plot_response_overview(), CSUMT scheduling, and impedance plotting. Focused coverage reaches 94% for overview, 83% for csumt, and 82% for impedance.

  • Tests Updated a glob-dispatch test for Python 3.13’s changed Path.rglob("**") semantics, and skip two Windows-only ctypes.windll tests on non-Windows CI runners instead of failing at import time.

Documentation#

Compatibility#

No public names or required parameters were removed. The optional warnings parameter on pycsamt.agents.AgentResult.failure() is backward compatible. Singular tensor failures now consistently use the documented pycsamt.exceptions.ZError boundary.


2.6.2 Feature New Fix Docs#

Released 2026-09-11.

An pycsamt.ai.processing release: four new learned tools – gap filling, uncertainty calibration, distortion-regime triage, and a raw-time-series denoiser – and the subpackage’s first full user guide, covering all eight tools. Building real examples against real data for the first time surfaced four bugs in already-released code, fixed here.

Added#

  • New Learned gap filling – pycsamt.ai.processing.imputer.EMImputer reconstructs genuinely missing (station, frequency) cells from the observed ones via masked reconstruction (BERT / image-inpainting style): every training step hides a random fraction of the truly-observed cells and computes loss only there, so training data needs no real missing-data examples with known ground truth. apply() writes reconstructed values only into cells genuinely missing on the input. RMSE 0.32 vs. a 0.71 naive zero-fill baseline on real gapped data.

  • New Learned uncertainty calibration – pycsamt.ai.processing.uncertainty.UncertaintyCalibrator regresses a fractional error \(|z_\text{err}|/|Z|\) from EMQCScorer’s own per-cell diagnostic features, then apply() rescales (not replaces) each cell’s existing z_err toward the calibrated level. snr was dropped from the feature set before release – it and the training target are built from the same pooled terms and are near-perfectly correlated, letting the network learn to invert one input column rather than a genuine relationship (a deceptive 0.994 correlation collapsed to an honest 0.60 once removed).

  • New Distortion-regime triage – pycsamt.ai.processing.distortion.DistortionTypeClassifier labels each station clean / static-shift-only / distorted and routes it toward pycsamt.emtools.ss or pycsamt.emtools.gb rather than re-solving the physics – the same role DimensionalityClassifier plays for dimensionality. Uses a spatial resistivity-trend deviation in place of Groom-Bailey’s own gain column, which the decomposition’s normalisation forces to exactly 1.0 every fit.

  • New Raw time-series denoising (MMF-SVM-K-SVD) – pycsamt.ai.processing.tsdenoise.TimeSeriesDenoiser suppresses strong, non-stationary interference directly on a TSData record, TSData-in / TSData-out via apply(), before ts_to_spectra() Fourier-transforms a contaminated segment. Three stages: mmf_split() (mathematical morphological filtering splits each channel into a low-frequency envelope and a high-frequency residual); SignalQualityClassifier (a linear-kernel SVM classifies windows of that residual clean/noisy from four complexity features – compute_entropy_features(): sample/fuzzy/approximate entropy and box-counting dimension – trained from generate_synthetic_library(), auto-calibrated to each channel’s own robust background amplitude rather than a fixed scale); and KSVDDenoiser (K-SVD dictionary learning denoises only the flagged windows, self-supervised from patches of the window itself). snr_db() and time_domain_ncc() reproduce the paper’s own validation metrics – 16.9 dB SNR gain and NCC 0.37 -> 0.95 on a synthetic record with a known clean reference; a real ~580-unit burst in the bundled data/MT/TS/kap103as.ts recording suppressed to ~4.8, near the channel’s own background.

  • New A ``history_`` property on every network-based estimator (EMDenoiser, AnomalyDetector, DimensionalityClassifier, and now EMImputer / UncertaintyCalibrator), recording per-epoch train/validation loss, plotted by the new shared plot_training_history().

  • New 23 new plotting functions across pycsamt.ai.processing.plot – one page’s worth for each of the eight tools: 3 for anomaly detection, 3 for denoising, 2 for dimensionality classification (plus a strike rose), 4 for gap filling, 3 for uncertainty calibration, 3 for distortion triage, and 3 for time-series denoising (plot_ts_denoise_mmf_split(), plot_ts_denoise_segments(), plot_ts_denoise_summary()).

Documentation#

Fixed#

  • Fix ``prepare_z_features`` silently corrupted extracted features for most stations in a survey – its frequency-grid interpolation called numpy.interp() without sorting the source frequency array first; real EDI/MT frequency arrays are conventionally stored high-to-low, and numpy.interp returns wrong, non-NaN values with no error for a descending xp. Every station except whichever one happened to supply freq_ref (default: the first site) had its EMDenoiser / AnomalyDetector features silently flattened toward the reference station’s own curve. Fixed by sorting the source frequency array before interpolating.

  • Fix A random-forest fallback could crash, or silently mis-map its probabilities, on a rare class – when neither PyTorch nor TensorFlow is installed, DimensionalityClassifier’s RandomForestClassifier fallback returns one probability column per class present in the training labels, not one per n_classes; with a genuinely rare class, the returned array can be narrower than the array the code assigned it into, raising ValueError. Fixed by mapping each returned column to its actual class index explicitly, in both DimensionalityClassifier and the new DistortionTypeClassifier.

  • Fix ``plot_qc_scores`` crashed on ``threshold_lw=0.0`` with a multi-profile dict input – a zero-width dashed legend handle was still built for the hidden threshold line, which crashes Matplotlib’s legend renderer. Fixed by guarding the handle the same way as the line itself.

  • Fix ``plot_qc_scores`` dropped custom ``station_labels`` for any multi-profile comparison, silently falling back to plain integer ticks. Fixed to apply per-profile labels when their length matches.

Compatibility#

Every addition is backward compatible. The first fix above changes values, not signatures: prepare_z_features() (and any EMDenoiser / AnomalyDetector model trained on its output) will produce different, now-correct numbers for a survey where the first site’s frequency grid was not already the shared reference – retrain rather than reuse a checkpoint trained before this release if that applied to your data. Nothing else changes behaviour for existing callers.


2.6.1 Feature New Fix Docs#

Released 2026-09-10.

A magnetotelluric-visualisation release: two new publication-style plotters, a phase-tensor colour system in pycsamt.api, a bundled and fully cited Broken Hill 3-D MT survey with an end-to-end tutorial, and three bugs surfaced while building them.

Added#

  • New MTPy-style apparent-resistivity / phase pseudo-section – pycsamt.emtools.PlotResPhasePseudoSection and the plot_res_phase_pseudosection wrapper. Apparent resistivity above phase, one column per impedance component (drawn in the order given, or auto-detected), one stacked [resistivity, phase] row-pair per site group. station_side puts the station axis – names plus the inverted-triangle markers from PYCSAMT_STATION_RENDERING – on "top" (default), "bottom", or "none"; res_phase_ratio sets the panel split; phase_range takes the "0-90" / "-45-45" / "-90-90" / "-180-180" / "auto" presets or a tuple; share_period=False gives each stacked line its own period window (for an AMT line above an LMT line); plus panel_labels, grid, share_scales, log_res / log_period, and an axes= grid to draw into. The period grid ends are set from the half-decade bins where a fraction of the line’s stations carry data, so one file’s stray band cannot stretch the axis across empty decades.

  • New Multi-frequency phase-tensor map grid – pycsamt.emtools.plot_phase_tensor_map_grid() draws phase-tensor ellipses and real-induction (Parkinson) arrows on a geographic grid of 1–6 panels, one per frequency or period, with shared ellipse scaling, one shared skew colour bar, optional station labels, a reference ellipse, an optional topography backdrop, and lat-corrected panel aspect. plot_phase_tensor_map itself gains ellipse_scale, topography (station elevations, a CSV, or a dict), show_colorbar, and abs_skew.

  • New Phase-tensor skew colour system – pycsamt.api.style registers pt_skew / pt_skew_r (blue–white–red diverging) and pt_skew_abs / pt_skew_abs_r (white–red sequential) with Matplotlib, and PhaseTensorEllipseStyle now auto-selects them per c_by (skew / |beta| / alpha), so every phase-tensor plot shares one MTPy-like skew ramp. The colour is configurable through pycsamt.api.PYCSAMT_STYLE.

  • New Geo-referenced ModEM depth-slice and conductance maps – pycsamt.models.modem.plot.PlotDepthMap is reworked from a raw-mesh slicer into a publication plotter: it crops to the station footprint by default (extent), masks outside the survey hull, renders as mesh / gouraud / contourf / image, and adds a depth-integrated layer-conductance mode (quantity="conductance"), contours, overlays, profile lines, a scale bar, a north arrow, a graticule, and configurable station markers, on one shared or per-panel colour bar.

  • New Broken Hill 3-D MT dataset and tutorial – data/MT/broken-hill/ bundles the 21-station AlQahtani et al. (2026, JGR Solid Earth, CC-BY) magnetotelluric survey and the published ModEM inversion, with a README.md and CITATION.cff. A new tutorial, Broken Hill: MT Field Data To A 3-D Resistivity Model, walks the whole path from EDI loading and QC through dimensionality, a ModEmConfig setup, the published InversionResult, and the depth-slice / conductance / section figures, and is regenerated and byte-verified by docs/scripts/generate_tutorial_broken_hill.py.

Changed#

  • Enhancement PlotDepthMap accepts the ncols and show_names keyword aliases used by the web app, alongside the canonical n_cols / station_labels.

Fixed#

  • Fix EDI files with a placeholder ``DATAID`` collapsed a whole survey onto one site – EMpower (and WinGLink) write a literal None / NULL into DATAID when the station id is unset; pycsamt.seg.EDIFile.station returned that string verbatim, so read_edis on such a folder keyed every file to one sounding. Placeholder ids ("", none, null, na, nan, -, unknown, …) are now treated as missing and the filename stem is used instead; the fast header parser in pycsamt.seg.cbase applies the same rule.

  • Fix ``InversionResult`` mis-read a short project stem as an iteration number – the pattern matching ModEM’s <stem>_NLCG_NNN counter also matched a stem like BH_31, so BH_31.dat was taken as “iteration 31” and the observed and predicted data files were swapped. ModEM writes the counter with %03d; the pattern now requires at least three digits.

  • Fix Two ``emtools`` docstrings broke the Sphinx build – pycsamt.emtools.ensure_sites() and pycsamt.emtools.smooth_rho_phase() wrapped a NumPy-style parameter type with a \ line continuation whose remainder sat at source column 0, which defeats inspect.cleandoc de-denting and makes Napoleon emit the Parameters / Examples headers raw (Sphinx CRITICAL: Unexpected section title). Both types are now single-line.

Compatibility#

Every addition is backward compatible and every changed signature is additive. pycsamt.seg.EDIFile.station now returns the filename stem instead of "None" / None for a file whose DATAID is a placeholder – code that special-cased that string should be reviewed. InversionResult may pick a different file as data_obs for a run folder that contains a non-iteration .dat whose stem ends in digits (e.g. BH_31.dat); the newly chosen file is the real input, not a response file.


2.6.0 Feature New#

Released 2026-09-07.

Added#

  • New Post-inversion data-fit diagnostics for ModEM – pycsamt.models.modem.plot.PlotDataFit and pycsamt.models.modem.plot.PlotMisfitMap.

    PlotDataFit is a publication-style observed-vs-model response panel: each station spans one column group of impedance components (apparent resistivity above, phase below, 2:1, no gap), observed data as error-bar markers on a dotted guide line, the model response as a solid line, per-panel component RMS in each title and the pooled per-station RMS in each header, coloured by mode (TE = Zxx/Zxy, TM = Zyx/Zyy) with one shared legend. Only components that actually carry unmasked data are drawn, so an off-diagonal file collapses to two columns per station instead of four half-empty ones. Two or more results can be stacked as labelled bands (a), b), …) to compare independent inversions.

    PlotMisfitMap is the spatial companion: every station drawn at its map position (geographic lon/lat when the data file carries it, model-grid easting/northing otherwise) and coloured by its pooled RMS normalised residual, with an optional per-component small-multiple layout. Survey lines are detected from the station-name prefix and labelled (show_line_labels, line_label_rotation); rotate_deg spins a narrow north–south survey into a local-kilometre frame that fills the page, and aspect / name_rotation are exposed too. The whole-survey RMS in the title reproduces the value ModEM’s own NLCG log reports.

    Both accept an InversionResult or a bare data_obs / data_pred pair of ModEmData objects, and are exported from pycsamt.models.modem.

  • New Boreholes in Map View – a dedicated Boreholes section and a Borehole Studio modal that imports, builds, and edits PCBH boreholes, then inserts them – correctly projected and depth-datum matched – into the 3-D block / iso-surface / fence / depth-slice scene and as collar markers on the 2-D basemap.

    • New Spreadsheet import – pycsamt.format.borehole.inspect_workbook() and pycsamt.format.borehole.boreholes_from_xlsx() turn a messy field workbook (merged multi-row header, a Rock name column, a From/To depth pair, no coordinates) into a canonical PCBHDocument: sheet + header-row selection, alias-based column mapping (shared with the combined-CSV importer via the new assemble_interval_document() core), an external per-hole or shared collar source, and a strict/permissive report. PAGEO-paper/boreholes/ ships the paper’s dataset built by this path: baohuashan_l22.pcbh.json – the three L22 confirmation holes ZK221 / ZK222 / ZK2203 (Kouabena et al. 2025), ZK2203 with its full 172-interval core log (193–1101 m) and ZK221/ZK222 with the published host lithology and Cu–Mo mineralization intervals – plus baohuashan_l26_context.pcpt.json (CK261, CK262, H01), collars.csv, and a reproducible build_boreholes.py.

    • New Scene alignment – pycsamt.map.geometry.SurveyFrame / survey_frame() project any WGS84 point into the survey’s local along/cross-strike frame (the same one the 3-D volume builders use), and pycsamt.map.borehole_align.align_boreholes_to_scene() walks each desurveyed trajectory into scene coordinates – collar CRS reprojection, a surface / collar_z / zero depth datum, a depth-window clip, and an inside / edge / outside relation.

    • New Shared Plotly adapters – pycsamt.app._borehole gains strip_log_figure (the side-by-side depth-column “linear” view), scene_borehole_traces (Mesh3d tubes or fat poly-lines), collar_map_markers, and a builder-draft bridge, all reused by the Map View studio and the web Borehole Builder page.

    • New Column-mapping in the Studio – the spreadsheet import tab shows the sheet’s headers as Borehole id / Lithology / Depth from / Depth to dropdowns (auto-guessed by alias, overridable), so a merged multi-row header imports without a script.

  • New PCPT – points and targets (pycsamt.format.pointset) – a tiny .pcpt.json format for drill targets, planned collars, samples, and anomalies, importable from JSON / CSV / .xlsx (read_points, points_from_csv, points_from_xlsx). Map View draws them as kind-coloured markers on the 2-D basemap and as depth-stemmed markers in the 3-D scene, independent of any borehole, and both PCBH and PCPT stores now travel in the session snapshot.

  • New Geology section in Map View – a fourth rail section, Geology, below Boreholes, and a Geology Studio modal that build and preview both a resistivity-to-geology legend and field structural evidence before applying either to the 3-D scene; the 3-D options accordion’s Interpretation item keeps only the two Apply switches.

    • Legend: manually, imported from a new .pcgl.json document or CSV (pycsamt.format.geology, GeologyLegend), or auto-suggested as log-spaced bins covering the resistivities actually in the current view – applied as a hard-stepped discrete colour scale (pycsamt.map.styles.geology_colorscale()) in place of the continuous ramp. A 3-D-toolbar Legend style picks a non-overlapping on-canvas swatch legend (pycsamt.map. styles.geology_legend_shapes_annotations(), positioned by row index so close-valued or numerous bands never crowd, default) or Plotly’s own colourbar with named ticks; a Legend button shows/hides either without losing plot width. A “Sync colours with loaded boreholes” action (pycsamt.app._geology. sync_legend_colors_with_boreholes) overwrites a row’s colour with the exact-name-matching borehole lithology’s colour, so the same unit reads as the same colour in both overlays. PCGL is a thin, versioned wrapper around the existing pycsamt.geology.lithology.RockDatabase classifier, not a parallel format.

    • Structure: fault traces and planar/linear measurements, imported from a new .pcgs.json document or up to three CSVs (pycsamt.format.structure, StructModel wrapping pycsamt.geology.structural.StructuralModel, which gained an optional line field for multi-line surveys), previewed on a profile-position/depth section, and inserted into the 3-D scene as tilted fault planes and measurement glyphs using the same per-line placement boreholes already use.

    • Patterns: pycsamt.geology.patterns imports a third-party pattern/swatch pack (folder, .zip, or single PDF/AI sheet, e.g. the USGS FGDC geologic-symbol set) into a persistent local cache (~/.pycsamt/geology_patterns), rasterizing any PDF/AI sheet via the new optional pymupdf dependency (pip install pycsamt[patterns]); the Studio’s Patterns tab crops a named swatch out of a sheet by hand (Plotly’s rectangle-draw tool) and assigns it to a Legend row’s pattern_id. A small built-in pattern set needs no import. Setting the 3-D toolbar’s Fill to “Pattern textures” then actually renders it on fence / depth-slice: each tile is read as an ink-density stencil (tile_stencil_array()), tinted by the row’s own colour (pycsamt.map.styles. pattern_band_stops()), and sampled at every cell’s true physical position – a real, position-true texture built entirely from geology_colorscale’s existing colorscale/cmin/cmax machinery (a new textured argument subdivides one band’s sub-range into a fine gradient), no new trace type. Block / iso-surface stay solid regardless – Plotly’s Volume/Isosurface traces have no per-cell colour-axis override to texture-map onto.

  • Fix Map View rail/toolbar polish – the Geology rail button referenced a filename with no matching icon asset and rendered blank; now uses the existing interpret glyph. Rail order is now Map / Boreholes / Geology / 3-D. The 3-D toolbar’s four separate depth-preset buttons (Full / 500 m / 1 km / 2 km) are now one compact dropdown.

  • Fix New Map View: the 3-D scene now holds its camera across control changes, plus a turntable toggle. Applying any inspector or toolbar change rebuilds the whole figure; uirevision alone did not keep the view because the 2-D MapLibre basemap re-applies its center/zoom on every rebuild and dcc.Loading blanked the 3-D camera mid-update, so a hand-set orbit or a 95% zoom snapped back on every edit. Map View now records the live camera / pan / zoom (per view) from every interaction and replays it onto the rebuilt figure; Fit and Reset view clear it, so they – and only they – return to the data-fitted default (pycsamt.app.mapview._render.figure_for gained a keyword-only viewport argument, default None). The 3-D toolbar also gains a Spin toggle: a browser-side turntable that orbits the camera about the vertical axis so every face of a block / iso-surface can be inspected without dragging.

  • New Structured (station+frequency+component) missingness for the domain-gap simulator – pycsamt.ai.domain_gap.survey_fit.StructuredMissingness and fit_structured_missingness() fit a logistic model of where a real survey’s coverage mask actually drops observations (a per-station effect, a per-frequency effect, and a per-component effect, each sum-to-zero), and pycsamt.ai.domain_gap.simulator.apply_structured_dropout() thins a synthetic SurveyData cell-by-cell from that fitted probability field. Unlike apply_dropout()’s three independent scalar rates – which are forced equal when each is derived from the same overall coverage deficit and compound multiplicatively – this reproduces missingness concentrated in particular stations or frequency bands. All three are re-exported from pycsamt.ai.domain_gap.


2.5.2 Fix Docs#

Released 2026-09-01.

A patch release. Converting a real, independent third-party ModEM 3-D inversion of the Baohuashan Cu–Mo survey to PCSF and exercising every Map View mode against it exposed five pre-existing defects in how a native ModEM volume’s own conventions reach the viewer – an air-fill colour scale, an upside-down topography drape, a depth axis referenced to the model top rather than the ground, a block/iso volume left empty by real multi-line station geometry, and zig-zag profile lines from a 3-decimal coordinate echo. None affected a pyCSAMT-written ModEM file or the bundled regression data.

Added#

  • New Map View 3-D robust colour scale – VolumeMapOptions gains crange_percentile (default (2.0, 98.0)) and rho_display_max. The fence / depth-slice / block auto colour range is now clipped to a percentile band instead of the raw min/max, and rho_display_max is a hard “hide every cell above this resistivity” cutoff that applies in every 3-D mode and drives the colour range (unlike the rho_range visibility band, which deliberately leaves the scale put). Both are exposed in the Map View inspector’s Appearance panel as “Colour-scale percentile” and “Hide cells above rho”.

  • New ``modem3d_to_pcsf`` gains ``air_threshold_ohm_m`` and ``station_z_convention`` – and pycsamt format convert gains the matching --air-threshold and --station-z options. See Fixed.

Fixed#

  • Fix A ModEM 3-D volume’s above-topography air fill hijacked Map View’s colour scale – ModEM leaves the model cells above the real ground surface at air-like resistivity (1e8–1e13 ohm.m) even when the run declares no dedicated air layers (n_air == 0; the WS header’s 4th field is a mapping method, not an air count). modem3d_to_pcsf() stored the whole rho_linear volume verbatim, and pycsamt.map.volume’s auto colour range took the raw min/max, so a fence colourbar ran to log10 rho ~13 and crushed the real 50–15,000 ohm.m earth into one flat band. The adapter now masks those cells to NaN in the canonical PCSFModel.resistivity (air_threshold_ohm_m=1e8 by default; resistivity_native keeps the raw array bit-exact; metadata["air_mask"] records it), any complete leading air layers are written to Grid3DGeometry.n_air, and pycsamt.map.inversion defensively masks the same fill in curtains sliced from an older .pcsf file. Pass air_threshold_ohm_m=None / --air-threshold 0 for an exact passthrough.

  • Fix ModEM topography was applied upside-down in every Map View 3-D mode – ModEM’s model z-axis points downward, so a .dat file’s own Z(m) station column is a positive-down depth below a datum, not an elevation. modem3d_to_pcsf copied it verbatim into StationTable.z, which every 3-D view (fence, depth-slice, block, terrain line) treats as a positive-up elevation – so deeper stations draped higher and hills rendered as hollows. The adapter’s new station_z_convention (default "auto") flips the column to a positive-up elevation when the .dat comments say it is a positive-down depth, reading an ... m a.s.l. datum from the file when present (else a relative elevation), and records the outcome in metadata["station_z"] with a UserWarning. "elevation" keeps the column verbatim (what pyCSAMT’s own ModEM writer produces); "depth_down" forces the flip.

  • Fix ``grid3d`` depth slices were referenced to the model top, not the ground surface – load_pcsf_lines gave every station the full model column measured from ModEM’s z = 0 (the flat model top), which for a topography-through-resistivity model sits ~100 m above the real ground, so a “200 m” depth slice really read ~90 m below surface and warped station-to-station with the terrain. For an n_air == 0 model with a known a.s.l. datum (metadata["station_z"], else the highest station), each column is now resampled onto true depth-below-its-surface; the earth axis is also re-zeroed to the earth top (matching pycsamt.models.modem.section.station_curtain()), and the deep, geometrically-graded boundary-condition cells – tens of km of no resolution that dominated every 3-D view’s vertical extent – are trimmed from the sliced curtain.

  • Fix Map View 3-D block / iso-surface rendered nothing on a real multi-line ModEM survey – lines whose stations project onto a shared along-strike position (routine for a dense or curved survey) produced duplicate x coordinates, and go.Volume/go.Isosurface’s grid interpolator needs strictly ascending axes, so every line was skipped and the volume came back all NaN (“resistivity not in the colour-bar range”). Coincident columns/rows are now merged (NaN-aware mean) before the interpolation, the same way the fence panels already de-duplicate, and the draped block keeps the model’s own near-surface cell density instead of a uniform lattice that buried it in the deep padding.

  • Fix Profile lines zig-zagged for a ModEM run whose station coordinates were a rewrite echo – InversionResult auto-picks the observed-data .dat from a run folder; when the folder held both the real input and a ModEM -R “read and rewrite” echo (which truncates GG_Lat/GG_Lon to 3 decimals – ~100 m at mid latitudes), case-insensitive file sorting on Windows chose the echo. On a survey with ~100 m station spacing the quantized coordinates made Map View’s profile polylines saw-tooth and collapsed adjacent stations onto one point. InversionResult now ranks candidates so a full-precision input beats a rewrite echo, and modem3d_to_pcsf() warns when the .dat file’s GG_Lat/GG_Lon are quantized to 3 decimals.

Docs & tooling#

  • Docs The modem3d_to_pcsf docstring and pycsamt format convert help document the new air_threshold_ohm_m / station_z_convention parameters and their CLI equivalents.


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 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), 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`` column – app/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 rows – OccamMesh.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 nothing – go.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 background – build_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 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_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 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_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 spacing – plot_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 namespace – PYCSAMT_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 spacing – station_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 pass – AirborneEMDataset 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-XML – containers.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 providers – RockPropertyProvider 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 primitives – StructuralMeasurement (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 inversion – DUHIInverter2D 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 infrastructure – generate_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 presets – mt_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 solver – pycsamt.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 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.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 data – pycsamt.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 utility – pycsamt.api.mesh.draw_mesh() and PYCSAMT_MESH draw structured-mesh cell boundaries with independent fill/edge alpha via a two-layer pcolormesh, with adapters for MaxwellMesh and GeologyGrid. plot_inversion_result_2d gained an opt-in show_mesh toggle 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) 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 heuristic – pycsamt.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 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; 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 command – ModEm3DAdapter._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 benchmark – pycsamt.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 resolution – pycsamt.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 topography – TriFEM2DAdapter 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 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 – Inv3DAgent 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 title – Inv2DAgent’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 swap – pycsamt.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 display – plot_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 surveys – plot_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 section – TEMtoEDI._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 sections – TEMtoEDI 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 ~6e5 – TEMtoEDI 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 metadata – pycsamt.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 frequency – pycsamt.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 metres – plot_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 plot – plot_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 ellipse – phase_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 styling – plot_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 labeling – plot_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 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_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 dependencies – triangle>=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 pages – Geoelectric 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 guesses – auto 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 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. Maxwell2DDatasetConfig generates 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 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 pipeline — pycsamt.pipeline chains 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 under pycsamt.models, and deep-learning inversion in pycsamt.ai (1-D/2-D/3-D nets, ensembles, uncertainty).

  • Feature Geological interpretation — pycsamt.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 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 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 previews — AgentCoordinator 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