Source code for pycsamt.airborne.mobilemt.base
# Author: LKouadio <etanoyau@gmail.com>
# License: LGPL-3.0
"""MobileMT-specific metadata that does not duplicate EMTF mathematics."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
import numpy as np
from ...core.base import CoreObject
from ...metadata import InstrumentMeta, SensorSpec, SiteMeta
from .constants import (
MOBILEMT_INPUT_CHANNELS,
MOBILEMT_NOMINAL_FREQUENCY_RANGE_HZ,
MOBILEMT_NOMINAL_MAX_WINDOWS,
MOBILEMT_NOMINAL_SAMPLING_RATE_HZ,
MOBILEMT_OUTPUT_CHANNELS,
)
__all__ = ["MobileMTSystemSpec", "MobileMTReferenceStation"]
[docs]
@dataclass(repr=False)
class MobileMTSystemSpec(CoreObject):
"""Published/declared MobileMT system characteristics.
These values are descriptive metadata, not hard validation limits. A
processed product may legitimately contain fewer windows or a subset of
the nominal frequency range.
"""
nominal_frequency_range_hz: tuple[float, float] = (
MOBILEMT_NOMINAL_FREQUENCY_RANGE_HZ
)
nominal_max_frequency_windows: int = MOBILEMT_NOMINAL_MAX_WINDOWS
nominal_sampling_rate_hz: float = MOBILEMT_NOMINAL_SAMPLING_RATE_HZ
input_channels: tuple[str, ...] = MOBILEMT_INPUT_CHANNELS
output_channels: tuple[str, ...] = MOBILEMT_OUTPUT_CHANNELS
attrs: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
self.validate()
[docs]
def validate(self) -> None:
low, high = (float(v) for v in self.nominal_frequency_range_hz)
if not np.isfinite(low) or not np.isfinite(high):
raise ValueError("nominal frequency range must be finite")
if low <= 0.0 or high <= low:
raise ValueError(
"nominal frequency range must satisfy 0 < low < high"
)
self.nominal_frequency_range_hz = (low, high)
nwin = int(self.nominal_max_frequency_windows)
if nwin <= 0:
raise ValueError("nominal_max_frequency_windows must be positive")
self.nominal_max_frequency_windows = nwin
rate = float(self.nominal_sampling_rate_hz)
if not np.isfinite(rate) or rate <= 0.0:
raise ValueError("nominal_sampling_rate_hz must be positive")
self.nominal_sampling_rate_hz = rate
self.input_channels = tuple(
str(v).strip() for v in self.input_channels
)
self.output_channels = tuple(
str(v).strip() for v in self.output_channels
)
if self.input_channels != MOBILEMT_INPUT_CHANNELS:
raise ValueError(
"MobileMT processed admittance inputs must be Ex/Ey"
)
if self.output_channels != MOBILEMT_OUTPUT_CHANNELS:
raise ValueError(
"MobileMT processed admittance outputs must be Hx/Hy/Hz"
)
self.attrs = dict(self.attrs or {})
[docs]
def nominal_frequency_mask(self, frequency: Any) -> np.ndarray:
"""Return a mask for values inside the published nominal range."""
freq = np.asarray(frequency, dtype=float)
low, high = self.nominal_frequency_range_hz
return np.isfinite(freq) & (freq >= low) & (freq <= high)
[docs]
@dataclass(repr=False)
class MobileMTReferenceStation(CoreObject):
"""Ground electric reference-station metadata for MobileMT processing.
This object intentionally describes only the processed reference station.
It does not invent a raw electrode-file schema or model the additional
internal reference electrode pairs used by proprietary processing.
"""
station_id: str | None = None
site: SiteMeta | None = None
electric_channels: tuple[str, str] = MOBILEMT_INPUT_CHANNELS
attrs: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
self.validate()
[docs]
def validate(self) -> None:
if self.station_id is not None:
text = str(self.station_id).strip()
self.station_id = text or None
if self.site is not None and not isinstance(self.site, SiteMeta):
raise TypeError("site must be a SiteMeta or None")
channels = tuple(str(v).strip() for v in self.electric_channels)
if channels != MOBILEMT_INPUT_CHANNELS:
raise ValueError(
"MobileMT reference electric channels must be Ex/Ey"
)
self.electric_channels = channels
self.attrs = dict(self.attrs or {})
[docs]
@property
def preferred_id(self) -> str | None:
"""Return explicit station ID, then SiteMeta preferred name."""
if self.station_id:
return self.station_id
if self.site is not None:
return self.site.preferred_name
return None