Source code for pycsamt.metadata.site
# Author: LKouadio <etanoyau@gmail.com>
# License: LGPL-3.0
"""Site-level metadata for electromagnetic transfer functions."""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from math import isfinite
from typing import Any
from ..api.property import PyCSAMTObject
__all__ = ["LocationMeta", "SiteMeta"]
[docs]
@dataclass(repr=False)
class LocationMeta(PyCSAMTObject):
"""Geographic location and optional magnetic declination metadata."""
latitude: float | None = None
longitude: float | None = None
elevation: float | None = None
datum: str | None = "WGS84"
elevation_units: str = "meters"
declination: float | None = None
declination_epoch: float | None = None
extra: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
self.validate()
[docs]
def validate(self) -> None:
if self.latitude is not None:
lat = float(self.latitude)
if not isfinite(lat) or not -90.0 <= lat <= 90.0:
raise ValueError(
"latitude must be finite and within [-90, 90]"
)
self.latitude = lat
if self.longitude is not None:
lon = float(self.longitude)
if not isfinite(lon) or not -180.0 <= lon <= 180.0:
raise ValueError(
"longitude must be finite and within [-180, 180]"
)
self.longitude = lon
for attr in ("elevation", "declination", "declination_epoch"):
value = getattr(self, attr)
if value is None:
continue
numeric = float(value)
if not isfinite(numeric):
raise ValueError(f"{attr} must be finite")
setattr(self, attr, numeric)
if self.datum is not None:
datum = str(self.datum).strip()
self.datum = datum or None
units = str(self.elevation_units).strip()
if not units:
raise ValueError("elevation_units must be non-empty")
self.elevation_units = units
self.extra = dict(self.extra or {})
[docs]
@property
def has_horizontal_coordinates(self) -> bool:
return self.latitude is not None and self.longitude is not None
[docs]
@dataclass(repr=False)
class SiteMeta(PyCSAMTObject):
"""Site identity, acquisition interval, and geographic location."""
project: str | None = None
survey: str | None = None
year_collected: int | None = None
country: str | None = None
site_id: str | int | None = None
name: str | None = None
location: LocationMeta | None = None
acquired_by: str | None = None
start: datetime | str | None = None
end: datetime | str | None = None
extra: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
self.validate()
[docs]
def validate(self) -> None:
for attr in ("project", "survey", "country", "name", "acquired_by"):
value = getattr(self, attr)
if value is not None:
text = str(value).strip()
setattr(self, attr, text or None)
if self.site_id is not None and isinstance(self.site_id, str):
value = self.site_id.strip()
self.site_id = value or None
if self.year_collected is not None:
year = int(self.year_collected)
if year < 0:
raise ValueError("year_collected must be non-negative")
self.year_collected = year
if self.location is not None and not isinstance(
self.location, LocationMeta
):
raise TypeError("location must be a LocationMeta")
for attr in ("start", "end"):
value = getattr(self, attr)
if value is not None and not isinstance(value, (datetime, str)):
raise TypeError(f"{attr} must be datetime, str, or None")
if isinstance(value, str):
text = value.strip()
setattr(self, attr, text or None)
if isinstance(self.start, datetime) and isinstance(self.end, datetime):
if self.start > self.end:
raise ValueError("site start must not be after site end")
self.extra = dict(self.extra or {})
[docs]
@property
def preferred_name(self) -> str | None:
"""Return human site name, falling back to the site identifier."""
if self.name:
return self.name
if self.site_id is None:
return None
return str(self.site_id)