Version 2.4.0#

pyCSAMT 2.4.0 New Fix Enhancement Docs Build#

Released 2026-08-19.

Four independent pieces of work landed in this release. First, the station confidence profile (plot_confidence_profile(), station_confidence_table(), frequency_confidence_table()): station distance is now computed from real survey coordinates instead of silently defaulting to a hardcoded spacing, with an opt-in escape hatch for surveys whose coordinates cannot be trusted, and decluttered point labels when many stations are flagged low-confidence. Thanks to @shahidalishah130-hub for reporting this with a clear reproducing figure in issue #76.

Second, Site/Sites – the station wrapper used across pycsamt.emtools, the CLI, and the desktop app – now accepts EMTF-XML transfer functions (EMTF) symmetrically with the historical SEG-EDI format, with no changes required anywhere that already consumes Site/Sites. Building the real, oracle-validated round trip behind this feature surfaced and fixed five further real bugs, three of them pre-existing and unrelated to XML at all – including one where Sites.write() had been silently writing placeholder files instead of real EDI content on every call, for as long as the method has existed.

Third, pycsamt.airborne – the MobileMT/ZTEM/AFMAG package added in 2.3.x – went through a full guideline-compliance pass: a new shared pycsamt.airborne.validation module now centralizes boundary validation that each technology adapter had independently duplicated (frequency/period resolution, matrix-shape checks, record masks, reference-station metadata mapping), every public class and function across the package gained complete NumPy-style docstrings, and one real inheritance mismatch was corrected. Alongside it, a new pycsamt.emtools.afmag module adds AFMAG-specific processing, diagnostics, and plotting – including a from-scratch implementation of the rotation-matrix motion-induced-noise method of Liu et al. (2018), grounded directly in that paper’s own equations.

Fourth, a new pycsamt.models.occam1d package brings a native, 1-D Occam smooth-model inversion engine into pyCSAMT: forward physics, analytic Jacobian, roughness regularization, and the nonlinear Occam loop itself are all implemented in Python/NumPy, with no external Occam1D binary required (Occam1DRunner remains available for those who still want one). Per-station and whole-survey batch inversion are both covered, backed by a new bundled example, examples/occam1_demo/, against three real Gabbs Valley soundings.

Fifth, a docs-only follow-up completes the story the first two pieces above started: a new, six-page Airborne EM Guide user guide and a new pycsamt.airborne reference document pycsamt.airborne in full, and Loading electromagnetic data now explains, in one place, that the same Site/Sites loading boundary handles EMTF-XML exactly like EDI, and that airborne surveys load through a separate, parallel boundary of their own.

Real EMTF-XML support in Site/Sites New#

Site previously only wrapped a parsed SEG-EDI object. It now accepts either an EDI object or an EMTF XML document, via a lazy dual-backend design: edi and tf are both always available on any Site regardless of which one it was built from – the representation not natively supplied is materialized from the other on first access and cached, using the existing, oracle-validated edi_to_emtf()/emtf_to_edi converters. Because edi always resolves to a real EDI-shaped object either way, every existing piece of code that reads .edi/.edi.Z/.edi.Tipper directly – across pycsamt.emtools, the CLI, and the desktop app – keeps working completely unmodified for an XML-native Site, with no code changes required anywhere outside pycsamt.site.

New surface: Site.from_xml() / Site.to_xml() (the from_edi constructor is now spelled out explicitly too, for symmetry), Site.backend ("edi" or "xml"), and typed metadata properties – site_meta, site_layout, provenance, processing, copyright, quality_meta – that are the same pycsamt.metadata objects the EMTF document itself already models (SiteMeta, SiteLayout, ProvenanceMeta, and so on), not a parallel copy. Sites gained the matching bulk operations, to_emtf_list() and write_xml(), mirroring the existing EDI-side to_edis/write. And the coercion layer behind to_sites() and ensure_sites() – the single entry point every emtools function validates its input through – now recognizes a single .xml path, an EMTF object, a list mixing either with EDI sources, and a directory containing *.xml files alongside (or instead of) *.edi files, with no changes needed in ensure_sites itself.

This was validated end to end, both numerically and by literally reading the files back off disk, in a new example, examples/emtf_xml_roundtrip_demo/ (see its README.md): three real field stations converted EDI -> XML -> EDI through both the Site/Sites API and the raw EMTF document API side by side, comparing periods, impedance, tipper, and variance at every step. Separately, the underlying EDI<->EMTF-XML conversion itself was validated against a real, locally compiled reference implementation (USGS EMTF-FCU v4.1) for the first time – periods, impedance, variance, site coordinates, channel geometry, and rotation math all matched to floating-point precision on real field data (pycsamt/emtf/tests/test_fcu_oracle.py, skipped rather than failed when no local FCU build is present).

The Site Tools pages are updated to match: Site Containers gains a “Working With EMTF-XML” section (real captured output against the bundled Gabbs Valley station, already used throughout Metadata and EMTF), and Site Metadata, Export And Reporting, and Site Selection each note where their existing, EDI-specific tooling still applies unchanged and where the new EMTF-XML surface takes over.

Five real bugs found and fixed Fix#

Building and validating the above surfaced five real, previously unnoticed bugs – two specific to the new XML coercion logic, and three pre-existing ones with no relation to XML support at all:

  • ensure_sites/to_sites silently returned an empty Sites for a plain Python list of already-constructed Site objects (a bare Site doesn’t look EDI-shaped itself, so it was dropped by the duck-typed unwrapping step) – a real limitation noted, but not root-caused, in an earlier audit. Fixed.

  • ordered() and select() forced every site through its materialized EDI view on every non-inplace call, silently downgrading an XML-native site back to EDI – and since ensure_sites always calls .ordered(...) on its way out, this meant newly-XML-aware Sites were downgraded again immediately after being built correctly. Fixed to preserve each site’s native backend.

  • ``Sites.write()`` had been writing placeholder files, not real EDI content, since the method was introduced – it looked for an EDIFile.to_file() method that has never existed (the real serializer is EDIFile.write()), silently fell back to a one-line placeholder, and the only existing test checked that the output file existed, never its content. Fixed to use pycsamt.emtf.converters.edi.write_edi(), the same writer already used elsewhere.

  • The EDI reader (pycsamt.seg.edi) mapped the historical EDI missing-data sentinel (EMPTY, typically 1.0E+32) to 0.0 instead of NaN for impedance, tipper, and variance – inventing a measured-exactly-zero value in place of a genuinely missing period. This affects any EDI with real gaps (dead bands, dropped channels), not only XML-related code paths. Fixed to emit NaN.

  • That fix then exposed a second, latent bug it had been masking: pycsamt.z.resphase.ResPhase.compute_resistivity_phase() had a guard that unconditionally rejected any NaN uncertainty paired with a valid impedance value, even though the surrounding per-component logic was already written to tolerate exactly that case gracefully. A measured impedance with an unavailable uncertainty is common in real field data. Relaxed to only reject genuinely malformed (negative or infinite) uncertainties.

Real inter-station distance, not a hardcoded spacing Fix#

The reported symptom: a real ~2050 m AMT line (41 stations, L48PLT) plotted with an x-axis stretched out to ~8000 m, even though the station names and their order were correct.

The root cause lived in the shared _station_positions helper. It only looked for east/north (or x/y, easting/northing) attributes directly on each station object – but real EDI-backed Site objects never carry those; they only expose lat/lon (read from the EDI >HEAD section). So that lookup always failed, and the function silently fell back to laying every station out at a flat spacing_m default of 200 m: 41 stations x 200 m = 8000 m, exactly matching the reported figure.

Station distance is now derived from real coordinates whenever at least two stations carry usable ones: EDI HEAD latitude/longitude is projected to true UTM easting/northing using the existing, dependency-free pycsamt.gis.utils.ll_to_utm() (no GDAL/pyproj required), then projected onto the bearing from the first to the last valid station – real chainage, the same convention already used for station ordering elsewhere in the project. An earlier internal draft of this fix used a naive equirectangular approximation (multiplying absolute longitude by a per-point cos(lat) factor); a regression test comparing against a known span caught a ~6% distortion from that shortcut before it shipped, which is why the final fix goes through a real UTM projection instead. Each station’s projected offset is cached on its underlying EDI object (keyed by the exact lat/lon it was derived from), so repeated calls across a session – e.g. several plot_confidence_profile calls on the same survey, each of which gets a freshly re-wrapped Sites instance – do not re-parse the header or redo the projection every time. spacing_m is now only ever a fallback: for individual stations without usable coordinates, or for the whole line when none have any.

Two new opt-in controls New Enhancement#

Force uniform spacing. Real coordinates are not always trustworthy – a bad GPS fix, a copy-paste error in the header, or a placeholder value can all corrupt station positions. force_spacing=True on plot_confidence_profile(), station_confidence_table(), and frequency_confidence_table() skips coordinate lookup entirely and lays every station out at uniform spacing_m steps, so a user who knows their coordinates are unreliable can trust their own spacing value instead.

Decluttered low-confidence labels. plot_confidence_profile already drew a rotated station-name label above every point below ci_lo (annotate_low=True) on top of the station names already on the top axis. For a survey where most stations are flagged low – exactly the L48PLT case in the original report – that meant nearly every point got its own label, stacked on top of the top-axis labels. The new annotate_low_step thins these the same way station_label_step already thins the top axis: left at its default None, it auto-thins once more than 18 stations are flagged low; annotate_low_step=1 restores the previous label-every-point behaviour; annotate_low=False turns per-point labels off entirely.

pycsamt.airborne guideline pass: shared validation, deeper docs Enhancement Docs#

pycsamt.airborne (MobileMT/ZTEM/AFMAG, added in 2.3.x) went through the project’s new-subpackage development guidelines module by module. The main structural change is a new shared pycsamt.airborne.validation module, which centralizes boundary validation that mobilemt, ztem, and afmag had each independently reimplemented: resolving a frequency-or-period axis, validating a transfer-function matrix or per-sample estimate array against its expected shape, resolving a record mask, and mapping a technology’s reference-station metadata into the shared EMTF model (reference_station_mapping(), merge_remote_reference_processing()). ztem and afmag were migrated onto it, removing several dozen lines of near-duplicate logic; in build_airmt_emtf(), two previously separate checks (tensor-shape validation and a frequency-count match) collapsed into one shared call with no behaviour change. A related duplicate was found one layer up: both mobilemt and afmag reimplemented the same “register this EMTF data type unless an incompatible one already exists” guard, now available once as pycsamt.emtf.datatypes.ensure_emtf_datatype_registered().

One real inheritance mismatch was corrected: AirborneEMDataset inherited MTBase although it performs no electromagnetic arithmetic itself (it only aggregates flight lines – the actual EM math lives in the EMTF documents it holds); it now inherits CoreObject, matching every sibling container in the package. The registry value objects (AirborneTechnologyDefinition, AirborneFormatDefinition) and the QC finding type (AirborneQCIssue) now inherit PyCSAMTObject, matching the equivalent objects already established in pycsamt.emtf and pycsamt.metadata. 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, such as why afmag’s own per-sample frequency-grid validation is deliberately stricter than the shared helper it otherwise reuses). This was a structural and documentation pass only: every step was verified against the full existing test suite, and no behaviour change is expected outside the corrected inheritance above.

Complete pycsamt.airborne user guide and API reference Docs#

pycsamt.airborne had real docstrings after the guideline pass above, but no narrative user guide and no dedicated API reference page at all – Airborne EM Guide previously covered only the data model in a single page, with no toctree children, and pycsamt.airborne did not exist. Both gaps are closed. The user guide is now six pages: Airborne Data Model Overview (the technology-neutral container map and the AFMAG/ZTEM/MobileMT subpackage table), Flight Lines and Datasets (NavigationTrack/AirborneEMRecord/AirborneEMLine/ AirborneEMDataset built from scratch, including a real terrain-following flight-profile figure derived from NavigationTrack.clearance_values), The Airborne Site View (AirborneSite/ AirborneSites reading all four committed synthetic sample surveys, and a three-panel diagnostic figure calling each technology’s own literature-grounded plotting function – plot_ztem_map(), plot_original_afmag_dual_frequency_profile(), plot_mobilemt_conductivity_psection() – directly on the same containers this page builds, with no conversion step), Technologies, Formats, and Native I/O (the technology/ format registry and native-I/O dispatch, including a real, previously undocumented naming inconsistency: EMTF.subtype spells the two AFMAG generations "afmag_original"/"afmag_airmt", while identify_airborne_technologies() canonicalizes them to "afmag"/"airmt"), and Structural Quality Control (assess_airborne_qc()’s severity philosophy, demonstrated on a real dataset built with five independent, genuine structural defects). Every code example across all four deep pages was verified byte-exact against a live interpreter session before being committed to the page.

pycsamt.airborne is a new automodule/autosummary reference page covering pycsamt.airborne’s core containers, site view, registry/I/O, QC, shared validation helpers, and all three technology adapters, registered in API reference alongside every other top-level package.

Finally, Loading electromagnetic data – previously EDI-only in both content and framing – now states plainly that ensure_sites() normalizes EMTF-XML exactly like EDI into the same Sites container (demonstrated again here against the bundled Gabbs Valley station, with a symmetric write-back example converting the same Sites to both EDI and EMTF-XML), and introduces ensure_asites() as the separate, parallel boundary airborne surveys load through, with a short, real example showing why an airborne station’s z is always None rather than a loading defect. read_edi()/ read_edis themselves are unchanged – they remain the deliberately narrow, EDI-only boundary they always were.

AFMAG motion-noise physics and diagnostics New#

pycsamt.emtools.afmag is a new module bringing AFMAG-specific processing into pycsamt.emtools, following that package’s existing conventions throughout: every processing function accepts sites (coerced through the same ensure_sites() every other emtools function uses) and every plot function accepts ax/axes and returns Axes/Figure – the latter is not just a style guideline, it is mechanically enforced by the package’s existing test_plot_api_signatures.py gate, which now also covers this module.

AFMAG is magnetic-field-only – there is no electric-field channel, so the module is built entirely on Site.tipper (the classical AFMAG tilt-angle readout is, in modern MT terms, the same object), never on Site.z. Three pieces:

  • Motion-coupling physics – a from-scratch, paper-grounded implementation of Liu et al. (2018)’s rotation-matrix method for simulating and removing platform-motion-induced noise: euler_rotation_matrix(), geomagnetic_field_direction(), coil_normal_direction(), motion_coupling_cosine()/_angle, simulate_motion_induced_voltage(), and correct_motion_induced_noise(). These operate on raw attitude (yaw/pitch/roll) and geomagnetic geometry rather than on SitesSite has no time axis or attitude field to run them on – and are validated both against hand-derived analytic cases (identity, 90-degree, orthogonality) and against the paper’s own qualitative findings (Fig. 2: theta(t) is linear in roll, and inclination shifts rather than reshapes the curve; yaw has no effect at zero pitch/roll for a z-axis coil).

  • Tilt-angle diagnosticsafmag_tilt_angles() derives the classical in-phase/quadrature AFMAG tilt angle directly from Site.tipper, and three new plots present it the way AFMAG data has traditionally been read: plot_afmag_tilt_profile() (the classic flight-line profile), plot_afmag_tilt_psection() (station x log-period pseudosection), and plot_afmag_tilt_polar().

  • Motion-coupling QC – the bridge between the two: motion_susceptibility_table() scores each station’s exposure to motion noise from a nominal attitude-amplitude envelope and the survey’s geomagnetic geometry, and flag_motion_susceptible_band() is the one mutating, Sites-in/Sites-out function in the module – it masks or drops a susceptible station’s low-frequency tipper band, following the same ensure_sites/mutation contract as notch_powerline(). plot_motion_susceptibility_map() and plot_afmag_correction_comparison() (a before/after/delta pseudosection triptych) visualize both.

No apparent-resistivity/conductivity formula is derived from tilt angle – unlike full MT, nothing in Ward (1959) or Liu et al. (2018) provides one, so none is invented – and the module does not attempt the paper’s literal time-domain “subtract predicted noise from raw movement data” step against a frequency-domain Sites object, which has no raw time series to run it on; that math is exposed as the reusable physics functions above instead.

Native Occam1D Inversion Engine New Docs#

pycsamt.models.occam1d is pyCSAMT’s own Occam1D engine. Unlike occam2d, which prepares files for an external Fortran executable, Occam1D’s forward model, analytic Jacobian, and nonlinear Occam iteration are implemented natively: Occam1DForwardModel recurses the isotropic layered-earth impedance from basement to surface (optionally Numba-compiled, roughly an order of magnitude faster per evaluation when the optional perf extra is installed), and Occam1DInversion runs the Lagrange- multiplier search, scoring every trial candidate through the full nonlinear forward model rather than a linearized estimate, and keeping every rejected candidate in the result rather than discarding it silently. Every station is inverted independently – there is no lateral mesh – so an entire survey can be built and inverted in two calls, build_all() / invert_all(), the latter optionally dispatching stations to separate processes via joblib (also part of perf).

The rest of the package rounds out the workflow around that core: Occam1DConfig for layer geometry and iteration control; Occam1DInputBuilder to build native data/model/startup files from EDI or site sources; Occam1DRunner to drive an external Occam1D-compatible binary instead of the native engine, when one is preferred; Occam1DResult and restart() to load completed runs and checkpoint/resume in-progress ones; and PlotModel/PlotResponse/PlotConvergence/PlotSummary for model, fit, convergence, and combined-summary figures, customizable through the shared PYCSAMT_OCCAM1D style registry without touching the inversion itself. mode="determinant" – fitting the rotation-invariant \(Z_d=\sqrt{-Z_{xy}Z_{yx}}\) response – is the default, chosen for field data whose true dimensionality is not yet known; "xy"/"yx" single-polarization modes remain available.

The new Occam1D inversion page walks through configuration, native-file construction, a single-station inversion, and batch inversion, with real captured output and figures against three stations (gv100, gv130, gv163) of the same public Gabbs Valley survey already used elsewhere in the documentation. The bundled examples/occam1_demo/ runs the same three-station workflow end to end from the command line, writing native files, text/JSON result products, and review figures per station. As with pycsamt.models.occam1d’s Numba/joblib acceleration, this is purely additive: the pycsamt invert CLI does not yet cover Occam1D (--solver there still accepts only occam2d and modem) – use the Python API shown on the new page until CLI support is added.

One real bug turned up while writing this section: PYCSAMT_OCCAM1D and the rest of pycsamt.api.occam1d’s public style API were never wired into pycsamt.api’s top-level namespace, unlike every other style registry in that package (PYCSAMT_STYLE, PYCSAMT_MESH, PYCSAMT_SECTION, and so on) – so from pycsamt.api import PYCSAMT_OCCAM1D, exactly as shown in examples/occam1_demo/README.md, raised ImportError. Fixed by adding the same from .occam1d import (...) wiring every sibling style module already has.

Added#

  • New Low-confidence point label declutteringplot_confidence_profile gains annotate_low_step, analogous to station_label_step but applied only to the (typically much smaller) subset of points below ci_lo.

  • New EMTF-XML support in ``Site``/``Sites``from_xml(), to_xml(), tf, backend, and typed pycsamt.metadata properties (site_meta, site_layout, provenance, processing, copyright, quality_meta) on Site; to_emtf_list() and write_xml() on Sites. to_sites()/ensure_sites now recognize .xml paths, EMTF objects, and directories mixing *.edi and *.xml files.

  • New ``examples/emtf_xml_roundtrip_demo/`` – converts three real field stations through EDI -> XML -> EDI via 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/emtf/tests/test_fcu_oracle.py`` – validates pycsamt’s EDI<->EMTF-XML conversion against a real, locally compiled EMTF-FCU v4.1 reference implementation; skips (doesn’t fail) when no local build is present.

  • New ``pycsamt.emtools.afmag`` – AFMAG-specific processing, diagnostics, and plotting: a from-scratch implementation of the Liu et al. (2018) rotation-matrix motion-induced-noise method (euler_rotation_matrix, motion_coupling_cosine, simulate_motion_induced_voltage, correct_motion_induced_noise, and friends), classical tilt-angle diagnostics from Site.tipper (afmag_tilt_angles), motion-coupling QC (motion_susceptibility_table, flag_motion_susceptible_band), and six new plots. See the module’s own documentation for the full list.

  • New ``pycsamt.airborne.validation`` – shared boundary-validation and reference-metadata-mapping module for mobilemt/ztem/ afmag, 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 (forward model, analytic Jacobian, regularization, nonlinear inversion loop, native-file I/O, results, plotting, and validation); see the summary above and Occam1D inversion for the full package map. 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 – station distance is now derived from real EDI coordinates (east/north, or lat/lon projected through a real UTM transform) whenever at least two stations carry usable ones, cached per EDI object. spacing_m is only ever a per-station fallback.

  • Fix ``ensure_sites``/``to_sites`` dropped a list of ``Site`` objects – a plain Python list of already-constructed Site instances silently resolved to an empty Sites, with no error.

  • Fix ``Sites.ordered()``/``Sites.select()`` silently downgraded XML-native sites to EDI on every non-inplace call, including the one ensure_sites always performs on its way out.

  • Fix ``Sites.write()`` wrote placeholder files instead of real EDI content – it looked for a nonexistent EDIFile.to_file() method and had been silently falling back to a one-line placeholder since the method was introduced; the only existing test never checked file content. Now uses 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 measurement 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 – unlike every other style registry in the package, PYCSAMT_OCCAM1D and friends were not importable via from pycsamt.api import PYCSAMT_OCCAM1D, the exact form documented in examples/occam1_demo/README.md.

Changed#

  • Enhancement User-controlled station spacingstation_confidence_table, frequency_confidence_table, and plot_confidence_profile gain a force_spacing parameter to bypass coordinate lookup entirely and trust a user-supplied spacing_m instead.

  • Enhancement ``pycsamt.airborne`` guideline-compliance passAirborneEMDataset now inherits CoreObject instead of MTBase (it performs no EM arithmetic itself); the registry definitions and AirborneQCIssue now inherit PyCSAMTObject, matching the equivalent objects in pycsamt.emtf/pycsamt.metadata; ztem and afmag were migrated onto the new validation module, 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-XMLSite Containers, Site Metadata, Export And Reporting, and Site Selection now cover the Site/Sites EMTF-XML backend, with real captured output against the bundled Gabbs Valley station. Also fixed a stale cross-reference in Metadata pointing at “the upcoming EMTF XML guide”, which now exists.

  • 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 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 (index/overview/ data_model/site/registry_and_io/quality_control, 6 pages) – real captured output and figures against the committed synthetic ZTEM/ AFMAG/MobileMT sample surveys throughout, including a literature- grade three-panel diagnostic composite in The Airborne Site View that calls each technology’s own pycsamt.emtools plotting function directly.

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

  • Docs ``user_guide/data_loading.rst`` covers EMTF-XML and airborne loading – documents that ensure_sites normalizes EMTF-XML the same way it normalizes EDI, with a symmetric write-back example, and introduces ensure_asites as airborne surveys’ separate loading boundary.

Compatibility#

This release changes default output for real surveys. Any call to plot_confidence_profile, station_confidence_table, or frequency_confidence_table on stations that carry lat/lon but no east/north attributes – the normal case for ordinary EDI-backed Site objects – previously plotted a distance axis stretched out by a uniform 200 m-per-station default; it now plots the real inter-station distance. This is a correction, not a behaviour change to preserve: station names, order, and confidence values are unaffected, only the x-axis distance scale. force_spacing and annotate_low_step are both new, opt-in parameters with defaults that keep every other existing call unaffected.

Site/Sites gain new capabilities without removing any existing ones – every EDI-only call site keeps working exactly as before, and Site.edi/Site.tf are additive. Any EDI parsed from a file with genuinely missing periods (rare, but real) now correctly reports NaN for those instead of a fabricated 0.0; code that assumed EDI-derived Z/tipper arrays are always fully finite should be checked against this. Sites.write() now writes real EDI content where it previously wrote a one-line placeholder – any code that happened to tolerate the placeholder output will see a real, larger file instead.

pycsamt.emtools.afmag and pycsamt.airborne.validation are both entirely new, additive modules. The one base-class change in pycsamt.airborne (AirborneEMDataset now inherits CoreObject instead of MTBase) removes access to MTBase’s electromagnetic numeric methods on that one class; nothing in pycsamt itself called them there, and this only matters to external code that did isinstance(dataset, MTBase) or called an MTBase method directly on an AirborneEMDataset instance.

pycsamt.models.occam1d is an entirely new, additive package with no interaction with any existing inversion engine; it does not touch pycsamt invert, occam2d, or modem. Its Numba/joblib acceleration is part of the existing, optional perf extra – without it, the package still works, only slower.