Source code for pycsamt.topo.config

# Author: LKouadio <etanoyau@gmail.com>
# License: LGPL-3.0
"""Package-wide topography configuration for 2-D section displays.

Follows the same singleton + configure/reset/context pattern as the
rest of :mod:`pycsamt.api`.  Import the singleton directly::

    from pycsamt.topo import PYCSAMT_TOPO, configure_topo

Or via the public API interface::

    from pycsamt.api.topo import configure_topo, reset_topo

Examples
--------
Enable topography globally (applies to every 2-D plot until reset)::

    from pycsamt.topo import configure_topo
    configure_topo(enabled=True)

Temporarily override inside a ``with`` block::

    from pycsamt.topo import PYCSAMT_TOPO
    with PYCSAMT_TOPO.context(enabled=True, exaggeration=3.0):
        fig = section.plot()
"""

from __future__ import annotations

import copy
from collections.abc import Generator
from contextlib import contextmanager
from dataclasses import dataclass, field, fields
from typing import Any

__all__ = [
    "TopoConfig",
    "PYCSAMT_TOPO",
    "Y_DEPTH_TYPES",
    "Y_FREQ_TYPES",
    "configure_topo",
    "reset_topo",
]

# Y-axis types where real elevation exists and topo rendering is meaningful.
Y_DEPTH_TYPES: frozenset[str] = frozenset(
    {"depth", "elevation", "skin_depth", "elev", "z"}
)

# Y-axis types that represent EM frequency or period — topo is not applicable.
Y_FREQ_TYPES: frozenset[str] = frozenset(
    {"period", "frequency", "freq", "per", "t", "f"}
)


[docs] @dataclass class TopoConfig: """Package-wide topography rendering policy for 2-D section plots. Once configured, the settings apply to every 2-D section plot (pseudosections, inversion sections, interpretation panels) until :meth:`reset` is called. Attributes ---------- enabled : bool Master switch. ``False`` → stations appear at a flat z = 0 datum (default). ``True`` → terrain-following geometry active. source : {"sites", "file", "array"} Where to read elevation data from. ``"sites"`` — read ``.elev`` from each Site's EDI HEAD (default). ``"file"`` — load from ``elev_file`` (CSV / parquet). ``"array"`` — use the ``elev_array`` ndarray directly. elev_array : array-like or None External elevation values (m a.s.l.) when ``source="array"``. Must have one value per station in profile order. elev_file : str or None Path to a tabular file when ``source="file"``. Expected columns: ``station, elevation`` (or lat / lon / elevation). interp_method : {"linear", "cubic", "nearest"} Method for interpolating station elevations to intermediate x positions on the section grid. exaggeration : float Vertical exaggeration factor applied to the elevation profile during rendering. 1.0 = true scale. fill_color : str Matplotlib color for the above-surface terrain fill polygon. fill_alpha : float Opacity of the terrain fill (0–1). line_color : str Color of the terrain surface polyline. line_width : float Line width of the terrain surface polyline. show_surface_line : bool Draw the surface polyline on top of the terrain fill. clip_below_surface : bool Mask model cells that lie above the terrain surface (set to NaN). station_pins_at_surface : bool Place station marker triangles at the real terrain elevation instead of at the flat z = 0 datum. show_topo_strip : bool For period-vs-station pseudosections: add a thin elevation profile strip above the main colour image. strip_height_ratio : float Height of the topo strip relative to the main axes (0–1). """ # ── Master switch ───────────────────────────────────────────────── enabled: bool = False # ── Elevation source ────────────────────────────────────────────── source: str = "sites" elev_array: Any = field(default=None, repr=False) elev_file: str | None = None # ── Interpolation ───────────────────────────────────────────────── interp_method: str = "linear" exaggeration: float = 1.0 # ── Terrain rendering style ─────────────────────────────────────── fill_color: str = "#a89070" fill_alpha: float = 0.40 line_color: str = "#6b4e2a" line_width: float = 1.2 show_surface_line: bool = True # ── Data masking + marker behaviour ────────────────────────────── clip_below_surface: bool = True station_pins_at_surface: bool = True # ── Pseudosection topo strip ────────────────────────────────────── show_topo_strip: bool = True strip_height_ratio: float = 0.18 # ── Station marker placement ─────────────────────────────────────── # Fraction of the visible y-range added above the topo surface to # separate the station marker from the terrain line. marker_pad_fraction: float = 0.015 # ── Public API ────────────────────────────────────────────────────
[docs] def configure(self, **kw: Any) -> TopoConfig: """Set one or more configuration attributes by keyword.""" for k, v in kw.items(): if not hasattr(self, k): valid = [f.name for f in fields(self)] raise AttributeError( f"Unknown topo config key {k!r}. Valid keys: {valid}" ) setattr(self, k, v) return self
[docs] @contextmanager def context(self, **kw: Any) -> Generator[TopoConfig, None, None]: """Temporarily override config values, then restore them.""" snapshot = {k: getattr(self, k) for k in kw} try: self.configure(**kw) yield self finally: for k, v in snapshot.items(): setattr(self, k, v)
[docs] def reset(self) -> None: """Restore all attributes to package defaults.""" _defaults = TopoConfig() for f in fields(self): setattr(self, f.name, getattr(_defaults, f.name))
[docs] def clone(self) -> TopoConfig: """Return a deep copy of this config.""" return copy.deepcopy(self)
[docs] def is_active_for(self, y_type: str) -> bool: """Return True when topo rendering should fire for a given y-axis type. Topo only makes physical sense when the y-axis represents a real spatial quantity (depth, elevation, skin depth). Period and frequency pseudosections carry no elevation information — topo rendering is silently skipped for those. Parameters ---------- y_type : str String tag describing what the y-axis represents. Recognised depth-like values: ``"depth"``, ``"elevation"``, ``"elev"``, ``"skin_depth"``, ``"z"``. Recognised freq-like values: ``"period"``, ``"frequency"``, ``"freq"``, ``"per"``, ``"t"``, ``"f"``. Returns ------- bool """ if not self.enabled: return False return y_type.lower() in Y_DEPTH_TYPES
[docs] def summary(self) -> str: """One-line status string.""" src = self.source if self.enabled else "disabled" return f"TopoConfig(enabled={self.enabled}, source={src!r}, exag={self.exaggeration})"
def __repr__(self) -> str: lines = ["TopoConfig("] for f in fields(self): lines.append(f" {f.name}={getattr(self, f.name)!r},") lines.append(")") return "\n".join(lines)
# --------------------------------------------------------------------------- # Singleton + convenience helpers # --------------------------------------------------------------------------- PYCSAMT_TOPO: TopoConfig = TopoConfig()
[docs] def configure_topo(**kw: Any) -> None: """Configure the global :data:`PYCSAMT_TOPO` singleton. Parameters ---------- **kw Keyword arguments forwarded to :meth:`TopoConfig.configure`. Examples -------- >>> from pycsamt.topo import configure_topo >>> configure_topo(enabled=True, exaggeration=2.0) """ PYCSAMT_TOPO.configure(**kw)
[docs] def reset_topo() -> None: """Reset :data:`PYCSAMT_TOPO` to package defaults.""" PYCSAMT_TOPO.reset()