2.23. pycsamt.format#
Backend-neutral PCSF and PCSM representations for electromagnetic inversion results, plus the human-readable PCBH borehole exchange contract. The package provides shared schemas, serialization, conversion adapters, model/borehole association, rendering contracts, and provenance helpers.
For format concepts, complete workflows, and browsable file examples, see PCSF — Common Subsurface Format. For spatial multi-hole projects, see PCBH — Common Borehole Format.
2.23.1. Public facade#
The most commonly used classes and functions are re-exported directly from
pycsamt.format.
pyCSAMT Common Subsurface Format (PCSF) — backend-neutral inversion results.
See PYCSAMT-PCSF-INVERSION-FORMAT-PLAN.md at the repository root
for the full design and phase-by-phase status.
Public second-level namespaces (importable as pycsamt.format.<name>
right after import pycsamt.format, not only via an explicit
from pycsamt.format.<name> import ...):
pycsamt.format.adapters— per-backend converters (adapters.occam2d_to_pcsf,adapters.modem3d_to_pcsf,adapters.mare2dem_to_pcsf), plus a solver-agnosticadapters.grid2d_to_pcsf/adapters.grid3d_to_pcsf/adapters.mesh_to_pcsffor any AI/DL inversion result (seepycsamt.format.adapters.genericandpycsamt.format.provenance).pycsamt.format.schema— the dataclasses re-exported at this top level (Grid2DGeometry,PCSFModel, …).pycsamt.format.io—read_pcsf/write_pcsf, also re-exported here.pycsamt.format.text—read_pcsm/write_pcsm/pcsf_to_pcsm/pcsm_to_pcsf: PCSM, the hand-editable ASCII sibling of a.pcsffile, also re-exported here.pycsamt.format.multiline,pycsamt.format.topography,pycsamt.format.pointcloud,pycsamt.format.regrid— likewise re-exported here.
- pycsamt.format.read_pcbh(path, *, validate=True, max_bytes=16777216, max_boreholes=10000, max_intervals=1000000, max_nesting=32)#
Read a canonical PCBH JSON file.
- Parameters:
path (path-like) – Input .pcbh.json file.
validate (bool, default True) – Run semantic validation before returning.
max_bytes (int) – Positive limits for untrusted input.
max_boreholes (int) – Positive limits for untrusted input.
max_intervals (int) – Positive limits for untrusted input.
max_nesting (int) – Positive limits for untrusted input.
- Returns:
Parsed and optionally validated document.
- Return type:
PCBHDocument
- Raises:
OSError – If the file cannot be read.
UnicodeError – If the file is not UTF-8.
ValueError – If JSON, structure, version, or limits are invalid.
PCBHValidationError – If semantic validation fails.
- pycsamt.format.write_pcbh(document, path, *, validate=True, indent=2)#
Atomically write a canonical PCBH JSON file.
- Parameters:
- Returns:
Destination path.
- Return type:
- Raises:
TypeError – If document is not a PCBH document.
ValueError – If validation, JSON values, or indent are invalid.
OSError – If the atomic write fails.
Notes
The temporary file is created beside the destination so
os.replace()remains an atomic same-filesystem operation.
- pycsamt.format.desurvey(borehole, *, boundaries=())#
Generate a 3-D centerline for borehole.
Survey stations after total depth are rejected by schema validation. If the first survey station is below the collar, its attitude is extended back to MD 0. Likewise, the final attitude is extended to total depth.
- pycsamt.format.boreholes_from_csv(path, *, columns=None, constants=None, strict=True, delimiter=None, document_id=None, created_by='pycsamt CSV importer', max_bytes=10485760, max_rows=250000)#
Import a combined collar-and-interval CSV as a PCBH document.
- Parameters:
path (path-like) – UTF-8 CSV containing repeated collar fields and interval rows.
columns (dict, optional) – Explicit
{canonical_field: source_header}mapping. Canonical names use dotted paths such asborehole.idandinterval.from_md.constants (dict, optional) – Constant canonical values, commonly
crs.horizontal. Constants take precedence over mapped row values.strict (bool, default=True) – Raise
PCBHCSVImportErrorif any error is recorded. In permissive mode, invalid rows are rejected and valid rows returned.delimiter ({',', ';', 't', '|'}, optional) – Explicit delimiter. If omitted, detection is restricted to this set.
document_id (str, optional) – PCBH document identifier. Defaults to
csv:<file stem>.created_by (str, default='pycsamt CSV importer') – Provenance name stored on the document.
max_bytes (int, default=10485760) – Maximum source size in bytes.
max_rows (int, default=250000) – Maximum number of data rows.
- Returns:
document (PCBHDocument) – Valid document containing every accepted row.
report (ImportReport) – Source checksum, mappings, inferences, counts, and diagnostics.
- Raises:
PCBHCSVImportError – If the file structure is unusable, no valid boreholes remain, or strict mode records an error. The exception exposes
report.ValueError – If a resource limit or delimiter parameter is invalid.
- Return type:
tuple[PCBHDocument, ImportReport]
Notes
Missing tokens are normalized before conversion and never stringified. Repeated collar and total-depth values must agree within a borehole.
- pycsamt.format.boreholes_from_csv_directory(directory, *, manifest='import.yaml', strict=True, document_id=None, created_by='pycsamt relational CSV importer')#
Import joined collar, survey, log, structure, sample, assay tables.
- pycsamt.format.write_csv_directory(document, directory)#
Export supported PCBH content as a relational CSV directory.
- pycsamt.format.borehole_from_las(path, *, collar, crs_horizontal, kind='unknown', status='unknown', depth_curve='DEPT', resistivity_curve='RESD', lithology_curve='LITH', max_samples=250000)#
Import a LAS 2.0 curve subset while retaining curve metadata.
- pycsamt.format.write_las_subset(borehole, path, *, null_value=-9999.25, company='pycsamt')#
Write inline PCBH curves, or an interval-derived LAS subset.
- pycsamt.format.build_render_model(document, *, family='lithology', selected_ids=None, radius_policy=None, lod_tolerance=0.0, sampling_step_md=10.0, max_points_per_hole=10000)#
Build a render model with a short-lived builder.
Parameters mirror
BoreholeRenderBuilder.build(). Distances are in the document coordinate/depth unit and no CRS transformation is applied.
- pycsamt.format.embed_pcbh(model, document, *, uri=None)#
Return a model copy with a validated portable PCBH attachment.
- pycsamt.format.reference_pcbh(model, uri, sha256)#
Return a model copy with an external PCBH reference.
- pycsamt.format.extract_pcbh(model)#
Return embedded PCBH data without resolving external resources.
- Parameters:
model (PCSFModel)
- Return type:
PCBHDocument | None
- pycsamt.format.align_pcbh_to_pcsf(document, model, *, vertical_offset=None, sampling_step_md=10.0)#
Transform PCBH paths into PCSF x/y/depth coordinates.
vertical_offsetis required when PCBH and PCSF vertical references cannot be established as identical. It is added to PCBH elevations before conversion to PCSF depth-positive-down coordinates.
- pycsamt.format.write_geojson(document, path, *, target_crs='EPSG:4326', include_z=True)#
Write WGS84 collar Points and trajectory LineStrings as GeoJSON.
- pycsamt.format.write_vtp(document, path, *, family='lithology', sides=8)#
Write interval tubes as ASCII VTK XML PolyData (
.vtp).
- pycsamt.format.write_gltf(document, path, *, family='lithology', sides=8)#
Write browser-ready glTF 2.0 (
.gltf) or binary GLB tubes.
- class pycsamt.format.SourceKind(category, path, is_dir, backend=None, geometry=None, target_geometry=None, confidence='high', detail='', hints=<factory>)#
Bases:
objectOutcome of
detect_source().- Variables:
category (str) –
"pcsf"|"pcsm"|"solver"|"ai_arrays"|"unknown".path (pathlib.Path) – The path that was probed (a file, or a solver working directory).
is_dir (bool) – Whether path is a directory.
backend (str or None) –
"occam2d"|"modem"|"mare2dem"forsolversources, elseNone.geometry (str or None) – For
pcsf/pcsm: the peeked geometry kind.target_geometry (str or None) – For
solver/ai_arrays: the PCSF geometry the conversion will produce.confidence (str) –
"high"|"medium"|"low".detail (str) – Human-readable one-line explanation.
hints (dict) – Extra paths / keys the converter needs (see module docstring).
- Parameters:
- pycsamt.format.detect_source(path, *, solver_hint=None)#
Detect what path is and how it should convert to PCSF/PCSM.
- Parameters:
path (path-like) – A
.pcsf/.pcsm/.npz/.npyfile, a solver-specific file (.iter,.rho,.poly,.resistivity, …), or a solver working directory.solver_hint ({"occam2d", "modem", "mare2dem"}, optional) – Force the solver backend instead of fingerprinting it. Ignored for PCSF/PCSM/array sources.
- Return type:
- Raises:
FileNotFoundError – If path does not exist.
- pycsamt.format.describe_source(sk)#
Return a short multi-line human summary of sk.
- Parameters:
sk (SourceKind)
- Return type:
- class pycsamt.format.Grid2DGeometry(x, z, x_nodes=None, z_nodes=None, origin=None, azimuth_deg=None)#
Bases:
PyCSAMTObjectSingle-profile rectilinear geometry (Occam2D / DUHI-via-Occam2D).
- Parameters:
x (ndarray (n_x,)) – Real station chainage, metres — never a solver’s mesh-local frame (see the Occam2D coordinate-frame note in
pycsamt.interp._base.ResistivityModel.from_occam2d()).z (ndarray (n_z,)) – Depth cell centres, metres, positive downward.
x_nodes (ndarray, optional) – Cell-edge coordinates, one longer than x/z.
z_nodes (ndarray, optional) – Cell-edge coordinates, one longer than x/z.
origin (ndarray (2,), optional) – Real-world offset when x is locally referenced.
azimuth_deg (float, optional) – Profile bearing, for georeferencing back to the survey line.
- validate()#
Validate object state.
Subclasses can override this hook. The base implementation intentionally accepts all states.
- Return type:
None
- class pycsamt.format.Grid3DGeometry(x, y, z, x_nodes=None, y_nodes=None, z_nodes=None, origin=None, rotation_deg=0.0, n_air=0)#
Bases:
PyCSAMTObjectNative 3-D tensor volume geometry (ModEM).
- Parameters:
x (ndarray) – Cell-centre coordinates, metres.
y (ndarray) – Cell-centre coordinates, metres.
z (ndarray) – Cell-centre coordinates, metres.
x_nodes (ndarray, optional) – Cell-edge coordinates.
y_nodes (ndarray, optional) – Cell-edge coordinates.
z_nodes (ndarray, optional) – Cell-edge coordinates.
origin (ndarray (3,), optional) – Real-world grid origin.
rotation_deg (float, default 0.0) – Grid rotation about the vertical axis.
n_air (int, default 0) – Explicit air-layer count (unlike Occam2D’s inferred count).
- validate()#
Validate object state.
Subclasses can override this hook. The base implementation intentionally accepts all states.
- Return type:
None
- class pycsamt.format.UnstructuredMeshGeometry(nodes, connectivity, region_ids, plane='xz')#
Bases:
PyCSAMTObjectNative unstructured triangular mesh geometry (MARE2DEM).
Preserves the mesh as-is (no forced regrid onto a tensor grid), so a MARE2DEM result keeps its real element resolution.
- Parameters:
nodes (ndarray (n, 2) or (n, 3)) – Node coordinates, metres.
connectivity (ndarray (m, 3), int) – Triangle node indices.
region_ids (ndarray (m,), int) – Region id per triangle.
plane ({"xz", "xy", "3d"}, default "xz") – Physical plane the mesh lives in. MARE2DEM profiles are conventionally in
(y, z)but stored generically asplane="xz"with x holding the profile’s own along-line coordinate.
- validate()#
Validate object state.
Subclasses can override this hook. The base implementation intentionally accepts all states.
- Return type:
None
- class pycsamt.format.LineEntry(line_id, geometry, resistivity, offset_y=0.0, offset_kind='synthetic', azimuth_deg=None)#
Bases:
PyCSAMTObjectOne profile within a
MultilineGeometry.- Parameters:
line_id (str) – Unique identifier for this line.
geometry (Grid2DGeometry) – The line’s own 2-D section geometry.
resistivity (ndarray (n_z, n_x)) – Canonical linear ohm.m resistivity for this line.
offset_y (float) – Cross-line position, metres.
offset_kind ({"real", "synthetic"}, default "synthetic") – Whether offset_y comes from real survey geometry or is a placeholder spacing for display only.
azimuth_deg (float, optional) – Line bearing.
- geometry: Grid2DGeometry#
- validate()#
Validate object state.
Subclasses can override this hook. The base implementation intentionally accepts all states.
- Return type:
None
- class pycsamt.format.DerivedVolume(grid, resistivity, derivation_method='linear_interp', derived_from=<factory>, synthesized=True)#
Bases:
PyCSAMTObjectOptional cached 3-D volume synthesized from stacked lines.
Kept explicitly tagged as synthesized so a reader never mistakes a stack-interpolated volume for a native 3-D inversion (see
derivation_method/synthesizedin the design plan’s §2).- Parameters:
- grid: Grid3DGeometry#
- validate()#
Validate object state.
Subclasses can override this hook. The base implementation intentionally accepts all states.
- Return type:
None
- class pycsamt.format.MultilineGeometry(lines=<factory>, derived_volume=None)#
Bases:
PyCSAMTObjectA set of profiles plus real line geometry (fence/block views).
Formalizes what
pycsamt/app/web/callbacks/map3d.pycurrently reconstructs at render time from a stack of independent 2-D sections. Each line carries its own resistivity; the optionalderived_volumeis a documented, reproducible synthesis rather than a render-time-only side effect.- Parameters:
derived_volume (DerivedVolume | None)
- derived_volume: DerivedVolume | None = None#
- validate()#
Validate object state.
Subclasses can override this hook. The base implementation intentionally accepts all states.
- Return type:
None
- class pycsamt.format.StationTable(name=<factory>, x=<factory>, y=<factory>, z=<factory>, line_id=None, lon=None, lat=None)#
Bases:
PyCSAMTObjectSurvey station positions, shared across geometry kinds.
x/y/zare geometry-local (along-profile chainage forgrid2d, the model grid’s own frame forgrid3d, whatever frame the caller supplied formesh_unstructured) — never assumed to be real-world geographic coordinates, per SPEC.md’s ownload_pcsf_linesconvention.lon/lat, when present, are the one explicit, unambiguous carrier of real-world position: WGS84 decimal degrees, the same convention every other real-coordinate source in this codebase already uses (EDI headers, a ModEM.datfile’sGG_Lat/GG_Loncolumns,pycsamt.map._core.StationRecord). A single-linegrid2d(or any other kind’s) PCSF file that sets these needs no separateknown_stationsmatch to place its stations on a real basemap.- Parameters:
- validate()#
Validate object state.
Subclasses can override this hook. The base implementation intentionally accepts all states.
- Return type:
None
- class pycsamt.format.TopographyPerStation(station_id=<factory>, elevation=<factory>)#
Bases:
PyCSAMTObjectScalar-per-station topography (matches the existing convention in
pycsamt.map.topo).- validate()#
Validate object state.
Subclasses can override this hook. The base implementation intentionally accepts all states.
- Return type:
None
- class pycsamt.format.TopographyRaster(x, y, elevation)#
Bases:
PyCSAMTObjectGridded-DEM topography — a regular elevation surface independent of any station table.
Unlike
TopographyPerStation, this carries no station identifiers at all: it is a standalone terrain surface a consumer can sample at any coordinate, not a per-station lookup table. It introduces no GDAL/rasterio-class dependency — construction is via plainx/y/elevationarrays a caller has already obtained by whatever means it likes (seepycsamt.format.topography.topography_from_grid()); PCSF itself never parses a georeferenced raster file format.- Parameters:
x (ndarray (n_x,)) – Grid x-coordinates (or longitude), increasing.
y (ndarray (n_y,)) – Grid y-coordinates (or latitude), increasing.
elevation (ndarray (n_y, n_x)) – Elevation surface, metres, sampled on the
(y, x)meshgrid implied by x/y — the same row-major conventionnumpy.meshgrid(x, y)(defaultindexing="xy") produces.
- validate()#
Validate object state.
Subclasses can override this hook. The base implementation intentionally accepts all states.
- Return type:
None
- class pycsamt.format.PCSFModel(geometry, resistivity=None, resistivity_native=None, resistivity_native_encoding=None, uncertainty=None, sensitivity=None, resistivity_by_region=None, resistivity_by_node=None, stations=None, topography=None, survey=<factory>, history=<factory>, source_backend='generic', created_by='', created_at='', crs=None, description='', metadata=<factory>, boreholes=None)#
Bases:
PyCSAMTObject,MetadataMixinBackend-neutral inversion-result container (one PCSF file).
- Parameters:
geometry (Grid2DGeometry | Grid3DGeometry | UnstructuredMeshGeometry | MultilineGeometry) – The model’s geometry, discriminated by
geometry.kind.resistivity (ndarray, optional) – Canonical linear ohm.m resistivity. Required for
grid2d/grid3d/mesh_unstructuredgeometries; must beNoneformultiline(each line carries its own resistivity — seeLineEntry).resistivity_native (ndarray, optional) – Passthrough of the source backend’s own array, for provenance.
resistivity_native_encoding ({"log10", "ln", "linear"}, optional) – Encoding of resistivity_native. Required whenever resistivity_native is given — never assumed.
uncertainty (ndarray, optional) – Same shape as resistivity, when available from the source.
sensitivity (ndarray, optional) – Same shape as resistivity, when available from the source.
resistivity_by_region (ndarray, optional) – Per-region resistivity table (
mesh_unstructuredonly), alongside the per-cell resistivity expanded from it.resistivity_by_node (ndarray, optional) – Per-node resistivity table (
mesh_unstructuredonly), shape(n_nodes,)– the natural output shape of a graph-based model (e.g. a GCN) that predicts one value per mesh vertex rather than per cell. Kept alongside the per-cell resistivity expanded from it (seepycsamt.format.adapters.generic.mesh_to_pcsf()), the same provenance relationship resistivity_by_region has to its own per-cell expansion.stations (StationTable, optional)
topography (TopographyPerStation | TopographyRaster, optional)
survey (dict) – Free-form survey metadata. Adapters populate this from
pycsamt.metadataobjects (SurveyMeta,BBox,ProvenanceMeta) via their own dict conversion; PCSF itself does not require a specific metadata class here.history (dict of ndarray) – Optional per-iteration series (e.g.
{"rms": ..., "lambda": ...}) from anInversionHistory-like source.source_backend (str, default "generic") –
"occam2d"|"modem3d"|"mare2dem"|"duhi"|"generic".crs (str, optional) – A pyproj-compatible CRS string.
created_by (str)
created_at (str)
description (str)
boreholes (Any | None)
- geometry: Grid2DGeometry | Grid3DGeometry | UnstructuredMeshGeometry | MultilineGeometry#
- stations: StationTable | None = None#
- topography: TopographyPerStation | TopographyRaster | None = None#
- validate()#
Validate object state.
Subclasses can override this hook. The base implementation intentionally accepts all states.
- Return type:
None
- pycsamt.format.read_pcsf(path)#
Read a
PCSFModelback from a.pcsf(HDF5) file.- Parameters:
path (path-like) – Source file.
- Returns:
Fully reconstructed and re-validated model.
- Return type:
- Raises:
ValueError – If
pcsf_versionis missing, malformed, or names an unrecognised MAJOR version (seepycsamt/format/SPEC.mdsection 5); ifgeometry/kindis missing or not a recognised value; or if the reconstructed model failsPCSFModel.validate().- Warns:
UserWarning – If the file’s
pcsf_versionMINOR component is newer than this reader’s — fields added since then are silently ignored rather than causing a hard failure.
- pycsamt.format.write_pcsf(model, path)#
Write a
PCSFModelto a.pcsf(HDF5) file.- Parameters:
model (PCSFModel) – The model to serialize. Validated before anything is written.
path (path-like) – Destination file. Parent directories are created if missing.
- Returns:
The path written to.
- Return type:
- Raises:
ValueError – If model fails
PCSFModel.validate().
Examples
>>> import numpy as np >>> from pycsamt.format import Grid2DGeometry, PCSFModel, write_pcsf, read_pcsf >>> geometry = Grid2DGeometry(x=np.array([0.0, 100.0]), z=np.array([10.0, 50.0])) >>> model = PCSFModel( ... geometry=geometry, ... resistivity=np.array([[100.0, 120.0], [50.0, 60.0]]), ... source_backend="occam2d", ... ) >>> path = write_pcsf(model, "example.pcsf") >>> round_tripped = read_pcsf(path)
- pycsamt.format.read_pcsm(path)#
Read a
PCSFModelback from a.pcsm(ASCII) file.- Parameters:
path (path-like) – Source file.
- Returns:
Fully reconstructed and re-validated model.
- Return type:
- Raises:
ValueError – If
PCSM_VERSIONis missing, malformed, or names an unrecognised MAJOR version (seepycsamt/format/SPEC.mdsection 5), if the file is otherwise malformed, or if the reconstructed model failsPCSFModel.validate().
- pycsamt.format.write_pcsm(model, path, *, log10_view=False)#
Write a
PCSFModelto a.pcsm(ASCII) file.The same model
write_pcsf()would encode as HDF5, encoded instead as human-readable, hand-editable text. See the module docstring for the grammar.Every resistivity block carries its own encoding as an inline comment right next to the data, not only in a separate header line – e.g.
RESISTIVITY # linear ohm.m (canonical, ...)– so a reader never has to guess or scroll elsewhere to tell linear from log10. The canonicalRESISTIVITYfield is always linear ohm.m (PCSF’s own binding rule, SPEC.md section 2): this cannot be changed by an option, since a reader/consumer relies on that invariant unconditionally, and reversing a10**log10(x)round-trip is not guaranteed bit-exact the way the rest of this format’srepr()-based float round-trip is.Pass
log10_view=Trueto additionally write aRESISTIVITY_LOG10block (and, for amultilinefile, one per line) alongside it – a convenience for reading resistivity in the log space many EM inversions (Occam2D among them) actually work in. This is a write-only, clearly-labelled derived view:read_pcsm()discards it, it never becomes part of the returnedPCSFModel, and it never influences the canonicalRESISTIVITYblock. (An Occam2D-sourced model already carries its own real log10 array asRESISTIVITY_NATIVEwheneverresistivity_nativeis set – that block is unaffected by this option and is written either way.)A path ending in
.gz(e.g.model.pcsm.gz) is written gzip-compressed. Large volumes (a nativegrid3dor a densemesh_unstructuredmesh) produce large plain-text files – the same trade-off ModEM’s own ASCII.rhoformat already accepts – and gzip substantially reduces that in practice (measured on the real, bundledwilly_27freq_watex_line02_sample590,400-cell ModEM volume, seeexamples/pcsm_conversion_demo: 19.7 MB plain, 3.9 MB gzipped – 5.0x smaller, real inverted resistivity compressing far better than synthetic/random test data would – vs. 4.1 MB for the equivalent.pcsf). A gzipped file is no longer something to casually open in a text editor, so this stays opt-in rather than the default: PCSM’s purpose is hand-editability, which a compressed file gives up.- Parameters:
model (PCSFModel) – The model to serialize. Validated before anything is written.
path (path-like) – Destination file. Parent directories are created if missing. A
.gzsuffix writes a gzip-compressed file.log10_view (bool, default False) – Also write a
RESISTIVITY_LOG10convenience block (see above). Off by default so existing files/output size are unaffected unless explicitly requested.
- Returns:
The path written to.
- Return type:
Examples
>>> import numpy as np >>> from pycsamt.format import Grid2DGeometry, PCSFModel >>> from pycsamt.format.text import write_pcsm, read_pcsm >>> geometry = Grid2DGeometry(x=np.array([0.0, 100.0]), z=np.array([10.0, 50.0])) >>> model = PCSFModel( ... geometry=geometry, ... resistivity=np.array([[100.0, 120.0], [50.0, 60.0]]), ... source_backend="occam2d", ... ) >>> path = write_pcsm(model, "example.pcsm") >>> round_tripped = read_pcsm(path) >>> gz_path = write_pcsm(model, "example.pcsm.gz") >>> with_log10 = write_pcsm(model, "example_log10.pcsm", log10_view=True)
- pycsamt.format.pcsf_to_pcsm(pcsf_path, pcsm_path, *, log10_view=False)#
Convert a
.pcsf(HDF5) file to a.pcsm(ASCII) file.log10_viewis passed through towrite_pcsm()— see there.
- pycsamt.format.pcsm_to_pcsf(pcsm_path, pcsf_path)#
Convert a
.pcsm(ASCII) file to a.pcsf(HDF5) file.
- pycsamt.format.read_pcsf_or_pcsm(path)#
Read a
PCSFModelfrom either encoding, by extension.A single entry point for a consumer (e.g.
app/mapview’s inversion-result importer) that wants to accept whichever of the two lossless PCSF encodings (seeSPEC.mdS9) a user hands it, without duplicating the.pcsfvs..pcsm/.pcsm.gzdispatch itself. Extension is the only signal used – content is never sniffed – matching every other reader in this package.- Parameters:
path (path-like) – A
.pcsf(binary HDF5),.pcsm(ASCII), or.pcsm.gz(gzip-compressed ASCII) file.- Raises:
ValueError – If path’s extension is none of the three recognised forms.
- Return type:
- pycsamt.format.peek_kind(path)#
Return a PCSF/PCSM file’s
geometry.kindwithout loading any array data – just the root/geometry attributes for.pcsf, or theGEOMETRY_KINDheader line for.pcsm/.pcsm.gz.Meant for a caller that wants to label several candidate files (e.g. a file picker UI, deciding which are
grid2d/multilineand therefore importable by a given consumer) before committing to a fullread_pcsf_or_pcsm()load of any one of them.- Raises:
ValueError – If path’s extension is not recognised, or the kind attribute/header cannot be found (e.g. a truncated file).
- Parameters:
- Return type:
- pycsamt.format.build_multiline_pcsf(profiles, *, line_spacing=1.0, fallback_unit=1000.0, cache_derived_volume=True, topo=None, epsg=None, utm_zone=None, latlon=False, on_mismatch='raise', survey=None, source_backend='generic', created_by='', crs=None, description='')#
Build a
multilinePCSFModelfrom a profiles dict.- Parameters:
profiles (mapping of str to mapping) –
{line_name: {"x": (n_x,), "z": (n_z,), "rho": (n_z, n_x), ...}}, the exact shapemap3d.py’s_profiles_from_pseudo/_profiles_from_inversion_resultalready produce.rhomust already be linear ohm.m (call_rho_log_to_ohm_m-equivalent conversion first, matching every other PCSF adapter’s canonical-linear convention). Optional per-line keyssta_x,sta_names,sta_elev,sta_lat,sta_lonpopulatePCSFModel.stations/PCSFModel.topographywhen present.sta_lat/sta_londo double duty:line_offsets_from_stations()uses them (viapycsamt.map.geometry.survey_uv()) to compute each line’s real cross-strike offset when every line carries them, falling back to a synthetic index-based stack otherwise (seeoffset_kindbelow) – and the same values are also persisted intoPCSFModel.stations’lon/lat, so a loaded multiline file is self-sufficiently geo-referenced too, not just correctly spaced.line_spacing (float) – Forwarded to
stack_lines_to_common_grid()for the optional cachedDerivedVolume(real per-line geometry itself never depends on these — only the synthetic offset fallback does).fallback_unit (float) – Forwarded to
stack_lines_to_common_grid()for the optional cachedDerivedVolume(real per-line geometry itself never depends on these — only the synthetic offset fallback does).cache_derived_volume (bool, default True) – When
Trueand there are at least two lines, also cache aDerivedVolume(each line resampled onto a common grid) so a large multiline file does not need to re-resample on every render. SetFalseto keep the file smaller when that convenience volume is not needed.topo (optional) – A “smart” real-coordinate source resolved via
pycsamt.format.topo_source.resolve_topo()– seepycsamt.format.adapters.occam2d.occam2d_to_pcsf()’s identical parameter for the full description of accepted source types. Populates each line’s ownsta_lat/sta_lonbefore the real-offset computation above runs, so passing topo is enough to get both a real cross-strike offset per line and a self-georeferenced file – no separate offset step is needed. Accepts either a single source matched by station name across every line combined (a .stn/.csv/Sites source covering the whole survey), or a{line_name: source}mapping / one-source-per-line sequence (inprofiles’s own key order) for e.g. one.blnfile per surveyed line. Takes precedence over anysta_lat/sta_lonalready present in profiles for every station it resolves (with aUserWarningif both were supplied). If topo only partially covers a line’s stations, the resultingnanentries makeline_offsets_from_stations()fall back to a synthetic offset for every line (mixing a real and a synthetic stack would look inconsistent – see that function’s own all-or-nothing behaviour) rather than silently using a partially-real one; the stations themselves still keep whatever real lon/lat topo did resolve.None(the default) leaves this function’s behaviour exactly as it was before topo existed.epsg (int | None) – Forwarded to
pycsamt.format.topo_source.resolve_topo(); see occam2d_to_pcsf’s identical parameters.utm_zone (Any | None) – Forwarded to
pycsamt.format.topo_source.resolve_topo(); see occam2d_to_pcsf’s identical parameters.latlon (bool) – Forwarded to
pycsamt.format.topo_source.resolve_topo(); see occam2d_to_pcsf’s identical parameters.on_mismatch (str) – Forwarded to
pycsamt.format.topo_source.resolve_topo(); see occam2d_to_pcsf’s identical parameters.survey (Any | Mapping[str, Any] | None) – Passed straight through to
PCSFModel.source_backend (str) – Passed straight through to
PCSFModel.
- Raises:
ValueError – If profiles is empty, or any line is missing
x/z/rho.- Return type:
Examples
>>> import numpy as np >>> from pycsamt.format.multiline import build_multiline_pcsf >>> profiles = { ... "L1": {"x": np.array([0.0, 100.0]), "z": np.array([10.0, 50.0]), ... "rho": np.array([[100.0, 110.0], [50.0, 55.0]])}, ... "L2": {"x": np.array([0.0, 100.0]), "z": np.array([10.0, 50.0]), ... "rho": np.array([[200.0, 210.0], [90.0, 95.0]])}, ... } >>> model = build_multiline_pcsf(profiles, cache_derived_volume=False) >>> model.kind 'multiline' >>> [line.line_id for line in model.geometry.lines] ['L1', 'L2']
- pycsamt.format.multiline_pcsf_to_profiles(model)#
Reconstruct a profiles dict from a
multilinePCSFModel.The exact inverse of
build_multiline_pcsf()’s per-line conversion — the result is a drop-in alternate data source formap3d.py’s existing_build_fence_fig/_assemble_3d_grid/_build_block_fig, which only need the{"x", "z", "rho", ...}shape, not any particular origin.- Parameters:
model (PCSFModel) – Must have
geometry.kind == "multiline".- Returns:
{line_id: {"x", "z", "rho", "sta_x", "sta_names", "sta_elev", "sta_lat", "sta_lon"}}. Station keys are populated fromPCSFModel.stationswhen present (matched to each line viaStationTable.line_id);sta_lat/sta_loncome fromStationTable.lat/.lonwhen the file has them (seebuild_multiline_pcsf()), else stay empty lists – an older file written before those fields existed round-trips the same way it always did.- Return type:
- Raises:
ValueError – If model is not a
multilinegeometry.
- pycsamt.format.line_offsets_from_stations(profiles)#
Cross-strike offset (m) for each line, from real station lat/lon.
Same algorithm as
map3d.py’s private_line_real_offsets(built onpycsamt.map.geometry.survey_uv()), so both the live-cache rendering path and this persisted-file path place lines identically. Requires every line to carrysta_lat/sta_lon/sta_namesof equal length; returnsNoneotherwise so callers fall back to a synthetic index-based stack viaresolve_offset().
- pycsamt.format.stack_lines_to_common_grid(profiles, *, line_spacing=1.0, fallback_unit=1000.0)#
Resample every line onto the first line’s own (x, z) grid.
Same algorithm as
map3d.py’s private_assemble_3d_grid: the first profile’s grid is the reference; other lines are resampled onto it via_resample_line_to_grid().- Returns:
x_arr, z_arr (ndarray) – The reference line’s own coordinates.
y_arr (ndarray, shape (n_lines,)) – Per-line cross-strike offset, from
line_offsets_from_stations()when available, otherwise a syntheticidx * spacing * fallback_unitstack (seepycsamt.map.geometry.resolve_offset()).rho_3d (ndarray, shape (n_lines, n_x, n_z))
- Parameters:
- Return type:
- pycsamt.format.topography_from_map_data(data)#
Build topography from a
MapData’s own station elevations (typically real, EDI-derived values).- Parameters:
data (MapData) – Survey data, e.g. from
pycsamt.map.load_lines().- Returns:
Nonewhen no station carries a finite elevation, so callers can leavePCSFModel.topographyunset rather than persisting an all-nantable.- Return type:
TopographyPerStation or None
Examples
>>> from pycsamt.map import load_lines >>> from pycsamt.format.topography import topography_from_map_data >>> data = load_lines("data/AMT/WILLY_DATA", detect="folder") >>> topo = topography_from_map_data(data)
- pycsamt.format.topography_from_elevation_file(content, filename)#
Build topography from an uploaded elevation file.
Thin wrapper around
pycsamt.map.topo.parse_elevation_file()(CSV / HDF5 / NPZ, flexible station-id and elevation column/array names) — the same parser the “Upload file” elevation source inpycsamt.app.webuses, so a file that works there also works here.- Parameters:
- Returns:
Nonewhen the file cannot be parsed (unrecognised format, missing id/elevation column) — matchesparse_elevation_file()’s own best-effort, non-raising contract.- Return type:
TopographyPerStation or None
- pycsamt.format.topography_to_elev_map(topo)#
Return
{station_id: elevation}, the inverse of both builders.The same shape
pycsamt.map.topo.apply_elevations()andpycsamt.map.topo.parse_elevation_file()already use, so a PCSF file’s topography can be applied straight back onto aMapDatawith no extra conversion.- Parameters:
topo (TopographyPerStation)
- Return type:
- pycsamt.format.topography_from_grid(x, y, elevation)#
Build a gridded-DEM
TopographyRaster.- Parameters:
- Return type:
Examples
>>> import numpy as np >>> from pycsamt.format.topography import topography_from_grid >>> x = np.linspace(0.0, 500.0, 6) >>> y = np.linspace(0.0, 300.0, 4) >>> elevation = 100.0 + 0.01 * np.add.outer(y, x) >>> topo = topography_from_grid(x, y, elevation) >>> topo.elevation.shape (4, 6)
- pycsamt.format.topography_raster_to_grid(topo)#
Return
(x, y, elevation), the inverse oftopography_from_grid().- Parameters:
topo (TopographyRaster)
- Return type:
- class pycsamt.format.TopoTable(lon, lat, elevation=None, names=None, source='<unknown>')#
Bases:
objectA parsed topo source, before it is matched to any station names.
- Variables:
names (list of str, optional) – Station identity carried by the source itself.
Nonefor a positional-only source (e.g. a bare.bln).lat (lon,) – WGS84 decimal degrees — already converted from easting/northing if the source needed that.
elevation (ndarray, shape (n,), optional) – Metres,
nanwhere genuinely unknown.source (str) – Human-readable provenance (file path, or a short description for an in-memory source), surfaced in error/warning messages.
- Parameters:
- class pycsamt.format.TopoAttribution(lon=<factory>, lat=<factory>, elevation=<factory>, matched=<factory>, unmatched_stations=<factory>, source='none')#
Bases:
objectPer-station real coordinates resolved from a topo source, ready to populate
lon/lat(and merge into elevation).- Parameters:
- pycsamt.format.read_topo_file(path, *, epsg=None, utm_zone=None, latlon=False)#
Parse a
.bln/.csv/.stntopo file into aTopoTable.- Parameters:
path (path-like) – A
.bln,.csv/.txt, or.stnfile.epsg (int, optional) – EPSG code of the source’s projected CRS, when it stores easting/northing rather than lat/lon. Takes precedence over utm_zone when both are given (matches
pycsamt.gis.utils.to_ll()’s own precedence).utm_zone (optional) – UTM zone designator (e.g.
"32N"), an alternative to epsg.latlon (bool, default False) –
.blnonly: setTruewhen the file’s ownx, ycolumns are alreadylon, lat(a.blncarries no CRS metadata to detect this from). Ignored for.csv/.stn, which are only treated as already-geographic when their own header sayslat/lon.
- Raises:
ValueError – Unrecognised extension, a malformed
.blnheader/body, no recognisable coordinate columns in a.csv/.stnfile, or projected coordinates with neither epsg nor utm_zone.- Return type:
- pycsamt.format.topo_from_sites(source)#
Extract lon/lat/elevation from an already-geo-located
Sites/MapData/iterable-of-station-record object.Duck-typed on purpose: works with anything iterable whose items (or whose
.stationsattribute’s items, for aMapData-like container) expose a station identifier (id/name/station) pluslongitude/latitudeand, optionally,elevation–pycsamt.map._core.StationRecordand similar objects all qualify without any adapter code.- Raises:
ValueError – If no station in source carries a usable id + lon/lat pair.
- Parameters:
source (Any)
- Return type:
- pycsamt.format.attribute_topo(topo, station_names, *, on_mismatch='raise')#
Match a parsed
TopoTableonto station_names.Name-based when
topo.namesis populated (exact match, then a normalized fallback); positional (in order, requiring an exact count match) otherwise. See the module docstring for the full rationale.- Parameters:
on_mismatch ({"raise", "warn"}, default "raise") – Only consulted for a positional (name-less) source.
"raise"rejects a station-count mismatch outright;"warn"issues aUserWarningand attributes only the overlapping prefix (min(topo.n, len(station_names))points, in order).topo (TopoTable)
- Return type:
- pycsamt.format.resolve_topo(topo, station_names, *, epsg=None, utm_zone=None, latlon=False, on_mismatch='raise')#
Resolve any accepted
topo=argument into aTopoAttributionagainst station_names.- Parameters:
topo (None, path-like, TopoTable, Sites/MapData-like, mapping, or sequence) –
None– returns an empty attribution (every adapter’s existingstation_elevations/station_lonlatbehaviour is unaffected).a
.bln/.csv/.stnpath, or an already-parsedTopoTable.a
Sites/MapData/iterable-of-station-record object (seetopo_from_sites()).a plain
{station_name: (lon, lat[, elevation])}mapping.when station_names is a mapping (multiline,
{line_id: [names, ...]}): a{line_id: <any of the above>}mapping, or a sequence with exactly one source per line, in the same order as station_names’s own keys. A single non-mapping, non-per-line-sequence source is instead matched by name across all lines’ stations combined – the natural choice for one combined.stn/.csv/Sites source covering a whole multiline survey.
station_names (sequence of str, or mapping of str to sequence of str) – The inversion’s own station names (flat), or
{line_id: names}for a multiline build.epsg (int | None) – Forwarded to
read_topo_file()/attribute_topo()for every file-based source encountered.utm_zone (Any | None) – Forwarded to
read_topo_file()/attribute_topo()for every file-based source encountered.latlon (bool) – Forwarded to
read_topo_file()/attribute_topo()for every file-based source encountered.on_mismatch (str) – Forwarded to
read_topo_file()/attribute_topo()for every file-based source encountered.
- Returns:
Empty (all fields blank,
source="none") when topo isNone.- Return type:
- class pycsamt.format.PointCloud(x, y, z, value, label)#
Bases:
objectFlat point cloud ready for a 3-D scatter view.
- Variables:
z (x, y,) – Position, metres.
zis elevation-like (positive up) — callers plotting depth sections see negative values below the surface, matching the sign convention already used bypycsamt.app.web.callbacks.map3d’s own 3-D views.value (ndarray, shape (n,)) – \(\log_{10}(\rho / \Omega\mathrm{m})\).
label (str) – Short description of what was plotted (geometry kind + any subsampling applied), for a status bar / axis title.
- Parameters:
- pycsamt.format.pcsf_to_point_cloud(model, *, max_points=200000, seed=0)#
Flatten any
PCSFModelgeometry into one 3-D point cloud.- Parameters:
model (PCSFModel) – Any geometry kind.
max_points (int, default 200_000) – Random (seeded, reproducible) subsample cap — a native
grid3d/mesh_unstructuredmodel can carry hundreds of thousands of cells, too many for an interactive scatter plot.seed (int, default 0) – Subsampling RNG seed, for a reproducible view across renders.
- Returns:
Non-finite values (masked cells, log of non-positive resistivity) are dropped, not zeroed.
- Return type:
- Raises:
ValueError – If
model.geometry.kindis not one ofGEOMETRY_KINDS.
Examples
>>> import numpy as np >>> from pycsamt.format import Grid2DGeometry, PCSFModel >>> from pycsamt.format.pointcloud import pcsf_to_point_cloud >>> geometry = Grid2DGeometry(x=np.array([0.0, 100.0]), z=np.array([10.0, 50.0])) >>> model = PCSFModel(geometry=geometry, resistivity=np.array([[100.0, 110.0], [50.0, 55.0]])) >>> cloud = pcsf_to_point_cloud(model) >>> cloud.x.shape (4,)
- class pycsamt.format.ModelProvenance(architecture='', framework='', framework_version='', checkpoint='', checkpoint_sha256='', training_data='', hyperparameters=<factory>, random_seed=None, git_commit='', authors=<factory>, contact='', notes='', extra=<factory>)#
Bases:
PyCSAMTObjectDescribe the AI/DL model that produced a resistivity result.
- Parameters:
architecture (str, optional) – Free-text model family/architecture, e.g.
"UNet","GCN","ResNet18", or any third-party name.framework (str, optional) – e.g.
"pytorch"/"2.3.0","tensorflow"/"2.16.1".framework_version (str, optional) – e.g.
"pytorch"/"2.3.0","tensorflow"/"2.16.1".checkpoint (str, optional) – Path or identifier of the trained weights used to produce the result (e.g. a filename, a model-hub id, a DOI).
checkpoint_sha256 (str, optional) – SHA-256 hex digest of the checkpoint file, so a reader can verify they are re-running the exact weights this result claims – see
compute_checkpoint_hash().training_data (str, optional) – Free-text reference to the training dataset (name, DOI, path).
hyperparameters (dict, default {}) – Free-form training/model hyperparameters.
random_seed (int, optional) – Seed used for training and/or inference, when reproducibility depends on it.
git_commit (str, optional) – Commit hash of the code that produced this result.
authors (list of str, default []) – Model authors/maintainers.
contact (str, optional) – Contact e-mail or URL for questions about this result.
notes (str, optional) – Free-text notes not covered by the fields above.
extra (dict, default {}) – Unmodelled provenance fields, retained losslessly.
Examples
>>> from pycsamt.format.provenance import ModelProvenance >>> prov = ModelProvenance( ... architecture="UNet", ... framework="pytorch", ... framework_version="2.3.0", ... checkpoint="unet_v3.pt", ... random_seed=42, ... ) >>> prov.to_dict()["architecture"] 'UNet'
- validate()#
Validate object state.
Subclasses can override this hook. The base implementation intentionally accepts all states.
- Return type:
None
- pycsamt.format.compute_checkpoint_hash(path, *, chunk_size=1048576)#
SHA-256 hex digest of a model-checkpoint file, streamed in chunks.
- Parameters:
path (path-like) – The checkpoint file to hash (e.g. a
.pt/.h5/.onnxfile).chunk_size (int, default 1 MiB) – Read block size; large checkpoints are hashed without loading the whole file into memory.
- Returns:
Lowercase hex digest, directly comparable to
ModelProvenance.checkpoint_sha256.- Return type:
Examples
>>> from pycsamt.format.provenance import compute_checkpoint_hash >>> compute_checkpoint_hash("unet_v3.pt") '3b1c...'
- pycsamt.format.mesh_to_grid2d(model, *, nx=200, nz=150)#
Regrid a
mesh_unstructuredPCSF model onto a rectilineargrid2dapproximation.- Parameters:
model (PCSFModel) – A model with
geometry.kind == "mesh_unstructured"and per-cell resistivity (shape(n_triangles,)– everypycsamt.format.adapterswriter already produces this; a region-collapsed(n_regions,)array must be expanded onto each triangle’s region id first).nx (int, default 200, 150) – Grid resolution. Chosen independently of the source mesh’s own resolution – there is no natural rectilinear resolution to inherit from an unstructured mesh – the same kind of pragmatic, documented default the rest of this format already makes (cf. PCSM’s row-width cap, the per-station curtain’s
n_zdefault) rather than leaving it unbounded.nz (int, default 200, 150) – Grid resolution. Chosen independently of the source mesh’s own resolution – there is no natural rectilinear resolution to inherit from an unstructured mesh – the same kind of pragmatic, documented default the rest of this format already makes (cf. PCSM’s row-width cap, the per-station curtain’s
n_zdefault) rather than leaving it unbounded.
- Returns:
A new
grid2dmodel. Grid points outside the source mesh’s triangulation arenan.source_backend,created_by, andcrsare carried over from model;descriptionandmetadatarecord that this is a synthesized regrid, not a native inversion output.- Return type:
- Raises:
ValueError – If model is not
mesh_unstructured, or its resistivity is not already per-cell.NotImplementedError – If the mesh’s
planeis not"xz"– the only plane any current adapter produces; a general 3-D mesh has no single rectilinear plane to regrid onto.
Examples
>>> import numpy as np >>> from pycsamt.format.schema import PCSFModel, UnstructuredMeshGeometry >>> from pycsamt.format.regrid import mesh_to_grid2d >>> geometry = UnstructuredMeshGeometry( ... nodes=np.array([[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]]), ... connectivity=np.array([[0, 1, 2], [1, 3, 2]]), ... region_ids=np.array([0, 1]), ... ) >>> model = PCSFModel(geometry=geometry, resistivity=np.array([10.0, 20.0])) >>> regridded = mesh_to_grid2d(model, nx=5, nz=5) >>> regridded.kind 'grid2d' >>> regridded.resistivity.shape (5, 5)
2.23.2. Schema and serialization#
|
In-memory schema for the pyCSAMT Common Subsurface Format (PCSF). |
|
HDF5 reader/writer for the pyCSAMT Common Subsurface Format (PCSF). |
|
PCSM — ASCII sibling of PCSF (pyCSAMT Common Subsurface Markup). |
2.23.3. Backend adapters#
The adapter namespace contains converters for supported inversion codes and generic constructors for classical, machine-learning, and deep-learning results that already expose their geometry and resistivity arrays.
|
Per-backend converters into |
|
Generic array-based -> PCSF adapter, for any AI/DL inversion result. |
|
Occam2D -> PCSF adapter (Phase 2 of the PCSF format plan). |
|
ModEM 3-D -> PCSF adapter (Phase 3 of the PCSF format plan). |
|
MARE2DEM -> PCSF adapter (Phase 4 of the PCSF format plan). |
2.23.4. Geometry and visualization support#
|
Multiline PCSF builder/reader — Phase 5 of the PCSF format plan. |
|
Generic point-cloud extraction from any |
|
Regridded |
|
Topography helpers — |
|
Attach real per-station spatial coordinates (and elevation) to a PCSF adapter's |
2.23.5. Provenance#
|
Machine-learning-model provenance for a PCSF/PCSM AI-inversion result. |
2.23.6. Borehole exchange#
The pycsamt.format.borehole namespace contains PCBH schema objects,
canonical JSON I/O, CSV/LAS adapters, trajectory derivation, PCSF association,
and application-neutral 3-D render/export contracts.
|
PCBH - pyCSAMT Common Borehole Format. |
|
In-memory schema and semantic validation for PCBH 0.1. |
|
Canonical UTF-8 JSON reader and writer for PCBH 0.1. |
|
Combined interval-CSV importer for PCBH 0.1. |
|
Loss-explicit LAS 2.0 subset import and export for PCBH. |
|
Manifest-driven relational CSV import and export for PCBH. |
|
Deterministic 3-D trajectory derivation for PCBH boreholes. |
|
PCBH association and coordinate alignment for PCSF models. |
|
Viewer-neutral render models for PCBH boreholes. |
|
GeoJSON, VTP, and glTF exports for PCBH visualization subsets. |