Source code for pycsamt.metadata.orientation
# Author: LKouadio <etanoyau@gmail.com>
# License: LGPL-3.0
"""Transfer-function orientation metadata independent of file format."""
from __future__ import annotations
from dataclasses import dataclass, field
from math import isfinite
from typing import Any
from ..api.property import PyCSAMTObject
__all__ = ["OrientationMeta"]
_ALLOWED_MODES = frozenset({"orthogonal", "sitelayout"})
_MODE_ALIASES = {
"site_layout": "sitelayout",
"site-layout": "sitelayout",
"layout": "sitelayout",
}
[docs]
@dataclass(repr=False)
class OrientationMeta(PyCSAMTObject):
"""Describe the coordinate orientation of transfer-function data.
``OrientationMeta`` describes the orientation of the *data*, not the
physical sensor geometry. Physical channel geometry is retained by
:class:`~pycsamt.metadata.channels.SiteLayout` so rotations remain
reversible where sufficient information is available.
Parameters
----------
mode : {"orthogonal", "sitelayout"}, optional
``"orthogonal"`` means the TF matrix is expressed in an orthogonal
coordinate frame. ``"sitelayout"`` means the TF follows the original
site channel layout.
angle_to_geographic_north : float, optional
Clockwise angle in degrees of the orthogonal x-axis relative to
geographic north. It is intentionally not fabricated when unknown.
rotation_info : str, optional
Human-readable record of known rotation history or ambiguity.
extra : dict
Additional orientation metadata.
"""
mode: str | None = None
angle_to_geographic_north: float | None = None
rotation_info: str | None = None
extra: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
self.validate()
[docs]
def validate(self) -> None:
if self.mode is not None:
mode = str(self.mode).strip().lower()
mode = _MODE_ALIASES.get(mode, mode)
if mode not in _ALLOWED_MODES:
raise ValueError(
"orientation mode must be one of "
f"{sorted(_ALLOWED_MODES)}"
)
self.mode = mode
if self.angle_to_geographic_north is not None:
angle = float(self.angle_to_geographic_north)
if not isfinite(angle):
raise ValueError("orientation angle must be finite")
if self.mode == "sitelayout":
raise ValueError(
"sitelayout orientation is defined by channel geometry; "
"angle_to_geographic_north must be None"
)
self.angle_to_geographic_north = angle
if self.rotation_info is not None:
value = str(self.rotation_info).strip()
self.rotation_info = value or None
self.extra = dict(self.extra or {})
[docs]
@property
def is_orthogonal(self) -> bool:
"""Return whether the data are in an orthogonal coordinate frame."""
return self.mode == "orthogonal"
[docs]
@property
def follows_site_layout(self) -> bool:
"""Return whether data orientation follows the sensor layout."""
return self.mode == "sitelayout"