# Author: LKouadio <etanoyau@gmail.com>
# License: LGPL-3.0
"""Registry of EMTF primary and derived electromagnetic data types.
The registry mirrors the semantic role of the ``DATATYPES`` directory in
EMTF FCU v4.1 while remaining independent of XML serialization. Entries are
Python value objects so future formats (including passive airborne EM) can
reuse the same scientific vocabulary without importing the XML reader.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from ..api.property import PyCSAMTObject
__all__ = [
"DataTypeDefinition",
"register_emtf_datatype",
"ensure_emtf_datatype_registered",
"get_emtf_datatype",
"list_emtf_datatypes",
]
_VALID_DATA_KINDS = frozenset({"real", "complex"})
_VALID_INTENTIONS = frozenset({"primary", "derived"})
[docs]
@dataclass(frozen=True, repr=False)
class DataTypeDefinition(PyCSAMTObject):
"""Describe one electromagnetic transfer-function data type.
Parameters
----------
name : str
Short EMTF/FCU code, for example ``"Z"`` or ``"T"``.
tag : str
Stable semantic key, for example ``"impedance"``.
data_kind : {"real", "complex"}
Numerical representation of the data.
input_kind, output_kind : str or None
Broad channel families used by EMTF (typically ``"H"`` or ``"E"``).
Scalar derived products may omit these values.
units : str or None
Native units recorded by the format definition.
intention : {"primary", "derived"}
Whether the quantity is measured/estimated as a primary TF or derived
from another response.
description : str
Human-readable description.
derived_from : str or None
Semantic tag of the parent quantity for derived products.
aliases : tuple of str
Alternate historical codes accepted for lookup. These are useful
because the 2020 EMTF paper and FCU v4.1 differ for a few names
(for example Q/P versus ZI/TI).
see_also : tuple of str
Related semantic tags.
"""
name: str
tag: str
data_kind: str
input_kind: str | None = None
output_kind: str | None = None
units: str | None = None
intention: str = "primary"
description: str = ""
derived_from: str | None = None
aliases: tuple[str, ...] = field(default_factory=tuple)
see_also: tuple[str, ...] = field(default_factory=tuple)
def __post_init__(self) -> None:
name = str(self.name).strip().upper()
tag = str(self.tag).strip().lower()
data_kind = str(self.data_kind).strip().lower()
intention = str(self.intention).strip().lower()
if not name:
raise ValueError("EMTF data-type name must be non-empty")
if not tag:
raise ValueError("EMTF data-type tag must be non-empty")
if data_kind not in _VALID_DATA_KINDS:
raise ValueError(
"data_kind must be one of " f"{sorted(_VALID_DATA_KINDS)}"
)
if intention not in _VALID_INTENTIONS:
raise ValueError(
"intention must be one of " f"{sorted(_VALID_INTENTIONS)}"
)
object.__setattr__(self, "name", name)
object.__setattr__(self, "tag", tag)
object.__setattr__(self, "data_kind", data_kind)
object.__setattr__(self, "intention", intention)
object.__setattr__(
self,
"aliases",
tuple(
str(v).strip().upper()
for v in self.aliases
if str(v).strip()
),
)
object.__setattr__(
self,
"see_also",
tuple(
str(v).strip().lower()
for v in self.see_also
if str(v).strip()
),
)
[docs]
@property
def is_primary(self) -> bool:
"""Return ``True`` for a primary transfer-function type."""
return self.intention == "primary"
[docs]
@property
def is_derived(self) -> bool:
"""Return ``True`` for a derived data type."""
return self.intention == "derived"
_BY_TAG: dict[str, DataTypeDefinition] = {}
_BY_NAME: dict[str, DataTypeDefinition] = {}
[docs]
def register_emtf_datatype(
definition: DataTypeDefinition,
*,
overwrite: bool = False,
) -> DataTypeDefinition:
"""Register and return an EMTF data-type definition.
The semantic ``tag`` is the unique registry identity. Short names can be
shared by compatible variants (notably full and off-diagonal impedance),
therefore the first short-name registration remains the default lookup
unless ``overwrite=True`` is explicitly requested.
"""
if not isinstance(definition, DataTypeDefinition):
raise TypeError("definition must be a DataTypeDefinition")
tag_key = definition.tag.lower()
if tag_key in _BY_TAG and not overwrite:
raise ValueError(f"EMTF data type already registered: {tag_key}")
_BY_TAG[tag_key] = definition
names = (definition.name, *definition.aliases)
for name in names:
key = name.upper()
if overwrite or key not in _BY_NAME:
_BY_NAME[key] = definition
return definition
[docs]
def ensure_emtf_datatype_registered(
definition: DataTypeDefinition,
) -> DataTypeDefinition:
"""Idempotently register *definition*, tolerating a compatible repeat.
:func:`register_emtf_datatype` raises whenever its ``tag`` is
already registered, unless the caller passes ``overwrite=True`` --
a reasonable default for one-shot registration, but awkward for a
technology adapter's module-level registration function, which may
legitimately run more than once in a process (a package re-import
under test isolation, or more than one caller defensively calling
the same ``register_<technology>_datatypes()``). This helper
treats a definition already registered under the same
``tag``/``name`` pair as a no-op and returns the existing (first-
registered) instance unchanged, while a genuine collision -- the
same ``tag`` or ``name`` claimed by a materially different
definition -- still raises rather than being silently papered
over. :mod:`pycsamt.airborne.mobilemt` and
:mod:`pycsamt.airborne.afmag` both register their derived
datatypes through this helper for exactly that reason.
Parameters
----------
definition : DataTypeDefinition
Definition to register.
Returns
-------
DataTypeDefinition
*definition* itself if newly registered, or the
already-registered definition sharing its ``tag``.
Raises
------
ValueError
If *definition*'s ``tag`` is already registered under a
different ``name``, or its ``name`` is already registered
(as a primary code or alias) under a different ``tag``.
"""
existing = get_emtf_datatype(definition.tag)
if existing is not None:
if existing.name != definition.name:
raise ValueError(
f"{definition.tag!r} is already registered with code "
f"{existing.name!r}"
)
return existing
by_code = get_emtf_datatype(definition.name)
if by_code is not None and by_code.tag != definition.tag:
raise ValueError(
f"EMTF datatype code {definition.name!r} is already "
f"registered for {by_code.tag!r}"
)
return register_emtf_datatype(definition)
[docs]
def get_emtf_datatype(key: str) -> DataTypeDefinition | None:
"""Return a registered definition by semantic tag, code, or alias."""
if not isinstance(key, str) or not key.strip():
return None
raw = key.strip()
return _BY_TAG.get(raw.lower()) or _BY_NAME.get(raw.upper())
[docs]
def list_emtf_datatypes(
*,
intention: str | None = None,
) -> dict[str, DataTypeDefinition]:
"""Return a copy of registered data types keyed by semantic tag."""
if intention is None:
return dict(_BY_TAG)
wanted = str(intention).strip().lower()
if wanted not in _VALID_INTENTIONS:
raise ValueError(
"intention must be one of " f"{sorted(_VALID_INTENTIONS)}"
)
return {
tag: definition
for tag, definition in _BY_TAG.items()
if definition.intention == wanted
}
def _register_defaults() -> None:
# Primary definitions follow EMTF FCU v4.1. Q/P are retained as aliases
# for the interstation names used in the 2020 publication table.
primary = (
DataTypeDefinition(
name="Z",
tag="impedance",
data_kind="complex",
input_kind="H",
output_kind="E",
units="[mV/km]/[nT]",
description="MT impedance",
),
DataTypeDefinition(
name="T",
tag="tipper",
data_kind="complex",
input_kind="H",
output_kind="H",
units="[]",
description="Vertical field transfer functions (tipper)",
),
DataTypeDefinition(
name="ZI",
tag="interstation_impedance",
data_kind="complex",
input_kind="H",
output_kind="E",
units="[mV/km]/[nT]",
description="MT interstation impedance",
aliases=("Q",),
),
DataTypeDefinition(
name="TI",
tag="interstation_transfer_functions",
data_kind="complex",
input_kind="H",
output_kind="H",
units="[]",
description="Interstation magnetic transfer functions",
aliases=("P",),
),
DataTypeDefinition(
name="Z",
tag="off_diagonal_impedance",
data_kind="complex",
input_kind="H",
output_kind="E",
units="[mV/km]/[nT]",
description="MT impedance - off-diagonal components only",
see_also=("impedance",),
),
)
derived = (
DataTypeDefinition(
name="PT",
tag="phase_tensor",
data_kind="real",
input_kind="H",
output_kind="E",
units="[]",
intention="derived",
description="MT phase tensor",
derived_from="impedance",
),
DataTypeDefinition(
name="RHO",
tag="apparent_resistivity",
data_kind="real",
input_kind="H",
output_kind="E",
units="[Ohm m]",
intention="derived",
description="Apparent resistivity computed from MT impedance",
derived_from="impedance",
see_also=("impedance_phase",),
),
DataTypeDefinition(
name="PHS",
tag="impedance_phase",
data_kind="real",
input_kind="H",
output_kind="E",
units="degrees",
intention="derived",
description="Impedance phase in degrees",
derived_from="impedance",
see_also=("apparent_resistivity",),
),
DataTypeDefinition(
name="EFF",
tag="effective_impedance",
data_kind="complex",
units="[mV/km]/[nT]",
intention="derived",
description="Square root of the impedance determinant",
derived_from="impedance",
aliases=("ZEFF",),
see_also=("impedance_determinant",),
),
DataTypeDefinition(
name="DET",
tag="impedance_determinant",
data_kind="complex",
units="([mV/km]/[nT])^2",
intention="derived",
description="Rotation-invariant determinant of MT impedance",
derived_from="impedance",
aliases=("ZDET",),
see_also=("effective_impedance",),
),
DataTypeDefinition(
name="ZSTRIKE",
tag="impedance_strike",
data_kind="real",
units="degrees",
intention="derived",
description="MT impedance strike",
derived_from="impedance",
see_also=("impedance_skew", "impedance_ellipticity"),
),
DataTypeDefinition(
name="ZSKEW",
tag="impedance_skew",
data_kind="real",
units="[]",
intention="derived",
description="MT impedance skew",
derived_from="impedance",
see_also=("impedance_strike", "impedance_ellipticity"),
),
DataTypeDefinition(
name="ZELLIP",
tag="impedance_ellipticity",
data_kind="real",
units="[]",
intention="derived",
description="MT impedance ellipticity",
derived_from="impedance",
see_also=("impedance_skew", "impedance_strike"),
),
DataTypeDefinition(
name="TIPMAG",
tag="tipper_magnitude",
data_kind="real",
units="[]",
intention="derived",
description="Tipper magnitude",
derived_from="tipper",
see_also=("tipper_phase",),
),
DataTypeDefinition(
name="TIPPHS",
tag="tipper_phase",
data_kind="real",
units="degrees",
intention="derived",
description="Tipper phase",
derived_from="tipper",
see_also=("tipper_magnitude",),
),
DataTypeDefinition(
name="INDMAG",
tag="induction_arrow_magnitude",
data_kind="complex",
units="[]",
intention="derived",
description="Induction arrow magnitude",
derived_from="tipper",
see_also=("induction_arrow_angle",),
),
DataTypeDefinition(
name="INDANG",
tag="induction_arrow_angle",
data_kind="complex",
units="degrees",
intention="derived",
description="Induction arrow angle (Parkinson convention)",
derived_from="tipper",
see_also=("induction_arrow_magnitude",),
),
DataTypeDefinition(
name="TSTRIKE",
tag="tipper_strike",
data_kind="real",
units="degrees",
intention="derived",
description="Tipper strike",
derived_from="tipper",
see_also=("tipper_skew", "tipper_ellipticity"),
),
DataTypeDefinition(
name="TSKEW",
tag="tipper_skew",
data_kind="real",
units="[]",
intention="derived",
description="Tipper skew",
derived_from="tipper",
see_also=("tipper_strike", "tipper_ellipticity"),
),
DataTypeDefinition(
name="TELLIP",
tag="tipper_ellipticity",
data_kind="real",
units="[]",
intention="derived",
description="Tipper ellipticity",
derived_from="tipper",
see_also=("tipper_skew", "tipper_strike"),
),
)
for definition in (*primary, *derived):
register_emtf_datatype(definition)
_register_defaults()