pycsamt.format.schema#

In-memory schema for the pyCSAMT Common Subsurface Format (PCSF).

PCSF is the backend-neutral container every inversion result (Occam2D, ModEM, MARE2DEM, DUHI) can be converted to, so pycsamt.format.io, app/mapview, and the web 3-D view share one on-disk representation instead of five ad hoc in-memory shapes. See PYCSAMT-PCSF-INVERSION-FORMAT-PLAN.md at the repository root for the full design rationale.

This module defines the schema only — geometry and container dataclasses, the frozen list of supported geometry kinds, and shape validation. Reading/writing .pcsf files lives in pycsamt.format.io; per-backend conversion lives in pycsamt.format.adapters (Phases 2-4).

Classes

DerivedVolume(grid, resistivity[, ...])

Optional cached 3-D volume synthesized from stacked lines.

Grid2DGeometry(x, z[, x_nodes, z_nodes, ...])

Single-profile rectilinear geometry (Occam2D / DUHI-via-Occam2D).

Grid3DGeometry(x, y, z[, x_nodes, y_nodes, ...])

Native 3-D tensor volume geometry (ModEM).

LineEntry(line_id, geometry, resistivity[, ...])

One profile within a MultilineGeometry.

MultilineGeometry([lines, derived_volume])

A set of profiles plus real line geometry (fence/block views).

PCSFModel(geometry[, resistivity, ...])

Backend-neutral inversion-result container (one PCSF file).

StationTable([name, x, y, z, line_id, lon, lat])

Survey station positions, shared across geometry kinds.

TopographyPerStation([station_id, elevation])

Scalar-per-station topography (matches the existing convention in pycsamt.map.topo).

TopographyRaster(x, y, elevation)

Gridded-DEM topography — a regular elevation surface independent of any station table.

UnstructuredMeshGeometry(nodes, ...[, plane])

Native unstructured triangular mesh geometry (MARE2DEM).

class pycsamt.format.schema.Grid2DGeometry(x, z, x_nodes=None, z_nodes=None, origin=None, azimuth_deg=None)[source]

Bases: PyCSAMTObject

Single-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.

kind: ClassVar[str] = 'grid2d'
x: ndarray
z: ndarray
x_nodes: ndarray | None = None
z_nodes: ndarray | None = None
origin: ndarray | None = None
azimuth_deg: float | None = None
validate()[source]

Validate object state.

Subclasses can override this hook. The base implementation intentionally accepts all states.

Return type:

None

property resistivity_shape: tuple[int, int][source]
class pycsamt.format.schema.Grid3DGeometry(x, y, z, x_nodes=None, y_nodes=None, z_nodes=None, origin=None, rotation_deg=0.0, n_air=0)[source]

Bases: PyCSAMTObject

Native 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).

kind: ClassVar[str] = 'grid3d'
x: ndarray
y: ndarray
z: ndarray
x_nodes: ndarray | None = None
y_nodes: ndarray | None = None
z_nodes: ndarray | None = None
origin: ndarray | None = None
rotation_deg: float = 0.0
n_air: int = 0
validate()[source]

Validate object state.

Subclasses can override this hook. The base implementation intentionally accepts all states.

Return type:

None

property resistivity_shape: tuple[int, int, int][source]
class pycsamt.format.schema.UnstructuredMeshGeometry(nodes, connectivity, region_ids, plane='xz')[source]

Bases: PyCSAMTObject

Native 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 as plane="xz" with x holding the profile’s own along-line coordinate.

kind: ClassVar[str] = 'mesh_unstructured'
nodes: ndarray
connectivity: ndarray
region_ids: ndarray
plane: str = 'xz'
validate()[source]

Validate object state.

Subclasses can override this hook. The base implementation intentionally accepts all states.

Return type:

None

property n_regions: int[source]
class pycsamt.format.schema.LineEntry(line_id, geometry, resistivity, offset_y=0.0, offset_kind='synthetic', azimuth_deg=None)[source]

Bases: PyCSAMTObject

One 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.

line_id: str
geometry: Grid2DGeometry
resistivity: ndarray
offset_y: float = 0.0
offset_kind: str = 'synthetic'
azimuth_deg: float | None = None
validate()[source]

Validate object state.

Subclasses can override this hook. The base implementation intentionally accepts all states.

Return type:

None

class pycsamt.format.schema.DerivedVolume(grid, resistivity, derivation_method='linear_interp', derived_from=<factory>, synthesized=True)[source]

Bases: PyCSAMTObject

Optional 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/synthesized in the design plan’s §2).

Parameters:
grid: Grid3DGeometry
resistivity: ndarray
derivation_method: str = 'linear_interp'
derived_from: list[str]
synthesized: bool = True
validate()[source]

Validate object state.

Subclasses can override this hook. The base implementation intentionally accepts all states.

Return type:

None

class pycsamt.format.schema.MultilineGeometry(lines=<factory>, derived_volume=None)[source]

Bases: PyCSAMTObject

A set of profiles plus real line geometry (fence/block views).

Formalizes what pycsamt/app/web/callbacks/map3d.py currently reconstructs at render time from a stack of independent 2-D sections. Each line carries its own resistivity; the optional derived_volume is a documented, reproducible synthesis rather than a render-time-only side effect.

Parameters:
kind: ClassVar[str] = 'multiline'
lines: list[LineEntry]
derived_volume: DerivedVolume | None = None
validate()[source]

Validate object state.

Subclasses can override this hook. The base implementation intentionally accepts all states.

Return type:

None

class pycsamt.format.schema.StationTable(name=<factory>, x=<factory>, y=<factory>, z=<factory>, line_id=None, lon=None, lat=None)[source]

Bases: PyCSAMTObject

Survey station positions, shared across geometry kinds.

x/y/z are geometry-local (along-profile chainage for grid2d, the model grid’s own frame for grid3d, whatever frame the caller supplied for mesh_unstructured) — never assumed to be real-world geographic coordinates, per SPEC.md’s own load_pcsf_lines convention. 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 .dat file’s GG_Lat/GG_Lon columns, pycsamt.map._core.StationRecord). A single-line grid2d (or any other kind’s) PCSF file that sets these needs no separate known_stations match to place its stations on a real basemap.

Parameters:
name: list[str]
x: ndarray
y: ndarray
z: ndarray
line_id: list[str] | None = None
lon: ndarray | None = None
lat: ndarray | None = None
validate()[source]

Validate object state.

Subclasses can override this hook. The base implementation intentionally accepts all states.

Return type:

None

class pycsamt.format.schema.TopographyPerStation(station_id=<factory>, elevation=<factory>)[source]

Bases: PyCSAMTObject

Scalar-per-station topography (matches the existing convention in pycsamt.map.topo).

Parameters:
kind: ClassVar[str] = 'per_station'
station_id: list[str]
elevation: ndarray
validate()[source]

Validate object state.

Subclasses can override this hook. The base implementation intentionally accepts all states.

Return type:

None

class pycsamt.format.schema.TopographyRaster(x, y, elevation)[source]

Bases: PyCSAMTObject

Gridded-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 plain x/y/elevation arrays a caller has already obtained by whatever means it likes (see pycsamt.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 convention numpy.meshgrid(x, y) (default indexing="xy") produces.

kind: ClassVar[str] = 'raster'
x: ndarray
y: ndarray
elevation: ndarray
validate()[source]

Validate object state.

Subclasses can override this hook. The base implementation intentionally accepts all states.

Return type:

None

class pycsamt.format.schema.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)[source]

Bases: PyCSAMTObject, MetadataMixin

Backend-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_unstructured geometries; must be None for multiline (each line carries its own resistivity — see LineEntry).

  • 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_unstructured only), alongside the per-cell resistivity expanded from it.

  • resistivity_by_node (ndarray, optional) – Per-node resistivity table (mesh_unstructured only), 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 (see pycsamt.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.metadata objects (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 an InversionHistory-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)

  • metadata (dict[str, Any])

  • boreholes (Any | None)

geometry: Grid2DGeometry | Grid3DGeometry | UnstructuredMeshGeometry | MultilineGeometry
resistivity: ndarray | None = None
resistivity_native: ndarray | None = None
resistivity_native_encoding: str | None = None
uncertainty: ndarray | None = None
sensitivity: ndarray | None = None
resistivity_by_region: ndarray | None = None
resistivity_by_node: ndarray | None = None
stations: StationTable | None = None
topography: TopographyPerStation | TopographyRaster | None = None
survey: dict[str, Any]
history: dict[str, ndarray]
source_backend: str = 'generic'
created_by: str = ''
created_at: str = ''
crs: str | None = None
description: str = ''
metadata: dict[str, Any]
boreholes: Any | None = None
property kind: str[source]
validate()[source]

Validate object state.

Subclasses can override this hook. The base implementation intentionally accepts all states.

Return type:

None