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, andpcsf_to_point_cloud(),write_pcsf(),write_pcsm(),pcsf_to_pcsm(), andpcsm_to_pcsf(). See the summary above for the full scientific and encoding contract.New ``pycsamt format`` command group –
convert(any solver / AI result /.pcsf/.pcsm->.pcsf/.pcsm, source auto-detected),detect,info, andvalidate. The shared detector is public aspycsamt.format.detect_source()/SourceKind. See Format Commands and the summary above.New ``Pcsf3DWindow`` – the desktop app’s first 3-D/volume panel, rendering any
.pcsffile throughpcsf_to_point_cloudon 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
.pcsffile with no session cache and no cached inversion result.New ``MapView.from_pcsf`` –
pycsamt.map.inversion.load_pcsf_lines()loads agrid2d/multilinePCSF file the same wayMapView.from_inversion_resultsalready loads a live ModEM folder;grid3d/mesh_unstructuredfiles raiseNotImplementedErrorby 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_mapwrites CSV and Surfer ASCIIDSAAproducts.New ``ModEmForwardControl`` –
ModEmForwardControlwrites 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.InputBuildernow writes one for every 3-D build alongsidecovariance, andModEmRunner.run/.commandauto-write or reference a default one whenevercovariance=is given without an explicitfwd_control=.New Map View 2-D “Resistivity @ depth” for inversion surveys –
pycsamt.map.resistivity_at_depth()andpycsamt.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 aStationMapOptions.show_markers/ “Station markers” toggle.New Map View 3-D station-label and smoothing options –
VolumeMapOptions.station_label_angle(rotate labels),station_label_fraction/station_label_namesandmax_stations(thin labels or name a subset without hiding markers; cap markers per line), andVolumeMapOptions.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.SafeRotatingFileHandlerswallows 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, matchingread_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_uploadnow delegates topycsamt.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 rows –
OccamMesh.from_dataandModEmModel3D.halfspaceboth 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_airare now always0for both.Fix ``ModEmControl`` inversion-control file column misalignment – the key-field width (44 columns) pushed the colon past Fortran’s fixed
a36read boundary into the numeric field, causing “Bad value during floating point read” against a realModEM.invfile. 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
Gedit descriptor on input requires an explicit decimal point or the format’s own decimal-digit count re-places one; Python’s%gformatting omits it for whole numbers (10.0->"10") and many small exponentials (1e-7->"1e-07"). Confirmed live: a real Mod3DMT echoed a written1e-7back as0.1000000E-13, and the already-runningModEM.invhadtarget_rmsitself written as a bare"1"– a likely root cause of an earlier stuck-optimizer run (RMS frozen while lambda exploded to1e9). 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 nothing –
go.Volume/go.Isosurfacewere handed the topography drape’sNaNcells (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-
NaNghost surfaces; the colour scale re-graduated per slice;n_slices=1cut 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 background –
pycsamt.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.logand forced a rollover on nearly every request, and Windows cannot rename a log another request thread holds open. Thewerkzeuglogger is now pinned atWARNINGwith no propagation, andSafeRotatingFileHandlerskips 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 term – PCSF, cross-referenced from the new page.
Docs Confidence-ratio guide and reproducible figure suite – Quality-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.