Source code for pycsamt.zonge.info

# Author: LKouadio <etanoyau@gmail.com>
# License: LGPL-3.0-or-later
"""
DataInfo - High-level AVG data aggregator.

This module provides the DataInfo class, which serves as a
primary facade for interacting with a complete Zonge AVG dataset.
It composes all other components (Header, Z, Resistivity, Phase,
and various QC metrics) into a single, convenient container.
"""

from __future__ import annotations

import warnings
from collections.abc import Mapping
from pathlib import Path
from typing import Any

import pandas as pd

from ..exceptions import AvgDataError
from .base import AVGFrame
from .config import Zonge
from .heads import Header
from .meas import Amps, CompMeas, Frequency
from .resphase import Phase, Resistivity
from .survey import Station
from .utils import _standardise_columns, load_avg
from .var_pc import PcEmag, PcHmag, PcRho
from .var_std import SEphz, SHphz, SPhz
from .z import Z

__all__ = ["DataInfo"]


[docs] class DataInfo(Zonge): r"""High-level aggregator for a complete Zonge AVG dataset. This class acts as the primary container and orchestrator for all data and metadata parsed from a Zonge AVG file. It composes all other data components (e.g., Header, Z, Resistivity, Phase, and QC metrics) into a single, convenient object. Its main role is to provide a unified interface, holding all the structured information in one place after the initial parsing is complete. Attributes ---------- df : pandas.DataFrame or None The core tidy DataFrame containing all available data columns after parsing and standardization. meta : mapping or None The raw metadata dictionary extracted from the file's header section. header : :class:`~.heads.Header` A component that aggregates all header-level metadata. station : :class:`~.survey.Station` A component that manages survey line geometry. z : :class:`~.z.Z` The component for computing the complex impedance tensor. resistivity, phase : :class:`~.resphase.Resistivity`, :class:`~.resphase.Phase` Components for apparent resistivity and phase data. frequency, amps, comp : :class:`~.meas.Frequency`, etc. Components for core measurement quantities. pc_emag, pc_hmag, pc_rho : :class:`~.var_pc.PcEmag`, etc. Components for percent-error quality control metrics. s_ephz, s_hphz, s_phz : :class:`~.var_std.SEphz`, etc. Components for phase standard deviation QC metrics. Methods ------- from_avg(avg, meta=None) A classmethod factory to build a `DataInfo` object from various sources, including a file path or DataFrame. read(source, meta=None) Orchestrates the population of all sub-components from a standardized DataFrame and metadata dictionary. Notes ----- The `read` method is the core of this class. It iterates through all its component attributes and calls their respective `read` methods. It includes a robust error- handling mechanism that will issue a warning and skip any component that fails to load (e.g., due to missing data in the source file), making the loading process resilient. Examples -------- While typically used internally by the `AVG` class, you could use `DataInfo` directly: >>> from pycsamt.zonge.info import DataInfo >>> from pycsamt.zonge.utils import load_avg >>> df, meta = load_avg('data/avg/K2.avg') >>> data_info = DataInfo() >>> data_info.read(df, meta) >>> print(data_info.station) Station(n=28, span=25.0–1375.0 m, inc=50.0) See Also -------- pycsamt.zonge.avg.AVG : The main user-facing class that uses `DataInfo`. pycsamt.zonge.base.AVGComponentBase : The base class for all components held by `DataInfo`. """ def __init__(self, verbose: bool = False) -> None: super().__init__(verbose=verbose) # Core data holders self._frame: AVGFrame | None = None self.df: pd.DataFrame | None = None self.meta: Mapping[str, Any] | None = None # Component containers self.header: Header = Header() self.station: Station = Station() self.z: Z = Z() self.resistivity: Resistivity = Resistivity() self.phase: Phase = Phase() self.frequency: Frequency = Frequency() self.amps: Amps = Amps() self.comp: CompMeas = CompMeas() # QC components self.pc_emag: PcEmag = PcEmag() self.pc_hmag: PcHmag = PcHmag() self.pc_rho: PcRho = PcRho() self.s_ephz: SEphz = SEphz() self.s_hphz: SHphz = SHphz() self.s_phz: SPhz = SPhz()
[docs] @classmethod def from_avg( cls, avg: str | Path | AVGFrame | pd.DataFrame | tuple[pd.DataFrame, Mapping[str, Any]], *, meta: Mapping[str, Any] | None = None, ) -> DataInfo: """ Build a DataInfo object from a path, AVGFrame, or DataFrame. """ if isinstance(avg, (str, Path)): df, m = load_avg(Path(avg)) frame = AVGFrame(df, m, Path(avg)) elif isinstance(avg, AVGFrame): frame = avg elif isinstance(avg, tuple) and len(avg) == 2: df, m = avg frame = AVGFrame(df, dict(m)) elif isinstance(avg, pd.DataFrame): frame = AVGFrame(avg, dict(meta or {})) else: raise TypeError( "from_avg expects Path|AVGFrame|DataFrame|" "(DataFrame, meta) tuple." ) obj = cls() obj.read(frame.data, frame.meta) obj._frame = frame return obj
[docs] def read( self, source: pd.DataFrame, meta: Mapping[str, Any] | None = None, ) -> None: """ Orchestrate reading data into all sub-components. """ self.df = _standardise_columns(source.copy()) if "comp" not in self.df.columns: self.df["comp"] = "ExHy" self.meta = meta or {} # Populate header from metadata self.header.read(meta=self.meta) # Populate data components from the DataFrame components = [ self.station, self.z, self.resistivity, self.phase, self.frequency, self.amps, self.comp, self.pc_emag, self.pc_hmag, self.pc_rho, self.s_ephz, self.s_hphz, self.s_phz, ] for comp in components: try: comp.read(self.df, self.meta) except AvgDataError as e: # Gracefully skip components if their data is missing msg = ( f"Notice: Could not load component " f"'{comp.__class__.__name__}': {e}" ) if self.verbose: self._logger.warning(msg) else: warnings.warn(msg, stacklevel=2) except Exception as e: msg = ( f"Warning: Unexpected error loading " f"'{comp.__class__.__name__}': {e}" ) if self.verbose: self._logger.error(msg) else: warnings.warn(msg, stacklevel=2) return self
def __str__(self) -> str: if self.df is None: return "DataInfo(empty)" n_st = ( self.df["station"].nunique() if "station" in self.df.columns else 0 ) n_f = self.df["freq"].nunique() if "freq" in self.df.columns else 0 return f"DataInfo(stations={n_st}, freqs={n_f}, rows={len(self.df)})" __repr__ = __str__