pycsamt.format.multiline#

Multiline PCSF builder/reader — Phase 5 of the PCSF format plan.

Formalizes what pycsamt/app/web/callbacks/map3d.py currently reconstructs at render time from a stack of independent 2-D sections: _line_real_offsets/_assemble_3d_grid derive real-vs-synthetic line offsets and resample each line onto a common (x, z) grid, purely in memory, from whatever cached InversionResult or pseudo-section data the current session happens to hold. Nothing is ever persisted.

This module is the inverse pair:

  • build_multiline_pcsf() turns a profiles dict — the same {line_name: {"x", "z", "rho", "sta_x", "sta_names", ...}} shape map3d.py’s own _profiles_from_pseudo/ _profiles_from_inversion_result already produce — into a PCSFModel with a multiline geometry, one real (unresampled) LineEntry per line.

  • multiline_pcsf_to_profiles() reconstructs that same profiles dict shape from a loaded file, so it is a drop-in alternate data source for map3d.py’s existing renderers (_build_fence_fig/_build_block_fig/_assemble_3d_grid) — no rendering code needs to change to consume a persisted file.

  • line_offsets_from_stations() and stack_lines_to_common_grid() are the same real-offset / common-grid-resampling algorithms map3d.py already implements privately as _line_real_offsets/_assemble_3d_grid, promoted here so both the live rendering path and the persisted-file path are provably the same computation, not two copies that can drift.

Functions

build_multiline_pcsf(profiles, *[, ...])

Build a multiline PCSFModel from a profiles dict.

line_offsets_from_stations(profiles)

Cross-strike offset (m) for each line, from real station lat/lon.

multiline_pcsf_to_profiles(model)

Reconstruct a profiles dict from a multiline PCSFModel.

stack_lines_to_common_grid(profiles, *[, ...])

Resample every line onto the first line's own (x, z) grid.

pycsamt.format.multiline.line_offsets_from_stations(profiles)[source]

Cross-strike offset (m) for each line, from real station lat/lon.

Same algorithm as map3d.py’s private _line_real_offsets (built on pycsamt.map.geometry.survey_uv()), so both the live-cache rendering path and this persisted-file path place lines identically. Requires every line to carry sta_lat/sta_lon/ sta_names of equal length; returns None otherwise so callers fall back to a synthetic index-based stack via resolve_offset().

Parameters:

profiles (Mapping[str, Mapping[str, Any]])

Return type:

dict[str, float] | None

pycsamt.format.multiline.stack_lines_to_common_grid(profiles, *, line_spacing=1.0, fallback_unit=1000.0)[source]

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 synthetic idx * spacing * fallback_unit stack (see pycsamt.map.geometry.resolve_offset()).

  • rho_3d (ndarray, shape (n_lines, n_x, n_z))

Parameters:
Return type:

tuple[ndarray, ndarray, ndarray, ndarray]

pycsamt.format.multiline.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='')[source]

Build a multiline PCSFModel from 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 shape map3d.py’s _profiles_from_pseudo/ _profiles_from_inversion_result already produce. rho must 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 keys sta_x, sta_names, sta_elev, sta_lat, sta_lon populate PCSFModel.stations/ PCSFModel.topography when present. sta_lat/ sta_lon do double duty: line_offsets_from_stations() uses them (via pycsamt.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 (see offset_kind below) – and the same values are also persisted into PCSFModel.stationslon/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 cached DerivedVolume (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 cached DerivedVolume (real per-line geometry itself never depends on these — only the synthetic offset fallback does).

  • cache_derived_volume (bool, default True) – When True and there are at least two lines, also cache a DerivedVolume (each line resampled onto a common grid) so a large multiline file does not need to re-resample on every render. Set False to 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() – see pycsamt.format.adapters.occam2d.occam2d_to_pcsf()’s identical parameter for the full description of accepted source types. Populates each line’s own sta_lat/sta_lon before 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 (in profiles’s own key order) for e.g. one .bln file per surveyed line. Takes precedence over any sta_lat/sta_lon already present in profiles for every station it resolves (with a UserWarning if both were supplied). If topo only partially covers a line’s stations, the resulting nan entries make line_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.

  • created_by (str) – Passed straight through to PCSFModel.

  • crs (str | None) – Passed straight through to PCSFModel.

  • description (str) – Passed straight through to PCSFModel.

Raises:

ValueError – If profiles is empty, or any line is missing x/z/rho.

Return type:

PCSFModel

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.multiline_pcsf_to_profiles(model)[source]

Reconstruct a profiles dict from a multiline PCSFModel.

The exact inverse of build_multiline_pcsf()’s per-line conversion — the result is a drop-in alternate data source for map3d.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 from PCSFModel.stations when present (matched to each line via StationTable.line_id); sta_lat/sta_lon come from StationTable.lat/.lon when the file has them (see build_multiline_pcsf()), else stay empty lists – an older file written before those fields existed round-trips the same way it always did.

Return type:

dict

Raises:

ValueError – If model is not a multiline geometry.