Version 2.5.0#

pyCSAMT 2.5.0 New Fix Docs#

Released 2026-08-28.

Version 2.5.0 is a major interoperability milestone built around two complementary common-format families. PCSF/PCSM standardize subsurface models and inversion results; PCBH standardizes the borehole observations and geological evidence used to constrain and interpret those models. Together they give pyCSAMT explicit, reusable contracts for both sides of an integrated 3-D interpretation instead of requiring every importer, solver, and viewer to reconstruct scientific meaning independently.

The pyCSAMT Common Subsurface Format has two lossless encodings of the same model: PCSF is a self-describing HDF5 container for routine interchange and large arrays, while PCSM is its human-readable ASCII projection. Occam2D, ModEM 3-D, MARE2DEM, multiline models, and generic AI/DL arrays converge on one geometry-aware representation with canonical linear resistivity. A downstream consumer therefore reads one model contract instead of branching on native resistivity encodings, axis orders, grid or mesh layouts, and solver metadata.

The pyCSAMT Common Borehole Format provides the complementary human-readable .pcbh.json contract for many water, mining, geotechnical, geothermal, petroleum, monitoring, or exploration boreholes. Collars, measured/deviated trajectories, geological intervals, structures, vocabularies, units, CRS and vertical reference, and provenance travel together. PCBH remains independently valid geological evidence, while a PCSF/PCSM model may embed or checksum-reference it and align it explicitly for shared 3-D display. The Borehole Builder, web 3-D view, and MapView all consume the same validated document and render contract.

Implementing and exercising PCSF/PCSM against real bundled data and full-scale Occam2D and ModEM 3-D runs also surfaced important pre-existing model-reader, topography-upload, and solver-launch defects. These include a new ModEmForwardControl that finally allows a covariance file to reach Mod3DMT correctly.

The confidence-ratio workflow also grows from a single-line profile into a survey-scale evaluation toolkit. New route, contour, component, comparison, risk, heatmap, distribution, ranking, grid, coverage, and before/after views use the same station-confidence API and support both the composite and presence methods. Geographic plots retain real longitude/latitude geometry, while profile chainage is derived from the corrected station coordinates rather than an assumed line length.

PCBH: universal borehole exchange and 3-D integration New#

pycsamt.format.borehole introduces the versioned PCBH 0.1 contract and canonical .pcbh.json encoding. One document declares the horizontal CRS, vertical reference, units, depth and orientation conventions, shared lithology/formation dictionaries, and provenance for every contained hole. Each borehole has a stable ID, kind and status, spatial XYZ collar, total measured depth, vertical shorthand or ordered deviation survey, categorical interval-log families, structural observations, and namespaced extensions. Inclination is measured from vertical down in the range 0–180 degrees, so upward/re-entry paths remain representable; intervals are half-open and may contain honest gaps, while overlap is rejected within each categorical family.

The public facade exposes read_pcbh() and write_pcbh(), dependency-free semantic validation, a checked-in JSON Schema and canonical fixture, deterministic minimum-curvature desurveying, resource limits for untrusted input, and structured diagnostics. Combined collar/interval CSV import supports explicit column mappings, bounded previews, strict or permissive rejection reports, and reusable mapping profiles. A manifested relational CSV projection preserves collars, surveys, logs, structures, checksums, CRS, and table relationships. LAS 2.0 import and export preserve supported depth curves and report unavoidable loss.

Compatibility remains additive. Promotion from the established pycsamt.geology.Borehole requires an explicit absolute collar and retains legacy profile chainage only as metadata; downgrade requires an explicit profile_x mapping and cannot silently treat collar easting as chainage. embed_pcbh() and reference_pcbh() associate an unchanged PCBH document with PCSF. align_pcbh_to_pcsf() transforms trajectories into a grid3d model frame, reports inside/intersecting/outside paths, and requires a caller-supplied vertical offset whenever datum compatibility cannot be demonstrated.

The shared render model supplies centerlines, colored interval segments, contacts, structure glyphs, material batches, selection state, and scientific hover values to both application front ends. MapView can load, filter, style, select, and export PCBH overlays alongside 3-D inversion blocks. The web application adds an accessible Interpret → Borehole Builder with project, collar, survey, interval, structure, water, construction, sample, and assay editors; CSV mapping preview; row-linked live validation; 2-D log and 3-D trajectory previews; browser-local recovery; canonical JSON download; and PCSF embedding. GeoJSON, VTP, glTF, and GLB exporters return explicit loss reports and never replace authoritative PCBH JSON.

See PCBH — Common Borehole Format for the complete workflow and pycsamt.format for the callable reference. PCBH stays at format version 0.1.0 while the pyCSAMT package advances to 2.5.0; these version numbers describe different contracts and intentionally do not move together.

Survey-scale confidence evaluation New#

pycsamt.emtools.qc now provides a coherent family of confidence diagnostics for a single line or multiple lines: plot_confidence_map(), plot_confidence_grid_map(), plot_confidence_component_map(), plot_confidence_method_comparison(), plot_confidence_risk_map(), plot_confidence_heatmap(), plot_confidence_distribution(), plot_confidence_rank(), plot_confidence_coverage_curve(), and plot_confidence_before_after().

The map API supports an observation-preserving route view and an interpolated contour view. Contour levels and isoline colour, width, style, labels, and label formatting are configurable. Its colour scale adapts to the finite data range instead of automatically starting at zero; operational boundaries such as 0.50, 0.85, 0.90, 0.95, and 1.00 are drawn only where the observed/interpolated range actually crosses them. export_confidence_map() writes the same confidence surface as a station CSV and a Surfer ASCII DSAA grid for use outside pyCSAMT.

The component map separates coverage, uncertainty, off-diagonal, diagonal, phase, and spatial contributions on shared geographic axes. The method-comparison and before/after views expose score changes rather than hiding them behind a single aggregate. Risk, ranking, distribution, heatmap, and coverage-retention views answer complementary questions: where review is needed, which stations should be reviewed first, how the survey population is distributed, how confidence varies by line, and how much station/data coverage remains at a selected threshold.

PCSF/PCSM: universal subsurface-model exchange New#

PCSF and PCSM are two lossless encodings of the same validated PCSFModel; they are not competing scientific models. PCSF stores typed arrays, attributes, and groups in HDF5 for compact routine interchange and large 2-D/3-D results. PCSM projects the same model into UTF-8 keyword blocks for inspection, version-control diffs, annotation, and software that should not depend on HDF5. .pcsm.gz adds whole-file compression when text-schema portability matters more than direct browsing. Conversion in either direction reconstructs the same geometry, canonical resistivity, stations, topography, history, and provenance.

pycsamt.format discriminates model geometry explicitly through geometry.kind rather than letting a reader infer it from array shape. Four kinds are supported: grid2d (Occam2D, and a DUHI-prepared result once folded back into an Occam2D run), grid3d (ModEM 3-D – the first genuinely persisted 3-D resistivity volume in pyCSAMT; earlier 3-D views only ever synthesized one at render time from stacked 2-D sections), mesh_unstructured (MARE2DEM’s real triangular mesh, never forced onto a rectilinear grid), and multiline (a formalized fence/stack of independent lines, with real-vs-synthetic cross-line offsets). Canonical resistivity is always linear \(\Omega\,\mathrm{m}\) in every kind; the source backend’s own encoding is kept alongside it, explicitly labelled, never assumed.

Both encodings enforce the same scientific invariants. Canonical model/resistivity/RESISTIVITY values are linear \(\Omega\,\mathrm{m}\); an optional source-native array remains separate and carries its declared linear, log10, or ln encoding. Grid coordinates, node edges, rotations, station locations, topography, uncertainty, sensitivity, and derivation metadata retain explicit identities. A multiline derived volume is labelled synthesized and is never presented as a native 3-D inversion.

occam2d_to_pcsf(), modem3d_to_pcsf(), and mare2dem_to_pcsf() build on each backend’s own existing result objects rather than rewriting them – build_multiline_pcsf() similarly reuses pycsamt.map.geometry’s existing real-vs-synthetic line-offset computation instead of reimplementing it. Every second-level module of the new package – adapters, schema, io, multiline, topography, pointcloud – is public immediately after import pycsamt.format, not only through an explicit submodule import.

write_pcsf(), read_pcsf(), write_pcsm(), and read_pcsm() provide the canonical I/O boundaries. pcsf_to_pcsm() and pcsm_to_pcsf() compose those readers and writers rather than maintaining a second conversion engine. Readers inspect the declared version and geometry kind before interpreting array shape, while the writers validate dimensions, finite scientific values, identifiers, and metadata relationships before serialization.

pcsf_to_point_cloud() flattens any of the four geometry kinds into one (x, y, z, log10_rho) point cloud, with a reproducible seeded subsample for a native grid3d/mesh_unstructured file that can carry hundreds of thousands of cells. It is the one function behind three new consumers, none of which carry backend-specific logic: the desktop app’s first 3-D/volume panel, a new “PCSF file” data source in the web 3-D view that renders directly from an uploaded file with no session cache at all, and pycsamt.map.MapView.from_pcsf() (which mirrors from_inversion_results()’s existing real-per-station-curtain approach for ModEM, so the existing fence/ depth-slice builders in pycsamt.map.volume render PCSF-sourced lines unmodified). pycsamt.format.topography wires a PCSF file’s topography through the existing pycsamt.map.topo elevation parser instead of adding a third, independent one.

PCSF/PCSM remain authoritative for model geometry and physical-property arrays. PCBH remains authoritative for borehole observations. A model may embed or checksum-reference PCBH, but this association preserves the two contracts and their provenance instead of rasterizing geology into model cells. See PCSF — Common Subsurface Format for complete HDF5 and ASCII layouts, real backend conversions, topography, multiline models, and round-trip examples.

One command to reach all of it: pycsamt format#

pycsamt format convert SOURCE [TARGET] is the CLI shortcut past the adapter-selection step. It calls one detector – pycsamt.format.detect_source(), now public alongside SourceKind – which reads only file names, extensions, and a few solver signatures (never a heavy array load) and reports whether SOURCE is 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. It then runs the matching converter: rebuilding the MARE2DEM triangulation from the run’s own .poly PSLG, recognising log10 / ln resistivity keys in an .npz and setting the encoding automatically, and carrying --topo / --epsg / --utm-zone / --origin through to the adapter. An existing .pcsf transcodes losslessly to .pcsm (and back). pycsamt format detect prints the classification without converting – and exits non-zero when the source is not convertible, so it works as a pre-flight check in a script. pycsamt format info summarises a written file (geometry, per-array resistivity statistics, stations, topography, provenance, history), and pycsamt format validate runs a header / load / schema / write-read-compare round-trip check.

Two real bugs found and fixed Fix#

Validating the ModEM adapter against a real bundled 3-D inversion volume (41x50x288 real cells) surfaced a real, previously unnoticed bug in ModEmModel3D: a real ModEM writer appends the grid’s real-world centre coordinates and an optional rotation angle immediately after the resistivity volume, and the WS-format reader parsed that trailing line only to discard it – nothing on the class ever stored it. It is now exposed as ModEmModel3D.origin/.rotation (metres/degrees), matching the convention read_mackie3d() already used on the same class – with the same defaults (zeros(3)/0.0, not None), since iotools.export/iotools.interpolate already depended on that exact default through a getattr(model, "origin", [0, 0, 0]) fallback; an initial None default broke both with a TypeError, caught by ten real test failures before the fix landed.

Wiring topography through pycsamt.format.topography surfaced a second bug: the web 3-D view’s own CSV/HDF5/NPZ elevation-upload parser (pycsamt.app.web.callbacks.map3d._parse_topo_upload) had reimplemented pycsamt.map.topo’s parser independently rather than reusing it, and had quietly drifted – its own id-column list never recognised a station_names column/dataset that the shared parser already did. An otherwise-valid upload could silently fail to parse through the web app’s own uploader while working fine through pycsamt.map.topo.apply_elevations()’s “Upload file” source elsewhere in the package. It now delegates to the shared parser entirely.

Four real Occam2D/ModEM solver bugs found running real inversions Fix#

Running a full 5-line Occam2D inversion and a block-wide ModEM 3-D inversion against the real WILLY_DATA survey (128 stations) – both against the vendored, natively compiled Occam2D.exe/Mod3DMT.exe binaries, not a Python re-implementation – exercised configurations no existing test had reached, and surfaced four more real, previously unnoticed bugs.

Both from_data() and halfspace() were inserting air-layer rows into meshes/models that Occam2D’s own PW2D format and ModEM’s WS format never expect – both formats derive or hardcode air separately. A real compiled Occam2D failed deep inside its own file I/O with “Model file error: # params <> # free bricks”; a real compiled Mod3DMT failed with “Mapping not supported yet in read_modelParam_WS”. Air rows/n_air are now always 0 for both. Separately, write()’s key-field width (44 columns) pushed the colon past Fortran’s fixed a36 read boundary into the adjacent numeric field, producing “Bad value during floating point read” against a real ModEM.inv file; the field is now 36 columns, matching the Fortran source exactly.

The third was more involved: Mod3DMT’s own -I NLCG command-line argument order is rFile_Model rFile_Data [rFile_invCtrl rFile_fwdCtrl] [rFile_Cov] (UserCtrl.f90) – the covariance file is the sixth positional argument, and is only reachable once a forward-solver control file (the fifth) is also present. Nothing built that fifth file, so passing covariance= to ModEmRunner landed it in the forward-control slot instead, and a real Mod3DMT then tried to parse a covariance file as forward-solver settings and aborted (“Bad value during integer read”) – covariance could not be passed to a 3-D inversion at all. New ModEmForwardControl writes this file matching Mod3DMT’s own compiled-in solver defaults exactly (QMR iteration count, divergence-correction limits, solver tolerances – confirmed against the real Fortran source, EMsolve3D.f90’s readEMsolveControl, a distinct fixed-column format from ModEmControl’s), so writing it changes nothing about forward-solver behaviour; its only effect is occupying the argument slot needed to reach the covariance file. run()/.command now auto-write or reference a default forward-control file whenever covariance= is supplied without an explicit fwd_control=, and InputBuilder writes one for every 3-D build alongside the covariance file.

The fourth was caught by comparing the new forward-control file’s own printed diagnostic against what had actually been written to it: Mod3DMT echoed a real 1e-7 tolerance back as 0.1000000E-13, a six-order-of-magnitude corruption. Fortran’s G edit descriptor on input requires an explicit decimal point in the field, or the format’s own decimal-digit count silently re-places one – and Python’s %g formatting (used by both ModEmControl.write()’s and ModEmForwardControl.write()’s previous implementation) omits the decimal point for whole numbers (10.0 -> "10") and many small-magnitude exponentials (1e-7 -> "1e-07"). The real, already-running ModEM.inv on disk had exactly this: Initial damping factor lambda: 10, To update lambda divide by: 100, and – most consequentially – Exit search when rms is less than: 1, meaning target_rms itself had been silently corrupted in every prior run, a strong candidate for the real root cause of the earlier stuck-optimizer symptom (RMS frozen across three full iterations while the damping parameter exploded from 1e-6 to 1e9) – independent of, and arguably more consequential than, the covariance-argument bug above. ModEM’s own usage-text examples in UserCtrl.f90 confirm this was always a real requirement: every example value includes a decimal point, even whole numbers ("1.", "10.", "1.0e-7", never "1", "10", or "1e-7"). Both writers now use %.6E scientific notation, which always includes a literal .; new regression tests assert every float field parses with a decimal point present, including the exact values that regressed. Verified end-to-end against the real compiled Mod3DMT: its own diagnostic now echoes back 1e-7, 1e-5, 10.0, 100.0, and 1.0 exactly, for every previously-corrupted field.

All four fixes were confirmed via real solver runs, not just the test suite.

Map View 3-D and 2-D refinements New Fix#

Now that a real, persisted resistivity volume can reach the viewer (PCSF grid3d, or a ModEM/MARE2DEM folder via from_inversion_results()), the Map View’s resistivity-focused modes were reworked to behave the way an inversion viewer is expected to, rather than the way an apparent-resistivity station map does.

In the 3-D view, selecting a resistivity range now masks the model instead of recolouring it: fence and depth-slice panels open real holes where the resistivity is out of the selected band (the smoothing spline used to refill that mask before rendering), and block / iso-surface mode renders that band as one closed body – isomin/isomax alone never isolate a value band, because the isomax iso-surface still wraps everything more resistive than it. Throughout, the colour scale stays pinned to the whole model, so scrubbing a filter or a slice depth never re-graduates the hues. Block and iso-surface mode also rendered nothing under topography until now – go.Volume/go.Isosurface were being handed the drape’s NaN cells (which break both traces) and an iso window that was never clamped to the data actually present. An opt-in “Smoothing” panel adds a structure-smoothing pass – a finer render lattice plus a Gaussian – for rounder iso-surfaces and softer anomaly edges, off by default. Station markers and their labels were also refined: labels can be rotated (drawn as scene annotations, which plain 3-D text cannot rotate), thinned to a fraction per line or a named subset without hiding any marker, and no longer overlap the marker glyph; block/iso markers are clipped to the reconstructed block footprint.

Depth-slice mode in particular was made robust for real inversion volumes. Slices are kept inside the model’s real depth extent (an endpoint at 0 m, or past the deepest cell, used to render as an invisible all-NaN ghost surface); a single slice now cuts the middle of the requested window rather than its floor; and a resistivity-range filter is applied against the resistivity actually interpolated at the slice depth, so it can no longer bridge across an out-of-band layer and paint the slice solid.

In the 2-D map view, an inversion result unlocks a new “Resistivity @ depth” colouring: resistivity_at_depth() slices every loaded section at a chosen depth and the result is drawn as a Surfer-style filled-contour raster with isolines over the basemap, on a log scale and a fixed whole-model colour range, the way ResIPy / Oasis / Surfer present a depth slice. The frequency slider is replaced by a slice-depth slider bounded by the model’s real z-extent (inversion_depth_range()); a new “Station markers” switch hides the scatter overlay to leave just the slice. Building that raster surfaced a real bug affecting every Surfer-style contour overlay in the package: it was rasterised onto a white Matplotlib figure, so the transparent PNG actually carried an opaque white background that hid the basemap everywhere outside the contoured hull – it is now genuinely transparent, and the 2-D station map figure drops its solid plot background too.

Logging: pycsamt-mapview (and every pyCSAMT Dash app) flooded the console with --- Logging error --- PermissionError [WinError 32] on Windows. The rotating file handlers sat on the root logger, so werkzeug’s one-line-per-request logging filled infos.log and forced a rollover on nearly every HTTP request, and Windows cannot rename a file another request thread still holds open. The werkzeug logger is now pinned at WARNING with no propagation, and a pycsamt.log.logger.SafeRotatingFileHandler skips a failed rollover instead of raising.

New user guide page and conversion demo Docs#

PCSF — Common Subsurface Format walks through PCSF’s four geometry kinds, converting a real Occam2D, ModEM, and MARE2DEM result with real captured output against each backend’s own bundled sample data, constructing a multiline fence and adding topography, and the pcsf_to_point_cloud consumers – including a real two-panel figure rendering an Occam2D grid2d model and a ModEM grid3d volume through the exact same function call, on one shared colour scale. A new bundled example, examples/pcsf_conversion_demo/, converts all three real backends’ bundled sample data to .pcsf (with and without topography for Occam2D and ModEM – the ModEM case uses real elevation, matched from data/AMT/WILLY_DATA by recognizing that ModEM’s own station names carry a survey-year prefix the real EDI station ids don’t), round-trip-verifying every file it writes and printing each one’s full HDF5 group/dataset tree to the console.

Added#

  • New PCSF/PCSM in :mod:`pycsamt.format` – backend-neutral subsurface-model exchange with compact HDF5 and lossless human-readable ASCII encodings: occam2d_to_pcsf(), modem3d_to_pcsf(), mare2dem_to_pcsf(), build_multiline_pcsf(), topography, and pcsf_to_point_cloud(), write_pcsf(), write_pcsm(), pcsf_to_pcsm(), and pcsm_to_pcsf(). See the summary above for the full scientific and encoding contract.

  • New ``pycsamt format`` command groupconvert (any solver / AI result / .pcsf / .pcsm -> .pcsf / .pcsm, source auto-detected), detect, info, and validate. The shared detector is public as pycsamt.format.detect_source() / SourceKind. See Format Commands and the summary above.

  • New ``Pcsf3DWindow`` – the desktop app’s first 3-D/volume panel, rendering any .pcsf file through pcsf_to_point_cloud on a plain Matplotlib 3-D scatter (View > PCSF 3D Viewer, Ctrl+Shift+M), independent of the loaded EDI session.

  • New Web 3-D view “PCSF file” data source – fence/block/ depth-slice figures render directly from an uploaded .pcsf file with no session cache and no cached inversion result.

  • New ``MapView.from_pcsf``pycsamt.map.inversion.load_pcsf_lines() loads a grid2d/multiline PCSF file the same way MapView.from_inversion_results already loads a live ModEM folder; grid3d/mesh_unstructured files raise NotImplementedError by design (a real per-station curtain has no way to slice a native volume without a station table).

  • New ``examples/pcsf_conversion_demo/`` – converts real Occam2D, ModEM, and MARE2DEM sample data to .pcsf, with and without topography, round-trip-verifying every file.

  • New Survey-scale confidence plotting and export – ten public confidence visualizations cover geographic route/contour and grid maps, component diagnostics, method comparison, risk, heatmap, distribution, ranking, threshold-retention curves, and before/after assessment. export_confidence_map writes CSV and Surfer ASCII DSAA products.

  • New ``ModEmForwardControl``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 can be passed at all; see the summary above. InputBuilder now writes one for every 3-D build alongside covariance, and ModEmRunner.run/.command auto-write or reference a default one whenever covariance= is given without an explicit fwd_control=.

  • New Map View 2-D “Resistivity @ depth” for inversion surveyspycsamt.map.resistivity_at_depth() and pycsamt.map.inversion_depth_range() slice a loaded inversion result at a chosen depth; the Map view draws the result 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 StationMapOptions.show_markers / “Station markers” toggle.

  • New Map View 3-D station-label and smoothing optionsVolumeMapOptions.station_label_angle (rotate labels), station_label_fraction / station_label_names and max_stations (thin labels or name a subset without hiding markers; cap markers per line), and VolumeMapOptions.volume_smoothing – an opt-in NaN-aware Gaussian blur of the reconstructed block / iso-surface / anomaly volume, 0.0 (unchanged behaviour) by default. Exposed through the Map View “Stations (3-D)” and new “Smoothing” panels.

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

Fixed#

  • Fix ``ModEmModel3D`` silently discarded a real ModEM model’s grid centre and rotation – now exposed as ModEmModel3D.origin/ .rotation, matching read_mackie3d’s existing convention on the same class.

  • Fix Web 3-D view’s topography upload silently rejected a valid ``station_names`` column_parse_topo_upload now delegates to pycsamt.map.topo.parse_elevation_file() entirely instead of keeping an independently drifted copy.

  • Fix Confidence maps and profiles could present misleading survey geometry – multi-line maps now retain each station’s real longitude and latitude, and the L18/L22 examples use corrected coordinate-derived chainage. The regenerated L18 confidence profile is about 2.4 km long, not the former 5 km display.

  • Fix Occam2D and ModEM 3-D meshes wrote spurious air-layer rowsOccamMesh.from_data and ModEmModel3D.halfspace both inserted air-layer rows that Occam2D’s PW2D format and ModEM’s WS format never expect (both derive/hardcode air separately), crashing a real compiled Occam2D (”# params <> # free bricks”) and a real compiled Mod3DMT (“Mapping not supported yet in read_modelParam_WS”). Air rows/n_air are now always 0 for both.

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

  • Fix ModEM 3-D covariance file was unreachable from ``ModEmRunner`` – see the summary above; fixed via the new ModEmForwardControl.

  • Fix ``ModEmControl``/``ModEmForwardControl`` numeric fields lost their decimal point, silently corrupting real solver runs – Fortran’s G edit descriptor on input requires an explicit decimal point or the format’s own decimal-digit count re-places one; Python’s %g formatting omits it for whole numbers (10.0 -> "10") and many small exponentials (1e-7 -> "1e-07"). Confirmed live: a real Mod3DMT echoed a written 1e-7 back as 0.1000000E-13, and the already-running ModEM.inv had target_rms itself written as a bare "1" – a likely root cause of an earlier stuck-optimizer run (RMS frozen while lambda exploded to 1e9). Both writers now use %.6E, which always includes a literal .; new regression tests assert every float field parses with a decimal point present.

  • 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 section-smoothing spline refilled the mask before rendering. In-range cells now keep their exact hue and out-of-range cells become real holes, on an unchanged colour scale.

  • Fix Map View 3-D block and iso-surface rendered nothinggo.Volume/go.Isosurface were handed the topography drape’s NaN cells (which break both traces) and an iso window never clamped to the values present. Both now render under topography and under any filter; a resistivity-band selection is drawn as one closed iso-surface body of that band rather than a whole-model volume tinted by it.

  • Fix Map View 3-D depth slices – slices at a depth the model never sampled rendered as invisible all-NaN ghost surfaces; the colour scale re-graduated per slice; n_slices=1 cut at the window floor; and the resistivity filter, applied before the depth interpolation, bridged across an out-of-band layer and painted the slice solid. Slices are now kept 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 against the resistivity interpolated at the slice depth.

  • Fix Map View 3-D station markers and labels – block / iso-surface markers drifted outside the reconstructed block footprint, and rotated station labels ran through the marker glyph. Markers are clipped to the footprint; labels are bottom-anchored clear of the glyph.

  • Fix Surfer-style contour overlays carried an opaque white backgroundpycsamt.map.overlays.build_geo_contour_image() rasterised onto a white Matplotlib figure, so the “transparent” PNG hid the basemap everywhere outside the contoured hull. It is now genuinely transparent, and the 2-D station map figure drops its solid plot background.

  • Fix ``pycsamt-mapview`` flooded the console with rotating-log errors on Windows – the rotating file handlers were attached to the root logger, so werkzeug’s per-request logging filled infos.log and forced a rollover on nearly every request, and Windows cannot rename a log another request thread holds open. The werkzeug logger is now pinned at WARNING with no propagation, and SafeRotatingFileHandler skips a failed rollover.

Docs & tooling#

  • Docs ``user_guide/models/pcsf_format.rst`` – new page, registered in Classical model integrations; see the summary above.

  • Docs New “PCSF” glossary termPCSF, cross-referenced from the new page.

  • Docs Confidence-ratio guide and reproducible figure suiteQuality-Control Confidence Scoring now derives the composite and presence implementations, component discrepancies, spatial score, risk, before/after change, retention curves, and AUC without repeating the same definitions per plot. It explains and interprets every confidence figure, including interpolation and threshold limitations. The bundled examples/confidence/ workflow and a documentation generator recreate all publication-ready figures from the WILLY survey data.

Compatibility#

pycsamt.format is an entirely new, additive package with no interaction with any existing inversion engine, result object, or public API – it does not touch occam2d, modem, or mare2dem themselves, only reads their already-loaded result objects. PCSF is at format version 0.1.0 (see pycsamt.format.schema.PCSF_VERSION); no formal versioning policy has been finalized yet, and a .pcsf file should not be treated as a long-term archival format until one is.

The ModEmModel3D.origin/.rotation fix is additive and matches an existing convention already relied upon elsewhere in the same module (read_mackie3d); no existing caller’s behaviour changes except that origin/rotation are now always present (previously absent unless a model came from read_mackie3d) and, for a WS-format file that carries a real trailing centre/rotation line, now correctly populated instead of silently [0, 0, 0]/0.0. The web 3-D view topography-parser fix is a pure bug fix: any upload that previously failed silently due to a station_names column now parses correctly; no previously-working upload is affected.

The air-layer, ModEmControl column-width, and decimal-point fixes change on-disk file content written by OccamMesh.from_data/ ModEmModel3D.halfspace/ModEmControl.write/ ModEmForwardControl.write – all three changes make files that a real Occam2D/ModEM solver either could not run at all, or silently misread, now correct, so no working file set regresses; any existing ModEM.inv or forward-control file written by the previous %g-based formatting should be rewritten (rerun the builder, or call .write again) before its next real solver run. ModEmRunner.run/.command and InputBuilder.build/.build_from_data gain a purely additive, keyword-only fwd_control parameter; 3-D builds now also return a "fwd_control" key in the returned file mapping alongside "covariance" – code that reads specific keys out of that mapping is unaffected, but an exact set(files) == {...}/sorted(files) == [...] comparison against a 3-D build’s result should include "fwd_control".

Every Map View change is additive or a fix. New VolumeMapOptions / StationMapOptions fields (station_label_angle, station_label_fraction, station_label_names, volume_smoothing, depth, show_markers) default to the previous behaviour – volume_smoothing=0.0 is exactly the old block/iso reconstruction, and show_markers=True / station_label_fraction=1.0 change nothing. The 3-D fence/block/ iso/depth fixes change what renders for a resistivity or depth filter (previously it was wrong – recoloured, blank, or a solid ghost), never the unfiltered figure. The contour-overlay transparency fix affects build_geo_contour_image output for every scalar overlay, not only the new depth slice: a Surfer-style overlay that previously painted a white box behind the contours now shows the basemap through it.