pycsamt.format.text#

PCSM — ASCII sibling of PCSF (pyCSAMT Common Subsurface Markup).

PCSF (pycsamt.format.io, HDF5, .pcsf) is the canonical, machine-optimal container. PCSM (.pcsm) is a lossless, hand-editable text projection of the exact same PCSFModel – the same relationship netCDF (binary) has to CDL, its ncdump/ ncgen ASCII text form: PCSF stays the source of truth; PCSM is a derived, round-trippable serialisation for scripting, teaching, diffing, and hand inspection, not a second schema to keep in sync.

Grammar (deliberately not YAML/JSON, so a reader can be hand-written in any language with no library, the same way Occam2D/ModEM/MARE2DEM’s own native ASCII files already are):

  • KEYWORD value header lines for scalars.

  • KEYWORD ... END_KEYWORD blocks for arrays – values may be spread across any number of lines; the terminator, not a declared count, ends the block, so a file remains easy to hand-edit (reflow, add blank lines, add/remove values) and errors are caught by comparing the collected count against the count declared earlier (NX, N_STATIONS, …) rather than silently misaligning.

  • # starts a comment: a whole line, or the remainder of a line after data, is ignored – something none of the three native solver formats PCSM interoperates with support. The handful of genuinely free-text fields (DESCRIPTION, CREATED_BY, CREATED_AT, CRS, SURVEY_JSON, METADATA_JSON) are the one exception: their value is everything after the keyword to end of line, taken verbatim, so a value that happens to contain # is not truncated.

  • Floats are written with Python’s repr() (the shortest string that round-trips exactly) – unlike ModEM’s own ASCII .rho format (~5 significant figures), a .pcsm round-trips a resistivity volume bit-exactly back through .pcsf.

A large volume (a native grid3d, a dense mesh_unstructured mesh) produces a large text file – the same trade-off ModEM’s own ASCII format already accepts, inherent to any plain-text encoding of bulk numeric data, not fixable while staying hand-editable. A path ending in .gz is written/read gzip-compressed as a mitigation (5.0x smaller on the real ModEM case in examples/pcsm_conversion_demo) at the cost of no longer being casually text-editor-readable, so it is opt-in rather than default.

Functions

pcsf_to_pcsm(pcsf_path, pcsm_path, *[, ...])

Convert a .pcsf (HDF5) file to a .pcsm (ASCII) file.

pcsm_to_pcsf(pcsm_path, pcsf_path)

Convert a .pcsm (ASCII) file to a .pcsf (HDF5) file.

peek_kind(path)

Return a PCSF/PCSM file's geometry.kind without loading any array data -- just the root/geometry attributes for .pcsf, or the GEOMETRY_KIND header line for .pcsm/.pcsm.gz.

read_pcsf_or_pcsm(path)

Read a PCSFModel from either encoding, by extension.

read_pcsm(path)

Read a PCSFModel back from a .pcsm (ASCII) file.

write_pcsm(model, path, *[, log10_view])

Write a PCSFModel to a .pcsm (ASCII) file.

pycsamt.format.text.write_pcsm(model, path, *, log10_view=False)[source]

Write a PCSFModel to 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 canonical RESISTIVITY field 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 a 10**log10(x) round-trip is not guaranteed bit-exact the way the rest of this format’s repr()-based float round-trip is.

Pass log10_view=True to additionally write a RESISTIVITY_LOG10 block (and, for a multiline file, 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 returned PCSFModel, and it never influences the canonical RESISTIVITY block. (An Occam2D-sourced model already carries its own real log10 array as RESISTIVITY_NATIVE whenever resistivity_native is 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 native grid3d or a dense mesh_unstructured mesh) produce large plain-text files – the same trade-off ModEM’s own ASCII .rho format already accepts – and gzip substantially reduces that in practice (measured on the real, bundled willy_27freq_watex_line02_sample 590,400-cell ModEM volume, see examples/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 .gz suffix writes a gzip-compressed file.

  • log10_view (bool, default False) – Also write a RESISTIVITY_LOG10 convenience block (see above). Off by default so existing files/output size are unaffected unless explicitly requested.

Returns:

The path written to.

Return type:

pathlib.Path

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.text.read_pcsm(path)[source]

Read a PCSFModel back from a .pcsm (ASCII) file.

Parameters:

path (path-like) – Source file.

Returns:

Fully reconstructed and re-validated model.

Return type:

PCSFModel

Raises:

ValueError – If PCSM_VERSION is missing, malformed, or names an unrecognised MAJOR version (see pycsamt/format/SPEC.md section 5), if the file is otherwise malformed, or if the reconstructed model fails PCSFModel.validate().

pycsamt.format.text.pcsf_to_pcsm(pcsf_path, pcsm_path, *, log10_view=False)[source]

Convert a .pcsf (HDF5) file to a .pcsm (ASCII) file.

log10_view is passed through to write_pcsm() — see there.

Parameters:
Return type:

Path

pycsamt.format.text.pcsm_to_pcsf(pcsm_path, pcsf_path)[source]

Convert a .pcsm (ASCII) file to a .pcsf (HDF5) file.

Parameters:
Return type:

Path

pycsamt.format.text.read_pcsf_or_pcsm(path)[source]

Read a PCSFModel from 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 (see SPEC.md S9) a user hands it, without duplicating the .pcsf vs. .pcsm/.pcsm.gz dispatch 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:

PCSFModel

pycsamt.format.text.peek_kind(path)[source]

Return a PCSF/PCSM file’s geometry.kind without loading any array data – just the root/geometry attributes for .pcsf, or the GEOMETRY_KIND header 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/multiline and therefore importable by a given consumer) before committing to a full read_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:

path (str | PathLike)

Return type:

str