Source code for pycsamt.zonge.tipper

# Author: LKouadio <etanoyau@gmail.com>
# License: LGPL-3.0-or-later
"""
Tipper Transfer Function Component.

This module provides the `Tipper` class for managing the
magnetotelluric Tipper transfer function, which relates the
vertical magnetic field (Hz) to the horizontal components
(Hx, Hy).
"""

from __future__ import annotations

from collections.abc import Mapping, Sequence
from typing import (
    Any,
)

import numpy as np
import pandas as pd

from ..exceptions import AvgDataError
from .base import AVGComponentBase
from .utils import _standardise_columns, _to_complex
from .utils import to_xarray as _to_xr

__all__ = ["Tipper"]


[docs] class Tipper(AVGComponentBase): r"""Geomagnetic transfer function (Tipper) component. The Tipper, also known as the induction vector, is a transfer function that relates the vertical component of the magnetic field (:math:`H_z`) to the horizontal components (:math:`H_x`, :math:`H_y`). .. math:: H_z = T_x H_x + T_y H_y This class is designed to hold the complex Tipper components :math:`T_x` and :math:`T_y` after they have been calculated. Attributes ---------- tx, ty : pd.Series The complex Tipper components for the x and y directions. See Also -------- pycsamt.zonge.avg.AMTAVG.calculate_tipper : The method used to compute the Tipper values. """ def __init__( self, data: pd.DataFrame | None = None, meta: Mapping[str, Any] | None = None, *, name: str | None = None, verbose: bool = False, ) -> None: """Initializes the Tipper component.""" super().__init__( data=data, meta=meta, name=name or "Tipper", verbose=verbose )
[docs] def read( self, source: pd.DataFrame, meta: Mapping[str, Any] | None = None, **kws: Any, ) -> None: """ Populate the Tipper component from a DataFrame. This method expects a DataFrame that contains the calculated Tipper components, named 'tx' and 'ty'. If these columns are missing, they will be created and filled with NaNs for structural consistency. """ if not isinstance(source, pd.DataFrame): raise TypeError("Tipper.read expects a pandas.DataFrame.") df = _standardise_columns(source.copy()) self._meta = dict(meta or {}) # Ensure required columns exist, creating them if necessary for col in ["tx", "ty"]: if col not in df.columns: df[col] = np.nan if self.verbose: self._logger.debug( f"'{col}' not in source. Creating empty column." ) # Ensure coordinates exist for consistency if "station" not in df.columns: df["station"] = np.nan if "freq" not in df.columns: raise AvgDataError("Tipper requires a 'freq' column.") # Normalize types from .utils import to_numeric_if_possible df["station"] = to_numeric_if_possible(df["station"]) df["freq"] = pd.to_numeric(df["freq"], errors="coerce") df["tx"] = df["tx"].map(_to_complex) df["ty"] = df["ty"].map(_to_complex) keep_cols = ["station", "freq", "tx", "ty"] self._frame = df.loc[:, [c for c in keep_cols if c in df.columns]] return self
[docs] def write(self) -> list[str]: """ Serialise to a compact CSV block with a meta preamble. """ if self._frame.empty: return [] return self._write_csv_block( cols=["station", "freq", "tx", "ty"], title="$Tipper Block", include_meta=True, stamp=True, )
[docs] @property def tx(self) -> pd.Series: """The complex Tipper component Tx.""" return self._frame.get("tx", pd.Series(dtype="complex128"))
[docs] @property def ty(self) -> pd.Series: """The complex Tipper component Ty.""" return self._frame.get("ty", pd.Series(dtype="complex128"))
[docs] def to_xarray( self, *, coords: Sequence[str] = ("station", "freq"), attrs: dict[str, Any] | None = None, ): """ Convert the Tipper data into an xarray.Dataset. """ if self._frame.empty: raise AvgDataError("Empty frame; nothing to export.") merged = dict(self._meta) if attrs: merged.update(attrs) return _to_xr( self._frame.copy(), coords=coords, data_vars=["tx", "ty"], attrs=merged, )
def __str__(self) -> str: """Provide a concise, human-readable representation.""" if self._frame.empty: return "Tipper(status=empty)" n_st = ( self._frame["station"].nunique() if "station" in self._frame.columns else 0 ) n_frq = ( self._frame["freq"].nunique() if "freq" in self._frame.columns else 0 ) return ( f"Tipper(rows={len(self._frame)}, stations={n_st}, freqs={n_frq})" ) def __repr__(self) -> str: """Provide an unambiguous developer representation.""" return self.__str__()