pycsamt.format.topo_source#

Attach real per-station spatial coordinates (and elevation) to a PCSF adapter’s StationTable from an external topo source.

Every PCSF adapter (pycsamt.format.adapters) already accepts a station_elevations/station_lonlat-style mapping the caller has to build by hand. This module is the “smart” layer on top of that: a single topo= argument that can be

  • a .bln (Golden Software Surfer blanking) file — a bare x, y point list, no station identity, matched to stations positionally (in survey order) since the format itself carries none;

  • a .stn (Zonge station-location) file, parsed by the same battle-tested low-level reader pycsamt.zonge already uses (pycsamt.zonge.utils.read_stn()) — never re-implemented here;

  • a .csv (or .txt) file with a header pycsamt can recognise (station/lat/lon/easting/northing/elevation and common synonyms);

  • an already-geo-located Sites/MapData/iterable-of- StationRecord-like object (duck-typed: anything exposing a station id plus latitude/longitude/elevation) — so a survey’s own EDI collection can supply topography directly, with no intermediate file at all;

  • a plain {station_name: (lon, lat)} / {station_name: (lon, lat, elevation)} mapping — the same shape station_lonlat already accepted, now handled by the same code path;

  • for a multiline build, a {line_id: <any of the above>} mapping, or a sequence of one source per line in line order — one .bln/ .stn file per surveyed line is a common real-world layout.

Design choices, stated explicitly because they are the crux of “smart” attribution rather than an implementation detail:

  1. A named source (``.stn``, ``.csv`` with a station column, a plain dict, or a Sites/MapData object) matches by station id — exact, then a normalized fallback (pycsamt.map._core.normalize_station_id(), the same one every other cross-source station match in this codebase already uses). Unmatched stations on either side are reported, never silently dropped or fabricated.

  2. A name-less source (a bare ``.bln``, or a ``.csv`` without a station column) is positional and therefore requires an exact station-count match — the whole reason to “first detect the number of stations passed and compare to the inversion stations” before attributing anything. A mismatch raises by default (on_mismatch="raise"); on_mismatch="warn" instead issues a UserWarning and attributes only the overlapping prefix, a deliberate best-effort escape hatch rather than the default.

  3. When ``topo`` is given, it takes precedence over any other lon/lat source (station_lonlat, a backend’s own native coordinates) for every station it successfully attributes — with a UserWarning when both were supplied, so the override is never silent. Elevation merges rather than fully overriding: a station the topo source has no elevation for keeps whatever station_elevations already gave it.

  4. Passing no ``topo`` behaves exactly as before this module existed — every adapter’s existing station_elevations/ station_lonlat parameters are unaffected when topo is None.

  5. Projected coordinates (easting/northing) need `epsg` or `utm_zone` to become lon/lat — conversion is delegated entirely to pycsamt.gis.utils.to_ll() (the project’s one existing UTM/EPSG conversion utility; pyproj stays an optional dependency, imported lazily only when a conversion is actually requested, matching PCSF’s own “no new mandatory dependency” principle in SPEC.md S2). A source already carrying lat/lon columns, or a .bln explicitly marked latlon=True, needs neither.

Functions

attribute_topo(topo, station_names, *[, ...])

Match a parsed TopoTable onto station_names.

read_topo_file(path, *[, epsg, utm_zone, latlon])

Parse a .bln/.csv/.stn topo file into a TopoTable.

resolve_topo(topo, station_names, *[, epsg, ...])

Resolve any accepted topo= argument into a TopoAttribution against station_names.

topo_from_sites(source)

Extract lon/lat/elevation from an already-geo-located Sites/MapData/iterable-of-station-record object.

Classes

TopoAttribution([lon, lat, elevation, ...])

Per-station real coordinates resolved from a topo source, ready to populate lon/lat (and merge into elevation).

TopoTable(lon, lat[, elevation, names, source])

A parsed topo source, before it is matched to any station names.

class pycsamt.format.topo_source.TopoTable(lon, lat, elevation=None, names=None, source='<unknown>')[source]

Bases: object

A parsed topo source, before it is matched to any station names.

Variables:
  • names (list of str, optional) – Station identity carried by the source itself. None for 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, nan where genuinely unknown.

  • source (str) – Human-readable provenance (file path, or a short description for an in-memory source), surfaced in error/warning messages.

Parameters:
lon: ndarray
lat: ndarray
elevation: ndarray | None = None
names: list[str] | None = None
source: str = '<unknown>'
property n: int[source]
class pycsamt.format.topo_source.TopoAttribution(lon=<factory>, lat=<factory>, elevation=<factory>, matched=<factory>, unmatched_stations=<factory>, source='none')[source]

Bases: object

Per-station real coordinates resolved from a topo source, ready to populate lon/lat (and merge into elevation).

Parameters:
lon: dict[str, float]
lat: dict[str, float]
elevation: dict[str, float]
matched: list[str]
unmatched_stations: list[str]
source: str = 'none'
pycsamt.format.topo_source.read_topo_file(path, *, epsg=None, utm_zone=None, latlon=False)[source]

Parse a .bln/.csv/.stn topo file into a TopoTable.

Parameters:
  • path (path-like) – A .bln, .csv/.txt, or .stn file.

  • 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) – .bln only: set True when the file’s own x, y columns are already lon, lat (a .bln carries no CRS metadata to detect this from). Ignored for .csv/.stn, which are only treated as already-geographic when their own header says lat/lon.

Raises:

ValueError – Unrecognised extension, a malformed .bln header/body, no recognisable coordinate columns in a .csv/.stn file, or projected coordinates with neither epsg nor utm_zone.

Return type:

TopoTable

pycsamt.format.topo_source.topo_from_sites(source)[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 .stations attribute’s items, for a MapData-like container) expose a station identifier (id/name/ station) plus longitude/latitude and, optionally, elevationpycsamt.map._core.StationRecord and 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:

TopoTable

pycsamt.format.topo_source.attribute_topo(topo, station_names, *, on_mismatch='raise')[source]

Match a parsed TopoTable onto station_names.

Name-based when topo.names is 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 a UserWarning and attributes only the overlapping prefix (min(topo.n, len(station_names)) points, in order).

  • topo (TopoTable)

  • station_names (Sequence[str])

Return type:

TopoAttribution

pycsamt.format.topo_source.resolve_topo(topo, station_names, *, epsg=None, utm_zone=None, latlon=False, on_mismatch='raise')[source]

Resolve any accepted topo= argument into a TopoAttribution against station_names.

Parameters:
  • topo (None, path-like, TopoTable, Sites/MapData-like, mapping, or sequence) –

    • None – returns an empty attribution (every adapter’s existing station_elevations/station_lonlat behaviour is unaffected).

    • a .bln/.csv/.stn path, or an already-parsed TopoTable.

    • a Sites/MapData/iterable-of-station-record object (see topo_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 is None.

Return type:

TopoAttribution